Class: Rigor::Cache::IncrementalSnapshot
- Inherits:
-
Object
- Object
- Rigor::Cache::IncrementalSnapshot
- Defined in:
- lib/rigor/cache/incremental_snapshot.rb
Overview
ADR-46 — disk persistence for the incremental analyzer's per-file state, so a --incremental session
survives across processes (one rigor check invocation reads the prior run's per-file diagnostics +
dependency graph, re-analyzes only the changed closure, and serves the rest from disk).
Unlike ADR-45's whole-run cache (record-and-validate ONE entry, invalidated by any analyzed-file change), this snapshot is loaded UNCONDITIONALLY when the global fingerprint matches — the per-file digests inside it drive the incremental re-analysis decision; they do not gate the load. The fingerprint captures the inputs whose change requires a full rebuild — the resolved configuration, the RBS environment, the engine version and (on a checkout) the engine's own source — but NOT the analyzed source contents. A fingerprint mismatch (config / gem / version / engine change) drops the snapshot and forces a full re-analysis, the conservative direction.
An engine change drops the WHOLE snapshot rather than part of it, which is a soundness point before it
is a simplicity one: every section here except digests is a value the analyzer computed, so a changed
engine can move any of it — including the dependency edges, where a new engine recording an edge the old
one missed would let a recheck skip the very file that needed re-analysing. Retaining the one
engine-independent section would not pay either: digests is 2.5% of a 2.5 MB snapshot of this repo,
and re-deriving it is a file-digest walk costing ~0.2% of the full run it would be saving.
Every operation is fault-tolerant: a missing, unreadable, schema-mismatched, fingerprint-mismatched, or corrupt snapshot loads as nil (→ a cold full run), and a write failure is swallowed (→ the next run is cold). A cache must never break a run (the ADR-45 invariant).
Defined Under Namespace
Classes: Payload
Constant Summary collapse
- SCHEMA =
Bump when the on-disk shape changes so stale snapshots are ignored rather than mis-deserialized. 5: the blob is zlib-deflated (ADR-54 WD2 parity with
Storeentries — the snapshot is the one cache artefact that does not go throughStore); a raw pre-5 blob fails the inflate and loads as nil, the usual fault-tolerant cold-run path. 6: adds the ADR-85 WD2seed_bundlessection (per-file discovery contributions with(node_id, name, fingerprint)def-node handles); a pre-6 blob mismatches the SCHEMA gate and loads as nil (a clean cold rebuild — no migration). 7: the seed bundle gains asingleton_def_sourcestable (ADR-46 slice 4 extended to class/singleton methods) ANDdigestsswitches to ADR-87 packed stat entries; a pre-7 blob mismatches the gate and loads as nil (clean cold rebuild). 8: each seed bundle additionally gains a comment-strippedcode_fingerprintfor the B1 bundle-equality gate; a pre-8 blob mismatches and loads as nil (clean cold rebuild). 9: adds the ADR-88 WD1plugin_fact_digest(a fingerprint of the plugin fact SURFACE — ADR-9 facts, ADR-60 producer values, andincremental_state_fingerprinthooks — that a cached diagnostic can depend on but the global fingerprint does not capture); a pre-9 blob mismatches the SCHEMA gate and loads as nil (a clean cold rebuild — no migration). 10: ADR-89 WD1 adds a per-filedeclaration_signatureto each seed bundle (the per-def parameter-shape / visibility / ancestry surface the declaration-stability gate compares) and WD2 addsreturn_summaries(per-def observed-key return descriptors + mutation-effect sets the behavioural-stability gate compares); a pre-10 blob mismatches the SCHEMA gate and loads as nil (a clean cold rebuild — no migration). 11: ADR-67 WD6c lift addsparam_table(the inferred-param seed table the run's diagnostics were computed under, diffed on the next recheck to invalidate a callee whose seeds moved because a caller changed); a pre-11 blob mismatches the SCHEMA gate and loads as nil (a clean cold rebuild — no migration). 11
Instance Attribute Summary collapse
-
#path ⇒ Object
readonly
Returns the value of attribute path.
Class Method Summary collapse
-
.fingerprint(configuration:, roots:) ⇒ Object
The global fingerprint that gates a snapshot load: a digest of the inputs whose change requires a full rebuild — the engine version + schema, the engine's own SOURCE when the version does not pin it, the resolved configuration, the analysis roots (the path arguments, e.g.
["lib"], NOT the expanded file list — so a snapshot is keyed to an invocation's roots but adding / removing a file under them is handled incrementally by the session, not a full rebuild), the resolved gem set (Gemfile.lock/rbs_collection), and the project's own RBS (signature_pathsfile contents).
Instance Method Summary collapse
-
#initialize(root:) ⇒ IncrementalSnapshot
constructor
A new instance of IncrementalSnapshot.
-
#load(fingerprint:) ⇒ Object
The stored Payload, or nil when absent / unreadable / schema or fingerprint mismatch / corrupt.
-
#load_any(fingerprints:) ⇒ Array(String, Payload)?
Issue #134 slice 2 — the same load against SEVERAL acceptable fingerprints, reading the blob once.
-
#save(fingerprint:, payload:) ⇒ Object
Persist
payloadunderfingerprint.
Constructor Details
#initialize(root:) ⇒ IncrementalSnapshot
Returns a new instance of IncrementalSnapshot.
153 154 155 |
# File 'lib/rigor/cache/incremental_snapshot.rb', line 153 def initialize(root:) @path = File.join(root.to_s, "incremental", "snapshot.bin") end |
Instance Attribute Details
#path ⇒ Object (readonly)
Returns the value of attribute path.
157 158 159 |
# File 'lib/rigor/cache/incremental_snapshot.rb', line 157 def path @path end |
Class Method Details
.fingerprint(configuration:, roots:) ⇒ Object
The global fingerprint that gates a snapshot load: a digest of the inputs whose change requires a full
rebuild — the engine version + schema, the engine's own SOURCE when the version does not pin it, the
resolved configuration, the analysis roots (the path arguments, e.g. ["lib"], NOT the expanded
file list — so a snapshot is keyed to an invocation's roots but adding / removing a file under them is
handled incrementally by the session, not a full rebuild), the resolved gem set (Gemfile.lock /
rbs_collection), and the project's own RBS (signature_paths file contents). Built WITHOUT
constructing the RBS environment so the warm path can gate the load cheaply, before the costly env
build. The --verify-incremental gate is the safety net for any under-capture (it would surface as an
incremental-vs-full mismatch). Returns nil on any error → the caller falls back to a non-persisted run.
Issue #285, wired here by #289 — every value in this snapshot is something the ANALYZER computed, so
Rigor::VERSION alone is not enough to identify what produced it. A version pins the engine's bytes
for a RubyGems install and for nothing else, so on a checkout a warm recheck served diagnostics a
pre-edit analyzer had computed: editing lib/rigor/inference/*.rb moved no ANALYZED file, the changed
set came back empty, and 357 unchanged files replayed their old answers.
EngineSource.process_identity closes it, and answers nil for a version-pinned tree — which adds no
part, so a released gem's fingerprint is byte-identical to the pre-#289 one and its warm snapshots
survive the upgrade untouched.
113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 |
# File 'lib/rigor/cache/incremental_snapshot.rb', line 113 def self.fingerprint(configuration:, roots:) parts = [ "engine:#{Rigor::VERSION}:#{SCHEMA}", "config:#{Digest::SHA256.hexdigest(Marshal.dump(configuration.to_h))}", "roots:#{Array(roots).map(&:to_s).sort.join("\n")}", "gems:#{digest_file_if_present('Gemfile.lock')}", "rbs_collection:#{digest_file_if_present('rbs_collection.lock.yaml')}", "sig:#{digest_signature_paths(configuration.signature_paths)}" ] identity = EngineSource.process_identity parts << "engine-source:#{identity}" if identity Digest::SHA256.hexdigest(parts.join("\x00")) rescue StandardError # {EngineSource::Unavailable} lands here too, and nil is the answer it requires rather than one it # merely tolerates: an engine we cannot identify must DISABLE the snapshot, never fall back to the # version-only key that is the blind spot above. Nil does disable it on both sides — # {Analysis::IncrementalSession} guards the load AND the save on the fingerprint, so nothing stale is # read and no nil-keyed blob is written for the next equally-unidentifiable run to match against. nil end |
Instance Method Details
#load(fingerprint:) ⇒ Object
The stored Payload, or nil when absent / unreadable / schema or fingerprint mismatch / corrupt. Never raises.
161 162 163 164 165 166 |
# File 'lib/rigor/cache/incremental_snapshot.rb', line 161 def load(fingerprint:) data = read_data return nil unless data && data[:fingerprint] == fingerprint payload_from(data) end |
#load_any(fingerprints:) ⇒ Array(String, Payload)?
Issue #134 slice 2 — the same load against SEVERAL acceptable fingerprints, reading the blob once.
A reader that did not itself write the snapshot cannot know which analysis ROOTS it was written under
(rigor check --incremental lib and a bare rigor check --incremental produce different fingerprints
for the same project), and #load would have to re-inflate + re-unmarshal the whole blob per candidate.
The fingerprint that matched is returned alongside the payload so the caller can mix it into ITS own
cache key — the snapshot's identity is exactly what "these dependency edges came from that world" means.
177 178 179 180 181 182 183 184 185 |
# File 'lib/rigor/cache/incremental_snapshot.rb', line 177 def load_any(fingerprints:) data = read_data return nil if data.nil? matched = Array(fingerprints).compact.find { |candidate| data[:fingerprint] == candidate } return nil if matched.nil? [matched, payload_from(data)] end |
#save(fingerprint:, payload:) ⇒ Object
Persist payload under fingerprint. Writes via a temp file + atomic rename so a concurrent reader
never sees a half-written snapshot. Returns true on success, false on any failure (never raises).
216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 |
# File 'lib/rigor/cache/incremental_snapshot.rb', line 216 def save(fingerprint:, payload:) FileUtils.mkdir_p(File.dirname(@path)) raw = Marshal.dump( schema: SCHEMA, fingerprint: fingerprint, cache: payload.cache, sources: payload.sources, digests: payload.digests, analyzed: payload.analyzed, symbol_sources: payload.symbol_sources, ancestry_sources: payload.ancestry_sources, symbol_fingerprints: payload.symbol_fingerprints, missing: payload.missing, class_decls: payload.class_decls, seed_bundles: payload.seed_bundles, plugin_fact_digest: payload.plugin_fact_digest, return_summaries: payload.return_summaries, param_table: payload.param_table ) blob = Zlib::Deflate.deflate(raw) tmp = "#{@path}.#{Process.pid}.tmp" File.binwrite(tmp, blob) File.rename(tmp, @path) true rescue StandardError false end |