Skip to content

fix(tokio): reload TLS only when the watched files change - #5716

Merged
kixelated merged 4 commits into
mainfrom
claude/confident-lewin-4cf60f
Oct 16, 2026
Merged

kixelated merged 4 commits into
mainfrom
claude/confident-lewin-4cf60f

Conversation

@kixelated

@kixelated kixelated commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

moq_tokio::watch watches each certificate's parent directory (so atomic renames and the Kubernetes ..data symlink swap are seen) and reloaded on any create or modify in that directory. When the relay's log file sits in the same directory, every reload writes a log line, which triggers the next reload. Reproduced locally by running moq-relay with cert.pem, key.pem, and its stdout redirected to relay.log in one directory: about 29,000 reloads in 3 seconds. A moq-dev/smoke nightly run logged about 60k.

Fix

watch::callback now passes events through a Filter that fires only when:

  • an event names a watched file, or the target a watched symlink currently resolves to (an in-place write reports the target's name). The file name and canonicalized directory must both match, since backends spell the directory differently; the cheap name check runs first so unrelated churn skips the syscall. Or:
  • a watched path now resolves (fs::canonicalize) somewhere different from before. This is how the kubelet's ..data_tmp -> ..data rename gets caught, because no event in that rotation names tls.crt.

The resolved targets are refreshed on every event, so one rotation fires once. This is the same approach viper and Istio's file watcher use.

It also reloads on notify's Rescan flag. An inotify queue overflow arrives as Ok(Other + Rescan), not Err, so the old code dropped it even though its comment said an overflow would reload.

Server certs/keys (Files) and reloadable root CAs (Reloading) both go through callback, so both get the fix.

Tests

  • unrelated_files_do_not_trigger_reload: synthetic events on relay.log are rejected; writes and renames of cert.pem/key.pem and a Rescan are accepted.
  • write_to_symlink_target_triggers_reload: with cert.pem -> actual.pem, a write to actual.pem reloads.
  • same_name_in_another_watched_directory_does_not_trigger_reload: with cert.pem and key.pem in different directories, a cert.pem in the key's directory is rejected.
  • kubernetes_rotation_reloads_and_log_writes_do_not (Linux, real inotify): sets up a kubelet-style mount, writes a log, does the ..data swap, then writes the log again. Each step blocks until its own last event arrives (no wall-clock deadline; nextest's timeout catches hangs), so the result doesn't depend on timing. Checks that log writes never reload and that the rotation reloads exactly once.

Both tests fail when the filter goes back to accepting every event. The Kubernetes test also fails when the filter only checks file names.

After the fix, the local relay repro shows 1 reload (from an mv rotation partway through the run) instead of a storm. just check passes.

API / wire impact

None. Filter is private; watch::Files keeps its signature and only its docs changed. The relay docs already say "Reloaded on change", which is still accurate, so doc/bin/relay/ is unchanged.

(Written by Claude Opus 5.5)

🤖 Generated with Claude Code

kixelated and others added 2 commits October 16, 2026 08:28
The certificate watcher watches each file's parent directory and reloaded on
any create or modify there. A relay logging into its certificate directory
reloaded on every line, and each reload logged another line: about 29k
reloads in 3s locally, and ~60k in a smoke run.

Filter events to the watched file names, plus a change in where a watched
path resolves through its symlinks, so the Kubernetes `..data` swap (which
never names the file) still reloads. Also reload on notify's Rescan flag,
which is how an inotify queue overflow arrives; it was being ignored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated
kixelated marked this pull request as ready for review October 16, 2026 16:17
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 16, 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-16T17:32:10.592904Z 9fbd5d6 New commits
ℹ️ 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.

@coderabbitai

coderabbitai Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

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 49 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 4 included reviews currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: e98d960a-6448-43be-87bf-ad764324c761
📥 Commits

Reviewing files that changed from the base of the PR and between ef21a30 and 9fbd5d6.

📒 Files selected for processing: 2
  • rs/moq-tokio/src/tls.rs
  • rs/moq-tokio/src/watch.rs
✨ Finishing Touches
✨ Simplify code
  • Commit to this branch
  • Create a new PR
  • 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 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1f714415fe

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

Comment thread rs/moq-tokio/src/watch.rs Outdated
Comment on lines +127 to +131
let named = event
.paths
.iter()
.filter_map(|p| p.file_name())
.any(|name| self.paths.iter().any(|p| p.file_name() == Some(name)));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Match event names within their watched directories

When TLS material spans multiple directories, this compares each event basename against every configured basename without checking the parent. For example, with /a/cert.pem and /b/key.pem watched, writing an unrelated /b/cert.pem passes named and triggers a reload, potentially recreating the reload/log storm this change is intended to prevent. Match the normalized parent and filename together rather than matching filenames globally. (Written by GPT-5.6 Sol)

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, fixed in 396dad0. A name match now also requires the event's canonicalized directory to match the watched file's, with the name check first so unrelated churn still skips the syscall. same_name_in_another_watched_directory_does_not_trigger_reload covers /b/cert.pem with /a/cert.pem + /b/key.pem watched.

(Written by Claude Opus 5.5)

Comment thread rs/moq-tokio/src/watch.rs Outdated
Comment on lines +296 to +298
let event = rx
.recv_timeout(Duration::from_secs(10))
.expect("missing event")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Remove the wall-clock deadline from the watcher test

On a loaded Linux CI host, correct inotify behavior can still fail this test if event delivery or test-thread scheduling exceeds the hard-coded ten-second deadline. This makes the unit test depend on wall-clock timing; block on recv and rely on nextest's suite timeout to diagnose a genuine hang instead. (Written by GPT-5.6 Sol)

AGENTS.md reference: AGENTS.md:L19-L19

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, fixed in 396dad0: the step now blocks on recv(), and a lost event hangs until nextest's suite timeout.

(Written by Claude Opus 5.5)

… wall-clock deadline

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 396dad070f

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

Comment thread rs/moq-tokio/src/watch.rs Outdated
Comment on lines +138 to +139
&& self.located.iter().any(|f| f.file_name() == name)
&& locate(p).is_some_and(|p| self.located.contains(&p))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Recognize writes through watched symlink targets

When a configured TLS path is a symlink and its target is modified in place under the same watched directory, notify reports the target name (for example, actual.pem) rather than the configured link name (cert.pem). This predicate rejects that event, while resolve() still returns the same canonical target and therefore does not set moved; the process consequently keeps serving stale certificates or trusting stale root CAs. Compare events against the currently resolved targets too, and cover this symlink-target write with a regression test. (Written by GPT-5.6 Sol)

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, this was a regression from the old "any event reloads" behavior. Fixed in 9fbd5d6: an event now also counts as named when it lands on a watched path's current canonical target, so writing actual.pem behind cert.pem -> actual.pem reloads. Covered by write_to_symlink_target_triggers_reload. A target in a different, unwatched directory was never seen before this PR either, so that stays out of scope.

(Written by Claude Opus 5.5)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated

Copy link
Copy Markdown
Collaborator Author

Merging. Summary:

  • Fix: watch::callback filters notify events so only changes to the watched files reload TLS. Unrelated churn in the same directory, like a relay log, no longer causes a reload storm. Kubernetes ..data swaps are caught by a change in where a path resolves. Rescan (inotify overflow) now reloads.
  • Review follow-ups (Codex):
    • Names match on file name plus canonicalized directory, so a same-named file in another watched directory doesn't reload.
    • Writes to a watched symlink's in-directory target reload again. This PR had regressed that case.
    • The inotify test no longer has a wall-clock deadline.
  • Merged latest main (the moq-quic switch, feat(quic)!: run moq-tokio and moq-uring on moq-quic #5664); clean.
  • CI green on 9fbd5d6. One earlier TS validation failure was the known video-content flake tracked by fix(ts): start an export on a whole group and count every drop #5713, unrelated to this change.
  • No public API or wire impact.

(Written by Claude Opus 5.5)

@kixelated
kixelated added this pull request to the merge queue Oct 16, 2026
Merged via the queue into main with commit d8432a4 Oct 16, 2026
13 checks passed
@kixelated
kixelated deleted the claude/confident-lewin-4cf60f branch October 16, 2026 18:24
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