Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
bca3881
Add durable owner-configured DateVoteMilestones
BenCodez Oct 8, 2026
3c29bd8
Respect reward processing and preserve queued date milestone handles
BenCodez Oct 8, 2026
1f787df
Initialize date vote definitions before proxy ingress
BenCodez Oct 8, 2026
073634b
Reserve date milestone rewards individually before submission
BenCodez Oct 8, 2026
bd3c738
Open proxy ingress after accepted vote handlers initialize
BenCodez Oct 8, 2026
ff41566
Retain canonical proxy envelopes when date accounting is incomplete
BenCodez Oct 8, 2026
31f5f2e
Require canonical transport identity and preserve original date vote …
BenCodez Oct 8, 2026
e2f5503
Preserve normal proxy effects and explicit date accounting identity
BenCodez Oct 8, 2026
57ebcbc
Gate local date ingress and validate ordered progress filters
BenCodez Oct 8, 2026
897f386
Start recovered date vote replay after its consumer is ready
BenCodez Oct 8, 2026
1535e27
Separate date occurrence provenance from normal vote processing time
BenCodez Oct 9, 2026
87545cc
Capture startup Votifier votes before reward pipeline readiness
BenCodez Oct 9, 2026
26441c1
Persist local vote occurrence identities across queue replay
BenCodez Oct 9, 2026
63d9a58
Retain captured votes across cancelled executor admission
BenCodez Oct 9, 2026
d6d6dbc
Ship an explicitly disabled date milestone example
BenCodez Oct 9, 2026
b4aa30d
Retain started Votifier receipts through shutdown cancellation
BenCodez Oct 9, 2026
42f5819
Normalize added date milestone lines for diff checks
BenCodez Oct 11, 2026
cf51409
Retain bounded vote captures across failed overflow transfers
BenCodez Oct 11, 2026
42398de
Retain overflow receipts until vote processing confirms completion
BenCodez Oct 12, 2026
f0cc7f1
Preserve date milestone receipts and file ownership across reloads
BenCodez Oct 12, 2026
013c9bb
Align date milestones and add guarded event progress menus
BenCodez Oct 14, 2026
4aa8f3c
Retain captured Votifier votes in bounded shutdown snapshots
BenCodez Oct 14, 2026
071ba3e
Retain capture retirement while queue ownership is pending
BenCodez Oct 14, 2026
0061705
Preserve date-event receipt ownership and integrate current lifecycle
BenCodez Oct 15, 2026
18f2fe9
Retain saturated Votifier receipts until overflow initialization
BenCodez Oct 16, 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
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,7 @@ Control behavior in production.

- Trace whether the code runs on the connector worker, proxy thread, Bukkit primary thread, or a SQL executor.
- Preserve queued votes across saturation, reload, runtime replacement, scheduler rejection/cancellation, shutdown, and restart; overflow handling must be bounded, durable when promised, and observable rather than silently dropping work. Scheduler admission is never proof of durable ownership.
- Local Votifier captures remain owned after STARTED until normal callback completion or confirmed overflow transfer. Transfer execution stays fenced while the original callback runs; successful non-interrupted completion reconciles only its exact occurrence, while interruption/exception or failed transfer retains bounded recovery ownership. A callback completing after the final overflow snapshot is an explicit uncertain reconciliation case; a retired queue must not overwrite a successor, and ordinary partial external effects retain the existing at-least-once boundary.
- 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
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,5 @@ or disabling it; inspections gate disabled state immediately and report a newly
```

`LATEST` resolves to the latest stable release.

Owner-defined [DateVoteMilestones](docs/date-vote-milestones.md) count qualifying accepted votes within explicit date windows, independently of normal totals. This optional feature is disabled until events are configured.
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,12 @@ private synchronized AtomicBoolean basicBungeeUpdateAdmission() {
private VotingPluginConfigHealth configHealth;
private VotifierIntegration votifierIntegration;
@Getter
private VotifierVoteOverflowQueue votifierVoteOverflowQueue;
private volatile VotifierVoteOverflowQueue votifierVoteOverflowQueue;
private VotiferEvent localVotifierListener;
private boolean acceptedVoteIngressRegistered;
private boolean timeQueueIngressRegistered;
private volatile boolean localVotifierIngressReady;
private volatile boolean localVotifierIngressClosed;
private VoteLogManager voteLogManager;
private VotingPluginWebhookManager webhookManager;

Expand Down Expand Up @@ -613,6 +618,11 @@ private void loadBungeeHandler() {
@Getter
private VoteStreakHandler voteStreakHandler;

@Getter
private final com.bencodez.votingplugin.specialrewards.datemilestones.DateVoteMilestones dateVoteMilestones =
new com.bencodez.votingplugin.specialrewards.datemilestones.DateVoteMilestones(this);


/**
* Registers a directly editable rewards path from SpecialRewards.yml.
*
Expand All @@ -626,6 +636,7 @@ public void loadDirectlyDefined() {
VoteSiteRewardRegistrar.register(this);
VotePartyRewardRegistrar.register(this);
VoteMilestoneRewardRegistrar.register(this);
dateVoteMilestones.reload();
Comment thread
BenCodez marked this conversation as resolved.
VoteStreakRewardRegistrar.register(this);
TopVoterRewardRegistrar.register(this);
VoteShopRewardRegistrar.register(this);
Expand Down Expand Up @@ -697,8 +708,36 @@ private void metrics() {
new VotingPluginMetrics().load(plugin);
}

/** Publish accepted and transport ingress only after the complete vote pipeline is initialized. */
void initializeDateVoteIngress(Runnable openProxy) {
if (localVotifierIngressClosed) throw new IllegalStateException("Vote ingress is shutting down");
if (localVotifierIngressReady) return;
if (voteMilestonesManager == null || voteStreakHandler == null || voteParty == null || specialRewards == null
|| topVoterHandler == null || voteShopManager == null || placeholders == null) {
throw new IllegalStateException("Accepted-vote handlers must initialize before transport ingress");
}
getDateVoteMilestones().reload();
if (!acceptedVoteIngressRegistered) {
getServer().getPluginManager().registerEvents(new PlayerVoteListener(this), this);
acceptedVoteIngressRegistered = true;
}
// Construction itself schedules persisted replay. Open it only after its consumer.
if (!timeQueueIngressRegistered && (!getBungeeSettings().isUseBungeecoord() || !getBungeeSettings().isGloblalDataEnabled())) {
timeQueueHandler = new TimeQueueHandler(this);
getServer().getPluginManager().registerEvents(timeQueueHandler, this);
timeQueueIngressRegistered = true;
}
if (getBungeeSettings().isUseBungeecoord()) openProxy.run();
Comment thread
BenCodez marked this conversation as resolved.
Comment thread
BenCodez marked this conversation as resolved.
localVotifierIngressReady = true;
VotifierVoteOverflowQueue overflow = votifierVoteOverflowQueue;
if (overflow != null) overflow.start();
}

@Override
public void onPostLoad() {
loadVoteTimer();
checkVotifier();
registerEarlyVotifierIngress();
ensureCommunicationSecret();
// auto conversion for Shop.yml
if (plugin.getShopFile().isJustCreated()) {
Expand Down Expand Up @@ -726,7 +765,6 @@ public void onPostLoad() {

voteTester = new VoteTester(plugin);

loadVoteTimer();
BoundedScheduledExecutor setupReminderWorker = new BoundedScheduledExecutor(1, 4);
setupReminderWorker.setThreadFactory(runnable -> {
Thread worker = new Thread(runnable, "VotingPlugin-SiteSetupReminder");
Expand All @@ -739,13 +777,8 @@ public void onPostLoad() {
autoCreateVoteSitesSetupReminder.start();
getVotingPluginUserManager().startSharedPointTransferRecovery();

if (bungeeSettings.isUseBungeecoord()) {
loadBungeeHandler();
}
// Proxy ingress opens below, after every accepted-vote handler is initialized.

if (!bungeeSettings.isUseBungeecoord() || !bungeeSettings.isGloblalDataEnabled()) {
this.timeQueueHandler = new TimeQueueHandler(this);
}

if (Bukkit.getPluginManager().getPlugin("PlaceholderAPI") != null) {
if (getConfigFile().isLoadInteralExpansion()) {
Expand All @@ -760,7 +793,6 @@ public void onPostLoad() {
voteStreakHandler.reload();

registerCommands();
checkVotifier();
registerEvents();
refreshPlaceholderPlayerPresence();

Expand Down Expand Up @@ -850,11 +882,7 @@ public Object getObject(OfflinePlayer player) {
}

VotingPluginRewardRegistrar.register(this);
// Recovered Votifier votes may now traverse the fully initialized vote,
// reward, placeholder, shop, and vote-party pipeline.
if (votifierVoteOverflowQueue != null) {
votifierVoteOverflowQueue.start();
}
initializeDateVoteIngress(this::loadBungeeHandler);
Comment thread
BenCodez marked this conversation as resolved.

plugin.getLogger().info("Enabled VotingPlugin " + plugin.getDescription().getVersion());
if (plugin.getDescription().getVersion().contains("SNAPSHOT")) {
Expand Down Expand Up @@ -1995,6 +2023,9 @@ private void shutdownVoteReminders() {

@Override
public void onUnLoad() {
localVotifierIngressClosed = true;
localVotifierIngressReady = false;
if (localVotifierListener != null) localVotifierListener.stop();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Persist retained captures before closing the overflow

When the durable overflow already contains 256 recovered entries, or has less free capacity than the capture registry needs, this stop() only starts asynchronous handoffs; rejected admissions remain solely in captured and schedule a retry. onUnLoad() has already disabled overflow processing and soon closes its worker, so the queue cannot drain to make room and those retries cannot transfer the remaining accepted votes before the listener is discarded. Unlike the earlier stuck-transferring issue, the fresh evidence here is the shutdown ordering that terminates the repaired retry path; shutdown must wait for confirmed handoff or include retained captures in a sufficiently bounded final durable snapshot.

AGENTS.md reference: AGENTS.md:L326-L327

Useful? React with 👍 / 👎.

stopAutoCreateVoteSitesSetupReminder();
placeholderPlayerPresence.clear();
stopBackendHostedControlLifecycle();
Expand Down Expand Up @@ -2076,19 +2107,24 @@ private void registerCommands() {

}

/**
* Register events.
*/
/** Capture external events early; only the existing bounded workers may buffer/process them. */
void registerEarlyVotifierIngress() {
if (localVotifierIngressClosed || !isVotifierLoaded() || localVotifierListener != null) return;
VotiferEvent listener = new VotiferEvent(this, () -> localVotifierIngressReady);
// Publish the memory owner before capture. Its independent worker loads
// disk state; vote-executor cancellation cannot discard queue setup.
votifierVoteOverflowQueue = VotifierVoteOverflowQueue.initializeConfirmedAsync(this,
listener::processVoteConfirmed, () -> localVotifierIngressReady && !localVotifierIngressClosed);
localVotifierListener = listener;
if (localVotifierIngressReady) votifierVoteOverflowQueue.start();
getServer().getPluginManager().registerEvents(listener, this);
}

private void registerEvents() {
PluginManager pm = getServer().getPluginManager();

pm.registerEvents(new PlayerJoinEvent(this), this);
if (isVotifierLoaded()) {
VotiferEvent votifierEvent = new VotiferEvent(this);
votifierVoteOverflowQueue = new VotifierVoteOverflowQueue(this, votifierEvent::processVote);
pm.registerEvents(votifierEvent, this);
}
pm.registerEvents(new PlayerVoteListener(this), this);
pm.registerEvents(new com.bencodez.votingplugin.listeners.DateVoteMenuOpenGuard(), this);
pm.registerEvents(new PlayerPostVoteLoggerListener(this), this);
pm.registerEvents(new PlayerSpecialRewardLoggerListener(this), this);
pm.registerEvents(new VoteShopPurchaseLoggerListener(this), this);
Expand All @@ -2102,9 +2138,6 @@ private void registerEvents() {
pm.registerEvents(new PlayerInteract(this), this);
}

if (timeQueueHandler != null) {
pm.registerEvents(timeQueueHandler, plugin);
}

pm.registerEvents(new VotingPluginUpdateEvent(this), this);
/*
Expand Down Expand Up @@ -2182,6 +2215,8 @@ private void reloadPlugin(boolean userStorage, boolean reconcileHostedControl,
// Re-evaluate after storage has reloaded; UserManager keeps this lifecycle task unique.
getVotingPluginUserManager().startSharedPointTransferRecovery();

getOptions().setServer(bungeeSettings.getServer());
getDateVoteMilestones().reload();
reloadBackendProxyRuntime(updateActiveBackendRuntime, userStorage);
checkYMLError();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -569,7 +569,8 @@ private void handleVoteBroadcast(JsonEnvelope msg) {
}

String totalsRaw = nvl(fields.get(VotingPluginWire.K_TOTALS));
VoteTotalsSnapshot totals = totalsRaw.isEmpty() ? null : VoteTotalsSnapshot.parseStorage(totalsRaw);
VoteTotalsSnapshot totals = !com.bencodez.votingplugin.proxy.VoteOccurrenceMetadata.hasPayload(totalsRaw)
? null : VoteTotalsSnapshot.parseStorage(totalsRaw);
VoteSite voteSite = plugin.getVoteSiteManager()
.getVoteSite(plugin.getVoteSiteManager().getVoteSiteName(true, service), true);
if (voteSite == null) {
Expand Down Expand Up @@ -715,16 +716,29 @@ private WireVoteResult handleWireVote(JsonEnvelope msg) {
user.cache();
boolean wasOnline = vote.wasOnlineKnown ? vote.wasOnline : user.isOnline();
boolean queuedDelivery = vote.queuedDeliveryKnown ? vote.queuedDelivery : vote.delayValidated;
var event = user.bungeeVotePluginMessagingEvent(vote.service, vote.time, totals, !vote.manageTotals,
wasOnline, vote.broadcast, vote.num, queuedDelivery, vote.delayValidationKnown,
vote.queuedDeliveryKnown, voteId,
VotingPluginWire.SUB_VOTE_ONLINE.equals(msg.getSubChannel()));
if ((event == null || !event.isCancelled()) && plugin.getBungeeSettings().isPerServerPoints()) {
com.bencodez.votingplugin.events.PlayerVoteEvent dispatchedEvent = null;
VotingPluginUser.DateMilestoneAccountingException dateAccountingFailure = null;
try {
// Keep totals correlation for replay fencing, but only the original wire ID
// identifies a canonical transport occurrence in the native event.
dispatchedEvent = user.bungeeVotePluginMessagingEvent(vote.service, vote.time, totals, !vote.manageTotals,
wasOnline, vote.broadcast, vote.num, queuedDelivery, vote.delayValidationKnown,
vote.queuedDeliveryKnown, vote.voteId,
VotingPluginWire.SUB_VOTE_ONLINE.equals(msg.getSubChannel()), vote.realVote,
vote.canonicalOccurrenceTime);
} catch (VotingPluginUser.DateMilestoneAccountingException incomplete) {
dateAccountingFailure = incomplete;
}
if ((dispatchedEvent == null || !dispatchedEvent.isCancelled())
&& plugin.getBungeeSettings().isPerServerPoints()) {
user.addPoints(plugin.getConfigFile().getPointsOnVote());
}
if (vote.service != null && !vote.service.isEmpty()) {
plugin.getServerData().addServiceSite(vote.service);
}
// Date accounting cannot suppress normal points/service observations. Keep
// the reservation incomplete so reconciliation cannot replay normal rewards.
if (dateAccountingFailure != null) throw dateAccountingFailure;
return new WireVoteResult(voteId, true, false);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3486,7 +3486,15 @@ public void updateReplacements() {
*/
private void loadVoteCommand() {
plugin.setVoteCommand(new ArrayList<>());
plugin.getVoteCommand().add(new CommandHandler(plugin, new String[] { "Help&?" },
plugin.getVoteCommand().add(new CommandHandler(plugin, new String[] { "DateEvents" },
"VotingPlugin.Commands.Vote.DateEvents|" + playerPerm, "View date-window vote milestone progress") {
@Override public void execute(CommandSender sender, String[] args) {
if (sender instanceof Player player) plugin.getDateVoteMilestones().progress(player);
else sender.sendMessage("Run /vote dateevents as a player.");
}
});

plugin.getVoteCommand().add(new CommandHandler(plugin, new String[] { "Help&?" },
"VotingPlugin.Commands.Vote.Help|" + playerPerm, "View help page") {

@Override
Expand Down
Loading
Loading