diff --git a/bungeecord/src/main/java/com/vexsoftware/votifier/bungee/NuVotifier.java b/bungeecord/src/main/java/com/vexsoftware/votifier/bungee/NuVotifier.java index c3a47ce7..20fc4af2 100644 --- a/bungeecord/src/main/java/com/vexsoftware/votifier/bungee/NuVotifier.java +++ b/bungeecord/src/main/java/com/vexsoftware/votifier/bungee/NuVotifier.java @@ -221,7 +221,7 @@ protected void initChannel(NioSocketChannel channel) throws Exception { public void operationComplete(ChannelFuture future) throws Exception { if (future.isSuccess()) { serverChannel = future.channel(); - getLogger().info("Votifier enabled on socket "+serverChannel.localAddress()+"."); + getLogger().info("Votifier enabled on socket " + serverChannel.localAddress() + "."); } else { SocketAddress socketAddress = future.channel().localAddress(); if (socketAddress == null) { @@ -255,8 +255,10 @@ public void operationComplete(ChannelFuture future) throws Exception { getLogger().info("Using in-memory cache for votes that are not able to be delivered."); } else if ("file".equals(cacheMethod)) { try { - voteCache = new FileVoteCache(ProxyServer.getInstance().getServers().size(), this, new File(getDataFolder(), - fwdCfg.getString("pluginMessaging.file.name"))); + voteCache = new FileVoteCache( + ProxyServer.getInstance().getServers().size(), this, + new File(getDataFolder(), fwdCfg.getString("pluginMessaging.file.name")), + fwdCfg.getInt("pluginMessaging.file.cacheTime", -1)); } catch (IOException e) { getLogger().log(Level.SEVERE, "Unload to load file cache. Votes will be lost!", e); } @@ -294,7 +296,7 @@ public void operationComplete(ChannelFuture future) throws Exception { } ProxyForwardingVoteSource.BackendServer server = new ProxyForwardingVoteSource.BackendServer(s, new InetSocketAddress(address, section.getShort("port")), - KeyCreator.createKeyFrom(section.getString("token",section.getString("key")))); + KeyCreator.createKeyFrom(section.getString("token", section.getString("key")))); serverList.add(server); } diff --git a/bungeecord/src/main/java/com/vexsoftware/votifier/bungee/forwarding/cache/FileVoteCache.java b/bungeecord/src/main/java/com/vexsoftware/votifier/bungee/forwarding/cache/FileVoteCache.java index 11532344..af496907 100644 --- a/bungeecord/src/main/java/com/vexsoftware/votifier/bungee/forwarding/cache/FileVoteCache.java +++ b/bungeecord/src/main/java/com/vexsoftware/votifier/bungee/forwarding/cache/FileVoteCache.java @@ -14,15 +14,21 @@ import java.util.*; import java.util.concurrent.TimeUnit; import java.util.logging.Level; +import java.util.logging.Logger; public class FileVoteCache extends MemoryVoteCache { + private final Logger l; private final File cacheFile; + private final int voteTTL; private final ScheduledTask saveTask; - public FileVoteCache(int initialMemorySize, final Plugin plugin, File cacheFile) throws IOException { + public FileVoteCache(int initialMemorySize, final Plugin plugin, File cacheFile, int voteTTL) throws IOException { super(initialMemorySize); this.cacheFile = cacheFile; + this.voteTTL = voteTTL; + this.l = plugin.getLogger(); + load(); saveTask = ProxyServer.getInstance().getScheduler().schedule(plugin, new Runnable() { @@ -31,7 +37,7 @@ public void run() { try { save(); } catch (IOException e) { - plugin.getLogger().log(Level.SEVERE, "Unable to save cached votes, votes will be lost if you restart.", e); + l.log(Level.SEVERE, "Unable to save cached votes, votes will be lost if you restart.", e); } } }, 3, 3, TimeUnit.MINUTES); @@ -52,22 +58,43 @@ private void load() throws IOException { List votes = new ArrayList<>(voteArray.length()); for (int i = 0; i < voteArray.length(); i++) { JSONObject voteObject = voteArray.getJSONObject(i); - votes.add(new Vote(voteObject)); + Vote v = new Vote(voteObject); + if (hasTimedOut(v)) + l.log(Level.WARNING, "Purging out of date vote.", v); + else + votes.add(v); } voteCache.put(((String) server), votes); } + } public void save() throws IOException { cacheLock.lock(); JSONObject votesObject = new JSONObject(); try { - // Create a copy of the votes. - for (Map.Entry> entry : voteCache.entrySet()) { + Iterator>> entryItr = voteCache.entrySet().iterator(); + while (entryItr.hasNext()) { + Map.Entry> entry = entryItr.next(); JSONArray array = new JSONArray(); - for (Vote vote : entry.getValue()) { - array.put(vote.serialize()); + Iterator voteItr = entry.getValue().iterator(); + while (voteItr.hasNext()) { + Vote vote = voteItr.next(); + + // if the vote is no longer valid, notify and remove + if (hasTimedOut(vote)) { + l.log(Level.WARNING, "Purging out of date vote.", vote); + voteItr.remove(); + } else { + array.put(vote.serialize()); + } + } + + // if, during our iteration, we TTL invalidated all of the votes + if (entry.getValue().isEmpty()) + entryItr.remove(); + votesObject.put(entry.getKey(), array); } } finally { @@ -79,6 +106,12 @@ public void save() throws IOException { } } + private boolean hasTimedOut(Vote v) { + if (voteTTL == -1) return false; + // scale voteTTL to milliseconds + return v.getLocalTimestamp() + voteTTL * 24 * 60 * 60 * 1000 < System.currentTimeMillis(); + } + public void halt() throws IOException { saveTask.cancel(); save(); diff --git a/bungeecord/src/main/resources/bungeeConfig.yml b/bungeecord/src/main/resources/bungeeConfig.yml index c6fe7e29..377a67de 100644 --- a/bungeecord/src/main/resources/bungeeConfig.yml +++ b/bungeecord/src/main/resources/bungeeConfig.yml @@ -53,6 +53,8 @@ forwarding: # Options for file caching. file: name: cached-votes.json + # days before a vote is considered 'dead' - removed from cache with a console warning + voteTTL: 10 # Specify servers to proxy votes for. proxy: Hub: diff --git a/common/src/main/java/com/vexsoftware/votifier/model/Vote.java b/common/src/main/java/com/vexsoftware/votifier/model/Vote.java index e9031b59..f48fbc0f 100644 --- a/common/src/main/java/com/vexsoftware/votifier/model/Vote.java +++ b/common/src/main/java/com/vexsoftware/votifier/model/Vote.java @@ -47,15 +47,26 @@ public class Vote { */ private String timeStamp; + /** + * Timestamp (unix-millis) normalized and taken from a known source + */ + private final long localTimestamp; + @Deprecated public Vote() { + localTimestamp = System.currentTimeMillis(); } public Vote(String serviceName, String username, String address, String timeStamp) { + this(serviceName, username, address, timeStamp, System.currentTimeMillis()); + } + + public Vote(String serviceName, String username, String address, String timeStamp, long localTimestamp) { this.serviceName = serviceName; this.username = username; this.address = address; this.timeStamp = timeStamp; + this.localTimestamp = localTimestamp; } private static String getTimestamp(JSONObject object) { @@ -67,13 +78,19 @@ private static String getTimestamp(JSONObject object) { } public Vote(JSONObject jsonObject) { - this(jsonObject.getString("serviceName"), jsonObject.getString("username"), jsonObject.getString("address"), getTimestamp(jsonObject)); + this(jsonObject.getString("serviceName"), + jsonObject.getString("username"), + jsonObject.getString("address"), + getTimestamp(jsonObject), + // maintained for backwards compatibility with <2.3.6 peers + (jsonObject.has("localTimestamp") ? jsonObject.getLong("localTimestamp") : System.currentTimeMillis())); } @Override public String toString() { return "Vote (from:" + serviceName + " username:" + username - + " address:" + address + " timeStamp:" + timeStamp + ")"; + + " address:" + address + " timeStamp:" + timeStamp + + " localTimestamp:" + localTimestamp + ")"; } /** @@ -152,12 +169,22 @@ public String getTimeStamp() { return timeStamp; } + /** + * Gets the local timestamp, in unix-millis. Calculated locally by a NuVotifier instance + * + * @return The local timestamp + */ + public long getLocalTimestamp() { + return localTimestamp; + } + public JSONObject serialize() { JSONObject ret = new JSONObject(); ret.put("serviceName", serviceName); ret.put("username", username); ret.put("address", address); ret.put("timestamp", timeStamp); + ret.put("localTimestamp", localTimestamp); return ret; } @@ -172,7 +199,6 @@ public boolean equals(Object o) { if (!username.equals(vote.username)) return false; if (!address.equals(vote.address)) return false; return timeStamp.equals(vote.timeStamp); - } @Override