-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Enhanced DistributedObjectAdmin to compare for inconsistencies between instances #426
base: develop
Are you sure you want to change the base?
Changes from 23 commits
16e5127
d189178
1849506
9d827a2
43f6886
787aa19
c1d848b
c5f4c67
0867cd9
f8dde4a
87d9656
3e0bad0
423d2e7
58dabe2
740261d
4eb6e0e
14a390c
010ceec
ae438e1
7be4e54
961233f
eaaed2f
3d54cb1
f930d0d
46f864a
78eca10
4b8f2e3
8f1fe7d
d52a6fc
425c695
139dcac
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
/* | ||
* This file belongs to Hoist, an application development toolkit | ||
* developed by Extremely Heavy Industries (www.xh.io | [email protected]) | ||
* | ||
* Copyright © 2022 Extremely Heavy Industries Inc. | ||
*/ | ||
package io.xh.hoist.admin | ||
|
||
import io.xh.hoist.BaseController | ||
import io.xh.hoist.security.Access | ||
|
||
@Access(['HOIST_ADMIN_READER']) | ||
class DistributedObjectAdminController extends BaseController { | ||
def distributedObjectAdminService | ||
|
||
def getDistributedObjectsReport() { | ||
renderJSON(distributedObjectAdminService.getDistributedObjectsReport()) | ||
} | ||
|
||
@Access(['HOIST_ADMIN']) | ||
def clearObjects() { | ||
def req = parseRequestJSON() | ||
distributedObjectAdminService.clearObjects(req.names) | ||
renderJSON([success: true]) | ||
} | ||
|
||
@Access(['HOIST_ADMIN']) | ||
def clearHibernateCaches() { | ||
distributedObjectAdminService.clearHibernateCaches() | ||
renderJSON([success: true]) | ||
} | ||
} |
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -91,6 +91,8 @@ class ClusterConfig { | |
Config createConfig() { | ||
def ret = new Config() | ||
|
||
System.out.println("ClusterConfig [INFO] | ${multiInstanceEnabled ? 'Multi-instance is enabled - instances will attempt to cluster.' : 'Multi-instance is disabled - instances will avoid clustering.'}") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think trying to mimic the log output is confusing --- can we just log this later in cluster service, when our logging is setup? |
||
|
||
ret.instanceName = instanceName | ||
ret.clusterName = clusterName | ||
ret.memberAttributeConfig.setAttribute('instanceName', instanceName) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,247 @@ | ||
/* | ||
* This file belongs to Hoist, an application development toolkit | ||
* developed by Extremely Heavy Industries (www.xh.io | [email protected]) | ||
* | ||
* Copyright © 2024 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<DistributedObjectInfo> listDistributedObjects() { | ||
// Services and their resources | ||
Map<String, BaseService> svcs = grailsApplication.mainContext.getBeansOfType(BaseService.class, false, false) | ||
def resourceObjs = svcs.collectMany { _, svc -> | ||
[ | ||
// Services themselves | ||
getInfo(obj: svc, name: svc.class.getName(), type: 'Service'), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. think we could clarify with two methods that each just take a single object, and do type switching as needed -- e.g. getHoistInfo(Object obj) and getHzInfo(DistributedObject obj) -- symmetry! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The issue I had with this was the fact that resource objects don't always know their full name - they often use a relative name. So a fully-symmetrical |
||
// Resources, excluding those that are also DistributedObject | ||
*svc.resources.findAll { k, v -> !(v instanceof DistributedObject)}.collect { k, v -> | ||
getInfo(obj: v, name: svc.hzName(k)) | ||
} | ||
] | ||
}, | ||
// Distributed objects | ||
hzObjs = clusterService | ||
.hzInstance | ||
.distributedObjects | ||
.findAll { !(it instanceof ExecutorServiceProxy) } | ||
.collect { getInfoForObject(it) } | ||
|
||
return [*hzObjs, *resourceObjs].findAll{ it } as List<DistributedObjectInfo> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: can be findAll() |
||
} | ||
static class ListDistributedObjects extends ClusterRequest<List<DistributedObjectInfo>> { | ||
List<DistributedObjectInfo> doCall() { | ||
appContext.distributedObjectAdminService.listDistributedObjects() | ||
} | ||
} | ||
|
||
void clearObjects(List<String> names) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Lets call this clearHibernateCaches and be specific! Could potentially combine with method below, or rename method below |
||
def all = clusterService.distributedObjects | ||
names.each { name -> | ||
def obj = all.find { it.getName() == name } | ||
/** Keep in sync with frontend clear set - `DistributedObjectsModel.clearableTypes`. */ | ||
if (obj instanceof CacheProxy) { | ||
obj.clear() | ||
logInfo("Cleared " + name) | ||
} else { | ||
logWarn('Cannot clear object - unsupported type', name) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. obsolete log text -- should be hibernate cache not found. |
||
} | ||
} | ||
} | ||
|
||
void clearHibernateCaches() { | ||
appContext.beanDefinitionNames | ||
.findAll { it.startsWith('sessionFactory') } | ||
.each { appContext.getBean(it)?.cache?.evictAllRegions() } | ||
} | ||
|
||
Map getAdminStatsForObject(DistributedObject obj) { | ||
return getInfoForObject(obj)?.adminStats | ||
} | ||
|
||
DistributedObjectInfo getInfo(Map args) { | ||
def obj = args.obj, | ||
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" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. would prefer curly braces on $e.message. |
||
} | ||
|
||
return new DistributedObjectInfo( | ||
comparisonFields: comparisonFields, | ||
adminStats: adminStats, | ||
error: error, | ||
*: args | ||
) | ||
} | ||
|
||
DistributedObjectInfo getInfoForObject(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 | ||
] | ||
) | ||
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), | ||
] | ||
) | ||
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, | ||
] | ||
) | ||
case ITopic: | ||
def stats = obj.getLocalTopicStats() | ||
return new DistributedObjectInfo( | ||
adminStats: [ | ||
name : obj.getName(), | ||
type : 'Topic', | ||
publishOperationCount: stats.publishOperationCount, | ||
receiveOperationCount: stats.receiveOperationCount | ||
] | ||
) | ||
case RingbufferProxy: | ||
return new DistributedObjectInfo( | ||
adminStats: [ | ||
name : obj.getName(), | ||
type : 'Ringbuffer', | ||
size : obj.size(), | ||
capacity: obj.capacity() | ||
] | ||
) | ||
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) | ||
] | ||
] | ||
) | ||
default: | ||
return new DistributedObjectInfo( | ||
adminStats: [ | ||
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' | ||
} | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
see below re: naming of these two endpoints to be more specific.