Class PanelResponderRegistry
A module owns at most one responder per message type. Ownership is decided against exactly one
source of truth for the framework's own types — PluginInitiationUtils.isFrameworkOwnedType(java.lang.String)
— so a module can never silently take over execute_command or any of the framework's other
23 inbound message types; that check runs before the module-owner check, and both refusals throw a
typed PluginModuleException naming the offender rather than a raw RuntimeException.
A module-owned type is served from the exact same lookup PluginInitiationUtils
#handleInboundMessage already uses for the framework's own 24 types — its unknown-type branch
consults this registry, not a second dispatch mechanism (01-CONTEXT D-10/D-11).
Deliberately does not require a <module>:<type> namespace prefix — D-26 rejected that as a
cross-repository protocol convention the panel would also have to honour, rather than an in-repo
check the framework can enforce alone.
- Since:
- 6.3.0
-
Nested Class Summary
Nested ClassesModifier and TypeClassDescriptionprivate static final classAn immutable pairing of a responder function and the module name that registered it. -
Field Summary
FieldsModifier and TypeFieldDescription(package private) static final longThe bounded wall-clock timedispatch(java.lang.String, com.google.gson.JsonObject, java.lang.String)gives a registered responder's future to complete before completing exceptionally on the caller's behalf (D-27).private final Map<String, PanelResponderRegistry.ResponderEntry> Pairs a responder function with the module name that registered it.private final ScheduledThreadPoolExecutorSchedulesRESPONDER_TIMEOUT_MILLIS's one-timeout-in-one-place enforcement. -
Constructor Summary
Constructors -
Method Summary
Modifier and TypeMethodDescriptionprivate static ScheduledThreadPoolExecutorCompletableFuture<com.google.gson.JsonObject> InvokesmessageType's registered responder and returns a future that is already bounded byRESPONDER_TIMEOUT_MILLIS— the single timeout-and-reply-shape point every failure mode routes through (D-27): the responder throws synchronously, before returning a future — caught and becomes a failed future rather than escaping; the responder returnsnull— treated as an explicit failure, not aNullPointerException; the responder's future never completes — the scheduled timeout task completes this method's returned future exceptionally withPluginModuleException.responderTimedOut(String, String, long); the responder's future completes exceptionally — that exception is relayed directly. On the fast path (the responder's future is already complete, or completes before the timeout fires), the scheduled timeout task is cancelled inside the samewhenCompletecallback that resolves the returned future, so nothing is left pending.booleanhasResponder(String messageType) Whether a responder is currently registered for the exact stringmessageType.(package private) booleanTest-only accessor for whethertimeoutSchedulerhas been shut down — provesshutdown()actually stops the dedicated timeout thread (WR-01), rather than merely proving the method exists and does not throw.(package private) intTest-only accessor for how many timeout tasks are still queued.voidregisterResponder(String messageType, Function<com.google.gson.JsonObject, CompletableFuture<com.google.gson.JsonObject>> responder, String ownerModule) Registersresponderas the single owner ofmessageType.voidshutdown()Shuts downtimeoutScheduler(WR-01, 06-REVIEW.md).voidunregisterAll(String moduleName) Removes every respondermoduleNameowns — the mirror ofEventBus.unregisterAll(String)'s one-String-parameter void signature and iterate-and-remove-by-owner shape, placed adjacent to the two existingeventBus.unregisterAll(...)call sites inPluginManagerrather than a new lifecycle mechanism.
-
Field Details
-
RESPONDER_TIMEOUT_MILLIS
static final long RESPONDER_TIMEOUT_MILLISThe bounded wall-clock timedispatch(java.lang.String, com.google.gson.JsonObject, java.lang.String)gives a registered responder's future to complete before completing exceptionally on the caller's behalf (D-27). One timeout, applied in exactly one place — insidedispatch— so no responder and no caller implements its own. 3 seconds: long enough for a responder doing real database or disk work, short enough that a human operator watching the panel does not start to think it froze. Package-private (not private) soPanelResponderRegistryTestcan bound its own wait without duplicating this value.- See Also:
-
responders
Pairs a responder function with the module name that registered it. Never exposed outside this class —hasResponder(String)/unregisterAll(String)are the only externally visible views of what this map holds. -
timeoutScheduler
SchedulesRESPONDER_TIMEOUT_MILLIS's one-timeout-in-one-place enforcement. A dedicated single-thread pool, not a shared framework scheduler, so one slow responder cannot starve unrelated timeout tasks.setRemoveOnCancelPolicy(true)makes a cancelled task — the fast path, when the responder completes before the timeout fires — removed from the queue synchronously insidecancel(), rather than lingering in the queue until its scheduled time; without it a fast responder would still "leak" a queued task forRESPONDER_TIMEOUT_MILLIS. There is noCompletableFuture.orTimeouton the Java 8 bytecode target this framework compiles to (that method is Java 9+), which is why the timeout is hand-scheduled here rather than chained.
-
-
Constructor Details
-
PanelResponderRegistry
public PanelResponderRegistry()
-
-
Method Details
-
createTimeoutScheduler
-
registerResponder
public void registerResponder(String messageType, Function<com.google.gson.JsonObject, CompletableFuture<com.google.gson.JsonObject>> responder, String ownerModule) Registersresponderas the single owner ofmessageType.Validates all three arguments first. Then checks
PluginInitiationUtils.isFrameworkOwnedType(String)before checking module ownership — deliberately in that order, so a module attemptingexecute_commandis told the framework owns it, not that some other module does. Finally uses an atomicputIfAbsent-shaped check so a collision — with the framework, with another module, or with the same module registering twice — always throws rather than silently displacing an existing registration.- Parameters:
messageType- the exact message type string this responder will serve, matched byString.equals— no case folding, no Unicode normalizationresponder- the responder function, invoked with the inbound message'sdataand returning aCompletableFuturethat resolves the replyownerModule- the name of the module registering this responder- Throws:
IllegalArgumentException- ifmessageTypeorownerModuleisnull/empty, orresponderisnullPluginModuleException- if the framework already ownsmessageType, or another registration (including the same module's own prior registration) already claims it
-
unregisterAll
Removes every respondermoduleNameowns — the mirror ofEventBus.unregisterAll(String)'s one-String-parameter void signature and iterate-and-remove-by-owner shape, placed adjacent to the two existingeventBus.unregisterAll(...)call sites inPluginManagerrather than a new lifecycle mechanism.A module that registered nothing is a no-op: nothing is removed and nothing is thrown.
moduleName == nullis also a no-op.- Parameters:
moduleName- the module whose responders should be removed, possiblynull
-
hasResponder
Whether a responder is currently registered for the exact stringmessageType.- Parameters:
messageType- the message type to check, possiblynull- Returns:
trueif a responder owns this exact type
-
dispatch
public CompletableFuture<com.google.gson.JsonObject> dispatch(String messageType, com.google.gson.JsonObject data, String requestId) InvokesmessageType's registered responder and returns a future that is already bounded byRESPONDER_TIMEOUT_MILLIS— the single timeout-and-reply-shape point every failure mode routes through (D-27):- the responder throws synchronously, before returning a future — caught and becomes a failed future rather than escaping;
- the responder returns
null— treated as an explicit failure, not aNullPointerException; - the responder's future never completes — the scheduled timeout task completes this
method's returned future exceptionally with
PluginModuleException.responderTimedOut(String, String, long); - the responder's future completes exceptionally — that exception is relayed directly.
whenCompletecallback that resolves the returned future, so nothing is left pending.- Parameters:
messageType- the message type to dispatch, expected to already have a registered responder (hasResponder(String))data- the inbound message'sdataobject, passed straight to the responderrequestId- the request's correlation id — not used bydispatchitself, but accepted so the call site does not have to separately track it; reserved for the caller assembling the outbound reply- Returns:
- a future that always completes — successfully with the responder's result, or exceptionally with a failure describing what went wrong
-
pendingTimeoutTaskCountForTesting
int pendingTimeoutTaskCountForTesting()Test-only accessor for how many timeout tasks are still queued. Proves the fast path indispatch(java.lang.String, com.google.gson.JsonObject, java.lang.String)leaves nothing pending, rather than merely proving a reply arrived.- Returns:
- the number of scheduled-but-not-yet-run-or-cancelled timeout tasks
-
isTimeoutSchedulerShutdownForTesting
boolean isTimeoutSchedulerShutdownForTesting()Test-only accessor for whethertimeoutSchedulerhas been shut down — provesshutdown()actually stops the dedicated timeout thread (WR-01), rather than merely proving the method exists and does not throw.- Returns:
trueiftimeoutSchedulerhas been shut down
-
shutdown
public void shutdown()Shuts downtimeoutScheduler(WR-01, 06-REVIEW.md).timeoutScheduleris a dedicated single-thread pool per instance — nothing else shares it and nothing else stops it, so without this call a plugin/reload(which constructs a freshPanelResponderRegistryon everyonEnablewithout ever disposing of the previous one) leaks one more daemonUltiTools-PanelResponderRegistry-Timeoutthread per cycle for the life of the process.ScheduledThreadPoolExecutor.shutdownNow(), not the gracefulshutdown(), because any timeout task still queued at this point belongs to a responder dispatch this instance is being torn down anyway — there is no in-flight work here worth draining.
-