Skip to content

fix(node): use readiness for peer liveness - #248

Open
MikeTomlin19 wants to merge 6 commits into
Gitlawb:mainfrom
MikeTomlin19:agent/fix-peer-readiness-176
Open

fix(node): use readiness for peer liveness#248
MikeTomlin19 wants to merge 6 commits into
Gitlawb:mainfrom
MikeTomlin19:agent/fix-peer-readiness-176

Conversation

@MikeTomlin19

@MikeTomlin19 MikeTomlin19 commented Jul 25, 2026

Copy link
Copy Markdown

Summary

Use the database-aware /ready endpoint for periodic and manual peer checks, while falling back to /health only when an older peer returns 404 for /ready. Periodic federation state now requires two consecutive failed samples before marking a peer unreachable; the anonymous manual-ping endpoint is read-only.

Motivation & context

/health intentionally reports process liveness and remains successful during a database outage. Using it for peer reachability lets a node continue treating a peer as healthy even though that peer cannot serve database-backed requests. A single transient readiness failure also should not remove a peer from federated repository results for the next five-minute interval.

Closes #176

Kind of change

  • Bug fix
  • Feature
  • Security fix
  • Docs
  • Tests / CI
  • Refactor (no behavior change)
  • Breaking or protocol change (issue required first)

What changed

  • Changed the shared peer probe from /health to /ready, with a 404-only /health fallback for nodes released before /ready existed.
  • Reused that helper from the manual peer-ping API so both paths have identical readiness and no-redirect behavior.
  • Kept the anonymous manual-ping endpoint read-only; only the periodic gossip loop updates persisted federation reachability.
  • Required two consecutive periodic failures before marking a peer unreachable, while restoring reachability immediately after a successful sample.
  • Added structured logs for readiness failures, legacy fallback, fallback failures, transport failures, and timeout exhaustion.
  • Added regression coverage proving the manual handler uses readiness without mutating the federation gate, one transient failure preserves the gate, sustained failure clears it, success restores it, legacy peers remain compatible, and neither request path follows redirects.
  • Updated the operator guide and degraded-router comment to distinguish liveness from readiness.

How a reviewer can verify

DATABASE_URL=postgres://postgres:postgres@localhost:5432/gitlawb_test cargo test --workspace
cargo fmt --all -- --check
cargo clippy --workspace --all-targets -- -D warnings
cargo build --release --workspace

The focused regressions can also be run with:

cargo test -p gitlawb-node gossip_ssrf_tests
DATABASE_URL=postgres://postgres:postgres@localhost:5432/gitlawb_test \
  cargo test -p gitlawb-node manual_ping_uses_readiness_without_mutating_federation_gate

Before you request review

  • Scope is one logical change; no unrelated churn
  • cargo test --workspace passes locally
  • New behavior is covered by tests (required for fixes)
  • cargo fmt --all and cargo clippy --workspace --all-targets -- -D warnings are clean
  • Commit titles use Conventional Commits (feat(...), fix(...), docs(...))
  • Docs / .env.example updated if behavior or config changed (N/A)
  • Checked existing PRs so this isn't a duplicate

Protocol & signing impact

N/A — this does not change identity, signatures, UCANs, ref certificates, or P2P wire formats.

Notes for reviewers

The repository's current MSRV lane fails before compilation because newly resolved AWS crates require Rust 1.91.1 while CI installs 1.91 (currently 1.91.0). That existing manifest/lock/dependency drift is tracked in #242 and #243; stable tests, clippy, formatting, and the release build are clean for this change.

Summary by CodeRabbit

  • New Features
    • Peer monitoring now checks readiness, including database-aware status, rather than process liveness alone.
    • Legacy peers without a readiness endpoint continue to be supported through a safe health-check fallback.
  • Bug Fixes
    • Improved handling of failed, unavailable, non-ready, and redirected peer checks.
  • Documentation
    • Operational guidance now requires both /health and /ready endpoints for public URLs.

@github-actions github-actions Bot added the needs-tests Source changed without accompanying tests (advisory) label Jul 25, 2026
@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

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

Next review available in: 59 minutes

Your organization has reached its usage spending cap. Adjust your spending cap in the billing tab.

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: 97d476a3-a16f-425e-ad4c-e2178b8669e9

📥 Commits

Reviewing files that changed from the base of the PR and between 9ace2c6 and 32e4578.

📒 Files selected for processing (1)
  • crates/gitlawb-node/src/main.rs
📝 Walkthrough

Walkthrough

Peer liveness checks now use DB-aware /ready semantics, fall back to /health for legacy peers, share timeout budgets, and preserve redirect protections. Gossip checks require two consecutive failures before persistence changes, while API pings use the shared helper.

Changes

Peer readiness probing

Layer / File(s) Summary
Readiness probe and validation
crates/gitlawb-node/src/main.rs
The shared probe requests /ready, treats only 2xx responses as ready, falls back to /health on 404, shares timeout handling, rejects redirects, and maps failures to false.
Peer check integration
crates/gitlawb-node/src/main.rs, crates/gitlawb-node/src/api/peers.rs
Gossip checks and API peer pings use the readiness helper; gossip persistence requires two consecutive failures, and manual pings do not rewrite the federation gate.
Operational readiness contract
crates/gitlawb-node/src/main.rs, docs/RUN-A-NODE.md
Degraded-server documentation covers /health and /ready, and the public URL checklist requires both endpoints with DB-aware readiness.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant GossipTask
  participant ping_peer_readiness
  participant PeerReadyEndpoint
  participant PeerHealthEndpoint
  participant PeerDatabase
  GossipTask->>ping_peer_readiness: Probe peer URL
  ping_peer_readiness->>PeerReadyEndpoint: GET /ready
  PeerReadyEndpoint-->>ping_peer_readiness: Readiness response
  ping_peer_readiness->>PeerHealthEndpoint: GET /health after 404
  PeerHealthEndpoint-->>ping_peer_readiness: Legacy response
  ping_peer_readiness-->>GossipTask: Ready boolean
  GossipTask->>PeerDatabase: Persist after success or second failure
Loading

Possibly related PRs

Suggested reviewers: jatmn, kevincodex1, beardthelion

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
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.
Title check ✅ Passed The title is concise and accurately summarizes the main change: switching peer checks to readiness.
Description check ✅ Passed The description matches the template and covers summary, motivation, change details, verification, and review notes.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@github-actions

Copy link
Copy Markdown
Contributor

Thanks for the contribution. A couple of things will help us review this faster:

  • This changes Rust source but no tests changed. Tests are required for fixes and strongly encouraged for features.

See CONTRIBUTING.md. Update the PR and these notes will clear automatically.

@beardthelion beardthelion added crate:node gitlawb-node — the serving node and REST API kind:bug Defect fix — wrong or unsafe behavior subsystem:peers Peer announce, discovery, and registry labels Jul 25, 2026
@MikeTomlin19

Copy link
Copy Markdown
Author

For reviewer context, the needs-tests signal is a triage false positive rather than missing coverage.

This PR adds regression coverage inside the existing #[cfg(test)] module in crates/gitlawb-node/src/main.rs, using Tokio's #[tokio::test] attribute. In particular, ping_peer_readiness_ignores_liveness_only_health proves that 200 /health plus 503 /ready is treated as unready and that /health is never requested.

The triage workflow currently recognizes newly added literal #[test] and #[cfg(test)] lines, but not #[tokio::test], so it misses these inline async tests.

Validation completed locally:

  • cargo test -p gitlawb-node gossip_ssrf_tests — 4 passed
  • cargo test --workspace against PostgreSQL 16 — passed
  • cargo fmt --all -- --check — passed
  • cargo clippy --workspace --all-targets -- -D warnings — passed

@MikeTomlin19
MikeTomlin19 marked this pull request as ready for review July 25, 2026 19:30

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I found issues that need to be addressed before this is ready.

Findings

  • [P1] Preserve compatibility with peers that do not implement /ready
    crates/gitlawb-node/src/main.rs:985
    This unconditionally changes the peer wire contract from /health to /ready. Released v0.5.0 nodes expose /health but not /ready, and #170 deliberately left the peer probe on /health for exactly that rolling-upgrade case. Their /ready response is therefore 404, which this helper maps to false; both the periodic job and the manual ping persist that value. Since federated repository listing filters on last_ping_ok, a healthy v0.5.0 peer disappears from federation after the first probe. Please add a version/capability transition (for example, a 404-only legacy /health fallback that still treats a real /ready 503 as unready) and cover the mixed-version case, or provide the required coordinated rollout and compatibility window.

  • [P2] Update the operator peer-endpoint requirement
    docs/RUN-A-NODE.md:180
    The operator guide currently says that serving /health is sufficient because peers ping it. After this change peers instead require /ready, so an operator following the documented contract can be reported unreachable. Update the guidance to require /ready (and retain /health only for its liveness purpose).

  • [P3] Correct the degraded-router peer-ping comment
    crates/gitlawb-node/src/main.rs:735
    This comment still says peer pings use a successful /health response. The changed callers now use /ready, so the comment gives the wrong rationale for the degraded response and is likely to mislead future outage-routing work.

@MikeTomlin19

Copy link
Copy Markdown
Author

Addressed all three findings in f9b4ce3:

  • /ready now falls back to /health only on a 404, preserving compatibility with pre-/ready nodes while keeping real readiness failures such as 503 fail-closed.
  • Added mixed-version coverage (404 /ready + 200 /health) and redirect-safety coverage for the legacy fallback. The existing test still proves 503 /ready never falls back to a successful /health.
  • Updated the operator checklist to require both liveness /health and DB-aware /ready.
  • Corrected the degraded-router comment to describe peer readiness probes rather than 2xx /health pings.

Validation:

  • cargo test -p gitlawb-node gossip_ssrf_tests — 6 passed
  • cargo test --workspace against PostgreSQL 16 — passed
  • cargo fmt --all -- --check — passed
  • cargo clippy --workspace --all-targets -- -D warnings — passed

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I found issues that need to be addressed before this is ready.

Findings

  • [P2] Keep the legacy fallback within one peer-probe deadline
    crates/gitlawb-node/src/main.rs:988
    The shared client gives each request a 10-second timeout, but the new 404 fallback starts a second independently timed /health request. A legacy (or attacker-controlled public) peer can therefore hold the serial gossip loop for nearly 20 seconds by delaying /ready before returning 404 and delaying /health; the prior probe had a single 10-second budget. Since the peer table is unbounded and the loop updates one row at a time, enough such rows cause sweeps to overrun the five-minute interval and leave peer reachability stale. Apply one deadline to the complete /ready-then-/health operation (or pass the remaining budget to the fallback), and cover a delayed-404 case.

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@beardthelion LGTM, requires your manual review.

@beardthelion beardthelion left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The readiness switch is the right call and all four findings from the earlier rounds landed properly. I confirmed the compatibility story by execution rather than by reading it: an axum router carrying only /health and no fallback, which is v0.5.0's shape, answers 404 for GET /ready, and the probe falls through to /health and returns ready. /ready first shipped in v0.5.1, so the peers that arm exists for really do return 404. A 403 and a 500 both fail closed without touching /health.

I also mutated each line inside ping_peer_readiness_with_timeout one at a time against the seven tests. Pointing the probe at /health, widening the fallback trigger past 404, giving the fallback its own timeout budget, letting the client follow redirects, flipping the catch-all to true, and deleting the 404 arm each turn at least one test red. That part is well covered. Four things below.

Findings

  • [P2] Bind the manual ping path to readiness with a test
    crates/gitlawb-node/src/api/peers.rs:442
    Reverting this line to the pre-PR inline /health probe leaves all seven tests green. Every line inside the helper reddens under mutation, so the harness works and this is a real hole: the handler's readiness behavior is unpinned, and a later edit can quietly return GET /api/v1/peers/{did}/ping to liveness-only while CI stays green. A test that drives the handler (or at minimum asserts the call site's behavior) closes it.

  • [P2] Do not let a single probe sample drop a peer from federation
    crates/gitlawb-node/src/main.rs:948-949
    The probe result is written to last_ping_ok unconditionally, and api/repos.rs:1451 filters the federated fan-out on that flag, so one failed sample removes a peer's repos from /api/v1/repos/federated until the next 300s tick. The single-sample write predates this PR; what changes is the set of states that flip it, since a peer whose database hiccups for two seconds now drops out where previously only an unreachable process did. Two consecutive failures before writing false, or one re-probe inside the existing budget, would fix it. Whichever shape you pick, the property to preserve is that a peer survives one transient failure and still drops out on a sustained one.

  • [P3] Log the probe outcome
    crates/gitlawb-node/src/main.rs:1000-1017
    Five materially different exits collapse into one bool with no tracing on any arm: ready, database-degraded 503, legacy 404 then healthy, refused redirect, and budget exhaustion. From the logs an operator cannot tell a peer whose database is down from one that is unreachable, which is the distinction this PR exists to draw. The bootstrap announce in the same function logs both its error and its timeout arm, so this is a gap against the file's own habit. The 503 arm and the legacy-404 downgrade are the two worth recording; the downgrade also gives you the list of peers still on the old build, which is what tells you when the fallback can be dropped.

  • [P3] Reconsider the anonymous ping route writing the federation gate
    crates/gitlawb-node/src/api/peers.rs:444, route wired at crates/gitlawb-node/src/server.rs:292
    peer_read_routes carries no auth layer and no per-IP brake, while sync_trigger_routes and peer_write_routes in the same file each carry one with a comment explaining that outbound fan-out needs it. This handler now drives a database-backed probe on the target and writes the flag that gates federation, from a single unauthenticated sample. Same lineage as the finding above, and the cheaper fix is probably to report the probe result without calling mark_peer_ping, leaving the flag owned solely by the gossip loop.

Not asks, just recording them

The probe URL is built by string concatenation, so a peer that announces https://host/?x=1 gets probed at / rather than /ready. I ran it: the /ready mock is never requested, / answers 200, and the peer reads ready even though /ready would have said 503. is_public_http_url validates scheme and host only, and upsert_peer stores the URL verbatim, so that shape survives announce. It is the same concatenation the pre-PR code used, and a hostile peer controls its own answer regardless, so it only really bites an honestly misconfigured operator. Url::join would be the tidy follow-up, separate from this PR.

README.md:276 and docs/OSS-READINESS-AUDIT.md:69 still list /health alone. Both predate this change and neither claims to state the peer contract, so I am not asking you to touch them here.

@github-actions github-actions Bot removed the needs-tests Source changed without accompanying tests (advisory) label Jul 27, 2026

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

🧹 Nitpick comments (1)
crates/gitlawb-node/src/main.rs (1)

941-962: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

failed_once never sheds entries for peers that disappear.

A DID is removed from the set only on a readiness success. Peers pruned from the peers table (or permanently down) leave their DID behind for the process lifetime, and DIDs are announce-influenceable. Retaining against the current snapshot each tick keeps the set bounded.

♻️ Prune the failure set against the current peer list
                 let peers = match state.db.list_peers().await {
                     Ok(p) => p,
                     Err(_) => continue,
                 };
+                // Drop bookkeeping for peers that no longer exist so the set
+                // cannot grow unbounded over the process lifetime.
+                let current: HashSet<&str> = peers.iter().map(|p| p.did.as_str()).collect();
+                failed_once.retain(|did: &String| current.contains(did.as_str()));
                 for peer in peers {
🤖 Prompt for 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.

In `@crates/gitlawb-node/src/main.rs` around lines 941 - 962, Update the peer
polling loop around failed_once and the list_peers result to prune failure
entries against the current peer snapshot on every tick. Retain only DIDs
present in peers, so removed or permanently unavailable peers cannot accumulate
in the HashSet while preserving the existing readiness update behavior.
🤖 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.

Nitpick comments:
In `@crates/gitlawb-node/src/main.rs`:
- Around line 941-962: Update the peer polling loop around failed_once and the
list_peers result to prune failure entries against the current peer snapshot on
every tick. Retain only DIDs present in peers, so removed or permanently
unavailable peers cannot accumulate in the HashSet while preserving the existing
readiness update behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 80a159f2-3246-4a44-ac4e-42951e15b196

📥 Commits

Reviewing files that changed from the base of the PR and between a1072e8 and 9ace2c6.

📒 Files selected for processing (2)
  • crates/gitlawb-node/src/api/peers.rs
  • crates/gitlawb-node/src/main.rs

@beardthelion
beardthelion dismissed their stale review July 28, 2026 21:17

Superseded: re-reviewed the new head (e8a000c).

@beardthelion beardthelion left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Both of my round-three asks landed, and both are now pinned by a test that fails without them. I re-ran the mutation matrix on this head rather than carrying last round's results forward: pointing the probe at /health reddens four tests, widening the fallback trigger past 404 reddens one, giving the fallback a fresh budget reddens one, flipping the catch-all to true reddens two, and letting the client follow redirects reddens two. Re-adding mark_peer_ping to ping_peer, or putting that handler back on an inline /health probe, each redden the new handler test.

The new hysteresis is the exception, and it is the one thing I want before merge.

Findings

  • [P2] Pin the gossip loop's use of the hysteresis, not just the helper
    crates/gitlawb-node/src/main.rs:956
    I replaced the whole match peer_ping_db_update(...) block with the pre-PR let _ = state.db.mark_peer_ping(&peer.did, ok).await;, which deletes the feature outright, and everything stayed green: 8 tests in gossip_ssrf_tests, 20 in api::peers::tests. The unit test covers the pure function and nothing covers its only call site, so this round's work is removable without CI noticing. Same shape as the manual-ping hole from last round, which you closed well. Deleting the failed_once.retain prune is green too, so one test can cover both. I built the fix before asking for it: extract the tick body into async fn gossip_ping_round(db: &crate::db::Db, client: &reqwest::Client, failed_once: &mut HashSet<String>, peers: Vec<crate::db::PeerRecord>), call it from the select! arm, and drive it from a #[sqlx::test] against a mockito peer answering /ready 503: after round one last_ping_ok is still TRUE, after round two it is FALSE. That is green as written and RED once the hysteresis inside the extracted function is deleted. Seed the peer with a raw INSERT the way your manual_ping... test does; upsert_peer rejects a loopback http_url and mockito binds on 127.0.0.1.

  • [P3] Give the gossip interval MissedTickBehavior::Delay
    crates/gitlawb-node/src/main.rs:940
    The hysteresis is counted in ticks, but its value is wall-clock: the second sample is meant to be 300s after the first. tokio::time::interval defaults to MissedTickBehavior::Burst in the pinned tokio 1.50, so a sweep that overruns the period is followed by catch-up ticks and the two samples can land seconds apart, which is the mass-failure case the guard exists for. A sequential sweep at up to 10s per peer needs roughly 30 unreachable peers to get there. Adding interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay); after the constructor is the whole change and it compiles clean. Worst case today is the guard degrading to the old behavior rather than doing something wrong, hence P3.

Not asks, recorded

  • The anonymous ping route still has no per-IP brake, and /ready costs the target a database query where /health did not. I checked the delta, not the state: the pre-PR handler made the same unauthenticated outbound request and additionally wrote last_ping_ok, and the probe now sits inside a single 10s budget covering both legs. This PR moves that surface in the right direction; the brake is a separate question and not something I want in this PR.
  • last_seen is no longer refreshed on a first-failure tick, since that tick skips the write. Every reader is display or ordering and none gates on freshness. No action.
  • A peer alternating ready/unready is never persisted unreachable, because one success clears the set. That is the tradeoff behind the invariant I gave last round: survive one transient failure, drop out on a sustained one. Settled as intended, not open.

Scope is closed after this. The two commits since my last round have never been through CI: PR Checks is awaiting approval on both, and the last green run was a1072e8, before the hysteresis and the prune.

@MikeTomlin19

Copy link
Copy Markdown
Author

Addressed the final review findings in 32e4578:

  • Extracted one periodic sampling pass into gossip_ping_round.
  • Added a PostgreSQL-backed test that drives a mock /ready 503 peer through the real round function: the first round preserves last_ping_ok = TRUE, the second persists FALSE, and removed peers are pruned from the bounded failure set.
  • Set the five-minute interval's missed-tick behavior to Delay.

Validation:

  • cargo test --workspace against PostgreSQL 16 — passed
  • cargo fmt --all -- --check — passed
  • cargo clippy --workspace --all-targets -- -D warnings — passed

@beardthelion
beardthelion dismissed their stale review July 29, 2026 01:29

Superseded: both round-four asks landed and are now pinned by tests that fail without them. Re-reviewing head 32e4578.

@beardthelion beardthelion left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approving. The premise is pinned now: pointing the probe back at /health reddens six tests across both modules, and reverting gossip_ping_round's match to the pre-PR let _ = db.mark_peer_ping(&peer.did, ok).await; fails gossip_ping_round_requires_two_failures_before_persisting_unreachable. That was my round-four finding and it is closed by execution rather than by reading the new test.

I re-ran the whole matrix on this head instead of carrying earlier results forward. Deleting the hysteresis in peer_ping_db_update reddens two tests, deleting the failed_once.retain prune reddens one, widening the fallback trigger past 404 reddens two, unbinding the shared /ready+/health deadline reddens one, letting the client follow redirects reddens two, flipping the transport-error arm to true reddens one, and re-adding mark_peer_ping(&did, ok) to the manual handler or putting it back on an inline /health probe each redden the handler test.

CI is green on this head across all nine jobs. Your note about the MSRV lane failing on the AWS crate drift is stale: the base is current with main and MSRV (Rust 1.91) passes.

Findings

None blocking.

Recorded, not asks

  • I overstated one thing last round. I wrote that re-adding mark_peer_ping to ping_peer reddens the handler test. That holds for the faithful revert, which writes the probe result: mark_peer_ping(&did, ok) with /ready at 503 writes false and the test fails. It does not hold for a write of a hardcoded true, which stays green because the test seeds last_ping_ok = TRUE and asserts the same value. mark_peer_ping also stamps last_seen (db/mod.rs:2106), so asserting last_seen is byte-identical would pin the no-write property in both directions. Not asking for it.
  • gossip_task has no test, so the cross-tick lifetime of failed_once is unpinned: moving let mut failed_once (main.rs:942) inside the interval.tick() arm turns the hysteresis into a no-op and all 29 tests stay green, as does deleting the MissedTickBehavior::Delay line. This is the next layer of the shape I asked about last round, and pinning it needs the loop extracted the way the round was. Not asking for that here: the feature is pinned at the pure function and at the round, and the residue is loop scaffolding.
  • Dropping the Some(true) arm's write leaves all 29 green. A regression there strands a peer as permanently unreachable with no operator path back, since the manual ping is read-only by design now. Recording it, not asking.
  • The two-tick guard is spaced in ticks but argued in wall-clock. Probes are serial at up to 10s each and list_peers orders by last_seen DESC (db/mod.rs:2118), so a peer probed last in one round and first in the next can have its samples land seconds apart. That is traced from the code rather than measured. It takes roughly 30 unreachable peers to get there and the cost is one peer missing from /api/v1/repos/federated for a cycle, so I am leaving it. Keying first-failure on an Instant would make it wall-clock by construction if it ever bites.
  • peer_read_routes (server.rs:290) still carries no auth layer and no per-IP brake, unlike sync_trigger_routes and peer_write_routes in the same file. What changes here is the shape of the probe, not its cost to us: I checked the pre-PR handler and it already made one outbound request under the same 10s client timeout and additionally wrote mark_peer_ping, so our own per-request DB work goes down. What is new is that a peer answering 404 draws a second outbound leg, and the probe lands on /ready, which runs a query on the target where /health (server.rs:497) ran none. Filing that separately rather than growing this PR.
  • Not yours: upsert_peer (db/mod.rs:2093) rewrites http_url on conflict and leaves last_ping_ok alone, and announce accepts unsigned bodies while require_signed_peer_writes defaults false, so a repointed peer URL inherits reachable=true. Pre-existing, I will file it.

Scope is closed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

crate:node gitlawb-node — the serving node and REST API kind:bug Defect fix — wrong or unsafe behavior subsystem:peers Peer announce, discovery, and registry

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Peer liveness ping targets /health (constant 200), so a mid-life DB outage doesn't drain peer traffic

3 participants