Class UltiPanelLogTransmitter
- Version:
- 1.0.0
- Author:
- UltiKits
-
Field Summary
FieldsModifier and TypeFieldDescriptionprivate booleanprivate final ObjectGuards the batch-vs-immediate DISPATCH DECISION insendLog(java.lang.String, java.lang.String, java.lang.String, java.lang.Throwable)against the enabled-to-disabled TRANSITION insetBatchEnabled(boolean)(Gate-2 finding, round 6).private final ScheduledExecutorServiceprivate ScheduledFuture<?> The currently-scheduled batch-send task, ornullwhile batching is disabled.private intprivate ObjectExternal coordination lock forflushLogs()(Gate-2 finding, round 9).private final AtomicBooleanprivate RunnableInvoked fromaddToBatch(JsonObject)whenever the queue reachesbatchSizeWHILEexternalDrainModeis active (Gate-2 finding, round 6).private intprivate final ConcurrentLinkedQueue<com.google.gson.JsonObject> private final AtomicBooleanprivate static final int(package private) static final intShared lower bound for the batch-send interval (#432/WR-01).private final Stringprivate final UltiPanelWebSocketClient -
Constructor Summary
ConstructorsConstructorDescriptionUltiPanelLogTransmitter(UltiPanelWebSocketClient webSocketClient, String serverId) Constructor. -
Method Summary
Modifier and TypeMethodDescriptionprivate voidaddToBatch(com.google.gson.JsonObject logData) Adds a log entry to the batch queue.voidprivate StringdetermineLoggerName(String source) Determines the logger name.private voidcom.google.gson.JsonArraydrainQueue(int maxItems) Drains log entries from the queue, returning them as a JsonArray.voidvoidImmediately sends every log currently in the queue.private StringGets the default server ID.intGets the current queue size.private StringgetStackTrace(Throwable throwable) Gets the exception stack trace as a string.voidConvenience methods: send a log at a specific level.booleanChecks whether external drain mode is active.booleanChecks whether log transmission is enabled.private voidSends the batched logs.voidSends a log message to the backend.private voidsendLogImmediately(com.google.gson.JsonObject logData) Sends a single log entry immediately.voidsetBatchEnabled(boolean batchEnabled) Enables or disables batched delivery, and -- unlike the field it replaces as a plain Lombok setter -- actually starts or stops the scheduled batch-send task to match (#432): disabling cancels it outright rather than leaving it running with nothing useful to flush; re-enabling starts a fresh one at the currentintervalMs.voidsetBatchSize(int batchSize) Sets the batch send-threshold size, rejecting anything below 1 (Gate-2 finding).voidSets the lockflushLogs()coordinates disable-time flushes against (Gate-2 finding, round 9).voidsetExternalDrainMode(boolean enabled) Sets external drain mode.voidsetExternalSizeThresholdCallback(Runnable callback) Sets the callback invoked when the queue reachesbatchSizewhile external drain mode is active (Gate-2 finding, round 6).voidsetIntervalMs(int intervalMs) Sets the batch-send interval, in milliseconds, and -- unlike the field it replaces as a plain Lombok setter -- reschedules the already-running batch-send task to actually use it (#432).voidsetLogTransmissionEnabled(boolean enabled) Enables/disables log transmission.voidshutdown()Shuts down the log transmitter.private void(Re)starts the batch-send task at the currentintervalMs.private voidCancels the currently-scheduled batch-send task, if any, and clears the reference.void
-
Field Details
-
MAX_QUEUE_SIZE
private static final int MAX_QUEUE_SIZE- See Also:
-
MIN_INTERVAL_MS
static final int MIN_INTERVAL_MSShared lower bound for the batch-send interval (#432/WR-01). Before this fix, the boot-time path (LogStreamManager.loadBatchConfiguration()) clamped to 1000ms via its own inlineMath.max(1000, interval), while this class's ownsetIntervalMs(int)-- reachable live, over the panel's WebSocketconfigaction -- only rejected a non-positive value, leaving every value from 1ms up accepted. A panel operator (or a compromised/buggy panel session) could drive the batch scheduler to a roughly 1000-sends/second cadence indefinitely. Both entry points now share this one constant instead of the live path being silently more permissive than the boot path.- See Also:
-
webSocketClient
-
serverId
-
logTransmissionEnabled
-
batchModeLock
Guards the batch-vs-immediate DISPATCH DECISION insendLog(java.lang.String, java.lang.String, java.lang.String, java.lang.Throwable)against the enabled-to-disabled TRANSITION insetBatchEnabled(boolean)(Gate-2 finding, round 6). Before this lock,setBatchEnabled(false)wrotethis.batchEnabled = falsebeforeflushLogs()drained the existing queue, so a concurrentsendLog()call could read the newfalsevalue and send a NEWER record immediately whileflushLogs()was still draining OLDER queued records -- the newer record then arrived at the panel before the older ones, breaking FIFO order. Holding this lock across bothsendLog's dispatch decision and the WHOLE disable transition means everysendLogcall observes the transition as atomic: either entirely before it (queued, and included in the flush that is about to run) or entirely after it (flag already false, sent immediately) -- never during. -
externalDrainMode
-
externalSizeThresholdCallback
Invoked fromaddToBatch(JsonObject)whenever the queue reachesbatchSizeWHILEexternalDrainModeis active (Gate-2 finding, round 6). Without this, reaching the size threshold under external drain mode had no effect at all --sendBatch()'s own early-return for external drain mode silently swallowed the threshold crossing that would otherwise have triggered an immediate send, so a documented size threshold never actually shortened delivery latency while monitoring was active, and a sustained burst could fillMAX_QUEUE_SIZEand start discarding old entries despite repeatedly crossing the threshold.nullby default (no external owner wired up);LogStreamManager.initialize(com.ultikits.ultitools.websocket.UltiPanelWebSocketClient)sets it alongsidesetExternalDrainMode(boolean). -
externalDrainCoordinationLock
External coordination lock forflushLogs()(Gate-2 finding, round 9).nullby default (no external owner wired up --flushLogs()runs unsynchronized, its original behaviour).LogStreamManager.initialize(com.ultikits.ultitools.websocket.UltiPanelWebSocketClient)wires this toServerMonitorManager's OWNlogDrainLock(the same objectServerMonitorManager.drainAndSendLogsOnly(com.ultikits.ultitools.manager.UltiPanelLogTransmitter)and its inlinesendBatchUpdatedrain already synchronize on) alongsidesetExternalSizeThresholdCallback(java.lang.Runnable). Without this, a livebatchConfig.enabled: falseupdate's flush ran entirely outside that lock, so it could still send NEWER records ahead of OLDER ones the monitor had already polled but not yet sent, even after round 7's monitor-side-only serialization. -
batchEnabled
private volatile boolean batchEnabled -
batchSize
private volatile int batchSize -
intervalMs
private volatile int intervalMs -
logQueue
-
batchScheduler
-
batchSenderTask
The currently-scheduled batch-send task, ornullwhile batching is disabled.Tracked so
setIntervalMs(int)andsetBatchEnabled(boolean)can cancel and resubmit it -- before #432, the interval was baked into the onescheduleWithFixedDelaycall the constructor made, and the setter only mutated the field without ever touching the already-running task.
-
-
Constructor Details
-
UltiPanelLogTransmitter
Constructor.- Parameters:
webSocketClient- the WebSocket clientserverId- the server ID
-
-
Method Details
-
sendLog
Sends a log message to the backend.- Parameters:
level- the log level (info, warning, error, debug)message- the log messagesource- the log source (e.g.: "server", "plugin:name")throwable- the exception object (optional)
-
info
Convenience methods: send a log at a specific level. -
warning
-
error
-
debug
-
sendLogImmediately
private void sendLogImmediately(com.google.gson.JsonObject logData) Sends a single log entry immediately. -
addToBatch
private void addToBatch(com.google.gson.JsonObject logData) Adds a log entry to the batch queue. -
startBatchSender
private void startBatchSender()(Re)starts the batch-send task at the currentintervalMs. Cancels whatever task was previously scheduled first, so this is safe to call to both start fresh and reschedule (#432). -
stopBatchSender
private void stopBatchSender()Cancels the currently-scheduled batch-send task, if any, and clears the reference. -
setIntervalMs
public void setIntervalMs(int intervalMs) Sets the batch-send interval, in milliseconds, and -- unlike the field it replaces as a plain Lombok setter -- reschedules the already-running batch-send task to actually use it (#432). A no-op, deliberately not touching the scheduler, when the requested value equals the current one, so a live-config-update round-trip that resends the same value does not churn the scheduler. Has no effect on the scheduler while batching is disabled (there is no running task to reschedule); the new value still takes effect the next time batching is enabled.- Parameters:
intervalMs- the new interval; must be at leastMIN_INTERVAL_MS- Throws:
IllegalArgumentException- ifintervalMsis belowMIN_INTERVAL_MS(including zero or negative) -- the previous interval is left in effect
-
setBatchSize
public void setBatchSize(int batchSize) Sets the batch send-threshold size, rejecting anything below 1 (Gate-2 finding). A value of zero or negative would makesendBatch()'s ownfor (int i = 0; i < batchSize ...)loop consume nothing on every scheduled run, whileaddToBatch(JsonObject)'slogQueue.size() >= batchSizecheck is simultaneously always true (any non-negative queue size satisfies>= 0or>= a negative number) -- so every single enqueued record would trigger an immediatesendBatch()call that dequeues nothing, silently stalling delivery while burning CPU on every log line, rather than the panel request being rejected outright.LogStreamManager#loadBatchConfiguration()'s own boot-time path already clamped toMath.max(1, batchSize); the live panel path had no lower bound at all before this fix, unlikesetIntervalMs(int)'s pre-existing floor.- Parameters:
batchSize- the new batch size; must be at least 1- Throws:
IllegalArgumentException- ifbatchSizeis below 1 -- the previous value is left in effect
-
setBatchEnabled
public void setBatchEnabled(boolean batchEnabled) Enables or disables batched delivery, and -- unlike the field it replaces as a plain Lombok setter -- actually starts or stops the scheduled batch-send task to match (#432): disabling cancels it outright rather than leaving it running with nothing useful to flush; re-enabling starts a fresh one at the currentintervalMs. A no-op when the requested value equals the current one.- Parameters:
batchEnabled- whether batched delivery should be active
-
sendBatch
private void sendBatch()Sends the batched logs. When externalDrainMode is true, only drops entries that exceed the queue cap and does not send.Gate-2 finding (round 10): this method is ALSO reached directly, on the scheduler's own thread, by the periodically-firing
batchSenderTask. Before this fix, that call was completely unsynchronized -- a livebatchConfig.enabled: falseupdate could holdbatchModeLockacross its ownflushLogs()call while the scheduled task fired concurrently on the scheduler's thread and called this method too, both polling the SAMElogQueueand both callingwebSocketClient.sendMessage(...)independently -- interleaving queue polls between two frames with no ordering guarantee between the twosendMessagecalls. Synchronizing the whole body onbatchModeLockserializes the scheduled tick againstsetBatchEnabled(false)'s own flush, againstflushLogs()however else it is reached (it now also acquires this same lock, see its own javadoc), and againstsendLog's own dispatch decision (reentrant when this method is reached viaaddToBatch's size-threshold trigger, since that call chain already holds the lock). No new deadlock risk: this method never acquires any OTHER lock, so it can only ever be the innermost link in any lock chain that reaches it. -
drainQueue
public com.google.gson.JsonArray drainQueue(int maxItems) Drains log entries from the queue, returning them as a JsonArray. For external callers to use (e.g. ServerMonitorManager's batch_update).- Parameters:
maxItems- the maximum number of entries to take- Returns:
- a JsonArray of log entries
-
setExternalDrainMode
public void setExternalDrainMode(boolean enabled) Sets external drain mode. When enabled, sendBatch() no longer sends logs automatically; they are obtained externally via drainQueue() instead.- Parameters:
enabled- whether to enable external drain mode
-
isExternalDrainMode
public boolean isExternalDrainMode()Checks whether external drain mode is active. -
setExternalSizeThresholdCallback
Sets the callback invoked when the queue reachesbatchSizewhile external drain mode is active (Gate-2 finding, round 6). Passnullto clear it.- Parameters:
callback- a no-argument, non-blocking callback; called on whichever threadsendLog(String, String, String, Throwable)happened to run on
-
setExternalDrainCoordinationLock
Sets the lockflushLogs()coordinates disable-time flushes against (Gate-2 finding, round 9). Passnullto clear it (flushLogs runs unsynchronized again).- Parameters:
lock- any object usable as a monitor; the SAME instance must be used by whatever external code also drains this transmitter's queue (see this field's own javadoc)
-
getStackTrace
Gets the exception stack trace as a string. -
determineLoggerName
Determines the logger name. -
getDefaultServerId
Gets the default server ID. -
setLogTransmissionEnabled
public void setLogTransmissionEnabled(boolean enabled) Enables/disables log transmission. -
isLogTransmissionEnabled
public boolean isLogTransmissionEnabled()Checks whether log transmission is enabled. -
getQueueSize
public int getQueueSize()Gets the current queue size. -
flushLogs
public void flushLogs()Immediately sends every log currently in the queue. Temporarily disables external drain mode to make sure the logs actually get sent.Gate-2 finding (round 10): now ALSO acquires
batchModeLockas the OUTERMOST lock, before the external coordination lock below --sendBatch()(which this method reaches viadoFlushLogs()) is itself now synchronized onbatchModeLock(see its own javadoc), so without this method also acquiring it first, a caller that had NOT already takenbatchModeLock(e.g.shutdown(), orSystemLogHandler.flush()/close()) would acquire the coordination lock FIRST and only then try forbatchModeLockinsidesendBatch()-- the reverse of the order every other path in this class already establishes (batchModeLockbeforelogDrainLock, seesetExternalDrainCoordinationLock(Object)'s own javadoc), and a reverse-order acquisition on two different threads is exactly how a lock-ordering deadlock happens. TakingbatchModeLockfirst here keeps every acquisition path in this class consistent with that one order, so this addition introduces no new deadlock risk -- verified by enumerating every lock-acquiring path in this class andServerMonitorManager(see this plan's gate record). -
doFlushLogs
private void doFlushLogs() -
shutdown
public void shutdown()Shuts down the log transmitter.
-