Skip to content

A restored clone replaces the snapshot's chronyd around its clock step - #1353

Merged
ejc3 merged 1 commit into
mainfrom
fix/clone-clock-slew
Oct 27, 2026
Merged

ejc3 merged 1 commit into
mainfrom
fix/clone-clock-slew

Conversation

@ejc3

@ejc3 ejc3 commented Oct 27, 2026

Copy link
Copy Markdown
Owner

A restored clone kills the chronyd it inherited from the snapshot before the restore steps its clock, and starts a fresh one after the step. This fixes the TRY 1 failures of test_clone_clock_matches_the_host_to_a_tenth_of_a_second in main CI run 40411315700 (Container-arm64 and Container-x64).

Mechanism

  • The restore sets the clone's clock to the host's with clock_settime, and chronyd is not told. chronyd only detects an unannounced forward jump larger than 10 s (JUMP_DETECT_THRESHOLD, chrony sched.c). A clone restored a few seconds after its snapshot stays under that, so the daemon keeps whatever correction it was making when the snapshot was taken.
  • The source VM's clock stops while a snapshot pauses it, and the source is resumed, not stepped. Its chronyd then measures the lag (about 0.9 s) and slews it away. When the next snapshot lands during that slew, the clone inherits the pending correction and applies it to a clock the restore already set correctly. If the restore's chronyc makestep runs, the correction lands as a step (x86_64: +2186 ms from the first sample, removed about 2 s later by a fresh NTP sample). Otherwise it lands as a slew at chronyd's maximum rate (arm64: +10 ms per ~120 ms sample, up to +146 ms, then back down).
  • Both failing tries had a baseline that cold-booted, so it was paused for the pre-start snapshot about 2 s before the fixture's snapshot. Both passing retries restored their baseline from the cached pre-start snapshot instead.

Local reproduction, not committed: cold-boot the baseline with --no-snapshot, take a throwaway snapshot so it falls behind, take the real snapshot 2.5 s later, and clone 3 s after that. The baseline's chronyc tracking read System time: 0.878573060 seconds slow of NTP time. Without the fix, the clone's chronyd logged System clock was stepped by 0.426088 seconds right after the restore's own step. The worst samples were 449, 422 and 407 ms in 3 of 4 runs. With the fix, the worst sample was 0.0 ms.

Change

  • fc-agent/src/agent.rs:
    • retire_chronyd_for_restore SIGKILLs every chronyd, waits on each pidfd, and zeroes the kernel frequency correction (adjtimex with ADJ_FREQUENCY | ADJ_TICK) that a killed slew leaves in place. It uses SIGKILL because on SIGTERM chronyd writes its frequency estimate to the drift file, and the next daemon would load it.
    • restart_chronyd_after_restore starts a fresh daemon on /etc/chrony.conf.
    • A mutex serializes both with the boot-time start_chronyd, which a pre-start snapshot can capture mid-way. The restore holds the mutex from the kill until after the step. If the boot setup has not written the config yet, the restore leaves starting the daemon to it.
  • fc-agent/src/restore.rs: the kill starts with the restore, overlapping the network phases, and is awaited just before the step. The fresh daemon starts after the step as a side job, off the ACK path. The chronyc makestep + burst side job is removed.
  • fc-agent/src/bootplan.rs: the clock-sync log line reports the size of the step (stepped +5.783919 s).
  • fc-agent/src/snapshot_network.rs: chrony_burst is now private. Only the source VM uses it.

Tests

  • test_clone_runs_a_chronyd_started_after_the_restore (new, integration): the clone's chronyd PID must differ from the source's. Without the fix it failed on both tries ("323" vs "323"). With the fix it passed ("430" vs "540").
  • the_snapshots_chronyd_is_gone_before_the_clock_step_and_a_fresh_one_starts_after (new, unit): source order in handle_clone_restore. Fails without the fix.
  • make test-root FILTER="-E 'binary(test_clone_restore_fixes) | test(test_sanity_chrony_ntp_configured) | binary(test_serial_console)'": 8 passed.
  • make test-unit FILTER="-p fc-agent": 286 passed. make lint: clean.

Contract: a restored clone's clock stays where the restore set it; no chronyd state from before the step survives the restore. Downstream impact: every clone restore (fc-agent restore path). The restore now waits for the old chronyd to exit before stepping (normally done during the ~45 ms network phase; bounded at 2 s).

Follow-up (not in this PR)

The source VM still falls behind by the length of each snapshot pause until its chronyd slews the lag away (about 0.9 s at chronyd's maximum slew rate takes over 10 s). This PR does not cause or change that. It only stops a clone from inheriting the correction.

The restore steps the clone's clock to the host's with clock_settime,
behind chronyd's back. chronyd detects an unannounced forward jump only
beyond 10 s (JUMP_DETECT_THRESHOLD in chrony's sched.c), and a clone
restored seconds after its snapshot is under that, so the daemon keeps
the correction it was making in the snapshot. A source VM falls behind
by the length of each snapshot pause, so its chronyd is often slewing
that lag away when the next snapshot is taken. The clone then applied
the leftover correction to an already correct clock: as a step when the
restore's `chronyc makestep` ran, otherwise as a slew at chronyd's
maximum rate. Main CI run 40411315700 failed
test_clone_clock_matches_the_host_to_a_tenth_of_a_second both ways
(x86_64: +2186 ms from the first sample; arm64: +10 ms per sample up to
+146 ms), each time on a baseline that cold-booted and so paused for the
pre-start snapshot about 2 s before the fixture's snapshot.

- agent.rs: retire_chronyd_for_restore SIGKILLs every chronyd (SIGTERM
  would write the daemon's frequency estimate to the drift file for the
  next daemon to load), waits on each pidfd, and zeroes the kernel's
  frequency correction a killed slew leaves behind.
  restart_chronyd_after_restore starts a fresh daemon on /etc/chrony.conf.
  A mutex serializes both with the boot setup, which a pre-start snapshot
  can capture mid-way; the restore holds it from the kill until after the
  step, and leaves starting the daemon to the boot setup if it has not
  written the config yet.
- restore.rs: the kill starts with the restore and is awaited just before
  the step; the fresh daemon starts after it as a side job. The
  `chronyc makestep` + burst side job is gone.
- bootplan.rs: the clock-sync log line reports the size of the step.
- snapshot_network.rs: chrony_burst is private, used by the source only.

Tests:
- test_clone_runs_a_chronyd_started_after_the_restore: the clone's
  chronyd PID differs from the source's. Red without the fix (323 == 323
  on both tries), green with it (430 vs 540).
- the_snapshots_chronyd_is_gone_before_the_clock_step_and_a_fresh_one_starts_after:
  source order in handle_clone_restore. Red without the fix.
- A local reproduction (baseline cold-booted with --no-snapshot, one
  throwaway snapshot to make it fall behind, snapshot 2.5 s later, clone
  3 s after that) reported chronyd "0.88 s slow of NTP time" in the
  baseline. Without the fix, the clone's chronyd logged "System clock was
  stepped by 0.426088 seconds" after the restore's step, and the worst
  sample was 449, 422, 407 ms in 3 of 4 runs; with it, 0.0 ms.

make test-root on binary(test_clone_restore_fixes),
test_sanity_chrony_ntp_configured, binary(test_serial_console): 8 passed.
make test-unit FILTER="-p fc-agent": 286 passed. make lint: clean.
@coderabbitai

coderabbitai Bot commented Oct 27, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 34 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: d11c1edf-a340-4277-9e5a-e7e846627c9d
Commits

Reviewing files that changed from the base of the PR and between 3a87ff7 and 271df04.

Files selected for processing: 5
  • fc-agent/src/agent.rs
  • fc-agent/src/bootplan.rs
  • fc-agent/src/restore.rs
  • fc-agent/src/snapshot_network.rs
  • tests/test_clone_restore_fixes.rs
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-27T01:53:47.345015Z 271df04 Manual request
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@ejc3

ejc3 commented Oct 27, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Delightful!

Reviewed commit: 271df04c1a

ℹ️ 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".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant