Skip to content

Broadcast proxy votes immediately in FORWARD mode - #1541

Merged
BenCodez merged 26 commits into
masterfrom
agent/forward-offline-proxy-broadcasts
Aug 8, 2026
Merged

Broadcast proxy votes immediately in FORWARD mode#1541
BenCodez merged 26 commits into
masterfrom
agent/forward-offline-proxy-broadcasts

Conversation

@BenCodez

@BenCodez BenCodez commented Aug 8, 2026

Copy link
Copy Markdown
Owner

Summary

  • forward accepted offline vote broadcasts immediately when ProxyBroadcast.OfflineMode is FORWARD
  • preserve original broadcast targets, per-backend delivery, reward delivery, and queued routing state across JSON and SQL caches
  • retry server-cache and voter-keyed online-cache broadcasts whenever a target backend gains a plugin-message carrier
  • record delivery only after transport confirmation: plugin-message acceptance, MySQL insert success, MQTT publish success, Redis subscriber acknowledgement, or socket connect/write completion
  • suppress cached non-target broadcasts and never recompute the original offline scope from the player's later login location
  • complete GlobalData rollover callbacks before loading totals, reserve accepted queued votes before forwarding, and validate vote delay before any announcement

Root cause

WaitForUserOnline: true coupled reward delivery to proxy broadcast timing. Aggregate forwarded state then lost the distinction between original targets, successful targets, pending targets, non-target reward servers, and reward delivery.

The GlobalData path also loaded acceptance/totals state across rollover callbacks, allowing announcements without a reserved delay slot and stale pre-reset totals to be written back after rollover completion.

User impact

With OfflineMode: FORWARD, an accepted offline vote is announced immediately to every reachable backend in the configured scope while its reward may remain queued until the voter joins.

Successful targets are not retried, failed targets remain pending and retry when transport becomes available, non-target cached rewards cannot rebroadcast the vote, and reward delivery happens at most once. Legacy cache entries retain their previous pending-broadcast behavior.

Validation

  • Java 21 Maven build and all 181 tests passed in GitHub Actions
  • parsed all modified Java sources successfully
  • repository whitespace checks passed
  • verified every published branch tree exactly matched the validated local tree
  • addressed all 15 review findings and started a fresh Codex review on the latest head

AI disclosure

This PR was created with assistance from OpenAI Codex.

@BenCodez
BenCodez marked this pull request as ready for review August 8, 2026 14:16

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 51896bc158

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1816 to +1819
boolean immediateProxyBroadcast = proxyBroadcastDecider.usesImmediateForwarding();
if (immediateProxyBroadcast) {
Set<String> targets = proxyBroadcastDecider.resolveTargets(playerOnline, playerServer);
sendProxyBroadcast(targets, uuid, player, service, time, text == null ? "" : text.toString());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Restrict standalone forwarding to offline voters

When OfflineMode is FORWARD but the voter is online and the scope targets multiple servers, this sends a standalone broadcast to every target and later suppresses broadcasting from the normal vote envelopes. Unlike the normal Bungee vote path, which preserves the proxy-wide event.isWasOnline() value, the standalone handler in BungeeHandler uses user.isOnline() on each receiving backend; therefore EVERY_VOTE_ONLINE_ONLY drops the announcement on every target except the backend currently hosting the voter. Immediate standalone forwarding should apply only when playerOnline is false.

Useful? React with 👍 / 👎.

Comment on lines +565 to +566
if (proxyBroadcastDecider.usesImmediateForwarding()) {
broadcastHere = false;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve broadcasts for pre-existing cached votes

When upgrading or reloading with FORWARD already configured—or when changing from QUEUE to FORWARD while votes remain cached—these cached entries predate the new standalone broadcast and were never announced. Unconditionally setting broadcastHere to false when the player logs in silently discards their pending announcements before removing the cached votes; suppression needs to distinguish votes that were actually broadcast immediately.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 25a108d62b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1828 to +1830
getVoteCacheHandler().addServerVote(s,
new OfflineBungeeVote(voteId, player, uuid, service, time, realVote, text.toString()));
new OfflineBungeeVote(voteId, player, uuid, service, time, realVote,
text.toString(), immediateProxyBroadcast));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Do not mark undelivered plugin broadcasts forwarded

When PLUGINMESSAGING is used and a targeted backend currently has no connected players, sendPluginMessageServer skips sending the standalone message, but this cached vote is still persisted with broadcastForwarded=true. When someone later joins and checkCachedVotes releases the vote, that flag suppresses the normal envelope's broadcast, so the announcement is permanently lost. Only record the broadcast as forwarded for targets where delivery was possible, or leave it pending in this case.

Useful? React with 👍 / 👎.

Comment on lines +482 to +483
globalMessageProxyHandler.sendMessage(targetServer, delay,
VotingPluginWire.voteBroadcast(uuid, player, service, time, text));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve the offline state in standalone broadcasts

The new offline-only gate does not put the sampled playerOnline=false state into this standalone envelope, so BungeeHandler instead evaluates user.isOnline() when the delayed message arrives. If the voter reconnects during the scheduler or broker delay, a backend configured with EVERY_VOTE_ONLINE_ONLY broadcasts a vote that was received while the player was offline. This delivery-time state check is fresh evidence that the earlier online-gating fix is incomplete; the envelope needs to carry the original online state.

Useful? React with 👍 / 👎.

Comment on lines +222 to +225
private void addBroadcastForwardedColumnIfMissingOnce() {
final String key = getDbType() + ":" + getTableName() + ":broadcastForwarded";
if (!MIGRATED_BROADCAST_FORWARDED.add(key)) {
return;

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 Include the database in the migration guard

The static guard is keyed only by database type and table name, although the supported reloadall flow tears down MySQL and reconnects using the newly loaded configuration. If an administrator switches the vote-cache connection to another existing database with the same prefix during that reload, this key is already present, so the new database never receives broadcastForwarded; subsequent inserts and reads reference the absent column and cached votes can be lost. The equivalent guard in ProxyOnlineVoteCacheTable has the same defect, so both keys need to distinguish the actual database/schema (and should only be retained after a successful migration).

Useful? React with 👍 / 👎.

Comment on lines +1807 to +1810
boolean immediateProxyBroadcast = proxyBroadcastDecider.usesImmediateForwarding(playerOnline);
if (immediateProxyBroadcast) {
Set<String> targets = proxyBroadcastDecider.resolveTargets(playerOnline, playerServer);
sendProxyBroadcast(targets, uuid, player, service, time, text == null ? "" : text.toString());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Forward broadcasts before entering the time-change queue

On Bungee, normal vote events call this method with timeQueue=true, and an active GlobalData time change returns at line 1654 after placing the vote in timeChangeQueue. Because the new immediate-forwarding block is below that return, an offline vote configured with OfflineMode=FORWARD is not broadcast until processQueue eventually replays it, which can delay the supposedly immediate announcement for the entire rollover operation. The forwarding state needs to be handled before this queueing return, while ensuring the replay does not broadcast it again.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 611cf633dc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1860 to +1861
new OfflineBungeeVote(voteId, player, uuid, service, time, realVote,
text.toString(), broadcastForwardedServers.contains(s)));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Mark non-target cached votes as already handled

When SendVotesToAllServers and WaitForUserOnline are enabled with Scope.Mode: PLAYER_SERVER, an offline vote successfully forwarded to a designated server such as the lobby is cached for every other server with broadcastForwarded=false. If the voter later joins one of those servers, checkCachedVotes recomputes PLAYER_SERVER using the new online location and broadcasts the already-announced vote again. Track that standalone routing was handled independently of whether this particular reward server was a forwarding target.

Useful? React with 👍 / 👎.

Comment on lines +1919 to +1923
boolean broadcastForwarded = standaloneProxyBroadcast && !proxyBroadcastTargets.isEmpty()
&& broadcastForwardedServers.containsAll(proxyBroadcastTargets);
getVoteCacheHandler().addOnlineVote(uuid,
new OfflineBungeeVote(voteId, player, uuid, service, time, realVote, text.toString()));
new OfflineBungeeVote(voteId, player, uuid, service, time, realVote, text.toString(),
broadcastForwarded));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve per-target state in the online vote cache

With SendVotesToAllServers: false, plugin messaging, and multiple offline-forward targets, a partial delivery is collapsed to broadcastForwarded=false unless every target succeeded. On login, checkOnlineVotes sends only one vote envelope to the player's current server and never retries the failed offline targets; it can also duplicate the announcement if that current server was among the successful targets. Persist the successful or pending target set for online-cache entries rather than reducing it to one aggregate boolean.

Useful? React with 👍 / 👎.

Comment on lines +1742 to +1744
Set<String> targets = proxyBroadcastDecider.resolveTargets(false, null);
broadcastForwardedServers.addAll(
sendProxyBroadcast(targets, uuid, player, service, time, "", false));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Validate queued votes before announcing them

During an active GlobalData time change, an offline FORWARD vote is broadcast here before the vote-delay check runs. If the vote is a duplicate that checkVoteDelay rejects when processQueue replays it, users have already seen an announcement for a vote that receives no normal processing or reward. The acceptance checks need to occur before this forwarding, or the announcement must remain pending until the queued vote is accepted.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a80c53edef

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1794 to +1795
broadcastForwardedServers.addAll(
sendProxyBroadcast(broadcastTargets, uuid, player, service, time, "", false));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reserve queued votes before forwarding their broadcasts

During an active GlobalData rollover with an offline voter and SendVotesToAllServers: false, two same-service votes can both pass this pre-forward delay check because the first vote exists only in timeChangeQueue; getLastVotesTime scans the database and the online/server caches but not that queue. Both votes are therefore announced here, while replaying the first adds it to the online cache and causes the second to be rejected without a reward. Fresh evidence beyond the earlier delay-order report is the missing timeChangeQueue lookup, so an accepted queued vote must reserve its delay slot before its standalone broadcast is sent.

Useful? React with 👍 / 👎.

Comment on lines 519 to +520
if (toSend) {
boolean broadcastHere = true;
if (getConfig().getProxyBroadcastEnabled()) {
boolean broadcastHere = cache.needsBroadcastOn(server);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Retry pending broadcasts before waiting for the voter

With PLUGINMESSAGING, SendVotesToAllServers, and WaitForUserOnline enabled, a FORWARD target with no connected carrier initially remains pending as intended, but when another player joins that target, checkCachedVotes rejects the entry at the toSend gate because the voter is still offline, so this new needsBroadcastOn path is never reached. The announcement then waits until the voter personally joins that backend, defeating FORWARD even though a carrier is available; retry the standalone pending target independently of the reward-delivery gate.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 18cb5269da

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +491 to +492
globalMessageProxyHandler.sendMessage(targetServer, delay, envelope);
forwarded.add(targetServer);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Record forwarding only after transport success

With a non-plugin transport, a failed send is still recorded as forwarded: for example, the MYSQL branch catches sendToBackend's SQLException inside GlobalMessageProxyHandler.sendMessage, after which this code unconditionally adds the target. A queued or cached vote will then persist that target as delivered and suppress its later normal-envelope broadcast. Fresh evidence beyond the earlier plugin-messaging case is that every other transport still uses a void wrapper that cannot report failure; only add the target when the selected transport confirms that it accepted the message.

Useful? React with 👍 / 👎.

Comment on lines +582 to +587
if (cache.isProxyBroadcastHandled()) {
Set<String> pendingTargets = new LinkedHashSet<>(cache.getBroadcastTargets());
pendingTargets.removeAll(cache.getBroadcastForwardedServers());
cache.getBroadcastForwardedServers().addAll(sendProxyBroadcast(pendingTargets,
cache.getUuid(), cache.getPlayerName(), cache.getService(), cache.getTime(),
cache.getText(), false));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Retry online-cache broadcasts when targets gain carriers

With PLUGINMESSAGING and SendVotesToAllServers: false, an offline vote is stored only in the voter-keyed online cache, so if an immediate target has no carrier this retry block cannot run when some other player later joins that target: checkOnlineVotes is invoked with the joining player's UUID, while checkCachedVotes scans only server caches. The pending broadcast therefore remains queued until the voter logs in or is online during a periodic check, contrary to FORWARD's voter-independent delivery. Fresh evidence beyond the earlier per-target persistence report is that the newly persisted pending state has no target-driven retry path for online-cache entries.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: db01da12c7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

redisHandler.publishEnvelope(getConfig().getRedisPrefix() + "VotingPlugin_" + server, envelope);
// RedisHandler currently swallows publish failures and exposes no acknowledgement,
// so the standalone delivery must remain pending.
return false;

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 Stop retrying successfully published broadcasts

With REDIS and offline FORWARD enabled, publishEnvelope can successfully deliver the standalone announcement, but this unconditional false leaves the target marked pending. In SendVotesToAllServers mode, the subsequent normal vote envelope therefore also sets broadcastHere=true, producing an immediate duplicate; voter-keyed cached votes are retained and republished on later logins as well. The identical unconditional failure result after client.sendEnvelope causes the same socket behavior. Fresh evidence after the earlier transport-success report is that the new implementation deliberately reports failure even after invoking a successful send.

Useful? React with 👍 / 👎.

// Totals processing (primary server OR no multiproxy)
if (getConfig().getPrimaryServer() || !getConfig().getMultiProxySupport()) {
if (getConfig().getBungeeManageTotals()) {
data = getProxyMySQL().getExactQuery(new Column("uuid", new DataValueString(uuid)));

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 Refresh totals after completing the rollover

When a vote arrives just as a GlobalData rollover finishes, data is loaded here before checkForFinishedTimeChanges(). That call can invoke onTimeChangedFinished, which wipes the period-total column and replays queued votes; if the second isTimeChangedHappened() check is then false, this vote continues using the pre-wipe snapshot and writes the old total back over both the reset and any replayed increments. Fresh evidence beyond the earlier queue-order reports is this stale database snapshot crossing the synchronous rollover-completion callback; complete the rollover before loading data, or reload it afterward.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e8abc3ecc6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +653 to 655
if (deliveredReward && getConfig().getMultiProxySupport()
&& getConfig().getMultiProxyOneGlobalReward()) {
multiProxyHandler.sendClearVote(uuid, player);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve pending broadcasts when clearing multi-proxy rewards

When MultiProxyOneGlobalReward is enabled and a voter-keyed vote still has failed FORWARD targets on any proxy, delivering the reward here broadcasts ClearVote. MultiProxyHandler handles that message through clearVote(uuid), whose implementation calls removeOnlineVotes(uuid), deleting the retained incomplete entries from every proxy; consequently, a target that lacked a plugin-message carrier during the initial attempt is never retried. Clear the reward eligibility without removing cache entries whose proxy broadcast is still incomplete.

Useful? React with 👍 / 👎.

Comment on lines +1568 to +1569
try (Jedis jedis = new Jedis(new HostAndPort(getConfig().getRedisHost(), getConfig().getRedisPort()),
config.build())) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reuse the Redis connection for message delivery

With BungeeMethod.REDIS, every call routed through globalMessageProxyHandler now constructs a fresh blocking Jedis connection, including ordinary vote, update, and status envelopes—not just standalone broadcasts. During a Redis outage, the per-target loops execute these calls serially with a two-second connection timeout while vote is synchronized, so a network with many backends can stall all vote processing for tens of seconds per vote. Use a reusable/pool-backed publisher that can return the Redis PUBLISH result instead of reconnecting for every envelope.

Useful? React with 👍 / 👎.

Comment on lines +165 to +166
+ qi("broadcastForwardedServers") + " = ?, " + qi("rewardDelivered") + " = ? WHERE "
+ qi("uuid") + " = ? AND " + qi("service") + " = ? AND " + qi("time") + " = ?;";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Key delivery-state updates by vote ID

When two cached votes for the same UUID and service receive the same millisecond timestamp, this predicate updates both rows with one vote's forwarding and reward state even though each row has a distinct voteid. This can occur with back-to-back votes when vote-delay enforcement is disabled, and after a reload one vote may consequently suppress a broadcast as already forwarded. The server-cache SQL updater and JSON update paths use the same non-unique tuple, so use voteid whenever it is available and retain the tuple only as a legacy fallback.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a0b8a935ef

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1626 to +1627
try (Socket socket = new Socket()) {
socket.connect(new InetSocketAddress(host, port), 2000);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reuse the configured socket clients for envelope delivery

With BungeeMethod=SOCKETS, globalMessageProxyHandler routes ordinary vote, update, and status envelopes here as well as standalone broadcasts, so every envelope now creates and synchronously connects a fresh socket. Because vote() is synchronized and sends to backend servers serially, each unreachable target can incur this two-second timeout for every attempted envelope, stalling all vote processing across a multi-server network; reuse the already initialized clientHandles or another persistent/pool-backed sender while obtaining delivery confirmation.

Useful? React with 👍 / 👎.

Comment on lines +1581 to +1583
if (publisherPool == null || System.currentTimeMillis() < redisPublisherRetryAfter) {
return false;
}

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 Do not drop normal Redis envelopes during the retry cooldown

With BungeeMethod=REDIS, one transient publish exception sets this global cooldown, after which every envelope for every target returns false for two seconds without attempting Redis. The caller in globalMessageProxyHandler ignores that result for ordinary vote and update envelopes, so in a SendVotesToAllServers loop a single stale pooled connection can cause the remaining backend rewards to be silently skipped without being cached; limit backoff to retryable standalone broadcasts or make normal delivery failures enter the vote cache rather than dropping them.

Useful? React with 👍 / 👎.

Comment on lines +683 to +685
if (cache.getBroadcastForwardedServers().addAll(forwarded)) {
cache.setBroadcastForwarded(cache.isProxyBroadcastComplete());
getVoteCacheHandler().updateOnlineVote(cachedUuid, cache);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Remove completed broadcast-only online-cache entries

When MultiProxyOneGlobalReward clears a vote whose standalone broadcast is still incomplete, clearOnlineVoteRewards correctly retains it with rewardDelivered=true; however, when this retry later delivers the last target, the code only persists the now-complete entry. If the voter remains offline, checkOnlineVotes never removes it, so it remains in memory and SQL/JSON until the optional age purge and is scanned for every subsequent login by retryPendingOnlineBroadcasts; remove the entry once both rewardDelivered and isProxyBroadcastComplete() are true.

Useful? React with 👍 / 👎.

Comment on lines +1959 to +1962
getVoteCacheHandler().getTimeChangeQueue().add(delayedVote);
if (proxyBroadcastHandled) {
Set<String> forwarded = sendProxyBroadcast(broadcastTargets, uuid, player, service, time, "",
false);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Persist rollover votes before announcing them

With an active GlobalData rollover and offline FORWARD mode, this adds the accepted vote only to the in-memory queue and then immediately publishes its announcement. Timed queue rows are serialized only by VoteCacheHandler.saveVoteCache(), which is called during graceful disable rather than when the vote is queued, so a proxy crash after this send but before rollover replay leaves users with an announced vote that disappears on restart and never updates totals or rewards; durably store the queued vote and its delivery state before publishing the standalone broadcast.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b4f9c2565d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1478 to +1479
vote(vote.getName(), vote.getService(), true, false, vote.getTime(), null, null, vote);
getVoteCacheHandler().removeTimeVote(vote);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Retain rollover votes when replay does not complete

When replay exits early—for example because getProxyMySQL() is unavailable—or catches an exception internally, vote(...) returns void, so this unconditional removal deletes the durable timed entry despite no totals or rewards being processed. Because FORWARD may already have announced the vote before the rollover finished, a transient replay failure permanently loses the corresponding vote; make replay report success and remove the entry only after successful processing.

Useful? React with 👍 / 👎.

Comment on lines +560 to +562
timeChangeQueue.remove(vote);
if (useMySQL) {
timedVoteCacheTable.removeVote(vote);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Confirm SQL deletion before dropping in-memory queue state

With MySQL cache storage, a transient failure in ProxyTimedVoteCacheTable.removeVote is caught and only logged, but the vote has already been removed from timeChangeQueue. The proxy therefore stops retrying the deletion while the SQL row remains; after the next restart, load() restores and replays that row, incrementing totals and delivering rewards a second time. Remove the in-memory entry only after the durable deletion succeeds, or retain it for a deletion retry.

Useful? React with 👍 / 👎.

Comment on lines +1980 to +1981
Set<String> forwarded = sendProxyBroadcast(Collections.singleton(target), uuid, player,
service, time, "", false);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve vote totals in rollover broadcasts

When an offline FORWARD vote arrives during a GlobalData rollover, this publishes the standalone broadcast with an empty totals payload before the queued replay updates the backend user's totals. BroadcastHandler evaluates PlaceholderAPI placeholders against that user, while the standalone receiver ignores the envelope's totals field, so broadcast formats containing vote-total placeholders render the pre-vote values. Supply the projected post-rollover snapshot to the receiver or defer rendering until those totals are available.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1bd7f56d14

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +155 to +157
placeholders.put("VotingPlugin_total", Integer.toString(monthTotal));
placeholders.put("VotingPlugin_total_alltime", Integer.toString(allTimeTotal));
placeholders.put("VotingPlugin_total_monthly", Integer.toString(monthTotal));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Use the configured primary monthly total

When UseMonthDateTotalsAsPrimaryTotal is enabled, %VotingPlugin_total% and %VotingPlugin_total_monthly% normally resolve through VotingPluginUser.getTotal(Monthly), which reads the dated monthly column, but the new pre-rendering replaces both with the legacy monthTotal. Whenever those columns differ—especially around monthly rollover—FORWARD broadcasts display a different total from every normal placeholder evaluation; use dateMonthTotal for these PAPI placeholders when it is the configured primary value.

Useful? React with 👍 / 👎.

Comment on lines +1526 to +1530
if (!vote(vote.getName(), vote.getService(), true, false, vote.getTime(), null, null, vote)) {
scheduleTimeVoteRetry();
return;
}
processedTimeVotesPendingRemoval.add(vote);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Make successful rollover replay durably idempotent

If the proxy stops after this vote(...) call succeeds but before removeTimeVote deletes the durable row, the row is loaded and processed again after restart, duplicating totals, vote-party progress, and rewards. The new processedTimeVotesPendingRemoval guard is fresh evidence beyond the prior deletion-retry issue because it exists only in memory and cannot close this normal crash window; record completion durably or make replay idempotent by voteId before applying side effects.

Useful? React with 👍 / 👎.

Comment on lines 1414 to 1416
checkCachedVotes(serverName);
retryPendingOnlineBroadcasts(serverName);
checkOnlineVotes(playerName, uuid, serverName);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Periodically retry broker-backed online-cache broadcasts

When SendVotesToAllServers is false and an offline FORWARD broadcast initially fails over Redis, MySQL, MQTT, or sockets, its pending state lives only in the voter-keyed online cache, but this retry method is invoked only from the login path. Unlike plugin messaging, recovery of those transports produces no carrier-login event, and the periodic tasks call checkOnlineVotes only for currently online voters, so a recovered transport may never receive the announcement while the voter remains offline and no player newly logs into the target; include these pending entries in a periodic transport retry.

Useful? React with 👍 / 👎.

Comment on lines +2047 to +2048
VoteTimeQueue delayedVote = new VoteTimeQueue(voteId, player, service, time,
proxyBroadcastHandled, broadcastTargets, broadcastForwardedServers);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve multi-proxy totals in the rollover queue

When a non-primary proxy receives a multi-proxy vote during an active GlobalData rollover, text contains the primary proxy's totals snapshot, but this queued record stores only routing state. processQueue later replays it with text=null; because a non-primary proxy has processesTotals=false, no replacement snapshot is created, and the subsequent text.toString() throws. The row is then retained and retried forever, incrementing vote-party state on each attempt without delivering the reward, so persist and restore the incoming snapshot with the queued vote.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3f41e9d953

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +262 to +264
if (totals != null) {
pendingTotals.put(uuid, totals);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Clear the snapshot when a batch returns to stored totals

When a forwarded offline vote starts a BATCH_WINDOW_PER_PLAYER batch and the same player then receives a normal vote before the flush, the second call supplies totals=null, but this conditional leaves the first vote's snapshot in pendingTotals. flushBatch consequently pre-renders the combined announcement with the older snapshot instead of letting PlaceholderAPI read the totals updated by the later vote, so total and points placeholders undercount. Remove the pending snapshot when a newer batch entry has no override, or otherwise track which source represents the latest totals.

Useful? React with 👍 / 👎.

Comment on lines +169 to +173
placeholders.put("VotingPlugin_total_monthly", Integer.toString(primaryMonthTotal));
placeholders.put("VotingPlugin_total_weekly", Integer.toString(weeklyTotal));
placeholders.put("VotingPlugin_total_daily", Integer.toString(dailyTotal));
placeholders.put("VotingPlugin_points", Integer.toString(points));
placeholders.put("VotingPlugin_points_format", numberFormat.format(points));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Apply vote-party values to standalone broadcasts

On a FORWARD target that has not yet received the normal vote envelope, formats containing %VotingPlugin_BungeeVotePartyVotesCurrent%, %VotingPlugin_BungeeVotePartyVotesNeeded%, or %VotingPlugin_BungeeVotePartyVotesRequired% still render the backend's old state. The standalone listener parses a snapshot containing votePartyCurrent and votePartyRequired, but this replacement map omits them and, unlike handleWireVote, the listener does not update BungeeHandler's vote-party state before PlaceholderAPI runs. Include these snapshot fields in standalone rendering without globally mutating state for an early announcement.

Useful? React with 👍 / 👎.

Comment on lines +1557 to +1559
if (!vote(vote.getName(), vote.getService(), true, false, vote.getTime(), queuedTotals, null, vote)) {
scheduleTimeVoteRetry();
return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Remove terminal rejections before retrying the queue

If the head rollover entry can no longer be replayed—for example its UUID cannot be resolved after restart, a changed Bedrock-prefix policy rejects its name, or the replay-time delay check rejects it—vote(...) returns false deterministically, but this path retains the entry and returns before examining any later votes. The scheduled retry then repeats the same head entry every five seconds forever, so one unrecoverable row blocks totals and rewards for every valid rollover vote behind it. Distinguish transient processing failures from terminal validation rejections and quarantine or remove only the latter before continuing the queue.

Useful? React with 👍 / 👎.

Comment on lines +2084 to +2087
if (proxyBroadcastHandled) {
for (String target : broadcastTargets) {
Set<String> forwarded = sendProxyBroadcast(Collections.singleton(target), uuid, player,
service, time, projectedTotals == null ? "" : projectedTotals.toString(), false);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Retry pending timed broadcasts during the rollover

When an offline FORWARD vote arrives during a GlobalData rollover and a target initially cannot accept the standalone message—for example a plugin-message backend has no carrier—the failed target is persisted in delayedVote, but no login or periodic retry scans timeChangeQueue; both retry methods only inspect the online cache, while checkCachedVotes only inspects server caches. Even if a carrier appears or a broker recovers immediately, the announcement therefore remains pending until processQueue runs after the rollover finishes. Include timed entries in the target-driven and periodic retry paths so a failed first attempt does not restore the rollover-length delay.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a454872430

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +940 to +941
return new VoteTotalsSnapshot(allTimeTotal, monthTotal, weeklyTotal, dailyTotal, points, votePartyVotes,
currentVotePartyVotesRequired, dateMonthTotal);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Project vote-party state for rollover broadcasts

When GlobalData rollover is active and the vote party is enabled, the queue branch creates this snapshot before addVoteParty() and then returns, while acceptedQueuedVotes is applied only to player totals. An offline FORWARD broadcast containing the vote-party placeholders therefore shows the pre-vote current/required values—and multiple queued votes all show the same state—even though ordinary votes increment and potentially trigger the party before creating their snapshot. Project the queued vote-party increments and threshold transitions into these fields as well.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5808c02986

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

voteCheckTask = server.getScheduler().buildTask(this, () -> {
if (getVotingPluginProxy().getGlobalDataHandler() == null
|| !getVotingPluginProxy().getGlobalDataHandler().isTimeChangedHappened()) {
getVotingPluginProxy().retryPendingOnlineBroadcasts();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Run Velocity broadcast retries during active rollovers

On Velocity, an offline FORWARD vote queued during a GlobalData rollover is not retried periodically if its initial standalone delivery fails, because this newly added call remains inside the !isTimeChangedHappened() guard; the retry method now scans timed votes, but it cannot run until the rollover ends unless a player login triggers the separate target-specific path. Fresh evidence after the earlier timed-retry report is this Velocity-only scheduling guard, which can still delay the supposedly immediate broadcast for the full rollover when no one logs into the target.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 89b041c4cd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

service, time, projectedTotals == null ? "" : projectedTotals.toString(), false);
if (delayedVote.getBroadcastForwardedServers().addAll(forwarded)) {
broadcastForwardedServers.addAll(forwarded);
getVoteCacheHandler().updateTimeVote(delayedVote);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Retry failed delivery-state persistence

When a standalone broadcast succeeds during an active rollover but updateTimeVote fails because of a transient SQL/JSON storage error, this return value is ignored and the in-memory target remains marked delivered. No later retry attempts to persist that state, so if the proxy restarts before the rollover completes, the durable row is loaded with the target still pending and republishes an already delivered announcement. Handle the failed update by retaining a persistence retry before considering the target durably forwarded.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cd51831051

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +2281 to +2282
broadcastForwardedServers.addAll(sendProxyBroadcast(remainingTargets, uuid, player, service, time,
text == null ? "" : text.toString(), false));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Delay forwarding until backend vote validation

When BungeeManageTotals is false and the backend's WaitUntilVoteDelay rejects a real vote, the proxy skips checkVoteDelay because managesTotals is false but still sends this standalone FORWARD announcement. The subsequent normal envelope returns from PlayerVoteListener before broadcasting or rewarding, so users see an announcement for a rejected vote; defer the standalone message until the backend confirms acceptance in this configuration.

Useful? React with 👍 / 👎.

Comment on lines +552 to +554
if (cache.getBroadcastForwardedServers().addAll(forwarded)) {
cache.setBroadcastForwarded(cache.isProxyBroadcastComplete());
getVoteCacheHandler().updateServerVote(server, cache);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Retry cached delivery-state persistence failures

With SQL cache storage and WaitForUserOnline keeping the reward cached, a transient failure in updateServerVote is swallowed by ProxyVoteCacheTable.updateProxyBroadcastState, while this in-memory entry remains marked as forwarded. If the proxy restarts before reward delivery, the durable row is loaded with the target still pending and republishes an already successful announcement; make this update report success and retain dirty state for retry, as the timed-vote path already does.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5f1210d975

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +654 to +655
if (cache.isProxyBroadcastHandled() && !cache.isProxyBroadcastComplete()) {
retained.add(cache);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reconcile pending targets after the fallback broadcast

With a voter-keyed cache and Redis, if an earlier standalone failure has set redisPublisherRetryAfter and the voter logs in during that cooldown, the retry above returns false, but the normal vote envelope is published through globalMessageProxyHandler without the cooldown and can successfully broadcast with broadcastHere=true. This condition nevertheless retains the vote as broadcast-incomplete, so the periodic retry later publishes the standalone announcement again. Fresh evidence beyond the earlier cooldown report is that normal publishing now bypasses the cooldown while its successful fallback broadcast is not reflected in the pending-target state.

Useful? React with 👍 / 👎.

Comment on lines +1731 to +1732
QueuedVoteResult result = vote(vote.getName(), vote.getService(), true, false, vote.getTime(), queuedTotals,
null, vote);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Replay rollover votes with their persisted UUID

When an online-mode player's name mapping changes or is unavailable during rollover replay, passing null here forces a fresh name-to-UUID lookup even though the accepted vote's UUID is now persisted in VoteTimeQueue. If that lookup fails—for example after a restart and rename with external UUID lookup disabled—the vote is classified as terminal and its durable row is removed without totals or rewards. Fresh evidence after the earlier queue-blocking report is that the new durable UUID field is loaded but still ignored during replay; pass vote.getUuid() instead.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 13bd7b5ebd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +640 to +644
Set<String> pendingTargets = new LinkedHashSet<>(cache.getBroadcastTargets());
pendingTargets.removeAll(cache.getBroadcastForwardedServers());
cache.getBroadcastForwardedServers().addAll(sendProxyBroadcast(pendingTargets,
cache.getUuid(), cache.getPlayerName(), cache.getService(), cache.getTime(),
cache.getText(), false));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Filter persisted retries against newly blocked servers

When a FORWARD target is added to BlockedServers after the vote was cached but before the voter logs in, this login path sends the entire persisted pending-target set without applying the current block list. The periodic retry path filters blocked targets, but checkOnlineVotes can still publish the announcement to a backend explicitly configured not to receive votes; filter or reconcile pending targets before calling sendProxyBroadcast here.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2a22f6e59b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +295 to +297
removeOnlineVotes(uuid);
for (OfflineBungeeVote vote : retained) {
addOnlineVote(uuid, vote);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Make retained online-vote rewrites atomic

When MultiProxyOneGlobalReward retains an entry with pending FORWARD targets, this deletes every persisted vote for the UUID and then reinserts the retained entries one by one, but removeOnlineVotes swallows SQL deletion failures and addOnlineVote likewise cannot report insertion failure. A transient failure can therefore either duplicate retained rows (failed delete followed by successful inserts), causing the pending broadcast to be sent multiple times after restart, or lose them (successful delete followed by failed insert). Replace the rows transactionally or propagate persistence success before changing the in-memory cache.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c3b0efe717

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +2377 to +2378
broadcastForwardedServers.addAll(sendProxyBroadcast(remainingTargets, uuid, player, service, time,
text == null ? "" : text.toString(), false));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Persist cached rewards before publishing the broadcast

When an offline FORWARD vote must subsequently enter the server or online cache, this publishes the announcement before addServerVote/addOnlineVote attempts durable storage. With MySQL cache storage, both insert methods swallow SQL failures and leave only the in-memory entry, so a transient cache-database failure followed by a proxy restart loses the reward even though users already saw the successful vote announcement. Make the initial cache insert report success and persist the reward and delivery state before sending the standalone broadcast.

Useful? React with 👍 / 👎.

Comment on lines +2240 to +2242
boolean processesTotals = getConfig().getPrimaryServer() || !getConfig().getMultiProxySupport();
boolean managesTotals = processesTotals && getConfig().getBungeeManageTotals();
boolean canValidateStandaloneBroadcast = canForwardStandaloneBroadcast(managesTotals);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Forward validated votes from secondary proxies

With MultiProxySupport enabled, every non-primary proxy sets processesTotals=false, which makes this validation gate false even when the vote was accepted by the totals-managing primary and arrived with its totals snapshot. If the voter is offline on that secondary, none of its local FORWARD targets receive a standalone broadcast; the vote is instead placed in the ordinary online cache and waits for a later login, while only the primary proxy's local backends were announced immediately. Preserve the primary's accepted/validated context so secondary proxies can perform their own configured FORWARD delivery.

Useful? React with 👍 / 👎.

@BenCodez
BenCodez merged commit c1c99b1 into master Aug 8, 2026
4 checks passed
@BenCodez
BenCodez deleted the agent/forward-offline-proxy-broadcasts branch August 8, 2026 20:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant