feat(tagging): rename before writing, and sync the database inline - #826
Merged
Conversation
Renaming used to happen after the write, and the database was told about it afterwards by an ``ImportTask`` queued behind whatever else the scribe was doing. Between those two moments the row pointed at a path that no longer existed, and any scan processed in the gap saw an unexplained delete plus create: it deleted the row and cascaded the comic's bookmarks and read progress away. Three rounds of fixes narrowed that gap without closing it, because the gap was the design. So the order is inverted. Each archive is renamed to the name comicbox predicts for it — with the pending patch overlaid, so the name reflects the tags about to be written — and every resulting move is applied to the database before ``write_tags`` returns, on the scribe's own thread. No scan can be processed while the two disagree. A stale delete then finds no row, and a stale create converges onto the row already at that path. A conversion still moves the file after the write, so it is synced the same way the moment the batch finishes. What stays queued is only the metadata re-read, which is safe precisely because it names a path the database already holds. Consequences: - Renaming is planned for the whole batch first, so two comics predicting one name, or a name already taken on disk or in the database, are reported instead of colliding. Both the interim and the post-conversion destination are checked, since comicbox refuses to convert onto an existing file and that refusal would land after the rename. - A move the database refuses puts the file back, so disk and database cannot diverge. - A second edit of the same comics now resolves the renamed paths, where before every write in it failed with "no such file". - Whether a library is watched no longer changes any of this. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The other half of the guard: the importer no longer holds back paths an in-flight tag write is moving through, because there is no longer a window in which to hold them. Moves are applied before ``write_tags`` returns, so any scan the importer processes is reconciling against a database that already agrees with the disk. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The redesign the audit pointed at. Renaming now happens before the write, and every move it causes is applied to the database before
write_tagsreturns — which removes the window three earlier rounds of fixes were narrowing without closing.Requires comicbox 4.8.6 (already pinned) for
predict_filename().The window, and why it was the design
Renaming used to happen after the write, and the database was told about it afterwards by an
ImportTaskqueued behind whatever else the scribe was doing. Between those two moments the row pointed at a path that no longer existed. Any scan processed in that gap saw an unexplained delete plus create, deleted the row, and cascaded the comic's bookmarks and read progress away.Every previous fix — recording watched-library renames, chasing conversions to
final_path, the guard registry — made the gap smaller. None removed it, because the gap was the design.So the order is inverted:
No scan can be processed while the two disagree. A stale delete finds no row; a stale create converges onto the row already at that path. What stays queued is only the metadata re-read, which is safe because it names a path the database already holds.
Verified end-to-end against real comicbox: a junk-named
.cbtis renamed, converted to.cbz, and the row follows it — same pk, and the queued task carriesfiles_moved={}.What this closes
Deleting the guard registry (
tagwrite_moves) removes the apparatus and the things it could not do — all findings from the audit:That last one was a user-visible bug: a follow-up edit of the same comics used to fail every write with "no such file", because
BulkTagWriteTaskoutranksImportTaskand the sync could never run between two queued writes. There is now nothing to run between them.Notes for review
library_eventsis gone. Watched or not no longer changes anything, and the re-read is requested either way because a watched library's own re-read is stat-only unless the import-metadata flag is on.COMICBOX_RENAME_CONFIGis gone too — it was a workaround for the extension bug that comicbox 4.8.6 now fixes at the source.apply()/finish()—finish()would end this batch's live progress status. Only the move phase runs, per library, wrapped so one library's failure can't abandon the rest.make lint,make ty, full pytest and vitest all pass.Remaining from the audit after this: the poller pairing relaxation (PR E) and the watcher unmount guard.