Class SpoolingAuditSink
- All Implemented Interfaces:
McpAuditSink
This is what makes the trail survive an outage. A plain KafkaAuditSink loses records
once the broker has been unreachable longer than its in-memory buffer absorbs; with a spool in
front, those records are on disk and are replayed — including across a restart, because the spool
is read back when the server starts.
Write-ahead, not fallback
Records are spooled before any delivery attempt, not after one fails. A fallback design
cannot work here: McpAuditSink.record(com.mcpdbwizard.pub.McpAuditEvent) returns void and an asynchronous sink has not
even attempted delivery by the time it returns, so there is nothing to fall back from. Writing
first also means a process killed mid-call still has the record.
The guarantee, stated precisely
At-least-once, and duplicates are possible. A segment is deleted only after the delegate
confirms via McpAuditSink.flush(), so a crash between delivering and deleting replays that
segment. Every event carries McpAuditEvent.getId() so a consumer can collapse the repeat.
What survives what: with MCP_AUDIT_SPOOL_FSYNC=never (the default) records
survive the process dying — a crash, a container restart, an OOM kill — because the bytes are in
the operating system's cache. They do not survive the machine losing power. Setting
always calls fsync per record, which survives that too and costs a disk round trip
on every tool call.
Disk is not infinite. Past MCP_AUDIT_SPOOL_MAX_BYTES the policy decides:
drop refuses new records and counts them, block makes the tool call wait for the
drainer to catch up. Dropping the oldest is deliberately not offered — silently discarding
the records already accepted for audit is the worst of the three.
Copyright 2003-2026 ATB Consultancy Services Ltd
(formerly Orinda Software Ltd, Dublin, Ireland)
-
Field Summary
Fields -
Constructor Summary
ConstructorsConstructorDescriptionSpoolingAuditSink(McpAuditSink theDelegateValue, Path theDirectoryValue, long theMaxBytesValue, long theSegmentBytesValue, boolean theBlockFlagValue, boolean theFsyncFlagValue) SpoolingAuditSink(McpAuditSink theDelegateValue, Path theDirectoryValue, long theMaxBytesValue, long theSegmentBytesValue, boolean theBlockFlagValue, boolean theFsyncFlagValue, SpoolCipher theCipherValue) As above, with the cipher supplied rather than read from the environment. -
Method Summary
Modifier and TypeMethodDescriptionvoidclose()describe()Names what the records finally go to, not just this wrapper.longDeliver every closed segment, oldest first, deleting each only once the delegate confirms.longRecords handed to the delegate and confirmed.longRecords refused because the spool was full.longHow many are still on disk awaiting delivery.voidrecord(McpAuditEvent theEvent) Record one call.voidStart the background drainer.static McpAuditSinkwrap(McpAuditSink theDelegateValue) Build from the environment, wrapping the given delegate.static McpAuditSinkwrap(McpAuditSink theDelegateValue, String theSubdirectoryValue) Aswrap(McpAuditSink), but placing the spool in a named subdirectory.Methods inherited from class java.lang.Object
clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, waitMethods inherited from interface com.mcpdbwizard.pub.McpAuditSink
flush
-
Field Details
-
DIRECTORY_VARIABLE
- See Also:
-
MAX_BYTES_VARIABLE
- See Also:
-
ON_FULL_VARIABLE
- See Also:
-
FSYNC_VARIABLE
- See Also:
-
SEGMENT_BYTES_VARIABLE
- See Also:
-
DEFAULT_MAX_BYTES
public static final long DEFAULT_MAX_BYTES- See Also:
-
DEFAULT_SEGMENT_BYTES
public static final long DEFAULT_SEGMENT_BYTES- See Also:
-
-
Constructor Details
-
SpoolingAuditSink
public SpoolingAuditSink(McpAuditSink theDelegateValue, Path theDirectoryValue, long theMaxBytesValue, long theSegmentBytesValue, boolean theBlockFlagValue, boolean theFsyncFlagValue) -
SpoolingAuditSink
public SpoolingAuditSink(McpAuditSink theDelegateValue, Path theDirectoryValue, long theMaxBytesValue, long theSegmentBytesValue, boolean theBlockFlagValue, boolean theFsyncFlagValue, SpoolCipher theCipherValue) As above, with the cipher supplied rather than read from the environment.- Parameters:
theCipherValue- encrypts each spooled line, or null to write plaintext
-
-
Method Details
-
wrap
Build from the environment, wrapping the given delegate. Returns the delegate unchanged when unset. -
wrap
Aswrap(McpAuditSink), but placing the spool in a named subdirectory.A spool has exactly one writer by construction: one active file, and a drainer that takes every closed segment it finds. Two processes sharing a directory would therefore interleave writes into one file and each deliver the other's segments. A generated server is the only process in its JVM and needs no subdirectory; the web application, which records proxied requests alongside however many generated servers it has launched, passes one so that each spool has a single owner.
- Parameters:
theSubdirectoryValue- a directory name to append, or null to use the configured directory as it stands
-
startDraining
public void startDraining()Start the background drainer. Separate from the constructor so tests can drain deterministically. -
record
Record one call. Must not throw.Appends to the spool. Never throws — a failure to audit must not become the caller's failure.
- Specified by:
recordin interfaceMcpAuditSink- Parameters:
theEvent- the call to record; never null
-
drainOnce
public long drainOnce()Deliver every closed segment, oldest first, deleting each only once the delegate confirms.- Returns:
- how many records were delivered
-
getDroppedCount
public long getDroppedCount()Records refused because the spool was full. Non-zero means the trail has holes.- Specified by:
getDroppedCountin interfaceMcpAuditSink
-
getDeliveredCount
public long getDeliveredCount()Records handed to the delegate and confirmed.- Specified by:
getDeliveredCountin interfaceMcpAuditSink
-
getPendingCount
public long getPendingCount()How many are still on disk awaiting delivery.- Specified by:
getPendingCountin interfaceMcpAuditSink
-
describe
Names what the records finally go to, not just this wrapper."SpoolingAuditSink" on its own answers the wrong question: the spool is how delivery survives an outage, while the destination is what an operator is checking.
- Specified by:
describein interfaceMcpAuditSink
-
close
public void close()- Specified by:
closein interfaceMcpAuditSink
-