You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Closes#4476
## Summary
Bounds runaway old-heap growth of `Shape.Consumer` processes (issue
#4476). On a production node with 5,043 consumers, ~36 GB was
unreclaimable garbage (7–9 MB heaps each holding ~8 KB of live state)
because consumers inherit the BEAM default `fullsweep_after: 65535` and
rarely hibernate, so a full sweep is effectively never triggered.
Two complementary, independently-tunable levers:
1. **Per-process spawn opts (env-driven).** `Consumer`,
`Consumer.Snapshotter`, and `Consumer.Materializer` now pass `spawn_opt:
Electric.StackConfig.spawn_opts(stack_id, key)` (keys `:consumer`,
`:consumer_snapshotter`, `:consumer_materializer`), mirroring
`ShapeLogCollector`. This lets `ELECTRIC_PROCESS_SPAWN_OPTS` set
`fullsweep_after` per process type. No baked-in defaults — purely
env-configured.
2. **Adaptive GC, opt-in.** After processing a transaction fragment
(including during the startup buffer drain), the consumer forces
`:erlang.garbage_collect()` only if its heap exceeds
`consumer_gc_heap_threshold` (bytes; `nil` = off, the default). The
threshold is cached per-consumer at startup (`State.new/2`), so changing
it affects consumers started afterwards. The forced sweep runs **off the
reply path** via a `{:continue, :maybe_gc}` step, so it never blocks the
`ShapeLogCollector`'s synchronous publish call, and a hysteresis
interval (`@gc_min_interval_ms`) caps how much CPU a busy consumer
spends sweeping. See `Electric.Shapes.Consumer.set_gc_heap_threshold/2`.
Request-handler (`GET /v1/shape`) processes are intentionally untouched
— they already expose `fullsweep_after` via
`ELECTRIC_TWEAKS_HANDLER_FULLSWEEP_AFTER` and force GC before long-poll
blocking.
## Config
| Env var | Meaning | Default |
|---|---|---|
| `ELECTRIC_PROCESS_SPAWN_OPTS` | JSON map; now accepts
`consumer`/`consumer_snapshotter`/`consumer_materializer` keys (e.g.
`{"consumer":{"fullsweep_after":4}}`) | `%{}` |
| `ELECTRIC_CONSUMER_GC_HEAP_THRESHOLD` | adaptive-GC heap threshold in
bytes; `nil` disables | `nil` |
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Oleksii Sholik <oleksii@sholik.dev>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bound Shape.Consumer heap growth: make the consumer family's process spawn options (incl. `fullsweep_after`) configurable per process via `ELECTRIC_PROCESS_SPAWN_OPTS`, and add an opt-in adaptive GC that runs after a transaction fragment when the consumer's heap exceeds the runtime-tunable `ELECTRIC_CONSUMER_GC_HEAP_THRESHOLD` (off by default).
0 commit comments