Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions crates/registry-platform-audit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,18 @@ async fn write_audit_entries() -> Result<(), Box<dyn std::error::Error>> {
and keeps its files owner-only.
- A failed write stops the writer until restart, so every later audited request
fails closed.
- A stalled `stdout` destination is not a failed write. A destination that
holds one line for 5 seconds is stalled: the request waiting on that line is
refused, every later audited request is refused at once, and `ready()`
reports false. When the destination takes the line the writer accepts
entries again, with no restart. The start and the end of a stall are each
logged once, with no entry content. The deadline is fixed and has no
configuration key.
- A `stdout` destination writes on a thread of its own, at most 4096 lines
behind the one being written; a line that finds the queue full is refused.
An append waits for its line on the Tokio timer, so the runtime that appends
needs its time driver; without one the append is refused before its line is
queued.
- An active file whose last line was torn by an interrupted write is recovered
at open: the bytes after the last complete line are copied to the owner-only
side file `<path>.torn`, the active file is truncated to its last complete
Expand Down Expand Up @@ -102,6 +114,94 @@ async fn write_audit_entries() -> Result<(), Box<dyn std::error::Error>> {
- Redaction helpers intentionally avoid preserving email local parts, phone
digits, or sensitive query values.

### Stream write deadline: review record

A `stdout` destination whose reader stops reading used to hold every audited
request for as long as the reader stayed away. One thread now owns the
stream, and a caller waits one fixed deadline for its line. The four elements
of the change:

- **Invariant.** A caller is told an entry was accepted only after its line
was written and flushed, and no caller waits on a stream destination longer
than the write deadline (5 seconds, `STREAM_WRITE_DEADLINE`). A stall
recovers by itself; a write error stops the writer until restart, as before.
The deadline sits below the 10-second request timeout the Base Registry
Engine defaults to and the Evidence reference projects set, so a stalled
destination is answered as an unavailable audit destination, not as a
timed-out request. The deadline bounds the wait on the audit destination,
not anything else the process writes to the same stream.
- **Enforcement point.** `LineStream` in `src/writer.rs`. `append` refuses
before queueing when the stream has failed or the write in progress is
older than the deadline, hands the line over with `try_send` so a full
queue refuses, and waits for the outcome under `tokio::time::timeout`. The
`audit-stream` thread is the only code that touches the sink; it skips a
line whose caller already stopped waiting, so a refused caller's queued
line is never written. `ready()` is false while the write in progress is
older than the deadline. Dropping the writer closes the queue and waits one
deadline for the thread, then leaves it and reports that once.
- **Executed proof.** In `src/writer.rs`, against a sink that holds its write:
`a_stalled_stream_refuses_the_append_at_the_deadline_and_later_ones_at_once`,
`readiness_follows_a_stall_and_returns_once_the_stream_drains`,
`a_write_error_still_stops_the_stream_after_its_sink_recovers`,
`a_stalled_stream_leaves_the_runtime_to_other_tasks`,
`dropping_the_writer_waits_at_most_one_deadline_for_a_blocked_stream`,
`dropping_the_writer_writes_the_entries_still_queued`,
`a_full_stream_queue_refuses_a_line_instead_of_growing`, and
`a_runtime_without_a_timer_refuses_the_append_before_queueing_its_line`.
Through a product,
`a_stalled_destination_is_not_ready_until_it_drains` in
`registry-scheduling` holds readiness to the writer over the real deadline.
- **Remaining uncertainty.** The line the destination is holding when its
caller's deadline passes cannot be withdrawn: the caller is refused and the
line is still written when the destination takes it, which is logged. That
is one line for each stall, and it records more than happened, never less.
A `request` entry written that way has no `response` entry: its request
was refused and ran no protected I/O. A `response` entry written that way
is followed by the request's unfinished response. A destination slow
enough that lines wait in the queue for most of a deadline can do the same
with no stall reported. The start of a stall is logged by the first append
or readiness check that sees it, not by a timer, so a writer nothing is
asking logs it late: at the latest when the destination takes the line,
just before the end. The two reports are made outside the stream's lock,
so a destination that takes its line at the instant a caller first sees
the stall can log the end ahead of the start. A full queue refuses lines
without turning `ready()` false and without a stall report. A destination
that never takes its line keeps one thread until the process exits, and
unfinished response entries queued behind it when the writer is dropped
are not written, which is reported once. An operator who
sets a request timeout under 5 seconds still sees a timed-out request
first. A refused entry does not undo work a product already committed: a
product that writes its outcome entry after its transaction commits reports
that operation as unresolved, as it does for a failed write.

Two more limits hold in a product. Both are read from each binary's
logging setup and neither was exercised:

- Every report this writer makes, the stall and loss reports included, is
logged under the target `registry_platform_audit::writer`, and an
operator sees it only where the product's log filter admits that target.
Evidence (`EVIDENCE_LOG`) and Registry Scheduling (`RUST_LOG`) admit
every target at `info` by default and show the reports. Registry Render
defaults to `registry_render=info` and shows them only when `RUST_LOG`
admits the target. The Base Registry Engine, Registry Casework, Registry
Messaging, `breg-mcp`, and `breg-review` admit only their own targets and
have no setting that admits another, so their operators see the refusals
and the failed readiness, never these reports.
- The Base Registry Engine, Registry Casework, `breg-mcp`, and
`breg-review` write their operational log to standard output, the stream
a `stdout` audit destination writes. While that stream holds an audit
line, a log record written by any thread waits for it, with no deadline.
The Base Registry Engine logs one `info` record for every request and
every probe before the response leaves, at its default level, and
`breg-mcp` and `breg-review` log an `error` record for each refused
audit entry before they answer, so in those three a request the writer
refused can hang instead of being answered. Registry Casework logs no
record on that path and blocks wherever it does log, such as a
background pass that fails. Evidence, Registry Scheduling, Registry
Messaging, and Registry Render log to standard error. The executed proof
above holds its sink through `from_line_sink`; no test blocks a real
standard output.

## Testing

```sh
Expand Down
Loading
Loading