From af335228e549fc0264ed4f8f5f1036d69e0b8dda Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Fri, 17 Jul 2026 12:57:09 +0100 Subject: [PATCH] docs: clarify deletion retention tradeoffs --- .env.example | 2 ++ docs/explanation/repository-lifecycle.md | 30 ++++++++++++++++++++++++ docs/reference/configuration.md | 2 ++ nix/module.nix | 13 ++++++---- src/config.rs | 3 +++ src/nostr/lifecycle/deletion/archival.rs | 8 +++++++ 6 files changed, 54 insertions(+), 4 deletions(-) diff --git a/.env.example b/.env.example index 9e0e449..721b793 100644 --- a/.env.example +++ b/.env.example @@ -286,6 +286,8 @@ # start at relay-observed first_seen_at; used clocks start at last_used_at. # "Additional" periods begin after their corresponding served period ends. # Used requests remain served indefinitely when deletion-request-disrespector is true. +# Seconds permit short tests; production periods should be at least one day and +# comfortably exceed worst-case deletion/archive processing time. # How long an unused request remains served from first_seen_at # CLI: --deletion-request-retention-unused-served-secs diff --git a/docs/explanation/repository-lifecycle.md b/docs/explanation/repository-lifecycle.md index ab5081f..18f9fe7 100644 --- a/docs/explanation/repository-lifecycle.md +++ b/docs/explanation/repository-lifecycle.md @@ -83,6 +83,18 @@ rejected under valid ownership, coordinate-cutoff, relay-targeting, and other NIP-09/NIP-62 rules. Merely matching a tag, inspecting the request, or discovering that a different author owns the target does not count as use. +The unserved gate is the deliberate observation period that establishes whether +an apparently unused request still has practical value. While the request was +served, clients that saw it could avoid sending the deleted target to this relay, +even if copies of that target continued circulating elsewhere. Removing the +request from relay queries gives those late copies an opportunity to reach the +relay again. The retained Tombstone gate still rejects a covered target; that +rejection is evidence of current utility, so the winning request becomes used, +is promoted back to Main, and begins the longer used lifecycle. If no covered +target arrives during the unserved period, the request has supplied no evidence +that its gate is still needed and can expire. Replaying the deletion or vanish +request itself is not such evidence and does not update `last_used_at`. + #### Used When a request is used: @@ -115,6 +127,14 @@ physical removal from Main and Tombstones is asynchronous cleanup and may occur on the next scheduled pass. This bounded cleanup delay is intentional and does not extend admission-gate eligibility past the deadline. +Durations use seconds for configuration consistency and to permit short automated +tests. Production values are expected to be at least one day and comfortably +longer than the worst-case processing time for a deletion or vanish request, +including repository archival and cascade work. Sub-day values are a testing +facility, not a supported production operating point; cleanup is therefore not +coordinated with an initial request handler across an artificially short total +lifetime. + ### Multiple matching requests If several pending requests would independently reject the same arriving event, @@ -217,6 +237,16 @@ correctness, but their retention clocks do not change. This rule is independent of database iteration order and prevents one event arrival from extending an arbitrary number of duplicate requests. +This deterministic single-winner rule applies when one later target admission is +matched against already-pending requests. Initial processing of distinct deletion +requests is intentionally less strict: two requests processed concurrently may +both observe the same stored target before either removal completes and may both +receive use credit. The database deletion API does not report an authoritative +per-event removed count, so eliminating that narrow race would require broader +target-level serialization. The occasional extra used request is accepted: it is +bounded by actual concurrency, does not change deletion correctness, and still +expires through the normal used lifecycle. + ### Simplification of target-set deduplication The lifecycle replaces semantic target-set deduplication and supersession as the diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 87a8c25..777d02f 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -1190,6 +1190,8 @@ These options govern the bounded lifecycle of accepted NIP-09 deletion requests The relay derives unused deadlines from relay-observed `first_seen_at`, never from the client-controlled event `created_at`. It derives used deadlines from `last_used_at`. Each `...ADDITIONAL...` option begins only after its corresponding served period ends; it is not a total retention duration. +Seconds are used for configuration consistency and short automated tests. In production, configure every period to at least one day and keep each total lifecycle comfortably longer than the worst-case deletion processing time, including repository archival and cascade work. Sub-day values are intended only for tests. + #### `NGIT_DELETION_REQUEST_RETENTION_UNUSED_SERVED_SECS` - **Description:** How long an unused deletion or vanish request remains served from relay-observed `first_seen_at` diff --git a/nix/module.nix b/nix/module.nix index 2d69750..ee3d385 100644 --- a/nix/module.nix +++ b/nix/module.nix @@ -198,7 +198,8 @@ let default = 2592000; description = '' Time in seconds an unused deletion or vanish request remains served - from relay-observed first_seen_at (default: 30 days). + from relay-observed first_seen_at (default: 30 days). Production + periods should be at least one day; sub-day values are for tests. ''; }; @@ -208,7 +209,8 @@ let description = '' Additional time in seconds after unused serving ends that a deletion or vanish request remains unserved but eligible to gate admission - (default: 180 days). + (default: 180 days). Production periods should be at least one day; + sub-day values are for tests. ''; }; @@ -219,7 +221,8 @@ let Normal-mode time in seconds a used deletion or vanish request remains served after last_used_at (default: 270 days, 9 fixed 30-day months). Used requests remain served indefinitely when - deletionRequestDisrespector is enabled. + deletionRequestDisrespector is enabled. Production periods should + be at least one day; sub-day values are for tests. ''; }; @@ -230,7 +233,9 @@ let Additional normal-mode time in seconds after used serving ends that a deletion or vanish request remains unserved but continues gating admission (default: 90 days, 3 fixed 30-day months). This duration - does not expire used requests when deletionRequestDisrespector is enabled. + does not expire used requests when deletionRequestDisrespector is + enabled. Production periods should be at least one day; sub-day + values are for tests. ''; }; }; diff --git a/src/config.rs b/src/config.rs index ccb22f0..270ca2a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -453,6 +453,9 @@ pub struct Config { )] pub holding_cleanup_interval_secs: u64, + // Retention is expressed in seconds for configuration consistency and short + // tests. Production deployments are expected to use periods of at least one + // day, comfortably exceeding worst-case deletion/archive processing time. /// How long an unused deletion or vanish request remains served after relay-observed first_seen_at. #[arg( long = "deletion-request-retention-unused-served-secs", diff --git a/src/nostr/lifecycle/deletion/archival.rs b/src/nostr/lifecycle/deletion/archival.rs index f97a6ac..6952791 100644 --- a/src/nostr/lifecycle/deletion/archival.rs +++ b/src/nostr/lifecycle/deletion/archival.rs @@ -447,6 +447,14 @@ impl DeletionPolicy { return outcome; } + // NostrDatabase::delete does not return a per-event removed count. Two + // concurrently processed requests can therefore both have observed one + // target and both receive use credit after successful delete calls. This + // narrow over-credit race is accepted: avoiding it would require broad + // target-level serialization, it cannot affect deletion correctness, + // and any extra credited request still follows bounded used retention. + // Deterministic single-winner attribution remains strict for the more + // important case of one later admission matching many pending requests. let deleted_count = deletable.len(); let delete_filter = Filter::new().ids(deletable); if let Err(e) = self.ctx.database.delete(delete_filter).await {