Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,16 @@ public void giveReward(VotingPluginUser user, boolean online, boolean useBungee)
*/
public void giveRewards(VotingPluginUser orgUser, boolean forceBungee) {
rewardHandler.giveRewards(orgUser, forceBungee);
reset(false);
// Clear this party's recipient list immediately, as before. Delaying it
// would risk wiping voters recorded for the next party.
setVotedUsers(new ArrayList<>());
// Shared-MySQL recipients capture eligibility on AdvancedCore's ordered
// storage worker. Only the persisted per-user counter wipe must run
// behind those reads; it must not overtake deferred eligibility checks.
com.bencodez.advancedcore.api.user.usercache.UserDataManager manager =
plugin.getUserManager().getDataManager();
if (manager != null && manager.deferSharedStorageWork(this::resetVotePartyCount)) return;
resetVotePartyCount();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import java.util.ArrayList;
import java.util.UUID;
import java.util.concurrent.ThreadLocalRandom;
import java.util.function.Consumer;
import java.util.function.Supplier;

import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
Expand All @@ -11,6 +13,7 @@
import com.bencodez.advancedcore.api.misc.MiscUtils;
import com.bencodez.advancedcore.api.rewards.RewardBuilder;
import com.bencodez.advancedcore.api.user.UserDataFetchMode;
import com.bencodez.advancedcore.api.user.usercache.UserDataManager;
import com.bencodez.simpleapi.array.ArrayUtils;
import com.bencodez.votingplugin.VotingPluginMain;
import com.bencodez.votingplugin.user.VotingPluginUser;
Expand All @@ -37,10 +40,15 @@ public void giveReward(VotingPluginUser user, boolean useBungee) {
}

public void giveReward(VotingPluginUser user, boolean online, boolean useBungee) {
givePreparedReward(user, online, useBungee, !plugin.getServerData().isLastVotePartySameDay());
}

private void givePreparedReward(VotingPluginUser user, boolean online, boolean useBungee,
boolean firstVotePartyToday) {
new RewardBuilder(plugin.getSpecialRewardsConfig().getData(),
plugin.getSpecialRewardsConfig().getVotePartyRewardsPath()).setOnline(online)
.withPlaceHolder("VotesRequired", "" + plugin.getSpecialRewardsConfig().getVotePartyVotesRequired())
.withPlaceHolder("FirstVotePartyToday", "" + !plugin.getServerData().isLastVotePartySameDay())
.withPlaceHolder("FirstVotePartyToday", "" + firstVotePartyToday)
.setServer(useBungee).send(user);
}

Expand Down Expand Up @@ -82,13 +90,14 @@ private void dispatchGlobalCommands(String randomPlayer) {
}
}

/** Dispatch to every eligible user, preserving each online player's scheduler owner. */
private void giveAllPlayers(VotingPluginUser triggeringUser, boolean forceBungee) {
plugin.debug("Trying to give all players vote party");
ArrayList<String> alreadyGiven = new ArrayList<>();
for (Player player : Bukkit.getOnlinePlayers()) {
VotingPluginUser user = resolveUser(triggeringUser, player.getUniqueId(), player);
if (!plugin.getSpecialRewardsConfig().isVotePartyGiveOnlinePlayersOnly() || user.isOnline()) {
voteParty.giveReward(user, forceBungee);
giveRewardWhenLoaded(user, forceBungee, player);
}
alreadyGiven.add(player.getUniqueId().toString());
}
Expand All @@ -99,30 +108,115 @@ private void giveAllPlayers(VotingPluginUser triggeringUser, boolean forceBungee
}
VotingPluginUser user = resolveUser(triggeringUser, UUID.fromString(uuid), null);
if (!plugin.getSpecialRewardsConfig().isVotePartyGiveOnlinePlayersOnly() || user.isOnline()) {
voteParty.giveReward(user, forceBungee);
giveRewardWhenLoaded(user, forceBungee, null);
}
}
}

/** Dispatch to users recorded for this party before its voter list is reset. */
private void giveVotedPlayers(VotingPluginUser triggeringUser, boolean forceBungee) {
plugin.debug("Trying to give all voted players vote party");
plugin.debug(ArrayUtils.makeStringList(voteParty.getVotedUsers()));
for (String uuid : voteParty.getVotedUsers()) {
VotingPluginUser user = resolveUser(triggeringUser, UUID.fromString(uuid), null);
if (!plugin.getSpecialRewardsConfig().isVotePartyGiveOnlinePlayersOnly() || user.isOnline()) {
voteParty.giveReward(user, forceBungee);
// Capture a live player only on a platform-owned thread. The vote path
// can also originate from a storage worker, where Bukkit lookups are unsafe.
UserDataManager manager = sharedDataManager();
Player owner = manager != null && manager.isPlatformOwnedThread()
? Bukkit.getPlayer(user.getJavaUUID()) : null;
giveRewardWhenLoaded(user, forceBungee, owner);
}
}
}

/** Resolve a recipient without forcing an uncached SQL read for shared storage. */
private VotingPluginUser resolveUser(VotingPluginUser triggeringUser, UUID uuid, Player onlinePlayer) {
if (triggeringUser != null && triggeringUser.getJavaUUID().equals(uuid)) {
return triggeringUser;
VotingPluginUser user = triggeringUser != null && triggeringUser.getJavaUUID().equals(uuid)
? triggeringUser
: onlinePlayer == null
? plugin.getVotingPluginUserManager().getVotingPluginUser(uuid, true)
: plugin.getVotingPluginUserManager().getVotingPluginUser(onlinePlayer);
if (sharedDataManager() != null) {
// Reward placeholders need the published shared cache, including for the triggering user.
user.userDataFetechMode(UserDataFetchMode.DEFAULT);
} else if (user != triggeringUser) {
// Preserve direct reads for file/SQLite storage.
user.userDataFetechMode(UserDataFetchMode.NO_CACHE);
}
VotingPluginUser user = onlinePlayer == null
? plugin.getVotingPluginUserManager().getVotingPluginUser(uuid, true)
: plugin.getVotingPluginUserManager().getVotingPluginUser(onlinePlayer);
user.userDataFetechMode(UserDataFetchMode.NO_CACHE);
return user;
}

/** Returns the active shared SQL manager or null for nonshared storage. */
private UserDataManager sharedDataManager() {
UserDataManager manager = plugin.getUserManager().getDataManager();
return manager != null && manager.hasSharedSqlBackend() ? manager : null;
}

/**
* Capture shared SQL reward eligibility on AdvancedCore's storage worker.
* This must finish before the VoteParty counter wipe is enqueued on that worker.
* A caller running on a platform thread must not check even a published cache
* directly because checking publication takes the cache's synchronized monitor.
*/
private void giveRewardWhenLoaded(VotingPluginUser user, boolean forceBungee, Player callbackOwner) {
UserDataManager manager = sharedDataManager();
if (manager == null) {
voteParty.giveReward(user, forceBungee);
return;
}
UUID uuid = user.getJavaUUID();

// VoteParty clears per-user counters and updates the last-party timestamp
// after dispatch. Capture these decisions before that reset, not when the
// deferred reward finally runs on the player scheduler.
final boolean firstPartyToday = !plugin.getServerData().isLastVotePartySameDay();
final int userVotesRequired = plugin.getSpecialRewardsConfig().getVotePartyUserVotesRequired();

// A mapped cache entry can still be an *unpublished* placeholder, so
// cacheUserIfNeeded() alone would not make reward placeholders safe to read.
Supplier<Integer> populate = () -> {
if (manager.getPublishedCache(uuid) == null) manager.cacheUser(uuid, null);
if (manager.getPublishedCache(uuid) == null) return null;
// Read eligibility on the storage worker, before VoteParty resets counters.
return userVotesRequired > 0 ? user.getVotePartyVotes() : 0;
};
Consumer<Integer> success = votes -> {
if (votes == null) {
plugin.getLogger().warning("VoteParty reward not sent: shared user data is unavailable for " + uuid);
return;
}
if (votes < userVotesRequired) return;

// Eligibility was captured on the storage worker before reset. Do not
// re-read the cache on a platform thread or re-check reset counters.
Runnable deliver = () -> givePreparedReward(user, user.isOnline(), forceBungee, firstPartyToday);
if (callbackOwner != null) {
deliver.run();
} else {
// A worker-originated vote has no captured Player. Its completion is
// on the global platform scheduler; discover a live owner there.
Player online = Bukkit.getPlayer(uuid);
if (online == null) deliver.run();
else plugin.getBukkitScheduler().runTask(plugin, deliver, online);
}
};
Consumer<Throwable> failure = error -> plugin.getLogger()
.warning("Failed to load shared user data for VoteParty reward " + uuid + ": " + error);

try {
if (manager.deferSharedStorageResult(populate, success, failure, callbackOwner)) return;

// Called from a non-platform worker: storage reads are safe here, but reward
// effects still need the Bukkit/Folia scheduler.
Integer ready = populate.get();
if (callbackOwner != null) {
plugin.getBukkitScheduler().runTask(plugin, () -> success.accept(ready), callbackOwner);
} else {
plugin.getBukkitScheduler().runTask(plugin, () -> success.accept(ready));
}
} catch (RuntimeException | Error error) {
failure.accept(error);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@
import static org.mockito.Mockito.when;

import java.util.ArrayList;
import java.lang.reflect.Field;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.atomic.AtomicReference;

import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
Expand All @@ -27,12 +29,15 @@
import org.mockito.Mockito;

import com.bencodez.advancedcore.api.user.UserManager;
import com.bencodez.advancedcore.api.user.usercache.UserDataManager;
import com.bencodez.simpleapi.sql.DataType;
import com.bencodez.votingplugin.VotingPluginMain;
import com.bencodez.votingplugin.config.Config;
import com.bencodez.votingplugin.config.SpecialRewardsConfig;
import com.bencodez.votingplugin.data.ServerData;
import com.bencodez.votingplugin.placeholders.PlaceHolders;
import com.bencodez.votingplugin.specialrewards.voteparty.VoteParty;
import com.bencodez.votingplugin.specialrewards.voteparty.VotePartyRewardHandler;
import com.bencodez.votingplugin.user.VotingPluginUser;

public class VotePartyTest {
Expand Down Expand Up @@ -190,4 +195,48 @@ public void getRandomPlayerName_NoPlayersOnline_ReturnsNoPlayer() {

assertEquals("No Player", playerName);
}

@Test
public void sharedMysqlResetWaitsForQueuedRewardEligibilityReads() throws Exception {
UserDataManager dataManager = Mockito.mock(UserDataManager.class);
when(plugin.getUserManager().getDataManager()).thenReturn(dataManager);
VotePartyRewardHandler handler = Mockito.mock(VotePartyRewardHandler.class);
replaceRewardHandler(handler);
AtomicReference<Runnable> queuedReset = new AtomicReference<>();
when(dataManager.deferSharedStorageWork(any(Runnable.class))).thenAnswer(invocation -> {
queuedReset.set(invocation.getArgument(0));
return true;
});

voteParty.giveRewards(user, false);

verify(handler).giveRewards(user, false);
// Voter-list state clears immediately; only the SQL counter wipe is deferred.
verify(plugin.getServerData()).setStringList(eq("VoteParty.Voted"), any());
org.junit.jupiter.api.Assertions.assertNotNull(queuedReset.get());
verify(plugin.getUserManager(), never()).removeAllKeyValues("VotePartyVotes", DataType.INTEGER);

queuedReset.get().run();
verify(plugin.getUserManager()).removeAllKeyValues("VotePartyVotes", DataType.INTEGER);
}

@Test
public void nonsharedResetRemainsImmediate() throws Exception {
UserDataManager dataManager = Mockito.mock(UserDataManager.class);
when(plugin.getUserManager().getDataManager()).thenReturn(dataManager);
VotePartyRewardHandler handler = Mockito.mock(VotePartyRewardHandler.class);
replaceRewardHandler(handler);

voteParty.giveRewards(user, false);

verify(handler).giveRewards(user, false);
verify(dataManager).deferSharedStorageWork(any(Runnable.class));
verify(plugin.getUserManager()).removeAllKeyValues("VotePartyVotes", DataType.INTEGER);
}

private void replaceRewardHandler(VotePartyRewardHandler handler) throws Exception {
Field field = VoteParty.class.getDeclaredField("rewardHandler");
field.setAccessible(true);
field.set(voteParty, handler);
}
}
Loading
Loading