Repository navigation
feat(vpp-offload): a steered keep-vpp restart diffs a seeded mirror without waiting out the replay - #356
feat(vpp-offload): a steered keep-vpp restart diffs a seeded mirror without waiting out the replay#356lunarthegrey wants to merge 8 commits into
Conversation
A steered seeded adoption is about to get a diff door that does not wait for the route source's replay, and that diff lands on a VPP carrying traffic. Whether it may land early depends on the diff itself: how many routes VPP forwards on it would withdraw or move. begin_resync computes that plan only by queueing it, so the answer came after the diff was already committed to. preview_resync runs the very same walk, through the same classification, and queues nothing: no pending op, no phase change, and no discard of the source's queued deltas, since a walk that queues nothing supersedes none. It does refresh what every walk refreshes about the kernel and the mirror (nexthop map, FDB, classification sets), which the next walk rebuilds from scratch anyway, so a preview that is not followed by a resync leaves those truthful rather than stale. The plan gains `replaces`: the upserts of prefixes VPP already holds, as opposed to additions. Together with withdrawals it is what a diff takes away from or moves under traffic already on VPP. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ithout waiting out the replay Since #323 a clean stop leaves both route ledgers, and a steered restart seeds the mirror and adopts VPP's FIB from them within seconds. But the diff between the two still waited for the route source's replay to go quiet, and every replayed element counts as activity (#153): about 11 minutes for 1.35M routes from FRR. For all of it VPP's FIB took no route change, `fib-synced` read "resync deferred", and the post-verify re-assert steer, which also moves keep rules to the RSS form (#325), waited too. AwaitingSeededDiff now takes #324's seeded-mirror door: an unreconciled seed, the feed session up in its first session, the authority's current word yes, the floor. That door was limited to the unsteered stage because VPP carried no traffic there. Here it does, so what matters is what an early diff does that the diff after the replay would not: it withdraws routes VPP holds that the seed lacks and the replay would bring back, and replaces routes the replay would set otherwise again. Additions are the same either way (seed routes stay until the source's GC). Two snapshots of one clean stop differ by churn only, re-advertising an unchanged route is no VPP delta, and what the seed holds that the source dropped leaves VPP through the GC as on the eBPF tier. That premise is not something vpp-offload can establish: a stop can write one ledger and not the other, a start without vpp-offload consumes only the fast-path one, and the walk resolves routes through whatever neighbours the resolver has re-announced seconds into the process. So the door counts the diff first (preview_resync) and opens only if its withdrawals plus replaces are at most 1% of the adopted table, the drift bound the authority itself uses to tell churn from a missing table. Past that the door is shut for the restart, latched, named in the journal, a `seeded_door_refused` event and the deferral row, and the diff waits for quiet exactly as before. The legs are read again after the preview, so a session that flapped during the walk is never acted on. Every other case keeps the quiet gate unchanged. VPP's route counts are re-checked before the preview, as at the quiet release. A seed that no longer holds hands the dump path the door's release as AwaitingFallback's `seeded_door`, so the unsteer goes out at once rather than after the replay, preserving the rule that traffic is not left on a FIB the seed misdescribes; that door releases the unsteer and the dump and diff of an idle VPP, never a diff onto traffic. Every adopted resync release is now recorded as `resync_released`, naming its door, and the journal line names it too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d mirror The runbooks said a steered adoption's diff waits for the replay to go quiet, and the fast-path runbook called the steered case unchanged. An operator reading either after this change would expect an 11-minute `resync deferred` that now lasts seconds, and would not know why a restart whose two ledgers disagree still waits. The vpp-offload runbook gains the steered door: its legs, why landing the diff on traffic is safe, the 1% guard and what its refusal looks like, the order at the door, and what still waits for quiet. The event log gains `resync_released` and `seeded_door_refused`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 87cb6bcb20
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…dditions included The early door's guard counted only withdrawals and replaces, against both families' tables together. Review found two ways that let through the disagreement the guard exists for. Additions are not "the same either way". The quiet door's diff runs two quiet seconds after the replay and the route source's GC five, so a seeded route the source has dropped sits in VPP for seconds there; the early door leaves it for the whole replay, and a stale more-specific pulls traffic off the less specific route VPP was using. The authority counts routes, not which ones, so two ledgers from different stops can agree in size while the seed holds prefixes VPP never had. Additions now count. Routes the previous process kept out of VPP do not inflate them: shadowed, kernel-delivered and link-local-only routes are left out of the walk's upserts, an upsert whose next hops all resolve to nothing sends VPP nothing and is no addition, and additions past the family's high-water mark are withheld, so only the room the withdrawals leave is counted. And a combined bound lets a large family cover a small one: 400k IPv6 routes allow 4,000 changes, enough to rewrite a whole small IPv4 table. The plan now carries a Disturbance per family from the one shared walk, the seed records its per-family size, and the door refuses if either family exceeds 1% of its own table, naming the family in the refusal, its event and the status row. Review findings on PR #356. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every adopted release logged, and recorded as `resync_released`, the route count the tick began with. On the dump path the steered seeded door hands over, the replay streams through a dump of seconds, so that count can be well off the mirror the diff was actually computed against; the early door's own release sits behind a VPP round trip and a preview walk, and the unsteered diff can sit behind a dump of an empty FIB. Each release now reads the count immediately before its walk and reports that. Review finding on PR #356. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@codex review |
🛡️ Codex Security Review · Automatically triggeredSecurity review completed. No security issues were found in this pull request. Reviewed commit: Only the user who started this review can view the report in Codex. ℹ️ About Codex security reviews in GitHubThis is an experimental Codex feature. Security reviews are triggered when:
Once complete, Codex will leave suggestions, or a comment if no findings are found. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8b81a1163e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
The guard capped additions at the room a full family had left once its withdrawals freed their slots, on the reasoning that the rest would be withheld. But the drain frees more than the withdrawals: a replace whose next hops resolve to nothing is withdrawn before it is classified, and the slot it frees is taken by an addition. On a full table, replaces of that kind plus additions passed as the replaces alone, while the drain then removed some routes from the steered VPP and installed others. Additions now count whole. A refusal only costs the old wait, and the clamp never helped where the door matters: on the reference table the high-water mark sits far above the adopted size. What it costs is that a table already over its capacity, with routes the previous process withheld, may see the door refused; that table is degraded anyway. Review finding on PR #356. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The early door counted its diff with a preview walk and then committed a second one. The route source mutates the mirror while it streams, and each walk rebuilds the nexthop map from the source's neighbours, so what the second walk queued was whatever it found, judged or not: a route replaced at constant count, or a next hop lost between the two, turned an approved unchanged table into hundreds of thousands of withdrawals on a steered VPP. The legs re-read in between saw count, session and authority, none of which moves when that happens. Now there is one walk. begin_resync_judged holds the walk's ops, puts the finished plan to a judge, and applies the held ops to the pending map only on its yes, in the order found — nothing the walk classifies by reads the pending map, so that is the state applying them at once would leave. On a no, nothing is queued and the phase and the ledger are untouched. The judge re-reads the door's legs before the guard, so a session that moved during the walk is still not acted on. preview_resync had no other use and is gone, and with it the second full walk on the supervision thread. The walk still discards the source's queued route deltas immediately before it, refused or not, and nothing is lost by that: apply_changes has one caller, below the deferral block that every unreleased deferral returns before, and every way out of a deferral runs a full walk first (the quiet door's, the dump path's, or start_resync's once VPP is replaced), reading the mirror as it then is. VPP's route counts are re-read immediately before that walk, on either door, so the window a route added by another client can open is the quiet door's own; the multi-second preview that used to sit after the check is gone. Review findings on PR #356. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 02485f620e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
🛡️ Codex Security Review · Automatically triggeredSecurity review completed. No security issues were found in this pull request. Reviewed commit: Only the user who started this review can view the report in Codex. ℹ️ About Codex security reviews in GitHubThis is an experimental Codex feature. Security reviews are triggered when:
Once complete, Codex will leave suggestions, or a comment if no findings are found. |
…n addition The walk counted an upsert of a prefix VPP does not hold as an addition only if one of its next hops resolved at that moment. But the upsert keeps its next hops, and the drain resolves them only when it reaches the route. A neighbour learned after the walk read its neighbours queues a delta no walk supersedes, and apply_changes hands it to the engine before the drain: every route through it then installs on the steered VPP. Any number of them passed the guard as nothing. Every upsert of a prefix the ledger does not record VPP holding now counts as an addition, resolved or not. That includes the routes the previous process left unresolvable or withheld, since VPP's preserved ledger records only what VPP installed. On a healthy box those are a handful (the status row names every unresolvable route, and a withheld one exists only on a table over its capacity); where there are more, a refusal costs the old wait. Review finding on PR #356. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@codex review |
|
Codex Review: Didn't find any major issues. Another round soon, please! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
🛡️ Codex Security Review · Automatically triggeredSecurity review completed. No security issues were found in this pull request. Reviewed commit: Only the user who started this review can view the report in Codex. ℹ️ About Codex security reviews in GitHubThis is an experimental Codex feature. Security reviews are triggered when:
Once complete, Codex will leave suggestions, or a comment if no findings are found. |
Summary
A steered keep-vpp restart that has both route ledgers from the stopping daemon now verifies and re-asserts its steer within seconds of the route source's first route. Until now it waited for the source's whole replay to go quiet: about 11 minutes for 1.35M IPv4 routes from FRR, plus IPv6.
DeferredResync::AwaitingSeededDiffgets #324's seeded-mirror door. The four legs are the same as for the unsteered door:This diff lands on a VPP that is carrying traffic, so the door adds a guard. The resync walk's own plan is put to the guard before any of it is queued (a new
ConvergenceEngine::begin_resync_judged), and the door opens only if, in each address family, withdrawals, replacements and additions together are at most 1% of that family's routes VPP holds (STEER_MAX_DRIFT). Anything bigger means the two ledgers disagree. The early door is then shut for the rest of that restart, and the diff waits for quiet as before.Every other case keeps the quiet gate unchanged:
Why
The replay re-advertises every route, and #153 counts each element as activity, so the quiet gate never sees quiet until the replay ends. For all of that time:
fib-syncedread Degradedresync deferred.Action::Steerre-assert waited too. That re-assert is also what moves keep rules from the queue-0 form to the RSS form (fix(vpp-offload): spread keep traffic over RSS, place queue 0's IRQ when it cannot, watch the kernel path #325).Why landing this diff on a VPP that carries traffic is safe
Compare the early diff with the one that would run after the replay. They differ by what the replay has not re-advertised yet, and by how long what the early one lands stays wrong:
When both ledgers come from one clean stop, all of that is near zero:
programmer.rsno-change shortcut). The walk also drops the deltas the seed queued (feat(vpp-offload): an unsteered adoption diffs a seeded mirror without waiting out the replay #324).The guard: needed, and the plan is available before the diff lands
"One stop, two snapshots" is a premise vpp-offload cannot check:
So the guard measures the disagreement instead of assuming it is small.
begin_resync_judgedholds the walk's ops, puts the finished plan to a judge, and applies them to the pending map in the order found only on a yes. On a no, nothing is queued, and the phase and the ledger are untouched. The route source mutates the mirror while it streams, so the first version (a preview walk, then a second walk that committed) let a route replaced at constant count, or a next hop lost between the two, become whatever the second walk found (review finding). The walk discards the source's queued deltas immediately before it, refused or not. Nothing is lost: no delta applies during a deferral, and every way out of a deferral runs a full walk first.ResyncPlangains per-family withdrawals, replaces and additions.ResyncPlancarries aDisturbance { withdrawals, replaces, additions }for IPv4 and one for IPv6, from the one shared walk, and the seed records its per-family size. Each family is held to its own table, so 400k IPv6 routes cannot cover a diff that rewrites a small IPv4 table, or the reverse (review finding). The refusal, its event (family) and the status row name the family; IPv4 is checked first.STEER_MAX_DRIFTof the family's adopted table, rounded down, with no floor. That is 13,500 at 1.35M routes and 146 on the lab's 14.6k. It is the authority's own line between churn and a missing table. It is the same constant on purpose: churn that needs one widened needs the other widened too. On a table small enough that 1% rounds to a handful, a refusal costs only a replay of a second or two.seeded_door_refusedevent, and appended to thefib-synceddeferral line.Order at the door.
seed_still_holds), as the quiet release does. Nothing that takes time sits between this and the walk, so the window is the quiet door's own (review finding).Interactions checked
AwaitingFallback. If the seed no longer holds at the early door, the dump path used to inherit an already-released quiet gate and unsteered at once. An early release has no quiet to hand over. Without a fix, traffic would have stayed for the whole replay on a FIB known to differ from the seed. The handover now carries the door asAwaitingFallback::seeded_door: the unsteer goes out at once, and the dump reads an idle VPP. After the dump, the release is re-checked by what it rested on: the door's legs, not the churn budget, since the replay streams through the door by design. The diff that follows is the unsteered one. The inherited door never releases a diff onto traffic. If a revocation put traffic back over a kept dump, that diff still waits for the quiet release, as before (the_inherited_door_never_diffs_onto_traffic).PreservedLedgerRejected. That path's "the eBPF tier is loaded" claim (supervisor and runbook) now also covers the door: its authority counted complete the mirror the eBPF tier forwards on. TheVerifyPassed→Action::Steerre-assert is a reconcile of the same plan, sosteer_holddoes not hold it (expands_diversionis false), and that reconcile is where keep forms migrate (fix(vpp-offload): spread keep traffic over RSS, place queue 0's IRQ when it cannot, watch the kernel path #325). The tests show it landing while the replay streams.preserve(). A refused deferral leaves the seed untouched (a refused walk changes no ledger), so a stop during it still preserves the ledger, as before.resync_releasedevent (door:quiet/seeded,path,steered,routes, what the diff queued), and the journal line carriesdoortoo.routesis the mirror the walk read, counted just before it, not the count the tick began with: on the dump path the replay streams through a dump of seconds (review finding). The existing quiet-path journal texts are unchanged.Tests
tests/vpp_runtime.rs, driver + runtime + fake VPP:a_steered_seeded_restart_verifies_and_re_steers_while_the_replay_streams: a 200-element replay every tick. The box reachesSteeredduring the replay with steering never down (["steer"]), no dump, and no route op.a_steered_seeded_release_lets_changes_reach_vpp_during_the_replay(productionRouteFeed): nothing VPP holds is sent again, and a withdrawal plus an addition made after the release reach VPP while the replay streams.a_steered_seeded_diff_without_every_leg_waits_for_the_replay_to_go_quietcovers five cases: unseeded mirror, no authority, an authority that disputes the mirror, a reconnected session, and a reconciled seed. Each stays deferred with nothing previewed and nothing sent, then releases through the quiet door.a_steered_seeded_diff_within_the_allowance_lands_while_the_replay_streams(1 of 200 withdrawn plus 1 added) and..._beyond_the_allowance_waits_for_the_replay_to_go_quiet(3 withdrawn, 3 replaced, or 3 added). The second checks that the refusal is reported in status and the row text, that the preview ran once rather than every tick, and that the quiet door later lands the same diff.a_session_that_flaps_during_the_walk_shuts_the_door: the judge re-reads the legs when the walk is done; nothing is queued, and nothing is latched.routes_replaced_as_the_walk_begins_are_judged_on_that_walk: 3 of 200 routes replaced at constant count as the walk begins. The walk's own plan is refused, withpending_ops == 0.a_next_hop_lost_after_the_walk_does_not_change_what_it_committed: the committed unchanged table stays in VPP.an_unresolved_addition_counts_though_its_neighbour_arrives_later: 50 absent routes go through a next hop whose neighbour is learned after the walk. The door refuses, nothing is queued, and the quiet door later installs all 50 once the neighbour delta arrives.a_full_table_counts_the_additions_its_unresolvable_replaces_make_room_for: a full table of 200 with 2 unresolvable replaces and 2 additions is refused; the quiet door later shows the drain freeing and refilling those slots.a_next_hop_not_yet_relearned_keeps_the_early_door_shut: a missing neighbour counts as 6 replaces, so the door refuses. Once the neighbour is back, the quiet diff sends nothing.a_seed_that_no_longer_holds_at_the_early_door_unsteers_at_once: the carry-over, with["unsteer", "steer"], one dump and the stranger route withdrawn, all during the replay.the_inherited_door_never_diffs_onto_traffic.one_family_over_its_bound_shuts_the_door_whatever_the_other_holds(dual-stack): 3 IPv4 withdrawals against 100 IPv4 and 200 IPv6 routes, and the reverse. Both are within 1% of the combined table, and both are refused, naming the family.a_release_after_a_dump_records_the_mirror_the_walk_read: the mirror grows from 6 to 13 routes during the dump, andresync_releasedsays 13. This test installs the test binary's event log and reads it back.Elsewhere:
tests/vpp_engine.rs:a_judged_resync_commits_only_the_plan_its_judge_saw. A refusal leavespendingempty and the phase and ledger untouched; an acceptance queues exactly the plan judged. It also covers per-family counts and a new route through an unresolvable next hop, which still counts as an addition.the_seeded_diff_guard_counts_what_lands_on_trafficcovers additions (counted whole), each family against its own table both ways, and the invariantallowance ≤ adopted × STEER_MAX_DRIFT < allowance + 1at every size from 0 to 50,000 and at large sizes. Alsoa_refused_seeded_door_explains_the_wait_on_the_deferral_row.Negative control against
origin/main. I put the newvpp_runtimetests into anorigin/maintree, with only the lines naming the new status field removed, and ran them once:restart_verifies_and_re_steers,release_lets_changes_reach_vpp,within_the_allowance_lands,seed_that_no_longer_holds…unsteers_at_once, andinherited_doorall stayAdoptedResyncingbecause the old code waits for the replay.beyond_the_allowanceand the session-flap test fail because the old code never counts the diff before it lands (measured in the first round, when the flap test was named…during_the_preview…).without_every_leg_waitsandnext_hop_not_yet_relearnedpin behaviour that must not change, and the old code always waits. The engine test and the two unit tests do not compile on the old code, because there is no preview or guard to test.Mutation checks. Each one removes one leg of the new code, and the named test(s) fail:
mirror_seededwithout_every_leg(+ #324's unseeded test, the door unit test)without_every_leg, door unit testwithout_every_leg,flaps_during_the_preview, #324's reconnect testbeyond_the_allowance,next_hop_not_yet_relearned, guard unit testbeyond_the_allowance(re-path case), guard unit testbeyond_the_allowance,next_hop_not_yet_relearned, engine testflaps_during_the_walkbeyond_the_allowance("previewed once")routes_replaced…,full_table…,beyond_the_allowance,one_family…, the engine test and otherswithin_the_allowanceseed_that_no_longer_holds…inherited_door_never_diffs_onto_trafficbeyond_the_allowance(added case), guard unit testone_family_over_its_bound…a_release_after_a_dump_records…Review round (Codex, 3 findings). I ran the new and changed tests against the previous head, 87cb6bc, with only the new
familyandadditionsfield references stubbed out. Three fail there, each for its finding:beyond_the_allowance: thess-beyond-addedcase lands early.one_family_over_its_bound…: bothss-v4-in-v6andss-v6-in-v4land early when each is run first.a_release_after_a_dump…: it records[Some(6)].Some rows of the table are from the review rounds; the walk-related rows were re-run against the one-walk code.
Review round 3 (Codex, 3 findings). I ran the new tests against 8b81a11. Each fails there, for its finding:
routes_replaced_as_the_walk_begins…lands early: the preview approved, and the commit walk found the replacement.a_next_hop_lost_after_the_walk…: 200 withdrawn from the steered VPP.a_full_table_counts_the_additions…lands early: the clamp hid the additions.The fingerprint finding needs no test of its own: there is no longer a step between the count re-read and the walk.
Review round 4 (Codex, 1 finding).
an_unresolved_addition_counts_though_its_neighbour_arrives_laterfails on 02485f6: the door opens early and the box steers while the replay streams. The engine test's inverted unresolvable-route assertion also fails there.Verified
In
pf-bpf-test:bookworm(Linux arm64, real BPF ELF,--cpus=4,PACKETFRAME_BPF_REQUIRED=1), ongit archive HEAD, rerun at 6acfccd after the review fixes:cargo fmt --all --check: clean.cargo clippy --workspace --all-targets --all-features -- -D warnings: clean.cargo test --workspace --no-runplusprivileged-tests.sh bundle: built.cargo test --workspace: all pass, including the 18 new tests.PACKETFRAME_BENCH_QUICK=1 privileged-tests.sh run native: all pass excepttc_parse_error_bumps_both_shared_and_tc_counter. That one is the known linuxkit 7.0 failure (tc TEST_RUN returns EINVAL for an IPv4 frame cut inside its IP header). It fails onmaintoo, and CI's 5.15 and 6.6 pass it.Also checked: host (macOS)
cargo fmt --check, hostcargo clippy --workspace --all-targets --all-features -D warnings, the same clippy cross-checked forx86_64-unknown-linux-gnu, and hostcargo test -p packetframe-vpp-offload.Docs
CHANGELOG.md(Unreleased).docs/runbooks/vpp-offload.md: a new "The steered diff and a seeded mirror" section. Updates to the release-gate door count, keep-vpp step 3, the fast-path ledger paragraph, "What you see", thePreservedLedgerRejectedclaim, and the "planned restart looks Degraded" section.docs/runbooks/packetframe-fib.md: the steered bullet.docs/runbooks/event-log.md: the two new kinds and the restart sequence.Hardware validation (lab EFG)
This needs the lab as it is: the 5.15 UniFi kernel, fast-path
forwarding-mode packetframe-fibwithroute-source bgpfed by the lab FRR (~14.6k routes),route-ledgeron (the default), and vpp-offload withrequire-table-complete onplus itsintegrity-authority frr …line, steered on one port.The lab's replay is a burst of a few seconds, so the drill keeps the stream busy across the restart. It loops
clear bgp ipv4 unicast <pf-peer> soft outon the lab FRR. Each loop re-sends the whole table to PacketFrame as identical UPDATEs: the same shape as the production replay, where every element counts as activity and nothing in the mirror changes. No prefixes are touched, and the guard sees nothing to count. It also keeps the BGP listener from ever being quiet for 5 s, so the seed stays unreconciled until the churn stops.0. Fetch both builds (Mac)
1. Unpack, and the two drill scripts (EFG, as root)
Use fresh directories, so each holds exactly one
.deb. Never glob a reused bundle directory.2. Phase A: the old build waits for the churn
Expected on the old build:
adopted resync deferred until the route source is loaded and quiet; the preserved FIB keeps forwarding untouched meanwhile, and steering stays up throughout.ssd-watch-A.log:fib-synced DEGRADED — resync deferred: … (release floor N met); the diff runs once the source is live and has gone quiet …on every line for the whole 180 s.route source loaded and quiet; diffing it against the preserved ledger while VPP keeps forwarding — no dump and no unsteer, and only the differences go to VPP, thenverify passed, thensteering UP: allowlisted traffic is now diverted to VPP (the eBPF tier remains the failover).adoption_path(path: preserved-ledger) at the start;verify_passed(seeded: true) andsteering_uptimestamped after CHURN-STOP-A;route_ledger_reconciledonce the stream has been quiet for 5 s after CHURN-STOP-A. There is noresync_released, because that kind is new.vpp=pid and the samerules=count, apart from the lines where status cannot answer during the stop/start.3. Install the new build
The keep-vpp restart onto the new build adopts A's ledgers. With no churn it takes the seeded door as well.
Both builds carry the same
VERSIONand identical state and ledger formats (their deb versions differ only in the~hwtestsuffix), so this is a same-version keep-vpp restart.4. Phase B: the new build releases at the first attestation
Expected on the new build, all within seconds of START-B and well before CHURN-STOP-B:
adopted resync deferred until the route source is loaded and quiet; over a mirror the fast-path seeded from its route ledger it runs sooner, once the route source streams and the completeness authority attests the seed, if the diff disturbs no more of either family's table than one clean stop's two snapshots can differ by; …route mirror seeded from the route ledger: ….the route mirror was seeded from the fast-path route ledger, the route source is streaming in its first session, the completeness authority agrees with it, and the diff disturbs no more of either family's table than one clean stop's two snapshots can differ by: diffing it against the preserved ledger now …withdoor="seeded". Expectwithdrawals=0 replaces=0andunchanged≈ the table: the soft-out churn changes nothing.additionsshould be 0, or the number of unresolvable routesfib-syncednamed before START-B, since VPP's preserved ledger does not record those.verify passed, thensteering UP: ….route_ledger_seededandadoption_path(path: preserved-ledger), thenresync_released(door: seeded,steered: true,path: preserved-ledger) →verify_passed(seeded: true) →steering_up, all timestamped before CHURN-STOP-B. Thenroute_ledger_reconciledonly once the stream has been quiet for 5 s after CHURN-STOP-B: the seed stayed unreconciled through the churn, which is the door's first leg.ssd-watch-B.log:resync deferredfor seconds only, brieflyresyncing the FIB the previous process verified and preserved …, thenfib-synced healthywhile the churn still runs. Every line has the samevpp=pid as before START-B and the samerules=count.seeded_door_refused. If one appears, itsfamily,withdrawals,replaces,additionsandallowance(146 on this table) say how far the two ledgers disagreed. Please send them: a refusal on an ordinary lab restart would mean the bound is too tight.5. Optional: a real change reaches VPP during the replay
Only if the lab can originate a TEST-NET prefix through the same next hop as the feed (as D2b/D3 did, a kernel route on the upstream router plus
redistribute kernel). During Phase B's churn, aftersteering UP, originate198.51.100.0/24there. Within seconds, while the churn still runs:It should show the route via the feed's next hop. On the old build it would only appear after the churn stopped. Remove it and confirm it leaves VPP the same way.
6. Clean up, and what to send back
Open points
STEER_MAX_DRIFTwith a stated reason, and it now covers additions and each family separately, so it is stricter than before. If the lab or the primary ever logsseeded_door_refusedon an ordinary restart, it is too tight. The disagreement it would show is the VPP drain backlog at the moment of the stop, plus any routes the previous process left unresolvable or withheld, which now count as additions.🤖 Generated with Claude Code