Skip to content

T1 persistence: save/load/invalidate the universal registry cache (implementation plan) #55

Description

@StefanKarpinski

Implementation plan for persisting the T1 artifact, written so a fresh Claude instance (or human) can execute it without further design work. Prereqs all merged: #49 (Problem/constraints as resolve-time data), #50 (T1 = universal per-registry cache; provider has zero query-dependent parameters), plus the caching design notes on #47 (two comments: "cache T1, everything else per-resolve" and the make-style T0/T1 tiering).

What to build

Two cache tiers, make-style dependencies (see #47):

  • T0, per registry: the parsed provider output for ONE registry — the expensive part (~2.3s parse for General). Keyed by identity header (registry UUID, registry tree hash, T0 format version). A registry changes → re-parse that one T0.
  • T1, per active registry SET: the pkg_info output for all packages (dependency closure of everything + arc-consistency + interchangeability classes) built from the merged T0s. ~1–1.5s to rebuild registry-wide. Its cache record stores the list of T0 identity headers it was built from plus a T1 builder version. Staleness check = header comparison, no content hashing at check time. Any mismatch or set change → rebuild T1 (cheap, per the numbers in Caching design for Pkg integration: tiers, artifacts, and open questions #47/T1 preprocessing: a universal per-registry cache #50).

Post-#50, the T1 artifact is query-independent (ordering, compat, pins, prereleases, yanked, julia bound are all per-resolve), so ONE artifact per registry state serves every resolve — the correctness model is requirement-subset monotonicity (T1 built for all packages serves any reqs; theory in docs/src/theory/layered.md, "filtering for a larger requirement set" + the rewritten cache-tier paragraph).

Where things are

  • src/PkgInfoFiles.jl: existing versioned serialization (format v2) for pkg-info dicts — extend, don't replace. Note PkgInfo gained a classes field in T1 preprocessing: a universal per-registry cache #50, recomputed on load via the 4-arg constructor (it's a function of the conflicts matrix). Decision to make: keep recomputing (~0.45s registry-wide, pass 2 of the class scan is 11–15× pass 1) or bump the format and store class ids. Storing is probably right for a cache whose entire point is amortization — measure both, say which won.
  • bin/Registries.jl: the provider. T0 = its per-registry parse. Today it merges all reachable_registries() up front; the T0 tier needs the parse decomposed per registry with merge-on-load (the merge is cheap; parsing is not). Registry tree hash: Pkg.Registry.RegistryInstance exposes the registry's tree info (reg.tree_info; verify the field name on both supported Pkg versions — bin/ has established patterns for Pkg version differences).
  • bin/resolve.jl: wiring + a --cache[=dir] flag (default e.g. ~/.julia/scratchspaces/<Resolver-UUID>/t1cache via Scratch-like layout — but bin/ avoids extra deps; a dot-dir under DEPOT_PATH[1] is fine).

Blocker

Whole-registry pkg_info currently crashes on Python_jll's malformed compat entry — see the dedicated issue (#54). Fix that first (one-line tolerance in compat_uuid_pairs).

API sketch

cached_pkg_info(; cache_dir = default_cache_dir()) :: Dict{UUID,PkgInfo}
  1. Enumerate active registries → current T0 identity headers.
  2. T1 record exists AND its stored headers == current headers AND builder version matches → load T1, done.
  3. Else: for each registry whose T0 is stale/missing, re-parse and save T0 (atomic rename); build T1 from T0s; save with the header manifest; return.

Concurrency: write-to-temp + atomic rename for both tiers; a stale reader never sees a torn file. No locking needed beyond that (last writer wins; content is deterministic for identical inputs).

Tests

  1. Roundtrip equality: resolves from a loaded T1 are identical to from-scratch resolves — reuse T1 preprocessing: a universal per-registry cache #50's one-artifact-serves-every-query harness (orderings × admission settings × julia bounds × reqs).
  2. Invalidation: mutate one T0 header → exactly that registry re-parses and T1 rebuilds; unchanged headers → pure cache hits (assert via timing or a parse counter).
  3. Set change: add/remove a registry from the active set → T1 rebuild.
  4. Format version bump → clean rebuild, no crash on old files.
  5. Atomicity: a truncated/corrupt cache file → falls back to rebuild, never errors out of resolve.

Numbers to report

Cold (no cache) vs. warm (T1 hit) vs. registry-bumped (one T0 stale) end-to-end bin/resolve.jl times; cache file sizes (whole-registry T1 was unmeasurable pre-#54 — the DiffEq-closure artifact is 17 MiB in memory as a reference point).

🤖 Opened with Claude Code at Stefan's request.
https://claude.ai/code/session_016p2sckwc5Gjjg5LG6gdEk7

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions