Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
74 commits
Select commit Hold shift + click to select a range
567531e
Add secure single-port HTTP proxy transport
BenCodez Aug 31, 2026
1fd3be9
Validate HTTP client certificates at TLS boundary
BenCodez Aug 31, 2026
d27636b
Harden HTTP transport lifecycle
BenCodez Aug 31, 2026
1d30c94
Address HTTP transport review findings
BenCodez Aug 31, 2026
e9bbe03
Harden HTTP shutdown and protocol parsing
BenCodez Aug 31, 2026
38a37dd
fix(http): retain votes rejected by transport queue
BenCodez Aug 31, 2026
5722685
fix(http): persist outbound deliveries until ack
BenCodez Aug 31, 2026
aa0f8d8
fix(http): persist poll-created backend queues
BenCodez Aug 31, 2026
63e8f9d
fix(http): validate enrollment codes in Control
BenCodez Aug 31, 2026
5b666dc
fix(http): serialize callbacks and renew transport CA
BenCodez Sep 1, 2026
dbf416f
fix(http): verify backend presence against proxy route
BenCodez Sep 1, 2026
ac6a947
fix(http): preserve proxy callback order
BenCodez Sep 1, 2026
f104e61
fix(http): preserve callback order and crash dedup
BenCodez Sep 1, 2026
13fd2b7
fix(http): normalize corrupt fence errors
BenCodez Sep 1, 2026
71f5ca4
fix(http): revoke pending enrollment codes
BenCodez Sep 1, 2026
6216a04
fix(http): distinguish incomplete inbound deliveries
BenCodez Sep 1, 2026
2a9fcc6
test(http): import filesystem helper
BenCodez Sep 1, 2026
9eae8b5
fix(http): bound responses and persist queue roots
BenCodez Sep 1, 2026
b13ce28
test(http): await resumed delivery acknowledgement
BenCodez Sep 1, 2026
0a938cf
fix(http): persist transport directory entries
BenCodez Sep 1, 2026
5949e11
fix(http): coordinate transport lifecycle handoffs
BenCodez Sep 1, 2026
dd1ef83
fix(ci): restore complete proxy source
BenCodez Sep 1, 2026
0e9cf5a
fix(test): disambiguate transport validation assertion
BenCodez Sep 1, 2026
f81aa90
fix(http): validate readiness off the server thread
BenCodez Sep 1, 2026
b7503b9
fix(control): cancel stale transport publication
BenCodez Sep 1, 2026
67e9d12
fix(http): open proxy listener after runtime setup
BenCodez Sep 1, 2026
425664b
Restore HTTP transport after failed control reload
BenCodez Sep 1, 2026
c8b0116
Merge master into codex/http-transport
BenCodez Sep 1, 2026
57517d7
fix(http): restore credentials after failed reload
BenCodez Sep 1, 2026
9b9232b
fix(http): preserve independent delivery retries
BenCodez Sep 2, 2026
59b0805
fix(http): retain rejected vote-party rewards
BenCodez Sep 2, 2026
c10236a
fix(http): import vote-party collection helpers
BenCodez Sep 2, 2026
7fec797
test(http): normalize legacy code expiry precision
BenCodez Sep 2, 2026
01dea25
fix(http): bound streamed response reads
BenCodez Sep 2, 2026
f811389
fix(http): cancel stalled body reads at deadline
BenCodez Sep 2, 2026
7495519
fix(http): recover incomplete TLS provisioning
BenCodez Sep 2, 2026
e23a969
fix(http): defer replacement presence publication
BenCodez Sep 2, 2026
fa1296e
fix(http): flush presence on backend shutdown
BenCodez Sep 4, 2026
bfea6f1
fix(control): defer plugin-message relay publication
BenCodez Sep 4, 2026
b6d0f77
fix(control): harden transport rollback lifecycle
BenCodez Sep 5, 2026
48841b8
Fix durable vote-party effect recovery
BenCodez Sep 5, 2026
a88e705
Use SimpleAPI HTTP transport
BenCodez Sep 7, 2026
63277bc
Declare SimpleAPI HTTP dependency
BenCodez Sep 7, 2026
e6ba72c
Resume vote parties after effect retry
BenCodez Sep 7, 2026
3e17e38
Preserve HTTP messages during replacement
BenCodez Sep 8, 2026
c984404
Merge remote-tracking branch 'origin/master' into codex/http-transport
BenCodez Sep 8, 2026
804271e
Keep HTTP transport retries active
BenCodez Sep 8, 2026
f08dd1c
fix: retry HTTP enrollment failures
BenCodez Sep 8, 2026
7c715cf
Fix HTTP transport replacement during enrollment
BenCodez Sep 8, 2026
bc36371
Defer HTTP callbacks until handler publication
BenCodez Sep 8, 2026
d3fc00e
Make backend transport replacement failure-atomic
BenCodez Sep 8, 2026
59e05f1
Preserve HTTP delivery across publication and shutdown
BenCodez Sep 8, 2026
e3e5774
Preserve proxy delivery across handoff and restart
BenCodez Sep 8, 2026
17a5bd5
Restore Redis transport cleanly on failed handoff
BenCodez Sep 8, 2026
a0ee827
Recover auxiliary HTTP vote deliveries
BenCodez Sep 8, 2026
f772730
Merge remote-tracking branch 'origin/master' into codex/http-transport
BenCodez Sep 8, 2026
158d403
Preserve HTTP delivery state across retries
BenCodez Sep 8, 2026
ce74439
Fence vote-party work during transport changes
BenCodez Sep 9, 2026
874d62f
Harden HTTP handoff and broadcast recovery
BenCodez Sep 9, 2026
c45f3ea
Preserve proxy deliveries across transport changes
BenCodez Sep 10, 2026
f1337fa
Preserve deliveries across transport replacement
BenCodez Sep 10, 2026
ba2c091
Replay Redis handoff after inbound activation
BenCodez Sep 10, 2026
f74a228
Avoid Redis handoff callback lock inversion
BenCodez Sep 10, 2026
38ae6f6
Complete bounded proxy transport handoffs
BenCodez Sep 10, 2026
bf8de86
Preserve proxy runtime during deferred handoffs
BenCodez Sep 10, 2026
058e8e7
Fix deferred transport retirement
BenCodez Sep 10, 2026
59b5e75
Preserve stopped presence during transport disable
BenCodez Sep 10, 2026
18ff986
Harden HTTP transition failure handling
BenCodez Sep 10, 2026
b117c36
Harden HTTP shutdown and cache durability
BenCodez Sep 10, 2026
3e5c168
Make proxy vote delivery crash durable
BenCodez Sep 11, 2026
cbe6046
Preserve live vote retries across reload
BenCodez Sep 11, 2026
d8fbbed
Keep vote completion retries idempotent
BenCodez Sep 11, 2026
a404248
Settle live vote retries before shutdown
BenCodez Sep 11, 2026
0da1c46
Make multi-proxy vote delivery durable
BenCodez Sep 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions VotingPlugin/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,10 @@
<shadedPattern>
${project.groupId}.votingplugin.bstats</shadedPattern>
</relocation>
<relocation>
<pattern>org.bouncycastle</pattern>
<shadedPattern>${project.groupId}.votingplugin.bouncycastle</shadedPattern>
</relocation>
<relocation>
<pattern>xyz.upperlevel.spigot</pattern>
<shadedPattern>
Expand Down Expand Up @@ -339,6 +343,12 @@
<version>3.8.2-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.bencodez</groupId>
<artifactId>simpleapi</artifactId>
<version>1.0.2-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ public class BackendProxyHandler implements Listener {
private final BackendGlobalDataSync globalDataSync;

private BackendPresenceManager presenceManager;
private boolean presenceReportingActivated;
private final Object inboundPublication = new Object();
private boolean inboundPublished;
private boolean inboundAborted;
private BackendProxyHandler inboundRollbackTarget;
private BackendVotePartySync votePartySync;
private BackendProxyMessageRouter messageRouter;

Expand All @@ -62,12 +67,26 @@ public BackendProxyHandler(VotingPluginMain plugin, ProcessedVoteCache processed
* Loads the configured backend/proxy communication components.
*/
public void load() {
load(true);
}

/** Loads a replacement without announcing a new presence generation before publication. */
public void loadForReplacement() {
load(false);
}

private void load(boolean activatePresenceReporting) {
plugin.debug("Loading backend proxy handler");
method = BungeeMethod.getByName(plugin.getBungeeSettings().getBungeeMethod());
plugin.getLogger().info("Using BungeeMethod: " + method.toString());

globalDataSync.load();
globalMessageHandler = new GlobalMessageHandler() {
@Override
public void onMessage(JsonEnvelope envelope) {
BackendProxyHandler.this.dispatchIncomingAfterPublication(envelope, () -> super.onMessage(envelope));
}

@Override
public void sendMessage(JsonEnvelope envelope) {
transportManager.send(envelope);
Expand All @@ -79,20 +98,40 @@ public void sendMessage(JsonEnvelope envelope) {
messageRouter = new BackendProxyMessageRouter(plugin, presenceManager, globalDataSync, votePartySync,
processedVoteCache);
messageRouter.register(globalMessageHandler, method);
transportManager.start(method, globalMessageHandler);
transportManager.start(method, globalMessageHandler, activatePresenceReporting);

if (plugin.getOptions().getServer().equalsIgnoreCase("pleaseset")) {
plugin.getLogger().warning("Server name for bungee voting is not set, please set it");
}
presenceManager.start();
if (activatePresenceReporting) {
activatePresenceReporting();
activateInboundMessages();
}
}

/** Starts presence only after a staged handler reaches the atomic publication boundary. */
public void activatePresenceReporting() {
if (presenceManager != null && !presenceReportingActivated) {
presenceManager.start();
presenceReportingActivated = true;
}
// Presence startup can throw while scheduling its heartbeat. Keep inbound
// HTTP callbacks behind the publication barrier until every fallible part of
// the replacement is active, so rollback cannot race a queued callback.
transportManager.activateAfterPublication();
}

/**
* Closes backend/proxy components and persists cached proxy state.
*/
public void close() {
if (presenceManager != null) {
synchronized (inboundPublication) {
if (!inboundPublished && !inboundAborted) inboundAborted = true;
inboundPublication.notifyAll();
}
if (presenceManager != null && presenceReportingActivated) {
presenceManager.stop();
presenceReportingActivated = false;
}
transportManager.close();
if (votePartySync != null) {
Expand All @@ -101,26 +140,177 @@ public void close() {
globalDataSync.close();
}

/** Releases a same-method subscriber/listener before its replacement starts. */
public void prepareForReplacement(BungeeMethod replacementMethod) {
if (method == replacementMethod && method != BungeeMethod.PLUGINMESSAGING && method != BungeeMethod.REDIS) {
/** Opens inbound dispatch only after the replacement and all handoffs are committed. */
public void activateInboundMessages() {
synchronized (inboundPublication) {
if (inboundAborted) return;
inboundPublished = true;
inboundPublication.notifyAll();
}
}

/** Routes an already accepted staged callback through the restored predecessor on rollback. */
public void abortStagedInboundTo(BackendProxyHandler previous) {
synchronized (inboundPublication) {
if (inboundPublished || inboundAborted) return;
inboundRollbackTarget = previous;
inboundAborted = true;
inboundPublication.notifyAll();
}
}

void dispatchIncomingAfterPublication(JsonEnvelope envelope, Runnable localDispatch) {
BackendProxyHandler rollbackTarget;
synchronized (inboundPublication) {
while (!inboundPublished && !inboundAborted) {
try {
inboundPublication.wait();
} catch (InterruptedException interrupted) {
Thread.currentThread().interrupt();
return;
}
}
rollbackTarget = inboundPublished ? null : inboundRollbackTarget;
if (!inboundPublished && rollbackTarget == null) return;
}
if (rollbackTarget != null) {
GlobalMessageHandler rollbackHandler = rollbackTarget.globalMessageHandler;
if (rollbackHandler != null) rollbackHandler.onMessage(envelope);
return;
}
plugin.getBukkitScheduler().executeOrScheduleSync(plugin, localDispatch);
}

/** Returns whether replacement preparation must preserve accepted deliveries. */
public boolean requiresPreparationForReplacement() {
return method == BungeeMethod.HTTP || method == BungeeMethod.PLUGINMESSAGING
|| transportManager.hasPendingAsyncHandoff() || transportManager.hasPendingRedisReplay();
}

/** Prepares HTTP state or waits off-thread for an earlier cross-transport handoff. */
public boolean prepareForReplacement(BungeeMethod replacementMethod) {
return prepareForReplacement(replacementMethod,
System.nanoTime() + java.util.concurrent.TimeUnit.SECONDS.toNanos(25));
}

public boolean prepareForReplacement(BungeeMethod replacementMethod, long deadlineNanos) {
if (method == BungeeMethod.HTTP) {
transportManager.prepareForReplacement();
return true;
}
if (method == BungeeMethod.PLUGINMESSAGING) {
transportManager.prepareAsyncHandoffForReplacement(deadlineNanos);
return true;
}
if (method == BungeeMethod.REDIS && replacementMethod != BungeeMethod.REDIS) {
if (transportManager.hasPendingAsyncHandoff())
transportManager.prepareAsyncHandoffForReplacement(deadlineNanos);
return transportManager.prepareRedisReplayTransition(replacementMethod, deadlineNanos);
}
if (method == BungeeMethod.REDIS) {
// Same-Redis retirement installs its send fence during the bounded
// off-thread handoff. Returning true here lets the staged replacement
// buffer its own sends until the predecessor FIFO is admitted at Bukkit
// publication.
return true;
}
if (!transportManager.hasPendingAsyncHandoff()) return false;
transportManager.prepareAsyncHandoffForReplacement(deadlineNanos);
return true;
}

/** Atomically fences new sends only when disabling cannot discard prepared HTTP messages. */
public boolean commitPreparedDisable() {
return transportManager.commitPreparedDisable();
}

/** Publishes the final presence update while fencing other sends before transport preparation. */
public void preparePresenceForDisable() {
transportManager.beginPreparedDisable();
if (presenceManager != null && presenceReportingActivated) {
// stopForDisable may reject when the transport cannot accept the final
// presence update. Mark this inactive first so rollback can start it again.
presenceReportingActivated = false;
presenceManager.stopForDisable();
}
}

/** Restores delivery and presence when a prepared disable is abandoned. */
public void restorePresenceAfterFailedDisablePreparation() {
transportManager.cancelPreparedDisable();
activatePresenceReporting();
}

public void beginPreparedHttpHandoff() {
transportManager.beginPreparedHttpHandoff();
}

/** Reserves staged HTTP capacity before publication can admit replacement sends. */
public void reservePreparedHttpHandoff(BackendProxyHandler replacement) {
transportManager.reservePreparedTransportHandoff(replacement.transportManager);
}

/** Restores a prepared HTTP transport when its replacement fails validation. */
public void restoreAfterFailedReplacement() {
transportManager.restoreAfterFailedReplacement();
}

/** Restores a failed same-Redis predecessor without discarding its replacement's replay FIFO. */
public void restoreAfterFailedReplacement(BackendProxyHandler failedReplacement) {
transportManager.restoreAfterFailedReplacement(
failedReplacement == null ? null : failedReplacement.transportManager);
}

/** Reasserts the old handler with a fresh presence generation after rollback. */
public void refreshPresenceAfterFailedReplacement() {
if (presenceManager != null && presenceReportingActivated) {
presenceManager.stop();
presenceManager.start();
}
}

public void awaitRestoreAfterFailedReplacement(long deadlineNanos) {
transportManager.awaitPreparedTransportRestoration(deadlineNanos);
}

/** Fails a configuration apply when its selected transport did not initialize. */
public void validateTransport() {
validateTransport(System.nanoTime() + java.util.concurrent.TimeUnit.SECONDS.toNanos(25));
}

/** Validates transport startup without extending the caller's existing deadline. */
public void validateTransport(long deadlineNanos) {
if (method == null || globalMessageHandler == null || presenceManager == null) {
throw new IllegalStateException("Backend proxy handler initialization failed");
}
transportManager.validate();
transportManager.validate(deadlineNanos);
}

/** Completes the no-loss/no-duplicate same-Redis subscriber handoff after validation. */
public void completeRedisHandoff(BackendProxyHandler replacement) {
if (method != BungeeMethod.REDIS || replacement.method != BungeeMethod.REDIS) return;
transportManager.closeRedisForHandoff();
replacement.transportManager.activateRedisAfterHandoff();
if (!requiresRedisHandoff(replacement)) return;
transportManager.completeRedisHandoff(replacement.transportManager);
}

/** Returns whether this handler owns a Redis listener whose shutdown can block. */
public boolean requiresRedisRetirement() {
return method == BungeeMethod.REDIS;
}

/** Returns whether this replacement needs the bounded same-Redis retirement path. */
public boolean requiresRedisHandoff(BackendProxyHandler replacement) {
return replacement != null && method == BungeeMethod.REDIS && replacement.method == BungeeMethod.REDIS;
}

/** Replays Redis handoff deliveries only after inbound publication is open. */
public void replayRedisAfterHandoffPublication() {
transportManager.replayRedisAfterHandoffPublication();
}

/** Forwards messages buffered while the previous transport was fenced. */
public void completeHttpHandoff(BackendProxyHandler replacement) {
if (replacement == null) return;
transportManager.completePreparedTransportHandoff(replacement.transportManager);
}

public void playerOnline(String playerName, String uuid) {
Expand All @@ -136,14 +326,15 @@ public void playerOffline(String playerName) {
}

public void reloadPresenceReporting() {
if (presenceManager != null) {
if (presenceManager != null && presenceReportingActivated) {
presenceManager.reload();
}
}

public void disablePresenceReporting() {
if (presenceManager != null) {
if (presenceManager != null && presenceReportingActivated) {
presenceManager.stop();
presenceReportingActivated = false;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public class ProcessedVoteCache {
private final LinkedHashMap<String, Long> processedRedisDeliveries = new LinkedHashMap<>();
private final LinkedHashMap<String, Integer> legacyRedisDeliveries = new LinkedHashMap<>();
private long legacyRedisDeliveryBytes;
private boolean legacyRedisHandoffOverflowed;
private Object activeRedisSubscriber;
private Object standbyRedisSubscriber;

Expand Down Expand Up @@ -82,14 +83,18 @@ public synchronized boolean reserveRedisDelivery(String deliveryId) {
return true;
}

public synchronized void registerRedisSubscriber(Object subscriber) {
public synchronized boolean registerRedisSubscriber(Object subscriber) {
if (activeRedisSubscriber == null) {
activeRedisSubscriber = subscriber;
return true;
} else if (activeRedisSubscriber != subscriber) {
standbyRedisSubscriber = subscriber;
legacyRedisDeliveries.clear();
legacyRedisDeliveryBytes = 0;
legacyRedisHandoffOverflowed = false;
return false;
}
return true;
}

/** Returns true only for the active subscriber and counts its legacy delivery during overlap. */
Expand All @@ -107,12 +112,16 @@ public synchronized boolean reserveLegacyRedisDelivery(Object subscriber, String
&& legacyRedisDeliveryBytes <= MAX_LEGACY_REDIS_TOTAL_BYTES - bytes) {
legacyRedisDeliveries.put(signature, 1);
legacyRedisDeliveryBytes += bytes;
}
} else legacyRedisHandoffOverflowed = true;
}
}
return true;
}

public synchronized boolean isLegacyRedisHandoffOverflowed() {
return legacyRedisHandoffOverflowed;
}

public synchronized void activateRedisSubscriber(Object subscriber) {
if (standbyRedisSubscriber != subscriber) {
throw new IllegalStateException("Redis replacement subscriber is not registered");
Expand All @@ -121,6 +130,15 @@ public synchronized void activateRedisSubscriber(Object subscriber) {
standbyRedisSubscriber = null;
}

/** Restores the retired active listener after a promoted replacement is rolled back. */
public synchronized void restoreRedisSubscriber(Object subscriber) {
activeRedisSubscriber = subscriber;
standbyRedisSubscriber = null;
legacyRedisDeliveries.clear();
legacyRedisDeliveryBytes = 0;
legacyRedisHandoffOverflowed = false;
}

/** Consumes one matching delivery processed by the previous active subscriber. */
public synchronized boolean consumeLegacyRedisDelivery(String signature) {
Integer count = legacyRedisDeliveries.get(signature);
Expand All @@ -135,13 +153,15 @@ public synchronized boolean consumeLegacyRedisDelivery(String signature) {
public synchronized void finishRedisHandoff() {
legacyRedisDeliveries.clear();
legacyRedisDeliveryBytes = 0;
legacyRedisHandoffOverflowed = false;
}

public synchronized void unregisterRedisSubscriber(Object subscriber) {
if (standbyRedisSubscriber == subscriber) {
standbyRedisSubscriber = null;
legacyRedisDeliveries.clear();
legacyRedisDeliveryBytes = 0;
legacyRedisHandoffOverflowed = false;
}
if (activeRedisSubscriber == subscriber) activeRedisSubscriber = null;
}
Expand Down
Loading
Loading