Skip to content

fix(etcp): count progress ahead of a probe as liveness and keep the catchup until the server speaks - #6

Merged
tphakala merged 2 commits into
mainfrom
fix/etcp-liveness-replay-trim
Sep 26, 2026
Merged

tphakala merged 2 commits into
mainfrom
fix/etcp-liveness-replay-trim

Conversation

@tphakala

@tphakala tphakala commented Sep 26, 2026 •

Copy link
Copy Markdown
Owner

Summary

Two reconnect-path fixes in internal/etcp.

Liveness under a slow upload. The watcher took only inbound frames as proof of life, and the KEEP_ALIVE probe queues behind any unsent backlog. On a slow uplink with a silent server, a healthy link was declared dead; each reconnect replayed the backlog and the probe's echo queued behind it again, so an upload could loop without finishing (at 1 KiB/s, 10 of 96 KiB arrived in an hour of fake time). The writer now sends each batch in 4 KiB pieces and signals liveness for a piece written while a probe still waits behind it. The echo cannot come before the probe is sent, and once the send buffer is full a write completes only as the peer acknowledges data. Writes with no probe behind them, the probe itself included, never count, so neither keystrokes nor large packets written into a dead link keep it alive. Two limits remain and are documented on Dialer.KeepAlive: an uplink under about 400 B/s, and data already in the kernel send buffer, which drains out of sight.

Replay trim after recover. recover trimmed the ring to ReplayLimit right after writing our catchup, but the server counts a catchup only once it has decoded the whole message. A link lost in between made the next recover fail with ErrReplayExceeded. The ring now holds everything from the sequence the peer acknowledged until the server's first frame on the new link, which upstream sends only after decoding our catchup (BackedWriter::write waits on the recover mutex that Connection::recover holds until then; src/base/BackedWriter.cpp:17-18 and src/base/Connection.cpp:109,134-142 at et-v7.0.0). The hold is capped at twice ReplayLimit of written bytes, beyond which a later recover fails with ErrReplayExceeded as before. The ReplayLimit doc now states the resulting bound (about three times ReplayLimit plus one packet).

Test plan

  • New tests: TestCatchupKeptUntilServerSpeaks, TestServerPacketReleasesCatchup, TestRingTrimHoldCeiling, TestLivenessSlowUploadKeepsLink, TestLivenessTypingDoesNotHideDeadLink, TestLivenessLargeWritesDoNotHideDeadLink. Each was seen failing before the fix or with the production line it pins removed, including each branch of probeBehind.
  • go test -race ./..., and go test ./internal/etcp -race -count=20 with no flakes.
  • golangci-lint run on linux and windows, go fix -diff, js/wasm and wasip1 vet, e2e vet.
  • e2e suite against a local etserver.
  • BenchmarkDrainBacklog against main: no significant change (benchstat p=0.57).

Summary by CodeRabbit

  • Bug Fixes
    • Improved connection liveness detection so ongoing upload progress can keep a connection active, while unanswered probes can still trigger a reconnect.
    • Preserved unacknowledged data for another recovery attempt until the server sends data on the reconnected link.

…atchup until the server speaks

Liveness: the watcher took only inbound frames as proof of life, and the
probe queues behind any unsent backlog. On a slow uplink with the server
silent, a healthy link was declared dead; each reconnect replayed the
backlog and the probe answer queued behind it again, so the upload could
loop without finishing. At 1 KiB/s, 10 of 96 KiB arrived in an hour of fake
time. The writer now sends each batch in 4 KiB pieces and signals liveness
for a piece written while a probe still waits behind it: the echo cannot
come before the probe is sent, and once the send buffer is full a write
completes only as the peer acknowledges data. Writes with no probe behind
them, the probe itself included, never count, so neither keystrokes nor
large packets written into a dead link keep it alive. Two limits remain and
are documented on Dialer.KeepAlive: an uplink under about 400 B/s, and data
already in the kernel send buffer, which drains out of sight.

Replay trim: recover trimmed the ring to ReplayLimit right after writing
our catchup, but the server counts a catchup only once it has decoded the
whole message. A link lost in between made the next recover fail with
ErrReplayExceeded. The ring now holds everything from the sequence the
peer acknowledged until the server's first frame on the new link, which
upstream sends only after decoding our catchup (BackedWriter::write waits
on the recover mutex, src/base/BackedWriter.cpp:17-18 and
src/base/Connection.cpp:109,134-142 at et-v7.0.0). The hold is capped at
twice ReplayLimit of written bytes, beyond which the next recover fails
with ErrReplayExceeded as before.

BenchmarkDrainBacklog shows no significant change against main
(benchstat p=0.57).

Verified: go test -race ./... (etcp also -count=20), golangci-lint (linux
and windows), go fix -diff, wasm vet, and the e2e suite against etserver on
localhost. Each new test was checked by removing the production line it
pins and watching it fail.
Copilot AI lite review requested due to automatic review settings September 26, 2026 06:52
@coderabbitai

coderabbitai Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Warning

Review limit reached

  • Ask an admin to enable usage-based reviews

Open in CodeRabbit

Reviews can continue after your included limit without a manual trigger. An admin must approve usage-based billing.

Next included review available in 51 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available. Your 61 included PR review attempts over the past 7 days set your current allowance at 1 review per hour.

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Essentials

Run ID: 7e985ce0-68fa-47c0-8403-2e9be7878da0

📥 Commits

Reviewing files that changed from the base of the PR and between c98e7ba and 753ce52.

📒 Files selected for processing (2)
  • internal/etcp/link.go
  • internal/etcp/throttle_test.go

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Essentials

Run ID: 88038b69-01d0-4c5a-bda2-e06e08688b04

📥 Commits

Reviewing files that changed from the base of the PR and between 9b57382 and c98e7ba.

📒 Files selected for processing (11)
  • internal/etcp/dialer.go
  • internal/etcp/drain_bench_test.go
  • internal/etcp/helpers_test.go
  • internal/etcp/link.go
  • internal/etcp/link_internal_test.go
  • internal/etcp/liveness_test.go
  • internal/etcp/recover.go
  • internal/etcp/recover_test.go
  • internal/etcp/ring.go
  • internal/etcp/ring_test.go
  • internal/etcp/throttle_test.go

Included review availability: This review used your included allowance. 0 included reviews remain after this review. Your included PR review attempts over the past 7 days set your current allowance at 1 review per hour.


Walkthrough

The link writer now reports progress in bounded pieces while a keepalive probe waits behind queued data. Recovery now retains unacknowledged catchup entries until the server sends a frame on the new link, then trims the ring.

Changes

ETCP Link Liveness and Recovery

Layer / File(s) Summary
Writer progress and liveness
internal/etcp/dialer.go, internal/etcp/link.go, internal/etcp/liveness_test.go, internal/etcp/link_internal_test.go, internal/etcp/drain_bench_test.go, internal/etcp/throttle_test.go
writeLoop sends batches in 4 KiB pieces and signals activity when a probe remains behind written progress. Liveness tests cover periodic writes, large writes, and slow uploads. The throttled test connection accepts a configurable per-KiB delay.
Recovery hold and ring trimming
internal/etcp/ring.go, internal/etcp/recover.go, internal/etcp/link.go, internal/etcp/helpers_test.go, internal/etcp/recover_test.go, internal/etcp/ring_test.go, internal/etcp/dialer.go
The ring retains held entries until the hold is released or written bytes exceed twice the limit. The first peer frame releases the hold and trims the ring. Recovery tests check catchup retention across reconnects and trimming after a server packet.

Priority: ⬇️ Low

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Bug fix

Merge Risk: ⚪ Minimal · up to c98e7

The reviewed reconnect changes have no established issue requiring resolution before merge.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 77.78% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 18 functions across 11 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes both primary changes: treating progress ahead of a probe as liveness and retaining catchup data until the server speaks.
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.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Sep 26, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot review overview

🟢 Approval recommended

The only noted issue is a non-blocking comment wording nit.

Review effort: Lite
Findings: 1 Low severity

Open (1)
What changed in this PR

Fixes reconnect reliability by recognizing upload progress as liveness and retaining replay data until the server responds.

Changes:

  • Adds chunked, probe-aware liveness signaling.
  • Retains catchup data with bounded ring growth.
  • Adds recovery, liveness, throttling, and benchmark coverage.
  • Updates keep-alive and replay-limit documentation.
File Description
internal/​etcp/​throttle_test.go Adds throttling test support; includes a minor comment-wording nit.
internal/​etcp/​ring.go Implements bounded hold-aware trimming.
internal/​etcp/​ring_test.go Tests ring retention limits.
internal/​etcp/​recover.go Preserves catchup data until the server speaks.
internal/​etcp/​recover_test.go Tests recovery and replay retention.
internal/​etcp/​liveness_test.go Tests liveness during slow and dead-link uploads.
internal/​etcp/​link.go Implements chunked writes and probe-aware liveness.
internal/​etcp/​link_internal_test.go Adds internal link behavior coverage.
internal/​etcp/​helpers_test.go Updates test helpers.
internal/​etcp/​drain_bench_test.go Updates backlog-draining benchmarks.
internal/​etcp/​dialer.go Documents updated liveness and replay bounds.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread internal/etcp/throttle_test.go Outdated
@tphakala
tphakala merged commit 4b2655c into main Sep 26, 2026
18 checks passed
@tphakala
tphakala deleted the fix/etcp-liveness-replay-trim branch September 26, 2026 07:01
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.

2 participants