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
30 changes: 25 additions & 5 deletions src/main/java/net/tfminecraft/marketblock/trade/MarketSale.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Properties;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

import net.tfminecraft.denareconomy.api.Denars;
import net.tfminecraft.denareconomy.api.Result;
Expand All @@ -20,6 +22,9 @@
* after the payment is applied.
*/
public final class MarketSale {
/** Sale folders whose entry in their parent folder this server run has flushed to disk. */
private static final Set<Path> FLUSHED_FOLDERS = ConcurrentHashMap.newKeySet();

private final String opId;
private final CharacterRef seller;
private final SaleTake take;
Expand Down Expand Up @@ -53,15 +58,26 @@ public Result settle() throws IOException {
}
if (result == Result.APPLIED || result == Result.REFUSED) {
Files.deleteIfExists(intent);
// Must stay: an intent that comes back after a crash blocks the seller's next sale (CREATE_NEW).
syncDirectory();
}
return result;
}

private void record() throws IOException {
Files.createDirectories(intent.getParent());
try (FileChannel parent = FileChannel.open(intent.getParent().getParent(), StandardOpenOption.READ)) {
parent.force(true);
Path folder = intent.getParent();
Path key = folder.toAbsolutePath().normalize();
boolean created = !Files.isDirectory(folder);
if (created) {
// A recreated folder is a new entry: forget the earlier flush until this one succeeds.
FLUSHED_FOLDERS.remove(key);
Files.createDirectories(folder);
}
// A new folder survives a crash only once its parent is flushed. An existing one is flushed once per
// run, in case the flush after it was made failed; after that its entry is already on disk.
if (created || !FLUSHED_FOLDERS.contains(key)) {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
force(folder.getParent());
FLUSHED_FOLDERS.add(key);
}
Properties data = new Properties();
data.setProperty("operation", opId);
Expand All @@ -80,8 +96,12 @@ private void record() throws IOException {
}

private void syncDirectory() throws IOException {
try (FileChannel directory = FileChannel.open(intent.getParent(), StandardOpenOption.READ)) {
directory.force(true);
force(intent.getParent());
}

static void force(Path directory) throws IOException {
try (FileChannel channel = FileChannel.open(directory, StandardOpenOption.READ)) {
channel.force(true);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,46 @@ void paidRetryUsesStoredResultAfterTaxChangesAndRefusesChangedGoods() throws Exc
}


@Test
void theParentFolderIsFlushedOnlyForANewSaleFolderOrOnceARunForAnExistingOne() throws Exception {
var seller = money.play(player);
player.getInventory().setItem(0, new ItemStack(Material.CARROT, 14));
Path sales = folder.resolve("sales");
Path existing = java.nio.file.Files.createDirectories(folder.resolve("existing"));
try (var flushes = mockStatic(MarketSale.class, CALLS_REAL_METHODS)) {
flushes.when(() -> MarketSale.force(any(Path.class))).thenAnswer(call -> null);

assertEquals(Result.APPLIED, sell(sales, seller));
assertEquals(Result.APPLIED, sell(sales, seller));
flushes.verify(() -> MarketSale.force(folder), times(1));
// Each sale still flushes its intent being written and being removed.
flushes.verify(() -> MarketSale.force(sales), times(4));

assertEquals(Result.APPLIED, sell(existing, seller));
assertEquals(Result.APPLIED, sell(existing, seller));
flushes.verify(() -> MarketSale.force(folder), times(2));

java.nio.file.Files.delete(sales);
assertEquals(Result.APPLIED, sell(sales, seller));
flushes.verify(() -> MarketSale.force(folder), times(3));
assertTrue(java.nio.file.Files.isDirectory(sales));

// A recreated folder whose flush fails is flushed again by the next sale, before it pays.
java.nio.file.Files.delete(sales);
flushes.when(() -> MarketSale.force(folder)).thenThrow(new java.io.IOException("simulated parent flush failure"));
assertThrows(java.io.IOException.class, () -> sell(sales, seller));
flushes.when(() -> MarketSale.force(folder)).thenAnswer(call -> null);
assertEquals(Result.APPLIED, sell(sales, seller));
flushes.verify(() -> MarketSale.force(folder), times(5));
}
assertEquals(2, player.getInventory().getItem(0).getAmount());
assertEquals(600, Denars.balance(Denars.pouch(seller)));
}

private Result sell(Path sales, net.tfminecraft.tlibs.character.CharacterRef seller) throws Exception {
return new MarketSale(sales, seller, InventoryUtils.planTake(player, "v.carrot", 2), 100).settle();
}

@Test void waitingForFactionTaxDoesNotCreateAnAmbiguousSale() throws Exception {
var seller=money.play(player);
MockBukkit.createMockPlugin("SimpleFactions");
Expand Down
Loading