…n in-memory kind, a conformance suite (#173)
The queue, releases, inputs and run records are kept through one store
contract (store_contract.go): reads by id and request key, the oldest
queued run, the chain's reads, and one serializable transaction whose
writes are create-once (a run under its id and request key, a release, a
chain entry), a run's versioned update, and journal entries. Faults are
typed (absent, taken, conflict, durability, unreadable, refused) and keep
their cause's text.
The sqlite kind (store_sqlite.go) is the code that kept them, moved: the
same statements, transactions, pragmas, schema, migration and lock, in the
same order at opening. The boot line's directory reaches the store only
through its constructor; a kind registry holds sqlite as the default, and
no other kind is selectable. State the contract does not cover yet stays
on the kind's database handle, named in docs/design/store-interface.md.
A queued run's change (the dispatcher's claim, or its end in the queue)
now requires the store to hold it queued, so a claim is exclusive by the
store and not only by the one dispatcher; with one dispatcher it never
refuses. Close gives the attempts directory up before the store.
An in-memory kind (test only) and a conformance suite run against both
kinds in the unit-tests step, which now requires both kinds' cases to
have run and passed.
Signed-off-by: kikashy <35789537+kikashy@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Closes #173. Design note:
docs/design/store-interface.md.What moved
internal/runner/store_contract.go). Reads, outside or inside a transaction:release(id),job(id),requestedRun(job, caller, key)(the request's digest, and the record decoded only when asked) andentrySequence(run). Reads outside a transaction only:run(id),oldestQueued(),entryLine(run)andchainPage(after, limit). Writes go throughupdate(fn), one serializable transaction that keeps whatfnwrote only iffnreturns nil and the commit succeeds. Inside a transaction:activeRuns,createRun(once per id and once per request key),putRun(r, expected)(a versioned update: the run's state is its version),runningRuns(in queue order),lastEntry,createEntry(once per sequence and run; a sequence below 1 is refused),createRelease(once per id) andappendJournal. Faults are typed:absent,taken,conflict,durability,unreadableandrefused. An error keeps its cause's text, anderrors.Isstill reaches the cause (sql.ErrNoRowsincluded). There is also a kind registry:sqliteis the default, and the boot line names no kind, so no other kind can be selected.internal/runner/store_sqlite.go). This is the code that kept this state, moved. The lock, the database file's create-once and private check,sql.Openwith one connection, the pragmas and schema, the trigger and chain schemas, the identity check and the migration (migrateStorebecomes(*sqliteStore).migrate) all run in the same order. The Runner's working directories run in between throughafterLock, and the manual-admission check throughopened. The boot line's directory reaches the store only throughopenSQLiteStoreandsqlitePreparedStore.release/job/run, the submission's transaction and its earlier mapping-v2 read,dispatchNext's read of the queue,changeRun,finishRunandappendRunEntry,restart(its occurrences, which are outside the contract, run in the same transaction throughsqliteTxOf),recordStop,saveRelease,runEntry/runEntrySequence/chainPage, and the 404 mapping (absent).internal/runner/store_memory_test.go, test only) and the conformance suite (internal/runner/store_conformance_test.go), run against both kinds.unit-testsstep undergo test -race ./.... The step then rerunsTestStoreKindsConform -vand fails unless bothsqliteandmemorypassed (scripts/checks/unit-tests.sh, whichjpdev.jsonalready maps). There is no new job and no new directory to map.Not behind the contract yet: triggers, occurrences, job creation and revision, briefs, workflow executions, the manual-admission journal, metadata, the journal's reads, and the listings. These stay on the sqlite kind's database handle. Manual store preparation, offline relocation and the source worker's store hold their SQLite files themselves. Section 6 of the design note lists each one, and every statement left on
runs,releasesandjobsoutside the kind.What is unchanged, and the evidence
store.go,events.goandrun_chain.goare the same set as those added instore_sqlite.go: 23 distinct, none removed without being added and none added without being removed. A script compared them overgit diff origin/main...HEAD. The multi-line pragma and schema block is byte-identical. The migration's body is identical apart froms.db.becomingk.db..errKeepNothing).apiError,errNoAuditBytes,errManualProtocol) come back unchanged, andTestACompletionWithoutTheRecordsBytesIsNotRecordedstill compareserrNoAuditBytesby identity.run_chain_test.gopassed a*sql.TxtoappendRunEntry, which is now a moved internal; its two call sites wrap it as&sqliteTx{...}, and its assertions are unchanged.What differs
changeRun(the dispatcher's claim, and a run's end in the queue: expired, or set aside) now requires the store to hold the run queued. Otherwise it writes nothing and returnsconflict. This is the claim's fence: a run leavesqueuedonce. The comparison sits between the two statements the claim already made. With one dispatcher per store it never refuses, and every existing test that callschangeRunpasses.finishRunandrestartwrite whatever the state is, as before. There is no lease, because a claim does not expire and the next process's lock ends a run left running.Closenow closes the attempts directory before the store. It used to close it between the database and the lock. The lock is still released last.Conformance cases (
TestStoreKindsConform, both kinds)A release is created once · a run is queued once under its id and once under its request key · the queue is served in the order runs were queued (ids sort against it) · a run read queued is claimed once, and an update expecting another state writes nothing · concurrent claimers (4 producers, 6 claimers, 40 runs) grant every run once and lose none, retrying on
conflictwith no lock in the test · a transaction that fails keeps nothing · a commit that fails isdurability, never a success, and keeps nothing (the sqlite kind's commit seam, the memory kind's seam) · the chain gives a run one entry and is read in order, and a completion without its record's bytes is not recorded · chain entries are read in order of sequence whatever order they were made in · nothing held readsabsent· a record that does not read isunreadable. sqlite only: a reopened store holds what it held (a claimed run still running, a completed run and its entry, the queue order, the release, the request, the journal) · one process holds the store.Mutants
Each mutant ran on a scratch copy against
go test -race -run '^TestStoreKindsConform$', and each copy was deleted after its run. A copy that failed to build counted as not run.putRun's expected-state check dropped (version check)update(claim exclusivity)-race; also the concurrent case without-race, 3 of 3 runschangeQueuedRunclaims at any statecreateRun's create-once check droppedtakentyping of PRIMARY KEY/UNIQUE dropped (create-once)updateignores a failed commit (durability)oldestQueuedordered by id (list order)runningRunsordered by id (list order)flock LOCK_SH)updateworks on the live state, not a copy (atomicity)chainPageordered by run (list order)putRun's expected-state check brokenabsenttyping droppedunreadabletyping droppedSelf-checks (AGENTS.md)
(a) The boundary path
internal/runnershows no admission or verdict change. The change is the store. The one added refusal ischangeRun'sconflict, a storage error that stops the dispatcher like any other, and it never fires with one dispatcher. (b)–(f) do not apply: no schema member, vector, second reader, mutation row, request refusal or checkpoint member is added. (g) The design note's operation tables quote the kind's statements as they are instore_sqlite.go, and its sections 6 and 8 were checked against the code.Checks
The installed development kit (
jpdev0.1.0 on this machine) rejects this repository'sdev.json(itshandovermember) and has no--changedorpush. I ran the repository's own runner under the same caps (the machine lock,ulimit -v 12000000, a timeout, a GOCACHE of the build's own) and pushed with theghcredential helper form AGENTS.md gives.scripts/dev/check.sh --changed origin/mainselected 11 of 15 steps:unit-testsdid not fail on a test.go test -race ./...reached go test's default 10-minute limit ininternal/runneron this machine, with every test that had run passing. On CI, main's last run took 449 s and 517 s for this package. I ran the step once more with only-timeout 40madded to itsgo test -race ./...(a scratch copy of the script; the committed script is unchanged), and then the three steps that had not run:Before that, with the pinned binaries, the targeted store tests (
Restart|Durab|Idempot|Chain|Journal|Interrupt|Queue|Dispatch|Completion|Append|Release|Manual|Store) passed. The conformance suite alone takes about 5 s under-race.🤖 Generated with Claude Code