Class: RSpecTracer::RemoteCache::RedisBackend Private
- Inherits:
-
Object
- Object
- RSpecTracer::RemoteCache::RedisBackend
- Defined in:
- lib/rspec_tracer/remote_cache/redis_backend.rb
Overview
This class is part of a private API. You should avoid using this class if possible, as it may be removed or be changed in the future.
Redis implementation of ‘RemoteCache::Backend`. Each cache ref is one Redis hash keyed under the two-tier layout:
<prefix>:main:<sha>[:<test_suite_id>] -> HASH
<prefix>:pr:<branch>:<sha>[:<test_suite_id>] -> HASH
<prefix>:pr:<branch>:branch_refs -> STRING (JSON)
Hash fields per ref:
_timestamp -> epoch float (string; microsecond resolution
to keep within-second orderings stable for
count-based prune)
last_run.json -> JSON content verbatim
<run_id>/<f>.json -> JSON content per file in the 15-file layout
Why hashmap and not a binary archive (like S3 / LocalFs): hash- per-ref is the idiomatic Redis data model, matches the brief, and gives operational visibility via ‘redis-cli HGETALL` / `HKEYS` / `HLEN` without extracting an archive first. The storage cost (no gzip) is negligible for realistic cache sizes.
Retention: identical dispatch to S3 / LocalFs. The orchestrator calls ‘prune!(count:, duration_seconds:, pr_branch_ttl_seconds:)` after each upload; this backend enumerates via SCAN + HGET on the per-ref `_timestamp` field, then DELs stale keys. TTL-on-SET (i.e. letting Redis EXPIRE handle it natively) is a reasonable ergonomic followup but is not required for correctness - the explicit prune pass already achieves the same eviction outcome.
Graceful-degradation contract:
- `redis` gem missing -> constructor raises RedisBackendError;
UserTasks rescues at the top, logs a clear "add gem to your
Gemfile" message, falls back to cold run. Never propagates.
- Wire failure (connection refused, timeout) -> redis-rb raises
Redis::BaseError subclasses. `download` catches and returns
false; `upload` lets them propagate for the Rake task to log.
Same rescue model as S3Backend.
The ‘redis` gem is an OPTIONAL runtime dependency - users add `gem ’redis’‘ to their own Gemfile to use RedisBackend. The constructor calls `require ’redis’‘ lazily and raises a clear RedisBackendError if the gem is absent, which UserTasks converts into a warning + cold run.
Defined Under Namespace
Classes: RedisBackendError
Constant Summary collapse
- MAIN_TIER =
This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.
Internal constant.
'main'- PR_TIER =
This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.
Internal constant.
'pr'- BRANCH_REFS_SUFFIX =
This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.
Internal constant.
'branch_refs'- PR_BRANCHES_SUFFIX =
This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.
Internal constant.
'pr_branches'- LAST_RUN_FIELD =
This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.
Internal constant.
'last_run.json'- TIMESTAMP_FIELD =
This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.
Internal constant.
'_timestamp'- ENCODING =
This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.
Internal constant.
'UTF-8'- DEFAULT_SCAN_COUNT =
This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.
Internal constant.
200
Instance Method Summary collapse
-
#branch_refs(branch_name) ⇒ Object
private
Read branch_refs for the given branch.
-
#download(ref, tree_sha: nil) ⇒ Object
private
Download cache for ‘ref`.
-
#initialize(prefix:, branch:, default_branch:, cache_path:, url: nil, redis_client: nil, test_suite_id: nil, logger: nil, ttl: nil) ⇒ RedisBackend
constructor
private
rubocop:disable Metrics/ParameterLists.
-
#prune!(count: nil, duration_seconds: nil, pr_branch_ttl_seconds: nil) ⇒ Object
private
Apply retention to own tier.
-
#prune_all!(pr_branch_ttl_seconds: nil) ⇒ Object
private
Cross-tier PR-branch cleanup.
-
#unbounded_warning(warn_threshold: 500) ⇒ Object
private
Warn when main tier has grown beyond threshold and no retention is configured.
-
#upload(ref, tree_sha: nil) ⇒ Object
private
Upload local cache as a hash under own-tier key.
-
#write_branch_refs(branch_name, refs) ⇒ Object
private
Persist branch_refs for the given branch.
Constructor Details
#initialize(prefix:, branch:, default_branch:, cache_path:, url: nil, redis_client: nil, test_suite_id: nil, logger: nil, ttl: nil) ⇒ RedisBackend
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
rubocop:disable Metrics/ParameterLists
87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 |
# File 'lib/rspec_tracer/remote_cache/redis_backend.rb', line 87 def initialize(prefix:, branch:, default_branch:, cache_path:, url: nil, redis_client: nil, test_suite_id: nil, logger: nil, ttl: nil) validate_required!(prefix: prefix, branch: branch, default_branch: default_branch, cache_path: cache_path) validate_connection_source!(url: url, redis_client: redis_client) validate_ttl!(ttl) @prefix = trim_trailing_colons(prefix.to_s) @branch = branch.to_s.chomp @default_branch = default_branch.to_s.chomp @test_suite_id = normalize_test_suite_id(test_suite_id) @cache_path = cache_path.to_s @logger = logger @ttl = ttl @redis = redis_client || build_client(url) end |
Instance Method Details
#branch_refs(branch_name) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Read branch_refs for the given branch. Returns ‘=> ts_epoch` or `{}` when missing / malformed.
144 145 146 147 148 149 150 151 152 153 154 155 |
# File 'lib/rspec_tracer/remote_cache/redis_backend.rb', line 144 def branch_refs(branch_name) return {} if blank?(branch_name) raw = @redis.get(branch_refs_key(branch_name)) return {} if raw.nil? || raw.empty? parsed = JSON.parse(raw) parsed.is_a?(Hash) ? parsed.transform_values(&:to_i) : {} rescue StandardError => e log_debug("branch_refs read failed (#{e.class}: #{e.}); treating as empty") {} end |
#download(ref, tree_sha: nil) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Download cache for ‘ref`. Tries own tier, falls back to main tier. Returns true on validated success, false otherwise. Cleans up partially-written files on failure.
‘tree_sha:` is accepted for protocol uniformity with S3Backend but is currently a no-op: the tree-SHA secondary index is an S3-only feature. Future enhancement may extend it here; the orchestrator already forwards the kwarg.
114 115 116 117 118 119 120 121 122 |
# File 'lib/rspec_tracer/remote_cache/redis_backend.rb', line 114 def download(ref, tree_sha: nil) _ = tree_sha return false if blank?(ref) tiers_to_try = [own_tier_segment] tiers_to_try << MAIN_TIER if pr_tier? tiers_to_try.any? { |tier| try_download_from(tier, ref) } end |
#prune!(count: nil, duration_seconds: nil, pr_branch_ttl_seconds: nil) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Apply retention to own tier. Returns count removed. Two or more knobs may be set; each applies independently. Never raises - a wire-level Redis error on any sub-prune logs + is absorbed. rubocop:disable Metrics/PerceivedComplexity
172 173 174 175 176 177 178 179 180 181 |
# File 'lib/rspec_tracer/remote_cache/redis_backend.rb', line 172 def prune!(count: nil, duration_seconds: nil, pr_branch_ttl_seconds: nil) removed = 0 removed += prune_by_count!(count) if count&.positive? removed += prune_by_duration!(duration_seconds) if duration_seconds&.positive? removed += prune_dead_pr_branch!(pr_branch_ttl_seconds) if pr_tier? && pr_branch_ttl_seconds&.positive? removed rescue StandardError => e log_warn("prune! failed (#{e.class}: #{e.}); returning #{removed}") removed end |
#prune_all!(pr_branch_ttl_seconds: nil) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Cross-tier PR-branch cleanup. Enumerates every PR branch under the configured prefix (by scanning for ‘<prefix>:pr:<branch>:branch_refs` keys and deriving branch names), applies the TTL to each, deletes dead branches whole. Returns total refs removed. No-op on nil / non-positive TTL.
189 190 191 192 193 194 195 196 197 198 |
# File 'lib/rspec_tracer/remote_cache/redis_backend.rb', line 189 def prune_all!(pr_branch_ttl_seconds: nil) return 0 unless pr_branch_ttl_seconds&.positive? cutoff = Time.now.to_i - pr_branch_ttl_seconds.to_i branches = discover_pr_branches branches.sum { |branch| maybe_prune_branch(branch, cutoff) } rescue StandardError => e log_warn("prune_all! failed (#{e.class}: #{e.})") 0 end |
#unbounded_warning(warn_threshold: 500) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Warn when main tier has grown beyond threshold and no retention is configured.
202 203 204 205 206 207 208 |
# File 'lib/rspec_tracer/remote_cache/redis_backend.rb', line 202 def unbounded_warning(warn_threshold: 500) count = count_tier_refs(MAIN_TIER) return nil unless count > warn_threshold "rspec-tracer remote cache has #{count} refs in #{@prefix}:#{MAIN_TIER}; " \ 'configure cache_retention_count or cache_retention_duration to cap growth' end |
#upload(ref, tree_sha: nil) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Upload local cache as a hash under own-tier key. Raises on I/O or Redis wire failure.
‘tree_sha:` is accepted for protocol uniformity with S3Backend (no-op here; see `download`).
129 130 131 132 133 134 135 136 137 138 139 140 |
# File 'lib/rspec_tracer/remote_cache/redis_backend.rb', line 129 def upload(ref, tree_sha: nil) _ = tree_sha raise RedisBackendError, 'ref is required' if blank?(ref) run_id = read_local_run_id raise RedisBackendError, "no local cache to upload (missing #{LAST_RUN_FIELD})" if run_id.nil? fields = build_upload_fields(run_id) key = ref_key(own_tier_segment, ref) write_upload_hash(key, fields) log_debug("uploaded cache for #{ref} to #{own_tier_segment} (#{fields.size} fields)") end |
#write_branch_refs(branch_name, refs) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Persist branch_refs for the given branch. No-op for main-branch writes. Raises on Redis wire failure for PR tier.
159 160 161 162 163 164 165 166 |
# File 'lib/rspec_tracer/remote_cache/redis_backend.rb', line 159 def write_branch_refs(branch_name, refs) return if blank?(branch_name) return if branch_name.to_s.chomp == @default_branch return if refs.nil? || refs.empty? @redis.set(branch_refs_key(branch_name), JSON.pretty_generate(refs)) log_debug("wrote branch_refs for #{branch_name}") end |