feat: keep the nodes a restart would forget #6

Merged
day01 merged %!d() commits from %!s() into %!s() 2026-08-07 07:30:09 +00:00
Owner

Runs now survive a restart. The nodes doing them did not: every identity, credential and unspent enrolment token was in memory. A restart locked out nodes that had done nothing wrong — they held a credential the control plane no longer recognised, and the only way back in was a token an operator had to issue by hand.

Why this stores state and not events

A node is not a history the way a run is. It is a current fact: who it is, what it may do, when it was last heard from. Nothing asks what a node used to be; what it is now decides whether it may be given work. So the row is replaced rather than appended to, and the store says so in its own words.

Every change is still written before it is believed: the node is changed on a copy, the copy is stored, and only then does it become what the registry hands out. A change the store refused never happened. That makes the node registry async, which reaches through the session and the sweep.

Enrolment tokens

Unspent tokens are stored too. A token an operator handed out and a restart quietly invalidated would be indistinguishable, to whoever holds it, from one that never worked. A spent token stays spent — were it to come back, a leaked token would enrol a second node after every restart.

Verification

Four more restart tests in tests/store.rs, against a real Postgres:

  • a node that enrolled is still known, and the credential it was told to use still works
  • a spent token cannot be spent again
  • a token nobody spent still enrols
  • what a node declared it can do outlives the restart, so it is employable without having to speak first

42 tests, fmt, clippy -D warnings, check-architecture.sh and check-rust-loc.sh pass. The binary was started against a real database and creates all three tables.

Worth a second opinion

Credentials and enrolment secrets are stored as they are, because the domain compares them directly. Storing a hash instead would be the better shape, but it changes the credential model rather than the storage, so it is not in this slice.

Still missing

Log lines are accepted and not stored. Only push events are modelled. The runner still does not speak this protocol — that is the next thing standing between this and a deployment.

Runs now survive a restart. The nodes doing them did not: every identity, credential and unspent enrolment token was in memory. A restart locked out nodes that had done nothing wrong — they held a credential the control plane no longer recognised, and the only way back in was a token an operator had to issue by hand. ## Why this stores state and not events A node is not a history the way a run is. It is a current fact: who it is, what it may do, when it was last heard from. Nothing asks what a node used to be; what it is now decides whether it may be given work. So the row is replaced rather than appended to, and the store says so in its own words. Every change is still written before it is believed: the node is changed on a copy, the copy is stored, and only then does it become what the registry hands out. A change the store refused never happened. That makes the node registry async, which reaches through the session and the sweep. ## Enrolment tokens Unspent tokens are stored too. A token an operator handed out and a restart quietly invalidated would be indistinguishable, to whoever holds it, from one that never worked. A spent token stays spent — were it to come back, a leaked token would enrol a second node after every restart. ## Verification Four more restart tests in `tests/store.rs`, against a real Postgres: - a node that enrolled is still known, and the credential it was told to use still works - a spent token cannot be spent again - a token nobody spent still enrols - what a node declared it can do outlives the restart, so it is employable without having to speak first 42 tests, `fmt`, `clippy -D warnings`, `check-architecture.sh` and `check-rust-loc.sh` pass. The binary was started against a real database and creates all three tables. ## Worth a second opinion Credentials and enrolment secrets are stored as they are, because the domain compares them directly. Storing a hash instead would be the better shape, but it changes the credential model rather than the storage, so it is not in this slice. ## Still missing Log lines are accepted and not stored. Only push events are modelled. The runner still does not speak this protocol — that is the next thing standing between this and a deployment.
day01 added 1 commit 2026-08-07 07:26:09 +00:00
		Runs survived a restart; the nodes doing them did not. Every identity, every
credential and every unspent enrolment token lived in memory, so a restart
locked out nodes that had done nothing wrong: they held a credential the
control plane no longer recognised, and the only way back in was a token an
operator had to issue by hand.

A node is not a history the way a run is — it is a current fact: who it is,
what it may do, and when it was last heard from. So this stores state and
overwrites it rather than appending events, and the crate says why.

Every change is written down before the registry hands the result back. A node
is changed on a copy, the copy is stored, and only then does it become what the
registry believes; a change the store refused never happened. That makes the
node registry async too, which reaches through the session and the sweep.

Unspent enrolment tokens are stored as well. A token an operator handed out and
a restart quietly invalidated would be indistinguishable, to whoever holds it,
from a token that never worked. A spent one stays spent: were it to come back,
a leaked token would enrol a second node after every restart.
	
day01 merged commit c9e72ce56f into %!s() 2026-08-07 07:30:09 +00:00
day01 deleted branch feat/keep-the-nodes-a-restart-would-forget 2026-08-07 07:30:09 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: syncode/control#6