Skip to content

Commit

Permalink
Merge branch 'develop' into alertTransport
Browse files Browse the repository at this point in the history
  • Loading branch information
amcclain committed Sep 17, 2024
2 parents 7d4d652 + 6f7fae6 commit 4d9be8e
Show file tree
Hide file tree
Showing 23 changed files with 485 additions and 286 deletions.
25 changes: 20 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,30 @@

## 22.0-SNAPSHOT

### ⚙️ Technical
### 💥 Breaking Changes (upgrade difficulty: 🟢 LOW)

* Updated `Timer`, `Cache`, and `CachedValue` objects to require a `name` property. Names are now
mandatory to better support new cluster features, logging, and Admin Console tooling.
* Migrated `BaseService` methods `getIMap()`, `getReplicatedMap()` and `getISet()` to
`createIMap()`, `createReplicatedMap()` and `createISet()`, respectively. Not expected to impact
most apps, as these APIs are new and only used for distributed, multi-instance data.

### 🎁 New Features

* Updated `ClusterService` to use Hoist's `InstanceNotFoundException` class to designate routine.
* Added new `BaseService` factories to create `Cache` and `CachedValue` objects. This streamlined
interface reduces boilerplate and is consistent with `Timer` creation.
* Improved `Timer` to maintain consistent execution timing across primary instance changes.
* Improved `RestController` to support domain objects linked to a non-primary `DataSource`.

### ⚙️ Technical

* Exposed `/xh/ping` as whitelisted route for basic uptime/reachability checks. Retained legacy
`/ping` alias, but prefer this new path going forward.

* Improvements to `RestController` to better support editing Domain Objects defined with secondary
domain objects.
* Improved handling + rendering of exceptions during authentication and authorization requests.
* Updated `ClusterService` to use Hoist's `InstanceNotFoundException`, ensuring that common errors
thrown due to instance changes are marked as routine and don't spam error reporting.
* Added new `BaseService.resources` property to track and provide access to `Cache` objects and
`Timer`s by name, replacing `BaseService.timers`.

## 21.0.1 - 2024-09-05

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import io.xh.hoist.exception.NotAuthorizedException
import io.xh.hoist.exception.NotFoundException
import io.xh.hoist.log.LogSupport
import io.xh.hoist.user.IdentityService
import io.xh.hoist.websocket.HoistWebSocketConfigurer

import java.lang.reflect.Method

import static org.springframework.util.ReflectionUtils.findMethod
Expand All @@ -28,66 +30,55 @@ class AccessInterceptor implements LogSupport {
}

boolean before() {

// Ignore Websockets -- these are destined for a non-controller based endpoint
// established via a spring-websocket configuration mapping. (Note this is *not* currently
// built into Hoist but is checked / allowed for here.)
if (isWebSocketHandshake()) {
return true
}

// Get controller method, or 404
Class clazz = controllerClass?.clazz
String actionNm = actionName ?: controllerClass?.defaultAction
Method method = clazz && actionNm ? findMethod(clazz, actionNm) : null
if (!method) return handleNotFound()

// Eval method annotations, and return true or 401
def access = method.getAnnotation(Access) ?:
method.getAnnotation(AccessAll) ?:
clazz.getAnnotation(Access) as Access ?:
clazz.getAnnotation(AccessAll) as AccessAll

if (access instanceof AccessAll ||
(access instanceof Access && identityService.user.hasAllRoles(access.value()))
) {
return true
try {
// Ignore websockets - these are destined for a non-controller based endpoint
// established via a spring-websocket configuration mapping.
// Note that websockets are not always enabled by Hoist apps but must be supported here.
if (isWebSocketHandshake()) {
return true
}

// Get controller method, or throw 404 (Not Found).
Class clazz = controllerClass?.clazz
String actionNm = actionName ?: controllerClass?.defaultAction
Method method = clazz && actionNm ? findMethod(clazz, actionNm) : null
if (!method) throw new NotFoundException()

// Eval @Access annotations, return true if allowed, or throw 403 (Forbidden).
def access = method.getAnnotation(Access) ?:
method.getAnnotation(AccessAll) ?:
clazz.getAnnotation(Access) as Access ?:
clazz.getAnnotation(AccessAll) as AccessAll

if (access instanceof AccessAll ||
(access instanceof Access && identityService.user.hasAllRoles(access.value()))
) {
return true
}

def username = identityService.username ?: 'UNKNOWN'
throw new NotAuthorizedException(
"You do not have the required role(s) for this action. Currently logged in as: $username."
)
} catch (Exception e) {
xhExceptionHandler.handleException(
exception: e,
logMessage: [controller: controllerClass?.name, action: actionName],
logTo: this,
renderTo: response
)
}

return handleUnauthorized()
}

//------------------------
// Implementation
//------------------------
private boolean handleUnauthorized() {
def username = identityService.username ?: 'UNKNOWN',
ex = new NotAuthorizedException("""
You do not have the application role(s) required.
Currently logged in as: $username.
""")
xhExceptionHandler.handleException(
exception: ex,
logTo: this,
logMessage: [controller: controllerClass?.name, action: actionName],
renderTo: response
)
return false
}

private boolean handleNotFound() {
xhExceptionHandler.handleException(
exception: new NotFoundException(),
logTo: this,
logMessage: [controller: controllerClass?.name, action: actionName],
renderTo: response
)
return false
}

private boolean isWebSocketHandshake() {
def upgradeHeader = request?.getHeader('upgrade')
return upgradeHeader == 'websocket'
def req = getRequest(),
upgradeHeader = req?.getHeader('upgrade'),
uri = req?.requestURI

return upgradeHeader == 'websocket' && uri?.endsWith(HoistWebSocketConfigurer.WEBSOCKET_PATH)
}

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

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

Expand Down Expand Up @@ -178,6 +179,6 @@ class MemoryMonitoringService extends BaseService {

Map getAdminStats() {[
config: configForAdminStats('xhMemoryMonitoringConfig'),
latestSnapshot: latestSnapshot,
latestSnapshot: latestSnapshot
]}
}
40 changes: 20 additions & 20 deletions grails-app/services/io/xh/hoist/admin/ServiceManagerService.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

package io.xh.hoist.admin

import com.hazelcast.core.DistributedObject
import io.xh.hoist.BaseService

class ServiceManagerService extends BaseService {
Expand All @@ -15,8 +16,6 @@ class ServiceManagerService extends BaseService {
clusterAdminService

Collection<Map> listServices() {


getServicesInternal().collect { name, svc ->
return [
name: name,
Expand All @@ -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<String> names) {
Expand All @@ -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<String, BaseService> getServicesInternal() {
return grailsApplication.mainContext.getBeansOfType(BaseService.class, false, false)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +43,14 @@ class AlertBannerService extends BaseService {
private final static String presetsBlobName = 'xhAlertBannerPresets'

private final Map emptyAlert = [active: false]
private CachedValue<Map> _alertBanner = new CachedValue<>(
name: 'alertBanner',
replicate: true,
svc: this
)
private CachedValue<Map> _alertBanner = createCachedValue(name: 'alertBanner', replicate: true)
private Timer timer

void init() {
timer = createTimer(
interval: 2 * MINUTES,
name: 'readFromSpec',
runFn: this.&readFromSpec,
interval: 2 * MINUTES,
primaryOnly: true
)
super.init()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,15 @@ class ClientErrorService extends BaseService {
}]
]

private IMap<String, Map> errors = getIMap('clientErrors')
private IMap<String, Map> errors = createIMap('clientErrors')
private int getMaxErrors() {configService.getMap('xhClientErrorConfig').maxErrors as int}
private int getAlertInterval() {configService.getMap('xhClientErrorConfig').intervalMins * MINUTES}

void init() {
super.init()
createTimer(
name: 'processErrors',
runFn: this.&processErrors,
interval: { alertInterval },
delay: 15 * SECONDS,
primaryOnly: true
Expand Down Expand Up @@ -99,7 +101,7 @@ class ClientErrorService extends BaseService {
// Implementation
//---------------------------------------------------------
@Transactional
void onTimer() {
private void processErrors() {
if (!errors) return

def maxErrors = getMaxErrors(),
Expand All @@ -121,8 +123,7 @@ class ClientErrorService extends BaseService {
}

Map getAdminStats() {[
config: configForAdminStats('xhClientErrorConfig'),
pendingErrorCount: errors.size()
config: configForAdminStats('xhClientErrorConfig')
]}

}
3 changes: 1 addition & 2 deletions grails-app/services/io/xh/hoist/config/ConfigService.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -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()])
}

//-------------------
Expand Down
6 changes: 3 additions & 3 deletions grails-app/services/io/xh/hoist/ldap/LdapService.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ class LdapService extends BaseService {

def configService

private Cache<String, List<LdapObject>> cache = new Cache<>(
expireTime: {config.cacheExpireSecs * SECONDS},
svc: this
private Cache<String, List<LdapObject>> cache = createCache(
name: 'queryCache',
expireTime: {config.cacheExpireSecs * SECONDS}
)

static clearCachesConfigs = ['xhLdapConfig', 'xhLdapUsername', 'xhLdapPassword']
Expand Down
12 changes: 5 additions & 7 deletions grails-app/services/io/xh/hoist/log/LogArchiveService.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> archiveLogs(Integer daysThreshold) {
Expand Down Expand Up @@ -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)
}
Expand Down
11 changes: 6 additions & 5 deletions grails-app/services/io/xh/hoist/log/LogLevelService.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,12 @@ class LogLevelService extends BaseService {
private List<LogLevelAdjustment> adjustments = []

void init() {
createTimer(interval: 30 * MINUTES, runImmediatelyAndBlock: true)
}

private void onTimer() {
calculateAdjustments()
createTimer(
name: 'calculateAdjustments',
runFn: this.&calculateAdjustments,
interval: 30 * MINUTES,
runImmediatelyAndBlock: true
)
}

// -------------------------------------------------------------------------------
Expand Down
Loading

0 comments on commit 4d9be8e

Please sign in to comment.