Commit Graph
4 Commits
Author SHA1 Message Date
DarkBreakpoint 525ed829e9 feat(dvr): key a recording file by the media it holds, not by who asked
Second step of the shared-recording model. A materialization is now
identified by what it contains rather than by the first request for it, so
two principals asking for the same media converge on one physical file with
two library entries and a reference count of two.

The identity rule stays in the DVR, which is the only place that knows what
makes two requests the same recording. It is supplied as a serialised,
field-named key rather than a Debug string, because it is persisted and has
to survive a build that reorders the enum. A task with no identity falls
back to a file keyed on its own uuid, so unidentified tasks cannot silently
collide on one file.

Detaching is now meaningful: dropping one entry leaves the file and the
other entry intact, and only the last detach removes the file. Six tests
cover convergence, per-principal views of one file, distinct media staying
distinct, partial detach and last detach.

Behaviour is unchanged. The service still rejects a cross-principal
duplicate before it reaches persistence, so no two entries share a file in
practice yet. Enabling that additionally requires the queue to stop
promoting one task at a time into a single `active` slot: two entries on one
file would otherwise each be promoted and each run a transfer to the same
path. That is the next step, and it is why this one stops at persistence.
2026-09-01 09:51:06 -05:00
DarkBreakpoint 64c7b2333a feat(dvr)!: split persistence into materializations and library entries
First step of the shared-recording model. A recording is now stored as two
records: a PersistedMaterialization, which is the file on disk and the work
that produces it, and a PersistedLibraryEntry, which is one principal's link
to it. The materialization carries no owner and no visibility, because a
file that several users have asked for cannot belong to any one of them.

The mapping is still one entry per file, so no behaviour changes yet. What
changes is where the truth lives: reference_count is now a stored field with
one owner, and read_records() refuses any state where it disagrees with the
links pointing at the file, or where a link names a file that is not there.
A file deleted while still reachable, or kept forever after its last
reference went away, is not something a later pass can work out by guessing.

The DVR still works in whole tasks; split() and join() are the repository's
job precisely so the count has a single place to be maintained. join() is
lossless, and the link is authoritative for owner, visibility and quota
while the file is authoritative for everything physical.

Entry ids and materialization ids are separate namespaces from the start
(mat-<uuid>), so they cannot be used interchangeably in the interval before
one file legitimately serves several entries.

The schema name and database file change to recording_library, because the
previous shape stored the owner inside the task and cannot be expressed as a
one-to-one value migration into two records. The queue does not migrate — as
already documented — so the old database and history are left untouched
beside the new pair rather than failing the open.

The startup tests now discover the repository's artefacts instead of naming
them; hardcoding the filenames would have silently stopped testing the
fail-closed path the moment they changed, which is exactly what happened
here.
2026-09-01 09:39:00 -05:00
DarkBreakpoint 154112de52 feat(dvr)!: persist the recording queue in a recoverable B+Tree
The queue was a single JSON document rewritten in full on every mutation.
That cannot survive a torn write, and a change to the record shape had no
upgrade path other than refusing to load — load_from_disk fails closed on
an unknown schema_version, which would have stranded every operator on a
version bump.

Replace it with RecordingRepository: one record per task in a B+Tree, with
every write routed through BPlusTreeRecoveryJournal. A deleted or corrupt
database is now rebuilt from the recovery history instead of being lost,
and a future record-shape change becomes a migration step rather than a
refusal to start.

- backend/repository/src/recording_repository.rs owns the persisted shape
  and RecordingRecoverySchema V1. It knows nothing about execution: the DVR
  decides what a task means and hands over a set to commit atomically.
- commit() diffs the candidate against what is stored, so the recovery
  journal stays proportional to what actually changed rather than to the
  size of the queue.
- The partition (queued/scheduled/active/finished) is stored explicitly.
  Deriving it from the state would be wrong: `active` is a distinct slot
  and two tasks in the same state can sit in different partitions.
- Repository calls run on spawn_blocking; it fsyncs both the journal and
  the B+Tree, so it must not run on a runtime worker thread.

RecordingTaskState moves to shared, along with the label() helper and the
TransferStatusDto conversion that were inherent impls in the DVR. The
repository crate cannot depend on the DVR, and the state is part of the
persisted contract either way.

Recovery generations are written under backup_dir, so they survive the loss
of the storage volume.

There is no migration from recordings_state.json: an existing queue is
discarded on upgrade.
2026-09-01 07:52:22 -05:00
euzuandGitHub 9ec775b35b Feature/multi crate (#837)
Refactored to multi crate project
2026-08-27 15:36:23 +02:00