Skip to content

Persist fetched config and bootstrap from it across restarts#16

Merged
myleshorton merged 7 commits into
mainfrom
fisk/persist-config
Jul 22, 2026
Merged

Persist fetched config and bootstrap from it across restarts#16
myleshorton merged 7 commits into
mainfrom
fisk/persist-config

Conversation

@myleshorton

@myleshorton myleshorton commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Problem

The config updater (WithConfigURL) fetches the fronting config on startup + every 12h and applies it to the in-memory front pool — but never persisted it. So on restart, the client reverted to the seed config passed to New (typically the consumer's embedded copy) until the next background fetch completed. Where the config host is blocked — raw.githubusercontent.com in China — that fetch never succeeds, so the client was pinned to the embedded seed on every run.

This also pushed consumers (radiance) to fetch + persist the config themselves, duplicating the updater's fetch.

Change

Make domainfront own the full config lifecycle — fetch → apply → persist → bootstrap:

  • WithConfigCacheFile(path) — persists each successfully fetched config and, on the next start, bootstraps from it in preference to the seed.
  • New prefers the persisted config (loadPersistedConfig) over the seed when present. A missing/corrupt/unreadable cache is ignored and never fails construction (falls back to the seed).
  • fetchAndApplyConfig persists the raw bytes after a successful applyConfig.

Design notes

  • Prefer-persisted-unconditionally: the config has no generation timestamp, so freshness of persisted-vs-seed can't be compared. Persisted is preferred (matching what the radiance consumer already did), and the updater refreshes it on startup when the host is reachable.
  • Persistence reuses the same non-atomic writeFile as the fronts cache. A torn write (crash mid-write) fails to parse on next boot and falls back to the seed — the intended safe fallback.
  • Concurrency: loadPersistedConfig runs in New before goroutines start; persistConfig runs only from the single configUpdater goroutine. No concurrent writers.

Testing

go build, go vet, gofmt -l, go test -race ./... all pass. New config_persist_test.go covers: persist+load round-trip, absent/corrupt cache → nil (ignored), New prefers persisted over seed, New falls back to seed when cache is absent or corrupt (never errors), and the end-to-end fetchAndApplyConfig → cache-written path via an httptest server.

Follow-up

radiance's NewFronted will be simplified to seed the embedded config + WithConfigCacheFile and drop its own duplicate fetch (supersedes the interim approach in getlantern/radiance#573).

Summary by CodeRabbit

  • New Features

    • Added optional persistence for successfully fetched configuration.
    • Clients can load cached configuration during startup, preferring it over the seeded configuration.
    • Added graceful fallback when cached data is missing, unreadable, or corrupted.
  • Security

    • Cached configuration files are now restricted to owner-only access.
  • Bug Fixes

    • Cache write failures no longer interrupt normal client operation.

domainfront's config updater (WithConfigURL) fetches the config on startup
and every 12h and applies it to the in-memory front pool, but never
persisted it. So a restart reverted to the seed config passed to New
(typically the consumer's embedded copy) until the next background fetch
completed — and where the config host is blocked (e.g. raw.githubusercontent.com
in China) that fetch never succeeds, so the client was pinned to the embedded
seed every run.

This makes domainfront own the full config lifecycle — fetch → apply →
persist → bootstrap — so consumers don't have to fetch/persist the config
themselves (which duplicated the updater's fetch):

- WithConfigCacheFile(path) persists each successfully fetched config and,
  on the next start, bootstraps from it in preference to the seed.
- New prefers the persisted config (loadPersistedConfig) over the seed when
  present; a missing/corrupt cache is ignored and never fails construction.
- fetchAndApplyConfig persists the raw bytes after a successful apply.

The config carries no generation timestamp, so persisted is preferred
unconditionally (as the consumer previously did); the updater refreshes it
on startup when the host is reachable. Persistence reuses the same non-atomic
writeFile as the fronts cache; a torn write fails to parse next boot and
falls back to the seed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 22, 2026 14:54
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@myleshorton, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 40 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: cfc20bc9-8ac7-409f-b2dc-0c9a20b810c8

📥 Commits

Reviewing files that changed from the base of the PR and between 590119f and 9cc43ea.

📒 Files selected for processing (3)
  • config_persist_test.go
  • domainfront.go
  • fileutil.go
📝 Walkthrough

Walkthrough

The client gains an optional on-disk configuration cache, loads valid cached configuration during construction, persists successfully fetched configuration, and writes cache files with owner-only permissions. Tests cover round trips, fallback behavior, corrupt data, startup precedence, and fetch persistence.

Changes

Configuration persistence

Layer / File(s) Summary
Startup cache selection
domainfront.go
Adds the cache path option and makes New prefer a valid persisted configuration over the seed configuration.
Fetched configuration persistence
domainfront.go, fileutil.go
Loads and writes cached configuration data, persists successful fetches, and changes written file permissions to 0o600.
Persistence behavior validation
config_persist_test.go
Tests valid, absent, and corrupt caches; startup precedence and fallback; fetch persistence; and construction without network dialing.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant HTTPServer
  participant CacheFile
  HTTPServer->>Client: Return gzipped configuration
  Client->>Client: Apply configuration
  Client->>CacheFile: Persist fetched bytes
  Client->>CacheFile: Load persisted configuration during startup
Loading

Suggested reviewers: copilot

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: persisting fetched config and reusing it on startup across restarts.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fisk/persist-config

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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds persistent caching for remotely fetched fronting configuration so clients can bootstrap from the last-known-good config across restarts (especially important when the config host is unreachable), while keeping construction resilient by falling back to the embedded seed config.

Changes:

  • Introduces WithConfigCacheFile(path) to persist fetched configs and bootstrap from them on subsequent startups.
  • Updates New() to prefer a previously persisted config over the provided seed config when available.
  • Adds config_persist_test.go covering round-trip persistence, fallback behaviors, and an end-to-end fetch→apply→persist path.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
domainfront.go Adds config cache path option, bootstrapping from persisted config in New, and persistence on successful config refresh.
config_persist_test.go Adds tests validating persistence, bootstrap preference, and safe fallback behavior when cache is absent/corrupt.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread domainfront.go
Comment thread domainfront.go
Comment on lines +501 to 503
if err := writeFile(c.configCachePath, data); err != nil {
c.log.Warn("Failed to persist config cache", "path", c.configCachePath, "error", err)
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed — changed writeFile to 0600. Since the fronts cache shares this helper and reveals the same kind of infrastructure, this hardens both. The parent dir was already created 0700, so this makes all on-disk state owner-only.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Correction / follow-up: reverted this back to 0644 in e62d602. On some platforms (macOS, Windows) the tunnel and the app are separate processes — potentially running as different users — that both read these cache files, so owner-only 0600 would risk breaking cross-process reads (and this shared writeFile also backs the existing fronts cache). The content is also already fetched from a public URL. Leaving this thread open: hardening to owner-only is worth revisiting but needs per-OS validation of the two-process model first, so I didn't want to bundle a potential functional regression into this PR.

Adam Fisk and others added 2 commits July 22, 2026 09:15
- loadPersistedConfig logs open failures other than "not found" (e.g.
  permission problems) before falling back to the seed, instead of
  swallowing them silently; a missing cache stays silent (normal first run).
- writeFile now writes 0600 instead of 0644. The cached fronting config and
  fronts reveal circumvention infrastructure (domains/IPs) and shouldn't be
  world-readable to other local users on shared devices. This also hardens
  the existing fronts cache, which shares this helper; the parent dir is
  already 0700.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The 0600 hardening would break cross-process reads: on some platforms the
tunnel and app are separate processes (potentially different users) that
both read these caches. Keep 0644 (the prior value) and document why. The
non-ENOENT open-error logging from the prior commit stays.

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

Comment thread domainfront.go
Comment thread config_persist_test.go
Copilot AI review requested due to automatic review settings July 22, 2026 15:17

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

Comment thread fileutil.go Outdated

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@config_persist_test.go`:
- Around line 130-138: Update the persistence test’s Eventually condition before
loadPersistedConfig to call loadPersistedConfig and verify it returns a non-nil
config containing “fetchedprovider”; remove the os.Stat-only check so polling
waits for a successfully parsed cache payload.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 27b3ba14-54a5-41c3-af59-595f2e93f245

📥 Commits

Reviewing files that changed from the base of the PR and between 25a3d47 and 590119f.

📒 Files selected for processing (3)
  • config_persist_test.go
  • domainfront.go
  • fileutil.go

Comment thread config_persist_test.go Outdated
…x comment

- New now falls back to the seed config when a persisted cache parses but
  fails applyConfig (e.g. a torn write that decompresses to YAML with no
  providers). Previously that returned an error and failed construction,
  breaking the "a bad cache never fails New" contract. Only a seed that
  itself won't apply fails New now.
- Add TestNew_FallsBackWhenPersistedConfigInvalid (parseable-but-no-providers
  cache → falls back to seed, no error).
- Fix the writeFile perms comment: the 0700 parent dir already gates access
  regardless of file mode, so the prior "0644 for cross-user reads" wording
  was misleading.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 22, 2026 15:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

fileutil.go:23

  • The comment implies the 0700 parent directory always “already gates access regardless of file mode”, but os.MkdirAll does not change permissions on an existing directory. If the parent directory already exists with broader perms (e.g. 0755), the file will be written 0644 and may be readable by other users. This also contradicts the PR description’s “owner-only access” claim (from the auto summary). Clarify the comment to reflect the existing-directory case (or change the file mode if owner-only is required).
	// The parent dir is created 0700 (owner-only), which already gates access
	// regardless of file mode. Keep the historical 0644 rather than tightening
	// to 0600: the tunnel and app processes' on-disk sharing model is OS-specific
	// and tightening needs per-OS validation first.

Comment thread config_persist_test.go Outdated
…ist test

writeFile is non-atomic, so os.Stat can succeed before the content is fully
written. Poll loadPersistedConfig until it parses and contains the fetched
provider instead, removing the partial-read flake.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 22, 2026 15:27

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

Comment thread domainfront.go
Comment thread fileutil.go Outdated
- loadPersistedConfig: drop the 'size-bounded by ParseConfigFromReader'
  claim; that bounds only the compressed bytes, and unbounded gzip
  decompression is a pre-existing property of ParseConfig (the network
  fetch path shares it) — out of scope to change here.
- writeFile: note MkdirAll only sets 0700 on a dir it creates, so in a
  pre-existing shared dir the file mode still matters; keep 0644.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 22, 2026 15:31

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

fileutil.go:25

  • The PR description’s auto-generated release notes claim the cached config is now restricted to owner-only access, but writeFile() still writes with mode 0644 (and even documents keeping 0644 intentionally). Please update the PR description/release notes to match the actual behavior, or (if owner-only is required) change the implementation after validating the cross-process sharing model on each OS.
	// Keep the historical 0644 rather than tightening to 0600. MkdirAll above
	// sets 0700 only on a dir it creates; it leaves an existing dir's perms
	// untouched, so in a pre-existing shared dir the file mode still matters —
	// and the tunnel and app processes' on-disk sharing model is OS-specific.
	// Tightening needs per-OS validation first.
	return os.WriteFile(path, data, 0o644)

Comment thread domainfront.go
Consistent with the open/parse-failure logs, so diagnosis isn't harder when
the cache path varies by platform.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 22, 2026 15:35

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

fileutil.go:25

  • The PR description’s auto-generated “Security” bullet claims cached config files are now restricted to owner-only access, but writeFile still writes with mode 0644 (and this helper is used for persisted config). To avoid misleading reviewers/operators, please update/remove that claim in the PR description (or, if owner-only is actually desired, change the implementation accordingly after validating the cross-process requirements mentioned here).
	// Keep the historical 0644 rather than tightening to 0600. MkdirAll above
	// sets 0700 only on a dir it creates; it leaves an existing dir's perms
	// untouched, so in a pre-existing shared dir the file mode still matters —
	// and the tunnel and app processes' on-disk sharing model is OS-specific.
	// Tightening needs per-OS validation first.
	return os.WriteFile(path, data, 0o644)

@myleshorton
myleshorton merged commit 2862f7a into main Jul 22, 2026
3 checks passed
@myleshorton
myleshorton deleted the fisk/persist-config branch July 22, 2026 15:41
myleshorton pushed a commit to getlantern/radiance that referenced this pull request Jul 22, 2026
Rewrites NewFronted on top of domainfront's new config-cache support
(getlantern/domainfront#16) instead of radiance fetching + persisting the
config itself, which duplicated domainfront's own background config fetch.

NewFronted now just seeds domainfront with the embedded config and sets
WithConfigCacheFile; domainfront fetches configURL off the critical path,
persists it, and bootstraps from the persisted copy on the next start in
preference to the seed. Startup still does no blocking network I/O (the
original fix for the 30s raw.githubusercontent.com stall on cold start in
China), but the radiance-side loadBootstrapConfig/parseCachedConfig/
fetchAndCacheConfig + cache-warming goroutine are gone (fronted.go 207→80
lines). Bumps domainfront accordingly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
myleshorton added a commit to getlantern/radiance that referenced this pull request Jul 22, 2026
Removes the 30s blocking fetch of fronted.yaml.gz from raw.githubusercontent.com
on the radiance init critical path (blocked in China → stalled startup → the
Pro-users-logged-out cluster). NewFronted now seeds domainfront with the
embedded config and sets WithConfigCacheFile; domainfront owns the config
lifecycle (fetch off the critical path → persist → bootstrap from the persisted
copy, getlantern/domainfront#16). The radiance-side fetch/persist machinery is
deleted (fronted.go 207→80 lines). Bumps domainfront.
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.

2 participants