mojira.dev
MC-312010

TicketStorage#purgeStaleTickets always calls setDirty(): chunk_tickets.dat rewritten on every save

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

  1. Start a fresh 26.3 server. Nobody joins.

  2. save-all flush a few times, a few seconds apart.

  3. 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/5

weather.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.

Attachments

Comments 2

Thank you for helping us improve Minecraft! We saved your files:

[media][media][media]

mk7a

(Unassigned)

Plausible

26.3

Retrieved