Skip to content

Commit

Permalink
All cluster configuration must be in place before instance started (#420
Browse files Browse the repository at this point in the history
)
  • Loading branch information
lbwexler authored Nov 15, 2024
1 parent 563cc64 commit 3d15fca
Show file tree
Hide file tree
Showing 7 changed files with 62 additions and 64 deletions.
12 changes: 11 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 24 additions & 0 deletions grails-app/init/io/xh/hoist/ClusterConfig.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ class ClusterConfig {

createDefaultConfigs(ret)
createHibernateConfigs(ret)
createServiceConfigs(ret)
createCachedValueConfigs(ret)

KryoSupport.setAsGlobalSerializer(ret)

Expand All @@ -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 {
Expand Down Expand Up @@ -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
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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,
Expand Down
37 changes: 0 additions & 37 deletions grails-app/services/io/xh/hoist/cluster/ClusterService.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -157,39 +153,6 @@ class ClusterService extends BaseService implements ApplicationListener<Applicat
.collectEntries { member, f -> [member.getAttribute('instanceName'), f.get()] }
}

//------------------
// Create Objects
//-----------------
static <K, V> IMap<K, V> configuredIMap(String name, Closure customizer = null) {
customizer?.call(hzConfig.getMapConfig(name))
hzInstance.getMap(name)
}

static <V> ISet<V> configuredISet(String name, Closure customizer = null) {
customizer?.call(hzConfig.getSetConfig(name))
hzInstance.getSet(name)
}

static <K, V> ReplicatedMap<K, V> configuredReplicatedMap(String name, Closure customizer = null) {
customizer?.call(hzConfig.getReplicatedMapConfig(name))
hzInstance.getReplicatedMap(name)
}

static <M> ITopic<M> configuredTopic(String name, Closure customizer = null) {
customizer?.call(hzConfig.getTopicConfig(name))
hzInstance.getTopic(name)
}

static <M> ITopic<M> configuredReliableTopic(
String name,
Closure customizer = null,
Closure ringBufferCustomizer = null
) {
ringBufferCustomizer?.call(hzConfig.getRingbufferConfig(name))
customizer?.call(hzConfig.getReliableTopicConfig(name))
hzInstance.getReliableTopic(name)
}

//------------------------------------
// Implementation
//------------------------------------
Expand Down
26 changes: 11 additions & 15 deletions src/main/groovy/io/xh/hoist/BaseService.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -127,32 +124,29 @@ 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.
*/
<K, V> IMap<K, V> createIMap(String name, Closure customizer = null) {
addResource(name, configuredIMap(hzName(name), customizer))
<K, V> IMap<K, V> createIMap(String name) {
addResource(name, hzInstance.getMap(hzName(name)))
}

/**
* Create and return a reference to a Hazelcast ISet.
*
* @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.
*/
<V> ISet<V> createISet(String name, Closure customizer = null) {
addResource(name, configuredISet(hzName(name), customizer))
<V> ISet<V> createISet(String name) {
addResource(name, hzInstance.getSet(hzName(name)))
}

/**
* Create and return a reference to a Hazelcast Replicated Map.
*
* @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.
*/
<K, V> ReplicatedMap<K, V> createReplicatedMap(String name, Closure customizer = null) {
addResource(name, configuredReplicatedMap(hzName(name), customizer))
<K, V> ReplicatedMap<K, V> createReplicatedMap(String name) {
addResource(name, hzInstance.getReplicatedMap(hzName(name)))
}

/**
Expand Down Expand Up @@ -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]"
}

//------------------------
Expand Down
10 changes: 1 addition & 9 deletions src/main/groovy/io/xh/hoist/cachedvalue/CachedValue.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -196,14 +195,7 @@ class CachedValue<V> implements LogSupport {
private ITopic<CachedValueEntry<V>> 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<CachedValueEntry<V>>() {
void onMessage(Message<CachedValueEntry<V>> message) {
Expand Down
2 changes: 1 addition & 1 deletion src/main/groovy/io/xh/hoist/util/Timer.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down

0 comments on commit 3d15fca

Please sign in to comment.