Storage#
On-disk job folder protocol: identity-addressed outputs, with provenance.
Layout under the orchestrator’s root:
<root>/<job_key>/
manifest.json parameters, callable location, provenance, timings
deps.json the environment fingerprint
metrics/*.csv append-only (step, value), one file per metric
DONE | FAILED completion marker; FAILED carries the traceback
A job’s state is read from its marker rather than from a mutable field, and a job is built in a staging directory that is renamed into place only once its marker is written. A job killed mid-flight therefore leaves no marker at the final path and is simply “not done” on the next pass: there is no folder that is incomplete yet marked complete, and no lock to reason about.
See notes/adr-2026-10-02-orchestrator-v2-a-design.md.
- class krum.orchestration.storage.JobFolder(path: str | PathLike[str])[source]#
Bases:
objectOne job’s directory on disk, and the questions one can ask of it.
- called() dict[str, str] | None[source]#
The functions this job entered, or None if it was not traced.
An untraced job is not the same as a job that called nothing, so the two are told apart: there is nothing to verify in the first case.
- class krum.orchestration.storage.JobStore(root: str | PathLike[str])[source]#
Bases:
objectThe orchestrator’s root directory, holding one folder per job.
- folder_for(key: Hash | str) JobFolder[source]#
The folder a job key maps to, whether or not it exists.
- promote(writer: JobWriter) JobFolder[source]#
Move a staged job to its final path, replacing any earlier attempt.
The replace is a rename, so the final path never holds a partially written job. Removing an earlier attempt first is safe: we only get here having decided to re-run it, and a crash in between leaves no marker, which the next pass reads as “not done”.
- prune_staging() int[source]#
Remove leftover staging directories from crashed jobs.
- Returns:
The number of directories removed.
- class krum.orchestration.storage.JobWriter(store: JobStore, key: str, manifest: Mapping[str, Any])[source]#
Bases:
MetricRecorderBuilds one job in a staging directory, then promotes it atomically.
- adopt(metrics: Mapping[str, dict[str, Any]]) None[source]#
Take on the metrics a child process registered on our behalf.
- class krum.orchestration.storage.MetricRecorder(path: str | PathLike[str], create: bool = False)[source]#
Bases:
objectWhere a running job writes its metrics: a directory and its open sinks.
This is the half of a job that a child process can own. It knows the directory being built and the metrics registered in it, but nothing about promoting that directory, which stays with the parent.
Metrics are flushed as they are pushed, so a crashed job’s partial output stays readable where it was staged, without ever being promoted to the job’s final path where the read path would pick it up.
- close() dict[str, dict[str, Any]][source]#
Close every open sink and return what was registered.
A sink that counted its rows contributes that count, so the size of a metric is recorded rather than needing the file read back to learn it.
A child process returns this to its parent, which is how a job run out of process still ends up with its metrics named in the manifest.
- krum.orchestration.storage.abbreviate(value: Any, length: int = 12) str[source]#
Shorten a value for a human-readable reason, hashes especially.
- krum.orchestration.storage.bind_job(recorder: MetricRecorder) Iterator[MetricRecorder][source]#
Make a job current for the duration of a block.
- krum.orchestration.storage.build_manifest(key: Hash | str, callable: Any, params: Mapping[str, Any], *, owned: Iterable[str] = (), lock: str | PathLike[str] | None = None, start: str | PathLike[str] | None = None) dict[str, Any][source]#
Assemble the manifest recorded alongside a job’s output.
- Parameters:
key – The job key.
callable – The user-defined function the job executes.
params – The bound parameters, in signature order.
owned – The module prefixes the key was computed over.
lock – The lock file to fingerprint.
start – A directory inside the repository to read provenance from, and to discover the lock file from.
- Returns:
The manifest, ready to be written as JSON.
- krum.orchestration.storage.current_job() MetricRecorder | None[source]#
The job being executed in this context, if any.
- krum.orchestration.storage.display_value(encoded: Any) Any[source]#
Render an encoded parameter as a single cell value.
- Parameters:
encoded – A value as returned by
encode_value().- Returns:
The scalar itself, a tagged value’s short name, or a compact string.
- krum.orchestration.storage.drift(recorded: Mapping[str, Any], current: Mapping[str, Any]) list[str][source]#
Explain why a stored result no longer matches the current environment.
- Parameters:
recorded – The witnesses stored with a completed job.
current – The witnesses of the environment now.
- Returns:
One reason per witness that changed, empty when the result still holds.
A job with no recorded witnesses cannot be checked, which counts as a
reason – an unverifiable result is treated as stale, erring towards a
needless re-run rather than a stale answer.
- krum.orchestration.storage.encode_params(params: Mapping[str, Any]) dict[str, Any][source]#
Encode a parameter mapping, preserving its order.
- krum.orchestration.storage.encode_value(value: Any) Any[source]#
Encode a parameter value as JSON, keeping enough to trace it back.
Scalars pass through. A class or function becomes a tagged object carrying both its full location, for traceability, and its short name, which is what reads well as a column value.
- Parameters:
value – The parameter value to encode.
- Returns:
A JSON-serializable encoding of the value.
- krum.orchestration.storage.environment_fingerprint(lock: str | PathLike[str] | None = None, start: str | PathLike[str] | None = None) dict[str, Any][source]#
Describe the environment a job ran in.
The lock file hash stands in for the resolved dependency versions, which is exact and cheap next to sniffing each distribution. This lives in the fingerprint rather than in the job key, so that a dependency bump marks stored results stale without orphaning them under a new key.
- Parameters:
lock – The lock file to hash; discovered from start when omitted.
start – Where to look for a lock file; the current directory by default.
- Returns:
A JSON-serializable description of the environment. A lock file that
cannot be found, or that does not exist, is recorded as None rather
than raising – an environment without one is still worth describing.
- krum.orchestration.storage.find_lock(start: str | PathLike[str] | None = None) Path | None[source]#
Find the nearest uv.lock at or above a directory.
- Parameters:
start – Where to start looking; the current directory by default.
- Returns:
The path to `uv.lock`, or None if there is none above `start`.
- krum.orchestration.storage.git_provenance(start: str | PathLike[str] | None = None) dict[str, Any] | None[source]#
Describe the working tree a job ran from, so an output can be traced back.
A dirty tree is recorded rather than rejected: the orchestrator warns, and leaves it to the user to decide whether the result is publishable.
- Parameters:
start – A directory inside the repository; the current one by default.
- Returns:
The commit and whether the tree was dirty, or None outside a repository.
- krum.orchestration.storage.hash_file(path: Path, cache: bool = True) str[source]#
Hash a file’s contents, as a hex digest.
- Parameters:
path – The file to hash.
cache – Reuse a digest computed earlier for the same path, size and modification time. A sweep fingerprints the same lock file once per job, which is worth not re-reading every time.
- Returns:
The hex digest of the file’s contents.
- krum.orchestration.storage.metric_filename(name: str) str[source]#
Encode a metric name as a file name, reversibly.
Metric names are unrestricted, so they are percent-encoded rather than sanitized, keeping the mapping unambiguous in both directions.
- krum.orchestration.storage.metric_name(filename: str) str[source]#
Recover a metric name from its file name.
- krum.orchestration.storage.read_manifest_params(manifest: Mapping[str, Any]) dict[str, Any][source]#
The manifest’s parameters, rendered one cell value each.
- krum.orchestration.storage.witnesses_of(environment: Mapping[str, Any]) dict[str, Any][source]#
Reduce a recorded environment to the facts a stored result depends on.
These are compared on a later pass to decide whether a completed job is still valid. Deriving them from the recorded environment, rather than computing them separately, is what keeps deps.json from disagreeing with the manifest beside it.
The lock file hash is the one witness that is not already covered by the job key: third-party code is folded into a key as a location only, never by content, so a dependency bump is invisible to the key by design. The interpreter and the optimization flag are recorded too, being cheap and robust should key derivation ever stop depending on bytecode.
The interpreter is compared on major and minor only. Bytecode is stable across patch releases, so a patch bump is not a reason to discard results.
- Parameters:
environment – An environment as recorded by
environment_fingerprint().- Returns:
The witnesses, as comparable JSON values.