Skip to content

feat(api): trim the library feed, so its expiry answer is finally exercised - #166

Merged
InstaZDLL merged 2 commits into
mainfrom
feat/library-event-retention
Aug 30, 2026
Merged

feat(api): trim the library feed, so its expiry answer is finally exercised#166
InstaZDLL merged 2 commits into
mainfrom
feat/library-event-retention

Conversation

@InstaZDLL

@InstaZDLL InstaZDLL commented Aug 30, 2026

Copy link
Copy Markdown
Owner

RFC-007 decision 7, built.

Until now nothing purged. library.events_purged_through sat at 0 by construction, which meant the feed's whole expiry path — the watermark read, the Conflict, the comment explaining why a floor derived from surviving rows would be wrong — was correct code that had never once run.

The rules

Two bounds, and the floor wins. Thirty days by default, never fewer than ten thousand events per library. Both are refused at startup if zero or negative rather than quietly defaulted, which is what parse_positive_env already does for every other bound here.

The age bound is exclusive: an event exactly thirty days old lives.

The delete and the watermark are one transaction. That is the whole of what makes the expiry answer honest — written separately there is a window where the watermark claims less than has gone, and a client reading into it is handed a catch-up that looks complete while silently skipping the gap. MAX on the update, so a pass cutting an older tail cannot lower a watermark an earlier one raised.

One transaction per library rather than one for all: the writer gate is process-wide, and holding it across every feed on a server with fifty libraries would stall every other mutation for the length of the pass.

Three rules removed to watch the test fall

Removed What happens
the exclusive bound the event sitting exactly on it is cut — 3 removed where the test wants 2
the floor three rows go where one should
the watermark advance the feed hands back a tail it should have refused

The test was flaky, and it said so on the second run

It read the clock, then let the purge read it again — and with an exclusive bound, "exactly thirty days old" is a single instant. The milliseconds between the two reads put the boundary event on the wrong side, so it passed once and failed the next time for nothing that had changed.

purge_library_events takes now_ms now, the way sweep_expired_sessions already did. A caller that cannot name the instant cannot place an event on it — it can only aim near it and hope, which is not a test of a boundary. Five consecutive runs green.

It was caught because the suite was run again rather than once.

Still decided and not built

Decision 8, the acknowledgement table. RFC-007's section says so, and the Implémentée par line names only what runs.

Gates

cargo fmt --all --check, cargo clippy --all-targets --all-features -- -D warnings, cargo test --all-features — 155 tests across fourteen targets, all green.

Summary by CodeRabbit

  • Nouvelles fonctionnalités

    • Ajout d’une purge automatique et quotidienne des événements de bibliothèque.
    • La rétention peut être configurée selon un âge maximal et un nombre minimal d’événements conservés.
    • Les suppressions sont effectuées de manière fiable, avec suivi de leur progression et gestion des erreurs.
  • Documentation

    • Mise à jour du RFC concernant l’état d’implémentation des décisions de rétention.

…rcised

RFC-007 decision 7, built. Until now nothing purged:
`library.events_purged_through` sat at 0 by construction, which meant the
feed's whole expiry path — the watermark read, the `Conflict`, the
comment explaining why a derived floor would be wrong — was correct code
that had never once run.

Two bounds, and the floor wins. Thirty days by default, never fewer than
ten thousand events per library, both refused at startup if zero or
negative rather than quietly defaulted. The age bound is exclusive: an
event exactly thirty days old lives.

**The delete and the watermark are one transaction.** That is the whole
of what makes the expiry answer honest — written separately there is a
window where the watermark claims less than has gone, and a client
reading into it is handed a catch-up that looks complete while silently
skipping the gap. `MAX` on the update, so a pass cutting an older tail
cannot lower a watermark an earlier one raised.

One transaction per library rather than one for all of them: the writer
gate is process-wide, and holding it across every feed on a server with
fifty libraries would stall every other mutation for the length of the
pass.

Three rules removed to watch the test fall: make the bound inclusive and
the event sitting on it is cut; drop the floor and three rows go where
one should; stop moving the watermark and the feed hands back a tail it
should have refused.

## The test was flaky, and it said so on the second run

It read the clock, then let the purge read it again — and with an
exclusive bound, "exactly thirty days old" is a single instant. The
milliseconds between the two reads put the boundary event on the wrong
side, so it passed once and failed the next time for nothing that had
changed.

`purge_library_events` takes `now_ms` now, the way
`sweep_expired_sessions` already did. A caller that cannot name the
instant cannot place an event on it — it can only aim near it and hope,
which is not a test of a boundary. Five consecutive runs green, and it
was caught because the suite was run again rather than once.

Claude-Session: https://claude.ai/code/session_01GtAmdsaBCg8Cs2rvrdLD7Z
Signed-off-by: InstaZDLL <github.105mh@8shield.net>
@github-actions github-actions Bot added scope: server Server core (Rust) scope: docs Docs, README, assets type: feat New feature labels Aug 30, 2026
The `Implémentée par` line stopped at #152 and had missed #159 since
this morning — the album events, decision 6 — while the line right below
it claims to be the one thing in this document that can be checked. A
line that says what runs is worth exactly as much as its last update.

#166 joins it for decision 7. Decision 8 stays absent, because it is
decided and not built, and that is the distinction the line exists to
carry.

Claude-Session: https://claude.ai/code/session_01GtAmdsaBCg8Cs2rvrdLD7Z
Signed-off-by: InstaZDLL <github.105mh@8shield.net>
@github-actions github-actions Bot added size: m 50-200 lines type: feat New feature and removed type: feat New feature labels Aug 30, 2026
@coderabbitai

coderabbitai Bot commented Aug 30, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 0ae26c58-bb95-4464-9499-bd8fc6fbd90e

📥 Commits

Reviewing files that changed from the base of the PR and between a3ae0e8 and a61aa89.

📒 Files selected for processing (6)
  • docs/rfcs/RFC-007-library-event-stream.md
  • src/config.rs
  • src/main.rs
  • src/services/library_events.rs
  • src/services/mod.rs
  • tests/catalog.rs

Included review availability: 1 review is currently available. Your included PR review attempts over the past 7 days set your current allowance at 3 reviews per hour.


📝 Walkthrough

Walkthrough

La configuration définit les seuils de rétention. Le serveur démarre une purge immédiate puis quotidienne. Chaque bibliothèque est purgée dans une transaction, avec conservation d’un nombre minimal d’événements et mise à jour du watermark.

Changes

Rétention des événements de bibliothèque

Layer / File(s) Summary
Configuration et transmission des seuils
src/config.rs, src/services/mod.rs
LibraryEventRetention définit la durée et le nombre minimal d’événements. Config charge ces valeurs depuis l’environnement et DomainServices les conserve.
Purge périodique et transactionnelle
src/services/library_events.rs, src/main.rs
Le serveur démarre le purgeur. Le service exécute une purge immédiate puis quotidienne, supprime les événements éligibles par bibliothèque, respecte min_events et avance events_purged_through atomiquement.
Tests et documentation RFC
tests/catalog.rs, docs/rfcs/RFC-007-library-event-stream.md
Le test couvre les bornes d’âge, le plancher, le watermark, les curseurs périmés et l’idempotence. La RFC indique la décision 7 comme construite et la décision 8 comme non construite.

Estimated code review effort: 4 (Complex) | ~45 minutes

Merge Risk: ⚪ Minimal · up to a61aa

The change adds transactional library-event retention with bounded configuration and exercises the expiry path; no actionable merge-blocking risk remains after normal checks and review.

Sequence Diagram(s)

sequenceDiagram
  participant Serveur
  participant DomainServices
  participant BaseDeDonnees
  Serveur->>DomainServices: Démarrer spawn_library_event_purge()
  DomainServices->>DomainServices: Exécuter purge_library_events(now_ms)
  DomainServices->>BaseDeDonnees: Purger les événements anciens par bibliothèque
  BaseDeDonnees-->>DomainServices: Retourner les suppressions et le watermark
  DomainServices-->>Serveur: Journaliser le résultat ou l’erreur
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.45% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 11 functions across 5 files. (1 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed Le titre décrit clairement l’implémentation de la purge et de l’expiration du flux d’événements de bibliothèque, qui constitue le changement principal.
Description check ✅ Passed La description est détaillée et couvre l’objectif, les règles de rétention, les garanties transactionnelles, les tests et l’état de la RFC. Elle ne reprend pas exactement les sections du modèle et ne …
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Description check

Explanation

La description est détaillée et couvre l’objectif, les règles de rétention, les garanties transactionnelles, les tests et l’état de la RFC. Elle ne reprend pas exactement les sections du modèle et ne contient pas la confirmation DCO, mais elle reste globalement complète et pertinente.

Full details: Docstring Coverage

Explanation

Docstring coverage is 45.45% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 11 functions across 5 files. (1 skipped: 1 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/library-event-retention

Warning

Your free Security trial is over. An organization admin can activate Security or dismiss this notice.


Comment @coderabbitai help to get the list of available commands.

Comment thread tests/catalog.rs Dismissed
@github-actions github-actions Bot added type: feat New feature and removed type: feat New feature labels Aug 30, 2026
@InstaZDLL
InstaZDLL merged commit e21ddfa into main Aug 30, 2026
14 of 15 checks passed
@InstaZDLL
InstaZDLL deleted the feat/library-event-retention branch August 30, 2026 16:33
InstaZDLL added a commit that referenced this pull request Aug 30, 2026
Three pull requests landed since this file last described the work
left: #165 decided the four questions RFC-007 and RFC-009 were carrying,
#166 built decision 7, and #163 was already recorded.

Batched rather than patched after each merge, which is the promise made
after the third amendment this morning. The reason to keep amending at
all is the same one this file's own traps section gives — a document
that lists finished work as pending is how another agent lost three
batches — but doing it per pull request turns the record into a
changelog of itself.

What the next agent picks up shrinks to three, and two of them are no
longer questions:

- Decision 8, the acknowledgement table: decided by #165 and not built,
  which is the distinction the `Implémentée par` line carries.
- Sweeping `artwork_dir`: queued work, not an arbitration. The canvas
  store has a sweeper; the cover store has the same property and none.
- §4, the four Subsonic clients, still the gate on `v2.0-beta` and still
  not something a session can do.

Claude-Session: https://claude.ai/code/session_01GtAmdsaBCg8Cs2rvrdLD7Z
Signed-off-by: InstaZDLL <github.105mh@8shield.net>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: docs Docs, README, assets scope: server Server core (Rust) size: m 50-200 lines type: feat New feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants