Race multiple config URLs; first valid response wins#17
Conversation
WithConfigURL is now variadic: given several URLs, the config updater fetches them concurrently and applies the first valid response, so a blocked or slow source (e.g. a raw.githubusercontent.com URL in a censored region) can't hold up a reachable mirror. Backward compatible — a single-URL call behaves as before. - fetchAndApplyConfig races all URLs under a shared cancellable context; losers are cancelled once one wins. The results channel is buffered to the URL count so no losing goroutine leaks on its send. - A source that fetches+parses but fails applyConfig (e.g. zero providers from a corrupt mirror) is skipped rather than treated as terminal — a backup mirror exists precisely to cover for that. Only a real non-200/parse/apply failure across all sources logs a single Warn; per-source failures are Debug. Tests: race-to-first-valid with a failing source first, skip an unapplyable source in favor of a delayed valid one, and all-sources-fail keeps the seed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Warning Review limit reached
Next review available in: 43 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughThe client now accepts multiple configuration URLs, fetches them concurrently, applies and persists the first usable configuration, skips failed or unapplyable sources, and preserves seeded configuration when all sources fail. Tests cover these startup behaviors. ChangesConfig bootstrap and fallback
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant ConfigSourceA
participant ConfigSourceB
participant ConfigState
Client->>ConfigSourceA: Fetch configuration
Client->>ConfigSourceB: Fetch configuration
ConfigSourceA-->>Client: Failure or unapplyable config
ConfigSourceB-->>Client: Raw bytes and parsed config
Client->>ConfigState: Apply and persist winning config
Client-->>ConfigSourceA: Cancel remaining request
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Pull request overview
This PR makes the config updater resilient to blocked/slow sources by allowing multiple config URLs to be raced concurrently and applying the first config that successfully fetches, parses, and applies—supporting censored-network scenarios (e.g., GitHub raw blocked in China).
Changes:
- Make
WithConfigURLvariadic and store multiple config URLs on the client. - Update
fetchAndApplyConfigto concurrently fetch from all URLs and apply the first valid result (with per-source fetch/parse failures treated as non-terminal). - Add tests covering “race to first valid”, “skip unapplyable source”, and “all sources fail keeps seed”.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| domainfront.go | Switches from a single config URL to racing multiple URLs; adds fetchConfigFrom helper and updates updater logic. |
| config_persist_test.go | Adds new tests to validate multi-URL racing behavior and failure-handling paths. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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 `@domainfront.go`:
- Around line 481-482: Bound each individual config fetch initiated by
fetchAndApplyConfig and fetchConfigFrom with a finite request timeout, using
context.WithTimeout or a per-client http.Client timeout. Ensure the timeout
applies before the HTTP request begins, is canceled after completion, and allows
the results loop over c.configURLs to continue when a mirror hangs.
- Around line 486-497: Update the config-loading loop around applyConfig and the
final aggregate warning to track whether any fetched config failed during
application, distinguishing that outcome from sources that failed to fetch or
parse. Keep the per-source warnings unchanged, and make the final c.log.Warn
message accurately report when all available configs were unapplyable rather
than labeling them as fetch failures.
🪄 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: 2a1e4aab-399b-497e-8970-56fc36dc01c5
📒 Files selected for processing (2)
config_persist_test.godomainfront.go
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (2)
domainfront.go:85
- WithConfigURL(""), which previously disabled the updater (since New checked c.configURL != ""), now sets configURLs to a 1-element slice and starts the updater, causing repeated failed fetch attempts and warnings. Also, storing the caller-provided slice allows accidental later mutation by the caller via WithConfigURL(urls...). Consider filtering out empty strings and copying into a new slice before assigning.
func WithConfigURL(urls ...string) Option { return func(c *Client) { c.configURLs = urls } }
domainfront.go:492
- The PR description says per-source failures are logged at Debug and only total failure is Warn. applyConfig failures are per-source in the raced set, but this is currently logged at Warn, which can be noisy for an expected-bad mirror while racing. Consider downgrading this to Debug to match the stated behavior (total failure is already logged once at Warn).
// Parseable but unusable (e.g. no providers). Don't give up — a
// backup mirror exists precisely to cover for a corrupt source, so
// keep reading the remaining results.
c.log.Warn("Failed to apply config update", "url", r.url, "error", err)
continue
- Bound fetchAndApplyConfig with a context timeout (defaultConfigFetchTimeout,
60s) so a hung source (timeout-less client on a black-holed connection) can't
stall the updater goroutine indefinitely.
- Explicitly cancel once a config is applied so losers' in-flight fetches stop
immediately rather than at function return; comments updated to match.
- WithConfigURL clones the variadic slice so a caller mutating a passed slice
can't race the updater.
- apply-failure per-source log demoted Warn -> Debug (per-source failures are
expected when racing; only the total failure warns once), and the aggregate
warning reworded ("No source produced a usable config") since it also covers
apply-failures, not just fetch-failures.
- README: document the variadic WithConfigURL + racing (options + migration table).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… 20s timeout
- WithConfigURL drops empty strings so WithConfigURL("") leaves configURLs
empty (no auto-update), instead of starting an updater that warns on every
cycle. Added TestWithConfigURL_FiltersEmpty (also asserts no slice aliasing).
- fetchConfigFrom reads maxConfigSize+1 and rejects oversized responses, so a
body larger than the cap with a valid gzip prefix isn't silently accepted
(matches ParseConfigFromReader).
- Config-fetch race timeout 60s -> 20s (background refresh; no reason to linger).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Picks up getlantern/domainfront#17 (variadic WithConfigURL, races config sources). go.mod/go.sum only; no kindling code change.
Picks up getlantern/domainfront#17 (variadic WithConfigURL that races config sources, first valid wins) via kindling. No radiance code change yet — this carries the capability to main; the mirror URL will be added to NewFronted's WithConfigURL once the China-reachable mirror is publishing (getlantern/engineering#3721). go.mod/go.sum only; go build + fronted tests pass. Co-authored-by: Adam Fisk <a@lantern.io> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Bumps radiance to 838849783d37 (kindling + domainfront move with it), carrying the variadic WithConfigURL / multi-URL config racing capability (getlantern/domainfront#17) into the client. No behavior change yet: the client still fetches from the single GitHub config URL; the China-reachable mirror URL will be added to the raced set once it's publishing (getlantern/engineering#3721). go.mod / go.sum only; ran go mod tidy; go build ./... passes. Co-authored-by: Adam Fisk <a@lantern.io> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* Fix Android user data startup race (#8915) * Fix Android user data startup race * code review updates * code review updates * Bump radiance for non-blocking fronted-config startup (#8920) Bumps radiance to b72b27d, carrying the fronted-config init fix into the client. domainfront (=> 2862f7a) moves with it as an indirect dep. radiance no longer fetches the fronting config (fronted.yaml.gz) from raw.githubusercontent.com synchronously on the init critical path — a fetch that stalled startup for 30s per cold start where GitHub is blocked (China), contributing to the Android "Pro users shown as logged-out" cluster (getlantern/engineering#3716, symptom fixed in #8915). domainfront now owns the config lifecycle (fetch off the critical path → persist → bootstrap from the persisted copy), so startup does no blocking network I/O. go.mod / go.sum only; ran `go mod tidy`. `go build ./...` passes. Co-authored-by: Adam Fisk <a@lantern.io> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> * Provide a writable WebView2 user-data folder on Windows (#8921) * Provide webview path for Windows. * go mod tidy: drop stray go-arg/go-scalar from go.sum These entries were added to go.sum with no corresponding go.mod require and nothing imports them, so go mod tidy removes them. Orphaned go.sum entries can cause Go tooling (notably gomobile) to resolve transitive deps to older hashed versions, so keep go.sum consistent with go.mod. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * windows/webview: bail on unexpanded path or failed folder creation Address review on EnsureWebView2UserDataFolder: - If %LOCALAPPDATA% (or any referenced var) is undefined, ExpandEnvironmentStringsW leaves it literal in the output; bail rather than create a garbage relative folder and set WEBVIEW2_USER_DATA_FOLDER to an unusable value. - Only set WEBVIEW2_USER_DATA_FOLDER after confirming the target exists as a directory (via GetFileAttributesW); if CreateDirectoryW failed, leave WebView2 to its default instead of pointing it at a nonexistent folder. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * windows/webview: narrow unexpanded-path guard to leading '%' '%' is legal elsewhere in a Windows folder name (e.g. a username), so bailing on any '%' could reject a valid expanded path. An undefined %LOCALAPPDATA% leaves the template literal, so the result starts with '%' — check only the leading char. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * windows/webview: treat empty WEBVIEW2_USER_DATA_FOLDER as unset The null-buffer size probe returns >0 for a set-but-empty variable, so an empty override would be respected and leave WebView2 with an unusable folder. Read into a buffer instead: GetEnvironmentVariableW returns 0 when the var is unset or empty, so we only bail on a real non-empty override. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: jigar-f <jigar@getlantern.org> Co-authored-by: Adam Fisk <a@lantern.io> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> * Bump radiance for multi-URL config racing (#8924) Bumps radiance to 838849783d37 (kindling + domainfront move with it), carrying the variadic WithConfigURL / multi-URL config racing capability (getlantern/domainfront#17) into the client. No behavior change yet: the client still fetches from the single GitHub config URL; the China-reachable mirror URL will be added to the raced set once it's publishing (getlantern/engineering#3721). go.mod / go.sum only; ran go mod tidy; go build ./... passes. Co-authored-by: Adam Fisk <a@lantern.io> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> * Fix referral reward UI edge cases * code review changes --------- Co-authored-by: Myles Horton <afisk@getlantern.org> Co-authored-by: Adam Fisk <a@lantern.io> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: jigar-f <jigar@getlantern.org>
* Referral UI updates * Referral notification dialogue * code review updates * update radiance * point back to radiance * update radiance * More UI change * Fix referral reward UI edge cases (#8925) * Fix Android user data startup race (#8915) * Fix Android user data startup race * code review updates * code review updates * Bump radiance for non-blocking fronted-config startup (#8920) Bumps radiance to b72b27d, carrying the fronted-config init fix into the client. domainfront (=> 2862f7a) moves with it as an indirect dep. radiance no longer fetches the fronting config (fronted.yaml.gz) from raw.githubusercontent.com synchronously on the init critical path — a fetch that stalled startup for 30s per cold start where GitHub is blocked (China), contributing to the Android "Pro users shown as logged-out" cluster (getlantern/engineering#3716, symptom fixed in #8915). domainfront now owns the config lifecycle (fetch off the critical path → persist → bootstrap from the persisted copy), so startup does no blocking network I/O. go.mod / go.sum only; ran `go mod tidy`. `go build ./...` passes. Co-authored-by: Adam Fisk <a@lantern.io> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> * Provide a writable WebView2 user-data folder on Windows (#8921) * Provide webview path for Windows. * go mod tidy: drop stray go-arg/go-scalar from go.sum These entries were added to go.sum with no corresponding go.mod require and nothing imports them, so go mod tidy removes them. Orphaned go.sum entries can cause Go tooling (notably gomobile) to resolve transitive deps to older hashed versions, so keep go.sum consistent with go.mod. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * windows/webview: bail on unexpanded path or failed folder creation Address review on EnsureWebView2UserDataFolder: - If %LOCALAPPDATA% (or any referenced var) is undefined, ExpandEnvironmentStringsW leaves it literal in the output; bail rather than create a garbage relative folder and set WEBVIEW2_USER_DATA_FOLDER to an unusable value. - Only set WEBVIEW2_USER_DATA_FOLDER after confirming the target exists as a directory (via GetFileAttributesW); if CreateDirectoryW failed, leave WebView2 to its default instead of pointing it at a nonexistent folder. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * windows/webview: narrow unexpanded-path guard to leading '%' '%' is legal elsewhere in a Windows folder name (e.g. a username), so bailing on any '%' could reject a valid expanded path. An undefined %LOCALAPPDATA% leaves the template literal, so the result starts with '%' — check only the leading char. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * windows/webview: treat empty WEBVIEW2_USER_DATA_FOLDER as unset The null-buffer size probe returns >0 for a set-but-empty variable, so an empty override would be respected and leave WebView2 with an unusable folder. Read into a buffer instead: GetEnvironmentVariableW returns 0 when the var is unset or empty, so we only bail on a real non-empty override. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: jigar-f <jigar@getlantern.org> Co-authored-by: Adam Fisk <a@lantern.io> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> * Bump radiance for multi-URL config racing (#8924) Bumps radiance to 838849783d37 (kindling + domainfront move with it), carrying the variadic WithConfigURL / multi-URL config racing capability (getlantern/domainfront#17) into the client. No behavior change yet: the client still fetches from the single GitHub config URL; the China-reachable mirror URL will be added to the raced set once it's publishing (getlantern/engineering#3721). go.mod / go.sum only; ran go mod tidy; go build ./... passes. Co-authored-by: Adam Fisk <a@lantern.io> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> * Fix referral reward UI edge cases * code review changes --------- Co-authored-by: Myles Horton <afisk@getlantern.org> Co-authored-by: Adam Fisk <a@lantern.io> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: jigar-f <jigar@getlantern.org> * code review updates * Update referral_reward_dialog.dart --------- Co-authored-by: atavism <atavism@users.noreply.github.com> Co-authored-by: Myles Horton <afisk@getlantern.org> Co-authored-by: Adam Fisk <a@lantern.io> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Why
The config updater fetches the fronting config from a single URL (
raw.githubusercontent.com/getlantern/domainfront/...), which is blocked in China. To distribute the config through a China-reachable mirror without committing to one source, the client should be able to try several and use whichever works — see getlantern/engineering#3721.Change
WithConfigURLis now variadic: given multiple URLs,fetchAndApplyConfigfetches them concurrently and applies the first valid (fetched + parsed + applied) response. A blocked or slow source can't hold up a reachable one. Backward compatible — a single-URL call behaves exactly as before.applyConfig(e.g. a corrupt mirror serving zero providers) is skipped, not treated as terminal — the point of a backup mirror is to cover for exactly that. Only when all sources fail is a singleWarnlogged; per-source failures areDebug(a blocked mirror is expected when racing).Testing
go build,go vet,gofmt -l,go test -race ./...all pass (racing tests also run clean under-count=5). New tests:providers: {}source is skipped in favor of a deliberately-delayed valid one (deterministically exercises the skip-and-continue path; would fail with a naivereturn);Follow-up
This is the client-side primitive for #3721. Next: publish
fronted.yaml.gzto a neutral-account jsDelivr mirror (server side) and add its URL to the raced set in radiance'sNewFronted(currently a single GitHub URL).Summary by CodeRabbit
New Features
Bug Fixes