From 000a3efc8abc44388e61d87f71b8fe85eb091ff8 Mon Sep 17 00:00:00 2001 From: "@permadeath.com" Date: Tue, 15 Sep 2026 11:15:58 -0400 Subject: [PATCH] docs(pds): say which parameters are an operator's and which are ours The eight constants that stay fixed each say why in their own doc: two segment sizes are the granularity a configured retention or budget is reclaimed in, the journal's sync interval holds a ratio to the heap's, the heap buffer is sized against one repository, the evaluation window is paired with the sweep that flushes it, the open-window cap is a backstop whose position is not observable, the queue depth follows from how fast policy drains it, and the blob page size is the lexicon's number. `--help` gains the new sections and the range worth staying inside for every key that has one. Change-Id: I7dad4adf6d6522b0d5835b89bab1a1d587d8d116 --- crates/didbot-pds/src/blobs.rs | 4 +- crates/didbot-pds/src/evaluation_log.rs | 14 ++++++ crates/didbot-pds/src/heap.rs | 4 ++ crates/didbot-pds/src/row_log.rs | 8 ++++ crates/didbot-pds/src/wal/mod.rs | 11 +++++ crates/didbot-pds/src/writequeue.rs | 5 +++ crates/didbot-serve/src/bin/didbot-pds.rs | 52 +++++++++++++++++++++- plan/config.md | 53 +++++++++++++++++++---- 8 files changed, 139 insertions(+), 12 deletions(-) diff --git a/crates/didbot-pds/src/blobs.rs b/crates/didbot-pds/src/blobs.rs index 6aede6d0..f1057b7c 100644 --- a/crates/didbot-pds/src/blobs.rs +++ b/crates/didbot-pds/src/blobs.rs @@ -74,7 +74,9 @@ pub const DEFAULT_ACCOUNT_QUOTA_BYTES: u64 = 64 * 1024 * 1024; /// How many CIDs a listing returns when the caller does not say. /// /// `com.atproto.sync.listBlobs` declares this default and a ceiling of a -/// thousand; both are the lexicon's rather than this project's. +/// thousand; both are the lexicon's rather than this project's, which is +/// why neither is a deployment's to set: a caller that asked for no limit +/// is entitled to the page size the lexicon says it will get. pub const DEFAULT_LIST_LIMIT: usize = 500; /// The ceiling `com.atproto.sync.listBlobs` puts on a page. diff --git a/crates/didbot-pds/src/evaluation_log.rs b/crates/didbot-pds/src/evaluation_log.rs index 679f2ce0..ff1fae78 100644 --- a/crates/didbot-pds/src/evaluation_log.rs +++ b/crates/didbot-pds/src/evaluation_log.rs @@ -92,6 +92,13 @@ pub use crate::row_log::{DEFAULT_RETENTION, DEFAULT_SEGMENT_BYTES}; /// enough to cover a burst an agent retries into on its own schedule, short /// enough that a row closes and becomes readable well before an operator /// investigating "is this still happening" would give up waiting. +/// +/// This crate's value rather than a deployment's, because it is paired with +/// the sweep that flushes stale windows: `didbot-pds`'s own sweep runs on +/// the same period, so a window set apart from it would either flush +/// windows still filling or leave filled ones sitting unread. What an +/// operator chooses about this log is how long its rows are kept, which is +/// `[capacity] evaluation_log_retention_days`. pub const DEFAULT_WINDOW: Duration = Duration::minutes(5); /// Identifies one evaluation that denied a write — or, once repeats are @@ -389,6 +396,13 @@ impl OpenWindow { /// than scanning every window on every call to find ones merely idle — see /// [`EvaluationLog::flush_stale`] for the sweep a deployment can run /// instead, on its own schedule. +/// +/// This crate's value rather than a deployment's. It is a backstop on +/// memory an attacker drives -- one open window per distinct attacking +/// account -- and the number that matters about it is that the map stays +/// bounded, not where the bound sits. An operator raising it buys nothing +/// they can observe, and lowering it evicts windows that were still +/// collapsing repeats, which is the work this log exists to do. pub const DEFAULT_MAX_OPEN_WINDOWS: usize = 4096; /// An operator-enabled capture of a refused write's own diff — the one diff --git a/crates/didbot-pds/src/heap.rs b/crates/didbot-pds/src/heap.rs index 4c899b1f..b81a44ee 100644 --- a/crates/didbot-pds/src/heap.rs +++ b/crates/didbot-pds/src/heap.rs @@ -69,6 +69,10 @@ use crate::wal::{Durability, HEAP_FILE}; /// write off the disk is one repository, not the deployment. A repository of /// a hundred thousand records at the wire sizes this server measures fits /// inside it. +/// +/// That makes it this crate's value rather than a deployment's: it is sized +/// against the largest single repository a commit signs over, so a server +/// holding more accounts wants the same buffer, not a larger one. pub const DEFAULT_BUFFER_BYTES: usize = 64 * 1024 * 1024; /// How long an unsynced heap waits before an append syncs it. diff --git a/crates/didbot-pds/src/row_log.rs b/crates/didbot-pds/src/row_log.rs index f37d611f..3a7a0e1e 100644 --- a/crates/didbot-pds/src/row_log.rs +++ b/crates/didbot-pds/src/row_log.rs @@ -66,6 +66,14 @@ pub trait Row: serde::Serialize + serde::de::DeserializeOwned { /// small and bounded, so a small segment is what lets a tight retention /// actually reclaim space rather than wait behind one segment that took /// months to fill. +/// +/// This crate's value rather than a deployment's. What an operator is +/// choosing when they think about one of these logs is how far back it +/// reads, and that is `[capacity] evaluation_log_retention_days` and +/// `write_log_retention_days`; a segment is only the granularity those trim +/// at, since [`RowLog::trim`] drops a segment whole once every row in it has +/// aged out. A megabyte keeps that granularity fine enough that what is on +/// disk tracks the window an operator configured. pub const DEFAULT_SEGMENT_BYTES: u64 = 1024 * 1024; /// How long a sealed segment is kept before [`RowLog::trim`] removes it, if a diff --git a/crates/didbot-pds/src/wal/mod.rs b/crates/didbot-pds/src/wal/mod.rs index 8a81ea85..f01cf9b9 100644 --- a/crates/didbot-pds/src/wal/mod.rs +++ b/crates/didbot-pds/src/wal/mod.rs @@ -148,6 +148,12 @@ use crate::journal::Mark; /// records written inside one. Short enough that the window is hard to /// notice, long enough that a burst of a thousand records costs a handful /// of `fsync`s rather than a thousand. +/// +/// This crate's value rather than a deployment's because it is half of a +/// pair: [`crate::heap::SYNC_INTERVAL`] is deliberately half of it, so that +/// in a steady stream of writes the heap's timer has already fired by the +/// time this one does. Two values that have to hold a ratio to each other +/// are one decision, and it is this crate's to keep. pub const DEFAULT_SYNC_INTERVAL: Duration = Duration::from_millis(250); /// The log's name inside the data directory, and the prefix its segments @@ -165,6 +171,11 @@ pub const LOG_FILE: &str = "pds.wal"; /// the granularity at which space comes back, and a segment is read whole /// at a boot that resumes inside it, so it bounds what a mark in the middle /// of one costs to read past. +/// +/// This crate's value rather than a deployment's, for the reason +/// [`crate::row_log::DEFAULT_SEGMENT_BYTES`] gives: what bounds this log's +/// size is `[capacity] log_budget_bytes`, and a segment is the granularity +/// that budget is reclaimed in. pub const DEFAULT_SEGMENT_BYTES: u64 = 64 * 1024 * 1024; /// Largest entry the log will accept, serialized. diff --git a/crates/didbot-pds/src/writequeue.rs b/crates/didbot-pds/src/writequeue.rs index 49e0ffc3..9646fcf1 100644 --- a/crates/didbot-pds/src/writequeue.rs +++ b/crates/didbot-pds/src/writequeue.rs @@ -65,6 +65,11 @@ use crate::policy::{Outcome, PolicyGate, PolicyVersion}; /// An agent that floods its own queue faster than policy drains it is a /// single misbehaving agent, and [`QueueError::Full`] is the answer it gets /// — not every other agent slowing down to match. +/// +/// This crate's value rather than a deployment's for the same reason it is +/// loose: it bounds one repository's backlog, which follows from how fast +/// policy drains a queue, not from how many accounts a deployment holds. A +/// server sized for more accounts wants the same per-repository budget. pub const DEFAULT_CAPACITY: usize = 64; /// Why a submitted write never reached a verdict from diff --git a/crates/didbot-serve/src/bin/didbot-pds.rs b/crates/didbot-serve/src/bin/didbot-pds.rs index 51718331..db405413 100644 --- a/crates/didbot-serve/src/bin/didbot-pds.rs +++ b/crates/didbot-serve/src/bin/didbot-pds.rs @@ -303,11 +303,54 @@ usage: didbot-pds [options] --config a sectioned TOML file to read settings from -- see didbot-config and plan/config.md. This binary reads - [blobs], [capacity], [disclosure], [oauth] and [relay], - and ignores every other section. An unknown key anywhere + [server], [zone], [tls], [blobs], [capacity], [names], + [admission], [ownership], [intervals], [disclosure], + [operator], [oauth] and [relay]. An unknown key anywhere in the file refuses startup. A flag always wins over the file for the same setting. + [server] takes bind and port. bind is the only way to + narrow what the listener answers on: unset binds [::] and + answers on every address this host holds, both families, + while 127.0.0.1 reaches only callers on this host. port + is 1024 to 65535 -- below that wants privileges this + process should not start with. + + [zone] takes zones, the same list --zone builds, whose + first entry is the primary; owner, the same DID --owner + names; and pds_endpoint, the URL agents are told to reach + this server at, derived from the primary zone when unset. + Passing --zone replaces the file's whole list rather than + adding to it. + + [names] takes spec and fallback, in the spellings --names + and --names-if-down take, and hold_days: 1 to 365, how + long a released name stays taken (default 30). A day + covers whatever still has the name cached; a year suits a + deployment where a name is an identity somebody wrote + down, at the cost of running a small word list dry. + + [tls] takes acme_environment and contact, mirroring + --acme-environment and --acme-contact. + + [admission] takes max_depth, 1 to 8 (default 4), the same + bound --admission-depth sets. + + [ownership] takes grace_window_hours, 1 to 168 (default + 6): how long an operator this server cannot reach keeps + it writing. Short, and an operator whose own PDS is down + for an afternoon takes these accounts down too; long, and + an operator who has stood down leaves them writing under + a vouch nobody is behind. + + [intervals] takes the background loops' periods in + seconds: blob_collect_secs (300 to 86400, default 3600), + confinement_poll_secs (60 to 3600, default 300) and + health_secs (5 to 300, default 10). They are set apart + because they load different things -- the confinement + poll spends a DNS provider's rate limit, blob collection + walks this server's disk, and health only reads memory. + [oauth] takes scope_ceiling, an enumeration of scope atoms no grant may exceed (it can only narrow what this build already allows; an atom outside that is dropped and @@ -319,6 +362,11 @@ usage: didbot-pds [options] below what max_accounts could need is kept as written but warned about at startup. + [capacity] also takes log_budget_bytes, the same byte + budget --log-budget sets, past which a write is refused + with StorageFull rather than at whatever size the volume + turns out to be. + [capacity] also takes evaluation_log_retention_days and write_log_retention_days: how long a sealed segment of the policy evaluation log, and of the write log, survives diff --git a/plan/config.md b/plan/config.md index ebe4c3f8..ba294098 100644 --- a/plan/config.md +++ b/plan/config.md @@ -53,13 +53,14 @@ alone. Grouped by what an operator is thinking about, read from the real flag set and from `didbot-serve/src/routes.rs`'s constants rather than guessed: -- **`[server]`** — bind address, port. -- **`[zone]`** — the zone agents are minted under, the owner DID, the pds - endpoint agents are told to use. +- **`[server]`** — bind address, port. Wired. +- **`[zone]`** — the zones agents are minted under, the owner DID, the pds + endpoint agents are told to use. Wired; `zones` holds the `--zone` list, + whose first entry is the primary. - **`[dns]`** — provider selection and its zone, once `didbot-dns` has more than one backend ([dns-providers](dns-providers.md)). - **`[tls]`** — ACME environment, contact address, certificate source - (`didbot-tls`). + (`didbot-tls`). `acme_environment` and `contact` are wired. - **`[attestation]`** — backend selection and the node allowlist. - **`[blobs]`** — `max_blob_bytes`, `account_quota_bytes`, and a name the blob garbage-collection work is free to add a retention setting under @@ -77,7 +78,8 @@ and from `didbot-serve/src/routes.rs`'s constants rather than guessed: volume, attributed writes grow with real use. Wired, with no flag mirroring either yet. - **`[names]`** — the naming spec or template, the fallback namer, the hold - period. + period. Wired, against `--names`, `--names-if-down` and `--name-hold`. A + spec that will not build names the tier it was written in. - **`[limits]`** — rate limits and their windows, once `didbot-serve/src/rate_limit.rs` and `oauth/rate_limit.rs` converge on one shape. @@ -86,6 +88,16 @@ and from `didbot-serve/src/routes.rs`'s constants rather than guessed: - **`[operator]`** — `session_ttl_secs`, how long a signed-in operator stays signed in, against `--operator-session-ttl`. Wired, and the section's own doc carries the range worth staying inside. +- **`[admission]`** — how many parent links a provenance chain may cross, + against `--admission-depth`. Wired. +- **`[ownership]`** — `grace_window_hours`, how long an operator this server + cannot reach keeps it writing, against `didbot_pds::DEFAULT_GRACE_WINDOW`. + Wired, with no flag mirroring it. +- **`[intervals]`** — the background loops' periods: blob collection, the + confinement poll and the health tick. A key each because they load + different things — the confinement poll spends a DNS provider's rate + limit, blob collection walks this server's disk, the health tick reads + memory. Wired, with no flags mirroring them. - **`[policy]`** — which DID's records this server reads for [policy-store](policy-store.md), once that epic exists to read anything. @@ -137,12 +149,9 @@ either provider crate. Left as flags, on purpose, and listed here rather than converted in this pass: -- [ ] `--port` / bind address (a sibling change is already in `didbot-pds.rs`) -- [ ] `--zone`, `--owner` - [ ] `--secret` itself (only the file-permission check for `--secret-file` is wired; the value's own precedence is decided above but not yet enforced in code) -- [ ] `--names`, `--names-if-down`, `--name-hold` - [ ] `--avatar` (development-only; may never belong in a deployment's file at all — worth deciding when this list is next revisited rather than now) @@ -152,7 +161,9 @@ pass: same reason its siblings are - [ ] DNS provider selection, once [dns-providers](dns-providers.md) has a second backend -- [ ] TLS section, once `didbot-tls`'s configuration surface is stable +- [ ] `[tls] cert_source`, a path to a fixed key pair, once `didbot-tls` + reads one. It is the field `check_secret_permissions` was written + for: the check runs on the path this key names. - [ ] `[policy]`, once [policy-store](policy-store.md) exists to read ## Done @@ -199,3 +210,27 @@ pass: anything the flag left alone; `[disclosure]`'s narrowing composes with `--close-disclosure`'s in either order because both only ever close a route. + +- [x] **Every section the binary parses reaches something that runs.** + `[server]`, `[zone]`, `[tls]`, `[blobs]`, `[capacity]`, `[names]`, + `[admission]`, `[ownership]`, `[intervals]`, `[disclosure]`, + `[operator]`, `[oauth]` and `[relay]` are applied, each through + `apply_config` or a `resolve_*` of its own. `[dns]` and `[policy]` + name settings whose readers belong to + [dns-providers](dns-providers.md) and + [policy-store](policy-store.md). + +- [x] **A parameter an operator should be choosing has a key, and one they + should not says why in its own doc.** `[capacity] log_budget_bytes`, + `[admission] max_depth`, `[ownership] grace_window_hours` and the + four `[intervals]` keys are the first half. The second is written + beside the constants: `row_log` and `wal`'s segment sizes are the + granularity a configured retention or budget is reclaimed in, + `wal::DEFAULT_SYNC_INTERVAL` holds a fixed ratio to + `heap::SYNC_INTERVAL`, `heap::DEFAULT_BUFFER_BYTES` is sized against + one repository rather than a deployment, + `evaluation_log::DEFAULT_WINDOW` is paired with the sweep that + flushes it, `DEFAULT_MAX_OPEN_WINDOWS` is a memory backstop whose + position is not observable, `writequeue::DEFAULT_CAPACITY` follows + from how fast policy drains a queue, and `blobs::DEFAULT_LIST_LIMIT` + is the lexicon's number. -- 2.51.2