From cc45decd90290ff54ee09d0286c90e3431f850f8 Mon Sep 17 00:00:00 2001 From: lbwexler Date: Wed, 11 Sep 2024 15:53:02 -0400 Subject: [PATCH 01/14] * Improvements to `Timer` to avoid extra executions when primary instance changes. --- CHANGELOG.md | 2 + src/main/groovy/io/xh/hoist/util/Timer.groovy | 41 +++++++++++++++---- 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c883f79..5e1f163c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ### ⚙️ Technical +* Improvements to `Timer` to avoid extra executions when primary instance changes. + * Updated `ClusterService` to use Hoist's `InstanceNotFoundException` class to designate routine. * Exposed `/xh/ping` as whitelisted route for basic uptime/reachability checks. Retained legacy diff --git a/src/main/groovy/io/xh/hoist/util/Timer.groovy b/src/main/groovy/io/xh/hoist/util/Timer.groovy index b46c7598..d4c6c446 100644 --- a/src/main/groovy/io/xh/hoist/util/Timer.groovy +++ b/src/main/groovy/io/xh/hoist/util/Timer.groovy @@ -7,6 +7,8 @@ package io.xh.hoist.util +import io.xh.hoist.BaseService +import io.xh.hoist.cache.CachedValue import io.xh.hoist.log.LogSupport import java.util.concurrent.ExecutionException @@ -31,10 +33,10 @@ class Timer { private static Long CONFIG_INTERVAL = 15 * SECONDS - /** Optional name for this timer (for logging purposes) **/ + /** Unique name for this timer, required for cluster aware timers (see `primaryOnly`) **/ final String name - /** Object using this timer (for logging purposes) **/ + /** Object using this timer **/ final LogSupport owner /** Closure to run */ @@ -73,7 +75,10 @@ class Timer { /** Block on an immediate initial run? Default is false. */ final boolean runImmediatelyAndBlock - /** Only run job when clustered instance is the primary instance? Default is false. */ + /** + * Only run job when clustered instance is the primary instance? Default is false. + * For timers owned by instances of BaseService only. + */ final boolean primaryOnly @@ -109,6 +114,9 @@ class Timer { private java.util.Timer configTimer + private CachedValue _lastCompletedOnCluster + + // Args from Grails 3.0 async promise implementation static ExecutorService executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue()) @@ -132,6 +140,16 @@ class Timer { if ([owner, interval, runFn].contains(null)) throw new RuntimeException('Missing required arguments for Timer.') if (config.delayUnits) throw new RuntimeException('delayUnits has been removed from the API. Specify delay in ms.') + if (primaryOnly) { + if (!name) { + throw new IllegalArgumentException("Cannot create a 'primaryOnly' timer without a unique name") + } + if (!owner instanceof BaseService) { + throw new IllegalArgumentException("A 'primaryOnly' timer must be owned by an instance of BaseService.") + } + _lastCompletedOnCluster = new CachedValue<>(name: "xhTimer_$name", svc: owner as BaseService) + } + intervalMs = calcIntervalMs() timeoutMs = calcTimeoutMs() delayMs = calcDelayMs() @@ -217,6 +235,7 @@ class Timer { } _lastRunCompleted = new Date() + _lastCompletedOnCluster?.set(_lastRunCompleted) _isRunning = false _lastRunStats = [ startTime: _lastRunStarted, @@ -287,15 +306,19 @@ class Timer { // frequently enough to pickup forceRun reasonably fast. Tighten down for the rare fast timer. //------------------------------------------------------------------------------------------- private void onCoreTimer() { - if (!isRunning) { - if ((intervalMs > 0 && intervalElapsed(intervalMs, lastRunCompleted)) || forceRun) { - boolean wasForced = forceRun - doRun() - if (wasForced) forceRun = false - } + if (!isRunning && (forceRun || isIntervalElapsed())) { + boolean wasForced = forceRun + doRun() + if (wasForced) forceRun = false } } + private boolean isIntervalElapsed() { + if (intervalMs <= 0) return false + def lastRun = _lastCompletedOnCluster ? _lastCompletedOnCluster.get() : _lastRunCompleted + return intervalElapsed(intervalMs, lastRun) + } + private Long calcCoreIntervalMs() { return (intervalMs > 2 * SECONDS) ? 1 * SECONDS : 250; } From 0116e55fd30feeb3fa954644dca69de69040f537 Mon Sep 17 00:00:00 2001 From: lbwexler Date: Wed, 11 Sep 2024 16:04:03 -0400 Subject: [PATCH 02/14] Consolidate transport mechanism for AlertBanner --- .../services/io/xh/hoist/alertbanner/AlertBannerService.groovy | 1 + .../services/io/xh/hoist/clienterror/ClientErrorService.groovy | 1 + .../services/io/xh/hoist/feedback/FeedbackEmailService.groovy | 1 + grails-app/services/io/xh/hoist/monitor/MonitorService.groovy | 1 + src/main/groovy/io/xh/hoist/cache/Cache.groovy | 1 + .../groovy/io/xh/hoist/role/provided/DefaultRoleService.groovy | 1 + 6 files changed, 6 insertions(+) diff --git a/grails-app/services/io/xh/hoist/alertbanner/AlertBannerService.groovy b/grails-app/services/io/xh/hoist/alertbanner/AlertBannerService.groovy index 88795e74..f369af57 100644 --- a/grails-app/services/io/xh/hoist/alertbanner/AlertBannerService.groovy +++ b/grails-app/services/io/xh/hoist/alertbanner/AlertBannerService.groovy @@ -51,6 +51,7 @@ class AlertBannerService extends BaseService { void init() { timer = createTimer( + name: 'readFromSpec', interval: 2 * MINUTES, runFn: this.&readFromSpec, primaryOnly: true diff --git a/grails-app/services/io/xh/hoist/clienterror/ClientErrorService.groovy b/grails-app/services/io/xh/hoist/clienterror/ClientErrorService.groovy index 808ee14d..d6e43dda 100644 --- a/grails-app/services/io/xh/hoist/clienterror/ClientErrorService.groovy +++ b/grails-app/services/io/xh/hoist/clienterror/ClientErrorService.groovy @@ -48,6 +48,7 @@ class ClientErrorService extends BaseService { void init() { super.init() createTimer( + name: 'processErrors', interval: { alertInterval }, delay: 15 * SECONDS, primaryOnly: true diff --git a/grails-app/services/io/xh/hoist/feedback/FeedbackEmailService.groovy b/grails-app/services/io/xh/hoist/feedback/FeedbackEmailService.groovy index 597bade4..c7ed4c77 100644 --- a/grails-app/services/io/xh/hoist/feedback/FeedbackEmailService.groovy +++ b/grails-app/services/io/xh/hoist/feedback/FeedbackEmailService.groovy @@ -16,6 +16,7 @@ class FeedbackEmailService extends BaseService { void init() { subscribeToTopic( + name: 'emailFeedback', topic: 'xhFeedbackReceived', onMessage: this.&emailFeedback, primaryOnly: true diff --git a/grails-app/services/io/xh/hoist/monitor/MonitorService.groovy b/grails-app/services/io/xh/hoist/monitor/MonitorService.groovy index 7208e4d5..aeebabd0 100644 --- a/grails-app/services/io/xh/hoist/monitor/MonitorService.groovy +++ b/grails-app/services/io/xh/hoist/monitor/MonitorService.groovy @@ -51,6 +51,7 @@ class MonitorService extends BaseService { void init() { timer = createTimer( + name: 'runMonitors', interval: { monitorInterval }, delay: startupDelay, primaryOnly: true diff --git a/src/main/groovy/io/xh/hoist/cache/Cache.groovy b/src/main/groovy/io/xh/hoist/cache/Cache.groovy index 26aa17c3..68343c1b 100644 --- a/src/main/groovy/io/xh/hoist/cache/Cache.groovy +++ b/src/main/groovy/io/xh/hoist/cache/Cache.groovy @@ -51,6 +51,7 @@ class Cache extends BaseCache { if (onChange) addChangeHandler(onChange) timer = new Timer( + name: name ? "cullEntries_$name" : 'cullEntries', owner: svc, primaryOnly: replicate, runFn: this.&cullEntries, diff --git a/src/main/groovy/io/xh/hoist/role/provided/DefaultRoleService.groovy b/src/main/groovy/io/xh/hoist/role/provided/DefaultRoleService.groovy index c9e10383..605d94be 100644 --- a/src/main/groovy/io/xh/hoist/role/provided/DefaultRoleService.groovy +++ b/src/main/groovy/io/xh/hoist/role/provided/DefaultRoleService.groovy @@ -102,6 +102,7 @@ class DefaultRoleService extends BaseRoleService { ensureRequiredConfigAndRolesCreated() timer = createTimer( + name: 'refreshRoles', interval: { config.refreshIntervalSecs as int * SECONDS }, runFn: this.&refreshRoleAssignments, runImmediatelyAndBlock: true, From 4208f11912cc952ec7df87ef2702b81ae7dc7d0a Mon Sep 17 00:00:00 2001 From: lbwexler Date: Sat, 14 Sep 2024 18:11:24 -0400 Subject: [PATCH 03/14] + New APIs + Docs + Use NamedVariant for Timer + Require/Check unique names for all service resouces + Logs should get archived on non-primary server. --- CHANGELOG.md | 9 ++ .../ConnectionPoolMonitoringService.groovy | 5 +- .../admin/MemoryMonitoringService.groovy | 5 +- .../alertbanner/AlertBannerService.groovy | 8 +- .../clienterror/ClientErrorService.groovy | 3 +- .../io/xh/hoist/ldap/LdapService.groovy | 6 +- .../io/xh/hoist/log/LogArchiveService.groovy | 12 +- .../io/xh/hoist/log/LogLevelService.groovy | 11 +- .../io/xh/hoist/monitor/MonitorService.groovy | 10 +- .../groovy/io/xh/hoist/BaseService.groovy | 126 +++++++++++++++--- .../groovy/io/xh/hoist/cache/BaseCache.groovy | 4 +- .../groovy/io/xh/hoist/cache/Cache.groovy | 28 ++-- .../io/xh/hoist/cache/CachedValue.groovy | 13 +- .../role/provided/DefaultRoleService.groovy | 5 +- src/main/groovy/io/xh/hoist/util/Timer.groovy | 48 ++++--- 15 files changed, 204 insertions(+), 89 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e1f163c..5beb98d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ ## 22.0-SNAPSHOT +### 💥 Breaking Changes (upgrade difficulty: 🟢 LOW) +* All `Timer`, `Cache`, and `CachedValue` object require a 'name' property. This property was +previously optional in many cases, but is now required in order to support new cluster features, +logging, and admin tools. + +### 🎁 New Features +* `Cache` and `CachedValue` may now be created using a factory on `BaseService`. This streamlined +interface reduces boilerplate, and provides a consistent interface with `Timer`. + ### ⚙️ Technical * Improvements to `Timer` to avoid extra executions when primary instance changes. diff --git a/grails-app/services/io/xh/hoist/admin/ConnectionPoolMonitoringService.groovy b/grails-app/services/io/xh/hoist/admin/ConnectionPoolMonitoringService.groovy index 3abc6688..37dc7e38 100644 --- a/grails-app/services/io/xh/hoist/admin/ConnectionPoolMonitoringService.groovy +++ b/grails-app/services/io/xh/hoist/admin/ConnectionPoolMonitoringService.groovy @@ -35,8 +35,9 @@ class ConnectionPoolMonitoringService extends BaseService { void init() { createTimer( - interval: {enabled ? config.snapshotInterval * DateTimeUtils.SECONDS: -1}, - runFn: this.&takeSnapshot + name: 'takeSnapshot', + runFn: this.&takeSnapshot, + interval: {enabled ? config.snapshotInterval * DateTimeUtils.SECONDS: -1} ) } diff --git a/grails-app/services/io/xh/hoist/admin/MemoryMonitoringService.groovy b/grails-app/services/io/xh/hoist/admin/MemoryMonitoringService.groovy index 386df695..3a33d019 100644 --- a/grails-app/services/io/xh/hoist/admin/MemoryMonitoringService.groovy +++ b/grails-app/services/io/xh/hoist/admin/MemoryMonitoringService.groovy @@ -33,8 +33,9 @@ class MemoryMonitoringService extends BaseService { void init() { createTimer( - interval: {this.enabled ? config.snapshotInterval * DateTimeUtils.SECONDS: -1}, - runFn: this.&takeSnapshot + name: 'takeSnapshot', + runFn: this.&takeSnapshot, + interval: {this.enabled ? config.snapshotInterval * DateTimeUtils.SECONDS: -1} ) } diff --git a/grails-app/services/io/xh/hoist/alertbanner/AlertBannerService.groovy b/grails-app/services/io/xh/hoist/alertbanner/AlertBannerService.groovy index f369af57..d93fc63b 100644 --- a/grails-app/services/io/xh/hoist/alertbanner/AlertBannerService.groovy +++ b/grails-app/services/io/xh/hoist/alertbanner/AlertBannerService.groovy @@ -42,18 +42,14 @@ class AlertBannerService extends BaseService { private final static String presetsBlobName = 'xhAlertBannerPresets' private final Map emptyAlert = [active: false] - private CachedValue _alertBanner = new CachedValue<>( - name: 'alertBanner', - replicate: true, - svc: this - ) + private CachedValue _alertBanner = createCachedValue(name: 'alertBanner', replicate: true) private Timer timer void init() { timer = createTimer( name: 'readFromSpec', - interval: 2 * MINUTES, runFn: this.&readFromSpec, + interval: 2 * MINUTES, primaryOnly: true ) super.init() diff --git a/grails-app/services/io/xh/hoist/clienterror/ClientErrorService.groovy b/grails-app/services/io/xh/hoist/clienterror/ClientErrorService.groovy index d6e43dda..abf41358 100644 --- a/grails-app/services/io/xh/hoist/clienterror/ClientErrorService.groovy +++ b/grails-app/services/io/xh/hoist/clienterror/ClientErrorService.groovy @@ -49,6 +49,7 @@ class ClientErrorService extends BaseService { super.init() createTimer( name: 'processErrors', + runFn: this.&processErrors, interval: { alertInterval }, delay: 15 * SECONDS, primaryOnly: true @@ -100,7 +101,7 @@ class ClientErrorService extends BaseService { // Implementation //--------------------------------------------------------- @Transactional - void onTimer() { + private void processErrors() { if (!errors) return def maxErrors = getMaxErrors(), diff --git a/grails-app/services/io/xh/hoist/ldap/LdapService.groovy b/grails-app/services/io/xh/hoist/ldap/LdapService.groovy index 06991157..52e18412 100644 --- a/grails-app/services/io/xh/hoist/ldap/LdapService.groovy +++ b/grails-app/services/io/xh/hoist/ldap/LdapService.groovy @@ -41,9 +41,9 @@ class LdapService extends BaseService { def configService - private Cache> cache = new Cache<>( - expireTime: {config.cacheExpireSecs * SECONDS}, - svc: this + private Cache> cache = createCache( + name: 'queryCache', + expireTime: {config.cacheExpireSecs * SECONDS} ) static clearCachesConfigs = ['xhLdapConfig', 'xhLdapUsername', 'xhLdapPassword'] diff --git a/grails-app/services/io/xh/hoist/log/LogArchiveService.groovy b/grails-app/services/io/xh/hoist/log/LogArchiveService.groovy index 27943b8a..b3ecdbf6 100644 --- a/grails-app/services/io/xh/hoist/log/LogArchiveService.groovy +++ b/grails-app/services/io/xh/hoist/log/LogArchiveService.groovy @@ -27,7 +27,11 @@ class LogArchiveService extends BaseService { logReaderService void init() { - createTimer(interval: 1 * DAYS) + createTimer( + name: 'archiveLogs', + runFn: { archiveLogs((Integer) config.archiveAfterDays)}, + interval: 1 * DAYS + ) } List archiveLogs(Integer daysThreshold) { @@ -69,12 +73,6 @@ class LogArchiveService extends BaseService { //------------------------ // Implementation //------------------------ - private void onTimer() { - if (isPrimary) { - archiveLogs((Integer) config.archiveAfterDays) - } - } - private File getArchiveDir(String logPath, String category) { return new File(logPath + separator + config.archiveFolder + separator + category) } diff --git a/grails-app/services/io/xh/hoist/log/LogLevelService.groovy b/grails-app/services/io/xh/hoist/log/LogLevelService.groovy index 1cca82a8..ae109116 100644 --- a/grails-app/services/io/xh/hoist/log/LogLevelService.groovy +++ b/grails-app/services/io/xh/hoist/log/LogLevelService.groovy @@ -23,11 +23,12 @@ class LogLevelService extends BaseService { private List adjustments = [] void init() { - createTimer(interval: 30 * MINUTES, runImmediatelyAndBlock: true) - } - - private void onTimer() { - calculateAdjustments() + createTimer( + name: 'calculateAdjustments', + runFn: this.&calculateAdjustments, + interval: 30 * MINUTES, + runImmediatelyAndBlock: true + ) } // ------------------------------------------------------------------------------- diff --git a/grails-app/services/io/xh/hoist/monitor/MonitorService.groovy b/grails-app/services/io/xh/hoist/monitor/MonitorService.groovy index aeebabd0..ffa8cb85 100644 --- a/grails-app/services/io/xh/hoist/monitor/MonitorService.groovy +++ b/grails-app/services/io/xh/hoist/monitor/MonitorService.groovy @@ -41,10 +41,9 @@ class MonitorService extends BaseService { // Shared state for all servers to read - gathered by primary from all instances. // Map of monitor code to aggregated (cross-instance) results. - private CachedValue> _results = new CachedValue<>( + private CachedValue> _results = createCachedValue( name: 'results', - replicate: true, - svc: this + replicate: true ) private Timer timer @@ -52,7 +51,8 @@ class MonitorService extends BaseService { void init() { timer = createTimer( name: 'runMonitors', - interval: { monitorInterval }, + runFn: this.&runMonitors, + interval: {monitorInterval}, delay: startupDelay, primaryOnly: true ) @@ -87,7 +87,7 @@ class MonitorService extends BaseService { //------------------ // Implementation //------------------ - private void onTimer() { + private void runMonitors() { // Gather per-instance results from across the cluster Map> newChecks = clusterService .submitToAllInstances(new RunAllMonitorsTask()) diff --git a/src/main/groovy/io/xh/hoist/BaseService.groovy b/src/main/groovy/io/xh/hoist/BaseService.groovy index 85b8e1c8..44d73a12 100644 --- a/src/main/groovy/io/xh/hoist/BaseService.groovy +++ b/src/main/groovy/io/xh/hoist/BaseService.groovy @@ -15,6 +15,10 @@ import com.hazelcast.topic.Message import grails.async.Promises import grails.util.GrailsClassUtils import groovy.transform.CompileDynamic +import groovy.transform.NamedParam +import groovy.transform.NamedVariant +import io.xh.hoist.cache.Cache +import io.xh.hoist.cache.CachedValue import io.xh.hoist.cluster.ClusterService import io.xh.hoist.exception.ExceptionHandler import io.xh.hoist.log.LogSupport @@ -32,6 +36,7 @@ import java.util.concurrent.TimeUnit import static grails.async.Promises.task import static io.xh.hoist.util.DateTimeUtils.SECONDS +import static io.xh.hoist.util.DateTimeUtils.MINUTES import static io.xh.hoist.util.Utils.appContext import static io.xh.hoist.util.Utils.getConfigService @@ -40,6 +45,12 @@ import static io.xh.hoist.util.Utils.getConfigService * Provides template methods for service lifecycle / state management plus support for user lookups. * As an abstract class, BaseService must reside in src/main/groovy to allow Java compilation and * to ensure it is not itself instantiated as a Grails service. + * + * BaseService also provides support for cluster aware state via factories to create + * Hoist objects such as Cache, CachedValue, Timer, as well as raw Hazelcast distributed + * data structures such as ReplicatedMap, ISet and IMap. Objects created with these factories + * will be associated with this service for the purposes of logging and management via the + * Hoist admin console. */ abstract class BaseService implements LogSupport, IdentitySupport, DisposableBean { @@ -60,6 +71,9 @@ abstract class BaseService implements LogSupport, IdentitySupport, DisposableBea private final Logger _log = LoggerFactory.getLogger(this.class) + private Set resourceNames = new HashSet() + + /** * Initialize a collection of BaseServices in parallel. * @@ -109,37 +123,110 @@ abstract class BaseService implements LogSupport, IdentitySupport, DisposableBea // Distributed Resources // Use static reference to ClusterService to allow access pre-init. //------------------------------------------------------------------ - IMap getIMap(String id) { - ClusterService.hzInstance.getMap(hzName(id)) + /** + * Get a reference to a Hazelcast IMap. + * + * @param name - unique name relative to all Caches, Timers and Hazelcast objects + * associated with this service. + */ + IMap getIMap(String name) { + ensureUniqueResourceName(name) + ClusterService.hzInstance.getMap(hzName(name)) } - ISet getISet(String id) { - ClusterService.hzInstance.getSet(hzName(id)) + /** + * Get a reference to a Hazelcast ISet. + * + * @param name - unique name relative to all Caches, Timers and Hazelcast objects + * associated with this service. + */ + ISet getISet(String name) { + ensureUniqueResourceName(name) + ClusterService.hzInstance.getSet(hzName(name)) } - ReplicatedMap getReplicatedMap(String id) { - ClusterService.hzInstance.getReplicatedMap(hzName(id)) + /** + * Get a reference to a Hazelcast Replicated Map. + * + * @param name - unique name relative to all Caches, Timers and Hazelcast objects + * associated with this service. + */ + ReplicatedMap getReplicatedMap(String name) { + ensureUniqueResourceName(name) + ClusterService.hzInstance.getReplicatedMap(hzName(name)) } - ITopic getTopic(String id) { + /** + * Get a reference to a Hazelcast Replicated topic. + */ + ITopic getTopic(String id) { ClusterService.hzInstance.getTopic(id) } /** * Create a new managed Timer bound to this service. - * @param args - arguments appropriate for a Hoist Timer. + * + * Note that the provided name should be unique with respect to all + * Caches, Timers and Hazelcast objects associated with this service. + * + * @see Timer */ @CompileDynamic - protected Timer createTimer(Map args) { - args.owner = this - if (!args.runFn && metaClass.respondsTo(this, 'onTimer')) { - args.runFn = this.&onTimer + @NamedVariant + Timer createTimer( + @NamedParam(required = true) String name, + @NamedParam Closure runFn = null, + @NamedParam Boolean primaryOnly = false, + @NamedParam Boolean runImmediatelyAndBlock = false, + @NamedParam Object interval = null, + @NamedParam Object timeout = 3 * MINUTES, + @NamedParam Object delay = false, + @NamedParam Long intervalUnits = 1, + @NamedParam Long timeoutUnits = 1 + ) { + ensureUniqueResourceName(name) + if (!runFn) { + if (metaClass.respondsTo(this, 'onTimer')) { + runFn = this.&onTimer + } else { + throw new IllegalArgumentException('Must specify a runFn, or provide an onTimer() method on this service.') + } } - def ret = new Timer(args) + + def ret = new Timer(name, this, runFn, primaryOnly, runImmediatelyAndBlock, interval, timeout, delay, intervalUnits, timeoutUnits) timers << ret return ret } + /** + * Create a new Cache bound to this service. + * + * Note that the provided name should be unique with respect to all + * Caches, Timers and Hazelcast objects associated with this service. + * + * @see Cache + */ + Cache createCache(Map mp) { + // Cannot use @NamedVariant, as incompatible with generics. We'll still get run-time checks. + ensureUniqueResourceName(mp.name) + return new Cache([*:mp, svc: this]) + } + + /** + * Create a new CachedValue bound to this service. + * + * Note that the provided name should be unique with respect to all + * Caches, Timers and Hazelcast objects associated with this service. + * + * @see CachedValue + */ + CachedValue createCachedValue(Map mp) { + // Cannot use @NamedVariant, as incompatible with generics. We'll still get run-time checks. + ensureUniqueResourceName(mp.name) + return new CachedValue([*:mp, svc: this]) + } + + /** * Managed Subscription to a Grails Event. * @@ -292,8 +379,17 @@ abstract class BaseService implements LogSupport, IdentitySupport, DisposableBea //------------------------ // Internal implementation //------------------------ - protected String hzName(String key) { - this.class.name + '_' + key + protected String hzName(String name) { + this.class.name + '_' + name + } + + private void ensureUniqueResourceName(String name) { + if (!name || resourceNames(name)) { + def msg = 'Service resource requires a unique name. ' + if (name) msg += "Name `$name` already used on this service." + throw new RuntimeException(msg) + } + resourceNames << name } /** @internal - for use by CachedValue */ diff --git a/src/main/groovy/io/xh/hoist/cache/BaseCache.groovy b/src/main/groovy/io/xh/hoist/cache/BaseCache.groovy index faed7bcc..fbe3528f 100644 --- a/src/main/groovy/io/xh/hoist/cache/BaseCache.groovy +++ b/src/main/groovy/io/xh/hoist/cache/BaseCache.groovy @@ -55,16 +55,16 @@ abstract class BaseCache { public final List onChange = [] BaseCache( - BaseService svc, String name, + BaseService svc, Object expireTime, Closure expireFn, Closure timestampFn, boolean replicate, boolean serializeOldValue ) { - this.svc = svc this.name = name + this.svc = svc this.expireTime = expireTime this.expireFn = expireFn this.timestampFn = timestampFn diff --git a/src/main/groovy/io/xh/hoist/cache/Cache.groovy b/src/main/groovy/io/xh/hoist/cache/Cache.groovy index 68343c1b..03e1fedb 100644 --- a/src/main/groovy/io/xh/hoist/cache/Cache.groovy +++ b/src/main/groovy/io/xh/hoist/cache/Cache.groovy @@ -30,33 +30,33 @@ class Cache extends BaseCache { private final Map> _map private final Timer timer + /** + * @internal + * + * Not typically created directly. Use BaseService.createCache() instead. + */ @NamedVariant Cache( + @NamedParam(required = true) String name, @NamedParam(required = true) BaseService svc, - @NamedParam String name, @NamedParam Object expireTime = null, @NamedParam Closure expireFn = null, @NamedParam Closure timestampFn = null, - @NamedParam boolean replicate = false, - @NamedParam boolean serializeOldValue = false, + @NamedParam Boolean replicate = false, + @NamedParam Boolean serializeOldValue = false, @NamedParam Closure onChange = null ) { - super(svc, name, expireTime, expireFn, timestampFn, replicate, serializeOldValue) - - if (replicate && !name) { - throw new IllegalArgumentException("Cannot create a replicated Cache without a unique name") - } + super(name, svc, expireTime, expireFn, timestampFn, replicate, serializeOldValue) _map = useCluster ? svc.getReplicatedMap(name) : new ConcurrentHashMap() if (onChange) addChangeHandler(onChange) - timer = new Timer( - name: name ? "cullEntries_$name" : 'cullEntries', - owner: svc, - primaryOnly: replicate, + timer = svc.createTimer( + name: "${name}_cullEntries", runFn: this.&cullEntries, interval: 15 * MINUTES, - delay: true + delay: true, + primaryOnly: replicate ) } @@ -176,7 +176,7 @@ class Cache extends BaseCache { } if (cullKeys) { - svc.logDebug("Cache '${name ?: "anon"}' culled ${cullKeys.size()} out of $oldSize entries") + svc.logDebug("Cache '$name' culled ${cullKeys.size()} out of $oldSize entries") } } } diff --git a/src/main/groovy/io/xh/hoist/cache/CachedValue.groovy b/src/main/groovy/io/xh/hoist/cache/CachedValue.groovy index 91103548..467e07bb 100644 --- a/src/main/groovy/io/xh/hoist/cache/CachedValue.groovy +++ b/src/main/groovy/io/xh/hoist/cache/CachedValue.groovy @@ -19,18 +19,23 @@ class CachedValue extends BaseCache { private final Map> _map + /** + * @internal + * + * Not typically created directly. Use BaseService.createCachedValue() instead. + */ @NamedVariant CachedValue( - @NamedParam(required = true) BaseService svc, @NamedParam(required = true) String name, + @NamedParam(required = true) BaseService svc, @NamedParam Object expireTime = null, @NamedParam Closure expireFn = null, @NamedParam Closure timestampFn = null, - @NamedParam boolean replicate = false, - @NamedParam boolean serializeOldValue = false, + @NamedParam Boolean replicate = false, + @NamedParam Boolean serializeOldValue = false, @NamedParam Closure onChange = null ) { - super(svc, name, expireTime, expireFn, timestampFn, replicate, serializeOldValue) + super(name, svc, expireTime, expireFn, timestampFn, replicate, serializeOldValue) _map = useCluster ? svc.replicatedCachedValuesMap : svc.localCachedValuesMap if (onChange) addChangeHandler(onChange) diff --git a/src/main/groovy/io/xh/hoist/role/provided/DefaultRoleService.groovy b/src/main/groovy/io/xh/hoist/role/provided/DefaultRoleService.groovy index 605d94be..372ef51c 100644 --- a/src/main/groovy/io/xh/hoist/role/provided/DefaultRoleService.groovy +++ b/src/main/groovy/io/xh/hoist/role/provided/DefaultRoleService.groovy @@ -81,10 +81,9 @@ class DefaultRoleService extends BaseRoleService { DefaultRoleUpdateService defaultRoleUpdateService private Timer timer - protected CachedValue>> _allRoleAssignments = new CachedValue( + protected CachedValue>> _allRoleAssignments = createCachedValue( name: 'roleAssignments', replicate: true, - svc: this, onChange: { _roleAssignmentsByUser = new ConcurrentHashMap() } @@ -103,8 +102,8 @@ class DefaultRoleService extends BaseRoleService { timer = createTimer( name: 'refreshRoles', - interval: { config.refreshIntervalSecs as int * SECONDS }, runFn: this.&refreshRoleAssignments, + interval: { config.refreshIntervalSecs as int * SECONDS }, runImmediatelyAndBlock: true, primaryOnly: true ) diff --git a/src/main/groovy/io/xh/hoist/util/Timer.groovy b/src/main/groovy/io/xh/hoist/util/Timer.groovy index d4c6c446..cfdb2013 100644 --- a/src/main/groovy/io/xh/hoist/util/Timer.groovy +++ b/src/main/groovy/io/xh/hoist/util/Timer.groovy @@ -7,6 +7,8 @@ package io.xh.hoist.util +import groovy.transform.NamedParam +import groovy.transform.NamedVariant import io.xh.hoist.BaseService import io.xh.hoist.cache.CachedValue import io.xh.hoist.log.LogSupport @@ -124,30 +126,36 @@ class Timer { * Applications should not typically use this constructor directly. Timers are typically * created by services using the createTimer() method supplied by io.xh.hoist.BaseService. */ - Timer(Map config) { - name = config.name - owner = config.owner - runFn = config.runFn - primaryOnly = config.primaryOnly ?: false - runImmediatelyAndBlock = config.runImmediatelyAndBlock ?: false - interval = parseDynamicValue(config.interval) - timeout = parseDynamicValue(config.containsKey('timeout') ? config.timeout : 3 * MINUTES) - delay = config.delay ?: false - - intervalUnits = config.intervalUnits ?: 1 - timeoutUnits = config.timeoutUnits ?: 1 - - if ([owner, interval, runFn].contains(null)) throw new RuntimeException('Missing required arguments for Timer.') - if (config.delayUnits) throw new RuntimeException('delayUnits has been removed from the API. Specify delay in ms.') + @NamedVariant + Timer( + @NamedParam(required = true) String name, + @NamedParam(required = true) LogSupport owner, + @NamedParam(required = true) Closure runFn, + @NamedParam Boolean primaryOnly = false, + @NamedParam Boolean runImmediatelyAndBlock = false, + @NamedParam Object interval = null, + @NamedParam Object timeout = 3 * MINUTES, + @NamedParam Object delay = false, + @NamedParam Long intervalUnits = 1, + @NamedParam Long timeoutUnits = 1 + ) { + this.name = name + this.owner = owner + this.runFn = runFn + this.primaryOnly = primaryOnly + this.runImmediatelyAndBlock = runImmediatelyAndBlock + this.interval = parseDynamicValue(interval) + this.timeout = parseDynamicValue(timeout) + this.delay = delay + this.intervalUnits = intervalUnits + this.timeoutUnits = timeoutUnits if (primaryOnly) { - if (!name) { - throw new IllegalArgumentException("Cannot create a 'primaryOnly' timer without a unique name") - } if (!owner instanceof BaseService) { throw new IllegalArgumentException("A 'primaryOnly' timer must be owned by an instance of BaseService.") } - _lastCompletedOnCluster = new CachedValue<>(name: "xhTimer_$name", svc: owner as BaseService) + + _lastCompletedOnCluster = (owner as BaseService).createCachedValue(name: "${name}_lastCompleted") } intervalMs = calcIntervalMs() @@ -248,7 +256,7 @@ class Timer { exceptionHandler.handleException( exception: throwable, logTo: owner, - logMessage: "Failure in ${name ?: 'timer'}" + logMessage: "Failure in '$name'" ) } catch (Throwable ignore) { owner.logError('Failed to handle exception in Timer') From 0e43f13a4215c0834ede24897fb29b76391ea85b Mon Sep 17 00:00:00 2001 From: lbwexler Date: Sat, 14 Sep 2024 18:25:21 -0400 Subject: [PATCH 04/14] Fix --- src/main/groovy/io/xh/hoist/BaseService.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/groovy/io/xh/hoist/BaseService.groovy b/src/main/groovy/io/xh/hoist/BaseService.groovy index 44d73a12..8bf18520 100644 --- a/src/main/groovy/io/xh/hoist/BaseService.groovy +++ b/src/main/groovy/io/xh/hoist/BaseService.groovy @@ -384,7 +384,7 @@ abstract class BaseService implements LogSupport, IdentitySupport, DisposableBea } private void ensureUniqueResourceName(String name) { - if (!name || resourceNames(name)) { + if (!name || resourceNames.contains(name)) { def msg = 'Service resource requires a unique name. ' if (name) msg += "Name `$name` already used on this service." throw new RuntimeException(msg) From c7a454dba03210275e1f343ebac7b83084b301a2 Mon Sep 17 00:00:00 2001 From: lbwexler Date: Sat, 14 Sep 2024 19:10:52 -0400 Subject: [PATCH 05/14] Fixes caught by new uniqueness check. --- src/main/groovy/io/xh/hoist/BaseService.groovy | 15 +++++++++++++-- src/main/groovy/io/xh/hoist/cache/Cache.groovy | 2 +- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/main/groovy/io/xh/hoist/BaseService.groovy b/src/main/groovy/io/xh/hoist/BaseService.groovy index 8bf18520..4f63c459 100644 --- a/src/main/groovy/io/xh/hoist/BaseService.groovy +++ b/src/main/groovy/io/xh/hoist/BaseService.groovy @@ -392,13 +392,24 @@ abstract class BaseService implements LogSupport, IdentitySupport, DisposableBea resourceNames << name } + /** @internal - for use by Cache */ + Map getCacheReplicatedMap(String name) { + ClusterService.hzInstance.getReplicatedMap(hzName(name)) + } + /** @internal - for use by CachedValue */ Map getReplicatedCachedValuesMap() { - _replicatedCachedValues ?= getReplicatedMap('cachedValues') + if (_replicatedCachedValues == null) { + _replicatedCachedValues = getReplicatedMap('cachedValues') + } + return _replicatedCachedValues } /** @internal - for use by CachedValue */ Map getLocalCachedValuesMap() { - _localCachedValues ?= new ConcurrentHashMap() + if (_localCachedValues == null) { + _localCachedValues = new ConcurrentHashMap() + } + return _localCachedValues } } diff --git a/src/main/groovy/io/xh/hoist/cache/Cache.groovy b/src/main/groovy/io/xh/hoist/cache/Cache.groovy index 03e1fedb..2e2e8beb 100644 --- a/src/main/groovy/io/xh/hoist/cache/Cache.groovy +++ b/src/main/groovy/io/xh/hoist/cache/Cache.groovy @@ -48,7 +48,7 @@ class Cache extends BaseCache { ) { super(name, svc, expireTime, expireFn, timestampFn, replicate, serializeOldValue) - _map = useCluster ? svc.getReplicatedMap(name) : new ConcurrentHashMap() + _map = useCluster ? svc.getCacheReplicatedMap(name) : new ConcurrentHashMap() if (onChange) addChangeHandler(onChange) timer = svc.createTimer( From 6206f87a84286fa5e314ad5c04414d9b641d2abf Mon Sep 17 00:00:00 2001 From: lbwexler Date: Sun, 15 Sep 2024 12:47:04 -0400 Subject: [PATCH 06/14] Tweak to tighten uniqueness check --- .../groovy/io/xh/hoist/BaseService.groovy | 33 +++++++++---------- .../groovy/io/xh/hoist/cache/BaseCache.groovy | 9 ++--- .../groovy/io/xh/hoist/cache/Cache.groovy | 4 +-- .../io/xh/hoist/cache/CachedValue.groovy | 2 +- 4 files changed, 22 insertions(+), 26 deletions(-) diff --git a/src/main/groovy/io/xh/hoist/BaseService.groovy b/src/main/groovy/io/xh/hoist/BaseService.groovy index 4f63c459..e0645ae0 100644 --- a/src/main/groovy/io/xh/hoist/BaseService.groovy +++ b/src/main/groovy/io/xh/hoist/BaseService.groovy @@ -65,8 +65,8 @@ abstract class BaseService implements LogSupport, IdentitySupport, DisposableBea protected final List timers = [] private boolean _destroyed = false - private Map _replicatedCachedValues - private Map _localCachedValues + private Map _replicatedValues + private Map _localValues private final Logger _log = LoggerFactory.getLogger(this.class) @@ -208,7 +208,6 @@ abstract class BaseService implements LogSupport, IdentitySupport, DisposableBea */ Cache createCache(Map mp) { // Cannot use @NamedVariant, as incompatible with generics. We'll still get run-time checks. - ensureUniqueResourceName(mp.name) return new Cache([*:mp, svc: this]) } @@ -222,7 +221,6 @@ abstract class BaseService implements LogSupport, IdentitySupport, DisposableBea */ CachedValue createCachedValue(Map mp) { // Cannot use @NamedVariant, as incompatible with generics. We'll still get run-time checks. - ensureUniqueResourceName(mp.name) return new CachedValue([*:mp, svc: this]) } @@ -393,23 +391,22 @@ abstract class BaseService implements LogSupport, IdentitySupport, DisposableBea } /** @internal - for use by Cache */ - Map getCacheReplicatedMap(String name) { - ClusterService.hzInstance.getReplicatedMap(hzName(name)) - } - - /** @internal - for use by CachedValue */ - Map getReplicatedCachedValuesMap() { - if (_replicatedCachedValues == null) { - _replicatedCachedValues = getReplicatedMap('cachedValues') - } - return _replicatedCachedValues + Map getMapForCache(Cache cache) { + ensureUniqueResourceName(cache.name) + cache.useCluster ? + ClusterService.hzInstance.getReplicatedMap(hzName(cache.name)) : + new ConcurrentHashMap() } /** @internal - for use by CachedValue */ - Map getLocalCachedValuesMap() { - if (_localCachedValues == null) { - _localCachedValues = new ConcurrentHashMap() + Map getMapForCachedValue(CachedValue cachedValue) { + ensureUniqueResourceName(cachedValue.name) + if (cachedValue.useCluster) { + if (_replicatedValues == null) _replicatedValues = getReplicatedMap('cachedValues') + return _replicatedValues + } else { + if (_localValues == null) _localValues = new ConcurrentHashMap() + return _localValues } - return _localCachedValues } } diff --git a/src/main/groovy/io/xh/hoist/cache/BaseCache.groovy b/src/main/groovy/io/xh/hoist/cache/BaseCache.groovy index fbe3528f..78612783 100644 --- a/src/main/groovy/io/xh/hoist/cache/BaseCache.groovy +++ b/src/main/groovy/io/xh/hoist/cache/BaseCache.groovy @@ -78,13 +78,14 @@ abstract class BaseCache { /** Clear all values. */ abstract void clear() - //------------------------ - // Implementation - //------------------------ - protected boolean getUseCluster() { + /** Is Cache to be stored on cluster? */ + boolean getUseCluster() { return replicate && ClusterService.multiInstanceEnabled } + //------------------------ + // Implementation + //------------------------ protected void fireOnChange(Object key, V oldValue, V value) { def change = new CacheValueChanged(this, key, oldValue, value) onChange.each { it.call(change) } diff --git a/src/main/groovy/io/xh/hoist/cache/Cache.groovy b/src/main/groovy/io/xh/hoist/cache/Cache.groovy index 2e2e8beb..74d3bfad 100644 --- a/src/main/groovy/io/xh/hoist/cache/Cache.groovy +++ b/src/main/groovy/io/xh/hoist/cache/Cache.groovy @@ -12,8 +12,6 @@ import groovy.transform.NamedParam import groovy.transform.NamedVariant import io.xh.hoist.BaseService import io.xh.hoist.util.Timer - -import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.TimeoutException import static io.xh.hoist.util.DateTimeUtils.MINUTES @@ -48,7 +46,7 @@ class Cache extends BaseCache { ) { super(name, svc, expireTime, expireFn, timestampFn, replicate, serializeOldValue) - _map = useCluster ? svc.getCacheReplicatedMap(name) : new ConcurrentHashMap() + _map = svc.getMapForCache(this) if (onChange) addChangeHandler(onChange) timer = svc.createTimer( diff --git a/src/main/groovy/io/xh/hoist/cache/CachedValue.groovy b/src/main/groovy/io/xh/hoist/cache/CachedValue.groovy index 467e07bb..1764d114 100644 --- a/src/main/groovy/io/xh/hoist/cache/CachedValue.groovy +++ b/src/main/groovy/io/xh/hoist/cache/CachedValue.groovy @@ -37,7 +37,7 @@ class CachedValue extends BaseCache { ) { super(name, svc, expireTime, expireFn, timestampFn, replicate, serializeOldValue) - _map = useCluster ? svc.replicatedCachedValuesMap : svc.localCachedValuesMap + _map = svc.getMapForCachedValue(this) if (onChange) addChangeHandler(onChange) } From 43d509e0d5eef9803dc1765b3341bf9aa9c7cc19 Mon Sep 17 00:00:00 2001 From: lbwexler Date: Mon, 16 Sep 2024 13:44:33 -0400 Subject: [PATCH 07/14] Add built-in admin display of all shared resources --- CHANGELOG.md | 3 +- .../hoist/admin/ServiceManagerService.groovy | 40 ++++++------- .../groovy/io/xh/hoist/BaseService.groovy | 58 +++++++++---------- .../groovy/io/xh/hoist/cache/BaseCache.groovy | 4 ++ .../groovy/io/xh/hoist/cache/Cache.groovy | 18 +++++- .../io/xh/hoist/cache/CachedValue.groovy | 8 +++ src/main/groovy/io/xh/hoist/util/Timer.groovy | 6 +- 7 files changed, 80 insertions(+), 57 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5beb98d9..78b31817 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,8 @@ ### 💥 Breaking Changes (upgrade difficulty: 🟢 LOW) * All `Timer`, `Cache`, and `CachedValue` object require a 'name' property. This property was previously optional in many cases, but is now required in order to support new cluster features, -logging, and admin tools. +logging, and admin tools. The new `BaseService.resources` property now will give access to all +resources by name, if needed and replaces `BaseService.timers`. ### 🎁 New Features * `Cache` and `CachedValue` may now be created using a factory on `BaseService`. This streamlined diff --git a/grails-app/services/io/xh/hoist/admin/ServiceManagerService.groovy b/grails-app/services/io/xh/hoist/admin/ServiceManagerService.groovy index 9e698ca9..56028b99 100644 --- a/grails-app/services/io/xh/hoist/admin/ServiceManagerService.groovy +++ b/grails-app/services/io/xh/hoist/admin/ServiceManagerService.groovy @@ -7,6 +7,7 @@ package io.xh.hoist.admin +import com.hazelcast.core.DistributedObject import io.xh.hoist.BaseService class ServiceManagerService extends BaseService { @@ -15,8 +16,6 @@ class ServiceManagerService extends BaseService { clusterAdminService Collection listServices() { - - getServicesInternal().collect { name, svc -> return [ name: name, @@ -28,24 +27,8 @@ class ServiceManagerService extends BaseService { Map getStats(String name) { def svc = grailsApplication.mainContext.getBean(name), - prefix = svc.class.name + '_', - timers = svc.timers*.adminStats, - distObjs = clusterService.distributedObjects - .findAll { it.getName().startsWith(prefix) } - .collect {clusterAdminService.getAdminStatsForObject(it)} - - Map ret = svc.adminStats - if (timers || distObjs) { - ret = ret.clone() - if (distObjs) ret.distributedObjects = distObjs - if (timers.size() == 1) { - ret.timer = timers[0] - } else if (timers.size() > 1) { - ret.timers = timers - } - } - - return ret + resources = getResourceStats(svc) + return resources ? [*: svc.adminStats, resources: resources] : svc.adminStats } void clearCaches(List names) { @@ -60,6 +43,23 @@ class ServiceManagerService extends BaseService { } } + //---------------------- + // Implementation + //---------------------- + private List getResourceStats(BaseService svc) { + svc.resources + .findAll { !it.key.startsWith('xh_') } // skip hoist implementation objects + .collect { k, v -> + Map stats = v instanceof DistributedObject ? + clusterAdminService.getAdminStatsForObject(v) : + v.adminStats + + // rely on the name (key) service knows, i.e avoid HZ prefix + return [*: stats, name: k] + } + } + + private Map getServicesInternal() { return grailsApplication.mainContext.getBeansOfType(BaseService.class, false, false) } diff --git a/src/main/groovy/io/xh/hoist/BaseService.groovy b/src/main/groovy/io/xh/hoist/BaseService.groovy index e0645ae0..d143f52f 100644 --- a/src/main/groovy/io/xh/hoist/BaseService.groovy +++ b/src/main/groovy/io/xh/hoist/BaseService.groovy @@ -62,7 +62,8 @@ abstract class BaseService implements LogSupport, IdentitySupport, DisposableBea Date initializedDate = null Date lastCachesCleared = null - protected final List timers = [] + // Caches, CachedValues and Timers and other distributed objects associated with this service + protected final ConcurrentHashMap resources = [:] private boolean _destroyed = false private Map _replicatedValues @@ -71,9 +72,6 @@ abstract class BaseService implements LogSupport, IdentitySupport, DisposableBea private final Logger _log = LoggerFactory.getLogger(this.class) - private Set resourceNames = new HashSet() - - /** * Initialize a collection of BaseServices in parallel. * @@ -130,8 +128,7 @@ abstract class BaseService implements LogSupport, IdentitySupport, DisposableBea * associated with this service. */ IMap getIMap(String name) { - ensureUniqueResourceName(name) - ClusterService.hzInstance.getMap(hzName(name)) + addResource(name, ClusterService.hzInstance.getMap(hzName(name))) } /** @@ -141,8 +138,7 @@ abstract class BaseService implements LogSupport, IdentitySupport, DisposableBea * associated with this service. */ ISet getISet(String name) { - ensureUniqueResourceName(name) - ClusterService.hzInstance.getSet(hzName(name)) + addResource(name, ClusterService.hzInstance.getSet(hzName(name))) } /** @@ -152,8 +148,7 @@ abstract class BaseService implements LogSupport, IdentitySupport, DisposableBea * associated with this service. */ ReplicatedMap getReplicatedMap(String name) { - ensureUniqueResourceName(name) - ClusterService.hzInstance.getReplicatedMap(hzName(name)) + addResource(name, ClusterService.hzInstance.getReplicatedMap(hzName(name))) } /** @@ -184,7 +179,6 @@ abstract class BaseService implements LogSupport, IdentitySupport, DisposableBea @NamedParam Long intervalUnits = 1, @NamedParam Long timeoutUnits = 1 ) { - ensureUniqueResourceName(name) if (!runFn) { if (metaClass.respondsTo(this, 'onTimer')) { runFn = this.&onTimer @@ -193,9 +187,10 @@ abstract class BaseService implements LogSupport, IdentitySupport, DisposableBea } } - def ret = new Timer(name, this, runFn, primaryOnly, runImmediatelyAndBlock, interval, timeout, delay, intervalUnits, timeoutUnits) - timers << ret - return ret + addResource( + name, + new Timer(name, this, runFn, primaryOnly, runImmediatelyAndBlock, interval, timeout, delay, intervalUnits, timeoutUnits) + ) } /** @@ -208,7 +203,7 @@ abstract class BaseService implements LogSupport, IdentitySupport, DisposableBea */ Cache createCache(Map mp) { // Cannot use @NamedVariant, as incompatible with generics. We'll still get run-time checks. - return new Cache([*:mp, svc: this]) + addResource(mp.name, new Cache([*:mp, svc: this])) } /** @@ -221,7 +216,7 @@ abstract class BaseService implements LogSupport, IdentitySupport, DisposableBea */ CachedValue createCachedValue(Map mp) { // Cannot use @NamedVariant, as incompatible with generics. We'll still get run-time checks. - return new CachedValue([*:mp, svc: this]) + addResource(mp.name, new CachedValue([*:mp, svc: this])) } @@ -315,8 +310,8 @@ abstract class BaseService implements LogSupport, IdentitySupport, DisposableBea * Called by Spring on a clean shutdown of the application. */ void destroy() { - timers.each { - it.cancel() + resources.each { k, v -> + if (v instanceof Timer) v.cancel() } _destroyed = true } @@ -381,28 +376,31 @@ abstract class BaseService implements LogSupport, IdentitySupport, DisposableBea this.class.name + '_' + name } - private void ensureUniqueResourceName(String name) { - if (!name || resourceNames.contains(name)) { + private T addResource(String name, T resource) { + if (!name || resources.containsKey(name)) { def msg = 'Service resource requires a unique name. ' - if (name) msg += "Name `$name` already used on this service." + if (name) msg += "Name '$name' already used on this service." throw new RuntimeException(msg) } - resourceNames << name + resources[name] = resource + return resource } - /** @internal - for use by Cache */ + /** + * @internal - for use by Cache. + */ Map getMapForCache(Cache cache) { - ensureUniqueResourceName(cache.name) - cache.useCluster ? - ClusterService.hzInstance.getReplicatedMap(hzName(cache.name)) : - new ConcurrentHashMap() + // register under xh name to avoid collisions, allow filtering out in admin + cache.useCluster ? getReplicatedMap("xh_${cache.name}") : new ConcurrentHashMap() } - /** @internal - for use by CachedValue */ + /** + * @internal - for use by CachedValue + */ Map getMapForCachedValue(CachedValue cachedValue) { - ensureUniqueResourceName(cachedValue.name) + // register with xh prefix to avoid collisions, allow filtering out in admin if (cachedValue.useCluster) { - if (_replicatedValues == null) _replicatedValues = getReplicatedMap('cachedValues') + if (_replicatedValues == null) _replicatedValues = getReplicatedMap('xh_cachedValues') return _replicatedValues } else { if (_localValues == null) _localValues = new ConcurrentHashMap() diff --git a/src/main/groovy/io/xh/hoist/cache/BaseCache.groovy b/src/main/groovy/io/xh/hoist/cache/BaseCache.groovy index 78612783..7d6dccc4 100644 --- a/src/main/groovy/io/xh/hoist/cache/BaseCache.groovy +++ b/src/main/groovy/io/xh/hoist/cache/BaseCache.groovy @@ -83,6 +83,10 @@ abstract class BaseCache { return replicate && ClusterService.multiInstanceEnabled } + + /** Information about this object for admin purposes */ + abstract Map getAdminStats() + //------------------------ // Implementation //------------------------ diff --git a/src/main/groovy/io/xh/hoist/cache/Cache.groovy b/src/main/groovy/io/xh/hoist/cache/Cache.groovy index 74d3bfad..68798293 100644 --- a/src/main/groovy/io/xh/hoist/cache/Cache.groovy +++ b/src/main/groovy/io/xh/hoist/cache/Cache.groovy @@ -26,7 +26,7 @@ import static java.lang.System.currentTimeMillis class Cache extends BaseCache { private final Map> _map - private final Timer timer + private final Timer cullTimer /** * @internal @@ -49,8 +49,8 @@ class Cache extends BaseCache { _map = svc.getMapForCache(this) if (onChange) addChangeHandler(onChange) - timer = svc.createTimer( - name: "${name}_cullEntries", + cullTimer = svc.createTimer( + name: "xh_${name}_cullEntries", runFn: this.&cullEntries, interval: 15 * MINUTES, delay: true, @@ -160,6 +160,18 @@ class Cache extends BaseCache { } } + + Map getAdminStats() { + [ + name : name, + type : 'Cache' + (replicate ? '(replicated)' : ''), + count : size(), + latestTimestamp: _map.max { it.value.dateEntered }?.value?.dateEntered, + lastCullTime : cullTimer.lastRunCompleted + ] + } + + //------------------------ // Implementation //------------------------ diff --git a/src/main/groovy/io/xh/hoist/cache/CachedValue.groovy b/src/main/groovy/io/xh/hoist/cache/CachedValue.groovy index 1764d114..4e9d3dce 100644 --- a/src/main/groovy/io/xh/hoist/cache/CachedValue.groovy +++ b/src/main/groovy/io/xh/hoist/cache/CachedValue.groovy @@ -114,4 +114,12 @@ class CachedValue extends BaseCache { } onChange << handler } + + Map getAdminStats() { + [ + name : name, + type : 'CachedValue' + (replicate ? ' (replicated)' : ''), + timestamp: timestamp + ] + } } diff --git a/src/main/groovy/io/xh/hoist/util/Timer.groovy b/src/main/groovy/io/xh/hoist/util/Timer.groovy index cfdb2013..401873f5 100644 --- a/src/main/groovy/io/xh/hoist/util/Timer.groovy +++ b/src/main/groovy/io/xh/hoist/util/Timer.groovy @@ -155,7 +155,7 @@ class Timer { throw new IllegalArgumentException("A 'primaryOnly' timer must be owned by an instance of BaseService.") } - _lastCompletedOnCluster = (owner as BaseService).createCachedValue(name: "${name}_lastCompleted") + _lastCompletedOnCluster = (owner as BaseService).createCachedValue(name: "xh_${name}_lastCompleted") } intervalMs = calcIntervalMs() @@ -201,12 +201,12 @@ class Timer { } /** - * Information about this time for admin purposes. + * Information about this timer for admin purposes. */ Map getAdminStats() { [ name: name, - primaryOnly: primaryOnly?: null, + type: 'Timer' + (primaryOnly ? ' (primary only)': ''), intervalMs: intervalMs, isRunning: isRunning, startTime: isRunning ? _lastRunStarted: null, From 3d39703fb3b2ec19f171561c0ada320064ebb0fe Mon Sep 17 00:00:00 2001 From: lbwexler Date: Mon, 16 Sep 2024 14:02:49 -0400 Subject: [PATCH 08/14] Tweaks + remove redundant data for ClientErrorService --- .../services/io/xh/hoist/admin/MemoryMonitoringService.groovy | 2 +- .../services/io/xh/hoist/clienterror/ClientErrorService.groovy | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/grails-app/services/io/xh/hoist/admin/MemoryMonitoringService.groovy b/grails-app/services/io/xh/hoist/admin/MemoryMonitoringService.groovy index 3a33d019..3d09b794 100644 --- a/grails-app/services/io/xh/hoist/admin/MemoryMonitoringService.groovy +++ b/grails-app/services/io/xh/hoist/admin/MemoryMonitoringService.groovy @@ -179,6 +179,6 @@ class MemoryMonitoringService extends BaseService { Map getAdminStats() {[ config: configForAdminStats('xhMemoryMonitoringConfig'), - latestSnapshot: latestSnapshot, + latestSnapshot: latestSnapshot ]} } diff --git a/grails-app/services/io/xh/hoist/clienterror/ClientErrorService.groovy b/grails-app/services/io/xh/hoist/clienterror/ClientErrorService.groovy index abf41358..7c6a66ea 100644 --- a/grails-app/services/io/xh/hoist/clienterror/ClientErrorService.groovy +++ b/grails-app/services/io/xh/hoist/clienterror/ClientErrorService.groovy @@ -123,8 +123,7 @@ class ClientErrorService extends BaseService { } Map getAdminStats() {[ - config: configForAdminStats('xhClientErrorConfig'), - pendingErrorCount: errors.size() + config: configForAdminStats('xhClientErrorConfig') ]} } From 6c0a0a7cd30051dce30deae9ca2d85e2bcab6a31 Mon Sep 17 00:00:00 2001 From: Anselm McClain Date: Mon, 16 Sep 2024 14:08:31 -0700 Subject: [PATCH 09/14] Use `@NamedVariant` for `subscribeToTopic` + Tweak `ConfigService` publishing of config changes - no need to get topic from ClusterService --- .../services/io/xh/hoist/config/ConfigService.groovy | 3 +-- .../io/xh/hoist/feedback/FeedbackEmailService.groovy | 1 - src/main/groovy/io/xh/hoist/BaseService.groovy | 12 ++++++------ 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/grails-app/services/io/xh/hoist/config/ConfigService.groovy b/grails-app/services/io/xh/hoist/config/ConfigService.groovy index 7b75bbf4..0279a331 100644 --- a/grails-app/services/io/xh/hoist/config/ConfigService.groovy +++ b/grails-app/services/io/xh/hoist/config/ConfigService.groovy @@ -207,8 +207,7 @@ class ConfigService extends BaseService { } void fireConfigChanged(AppConfig obj) { - def topic = clusterService.getTopic('xhConfigChanged') - topic.publishAsync([key: obj.name, value: obj.externalValue()]) + getTopic('xhConfigChanged').publishAsync([key: obj.name, value: obj.externalValue()]) } //------------------- diff --git a/grails-app/services/io/xh/hoist/feedback/FeedbackEmailService.groovy b/grails-app/services/io/xh/hoist/feedback/FeedbackEmailService.groovy index c7ed4c77..597bade4 100644 --- a/grails-app/services/io/xh/hoist/feedback/FeedbackEmailService.groovy +++ b/grails-app/services/io/xh/hoist/feedback/FeedbackEmailService.groovy @@ -16,7 +16,6 @@ class FeedbackEmailService extends BaseService { void init() { subscribeToTopic( - name: 'emailFeedback', topic: 'xhFeedbackReceived', onMessage: this.&emailFeedback, primaryOnly: true diff --git a/src/main/groovy/io/xh/hoist/BaseService.groovy b/src/main/groovy/io/xh/hoist/BaseService.groovy index d143f52f..1ec5a44b 100644 --- a/src/main/groovy/io/xh/hoist/BaseService.groovy +++ b/src/main/groovy/io/xh/hoist/BaseService.groovy @@ -258,12 +258,12 @@ abstract class BaseService implements LogSupport, IdentitySupport, DisposableBea * This subscription also avoids firing handlers on destroyed services. This is important in a * hot-reloading scenario where multiple instances of singleton services may be created. */ - protected void subscribeToTopic(Map config) { - def topic = config.topic as String, - onMessage = config.onMessage as Closure, - primaryOnly = config.primaryOnly as Boolean - - + @NamedVariant + protected void subscribeToTopic( + @NamedParam(required = true) String topic, + @NamedParam(required = true) Closure onMessage, + @NamedParam Boolean primaryOnly = false + ) { getTopic(topic).addMessageListener { Message m -> if (destroyed || (primaryOnly && !isPrimary)) return try { From 8f995ee333f44fa39873c84e8ddd359e34194489 Mon Sep 17 00:00:00 2001 From: Anselm McClain Date: Mon, 16 Sep 2024 14:09:11 -0700 Subject: [PATCH 10/14] Doc comment extensions + tweaks --- .../groovy/io/xh/hoist/BaseService.groovy | 62 +++++++++---------- .../groovy/io/xh/hoist/cache/BaseCache.groovy | 5 +- .../groovy/io/xh/hoist/cache/Cache.groovy | 6 +- .../io/xh/hoist/cache/CachedValue.groovy | 6 +- src/main/groovy/io/xh/hoist/util/Timer.groovy | 31 ++++++---- 5 files changed, 52 insertions(+), 58 deletions(-) diff --git a/src/main/groovy/io/xh/hoist/BaseService.groovy b/src/main/groovy/io/xh/hoist/BaseService.groovy index 1ec5a44b..188994e6 100644 --- a/src/main/groovy/io/xh/hoist/BaseService.groovy +++ b/src/main/groovy/io/xh/hoist/BaseService.groovy @@ -122,49 +122,48 @@ abstract class BaseService implements LogSupport, IdentitySupport, DisposableBea // Use static reference to ClusterService to allow access pre-init. //------------------------------------------------------------------ /** - * Get a reference to a Hazelcast IMap. + * Create and return a reference to a Hazelcast IMap. * - * @param name - unique name relative to all Caches, Timers and Hazelcast objects - * associated with this service. + * @param name - must be unique across all Caches, Timers and distributed Hazelcast objects + * associated with this service. */ IMap getIMap(String name) { addResource(name, ClusterService.hzInstance.getMap(hzName(name))) } /** - * Get a reference to a Hazelcast ISet. + * Create and return a reference to a Hazelcast ISet. * - * @param name - unique name relative to all Caches, Timers and Hazelcast objects - * associated with this service. + * @param name - must be unique across all Caches, Timers and distributed Hazelcast objects + * associated with this service. */ ISet getISet(String name) { addResource(name, ClusterService.hzInstance.getSet(hzName(name))) } /** - * Get a reference to a Hazelcast Replicated Map. + * Create and return a reference to a Hazelcast Replicated Map. * - * @param name - unique name relative to all Caches, Timers and Hazelcast objects - * associated with this service. + * @param name - must be unique across all Caches, Timers and distributed Hazelcast objects + * associated with this service. */ ReplicatedMap getReplicatedMap(String name) { addResource(name, ClusterService.hzInstance.getReplicatedMap(hzName(name))) } /** - * Get a reference to a Hazelcast Replicated topic. + * Get a reference to a Hazelcast Replicated topic, useful to publish to a cluster-wide topic. + * To subscribe to events fired by other services on a topic, use {@link #subscribeToTopic}. */ ITopic getTopic(String id) { ClusterService.hzInstance.getTopic(id) } /** - * Create a new managed Timer bound to this service. + * Create a new managed {@link Timer} bound to this service. * - * Note that the provided name should be unique with respect to all - * Caches, Timers and Hazelcast objects associated with this service. - * - * @see Timer + * Note that the provided name must be unique across all Caches, Timers and distributed + * Hazelcast objects associated with this service. */ @CompileDynamic @NamedVariant @@ -194,12 +193,10 @@ abstract class BaseService implements LogSupport, IdentitySupport, DisposableBea } /** - * Create a new Cache bound to this service. - * - * Note that the provided name should be unique with respect to all - * Caches, Timers and Hazelcast objects associated with this service. + * Create a new {@link Cache} bound to this service. * - * @see Cache + * Note that the provided name must be unique across all Caches, Timers and distributed + * Hazelcast objects associated with this service. */ Cache createCache(Map mp) { // Cannot use @NamedVariant, as incompatible with generics. We'll still get run-time checks. @@ -207,12 +204,10 @@ abstract class BaseService implements LogSupport, IdentitySupport, DisposableBea } /** - * Create a new CachedValue bound to this service. - * - * Note that the provided name should be unique with respect to all - * Caches, Timers and Hazelcast objects associated with this service. + * Create a new {@link CachedValue} bound to this service. * - * @see CachedValue + * Note that the provided name must be unique across all Caches, Timers and distributed + * Hazelcast objects associated with this service. */ CachedValue createCachedValue(Map mp) { // Cannot use @NamedVariant, as incompatible with generics. We'll still get run-time checks. @@ -221,14 +216,14 @@ abstract class BaseService implements LogSupport, IdentitySupport, DisposableBea /** - * Managed Subscription to a Grails Event. + * Create a managed subscription to events on the instance-local Grails event bus. * - * NOTE: Use this method to subscribe to local Grails events on the given server - * instance only. To subscribe to cluster-wide topics, use 'subscribeToTopic' instead. + * NOTE: this method subscribes to Grails events on the current server instance only. + * To subscribe to cluster-wide topics, use {@link #subscribeToTopic} instead. * * This method will catch (and log) any exceptions thrown by its handler closure. - * This is important because the core grails EventBus.subscribe() will silently swallow - * exceptions, and stop processing subsequent handlers. + * This is important because the core Grails `EventBus.subscribe()` will silently swallow + * exceptions and stop processing subsequent handlers. * * This subscription also avoids firing handlers on destroyed services. This is important in a * hot-reloading scenario where multiple instances of singleton services may be created. @@ -248,10 +243,11 @@ abstract class BaseService implements LogSupport, IdentitySupport, DisposableBea /** * - * Managed Subscription to a cluster topic. + * Create a managed subscription to a cluster topic. * - * NOTE: Use this method to subscribe to cluster-wide topics. To subscribe to local - * Grails events on this instance only, use subscribe instead. + * NOTE: this subscribes to cluster-wide topics. To subscribe to local Grails events on this + * instance only, use {@link #subscribe} instead. That said, this is most likely the method you + * want, as most pub/sub use cases should take multi-instance operation into account. * * This method will catch (and log) any exceptions thrown by its handler closure. * diff --git a/src/main/groovy/io/xh/hoist/cache/BaseCache.groovy b/src/main/groovy/io/xh/hoist/cache/BaseCache.groovy index 7d6dccc4..94dcfc93 100644 --- a/src/main/groovy/io/xh/hoist/cache/BaseCache.groovy +++ b/src/main/groovy/io/xh/hoist/cache/BaseCache.groovy @@ -78,13 +78,12 @@ abstract class BaseCache { /** Clear all values. */ abstract void clear() - /** Is Cache to be stored on cluster? */ + /** True if this Cache should be stored across the cluster (backed by a ReplicatedMap). */ boolean getUseCluster() { return replicate && ClusterService.multiInstanceEnabled } - - /** Information about this object for admin purposes */ + /** Information about this object, accessible via the Hoist Admin Console. */ abstract Map getAdminStats() //------------------------ diff --git a/src/main/groovy/io/xh/hoist/cache/Cache.groovy b/src/main/groovy/io/xh/hoist/cache/Cache.groovy index 68798293..641b2777 100644 --- a/src/main/groovy/io/xh/hoist/cache/Cache.groovy +++ b/src/main/groovy/io/xh/hoist/cache/Cache.groovy @@ -28,11 +28,7 @@ class Cache extends BaseCache { private final Map> _map private final Timer cullTimer - /** - * @internal - * - * Not typically created directly. Use BaseService.createCache() instead. - */ + /** @internal - do not construct directly - use {@link BaseService#createCache}. */ @NamedVariant Cache( @NamedParam(required = true) String name, diff --git a/src/main/groovy/io/xh/hoist/cache/CachedValue.groovy b/src/main/groovy/io/xh/hoist/cache/CachedValue.groovy index 4e9d3dce..ee8999af 100644 --- a/src/main/groovy/io/xh/hoist/cache/CachedValue.groovy +++ b/src/main/groovy/io/xh/hoist/cache/CachedValue.groovy @@ -19,11 +19,7 @@ class CachedValue extends BaseCache { private final Map> _map - /** - * @internal - * - * Not typically created directly. Use BaseService.createCachedValue() instead. - */ + /** @internal - do not construct directly - use {@link BaseService#createCachedValue}. */ @NamedVariant CachedValue( @NamedParam(required = true) String name, diff --git a/src/main/groovy/io/xh/hoist/util/Timer.groovy b/src/main/groovy/io/xh/hoist/util/Timer.groovy index 401873f5..260f4a4f 100644 --- a/src/main/groovy/io/xh/hoist/util/Timer.groovy +++ b/src/main/groovy/io/xh/hoist/util/Timer.groovy @@ -26,10 +26,19 @@ import static io.xh.hoist.util.Utils.configService import static io.xh.hoist.util.Utils.getExceptionHandler /** - * Core Hoist Timer object. + * Hoist's implementation of an interval-based Timer, for running tasks on a repeated interval. + * Supports a dynamic / configurable run interval, startup delay, and timeout. Used by services + * that need to schedule work to maintain internal state, eg regularly refreshing a cache from an + * external data source. * - * This object is typically used by services that need to schedule work to maintain - * internal state. + * This class ensures that only one instance of the task is running at a time. To schedule an ad hoc + * run, call `forceRun()` to run again as soon as any in-progress run completes, or ASAP on the next + * tick of the Timer's internal (and fast) interval-evaluation heartbeat. + * + * Timers can be configured to run only on the primary instance in a clustered environment, to + * ensure that tasks with external side effects are not run on every instance unless so desired. + * A common pattern would be to have the primary instance run a Timer-based job to load data into + * a cache, with the cache then replicated across the cluster. */ class Timer { @@ -181,28 +190,26 @@ class Timer { } /** - * Force a new execution as soon as possible. + * Force a new execution as soon as possible, on the next scheduled internal heartbeat, or as + * soon as any already in-progress execution completes. * - * This will occur on the next scheduled heartbeat, or as soon as any in-progress executions complete. - * Any subsequent calls to this method before this additional execution has completed will be ignored. + * Note that any additional calls to this method before an already-requested force run has + * completed will be ignored. */ void forceRun() { forceRun = true } /** - * Cancel this timer. - * - * This will prevent any additional executions of this timer. In-progress executions will be unaffected. + * Cancel this timer, permanently preventing any additional executions. + * In-progress executions will be unaffected. */ void cancel() { coreTimer?.cancel() configTimer?.cancel() } - /** - * Information about this timer for admin purposes. - */ + /** Information about this timer, accessible via the Hoist Admin Console. */ Map getAdminStats() { [ name: name, From 21816cbc6da95d0d9421c0603090f49c38d0d206 Mon Sep 17 00:00:00 2001 From: Anselm McClain Date: Mon, 16 Sep 2024 14:14:57 -0700 Subject: [PATCH 11/14] Whitespace / fmt tweaks --- .../groovy/io/xh/hoist/cache/Cache.groovy | 31 +++++++++---------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/src/main/groovy/io/xh/hoist/cache/Cache.groovy b/src/main/groovy/io/xh/hoist/cache/Cache.groovy index 641b2777..5de8d6da 100644 --- a/src/main/groovy/io/xh/hoist/cache/Cache.groovy +++ b/src/main/groovy/io/xh/hoist/cache/Cache.groovy @@ -23,12 +23,12 @@ import static java.lang.System.currentTimeMillis * A key-value Cache, with support for optional entry TTL and replication across a cluster. */ @CompileStatic -class Cache extends BaseCache { +class Cache extends BaseCache { private final Map> _map private final Timer cullTimer - /** @internal - do not construct directly - use {@link BaseService#createCache}. */ + /** @internal - do not construct directly - use {@link BaseService#createCache}. */ @NamedVariant Cache( @NamedParam(required = true) String name, @@ -54,12 +54,12 @@ class Cache extends BaseCache { ) } - /** @returns the cached value at key. */ + /** @returns the cached value at key. */ V get(K key) { return getEntry(key)?.value } - /** @returns the cached Entry at key. */ + /** @returns the cached Entry at key. */ Entry getEntry(K key) { def ret = _map[key] if (ret && shouldExpire(ret)) { @@ -86,7 +86,7 @@ class Cache extends BaseCache { if (!useCluster) fireOnChange(this, oldEntry?.value, obj) } - /** @returns cached value for key, or lazily creates if needed. */ + /** @returns cached value for key, or lazily creates if needed. */ V getOrCreate(K key, Closure c) { V ret = get(key) if (ret == null) { @@ -96,13 +96,13 @@ class Cache extends BaseCache { return ret } - /** @returns a Map representation of currently cached data. */ + /** @returns a Map representation of currently cached data. */ Map getMap() { cullEntries() - return (Map) _map.collectEntries {k, v -> [k, v.value]} + return (Map) _map.collectEntries { k, v -> [k, v.value] } } - /** @returns the timestamp of the cached Entry at key. */ + /** @returns the timestamp of the cached Entry at key. */ Long getTimestamp(K key) { return getEntryTimestamp(_map[key]) } @@ -119,7 +119,7 @@ class Cache extends BaseCache { void clear() { // Remove key-wise to ensure that we get the proper removal message for each value and // work around exceptions with clear on replicated map. - _map.each { k, v -> remove(k)} + _map.each { k, v -> remove(k) } } void addChangeHandler(Closure handler) { @@ -132,10 +132,10 @@ class Cache extends BaseCache { /** * Wait for the cache entry to be populated. - * @param key, entry to check - * @param timeout, time in ms to wait. -1 to wait indefinitely (not recommended). - * @param interval, time in ms to wait between tests. - * @param timeoutMessage, custom message associated with any timeout. + * @param key - entry to check + * @param timeout - time in ms to wait. -1 to wait indefinitely (not recommended). + * @param interval - time in ms to wait between tests. + * @param timeoutMessage - custom message associated with any timeout. */ @NamedVariant void ensureAvailable( @@ -156,14 +156,13 @@ class Cache extends BaseCache { } } - Map getAdminStats() { [ name : name, - type : 'Cache' + (replicate ? '(replicated)' : ''), + type : 'Cache' + (replicate ? ' (replicated)' : ''), count : size(), latestTimestamp: _map.max { it.value.dateEntered }?.value?.dateEntered, - lastCullTime : cullTimer.lastRunCompleted + lastCullTime : cullTimer.lastRunCompleted ] } From b56a175319bce677ec261154db18a6667960f1b7 Mon Sep 17 00:00:00 2001 From: lbwexler Date: Mon, 16 Sep 2024 17:37:11 -0400 Subject: [PATCH 12/14] Changes from ATM review --- .../clienterror/ClientErrorService.groovy | 2 +- .../groovy/io/xh/hoist/BaseService.groovy | 32 ++++++++++++------- .../groovy/io/xh/hoist/cache/Cache.groovy | 2 +- 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/grails-app/services/io/xh/hoist/clienterror/ClientErrorService.groovy b/grails-app/services/io/xh/hoist/clienterror/ClientErrorService.groovy index 7c6a66ea..e5bb9575 100644 --- a/grails-app/services/io/xh/hoist/clienterror/ClientErrorService.groovy +++ b/grails-app/services/io/xh/hoist/clienterror/ClientErrorService.groovy @@ -41,7 +41,7 @@ class ClientErrorService extends BaseService { }] ] - private IMap errors = getIMap('clientErrors') + private IMap errors = createIMap('clientErrors') private int getMaxErrors() {configService.getMap('xhClientErrorConfig').maxErrors as int} private int getAlertInterval() {configService.getMap('xhClientErrorConfig').intervalMins * MINUTES} diff --git a/src/main/groovy/io/xh/hoist/BaseService.groovy b/src/main/groovy/io/xh/hoist/BaseService.groovy index 188994e6..4f104442 100644 --- a/src/main/groovy/io/xh/hoist/BaseService.groovy +++ b/src/main/groovy/io/xh/hoist/BaseService.groovy @@ -127,7 +127,7 @@ abstract class BaseService implements LogSupport, IdentitySupport, DisposableBea * @param name - must be unique across all Caches, Timers and distributed Hazelcast objects * associated with this service. */ - IMap getIMap(String name) { + IMap createIMap(String name) { addResource(name, ClusterService.hzInstance.getMap(hzName(name))) } @@ -137,7 +137,7 @@ abstract class BaseService implements LogSupport, IdentitySupport, DisposableBea * @param name - must be unique across all Caches, Timers and distributed Hazelcast objects * associated with this service. */ - ISet getISet(String name) { + ISet createISet(String name) { addResource(name, ClusterService.hzInstance.getSet(hzName(name))) } @@ -147,7 +147,7 @@ abstract class BaseService implements LogSupport, IdentitySupport, DisposableBea * @param name - must be unique across all Caches, Timers and distributed Hazelcast objects * associated with this service. */ - ReplicatedMap getReplicatedMap(String name) { + ReplicatedMap createReplicatedMap(String name) { addResource(name, ClusterService.hzInstance.getReplicatedMap(hzName(name))) } @@ -186,9 +186,19 @@ abstract class BaseService implements LogSupport, IdentitySupport, DisposableBea } } - addResource( - name, - new Timer(name, this, runFn, primaryOnly, runImmediatelyAndBlock, interval, timeout, delay, intervalUnits, timeoutUnits) + addResource(name, + new Timer( + name, + this, + runFn, + primaryOnly, + runImmediatelyAndBlock, + interval, + timeout, + delay, + intervalUnits, + timeoutUnits + ) ) } @@ -200,7 +210,7 @@ abstract class BaseService implements LogSupport, IdentitySupport, DisposableBea */ Cache createCache(Map mp) { // Cannot use @NamedVariant, as incompatible with generics. We'll still get run-time checks. - addResource(mp.name, new Cache([*:mp, svc: this])) + addResource(mp.name as String, new Cache([*:mp, svc: this])) } /** @@ -211,7 +221,7 @@ abstract class BaseService implements LogSupport, IdentitySupport, DisposableBea */ CachedValue createCachedValue(Map mp) { // Cannot use @NamedVariant, as incompatible with generics. We'll still get run-time checks. - addResource(mp.name, new CachedValue([*:mp, svc: this])) + addResource(mp.name as String, new CachedValue([*:mp, svc: this])) } @@ -386,8 +396,8 @@ abstract class BaseService implements LogSupport, IdentitySupport, DisposableBea * @internal - for use by Cache. */ Map getMapForCache(Cache cache) { - // register under xh name to avoid collisions, allow filtering out in admin - cache.useCluster ? getReplicatedMap("xh_${cache.name}") : new ConcurrentHashMap() + // register with xh prefix to avoid collisions, allow filtering out in admin + cache.useCluster ? createReplicatedMap("xh_${cache.name}") : new ConcurrentHashMap() } /** @@ -396,7 +406,7 @@ abstract class BaseService implements LogSupport, IdentitySupport, DisposableBea Map getMapForCachedValue(CachedValue cachedValue) { // register with xh prefix to avoid collisions, allow filtering out in admin if (cachedValue.useCluster) { - if (_replicatedValues == null) _replicatedValues = getReplicatedMap('xh_cachedValues') + if (_replicatedValues == null) _replicatedValues = createReplicatedMap('xh_cachedValues') return _replicatedValues } else { if (_localValues == null) _localValues = new ConcurrentHashMap() diff --git a/src/main/groovy/io/xh/hoist/cache/Cache.groovy b/src/main/groovy/io/xh/hoist/cache/Cache.groovy index 5de8d6da..4e1e36d0 100644 --- a/src/main/groovy/io/xh/hoist/cache/Cache.groovy +++ b/src/main/groovy/io/xh/hoist/cache/Cache.groovy @@ -50,7 +50,7 @@ class Cache extends BaseCache { runFn: this.&cullEntries, interval: 15 * MINUTES, delay: true, - primaryOnly: replicate + primaryOnly: useCluster ) } From 5c34c296dbdfd34d5408c62bef7bd1f24fdcdf77 Mon Sep 17 00:00:00 2001 From: lbwexler Date: Mon, 16 Sep 2024 17:45:18 -0400 Subject: [PATCH 13/14] Changelog message update --- CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 78b31817..900bab94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,8 +8,13 @@ previously optional in many cases, but is now required in order to support new c logging, and admin tools. The new `BaseService.resources` property now will give access to all resources by name, if needed and replaces `BaseService.timers`. +* `BaseService` methods `getIMap()`, `getReplicatedMap()` and `getISet()` have been changed to + `createIMap()`, `createReplicatedMap()` and `createISet()`, respectively. This change provides + a consistent interface for all resources on BaseService and is not expected to impact most + applications. + ### 🎁 New Features -* `Cache` and `CachedValue` may now be created using a factory on `BaseService`. This streamlined +* `Cache` and `CachedValue` should now be created using a factory on `BaseService`. This streamlined interface reduces boilerplate, and provides a consistent interface with `Timer`. ### ⚙️ Technical From e664370cc6eea8450c1b862d69cde9a565b239dc Mon Sep 17 00:00:00 2001 From: lbwexler Date: Mon, 16 Sep 2024 17:48:58 -0400 Subject: [PATCH 14/14] Tweak from ATM review --- src/main/groovy/io/xh/hoist/util/Timer.groovy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/groovy/io/xh/hoist/util/Timer.groovy b/src/main/groovy/io/xh/hoist/util/Timer.groovy index 260f4a4f..eda05508 100644 --- a/src/main/groovy/io/xh/hoist/util/Timer.groovy +++ b/src/main/groovy/io/xh/hoist/util/Timer.groovy @@ -321,14 +321,14 @@ class Timer { // frequently enough to pickup forceRun reasonably fast. Tighten down for the rare fast timer. //------------------------------------------------------------------------------------------- private void onCoreTimer() { - if (!isRunning && (forceRun || isIntervalElapsed())) { + if (!isRunning && (forceRun || intervalHasElapsed())) { boolean wasForced = forceRun doRun() if (wasForced) forceRun = false } } - private boolean isIntervalElapsed() { + private boolean intervalHasElapsed() { if (intervalMs <= 0) return false def lastRun = _lastCompletedOnCluster ? _lastCompletedOnCluster.get() : _lastRunCompleted return intervalElapsed(intervalMs, lastRun)