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 @@ -77,6 +77,8 @@ public final class AnvilDiagnostics {
private final LongAdder chunksWithoutEntry;
private final LongAdder partialChunks;
private final LongAdder unsupportedChunks;
private final LongAdder chunksMigrated;
private final Map<String, LongAdder> migratedSourceVersions;

/**
* Creates a new diagnostics instance with empty counters.
Expand All @@ -96,6 +98,8 @@ public AnvilDiagnostics() {
this.chunksWithoutEntry = new LongAdder();
this.partialChunks = new LongAdder();
this.unsupportedChunks = new LongAdder();
this.chunksMigrated = new LongAdder();
this.migratedSourceVersions = new ConcurrentHashMap<>();
}

/**
Expand Down Expand Up @@ -267,6 +271,58 @@ public void countChunkSaved() {
this.chunksSaved.increment();
}

/**
* Counts a chunk which was translated from an older version to the one the server writes.
* <p>
* Counted per source version as well as in total, because the two answer different questions. A
* total says how much work migration is costing this run; the breakdown says which versions a
* world actually holds, which is what tells somebody whether a conversion is nearly finished or
* has barely begun. The same per-version cap as elsewhere in this class applies: a version
* beyond it is still counted in {@link #chunksMigrated()} and only loses its own entry.
* </p>
*
* @param sourceVersion the data version the chunk carried before it was translated
* @since 2.2.0
*/
public void countChunkMigrated(int sourceVersion) {
this.chunksMigrated.increment();
String version = Integer.toString(sourceVersion);
LongAdder counter = this.migratedSourceVersions.get(version);

if (counter == null) {
if (this.migratedSourceVersions.size() >= MAX_TRACKED_NAMES) {
return;
}
LongAdder created = new LongAdder();
LongAdder previous = this.migratedSourceVersions.putIfAbsent(version, created);
(previous == null ? created : previous).increment();
return;
}
counter.increment();
}

/**
* Returns the amount of chunks which were translated from an older version.
*
* @return the amount of migrated chunks
* @since 2.2.0
*/
public long chunksMigrated() {
return this.chunksMigrated.sum();
}

/**
* Returns how many chunks were migrated per stored source version.
*
* @return the amount of migrated chunks per source version
* @since 2.2.0
*/
public @Unmodifiable Map<String, Long> migratedSourceVersions() {
Map<String, Long> snapshot = new java.util.HashMap<>();
this.migratedSourceVersions.forEach((version, counter) -> snapshot.put(version, counter.sum()));
return Map.copyOf(snapshot);
}

/**
* Counts a chunk which could not be loaded or saved.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package net.onelitefeather.falco.anvil;

import org.jetbrains.annotations.ApiStatus;

/**
* How far a loader carries a chunk that was written by an older version of the game.
* <p>
* The three modes differ in one question only — what happens to the migrated chunk after it has
* been decoded — and that question is a trade between time, disk and safety rather than a matter of
* correctness. All three read the same chunks and hand the same blocks to the server; what changes
* is how often that work is repeated and whether the world on disk is touched.
* </p>
* <p>
* <b>Why this is off unless asked for.</b> Migration is not free and it is not reversible in the
* {@link #ON_DISK} case, so neither cost may be taken on behalf of a caller who never asked. A
* loader in {@link #OFF} behaves exactly as it did before this option existed.
* </p>
*
* @author TheMeinerLP
* @version 1.0.0
* @since 2.2.0
*/
@ApiStatus.Experimental
public enum ChunkMigrationMode {

/**
* No migration at all: a chunk is decoded exactly as it is stored.
* <p>
* This is the default, and it is the mode in which a world older than the running server loses
* whatever it holds that the server no longer knows by name. A block whose name was changed
* since the chunk was written is not recognised, and the configured {@link UnknownEntryPolicy}
* decides what stands in its place — air, with the shipped default. Nothing reports how much
* of the world that affected beyond one log line per distinct name.
* </p>
*/
OFF,

/**
* Every chunk is migrated as it is read, and the world on disk is left untouched.
* <p>
* <b>This costs time on every single load.</b> A chunk that is loaded, unloaded and loaded again
* is migrated twice, because nothing of the first migration was kept. On a world whose chunks
* are mostly older than the server, that work lands on the chunk loading path a player waits
* for, and it does not diminish with uptime the way a cache would.
* </p>
* <p>
* What it buys is that the world on disk is exactly what it was before the server started. A
* world in this mode can still be opened by the older server it came from, and a mistake in a
* migration rule cannot damage anything permanently, because nothing is written back.
* </p>
*/
IN_MEMORY,

/**
* Every chunk is migrated as it is read and the migrated form is written back to the region
* file, so each chunk pays the cost once rather than on every load.
* <p>
* <b>This rewrites the world.</b> After a chunk has been migrated in this mode, the stored chunk
* is the migrated one and the original is gone from the region file — which is why a loader in
* this mode refuses to start without a backup directory it could restore from. See
* {@code FalcoAnvilLoader.Builder#migration} for how that backup is taken.
* </p>
* <p>
* The rewrite also means the world stops being readable by the older server it came from, since
* its chunks now carry the running server's data version. That is the point of the mode and not
* a side effect, but it is a one-way step and the reason the backup is not optional.
* </p>
*/
ON_DISK
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package net.onelitefeather.falco.anvil;

import net.kyori.adventure.nbt.CompoundBinaryTag;
import org.jetbrains.annotations.ApiStatus;

/**
* Lifts the stored form of a chunk from the version it was written by to the version the server
* runs.
* <p>
* This interface exists in {@code falco-anvil} rather than next to the engine that implements it
* because the dependency only runs one way: {@code falco-migration} depends on this module, so this
* module cannot depend on it back. A loader therefore names the capability and finds a provider on
* the classpath, exactly as it does for {@link ChunkVersionPolicy} and {@link UnknownEntryPolicy}.
* A deployment that never puts a migration engine on the classpath carries no migration code at
* all.
* </p>
* <p>
* <b>A migrator translates and nothing else.</b> It does not decide whether migration should happen
* — {@link ChunkMigrationMode} does — it does not read or write region files, and it does not log or
* count. It is handed the root compound of one chunk and returns the root compound that same chunk
* would have if the current version had written it.
* </p>
* <p>
* <b>Called from several threads at once.</b> The migrator is resolved once, when the loader is
* built, and every load after that consults the same instance, including every parallel load. An
* implementation has to be thread-safe on its own; the loader takes no lock around the call.
* </p>
*
* @author TheMeinerLP
* @version 1.0.0
* @since 2.2.0
*/
@ApiStatus.Experimental
public interface ChunkMigrator {

/**
* Reports whether this migrator can lift a chunk of the given stored version.
* <p>
* Asked before {@link #migrate} so that a chunk this migrator cannot help with is passed through
* untouched rather than failing the load. An engine that starts at Minecraft 1.13 answers
* {@code false} for everything below it, and the loader then treats the chunk exactly as it
* would in {@link ChunkMigrationMode#OFF} — which is what the caller had before, not a
* regression.
* </p>
*
* @param sourceVersion the data version the chunk carries
* @param targetVersion the data version the server writes
* @return whether {@link #migrate} would do anything useful with such a chunk
*/
boolean canMigrate(int sourceVersion, int targetVersion);

/**
* Translates one chunk into the form the target version would have written.
*
* @param data the root compound of the chunk, as read from the region file
* @param targetVersion the data version the server writes
* @return the translated root compound, which may be {@code data} itself if nothing applied
* @throws ChunkDataException if the chunk cannot be translated, which fails that one chunk's
* load rather than being silently passed through — a chunk that
* could not be migrated would otherwise reach the server as the
* partly-unreadable data this whole option exists to prevent
*/
CompoundBinaryTag migrate(CompoundBinaryTag data, int targetVersion) throws ChunkDataException;
}
Loading
Loading