2.88.3
Pre-release2.88.3
Fixed
-
The shared processing status file corrupted under concurrent writes, roughly
1.7 times a day on a two-worker instance. Every writer used one temp filename
and opened it withw, which truncates before the lock is taken, so one
worker could wipe another's partial write and both renamed the result into
place. The next reader hit a JSON error and the file was reset, dropping the
current job, the queue display, and the recorded server start time. Each
writer now gets its own temp file and an atomic replace, through a shared
helper that also fixes the identical pattern in the processing queue's own
state file. -
Status updates were also lost outright between workers, since the only guard
around read-modify-write was athreading.Lockthat does not reach past its
own process. With four concurrent writers, 7 of 40 queue additions survived.
A file lock now spans workers, and all 40 survive. -
Reading the status file rewrote it whenever it expired a stale job, so a
status poll could write and the timeout warning fired from whichever SSE
thread happened to read first. Expiry is now separate: read-only callers
apply it in memory, and only the paths that were already writing persist it.
Recovering a corrupt file still rewrites, since that is one-shot. -
Startup reconciliation held the cross-process status lock across a SQLite
write, which can block for the 30 second busy timeout with every status
request queued behind it. The database reset now runs outside the lock. -
An SSE subscriber was handed a status snapshot and then discarded it to
fetch the same data again, costing one extra lock acquisition per open
stream per update.to_dictnow accepts the snapshot. -
_migrate_sponsor_fkcould leave its table rebuild in an open transaction
and the connection with foreign keys disabled if a step failed, letting a
later migration commit a half-finished rebuild. It now rolls back, the same
fix applied to the fingerprint migration.
2.88.2
Fixed
- Pattern deduplication deleted the audio fingerprints of every duplicate it
merged away, so a survivor could end up with no fingerprint even though a
duplicate had one. Duplicates share a text template, so that fingerprint
describes the survivor's audio too; it now moves across when the survivor
has none. Only fresh audio can rebuild one, and 2.88.1 made the loss likelier
by ranking operator-written patterns, which rarely carry a fingerprint, above
auto-learned ones, which usually do. The fingerprint moves only onto an
active survivor, since fingerprint matching ignoresis_activeand would
otherwise keep cutting audio the operator had switched off. - Deduplication ranked a disabled pattern above an active one, so a row the
operator had switched off could delete the live pattern and absorb its stats.
Active now outranks tier and confirmation count both. - Deleting a pattern left its audio fingerprint behind. The matcher loads
fingerprints without checking that the pattern still exists, so the orphan
went on cutting the same audio with nothing left to disable or inspect. The
single and bulk delete paths now take the fingerprint with the pattern, which
the other deletion paths already did.
Changed
audio_fingerprints.pattern_idnow carries a real foreign key against
ad_patternswithON DELETE CASCADE, so a fingerprint can no longer outlive
its pattern because a caller forgot to clean up. Existing databases migrate on
startup by rebuilding the table. Fingerprints whose pattern is already gone
cannot satisfy the constraint, so the migration copies them to
_orphaned_audio_fingerprintsinstead of deleting them, and aborts before the
destructive step if the row count does not match. Databases created around
v0.1.107 already carry this constraint, since v0.1.108 dropped it from the
schema without rebuilding tables that already existed; those are detected and
left alone.