Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
de7e13d
Add codebase audit report
Jakubk15 Jun 20, 2026
62c9e4e
Fix C1: refund parcel fee when persistence fails
Jakubk15 Jun 20, 2026
5ab5f98
Fix C2: fully roll back a parcel when storage clearing fails
Jakubk15 Jun 20, 2026
3e6df14
Fix C3: consume the locker item when a locker is created
Jakubk15 Jun 20, 2026
364c223
Fix C4: cancel locker break to prevent chest/contents duplication
Jakubk15 Jun 20, 2026
9107ab5
Fix H1: make parcel collection space-check and delivery race-safe
Jakubk15 Jun 20, 2026
11fff9e
Fix H2: close the locker-fullness TOCTOU race
Jakubk15 Jun 20, 2026
ea93d72
Fix H3: page lockers from the repository, not the partial cache
Jakubk15 Jun 20, 2026
113e608
Fix H4: cancel chest interaction in-tick so the vanilla chest stays shut
Jakubk15 Jun 20, 2026
6a9723c
Fix H6: populate the user cache on read misses
Jakubk15 Jun 20, 2026
9355bf3
Fix H5: detect user conflicts against the database, not just the cache
Jakubk15 Jun 20, 2026
dd6289c
Fix M1: delete the delivery only after the status update succeeds
Jakubk15 Jun 20, 2026
abd3fa5
Fix M3: stop mutating the cache from inside its own loader
Jakubk15 Jun 20, 2026
5667f23
Fix M4: surface item-storage save failures and return staged items
Jakubk15 Jun 20, 2026
4f127cd
Fix M5: fail fast when the item storage table cannot be created
Jakubk15 Jun 20, 2026
1a940b6
Fix M6: rename ambiguous repository wrapper methods
Jakubk15 Jun 20, 2026
73629b0
Fix M7: make DAO cache population atomic
Jakubk15 Jun 20, 2026
81fdf00
Fix M8: make connection pool tuning configurable
Jakubk15 Jun 20, 2026
32ca84d
Fix M2: make domain event firing thread deterministic
Jakubk15 Jun 20, 2026
0e0523f
Fix L1: switch on the DatabaseType enum directly
Jakubk15 Jun 20, 2026
bfd55ab
Fix L3: tear down listeners/tasks before closing the datasource
Jakubk15 Jun 20, 2026
054f7d6
Fix L4: remove misleading stale comment in LockerPlaceController
Jakubk15 Jun 20, 2026
3da519e
Fix L5: account for stacking in the collection space check
Jakubk15 Jun 20, 2026
cb9c16b
Refactor D5: collapse duplicate createActiveItem overloads
Jakubk15 Jun 20, 2026
6773e02
Refactor D1: extract repository table bootstrap into the base class
Jakubk15 Jun 20, 2026
779167a
Refactor D2: extract shared paginated-query helper
Jakubk15 Jun 20, 2026
d246ff5
Refactor D4: share cancel handling via a CancellableEvent base
Jakubk15 Jun 20, 2026
f565a54
Record audit resolution status in AUDIT.md
Jakubk15 Jun 20, 2026
f859387
Address PR review feedback (cache/rollback/item-loss edge cases)
Jakubk15 Jun 20, 2026
7c16c7d
Merge branch 'master' into audit-fixes
Jakubk15 Jun 20, 2026
d6ec8a5
Address second review round (@claude): cache desyncs + UX
Jakubk15 Jun 20, 2026
d5a200f
Update src/main/java/com/eternalcode/parcellockers/user/UserManagerIm…
Jakubk15 Jun 20, 2026
0d5463e
Update src/main/java/com/eternalcode/parcellockers/user/UserManagerIm…
Jakubk15 Jun 20, 2026
83b6bdd
Update build configuration and enhance locker item consumption logic
Jakubk15 Jun 21, 2026
cfcc33f
Delete AUDIT.md
Jakubk15 Jun 21, 2026
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
6 changes: 3 additions & 3 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ tasks.withType<JavaCompile> {
tasks.withType<AbstractRun> {
javaLauncher = javaToolchains.launcherFor {
vendor = JvmVendorSpec.JETBRAINS
languageVersion = JavaLanguageVersion.of(21)
languageVersion = JavaLanguageVersion.of(25)
}
}

Expand All @@ -150,11 +150,11 @@ modrinth {

tasks {
runServer {
minecraftVersion("1.21.11")
minecraftVersion("26.1.2")
downloadPlugins {
modrinth("luckperms", "v5.5.17-bukkit")
modrinth("vaultunlocked", "2.17.0")
modrinth("essentialsx", "2.21.2")
modrinth("essentialsx", "2.22.0")
// modrinth("discordsrv", "1.30.4") // uncomment to test with DiscordSRV integration
}
jvmArgs("-Dcom.mojang.eula.agree=true", "-XX:+AllowEnhancedClassRedefinition")
Expand Down
14 changes: 10 additions & 4 deletions src/main/java/com/eternalcode/parcellockers/ParcelLockers.java
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
import org.bstats.bukkit.Metrics;
import org.bukkit.Server;
import org.bukkit.command.CommandSender;
import org.bukkit.event.HandlerList;
import org.bukkit.plugin.RegisteredServiceProvider;
import org.bukkit.plugin.java.JavaPlugin;

Expand Down Expand Up @@ -134,7 +135,7 @@ public void onEnable() {
UserValidationService userValidationService = new UserValidator();
UserManager userManager = new UserManagerImpl(userRepository, userValidationService, server);
LockerValidationService lockerValidationService = new LockerValidator();
LockerManager lockerManager = new LockerManager(config, lockerRepository, lockerValidationService, parcelRepository, server);
LockerManager lockerManager = new LockerManager(config, lockerRepository, lockerValidationService, parcelRepository, server, scheduler);
ParcelContentManager parcelContentManager = new ParcelContentManager(parcelContentRepository);
ItemStorageManager itemStorageManager = new ItemStorageManager(itemStorageRepository, server);
DeliveryManager deliveryManager = new DeliveryManager(deliveryRepository);
Expand Down Expand Up @@ -230,9 +231,10 @@ public void onEnable() {

@Override
public void onDisable() {
if (this.databaseManager != null) {
this.databaseManager.disconnect();
}
// Stop accepting new work before closing the datasource, so fewer in-flight async DB tasks
// run into an already-closed connection pool.
HandlerList.unregisterAll(this);
this.getServer().getScheduler().cancelTasks(this);

if (this.liteCommands != null) {
this.liteCommands.unregister();
Expand All @@ -241,6 +243,10 @@ public void onDisable() {
if (this.discordClientManager != null) {
this.discordClientManager.shutdown();
}

if (this.databaseManager != null) {
this.databaseManager.disconnect();
}
}

private boolean setupEconomy() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,19 @@ public static class Settings extends OkaeriConfig {
@Comment({ "", "# The database password." })
public String password = "";

@Comment({ "", "# Maximum number of connections held in the database pool." })
public int connectionPoolSize = 10;

@Comment({ "", "# How long (in milliseconds) to wait for a free connection before failing." })
public long connectionTimeoutMillis = 5000;

@Comment({
"",
"# Connection leak detection threshold in milliseconds (0 disables it).",
"# Set this comfortably above your slowest expected query to avoid false warnings."
})
public long leakDetectionThresholdMillis = 30000;

@Comment({ "", "# The parcel locker item." })
public ConfigItem parcelLockerItem = new ConfigItem()
.name("&3Parcel locker")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@
import com.eternalcode.parcellockers.content.ParcelContent;
import com.eternalcode.parcellockers.database.DatabaseManager;
import com.eternalcode.parcellockers.database.wrapper.AbstractRepositoryOrmLite;
import com.eternalcode.parcellockers.shared.exception.DatabaseException;
import com.j256.ormlite.table.TableUtils;
import java.sql.SQLException;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
Expand All @@ -15,17 +12,12 @@ public class ParcelContentRepositoryOrmLite extends AbstractRepositoryOrmLite im

public ParcelContentRepositoryOrmLite(DatabaseManager databaseManager, Scheduler scheduler) {
super(databaseManager, scheduler);

try {
TableUtils.createTableIfNotExists(databaseManager.connectionSource(), ParcelContentTable.class);
} catch (SQLException exception) {
throw new DatabaseException("Failed to create ParcelContent table", exception);
}
this.createTable(ParcelContentTable.class);
}

@Override
public CompletableFuture<Void> save(ParcelContent parcelContent) {
return this.saveIfNotExist(ParcelContentTable.class, ParcelContentTable.from(parcelContent)).thenApply(dao -> null);
return this.insertIfAbsent(ParcelContentTable.class, ParcelContentTable.from(parcelContent)).thenApply(dao -> null);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,13 @@ public void connect() throws SQLException {
this.dataSource.addDataSourceProperty("prepStmtCacheSqlLimit", 2048);
this.dataSource.addDataSourceProperty("useServerPrepStmts", true);

this.dataSource.setMaximumPoolSize(5);
this.dataSource.setConnectionTimeout(5000);
this.dataSource.setLeakDetectionThreshold(5000);
this.dataSource.setMaximumPoolSize(this.config.settings.connectionPoolSize);
this.dataSource.setConnectionTimeout(this.config.settings.connectionTimeoutMillis);
this.dataSource.setLeakDetectionThreshold(this.config.settings.leakDetectionThresholdMillis);
this.dataSource.setUsername(this.config.settings.user);
this.dataSource.setPassword(this.config.settings.password);

switch (DatabaseType.valueOf(databaseType.toString().toUpperCase())) {
switch (databaseType) {

case MYSQL -> {
this.dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
Expand Down Expand Up @@ -97,18 +97,15 @@ public void disconnect() {

@SuppressWarnings("unchecked")
public <T, ID> Dao<T, ID> getDao(Class<T> type) {
try {
Dao<?, ?> dao = this.cachedDao.get(type);

if (dao == null) {
dao = DaoManager.createDao(this.connectionSource, type);
this.cachedDao.put(type, dao);
Dao<?, ?> dao = this.cachedDao.computeIfAbsent(type, key -> {
try {
return DaoManager.createDao(this.connectionSource, key);
} catch (SQLException exception) {
throw new DatabaseException("Failed to get DAO for type: " + key.getSimpleName(), exception);
}
});

return (Dao<T, ID>) dao;
} catch (SQLException exception) {
throw new DatabaseException("Failed to get DAO for type: " + type.getSimpleName(), exception);
}
return (Dao<T, ID>) dao;
}

public ConnectionSource connectionSource() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,22 @@
import com.eternalcode.commons.ThrowingFunction;
import com.eternalcode.commons.scheduler.Scheduler;
import com.eternalcode.parcellockers.database.DatabaseManager;
import com.eternalcode.parcellockers.shared.Page;
import com.eternalcode.parcellockers.shared.PageResult;
import com.eternalcode.parcellockers.shared.exception.DatabaseException;
import com.j256.ormlite.dao.Dao;
import com.j256.ormlite.stmt.QueryBuilder;
import com.j256.ormlite.table.TableUtils;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;

public abstract class AbstractRepositoryOrmLite {

Expand All @@ -24,11 +32,22 @@ protected AbstractRepositoryOrmLite(DatabaseManager databaseManager, Scheduler s
this.scheduler = scheduler;
}

protected <T> CompletableFuture<Dao.CreateOrUpdateStatus> save(Class<T> type, T entity) {
/** Creates the backing table if it does not exist, failing fast with a {@link DatabaseException}. */
protected void createTable(Class<?> tableType) {
try {
TableUtils.createTableIfNotExists(this.databaseManager.connectionSource(), tableType);
} catch (SQLException exception) {
throw new DatabaseException("Failed to initialize table " + tableType.getSimpleName(), exception);
}
}

/** Inserts the entity, or updates it if a row with the same id already exists. */
protected <T> CompletableFuture<Dao.CreateOrUpdateStatus> upsert(Class<T> type, T entity) {
return this.action(type, dao -> dao.createOrUpdate(entity));
}

protected <T> CompletableFuture<T> saveIfNotExist(Class<T> type, T entity) {
/** Inserts the entity only if no row with the same id exists; an existing row is left untouched. */
protected <T> CompletableFuture<T> insertIfAbsent(Class<T> type, T entity) {
return this.action(type, dao -> dao.createIfNotExists(entity));
}

Expand Down Expand Up @@ -56,6 +75,36 @@ protected <T> CompletableFuture<List<T>> selectAll(Class<T> type) {
return this.action(type, Dao::queryForAll);
}

/**
* Runs a paginated query. The {@code configure} callback may apply filters (e.g. a WHERE clause)
* to the query builder; one extra row is fetched to determine whether a following page exists.
*/
protected <T, ID, D> CompletableFuture<PageResult<D>> queryPage(
Class<T> type,
Page page,
ThrowingFunction<QueryBuilder<T, ID>, QueryBuilder<T, ID>, SQLException> configure,
Function<T, D> mapper
) {
return this.<T, ID, PageResult<D>>action(type, dao -> {
QueryBuilder<T, ID> builder = configure.apply(dao.queryBuilder());

List<D> items = builder
.limit((long) page.getLimit() + 1)
.offset((long) page.getOffset())
.query()
.stream()
.map(mapper)
.collect(Collectors.toCollection(ArrayList::new));

boolean hasNext = items.size() > page.getLimit();
if (hasNext) {
items.removeLast();
}

return new PageResult<>(Collections.unmodifiableList(items), hasNext);
});
}

protected <T, ID, R> CompletableFuture<R> action(Class<T> type, ThrowingFunction<Dao<T, ID>, R, SQLException> action) {
CompletableFuture<R> completableFuture = new CompletableFuture<>();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
import com.eternalcode.parcellockers.database.DatabaseManager;
import com.eternalcode.parcellockers.database.wrapper.AbstractRepositoryOrmLite;
import com.eternalcode.parcellockers.delivery.Delivery;
import com.j256.ormlite.table.TableUtils;
import java.sql.SQLException;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
Expand All @@ -15,17 +13,12 @@ public class DeliveryRepositoryOrmLite extends AbstractRepositoryOrmLite impleme

public DeliveryRepositoryOrmLite(DatabaseManager databaseManager, Scheduler scheduler) {
super(databaseManager, scheduler);

try {
TableUtils.createTableIfNotExists(databaseManager.connectionSource(), DeliveryTable.class);
} catch (SQLException exception) {
exception.printStackTrace();
}
this.createTable(DeliveryTable.class);
}

@Override
public CompletableFuture<Void> save(Delivery delivery) {
return this.saveIfNotExist(DeliveryTable.class, DeliveryTable.from(delivery)).thenApply(dao -> null);
return this.insertIfAbsent(DeliveryTable.class, DeliveryTable.from(delivery)).thenApply(dao -> null);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,7 @@
import com.eternalcode.parcellockers.database.DatabaseManager;
import com.eternalcode.parcellockers.database.wrapper.AbstractRepositoryOrmLite;
import com.eternalcode.parcellockers.discord.DiscordLink;
import com.eternalcode.parcellockers.shared.exception.DatabaseException;
import com.j256.ormlite.stmt.DeleteBuilder;
import com.j256.ormlite.table.TableUtils;
import java.sql.SQLException;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
Expand All @@ -16,17 +13,12 @@ public class DiscordLinkRepositoryOrmLite extends AbstractRepositoryOrmLite impl

public DiscordLinkRepositoryOrmLite(DatabaseManager databaseManager, Scheduler scheduler) {
super(databaseManager, scheduler);

try {
TableUtils.createTableIfNotExists(databaseManager.connectionSource(), DiscordLinkEntity.class);
} catch (SQLException ex) {
throw new DatabaseException("Failed to initialize DiscordLink table", ex);
}
this.createTable(DiscordLinkEntity.class);
}

@Override
public CompletableFuture<Boolean> save(DiscordLink link) {
return this.save(DiscordLinkEntity.class, DiscordLinkEntity.fromDomain(link))
return this.upsert(DiscordLinkEntity.class, DiscordLinkEntity.fromDomain(link))
.thenApply(status -> status.isCreated() || status.isUpdated());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,8 @@ public CompletableFuture<ItemStorage> getItemStorage(UUID owner) {
.thenApply(optional -> optional.orElse(new ItemStorage(owner, List.of())));
}

public void saveItemStorage(UUID player, List<ItemStack> items) {
this.itemStorageManager.create(player, items);
public CompletableFuture<ItemStorage> saveItemStorage(UUID player, List<ItemStack> items) {
return this.itemStorageManager.create(player, items);
}

public CompletableFuture<Boolean> deleteItemStorage(UUID owner) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,17 +104,20 @@ void show(Player player, ParcelSize size) {
}

this.guiManager.deleteItemStorage(player.getUniqueId())
.thenAccept(action -> {
this.guiManager.saveItemStorage(player.getUniqueId(), items);
new SendingGui(
this.scheduler,
this.guiSettings,
this.miniMessage,
this.noticeService,
this.guiManager,
this.state
).show(player);
}).exceptionally(FutureHandler::handleException);
.thenCompose(action -> this.guiManager.saveItemStorage(player.getUniqueId(), items))
.thenAccept(saved -> new SendingGui(
this.scheduler,
this.guiSettings,
this.miniMessage,
this.noticeService,
this.guiManager,
this.state
).show(player))
.exceptionally(throwable -> {
// Persisting the staged items failed; hand them back so they are not lost.
this.scheduler.run(() -> items.forEach(item -> ItemUtil.giveItem(player, item)));
return FutureHandler.handleException(throwable);
});
});

this.guiManager.getItemStorage(player.getUniqueId()).thenAccept(itemStorage -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -417,13 +417,7 @@ public void updateStorageItem(Player player) {
}

private ItemStack createActiveItem(ConfigItem item, String appendLore) {
List<String> itemLore = new ArrayList<>(item.lore());
itemLore.add(appendLore);

return item.toBuilder()
.lore(itemLore.stream().map(element -> resetItalic(this.miniMessage.deserialize(element))).toList())
.glow(true)
.build();
return this.createActiveItem(item, List.of(appendLore));
}

private ItemStack createActiveItem(ConfigItem item, List<String> appendLore) {
Expand Down
Loading
Loading