From f5c91e7d927d85206a68cef64e8921680953d1b3 Mon Sep 17 00:00:00 2001 From: lbwexler Date: Fri, 3 Jan 2025 18:10:11 -0500 Subject: [PATCH 1/2] Checkpoint --- ...y => ClusterObjectsAdminController.groovy} | 12 +- .../hoist/admin/ClusterObjectsService.groovy | 81 ++++++ .../ConnectionPoolMonitoringService.groovy | 3 +- .../DistributedObjectAdminService.groovy | 264 ------------------ .../hoist/admin/ServiceManagerService.groovy | 15 +- src/main/groovy/io/xh/hoist/AdminStats.groovy | 17 ++ .../groovy/io/xh/hoist/BaseService.groovy | 9 +- .../xh/hoist/admin/ClusterObjectInfo.groovy | 46 +++ .../ClusterObjectsReport.groovy} | 27 +- .../io/xh/hoist/admin/HzAdminStats.groovy | 157 +++++++++++ .../groovy/io/xh/hoist/cache/Cache.groovy | 5 +- .../xh/hoist/cachedvalue/CachedValue.groovy | 6 +- .../cluster/DistributedObjectInfo.groovy | 45 --- 13 files changed, 338 insertions(+), 349 deletions(-) rename grails-app/controllers/io/xh/hoist/admin/cluster/{DistributedObjectAdminController.groovy => ClusterObjectsAdminController.groovy} (62%) create mode 100644 grails-app/services/io/xh/hoist/admin/ClusterObjectsService.groovy delete mode 100644 grails-app/services/io/xh/hoist/admin/DistributedObjectAdminService.groovy create mode 100644 src/main/groovy/io/xh/hoist/AdminStats.groovy create mode 100644 src/main/groovy/io/xh/hoist/admin/ClusterObjectInfo.groovy rename src/main/groovy/io/xh/hoist/{cluster/DistributedObjectsReport.groovy => admin/ClusterObjectsReport.groovy} (67%) create mode 100644 src/main/groovy/io/xh/hoist/admin/HzAdminStats.groovy delete mode 100644 src/main/groovy/io/xh/hoist/cluster/DistributedObjectInfo.groovy diff --git a/grails-app/controllers/io/xh/hoist/admin/cluster/DistributedObjectAdminController.groovy b/grails-app/controllers/io/xh/hoist/admin/cluster/ClusterObjectsAdminController.groovy similarity index 62% rename from grails-app/controllers/io/xh/hoist/admin/cluster/DistributedObjectAdminController.groovy rename to grails-app/controllers/io/xh/hoist/admin/cluster/ClusterObjectsAdminController.groovy index a0955050..9922ddb7 100644 --- a/grails-app/controllers/io/xh/hoist/admin/cluster/DistributedObjectAdminController.groovy +++ b/grails-app/controllers/io/xh/hoist/admin/cluster/ClusterObjectsAdminController.groovy @@ -10,23 +10,23 @@ import io.xh.hoist.BaseController import io.xh.hoist.security.Access @Access(['HOIST_ADMIN_READER']) -class DistributedObjectAdminController extends BaseController { - def distributedObjectAdminService +class ClusterObjectsAdminController extends BaseController { + def clusterObjectsService - def getDistributedObjectsReport() { - renderJSON(distributedObjectAdminService.getDistributedObjectsReport()) + def getClusterObjectsReport() { + renderJSON(clusterObjectsService.getClusterObjectsReport()) } @Access(['HOIST_ADMIN']) def clearHibernateCaches() { def req = parseRequestJSON() - distributedObjectAdminService.clearHibernateCaches(req.names) + clusterObjectsService.clearHibernateCaches(req.names) renderJSON([success: true]) } @Access(['HOIST_ADMIN']) def clearAllHibernateCaches() { - distributedObjectAdminService.clearHibernateCaches() + clusterObjectsService.clearHibernateCaches() renderJSON([success: true]) } } diff --git a/grails-app/services/io/xh/hoist/admin/ClusterObjectsService.groovy b/grails-app/services/io/xh/hoist/admin/ClusterObjectsService.groovy new file mode 100644 index 00000000..0fc4bf75 --- /dev/null +++ b/grails-app/services/io/xh/hoist/admin/ClusterObjectsService.groovy @@ -0,0 +1,81 @@ +/* + * This file belongs to Hoist, an application development toolkit + * developed by Extremely Heavy Industries (www.xh.io | info@xh.io) + * + * Copyright © 2025 Extremely Heavy Industries Inc. + */ +package io.xh.hoist.admin + +import com.hazelcast.cache.impl.CacheProxy +import com.hazelcast.executor.impl.ExecutorServiceProxy +import io.xh.hoist.AdminStats +import io.xh.hoist.BaseService +import io.xh.hoist.cluster.ClusterRequest + +import static io.xh.hoist.util.Utils.appContext +import static java.lang.System.currentTimeMillis + +class ClusterObjectsService extends BaseService { + def grailsApplication + + ClusterObjectsReport getClusterObjectsReport() { + def startTimestamp = currentTimeMillis(), + info = clusterService + .submitToAllInstances(new ListClusterObjects()) + .collectMany { it.value.value } + + return new ClusterObjectsReport( + info: info, + startTimestamp: startTimestamp, + endTimestamp: currentTimeMillis() + ) + } + + /** + * Clear all Hibernate caches, or a specific list of caches by name. + */ + void clearHibernateCaches(List names = null) { + def caches = clusterService.distributedObjects.findAll {it instanceof CacheProxy} + names ?= caches*.name + names.each { name -> + def obj = caches.find { it.name == name } + if (obj) { + obj.clear() + logInfo('Cleared ' + name) + } else { + logWarn('Cannot find cache', name) + } + } + } + + //-------------------- + // Implementation + //-------------------- + private List listClusterObjects() { + // Services and their AdminStat implementing resources + Map svcs = grailsApplication.mainContext.getBeansOfType(BaseService.class, false, false) + def hoistObjs = svcs.collectMany { _, svc -> + [ + new ClusterObjectInfo(svc, [name: svc.class.name, type: 'Service']), + *svc.resources + .findAll { k, v -> v instanceof AdminStats } + .collect { k, v -> new ClusterObjectInfo(v as AdminStats, [name: svc.hzName(k)]) } + ] + } + + // Hazelcast built-ins + def hzObjs = clusterService + .hzInstance + .distributedObjects + .findAll { !(it instanceof ExecutorServiceProxy) } + .collect { new ClusterObjectInfo(new HzAdminStats(it)) } + + return (hzObjs + hoistObjs) as List + } + + static class ListClusterObjects extends ClusterRequest> { + List doCall() { + appContext.clusterObjectsService.listClusterObjects() + } + } +} diff --git a/grails-app/services/io/xh/hoist/admin/ConnectionPoolMonitoringService.groovy b/grails-app/services/io/xh/hoist/admin/ConnectionPoolMonitoringService.groovy index e4969703..af232225 100644 --- a/grails-app/services/io/xh/hoist/admin/ConnectionPoolMonitoringService.groovy +++ b/grails-app/services/io/xh/hoist/admin/ConnectionPoolMonitoringService.groovy @@ -9,7 +9,6 @@ package io.xh.hoist.admin import io.xh.hoist.BaseService import io.xh.hoist.exception.DataNotAvailableException -import io.xh.hoist.util.DateTimeUtils import org.apache.tomcat.jdbc.pool.DataSource as PooledDataSource import org.apache.tomcat.jdbc.pool.PoolConfiguration import org.springframework.boot.jdbc.DataSourceUnwrapper @@ -37,7 +36,7 @@ class ConnectionPoolMonitoringService extends BaseService { createTimer( name: 'takeSnapshot', runFn: this.&takeSnapshot, - interval: {enabled ? config.snapshotInterval * DateTimeUtils.SECONDS: -1} + interval: {enabled ? config.snapshotInterval * SECONDS: -1} ) } diff --git a/grails-app/services/io/xh/hoist/admin/DistributedObjectAdminService.groovy b/grails-app/services/io/xh/hoist/admin/DistributedObjectAdminService.groovy deleted file mode 100644 index 78f2dcb8..00000000 --- a/grails-app/services/io/xh/hoist/admin/DistributedObjectAdminService.groovy +++ /dev/null @@ -1,264 +0,0 @@ -/* - * This file belongs to Hoist, an application development toolkit - * developed by Extremely Heavy Industries (www.xh.io | info@xh.io) - * - * Copyright © 2025 Extremely Heavy Industries Inc. - */ -package io.xh.hoist.admin - -import com.hazelcast.cache.impl.CacheProxy -import com.hazelcast.collection.ISet -import com.hazelcast.core.DistributedObject -import com.hazelcast.executor.impl.ExecutorServiceProxy -import com.hazelcast.map.IMap -import com.hazelcast.nearcache.NearCacheStats -import com.hazelcast.replicatedmap.ReplicatedMap -import com.hazelcast.ringbuffer.impl.RingbufferProxy -import com.hazelcast.topic.ITopic -import io.xh.hoist.BaseService -import io.xh.hoist.cluster.ClusterRequest -import io.xh.hoist.cluster.DistributedObjectInfo -import io.xh.hoist.cluster.DistributedObjectsReport - -import javax.cache.expiry.Duration -import javax.cache.expiry.ExpiryPolicy - -import static io.xh.hoist.util.Utils.appContext - -class DistributedObjectAdminService extends BaseService { - def grailsApplication - - DistributedObjectsReport getDistributedObjectsReport() { - def startTimestamp = System.currentTimeMillis(), - responsesByInstance = clusterService.submitToAllInstances(new ListDistributedObjects()) - return new DistributedObjectsReport( - info: responsesByInstance.collectMany {it.value.value}, - startTimestamp: startTimestamp, - endTimestamp: System.currentTimeMillis() - ) - } - - private List listDistributedObjects() { - // Services and their resources - Map svcs = grailsApplication.mainContext.getBeansOfType(BaseService.class, false, false) - def resourceObjs = svcs.collectMany { _, svc -> - [ - // Services themselves - getHoistInfo(svc, name: svc.class.getName(), type: 'Service'), - // Resources, excluding those that are also DistributedObject - *svc.resources.findAll { k, v -> !(v instanceof DistributedObject)}.collect { k, v -> - getHoistInfo(v, name: svc.hzName(k)) - } - ] - }, - // Distributed objects - hzObjs = clusterService - .hzInstance - .distributedObjects - .findAll { !(it instanceof ExecutorServiceProxy) } - .collect { getHzInfo(it) } - - return [*hzObjs, *resourceObjs].findAll{ it } as List - } - static class ListDistributedObjects extends ClusterRequest> { - List doCall() { - appContext.distributedObjectAdminService.listDistributedObjects() - } - } - - // Clear all Hibernate caches, or a specific list of caches by name. - void clearHibernateCaches(List names = null) { - def hibernateCaches = clusterService.distributedObjects.findAll { it instanceof CacheProxy } - if (!names) { - names = hibernateCaches.collect { it.getName() } - } - - names.each { name -> - def obj = hibernateCaches.find { it.getName() == name } - if (obj) { - obj.clear() - logInfo("Cleared " + name) - } else { - logWarn('Cannot clear object - unsupported type', name) - } - } - } - - // No named arg overloads - DistributedObjectInfo getInfo(def obj) { - getInfo([:], obj) - } - - DistributedObjectInfo getHoistInfo(def obj) { - getHoistInfo([:], obj) - } - - DistributedObjectInfo getHzInfo(DistributedObject obj) { - getHzInfo([:], obj) - } - - DistributedObjectInfo getInfo(Map overrides, def obj) { - obj instanceof DistributedObject ? getHzInfo(overrides, obj) : getHoistInfo(overrides, obj) - } - - DistributedObjectInfo getHoistInfo(Map overrides, def obj) { - def comparisonFields = null, - adminStats = null, - error = null - - try { - comparisonFields = obj.hasProperty('comparisonFields') ? obj.comparisonFields : null - adminStats = obj.hasProperty('adminStats') ? obj.adminStats : null - } catch (Exception e) { - def msg = 'Error extracting admin stats' - logError(msg, e) - error = "$msg | ${e.message}" - } - - return new DistributedObjectInfo( - comparisonFields: comparisonFields, - adminStats: adminStats, - error: error, - *: overrides - ) - } - - DistributedObjectInfo getHzInfo(Map overrides, DistributedObject obj) { - switch (obj) { - case ReplicatedMap: - def stats = obj.getReplicatedMapStats() - return new DistributedObjectInfo( - comparisonFields: ['size'], - adminStats: [ - name : obj.getName(), - type : 'ReplicatedMap', - size : obj.size(), - lastUpdateTime: stats.lastUpdateTime ?: null, - lastAccessTime: stats.lastAccessTime ?: null, - - hits : stats.hits, - gets : stats.getOperationCount, - puts : stats.putOperationCount - ], - *: overrides - ) - case IMap: - def stats = obj.getLocalMapStats() - return new DistributedObjectInfo( - comparisonFields: ['size'], - adminStats: [ - name : obj.getName(), - type : 'IMap', - size : obj.size(), - lastUpdateTime : stats.lastUpdateTime ?: null, - lastAccessTime : stats.lastAccessTime ?: null, - - ownedEntryCount: stats.ownedEntryCount, - hits : stats.hits, - gets : stats.getOperationCount, - sets : stats.setOperationCount, - puts : stats.putOperationCount, - nearCache : getNearCacheStats(stats.nearCacheStats), - ], - *: overrides - ) - case ISet: - def stats = obj.getLocalSetStats() - return new DistributedObjectInfo( - comparisonFields: ['size'], - adminStats: [ - name : obj.getName(), - type : 'ISet', - size : obj.size(), - lastUpdateTime: stats.lastUpdateTime ?: null, - lastAccessTime: stats.lastAccessTime ?: null, - ], - *: overrides - ) - case ITopic: - def stats = obj.getLocalTopicStats() - return new DistributedObjectInfo( - adminStats: [ - name : obj.getName(), - type : 'Topic', - publishOperationCount: stats.publishOperationCount, - receiveOperationCount: stats.receiveOperationCount - ], - *: overrides - ) - case RingbufferProxy: - return new DistributedObjectInfo( - adminStats: [ - name : obj.getName(), - type : 'Ringbuffer', - size : obj.size(), - capacity: obj.capacity() - ], - *: overrides - ) - case CacheProxy: - def evictionConfig = obj.cacheConfig.evictionConfig, - expiryPolicy = obj.cacheConfig.expiryPolicyFactory.create(), - stats = obj.localCacheStatistics - return new DistributedObjectInfo( - comparisonFields: ['size'], - adminStats: [ - name : obj.getName(), - type : 'Hibernate Cache', - size : obj.size(), - lastUpdateTime : stats.lastUpdateTime ?: null, - lastAccessTime : stats.lastAccessTime ?: null, - - ownedEntryCount : stats.ownedEntryCount, - cacheHits : stats.cacheHits, - cacheHitPercentage: stats.cacheHitPercentage?.round(0), - config : [ - size : evictionConfig.size, - maxSizePolicy : evictionConfig.maxSizePolicy, - evictionPolicy: evictionConfig.evictionPolicy, - expiryPolicy : formatExpiryPolicy(expiryPolicy) - ] - ], - *: overrides - ) - default: - return new DistributedObjectInfo( - adminStats: [ - name: obj.getName(), - type: obj.class.toString() - ], - *: overrides - ) - } - } - - //-------------------- - // Implementation - //-------------------- - private Map getNearCacheStats(NearCacheStats stats) { - if (!stats) return null - [ - ownedEntryCount : stats.ownedEntryCount, - lastPersistenceTime: stats.lastPersistenceTime, - hits : stats.hits, - misses : stats.misses, - ratio : stats.ratio.round(2) - ] - } - - private Map formatExpiryPolicy(ExpiryPolicy policy) { - def ret = [:] - if (policy.expiryForCreation) ret.creation = formatDuration(policy.expiryForCreation) - if (policy.expiryForAccess) ret.access = formatDuration(policy.expiryForAccess) - if (policy.expiryForUpdate) ret.update = formatDuration(policy.expiryForUpdate) - return ret - } - - - private String formatDuration(Duration duration) { - if (duration.isZero()) return 0 - if (duration.isEternal()) return 'eternal' - return duration.timeUnit.toSeconds(duration.durationAmount) + 's' - } - -} diff --git a/grails-app/services/io/xh/hoist/admin/ServiceManagerService.groovy b/grails-app/services/io/xh/hoist/admin/ServiceManagerService.groovy index 013d23d7..98a5ba39 100644 --- a/grails-app/services/io/xh/hoist/admin/ServiceManagerService.groovy +++ b/grails-app/services/io/xh/hoist/admin/ServiceManagerService.groovy @@ -8,12 +8,12 @@ package io.xh.hoist.admin import com.hazelcast.core.DistributedObject +import io.xh.hoist.AdminStats import io.xh.hoist.BaseService class ServiceManagerService extends BaseService { - def grailsApplication, - distributedObjectAdminService + def grailsApplication Collection listServices() { getServicesInternal().collect { name, svc -> @@ -47,16 +47,19 @@ class ServiceManagerService extends BaseService { // Implementation //---------------------- private List getResourceStats(BaseService svc) { + def ret = [] svc.resources .findAll { !it.key.startsWith('xh_') } // skip hoist implementation objects - .collect { k, v -> - Map stats = distributedObjectAdminService.getInfo(v)?.adminStats + .each { k, v -> + AdminStats stats = null + if (v instanceof AdminStats) stats = v + if (v instanceof DistributedObject) stats = new HzAdminStats(v) // rely on the name (key) service knows, i.e avoid HZ prefix - return [*: stats, name: k] + if (stats) ret << [*: stats.adminStats, name: k] } + return ret } - private Map getServicesInternal() { return grailsApplication.mainContext.getBeansOfType(BaseService.class, false, false) } diff --git a/src/main/groovy/io/xh/hoist/AdminStats.groovy b/src/main/groovy/io/xh/hoist/AdminStats.groovy new file mode 100644 index 00000000..5eafb2d7 --- /dev/null +++ b/src/main/groovy/io/xh/hoist/AdminStats.groovy @@ -0,0 +1,17 @@ +package io.xh.hoist + +interface AdminStats { + + /** + * Stats to report to the Hoist Admin client. + * @returns a JSON serializable map. + */ + Map getAdminStats() + + + /** + * Keys for stats in getAdminStats() that can be compared for equality across instances. + */ + List getComparableAdminStats() + +} \ No newline at end of file diff --git a/src/main/groovy/io/xh/hoist/BaseService.groovy b/src/main/groovy/io/xh/hoist/BaseService.groovy index 984d0f2b..1f2dc336 100644 --- a/src/main/groovy/io/xh/hoist/BaseService.groovy +++ b/src/main/groovy/io/xh/hoist/BaseService.groovy @@ -53,7 +53,7 @@ import static io.xh.hoist.cluster.ClusterService.hzInstance * 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 { +abstract class BaseService implements LogSupport, IdentitySupport, DisposableBean, AdminStats { IdentityService identityService ClusterService clusterService @@ -327,11 +327,8 @@ abstract class BaseService implements LogSupport, IdentitySupport, DisposableBea */ Map getAdminStats(){[:]} - /** - * A list of keys of the getAdminStats() map above that should be actively compared between - * instances by the Hoist admin client. - */ - List getComparisonFields() {[]} + + List getComparableAdminStats() {[]} /** * Return a map of specified config values, appropriate for including in diff --git a/src/main/groovy/io/xh/hoist/admin/ClusterObjectInfo.groovy b/src/main/groovy/io/xh/hoist/admin/ClusterObjectInfo.groovy new file mode 100644 index 00000000..2280cb72 --- /dev/null +++ b/src/main/groovy/io/xh/hoist/admin/ClusterObjectInfo.groovy @@ -0,0 +1,46 @@ +package io.xh.hoist.admin + +import io.xh.hoist.AdminStats +import io.xh.hoist.json.JSONFormat + +import static io.xh.hoist.util.Utils.getClusterService +import static java.util.Collections.emptyList +import static java.util.Collections.emptyMap + +class ClusterObjectInfo implements JSONFormat { + String name // Absolute name. Make sure to use `svc.hzName(name)` on relative-named objects + String type + Map adminStats + List comparableAdminStats + String instanceName + String error + + ClusterObjectInfo(AdminStats target, Map meta = [:] ) { + try { + adminStats = target.adminStats + comparableAdminStats = target.comparableAdminStats + } catch (Exception e) { + adminStats = emptyMap() + comparableAdminStats = emptyList() + error = "Error computing admin stats | ${e.message}" + } + name = meta.name ?: adminStats.name + type = meta.type ?: adminStats.type + instanceName = clusterService.localName + } + + boolean isMatching(ClusterObjectInfo other) { + comparableAdminStats == other.comparableAdminStats && + comparableAdminStats.every { f -> adminStats[f] == other.adminStats[f]} + } + + Map formatForJSON() { + return [ + name: name, + type: type, + instanceName: instanceName, + adminStats: adminStats, + comparableAdminStats: comparableAdminStats + ] + } +} \ No newline at end of file diff --git a/src/main/groovy/io/xh/hoist/cluster/DistributedObjectsReport.groovy b/src/main/groovy/io/xh/hoist/admin/ClusterObjectsReport.groovy similarity index 67% rename from src/main/groovy/io/xh/hoist/cluster/DistributedObjectsReport.groovy rename to src/main/groovy/io/xh/hoist/admin/ClusterObjectsReport.groovy index 58365fe0..9853f4c7 100644 --- a/src/main/groovy/io/xh/hoist/cluster/DistributedObjectsReport.groovy +++ b/src/main/groovy/io/xh/hoist/admin/ClusterObjectsReport.groovy @@ -1,24 +1,21 @@ -package io.xh.hoist.cluster +package io.xh.hoist.admin + -import groovy.transform.MapConstructor import io.xh.hoist.json.JSONFormat -class DistributedObjectsReport implements JSONFormat { +class ClusterObjectsReport implements JSONFormat { // List of all of the distributed object data from all of the instances in the cluster. - List info - // Roughly when this report was generated, how long it took. + List info + Map>> breaks Long startTimestamp Long endTimestamp - // Map of mismatches - Map>> breaks - DistributedObjectsReport(Map args) { - info = args.info as List + ClusterObjectsReport(Map args) { + info = args.info as List + breaks = createBreaks() startTimestamp = args.startTimestamp as Long endTimestamp = args.endTimestamp as Long - - breaks = createBreaks() } Map formatForJSON() { @@ -30,14 +27,14 @@ class DistributedObjectsReport implements JSONFormat { ] } + //------------------ + // Implementation + //------------------ private Map>> createBreaks() { Map>> breaks = [:].withDefault { [] } info.groupBy { it.name }.each { name, infoObjs -> [infoObjs, infoObjs].eachCombination { a, b -> - // Skip comparing objects to themselves. - if (a === b) return - - if (!a.isMatching(b)) { + if (a !== b && !a.isMatching(b)) { breaks[name].push([a.instanceName, b.instanceName]) } } diff --git a/src/main/groovy/io/xh/hoist/admin/HzAdminStats.groovy b/src/main/groovy/io/xh/hoist/admin/HzAdminStats.groovy new file mode 100644 index 00000000..659cb8ca --- /dev/null +++ b/src/main/groovy/io/xh/hoist/admin/HzAdminStats.groovy @@ -0,0 +1,157 @@ +package io.xh.hoist.admin + +import com.hazelcast.cache.impl.CacheProxy +import com.hazelcast.collection.ISet +import com.hazelcast.core.DistributedObject +import com.hazelcast.map.IMap +import com.hazelcast.nearcache.NearCacheStats +import com.hazelcast.replicatedmap.ReplicatedMap +import com.hazelcast.ringbuffer.impl.RingbufferProxy +import com.hazelcast.topic.ITopic +import io.xh.hoist.AdminStats + +import javax.cache.expiry.Duration +import javax.cache.expiry.ExpiryPolicy + +import static java.util.Collections.emptyList +import static java.util.Collections.emptyMap + +/** + * Admin stats for Hazelcast built-in objects. + */ +class HzAdminStats implements AdminStats { + + private Map _stats = emptyMap() + private List _comparables = emptyList() + + Map getAdminStats() { + _stats + } + + List getComparableAdminStats() { + _comparables + } + + HzAdminStats(DistributedObject obj) { + switch (obj) { + case ReplicatedMap: + def stats = obj.getReplicatedMapStats() + _comparables = ['size'] + _stats = [ + name : obj.getName(), + type : 'ReplicatedMap', + size : obj.size(), + lastUpdateTime: stats.lastUpdateTime ?: null, + lastAccessTime: stats.lastAccessTime ?: null, + + hits : stats.hits, + gets : stats.getOperationCount, + puts : stats.putOperationCount + ] + break + case IMap: + def stats = obj.getLocalMapStats() + _comparables = ['size'] + _stats = [ + name : obj.getName(), + type : 'IMap', + size : obj.size(), + lastUpdateTime : stats.lastUpdateTime ?: null, + lastAccessTime : stats.lastAccessTime ?: null, + + ownedEntryCount: stats.ownedEntryCount, + hits : stats.hits, + gets : stats.getOperationCount, + sets : stats.setOperationCount, + puts : stats.putOperationCount, + nearCache : getNearCacheStats(stats.nearCacheStats), + ] + break + case ISet: + def stats = obj.getLocalSetStats() + _comparables = ['size'] + _stats = [ + name : obj.getName(), + type : 'ISet', + size : obj.size(), + lastUpdateTime: stats.lastUpdateTime ?: null, + lastAccessTime: stats.lastAccessTime ?: null, + ] + break + case ITopic: + def stats = obj.getLocalTopicStats() + _stats = [ + name : obj.getName(), + type : 'Topic', + publishOperationCount: stats.publishOperationCount, + receiveOperationCount: stats.receiveOperationCount + ] + break + case RingbufferProxy: + _stats = [ + name : obj.getName(), + type : 'Ringbuffer', + size : obj.size(), + capacity: obj.capacity() + ] + break + case CacheProxy: + def evictionConfig = obj.cacheConfig.evictionConfig, + expiryPolicy = obj.cacheConfig.expiryPolicyFactory.create(), + stats = obj.localCacheStatistics + _comparables = ['size'] + _stats = [ + name : obj.getName(), + type : 'Hibernate Cache', + size : obj.size(), + lastUpdateTime : stats.lastUpdateTime ?: null, + lastAccessTime : stats.lastAccessTime ?: null, + + ownedEntryCount : stats.ownedEntryCount, + cacheHits : stats.cacheHits, + cacheHitPercentage: stats.cacheHitPercentage?.round(0), + config : [ + size : evictionConfig.size, + maxSizePolicy : evictionConfig.maxSizePolicy, + evictionPolicy: evictionConfig.evictionPolicy, + expiryPolicy : formatExpiryPolicy(expiryPolicy) + ] + ] + break + default: + _stats = [ + name: obj.getName(), + type: obj.class.toString() + ] + } + } + + + //-------------------- + // Implementation + //-------------------- + private Map getNearCacheStats(NearCacheStats stats) { + if (!stats) return null + [ + ownedEntryCount : stats.ownedEntryCount, + lastPersistenceTime: stats.lastPersistenceTime, + hits : stats.hits, + misses : stats.misses, + ratio : stats.ratio.round(2) + ] + } + + private Map formatExpiryPolicy(ExpiryPolicy policy) { + def ret = [:] + if (policy.expiryForCreation) ret.creation = formatDuration(policy.expiryForCreation) + if (policy.expiryForAccess) ret.access = formatDuration(policy.expiryForAccess) + if (policy.expiryForUpdate) ret.update = formatDuration(policy.expiryForUpdate) + return ret + } + + private String formatDuration(Duration duration) { + if (duration.isZero()) return 0 + if (duration.isEternal()) return 'eternal' + return duration.timeUnit.toSeconds(duration.durationAmount) + 's' + } +} diff --git a/src/main/groovy/io/xh/hoist/cache/Cache.groovy b/src/main/groovy/io/xh/hoist/cache/Cache.groovy index 185aac9e..39242f1f 100644 --- a/src/main/groovy/io/xh/hoist/cache/Cache.groovy +++ b/src/main/groovy/io/xh/hoist/cache/Cache.groovy @@ -10,6 +10,7 @@ import com.hazelcast.replicatedmap.ReplicatedMap import groovy.transform.CompileStatic import groovy.transform.NamedParam import groovy.transform.NamedVariant +import io.xh.hoist.AdminStats import io.xh.hoist.BaseService import io.xh.hoist.cluster.ClusterService import io.xh.hoist.log.LogSupport @@ -31,7 +32,7 @@ import static java.lang.System.currentTimeMillis * A key-value Cache, with support for optional entry TTL and replication across a cluster. */ @CompileStatic -class Cache implements LogSupport { +class Cache implements LogSupport, AdminStats { /** Service using this object. */ public final BaseService svc @@ -229,7 +230,7 @@ class Cache implements LogSupport { ] } - List getComparisonFields() { + List getComparableAdminStats() { replicate ? ['count', 'latestTimestamp'] : [] } diff --git a/src/main/groovy/io/xh/hoist/cachedvalue/CachedValue.groovy b/src/main/groovy/io/xh/hoist/cachedvalue/CachedValue.groovy index 7c5cbffe..6e76d1c8 100644 --- a/src/main/groovy/io/xh/hoist/cachedvalue/CachedValue.groovy +++ b/src/main/groovy/io/xh/hoist/cachedvalue/CachedValue.groovy @@ -12,6 +12,7 @@ import com.hazelcast.topic.Message import com.hazelcast.topic.ReliableMessageListener import groovy.transform.NamedParam import groovy.transform.NamedVariant +import io.xh.hoist.AdminStats import io.xh.hoist.BaseService import io.xh.hoist.cluster.ClusterService import io.xh.hoist.log.LogSupport @@ -30,7 +31,7 @@ import static java.lang.System.currentTimeMillis * Similar to {@link io.xh.hoist.cache.Cache}, but a single value that can be read, written, and expired. * Like Cache, this object supports replication across the cluster. */ -class CachedValue implements LogSupport { +class CachedValue implements LogSupport, AdminStats { /** Service using this object. */ public final BaseService svc @@ -246,7 +247,7 @@ class CachedValue implements LogSupport { return ret } - List getComparisonFields() { + List getComparableAdminStats() { if (!replicate) return [] def val = get(), ret = ['timestamp'] @@ -255,5 +256,4 @@ class CachedValue implements LogSupport { } return ret } - } diff --git a/src/main/groovy/io/xh/hoist/cluster/DistributedObjectInfo.groovy b/src/main/groovy/io/xh/hoist/cluster/DistributedObjectInfo.groovy deleted file mode 100644 index 09c14231..00000000 --- a/src/main/groovy/io/xh/hoist/cluster/DistributedObjectInfo.groovy +++ /dev/null @@ -1,45 +0,0 @@ -package io.xh.hoist.cluster - -import io.xh.hoist.json.JSONFormat - -import static io.xh.hoist.util.Utils.getClusterService - -class DistributedObjectInfo implements JSONFormat { - // Absolute name of the object, make sure to use `svc.hzName(name)` on relative-named objects - String name - String type - Map adminStats - List comparisonFields - String instanceName - // Error encountered while collecting this object's data - String error - - DistributedObjectInfo(Map args) { - adminStats = (args.adminStats ?: Collections.emptyMap()) as Map - comparisonFields = (args.comparisonFields ?: Collections.emptyList()) as List - instanceName = clusterService.localName - name = args.name ?: adminStats.name - type = args.type ?: adminStats.type - error = args.error - } - - boolean isMatching(DistributedObjectInfo other) { - return this.comparisonFields == other.comparisonFields && this.comparisonFields.every { field -> - this.adminStats[field] == other.adminStats[field] - } - } - - boolean isComparableWith(DistributedObjectInfo other) { - return this.name == other.name - } - - Map formatForJSON() { - return [ - name: name, - type: type, - instanceName: instanceName, - comparisonFields: comparisonFields, - adminStats: adminStats - ] - } -} \ No newline at end of file From 72c2d3c5b3142220add7d08f0f51fa8236344692 Mon Sep 17 00:00:00 2001 From: lbwexler Date: Fri, 3 Jan 2025 18:47:26 -0500 Subject: [PATCH 2/2] changes from CR --- .../io/xh/hoist/admin/ClusterObjectsService.groovy | 6 +++--- src/main/groovy/io/xh/hoist/admin/ClusterObjectInfo.groovy | 7 ++++--- src/main/groovy/io/xh/hoist/admin/HzAdminStats.groovy | 4 ++-- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/grails-app/services/io/xh/hoist/admin/ClusterObjectsService.groovy b/grails-app/services/io/xh/hoist/admin/ClusterObjectsService.groovy index 0fc4bf75..533c5c9f 100644 --- a/grails-app/services/io/xh/hoist/admin/ClusterObjectsService.groovy +++ b/grails-app/services/io/xh/hoist/admin/ClusterObjectsService.groovy @@ -56,10 +56,10 @@ class ClusterObjectsService extends BaseService { Map svcs = grailsApplication.mainContext.getBeansOfType(BaseService.class, false, false) def hoistObjs = svcs.collectMany { _, svc -> [ - new ClusterObjectInfo(svc, [name: svc.class.name, type: 'Service']), + new ClusterObjectInfo(name: svc.class.name, type: 'Service', target: svc), *svc.resources .findAll { k, v -> v instanceof AdminStats } - .collect { k, v -> new ClusterObjectInfo(v as AdminStats, [name: svc.hzName(k)]) } + .collect { k, v -> new ClusterObjectInfo(name: svc.hzName(k), target: v) } ] } @@ -68,7 +68,7 @@ class ClusterObjectsService extends BaseService { .hzInstance .distributedObjects .findAll { !(it instanceof ExecutorServiceProxy) } - .collect { new ClusterObjectInfo(new HzAdminStats(it)) } + .collect { new ClusterObjectInfo(target: new HzAdminStats(it)) } return (hzObjs + hoistObjs) as List } diff --git a/src/main/groovy/io/xh/hoist/admin/ClusterObjectInfo.groovy b/src/main/groovy/io/xh/hoist/admin/ClusterObjectInfo.groovy index 2280cb72..0dcddbf1 100644 --- a/src/main/groovy/io/xh/hoist/admin/ClusterObjectInfo.groovy +++ b/src/main/groovy/io/xh/hoist/admin/ClusterObjectInfo.groovy @@ -15,8 +15,9 @@ class ClusterObjectInfo implements JSONFormat { String instanceName String error - ClusterObjectInfo(AdminStats target, Map meta = [:] ) { + ClusterObjectInfo(Map config = [:] ) { try { + def target = config.target as AdminStats adminStats = target.adminStats comparableAdminStats = target.comparableAdminStats } catch (Exception e) { @@ -24,8 +25,8 @@ class ClusterObjectInfo implements JSONFormat { comparableAdminStats = emptyList() error = "Error computing admin stats | ${e.message}" } - name = meta.name ?: adminStats.name - type = meta.type ?: adminStats.type + name = config.name ?: adminStats.name + type = config.type ?: adminStats.type instanceName = clusterService.localName } diff --git a/src/main/groovy/io/xh/hoist/admin/HzAdminStats.groovy b/src/main/groovy/io/xh/hoist/admin/HzAdminStats.groovy index 659cb8ca..10280ee7 100644 --- a/src/main/groovy/io/xh/hoist/admin/HzAdminStats.groovy +++ b/src/main/groovy/io/xh/hoist/admin/HzAdminStats.groovy @@ -64,7 +64,7 @@ class HzAdminStats implements AdminStats { gets : stats.getOperationCount, sets : stats.setOperationCount, puts : stats.putOperationCount, - nearCache : getNearCacheStats(stats.nearCacheStats), + nearCache : getNearCacheStats(stats.nearCacheStats) ] break case ISet: @@ -75,7 +75,7 @@ class HzAdminStats implements AdminStats { type : 'ISet', size : obj.size(), lastUpdateTime: stats.lastUpdateTime ?: null, - lastAccessTime: stats.lastAccessTime ?: null, + lastAccessTime: stats.lastAccessTime ?: null ] break case ITopic: