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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant