Skip to content

Forward-port #898 and #899 (WatchAddRejected) to master - #910

Merged
bkeroack merged 2 commits into
masterfrom
fix/forward-port-watch-add-rejected
Oct 6, 2026
Merged

bkeroack merged 2 commits into
masterfrom
fix/forward-port-watch-add-rejected

Conversation

@bkeroack

@bkeroack bkeroack commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

Brings two fixes merged into release/0.6 for 0.6.1 back to master, one commit each, cherry-picked with -x:

No master commit since the release/0.6 fork touches the code or docs these change, so both apply unchanged. Apart from the release notes, the patch is identical to the one on release/0.6.

Release notes

#899's CHANGELOG.md bullet and docs/release-notes/0.6.1-pre.md stay on release/0.6. They describe 0.6.1 and will come to master with the 0.6.1 cut, as #901 brought 0.6.0. Nothing is added to 0.7.0-pre.md. (#901 expected #899's notes there; that was before #899 moved to 0.6.1.)

The Operator Manual on GitHub Pages builds from master, so the corrected streaming chapters go live when this merges.

Merging

"Rebase and merge" keeps the two commits separate, each with its cherry picked from line.

Verified

Ran locally on this branch: cargo clippy --all-targets --all-features -- -D warnings, cargo clippy -p satd-events-client --no-default-features --all-targets -- -D warnings, the satd-events tests (182), the satd-events-client tests with all features (154) and with none (134), the streaming::, parity:: and sdk:: e2e tests (66, --features e2e), clients/go/lint.sh, go test ./..., clients/go/gen.sh with no diff in eventspb, and mdbook build docs/manual. The same set passed on release/0.6 after both merges, which has no push CI.

🤖 Generated with Claude Code

bkeroack and others added 2 commits October 6, 2026 10:02
- eventsgrpcmaxsubscriptions was described as the watch-set size per
  gRPC stream. It caps concurrent Subscribe and Watch streams across all
  connections, and eventsgrpcmaxconns caps connections, not streams. The
  config reference and the GrpcLimits doc comments named only Subscribe.
- Both SDKs said the server silently drops an over-rate SetCursor. Since
  #441 it answers in-band with CursorRejected RATE_LIMITED.
- Two proto comments still said silent-payment matching and BlockTweaks
  emit "land in a later change"; both shipped. The Go bindings are
  regenerated for the comment change.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit e019c7d)
* events: report a refused watch add in-band with WatchAddRejected

An incremental Add* that the watch-set refused (over quota, over the
per-add rate limit, over a per-connection cap, missing stream:watch, or
malformed as a whole) was logged on the node and dropped. The client got
no signal and could believe it was watching addresses it was not.

The WatchSet add paths now return the refusal and the net-new items it
covered. Both carriers hand it to the outbound task over a blocking
channel, like the SetWatchSet result, and emit a WatchAddRejected event
(NodeEvent tag 32) on gRPC and its JSON mirror on WS. The event names
the kind, the reason with the numbers behind it, and the refused items;
a refused descriptor slide says whether the earlier window is kept.

The WS per-connection entry cap moves into the watch-set
(WatchSet::with_entry_cap), so a capped add is reported like any other
and an add that only re-asserts held items is no longer shed at the cap.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* SDKs surface WatchAddRejected and drop refused items from the mirror

Both SDKs decode the new event (Rust Event::WatchAddRejected, Go
*WatchAddRejected) with its kind, reason, numbers and items. When it
arrives, ResilientWatch drops the refused items from its mirror so a
reconnect re-registers only what the node holds; a refused descriptor
slide falls back to the window the node kept. The event is still
handed to the caller.

The parity harness renders the event the same way from both SDKs, and
two e2e tests drive it over the real binary: an over-quota gRPC add and
a WS add over the per-connection entry cap.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs: describe WatchAddRejected; RESOURCE_EXHAUSTED is only at stream open

The streaming spec gains §7.3.2 on the new event; the quota section,
the operator manual (streaming, authentication, Rust and Go SDK
chapters) and both SDKs' QuotaExhausted docs stop promising
RESOURCE_EXHAUSTED / 429 for an over-quota add and point at the event.
CHANGELOG and the release notes describe the fix.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* satd-events-client: build the rejected descriptor with then_some

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* SDKs re-send a rate-limited watch add instead of dropping it

A rate limit is transient, so ResilientWatch keeps the items of a
RATE_LIMITED WatchAddRejected in its mirror and re-sends them after the
node's retry_after_secs or the backoff delay, whichever is later,
absorbing the event. Each item has its own retry budget (the backoff's
max_retries); once one is spent the add is handled like any other
refusal: dropped from the mirror and handed to the caller. The re-send
is built from the mirror at send time, so a removal since the refusal
wins. A reconnect re-sends the whole mirror and resets the budgets.

Rust drives the retries from next(), racing the stream read against the
earliest deadline (EventStream::message is cancel-safe). Go schedules a
timer that re-checks it is still on the same stream and sends under mu,
as caller edits do.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* e2e: ResilientWatch re-sends a watch add the node throttled

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* SP label re-asserts are free; a refused slide falls back past every refusal

Review round 1:

- A silent-payment add that only re-asserted held targets (a label
  update) spent a rate token and could come back RATE_LIMITED naming no
  items. Re-asserted targets are now free and always applied, like a
  re-asserted script's floor in add_items_priced; only net-new targets
  are charged, and a refusal names only them. Their label updates apply
  even when the net-new targets are refused.
- ResilientWatch kept one previous window per descriptor, so two refused
  slides in flight restored the first refused window. Both SDKs now keep
  a short history of earlier windows: a refused window leaves it, and a
  refused latest window falls back to the latest earlier one that was
  not refused.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* clients/go: regenerate bindings for the WatchAddRejected comment

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs: open the 0.6.1 notes with the WatchAddRejected fix

The fix ships in 0.6.1 from release/0.6, so its changelog bullet and
write-up go into the 0.6.1 cycle rather than the 0.6.0 notes it was
first written against.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Forward-port to master: the CHANGELOG bullet and docs/release-notes/0.6.1-pre.md
stay on release/0.6 and come to master with the 0.6.1 cut.
(cherry picked from commit e10c19e)
@bkeroack
bkeroack merged commit d5cb9bb into master Oct 6, 2026
51 checks passed
@bkeroack
bkeroack deleted the fix/forward-port-watch-add-rejected branch October 6, 2026 16:55
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.

1 participant