Skip to content

rpc: evict idle polling filters after a timeout, add subscription metrics#22261

Merged
yperbasis merged 12 commits into
mainfrom
yperbasis/evictable-rpc-filters
Jul 7, 2026
Merged

rpc: evict idle polling filters after a timeout, add subscription metrics#22261
yperbasis merged 12 commits into
mainfrom
yperbasis/evictable-rpc-filters

Conversation

@yperbasis

@yperbasis yperbasis commented Jul 6, 2026

Copy link
Copy Markdown
Member

Supersedes #18591 by @onelapahead (his branch lives on an org fork that maintainers cannot push to; the original commit is carried here with authorship preserved).

Polling filters (eth_newFilter, eth_newBlockFilter, eth_newPendingTransactionFilter) previously lived until eth_uninstallFilter or a restart, accumulating buffered heap for clients that walked away. They are now evicted when not polled within --rpc.subscription.filters.timeout (default 5 minutes, matching the stale-filter deadline in geth's eth/filtersFilterAPI.timeoutLoop with Config.Timeout; 0 disables eviction). Any poll (eth_getFilterChanges, eth_getFilterLogs) resets the filter's deadline. WebSocket subscriptions are unaffected — they end with their connection.

Behavior change: a client that polls slower than the timeout gets filter not found and must recreate its filter — same as geth.

Metrics

  • subscriptions_active{filter,protocol} gauge — current live subscriptions
  • subscriptions_created_total / subscriptions_unsubscribed_total counters (active = created − unsubscribed holds exactly)
  • subscriptions_reaped_total{filter} — the timeout-evicted subset
  • Counters are registered once as cached vecs (metrics.GetOrCreateCounterVec is new in diagnostics/metrics, mirroring GaugeVec)

Implementation notes

  • Pollable subscriptions live in a single id→type registry: Subscribe* takes the protocol at creation (http = evictable polling filter, ws = connection-scoped; internal subscriptions are neither evicted nor counted), so a subscription cannot be created untracked. Polls resolve the filter type and reset the deadline in one map lookup, and one eviction loop iterates the registry for all filter types.
  • Eviction decides staleness and closes the subscription under the same lock the poll-side touch uses, so an actively-polled filter can never be evicted; the metrics decrement is gated on the map delete, so eviction racing a client unsubscribe counts once.
  • Teardown is orphan-proof: a forwarding goroutine draining a closed channel can no longer recreate a per-filter store entry after it was deleted (the write is dropped under the store lock when the subscription is gone) — for logs, heads, and pending txs alike.
  • One remote logs-filter update per eviction cycle, however many logs filters expire in it.
  • eth_getFilterChanges now returns all buffered pending-tx batches (previously only the first batch was reported while all were consumed — silent data loss), and each poll costs a single subscription-map lookup.
  • eth_newFilter and eth_subscribe (logs/receipts) now return an error when the remote filter update fails, instead of handing out a dead filter id / never-firing subscription (that silent failure predates this PR).
  • The dead, commented-out geth PublicFilterAPI port in rpc/filters/api.go is removed; the criteria types and FilterCriteria JSON decoding stay.

Covered by unit tests in rpc/rpchelper/filters_eviction_test.go (eviction, touch-prevents-eviction, WS exemption, orphan guard, batched remote update, and a concurrent touch/evict exercise run under -race) plus an eth_getFilterChanges batching test in rpc/jsonrpc. ChangeLog updated (breaking-change entry + new flag/metrics).

onelapahead and others added 2 commits May 21, 2026 19:59
…viction

Signed-off-by: hfuss <hayden.fuss@kaleido.io>

self-review fixes

Signed-off-by: hfuss <hayden.fuss@kaleido.io>
Conflict resolution: main refactored the WS subscription handlers into the
generic subscribeRPC helper (#22180), so the SetSubscriptionProtocol calls
from this PR were re-homed into each subscribe closure (NewHeads,
subscribePendingTransactions, Logs). The new TransactionReceipts
subscription stays untracked since this PR defines no receipts filter type.

Copilot AI 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.

Pull request overview

This PR adds timeout-based eviction for idle HTTP polling filters (matching geth-style behavior) and extends subscription tracking/metrics so operators can detect and prevent unbounded growth of unused filters.

Changes:

  • Add per-subscription protocol + last-access tracking, plus a background eviction loop keyed off a new rpc.subscription.filters.timeout config/flag.
  • Touch HTTP filters on polling RPCs (eth_getFilterChanges, eth_getFilterLogs) to keep actively-polled filters from being evicted.
  • Introduce protocol-labeled subscription metrics and counters for created/unsubscribed/reaped subscriptions.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
rpc/rpchelper/subscription.go Adds protocol + last-access tracking and eviction helpers to subscription channels.
rpc/rpchelper/metrics.go Adds protocol label support and counters for subscription lifecycle tracking.
rpc/rpchelper/filters.go Starts eviction loop, implements stale-sub eviction, and wires metrics + tracking into unsubscribe paths.
rpc/rpchelper/config.go Adds RpcSubscriptionFiltersTimeout to filter config with default 0 (disabled).
rpc/jsonrpc/eth_filters.go Enables tracking/touching for HTTP filters and protocol labeling for WS subscriptions.
node/cli/flags.go Adds rpc.subscription.filters.timeout duration flag for embedded node CLI.
node/cli/default_flags.go Registers the new timeout flag in default CLI flags.
cmd/rpcdaemon/cli/config.go Adds the timeout flag to rpcdaemon’s Cobra config.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread rpc/rpchelper/filters.go
Comment thread rpc/rpchelper/subscription.go Outdated
Comment thread rpc/rpchelper/filters.go Outdated
yperbasis added 2 commits July 6, 2026 11:34
…rChanges

ReadPendingTxs consumes every batch accumulated since the last poll, but
GetFilterChanges only reported txs[0], silently dropping the hashes of any
later batches.
… check

Replace ShouldEvict + re-check with CloseIfIdle, which decides staleness and
closes under the same lock Touch uses, so a subscription polled within the
timeout can never be evicted; this removes the TOCTOU race the old two-phase
eviction acknowledged as unavoidable. lastAccess and protocol move from
atomics to the existing chan_sub mutex.

Also compute the eviction check interval once in New so the startup log
matches the actual ticker (it previously logged timeout/2 while the loop
floored to 1s), fix the evictStaleSubscriptions doc-comment name, and add
eviction/touch coverage tests. Addresses Copilot review comments from #18591.
@yperbasis

Copy link
Copy Markdown
Member Author

Addressed the Copilot review comments from #18591 in a05032f:

  • Misleading checkInterval in the startup log — the interval (with its 1s floor) is now computed once in New and passed to timeoutLoop, so the log matches the actual ticker.
  • evictStaleFilters doc-comment name mismatch — fixed to evictStaleSubscriptions.
  • Missing eviction tests — added rpc/rpchelper/filters_eviction_test.go: idle filters of all three types are evicted (and their channels closed), TouchSubscription prevents eviction, WS subscriptions are never evicted, plus a concurrent touch/evict exercise run under -race.
  • TrackSubscription always labels polling filters http even over WS/IPC transports — deliberately not applied as suggested. In this design the protocol label doubles as eviction semantics (WS-labeled subscriptions are never evicted because they die with their connection). Polling filters are ID-based and survive the connection regardless of transport, so labeling them by transport would exempt WS-created polling filters from eviction and reintroduce the leak this PR fixes. If transport-accurate metrics are wanted later, the label should be decoupled from eviction (poll vs push) first.

While touching those lines, two more fixes landed:

  • 92c3247: eth_getFilterChanges for pending-tx filters reported only txs[0] while ReadPendingTxs consumes all buffered batches — later batches were silently lost. Now all batches are returned (test-driven fix).
  • a05032f: the eviction TOCTOU race (acknowledged in the original code as unavoidable) is removed — CloseIfIdle decides staleness and closes under the same lock Touch uses, so an actively-polled filter can never be evicted.

Each evicted logs filter rebuilt the aggregate request and issued its own
remote update; one update at the end of the eviction cycle covers them all.
Client-driven UnsubscribeLogs keeps its per-call update.

Copilot AI 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.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.

Comment thread rpc/rpchelper/metrics.go Outdated
Comment thread rpc/rpchelper/metrics.go Outdated
Comment thread rpc/rpchelper/filters_eviction_test.go
yperbasis added 5 commits July 6, 2026 12:31
…eviction plumbing

Drop late writes to the per-filter stores: the forwarding goroutine can
drain channel-buffered items after unsubscribe/eviction has deleted the
store, and the recreated entry is unreachable (reads are gated on the
subscription existing), so it leaked forever. The Add* functions now check
subscription existence under the store lock and delete instead of storing
when it is gone.

Deduplicate the tracking/eviction plumbing: one trackedSub resolver (via a
new SubTracker interface) behind Touch/Track/SetSubscriptionProtocol, one
generic evictIdleSubs for heads and pending txs, and one generic
unsubscribeSubInternal for the channel-sub teardown.

Make each eth_getFilterChanges poll cost a single subscription-map lookup:
TouchSubscription reports existence, so the type dispatch probes it
directly instead of Has* + Touch + re-Get. Metrics helpers switch to
WithLabelValues, avoiding a per-call label-map allocation.
Dead since its import; erigon's filter machinery lives in rpc/rpchelper.
Keeps the criteria types and FilterCriteria JSON decoding, and fixes the
ReceiptsFilterCriteria doc-comment name.
@yperbasis yperbasis changed the title rpc: add tracking to HTTP subscriptions for unused filter eviction rpc: evict idle polling filters after a timeout, add subscription metrics Jul 6, 2026
@yperbasis yperbasis requested a review from Copilot July 6, 2026 11:17
@yperbasis yperbasis marked this pull request as ready for review July 6, 2026 11:17

Copilot AI 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.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.

Comment thread rpc/rpchelper/filters.go Outdated
Comment thread rpc/rpchelper/filters.go Outdated
@yperbasis yperbasis added this to the 3.6.0 milestone Jul 6, 2026
- Track pollable subscriptions in a single id->type registry: Subscribe*
  now takes the protocol at creation (http = evictable polling filter,
  ws = connection-scoped, empty = internal), so tracking cannot be
  forgotten; TouchSubscription resolves the filter type in one map
  lookup; one eviction loop replaces evictIdleSubs/evictIdleLogsSubs
  and captures the protocol during the Range.
- Add metrics.CounterVec (mirroring GaugeVec) and cache the
  subscription lifecycle counters as package vars instead of building
  a label string and hitting the global registry on every event.
- hasLogsFilter: RLock instead of exclusive Lock; it runs per
  delivered log inside AddLogs' orphan guard.
- Drop the now test-only Has*Subscription methods; reshape
  UnsubscribeLogs to the early-return shape of its siblings.
@yperbasis yperbasis requested a review from Copilot July 6, 2026 14:23

Copilot AI 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.

Pull request overview

Copilot reviewed 20 out of 20 changed files in this pull request and generated 1 comment.

Comment thread rpc/rpchelper/filters.go
…emote filter

Previously SubscribeLogs/SubscribeReceipts removed the just-created
filter on a failed remote-filter update but still returned the (closed)
channel and dead id, so eth_newFilter appeared to succeed while every
subsequent poll returned 'filter not found', and WS/sync subscribers
waited on a channel that would never fire. They now return an error and
install nothing; eth_newFilter, eth_subscribe (logs, receipts), and
eth_sendRawTransactionSync propagate it to the client.
@yperbasis yperbasis added this pull request to the merge queue Jul 7, 2026
Merged via the queue into main with commit fda04fe Jul 7, 2026
94 checks passed
@yperbasis yperbasis deleted the yperbasis/evictable-rpc-filters branch July 7, 2026 14:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants