Repository navigation
rustjunosmcp v0.24.0
Added
-
A crashed commit is now re-probed against the device at startup (#370).
commit_operationpersists the caller's attribution -- includingrequest_id-- immediately before
the commit RPC is sent, andformat_attributionputs that same id into the Junos commit comment as
request.id=<uuid>. A process that dies between sending the commit and recording its result therefore
leaves behind a record naming something the device can be asked about. On startup the server now asks:
for every operationChangesetCoordinator::loadleftIndeterminatethat carries an attribution, it
reads the device's commit log and settles the recordCommittedif the id is there.The selection is
Indeterminateplus an attribution, notCommitting.loadrewrites every
non-terminal operation toIndeterminatebefore any sweep could observe it, and attribution is written
nowhere butcommit_operation-- so its presence is what separates a crash during the commit from a
crash during staging, which reaches the same state with no attribution.Only a hit settles anything. Junos keeps a bounded commit history, so an entry can age out, and a
device can simply be unreachable at boot. Neither is evidence the commit did not happen, so both leave
the recordIndeterminateforstate resolve. The sweep never writesFailed.The commit-log hit settles the commit; the lock flag is cleared only when lock freedom was proven.
Finding the id in the log proves the commit landed, but not that the candidate lock was released — the
process may have died between the commit and the unlock. So after finding a hit, the sweep probes lock
freedom by taking and releasing the lock, matching the abandon path.config_lock_heldis cleared only
when that probe succeeds; if the lock cannot be taken, cannot be confirmed returned, or the probe times
out or errors, the record is still settledCommitted(the commit is proven) but the flag remains set
and thedetailsnote that the lock state could not be verified.A confirmed commit is not settled either. Finding the id proves the provisional commit entered the
log, not that its rollback was ever cancelled; settling itCommittedwould make the record terminal,
freeing the device for an apply whose commit would cancel a rollback still armed, and would report as
permanently live a change the device may yet revert. Junos marks these on the entry's header
(commit confirmed, rollback in Nmins), so the match groups the log into entries rather than searching
it flat, and a deadline already on the record is honoured even when the log is ambiguous.Known gap: a settle writes the record directly and emits no SSDF
result_receipt, so a crash after the
apply intent was spooled leaves that evidence chain unterminated even once the device has confirmed the
outcome. Tracked separately.The sweep is detached rather than awaited: a candidate on an unreachable device costs 20s, longer than
the readiness budget the test harness and package smoke allow, so blocking startup on it would turn a
recoverable record into a server killed and retried on every boot. Nothing is lost by serving first --
a non-terminal operation already blocks a new one on its device, so these records keep gating writes
until the sweep settles them. Probing is bounded-concurrent (4 at a time) and the candidate order is
rotated each run, so a prefix of unreachable devices in the coordinator's stable map order cannot
starve a reachable candidate behind them -- either within one sweep or across every restart. 20s per
device, 120s for the sweep. A provisional finding is reported but deliberately not written back: doing so
would mean writingIndeterminateand the old deadline over a record aconfirm_junos_change_setmay
have settled in between, un-confirming a commit the operator did confirm, and mecmcp has no
compare-and-update for operations to make that check atomic. No rollback deadline is invented either: reconstructing one from the device's clock would be
the same kind of unobserved claim the rest of this refuses to make. Because the sweep now runs alongside live traffic, the settle
path re-reads the record and skips it if anything settled it while the probe was in flight; that path
is safe from the remaining window by construction, sincecommittedis only reached for a record that
carried no deadline and so writes exactly what a concurrent confirm would have written. The candidate
rotation is seeded per process rather than from the clock, because a wall-clock modulo is periodic --
60 records restarted daily lands on86400 % 60 == 0and would reselect the same slow prefix forever.Verified on hardware (vSRX 24.4R1.9), not just offline: a real change-set commit was confirmed to stamp
the full untruncatedrequest.id=into the device's commit log; a record flipped toIndeterminatewas
settledCommittedfrom that log after a restart; an id the device had never seen was left untouched;
and an unreachable device timed out at 20s with the record untouched and the server still starting.
Fixed
-
get_junos_change_set_statusnow distinguishes a live commit from one the device reverted (#384).
apply_junos_change_setwithconfirm_timeout_minsissues a Junos confirmed commit, which the
device rolls back unlessconfirm_junos_change_setarrives before the deadline. Status reported
appliedeither way. The cause is whereAppliedis written:apply_change_setsets it as soon as
staging succeeds, before the commit is issued, and nothing rewrites it afterwards -- so the
change-set state is settled before the device has been asked to do anything.The verdict now comes from the linked operation record, reached through
ChangeSetRecord::operation_id:
awaiting_confirmationwhile a rollback deadline is still in the future,presumed_auto_reverted
once it passes with no confirmation recorded,committedonce the operation reachesCommitted, and
discardedwhen one was reconciled bystate resolve.A recorded deadline is the only evidence this server holds that a confirmed commit was issued, so it
decides whether a verdict is owed at all, and it outlives a manual resolve.resolve_persisted_operation
settles the state but leavesrollback_deadline_unixin place, andstate resolve ... AS COMMITTED
does not stop the device: reportingcommittedthere would hide a rollback still counting down.
AS DISCARDEDis an operator saying the change is not on the device, which a timer only reinforces.
AnAS COMMITTEDrecord whose window has since closed reportsindeterminaterather than guessing:
state resolveneither stops the device nor records when it ran, so that shape is either a confirm
made out of band and then reconciled -- the remedy for a confirming commit whose durable write failed
-- or a reconciliation made early on a timer that then fired.committedwould hide a revert and
presumed_auto_revertedwould contradict a human who looked at the device, so the tool reports the
uncertainty and sends the reader to the device.
An operation with no deadline that is notCommittedgets no verdict at all -- an ordinary failed
apply terminalizes asDiscarded, and that is not a confirmed-commit outcome.
The response also carriesoperation_id,operation_stateandrollback_deadline_unixso a reader
can see the basis. All additions are additive -- no existing field or argument name changed.presumed_auto_revertedis a presumption, not an observation. It is the absence of a recorded
confirmation, and it is wrong if a confirming commit was issued out of band on the device, if the
server and device clocks disagree, or ifconfirm_junos_change_setreached the device but its
durable write failed. Only the device settles it. Likewisecommittedcannot separate a confirmed
commit from a plain one, because confirming clears the deadline -- both mean the configuration is
live, which is the distinction that matters.The derivation keys on the deadline rather than on
LifecycleState::Committing, because a restart
rewrites every non-terminal operation toIndeterminatewhile leaving the deadline intact. Since the
Junos rollback belongs to the device rather than the session, that window is still open and still
confirmable; keying onCommittingdropped the deadline from the response at exactly the moment an
operator needed it.
Full changes: v0.23.0...v0.24.0
Backfilled GitHub release for an existing tag (2026-10-07).