Skip to content

Commit

Permalink
Update /xh endpoints and config for version/instance change detecti…
Browse files Browse the repository at this point in the history
…on (#387)

* Rename `xhAppVersionCheck` -> `xhAppStatusCheck` and provide more appropriate defaults

* Update CHANGELOG

* Fix `xhAppStatusCheck` note in Bootstrap

* Updates to environment polling / endpoints

+ Tailor /xh/environment return based on auth user - support minimal unauthorized payload for backwards compat with existing usages, but mix-in full details for authenticated users.
+ Client now calls this endpoint both for initial load and for ongoing polling.
+ Rework `xhAppStatusCheck` to `xhEnvPollingConfig`, nest within authenticated /xh/environment return. Client reads and respects changes to config updates, with some sanity checks.
+ Restore /xh/version for backwards compat, return minimal environment summary.

* Changelog updates

* Checkpoint

* Take 2 on Environment Service changes

* Tweaks

* Use XhController.version for /ping

+ Remove dedicated `PingController`
+ Add extra info - appCode, timestamp, success:true - from ping to version output.

---------

Co-authored-by: Anselm McClain <atm@xh.io>
Co-authored-by: lbwexler <lbwexler@xh.io>
  • Loading branch information
3 people committed Sep 3, 2024
1 parent 4690e66 commit 5df3ca0
Show file tree
Hide file tree
Showing 7 changed files with 78 additions and 77 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@
* `ReplicatedValue` has been replaced with the enhanced `CachedValue`. This new object provides
all the functionality of the old, plus additional features from the `Cache` API such as expiry,
`getOrCreate()`, event support, and blocking support for non-primary nodes.
* Migrated previous `xhAppVersionCheck` to new `xhEnvPollConfig`, which now governs a single polling
interval on the client to check for app version and connected instance changes. The previous
config's `mode` value will be automatically migrated to the new `onVersionChange` key. A shorter
default interval of 10s will be set in all cases, to ensure timely detection of instance changes.
* The `/xh/environment` endpoint is no longer whitelisted and requires / will trigger
authentication flow.

### 🎁 New Features

Expand Down
31 changes: 0 additions & 31 deletions grails-app/controllers/io/xh/hoist/PingController.groovy

This file was deleted.

2 changes: 1 addition & 1 deletion grails-app/controllers/io/xh/hoist/UrlMappings.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class UrlMappings {
"/$controller/$action?/$id?(.$format)?"{}

"404" (controller: 'xh', action: 'notFound')
"/ping" (controller: 'xh', action: 'version')

//------------------------
// Rest Support
Expand All @@ -29,7 +30,6 @@ class UrlMappings {
action = [POST: 'create', GET: 'read', PUT: 'update', DELETE: 'delete']
}


//------------------------
// Proxy Support
//------------------------
Expand Down
45 changes: 29 additions & 16 deletions grails-app/controllers/io/xh/hoist/impl/XhController.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,20 @@ class XhController extends BaseController {
BaseUserService userService
ClusterService clusterService


//------------------------
// Identity / Auth
//------------------------
def authStatus() {
renderJSON(authenticated: authUser != null)
}

/** Whitelisted endpoint to return auth-related settings for client bootstrap. */
def authConfig() {
def svc = Utils.appContext.getBean(BaseAuthenticationService)
renderJSON(svc.clientConfig)
}

def getIdentity() {
renderJSON(identityService.clientConfig)
}
Expand Down Expand Up @@ -170,6 +177,7 @@ class XhController extends BaseController {
renderJSON(success: true)
}


//------------------------
// Json Blobs
//------------------------
Expand Down Expand Up @@ -219,16 +227,11 @@ class XhController extends BaseController {
renderJSON(environmentService.getEnvironment())
}

def version() {
def options = configService.getMap('xhAppVersionCheck', [:])
renderJSON(
*: options,
instanceName: clusterService.instanceName,
appVersion: Utils.appVersion,
appBuild: Utils.appBuild
)
def environmentPoll() {
renderJSON(environmentService.environmentPoll())
}


//------------------------
// Client Errors
//------------------------
Expand All @@ -246,6 +249,8 @@ class XhController extends BaseController {
renderJSON(success: true)
}



//------------------------
// Feedback
//------------------------
Expand All @@ -258,16 +263,32 @@ class XhController extends BaseController {
renderJSON(success: true)
}


//----------------------
// Alert Banner
//----------------------
def alertBanner() {
renderJSON(alertBannerService.alertBanner)
}


//-----------------------
// Misc
//-----------------------
/**
* Whitelisted (pre-auth) endpoint with minimal app identifier and version info.
* Also reachable (for backwards compatibility) via /ping, as per `UrlMappings`.
*/
def version() {
renderJSON(
appCode: Utils.appCode,
appVersion: Utils.appVersion,
appBuild: Utils.appBuild,
timestamp: System.currentTimeMillis(),
success: true
)
}

/**
* Returns the timezone offset for a given timezone ID.
* While abbrevs (e.g. 'GMT', 'PST', 'UTC+04') are supported, fully qualified IDs (e.g.
Expand All @@ -283,14 +304,6 @@ class XhController extends BaseController {
renderJSON([offset: tz.getOffset(System.currentTimeMillis())])
}

/**
* Auth-related settings for the client. Accessible pre-auth via whitelist.
*/
def authConfig() {
def svc = Utils.appContext.getBean(BaseAuthenticationService)
renderJSON(svc.clientConfig)
}

/**
* Utility to echo all headers received on the request. Useful in particular for verifying
* headers (e.g. `jespa_connection_id`) that are installed by or must pass through multiple
Expand Down
29 changes: 14 additions & 15 deletions grails-app/init/io/xh/hoist/BootStrap.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -99,21 +99,6 @@ class BootStrap implements LogSupport {
groupName: 'xh.io',
note: 'Official TimeZone for this application - e.g. the zone of the head office. Used to format/parse business related dates that need to be considered and displayed consistently at all locations. Set to a valid Java TimeZone ID.'
],
xhAppVersionCheck: [
valueType: 'json',
defaultValue: [
interval: configService.getInt('xhAppVersionCheckSecs', 30),
mode: configService.getBool('xhAppVersionCheckEnabled', true) ? 'promptReload' : 'silent'
],
clientVisible: true,
groupName: 'xh.io',
note: "Controls application behaviour when the server reports to the client that a new version is available. Supports the following options:\n\n" +
"-'interval': Frequency (in seconds) with which the version of the app should be checked. Value of -1 disables version checking.\n" +
"-'mode': Action taken by client upon a new version becoming available. Supports the following options:\n" +
"\t+ 'forceReload': Force clients to refresh immediately. To be used when an updated server is known to be incompatible with a previously deployed client.\n" +
"\t+ 'promptReload': Show an update prompt banner, allowing users to refresh when convenient.\n" +
"\t+ 'silent': No action taken."
],
xhAutoRefreshIntervals: [
valueType: 'json',
defaultValue: [app: -1],
Expand Down Expand Up @@ -197,6 +182,20 @@ class BootStrap implements LogSupport {
groupName: 'xh.io',
note: 'True to enable the monitor tab included with the Hoist Admin console and the associated server-side jobs'
],
xhEnvPollConfig: [
valueType: 'json',
defaultValue: [
interval: 10,
onVersionChange: configService.getMap('xhAppVersionCheck', [mode: 'promptReload']).get('mode')
],
groupName: 'xh.io',
note: "Controls client calls to server to poll for version, instance changes, or auth changes. Supports the following options:\n\n" +
"- interval: Frequency (in seconds) with which the status of the app server should be polled. Value of -1 disables checking.\n" +
"- onVersionChange: Action taken by client upon a new version becoming available, one of:\n" +
"\t+ 'forceReload': Force clients to refresh immediately. To be used when an updated server is known to be incompatible with a previously deployed client.\n" +
"\t+ 'promptReload': Show an update prompt banner, allowing users to refresh when convenient.\n" +
"\t+ 'silent': No action taken."
],
xhExpectedServerTimeZone: [
valueType: 'string',
defaultValue: '*',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import grails.plugins.GrailsPlugin
import grails.util.GrailsUtil
import grails.util.Holders
import io.xh.hoist.BaseService
import io.xh.hoist.config.ConfigService
import io.xh.hoist.util.Utils
import io.xh.hoist.websocket.WebSocketService


/**
Expand All @@ -20,16 +22,13 @@ import io.xh.hoist.util.Utils
*/
class EnvironmentService extends BaseService {

def configService,
webSocketService
ConfigService configService
WebSocketService webSocketService

private TimeZone _appTimeZone
private Map _pollResult

static clearCachesConfigs = ['xhAppTimeZone']

void init() {
_appTimeZone = calcAppTimeZone()
}
static clearCachesConfigs = ['xhAppTimeZone', 'xhEnvPollConfig']

/**
* Official TimeZone for this application - e.g. the zone of the head office or trading center.
Expand All @@ -39,15 +38,15 @@ class EnvironmentService extends BaseService {
* Not to be confused with `serverTimeZone` below.
*/
TimeZone getAppTimeZone() {
return _appTimeZone
return _appTimeZone ?= calcAppTimeZone()
}

/** TimeZone of the server/JVM running this application. */
TimeZone getServerTimeZone() {
return Calendar.instance.timeZone
}

/** Bundle of environment-related metadata, for serialization to JS clients. */
/** Full bundle of environment-related metadata, for serialization to JS clients. */
Map getEnvironment() {
def serverTz = serverTimeZone,
appTz = appTimeZone,
Expand All @@ -66,23 +65,38 @@ class EnvironmentService extends BaseService {
appTimeZone: appTz.toZoneId().id,
appTimeZoneOffset: appTz.getOffset(now),
webSocketsEnabled: webSocketService.enabled,
instanceName: clusterService.instanceName
instanceName: clusterService.instanceName,
pollConfig: configService.getMap('xhEnvPollConfig')
]

hoistGrailsPlugins.each {it ->
ret[it.name + 'Version'] = it.version
}

def user = authUser
if (user?.isHoistAdminReader) {
if (authUser.isHoistAdminReader) {
def dataSource = Utils.dataSourceConfig
ret.databaseConnectionString = dataSource.url
ret.databaseUser = dataSource.username
ret.databaseCreateMode = dataSource.dbCreate
}

return ret
}

/**
* Report server version and instance identity to the client.
* Designed to be called frequently by client. Should be minimal and highly optimized.
*/
Map environmentPoll() {
return _pollResult ?= [
appCode : Utils.appCode,
appVersion : Utils.appVersion,
appBuild : Utils.appBuild,
instanceName: clusterService.instanceName,
pollConfig : configService.getMap('xhEnvPollConfig'),
]
}


//---------------------
// Implementation
Expand All @@ -104,7 +118,8 @@ class EnvironmentService extends BaseService {
}

void clearCaches() {
this._appTimeZone = calcAppTimeZone()
_appTimeZone = null
_pollResult = null
super.clearCaches()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,6 @@ abstract class BaseAuthenticationService extends BaseService {
'/ping',
'/xh/login',
'/xh/logout',
'/xh/environment',
'/xh/version',
'/xh/authConfig'
]
Expand Down

0 comments on commit 5df3ca0

Please sign in to comment.