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.
…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.
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.
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
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.
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.
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.
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
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.
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
…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
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.
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.
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.
The 2,000-row .why full proof took 38-48 ms in release, below the 100 ms needed to revoke during it, so the test failed there. Grow the fixture until the fastest of two proofs takes 100 ms, and revoke at a quarter of that, so it holds in debug and release.
Consecutive fact statements of a program (+, -, conditional delete, update) are staged without touching the knowledge graph and committed together under the KG's existing write lock: one effective delta, one WAL record at one logical time, one snapshot publish, and one notification per changed relation after the publish. A statement with other persistent effects (schema, rule, meta command) or the end of the program commits the run first. If any statement of a run fails, none of them is applied and none reports success. Conditional deletes and updates evaluate on the snapshot plus the changes staged before them and record the relations their query reads. The commit refuses a stale read when one of those relations or a rule changed, and the run is staged again with a short random backoff, up to 8 times, before failing with a conflict. Writes to other relations never conflict. A cancelled (timed out) program never commits. insert_tuples_into and delete_tuples_from are one-statement programs on the same path, so there is one write path. Update statements now notify the relation they change instead of "multiple". State-dependent insert rejections (arity, view, vector index, schema) report code validation. Partial #131 (slice #169).
A program that changes persistent state now commits all of it as one transaction on its knowledge graph: facts, schema declarations, persistent rules and rule removals (-name, .rule drop, .rule drop prefix, .rule remove, .rule clear). Statements apply in order to staged copies of the catalogs under the KG's existing write lock, so a fact is checked against a schema declared earlier and a conditional delete or update after a rule change evaluates with the new rule. Everything is written as one WAL record and published as one snapshot: readers see one rule generation with its data. The first failing statement fails the program; nothing is applied, its query does not run, and subscribers are not notified. Commands that cannot join the transaction (.kg, .rel, .rel drop, .clear, .index, .compact, inspection, user, ACL, ontology, subscription) are rejected with code unsupported before any statement runs when they share a program with writes. Programs without writes run as before. - TxnOp::Catalog carries each changed rule's and persistent schema's state after the commit, so replay is idempotent. Catalog files record the revision they reflect and are written atomically; the WAL keeps a catalog change until both files are saved past it, and startup replays what the files miss. KG drop removes its WAL catalog changes. - Every catalog API is a one-statement program: one write path. - .ontology install deploys as one transaction; .ontology remove drops a pack's rules in one transaction and each relation on its own. - Python SDK load() docs state the real transaction boundary. Hot path: fact commits take the same locks as before (the catalog log lock only when a transaction carries catalog changes); queries are untouched. Rule and schema changes now cost a WAL fsync and two atomic catalog saves. Part of #131 (slice #170).
# Conflicts: # .github/workflows/ci.yml # Cargo.toml # docs/content/docs/guides/persistence.mdx # docs/guides/persistence.md # packages/inputlayer-py/src/inputlayer/knowledge_graph.py # src/protocol/handler.rs # src/storage/persist/mod.rs # src/storage/persist/transaction.rs # src/storage/persist/wal.rs # src/storage/persist/wal_record.rs # src/storage_engine/mod.rs # tests/wal_transaction_recovery.rs
…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
jsam
marked this pull request as ready for review
October 3, 2026 20:24
jsam
added a commit
that referenced
this pull request
Oct 3, 2026
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
added a commit
that referenced
this pull request
Oct 3, 2026
This was referenced Oct 3, 2026
Closed
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.
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.
Draft. W04: stream generation/revision, snapshot handoff and recovery; ordered notifications with an epoch cursor; revision-tagged subscription deltas. Depends on W01 request IDs (reconciled in #214) and T01-T03 (#208, on main). Validated through no-mistakes before merge.
Tracker: #199