mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 22:10:18 +00:00
Group commit generation history
- Semantic journal and saved-biome appends write without forcing; a shared flusher forces touched files within 2s outside every store lock - GenerationHistory.sync forces pending history at world save, engine shutdown, pregen completion and before activation promotion - A failed deferred force poisons the store the same way an uncertain append did - Cache the active epoch pack root instead of regex-validating it on every stage open - Tests pin append-without-force, sync barriers, failure poisoning and the save, shutdown and pregen hooks
This commit is contained in:
1 parent
241260a762
commit
a35ecca7bd
24 files changed
+877
-136
No files matched your search
@@ -250,6 +250,7 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
||||
|
||||
default void save() {
|
||||
NativeStructureOwnershipStore.flush(this);
|
||||
syncGenerationHistory();
|
||||
getMantle().save();
|
||||
getWorldManager().onSave();
|
||||
saveEngineData();
|
||||
@@ -259,6 +260,7 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
||||
|
||||
default void saveNow() {
|
||||
NativeStructureOwnershipStore.flush(this);
|
||||
syncGenerationHistory();
|
||||
getMantle().saveAllNow();
|
||||
saveEngineData();
|
||||
}
|
||||
@@ -267,6 +269,8 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
||||
|
||||
void saveEngineData();
|
||||
|
||||
void syncGenerationHistory();
|
||||
|
||||
default String getName() {
|
||||
return getDimension().getName();
|
||||
}
|
||||
|
||||
@@ -1529,6 +1529,7 @@ public class IrisEngine implements Engine {
|
||||
|
||||
private void scheduleWorldSave() {
|
||||
backgroundTasks.scheduleAdmittedTask(this, () -> NativeStructureOwnershipStore.flush(this));
|
||||
backgroundTasks.scheduleAdmittedTask(this, this::syncGenerationHistory);
|
||||
getMantle().save();
|
||||
getWorldManager().onSave();
|
||||
saveEngineData();
|
||||
@@ -1539,6 +1540,20 @@ public class IrisEngine implements Engine {
|
||||
engineDataStore.saveEngineData();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void syncGenerationHistory() {
|
||||
GenerationHistoryRuntimeRouter router = getGenerationHistoryRuntimeRouter().orElse(null);
|
||||
if (router == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
router.history().sync();
|
||||
} catch (IOException failure) {
|
||||
throw new IllegalStateException("Unable to force Iris generation history to stable storage for "
|
||||
+ getWorld().name() + ".", failure);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void blockUpdatedMetric() {
|
||||
bud.incrementAndGet();
|
||||
|
||||
@@ -82,6 +82,27 @@ public final class GenerationHistory {
|
||||
return savedBiomes;
|
||||
}
|
||||
|
||||
public void sync() throws IOException {
|
||||
IOException failure = null;
|
||||
try {
|
||||
semantics.sync();
|
||||
} catch (IOException semanticFailure) {
|
||||
failure = semanticFailure;
|
||||
}
|
||||
try {
|
||||
savedBiomes.sync();
|
||||
} catch (IOException biomeFailure) {
|
||||
if (failure == null) {
|
||||
failure = biomeFailure;
|
||||
} else {
|
||||
failure.addSuppressed(biomeFailure);
|
||||
}
|
||||
}
|
||||
if (failure != null) {
|
||||
throw failure;
|
||||
}
|
||||
}
|
||||
|
||||
public static GenerationHistory create(
|
||||
Path dimensionRoot,
|
||||
Path packSource,
|
||||
@@ -478,6 +499,7 @@ public final class GenerationHistory {
|
||||
private GenerationActivation promotePendingLocked(
|
||||
BoundarySignatureCapture signatureCapture, boolean startupRecovery, boolean validateSemantics
|
||||
) throws IOException {
|
||||
sync();
|
||||
WorldChunkInventory inventory = recoverUnstoredClaims(startupRecovery);
|
||||
validateReferencedState(Optional.empty(), validateSemantics);
|
||||
Optional<GenerationActivation> pending = store.pendingActivation();
|
||||
|
||||
@@ -26,6 +26,7 @@ public final class GenerationHistoryPaths {
|
||||
private final Path legacyPackRoot;
|
||||
private final Path legacyMantleRoot;
|
||||
private final Path regionRoot;
|
||||
private volatile PackRoot lastPackRoot;
|
||||
|
||||
private GenerationHistoryPaths(Path dimensionRoot) {
|
||||
this.dimensionRoot = Objects.requireNonNull(dimensionRoot, "dimensionRoot")
|
||||
@@ -71,7 +72,13 @@ public final class GenerationHistoryPaths {
|
||||
}
|
||||
|
||||
public Path packRoot(String epochId) {
|
||||
return epochRoot(epochId).resolve(LEGACY_PACK_DIRECTORY_NAME);
|
||||
PackRoot cached = lastPackRoot;
|
||||
if (cached != null && cached.epochId().equals(epochId)) {
|
||||
return cached.path();
|
||||
}
|
||||
Path resolved = epochRoot(epochId).resolve(LEGACY_PACK_DIRECTORY_NAME);
|
||||
lastPackRoot = new PackRoot(epochId, resolved);
|
||||
return resolved;
|
||||
}
|
||||
|
||||
public Path activationsRoot() {
|
||||
@@ -108,4 +115,7 @@ public final class GenerationHistoryPaths {
|
||||
public Path regionRoot() {
|
||||
return regionRoot;
|
||||
}
|
||||
|
||||
private record PackRoot(String epochId, Path path) {
|
||||
}
|
||||
}
|
||||
@@ -500,6 +500,11 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
|
||||
} catch (Throwable detachFailure) {
|
||||
failure = detachFailure;
|
||||
}
|
||||
try {
|
||||
history.sync();
|
||||
} catch (Throwable syncFailure) {
|
||||
failure = appendFailure(failure, syncFailure);
|
||||
}
|
||||
try {
|
||||
biomes.close();
|
||||
} catch (Throwable biomeFailure) {
|
||||
|
||||
@@ -2,6 +2,7 @@ package art.arcane.iris.world.history;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.HydrologyFeatureType;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.world.storage.DeferredDurability;
|
||||
import art.arcane.iris.world.storage.Durability;
|
||||
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
||||
@@ -93,6 +94,7 @@ public final class GenerationSemanticIndex {
|
||||
private final PointerPublisher pointerPublisher;
|
||||
private final CatalogPublisher catalogPublisher;
|
||||
private final boolean unpublished;
|
||||
private final DeferredDurability durability;
|
||||
private long regionDecodeCount;
|
||||
private volatile IOException journalFailure;
|
||||
private long appendingRegionKey;
|
||||
@@ -118,6 +120,8 @@ public final class GenerationSemanticIndex {
|
||||
this.pointerPublisher = pointerPublisher;
|
||||
this.catalogPublisher = catalogPublisher;
|
||||
this.unpublished = unpublished;
|
||||
this.durability = new DeferredDurability(
|
||||
DeferredDurability.DEFAULT_FLUSH_DELAY_MILLIS, failure -> journalFailure = failure);
|
||||
}
|
||||
|
||||
public static GenerationSemanticIndex load(Path dimensionRoot) throws IOException {
|
||||
@@ -265,7 +269,7 @@ public final class GenerationSemanticIndex {
|
||||
appendingBase = baseRegion;
|
||||
publicationLock.unlock();
|
||||
try {
|
||||
SemanticJournal.append(directory, regionX, regionZ, updates, unpublished);
|
||||
SemanticJournal.append(directory, regionX, regionZ, updates, unpublished, durability);
|
||||
} catch (JournalAppendFailure failure) {
|
||||
journalFailure = failure;
|
||||
throw failure;
|
||||
@@ -289,6 +293,15 @@ public final class GenerationSemanticIndex {
|
||||
}
|
||||
}
|
||||
|
||||
public void sync() throws IOException {
|
||||
try {
|
||||
durability.sync();
|
||||
} catch (IOException failure) {
|
||||
journalFailure = failure;
|
||||
throw failure;
|
||||
}
|
||||
}
|
||||
|
||||
public int discardUnstoredClaims(WorldChunkInventory stored, Set<Long> activationIds) throws IOException {
|
||||
Objects.requireNonNull(stored, "stored chunks");
|
||||
Set<Long> selected = Set.copyOf(activationIds);
|
||||
@@ -1940,7 +1953,8 @@ public final class GenerationSemanticIndex {
|
||||
int regionX,
|
||||
int regionZ,
|
||||
List<ChunkGenerationSemantics> claims,
|
||||
boolean unpublished
|
||||
boolean unpublished,
|
||||
DeferredDurability durability
|
||||
) throws IOException {
|
||||
Path file = directory.resolve(journalFileName(regionX, regionZ));
|
||||
boolean created = !Files.exists(file, LinkOption.NOFOLLOW_LINKS);
|
||||
@@ -1974,10 +1988,10 @@ public final class GenerationSemanticIndex {
|
||||
}
|
||||
}
|
||||
if (!unpublished) {
|
||||
Durability.force(channel);
|
||||
durability.written(file);
|
||||
}
|
||||
if (created) {
|
||||
RegionShard.forceDirectory(directory);
|
||||
durability.linked(directory);
|
||||
}
|
||||
} catch (IOException failure) {
|
||||
try {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package art.arcane.iris.world.history;
|
||||
|
||||
import art.arcane.iris.world.storage.DeferredDurability;
|
||||
import art.arcane.iris.world.storage.Durability;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
@@ -52,6 +53,7 @@ public final class SavedBiomeStore {
|
||||
private final Path dimensionRoot;
|
||||
private final Path directory;
|
||||
private final boolean unpublished;
|
||||
private final DeferredDurability durability;
|
||||
private final WriteStripe[] regionLocks = new WriteStripe[64];
|
||||
private final LinkedHashMap<Long, RegionIndex> regions = new LinkedHashMap<>(64, 0.75F, true);
|
||||
private final LinkedHashMap<Long, Optional<SavedBiomeChunk>> chunks = new LinkedHashMap<>(128, 0.75F, true);
|
||||
@@ -61,6 +63,8 @@ public final class SavedBiomeStore {
|
||||
private SavedBiomeStore(Path dimensionRoot, boolean unpublished) throws IOException {
|
||||
this.dimensionRoot = Objects.requireNonNull(dimensionRoot, "dimensionRoot").toAbsolutePath().normalize();
|
||||
this.unpublished = unpublished;
|
||||
this.durability = new DeferredDurability(
|
||||
DeferredDurability.DEFAULT_FLUSH_DELAY_MILLIS, failure -> writeFailure = failure);
|
||||
directory = this.dimensionRoot.resolve("iris/generation/biomes");
|
||||
requireSafeAncestors();
|
||||
for (int index = 0; index < regionLocks.length; index++) {
|
||||
@@ -227,7 +231,7 @@ public final class SavedBiomeStore {
|
||||
}
|
||||
}
|
||||
if (!unpublished) {
|
||||
Durability.force(output.getChannel());
|
||||
durability.written(region.path);
|
||||
}
|
||||
appendCompleted = true;
|
||||
} catch (IOException failure) {
|
||||
@@ -269,6 +273,15 @@ public final class SavedBiomeStore {
|
||||
}
|
||||
}
|
||||
|
||||
public void sync() throws IOException {
|
||||
try {
|
||||
durability.sync();
|
||||
} catch (IOException failure) {
|
||||
writeFailure = failure;
|
||||
throw failure;
|
||||
}
|
||||
}
|
||||
|
||||
public int discardUnstoredClaims(WorldChunkInventory inventory, Set<Long> selectedActivations) throws IOException {
|
||||
Objects.requireNonNull(inventory, "inventory");
|
||||
Set<Long> selected = Set.copyOf(selectedActivations);
|
||||
|
||||
@@ -779,6 +779,7 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
||||
TimeUnit.SECONDS,
|
||||
() -> IrisLogging.warn("Async pregen is still draining outstanding chunks. " + metricsSnapshot())
|
||||
);
|
||||
syncGenerationHistory();
|
||||
|
||||
mantleCleanup.close(MANTLE_CLEANUP_DRAIN_SECONDS, TimeUnit.SECONDS);
|
||||
flushAllRemainingChunks();
|
||||
@@ -807,6 +808,19 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
||||
}
|
||||
}
|
||||
|
||||
private void syncGenerationHistory() {
|
||||
Engine engine = resolveMetricsEngine();
|
||||
if (engine == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
engine.syncGenerationHistory();
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError("Async pregen could not force generation history to stable storage for world "
|
||||
+ world.getName() + ".", e);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isCancelled() {
|
||||
return closing.get() || Thread.currentThread().isInterrupted();
|
||||
}
|
||||
|
||||
@@ -112,6 +112,19 @@ public class MedievalPregenMethod implements PregeneratorMethod {
|
||||
return cached;
|
||||
}
|
||||
|
||||
private void syncGenerationHistory() {
|
||||
Engine engine = resolveEngine();
|
||||
if (engine == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
engine.syncGenerationHistory();
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError("Medieval pregen could not force generation history to stable storage for world "
|
||||
+ world.getName() + ".", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void waitForChunks() {
|
||||
for (CompletableFuture<?> i : futures) {
|
||||
try {
|
||||
@@ -179,6 +192,7 @@ public class MedievalPregenMethod implements PregeneratorMethod {
|
||||
if (prefetchPool != null) {
|
||||
prefetchPool.shutdownNow();
|
||||
}
|
||||
syncGenerationHistory();
|
||||
unloadAndSaveAllChunks(true);
|
||||
chunkFlush.close(UNLOAD_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
} finally {
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
package art.arcane.iris.world.storage;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.file.NoSuchFileException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Group commit for append-only stores: writers record which files and directory entries they
|
||||
* changed, and one background pass forces them shortly afterwards, outside every store lock.
|
||||
* {@link #sync()} is the durability barrier for saves, shutdown and pregeneration completion.
|
||||
*/
|
||||
public final class DeferredDurability {
|
||||
public static final long DEFAULT_FLUSH_DELAY_MILLIS = 2_000L;
|
||||
private static final ScheduledThreadPoolExecutor FLUSHER = createFlusher();
|
||||
|
||||
private final Set<Path> files = ConcurrentHashMap.newKeySet();
|
||||
private final Set<Path> directories = ConcurrentHashMap.newKeySet();
|
||||
private final ReentrantLock flushLock = new ReentrantLock();
|
||||
private final AtomicBoolean scheduled = new AtomicBoolean();
|
||||
private final long flushDelayMillis;
|
||||
private final Consumer<IOException> backgroundFailure;
|
||||
|
||||
public DeferredDurability(long flushDelayMillis, Consumer<IOException> backgroundFailure) {
|
||||
if (flushDelayMillis < 0L) {
|
||||
throw new IllegalArgumentException("Flush delay must not be negative.");
|
||||
}
|
||||
this.flushDelayMillis = flushDelayMillis;
|
||||
this.backgroundFailure = Objects.requireNonNull(backgroundFailure, "backgroundFailure");
|
||||
}
|
||||
|
||||
public void written(Path file) {
|
||||
if (!Durability.enabled()) {
|
||||
return;
|
||||
}
|
||||
files.add(file);
|
||||
schedule();
|
||||
}
|
||||
|
||||
public void linked(Path directory) {
|
||||
if (!Durability.enabled() || File.separatorChar == '\\') {
|
||||
return;
|
||||
}
|
||||
directories.add(directory);
|
||||
schedule();
|
||||
}
|
||||
|
||||
public void sync() throws IOException {
|
||||
flushLock.lock();
|
||||
try {
|
||||
IOException failure = null;
|
||||
for (Path file : files) {
|
||||
files.remove(file);
|
||||
try {
|
||||
forceFile(file);
|
||||
} catch (IOException fileFailure) {
|
||||
failure = append(failure, fileFailure);
|
||||
}
|
||||
}
|
||||
for (Path directory : directories) {
|
||||
directories.remove(directory);
|
||||
try {
|
||||
forceDirectory(directory);
|
||||
} catch (IOException directoryFailure) {
|
||||
failure = append(failure, directoryFailure);
|
||||
}
|
||||
}
|
||||
if (failure != null) {
|
||||
throw failure;
|
||||
}
|
||||
} finally {
|
||||
flushLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private void schedule() {
|
||||
if (scheduled.compareAndSet(false, true)) {
|
||||
FLUSHER.schedule(this::flushInBackground, flushDelayMillis, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
private void flushInBackground() {
|
||||
scheduled.set(false);
|
||||
try {
|
||||
sync();
|
||||
} catch (IOException failure) {
|
||||
backgroundFailure.accept(failure);
|
||||
} catch (RuntimeException failure) {
|
||||
backgroundFailure.accept(new IOException("Deferred durability sync failed unexpectedly.", failure));
|
||||
}
|
||||
}
|
||||
|
||||
private static void forceFile(Path file) throws IOException {
|
||||
try (FileChannel channel = FileChannel.open(file, StandardOpenOption.WRITE)) {
|
||||
Durability.force(channel);
|
||||
} catch (NoSuchFileException replaced) {
|
||||
// Compaction or rewrite already published this file's contents durably under another name.
|
||||
}
|
||||
}
|
||||
|
||||
private static void forceDirectory(Path directory) throws IOException {
|
||||
try (FileChannel channel = FileChannel.open(directory, StandardOpenOption.READ)) {
|
||||
Durability.force(channel);
|
||||
} catch (UnsupportedOperationException failure) {
|
||||
throw new IOException("Directory cannot be durability-synced: " + directory, failure);
|
||||
}
|
||||
}
|
||||
|
||||
private static IOException append(IOException failure, IOException additional) {
|
||||
if (failure == null) {
|
||||
return additional;
|
||||
}
|
||||
failure.addSuppressed(additional);
|
||||
return failure;
|
||||
}
|
||||
|
||||
private static ScheduledThreadPoolExecutor createFlusher() {
|
||||
ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1, runnable -> {
|
||||
Thread thread = new Thread(runnable, "Iris Durability Flush");
|
||||
thread.setDaemon(true);
|
||||
return thread;
|
||||
});
|
||||
executor.setKeepAliveTime(30L, TimeUnit.SECONDS);
|
||||
executor.allowCoreThreadTimeOut(true);
|
||||
executor.setRemoveOnCancelPolicy(true);
|
||||
return executor;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user