Skip to content
Open
6 changes: 6 additions & 0 deletions .mex/events/decisions.jsonl
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{"timestamp":"2026-09-21T10:51:59.003Z","kind":"decision","message":"Proxy-to-backend reward votes use a capability-negotiated durable outbox and completion acknowledgement. The guarantee is at least once; HTTP retains its existing delivery path, old backends retain legacy semantics, and exactly-once reward effects remain out of scope.","files":["VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/ReliableVoteDeliveryOutbox.java","VotingPlugin/src/main/java/com/bencodez/votingplugin/backendproxy/BackendProxyHandler.java"],"cwd":".","source":"agent","status":"implemented"}
{"timestamp":"2026-09-21T11:07:28.400Z","kind":"decision","message":"Reliable backend votes persist completed vote IDs for seven days before acknowledgement. Receipt persistence failure retries without repeating effects in the active process; restart deduplication covers durable completions, while a crash during non-transactional reward effects remains at-least-once.","files":["VotingPlugin/src/main/java/com/bencodez/votingplugin/backendproxy/cache/DurableVoteReceiptStore.java","VotingPlugin/src/main/java/com/bencodez/votingplugin/backendproxy/messaging/BackendProxyMessageRouter.java"],"cwd":".","source":"agent","status":"implemented"}
{"timestamp":"2026-09-21T11:41:29.374Z","kind":"decision","message":"Supersedes the seven-day receipt decision: completion receipts have no time expiry and fail closed at the bounded journal capacity, HTTP participates in the completion-acknowledged outbox, and receipt-write failures keep an in-memory post-effect fence until persistence succeeds.","files":["VotingPlugin/src/main/java/com/bencodez/votingplugin/backendproxy/cache/DurableVoteReceiptStore.java","VotingPlugin/src/main/java/com/bencodez/votingplugin/backendproxy/cache/ProcessedVoteCache.java","VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/VotingPluginProxy.java"],"cwd":".","source":"agent","status":"implemented"}
{"timestamp":"2026-09-21T12:13:09.371Z","kind":"decision","message":"Supersedes indefinite unreclaimed completion receipts: reliable vote delivery uses a durable two-phase receipt-release handshake. After the proxy durably records backend completion, it retries a release marker until the backend durably removes the receipt and acknowledges retirement; only then does the proxy remove the marker.","files":["VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/ReliableVoteDeliveryOutbox.java","VotingPlugin/src/main/java/com/bencodez/votingplugin/backendproxy/cache/DurableVoteReceiptStore.java","VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/VotingPluginWire.java"],"cwd":".","source":"agent","status":"implemented"}
{"timestamp":"2026-09-21T12:30:33.599Z","kind":"decision","message":"Supersedes immediate receipt removal after release: backend receipt release creates a 24-hour durable tombstone to fence deliveries already in flight, then reclaims it. Legacy downgrade delivery preserves the proxy release marker so a later capable backend can retire any receipt whose completion acknowledgement was lost.","files":["VotingPlugin/src/main/java/com/bencodez/votingplugin/backendproxy/cache/DurableVoteReceiptStore.java","VotingPlugin/src/main/java/com/bencodez/votingplugin/backendproxy/cache/ProcessedVoteCache.java","VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/VotingPluginProxy.java"],"cwd":".","source":"agent","status":"implemented"}
{"timestamp":"2026-09-21T12:46:34.284Z","kind":"decision","message":"Receipt releases persist a 24-hour tombstone even when the backend has no active receipt, fencing a late legacy retry. The bounded receipt store keeps completion headroom larger than the entire ordered lane plus separate tombstone capacity. Releases backed by an existing durable receipt may bypass a capacity-blocked vote because durable completion makes that reordering safe; unknown releases remain ordered.","files":["VotingPlugin/src/main/java/com/bencodez/votingplugin/backendproxy/cache/DurableVoteReceiptStore.java","VotingPlugin/src/main/java/com/bencodez/votingplugin/backendproxy/cache/ProcessedVoteCache.java","VotingPlugin/src/main/java/com/bencodez/votingplugin/backendproxy/BackendProxyHandler.java"],"cwd":".","source":"agent","status":"implemented"}
5 changes: 5 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,11 @@ credentials, generated JARs, dependency caches, IDE output, or unrelated formatt

- Trace whether the code runs on the connector worker, proxy thread, Bukkit primary thread, or a SQL executor.
- Preserve queued votes across saturation, shutdown, and restart; overflow handling must be bounded, durable when promised, and observable rather than silently dropping work.
- Proxy-to-backend guaranteed delivery is capability negotiated and at least once. Journal a reward-bearing envelope before
reporting transport acceptance, retain it until the matching backend completion acknowledgement is durable, persist
completed IDs before acknowledgement for restart-safe deduplication, retire receipts only through the durable
proxy-confirmed release handshake, retain a bounded durable tombstone for in-flight retries, and keep legacy send
behavior for backends that do not advertise the capability.
- Treat scheduler units explicitly. Verify whether each delay is in ticks, milliseconds, or seconds, especially across Bukkit, Folia, BungeeCord, and Velocity adapters.
- Register listeners and lifecycle wakeups before producers can publish work; startup/reload ordering must not strand already-persisted or newly-arriving operations.
- Protocol-mode changes must not silently broaden legacy v1/RSA acceptance when token-only operation is configured or intended; cover downgrade behavior with tests.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ public File getLoadedPluginJarFile() {

@Getter
private BackendProxyHandler backendProxyHandler;
private final ProcessedVoteCache backendProcessedVoteCache = new ProcessedVoteCache();
private ProcessedVoteCache backendProcessedVoteCache;
private BackendOrderedVoteOverflowQueue backendOrderedVoteOverflowQueue;
private final AtomicReference<GlobalMessageHandler> backendPluginMessageTarget = new AtomicReference<>();
private PluginMessageHandler backendPluginMessageRelay;
Expand Down Expand Up @@ -487,8 +487,16 @@ private synchronized BackendOrderedVoteOverflowQueue getOrCreateBackendOrderedVo
return backendOrderedVoteOverflowQueue;
}

private synchronized ProcessedVoteCache getOrCreateBackendProcessedVoteCache() {
if (backendProcessedVoteCache == null) {
backendProcessedVoteCache = new ProcessedVoteCache(
new File(getDataFolder(), "BackendProcessedVotes.dat").toPath());
}
return backendProcessedVoteCache;
}

private void loadBungeeHandler() {
BackendProxyHandler candidate = new BackendProxyHandler(this, backendProcessedVoteCache,
BackendProxyHandler candidate = new BackendProxyHandler(this, getOrCreateBackendProcessedVoteCache(),
getOrCreateBackendOrderedVoteOverflowQueue());
try {
candidate.load();
Expand Down Expand Up @@ -1331,7 +1339,7 @@ public synchronized BackendProxyRestart prepareBackendProxyHandlerRestart() {
boolean previousRequiresPreparation = previous != null
&& (deferredReplacementLoad || previous.requiresPreparationForReplacement()
|| previous.requiresRedisRetirement());
BackendProxyHandler replacement = new BackendProxyHandler(this, backendProcessedVoteCache,
BackendProxyHandler replacement = new BackendProxyHandler(this, getOrCreateBackendProcessedVoteCache(),
getOrCreateBackendOrderedVoteOverflowQueue());
if (!deferredReplacementLoad) {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,8 @@ private static boolean isOrderedVoteMessage(JsonEnvelope envelope) {
String subChannel = envelope.getSubChannel();
return VotingPluginWire.SUB_VOTE.equals(subChannel)
|| VotingPluginWire.SUB_VOTE_ONLINE.equals(subChannel)
|| VotingPluginWire.SUB_VOTE_UPDATE.equals(subChannel);
|| VotingPluginWire.SUB_VOTE_UPDATE.equals(subChannel)
|| VotingPluginWire.SUB_VOTE_DELIVERY_RECEIPT_RELEASE.equals(subChannel);
}

private void requestPersistenceLocked() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ public class BackendProxyHandler implements Listener {
private boolean orderedVoteDispatchPaused = true;
private boolean orderedVoteDispatchClosing;
private boolean orderedVoteQuarantineFailed;
private final AtomicBoolean durableReceiptReleaseActive = new AtomicBoolean();
private JsonEnvelope orderedVoteDispatchInFlight;
private BackendOrderedVoteOverflowQueue.PendingEnvelope orderedVoteOverflowInFlight;
private JsonEnvelope orderedVoteShutdownQuarantined;
Expand Down Expand Up @@ -229,10 +230,47 @@ private boolean isOrderedVoteMessage(JsonEnvelope envelope) {
String subChannel = envelope.getSubChannel();
return VotingPluginWire.SUB_VOTE.equals(subChannel)
|| VotingPluginWire.SUB_VOTE_ONLINE.equals(subChannel)
|| VotingPluginWire.SUB_VOTE_UPDATE.equals(subChannel);
|| VotingPluginWire.SUB_VOTE_UPDATE.equals(subChannel)
|| VotingPluginWire.SUB_VOTE_DELIVERY_RECEIPT_RELEASE.equals(subChannel);
}

private void dispatchOrderedVote(JsonEnvelope envelope, Runnable ignoredLocalDispatch) {
BackendProxyMessageRouter router = messageRouter;
if (router != null && router.hasDurableReceiptForRelease(envelope)
&& durableReceiptReleaseActive.compareAndSet(false, true)) {
try {
plugin.getBukkitScheduler().runTaskAsynchronously(plugin,
() -> processDurableReceiptRelease(router, envelope, ignoredLocalDispatch));
return;
} catch (RuntimeException schedulingFailure) {
plugin.debug(schedulingFailure);
processDurableReceiptRelease(router, envelope, ignoredLocalDispatch);
return;
}
}
enqueueOrderedVote(envelope, ignoredLocalDispatch);
}

private void processDurableReceiptRelease(BackendProxyMessageRouter router, JsonEnvelope envelope,
Runnable ignoredLocalDispatch) {
AtomicBoolean completed = new AtomicBoolean();
java.util.function.Consumer<OrderedVoteOutcome> completion = outcome -> {
if (!completed.compareAndSet(false, true)) return;
durableReceiptReleaseActive.set(false);
if (outcome != OrderedVoteOutcome.COMPLETE) enqueueOrderedVote(envelope, ignoredLocalDispatch);
};
try {
router.handleOrderedVote(envelope, completion);
} catch (RuntimeException | Error failure) {
if (completed.compareAndSet(false, true)) {
durableReceiptReleaseActive.set(false);
enqueueOrderedVote(envelope, ignoredLocalDispatch);
}
throw failure;
}
}

private void enqueueOrderedVote(JsonEnvelope envelope, Runnable ignoredLocalDispatch) {
BackendProxyHandler handoffTarget;
synchronized (orderedVoteDispatch) {
handoffTarget = orderedVoteHandoffTarget;
Expand Down Expand Up @@ -417,7 +455,7 @@ private void finishOrderedVoteDispatch(BackendOrderedVoteOverflowQueue.PendingEn
boolean successful = outcome == OrderedVoteOutcome.COMPLETE;
if (successful && overflowEntry != null && orderedVoteOverflow != null) {
orderedVoteOverflow.acknowledgeAsync(overflowEntry,
stored -> completeOrderedVoteAcknowledgement(stored));
stored -> completeOrderedVoteAcknowledgement(stored, envelope));
return;
}
synchronized (orderedVoteDispatch) {
Expand All @@ -432,9 +470,10 @@ private void finishOrderedVoteDispatch(BackendOrderedVoteOverflowQueue.PendingEn
if (successful) scheduleOrderedVoteDispatchLocked();
else retryOrderedVoteDispatchLocked();
}
if (successful) sendVoteDeliveryAcknowledgement(envelope);
}

private void completeOrderedVoteAcknowledgement(boolean stored) {
private void completeOrderedVoteAcknowledgement(boolean stored, JsonEnvelope envelope) {
synchronized (orderedVoteDispatch) {
if (stored) {
orderedVoteDispatchInFlight = null;
Expand All @@ -450,6 +489,23 @@ private void completeOrderedVoteAcknowledgement(boolean stored) {
orderedVoteDispatch.notifyAll();
if (stored) scheduleOrderedVoteDispatchLocked();
}
if (stored) sendVoteDeliveryAcknowledgement(envelope);
}

private void sendVoteDeliveryAcknowledgement(JsonEnvelope envelope) {
if ((!VotingPluginWire.SUB_VOTE.equals(envelope.getSubChannel())
&& !VotingPluginWire.SUB_VOTE_ONLINE.equals(envelope.getSubChannel()))
|| !VotingPluginWire.requestsVoteDeliveryAcknowledgement(envelope)
|| globalMessageHandler == null) return;
String voteId = envelope.getFields().get(VotingPluginWire.K_VOTE_ID);
try {
UUID parsed = voteId == null || voteId.isBlank() ? null : UUID.fromString(voteId);
if (parsed == null) return;
globalMessageHandler.sendMessage(VotingPluginWire.voteDeliveryAcknowledgement(
plugin.getBungeeSettings().getServer(), parsed, envelope.getSubChannel()));
} catch (IllegalArgumentException invalidVoteId) {
plugin.debug("Unable to acknowledge proxy vote with invalid vote ID");
}
}

private void completeOrderedVoteQuarantine(BackendOrderedVoteOverflowQueue.PendingEnvelope overflowEntry,
Expand Down
Loading
Loading