Skip to content

fix(streams): ephemeral broadcasts over the NOTIFY cap auto-degrade to durable; coalescer flush errors reach ErrorReporter - #394

Merged
mhenrixon merged 4 commits into
mainfrom
issue-391-ephemeral-payload-cap
Aug 4, 2026
Merged

fix(streams): ephemeral broadcasts over the NOTIFY cap auto-degrade to durable; coalescer flush errors reach ErrorReporter#394
mhenrixon merged 4 commits into
mainfrom
issue-391-ephemeral-payload-cap

Conversation

@mhenrixon

@mhenrixon mhenrixon commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Summary

Production repro (pgbus 0.13.x, streams_default_broadcast_mode left at its :ephemeral default): any rendered-component broadcast over the ~8KB PG NOTIFY payload cap raised a misleading PGMQ::Errors::ConnectionError on the sync path — and on the coalesce: path the raise happened in the coalescer's flush thread, reaching no caller, no ErrorReporter, no log. Small frames delivered, big frames vanished.

Three changes:

  1. Auto-fallbackStream#broadcast (lib/pgbus/streams.rb) measures the wrapped JSON before the NOTIFY. An over-budget frame publishes durably instead: payload stored in PGMQ, the queue's insert trigger fires the NOTIFY as a bare wake on the same pgmq.q_<name>.INSERT channel the subscriber already LISTENs on — delivery preserved on both the sync and coalesced paths, zero dispatcher changes needed. Warn-logged and instrumented (pgbus.stream.broadcast with ephemeral_fallback: true). The JSON is generated once and passed pre-serialized to notify_stream, so the size check adds no allocation to the hot path.
  2. Typed publish-time validationClient#notify_stream (lib/pgbus/client/notify_stream.rb) raises Pgbus::Streams::PayloadTooLarge (a Pgbus::Error) at the call site for direct callers, naming the stream, byte count, cap (NOTIFY_PAYLOAD_LIMIT_BYTES = 7999), and the durable-mode escape hatch — instead of the driver's "connection error" that sent diagnosis the wrong way.
  3. Coalescer visibilityCoalescer#flush_key (lib/pgbus/streams/coalescer.rb) routes any flush error through ErrorReporter with stream/target context (same report-don't-log reasoning as Dedicated LISTEN connections (streamer, worker NotifyListener) fail under connection_guc_mode = :session — invalid connection option "variables" #352); the key stays usable for the next window.

CHANGELOG gains the fix entry plus a ⚠️ upgrade callout: the :ephemeral default is a behavior change for installs broadcasting rendered components — pin :durable (right for turbo-stream UI anyway; since-id replay needs the archive) or rely on the fallback for what stays ephemeral.

Closes #391

Test plan

  • bundle exec rspec spec/pgbus/streams/ spec/pgbus/client/ spec/pgbus/web/streamer/ — 742 examples, 0 failures
  • Full suite: only the 2 pre-existing i18n baseline failures
  • bundle exec rubocop clean on all changed files
  • Repro from the issue: Pgbus.stream("probe").broadcast("<div>#{"x" * 9000}</div>", target: "t") now delivers (durable fallback) on both sync and coalesce: true paths

Deviations & judgment calls

  • User-confirmed: oversized ephemeral frames auto-fallback to a durable publish rather than raising; the typed PayloadTooLarge is kept as a guard for direct Client#notify_stream callers.
  • The durable fallback stays synchronous — no after_commit deferral — matching the fire-and-forget timing of the ephemeral path it replaces (pg_notify runs on the PGMQ pool connection, outside the request's AR transaction, so ephemeral never deferred either).
  • The fallback returns the durable msg_id where ephemeral broadcasts return nil — strictly more information, matches broadcast's documented contract.
  • Stream#broadcast_ephemeral now passes the pre-serialized JSON string to notify_stream (which already accepted strings) — one JSON.generate per broadcast instead of two; two existing spec expectations updated from hash to JSON-string args.
  • ErrorReporter.report lives in Coalescer#flush_key (the background-thread boundary), not only the streams flush lambda, so any injected flush gets APM visibility.
  • The "0.13 notes" the issue references live in CHANGELOG's [Unreleased] block (no ## [0.13.x] headers exist), so the ⚠️ callout was added there rather than to a released section.
  • Docs site (docs/) not touched: it never documented the payload cap or mode default; the issue asked for CHANGELOG/upgrade-guide coverage, which the CHANGELOG entry carries.
  • NOTIFY_PAYLOAD_LIMIT_BYTES = 7999: Postgres rejects payloads of 8000 bytes or more, so 7999 is the largest deliverable payload.

Summary by CodeRabbit

  • Bug Fixes

    • Oversized ephemeral broadcasts now automatically use durable delivery instead of failing.
    • Direct notifications exceeding the 7,999-byte limit now return a clear, typed error.
    • Coalesced broadcast failures are reported without disrupting scheduled processing.
    • Ephemeral coalesced broadcasts inside transactions are delivered immediately.
  • Reliability

    • Added warnings and instrumentation for durable-delivery fallback.
    • Preserved delivery metadata and returned durable message identifiers for oversized broadcasts.

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 437a174d-c08c-4bd4-9e1d-77b317e3e66b

📥 Commits

Reviewing files that changed from the base of the PR and between 2286f3f and 593000c.

📒 Files selected for processing (3)
  • spec/pgbus/streams/coalescer_spec.rb
  • spec/pgbus/streams/ephemeral_overflow_spec.rb
  • spec/pgbus/streams_spec.rb

📝 Walkthrough

Walkthrough

Ephemeral broadcasts validate serialized NOTIFY payloads, use durable PGMQ delivery when oversized, and return durable message IDs. Direct oversized notifications raise a typed error. Coalescer flush failures are reported through ErrorReporter.

Changes

Ephemeral delivery safeguards

Layer / File(s) Summary
NOTIFY payload validation
lib/pgbus/streams.rb, lib/pgbus/client/notify_stream.rb, spec/pgbus/client/notify_stream_spec.rb
Adds the 7,999-byte limit and PayloadTooLarge error. Validation measures serialized payload bytes before pg_notify.
Oversized broadcast fallback
lib/pgbus/streams.rb, spec/pgbus/streams/ephemeral_broadcast_spec.rb, spec/pgbus/streams/ephemeral_overflow_spec.rb, CHANGELOG.md
Serializes ephemeral payloads once. Oversized payloads use durable PGMQ delivery with queue initialization, warning logging, instrumentation, metadata propagation, and returned message IDs.
Coalescer flush error reporting
lib/pgbus/streams/coalescer.rb, spec/pgbus/streams/coalescer_spec.rb, spec/pgbus/streams_spec.rb
Reports flush errors through ErrorReporter, preserves subsequent coalescer windows, and verifies immediate submission inside transactions.

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

Sequence Diagram(s)

sequenceDiagram
  participant Stream
  participant NotifyStream
  participant PGMQ
  participant ErrorReporter
  Stream->>NotifyStream: Send serialized ephemeral payload
  NotifyStream-->>Stream: Return success or PayloadTooLarge
  Stream->>PGMQ: Ensure queue and publish oversized payload
  PGMQ-->>Stream: Return durable message ID
  Stream->>ErrorReporter: Report coalescer flush failures with context
Loading

Possibly related PRs

Suggested labels: bug

Poem

A rabbit checks the payload size,
Then sends large streams where storage lies.
Small notes pass through Notify’s gate,
Flush errors report instead of wait.
Durable messages return an ID.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 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: durable fallback for oversized ephemeral broadcasts and ErrorReporter reporting for coalescer flush errors.
Linked Issues check ✅ Passed The changes satisfy issue #391 by adding durable fallback, typed payload errors, ErrorReporter reporting, regression tests, and changelog documentation.
Out of Scope Changes check ✅ Passed All code and test changes directly support the requirements in issue #391, with no unrelated scope identified.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-391-ephemeral-payload-cap

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

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

Actionable comments posted: 1

🤖 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.

Inline comments:
In `@CHANGELOG.md`:
- Line 5: Correct the CHANGELOG description of the PayloadTooLarge threshold to
state that 7,999 bytes is the maximum accepted payload, with the exception
raised only when the payload exceeds that limit.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 14d1a597-0ac5-48f0-b2a5-8ec006b88013

📥 Commits

Reviewing files that changed from the base of the PR and between b1f5950 and 6165974.

📒 Files selected for processing (8)
  • CHANGELOG.md
  • lib/pgbus/client/notify_stream.rb
  • lib/pgbus/streams.rb
  • lib/pgbus/streams/coalescer.rb
  • spec/pgbus/client/notify_stream_spec.rb
  • spec/pgbus/streams/coalescer_spec.rb
  • spec/pgbus/streams/ephemeral_broadcast_spec.rb
  • spec/pgbus/streams/ephemeral_overflow_spec.rb

Comment thread CHANGELOG.md Outdated

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

Actionable comments posted: 2

🤖 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.

Inline comments:
In `@CHANGELOG.md`:
- Line 5: Update the upgrade note in the changelog to limit the durable-mode
recommendation to Turbo Stream UI that requires since-id replay or archived
messages. Remove the unconditional claim that durable mode is appropriate for
all Turbo Stream UI, while preserving the existing configuration examples and
auto-fallback guidance.
- Line 5: Update the changelog’s fallback observability wording to state that
oversized ephemeral frames use durable fallback on both synchronous and
coalesced broadcast paths, with warning logs and instrumentation; only direct
Client#notify_stream calls raise PayloadTooLarge. Also retain that coalescer
flush failures are reported through ErrorReporter, rather than describing the
coalescer fallback as silent.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: eefa0818-5dee-4338-afdd-877a72c8a9b0

📥 Commits

Reviewing files that changed from the base of the PR and between f52c7b9 and ef12b0e.

📒 Files selected for processing (1)
  • CHANGELOG.md

Comment thread CHANGELOG.md
@mhenrixon mhenrixon self-assigned this Aug 4, 2026
@mhenrixon mhenrixon added streaming Changes related to streaming stability Process-model and runtime stability labels Aug 4, 2026
…o durable; coalescer flush errors reach ErrorReporter

Ephemeral frames ride the PG NOTIFY payload, capped below 8000 bytes.
An oversized frame raised a misleading PGMQ ConnectionError on the sync
path and vanished silently inside the coalescer's flush thread. Now:
Stream#broadcast measures the wrapped JSON and publishes over-budget
frames durably (PGMQ payload, trigger NOTIFY as wake — same channel,
delivery preserved); Client#notify_stream raises a typed
Pgbus::Streams::PayloadTooLarge for direct callers; the coalescer routes
flush errors through ErrorReporter so APM sees them.

- notify_stream_spec: typed raise, byte-not-char measurement, at-limit boundary, no NOTIFY attempted
- ephemeral_overflow_spec: durable fallback (queue ensured, msg_id returned, warn log, instrumentation), boundary incl. metadata overflow, small-frame NOTIFY path
- coalescer_spec: raising flush reports via ErrorReporter with stream/target context, no re-raise, key reusable next window

- [x] bundle exec rubocop passes
- [x] bundle exec rspec passes (only pre-existing i18n baseline failures)

Refs #391
…sced frames

The oversized-frame durable fallback runs at flush, on the coalescer
thread, where the request's transaction is structurally invisible — so
gating must be (and is) decided at submit time by the requested mode.
Pins the ephemeral half: fire-and-forget submits immediately even inside
an open transaction, so the fallback inherits the ephemeral contract the
caller chose. Refs #391.
@mhenrixon
mhenrixon force-pushed the issue-391-ephemeral-payload-cap branch from ef12b0e to 2286f3f Compare August 4, 2026 20:49
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

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

Actionable comments posted: 4

🤖 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.

Inline comments:
In `@lib/pgbus/streams/coalescer.rb`:
- Around line 76-81: Update the rescue path around the coalescer flush to
persist failed durable frames in pgbus_failed_events before or alongside
ErrorReporter.report. Include sufficient stream identity and buffered payload
context for retry or replay, while preserving ErrorReporter reporting; if an
existing durable failure helper is available, reuse it rather than adding a
parallel mechanism.

In `@spec/pgbus/streams_spec.rb`:
- Around line 221-225: Update the expectation in the “submits an ephemeral
coalesced frame immediately, even inside the transaction” example to require
that coalescer.submit receives durable: false in addition to target: "t". Keep
the existing immediate-submission assertion unchanged.

In `@spec/pgbus/streams/coalescer_spec.rb`:
- Around line 124-132: Update the ErrorReporter expectation in the “routes a
raising flush through ErrorReporter with stream/target context” example to
assert component: "streams.coalescer" alongside the existing stream and target
context, ensuring the complete reporting context is verified.

In `@spec/pgbus/streams/ephemeral_overflow_spec.rb`:
- Around line 52-56: Update the warning assertion in the “warn-logs the fallback
with stream and byte count” example to execute the supplied block and verify the
warning message includes the stream name and serialized byte count, not merely
that Pgbus.logger.warn was called.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: e207e607-8ca0-4d0b-b297-302b6b7a7df3

📥 Commits

Reviewing files that changed from the base of the PR and between 94366ff and 2286f3f.

📒 Files selected for processing (9)
  • CHANGELOG.md
  • lib/pgbus/client/notify_stream.rb
  • lib/pgbus/streams.rb
  • lib/pgbus/streams/coalescer.rb
  • spec/pgbus/client/notify_stream_spec.rb
  • spec/pgbus/streams/coalescer_spec.rb
  • spec/pgbus/streams/ephemeral_broadcast_spec.rb
  • spec/pgbus/streams/ephemeral_overflow_spec.rb
  • spec/pgbus/streams_spec.rb

Comment thread lib/pgbus/streams/coalescer.rb
Comment thread spec/pgbus/streams_spec.rb Outdated
Comment thread spec/pgbus/streams/coalescer_spec.rb
Comment thread spec/pgbus/streams/ephemeral_overflow_spec.rb
- assert durable: false rides the ephemeral coalesced submit
- assert component: "streams.coalescer" in ErrorReporter context
- assert the fallback warning carries stream name and byte count
@mhenrixon
mhenrixon merged commit 422c4c5 into main Aug 4, 2026
24 of 25 checks passed
@mhenrixon
mhenrixon deleted the issue-391-ephemeral-payload-cap branch August 4, 2026 21:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

stability Process-model and runtime stability streaming Changes related to streaming

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Ephemeral broadcast over the PG NOTIFY payload cap fails silently in the coalescer flush thread

1 participant