Skip to content

Fix orphaned entity data - #1027

Closed
tonyjamesstark wants to merge 6 commits into
PlayPro:masterfrom
tonyjamesstark:fix-orphaned-entity-data
Closed

tonyjamesstark wants to merge 6 commits into
PlayPro:masterfrom
tonyjamesstark:fix-orphaned-entity-data

Conversation

@tonyjamesstark

Copy link
Copy Markdown
Contributor

co_entity stores the entity state for each mob kill, and it is referenced by co_block.data where action = 3 and type <> 0. It has no world column, so r:#world purges delete the kill rows but never the co_entity rows. Those blobs become orphans that no later purge removes.

  • SQLite: the purge copies only co_entity rows that a retained kill row still references. This also removes orphans that earlier purges left.
  • MySQL and DuckDB: the purge deletes the co_entity rows of the purged kill rows, before co_block is purged. MySQL uses a multi-table DELETE ... JOIN. On MySQL 8, EXPLAIN shows it reads co_block through the wid index and deletes by primary key. Global purges keep the existing time-based delete.
  • MySQL with #optimize: the purge deletes every orphaned co_entity row through a temporary table of referenced ids. This needs the CREATE TEMPORARY TABLES privilege. A failure is reported the same way as the table loop, and the rest of the purge still runs.

Player kills (type = 0, data = user id) are never treated as co_entity references.

Removing orphaned rows now also means the v26 migration of SQLite and MySQL entity data converts fewer rows.

authored and verified with claude

co_entity has no world column, so /co purge r:#world deleted the kill
rows in co_block but kept every co_entity row. Those blobs became
orphans that no later purge removed.

- SQLite: copy only the co_entity rows that a retained kill row still
  references. This also drops orphans left by earlier purges.
- MySQL/DuckDB: delete the co_entity rows of the kill rows a world purge
  removes, before co_block is purged. MySQL uses a join so MariaDB and
  MySQL 5.7 do not run a dependent subquery. Global purges keep the
  cheaper time-based delete, which is equivalent. If MySQL stops between
  the two deletes, running the same purge again removes the remaining
  kill rows.

Player kills use the kill action with type 0 and a user id in data, so
they are never treated as co_entity references.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PdEuKbjcVoVinVQJq4te1f
MySQL deletes in place, so co_entity rows orphaned by earlier world
purges stay until something removes them. With #optimize, delete every
co_entity row that no kill row references, through a temporary table of
referenced ids (avoids an anti-join on the unindexed co_block.data), then
let OPTIMIZE reclaim the space. Failures are reported like the table
loop, so the entity_spawn link cleanup still runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PdEuKbjcVoVinVQJq4te1f
@Intelli

Intelli commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

Before merging, please address two MySQL safety issues:

  1. The #optimize sweep snapshots referenced IDs and subsequently deletes everything absent from that snapshot. A second installation can commit a new kill between those statements, causing its valid entity state to be deleted. Please make the sweep safe against concurrent writers, or enforce exclusive maintenance before allowing it.
  2. The world-specific entity-state deletion and corresponding block deletion need to succeed or roll back together. Currently, a block-delete failure or interruption can leave retained kill records without their rollback data. Please make those deletions atomic on MySQL.

TonyJamesStark and others added 3 commits October 5, 2026 10:54
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The #optimize orphan sweep snapshots the co_entity ids that kill rows
reference, then deletes every row missing from the snapshot. Another
installation sharing the database could commit a kill between the two
statements and lose its entity data. The sweep now only deletes rows
inside the purge time range. A kill always writes its co_entity row with
the current time, and a purge range ends at least 24 hours ago, so a
concurrent kill never matches.

A world purge deletes the entity data of the purged kills and
then the kill rows. On MySQL a failure or cancellation between the two
left kept kill rows without their rollback data. Both deletes now run in
one transaction.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A SQLite purge dropped every co_entity row no kill referenced, including
rows newer than the purge range, and counted them as deleted. It now only
removes unreferenced rows inside the range, like the MySQL #optimize sweep.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Intelli

Intelli commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

Accepted, but merge conflicts must be resolved.

…ty-data

# Conflicts:
#	src/main/java/net/coreprotect/command/PurgeCommand.java
#	src/main/java/net/coreprotect/database/PurgeFilter.java
@Intelli

Intelli commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

These changes were included in #1030.

@Intelli Intelli closed this Oct 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants