diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/cache/VoteCacheHandler.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/cache/VoteCacheHandler.java index 90889727c..0781e544e 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/cache/VoteCacheHandler.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/cache/VoteCacheHandler.java @@ -88,7 +88,7 @@ public int getProxyCachedTotal(String uuid) { * @param server the server name * @param vote the vote to add */ - public void addServerVote(String server, OfflineBungeeVote vote) { + public synchronized void addServerVote(String server, OfflineBungeeVote vote) { if (containsServerVote(server, vote.getVoteId())) { debug1("Not caching duplicate vote " + vote.getVoteId() + " for server " + server); return; @@ -165,7 +165,7 @@ public ArrayList getOnlineVotes(String uuid) { * @param uuid the player UUID * @param vote the vote to add */ - public void addOnlineVote(String uuid, OfflineBungeeVote vote) { + public synchronized void addOnlineVote(String uuid, OfflineBungeeVote vote) { if (containsOnlineVote(uuid, vote.getVoteId())) { debug1("Not caching duplicate online vote " + vote.getVoteId() + " for " + uuid); return; diff --git a/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/VoteCacheHandlerVoteIdTest.java b/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/VoteCacheHandlerVoteIdTest.java index 64a11d7a1..748cceae1 100644 --- a/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/VoteCacheHandlerVoteIdTest.java +++ b/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/VoteCacheHandlerVoteIdTest.java @@ -5,12 +5,17 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.same; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.util.Collections; import java.util.List; import java.util.UUID; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -60,6 +65,30 @@ public void onlineCacheRejectsDuplicateVoteId() { verify(storage).addVoteOnline("player-uuid", 0, handler.getOnlineVotes("player-uuid").get(0)); } + @Test + public void concurrentServerDeliveriesReserveVoteIdAtomically() throws Exception { + UUID voteId = UUID.randomUUID(); + + runConcurrently(() -> handler.addServerVote("server", vote(voteId, 100L)), + () -> handler.addServerVote("server", vote(voteId, 101L))); + + assertEquals(1, handler.getVotes("server").size()); + verify(storage, times(1)).addVote(org.mockito.ArgumentMatchers.eq("server"), + org.mockito.ArgumentMatchers.anyInt(), org.mockito.ArgumentMatchers.any(OfflineBungeeVote.class)); + } + + @Test + public void concurrentOnlineDeliveriesReserveVoteIdAtomically() throws Exception { + UUID voteId = UUID.randomUUID(); + + runConcurrently(() -> handler.addOnlineVote("player-uuid", vote(voteId, 100L)), + () -> handler.addOnlineVote("player-uuid", vote(voteId, 101L))); + + assertEquals(1, handler.getOnlineVotes("player-uuid").size()); + verify(storage, times(1)).addVoteOnline(org.mockito.ArgumentMatchers.eq("player-uuid"), + org.mockito.ArgumentMatchers.anyInt(), org.mockito.ArgumentMatchers.any(OfflineBungeeVote.class)); + } + @Test public void distinctVoteIdsAreNotCollapsed() { handler.addServerVote("server", vote(UUID.randomUUID(), 100L)); @@ -159,6 +188,39 @@ private static void stubBoolean(DataNode parent, String key, boolean value) { when(child.asBoolean()).thenReturn(value); } + private static void runConcurrently(Runnable first, Runnable second) throws Exception { + ExecutorService executor = Executors.newFixedThreadPool(2); + CountDownLatch ready = new CountDownLatch(2); + CountDownLatch start = new CountDownLatch(1); + try { + Future firstFuture = executor.submit(() -> { + ready.countDown(); + await(start); + first.run(); + }); + Future secondFuture = executor.submit(() -> { + ready.countDown(); + await(start); + second.run(); + }); + ready.await(); + start.countDown(); + firstFuture.get(); + secondFuture.get(); + } finally { + executor.shutdownNow(); + } + } + + private static void await(CountDownLatch latch) { + try { + latch.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException(e); + } + } + private static OfflineBungeeVote vote(UUID voteId, long time) { return new OfflineBungeeVote(voteId, "Player", "player-uuid", "Service", time, true, "totals"); }