Skip to content

fix streaming receiver discarding delivered data on child disconnect - #23118

Merged
ktsaou merged 1 commit into
netdata:masterfrom
ktsaou:stream-receiver-drain-on-close
Jul 14, 2026
Merged

fix streaming receiver discarding delivered data on child disconnect#23118
ktsaou merged 1 commit into
netdata:masterfrom
ktsaou:stream-receiver-drain-on-close

Conversation

@ktsaou

@ktsaou ktsaou commented Jul 13, 2026

Copy link
Copy Markdown
Member

Problem

When a streaming child disconnects immediately after writing — a crash, a restart, a short-lived connection, or completing replication and moving on — the parent discards data TCP has already delivered to it.

stream_receive_process_poll_events() removes the receiver on ND_POLL_HUP before processing ND_POLL_READ. Linux epoll (level-triggered in nd-poll.c) reports EPOLLIN|EPOLLRDHUP together from the moment the peer's FIN is queued — regardless of how much delivered data is still unread — so the teardown runs with data pending in the socket buffer, and that data is silently dropped.

Measured with a protocol-level fake child pushing a 3000-row replication burst and closing right after the final REND: only 221 rows were stored. The receiver's own accounting shows exactly one read buffer consumed (bytes_in = chart metadata + 15487 bytes = one PLUGINSD_LINE_MAX read); strace shows the next epoll_wait returning EPOLLIN|EPOLLRDHUP and no further recvfrom on the socket. Bursts smaller than one read buffer lose a timing-dependent amount (zero to everything); anything larger loses deterministically.

Replication heals this loss only when the child reconnects with local retention. Ephemeral children (k8s pods, autoscaled VMs), children running [db] mode = none or with small retention, and setups with replication disabled lose the delivered data permanently — and every child restart leaves a parent-side gap until the reconnect happens.

Fix

Remove the receiver immediately only on ND_POLL_ERROR/ND_POLL_INVALID (real socket errors — on RST the kernel has already dropped the receive queue), or on ND_POLL_HUP without ND_POLL_READ (EOF with nothing readable). When HUP arrives together with READ, run the normal read path: one buffer per poll cycle — the same fairness any connected child gets — while level-triggered polling keeps re-reporting both flags; when the buffer is drained, recv() returns 0 and the existing read-path teardown removes the receiver with the same disconnect reason as before.

No new state, no drain loop, no protocol or configuration change. SSL connections terminate the same way: SSL_ERROR_ZERO_RETURN maps to 0 (clean close) and transport EOF maps to -1 (SSL_ERROR_SYSCALL), so the drain cannot spin.

Validation

End-to-end against a stock daemon, with a fake child speaking the real streaming protocol at a fixed epoch:

  • 30,000-point live BEGIN2/SET2 burst + immediate close: before 723/30000 stored, after 30000/30000.
  • 30,000-point replication (RBEGIN/RSET/REND) burst + immediate close: before 3716/30000, after 30000/30000.
  • Byte ledger: bytes_in = metadata + full payload after the fix (210245 = 178 + 210067), for every iteration.
  • Round-trip regression suite unchanged: live and replication ingestion byte-exact (values, gaps, anomaly bits, annotations), multiple children per context, chart labels, restart/journal-v2 replay.
  • Teardown robustness: a child disconnecting mid-replication-dialogue (the parent's queued REPLAY_CHART reply hits a dead socket) leaves the daemon healthy and serving; a 30-cycle connect/burst/immediate-close soak shows no crash and no stuck receivers.

Notes

  • RST with buffered data still discards — the kernel drops the receive queue on RST; unchanged, unfixable at this layer.
  • The sender-side event loop (stream-sender.c) has the same HUP-before-read ordering; its inbound traffic is parent control messages. Left untouched here pending evidence of impact.

Summary by cubic

Prevents data loss in the stream receiver when a child disconnects right after writing by draining readable data before teardown. We now keep reading on HUP if data is pending, so delivered rows are not dropped.

  • Bug Fixes
    • Only remove the receiver immediately on ND_POLL_ERROR/ND_POLL_INVALID, or on ND_POLL_HUP without ND_POLL_READ.
    • If ND_POLL_HUP arrives with ND_POLL_READ, continue the normal read path until recv() returns 0, then tear down.
    • No new state or loops; protocol and SSL behavior unchanged. Verified full ingestion of burst writes with immediate close.

Written for commit a4e763c. Summary will update on new commits.

Review in cubic

The stream thread handled poll HUP events before pending reads. Linux
epoll (level-triggered) reports EPOLLIN|EPOLLRDHUP together once the
peer's FIN is queued, regardless of how much delivered data remains
unread, so the receiver was removed with data still in the socket
buffer - discarding up to the entire in-flight payload. Measured: 221
of 3000 replicated rows survived a child that disconnected right after
its final write; the rest were silently dropped.

Remove the receiver immediately only on ERROR/INVALID, or on HUP
without READ (EOF with nothing readable). When HUP arrives together
with READ, keep the normal read path: one buffer per cycle, the same
fairness as any connected child, with level-triggered polling
re-reporting both flags until recv() returns 0, when the existing
read-path teardown removes the receiver with the same disconnect
reason as before.

Replication heals such losses only for children that reconnect with
local retention; ephemeral children, children without retention, and
setups with replication disabled lost the delivered data permanently.

@cubic-dev-ai cubic-dev-ai 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.

No issues found across 1 file

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Re-trigger cubic

@sonarqubecloud

Copy link
Copy Markdown

@ktsaou
ktsaou merged commit e745bd9 into netdata:master Jul 14, 2026
155 checks passed
@stelfrag stelfrag mentioned this pull request Jul 14, 2026
ktsaou added a commit to ktsaou/netdata that referenced this pull request Jul 21, 2026
…ngine

A Go test suite that boots a completely stock netdata daemon, feeds it
deterministic fixtures through the real streaming protocol (a fake child
speaking the full handshake, live BEGIN2/SET2 and the replication
dialogue), and verifies query results over HTTP against expectations
computed from the fixture definitions - never from the engine.

The suite is built as a ladder of layers, each pinning one part of the
query contract before the next builds on it:

- layer 0: harness self-test - byte-exact round-trips on the live path,
  the replication path, and across a daemon restart;
- layer 1: tier0 ingestion - the edge-data palette (gap runs, resets,
  anomaly runs, negatives, zeros, non-default update every), the
  storage_number quantization contract as a Go port of pack/unpack, and
  single-point and beyond-retention windows;
- layer 2: tier rollups - tier1/tier2 sum/min/max/count/anomaly-count
  derivation incl. partial and all-gap windows, float32 page rounding,
  and the fact that rollups aggregate original values, not quantized;
- layer 3: time-aggregations - every registry time_group against exact
  Go ports of the grouping arithmetic, incl. options grammars,
  sign-dependent slot walks, running smoothing state and the
  incremental-sum carry;
- layer 4 (parts a+b): tier fetch semantics per family over rollup
  tiers, and the automatic tier selection rule.

MANIFEST.md tracks every case with green/red status: green cases must
pass, red cases must reproduce a known bug and fail loudly when a fix
lands, demanding the flip with the fixing PR. The suite found and
pinned the receiver disconnect data loss (netdata#23118), the forgotten
fresh-host on graceful restart (netdata#23120), and the tier query boundary
absorption (netdata#23127) this way.

The suite is self-contained under tests/query-corpus (go test ./...
with a built netdata binary), uses no external Go dependencies, and
runs in ~3.5 minutes.
ktsaou added a commit to ktsaou/netdata that referenced this pull request Jul 21, 2026
…ngine

A Go test suite that boots a completely stock netdata daemon, feeds it
deterministic fixtures through the real streaming protocol (a fake child
speaking the full handshake, live BEGIN2/SET2 and the replication
dialogue), and verifies query results over HTTP against expectations
computed from the fixture definitions - never from the engine.

The suite is built as a ladder of layers, each pinning one part of the
query contract before the next builds on it:

- layer 0: harness self-test - byte-exact round-trips on the live path,
  the replication path, and across a daemon restart;
- layer 1: tier0 ingestion - the edge-data palette (gap runs, resets,
  anomaly runs, negatives, zeros, non-default update every), the
  storage_number quantization contract as a Go port of pack/unpack, and
  single-point and beyond-retention windows;
- layer 2: tier rollups - tier1/tier2 sum/min/max/count/anomaly-count
  derivation incl. partial and all-gap windows, float32 page rounding,
  and the fact that rollups aggregate original values, not quantized;
- layer 3: time-aggregations - every registry time_group against exact
  Go ports of the grouping arithmetic, incl. options grammars,
  sign-dependent slot walks, running smoothing state and the
  incremental-sum carry;
- layer 4 (parts a+b): tier fetch semantics per family over rollup
  tiers, and the automatic tier selection rule.

MANIFEST.md tracks every case with green/red status: green cases must
pass, red cases must reproduce a known bug and fail loudly when a fix
lands, demanding the flip with the fixing PR. The suite found and
pinned the receiver disconnect data loss (netdata#23118), the forgotten
fresh-host on graceful restart (netdata#23120), and the tier query boundary
absorption (netdata#23127) this way.

The suite is self-contained under tests/query-corpus (go test ./...
with a built netdata binary), uses no external Go dependencies, and
runs in ~3.5 minutes.
ktsaou added a commit to ktsaou/netdata that referenced this pull request Jul 23, 2026
…ngine

A Go test suite that boots a completely stock netdata daemon, feeds it
deterministic fixtures through the real streaming protocol (a fake child
speaking the full handshake, live BEGIN2/SET2 and the replication
dialogue), and verifies query results over HTTP against expectations
computed from the fixture definitions - never from the engine.

The suite is built as a ladder of layers, each pinning one part of the
query contract before the next builds on it:

- layer 0: harness self-test - byte-exact round-trips on the live path,
  the replication path, and across a daemon restart;
- layer 1: tier0 ingestion - the edge-data palette (gap runs, resets,
  anomaly runs, negatives, zeros, non-default update every), the
  storage_number quantization contract as a Go port of pack/unpack, and
  single-point and beyond-retention windows;
- layer 2: tier rollups - tier1/tier2 sum/min/max/count/anomaly-count
  derivation incl. partial and all-gap windows, float32 page rounding,
  and the fact that rollups aggregate original values, not quantized;
- layer 3: time-aggregations - every registry time_group against exact
  Go ports of the grouping arithmetic, incl. options grammars,
  sign-dependent slot walks, running smoothing state and the
  incremental-sum carry;
- layer 4 (parts a+b): tier fetch semantics per family over rollup
  tiers, and the automatic tier selection rule.

MANIFEST.md tracks every case with green/red status: green cases must
pass, red cases must reproduce a known bug and fail loudly when a fix
lands, demanding the flip with the fixing PR. The suite found and
pinned the receiver disconnect data loss (netdata#23118), the forgotten
fresh-host on graceful restart (netdata#23120), and the tier query boundary
absorption (netdata#23127) this way.

The suite is self-contained under tests/query-corpus (go test ./...
with a built netdata binary), uses no external Go dependencies, and
runs in ~3.5 minutes.
ktsaou added a commit to ktsaou/netdata that referenced this pull request Jul 23, 2026
…ngine

A Go test suite that boots a completely stock netdata daemon, feeds it
deterministic fixtures through the real streaming protocol (a fake child
speaking the full handshake, live BEGIN2/SET2 and the replication
dialogue), and verifies query results over HTTP against expectations
computed from the fixture definitions - never from the engine.

The suite is built as a ladder of layers, each pinning one part of the
query contract before the next builds on it:

- layer 0: harness self-test - byte-exact round-trips on the live path,
  the replication path, and across a daemon restart;
- layer 1: tier0 ingestion - the edge-data palette (gap runs, resets,
  anomaly runs, negatives, zeros, non-default update every), the
  storage_number quantization contract as a Go port of pack/unpack, and
  single-point and beyond-retention windows;
- layer 2: tier rollups - tier1/tier2 sum/min/max/count/anomaly-count
  derivation incl. partial and all-gap windows, float32 page rounding,
  and the fact that rollups aggregate original values, not quantized;
- layer 3: time-aggregations - every registry time_group against exact
  Go ports of the grouping arithmetic, incl. options grammars,
  sign-dependent slot walks, running smoothing state and the
  incremental-sum carry;
- layer 4 (parts a+b): tier fetch semantics per family over rollup
  tiers, and the automatic tier selection rule.

MANIFEST.md tracks every case with green/red status: green cases must
pass, red cases must reproduce a known bug and fail loudly when a fix
lands, demanding the flip with the fixing PR. The suite found and
pinned the receiver disconnect data loss (netdata#23118), the forgotten
fresh-host on graceful restart (netdata#23120), and the tier query boundary
absorption (netdata#23127) this way.

The suite is self-contained under tests/query-corpus (go test ./...
with a built netdata binary), uses no external Go dependencies, and
runs in ~3.5 minutes.
ktsaou added a commit to ktsaou/netdata that referenced this pull request Jul 24, 2026
…ngine

A Go test suite that boots a completely stock netdata daemon, feeds it
deterministic fixtures through the real streaming protocol (a fake child
speaking the full handshake, live BEGIN2/SET2 and the replication
dialogue), and verifies query results over HTTP against expectations
computed from the fixture definitions - never from the engine.

The suite is built as a ladder of layers, each pinning one part of the
query contract before the next builds on it:

- layer 0: harness self-test - byte-exact round-trips on the live path,
  the replication path, and across a daemon restart;
- layer 1: tier0 ingestion - the edge-data palette (gap runs, resets,
  anomaly runs, negatives, zeros, non-default update every), the
  storage_number quantization contract as a Go port of pack/unpack, and
  single-point and beyond-retention windows;
- layer 2: tier rollups - tier1/tier2 sum/min/max/count/anomaly-count
  derivation incl. partial and all-gap windows, float32 page rounding,
  and the fact that rollups aggregate original values, not quantized;
- layer 3: time-aggregations - every registry time_group against exact
  Go ports of the grouping arithmetic, incl. options grammars,
  sign-dependent slot walks, running smoothing state and the
  incremental-sum carry;
- layer 4 (parts a+b): tier fetch semantics per family over rollup
  tiers, and the automatic tier selection rule.

MANIFEST.md tracks every case with green/red status: green cases must
pass, red cases must reproduce a known bug and fail loudly when a fix
lands, demanding the flip with the fixing PR. The suite found and
pinned the receiver disconnect data loss (netdata#23118), the forgotten
fresh-host on graceful restart (netdata#23120), and the tier query boundary
absorption (netdata#23127) this way.

The suite is self-contained under tests/query-corpus (go test ./...
with a built netdata binary), uses no external Go dependencies, and
runs in ~3.5 minutes.
ktsaou added a commit to ktsaou/netdata that referenced this pull request Aug 11, 2026
…ngine

A Go test suite that boots a completely stock netdata daemon, feeds it
deterministic fixtures through the real streaming protocol (a fake child
speaking the full handshake, live BEGIN2/SET2 and the replication
dialogue), and verifies query results over HTTP against expectations
computed from the fixture definitions - never from the engine.

The suite is built as a ladder of layers, each pinning one part of the
query contract before the next builds on it:

- layer 0: harness self-test - byte-exact round-trips on the live path,
  the replication path, and across a daemon restart;
- layer 1: tier0 ingestion - the edge-data palette (gap runs, resets,
  anomaly runs, negatives, zeros, non-default update every), the
  storage_number quantization contract as a Go port of pack/unpack, and
  single-point and beyond-retention windows;
- layer 2: tier rollups - tier1/tier2 sum/min/max/count/anomaly-count
  derivation incl. partial and all-gap windows, float32 page rounding,
  and the fact that rollups aggregate original values, not quantized;
- layer 3: time-aggregations - every registry time_group against exact
  Go ports of the grouping arithmetic, incl. options grammars,
  sign-dependent slot walks, running smoothing state and the
  incremental-sum carry;
- layer 4 (parts a+b): tier fetch semantics per family over rollup
  tiers, and the automatic tier selection rule.

MANIFEST.md tracks every case with green/red status: green cases must
pass, red cases must reproduce a known bug and fail loudly when a fix
lands, demanding the flip with the fixing PR. The suite found and
pinned the receiver disconnect data loss (netdata#23118), the forgotten
fresh-host on graceful restart (netdata#23120), and the tier query boundary
absorption (netdata#23127) this way.

The suite is self-contained under tests/query-corpus (go test ./...
with a built netdata binary), uses no external Go dependencies, and
runs in ~3.5 minutes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant