Skip to content

feat(ws): pipeline each connection's requests behind ordering barriers - #210

Closed
jsam wants to merge 1 commit into
mainfrom
fm/il-w02
Closed

jsam wants to merge 1 commit into
mainfrom
fm/il-w02

Conversation

@jsam

@jsam jsam commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Draft for visibility. Stacked on the release checkpoint 1 branch (fm/il-rc1); it will be retargeted to main once checkpoint 1 lands. Plan item W02 (partial #132).

What it does

Each /ws connection becomes one loop that owns its read half, the single writer, its subscriptions and a bounded request pipeline. Requests run as pipeline tasks, so a long query no longer holds up the connection's notifications and subscription deltas.

  • Ordering barriers. Programs made only of queries overlap. Writes, rules, session facts/rules, .kg …, .session …, .subscribe/.unsubscribe and other meta commands run alone and in order. A pipelined insert sent after .kg use other always lands in other.
  • In-order replies. Replies are released in request order, so clients without request IDs keep working unchanged.
  • Bounded. At most http.rate_limit.ws_max_in_flight_requests (default 16) requests can be unanswered at once; at that bound the loop stops reading the socket.
  • Subscribe off the loop. .subscribe evaluates its snapshot in a task. Commits that land meanwhile are recorded and re-evaluated after registration, so no delta is lost.
  • Disconnect. Running queries are abandoned; a started write finishes before the session closes.
  • No new locks. All state is owned by the connection task. Query classification is lexical (no extra parse).

Docs: WebSocket guide, AsyncAPI spec, configuration reference, TESTING.md.

Tests

  • Pipeline unit tests (ordering, barriers, capacity, panic release, shutdown abort/drain).
  • tests/ws_pipeline_tests.rs, in-process:
    • a query running ≥500 ms while an external write produces a delta before the query's reply;
    • KG switch + insert ordering behind a running query (fails if every program is treated as a concurrent query);
    • session and subscription command ordering;
    • bounded pipeline;
    • disconnect cleanup;
    • saturated mixed query/write/delta traffic across 4 subscribers.
  • New required e2e_reactive scenario: an agent receives deltas while its own long query runs (release; writer ack to delta ≤250 ms).

Performance

Depends on TCP_NODELAY from #200. Because pushes now go out while a request runs, a writer's own persistent_update often precedes its reply. Under Nagle that delays the write ack by ~40 ms (perf gate: insert ack p50 10.6 → 43 ms without it).

Requests are futures polled by the connection task (no task per request); results over 256 rows are serialized on the blocking pool; the idle timeout is one pinned timer.

Perf gate vs fm/il-rc1, final code with #200's change applied, 10 rounds: no FAIL, 15 required metrics PASS and 5 INCONCLUSIVE. The INCONCLUSIVE point estimates are ≤ 1.014; their intervals straddle the budget from noise.

  • cheap_query (20 rounds): p50 1.034 (1.014..1.045), p99, qps and concurrent all PASS.
  • Writes are far faster than baseline: insert_single ack p99 42.1 → 4.9 ms, facts/s 47 → 627.
  • Deltas unchanged.

Also run: make test-fast, make e2e-reactive (11/11), full .iql.out snapshot suite (1149/1149, also as a 96-worker stress run), make oracle-test.

Tracker: #199

Each /ws connection is one loop that owns its read half, the single
writer, its subscriptions and a bounded request pipeline. Requests run
as pipeline tasks, so a long query no longer holds up the connection's
notifications and subscription deltas.

- Programs made only of queries overlap; writes, rules, session
  changes, KG switches and subscription commands run alone, in order,
  so a pipelined insert after .kg use always lands on the new KG.
- Replies are released in request order (no request IDs needed).
- At most http.rate_limit.ws_max_in_flight_requests (default 16)
  requests are unanswered; at the bound the loop stops reading.
- .subscribe evaluates its snapshot off the loop; commits landing
  meanwhile are recorded and re-evaluated after registration.
- Disconnect aborts running queries and lets a started write finish.

Tests: pipeline unit tests, in-process ws_pipeline_tests (500 ms+
query with concurrent delta, KG switch + insert ordering, disconnect
cleanup, bounded pipeline, saturated mixed traffic) and a required
e2e_reactive scenario; the testkit client learns pipelined requests.

Includes perf(ws): poll requests in the connection task and keep one idle timer:

Requests were spawned as tasks; a woken idle worker would steal each
one, so every request crossed cores twice. They are now futures owned
by the pipeline and polled by the connection loop; computation already
runs on the blocking pool, and results over 256 rows are serialized
there too, so nothing heavy runs on the loop. A panic is caught per
request and released as an internal error.

The idle timeout used to be a freshly boxed timer every loop turn; it
is now one pinned timer reset on activity, armed only while no request
is pending (covered by a new test).

perf gate, cheap_query vs fm/il-rc1, 20 rounds: latency p50 1.034
(1.014..1.045), p99 1.015, qps 1.009, all PASS.

Rebased onto main with W01 request IDs and W04 revision-tagged
subscriptions: a .subscribe pushes the snapshot's revision, and
registration both replays changes recorded during the snapshot and
re-evaluates when the knowledge graph moved past the snapshot's revision.
@jsam
jsam changed the base branch from fm/il-rc1 to main October 3, 2026 20:39
@jsam
jsam marked this pull request as ready for review October 3, 2026 20:39
@jsam jsam closed this Oct 3, 2026
@jsam jsam reopened this Oct 3, 2026
@jsam

jsam commented Oct 3, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by #214, which carries these W02 commits rebased onto main.

@jsam jsam closed this Oct 3, 2026
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