Skip to content

Hold 0007's four states where two clock readings and a counter decide them - #262

Merged
iderex merged 1 commit into
mainfrom
the-four-states-a-request-ages-through-44
Aug 31, 2026
Merged

Hold 0007's four states where two clock readings and a counter decide them#262
iderex merged 1 commit into
mainfrom
the-four-states-a-request-ages-through-44

Conversation

@iderex

@iderex iderex commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

The issue this belongs to

#44. This does not close it, and the reason is at the end.

What changed

src/server/states.rs holds the part of 0007 two readings of one clock and a
counter settle. Before it, neither the 400 ms threshold nor any of the four state
names existed anywhere in this tree.

AgingRequest answers what a reading of the steady clock has newly made true. A
request is late at 400 ms and abandoned at the deadline src/server/transport.rs
already declares - read from that constant rather than written a second time -
and each is said once and never unsaid. A reading first taken past the deadline
reports the abandonment alone rather than a late that stopped being useful
seconds ago; that is a decision and the argument for it is on the function.

what_an_outcome_says answers whether a transport outcome is evidence the server
is absent. Three are, and none of them needs a threshold because none of them
waited. A refused certificate is not, which is the near miss 0007 names.

ConsecutiveAbandonments counts abandonments against one server with no success
between them, and answers unreachable on the second. It takes no attempt count,
so there is nothing to charge three of, and a success resets the run.

The 400 ms carries its derivation rather than only its value: it is one third of
the 1.2 seconds #62 publishes, so a client can commit to a cached answer with two
thirds of the budget left to read, decode and draw. That is a choice rather than a
measurement, and the constant says so.

What failure it prevents

The two 0007 exists against, both of which are a client drawing the wrong thing
because the core told it one pending state.

A person told their server is gone because one endpoint was slow. That is a
client treating abandoned and unreachable as the same thing, and the reason it
happens is that a core reporting one failure gives it nothing else to treat them
as. The counter here is what makes them different: one abandonment is a fact about
a request and two consecutive ones are a fact about the server.

A person shown a spinner and then an error for a request that was about to
succeed. That is a client treating late and abandoned as the same thing, and
WhatToReport keeps them apart at the point the core says either.

Beside those, a gap src/server/retry.rs states about itself and could not close.
0038 decides that a call spending all three attempts is one abandonment rather
than three, because 0007 declares a server unreachable after two consecutive ones;
that module records the decision, enforces nothing, and says in as many words that
a loop charging three would pass every check in it. Charging three now fails a
case, and the signature has no argument to charge it with.

None of the three has happened here. There is no request loop in this tree to have
got any of them wrong yet, which is the point of deciding before the caller
exists.

Evidence

Every command run at 0fa121b3bbb98aafd3116839b1775dc6b90dd48a, the head being
pushed.

cargo build --locked --all-targets
Finished `dev` profile [unoptimized + debuginfo] target(s) in 4.61s

cargo test --locked 2>&1 | grep -c '^test .* ok$'
606

cargo test --locked 2>&1 | grep -c '^test result: FAILED'
0

cargo test --locked --lib server::states 2>&1 | grep -E '^test result'
test result: ok. 13 passed; 0 failed; 0 ignored; 0 measured; 441 filtered out; finished in 0.00s

The legs of the gate this machine can run, each by its own script and each by its
own exit code:

for s in format invariants doc-paths lint test excluded-targets targets; do
  bash .github/$s/$s.sh check >/dev/null 2>&1; echo "$s exit=$?"
done
format exit=0
invariants exit=0
doc-paths exit=0
lint exit=0
test exit=0
excluded-targets exit=0
targets exit=0

cargo clippy --all-targets -- -D warnings -D clippy::all -D clippy::pedantic -D clippy::cargo 2>&1 | grep -cE '^(error|warning)'
0

Every case here moves an injected clock rather than waiting on one, which is what
0102 requires of the suite and why a five second threshold costs microseconds:
the whole module runs in 0.00s above.

What a guard here refuses, and the proof it bites

Four violations, each applied to the working tree, run, and reverted.

The threshold is reached at rather than passed. The one-character mistake:

-        if age >= A_REQUEST_IS_LATE_AFTER && self.said < WhatHasBeenSaid::ThatItIsLate {
+        if age > A_REQUEST_IS_LATE_AFTER && self.said < WhatHasBeenSaid::ThatItIsLate {

cargo test --locked --lib server::states 2>&1 | grep -E '^    server|^test result'
    server::states::tests::a_request_is_late_at_four_hundred_milliseconds_and_not_before
    server::states::tests::a_watched_request_reports_late_then_abandoned_and_then_nothing
    server::states::tests::late_is_reported_once_however_often_the_clock_is_read
test result: FAILED. 10 passed; 3 failed; 0 ignored; 0 measured; 441 filtered out

Each state is said once. The violation is the guard on what has already been
said, dropped from the abandonment arm:

-            if self.said < WhatHasBeenSaid::ThatItWasAbandoned {
-                self.said = WhatHasBeenSaid::ThatItWasAbandoned;
-                return WhatToReport::Abandoned;
-            }
-            return WhatToReport::Nothing;
+            self.said = WhatHasBeenSaid::ThatItWasAbandoned;
+            return WhatToReport::Abandoned;

cargo test --locked --lib server::states 2>&1 | grep -E '^    server|^test result'
    server::states::tests::a_request_first_read_past_the_deadline_does_not_report_a_late_that_is_over
    server::states::tests::a_watched_request_reports_late_then_abandoned_and_then_nothing
test result: FAILED. 11 passed; 2 failed; 0 ignored; 0 measured; 441 filtered out

A success ends the run. The violation is a counter that only ever rises, which
declares a server gone on two abandonments a week apart with a thousand successful
calls between them:

-    pub const fn answered(&mut self) {
-        self.counted = 0;
-    }
+    pub const fn answered(&mut self) {}

cargo test --locked --lib server::states 2>&1 | grep -E '^    server|^test result'
    server::states::tests::a_success_resets_the_run
test result: FAILED. 12 passed; 1 failed; 0 ignored; 0 measured; 441 filtered out

A refused certificate is not an absent server. This is 0007's own near miss,
and the violation is the arm moved by one line:

-        | TransportOutcome::AnswerStalledMidBody { .. }
-        | TransportOutcome::PeerNotTrusted { .. } => {
-            WhatAnOutcomeSaysAboutTheServer::NothingAboutTheServer
-        }
+        | TransportOutcome::AnswerStalledMidBody { .. } => {
+            WhatAnOutcomeSaysAboutTheServer::NothingAboutTheServer
+        }
+        TransportOutcome::PeerNotTrusted { .. } => WhatAnOutcomeSaysAboutTheServer::ItIsNotThere,

cargo test --locked --lib server::states 2>&1 | grep -E 'was read as evidence|^    server|^test result'
assertion `left == right` failed: PeerNotTrusted { reason: SelfSigned, fingerprint: "a-fingerprint" } was read as evidence that the server is absent
    server::states::tests::only_the_three_outcomes_that_never_reached_a_server_are_evidence_of_absence
test result: FAILED. 12 passed; 1 failed; 0 ignored; 0 measured; 441 filtered out

With all four restored:

cargo test --locked --lib server::states 2>&1 | grep -E '^test result'
test result: ok. 13 passed; 0 failed; 0 ignored; 0 measured; 441 filtered out; finished in 0.00s

What this does not cover

#44's own condition is untouched and this does not close it. It drives the
fake server at several delays and asserts the exact sequence of stages for each,
including the case where a cached answer exists and the case where none does.
Nothing here makes a request for a delay to answer, which is #27, and nothing here
produces the cached answer or its age, which is #43. The readings already on the
issue about both are not restated in this body.

The payload each report carries is not here. 0007 has the report at nought
carry whether a cached answer exists and its age, and the later reports carry the
same. That number is 0043's and the read that produces it is #43, so this holds
the states a report is built around and not the report.

Nothing charges the counter. The rule now has a subject and a near miss, and
no loop in this tree runs a call, so a caller that charged three abandonments per
call would still fail nothing - there is no caller. That is stated in the module
as well, so a later reader does not take the row for coverage of the loop.

No recovery. What happens after a server is declared unreachable is 0045 and
is already in src/server/recovery.rs. Nothing here probes, and nothing here
reports a server answering again.

The coverage leg was not judged on this machine. Its instrumentation reads
zero lines for every area here, on this branch and on an unmodified checkout of
the default branch alike, so the number is absent rather than low.
.github/workflows/coverage.yml runs on this pull request and is what judges it.
The pinned-surface register carries the new module with its reason.

Three other legs were not run here. headless needs a program this machine
does not carry (ip: command not found), thread-detector needs a nightly
compiler this tree deliberately does not pin, and mutation is weekly rather than
a pull-request leg. None was skipped by editing anything; they were not run.

Who has read it

Nobody but me has read this change. There is no second reader on this board
tonight, and the evidence above stands in place of one rather than the question
being left open.

… them

0007 decides that slow and absent are separate conditions, that the core reports
four states rather than one pending state, and that it reports them
progressively as a request ages rather than once at the end. None of it was in
the tree: the 400 ms threshold existed nowhere, and neither did any of the four
names. `src/server/states.rs` holds the part two readings of one clock and a
counter settle.

A request is late at 400 ms and abandoned at the deadline the transport already
declares, each said once and never unsaid, because a loop polling every frame
would report `late` sixty times a second and a client receiving it twice cannot
tell one slow request from two. A reading first taken past the deadline reports
the abandonment alone: the late report exists so a client can commit to a cached
answer with budget left to draw it, and five seconds later that budget is spent.

Three transport outcomes are evidence the server is absent and need no
threshold, because none of them waited. A refused certificate is not one of
them, which 0007 names as the near miss: it is a server that answered, and
reporting it as unreachable sends a person looking for a network problem that
does not exist.

The abandonment count is here rather than beside the retry policy, and that
closes a gap `src/server/retry.rs` states about itself: 0038 decides that a call
spending all three attempts is one abandonment rather than three, that module
records the decision and enforces nothing, and a loop charging three would pass
every check in it. `ConsecutiveAbandonments::abandoned` takes no attempt count,
so there is nothing to charge three of, and a success resets the run because the
count is of consecutive abandonments rather than of abandonments.

What that prevents is the two failures 0007 exists against: a client told its
server is gone because one endpoint was slow, and a person shown a spinner and
then an error for a request that was about to succeed.

#44 stays open. Its condition drives the fake server at several delays and
asserts the sequence for each, including the case where a cached answer exists;
the request is #27 and the cached answer is #43.

Signed-off-by: Nils Lehnen <30603423+iderex@users.noreply.github.com>
@iderex iderex added architecture Shape of the core: boundaries, interfaces, contracts performance The speed budget and what measures it labels Aug 31, 2026
@iderex iderex self-assigned this Aug 31, 2026
@iderex
iderex merged commit 31be70c into main Aug 31, 2026
24 checks passed
@iderex
iderex deleted the the-four-states-a-request-ages-through-44 branch August 31, 2026 10:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

architecture Shape of the core: boundaries, interfaces, contracts performance The speed budget and what measures it

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant