Skip to content

Commit 6c8e2c3

Browse files
committed
fix(economy): batch balance persistence
1 parent 0713502 commit 6c8e2c3

3 files changed

Lines changed: 132 additions & 28 deletions

File tree

src/main/java/fr/openmc/core/features/economy/EconomyManager.java

Lines changed: 105 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,17 @@
99
import fr.openmc.core.bootstrap.features.annotations.Credit;
1010
import fr.openmc.core.bootstrap.features.types.DatabaseFeature;
1111
import fr.openmc.core.bootstrap.features.types.HasCommands;
12+
import fr.openmc.core.bootstrap.integration.OMCLogger;
13+
import fr.openmc.core.OMCPlugin;
1214
import fr.openmc.core.features.economy.commands.Baltop;
1315
import fr.openmc.core.features.economy.commands.History;
1416
import fr.openmc.core.features.economy.commands.Money;
1517
import fr.openmc.core.features.economy.commands.Pay;
1618
import fr.openmc.core.features.economy.models.EconomyPlayer;
1719
import fr.openmc.core.hooks.itemsadder.ItemsAdderHook;
1820
import lombok.Getter;
21+
import org.bukkit.Bukkit;
22+
import org.bukkit.scheduler.BukkitTask;
1923

2024
import javax.annotation.Nullable;
2125
import java.math.BigDecimal;
@@ -30,6 +34,10 @@ public class EconomyManager extends Feature implements DatabaseFeature, HasComma
3034
private static Map<UUID, EconomyPlayer> balances;
3135

3236
private static Dao<EconomyPlayer, String> playersDao;
37+
private static final Set<UUID> dirtyBalances = new HashSet<>();
38+
private static final Object balancesLock = new Object();
39+
private static final long AUTO_SAVE_INTERVAL_TICKS = 20L * 60L * 5L;
40+
private static BukkitTask autoSaveTask;
3341

3442
private static final DecimalFormat decimalFormat = new DecimalFormat("#.##");
3543
private static final NavigableMap<Long, String> suffixes = new TreeMap<>(Map.of(
@@ -43,6 +51,8 @@ public class EconomyManager extends Feature implements DatabaseFeature, HasComma
4351
@Override
4452
public void init() {
4553
balances = loadAllBalances();
54+
dirtyBalances.clear();
55+
startAutoSaveTask();
4656
}
4757

4858
@Override
@@ -61,18 +71,29 @@ public void initDB(ConnectionSource connectionSource) throws SQLException {
6171
playersDao = DaoManager.createDao(connectionSource, EconomyPlayer.class);
6272
}
6373

74+
@Override
75+
protected void save() {
76+
stopAutoSaveTask();
77+
saveAllBalances();
78+
}
79+
6480
public static double getBalance(UUID playerUUID) {
65-
EconomyPlayer bank = getPlayerBank(playerUUID);
66-
return bank.getBalance();
81+
synchronized (balancesLock) {
82+
EconomyPlayer bank = getPlayerBank(playerUUID);
83+
return bank.getBalance();
84+
}
6785
}
6886

6987
public static void addBalance(UUID playerUUID, double amount) {
7088
addBalance(playerUUID, amount, null);
7189
}
7290

7391
public static void addBalance(UUID playerUUID, double amount, @Nullable String reason) {
74-
EconomyPlayer bank = getPlayerBank(playerUUID);
75-
bank.deposit(amount);
92+
synchronized (balancesLock) {
93+
EconomyPlayer bank = getPlayerBank(playerUUID);
94+
bank.deposit(amount);
95+
savePlayerBank(bank);
96+
}
7697

7798
if (reason != null) {
7899
TransactionsManager.registerTransaction(new Transaction(
@@ -83,32 +104,33 @@ public static void addBalance(UUID playerUUID, double amount, @Nullable String r
83104
));
84105
}
85106

86-
savePlayerBank(bank);
87107
}
88108

89109
public static boolean withdrawBalance(UUID playerUUID, double amount) {
90110
return withdrawBalance(playerUUID, amount, null);
91111
}
92112

93113
public static boolean withdrawBalance(UUID playerUUID, double amount, @Nullable String reason) {
94-
EconomyPlayer bank = getPlayerBank(playerUUID);
114+
synchronized (balancesLock) {
115+
EconomyPlayer bank = getPlayerBank(playerUUID);
95116

96-
if (bank.withdraw(amount)) {
97-
if (reason != null) {
98-
TransactionsManager.registerTransaction(new Transaction(
99-
"CONSOLE",
100-
playerUUID.toString(),
101-
amount,
102-
reason
103-
));
117+
if (!bank.withdraw(amount)) {
118+
return false;
104119
}
105120

106121
savePlayerBank(bank);
122+
}
107123

108-
return true;
124+
if (reason != null) {
125+
TransactionsManager.registerTransaction(new Transaction(
126+
"CONSOLE",
127+
playerUUID.toString(),
128+
amount,
129+
reason
130+
));
109131
}
110132

111-
return false;
133+
return true;
112134
}
113135

114136
/**
@@ -152,10 +174,11 @@ public static boolean transferBalance(UUID fromPlayer, UUID toPlayer, double amo
152174
}
153175

154176
public static void setBalance(UUID playerUUID, double amount) {
155-
EconomyPlayer bank = getPlayerBank(playerUUID);
156-
bank.withdraw(bank.getBalance());
157-
bank.deposit(amount);
158-
savePlayerBank(bank);
177+
synchronized (balancesLock) {
178+
EconomyPlayer bank = getPlayerBank(playerUUID);
179+
bank.setBalance(amount);
180+
savePlayerBank(bank);
181+
}
159182
}
160183

161184
public static String getMiniBalance(UUID playerUUID) {
@@ -165,19 +188,51 @@ public static String getMiniBalance(UUID playerUUID) {
165188
}
166189

167190
public static void savePlayerBank(EconomyPlayer player) {
168-
try {
191+
synchronized (balancesLock) {
169192
balances.put(player.getPlayerUUID(), player);
170-
playersDao.createOrUpdate(player);
171-
} catch (SQLException e) {
172-
throw new RuntimeException(e);
193+
dirtyBalances.add(player.getPlayerUUID());
173194
}
174195
}
175196

176197
public static EconomyPlayer getPlayerBank(UUID playerUUID) {
177-
EconomyPlayer bank = balances.get(playerUUID);
178-
if (bank != null)
179-
return bank;
180-
return new EconomyPlayer(playerUUID);
198+
synchronized (balancesLock) {
199+
return balances.computeIfAbsent(playerUUID, EconomyPlayer::new);
200+
}
201+
}
202+
203+
public static void saveAllBalances() {
204+
List<EconomyPlayer> playersToSave;
205+
206+
synchronized (balancesLock) {
207+
if (dirtyBalances.isEmpty()) {
208+
return;
209+
}
210+
211+
playersToSave = dirtyBalances.stream()
212+
.map(balances::get)
213+
.filter(Objects::nonNull)
214+
.map(player -> new EconomyPlayer(player.getPlayerUUID(), player.getBalance()))
215+
.toList();
216+
dirtyBalances.clear();
217+
}
218+
219+
try {
220+
playersDao.callBatchTasks(() -> {
221+
for (EconomyPlayer player : playersToSave) {
222+
playersDao.createOrUpdate(player);
223+
}
224+
225+
return null;
226+
});
227+
} catch (Exception e) {
228+
synchronized (balancesLock) {
229+
for (EconomyPlayer player : playersToSave) {
230+
dirtyBalances.add(player.getPlayerUUID());
231+
}
232+
}
233+
234+
OMCLogger.error("Failed to save economy balances", e);
235+
}
181236
}
182237

183238
public static Map<UUID, EconomyPlayer> loadAllBalances() {
@@ -194,6 +249,28 @@ public static Map<UUID, EconomyPlayer> loadAllBalances() {
194249
return balances;
195250
}
196251

252+
private static void startAutoSaveTask() {
253+
if (OMCPlugin.isUnitTestVersion() || autoSaveTask != null) {
254+
return;
255+
}
256+
257+
autoSaveTask = Bukkit.getScheduler().runTaskTimerAsynchronously(
258+
OMCPlugin.getInstance(),
259+
EconomyManager::saveAllBalances,
260+
AUTO_SAVE_INTERVAL_TICKS,
261+
AUTO_SAVE_INTERVAL_TICKS
262+
);
263+
}
264+
265+
private static void stopAutoSaveTask() {
266+
if (autoSaveTask == null) {
267+
return;
268+
}
269+
270+
autoSaveTask.cancel();
271+
autoSaveTask = null;
272+
}
273+
197274
public static String getFormattedBalance(UUID playerUUID) {
198275
String balance = String.valueOf(getBalance(playerUUID));
199276
Currency currency = Currency.getInstance(Locale.FRANCE);

src/main/java/fr/openmc/core/features/economy/models/EconomyPlayer.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ public EconomyPlayer(UUID playerUUID) {
2424
this.balance = 0;
2525
}
2626

27+
public EconomyPlayer(UUID playerUUID, double balance) {
28+
this.playerUUID = playerUUID;
29+
this.balance = balance;
30+
}
31+
2732
public void deposit(double amount) {
2833
balance += amount;
2934
}
@@ -35,4 +40,8 @@ public boolean withdraw(double amount) {
3540
}
3641
return false;
3742
}
43+
44+
public void setBalance(double balance) {
45+
this.balance = balance;
46+
}
3847
}

src/test/java/fr/openmc/core/features/economy/EconomyManagerTest.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import static org.junit.jupiter.api.Assertions.assertTrue;
66

77
import java.util.List;
8+
import java.util.Map;
9+
import java.util.UUID;
810

911
import org.junit.jupiter.api.AfterEach;
1012
import org.junit.jupiter.api.BeforeEach;
@@ -13,6 +15,7 @@
1315
import org.mockbukkit.mockbukkit.entity.PlayerMock;
1416

1517
import fr.openmc.core.OMCPlugin;
18+
import fr.openmc.core.features.economy.models.EconomyPlayer;
1619
import fr.openmc.mock.MockBukkitHelper;
1720
import fr.openmc.mock.ServerMock;
1821

@@ -71,6 +74,21 @@ public void testSetBalance() {
7174
assertEquals(EconomyManager.getBalance(player1.getUniqueId()), 500.0);
7275
}
7376

77+
@Test
78+
public void testBalanceChangesAreSavedOnSaveAll() {
79+
UUID playerUUID = player1.getUniqueId();
80+
81+
EconomyManager.setBalance(playerUUID, 500.0);
82+
83+
Map<UUID, EconomyPlayer> balancesBeforeSave = EconomyManager.loadAllBalances();
84+
assertFalse(balancesBeforeSave.containsKey(playerUUID));
85+
86+
EconomyManager.saveAllBalances();
87+
88+
Map<UUID, EconomyPlayer> balancesAfterSave = EconomyManager.loadAllBalances();
89+
assertEquals(500.0, balancesAfterSave.get(playerUUID).getBalance());
90+
}
91+
7492
@Test
7593
public void testAddBalanceWithReasonRegistersTransaction() {
7694
EconomyManager.addBalance(player1.getUniqueId(), 100.0, "Test Reason");

0 commit comments

Comments
 (0)