Class: KnoxCall::InterceptManifestStore
- Inherits:
-
Object
- Object
- KnoxCall::InterceptManifestStore
- Defined in:
- lib/knoxcall/intercept_store.rb
Overview
The SDK-side copy of the intercept manifest — fetched, held, refreshed
(route-aware-interception-plan.md §2.5; PARITY §21.1). One per transport /
interceptor; process memory only; dropped on stop.
Ruby idiom (as Python): the manifest is refreshed LAZILY at the TTL — the
first request after ttl_seconds pays one management call — rather than
by a background thread. Behaviourally the same contract ("hold the manifest
for ttl_seconds, then refresh"), and it works identically under Puma
threads, a forked Unicorn/Sidekiq worker and a plain script, with no
thread to own.
- single-flight: concurrent refreshes share one fetch (a caller that waited on the mutex while another refreshed takes that answer);
- stale-but-valid: a failed refresh keeps the last GOOD manifest and backs off exponentially from the second consecutive failure (cap 8×TTL);
- a 401/403/404 from the manifest endpoint — the credential lacks routes:read, or an older server — is NOT a routing failure: the store warns once, behaves as "no manifest" (every listed host stays on the ephemeral path exactly as before this feature), and re-checks at 10×TTL;
- out-of-cycle refreshes (a route-mode refusal, a promoted-route hint, an
explicit
refresh) are rate-limited so a burst costs one call.
Discovery failing open is deliberate and bounded: it can only leave a host on the path it was on before the manifest existed. The DATA-PLANE hop is where fail-closed lives (D4), and that is in the intercept pipeline.
Constant Summary collapse
- DEFAULT_TTL_SECONDS =
60- MAX_BACKOFF_FACTOR =
8- PERMISSION_RECHECK_FACTOR =
10- MONOTONIC =
-> { Process.clock_gettime(Process::CLOCK_MONOTONIC) }
Instance Attribute Summary collapse
-
#last_error ⇒ Object
readonly
Returns the value of attribute last_error.
-
#manifest ⇒ Object
readonly
Returns the value of attribute manifest.
-
#min_refresh_gap ⇒ Object
Seconds between out-of-cycle refreshes (a refusal, a hint, an explicit refresh); a burst inside the gap costs one call.
-
#version ⇒ Object
readonly
Returns the value of attribute version.
Class Method Summary collapse
-
.accepts_if_none_match?(fetch) ⇒ Boolean
Whether
fetchcan takeif_none_match:(a keyword, or **rest).
Instance Method Summary collapse
-
#ensure ⇒ Object
Refresh if stale (first load, TTL expiry, a hint), then return the manifest.
-
#hint ⇒ Object
A promoted-route hint arrived: make the NEXT request refresh (rate-limited).
-
#initialize(fetch, on_refresh: nil, on_error: nil, min_refresh_gap: 5.0, now: MONOTONIC) ⇒ InterceptManifestStore
constructor
A new instance of InterceptManifestStore.
- #permission_denied? ⇒ Boolean
-
#refresh(reason, force: false) ⇒ Object
Refresh now.
-
#stale? ⇒ Boolean
Whether the next request should refresh before deciding.
-
#stop ⇒ Object
Drop the manifest and refuse further refreshes.
- #stopped? ⇒ Boolean
Constructor Details
#initialize(fetch, on_refresh: nil, on_error: nil, min_refresh_gap: 5.0, now: MONOTONIC) ⇒ InterceptManifestStore
Returns a new instance of InterceptManifestStore.
51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
# File 'lib/knoxcall/intercept_store.rb', line 51 def initialize(fetch, on_refresh: nil, on_error: nil, min_refresh_gap: 5.0, now: MONOTONIC) @fetch = fetch @fetch_conditional = self.class.accepts_if_none_match?(fetch) @on_refresh = on_refresh @on_error = on_error @min_refresh_gap = min_refresh_gap @now = now @manifest = nil @version = nil @expires_at = 0.0 # stale until the first refresh @last_refresh_at = -1e9 @failures = 0 @permission_denied = false @last_error = nil @stopped = false @seq = 0 @mutex = Mutex.new end |
Instance Attribute Details
#last_error ⇒ Object (readonly)
Returns the value of attribute last_error.
36 37 38 |
# File 'lib/knoxcall/intercept_store.rb', line 36 def last_error @last_error end |
#manifest ⇒ Object (readonly)
Returns the value of attribute manifest.
36 37 38 |
# File 'lib/knoxcall/intercept_store.rb', line 36 def manifest @manifest end |
#min_refresh_gap ⇒ Object
Seconds between out-of-cycle refreshes (a refusal, a hint, an explicit refresh); a burst inside the gap costs one call.
39 40 41 |
# File 'lib/knoxcall/intercept_store.rb', line 39 def min_refresh_gap @min_refresh_gap end |
#version ⇒ Object (readonly)
Returns the value of attribute version.
36 37 38 |
# File 'lib/knoxcall/intercept_store.rb', line 36 def version @version end |
Class Method Details
.accepts_if_none_match?(fetch) ⇒ Boolean
Whether fetch can take if_none_match: (a keyword, or **rest). Decided
once at construction so a zero-argument test/user seam keeps working.
75 76 77 78 79 80 81 |
# File 'lib/knoxcall/intercept_store.rb', line 75 def self.accepts_if_none_match?(fetch) return false unless fetch.respond_to?(:parameters) fetch.parameters.any? do |kind, name| (%i[key keyreq].include?(kind) && name == :if_none_match) || kind == :keyrest end end |
Instance Method Details
#ensure ⇒ Object
Refresh if stale (first load, TTL expiry, a hint), then return the manifest.
103 104 105 106 |
# File 'lib/knoxcall/intercept_store.rb', line 103 def ensure refresh("ttl", force: true) if stale? @manifest end |
#hint ⇒ Object
A promoted-route hint arrived: make the NEXT request refresh (rate-limited).
89 90 91 |
# File 'lib/knoxcall/intercept_store.rb', line 89 def hint @mutex.synchronize { @expires_at = @now.call if @now.call - @last_refresh_at >= @min_refresh_gap } end |
#permission_denied? ⇒ Boolean
70 |
# File 'lib/knoxcall/intercept_store.rb', line 70 def = @permission_denied |
#refresh(reason, force: false) ⇒ Object
Refresh now. Single-flight; rate-limited unless force. Returns the
manifest the store holds afterwards (nil after a permission refusal).
Never raises for a fetch failure — the store has already applied
stale-keep / backoff; read last_error.
112 113 114 115 116 117 118 119 120 121 122 123 124 125 |
# File 'lib/knoxcall/intercept_store.rb', line 112 def refresh(reason, force: false) return nil if @stopped seq_before = @seq # bumped when an attempt COMPLETES @mutex.synchronize do return nil if @stopped # An attempt completed while we waited on the mutex: take its answer # (single-flight — the refresh we queued behind is the one we wanted). return @manifest if @seq != seq_before return @manifest if !force && @now.call - @last_refresh_at < @min_refresh_gap do_refresh(reason) end end |
#stale? ⇒ Boolean
Whether the next request should refresh before deciding.
84 85 86 |
# File 'lib/knoxcall/intercept_store.rb', line 84 def stale? !@stopped && @now.call >= @expires_at end |
#stop ⇒ Object
Drop the manifest and refuse further refreshes.
94 95 96 97 98 99 100 |
# File 'lib/knoxcall/intercept_store.rb', line 94 def stop @mutex.synchronize do @stopped = true @manifest = nil @version = nil end end |
#stopped? ⇒ Boolean
71 |
# File 'lib/knoxcall/intercept_store.rb', line 71 def stopped? = @stopped |