Upgrading a fleet¶
A fastcache-compile-node fleet upgrades as a unit. A rolling upgrade — replacing
one node at a time while the others keep serving — is not supported in this release,
and this page is here so that is something you plan around rather than discover during
a rollout.
Read this before upgrading more than one machine.
Why¶
Every 0xFC exchange carries a wire version. A build accepts exactly one:
MinSupportedVersion equals CurrentVersion in CompileCacheWire.hpp, and has at
every point in this project's history. So the moment one end moves, every peer still on
the old version is refused.
That is a deliberate choice rather than an omission, and the reasoning is recorded at
the constants themselves. In short: a reply on this wire carries a status byte and a
length and no kind, so the "step over what you do not understand" property that the
request framing has does not exist on the way back. An old client meeting a new reply
does not skip it — depending on the change it either abandons a compile several minutes
in, as a transport failure naming nothing, or reads a record it does not understand as
the field it expected and continues. Refusing the older version outright is
UnsupportedVersion, which names the supported range and arrives before any source is
sent. A loud, immediate, named refusal is the better failure.
Version 9 is such a step: it adds the live-stats stream fastcache-cli live-stats
subscribes to (#1399), and
every node, every fastcached serving 0xFC, every fastcache-cc and every
fastcache-cli has to move to it together.
What makes it a fleet problem rather than a daemon problem is that a fleet has more than one process. A cache daemon is upgraded, restarted, and done; twenty nodes cannot be.
The supported procedure¶
- Stop the builds. Anything running
fastcache-ccagainst the fleet should be quiescent. Builds do not break — see What a mismatch looks like — but they compile locally, so a build started here is slow rather than wrong. - Stop every node, including any machine running only the scheduler or only a cache tier.
- Upgrade every node and every client to the same release. The launcher
(
fastcache-cc) speaks this wire too, so a client left behind is a client that silently stops using the cache. - Start the nodes, schedulers first if you run them separately.
- Confirm with the counter below before releasing the builds.
The order matters only in that nothing old should be running once anything new is.
What a mismatch looks like, and why you must go looking¶
A version mismatch never fails a build. fastcache-cc treats an unusable cache as a
miss and compiles locally, which is the contract that keeps a cache problem from
becoming a build problem. The cost is silent: every affected client loses the cache and
every dispatched compile stops being dispatched.
The signal is server-side, and it is spread over four counters because a fleet has four places a frame can be refused. Watch all of them: a mismatch shows up only on the surface the stragglers actually talk to, so seeing zero on one counter tells you nothing about the others.
| Series | Refused where |
|---|---|
fastcache_node_cache_requests_refused_unsupported_version_total |
a node's cache tier |
fastcache_worker_frames_refused_unsupported_version_total |
a node's compile worker |
fastcached_dispatch_frames_refused_unsupported_version_total |
the fleet scheduler's port |
fastcached_cache_frames_refused_unsupported_version_total |
the fastcached daemon's compile-cache port |
Any of them non-zero and rising means something is still speaking the other version. These are the only places a mismatch is visible, so an operator who has not been told to watch them will not see one — which is why they are named here rather than left to be found during a rollout.
All four are exported only when --metrics is set. If you do not scrape, the honest
statement is that you cannot tell whether the upgrade was complete, which is a reason
to do step 5 rather than a reason to skip it.
The asymmetry is worth stating plainly: the server can see this and the client cannot. Check the servers.
The consensus peer wire, version 4¶
#178 made every consensus connection prove each member's OWN identity key, where it
proved the cluster's shared key, so the peer wire moved from version 3 to 4 and the two
cannot talk: an older peer proves only the shared key, and accepting it would be the
fallback the handshake exists to refuse. Upgrade a cluster's consensus members together,
and before you do, give each member's --raft-peer list every other member's key:
- On each member, run
fastcache-compile-node --print-identitywith the flags it runs with, as the account it runs as. It prints the member'sraft-peertoken, key included. A member that already has a state directory keeps its id; a key is minted beside it. - Put every token into every member's
--raft-peerlist. - Stop all of them, upgrade, start all of them.
A member left out of step 2 is refused by the others as a key never given
(fastcache_raft_peer_connections_refused_unknown_key_total) until the cluster records its
key -- which the leader does for itself when it leads, and which --cluster-admit with
@<key> does for anybody else. A mixed cluster shows as
fastcache_raft_peer_connections_refused_no_handshake_total on the new nodes.
Signed leases and the certified roster¶
#178 signs every lease with the issuing scheduler's own identity key and has every
worker check it against a roster of the cluster's voters, where a lease was an HMAC
under the shared key. 0xFC moved to version 13 for it (NODE-ANNOUNCE carries a
voter's endorsement out and the certified roster back), the replicated cluster state
to version 7 (it records the roster's version), the replicated commands to version 4
(--cluster-forget now revokes the key of what it forgets, #1555), and the lease format
to 3. Nothing older reads any of them, so this is the whole-fleet step above, with three
changes to what each machine is started with:
- Every scheduler runs consensus, even alone. Add
--listen-raftand--raft-self(and--cluster-dir, for a service) to a scheduler that had none; it is a cluster of one. One without them is refused at startup, by name. - Every worker another machine can reach names the voters. Run
--print-identityon each voter, with the flags it runs with, and give every such worker one--voter-key=<public-key>per voter. Without one it is refused at startup; a worker only its own machine can reach needs none. With--cluster-dirit keeps the roster it adopts, and from then on that roster certifies its successor. - Confirm on the workers.
fastcache_node_roster_expires_in_secondsappears on every checking worker once it holds a roster, andfastcache_worker_jobs_refused_lease_no_roster_totalrising without stopping means a worker never reached a leader its--voter-keyvoters endorse.
The consensus state directory¶
#1449 (learners) changed what a consensus member writes to disk and says to its peers, so a fleet running consensus crosses it as one step as well. Three formats moved, each refused by name rather than misread:
| What | Now | Refused as |
|---|---|---|
The Raft store in --cluster-dir (raft-state, raft-log, raft-snapshot) |
format 2: a configuration carries voters and learners, and every log record carries the format it was written in | UnsupportedFormatVersion — never the damage code — naming the format it found and the one it reads |
| The consensus peer wire | version 3 (4 since #178, above) | refused at the handshake by its version, never read as this layout -- a fleet's consensus members upgrade together |
The replicated cluster state (a snapshot, and the --cluster-status reply) |
version 5: each member records its seat | UnsupportedVersion, naming both versions |
The store has no conversion, and a node started on an older one refuses to start, saying so. So does a node whose store is this build's but whose snapshot or retained log entries hold the cluster state or a command in an older encoding — it names the directory, which part it could not read and both versions, rather than running on the part it could (which, for a snapshot, is a cluster with no members and no forget tombstones). Either way the directory is intact; what an operator does is:
- Stop the node.
- Move
raft-state,raft-logandraft-snapshotout of its--cluster-dir, leavingnode-idandnode-keywhere they are — the identity lives in the same directory, and a wiped identity is a different node, which the cluster would have to admit while it went on counting the old one. - If its cluster is already running on this build, start it again with
--raft-joinadded and its--raft-peerlist unchanged. It waits to be admitted instead of bootstrapping a cluster of itself — which a node whose bootstrap set names only itself would otherwise do — and a cluster that still counts it catches it up from the leader; one that has forgotten it admits it again with--cluster-admit(or--cluster-admit-learner). It keeps its identity, so it needs no--enroll-from. - If every member was moved aside together, start them as they were. They come back
with empty logs under their bootstrap configuration (
--raft-peer), or waiting to be admitted if they were started with--raft-join.
A RUNNING member offered a snapshot by a leader on another state format does not stop: it
refuses the snapshot and stays behind, raising the unreadable-leader-snapshot condition
(fastcache-cli node-conditions, and the fleet page). It follows no change the cluster makes
until it can read what the leader sends, and catches up by itself once it runs the leader's
build — nothing needs moving aside. That is the one condition a mixed fleet shows on its own,
and the reason to finish an upgrade rather than leave it half done.
Whatever the cluster agreed at runtime has to be agreed again once a leader is
elected: members admitted with --cluster-admit or --cluster-admit-learner, and
cluster settings (--cluster-set). Members found by --discovery are re-admitted by
discovery itself, as learners — promote the ones that voted again with
--cluster-admit — and so is a machine the cluster had forgotten, because its tombstone
was part of the state that was moved aside; forget it again. Nothing a build writes
into --cache-dir is involved.
This is backwards compatibility not being owed yet, and it is stated rather than discovered: the version on each format is what turns an older store into a refusal that names itself instead of a store read as damage.
Downgrading¶
The same procedure, in the same order. Nothing in the on-disk cache format is tied to the wire version, so a node that is downgraded serves its existing cache; entries it cannot decode are treated as misses rather than as corruption.
When this changes¶
A compatibility window — a build accepting more than one wire version — is a real
possibility rather than a promise, and it would be stated per verb rather than implied
by a single constant, because the verbs differ in how much room they have.
Op::Compile has exact arity and is the binding constraint; the cache verbs are
cheaper to widen. Until such a window is documented here, assume none exists and treat
every upgrade as a flag day.