From fad7b2d1644a52ac6a6dde87f0babe4127f5d227 Mon Sep 17 00:00:00 2001 From: lbwexler Date: Wed, 27 Nov 2024 19:17:07 -0500 Subject: [PATCH 1/7] Simplify serialization of WebSocketSession to avoid sending entire users over the wire/to the client, and to avoid serialization problems for complex users. --- .../groovy/io/xh/hoist/websocket/HoistWebSocketChannel.groovy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/groovy/io/xh/hoist/websocket/HoistWebSocketChannel.groovy b/src/main/groovy/io/xh/hoist/websocket/HoistWebSocketChannel.groovy index d73f5b4a..f0771c18 100644 --- a/src/main/groovy/io/xh/hoist/websocket/HoistWebSocketChannel.groovy +++ b/src/main/groovy/io/xh/hoist/websocket/HoistWebSocketChannel.groovy @@ -97,8 +97,8 @@ class HoistWebSocketChannel implements JSONFormat, LogSupport { Map formatForJSON() { return [ key: key, - authUser: authUser, - apparentUser: apparentUser, + authUser: [username: authUsername], + apparentUser: [username: apparentUsername], isOpen: session.isOpen(), createdTime: createdTime, sentMessageCount: sentMessageCount, From 28f511d1a2e0f141fb8832b77d809ce35a4d5321 Mon Sep 17 00:00:00 2001 From: lbwexler Date: Thu, 28 Nov 2024 08:11:18 -0500 Subject: [PATCH 2/7] New ClusterJsonRequest for more lightweight calling of controllers on other cluster members cr: Jakub (pending) --- .../io/xh/hoist/BaseController.groovy | 17 ++++--- .../MonitorResultsAdminController.groovy | 5 +- .../cluster/ClusterAdminController.groovy | 4 +- ...onnectionPoolMonitorAdminController.groovy | 8 ++-- .../admin/cluster/EnvAdminController.groovy | 4 +- .../cluster/HzObjectAdminController.groovy | 8 ++-- .../cluster/LogViewerAdminController.groovy | 9 ++-- .../MemoryMonitorAdminController.groovy | 10 ++-- .../ServiceManagerAdminController.groovy | 8 ++-- .../cluster/WebSocketAdminController.groovy | 6 +-- .../io/xh/hoist/cluster/ClusterService.groovy | 8 ++-- .../hoist/cluster/ClusterJsonRequest.groovy | 46 +++++++++++++++++++ .../hoist/cluster/ClusterJsonResponse.groovy | 8 ++++ .../hoist/exception/ExceptionHandler.groovy | 22 +++++---- .../websocket/HoistWebSocketChannel.groovy | 4 +- 15 files changed, 113 insertions(+), 54 deletions(-) create mode 100644 src/main/groovy/io/xh/hoist/cluster/ClusterJsonRequest.groovy create mode 100644 src/main/groovy/io/xh/hoist/cluster/ClusterJsonResponse.groovy diff --git a/grails-app/controllers/io/xh/hoist/BaseController.groovy b/grails-app/controllers/io/xh/hoist/BaseController.groovy index a45c861c..98255e76 100644 --- a/grails-app/controllers/io/xh/hoist/BaseController.groovy +++ b/grails-app/controllers/io/xh/hoist/BaseController.groovy @@ -9,6 +9,8 @@ package io.xh.hoist import grails.async.Promise import groovy.transform.CompileStatic +import io.xh.hoist.cluster.ClusterJsonRequest +import io.xh.hoist.cluster.ClusterJsonResponse import io.xh.hoist.cluster.ClusterRequest import io.xh.hoist.cluster.ClusterService import io.xh.hoist.exception.ExceptionHandler @@ -88,18 +90,15 @@ abstract class BaseController implements LogSupport, IdentitySupport { * Note: Any exception that occurs is assumed to be logged on the target instance * and will not be logged on the instance running this action. */ - protected void runOnInstance(ClusterRequest task, String instance) { - def ret = clusterService.submitToInstance(task, instance) - if (ret.exception) { - // Just render exception, was already logged on target instance - xhExceptionHandler.handleException(exception: ret.exception, renderTo: response) - return - } - renderJSON(ret.value) + protected void runOnInstance(ClusterJsonRequest task, String instance) { + ClusterJsonResponse ret = clusterService.submitToInstance(task, instance) + response.setStatus(ret.httpStatus) + response.setContentType('application/json; charset=UTF-8') + render(ret.json) } /** Run a task on the primary cluster instance. */ - protected void runOnPrimary(ClusterRequest task) { + protected void runOnPrimary(ClusterJsonRequest task) { runOnInstance(task, clusterService.primaryName) } diff --git a/grails-app/controllers/io/xh/hoist/admin/MonitorResultsAdminController.groovy b/grails-app/controllers/io/xh/hoist/admin/MonitorResultsAdminController.groovy index f760b93f..7f8b4002 100644 --- a/grails-app/controllers/io/xh/hoist/admin/MonitorResultsAdminController.groovy +++ b/grails-app/controllers/io/xh/hoist/admin/MonitorResultsAdminController.groovy @@ -8,7 +8,7 @@ package io.xh.hoist.admin import io.xh.hoist.BaseController -import io.xh.hoist.cluster.ClusterRequest +import io.xh.hoist.cluster.ClusterJsonRequest import io.xh.hoist.security.Access import static io.xh.hoist.util.Utils.getAppContext @@ -25,9 +25,10 @@ class MonitorResultsAdminController extends BaseController { def forceRunAllMonitors() { runOnPrimary(new ForceRunAllMonitors()) } - static class ForceRunAllMonitors extends ClusterRequest { + static class ForceRunAllMonitors extends ClusterJsonRequest { def doCall() { appContext.monitorService.forceRun() + [success: true] } } } diff --git a/grails-app/controllers/io/xh/hoist/admin/cluster/ClusterAdminController.groovy b/grails-app/controllers/io/xh/hoist/admin/cluster/ClusterAdminController.groovy index ddce2adf..e7b537c7 100644 --- a/grails-app/controllers/io/xh/hoist/admin/cluster/ClusterAdminController.groovy +++ b/grails-app/controllers/io/xh/hoist/admin/cluster/ClusterAdminController.groovy @@ -8,7 +8,7 @@ package io.xh.hoist.admin.cluster import io.xh.hoist.BaseController -import io.xh.hoist.cluster.ClusterRequest +import io.xh.hoist.cluster.ClusterJsonRequest import io.xh.hoist.security.Access import io.xh.hoist.util.Utils @@ -42,7 +42,7 @@ class ClusterAdminController extends BaseController { // Wait enough to let async call below complete sleep(5 * SECONDS) } - static class ShutdownInstance extends ClusterRequest { + static class ShutdownInstance extends ClusterJsonRequest { def doCall() { // Run async to allow this call to successfully return. task { diff --git a/grails-app/controllers/io/xh/hoist/admin/cluster/ConnectionPoolMonitorAdminController.groovy b/grails-app/controllers/io/xh/hoist/admin/cluster/ConnectionPoolMonitorAdminController.groovy index 588b9317..84483fa0 100644 --- a/grails-app/controllers/io/xh/hoist/admin/cluster/ConnectionPoolMonitorAdminController.groovy +++ b/grails-app/controllers/io/xh/hoist/admin/cluster/ConnectionPoolMonitorAdminController.groovy @@ -7,7 +7,7 @@ package io.xh.hoist.admin.cluster import io.xh.hoist.BaseController -import io.xh.hoist.cluster.ClusterRequest +import io.xh.hoist.cluster.ClusterJsonRequest import io.xh.hoist.security.Access import static io.xh.hoist.util.Utils.appContext @@ -18,7 +18,7 @@ class ConnectionPoolMonitorAdminController extends BaseController { def snapshots(String instance) { runOnInstance(new Snapshots(), instance) } - static class Snapshots extends ClusterRequest { + static class Snapshots extends ClusterJsonRequest { def doCall() { def svc = appContext.connectionPoolMonitoringService return [ @@ -34,7 +34,7 @@ class ConnectionPoolMonitorAdminController extends BaseController { def takeSnapshot(String instance) { runOnInstance(new TakeSnapshot(), instance) } - static class TakeSnapshot extends ClusterRequest { + static class TakeSnapshot extends ClusterJsonRequest { def doCall() { appContext.connectionPoolMonitoringService.takeSnapshot() } @@ -44,7 +44,7 @@ class ConnectionPoolMonitorAdminController extends BaseController { def resetStats() { runOnInstance(new ResetStats()) } - static class ResetStats extends ClusterRequest { + static class ResetStats extends ClusterJsonRequest { def doCall() { appContext.connectionPoolMonitoringService.resetStats() } diff --git a/grails-app/controllers/io/xh/hoist/admin/cluster/EnvAdminController.groovy b/grails-app/controllers/io/xh/hoist/admin/cluster/EnvAdminController.groovy index f29c56a9..5156da28 100644 --- a/grails-app/controllers/io/xh/hoist/admin/cluster/EnvAdminController.groovy +++ b/grails-app/controllers/io/xh/hoist/admin/cluster/EnvAdminController.groovy @@ -7,7 +7,7 @@ package io.xh.hoist.admin.cluster import io.xh.hoist.BaseController -import io.xh.hoist.cluster.ClusterRequest +import io.xh.hoist.cluster.ClusterJsonRequest import io.xh.hoist.security.Access import static io.xh.hoist.util.Utils.isSensitiveParamName @@ -19,7 +19,7 @@ class EnvAdminController extends BaseController { def index(String instance) { runOnInstance(new Index(), instance) } - static class Index extends ClusterRequest { + static class Index extends ClusterJsonRequest { def doCall() { [ environment: System.getenv().collectEntries { diff --git a/grails-app/controllers/io/xh/hoist/admin/cluster/HzObjectAdminController.groovy b/grails-app/controllers/io/xh/hoist/admin/cluster/HzObjectAdminController.groovy index e6f8d31a..e98fff36 100644 --- a/grails-app/controllers/io/xh/hoist/admin/cluster/HzObjectAdminController.groovy +++ b/grails-app/controllers/io/xh/hoist/admin/cluster/HzObjectAdminController.groovy @@ -7,7 +7,7 @@ package io.xh.hoist.admin.cluster import io.xh.hoist.BaseController -import io.xh.hoist.cluster.ClusterRequest +import io.xh.hoist.cluster.ClusterJsonRequest import io.xh.hoist.security.Access import static io.xh.hoist.util.Utils.appContext @@ -20,7 +20,7 @@ class HzObjectAdminController extends BaseController { runOnInstance(new ListObjects(), instance) } - static class ListObjects extends ClusterRequest { + static class ListObjects extends ClusterJsonRequest { def doCall() { appContext.clusterAdminService.listObjects() } @@ -31,7 +31,7 @@ class HzObjectAdminController extends BaseController { runOnInstance(new ClearObjects(names: params.list('names')), instance) } - static class ClearObjects extends ClusterRequest { + static class ClearObjects extends ClusterJsonRequest { List names def doCall() { @@ -45,7 +45,7 @@ class HzObjectAdminController extends BaseController { runOnInstance(new ClearHibernateCaches(), instance) } - static class ClearHibernateCaches extends ClusterRequest { + static class ClearHibernateCaches extends ClusterJsonRequest { def doCall() { appContext.clusterAdminService.clearHibernateCaches() return [success: true] diff --git a/grails-app/controllers/io/xh/hoist/admin/cluster/LogViewerAdminController.groovy b/grails-app/controllers/io/xh/hoist/admin/cluster/LogViewerAdminController.groovy index 0b787a80..ef1305ab 100644 --- a/grails-app/controllers/io/xh/hoist/admin/cluster/LogViewerAdminController.groovy +++ b/grails-app/controllers/io/xh/hoist/admin/cluster/LogViewerAdminController.groovy @@ -10,6 +10,7 @@ package io.xh.hoist.admin.cluster import groovy.io.FileType import io.xh.hoist.BaseController import io.xh.hoist.cluster.ClusterRequest +import io.xh.hoist.cluster.ClusterJsonRequest import io.xh.hoist.configuration.LogbackConfig import io.xh.hoist.security.Access import io.xh.hoist.exception.RoutineRuntimeException @@ -23,7 +24,7 @@ class LogViewerAdminController extends BaseController { runOnInstance(new ListFiles(), instance) } - static class ListFiles extends ClusterRequest { + static class ListFiles extends ClusterJsonRequest { def doCall() { def logRootPath = appContext.logReaderService.logDir.absolutePath, files = availableFiles.collect { @@ -58,7 +59,7 @@ class LogViewerAdminController extends BaseController { ) } - static class GetFile extends ClusterRequest { + static class GetFile extends ClusterJsonRequest { String filename Integer startLine Integer maxLines @@ -112,7 +113,7 @@ class LogViewerAdminController extends BaseController { runOnInstance(new DeleteFiles(filenames: params.list('filenames')), instance) } - static class DeleteFiles extends ClusterRequest { + static class DeleteFiles extends ClusterJsonRequest { List filenames def doCall() { @@ -138,7 +139,7 @@ class LogViewerAdminController extends BaseController { runOnInstance(new ArchiveLogs(daysThreshold: daysThreshold), instance) } - static class ArchiveLogs extends ClusterRequest { + static class ArchiveLogs extends ClusterJsonRequest { Integer daysThreshold def doCall() { diff --git a/grails-app/controllers/io/xh/hoist/admin/cluster/MemoryMonitorAdminController.groovy b/grails-app/controllers/io/xh/hoist/admin/cluster/MemoryMonitorAdminController.groovy index 0378394e..828a3ea6 100644 --- a/grails-app/controllers/io/xh/hoist/admin/cluster/MemoryMonitorAdminController.groovy +++ b/grails-app/controllers/io/xh/hoist/admin/cluster/MemoryMonitorAdminController.groovy @@ -8,7 +8,7 @@ package io.xh.hoist.admin.cluster import io.xh.hoist.BaseController -import io.xh.hoist.cluster.ClusterRequest +import io.xh.hoist.cluster.ClusterJsonRequest import io.xh.hoist.security.Access import static io.xh.hoist.util.Utils.appContext @@ -21,7 +21,7 @@ class MemoryMonitorAdminController extends BaseController { def snapshots(String instance) { runOnInstance(new Snapshots(), instance) } - static class Snapshots extends ClusterRequest { + static class Snapshots extends ClusterJsonRequest { def doCall() { appContext.memoryMonitoringService.snapshots } @@ -31,7 +31,7 @@ class MemoryMonitorAdminController extends BaseController { def takeSnapshot(String instance) { runOnInstance(new TakeSnapshot(), instance) } - static class TakeSnapshot extends ClusterRequest { + static class TakeSnapshot extends ClusterJsonRequest { def doCall() { appContext.memoryMonitoringService.takeSnapshot() } @@ -42,7 +42,7 @@ class MemoryMonitorAdminController extends BaseController { def requestGc(String instance) { runOnInstance(new RequestGc(), instance) } - static class RequestGc extends ClusterRequest { + static class RequestGc extends ClusterJsonRequest { def doCall() { appContext.memoryMonitoringService.requestGc() } @@ -52,7 +52,7 @@ class MemoryMonitorAdminController extends BaseController { def dumpHeap(String filename, String instance) { runOnInstance(new DumpHeap(filename: filename), instance) } - static class DumpHeap extends ClusterRequest { + static class DumpHeap extends ClusterJsonRequest { String filename def doCall() { diff --git a/grails-app/controllers/io/xh/hoist/admin/cluster/ServiceManagerAdminController.groovy b/grails-app/controllers/io/xh/hoist/admin/cluster/ServiceManagerAdminController.groovy index 788d9840..2238894e 100644 --- a/grails-app/controllers/io/xh/hoist/admin/cluster/ServiceManagerAdminController.groovy +++ b/grails-app/controllers/io/xh/hoist/admin/cluster/ServiceManagerAdminController.groovy @@ -7,7 +7,7 @@ package io.xh.hoist.admin.cluster import io.xh.hoist.BaseController -import io.xh.hoist.cluster.ClusterRequest +import io.xh.hoist.cluster.ClusterJsonRequest import io.xh.hoist.security.Access import static io.xh.hoist.util.Utils.appContext @@ -18,7 +18,7 @@ class ServiceManagerAdminController extends BaseController { def listServices(String instance) { runOnInstance(new ListServices(), instance) } - static class ListServices extends ClusterRequest { + static class ListServices extends ClusterJsonRequest { def doCall() { appContext.serviceManagerService.listServices() } @@ -28,7 +28,7 @@ class ServiceManagerAdminController extends BaseController { def task = new GetStats(name: name) runOnInstance(task, instance) } - static class GetStats extends ClusterRequest { + static class GetStats extends ClusterJsonRequest { String name def doCall() { appContext.serviceManagerService.getStats(name) @@ -40,7 +40,7 @@ class ServiceManagerAdminController extends BaseController { def task = new ClearCaches(names: params.list('names')) instance ? runOnInstance(task, instance) : runOnAllInstances(task) } - static class ClearCaches extends ClusterRequest { + static class ClearCaches extends ClusterJsonRequest { List names def doCall() { diff --git a/grails-app/controllers/io/xh/hoist/admin/cluster/WebSocketAdminController.groovy b/grails-app/controllers/io/xh/hoist/admin/cluster/WebSocketAdminController.groovy index a0dea425..3c7e1a03 100644 --- a/grails-app/controllers/io/xh/hoist/admin/cluster/WebSocketAdminController.groovy +++ b/grails-app/controllers/io/xh/hoist/admin/cluster/WebSocketAdminController.groovy @@ -8,7 +8,7 @@ package io.xh.hoist.admin.cluster import io.xh.hoist.BaseController -import io.xh.hoist.cluster.ClusterRequest +import io.xh.hoist.cluster.ClusterJsonRequest import io.xh.hoist.security.Access import static io.xh.hoist.util.Utils.getAppContext @@ -19,7 +19,7 @@ class WebSocketAdminController extends BaseController { def allChannels(String instance) { runOnInstance(new AllChannels(), instance) } - static class AllChannels extends ClusterRequest { + static class AllChannels extends ClusterJsonRequest { def doCall() { appContext.webSocketService.allChannels*.formatForJSON() } @@ -29,7 +29,7 @@ class WebSocketAdminController extends BaseController { def pushToChannel(String channelKey, String topic, String message, String instance) { runOnInstance(new PushToChannel(channelKey: channelKey, topic: topic, message: message), instance) } - static class PushToChannel extends ClusterRequest { + static class PushToChannel extends ClusterJsonRequest { String channelKey String topic String message diff --git a/grails-app/services/io/xh/hoist/cluster/ClusterService.groovy b/grails-app/services/io/xh/hoist/cluster/ClusterService.groovy index 2fcb9ebd..0b2a8145 100644 --- a/grails-app/services/io/xh/hoist/cluster/ClusterService.groovy +++ b/grails-app/services/io/xh/hoist/cluster/ClusterService.groovy @@ -16,6 +16,8 @@ import io.xh.hoist.util.Utils import org.springframework.boot.context.event.ApplicationReadyEvent import org.springframework.context.ApplicationListener +import java.util.concurrent.Callable + class ClusterService extends BaseService implements ApplicationListener { /** @@ -143,11 +145,11 @@ class ClusterService extends BaseService implements ApplicationListener ClusterResponse submitToInstance(ClusterRequest c, String instance) { - executorService.submitToMember(c, getMember(instance)).get() + T submitToInstance(Callable c, String instance) { + executorService.submitToMember(c, getMember(instance)).get() } - Map> submitToAllInstances(ClusterRequest c) { + Map submitToAllInstances(Callable c) { executorService .submitToAllMembers(c) .collectEntries { member, f -> [member.getAttribute('instanceName'), f.get()] } diff --git a/src/main/groovy/io/xh/hoist/cluster/ClusterJsonRequest.groovy b/src/main/groovy/io/xh/hoist/cluster/ClusterJsonRequest.groovy new file mode 100644 index 00000000..c2a747f2 --- /dev/null +++ b/src/main/groovy/io/xh/hoist/cluster/ClusterJsonRequest.groovy @@ -0,0 +1,46 @@ +package io.xh.hoist.cluster + +import io.xh.hoist.exception.InstanceNotAvailableException +import io.xh.hoist.log.LogSupport +import io.xh.hoist.util.Utils + +import java.util.concurrent.Callable + +import static io.xh.hoist.json.JSONSerializer.serialize +import static io.xh.hoist.util.Utils.isInstanceReady +import static org.apache.hc.core5.http.HttpStatus.SC_OK + +abstract class ClusterJsonRequest implements Callable, LogSupport { + + ClusterJsonResponse call() { + try { + if (!instanceReady) { + throw new InstanceNotAvailableException('Instance not available and may be initializing.') + } + def value = doCall() + new ClusterJsonResponse( + httpStatus: SC_OK, + json: serialize(value) + ) + } catch (Throwable t) { + def exceptionHandler = Utils.exceptionHandler + try { + exceptionHandler.handleException( + exception: t, + logTo: this, + logMessage: [_action: this.class.simpleName] + ) + } catch (Exception e) { + // Even logging failing -- just catch quietly and return neatly to calling member. + } + return new ClusterJsonResponse( + httpStatus: exceptionHandler.getHttpStatus(t), + json: serialize(t) + ) + } + } + + abstract Object doCall() +} + + diff --git a/src/main/groovy/io/xh/hoist/cluster/ClusterJsonResponse.groovy b/src/main/groovy/io/xh/hoist/cluster/ClusterJsonResponse.groovy new file mode 100644 index 00000000..c8310e31 --- /dev/null +++ b/src/main/groovy/io/xh/hoist/cluster/ClusterJsonResponse.groovy @@ -0,0 +1,8 @@ +package io.xh.hoist.cluster + +class ClusterJsonResponse { + int httpStatus + String json +} + + diff --git a/src/main/groovy/io/xh/hoist/exception/ExceptionHandler.groovy b/src/main/groovy/io/xh/hoist/exception/ExceptionHandler.groovy index 91d3c8fd..9ddb27ed 100644 --- a/src/main/groovy/io/xh/hoist/exception/ExceptionHandler.groovy +++ b/src/main/groovy/io/xh/hoist/exception/ExceptionHandler.groovy @@ -75,6 +75,18 @@ class ExceptionHandler { summaryTextInternal(t, true) } + /** HttpStatus dode for this exception. */ + int getHttpStatus(Throwable t) { + if (t instanceof HttpException && !(t instanceof ExternalHttpException)) { + return ((HttpException) t).statusCode + } + + return t instanceof RoutineException ? + SC_BAD_REQUEST : + SC_INTERNAL_SERVER_ERROR + } + + //--------------------------------------------- // Template methods. For application override //--------------------------------------------- @@ -90,16 +102,6 @@ class ExceptionHandler { return t instanceof RoutineException } - protected int getHttpStatus(Throwable t) { - if (t instanceof HttpException && !(t instanceof ExternalHttpException)) { - return ((HttpException) t).statusCode - } - - return t instanceof RoutineException ? - SC_BAD_REQUEST : - SC_INTERNAL_SERVER_ERROR - } - //--------------------------- // Implementation //--------------------------- diff --git a/src/main/groovy/io/xh/hoist/websocket/HoistWebSocketChannel.groovy b/src/main/groovy/io/xh/hoist/websocket/HoistWebSocketChannel.groovy index f0771c18..d73f5b4a 100644 --- a/src/main/groovy/io/xh/hoist/websocket/HoistWebSocketChannel.groovy +++ b/src/main/groovy/io/xh/hoist/websocket/HoistWebSocketChannel.groovy @@ -97,8 +97,8 @@ class HoistWebSocketChannel implements JSONFormat, LogSupport { Map formatForJSON() { return [ key: key, - authUser: [username: authUsername], - apparentUser: [username: apparentUsername], + authUser: authUser, + apparentUser: apparentUser, isOpen: session.isOpen(), createdTime: createdTime, sentMessageCount: sentMessageCount, From 630f402f694adf2cbe758b18d6bcc750988f8936 Mon Sep 17 00:00:00 2001 From: lbwexler Date: Thu, 28 Nov 2024 08:23:43 -0500 Subject: [PATCH 3/7] New ClusterJsonRequest for more lightweight calling of controllers on other cluster members cr: Jakub (pending) --- CHANGELOG.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 57a5c2d4..3fe8cee8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ ## 26.0-SNAPSHOT - unreleased + +### 💥 Breaking Changes (upgrade difficulty: 🟢 TRIVIAL - change to runOnInstance signature.) + +### 🎁 New Features +* `BaseController.runOnInstance` now performs the json serialization on the target instance. This +allows lighter-weight remote endpoint executions, that do not require object serialization. +Applications must now provide a `ClusterJsonRequest` to this method rather than a `JsonRequest` + ### ⚙️ Technical * Align all-built in log names to have form "App-Cluster-xxx.log" @@ -37,7 +45,7 @@ ALTER TABLE xh_role ALTER COLUMN category VARCHAR(100) null * Requires `hoist-react >= 69` to support revised API for Activity Tracking and User Preference POSTs from client. -### 🎁 New Features +_### 🎁 New Features_ * Updated Activity Tracking endpoint to support client POSTing multiple track logs in a single request, helping to reduce network overhead for chatty apps. From 14d7765490a7f795b982f780834315893d3cae8a Mon Sep 17 00:00:00 2001 From: lbwexler Date: Thu, 28 Nov 2024 08:27:16 -0500 Subject: [PATCH 4/7] New ClusterJsonRequest for more lightweight calling of controllers on other cluster members cr: Jakub (pending) --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fe8cee8..a08e45f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ ### 🎁 New Features * `BaseController.runOnInstance` now performs the json serialization on the target instance. This allows lighter-weight remote endpoint executions, that do not require object serialization. -Applications must now provide a `ClusterJsonRequest` to this method rather than a `JsonRequest` +Applications must now provide a `ClusterJsonRequest` to this method rather than a `ClusterRequest` ### ⚙️ Technical * Align all-built in log names to have form "App-Cluster-xxx.log" From 976b073ab32753a924b7c316afd4f2637162f67b Mon Sep 17 00:00:00 2001 From: lbwexler Date: Thu, 28 Nov 2024 08:30:41 -0500 Subject: [PATCH 5/7] New ClusterJsonRequest for more lightweight calling of controllers on other cluster members cr: Jakub (pending) --- CHANGELOG.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a08e45f9..5b3d9ac6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,6 @@ ## 26.0-SNAPSHOT - unreleased - ### 💥 Breaking Changes (upgrade difficulty: 🟢 TRIVIAL - change to runOnInstance signature.) ### 🎁 New Features @@ -45,7 +44,7 @@ ALTER TABLE xh_role ALTER COLUMN category VARCHAR(100) null * Requires `hoist-react >= 69` to support revised API for Activity Tracking and User Preference POSTs from client. -_### 🎁 New Features_ +### 🎁 New Features * Updated Activity Tracking endpoint to support client POSTing multiple track logs in a single request, helping to reduce network overhead for chatty apps. From 3ad70a4e041aeaa82de548f78206013052eda66d Mon Sep 17 00:00:00 2001 From: lbwexler Date: Thu, 28 Nov 2024 09:17:35 -0500 Subject: [PATCH 6/7] New ClusterJsonRequest for more lightweight calling of controllers on other cluster members cr: Jakub (pending) --- .../io/xh/hoist/admin/cluster/WebSocketAdminController.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grails-app/controllers/io/xh/hoist/admin/cluster/WebSocketAdminController.groovy b/grails-app/controllers/io/xh/hoist/admin/cluster/WebSocketAdminController.groovy index 3c7e1a03..d14e6ab1 100644 --- a/grails-app/controllers/io/xh/hoist/admin/cluster/WebSocketAdminController.groovy +++ b/grails-app/controllers/io/xh/hoist/admin/cluster/WebSocketAdminController.groovy @@ -21,7 +21,7 @@ class WebSocketAdminController extends BaseController { } static class AllChannels extends ClusterJsonRequest { def doCall() { - appContext.webSocketService.allChannels*.formatForJSON() + appContext.webSocketService.allChannels } } From e698ee47c2c0ce1226657f003124dd895bd9f6bb Mon Sep 17 00:00:00 2001 From: lbwexler Date: Thu, 28 Nov 2024 10:44:04 -0500 Subject: [PATCH 7/7] + Tighten LogViewer download + Harden Exception handling on Remote Exception cr: Jakub (pending) --- .../cluster/LogViewerAdminController.groovy | 18 +++++++------- .../io/xh/hoist/cluster/ClusterRequest.groovy | 7 ++++-- .../ClusterExecutionException.groovy | 24 +++++++++++++++++++ 3 files changed, 38 insertions(+), 11 deletions(-) create mode 100644 src/main/groovy/io/xh/hoist/exception/ClusterExecutionException.groovy diff --git a/grails-app/controllers/io/xh/hoist/admin/cluster/LogViewerAdminController.groovy b/grails-app/controllers/io/xh/hoist/admin/cluster/LogViewerAdminController.groovy index ef1305ab..ee326ff7 100644 --- a/grails-app/controllers/io/xh/hoist/admin/cluster/LogViewerAdminController.groovy +++ b/grails-app/controllers/io/xh/hoist/admin/cluster/LogViewerAdminController.groovy @@ -87,19 +87,19 @@ class LogViewerAdminController extends BaseController { return } - render( - file: ret.value, - fileName: filename, - contentType: 'application/octet-stream' - ) + render(ret) } - static class Download extends ClusterRequest { + static class Download extends ClusterRequest { String filename - - File doCall() { + Map doCall() { if (!availableFiles[filename]) throwUnavailable(filename) - return appContext.logReaderService.get(filename) + def file = appContext.logReaderService.get(filename) + [ + file: file.bytes, + fileName: filename, + contentType: 'application/octet-stream' + ] } } diff --git a/src/main/groovy/io/xh/hoist/cluster/ClusterRequest.groovy b/src/main/groovy/io/xh/hoist/cluster/ClusterRequest.groovy index 40e6a8bc..e63e2eca 100644 --- a/src/main/groovy/io/xh/hoist/cluster/ClusterRequest.groovy +++ b/src/main/groovy/io/xh/hoist/cluster/ClusterRequest.groovy @@ -1,5 +1,6 @@ package io.xh.hoist.cluster +import io.xh.hoist.exception.ClusterExecutionException import io.xh.hoist.exception.InstanceNotAvailableException import io.xh.hoist.log.LogSupport @@ -17,15 +18,17 @@ abstract class ClusterRequest implements Callable>, LogSup } return new ClusterResponse(value: doCall()) } catch (Throwable t) { + def simpleName = this.class.simpleName try { exceptionHandler.handleException( exception: t, logTo: this, - logMessage: [_action: this.class.simpleName] + logMessage: [_action: simpleName] ) } catch (Exception e) { - // Even logging failing -- just catch quietly and return neatly to calling member. + // Don't let logging ever prevent us from returning *actual* exception to caller. } + t = new ClusterExecutionException("Failed to execute ${simpleName}: ${t.message}", t) return new ClusterResponse(exception: t) } } diff --git a/src/main/groovy/io/xh/hoist/exception/ClusterExecutionException.groovy b/src/main/groovy/io/xh/hoist/exception/ClusterExecutionException.groovy new file mode 100644 index 00000000..1614fde7 --- /dev/null +++ b/src/main/groovy/io/xh/hoist/exception/ClusterExecutionException.groovy @@ -0,0 +1,24 @@ +/* + * This file belongs to Hoist, an application development toolkit + * developed by Extremely Heavy Industries (www.xh.io | info@xh.io) + * + * Copyright © 2023 Extremely Heavy Industries Inc. + */ + +package io.xh.hoist.exception + + +/** + * Wrapper Exception to be returned from a failed ClusterResponse. + * + * Exceptions are supposed to be serializable but can cause problems in practice, especially in + * Kryo. Note that we have already typically logged the actual exception on remote server. + */ +class ClusterExecutionException extends Exception { + String causeName + + ClusterExecutionException(String msg, Throwable t) { + super(msg) + causeName = t.class.simpleName + } +}