From cab6d3eaa0e6cd5d73fd2224a06c22bced9052cc Mon Sep 17 00:00:00 2001 From: lbwexler Date: Thu, 14 Nov 2024 15:02:57 -0500 Subject: [PATCH] All cluster configuration must be in place before instance started --- CHANGELOG.md | 12 +++++- .../init/io/xh/hoist/ClusterConfig.groovy | 24 ++++++++++++ .../clienterror/ClientErrorService.groovy | 15 +++++++- .../io/xh/hoist/cluster/ClusterService.groovy | 37 ------------------- .../groovy/io/xh/hoist/BaseService.groovy | 26 ++++++------- .../xh/hoist/cachedvalue/CachedValue.groovy | 10 +---- src/main/groovy/io/xh/hoist/util/Timer.groovy | 2 +- 7 files changed, 62 insertions(+), 64 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 55e513dc..d234b369 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,18 @@ ## 25.0-SNAPSHOT - unreleased -### ⚙️ Technical +### 💥 Breaking Changes (upgrade difficulty: 🟢 LOW) +* Dynamic configuration for distributed hazelcast objects is no longer supported -- all configuration + must be in place before an instance is started, per Hazelcast documentation. Therefore the + `ClusterService.configureXXX` methods have been removed, and have been replaced by support for + specifying a static closure `ClusterService.configureCluster`. This is not expected to have a + practical impact on any existing applications. + +### 🐞 Bug Fixes +* Fix to issue with a `Timer` interval specified as a config names failing to update dynamically. + +### ⚙️ Technical * Increased max length of `Role.category` string to 100 chars. * Requires column modification to `xh_role` table with the following SQL or equivalent: ```mysql diff --git a/grails-app/init/io/xh/hoist/ClusterConfig.groovy b/grails-app/init/io/xh/hoist/ClusterConfig.groovy index a092c311..8ea5ca41 100755 --- a/grails-app/init/io/xh/hoist/ClusterConfig.groovy +++ b/grails-app/init/io/xh/hoist/ClusterConfig.groovy @@ -99,6 +99,8 @@ class ClusterConfig { createDefaultConfigs(ret) createHibernateConfigs(ret) + createServiceConfigs(ret) + createCachedValueConfigs(ret) KryoSupport.setAsGlobalSerializer(ret) @@ -124,6 +126,10 @@ class ClusterConfig { * * - a static 'cache' property on Grails domain objects to customize associated * Hibernate caches. See toolbox's `Phase` object for examples. + * + * - a static 'configureCluster' property on Grails Service to allow services to provide + * configuration for any custom hazelcast structures that they will user. See + * Hoist Core's `ClientErrorService` for an example. */ protected void createDefaultConfigs(Config config) { config.getMapConfig('default').with { @@ -191,4 +197,22 @@ class ClusterConfig { } } } + + private void createServiceConfigs(Config config) { + // Ad-Hoc per service configuration, via static closure + grailsApplication.serviceClasses.each { GrailsClass gc -> + def customizer = gc.getPropertyValue('configureCluster') as Closure + customizer?.call(config) + } + } + + private void createCachedValueConfigs(Config config) { + config.getReliableTopicConfig('xhcachedvalue.*').with { + readBatchSize = 1 + } + config.getRingbufferConfig('xhcachedvalue.*').with { + inMemoryFormat = InMemoryFormat.OBJECT + capacity = 1 + } + } } \ No newline at end of file diff --git a/grails-app/services/io/xh/hoist/clienterror/ClientErrorService.groovy b/grails-app/services/io/xh/hoist/clienterror/ClientErrorService.groovy index 691535de..caa607b0 100644 --- a/grails-app/services/io/xh/hoist/clienterror/ClientErrorService.groovy +++ b/grails-app/services/io/xh/hoist/clienterror/ClientErrorService.groovy @@ -7,6 +7,7 @@ package io.xh.hoist.clienterror +import com.hazelcast.config.Config import com.hazelcast.map.IMap import grails.gorm.transactions.Transactional import io.xh.hoist.BaseService @@ -32,6 +33,18 @@ import static java.lang.System.currentTimeMillis */ class ClientErrorService extends BaseService { + /** + * An example of a closure for custom configuration of associated Hazelcast structures. + * This is provided statically to allow configuration to be in place before the Hazelcast + * instance is instantiated. + * Note the call to `hzName` to get the appropriately qualified name of the resource. + */ + static configureCluster = { Config c -> + c.getMapConfig(hzName('clientErrors', this)).with { + evictionConfig.size = 100 + } + } + def clientErrorEmailService, configService @@ -42,7 +55,7 @@ class ClientErrorService extends BaseService { void init() { super.init() - errors = createIMap('clientErrors') {it.evictionConfig.size = 100} + errors = createIMap('clientErrors') createTimer( name: 'processErrors', runFn: this.&processErrors, diff --git a/grails-app/services/io/xh/hoist/cluster/ClusterService.groovy b/grails-app/services/io/xh/hoist/cluster/ClusterService.groovy index 32334dae..2fcb9ebd 100644 --- a/grails-app/services/io/xh/hoist/cluster/ClusterService.groovy +++ b/grails-app/services/io/xh/hoist/cluster/ClusterService.groovy @@ -4,15 +4,11 @@ import com.hazelcast.cluster.Cluster import com.hazelcast.cluster.Member import com.hazelcast.cluster.MembershipEvent import com.hazelcast.cluster.MembershipListener -import com.hazelcast.collection.ISet import com.hazelcast.config.Config import com.hazelcast.core.DistributedObject import com.hazelcast.core.Hazelcast import com.hazelcast.core.HazelcastInstance import com.hazelcast.core.IExecutorService -import com.hazelcast.map.IMap -import com.hazelcast.replicatedmap.ReplicatedMap -import com.hazelcast.topic.ITopic import io.xh.hoist.BaseService import io.xh.hoist.ClusterConfig import io.xh.hoist.exception.InstanceNotFoundException @@ -157,39 +153,6 @@ class ClusterService extends BaseService implements ApplicationListener [member.getAttribute('instanceName'), f.get()] } } - //------------------ - // Create Objects - //----------------- - static IMap configuredIMap(String name, Closure customizer = null) { - customizer?.call(hzConfig.getMapConfig(name)) - hzInstance.getMap(name) - } - - static ISet configuredISet(String name, Closure customizer = null) { - customizer?.call(hzConfig.getSetConfig(name)) - hzInstance.getSet(name) - } - - static ReplicatedMap configuredReplicatedMap(String name, Closure customizer = null) { - customizer?.call(hzConfig.getReplicatedMapConfig(name)) - hzInstance.getReplicatedMap(name) - } - - static ITopic configuredTopic(String name, Closure customizer = null) { - customizer?.call(hzConfig.getTopicConfig(name)) - hzInstance.getTopic(name) - } - - static ITopic configuredReliableTopic( - String name, - Closure customizer = null, - Closure ringBufferCustomizer = null - ) { - ringBufferCustomizer?.call(hzConfig.getRingbufferConfig(name)) - customizer?.call(hzConfig.getReliableTopicConfig(name)) - hzInstance.getReliableTopic(name) - } - //------------------------------------ // Implementation //------------------------------------ diff --git a/src/main/groovy/io/xh/hoist/BaseService.groovy b/src/main/groovy/io/xh/hoist/BaseService.groovy index cfc1d787..e1a5d632 100644 --- a/src/main/groovy/io/xh/hoist/BaseService.groovy +++ b/src/main/groovy/io/xh/hoist/BaseService.groovy @@ -35,9 +35,6 @@ import java.util.concurrent.ExecutionException import java.util.concurrent.TimeUnit import static grails.async.Promises.task -import static io.xh.hoist.cluster.ClusterService.configuredIMap -import static io.xh.hoist.cluster.ClusterService.configuredISet -import static io.xh.hoist.cluster.ClusterService.configuredReplicatedMap import static io.xh.hoist.util.DateTimeUtils.SECONDS import static io.xh.hoist.util.DateTimeUtils.MINUTES import static io.xh.hoist.util.Utils.appContext @@ -127,10 +124,9 @@ abstract class BaseService implements LogSupport, IdentitySupport, DisposableBea * * @param name - must be unique across all Caches, Timers and distributed Hazelcast objects * associated with this service. - * @param customizer - closure receiving a Hazelcast MapConfig. Mutate to customize. */ - IMap createIMap(String name, Closure customizer = null) { - addResource(name, configuredIMap(hzName(name), customizer)) + IMap createIMap(String name) { + addResource(name, hzInstance.getMap(hzName(name))) } /** @@ -138,10 +134,9 @@ abstract class BaseService implements LogSupport, IdentitySupport, DisposableBea * * @param name - must be unique across all Caches, Timers and distributed Hazelcast objects * associated with this service. - * @param customizer - closure receiving a Hazelcast SetConfig. Mutate to customize. */ - ISet createISet(String name, Closure customizer = null) { - addResource(name, configuredISet(hzName(name), customizer)) + ISet createISet(String name) { + addResource(name, hzInstance.getSet(hzName(name))) } /** @@ -149,10 +144,9 @@ abstract class BaseService implements LogSupport, IdentitySupport, DisposableBea * * @param name - must be unique across all Caches, Timers and distributed Hazelcast objects * associated with this service. - * @param customizer - closure receiving a Hazelcast ReplicatedMapConfig. Mutate to customize. */ - ReplicatedMap createReplicatedMap(String name, Closure customizer = null) { - addResource(name, configuredReplicatedMap(hzName(name), customizer)) + ReplicatedMap createReplicatedMap(String name) { + addResource(name, hzInstance.getReplicatedMap(hzName(name))) } /** @@ -382,11 +376,13 @@ abstract class BaseService implements LogSupport, IdentitySupport, DisposableBea * * Not typically called directly by applications. Applications should aim to create * Hazelcast distributed objects using the methods in this class. - * - * @internal */ String hzName(String name) { - "${this.class.name}[$name]" + hzName(name, this.class) + } + + static String hzName(String name, Class clazz) { + "${clazz.name}[$name]" } //------------------------ diff --git a/src/main/groovy/io/xh/hoist/cachedvalue/CachedValue.groovy b/src/main/groovy/io/xh/hoist/cachedvalue/CachedValue.groovy index 2edbdbee..8ce8bc13 100644 --- a/src/main/groovy/io/xh/hoist/cachedvalue/CachedValue.groovy +++ b/src/main/groovy/io/xh/hoist/cachedvalue/CachedValue.groovy @@ -16,7 +16,6 @@ import org.slf4j.LoggerFactory import java.util.concurrent.TimeoutException import static grails.async.Promises.task -import static io.xh.hoist.cluster.ClusterService.configuredReliableTopic import static io.xh.hoist.util.DateTimeUtils.asEpochMilli import static io.xh.hoist.util.DateTimeUtils.intervalElapsed import static java.lang.System.currentTimeMillis @@ -196,14 +195,7 @@ class CachedValue implements LogSupport { private ITopic> createUpdateTopic() { // Create a durable topic with room for just a single item // and register for all events, including replay of event before this instance existed. - def ret = configuredReliableTopic( - svc.hzName(name), - {it.readBatchSize = 1}, - { - it.capacity = 1 - it.inMemoryFormat = InMemoryFormat.OBJECT - } - ) + def ret = ClusterService.hzInstance.getReliableTopic('xhcachedvalue.' + svc.hzName(name)) ret.addMessageListener( new ReliableMessageListener>() { void onMessage(Message> message) { diff --git a/src/main/groovy/io/xh/hoist/util/Timer.groovy b/src/main/groovy/io/xh/hoist/util/Timer.groovy index e0389f3a..e6bcc2f1 100644 --- a/src/main/groovy/io/xh/hoist/util/Timer.groovy +++ b/src/main/groovy/io/xh/hoist/util/Timer.groovy @@ -169,7 +169,7 @@ class Timer implements LogSupport { coreIntervalMs = calcCoreIntervalMs() if (useCluster && lastCompletedOnCluster == null) { - lastCompletedOnCluster = ClusterService.configuredReplicatedMap('xhTimersLastCompleted') + lastCompletedOnCluster = ClusterService.hzInstance.getReplicatedMap('xhTimersLastCompleted') } if (runImmediatelyAndBlock) {