Skip to content

fix: skip needless remote connects in local read paths - #115

Open
lehuan5062 wants to merge 9 commits into
EpicGames:mainfrom
lehuan5062:fix/local-reads-skip-connect
Open

fix: skip needless remote connects in local read paths#115
lehuan5062 wants to merge 9 commits into
EpicGames:mainfrom
lehuan5062:fix/local-reads-skip-connect

Conversation

@lehuan5062

Copy link
Copy Markdown
Member

Problem

#107 bounded the remote-connect stall with 5s transport timeouts, but several read verbs still drive a remote connect even when local data is what gets returned. Against an unreachable server, every such read still pays the bounded ~5-10s wait for nothing: the fetched remote value is discarded, or the connect happens before the code even checks whether the caller asked for local-only data. This slows down lore log <branch>, the desktop app, and my lore-web branch graph. Under --local/--offline the connect is also a contract violation, not just wasted time.

Change

  • revision::resolve (revision.rs): the branch@LATEST/HEAD path drove the connect before checking should_search_remote, so --local/--offline connected anyway. Reordered to match the sibling revision-number path (branch@<n>), which already did this correctly.
  • Explicit-branch revision history (revision/history.rs): connected unconditionally, then discarded the remote latest unless --remote was set. The fetch now only happens inside the --remote branch.
  • File history (file/history.rs): connected before taking the --local early-return. The local latest and the early-return now come first; default-mode reconciliation is unchanged.
  • auth::resolve_user_info (auth/userinfo.rs): the CLI resolves user display names after every history listing, and this drove a connect regardless of --local/--offline. It now skips resolution in those modes; callers fall back to the raw ids.

Validation

cargo build, cargo clippy -p lore-revision, cargo test -p lore-revision -> all clean.
New Rust unit test in lore-revision/tests/revision.rs for the local-only branch@LATEST resolve.
New integration test scripts/test/test_local_reads_skip_connect.py: seeds and pushes a branch on a dedicated server, kills it, then asserts --local reads return the correct local latest well under the connect timeout, and the default-mode read doesn't stack connect waits.
Existing suites test_branch.py, test_dirty.py, test_branch_switch_reconnect.py -> 89 passed. One failure, test_branch_merge_carries_dirty_through_clean_merge, is a pre-existing stack overflow already present on main, unrelated to this PR.
Measured against a killed server:

  • revision history --branch --local: ~4.1s -> ~150ms
  • file history --local: ~125ms
  • --remote still queries the server and fails fast when it's down

Verified default behavior with the server up is unchanged.

AI Assistance Disclosure

I used an LLM (Claude Code) to help with implementation, code comments, and documentation on this change, and to help word things clearly as a non-native English speaker. I reviewed and tested all the code myself for its correctness, security, and license compliance.

lehuan5062 and others added 9 commits July 11, 2026 21:20
Add 5-second connect timeouts to gRPC and QUIC transports, and time-box
the remote branch resolution in status() to 3 seconds. When the configured
remote is unreachable, status calls now degrade gracefully instead of
stalling for the full idle timeout (~30s for QUIC, 10s+ for retries).

Critical fixes:
1. Time-box the entire remote resolution operation (both repository.remote()
   and branch::load_remote()), not just the fast path. This ensures the
   3-second deadline covers the actual RPC calls that can hang indefinitely,
   even when the remote connection is already warm.

2. Fix available flag semantics: distinguish between "remote not reachable"
   (available=false) and "remote reachable but query failed" (available=true).
   A server that responds with a permission error or other non-branch-not-found
   error is still reachable and should not be reported as unavailable to users.

Changes:
- lore-transport: gRPC endpoint gets connect_timeout for initial connection
- lore-revision: extract resolve_remote_latest() helper that returns
  (latest, authorized, available) where available reflects connection success
  rather than query success; eliminate double remote() call on event emission
- tests: replace overfitted tautological test with two real unit tests

Signed-off-by: Huân Lê-Vương <65440815+lehuan5062@users.noreply.github.com>
clippy::doc_markdown (denied via -D warnings in the pre-commit hook)
flags bare type-name references in doc comments. CI caught this;
local clippy runs missed it because they weren't re-run after this
doc comment was added in a later revision of the same change.

Signed-off-by: Huân Lê-Vương <65440815+lehuan5062@users.noreply.github.com>
Drop the 3-second status-level deadline from resolve_remote_latest and rely
solely on transport-level connect timeouts (5s for gRPC handshake). The
status-level timeout was layering two separate concerns and timing out slow
lookups instead of bounding unreachable remotes.

The root cause is QUIC's 30-second idle timeout on unreachable remotes: UDP
offers no connection-refused signal, so every local status read would stall
~10–30s waiting for the timeout. Transport-level connect timeouts now bound
this wait and are the single timeout layer.

Update resolve_remote_latest to log previously-discarded errors at debug
level before degrading gracefully. Remove the hung-remote timeout test (now
invalid without a local timeout); add a failed-remote test mirroring terminal
state from a bounded transport connect. Update doc comments to explain the
degradation contract.

Signed-off-by: Huân Lê-Vương <65440815+lehuan5062@users.noreply.github.com>
Lore's comment standard calls for minimal, purposeful doc comments and no
restating of self-explanatory code. The rustdoc # Arguments/# Returns sections
here described obvious parameters (repository, branch_id) and are rare elsewhere
in the crate. Collapse to a terse summary that keeps only the non-obvious facts:
graceful NoRemote degradation and the available flag reflecting connectivity
rather than query success.

Signed-off-by: Huân Lê-Vương <65440815+lehuan5062@users.noreply.github.com>
Several read verbs drove a remote connect even when local data is what
gets returned, stalling for the bounded connect timeout against an
unreachable server:

- revision::resolve drove the connect before checking
  should_search_remote, so branch@LATEST under --local/--offline
  connected anyway; reorder the guard to match the @Number path.
- Explicit-branch revision history connected unconditionally and then
  discarded the remote latest unless --remote was set; move the fetch
  inside the --remote branch.
- File history connected before taking the --local early-return;
  compute the local latest and take the early-return first.
- auth::resolve_user_info (driven by the CLI after history listings to
  prettify user ids) connected regardless of --local/--offline; skip
  resolution there and fall back to raw ids.

With a killed server, revision history --local drops from ~4.1s to
~150ms; default-mode explicit-branch history no longer wastes a connect
in the history lookup itself. Guarded by a new Python integration test
against a killed server and a Rust unit test for the local-only
branch@LATEST resolve.

Signed-off-by: Huân Lê-Vương <65440815+lehuan5062@users.noreply.github.com>
@ajcarberry ajcarberry added the area:core Core library and its interfaces (lib, C API); revision, storage, transport, protocol internals label Aug 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:core Core library and its interfaces (lib, C API); revision, storage, transport, protocol internals

Development

Successfully merging this pull request may close these issues.

2 participants