Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
940f563
Make period changes resumable across shutdown
BenCodez Sep 21, 2026
7041db5
Stabilize period recovery inputs
BenCodez Sep 22, 2026
ff1bab4
Make time-change recovery snapshots stable
BenCodez Sep 22, 2026
647da92
Make period reset retries idempotent
BenCodez Sep 22, 2026
1019480
Make period resets preserve concurrent votes
BenCodez Sep 22, 2026
8c63793
Use boundary totals for weekly recovery
BenCodez Sep 22, 2026
dd204ec
Keep recovered period boundaries stable
BenCodez Sep 22, 2026
fc6e886
Fix period boundary recovery ordering
BenCodez Sep 22, 2026
4dc22c3
Snapshot daily streak recovery state
BenCodez Sep 22, 2026
96f9359
Harden period reward recovery boundaries
BenCodez Sep 22, 2026
f75b499
Coordinate durable period recovery
BenCodez Sep 22, 2026
65a9d55
Fence shared period mutations
BenCodez Sep 22, 2026
54db523
Make shared vote accounting retry safe
BenCodez Sep 22, 2026
c06bf3f
Recover deferred shared vote accounting
BenCodez Sep 22, 2026
baca6da
Fence vote accounting across period transitions
BenCodez Sep 22, 2026
4933c26
Retain votes across accounting retries
BenCodez Sep 22, 2026
4c35e11
Admit daily streak work before vote effects
BenCodez Sep 22, 2026
b751979
Complete streak rewards after delivery
BenCodez Sep 22, 2026
06bd000
Keep ambiguous streak rewards from blocking recovery
BenCodez Sep 23, 2026
e47d8a2
Keep vote accounting decisions consistent
BenCodez Sep 23, 2026
1caceaf
Fence recovered daily streak rewards
BenCodez Sep 23, 2026
6c49ff5
Finish copied streak transitions after config changes
BenCodez Sep 23, 2026
3d49497
Recover copied streaks after settings change
BenCodez Sep 23, 2026
b24b3c3
Bound period recovery memory and checkpoints
BenCodez Sep 23, 2026
b8660f9
Close period recovery ordering gaps
BenCodez Sep 23, 2026
45cb5d3
Preserve queued changes during period recovery
BenCodez Sep 23, 2026
b3c1da7
Freeze daily streak recovery policy
BenCodez Sep 23, 2026
ef31947
Freeze recoverable transition inputs
BenCodez Sep 23, 2026
f5489e4
Reuse durable vote and reset decisions
BenCodez Sep 23, 2026
81d6f55
Freeze top voter policy during recovery
BenCodez Sep 23, 2026
1026889
Persist vote points eligibility
BenCodez Sep 23, 2026
ab1d89e
Preserve eligible top voter ranking
BenCodez Sep 23, 2026
7093916
Preserve reset identifiers and reward routing
BenCodez Sep 23, 2026
3ffd58a
Address remaining lifecycle review findings
BenCodez Sep 23, 2026
6d5fb91
Make vote accounting replay idempotent
BenCodez Sep 23, 2026
5c5e5a0
Require a confirmed proxy time boundary
BenCodez Sep 23, 2026
a1d4b47
Close remaining vote recovery gaps
BenCodez Sep 23, 2026
9f79260
Propagate incomplete vote processing
BenCodez Sep 23, 2026
b60b728
Quarantine ambiguous vote retries
BenCodez Sep 23, 2026
a9230ff
Persist vote effect admission state
BenCodez Sep 23, 2026
0e94ca4
Merge remote-tracking branch 'origin/master' into codex/pr1623-follow…
BenCodez Sep 24, 2026
7c60155
Retire acknowledged vote replay fences
BenCodez Sep 24, 2026
b80d9ba
Preserve failed vote recovery records
BenCodez Sep 24, 2026
e9db716
Make vote point accounting replay stable
BenCodez Sep 24, 2026
de0fd9b
Persist ambiguous vote recovery safely
BenCodez Sep 24, 2026
a0dd95a
Preserve durable vote and rolling period handoffs
BenCodez Sep 24, 2026
f77141f
Preserve capped totals and Bukkit vote ownership
BenCodez Sep 24, 2026
81fe601
Reserve capped monthly vote increments
BenCodez Sep 24, 2026
c7f49bb
Merge remote-tracking branch 'origin/master' into codex/pr1623-follow…
BenCodez Sep 25, 2026
0095979
Close durable vote handoff gaps
BenCodez Sep 25, 2026
41cde6a
Preserve quarantined duplicate vote fences
BenCodez Sep 25, 2026
83954a6
Freeze streak recovery site counts
BenCodez Sep 25, 2026
1f64e75
Honor reserved pending accounting
BenCodez Sep 25, 2026
a0f3800
Roll back failed vote snapshot saves
BenCodez Sep 26, 2026
8763bf2
Preserve in-flight timed votes on shutdown
BenCodez Sep 26, 2026
ed94e71
Merge master into recoverable period changes
BenCodez Sep 26, 2026
998c9be
Preserve async vote retry ownership
BenCodez Sep 26, 2026
87bc543
Preserve vote capture and receipt retirement
BenCodez Sep 26, 2026
84db044
Merge remote-tracking branch 'origin/master' into codex/voting-time-c…
BenCodez Sep 27, 2026
fd8173c
Complete cancelled vote processing signals
BenCodez Sep 27, 2026
672b456
Close vote recovery durability gaps
BenCodez Sep 27, 2026
9c086cc
Fix vote event dispatch and accounting cleanup
BenCodez Sep 27, 2026
d42eee2
Dispatch admin votes from the target owner
BenCodez Sep 27, 2026
b2631ca
Preserve failed vote completion ownership
BenCodez Sep 27, 2026
d0b7e99
Retire completed vote point receipts
BenCodez Sep 27, 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
Original file line number Diff line number Diff line change
Expand Up @@ -736,12 +736,12 @@ public void onPostLoad() {

loadVoteBroadcast();

specialRewards = new SpecialRewards(this);
loadVoteShopManager();

loadDirectlyDefined();
checkUpdate = new CheckUpdate(this);
checkUpdate.startUp();
specialRewards = new SpecialRewards(this);
signs = new Signs(this);

coolDownCheck.checkEnabled();
Expand Down Expand Up @@ -2016,7 +2016,8 @@ private void registerEvents() {
pm.registerEvents(new PlayerJoinEvent(this), this);
if (isVotifierLoaded()) {
VotiferEvent votifierEvent = new VotiferEvent(this);
votifierVoteOverflowQueue = new VotifierVoteOverflowQueue(this, votifierEvent::processVote);
votifierVoteOverflowQueue = new VotifierVoteOverflowQueue(this,
(serviceSite, username, voteId) -> votifierEvent.processQueuedVoteOutcome(serviceSite, username, voteId));
pm.registerEvents(votifierEvent, this);
}
pm.registerEvents(new PlayerVoteListener(this), this);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -341,11 +341,26 @@ PendingEnvelope peekDurable() {

/** Persists completion off the platform thread before removing the durable head. */
void acknowledgeAsync(PendingEnvelope expected, Consumer<Boolean> completion) {
completeHeadAsync(expected, null, completion);
}

/** Atomically replaces the durable head with follow-up work. */
void replaceAsync(PendingEnvelope expected, JsonEnvelope replacement, Consumer<Boolean> completion) {
PendingEnvelope pendingReplacement = pending(replacement);
if (pendingReplacement == null) {
completion.accept(false);
return;
}
completeHeadAsync(expected, pendingReplacement, completion);
}

private void completeHeadAsync(PendingEnvelope expected, PendingEnvelope replacement,
Consumer<Boolean> completion) {
PendingAcknowledgement request = null;
synchronized (lock) {
if (!closeRequested.get() && !closed && !loadFailed && expected != null && entries.peekFirst() == expected
&& pendingAcknowledgement == null && pendingFailure == null) {
request = new PendingAcknowledgement(expected, completion);
request = new PendingAcknowledgement(expected, replacement, completion);
pendingAcknowledgement = request;
}
}
Expand Down Expand Up @@ -382,13 +397,15 @@ private Boolean acknowledge(PendingAcknowledgement request) {
}
List<String> active = payloadSnapshotLocked();
active.remove(0);
if (request.replacement != null) active.add(0, request.replacement.payload);
try {
writeSnapshotLocked(active, failedSnapshotLocked());
} catch (IOException failure) {
warn("Unable to acknowledge ordered proxy vote overflow", failure);
return false;
}
entries.removeFirst();
if (request.replacement != null) entries.addFirst(request.replacement);
durableVersion = ++stateVersion;
request.stored = true;
pendingAcknowledgement = null;
Expand Down Expand Up @@ -464,7 +481,8 @@ private static boolean isOrderedVoteMessage(JsonEnvelope envelope) {
return VotingPluginWire.SUB_VOTE.equals(subChannel)
|| VotingPluginWire.SUB_VOTE_ONLINE.equals(subChannel)
|| VotingPluginWire.SUB_VOTE_UPDATE.equals(subChannel)
|| VotingPluginWire.SUB_VOTE_DELIVERY_RECEIPT_RELEASE.equals(subChannel);
|| VotingPluginWire.SUB_VOTE_DELIVERY_RECEIPT_RELEASE.equals(subChannel)
|| BackendProxyHandler.isLegacyDeliveryRetirement(envelope);
}

private void requestPersistenceLocked() {
Expand Down Expand Up @@ -689,7 +707,12 @@ private void closeBlocking() {
&& (closingFailure.expected == null || entries.peekFirst() == closingFailure.expected);
canStoreAcknowledgement = closingAcknowledgement != null && !closingAcknowledgement.stored
&& entries.peekFirst() == closingAcknowledgement.expected;
if (canStoreAcknowledgement) snapshot.remove(0);
if (canStoreAcknowledgement) {
snapshot.remove(0);
if (closingAcknowledgement.replacement != null) {
snapshot.add(0, closingAcknowledgement.replacement.payload);
}
}
if (canStoreFailure) {
if (closingFailure.expected != null) snapshot.remove(0);
if (!closingFailureAlreadyStored) failures.add(closingFailure.failed.payload);
Expand Down Expand Up @@ -733,6 +756,9 @@ private void closeBlocking() {
if (saved && canStoreAcknowledgement) {
synchronized (lock) {
entries.removeFirst();
if (closingAcknowledgement.replacement != null) {
entries.addFirst(closingAcknowledgement.replacement);
}
closingAcknowledgement.stored = true;
}
}
Expand All @@ -756,12 +782,15 @@ enum CloseState { OPEN, CLOSING, WAITING_TO_PERSIST, SNAPSHOT_REPLACED, COMPLETE

private static final class PendingAcknowledgement {
private final PendingEnvelope expected;
private final PendingEnvelope replacement;
private final Consumer<Boolean> completion;
private final AtomicBoolean completed = new AtomicBoolean();
private boolean stored;

private PendingAcknowledgement(PendingEnvelope expected, Consumer<Boolean> completion) {
private PendingAcknowledgement(PendingEnvelope expected, PendingEnvelope replacement,
Consumer<Boolean> completion) {
this.expected = expected;
this.replacement = replacement;
this.completion = completion;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,16 @@
import com.bencodez.votingplugin.backendproxy.voteparty.BackendVotePartySync;
import com.bencodez.votingplugin.proxy.BungeeMethod;
import com.bencodez.votingplugin.proxy.VotingPluginWire;
import com.bencodez.votingplugin.util.VoteTaskAdmission;
import com.bencodez.votingplugin.voteshop.service.VoteShopPurchaseService;

import lombok.Getter;

/**
* Coordinates backend/proxy communication components.
*/
public class BackendProxyHandler implements Listener {
private static final String SUB_LEGACY_DELIVERY_RETIREMENT = "LegacyVoteDeliveryRetirement";

private final VotingPluginMain plugin;
static final int MAX_ORDERED_VOTE_QUEUE = 256;
Expand Down Expand Up @@ -360,7 +363,8 @@ private void scheduleOrderedVoteDispatchLocked() {
}
orderedVoteDispatchActive = true;
try {
plugin.getBukkitScheduler().runTaskAsynchronously(plugin, this::runNextOrderedVoteDispatch);
plugin.getBukkitScheduler().runTaskAsynchronously(plugin,
VoteTaskAdmission.ownedTask(this::runNextOrderedVoteDispatch));
} catch (RuntimeException schedulingFailure) {
orderedVoteDispatchActive = false;
orderedVoteDispatch.notifyAll();
Expand Down Expand Up @@ -433,6 +437,16 @@ private void runNextOrderedVoteDispatch() {
}
};
try {
if (isLegacyDeliveryRetirement(next)) {
try {
completeLegacyDeliveryRetirement(next);
complete.accept(OrderedVoteOutcome.COMPLETE_WITHOUT_RETIREMENT);
} catch (RuntimeException retirementFailure) {
plugin.debug(retirementFailure);
complete.accept(OrderedVoteOutcome.RETRY);
}
return;
}
if (dispatchDurableReceiptRelease(messageRouter, next)) {
complete.accept(OrderedVoteOutcome.COMPLETE);
return;
Expand Down Expand Up @@ -464,16 +478,30 @@ private void finishOrderedVoteDispatch(BackendOrderedVoteOverflowQueue.PendingEn
}
return;
}
boolean successful = outcome == OrderedVoteOutcome.COMPLETE;
boolean successful = outcome == OrderedVoteOutcome.COMPLETE
|| outcome == OrderedVoteOutcome.COMPLETE_WITHOUT_RETIREMENT;
boolean retireDelivery = outcome == OrderedVoteOutcome.COMPLETE;
if (successful && retireDelivery && requiresLegacyDeliveryRetirement(envelope)) {
JsonEnvelope retirement = legacyDeliveryRetirement(envelope);
if (overflowEntry != null && orderedVoteOverflow != null) {
orderedVoteOverflow.replaceAsync(overflowEntry, retirement,
stored -> completeOrderedVoteAcknowledgement(stored, retirement, false));
} else {
replaceInMemoryVoteWithRetirement(envelope, retirement);
}
return;
}
if (successful && overflowEntry != null && orderedVoteOverflow != null) {
orderedVoteOverflow.acknowledgeAsync(overflowEntry,
stored -> completeOrderedVoteAcknowledgement(stored, envelope));
stored -> completeOrderedVoteAcknowledgement(stored, envelope, retireDelivery));
return;
}
boolean completedInMemory = false;
synchronized (orderedVoteDispatch) {
if (successful && overflowEntry == null
&& orderedVoteDispatchQueue.peekFirst() == orderedVoteDispatchInFlight) {
orderedVoteDispatchQueue.removeFirst();
completedInMemory = true;
}
orderedVoteDispatchInFlight = null;
orderedVoteOverflowInFlight = null;
Expand All @@ -482,10 +510,52 @@ private void finishOrderedVoteDispatch(BackendOrderedVoteOverflowQueue.PendingEn
if (successful) scheduleOrderedVoteDispatchLocked();
else retryOrderedVoteDispatchLocked();
}
if (successful) sendVoteDeliveryAcknowledgement(envelope);
if (completedInMemory && retireDelivery) completeVoteDelivery(envelope);
}

private void completeOrderedVoteAcknowledgement(boolean stored, JsonEnvelope envelope) {
private boolean requiresLegacyDeliveryRetirement(JsonEnvelope envelope) {
if (envelope == null || VotingPluginWire.requestsVoteDeliveryAcknowledgement(envelope)) return false;
if (!VotingPluginWire.SUB_VOTE.equals(envelope.getSubChannel())
&& !VotingPluginWire.SUB_VOTE_ONLINE.equals(envelope.getSubChannel())) return false;
return VotingPluginWire.resolveVoteId(VotingPluginWire.readVote(envelope)) != null;
}

static boolean isLegacyDeliveryRetirement(JsonEnvelope envelope) {
return envelope != null && SUB_LEGACY_DELIVERY_RETIREMENT.equals(envelope.getSubChannel());
}

private JsonEnvelope legacyDeliveryRetirement(JsonEnvelope envelope) {
UUID voteId = VotingPluginWire.resolveVoteId(VotingPluginWire.readVote(envelope));
return JsonEnvelope.builder(SUB_LEGACY_DELIVERY_RETIREMENT)
.put(VotingPluginWire.K_VOTE_ID, voteId.toString()).build();
}

private void completeLegacyDeliveryRetirement(JsonEnvelope envelope) {
VoteShopPurchaseService.completeVoteDelivery(plugin,
UUID.fromString(envelope.getFields().get(VotingPluginWire.K_VOTE_ID)));
}

private void replaceInMemoryVoteWithRetirement(JsonEnvelope envelope, JsonEnvelope retirement) {
synchronized (orderedVoteDispatch) {
if (orderedVoteDispatchInFlight != envelope || orderedVoteDispatchQueue.peekFirst() != envelope) {
orderedVoteQuarantineFailed = true;
orderedVoteDispatchPaused = true;
if (plugin != null && plugin.getLogger() != null) {
plugin.getLogger().severe("Unable to retain legacy proxy vote retirement; processing has stopped");
}
} else {
orderedVoteDispatchQueue.removeFirst();
orderedVoteDispatchQueue.addFirst(retirement);
}
orderedVoteDispatchInFlight = null;
orderedVoteOverflowInFlight = null;
orderedVoteDispatchActive = false;
orderedVoteDispatch.notifyAll();
scheduleOrderedVoteDispatchLocked();
}
}

private void completeOrderedVoteAcknowledgement(boolean stored, JsonEnvelope envelope, boolean retireDelivery) {
synchronized (orderedVoteDispatch) {
if (stored) {
orderedVoteDispatchInFlight = null;
Expand All @@ -501,7 +571,16 @@ private void completeOrderedVoteAcknowledgement(boolean stored, JsonEnvelope env
orderedVoteDispatch.notifyAll();
if (stored) scheduleOrderedVoteDispatchLocked();
}
if (stored) sendVoteDeliveryAcknowledgement(envelope);
if (stored && retireDelivery) completeVoteDelivery(envelope);
}

private void completeVoteDelivery(JsonEnvelope envelope) {
String subChannel = envelope == null ? null : envelope.getSubChannel();
if (!VotingPluginWire.SUB_VOTE.equals(subChannel)
&& !VotingPluginWire.SUB_VOTE_ONLINE.equals(subChannel)) return;
VoteShopPurchaseService.completeVoteDelivery(plugin,
VotingPluginWire.resolveVoteId(VotingPluginWire.readVote(envelope)));
Comment thread
BenCodez marked this conversation as resolved.
Resolved
Comment thread
BenCodez marked this conversation as resolved.
Resolved
sendVoteDeliveryAcknowledgement(envelope);
}

private void sendVoteDeliveryAcknowledgement(JsonEnvelope envelope) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,11 @@ public boolean reserve(UUID voteId) {
}
}

/** Releases an admission that failed before any vote side effects ran. */
public void release(UUID voteId) {
if (voteId != null) processedVotes.remove(voteId);
}

/** Persists successful processing before the backend emits a delivery acknowledgement. */
public boolean complete(UUID voteId) {
if (voteId == null) return true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import com.bencodez.simpleapi.servercomm.codec.JsonEnvelope;
import com.bencodez.simpleapi.sql.data.DataValue;
import com.bencodez.simpleapi.sql.data.DataValueBoolean;
import com.bencodez.simpleapi.sql.data.DataValueString;
import com.bencodez.simpleapi.sql.mysql.config.MysqlConfigSpigot;
import com.bencodez.votingplugin.VotingPluginMain;
import com.bencodez.votingplugin.proxy.VotingPluginWire;
Expand Down Expand Up @@ -264,6 +265,10 @@ private boolean checkGlobalDataTime(TimeType type, HashMap<String, DataValue> da
expectedHandler.setBoolean(plugin.getBungeeSettings().getServer(), type.toString(), false);
return false;
}
String transitionId = data.containsKey(VotingPluginWire.timeChangeTransitionKey(type.toString()))
? data.get(VotingPluginWire.timeChangeTransitionKey(type.toString())).getString() : "";
boolean confirmsBoundary = transitionId != null && !transitionId.isBlank()
&& !VotingPluginWire.LEGACY_TIME_CHANGE_TRANSITION.equals(transitionId);
String serverName = plugin.getBungeeSettings().getServer();
GlobalDataHandler transitionHandler = admitTimeChange(type, serverName, expectedHandler);
if (transitionHandler == null) return false;
Expand All @@ -284,7 +289,7 @@ private boolean checkGlobalDataTime(TimeType type, HashMap<String, DataValue> da
return;
}
try {
finishTimeChange(transitionHandler, type, serverName);
finishTimeChange(transitionHandler, type, serverName, transitionId, confirmsBoundary);
} catch (RuntimeException failure) {
// finishTimeChange releases the pinned handler in its finally block.
plugin.debug(failure);
Expand Down Expand Up @@ -332,10 +337,19 @@ private GlobalDataHandler admitTimeChange(TimeType type, String serverName,
}
}

private void finishTimeChange(GlobalDataHandler handler, TimeType type, String serverName) {
private void finishTimeChange(GlobalDataHandler handler, TimeType type, String serverName,
String transitionId, boolean confirmsBoundary) {
boolean completed = false;
try {
handler.setBoolean(serverName, type.toString(), false);
if (confirmsBoundary) {
HashMap<String, DataValue> completion = new HashMap<>();
completion.put(type.toString(), new DataValueBoolean(false));
completion.put(VotingPluginWire.timeChangeBoundaryCapturedKey(type.toString()),
new DataValueString(transitionId));
handler.setData(serverName, completion);
} else {
handler.setBoolean(serverName, type.toString(), false);
}
JsonEnvelope.Builder builder = JsonEnvelope.builder("TimeChangeFinished")
.schema(VotingPluginWire.SCHEMA_VERSION);
builder.put("server", serverName);
Expand Down Expand Up @@ -500,17 +514,41 @@ public void load() {
"ForceUpdate", "VARCHAR(5)").entrySet()) {
loadedHandler.getGlobalMysql().alterColumnType(column.getKey(), column.getValue());
}
for (TimeType type : TimeType.values()) {
loadedHandler.getGlobalMysql().alterColumnType(
VotingPluginWire.timeChangeBoundaryCapturedKey(type.toString()), "VARCHAR(36)");
loadedHandler.getGlobalMysql().alterColumnType(
VotingPluginWire.timeChangeTransitionKey(type.toString()), "VARCHAR(36)");
}
loadedHandler.getGlobalMysql().alterColumnType(
VotingPluginWire.TIME_CHANGE_BOUNDARY_PROTOCOL_KEY, "VARCHAR(32)");
acceptingWork = true;
timer = Executors.newSingleThreadScheduledExecutor(task -> {
Thread thread = new Thread(task, "VotingPlugin-GlobalData");
thread.setDaemon(true);
return thread;
});
timer.scheduleWithFixedDelay(this::checkGlobalData, 60, 10, TimeUnit.SECONDS);
timer.scheduleWithFixedDelay(this::updateLastOnline, 1, 60, TimeUnit.MINUTES);
publishBoundaryProtocolHeartbeat();
timer.scheduleWithFixedDelay(this::publishBoundaryProtocolHeartbeat, 1, 60, TimeUnit.MINUTES);
plugin.getTimeChecker().setProcessingEnabled(false);
}

private void publishBoundaryProtocolHeartbeat() {
GlobalDataHandler pollingHandler = acquireHandlerReference();
if (pollingHandler == null) return;
try {
String heartbeat = "" + LocalDateTime.now().atZone(ZoneOffset.UTC).toInstant().toEpochMilli();
HashMap<String, DataValue> state = new HashMap<>();
state.put("LastOnline", new DataValueString(heartbeat));
state.put(VotingPluginWire.TIME_CHANGE_BOUNDARY_PROTOCOL_KEY,
new DataValueString(VotingPluginWire.timeChangeBoundaryProtocolHeartbeat(heartbeat)));
pollingHandler.setData(plugin.getBungeeSettings().getServer(), state);
} finally {
releaseHandlerReference(pollingHandler);
}
}

public void close() {
close(CLOSE_GRACE_SECONDS, TimeUnit.SECONDS);
}
Expand Down
Loading
Loading