rigortype 0.2.8 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +2 -2
- data/data/core_overlay/csv.rbs +28 -0
- data/data/core_overlay/psych.rbs +22 -0
- data/docs/handbook/01-getting-started.md +9 -1
- data/docs/handbook/02-everyday-types.md +4 -1
- data/docs/handbook/03-narrowing.md +2 -1
- data/docs/handbook/04-tuples-and-shapes.md +7 -3
- data/docs/handbook/06-classes.md +18 -9
- data/docs/handbook/08-understanding-errors.md +4 -3
- data/docs/handbook/11-sig-gen.md +11 -1
- data/docs/handbook/appendix-mypy.md +4 -3
- data/docs/install.md +14 -6
- data/docs/manual/01-installation.md +99 -4
- data/docs/manual/02-cli-reference.md +65 -19
- data/docs/manual/03-configuration.md +34 -2
- data/docs/manual/04-diagnostics.md +23 -0
- data/docs/manual/07-plugins.md +5 -5
- data/docs/manual/09-editor-integration.md +4 -4
- data/docs/manual/11-ci.md +51 -0
- data/docs/manual/12-caching.md +65 -0
- data/docs/manual/14-rails-quickstart.md +4 -2
- data/docs/manual/15-type-protection-coverage.md +21 -0
- data/docs/manual/plugins/rigor-actionpack.md +1 -1
- data/docs/manual/plugins/rigor-activerecord.md +12 -5
- data/docs/manual/plugins/rigor-rails-routes.md +11 -0
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +9 -1
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +5 -3
- data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +128 -0
- data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +60 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -2
- data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +117 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +125 -0
- data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +2 -1
- data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +283 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +10 -1
- data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +93 -0
- data/lib/rigor/analysis/check_rules.rb +557 -95
- data/lib/rigor/analysis/dependency_recorder.rb +93 -9
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/incremental_session.rb +456 -51
- data/lib/rigor/analysis/path_expansion.rb +42 -0
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +188 -0
- data/lib/rigor/analysis/rule_catalog.rb +226 -1
- data/lib/rigor/analysis/run_cache_key.rb +68 -0
- data/lib/rigor/analysis/run_cache_probe.rb +72 -0
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +181 -28
- data/lib/rigor/analysis/runner/pool_coordinator.rb +77 -15
- data/lib/rigor/analysis/runner/project_pre_passes.rb +95 -49
- data/lib/rigor/analysis/runner/run_snapshots.rb +9 -2
- data/lib/rigor/analysis/runner.rb +343 -68
- data/lib/rigor/analysis/severity_stamp.rb +43 -0
- data/lib/rigor/analysis/worker_session.rb +30 -2
- data/lib/rigor/bleeding_edge.rb +37 -8
- data/lib/rigor/builtins/regex_refinement.rb +36 -1
- data/lib/rigor/cache/descriptor.rb +88 -28
- data/lib/rigor/cache/file_digest.rb +158 -0
- data/lib/rigor/cache/incremental_snapshot.rb +41 -5
- data/lib/rigor/cache/rbs_descriptor.rb +56 -10
- data/lib/rigor/cache/store.rb +207 -60
- data/lib/rigor/ci_detector.rb +90 -0
- data/lib/rigor/cli/annotate_command.rb +15 -9
- data/lib/rigor/cli/baseline_command.rb +4 -1
- data/lib/rigor/cli/check_command.rb +212 -27
- data/lib/rigor/cli/coverage_command.rb +56 -30
- data/lib/rigor/cli/diagnostic_formats.rb +4 -1
- data/lib/rigor/cli/docs_command.rb +0 -30
- data/lib/rigor/cli/doctor_command.rb +163 -23
- data/lib/rigor/cli/lsp_command.rb +5 -0
- data/lib/rigor/cli/mcp_command.rb +5 -0
- data/lib/rigor/cli/plugins_command.rb +36 -6
- data/lib/rigor/cli/plugins_renderer.rb +44 -12
- data/lib/rigor/cli/probe_environment.rb +85 -0
- data/lib/rigor/cli/protection_fork_scan.rb +55 -0
- data/lib/rigor/cli/protection_report.rb +7 -1
- data/lib/rigor/cli/sig_gen_command.rb +36 -9
- data/lib/rigor/cli/skill_command.rb +1 -32
- data/lib/rigor/cli/trace_command.rb +5 -6
- data/lib/rigor/cli/type_of_command.rb +9 -8
- data/lib/rigor/cli/type_scan_command.rb +8 -8
- data/lib/rigor/cli.rb +15 -4
- data/lib/rigor/config_audit.rb +48 -2
- data/lib/rigor/configuration/severity_profile.rb +31 -0
- data/lib/rigor/configuration.rb +156 -8
- data/lib/rigor/environment/default_libraries.rb +35 -0
- data/lib/rigor/environment/missing_gem_constant_index.rb +128 -0
- data/lib/rigor/environment/rbs_loader.rb +164 -5
- data/lib/rigor/environment.rb +71 -43
- data/lib/rigor/inference/budget_trace.rb +77 -2
- data/lib/rigor/inference/closure_escape_analyzer.rb +14 -1
- data/lib/rigor/inference/def_handle.rb +23 -0
- data/lib/rigor/inference/def_node_resolver.rb +90 -0
- data/lib/rigor/inference/def_return_typer.rb +2 -1
- data/lib/rigor/inference/expression_typer.rb +379 -113
- data/lib/rigor/inference/fork_map.rb +87 -0
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +30 -5
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +200 -0
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +22 -16
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +33 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +11 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +42 -9
- data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +5 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +28 -35
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +40 -8
- data/lib/rigor/inference/method_dispatcher.rb +57 -1
- data/lib/rigor/inference/mutation_widening.rb +42 -8
- data/lib/rigor/inference/narrowing.rb +245 -17
- data/lib/rigor/inference/parameter_inference_collector.rb +70 -10
- data/lib/rigor/inference/project_patched_scanner.rb +2 -1
- data/lib/rigor/inference/rbs_type_translator.rb +8 -1
- data/lib/rigor/inference/scope_indexer.rb +460 -49
- data/lib/rigor/inference/statement_evaluator.rb +111 -17
- data/lib/rigor/inference/struct_fold_safety.rb +48 -11
- data/lib/rigor/inference/synthetic_method_scanner.rb +15 -14
- data/lib/rigor/inference/void_origin.rb +25 -0
- data/lib/rigor/inference/void_tail_summary.rb +220 -0
- data/lib/rigor/language_server/completion_provider.rb +2 -1
- data/lib/rigor/language_server/document_symbol_provider.rb +2 -1
- data/lib/rigor/language_server/folding_range_provider.rb +2 -1
- data/lib/rigor/language_server/selection_range_provider.rb +2 -1
- data/lib/rigor/language_server/signature_help_provider.rb +2 -1
- data/lib/rigor/plugin/base.rb +9 -22
- data/lib/rigor/plugin/inflector.rb +12 -3
- data/lib/rigor/plugin/io_boundary.rb +5 -1
- data/lib/rigor/plugin/isolation.rb +81 -11
- data/lib/rigor/plugin/load_error.rb +10 -1
- data/lib/rigor/plugin/loader.rb +96 -14
- data/lib/rigor/plugin/node_rule_walk.rb +5 -3
- data/lib/rigor/plugin/registry.rb +32 -23
- data/lib/rigor/plugin.rb +26 -0
- data/lib/rigor/protection/mutator.rb +3 -2
- data/lib/rigor/reflection.rb +64 -0
- data/lib/rigor/runtime/jit.rb +128 -0
- data/lib/rigor/scope/discovery_index.rb +12 -2
- data/lib/rigor/scope.rb +122 -28
- data/lib/rigor/sig_gen/classification.rb +5 -1
- data/lib/rigor/sig_gen/generator.rb +38 -6
- data/lib/rigor/sig_gen/observation_collector.rb +4 -3
- data/lib/rigor/sig_gen/rbs_validity.rb +44 -0
- data/lib/rigor/sig_gen/renderer.rb +10 -0
- data/lib/rigor/sig_gen/write_result.rb +9 -4
- data/lib/rigor/sig_gen/writer.rb +24 -2
- data/lib/rigor/source/node_children.rb +116 -0
- data/lib/rigor/source/node_locator.rb +3 -1
- data/lib/rigor/source/node_walker.rb +4 -2
- data/lib/rigor/source.rb +1 -0
- data/lib/rigor/type/difference.rb +28 -24
- data/lib/rigor/type/hash_shape.rb +34 -10
- data/lib/rigor/version.rb +1 -1
- data/lib/rigortype.rb +24 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +4 -2
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +6 -4
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +23 -8
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +5 -3
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +21 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +13 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +65 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +20 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +172 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +22 -8
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +3 -1
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +4 -2
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +32 -0
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +38 -6
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +3 -1
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +3 -1
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +10 -0
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +11 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +3 -1
- data/plugins/rigor-rails/lib/rigor-rails.rb +11 -12
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +191 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +3 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +19 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +48 -15
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +42 -12
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +83 -9
- data/plugins/rigor-rbs-inline/lib/rigor-rbs-inline.rb +9 -5
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +3 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +3 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +11 -0
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +4 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +4 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +3 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +74 -14
- data/sig/prism_node_children.rbs +9 -0
- data/sig/rigor/environment.rbs +1 -0
- data/sig/rigor/inference.rbs +7 -0
- data/sig/rigor/plugin/base.rbs +2 -3
- data/sig/rigor/reflection.rbs +1 -0
- data/sig/rigor/scope.rbs +12 -1
- data/skills/rigor-ci-setup/SKILL.md +10 -0
- data/skills/rigor-editor-setup/SKILL.md +11 -0
- data/skills/rigor-next-steps/SKILL.md +23 -2
- data/skills/rigor-plugin-author/SKILL.md +3 -3
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +2 -2
- data/skills/rigor-plugin-review/SKILL.md +2 -2
- data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +5 -4
- data/skills/rigor-project-init/SKILL.md +29 -3
- data/skills/rigor-project-init/references/01-detect.md +1 -1
- data/skills/rigor-project-init/references/02-configure.md +39 -5
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +20 -0
- data/skills/rigor-project-init/references/05-jit-performance.md +125 -0
- metadata +49 -24
- data/lib/rigor/cli/ci_detector.rb +0 -89
|
@@ -24,8 +24,22 @@ module Rigor
|
|
|
24
24
|
# Bump when the on-disk shape changes so stale snapshots are ignored rather than mis-deserialized. 5:
|
|
25
25
|
# the blob is zlib-deflated (ADR-54 WD2 parity with `Store` entries — the snapshot is the one cache
|
|
26
26
|
# artefact that does not go through `Store`); a raw pre-5 blob fails the inflate and loads as nil, the
|
|
27
|
-
# usual fault-tolerant cold-run path.
|
|
28
|
-
|
|
27
|
+
# usual fault-tolerant cold-run path. 6: adds the ADR-85 WD2 `seed_bundles` section (per-file discovery
|
|
28
|
+
# contributions with `(node_id, name, fingerprint)` def-node handles); a pre-6 blob mismatches the
|
|
29
|
+
# SCHEMA gate and loads as nil (a clean cold rebuild — no migration). 7: the seed bundle gains a
|
|
30
|
+
# `singleton_def_sources` table (ADR-46 slice 4 extended to class/singleton methods) AND `digests`
|
|
31
|
+
# switches to ADR-87 packed stat entries; a pre-7 blob mismatches the gate and loads as nil (clean cold
|
|
32
|
+
# rebuild). 8: each seed bundle additionally gains a comment-stripped `code_fingerprint` for the B1
|
|
33
|
+
# bundle-equality gate; a pre-8 blob mismatches and loads as nil (clean cold rebuild). 9: adds the ADR-88
|
|
34
|
+
# WD1 `plugin_fact_digest` (a fingerprint of the plugin fact SURFACE — ADR-9 facts, ADR-60 producer
|
|
35
|
+
# values, and `incremental_state_fingerprint` hooks — that a cached diagnostic can depend on but the
|
|
36
|
+
# global fingerprint does not capture); a pre-9 blob mismatches the SCHEMA gate and loads as nil (a clean
|
|
37
|
+
# cold rebuild — no migration). 10: ADR-89 WD1 adds a per-file `declaration_signature` to each seed
|
|
38
|
+
# bundle (the per-def parameter-shape / visibility / ancestry surface the declaration-stability gate
|
|
39
|
+
# compares) and WD2 adds `return_summaries` (per-def observed-key return descriptors + mutation-effect
|
|
40
|
+
# sets the behavioural-stability gate compares); a pre-10 blob mismatches the SCHEMA gate and loads as
|
|
41
|
+
# nil (a clean cold rebuild — no migration).
|
|
42
|
+
SCHEMA = 10
|
|
29
43
|
|
|
30
44
|
# The persisted per-file state.
|
|
31
45
|
# `cache` maps an analyzed file to its diagnostics.
|
|
@@ -39,9 +53,25 @@ module Rigor
|
|
|
39
53
|
# ADR-46 slice 3:
|
|
40
54
|
# `missing` maps a consumer to Set<"kind:name"> it looked up and missed.
|
|
41
55
|
# `class_decls` maps a path to Set<qualified class name> it declares.
|
|
56
|
+
# ADR-85 WD2:
|
|
57
|
+
# `seed_bundles` maps an analyzed path to its per-file discovery contribution (plain-data tables +
|
|
58
|
+
# `(node_id, name, fingerprint)` def-node handles + content digest), so a warm recheck rebuilds the
|
|
59
|
+
# cross-file index by folding bundles instead of parsing every file.
|
|
60
|
+
# ADR-88 WD1:
|
|
61
|
+
# `plugin_fact_digest` is a SHA-256 hex fingerprint of the plugin fact surface at the run that wrote the
|
|
62
|
+
# snapshot (or nil for a plugin-free project). A warm recheck recomputes it and, on a mismatch, discards
|
|
63
|
+
# the snapshot and runs a full analysis — the guard for a plugin sig/catalog edit that the global
|
|
64
|
+
# fingerprint cannot see.
|
|
65
|
+
# ADR-89 WD2:
|
|
66
|
+
# `return_summaries` maps a `[path, "Class#method" | "Class.method"]` to the callee's persisted
|
|
67
|
+
# behavioural surface `{ keys:, returns:, effects: }` — the observed `[receiver, arg_types]` call keys
|
|
68
|
+
# (Marshal-clean type tuples), their `describe(:short)` return descriptors, and the content-mutated
|
|
69
|
+
# parameter positions. A recheck re-evaluates a declaration-stable changed callee at these keys and,
|
|
70
|
+
# when every return + the effects are unchanged, skips its symbol dependents.
|
|
42
71
|
Payload = Data.define(:cache, :sources, :digests, :analyzed,
|
|
43
72
|
:symbol_sources, :ancestry_sources, :symbol_fingerprints,
|
|
44
|
-
:missing, :class_decls
|
|
73
|
+
:missing, :class_decls, :seed_bundles, :plugin_fact_digest,
|
|
74
|
+
:return_summaries)
|
|
45
75
|
|
|
46
76
|
# The global fingerprint that gates a snapshot load: a digest of the inputs whose change requires a full
|
|
47
77
|
# rebuild — the engine version + schema, the resolved configuration, the analysis **roots** (the path
|
|
@@ -104,7 +134,10 @@ module Rigor
|
|
|
104
134
|
ancestry_sources: data[:ancestry_sources] || {},
|
|
105
135
|
symbol_fingerprints: data[:symbol_fingerprints] || {},
|
|
106
136
|
missing: data[:missing] || {},
|
|
107
|
-
class_decls: data[:class_decls] || {}
|
|
137
|
+
class_decls: data[:class_decls] || {},
|
|
138
|
+
seed_bundles: data[:seed_bundles] || {},
|
|
139
|
+
plugin_fact_digest: data[:plugin_fact_digest],
|
|
140
|
+
return_summaries: data[:return_summaries] || {}
|
|
108
141
|
)
|
|
109
142
|
rescue StandardError
|
|
110
143
|
nil
|
|
@@ -122,7 +155,10 @@ module Rigor
|
|
|
122
155
|
ancestry_sources: payload.ancestry_sources,
|
|
123
156
|
symbol_fingerprints: payload.symbol_fingerprints,
|
|
124
157
|
missing: payload.missing,
|
|
125
|
-
class_decls: payload.class_decls
|
|
158
|
+
class_decls: payload.class_decls,
|
|
159
|
+
seed_bundles: payload.seed_bundles,
|
|
160
|
+
plugin_fact_digest: payload.plugin_fact_digest,
|
|
161
|
+
return_summaries: payload.return_summaries
|
|
126
162
|
)
|
|
127
163
|
blob = Zlib::Deflate.deflate(raw)
|
|
128
164
|
tmp = "#{@path}.#{Process.pid}.tmp"
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "digest"
|
|
4
4
|
|
|
5
5
|
require_relative "descriptor"
|
|
6
|
+
require_relative "file_digest"
|
|
6
7
|
|
|
7
8
|
module Rigor
|
|
8
9
|
module Cache
|
|
@@ -17,15 +18,36 @@ module Rigor
|
|
|
17
18
|
Descriptor.new(
|
|
18
19
|
gems: [rbs_gem_entry],
|
|
19
20
|
files: file_entries(loader),
|
|
20
|
-
configs:
|
|
21
|
+
configs: config_entries(loader)
|
|
21
22
|
)
|
|
22
23
|
end
|
|
23
24
|
|
|
25
|
+
# Lazy-files variant for the ADR-45 run-diagnostics record-and-validate cache. The cache KEY reads only
|
|
26
|
+
# `gems` + `configs` ({Runner#run_key_descriptor}); the RBS signature-tree `files` are read solely on a
|
|
27
|
+
# MISS, by the dependency descriptor ({Runner#run_dependency_descriptor}). So a warm HIT never digests
|
|
28
|
+
# the (large, vendored) RBS tree. {RunDescriptor} is NOT a {Descriptor} — it is never composed, hashed,
|
|
29
|
+
# or `==`'d, only its three readers are consulted — so deferring `files` costs no soundness, and `gems`
|
|
30
|
+
# / `configs` are byte-identical to {.build} (the key is unchanged).
|
|
31
|
+
def self.build_run(loader)
|
|
32
|
+
RunDescriptor.new(loader: loader, gems: [rbs_gem_entry], configs: config_entries(loader))
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# The `gems` + `configs` slots the run cache key reads. Cheap — no RBS env load, no file digesting
|
|
36
|
+
# (only the configured library names + any plugin-synthesised virtual RBS are hashed).
|
|
37
|
+
def self.config_entries(loader)
|
|
38
|
+
[libraries_entry(loader.libraries), virtual_rbs_entry(loader)].compact
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Public (ADR-87 WD4) so the boot-slimming run-cache probe reconstructs the identical `gems` +
|
|
42
|
+
# `rbs.libraries` key slots the runner writes, from the config-derived library list, without a loader.
|
|
24
43
|
def self.rbs_gem_entry
|
|
25
44
|
Descriptor::GemEntry.new(name: "rbs", requirement: ">= 0", locked: ::RBS::VERSION.to_s)
|
|
26
45
|
end
|
|
27
46
|
|
|
28
|
-
|
|
47
|
+
# @param comparator [Symbol] `:digest` (default) for the env-cache KEY descriptor ({.build}), where the
|
|
48
|
+
# value must be deterministic; `:stat` (ADR-87 WD1) for the validation-only run-dependency descriptor
|
|
49
|
+
# ({RunDescriptor#files}), where the stat tier short-circuits the SHA-256 on an unmoved file.
|
|
50
|
+
def self.file_entries(loader, comparator: :digest)
|
|
29
51
|
roots = loader.signature_paths +
|
|
30
52
|
Rigor::Environment::RbsLoader.vendored_gem_sig_paths +
|
|
31
53
|
Rigor::Environment::RbsLoader.core_overlay_sig_paths
|
|
@@ -33,17 +55,20 @@ module Rigor
|
|
|
33
55
|
next [] unless root.directory?
|
|
34
56
|
|
|
35
57
|
Dir.glob(root.join("**", "*.rbs")).map do |path|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
58
|
+
digest = FileDigest.hexdigest(path)
|
|
59
|
+
if comparator == :stat
|
|
60
|
+
Descriptor::FileEntry.stat(path: path, digest: digest)
|
|
61
|
+
else
|
|
62
|
+
Descriptor::FileEntry.new(path: path, comparator: :digest, value: digest)
|
|
63
|
+
end
|
|
41
64
|
end
|
|
42
65
|
end
|
|
43
66
|
end
|
|
44
67
|
|
|
45
|
-
|
|
46
|
-
|
|
68
|
+
# @param library_names [Array<String, Symbol>] the loader's merged library list (or, on the WD4 probe
|
|
69
|
+
# path, `Environment::DEFAULT_LIBRARIES + config.libraries` reconstructed without a loader).
|
|
70
|
+
def self.libraries_entry(library_names)
|
|
71
|
+
sorted = library_names.map(&:to_s).sort
|
|
47
72
|
Descriptor::ConfigEntry.new(
|
|
48
73
|
key: "rbs.libraries",
|
|
49
74
|
value_hash: Digest::SHA256.hexdigest(sorted.join("\n"))
|
|
@@ -66,7 +91,28 @@ module Rigor
|
|
|
66
91
|
)
|
|
67
92
|
end
|
|
68
93
|
|
|
69
|
-
private_class_method :
|
|
94
|
+
private_class_method :virtual_rbs_entry
|
|
95
|
+
|
|
96
|
+
# The lazy-files run descriptor {RbsDescriptor.build_run} returns. Exposes the three readers the
|
|
97
|
+
# run-diagnostics cache consults — `gems` + `configs` are supplied eagerly (they feed the cache KEY,
|
|
98
|
+
# and are cheap); `files` (the RBS signature-tree digests, read only on a MISS) is computed once on
|
|
99
|
+
# first access and memoised, so a warm HIT never pays for it.
|
|
100
|
+
class RunDescriptor
|
|
101
|
+
attr_reader :gems, :configs
|
|
102
|
+
|
|
103
|
+
def initialize(loader:, gems:, configs:)
|
|
104
|
+
@loader = loader
|
|
105
|
+
@gems = gems
|
|
106
|
+
@configs = configs
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def files
|
|
110
|
+
# ADR-87 WD1 — this descriptor is validated (never a cache key), so the RBS signature tree rides the
|
|
111
|
+
# stat-then-digest `:stat` tier: a warm run stat-checks the (large, vendored) tree instead of
|
|
112
|
+
# re-hashing it.
|
|
113
|
+
@files ||= RbsDescriptor.file_entries(@loader, comparator: :stat)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
70
116
|
end
|
|
71
117
|
end
|
|
72
118
|
end
|
data/lib/rigor/cache/store.rb
CHANGED
|
@@ -7,6 +7,7 @@ require "monitor"
|
|
|
7
7
|
require "securerandom"
|
|
8
8
|
require "zlib"
|
|
9
9
|
|
|
10
|
+
require_relative "../version"
|
|
10
11
|
require_relative "descriptor"
|
|
11
12
|
|
|
12
13
|
module Rigor
|
|
@@ -30,6 +31,27 @@ module Rigor
|
|
|
30
31
|
# bytes.
|
|
31
32
|
FORMAT_VERSION = 2
|
|
32
33
|
|
|
34
|
+
# Payload ABI version. Store values are mostly Marshal blobs of Rigor/RBS objects, so a Rigor release
|
|
35
|
+
# upgrade is an ABI boundary even when the byte layout and descriptor schema are unchanged. Folding the
|
|
36
|
+
# gem version into the root marker makes installed-version upgrades rebuild rather than silently reuse a
|
|
37
|
+
# blob whose class layout still happens to unmarshal.
|
|
38
|
+
PAYLOAD_ABI_VERSION = Rigor::VERSION
|
|
39
|
+
|
|
40
|
+
# Whole-project producers are content-keyed, so dependency / signature churn writes a new entry and leaves
|
|
41
|
+
# the old generation unreachable. The global 256 MB cap is intentionally generous and often never fires on
|
|
42
|
+
# one project, so these producers get a small generation cap as a second compaction axis. Per-file / plugin
|
|
43
|
+
# producers are deliberately absent from this table: many current entries under one producer id can be live.
|
|
44
|
+
GENERATION_CAP_BY_PRODUCER = {
|
|
45
|
+
"analysis.run-diagnostics" => 16,
|
|
46
|
+
"rbs.class_ancestor_table" => 2,
|
|
47
|
+
"rbs.class_type_param_names" => 2,
|
|
48
|
+
"rbs.constant_type_table" => 2,
|
|
49
|
+
"rbs.environment" => 2,
|
|
50
|
+
"rbs.known_class_names" => 2
|
|
51
|
+
}.freeze
|
|
52
|
+
|
|
53
|
+
STALE_TEMP_FILE_AGE_SECONDS = 60 * 60
|
|
54
|
+
|
|
33
55
|
# Header literal: 5-byte ASCII magic, 1-byte separator, 1-byte format version.
|
|
34
56
|
HEADER = "RIGOR\x00#{FORMAT_VERSION.chr}".b.freeze
|
|
35
57
|
|
|
@@ -37,17 +59,19 @@ module Rigor
|
|
|
37
59
|
|
|
38
60
|
# @param root [String] cache root directory.
|
|
39
61
|
# @param read_only [Boolean] when true, every disk-side side-effect is suppressed: `fetch_or_compute`
|
|
40
|
-
# still reads existing entries (hits
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
#
|
|
45
|
-
# behaviour".
|
|
62
|
+
# still reads existing entries (hits, gated on a current `schema_version.txt` marker — see
|
|
63
|
+
# {#ensure_schema_version!}) and still runs the producer block on miss, but it does NOT write the
|
|
64
|
+
# produced value to disk, does NOT update the marker, and does NOT touch the on-disk root directory.
|
|
65
|
+
# The in-process memo is still populated so repeated lookups within the same run stay cheap. Used by
|
|
66
|
+
# editor mode so multiple buffer-mode invocations can read from the same cache concurrently without
|
|
67
|
+
# churning it. See `docs/design/20260516-editor-mode.md` § "Cache behaviour".
|
|
46
68
|
def initialize(root:, read_only: false, max_bytes: nil)
|
|
47
69
|
@root = root.to_s.dup.freeze
|
|
48
70
|
@read_only = read_only
|
|
49
71
|
@max_bytes = max_bytes&.then { |n| Integer(n) }
|
|
50
|
-
|
|
72
|
+
# Tri-state: nil = not yet checked, true/false = the disk tier's availability for this Store's
|
|
73
|
+
# lifetime (see {#ensure_schema_version!}).
|
|
74
|
+
@disk_available = nil
|
|
51
75
|
@hits = 0
|
|
52
76
|
@misses = 0
|
|
53
77
|
@writes = 0
|
|
@@ -100,7 +124,7 @@ module Rigor
|
|
|
100
124
|
# the eviction cap forever. Folding the format version into the marker routes the bump through the
|
|
101
125
|
# established clear-the-root path instead.
|
|
102
126
|
def self.schema_marker_value
|
|
103
|
-
"#{Descriptor::SCHEMA_VERSION}.#{FORMAT_VERSION}"
|
|
127
|
+
"#{PAYLOAD_ABI_VERSION}.#{Descriptor::SCHEMA_VERSION}.#{FORMAT_VERSION}"
|
|
104
128
|
end
|
|
105
129
|
|
|
106
130
|
def self.disk_inventory(root:)
|
|
@@ -154,7 +178,7 @@ module Rigor
|
|
|
154
178
|
def fetch_or_compute(producer_id:, params:, descriptor:,
|
|
155
179
|
serialize: nil, deserialize: nil, &block)
|
|
156
180
|
validate_producer_id!(producer_id)
|
|
157
|
-
ensure_schema_version!
|
|
181
|
+
disk = ensure_schema_version!
|
|
158
182
|
|
|
159
183
|
key = descriptor.cache_key_for(producer_id: producer_id, params: params)
|
|
160
184
|
memo_key = [producer_id, key].freeze
|
|
@@ -164,8 +188,8 @@ module Rigor
|
|
|
164
188
|
return memoed
|
|
165
189
|
end
|
|
166
190
|
|
|
167
|
-
path = entry_path(producer_id, key)
|
|
168
|
-
cached = read_entry(path, deserialize: deserialize)
|
|
191
|
+
path = disk ? entry_path(producer_id, key) : nil
|
|
192
|
+
cached = path && read_entry(path, deserialize: deserialize)
|
|
169
193
|
unless cached.nil?
|
|
170
194
|
@monitor.synchronize do
|
|
171
195
|
record(:hits, producer_id)
|
|
@@ -175,10 +199,10 @@ module Rigor
|
|
|
175
199
|
end
|
|
176
200
|
|
|
177
201
|
value = block.call
|
|
178
|
-
|
|
202
|
+
wrote = path && try_write_entry(path, descriptor, value, serialize: serialize)
|
|
179
203
|
@monitor.synchronize do
|
|
180
204
|
record(:misses, producer_id)
|
|
181
|
-
record(:writes, producer_id)
|
|
205
|
+
record(:writes, producer_id) if wrote
|
|
182
206
|
@memo[memo_key] = value
|
|
183
207
|
end
|
|
184
208
|
value
|
|
@@ -195,29 +219,18 @@ module Rigor
|
|
|
195
219
|
# validation always re-checks the filesystem — but a single run only looks up once.
|
|
196
220
|
def fetch_or_validate(producer_id:, key_descriptor:, params: {}, serialize: nil, deserialize: nil)
|
|
197
221
|
validate_producer_id!(producer_id)
|
|
198
|
-
ensure_schema_version!
|
|
222
|
+
disk = ensure_schema_version!
|
|
199
223
|
|
|
200
224
|
key = key_descriptor.cache_key_for(producer_id: producer_id, params: params)
|
|
201
|
-
path = entry_path(producer_id, key)
|
|
202
|
-
cached = read_entry(path, deserialize: deserialize)
|
|
203
|
-
if
|
|
204
|
-
pair[1].is_a?(Descriptor) && pair[1].fresh?
|
|
225
|
+
path = disk ? entry_path(producer_id, key) : nil
|
|
226
|
+
cached = path && read_entry(path, deserialize: deserialize)
|
|
227
|
+
if (validated = fresh_pair_value(cached))
|
|
205
228
|
@monitor.synchronize { record(:hits, producer_id) }
|
|
206
|
-
return
|
|
229
|
+
return validated[0]
|
|
207
230
|
end
|
|
208
231
|
|
|
209
232
|
value, dependency_descriptor = block_given? ? yield : [nil, Descriptor.new]
|
|
210
|
-
wrote =
|
|
211
|
-
unless @read_only
|
|
212
|
-
# A cache write must never break the run. If the value is not Marshal-clean (or any disk error
|
|
213
|
-
# occurs) skip caching and return the freshly-computed value — the next run recomputes.
|
|
214
|
-
begin
|
|
215
|
-
write_entry(path, key_descriptor, [value, dependency_descriptor], serialize: serialize)
|
|
216
|
-
wrote = true
|
|
217
|
-
rescue StandardError
|
|
218
|
-
wrote = false
|
|
219
|
-
end
|
|
220
|
-
end
|
|
233
|
+
wrote = path && try_write_entry(path, key_descriptor, [value, dependency_descriptor], serialize: serialize)
|
|
221
234
|
@monitor.synchronize do
|
|
222
235
|
record(:misses, producer_id)
|
|
223
236
|
record(:writes, producer_id) if wrote
|
|
@@ -225,27 +238,48 @@ module Rigor
|
|
|
225
238
|
value
|
|
226
239
|
end
|
|
227
240
|
|
|
228
|
-
# ADR-
|
|
229
|
-
#
|
|
230
|
-
#
|
|
231
|
-
#
|
|
232
|
-
#
|
|
233
|
-
|
|
241
|
+
# ADR-87 WD4 — the READ half of {#fetch_or_validate} with no compute and no write: returns the cached
|
|
242
|
+
# value on a fresh hit, nil on a miss / stale / unavailable-disk. The boot-slimming hit probe calls this
|
|
243
|
+
# to serve a run's diagnostics WITHOUT loading the inference engine — it never runs a producer block, so
|
|
244
|
+
# there is nothing to write. Records a hit (for `--cache-stats` parity) but never a miss (a probe miss
|
|
245
|
+
# hands off to the full path, which records its own).
|
|
246
|
+
def peek_validated(producer_id:, key_descriptor:, params: {}, deserialize: nil)
|
|
247
|
+
validate_producer_id!(producer_id)
|
|
248
|
+
return nil unless ensure_schema_version!
|
|
249
|
+
|
|
250
|
+
key = key_descriptor.cache_key_for(producer_id: producer_id, params: params)
|
|
251
|
+
cached = read_entry(entry_path(producer_id, key), deserialize: deserialize)
|
|
252
|
+
validated = fresh_pair_value(cached)
|
|
253
|
+
return nil if validated.nil?
|
|
254
|
+
|
|
255
|
+
@monitor.synchronize { record(:hits, producer_id) }
|
|
256
|
+
validated[0]
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# ADR-6 § "Eviction" — compaction pass over the on-disk cache. No-op when the store is read-only. Stale
|
|
260
|
+
# temp file cleanup and the whole-project generation cap run regardless of `max_bytes:` — they reclaim
|
|
261
|
+
# provably-dead bytes (leaked temp files, unreachable content-keyed generations) rather than enforcing a
|
|
262
|
+
# size budget, so an explicitly unbounded store (`max_bytes: nil`) still benefits from them. The
|
|
263
|
+
# size-based LRU pass below stays gated on `max_bytes:` being configured: it walks all remaining
|
|
264
|
+
# `.entry` files, sorts by mtime ascending (oldest = least recently used), and unlinks from the oldest
|
|
265
|
+
# until the total is at or below the cap. Touch-on-disk-read ({read_entry}) is the cross-process LRU
|
|
266
|
+
# signal: every disk hit (not in-process-memo hit) updates the mtime so recently-read entries survive
|
|
267
|
+
# the eviction pass. Any FS error is swallowed — eviction must never break a run.
|
|
234
268
|
def evict!
|
|
235
|
-
return if @
|
|
269
|
+
return if @read_only
|
|
236
270
|
|
|
237
|
-
|
|
238
|
-
|
|
271
|
+
cleanup_stale_temp_files
|
|
272
|
+
entries = evict_excess_generations(collect_entry_stats)
|
|
273
|
+
return if @max_bytes.nil?
|
|
274
|
+
|
|
275
|
+
total = entries.sum { |e| e[:bytes] }
|
|
239
276
|
return if total <= @max_bytes
|
|
240
277
|
|
|
241
278
|
entries.sort_by! { |e| e[:mtime] }
|
|
242
279
|
entries.each do |entry|
|
|
243
280
|
break if total <= @max_bytes
|
|
244
281
|
|
|
245
|
-
|
|
246
|
-
total -= entry[:bytes]
|
|
247
|
-
rescue StandardError
|
|
248
|
-
next
|
|
282
|
+
total -= entry[:bytes] if unlink_entry(entry[:path])
|
|
249
283
|
end
|
|
250
284
|
nil
|
|
251
285
|
rescue StandardError
|
|
@@ -257,6 +291,19 @@ module Rigor
|
|
|
257
291
|
Entry = Data.define(:descriptor_bytes, :value)
|
|
258
292
|
private_constant :Entry
|
|
259
293
|
|
|
294
|
+
# A record-and-validate entry is a fresh hit iff it deserialised to a `[value, dependency_descriptor]`
|
|
295
|
+
# pair whose descriptor still validates against the filesystem. Returns the pair on a fresh hit, nil
|
|
296
|
+
# otherwise. Shared by {#fetch_or_validate} and {#peek_validated} so both apply identical hit criteria.
|
|
297
|
+
def fresh_pair_value(cached)
|
|
298
|
+
return nil if cached.nil?
|
|
299
|
+
|
|
300
|
+
pair = cached.value
|
|
301
|
+
return nil unless pair.is_a?(Array) && pair.size == 2 &&
|
|
302
|
+
pair[1].is_a?(Descriptor) && pair[1].fresh?
|
|
303
|
+
|
|
304
|
+
pair
|
|
305
|
+
end
|
|
306
|
+
|
|
260
307
|
def record(counter, producer_id)
|
|
261
308
|
case counter
|
|
262
309
|
when :hits then @hits += 1
|
|
@@ -372,38 +419,68 @@ module Rigor
|
|
|
372
419
|
File.open(path, File::RDWR | File::CREAT, 0o644) do |lock_fd|
|
|
373
420
|
lock_fd.flock(File::LOCK_EX)
|
|
374
421
|
tmp = "#{path}.tmp.#{Process.pid}.#{SecureRandom.hex(4)}"
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
422
|
+
begin
|
|
423
|
+
File.open(tmp, "wb") do |f|
|
|
424
|
+
f.write(body)
|
|
425
|
+
f.fsync
|
|
426
|
+
end
|
|
427
|
+
File.rename(tmp, path)
|
|
428
|
+
ensure
|
|
429
|
+
# A failed write/rename must not leak its temp file — rely on the 1-hour `cleanup_stale_temp_files`
|
|
430
|
+
# sweep only as a backstop for crashes that skip this ensure entirely.
|
|
431
|
+
unlink_entry(tmp) if File.exist?(tmp)
|
|
378
432
|
end
|
|
379
|
-
File.
|
|
433
|
+
fsync_directory(File.dirname(path))
|
|
380
434
|
end
|
|
381
435
|
end
|
|
382
436
|
|
|
437
|
+
# Checks (and, for a writable store, repairs) the `schema_version.txt` marker, and reports whether the
|
|
438
|
+
# disk tier is usable for the rest of this Store's lifetime. The result is memoized in `@disk_available`
|
|
439
|
+
# — one check per Store is enough; a benign double-check under a thread race would just repeat
|
|
440
|
+
# idempotent work.
|
|
441
|
+
#
|
|
442
|
+
# A writable store clears the cache root and rewrites the marker on a stale/missing marker, then reports
|
|
443
|
+
# available. A read-only store never touches the root — no mkdir, no marker write, no destructive clear
|
|
444
|
+
# — so it reports available ONLY when the on-disk marker already matches current exactly; a stale or
|
|
445
|
+
# missing marker (e.g. a Rigor upgrade with no writable run yet, as in LSP / editor mode) reports
|
|
446
|
+
# unavailable rather than risk unmarshalling a payload from a different ABI. Any filesystem failure
|
|
447
|
+
# (permission, disk full, deleted root) also reports unavailable, degrading this Store instance to
|
|
448
|
+
# in-memory-memo-only for the rest of its lifetime — the producer block still runs, its result still
|
|
449
|
+
# lands in `@memo`, and disk reads/writes are simply skipped.
|
|
450
|
+
#
|
|
451
|
+
# @return [Boolean]
|
|
383
452
|
def ensure_schema_version!
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
return
|
|
453
|
+
return @disk_available unless @disk_available.nil?
|
|
454
|
+
|
|
455
|
+
@disk_available = @read_only ? read_only_marker_current? : repair_writable_marker!
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
def read_only_marker_current?
|
|
459
|
+
marker = File.join(@root, "schema_version.txt")
|
|
460
|
+
return false unless File.file?(marker)
|
|
461
|
+
|
|
462
|
+
File.read(marker).strip == self.class.schema_marker_value
|
|
463
|
+
rescue StandardError
|
|
464
|
+
false
|
|
465
|
+
end
|
|
392
466
|
|
|
393
|
-
|
|
467
|
+
def repair_writable_marker!
|
|
394
468
|
FileUtils.mkdir_p(@root)
|
|
395
469
|
marker = File.join(@root, "schema_version.txt")
|
|
396
470
|
current = self.class.schema_marker_value
|
|
397
471
|
|
|
398
472
|
if File.file?(marker)
|
|
399
473
|
on_disk = File.read(marker).strip
|
|
400
|
-
return if on_disk == current
|
|
474
|
+
return true if on_disk == current
|
|
401
475
|
|
|
402
476
|
clear_cache_root!
|
|
403
477
|
end
|
|
404
478
|
|
|
405
479
|
FileUtils.mkdir_p(@root)
|
|
406
480
|
File.write(marker, "#{current}\n")
|
|
481
|
+
true
|
|
482
|
+
rescue StandardError
|
|
483
|
+
false
|
|
407
484
|
end
|
|
408
485
|
|
|
409
486
|
def clear_cache_root!
|
|
@@ -412,6 +489,27 @@ module Rigor
|
|
|
412
489
|
end
|
|
413
490
|
end
|
|
414
491
|
|
|
492
|
+
# Shared write path for both {#fetch_or_compute} and {#fetch_or_validate}. A cache write must never
|
|
493
|
+
# break the run: filesystem-side failures (permission, disk full, deleted root, read-only mount) are
|
|
494
|
+
# swallowed and read as "did not write". Programmer errors — a custom serializer that doesn't return a
|
|
495
|
+
# `String`, or any other producer contract violation — still raise so the bug is visible.
|
|
496
|
+
def try_write_entry(path, descriptor, value, serialize: nil)
|
|
497
|
+
return false if @read_only
|
|
498
|
+
|
|
499
|
+
write_entry(path, descriptor, value, serialize: serialize)
|
|
500
|
+
true
|
|
501
|
+
rescue SystemCallError, IOError
|
|
502
|
+
false
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
# Best-effort durability for the rename itself. Some platforms cannot fsync directories (or do not need to),
|
|
506
|
+
# so failures are ignored; the entry envelope still turns any lost/partial write into a miss.
|
|
507
|
+
def fsync_directory(dir)
|
|
508
|
+
File.open(dir, File::RDONLY, &:fsync)
|
|
509
|
+
rescue StandardError
|
|
510
|
+
nil
|
|
511
|
+
end
|
|
512
|
+
|
|
415
513
|
# LEB128 unsigned varint encoder/decoder. Lengths fit easily in five bytes (cap at 2^35); the cache
|
|
416
514
|
# layer never writes a value larger than that in practice.
|
|
417
515
|
def write_varint(bytes, value)
|
|
@@ -437,17 +535,66 @@ module Rigor
|
|
|
437
535
|
nil
|
|
438
536
|
end
|
|
439
537
|
|
|
440
|
-
|
|
441
|
-
|
|
538
|
+
def cleanup_stale_temp_files
|
|
539
|
+
cutoff = Time.now - STALE_TEMP_FILE_AGE_SECONDS
|
|
540
|
+
Dir.glob(File.join(@root, "**", "*.tmp.*")).each do |path|
|
|
541
|
+
next unless File.file?(path)
|
|
542
|
+
next if File.mtime(path) > cutoff
|
|
543
|
+
|
|
544
|
+
unlink_entry(path)
|
|
545
|
+
rescue StandardError
|
|
546
|
+
next
|
|
547
|
+
end
|
|
548
|
+
rescue StandardError
|
|
549
|
+
nil
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
def evict_excess_generations(entries)
|
|
553
|
+
removed = {}
|
|
554
|
+
entries.group_by { |entry| entry[:producer] }.each do |producer, producer_entries|
|
|
555
|
+
cap = GENERATION_CAP_BY_PRODUCER[producer]
|
|
556
|
+
next if cap.nil? || producer_entries.size <= cap
|
|
557
|
+
|
|
558
|
+
producer_entries.sort_by { |entry| [entry[:mtime], entry[:path]] }
|
|
559
|
+
.first(producer_entries.size - cap)
|
|
560
|
+
.each do |entry|
|
|
561
|
+
removed[entry[:path]] = true if unlink_entry(entry[:path])
|
|
562
|
+
end
|
|
563
|
+
end
|
|
564
|
+
return entries if removed.empty?
|
|
565
|
+
|
|
566
|
+
entries.reject { |entry| removed[entry[:path]] }
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
def unlink_entry(path)
|
|
570
|
+
File.unlink(path)
|
|
571
|
+
true
|
|
572
|
+
rescue StandardError
|
|
573
|
+
false
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
# Returns an array of `{ path:, producer:, mtime:, bytes: }` hashes for every `.entry` file under the
|
|
577
|
+
# cache root, skipping unreadable entries.
|
|
442
578
|
def collect_entry_stats
|
|
443
579
|
Dir.glob(File.join(@root, "**", "*.entry")).filter_map do |path|
|
|
444
580
|
stat = File.stat(path)
|
|
445
|
-
|
|
581
|
+
producer = producer_id_for_entry(path)
|
|
582
|
+
next nil if producer.nil?
|
|
583
|
+
|
|
584
|
+
{ path: path, producer: producer, mtime: stat.mtime, bytes: stat.size }
|
|
446
585
|
rescue StandardError
|
|
447
586
|
nil
|
|
448
587
|
end
|
|
449
588
|
end
|
|
450
589
|
|
|
590
|
+
def producer_id_for_entry(path)
|
|
591
|
+
root_prefix = @root.end_with?(File::SEPARATOR) ? @root : "#{@root}#{File::SEPARATOR}"
|
|
592
|
+
return nil unless path.start_with?(root_prefix)
|
|
593
|
+
|
|
594
|
+
producer = path.delete_prefix(root_prefix).split(File::SEPARATOR, 2).first
|
|
595
|
+
producer.empty? ? nil : producer
|
|
596
|
+
end
|
|
597
|
+
|
|
451
598
|
def read_varint(bytes, offset)
|
|
452
599
|
result = 0
|
|
453
600
|
shift = 0
|