ExperimentStore and JobRunner
The port that keeps experiments and each cell a run scored, and the port that runs one in the background.
Ports: ExperimentStore and JobRunner, declared in
indx_interfaces.protocols
over the models in
indx_interfaces.experiments
(ADR-0082,
ADR-0083).
An Experiment is a key, a name, the dataset and router it was made from, its
candidates (each a key, a name and a router Branch, production first), and what it
froze when it was created: the type, the router version, the documents and the labels
they held. A CellResult is one candidate on one document: the rows it scored, the
pages, the pages still needing a person, and the cost and time the result reported, or
the error it failed with.
class ExperimentStore(Protocol): def put(self, owner: OwnerId, experiment: Experiment) -> None: ... def get(self, owner: OwnerId, experiment_id: ExperimentId) -> Experiment | None: ... def list(self, owner: OwnerId) -> tuple[Experiment, ...]: ... def delete(self, owner: OwnerId, experiment_id: ExperimentId) -> bool: ... def put_result( self, owner: OwnerId, experiment_id: ExperimentId, result: CellResult ) -> None: ... def results(self, owner: OwnerId, experiment_id: ExperimentId) -> tuple[CellResult, ...]: ...
class JobRunner(Protocol): def submit(self, job_id: str, work: Callable[[Callable[[], bool]], None]) -> bool: ... def cancel(self, job_id: str) -> bool: ... def active(self, job_id: str) -> bool: ... def close(self) -> None: ...Results are rows of their own rather than a list on the experiment, because a run
writes one cell at a time and two cells must never share a write. Nothing stores a
status: the outstanding cells are the candidates times the documents less the stored
results, so a run that died with its process reads as stopped and resumes when asked.
Three backends ship the store: memory, sqlite and postgres, named by
INDX_EXPERIMENT_STORE.
JobRunner is what lets the domain start work that outlives a request without
building a thread itself. A runner is a plugin like a store: an entry point in
indx.job_runners, named by INDX_JOB_RUNNER, which no first-party package imports.
One ships, indx-jobs-memory: a queue and one worker thread in the server’s process,
gone with it. A deployment that names no runner keeps its experiments and runs none.
Contract
Section titled “Contract”- Owner. Every store method takes an
OwnerIdfirst, and nothing crosses owners. - Identity. Within an owner, an experiment is its key, and a result is its
experiment, candidate and document;
putandput_resultreplace. - Idempotence.
deleteof a key that is not there isFalse, and it removes the experiment’s results with it. - Order.
listreturns experiments by key;resultsby candidate, then digest. - Runner.
submitof a job that is already active starts nothing and isFalse.cancelasks the work to stop before its next step and isFalsefor a job that is not active. Work that raises is logged and never stops the runner.closeasks every job to stop and waits for the one in flight; the app calls it when it stops.
tests/unit/test_store_conformance.py
holds every installed store backend to these rules.