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),
which as of 6.3.1 answers from the UltiCloud protocol v2 message registry — so a module can never
silently take over command.execute or any other message type of the framework's protocol;
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 (<module>.<action>) is served by the protocol v2 module.request
handler, which looks the type up in this registry, not through a second dispatch mechanism
(01-CONTEXT D-10/D-11). The protocol v1 inbound dispatch that used to consult it was removed in 6.3.1.
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.
Result shapes
A responder answers with a future of anyJsonElement: an object, an array, a primitive,
or "no content". The future completing with Java null or with JsonNull means
"answered successfully, no content", and dispatch(java.lang.String, com.google.gson.JsonObject, java.lang.String) then settles with
JsonNull.INSTANCE; it never settles successfully with a Java null. Only a
responder that returns no future at all (null instead of a future), throws, or whose
future completes exceptionally fails the request. Over UltiCloud protocol v2 the result becomes
the data of an ok module.response unchanged, including data: null.
A responder written against 6.3.0, typed Function<JsonObject, CompletableFuture<JsonObject>>,
keeps compiling and behaving as before, except that completing with null is now a success
with no content rather than a failure.
Scheduling contract for responders
- Thread. A responder is called on one of this registry's own worker threads — never
the server main thread, and never the thread reading the panel connection, so a slow call
does not hold up other panel messages or the connection's heartbeat. A responder that uses
Bukkit API schedules that part onto the main thread itself (for example with
Bukkit.getScheduler().runTask) and completes its future from there. - Deadline. One deadline of
RESPONDER_TIMEOUT_MILLISstarts when the request is admitted (dispatch(java.lang.String, com.google.gson.JsonObject, java.lang.String)is called) and covers waiting for a worker, the call itself and the returned future. When it passes, the request fails with a timeout error; a result that arrives later is dropped and produces no second reply. - Bounds. At most
RESPONDER_WORKER_THREADScalls run at once and at mostRESPONDER_QUEUE_CAPACITYadmitted requests wait for a worker; a request beyond that is refused at once with an error reply naming its type and module. Known limitation: if the admitting thread stalls for longer than the deadline between starting a request's deadline and queueing the request, while every worker is blocked, the already-expired request is queued afterwards and holds one queue slot until a worker frees, so a later request can be refused as if the queue were full; no wrong reply is sent. - Concurrency. Responders may therefore run concurrently — up to
RESPONDER_WORKER_THREADSat once, including several calls of the same responder — and may finish out of request order, so a responder must be thread-safe. - No interruption. A call that blocks past its deadline keeps its worker until it returns; the framework never interrupts it, so uninterruptible database or disk I/O in module code is never cut off midway.
- Context. The call sees the context class loader of the thread that admitted the
request and this registry's registration scope (
beginRegistrationScope(com.ultikits.ultitools.abstracts.UltiToolsPlugin)) of that thread, and nothing else: no otherThreadLocal, no transaction and no audited-entity user is carried. The worker's own values are restored after every call, so nothing one module's call leaves behind reaches the next call. Worker threads are created lazily on the thread that admits a request (the panel connection's reader thread), so each copies that thread'sInheritableThreadLocalvalues once, at creation; the framework declares none. - Data. The responder receives its own copy of the request's
data, taken when the request is admitted; it may change that copy freely, and its changes reach neither the caller's object nor thePanelMessageEventpublished for the request. - Ownership. The responder registered when the request was admitted handles it, even if the type is reassigned or the owning module unloads before the call runs; a module unloading while one of its requests is still waiting or running can therefore see that call.
- Shutdown.
shutdown()settles every admitted request with a failure, runs no request that has not started when it begins (including one a worker takes from the queue while it is in progress) and interrupts no running call. - Callbacks. A callback attached to the future
dispatch(java.lang.String, com.google.gson.JsonObject, java.lang.String)returns (the framework's reply is one) runs on whichever thread settles that future — a worker, the deadline timer, a module's own thread or the thread callingshutdown()— or at once on the attaching thread when the future is already settled.
- Since:
- 6.3.0
-
Nested Class Summary
Nested ClassesModifier and TypeClassDescriptionprivate final classOne admitted request: the responder entry captured at admission, its deadline and its outcome.private static final classAn immutable pairing of a responder function with the module name that registered it and, when the registration named one, the module instance (#506). -
Field Summary
FieldsModifier and TypeFieldDescriptionprivate final Set<PanelResponderRegistry.Invocation> Every admitted request whose outcome has not settled yet;shutdown()settles them.private final longThe deadline, in milliseconds, every admitted request gets;RESPONDER_TIMEOUT_MILLISoutside tests.private static final Loggerprivate final intHow many admitted requests may wait for a free worker; named in a refusal.private final ThreadLocal<UltiToolsPlugin> The module instance the framework is loading on this thread, if any (#506): set bybeginRegistrationScope(UltiToolsPlugin)while a module's container refreshes and itsregisterSelf()runs, and recorded on every responder registered meanwhile without an explicit owner instance.(package private) static final intHow many admitted requests may wait for a free worker before a request is refused.(package private) static final longThe one deadline every admitted request gets (D-27), counted from admission — the momentdispatch(java.lang.String, com.google.gson.JsonObject, java.lang.String)is called — and covering the wait for a worker, the responder's call and its returned future.(package private) static final intHow many responder calls run at once, each on its own registry-owned worker thread.private final Map<String, PanelResponderRegistry.ResponderEntry> Pairs a responder function with the module name (and, when recorded, the module instance) that registered it.private booleanSet first thing inshutdown(); a request admitted afterwards fails at once, and a worker reading it at the start of a call settles that request instead of running it.private final ScheduledThreadPoolExecutorHolds only the deadline tasks: each admitted request's deadline is scheduled here the momentdispatch(java.lang.String, com.google.gson.JsonObject, java.lang.String)admits it.private static final longHow long an idle worker thread is kept before it ends, so an idle registry holds no worker.private final ThreadPoolExecutorRuns responder calls, separate fromtimeoutSchedulerso a blocked call can never delay a deadline. -
Constructor Summary
ConstructorsConstructorDescriptionCreates a registry with the shipped deadline (RESPONDER_TIMEOUT_MILLIS) and worker bounds (RESPONDER_WORKER_THREADS,RESPONDER_QUEUE_CAPACITY).PanelResponderRegistry(long deadlineMillis, int workerThreads, int queueCapacity) Test seam: a registry with its own deadline and worker bounds. -
Method Summary
Modifier and TypeMethodDescriptionvoidAttributes every responder registered on this thread, untilendRegistrationScope(), toownerwhen the registration names no owner instance itself (#506).private static ScheduledThreadPoolExecutorprivate static ThreadPoolExecutorcreateWorkers(int workerThreads, int queueCapacity) CompletableFuture<com.google.gson.JsonElement> Admits a request formessageType's registered responder and returns its outcome at once, without waiting for the responder: admission captures the responder registered now (and its owner), a copy ofdata, the calling thread's context class loader and this registry's registration scope on the calling thread, starts the request's one deadline ofRESPONDER_TIMEOUT_MILLIS, and queues the call for a worker thread.CompletableFuture<com.google.gson.JsonElement> dispatch(String, JsonObject, String), also telling the caller when the module's work for the request has ended (22-23 second verification):workEndedruns exactly once, after the responder call returned and the future it returned completed, or as soon as the request settles without the responder having started (no responder, the queue full, the registry shut down, the deadline or a cancel while it waited).voidEnds the scope started bybeginRegistrationScope(UltiToolsPlugin)on this thread.booleanhasResponder(String messageType) Whether a responder is currently registered for the exact stringmessageType.private static StringThe identifier of an entry's module; seemoduleIdentifierOf(String).(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) booleanTest-only accessor for whetherworkershas been shut down.private static UltiToolsPluginlistedCopyNamed(String moduleName) The copy listed as loaded undermoduleName, which a registration made by name outside every load scope belongs to (#562 item 1);nullbefore the framework is running.moduleIdentifierOf(String ownerModule) The UltiCloud module identifier of the module that owns responders underownerModule(22-26 fix round): the identify-string of the recorded module instance, or of the copy listed as loaded under that name, lower-cased (ModuleIds.of(UltiToolsPlugin)); when neither gives one, the owner name itself through the same derivation, as in 6.3.0.moduleRequestType(String moduleIdentifier, String action) The registered message type a UltiCloudmodule.requestformoduleIdentifierandactionis dispatched to (22-26 fix round), ornullwhen there is none: the type<moduleIdentifier>.<action>, or<owner name lower-cased>.<action>as 6.3.0 registered it, and only when the module owning that responder hasmoduleIdentifieras its identifier (moduleIdentifierOf(String)).ownerModuleOf(String messageType) The module name owning the responder registered for the exact stringmessageType, as the registration recorded it.The distinct module names owning at least one registered responder, in ascending name order.(package private) intTest-only accessor for how many timeout tasks are still queued.(package private) intTest-only accessor for how many admitted invocations are waiting for a free worker.voidregisterResponder(String messageType, Function<com.google.gson.JsonObject, ? extends CompletableFuture<? extends com.google.gson.JsonElement>> responder, String ownerModule) Registersresponderas the single owner ofmessageType.voidregisterResponder(String messageType, Function<com.google.gson.JsonObject, ? extends CompletableFuture<? extends com.google.gson.JsonElement>> responder, String ownerModule, UltiToolsPlugin ownerInstance) Same asregisterResponder(String, Function, String), and additionally records the module instance that owns the responder (#506), sounregisterByOwnerInstance(UltiToolsPlugin)releases exactly that instance's responders whatever name they were filed under.releaseForSupersede(UltiToolsPlugin ownerInstance) Releases every responder recorded againstownerInstanceand returns the action that gives them back (#562).private voidrestore(Map<String, PanelResponderRegistry.ResponderEntry> released) private static voidrunQuietly(Runnable hook) voidshutdown()Shuts this registry down (WR-01, 06-REVIEW.md): stops the deadline timer and the workers and settles every admitted request.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.voidunregisterByOwnerInstance(UltiToolsPlugin ownerInstance) Removes every responder registered withownerInstanceas its owner (#506), whatever module name it was filed under.
-
Field Details
-
LOGGER
-
RESPONDER_TIMEOUT_MILLIS
static final long RESPONDER_TIMEOUT_MILLISThe one deadline every admitted request gets (D-27), counted from admission — the momentdispatch(java.lang.String, com.google.gson.JsonObject, java.lang.String)is called — and covering the wait for a worker, the responder's call and its returned future. One deadline, applied in exactly one place, 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. A call still running when the deadline passes is not interrupted; its later result is dropped. Package-private (not private) so tests can bound their own waits without duplicating this value.- See Also:
-
RESPONDER_WORKER_THREADS
static final int RESPONDER_WORKER_THREADSHow many responder calls run at once, each on its own registry-owned worker thread.- See Also:
-
RESPONDER_QUEUE_CAPACITY
static final int RESPONDER_QUEUE_CAPACITYHow many admitted requests may wait for a free worker before a request is refused.- See Also:
-
WORKER_KEEP_ALIVE_SECONDS
private static final long WORKER_KEEP_ALIVE_SECONDSHow long an idle worker thread is kept before it ends, so an idle registry holds no worker.- See Also:
-
responders
Pairs a responder function with the module name (and, when recorded, the module instance) that registered it. Never exposed outside this class —hasResponder(String),unregisterAll(String),unregisterByOwnerInstance(UltiToolsPlugin)andreleaseForSupersede(UltiToolsPlugin)are the only externally visible views of what this map holds. -
timeoutScheduler
Holds only the deadline tasks: each admitted request's deadline is scheduled here the momentdispatch(java.lang.String, com.google.gson.JsonObject, java.lang.String)admits it. A dedicated single-thread pool, separate fromworkers, so a responder call that blocks can never delay a deadline.setRemoveOnCancelPolicy(true)makes a cancelled deadline — the fast path, when the request settles before its deadline — leave the queue synchronously insidecancel(), rather than lingering until its scheduled time. There is noCompletableFuture.orTimeouton the Java 8 bytecode target this framework compiles to (that method is Java 9+), which is why the deadline is hand-scheduled here rather than chained. -
deadlineMillis
private final long deadlineMillisThe deadline, in milliseconds, every admitted request gets;RESPONDER_TIMEOUT_MILLISoutside tests. -
workers
Runs responder calls, separate fromtimeoutSchedulerso a blocked call can never delay a deadline. Bounded threads, a bounded queue and an abort policy; invocations are handed over withexecute, neversubmit, so a deadline can take a not-yet-started invocation back out of the queue withThreadPoolExecutor.remove(Runnable). -
queueCapacity
private final int queueCapacityHow many admitted requests may wait for a free worker; named in a refusal. -
admitted
Every admitted request whose outcome has not settled yet;shutdown()settles them. An invocation leaves this set through the completion hook registered at admission. -
shutDown
private volatile boolean shutDownSet first thing inshutdown(); a request admitted afterwards fails at once, and a worker reading it at the start of a call settles that request instead of running it. -
registrationScopeOwner
The module instance the framework is loading on this thread, if any (#506): set bybeginRegistrationScope(UltiToolsPlugin)while a module's container refreshes and itsregisterSelf()runs, and recorded on every responder registered meanwhile without an explicit owner instance.
-
-
Constructor Details
-
PanelResponderRegistry
public PanelResponderRegistry()Creates a registry with the shipped deadline (RESPONDER_TIMEOUT_MILLIS) and worker bounds (RESPONDER_WORKER_THREADS,RESPONDER_QUEUE_CAPACITY). -
PanelResponderRegistry
PanelResponderRegistry(long deadlineMillis, int workerThreads, int queueCapacity) Test seam: a registry with its own deadline and worker bounds.- Parameters:
deadlineMillis- the deadline every admitted request gets, in millisecondsworkerThreads- how many responder calls run at oncequeueCapacity- how many admitted requests may wait for a free worker
-
-
Method Details
-
createTimeoutScheduler
-
createWorkers
-
registerResponder
public void registerResponder(String messageType, Function<com.google.gson.JsonObject, ? extends CompletableFuture<? extends com.google.gson.JsonElement>> 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 attemptingcommand.executeis 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.responderis called on one of this registry's worker threads, never the server main thread, within one deadline ofRESPONDER_TIMEOUT_MILLISfrom admission, and possibly concurrently with itself and other responders (up toRESPONDER_WORKER_THREADScalls at once, finishing out of request order), so it must be thread-safe; Bukkit API work belongs on the main thread, scheduled by the responder itself. See the class documentation for the full scheduling contract.UltiCloud protocol v2 module types
A module that answers UltiCloud protocol v2
module.requestmessages registers its responder under<prefix>.<action>, where the prefix is either the module's own name lower-cased (as in 6.3.0) or its UltiCloud module identifier. UltiCloud addresses the module by its identifier: itsidentify-string, lower-case (ModuleIds), which for some modules differs from the name. The v2 bridge takesmoduleandactionfrom the request payload and dispatches only to a responder of the module whose identifier equalsmoduleand registered under<module>.<action>or<its name lower-cased>.<action>; any other request is answeredmodule_not_foundand never dispatched (D-25, contract answer 11). An action therefore needs no namespace of its own: the module is the namespace. The identifier is taken from the recorded module instance (seeownerModule); a responder with no instance recorded or listed, or whose module declares no usable identify-string, keeps the 6.3.0 behaviour: its owner name lower-cased stands for the identifier.- 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 reply: anyJsonElement(object, array or primitive), ornull/JsonNullfor "answered, no content"; see "Result shapes" in the class documentation. Returningnullinstead of a future is a failureownerModule- the name of the module registering this responder. The responder is recorded against a module instance (#506, #562 item 1): the module the framework is loading on this thread, if any; otherwise the copy listed as loaded underownerModule(the first listed when more than one is); otherwise none, and it is filed under the name only. PreferregisterResponder(String, Function, String, UltiToolsPlugin)when the module instance is at hand- Throws:
IllegalArgumentException- ifmessageTypeorownerModuleisnull/empty, orresponderisnullPluginModuleException- if the framework already ownsmessageType, or another registration (including the same module's own prior registration) already claims it
-
registerResponder
public void registerResponder(String messageType, Function<com.google.gson.JsonObject, ? extends CompletableFuture<? extends com.google.gson.JsonElement>> responder, String ownerModule, UltiToolsPlugin ownerInstance) Same asregisterResponder(String, Function, String), and additionally records the module instance that owns the responder (#506), sounregisterByOwnerInstance(UltiToolsPlugin)releases exactly that instance's responders whatever name they were filed under. Two copies of one module share a name; only the instance tells a superseded copy's responders from its replacement's.responderis called on one of this registry's worker threads under the same scheduling contract asregisterResponder(String, Function, String): a deadline from admission, possibly concurrent calls finishing out of order, so it must be thread-safe.- Parameters:
messageType- the exact message type string this responder will serveresponder- the responder function; its result shapes are those ofregisterResponder(String, Function, String)ownerModule- the name of the module registering this responderownerInstance- the module instance registering it;nullrecords the module the framework is loading on this thread, if any (seebeginRegistrationScope(UltiToolsPlugin)), otherwise the copy listed as loaded underownerModule(PluginManager.findRegistrationOwner(String)), which is exactlyregisterResponder(String, Function, String)- Throws:
IllegalArgumentException- ifmessageTypeorownerModuleisnull/empty, orresponderisnullPluginModuleException- if the framework already ownsmessageType, or another registration already claims it- Since:
- 6.3.0
-
listedCopyNamed
The copy listed as loaded undermoduleName, which a registration made by name outside every load scope belongs to (#562 item 1);nullbefore the framework is running. -
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
-
unregisterByOwnerInstance
Removes every responder registered withownerInstanceas its owner (#506), whatever module name it was filed under. A responder registered with a name only is not matched here;unregisterAll(String)releases those.- Parameters:
ownerInstance- the module instance being unloaded;nullis a no-op- Since:
- 6.3.0
-
releaseForSupersede
Releases every responder recorded againstownerInstanceand returns the action that gives them back (#562).PluginManagercalls this for a loaded copy of a module just before a newer copy of the same module builds its container and runsregisterSelf(), so the newer copy can claim the message types the older copy held: while both copies exist, a type belongs to exactly one of them, and the older copy holds none of its types while the newer copy registers. When the newer copy fails to load, the framework first releases what the failed copy registered and then runs the returned action, which puts each released responder back under its type, unchanged; when the newer copy loads, the action is dropped and the older copy is unloaded. Running the action more than once restores nothing more.Each responder is removed only while it is still the exact registration this call matched, and put back only while its type is free: a type another registration claimed in between keeps that registration, and one WARNING names the type and the module that lost it -- a copy never takes over, or keeps, a type another copy holds. Intended for
PluginManager, not for module authors.- Parameters:
ownerInstance- the loaded copy being superseded;nullreleases nothing- Returns:
- the action that restores what this call released
- Since:
- 6.3.0
-
restore
-
beginRegistrationScope
Attributes every responder registered on this thread, untilendRegistrationScope(), toownerwhen the registration names no owner instance itself (#506). The framework opens this scope while it loads a module -- around its container refresh and itsregisterSelf()-- so a responder a module registers while it loads, throughregisterResponder(String, Function, String), is released with that module instance. Intended forPluginManager, not for module authors. Scopes do not nest.- Parameters:
owner- the module instance being loaded- Since:
- 6.3.0
-
endRegistrationScope
public void endRegistrationScope()Ends the scope started bybeginRegistrationScope(UltiToolsPlugin)on this thread.- Since:
- 6.3.0
-
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
-
ownerModuleOf
The module name owning the responder registered for the exact stringmessageType, as the registration recorded it. A read-only view of the ownership a dispatch of this type would use, so a caller can decide whether a request may reach it before admitting it.- Parameters:
messageType- the message type to look up, possiblynull- Returns:
- the owner module name, or
nullwhen no responder owns this exact type - Since:
- 6.3.1
-
moduleIdentifierOf
The UltiCloud module identifier of the module that owns responders underownerModule(22-26 fix round): the identify-string of the recorded module instance, or of the copy listed as loaded under that name, lower-cased (ModuleIds.of(UltiToolsPlugin)); when neither gives one, the owner name itself through the same derivation, as in 6.3.0. Internal, not for module use.- Parameters:
ownerModule- an owner module name asownerModules()lists it- Returns:
- the identifier, or
nullwhen the module owns no responder or none can be derived - Since:
- 6.3.1
-
moduleRequestType
The registered message type a UltiCloudmodule.requestformoduleIdentifierandactionis dispatched to (22-26 fix round), ornullwhen there is none: the type<moduleIdentifier>.<action>, or<owner name lower-cased>.<action>as 6.3.0 registered it, and only when the module owning that responder hasmoduleIdentifieras its identifier (moduleIdentifierOf(String)). The identifier form wins when both exist. Compared lower-case. Internal, not for module use.- Parameters:
moduleIdentifier- the request'smoduleaction- the request'saction- Returns:
- the type to dispatch, or
nullwhen no responder of that module serves the action - Since:
- 6.3.1
-
identifierOf
The identifier of an entry's module; seemoduleIdentifierOf(String). -
ownerModules
The distinct module names owning at least one registered responder, in ascending name order. A read-only snapshot: later registrations and removals do not change the returned list. One name appears once however many types the module owns, and a module with no responder at the moment of the call does not appear at all.- Returns:
- an unmodifiable list of owner module names; empty when no responder is registered
- Since:
- 6.3.1
-
dispatch
public CompletableFuture<com.google.gson.JsonElement> dispatch(String messageType, com.google.gson.JsonObject data, String requestId) Admits a request formessageType's registered responder and returns its outcome at once, without waiting for the responder: admission captures the responder registered now (and its owner), a copy ofdata, the calling thread's context class loader and this registry's registration scope on the calling thread, starts the request's one deadline ofRESPONDER_TIMEOUT_MILLIS, and queues the call for a worker thread. The returned future settles exactly once — the single reply-shape point every outcome routes through (D-27):- the responder's future completes with a
JsonElementother thanJsonNull— that value, whatever its shape (object, array or primitive); - the responder's future completes with
nullorJsonNull—JsonNull.INSTANCE, a success with no content; - the responder throws, including a checked exception thrown sneakily or a linkage error from a closed module class loader — that throwable;
- the responder returns
nullinstead of a future — an explicitIllegalStateExceptionnaming the type and owning module; - the responder's future completes exceptionally — that exception, relayed;
- the deadline passes while the request waits, runs or awaits its future —
PluginModuleException.responderTimedOut(String, String, long)naming the owner admitted with the request; a request still waiting is taken out of the queue and never runs, and a later result is dropped; RESPONDER_WORKER_THREADScalls are running andRESPONDER_QUEUE_CAPACITYrequests are waiting — at once, aRejectedExecutionExceptionnaming the type, the owning module and the bound;- the registry is shut down, before or after admission — a
RejectedExecutionExceptionstating so; - the caller cancels the returned future — cancelled; a request still waiting leaves the queue and never runs.
- Parameters:
messageType- the message type to dispatch, expected to already have a registered responder (hasResponder(String))data- the inbound message'sdataobject, possiblynull; the responder receives a copy taken when this method is called, so its changes never reachdatarequestId- 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 settles — successfully with the responder's result, never a
Java
null(JsonNull.INSTANCEfor "no content"), or exceptionally with a failure describing what went wrong
- the responder's future completes with a
-
dispatch
@Internal public CompletableFuture<com.google.gson.JsonElement> dispatch(String messageType, com.google.gson.JsonObject data, String requestId, Runnable workEnded) dispatch(String, JsonObject, String), also telling the caller when the module's work for the request has ended (22-23 second verification):workEndedruns exactly once, after the responder call returned and the future it returned completed, or as soon as the request settles without the responder having started (no responder, the queue full, the registry shut down, the deadline or a cancel while it waited). Unlike the returned future it does not run at the deadline while the responder is still running. It never runs for a responder whose future never completes. It runs on whichever thread ends the work; aworkEndedthat throws is ignored. Internal, not for module use.- Parameters:
messageType- seedispatch(String, JsonObject, String)data- seedispatch(String, JsonObject, String)requestId- seedispatch(String, JsonObject, String)workEnded- runs once when the module's work for this request has ended;nullfor none- Returns:
- see
dispatch(String, JsonObject, String) - Since:
- 6.3.1
-
runQuietly
-
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
-
isWorkerPoolShutdownForTesting
boolean isWorkerPoolShutdownForTesting()Test-only accessor for whetherworkershas been shut down.- Returns:
trueif the worker executor has been shut down
-
queuedInvocationCountForTesting
int queuedInvocationCountForTesting()Test-only accessor for how many admitted invocations are waiting for a free worker.- Returns:
- the number of queued, not-yet-started invocations
-
shutdown
public void shutdown()Shuts this registry down (WR-01, 06-REVIEW.md): stops the deadline timer and the workers and settles every admitted request.Both executors are owned by this instance — nothing else shares or stops them — so without this call a plugin
/reload(which constructs a freshPanelResponderRegistryon everyonEnable) would leak their threads for the life of the process. The deadline timer is stopped withScheduledThreadPoolExecutor.shutdownNow(): it holds only deadline tasks, and every request they belong to is settled here anyway. Requests still waiting for a worker are taken out of the queue and never run; a request a worker takes after this method began, before the queue is emptied, is settled with the same failure by that worker instead of being run. The workers are stopped with the gracefulThreadPoolExecutor.shutdown(), nevershutdownNow(): a running module call is not interrupted, it returns on its own daemon thread and its result is dropped. Every admitted request not yet settled fails with aRejectedExecutionExceptionstating that the registry shut down before its responder completed, and a request admitted afterwards fails the same way at once.
-