Files
VotingPlugin/VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/OfflineBungeeVote.java
T

490 lines
19 KiB
Java

package com.bencodez.votingplugin.proxy;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Locale;
import com.bencodez.votingplugin.timequeue.VoteTimeQueue;
import lombok.Getter;
import lombok.Setter;
/**
* Represents an offline bungee vote.
*/
public class OfflineBungeeVote {
@Getter
private String playerName;
@Getter
private boolean realVote;
/** Player online state sampled when the proxy first accepted this vote. */
@Getter
private boolean wasOnline;
@Getter
private boolean wasOnlineKnown;
@Getter
private boolean delayValidated;
@Getter
private boolean delayValidationKnown;
/** True only for cache rows written before queue classification was persisted. */
private boolean legacyQueueClassification;
@Getter
private String service;
@Getter
@Setter
private String text;
@Getter
private long time;
@Getter
private String uuid;
@Getter
private UUID voteId;
@Getter
@Setter
private boolean broadcastForwarded;
@Getter
@Setter
private boolean proxyBroadcastHandled;
@Getter
private Set<String> broadcastTargets;
@Getter
private Set<String> broadcastForwardedServers;
@Getter
@Setter
private boolean rewardDelivered;
@Getter
@Setter
private boolean deliveryStateDirty;
/** SQL primary key for this vote's server-keyed cache row, when applicable. */
@Getter
@Setter
private int serverVoteCacheRowId;
/** JSON entry key for this vote's server-keyed emergency cache row, when applicable. */
@Getter
@Setter
private String serverVoteCacheJsonKey;
/** SQL primary key for this vote's voter-keyed cache row, when applicable. */
@Getter
@Setter
private int onlineVoteCacheRowId;
/** JSON entry key for this vote's voter-keyed emergency cache row, when applicable. */
@Getter
@Setter
private String onlineVoteCacheJsonKey;
/** Stable HTTP delivery IDs that must be reused for each target server. */
private final Map<String, String> httpDeliveryIds;
/** Stable HTTP standalone-broadcast IDs, separate from reward delivery IDs. */
private final Map<String, String> httpBroadcastDeliveryIds;
/** Original cached replay position, retained when only part of a batch is sent. */
@Getter
private int cachedBatchNumber;
@Getter
private int cachedBatchSize;
/** SQL row whose batch metadata is journaled in the JSON emergency store. */
@Getter
@Setter
private int cachedBatchSourceRowId;
@Getter
@Setter
private boolean cachedBatchDurable;
public void setCachedVoteBatch(int number, int size) {
cachedBatchNumber = number;
cachedBatchSize = size;
cachedBatchDurable = false;
}
public boolean hasCachedVoteBatch() {
return cachedBatchNumber > 0 && cachedBatchNumber <= cachedBatchSize;
}
public boolean isCachedVoteBatchValid() {
return cachedBatchSourceRowId >= 0 && (cachedBatchNumber == 0 && cachedBatchSize == 0
|| hasCachedVoteBatch());
}
/** Copies only replay metadata, leaving this cache lane's ownership unchanged. */
public void copyCachedVoteBatch(OfflineBungeeVote source) {
cachedBatchNumber = source.cachedBatchNumber;
cachedBatchSize = source.cachedBatchSize;
cachedBatchSourceRowId = source.cachedBatchSourceRowId;
cachedBatchDurable = source.cachedBatchDurable;
}
/** Assigns a legacy replay ID after its journal owner has been persisted. */
public void assignCachedVoteId(UUID identity) {
if (identity == null || voteId != null && !voteId.equals(identity))
throw new IllegalArgumentException("A cached vote identity cannot be replaced");
voteId = identity;
}
/**
* Constructor with UUID voteId.
* @param voteId the vote ID
* @param playerName the player name
* @param uuid the player UUID
* @param service the vote service
* @param time the vote time
* @param realVote whether this is a real vote
* @param text additional text
*/
public OfflineBungeeVote(UUID voteId, String playerName, String uuid, String service, long time, boolean realVote,
String text) {
this(voteId, playerName, uuid, service, time, realVote, text, false);
}
/**
* Constructor with UUID voteId and proxy broadcast delivery state.
* @param voteId the vote ID
* @param playerName the player name
* @param uuid the player UUID
* @param service the vote service
* @param time the vote time
* @param realVote whether this is a real vote
* @param text additional text
* @param broadcastForwarded whether the proxy already forwarded the broadcast
*/
public OfflineBungeeVote(UUID voteId, String playerName, String uuid, String service, long time, boolean realVote,
String text, boolean broadcastForwarded) {
this(voteId, playerName, uuid, service, time, realVote, text, broadcastForwarded, false,
Collections.emptySet(), Collections.emptySet(), false);
}
/**
* Constructor with full proxy broadcast delivery state.
* @param voteId the vote ID
* @param playerName the player name
* @param uuid the player UUID
* @param service the vote service
* @param time the vote time
* @param realVote whether this is a real vote
* @param text additional text
* @param broadcastForwarded legacy aggregate forwarded state
* @param proxyBroadcastHandled whether standalone proxy routing was selected
* @param broadcastTargets original standalone broadcast targets
* @param broadcastForwardedServers targets that accepted standalone delivery
* @param rewardDelivered whether the cached reward vote was already delivered
*/
public OfflineBungeeVote(UUID voteId, String playerName, String uuid, String service, long time, boolean realVote,
String text, boolean broadcastForwarded, boolean proxyBroadcastHandled, Set<String> broadcastTargets,
Set<String> broadcastForwardedServers, boolean rewardDelivered) {
this(voteId, playerName, uuid, service, time, realVote, text, broadcastForwarded, proxyBroadcastHandled,
broadcastTargets, broadcastForwardedServers, rewardDelivered, Collections.emptyMap());
}
/**
* Constructor with full proxy state and stable HTTP delivery IDs.
* @param httpDeliveryIds stable HTTP delivery ID by target server
*/
public OfflineBungeeVote(UUID voteId, String playerName, String uuid, String service, long time, boolean realVote,
String text, boolean broadcastForwarded, boolean proxyBroadcastHandled, Set<String> broadcastTargets,
Set<String> broadcastForwardedServers, boolean rewardDelivered, Map<String, String> httpDeliveryIds) {
this(voteId, playerName, uuid, service, time, realVote, text, broadcastForwarded, proxyBroadcastHandled,
broadcastTargets, broadcastForwardedServers, rewardDelivered, httpDeliveryIds, Collections.emptyMap());
}
public OfflineBungeeVote(UUID voteId, String playerName, String uuid, String service, long time, boolean realVote,
String text, boolean broadcastForwarded, boolean proxyBroadcastHandled, Set<String> broadcastTargets,
Set<String> broadcastForwardedServers, boolean rewardDelivered, Map<String, String> httpDeliveryIds,
Map<String, String> httpBroadcastDeliveryIds) {
this.playerName = playerName;
this.uuid = uuid;
this.service = service;
this.time = time;
this.realVote = realVote;
this.text = text;
this.voteId = voteId;
this.broadcastForwarded = broadcastForwarded;
this.proxyBroadcastHandled = proxyBroadcastHandled;
setBroadcastTargets(broadcastTargets);
setBroadcastForwardedServers(broadcastForwardedServers);
this.rewardDelivered = rewardDelivered;
this.httpDeliveryIds = new LinkedHashMap<>();
if (httpDeliveryIds != null) {
httpDeliveryIds.forEach(this::setHttpDeliveryId);
}
this.httpBroadcastDeliveryIds = new LinkedHashMap<>();
if (httpBroadcastDeliveryIds != null) httpBroadcastDeliveryIds.forEach(this::setHttpBroadcastDeliveryId);
}
/**
* Constructor with String voteId.
* @param voteId the vote ID as string
* @param playerName the player name
* @param uuid the player UUID
* @param service the vote service
* @param time the vote time
* @param realVote whether this is a real vote
* @param text additional text
*/
public OfflineBungeeVote(String voteId, String playerName, String uuid, String service, long time, boolean realVote,
String text) {
this(voteId, playerName, uuid, service, time, realVote, text, false);
}
/**
* Constructor with String voteId and proxy broadcast delivery state.
* @param voteId the vote ID as string
* @param playerName the player name
* @param uuid the player UUID
* @param service the vote service
* @param time the vote time
* @param realVote whether this is a real vote
* @param text additional text
* @param broadcastForwarded whether the proxy already forwarded the broadcast
*/
public OfflineBungeeVote(String voteId, String playerName, String uuid, String service, long time, boolean realVote,
String text, boolean broadcastForwarded) {
this(parseVoteId(voteId), playerName, uuid, service, time, realVote, text, broadcastForwarded);
}
/**
* Constructor with String voteId and full proxy broadcast delivery state.
* @param voteId the vote ID as string
* @param playerName the player name
* @param uuid the player UUID
* @param service the vote service
* @param time the vote time
* @param realVote whether this is a real vote
* @param text additional text
* @param broadcastForwarded legacy aggregate forwarded state
* @param proxyBroadcastHandled whether standalone proxy routing was selected
* @param broadcastTargets original standalone broadcast targets
* @param broadcastForwardedServers targets that accepted standalone delivery
* @param rewardDelivered whether the cached reward vote was already delivered
*/
public OfflineBungeeVote(String voteId, String playerName, String uuid, String service, long time, boolean realVote,
String text, boolean broadcastForwarded, boolean proxyBroadcastHandled, Set<String> broadcastTargets,
Set<String> broadcastForwardedServers, boolean rewardDelivered) {
this(parseVoteId(voteId), playerName, uuid, service, time, realVote, text, broadcastForwarded,
proxyBroadcastHandled, broadcastTargets, broadcastForwardedServers, rewardDelivered);
}
public OfflineBungeeVote(String voteId, String playerName, String uuid, String service, long time, boolean realVote,
String text, boolean broadcastForwarded, boolean proxyBroadcastHandled, Set<String> broadcastTargets,
Set<String> broadcastForwardedServers, boolean rewardDelivered, Map<String, String> httpDeliveryIds) {
this(parseVoteId(voteId), playerName, uuid, service, time, realVote, text, broadcastForwarded,
proxyBroadcastHandled, broadcastTargets, broadcastForwardedServers, rewardDelivered, httpDeliveryIds);
}
public OfflineBungeeVote(String voteId, String playerName, String uuid, String service, long time, boolean realVote,
String text, boolean broadcastForwarded, boolean proxyBroadcastHandled, Set<String> broadcastTargets,
Set<String> broadcastForwardedServers, boolean rewardDelivered, Map<String, String> httpDeliveryIds,
Map<String, String> httpBroadcastDeliveryIds) {
this(parseVoteId(voteId), playerName, uuid, service, time, realVote, text, broadcastForwarded,
proxyBroadcastHandled, broadcastTargets, broadcastForwardedServers, rewardDelivered, httpDeliveryIds,
httpBroadcastDeliveryIds);
}
/**
* Sets the sampled vote-time online state and marks it as known.
*/
public void setWasOnline(boolean wasOnline) {
this.wasOnline = wasOnline;
this.wasOnlineKnown = true;
}
private static UUID parseVoteId(String voteId) {
return voteId == null || voteId.isEmpty() ? null : UUID.fromString(voteId);
}
/**
* Replaces the original standalone broadcast targets.
* @param targets target servers
*/
public void setBroadcastTargets(Set<String> targets) {
broadcastTargets = targets == null ? new LinkedHashSet<>() : new LinkedHashSet<>(targets);
}
/**
* Replaces the targets that accepted standalone broadcast delivery.
* @param forwardedServers delivered target servers
*/
public void setBroadcastForwardedServers(Set<String> forwardedServers) {
broadcastForwardedServers = forwardedServers == null ? new LinkedHashSet<>()
: new LinkedHashSet<>(forwardedServers);
}
/**
* Checks whether every original standalone target accepted delivery.
* @return true when standalone routing is complete
*/
public boolean isProxyBroadcastComplete() {
return proxyBroadcastHandled && broadcastForwardedServers.containsAll(broadcastTargets);
}
/**
* Checks whether this cached vote still needs to broadcast on a server.
* @param server backend server receiving the cached vote
* @return true when this server is an original undelivered target
*/
public boolean needsBroadcastOn(String server) {
if (!proxyBroadcastHandled) {
return !broadcastForwarded;
}
return server != null && broadcastTargets.contains(server) && !broadcastForwardedServers.contains(server);
}
/** Stores the proxy delay decision made when this vote was admitted. */
public void setDelayValidated(boolean delayValidated) {
this.delayValidated = delayValidated;
this.delayValidationKnown = true;
this.legacyQueueClassification = false;
}
/** Restores the legacy state for cache rows created before delay decisions were persisted. */
public void clearDelayValidation() {
this.delayValidated = false;
this.delayValidationKnown = false;
this.legacyQueueClassification = true;
}
/** Whether this row can explicitly classify delivery as queued or unvalidated. */
public boolean isQueueClassificationKnown() {
return !legacyQueueClassification;
}
/**
* Returns the stable HTTP delivery ID for a target, if one is pending.
* @param server target server
* @return stable delivery ID or null
*/
public String getHttpDeliveryId(String server) {
return server == null ? null : httpDeliveryIds.get(server.toLowerCase(Locale.ROOT));
}
/** Returns a copy of stable reward delivery IDs by normalized target key. */
public Map<String, String> getHttpDeliveryIds() {
return new LinkedHashMap<>(httpDeliveryIds);
}
/**
* Stores or removes a stable HTTP delivery ID for a target.
* @param server target server
* @param deliveryId stable delivery ID, or null to remove
*/
public void setHttpDeliveryId(String server, String deliveryId) {
if (server == null || server.isBlank()) return;
String key = server.toLowerCase(Locale.ROOT);
if (deliveryId == null || deliveryId.isBlank()) httpDeliveryIds.remove(key);
else httpDeliveryIds.put(key, deliveryId);
}
public String getHttpBroadcastDeliveryId(String server) {
return server == null ? null : httpBroadcastDeliveryIds.get(server.toLowerCase(Locale.ROOT));
}
public void setHttpBroadcastDeliveryId(String server, String deliveryId) {
if (server == null || server.isBlank()) return;
String key = server.toLowerCase(Locale.ROOT);
if (deliveryId == null || deliveryId.isBlank()) httpBroadcastDeliveryIds.remove(key);
else httpBroadcastDeliveryIds.put(key, deliveryId);
}
/**
* Returns a copy of pending standalone HTTP delivery IDs for cache handoff.
* @return pending standalone delivery IDs by target server
*/
public Map<String, String> getHttpBroadcastDeliveryIds() {
return new LinkedHashMap<>(httpBroadcastDeliveryIds);
}
/** Returns whether any reward or standalone HTTP delivery is still pending. */
public boolean hasPendingHttpDeliveryIds() {
return !httpDeliveryIds.isEmpty() || hasPendingHttpBroadcastDeliveryIds();
}
/** Returns whether standalone HTTP broadcasts, rather than internal markers, remain. */
public boolean hasPendingHttpBroadcastDeliveryIds() {
return httpBroadcastDeliveryIds.keySet().stream().anyMatch(key -> !key.startsWith(
VotingPluginWire.REWARD_QUEUE_METADATA_PREFIX));
}
/**
* Encodes stable delivery IDs for JSON/SQL cache storage.
* @return bounded delimiter-safe encoding
*/
public String encodeHttpDeliveryIds() {
return encodeDeliveryIds(httpDeliveryIds);
}
public String encodeHttpBroadcastDeliveryIds() {
return encodeDeliveryIds(httpBroadcastDeliveryIds);
}
private static String encodeDeliveryIds(Map<String, String> values) {
StringBuilder encoded = new StringBuilder();
for (Map.Entry<String, String> entry : values.entrySet()) {
if (encoded.length() > 0) encoded.append('.');
encoded.append(Base64.getUrlEncoder().withoutPadding()
.encodeToString(entry.getKey().getBytes(StandardCharsets.UTF_8)));
encoded.append('~');
encoded.append(Base64.getUrlEncoder().withoutPadding()
.encodeToString(entry.getValue().getBytes(StandardCharsets.UTF_8)));
}
return encoded.toString();
}
/**
* Decodes stable delivery IDs from cache storage. Malformed entries are ignored.
* @param encoded encoded map
* @return decoded map
*/
public static Map<String, String> decodeHttpDeliveryIds(String encoded) {
return decodeDeliveryIds(encoded);
}
public static Map<String, String> decodeHttpBroadcastDeliveryIds(String encoded) {
return decodeDeliveryIds(encoded);
}
private static Map<String, String> decodeDeliveryIds(String encoded) {
Map<String, String> decoded = new LinkedHashMap<>();
if (encoded == null || encoded.isBlank()) return decoded;
for (String entry : encoded.split("\\.", -1)) {
int separator = entry.indexOf('~');
if (separator <= 0 || separator == entry.length() - 1) continue;
try {
String server = new String(Base64.getUrlDecoder().decode(entry.substring(0, separator)), StandardCharsets.UTF_8);
String deliveryId = new String(Base64.getUrlDecoder().decode(entry.substring(separator + 1)), StandardCharsets.UTF_8);
if (!server.isBlank() && deliveryId.matches("[0-9a-fA-F-]{36}"))
decoded.put(server.toLowerCase(Locale.ROOT), deliveryId);
} catch (IllegalArgumentException ignored) {
// Ignore corrupt optional delivery state and retain the vote itself.
}
}
return decoded;
}
/**
* Encodes the original broadcast targets for cache storage.
* @return encoded target set
*/
public String encodeBroadcastTargets() {
return VoteTimeQueue.encodeBroadcastServers(broadcastTargets);
}
/**
* Encodes delivered broadcast targets for cache storage.
* @return encoded delivered target set
*/
public String encodeBroadcastForwardedServers() {
return VoteTimeQueue.encodeBroadcastServers(broadcastForwardedServers);
}
@Override
public String toString() {
return "VoteCache:" + playerName + "/" + uuid + "/" + service + "/" + time + "/" + realVote + "/" + text + "/"
+ voteId + "/" + broadcastForwarded + "/" + proxyBroadcastHandled + "/" + broadcastTargets + "/"
+ broadcastForwardedServers + "/" + rewardDelivered + "/" + (wasOnlineKnown ? wasOnline : "unknown");
}
}