Borrow the library to index it, and give it back
The passes that need every photograph's bytes — thumbnails, face indexing — now borrow each one and release it at the end. On a placeholder library that is the difference between peak disk being the working set and being the whole library. Including on cancellation, which was nearly missed: the face sweep returns mid-loop when the user presses Stop, and without releasing there the disk is spent and nothing is delivered for it. `materialise` now answers whether *it* fetched the content. The pool used to work that out by listing a file's parent directory — one listing per file across a library — when the backend already had to `stat` it to decide whether to ask. One syscall instead of a directory walk, and it removes the bug class the tests found earlier: a file at the library root has no `parent()`, so every one of them read as already-downloaded. **Pinning is the retention control**, and it drives the model the catalog already had rather than a second one. `tier_desired` is what the user asked to keep hydrated, `pending_pins` is the resumable work list, and a pinned collection is never dehydrated for the same reason it was never evicted. It was in fact *broken* here before: `get` on a stub failed, and the pin worker logged "one unreadable file must not abandon the whole pin" and silently did nothing. Pinned originals on such a library are recorded with `path = NULL` (`Cache::record_in_place`) rather than copied under `originals/`. Two reasons, and the second is the important one. A copy would hold every pinned photograph twice, with the budget able to evict the half that was not costing the disk. And `release` deletes the file a row names — so a row that names none cannot delete anything, which puts the one catastrophic operation out of reach by construction rather than by remembering not to call it. Deleting a materialised file inside a synced tree removes the photograph from the server and every other device. Handing disk back is `spawn_dehydrate`, which asks the client. Two gaps written down rather than papered over (docs/storage.md §7): a hydrating pass cannot yet quote its cost, because a stub reports no size; and the two sweeps hold separate pools, so a library indexed for both fetches twice.
This commit is contained in:
@@ -222,6 +222,56 @@ impl Cache {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// TRACES: FR-NC-6c | FR-NC-6a
|
||||
/// Record an original this cache does **not** own the bytes of.
|
||||
///
|
||||
/// The virtual-filesystem case. On a library kept by a sync client the
|
||||
/// original is materialised *in the library folder itself*, so copying it
|
||||
/// under `originals/` would hold two copies of every pinned photograph —
|
||||
/// and the copy would be the one the budget could evict while the real
|
||||
/// disk cost stayed.
|
||||
///
|
||||
/// So the bytes are left where they are and only the bookkeeping is kept.
|
||||
/// `path` is deliberately `NULL`, which is what makes this safe:
|
||||
/// [`release`](Self::release) deletes the file a row names, and a row that
|
||||
/// names none deletes nothing. **That matters more than it sounds.**
|
||||
/// Deleting a materialised file inside a synced folder does not free a
|
||||
/// cache — it deletes the photograph, and the client propagates that to
|
||||
/// the server and to every other device. Handing the disk back is the
|
||||
/// backend's job (`RemoteBackend::dematerialise`), not this one's.
|
||||
///
|
||||
/// `bytes` is what the original occupies where it lies, for the budget and
|
||||
/// for reporting; pass 0 where it is not known.
|
||||
pub fn record_in_place(
|
||||
&self,
|
||||
conn: &Connection,
|
||||
image: ImageId,
|
||||
bytes: u64,
|
||||
pinned: bool,
|
||||
now: i64,
|
||||
) -> Result<(), CatalogError> {
|
||||
conn.execute(
|
||||
"INSERT INTO image_cache
|
||||
(image_id, tier_actual, tier_desired, bytes, last_used, pinned, path)
|
||||
VALUES (?1, ?2, ?2, ?3, ?4, ?5, NULL)
|
||||
ON CONFLICT(image_id) DO UPDATE SET
|
||||
tier_actual = ?2,
|
||||
tier_desired = max(tier_desired, ?2),
|
||||
bytes = ?3,
|
||||
last_used = ?4,
|
||||
pinned = max(pinned, ?5),
|
||||
path = NULL",
|
||||
rusqlite::params![
|
||||
image.0 as i64,
|
||||
Tier::Original.stored(),
|
||||
bytes as i64,
|
||||
now,
|
||||
i64::from(pinned),
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read a cached original back, if it is here.
|
||||
///
|
||||
/// Touches `last_used`, which is what makes the eviction order reflect
|
||||
|
||||
Reference in New Issue
Block a user