Repository navigation
Conversation
Adds perf-gate/, a standalone tool workspace that drives real inputlayer-server binaries over /ws: warm cheap and bound queries, durable single and batch inserts, writer-to-agent delta latency for 1 and 64 subscribers, and isolation under a long request and a slow consumer. Baseline and candidate run in interleaved rounds with fresh servers; every timed result is checked against a client-computed answer. compare judges cost ratios with a bootstrap interval over rounds under perf-gate/policy.toml and exits zero only on PASS. make perf-gate builds the approved baseline commit and the working tree and runs the gate; make perf-gate-check lints and tests the tool.
Both SDKs turned an engine `error` frame into a one-row result with an `error` column, and ignored `errors[]`, so a failed insert, load or migration read as success (Python insert returned InsertResult(count=1) for a WAL append failure, #93). One error contract per SDK, applied in the connection: - `error` frame -> QueryError with `code` and validation errors. - non-empty `errors[]` -> StatementFailedError with each failed statement and the whole result; chunked results keep `errors` and `timing_breakdown` through assembly, and an error frame mid-stream raises typed. - KG auto-create, migration recorder and langchain/langgraph helpers branch on the exception and `code`, never on message text. - insert() counts the facts the engine reports storing. JS fixes the error contract exposed: - the reader queues a reply's frames, so a chunked reply sent in one burst is no longer dropped after its first frame; - load() sends the local file instead of the client-only `.load`; - vectorSearch() sends a plain query and sorts and limits locally instead of a head aggregate the engine rejects; - dropKnowledgeGraph() leaves the KG before dropping it.
A capped result was adopted as the full set: .subscribe returned it as a complete snapshot (truncated: false) and a re-evaluation crossing the cap diffed against the partial rows. Reject a truncated result before touching state: .subscribe errors without registering, re-evaluation pushes a subscription_error and keeps the last complete result, and the next delta once the result fits is relative to it.
QueryJob::execute holds a storage read guard for the whole program, but .debug, .why, .why full, .why_not and the .index commands acquired a second read on the same lock. parking_lot's RwLock prefers writers, so a writer queued between the two reads deadlocked the query. - .debug and .index reuse the outer guard. - .why and .why_not capture a ProofSnapshot (KG snapshot plus index metrics) under the outer guard, release it, then run query evaluation and proof search on the snapshot alone. A failed proof re-acquires the guard for the statements that follow, as .compact does. - StorageEngine::proof_snapshot_on and KnowledgeGraphSnapshot::proof_inputs separate snapshot capture from evaluation; execute_and_get_context is built from them and the unused get_rules_and_data is removed. A test-only hook queues a writer after execute takes its guard and before the meta command runs; every covered command deadlocked before this fix and completes after it. No lock is added.
Two servers on one data_dir (for example a Kubernetes RollingUpdate on a ReadWriteOnce volume) interleave WAL appends and shard rewrites and corrupt both. StorageEngine::new now takes an OS advisory lock (flock / LockFileEx via fs4) on data_dir/LOCK before creating, reading or writing anything else, and holds it as the engine's last-dropped field. A second engine on a locked directory fails with StorageError::DataDirLocked naming the directory and the owner's recorded pid, and leaves the directory byte-identical. The OS releases the lock when the owner exits, including SIGKILL, so a stale LOCK file never blocks a restart. Distinct directories lock independently. Startup only: one open + flock + pid write (~80us measured); no per-request lock or hot-path change. Closes #142
Sessions authenticated over /ws kept working for up to 24 h after their API key was revoked, their password changed or their user dropped: execute_program only re-read the role, and queued results, notifications and subscription pushes were sent without any credential check. Credentials now live in an in-memory CredentialRegistry, loaded from _internal at startup and updated by every user and key mutation after it is persisted. Authentication returns a Principal bound to exactly one credential: one API key, or one generation of a user's password, each with a canonical CredentialId. Revoking a credential trips its flag before the command returns; other credentials, including other keys of the same user, are untouched. Enforcement: - execute_program checks the principal at admission (one immutable permission snapshot for the whole program, replacing the per-request users scan) and again at release, so a credential revoked mid-run gets none of the output; writes committed before revocation stay. - every outbound /ws data frame passes the fence after the socket is ready and before the frame is committed to it; - a per-connection one-shot revocation signal closes the connection, stops its standing queries and sends one final notice. - kg_change notices for other KGs reach admins only, and _internal changes are never sent, live or on replay. Checks are an atomic load on state the session already holds; no shared lock, lookup or storage access per request or frame. Login and REST key checks use one registry lookup instead of a snapshot scan. Closes #137
Add `make e2e-reactive`: each test starts a real inputlayer-server process, agents subscribe to standing queries over /ws, and independent writer connections insert and retract facts and change rules. Agents must receive the exact added/retracted rows with contiguous seq, without re-querying, and end equal to a fresh full query. Covers one subscriber, 64 subscribers, reconnect/resubscribe and crash-restart, unrelated writes, and write bursts. Defects tracked by the reactive plan run as expected failures that fail as XPASS once fixed: capped snapshots and refreshes adopted as complete (S05), out-of-order notifications and a restart-reused reconnect cursor (W04), and an undeliverable oversized delta that still advances the subscription (W05). The harness is the test-only `testkit` workspace crate (engine process, /ws agent client, fixtures, raw writer->agent latency samples in schema inputlayer.reactive.delta_latency.v1), shared with the benches. The suite runs in test-all, ci-test-all and lint; plain `cargo test` skips it. No production code changes.
…terval Each round's two arms run back to back, so judge the median of the per-round cost ratios; host drift between rounds cancels. The exact binomial interval of a median replaces the bootstrap: deterministic, and at 10 rounds it tolerates one outlier round per side. Agents now drain against a deadline from the writer's last write, so a slow writer no longer counts against them.
…tions (#88) Replay every corpus.json scenario, controls included, and every benchmark.json recorded reference extraction through the real rule pack, and score both with one evaluator contract. Prints per-family detection, the false-alarm rate over control rows and revision fidelity over correction rows; fails on any control finding, stale correction, over-retraction, finding-kind drift, unevaluated scenario, vacuous control, missing family or empty metric. No model calls. - evaluator.py: pure verdict/metric/contract module, self-tested by test_evaluator.py with planted defects - engine_replay.py: corpus facts -> one namespaced KG, two-phase assert/retract, per-scenario observation with cross-scenario leak check; replaces the duplicate full_bench.engine_pass - gate.py + scripts/run_vc_gate.sh + make vc-gate: CI entry on a throwaway engine; path-filtered workflow verified-completions.yml - corpus controls now carry extractor-truth facts (corrections retract the superseded fact); fixes six restatement controls whose "$1k" vs "1500 USD" was a real contradiction - poc_verify scores recorded extractions through the shared contract and checks presence against the engine; verify_each no longer passes controls without evaluating them
Adds `perf-gate genbi` and `make bench-genbi`: each genbi-trust scenario (read in place from GENBI_TRUST_DIR, never copied) runs on a fresh server. The IQL seed is cold-loaded, every business question becomes a standing subscription over /ws, and an independent writer applies the ordered mutations. Per mutation it records writer-send to agent-delta latency (split insert vs retraction, and propagation after the durable ack), write ack, a full re-query baseline, memory and throughput; every checkpoint compares the agent's maintained answer with the evaluator-private expected rows (counts only are reported) and with a fresh evaluation, so a broken delta path fails as delta divergence. --fault drop-retractions proves the checks catch one. The perf-gate client gains rows-returning query() and subscription ids on pushed frames; the server helper reports current RSS.
Add tests/differential_oracle: one history (statements, restarts, named checkpoints) replayed through independent adapters and compared at every checkpoint as Z-sets: - reference: naive stratified fixpoint over finite sets in the test, sharing only the parser with the engine; unmodeled constructs desync it with an explicit, reported reason - recompute: the snapshot evaluator queried afresh - subscription: standing queries assembled purely from pushed deltas via the real notification, dependency-filtering and coalescing registry - spec: results recorded in .iql.out transcripts (corpus cases) Histories come from hand-written scenarios (duplicate supports, recursive edge removal, negation, aggregates, rule replacement, restart), seeded random generation and 620 .iql.out cases. Divergences are minimized with delta debugging; a deliberately corrupted adapter (dropped retractions) is caught and shrunk to two statements. The Adapter trait takes the revision an answer must reflect, so a persistent per-KG dataflow adapter plugs in. Production change: the notification-to-ChangeSet mapping moves into subscription::change_of so the oracle drives the same mapping the WebSocket loop uses. Add `make oracle-test` and document it in TESTING.md.
# Conflicts: # CONTRIBUTING # Cargo.toml # Makefile # TESTING.md
A write now commits a Transaction: every change of one commit, across any number of shards, at one revision. The WAL stores it as one checksummed record whose trailing newline is the commit boundary, written with one append and (in immediate mode) one fsync. Recovery replays the longest intact prefix of records, so a restart yields the state after an exact number of commits, never part of one. - PersistBackend::append(shard, updates) becomes commit(txn). Inserts and deletes commit one-op transactions; startup multiplicity corrections and prefix clears commit one transaction across all their relations, so a failed prefix clear now leaves every relation intact. - TxnOp is a typed enum (facts today); rule and schema changes become new variants. A checksummed record this server cannot decode fails startup with StorageError::WalUnreadable instead of being skipped. - Tuples are stored with the lossless batch codec (base64), fixing NaN and infinite floats that JSON wrote as null and replay then dropped. v1 records still replay so v1 data dirs migrate with their WAL. - A crash-torn last line is cut off. Any other damage stops replay there and the cut-off bytes are saved to wal/current.wal.<ms>.corrupt, instead of skipping the damaged record and replaying later ones out of order. - A failed write or fsync is cut back to the exact committed length; if that fails, the repair runs before the next write, rewrite or shutdown, so a commit that returned an error is not replayed. - Remove the unused legacy src/storage/wal.rs, a second WAL format. Hot path: same locks (WAL mutex, shard map, per-KG write lock), nothing on the query path; one record per commit instead of one per tuple. Part of #131
S05 landed in this checkpoint, so its two expected-failure markers flip to required passes that pin the exact contract: a capped snapshot is refused with nothing registered, and a refresh over the cap pushes subscription_error, keeps the last complete result and recovers with a contiguous delta.
# Conflicts: # Makefile
# Conflicts: # Makefile # TESTING.md
Credential revocation (S03) shares subscription re-evaluation with the fail-closed result cap (S05) and the handler with the guard-free proof path (S06). One test holds a subscriber at the cap and revokes it: the recovery delta never reaches it. The other revokes during .why proof search, which now runs after the storage guard is released: no proof is delivered.
make doc-check (rustdoc -D warnings) rejects intra-doc links from public module docs to private modules; name them in plain text instead.
Quiet-window A/A of the approved baseline at 20 rounds with pinned servers: 17 of 20 required metrics pass against themselves; durable-ack p50 and interference delta p99 stay inconclusive on this host's virtual disk. Raw samples and host notes sit beside approved.toml.
Adds perf-gate/, a standalone tool workspace that drives real inputlayer-server binaries over /ws: warm cheap and bound queries, durable single and batch inserts, writer-to-agent delta latency for 1 and 64 subscribers, and isolation under a long request and a slow consumer. Baseline and candidate run in interleaved rounds with fresh servers; every timed result is checked against a client-computed answer. compare judges cost ratios with a bootstrap interval over rounds under perf-gate/policy.toml and exits zero only on PASS. make perf-gate builds the approved baseline commit and the working tree and runs the gate; make perf-gate-check lints and tests the tool.
Reconcile W01 (request IDs, typed notices, shared ws-protocol crate) with
W02 (per-connection request pipeline, ordering barriers):
- ws.rs keeps W01's auth phase, notices and ServerFrame types around W02's
pipelined loop; ws/execute.rs builds W01's frames as encoded replies for
the pipeline, serializing large results on the blocking pool (W02).
- ws/request.rs classifies ClientFrames into jobs carrying the request id;
a panicked request's error now echoes its id.
- ws/in_flight.rs: an id is unique among unanswered requests; reusing one
in flight gets invalid_request (W01 deferred this to pipelining).
- registry.add returns the generation with the follow-up evaluation, and
the .subscribe reply names {subscription, generation}.
- testkit WsClient pipelines with ids: replies are matched to the oldest
outstanding id.
- e2e wire scenario: a pipelined reuse of an unanswered id is refused;
reuse after the reply still works.
- Docs: request IDs and pipelining sections combined; docs bundle
regenerated.
…ed subscriptions Change notifications and standing-query results now have explicit, separate stream contracts. - One NotificationLog per engine run numbers, retains and broadcasts each notification under its existing ring lock, so seq order, ring order and delivery order agree; the two duplicated publish paths and their three fields are gone. - authenticated carries stream_epoch, a random id of the engine run. A reconnect presents last_seq with epoch; the server replays the retained notifications after it with live ones following without gap or overlap, or sends one non-closing replay_gap notice (other epoch, evicted, ahead). Replay passes the credential fence frame by frame. - Snapshots carry a revision from one engine-wide counter, assigned under the KG write lock. A subscription evaluation pins one snapshot (rules, data and revision) via Handler::query_snapshot; subscribed and every subscription_delta name the revision they are the exact answer at. - Registration compares the KG's current revision with the snapshot's and re-evaluates at once when it moved on, so a commit announced before the subscription existed is not lost. Hot path: no new lock. Publication extends the existing ring critical section by the broadcast send; subscribe adds one snapshot read. Part of #174, #136
…ng-subscribe delivery The W04 known-defect markers become required passes in a new stream module: concurrent commits notify in strictly increasing seq order; a cursor from before an engine restart gets one replay_gap notice and no replayed notifications of the new run; and commits racing a .subscribe all reach the agent, whose view ends equal to a fresh query. The testkit agent now checks that every delta names a higher revision than the state it applies to (Violation::StaleRevision) and exposes the stream epoch. In-process tests cover the reconnect cursor over a real /ws connection (replay then live without overlap; other or missing epoch and evicted cursors get replay_gap) and increasing revisions on deltas. Part of #174
The AsyncAPI spec, the WebSocket guide, the ws-protocol crate docs and TESTING.md describe the two stream contracts: notification seq within a stream epoch with last_seq+epoch replay or replay_gap, and per-subscription gapless seq with increasing knowledge graph revisions. The Python and JS SDK and GUI frame types gain stream_epoch, Subscribed.revision, SubscriptionDelta.revision and the replay_gap notice code (non-closing). The GUI docs bundle is regenerated, which also picks up earlier guide changes it had not yet bundled. Part of #174, #136
A session kept receiving its knowledge graph's change notifications, live and on replay, after .kg acl revoke took its access away, and a subscription delta computed before the revocation could still be sent. Every pushed frame now passes a read-access check for its knowledge graph. The handler counts access-list changes (grant, revoke, KG drop cleanup, user drop); each connection caches its answer per knowledge graph and looks the access list up again only when that generation moved. A withheld delta becomes a subscription_error naming its skipped seq. Hot path: one atomic load per pushed frame while access lists are unchanged. Part of #174
# Conflicts: # Cargo.toml # src/protocol/handler.rs # src/storage/persist/mod.rs # src/storage/persist/transaction.rs # src/storage/persist/wal.rs # src/storage_engine/catalog_commit_tests.rs # src/storage_engine/program_commit_tests.rs # tests/atomic_program_tests.rs # tests/fact_program_tests.rs
…llable commit
Every request now runs under one RequestControl (execution::request_control,
replacing the unused QueryTimeout/CancelHandle): one deadline spans waiting
behind earlier requests, waiting for a compute permit, the blocking pool and
computation. Its state leaves Running exactly once: stopped (deadline or
cancel), committing, or finished, so a stop and the start of a commit can
never both win.
- handler/supervise.rs: compute admission and the blocking task run under the
control. A request stopped while queued never starts; one stopped while
running is answered at once and its computation exits at the existing
cooperative checks (the code generator's thread-local now holds the
control: one relaxed load per check, no lock). A committing request is
awaited and returns what it committed; a panic after the commit began is
outcome_unknown.
- commit_program enters the commit under the KG lock just before the WAL
write; a failure applying an already-durable WAL record is now
CommitError::Unknown, not "nothing applied".
- Durable meta commands (MetaCommand::changes_durable_state) and mutations
answered by the handler's own intercepts enter the commit before they run;
a stopped request runs no further statement.
- Handler internals return ProgramError, so stops reach the wire typed:
error codes deadline_exceeded, cancelled, outcome_unknown.
- Wire (ws-protocol): execute.timeout_ms (capped by query_timeout_ms),
cancel {target} answered by cancel_ack {target, outcome: cancelled |
too_late | not_found}. A cancel acts on arrival, also on requests queued
behind the pipeline's barriers; disconnect stops running reads.
- Tests: tests/ws_cancel_tests.rs (queued cancel and queued deadline never
run later, running query stops promptly, cancel at increasing delays across
a write's commit boundary agrees with the data and a blind retry leaves one
copy), supervise/request_control/in_flight/commit unit tests, and a
required e2e_reactive scenario: an agent cancels its own long query and
keeps its subscription.
- Docs: WebSocket guide, AsyncAPI, errors, configuration; SDK and GUI
ErrorCode mirrors.
Bring W04 (stream epoch cursor, revision-tagged subscriptions, KG access fence), which main carries as the #215 squash, onto W02+W03: - Subscription handoff: one mechanism. W02 evaluates a .subscribe snapshot off the connection loop (Opening/Opened); registration now uses W04's revision check (re-evaluate when the KG moved past the snapshot) instead of W02's record of notifications seen while opening, which could miss a commit whose notification had not reached the loop yet. registry.add loses its `missed` argument; W02's two tests for it are removed. - ws.rs: W04's epoch cursor, replay and per-push KG access fence in W02's pipelined loop; the .subscribe reply names the snapshot revision. - handler.rs: W04's run_job (pinned snapshot jobs) runs under the request control via supervise::run_blocking; query_snapshot gets the server deadline; execute_program_status finishes the control before W04's settle_result.
jsam
marked this pull request as ready for review
October 3, 2026 21:05
This was referenced Oct 3, 2026
jsam
added a commit
that referenced
this pull request
Oct 4, 2026
…fixed size (#226) agent_receives_deltas_while_its_own_long_query_runs failed on the 4-vCPU CI runner with 'fixture too fast to show blocking' (472 ms at #214, 469 ms at 55ec34d): the fixed side-14 bipartite graph is too small for that host. The engine is unchanged in speed and the query result is correct (empty). The test now probes TRIANGLES and grows the graph until it runs for twice LONG_QUERY on the host at hand. DELTA_BUDGET and every assertion are unchanged.
This was referenced Oct 4, 2026
Closed
jsam
added a commit
that referenced
this pull request
Oct 4, 2026
…ry request (#249) The release-checkpoint performance gate fails on main: cheap_query serial p50 is 1.110x the approved baseline e732cd5 on the 32-vCPU runner (115 -> 126 us). Concurrent latency and throughput did not move. Bisected to #214 (per-connection request pipelining), made worse by #222. Cause: the pipeline runs requests in a `FuturesUnordered`. Its `poll_next` wakes its own task and returns `Pending` after any poll that polled every future in the set. With one request in flight, which is every serial client, that happens on each request's first poll. The connection task wakes itself, tokio schedules that as a yield, and a yield wakes an idle worker to take the task. Each request therefore costs one more context switch (4.0 -> 5.0 per query) and a hand-off of the connection task to another core. #222 added no work per query but made the hand-off dearer: user cycles per query rose 15% at the same instruction count. Fix: keep one never-woken future in the set. After the pipeline's first poll it is never polled again, so no poll reaches every future and the forced yield no longer fires. The yield after futures that wake themselves still applies. Pipelining, ordering, deadlines, cancellation and the credential fence are unchanged. Nothing is added per request: no lock, atomic or allocation. Each connection gets one more set node. Bisect (cheap_query, `?edge(1, Y)`, serial). Server counters per query from perf stat. Ratios are against e732cd5, from an interleaved probe with one pinned server per commit, 40 rounds: | commit | PR | user instr/query | ctx switches/query | p50 ratio | |---|---|---|---|---| | e732cd5 | baseline | 1.282M | 4.0 | 1.000 | | d75ce6e | #208 atomic programs | 1.279M | 4.0 | 0.993 | | 4578fe6 | #215 notification stream | 1.271M | 4.0 | 0.991 | | 71a4d24 | #216 online backup | 1.275M | 4.0 | 1.016 | | 2f621c5 | #200 first-update latency | 1.272M | 4.0 | 1.009 | | e96df5c | #207 | 1.279M | 4.0 | 0.998 | | 4dd8fe9 | #209 shard index | 1.272M | 4.0 | 0.987 | | 277c7f0 | #206 | 1.274M | 4.0 | 1.008 | | 3554c98 | #214 pipelining, deadlines | 1.296M | 5.0 | 1.020 | | 3492a59 | #213 | 1.294M | 5.0 | 1.030 | | 2ba9a5a | #212 provenance | 1.297M | 5.0 | 1.029 | | 9ed61b4 | #204 key expiry | 1.297M | 5.0 | 1.027 | | be35ee3 | #221 | 1.297M | 5.0 | 1.013 | | c98a590 | #222 bounded streaming | 1.298M | 5.0 | 1.047 | | a6fd628 | #219 wildcard fix | 1.297M | 5.0 | 1.061 | | b7f7ad6 | #220 WAL durability | 1.300M | 5.0 | 1.056 | | de54807 | #223 shared standing eval | 1.299M | 5.0 | 1.071 | | 55ec34d | #225 checkpoint-1 fixes | 1.300M | 5.0 | 1.045 | | 23712b8 | main | 1.300M | 5.0 | 1.064 | | this change | | 1.300M | 4.0 | 1.033 | Confirming A/Bs with the gate's own fixture (fresh pinned servers each round, 40 rounds, ratio [95% interval]): - #214 vs its parent 277c7f0: 1.039 [1.027..1.049] - #222 vs its parent be35ee3: 1.025 [1.006..1.044] - A/A 3554c98: 1.010 [1.002..1.025] - e732cd5 -> main: 1.048 [1.028..1.066] - main -> this change: 0.978 [0.958..0.989] - e732cd5 -> this change: 1.021 [1.009..1.044], PASS (budget 1.05) User cycles per query (perf stat, same instruction count): e732cd5 958K, be35ee3 1053K, c98a590 1206K, this change 1006K. Full gate, all fixtures, 20 rounds, servers pinned to CPUs 2-15: - main -> this change: no metric worse. Every required metric is PASS, or INCONCLUSIVE with an interval that includes 1.0. cheap_query p50 0.979 [0.953..1.007]. - e732cd5 -> this change: cheap_query p50 1.029 [1.019..1.072], INCONCLUSIVE on a busy host (load average up to 22); bound_query p50 1.022, PASS. The insert and delta metrics improved since the baseline. The shared host is about 4x slower per query than the CI runner, so the same hand-off is a smaller share of each query here than there. The remaining ~1.7% of user instructions per query since #214 is the pipeline's own per-request bookkeeping: the boxed request future (one 4-8 KiB allocation), its set node, the request control and the in-flight maps.
Closed
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Combines three wire-protocol items that touch the same code and were reconciled together: W01 canonical request IDs and typed notices, W02 per-connection request pipelining behind ordering barriers (supersedes #210), W03 one request deadline over admission/queue/compute, cancel-by-id, and a non-cancellable commit with committed/unknown outcome. Rebased on main (T01-T03 and W04 already there). Diff: 56 files, +3836/-999. Local: lib, integration, streaming e2e 14/14, snapshot 1149/1149, SDK, clippy, docs green.
Tracker: #199