statement-store: new api implementation#11989
Conversation
|
/cmd fmt |
|
/cmd fmt |
…' into denzelpenzel/statement-store-api # Conflicts: # substrate/client/statement-store/src/subscription.rs
|
/cmd prdoc --audience node_dev --bump patch |
…e_dev --bump patch'
…ent-store-api # Conflicts: # cumulus/zombienet/zombienet-sdk/tests/zombie_ci/statement_store/integration.rs
| { | ||
| let state = self.state.lock(); | ||
| if state.active_filter_ids.len() >= MAX_FILTERS_PER_SUBSCRIPTION || | ||
| state.pending_replays.len() >= PENDING_REPLAYS_HARD_CAP |
There was a problem hiding this comment.
Although this is capped, the size of the response is not, 128 matchAny requests can be open per every 16 connections. So it is 2048 matchAny that basically want to return all the statement store. And while 1 filter is streaming to the client everything else sits in RAM. Because as soon as filter request received we scale decode it and keep in store.pending_replays inside register_filter_with_snapshot. Even with 100MB state it will be a 200Gb.
There should be a way to make a "lazy" approach.
There was a problem hiding this comment.
I agree this is a real issue, see two implementation options:
- Option A – lazy by hashes, not bytes: At filter registration time, keep the current snapshot/register atomicity, but store only the matching statement hashes in the pending replay state instead of storing the full SCALE-encoded statements
- Option B – real cursor with snapshot watermark: add a replay cursor/watermark at the store level and replay matching statements lazily from the store up to that boundary
There was a problem hiding this comment.
- Option C -> Have a temporary column where is store the pending replys.
There was a problem hiding this comment.
Yes, column in the database.
There was a problem hiding this comment.
let's continue track this issue here #12153
Statement Store RPC Bench Report
Aggregate ResultLower latency, send time, and elapsed time are better. On the main latency metric,
Per-Run Datav1:
|
| Run | Send avg, s | Receive avg, s | Latency avg, s | Latency max, s | Elapsed, s |
|---|---|---|---|---|---|
| 1 | 41.186 | 0.001 | 41.187 | 60.614 | 197 |
| 2 | 39.830 | 0.001 | 39.831 | 60.698 | 191 |
| 3 | 43.228 | 0.001 | 43.229 | 61.045 | 192 |
| 4 | 46.910 | 0.001 | 46.911 | 68.892 | 201 |
| 5 | 39.224 | 0.001 | 39.224 | 58.607 | 189 |
| 6 | 39.419 | 0.001 | 39.420 | 59.423 | 190 |
| 7 | 44.629 | 0.001 | 44.630 | 65.223 | 194 |
| 8 | 47.242 | 0.001 | 47.243 | 69.187 | 202 |
| 9 | 46.157 | 0.001 | 46.158 | 67.934 | 199 |
v2: unstable_bench
| Run | Send avg, s | Receive avg, s | Latency avg, s | Latency max, s | Elapsed, s |
|---|---|---|---|---|---|
| 1 | 33.534 | 0.937 | 34.471 | 46.233 | 179 |
| 2 | 33.413 | 0.809 | 34.222 | 43.961 | 177 |
| 3 | 35.799 | 0.936 | 36.735 | 47.177 | 179 |
| 4 | 27.989 | 0.625 | 28.614 | 41.925 | 173 |
| 5 | 31.313 | 0.785 | 32.098 | 42.184 | 171 |
| 6 | 42.028 | 1.097 | 43.125 | 57.402 | 189 |
| 7 | 32.255 | 0.819 | 33.075 | 49.644 | 181 |
| 8 | 28.545 | 0.655 | 29.200 | 38.125 | 166 |
| 9 | 35.319 | 0.908 | 36.226 | 47.307 | 177 |
Tail Behavior
v2 improves both average and tail latency in this run set.
| Metric | v1 bench |
v2 unstable_bench |
v2 vs v1 |
|---|---|---|---|
| Median latency avg, s | 43.229 | 34.222 | -20.84% |
| Mean of latency max, s | 63.514 | 45.995 | -27.58% |
| Worst latency max, s | 69.187 | 57.402 | -17.03% |
Conclusion
v2 is better on the primary latency path for this run set:
| Comparison | Result |
|---|---|
| Average latency | v2 is 20.64% faster |
| Average send time | v2 is 22.60% faster |
| Median per-run latency | v2 is 20.84% faster |
| Mean max latency | v2 is 27.58% better |
| Worst observed max latency | v2 is 17.03% better |
| Average elapsed time | v2 is 9.29% faster |
| Compared sample size | 9 runs per bench |
|
/cmd fmt |
113bb18 to
61be0eb
Compare
|
Had a detailed look, good job @DenzelPenzel. If left you a few more comments, nothing major, with those applied this should be ready for approving from my side. |
3f29e47 to
b626640
Compare
…ech/polkadot-sdk into denzelpenzel/statement-store-api
f6debbb to
b362729
Compare
|
All GitHub workflows were cancelled due to failure one of the required jobs. |
01522dd to
1042495
Compare
|
/cmd fmt |
Impl #10997
Summary
In this PR, we add the unstable statement-store JSON-RPC surface and wire it into the parachain node RPC stack. This lets clients submit SCALE-encoded statements over RPC, open one long-lived statement subscription, and then attach or remove topic filters on that subscription without opening a new stream for each filter.
The subscription flow is split into
statement_unstable_subscribeandstatement_unstable_addFilter. A subscription starts empty, each added filter gets its ownfilterId, and live notifications carry the ids of the filters that matched the statement. That lets a client track several statement topics over a single RPC subscription while still knowing which filters produced each replay or live event.RPC Shape
We add
statement_unstable_submit,statement_unstable_subscribe,statement_unstable_addFilter, andstatement_unstable_removeFilterundersc-rpc-spec-v2::statement.statement_unstable_submitdecodes submitted statement bytes and maps store results into RPC-level outcomes:new,known,rejected, orinvalid(an expired statement is reported asinvalid). Subscription state is scoped to the jsonrpsee connection that created it, so a filter can only be added to or removed from a subscription owned by the same connection.For filters, the unstable RPC accepts
anyandmatchAll.matchAnyis rejected at the RPC boundary for now, which keeps the external API aligned with the current unstable contract while the store internals can still use the optimized filter representation.Subscription Semantics
A subscription is recorded in the per-connection registry before the jsonrpsee subscription is accepted. The subscription id is read from the pending sink (
PendingSubscriptionSink::subscription_id(), available since jsonrpsee 0.24.11) and registered first, so registration happens-before the id is handed to the client. As a result, anaddFilterthat arrives immediately aftersubscribealways resolves its subscription – there is no accept/addFilterrace window and no timeout-based lookup. If the accept fails, the registry entry is dropped and the subscription is unregistered.Multi-filter subscriptions are handled by the existing statement subscription matcher workers.
addFiltervalidates capacity, allocates afilterId, queues anAddFiltermessage for the matcher, and returns without waiting for replay snapshot collection. The matcher then collects the replay snapshot and registers the filter in the same critical section, so live statements cannot slip between the snapshot and filter registration.For each added filter the subscription emits:
replayStatementsbatches for already-admitted matching statementsreplayDoneonce that filter's replay is drainednewStatementsfor live statements, including all matchingfilterIdsstopif local subscription resource caps are hitLive statements that arrive while a replay is still in progress are kept in matcher-owned pending state, then released once replay ordering allows it. Statements already delivered by replay are kept out of the live path for that filter, avoiding duplicate delivery for the common "submit, then subscribe" case.
Each subscription is capped at 128 active filters (
MAX_FILTERS_PER_SUBSCRIPTION). Filter removal is idempotent, and dropping the RPC subscription cleans up matcher state.