Skip to content

fix(core): retry failed discovery regions once before failing a launch - #1304

Open
etgbrown wants to merge 4 commits into
OpenCloudGaming:devfrom
etgbrown:discovery-retry
Open

etgbrown wants to merge 4 commits into
OpenCloudGaming:devfrom
etgbrown:discovery-retry

Conversation

@etgbrown

@etgbrown etgbrown commented Oct 25, 2026 •

Copy link
Copy Markdown

Problem

A direct launch (--launch-app-id, opennow://launch, Playnite) runs its own session.remote.list about a second after the startup background sweep. If one regional request in that sweep failed and no session was found, discovery returned session_discovery_failed and the launch stopped, although the next click worked.

One current.log with v1.0.3's discovery code shows how often: 10 of 23 launches that started within 2.2 s of the previous sweep failed this way, against 0 of 15 with a gap of 2.7 s or more and 4 of 83 background sweeps. The failing sweeps took 1.5–3.1 s, so these were fast per-region errors, not the 12 s budget running out. The logs attached to #985 (a color-mode report) show the same failure.

Since #1334: a new launch now continues to session.create after session_discovery_failed, so this no longer blocks first-click launches. Reconnect, reconciliation and known-conflict discovery remain strict, though, so one transient regional failure still fails those. With this PR the sweep itself recovers from such failures. An existing session in a briefly failing region is then found and resumed directly, without the create → session_conflict round trip.

Cause

discover_sessions had no retry, so one transient regional failure decided the whole sweep. The per-region error was also discarded, so the log could not say which region failed or why.

Change

Core only, no QML changes.

  • While no session has been found and no region rejected the credentials, transiently failed regions are retried once with a 1.5 s timeout: a fast failure 750 ms later, a timeout (which already waited 3 s) at once. The retry runs only if more than the delay plus 1.5 s of the 12 s budget remain, so a region that keeps timing out fails the sweep after about 4.5 s. v2/serverInfo gets the same single retry.
  • Transient means HTTP 408/425/429/500/502/503/504, the set get_session already retries (now one shared helper), or any transport failure. Other statuses, invalid bodies and rejected requestStatus values are final, and auth errors still short-circuit.
  • The delay is a new Cancellation::sleep: cancelling the RPC ends it and sends no retry.
  • Fail-closed (fix(session): recover existing games instead of failing launch #898) is unchanged: a region that still failed or was never checked, with no session found, returns session_discovery_failed.
  • Each sweep writes one bounded line to current.log, for example [session] discovery: serverInfo=ok regions=25 failed=2 retried=2 recovered=2 failures=region19:timeout#1,Bulgaria:closed#1. Regions are named by CloudMatch metadata key, never by URL. FailureKind takes the timeout/dns/tls/connect stage from fix(core): name the failed transport stage in network errors #1109's transport_failure and adds what the retry and the log need on top: HTTP status, closed connections, invalid bodies and rejected requests.
  • gfn.rs remote_sessions holds session_routing and auth_operation for the whole sweep, so a sweep that retries holds them longer (the delay plus the retried requests, each at most 1.5 s); since dev's allocation admission lock, a retrying sweep also holds that lock for the same extra time, still within the 12 s budget.

Verification

  • cargo fmt --check and cargo clippy --locked --all-targets -- -D warnings (core): clean.
  • cargo test --locked (core, debug, merged with dev 5b89338): 926 passed, 0 failed, 4 ignored across the core binaries and integration tests. New tests cover recovery on the retry, fail-closed after a failed retry, no retry for 4xx/501/body/rejected/auth failures, a found session, a missing budget, cancellation during the delay, the serverInfo retry, the log line, and closed versus timeout classification. Four run over loopback HTTP.
  • The update_apply lib tests fail intermittently here, with the same assertion on unmodified dev; this change does not touch that target.
  • Live, Linux, 25 regions: an earlier build of this change, which retried every non-auth failure, swapped into the v1.0.3 AppImage. All 7 launches started on the first click. The 2 that created a fresh session were rescued by the retry (timeout and closed regions, all recovered), kinds this version still retries. The other 5 resumed an existing session.
  • Still present on dev: the unmodified Linux nightly v1.0.4-nightly.1247.1 (dev 08822ce) failed 5 of 7 direct launches (--launch-app-id) with session.remote.list: outcome=session_discovery_failed. Each failing sweep took 1.4–2.2 s and ran 2–3 s after the startup sweep. One launch failed 3 times in a row before a relaunch streamed.

Not verified

  • This dev-based build live, including on the Qt dev nightly.
  • Windows, macOS and Playnite launches.
  • Windows, macOS and Qt ctest, left to CI.

Related

#985, #898, #1109

🤖 Generated with Claude Code

Problem: a direct launch (--launch-app-id, opennow://launch, Playnite) runs
its own session.remote.list about a second after the startup background
sweep. When one regional request of that sweep failed and no session was
found, discovery returned session_discovery_failed and the launch stopped,
although the next click worked.

Cause: discover_sessions had no retry, so a single transient regional
failure, such as a timeout or a dropped keep-alive connection, decided the
whole sweep. The per-region error was also discarded, so current.log could
not say which region failed or why.

Change:
- While no session has been found and no region rejected the credentials,
  regions whose failure is transient are retried once, 750 ms later, inside
  the existing 12 s discovery budget. The retry runs only if more than
  2.25 s of the budget remain (the delay plus 1.5 s for a useful attempt),
  and retried requests keep the min(remaining, 3 s) timeout. v2/serverInfo
  gets the same single retry.
- Transient means HTTP 408, 425, 429, 500, 502, 503 or 504, the set
  get_session already retries (now shared as is_transient_status), or any
  transport failure. Other statuses, invalid bodies and rejected
  requestStatus values are final, and authentication errors still
  short-circuit.
- The delay uses a new Cancellation::sleep that waits on the request's
  condvar, so cancelling the RPC ends the wait and no retry is sent.
- The fail-closed contract in docs/core-protocol.md (OpenCloudGaming#898) is unchanged: if
  a region still failed or was never checked and no session was found,
  discovery returns session_discovery_failed, and callers must not create
  a session.
- Each sweep queues one diagnostics line (at most four queued, oldest
  dropped), and the session.remote.list and session.active.get handlers
  write every queued line to current.log:
    [session] discovery: serverInfo=<ok|recovered|failed> regions=N
    failed=N retried=N recovered=N failures=<name>:<kind>#<attempt>,...
  Kinds are http=<status>, timeout, dns, tls, connect, closed, body,
  rejected, network and unchecked, and the list stops after six entries
  with +N. The timeout, dns, tls and connect stages come from the shared
  transport_failure classifier (OpenCloudGaming#1109); closed marks a connection closed
  or reset under the request. Regions are named by CloudMatch metadata key
  (region<index> when the key is not a plain identifier). Lines never
  contain URLs, tokens or error messages.
- gfn.rs remote_sessions, like hinted session reconciliation, holds the
  session_routing and auth_operation locks for the whole sweep, so a sweep
  that retries holds them longer: the 750 ms delay plus the retried
  requests, still within the 12 s budget. Concurrent routing calls wait
  for it.

Verification (native/opennow-core, debug, as in CI):
- cargo fmt --check and cargo clippy --locked --all-targets -- -D warnings:
  clean.
- cargo test --locked: the opennow-core binary passes 713/713, and the
  other binaries and integration tests pass. New tests cover a transient
  failure recovering on the retry, fail-closed after a failed retry, no
  retry for 4xx, 501, body, rejected or authentication failures, no retry
  once a session is found or without budget, cancellation during the
  delay, the serverInfo retry, the summary queue and line format, and
  closed versus timeout classification. Four of them run over loopback
  HTTP.
- The library target's update_apply process tests fail intermittently in
  this environment, with the same assertion on unmodified dev. That target
  builds only push/ and update_apply/, which this change does not touch.

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

coderabbitai Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

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: c1f0876b-90b2-4b0a-a298-ff7a73306ef5

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • Trigger review
  • 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

@capy-ai

capy-ai Bot commented Oct 25, 2026

Copy link
Copy Markdown
Contributor

Thanks. The retry is well bounded otherwise: 4 workers, at most 32 regions, one retry per failed endpoint per sweep, a delay that respects cancellation, and failure stays closed. One issue before merge:

  • P2, native/opennow-core/src/cloudmatch.rs:2611-2614,2669: a region that keeps timing out gets a second full 3 s timeout after the 750 ms delay. Calling the real discovery helper took 6.755 s versus 3.000 s to end in session_discovery_failed, so a real outage now takes more than twice as long to show up. Either retry only fast failures (connection refused/reset, 5xx) and not timeouts, or cap the retry's extra latency, and add a timing test for the persistent-timeout case.

Tests: 30 discovery tests and the full core suite (859, 4 ignored) pass, and fmt is clean.

A region that kept timing out got a second full 3 s request after the
750 ms delay, so a persistent timeout took 6.75 s to fail discovery
instead of 3 s.

- The retry pass now uses its own 1.5 s per-request timeout.
- A timed-out request is retried at once, since it already waited out its
  timeout. Fast failures still wait 750 ms first.
- The retry budget check is now the delay plus 1.5 s.
- v2/serverInfo is retried the same way.

A region that keeps timing out now fails the sweep after about 4.5 s.
Timeouts stay retryable: in the live runs, both launches the retry
rescued were timeouts that succeeded on the second attempt.

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

Copy link
Copy Markdown
Author

Thanks. Agreed: a persistent timeout shouldn't double the time to fail. Fixed in 8abf4c5 by capping the retry:

  • Shorter retry timeout: the retry pass has its own 1.5 s per-request timeout (DISCOVERY_RETRY_REQUEST_TIMEOUT).
  • No delay after a timeout: a timed-out request is retried at once, since it already waited 3 s. Fast failures (refused/reset/5xx) still wait 750 ms.
  • Budget: the check is now the delay plus 1.5 s.
  • v2/serverInfo: gets the same treatment.

Result: a region that keeps timing out now fails the sweep after about 4.5 s instead of 6.75 s. The worst case, a fast failure plus a persistent timeout in the same sweep, is 5.25 s.

Why I kept timeouts retryable: in the live runs behind this PR, both launches the retry rescued were timeouts that succeeded on the second attempt (Bulgaria:timeout#1 and region19:timeout#1, both recovered). Normal regional replies are well inside 1.5 s: 25 regions finish in 1–3.5 s over 4 workers.

Tests:

  • New: a_region_that_keeps_timing_out_fails_after_one_shorter_retry drives discover_sessions with a region that sleeps out its timeout and then fails. It asserts the two request timeouts are 3 s then 1.5 s, and that the sweep finishes in at least 4.5 s and under 5.25 s, which fails if the old full retry or the delay comes back.
  • Three existing tests used a timeout to exercise the delay or its cancellation. They now use a closed connection, so they still cover that path.
  • Docs: docs/core-protocol.md and the PR description are updated.

cargo fmt --check and cargo clippy --locked --all-targets -- -D warnings are clean. Core cargo test --locked: 860 passed, 0 failed, 4 ignored.

@capy-ai

capy-ai Bot commented Oct 29, 2026

Copy link
Copy Markdown
Contributor

@etgbrown GitHub currently reports merge conflicts with dev, so this PR cannot be merged as it stands. Please update your branch from the latest dev, resolve the conflicts, and push the result here. Once updated, please rerun the relevant tests and make sure the PR's CI passes so we can reassess it for merge.

Resolves the conflicts with dev and ports dev's discovery changes into the
retrying sweep:

- remote_sessions keeps dev's allocation admission lock and its
  completeness-gated cleanup of unresolved allocations and pending stops.
  discover_remote_sessions now returns a DiscoverySweep with the sessions,
  whether every region was checked, and the endpoints it covered.
- discover_sessions_with_status carries the retry and returns completeness;
  a region that recovers on the retry counts as checked.
- fetch_region_sessions applies dev's per-region rules: ended (status 7)
  sessions are skipped and a malformed entry fails the region as a final
  (non-retried) body failure.
- Cancellation::sleep waits on dev's combined receipt lock and checks
  cancellation on the guarded receipt, rather than calling cancelled(),
  which takes the same lock.
- docs/core-protocol.md describes both the retry and OpenCloudGaming#1334's new-launch
  fallback.

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

etgbrown commented Oct 31, 2026 •

Copy link
Copy Markdown
Author

Updated: I merged the latest dev (5b89338) in c7af7e8. GitHub now reports no conflicts.

Conflicts resolved:

  • cloudmatch.rs: dev's discovery changes are now inside the retrying sweep:
    • Admission lock: remote_sessions keeps dev's allocation admission lock.
    • Completeness-gated cleanup: the cleanup of unresolved allocations and pending stops is kept. discover_remote_sessions now returns the sessions, the complete flag and the endpoints it covered.
    • Recovered regions: a region that recovers on the retry counts as checked.
    • Per-region rules: the region fetch skips ended (status 7) sessions, and a malformed entry fails the region as a final, non-retried failure.
  • requests.rs: this merged without a conflict but no longer compiled after dev's combined receipt lock. Cancellation::sleep now waits on receipt and checks cancellation on the guarded value, instead of calling cancelled(), which takes the same lock.
  • docs/core-protocol.md: the discovery paragraph now describes both this retry and fix(qt): allow new launches during regional discovery outages #1334's new-launch fallback.

How this relates to #1334: with #1334, new launches no longer depend on a perfect sweep. Reconnect, reconciliation and known-conflict discovery stay strict, so this retry is what keeps a briefly failing region from failing those. The PR description has a note on it.

Checks (core, merged tree):

  • cargo fmt --check: clean.
  • cargo clippy --locked --all-targets -- -D warnings: clean.
  • cargo test --locked: 926 passed, 0 failed, 4 ignored. This includes dev's cleanup_absence_requires_complete_discovery_and_durable_record_removal and the retry, timing and sleep-cancellation tests.

The qt-ci and CodeQL runs for c7af7e8 show "action required". As a first-time contributor, my runs need a maintainer to approve them. @zortos293, could you approve the workflow runs for this PR? I'll fix anything CI flags. Nothing in this diff touches Qt. The Qt launch fixtures, including #1334's, use mock core clients, so the core changes above can't affect them.

Resolves the conflict with dev's unresolved-allocation discovery: the
region of a verified unresolved allocation now joins the sweep's starting
regions (after the selected endpoint), so the retrying sweep checks it
even when serverInfo omits it, as dev's inline sweep does.
discover_remote_sessions takes the starting regions as one list.

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

Copy link
Copy Markdown
Author

Merged the latest dev (2b21b2a) in 80a2ec0; this resolves the new conflict in cloudmatch.rs.

Dev's unresolved-allocation change now runs inside the retrying sweep. The region of a verified unresolved allocation joins the sweep's starting regions, right after the selected endpoint, so it is checked even when serverInfo omits it, just as in dev's inline sweep. To keep the argument count within clippy's limit, discover_remote_sessions now takes the starting regions as one list.

Core checks on the merged tree:

  • cargo fmt --check and cargo clippy --locked --all-targets -- -D warnings: clean.
  • cargo test --locked: 957 passed, 0 failed, 4 ignored, including dev's unresolved-allocation and allocation-recovery tests.

This branch has not been deployed

No deployments
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