TicketStorage#purgeStaleTickets ends with an unconditional setDirty() and ServerChunkCache#tick calls it every tick. So chunk_tickets.dat is always dirty, and every autosave and every save-all rewrites it in all three dimensions with the same bytes as before. On an idle server that's a 48 byte file with an empty list in it, written with O_SYNC three times per save.
Same kind of thing as MC-274519 (all maps resaved on every autosave), just smaller.
Steps to reproduce
Start a fresh 26.3 server. Nobody joins.
save-all flusha few times, a few seconds apart.After each one, check the modification time and a checksum of
world/dimensions/minecraft/*/data/minecraft/chunk_tickets.dat.
Or python3 saveddata_repro.py --accept-eula ticket-churn, which does the same against a stock server.
Expected result
chunk_tickets.dat is only written when a ticket that actually gets saved was added, removed or changed.
Actual result
Five saves, idle server:
data/minecraft/game_rules.dat 818 B rewritten 0/5, byte-identical 0/5
data/minecraft/scoreboard.dat 48 B rewritten 0/5, byte-identical 0/5
data/minecraft/weather.dat 112 B rewritten 5/5, byte-identical 0/5
data/minecraft/world_clocks.dat 103 B rewritten 5/5, byte-identical 0/5
dimensions/minecraft/overworld/data/minecraft/chunk_tickets.dat 48 B rewritten 5/5, byte-identical 5/5
dimensions/minecraft/the_end/data/minecraft/chunk_tickets.dat 48 B rewritten 5/5, byte-identical 5/5
dimensions/minecraft/the_nether/data/minecraft/chunk_tickets.dat 48 B rewritten 5/5, byte-identical 5/5weather.dat and world_clocks.dat get rewritten too, but their content really changes. The three chunk_tickets.dat are the only files rewritten with identical content every time.
Code analysis
// TicketStorage
public void purgeStaleTickets(final ChunkMap chunkMap) {
this.removeTicketIf((ticket, chunkPos) -> {
if (this.canTicketExpire(chunkMap, ticket, chunkPos)) {
ticket.decreaseTicksLeft();
return ticket.isTimedOut();
} else {
return false;
}
}, null);
this.setDirty();
}removeTicketIf already calls setDirty() itself when it removes something, so the one at the end only matters for decreaseTicksLeft(). ticks_left is in the ticket codec, but packTickets only saves types with persist(), which is forced and portal, and only portal has a timeout. So the flag is needed while a portal ticket is counting down and that's it.
addTicket has the same problem. It sets the flag for every type, including player_loading and player_simulation, which are never saved.
Only setting it when the ticket's type has persist() would fix both, I think.
It's one small synchronous write per dimension per save, so not a big cost on its own. I ran into it because it means there are always at least three files going through the truncate and rewrite described in MC-312008.
Environment
Dedicated server. Linux 6.10 (ext4) with Temurin 25.0.4, macOS 27 (APFS) with Temurin 25.0.3.
Thank you for helping us improve Minecraft! We saved your files: