cru-flags 0.1.2 → 0.2.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/CHANGELOG.md +7 -0
- data/README.md +6 -0
- data/docs/design.md +117 -11
- data/lib/cru_flags/client.rb +30 -1
- data/lib/cru_flags/railtie.rb +7 -0
- data/lib/cru_flags/version.rb +1 -1
- data/lib/cru_flags.rb +23 -2
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f7095f1356c7bbd71e37d86dbcd3b2bd4b3fdfd8eab1596f72f4b3d0e7aced11
|
|
4
|
+
data.tar.gz: 0b7ad1b83c3346b6cf4e4ab1e6c471e7ee7c2ae19e2c1baa7fc752b08e40a3b4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 57b7ca9b139f2880951c8365c86dcfe613c52cc4f287ba3e4960da4057df4cb159789506ff48726e2c3bbab70e2aeca1be0d2df9e59b94996ddd43c017b088bd
|
|
7
|
+
data.tar.gz: 0d657b212c65ade4858620199c18ca91bae2e810d9382cedf67ef1ba00798aaf3933dd31f2ffaef6907a51263a11fba8bfc8f3c17662ac42761025c979a3a52f
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.2.0](https://github.com/CruGlobal/cru-flags-ruby/compare/cru-flags/v0.1.2...cru-flags/v0.2.0) (2026-10-29)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* keep a fallback copy of the flag document in Rails.cache ([#12](https://github.com/CruGlobal/cru-flags-ruby/issues/12)) ([cf03f02](https://github.com/CruGlobal/cru-flags-ruby/commit/cf03f02adf998774ae1b258af5685ddb668140d5))
|
|
9
|
+
|
|
3
10
|
## [0.1.2](https://github.com/CruGlobal/cru-flags-ruby/compare/cru-flags/v0.1.1...cru-flags/v0.1.2) (2026-08-24)
|
|
4
11
|
|
|
5
12
|
|
data/README.md
CHANGED
|
@@ -32,4 +32,10 @@ until the first successful fetch; after that, an outage means flags stop
|
|
|
32
32
|
changing — never that they turn off. Reads never raise and never do I/O.
|
|
33
33
|
Health transitions (and only transitions) are logged.
|
|
34
34
|
|
|
35
|
+
In Rails, the client also keeps a copy of the last good document in
|
|
36
|
+
`Rails.cache` (no expiry). A process that boots while the flag service is down
|
|
37
|
+
serves that copy instead of every flag off. To opt out, set
|
|
38
|
+
`CruFlags.fallback_store = nil` anywhere in your app's config (application,
|
|
39
|
+
environment file, or initializer).
|
|
40
|
+
|
|
35
41
|
CRuby >= 3.2 only. See [docs/design.md](docs/design.md) for everything else.
|
data/docs/design.md
CHANGED
|
@@ -44,6 +44,9 @@ underneath it. What it replaces, and why, is §4.
|
|
|
44
44
|
background mode), and never changes answer because the network broke.
|
|
45
45
|
- A read-only `CruFlags::FlipperAdapter` over the snapshot, plus a Railtie
|
|
46
46
|
that wires it into Flipper with zero app code.
|
|
47
|
+
- An optional cross-process **fallback copy** of the last good document
|
|
48
|
+
(`Rails.cache` by default in Rails), read only by a process that boots
|
|
49
|
+
while the service is down (§6.1).
|
|
47
50
|
- Stdlib-only client core (`net/http`, `json`, `thread`); Ruby >= 3.2.
|
|
48
51
|
|
|
49
52
|
### Out of scope
|
|
@@ -66,9 +69,11 @@ underneath it. What it replaces, and why, is §4.
|
|
|
66
69
|
- We do **not** raise on misconfiguration. An application missing
|
|
67
70
|
`CRU_FLAGS_URL` must still boot and serve traffic, with every flag off —
|
|
68
71
|
exactly ararat's current Memory-adapter fallback posture.
|
|
69
|
-
- We do **not**
|
|
70
|
-
there is no TTL, no Marshal round-trip, and therefore
|
|
71
|
-
default_proc trap (§4).
|
|
72
|
+
- We do **not** read flags through `Rails.cache`. The snapshot is
|
|
73
|
+
process-local memory; there is no TTL, no Marshal round-trip, and therefore
|
|
74
|
+
no TTL cliff and no default_proc trap (§4). The §6.1 fallback copy is not a
|
|
75
|
+
read-through cache: it is written with no expiry, stored as a JSON string,
|
|
76
|
+
and read only by a process that has no snapshot at all.
|
|
72
77
|
|
|
73
78
|
---
|
|
74
79
|
|
|
@@ -134,8 +139,12 @@ CruFlags::Client.new(
|
|
|
134
139
|
poll_seconds: 30.0, # refresh interval, ±20% jitter
|
|
135
140
|
fetch_timeout: 2.0, # per-request open AND read timeout
|
|
136
141
|
on_error: nil, # nil -> log to the "cru_flags" logger (§3.6)
|
|
137
|
-
refresh_mode: nil
|
|
142
|
+
refresh_mode: nil, # nil -> read CRU_FLAGS_REFRESH_MODE, else "background"
|
|
143
|
+
fallback_store: nil # nil -> no fallback copy; a store, or a Proc returning one (§6.1)
|
|
138
144
|
)
|
|
145
|
+
|
|
146
|
+
CruFlags.fallback_store = nil # opt out; set anywhere in app config
|
|
147
|
+
CruFlags.fallback_store = -> { Rails.cache } # a store, or a Proc returning one
|
|
139
148
|
```
|
|
140
149
|
|
|
141
150
|
### 3.1 `CruFlags` module methods
|
|
@@ -145,6 +154,15 @@ The module methods delegate to a lazily-constructed process-wide
|
|
|
145
154
|
module-level `flags`. Requiring the gem does **nothing**: no environment
|
|
146
155
|
read, no socket, no thread (§7). The singleton is constructed on first use,
|
|
147
156
|
so tests and dotenv can set `CRU_FLAGS_URL` any time before the first lookup.
|
|
157
|
+
The singleton resolves `CruFlags.fallback_store` on every use, not once at
|
|
158
|
+
construction (§6.1): an explicit assignment (nil included) if there was one,
|
|
159
|
+
else `CruFlags.default_fallback_store`, the Railtie's `-> { Rails.cache }`. A
|
|
160
|
+
Proc is called on each use. That matters in `config/application.rb` and
|
|
161
|
+
environment files, where `Rails.cache` is still nil (Bootstrap has not built
|
|
162
|
+
it yet): a bare `Rails.cache` there assigns nil and turns the fallback off,
|
|
163
|
+
while `-> { Rails.cache }` works anywhere. `default_fallback_store=` belongs
|
|
164
|
+
to the Railtie, which overwrites it unconditionally; apps use
|
|
165
|
+
`fallback_store=`.
|
|
148
166
|
|
|
149
167
|
### 3.2 `enabled?(name) -> bool`
|
|
150
168
|
|
|
@@ -156,24 +174,26 @@ Symbol; Symbols are converted with `to_s` (the document's keys are strings;
|
|
|
156
174
|
Flipper call sites pass symbols).
|
|
157
175
|
|
|
158
176
|
Unknown flag, unset flag, no document yet, no `CRU_FLAGS_URL`, service down
|
|
159
|
-
before the first success — all `false`.
|
|
177
|
+
before the first success with no fallback copy (§6.1) — all `false`.
|
|
160
178
|
|
|
161
179
|
### 3.3 `ready(timeout: nil) -> bool`
|
|
162
180
|
|
|
163
181
|
Blocks until the **first fetch attempt completes** — success *or* failure —
|
|
164
182
|
and returns whether it completed within `timeout`. Returns `true` after a
|
|
165
|
-
failed first attempt (the attempt did complete, and
|
|
166
|
-
|
|
183
|
+
failed first attempt (the attempt did complete, and its answer stands until
|
|
184
|
+
the next poll: all off, or the §6.1 fallback copy). Returns `false` immediately, without blocking,
|
|
167
185
|
when the client is inert (no URL) — there is no attempt to wait for and never
|
|
168
186
|
will be. In on-demand mode `ready` *performs* the first attempt like any
|
|
169
187
|
other read, so `timeout:` is unused there — the call is bounded by the fetch
|
|
170
188
|
timeouts (up to ~2s by default) regardless of a smaller `timeout:` value,
|
|
171
|
-
exactly as the python client documents.
|
|
189
|
+
exactly as the python client documents. With a `fallback_store`, the attempt
|
|
190
|
+
also includes at most one store call (§6.1), bounded by the store's own timeouts.
|
|
191
|
+
Never raises.
|
|
172
192
|
|
|
173
193
|
### 3.4 `snapshot -> Hash`
|
|
174
194
|
|
|
175
195
|
A plain, deep-copied, JSON-serializable Hash of the last document received
|
|
176
|
-
(`{}` before the first success). `JSON.generate(CruFlags.snapshot)`
|
|
196
|
+
(`{}` before the first success, unless a §6.1 fallback copy was restored). `JSON.generate(CruFlags.snapshot)`
|
|
177
197
|
round-trips to exactly what the service sent. The copy exists so a caller
|
|
178
198
|
cannot mutate library state; the internally stored snapshot is genuinely
|
|
179
199
|
frozen (§6).
|
|
@@ -183,7 +203,8 @@ frozen (§6).
|
|
|
183
203
|
Fetches on the *calling* thread and returns whether the snapshot is fresh (an
|
|
184
204
|
attempt has completed and the most recent one succeeded). Without `force` it
|
|
185
205
|
is a no-op while the last attempt is younger than `poll_seconds`. Blocks for
|
|
186
|
-
at most the fetch timeouts
|
|
206
|
+
at most the fetch timeouts, plus one fallback-store call when one is
|
|
207
|
+
configured (§6.1). Never raises. This is the refresh primitive in
|
|
187
208
|
on-demand mode, and an out-of-band poke in background mode — where, per the
|
|
188
209
|
Node contract, concurrent `refresh` calls and a concurrent poller tick all
|
|
189
210
|
**share one in-flight request** rather than stacking simultaneous fetches.
|
|
@@ -226,7 +247,10 @@ fatal.**
|
|
|
226
247
|
post-fork poller, still lock-free and network-free — the guard only takes
|
|
227
248
|
a lock on the (rare) path where it actually needs to respawn.
|
|
228
249
|
2. **All flags are `false` until the first successful fetch.** A flag guards
|
|
229
|
-
*new* behaviour; the safe answer while ignorant is the old behaviour.
|
|
250
|
+
*new* behaviour; the safe answer while ignorant is the old behaviour. The
|
|
251
|
+
one exception is §6.1: a process whose first fetch fails serves the
|
|
252
|
+
fleet's last good copy if one exists, because a flag that is already on
|
|
253
|
+
for every other process is no longer "new" to the users of this one.
|
|
230
254
|
3. **Last-known-good persists indefinitely. There is no TTL.** This is the
|
|
231
255
|
failure-mode semantic §6 of the fleet's original flags design doc (the
|
|
232
256
|
Google-Doc-era spec, snapshotted as cru-flags-design-doc.md) promised and
|
|
@@ -329,6 +353,13 @@ that, in an initializer:
|
|
|
329
353
|
projects legitimately have no flag document, and `Flipper.add` is never
|
|
330
354
|
called by app code (flags are born in the service), so dev-mode `:warn`
|
|
331
355
|
would fire on every check.
|
|
356
|
+
- in a second initializer, sets `CruFlags.default_fallback_store =
|
|
357
|
+
-> { Rails.cache }` (§6.1). It is a default, not an assignment: a
|
|
358
|
+
`CruFlags.fallback_store =` anywhere in the app (`config/application.rb`
|
|
359
|
+
and environment files run *before* Railtie initializers,
|
|
360
|
+
`config/initializers` after) wins whatever the order, nil included. Both
|
|
361
|
+
are resolved on each use, so a flag read during boot cannot lock in the
|
|
362
|
+
default, and a `Rails.cache` swapped in later is the one used.
|
|
332
363
|
|
|
333
364
|
With `CRU_FLAGS_URL` unset the client is inert, the snapshot is empty, and
|
|
334
365
|
every flag reads `false` — byte-for-byte ararat's current Memory-adapter
|
|
@@ -364,6 +395,68 @@ thawed deep copy so the public API stays ordinary Ruby.
|
|
|
364
395
|
Consistency guarantee: a reader sees either the whole old document or the
|
|
365
396
|
whole new one, never a half-applied update.
|
|
366
397
|
|
|
398
|
+
### 6.1 Fallback copy
|
|
399
|
+
|
|
400
|
+
The snapshot lives in process memory, so a process that boots while the flag
|
|
401
|
+
service is down has nothing: every flag reads `false` until the service comes
|
|
402
|
+
back. For a flag that is off everywhere that changes nothing. For a flag that
|
|
403
|
+
is **on** in production (launched, not yet retired), the new process serves
|
|
404
|
+
the old behaviour while every older process serves the new one, and users see
|
|
405
|
+
the feature come and go between requests. Deploys start new processes, but
|
|
406
|
+
so do autoscaling and crash or health-check restarts, which happen with or
|
|
407
|
+
without a deploy.
|
|
408
|
+
|
|
409
|
+
A client built with `fallback_store:` keeps a second copy of the last good
|
|
410
|
+
document in a store shared across processes (in Rails, `Rails.cache`, so
|
|
411
|
+
Redis in most of the fleet):
|
|
412
|
+
|
|
413
|
+
- **Write**: after every successful tick (`200`, `304`, `404`), write the
|
|
414
|
+
current document as a JSON string under `cru_flags/v1/<url>`, with
|
|
415
|
+
`expires_in: nil`. The URL carries project and environment, so stage and
|
|
416
|
+
production never share a key even on a shared Redis. Rewriting on `304`
|
|
417
|
+
too means an evicted or flushed key comes back within one interval, and
|
|
418
|
+
the fleet converges on the newest document. A `404`'s empty document is
|
|
419
|
+
written like any other: "this project has no flags" is data (§4).
|
|
420
|
+
- **Read**: only on a **failed** tick, and only while the process has no
|
|
421
|
+
document at all. A warm process keeps its own document even if another
|
|
422
|
+
process has since written a newer copy: the shared copy exists to cover a
|
|
423
|
+
process with nothing, and the warm one picks up any change on its next
|
|
424
|
+
successful tick. A found copy is
|
|
425
|
+
parsed with `Document.parse`, so it is validated and deep-frozen exactly
|
|
426
|
+
like a fetched one; anything else (a miss, a non-String, bad JSON, a
|
|
427
|
+
wrong-typed `Flags`) is ignored and the process stays at all-`false`.
|
|
428
|
+
- The restored copy sets **no ETag**, so the first successful fetch after
|
|
429
|
+
recovery is a full `200` that replaces it. Health stays `failing`: the
|
|
430
|
+
fetch did fail, and §3.6 reports it as usual.
|
|
431
|
+
- Both sides run inside `attempt_fetch`, under the fetch mutex, and both
|
|
432
|
+
swallow every exception; the health transition is reported before the
|
|
433
|
+
read, so a broken store can never silence an outage. In background mode
|
|
434
|
+
that is the poller thread, so a slow or dead store costs the poller time,
|
|
435
|
+
never a request. In on-demand mode (§7.1) the fetch already runs on the
|
|
436
|
+
reading thread, so once per interval a request also waits on one store
|
|
437
|
+
call, bounded by the store's own timeouts.
|
|
438
|
+
- Stored as a JSON string, not a Hash, so no cache store ever Marshals a
|
|
439
|
+
frozen tree (the §4 default_proc trap cannot apply).
|
|
440
|
+
|
|
441
|
+
The copy is only as durable as the store. `expires_in: nil` stops a TTL, not
|
|
442
|
+
eviction: a full or LRU-evicting Redis can drop the key, and SolidCache
|
|
443
|
+
removes rows by age (`max_age`, two weeks by default) and evicts oldest
|
|
444
|
+
first, and its upsert does not refresh a row's age. While the service is up,
|
|
445
|
+
the every-tick rewrite puts the copy back within one interval; during an
|
|
446
|
+
outage nothing can. A per-container store (`FileStore`, `MemoryStore`) only
|
|
447
|
+
helps a restart inside the same container. This assumes `CRU_FLAGS_URL` is
|
|
448
|
+
unset during image builds (the fleet injects it at runtime), which leaves the
|
|
449
|
+
client inert; a build that sets it and reads a flag would bake a copy into
|
|
450
|
+
the image's `tmp/cache` under `FileStore`.
|
|
451
|
+
|
|
452
|
+
Trade-off accepted: the copy is only as fresh as the last process that
|
|
453
|
+
reached the service. That is the same promise a warm process makes during an
|
|
454
|
+
outage (§4, point 3), extended to processes that start during one.
|
|
455
|
+
|
|
456
|
+
The siblings have the same cold-start gap. Their runtimes (Lambda, Cloud
|
|
457
|
+
Run) rarely have a shared cache next to them, so this ships Ruby-first, like
|
|
458
|
+
§8's redirect and body-cap hardening.
|
|
459
|
+
|
|
367
460
|
---
|
|
368
461
|
|
|
369
462
|
## 7. Threading model
|
|
@@ -464,6 +557,9 @@ One tick:
|
|
|
464
557
|
the client would pin itself to the bad response via endless 304s.
|
|
465
558
|
|
|
466
559
|
4. Report the health transition (§3.6), if any.
|
|
560
|
+
5. With a `fallback_store` (§6.1): on an `ok` outcome, write the current
|
|
561
|
+
document to it; on a `failing` outcome while there is no snapshot yet,
|
|
562
|
+
publish the stored copy if it parses.
|
|
467
563
|
|
|
468
564
|
Parsing is lenient exactly like the siblings, with the Node client's §6.1
|
|
469
565
|
rules as the reference: missing/`null` `Flags` becomes `{}`, but `Flags`
|
|
@@ -500,6 +596,16 @@ Plus, ported from the siblings and ararat:
|
|
|
500
596
|
returns.
|
|
501
597
|
- On-demand mode: its own section (coalescing, one-fetch-per-interval while
|
|
502
598
|
down, no thread).
|
|
599
|
+
- Fallback copy (§6.1): saved with no expiry on every `ok` tick, keyed by
|
|
600
|
+
URL; restored only by a process with no document; garbage and a raising
|
|
601
|
+
store ignored; an `ActiveSupport::Cache::MemoryStore` with a default TTL
|
|
602
|
+
keeps the copy past it; a raising store read still reports the outage;
|
|
603
|
+
on-demand mode restores too; an assignment beats the default in either
|
|
604
|
+
order, even after the singleton exists; a Proc works through both
|
|
605
|
+
`Client.new` and the module setter; and four real Rails boots: zero-config
|
|
606
|
+
uses `Rails.cache`, `-> { Rails.cache }` set in app config resolves to it,
|
|
607
|
+
and a `nil` set in app config (before the Railtie) or in an initializer
|
|
608
|
+
(after it) wins.
|
|
503
609
|
- `test/verify_live.rb`, opt-in and networked, against the real public
|
|
504
610
|
endpoint (document parses; second conditional fetch is 304). Not in CI.
|
|
505
611
|
|
data/lib/cru_flags/client.rb
CHANGED
|
@@ -22,12 +22,13 @@ module CruFlags
|
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
def initialize(url: nil, poll_seconds: 30.0, fetch_timeout: 2.0,
|
|
25
|
-
on_error: nil, refresh_mode: nil)
|
|
25
|
+
on_error: nil, refresh_mode: nil, fallback_store: nil)
|
|
26
26
|
@poll_seconds = positive(poll_seconds, 30.0)
|
|
27
27
|
@fetch_timeout = positive(fetch_timeout, 2.0)
|
|
28
28
|
@on_error = on_error || default_on_error
|
|
29
29
|
@refresh_mode = resolve_mode(refresh_mode)
|
|
30
30
|
@url = url || self.class.url_from_env
|
|
31
|
+
@fallback_store = fallback_store
|
|
31
32
|
@document = nil
|
|
32
33
|
@etag = nil
|
|
33
34
|
@healthy = true
|
|
@@ -268,10 +269,38 @@ module CruFlags
|
|
|
268
269
|
became(healthy: true)
|
|
269
270
|
when :failed
|
|
270
271
|
became(healthy: false, error: outcome.error)
|
|
272
|
+
restore_fallback if @document.nil?
|
|
271
273
|
end
|
|
272
274
|
@last_attempt_ok = outcome.kind != :failed
|
|
275
|
+
save_fallback if @last_attempt_ok
|
|
273
276
|
end
|
|
274
277
|
|
|
278
|
+
# Design doc §6.1. Runs inside attempt_fetch, never on enabled?'s
|
|
279
|
+
# background-mode read path, and never raises.
|
|
280
|
+
def save_fallback
|
|
281
|
+
store = fallback_store or return
|
|
282
|
+
# expires_in: nil overrides any store-wide default TTL: a copy that
|
|
283
|
+
# expires is the TTL cliff §4 rules out.
|
|
284
|
+
store.write(fallback_key, JSON.generate(@document), expires_in: nil)
|
|
285
|
+
rescue
|
|
286
|
+
nil
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# Only for a process that has never held a document.
|
|
290
|
+
def restore_fallback
|
|
291
|
+
store = fallback_store or return
|
|
292
|
+
body = store.read(fallback_key)
|
|
293
|
+
@document = Document.parse(body) if body.is_a?(String)
|
|
294
|
+
rescue
|
|
295
|
+
nil
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def fallback_store
|
|
299
|
+
@fallback_store.is_a?(Proc) ? @fallback_store.call : @fallback_store
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def fallback_key = "cru_flags/v1/#{@url}"
|
|
303
|
+
|
|
275
304
|
def became(healthy:, error: nil)
|
|
276
305
|
return if healthy == @healthy
|
|
277
306
|
@healthy = healthy
|
data/lib/cru_flags/railtie.rb
CHANGED
|
@@ -41,5 +41,12 @@ module CruFlags
|
|
|
41
41
|
app.config.flipper.strict = false
|
|
42
42
|
end
|
|
43
43
|
end
|
|
44
|
+
|
|
45
|
+
# A default, not an assignment: CruFlags.fallback_store set anywhere
|
|
46
|
+
# (application.rb, an environment file, an initializer), before or after
|
|
47
|
+
# this runs, wins. Rails.cache is looked up on each use.
|
|
48
|
+
initializer "cru_flags.fallback_store" do
|
|
49
|
+
CruFlags.default_fallback_store = -> { Rails.cache }
|
|
50
|
+
end
|
|
44
51
|
end
|
|
45
52
|
end
|
data/lib/cru_flags/version.rb
CHANGED
data/lib/cru_flags.rb
CHANGED
|
@@ -15,13 +15,32 @@ module CruFlags
|
|
|
15
15
|
@client = nil
|
|
16
16
|
@flipper_adapter_mutex = Mutex.new
|
|
17
17
|
@flipper_adapter = nil
|
|
18
|
+
UNSET = Object.new.freeze
|
|
19
|
+
private_constant :UNSET
|
|
20
|
+
@fallback_store = UNSET
|
|
21
|
+
@default_fallback_store = nil
|
|
18
22
|
|
|
19
23
|
class << self
|
|
24
|
+
# The singleton's cross-process fallback store (design doc §6.1): a
|
|
25
|
+
# store, or a Proc returning one. Any assignment, nil included, beats
|
|
26
|
+
# default_fallback_store whenever it happens: the singleton resolves the
|
|
27
|
+
# store on every use, not once at construction.
|
|
28
|
+
attr_writer :fallback_store
|
|
29
|
+
|
|
30
|
+
# :nodoc: Owned by the Railtie, which overwrites it unconditionally.
|
|
31
|
+
attr_writer :default_fallback_store
|
|
32
|
+
|
|
33
|
+
def fallback_store
|
|
34
|
+
store = @fallback_store
|
|
35
|
+
store = @default_fallback_store if store.equal?(UNSET)
|
|
36
|
+
store.is_a?(Proc) ? store.call : store
|
|
37
|
+
end
|
|
38
|
+
|
|
20
39
|
# The env read happens inside Client (Client.url_from_env), so the
|
|
21
40
|
# singleton and a hand-rolled `Client.new` share one normalization
|
|
22
41
|
# rather than two copies that can drift.
|
|
23
42
|
def client
|
|
24
|
-
@client || @client_mutex.synchronize { @client ||= Client.new }
|
|
43
|
+
@client || @client_mutex.synchronize { @client ||= Client.new(fallback_store: -> { fallback_store }) }
|
|
25
44
|
end
|
|
26
45
|
|
|
27
46
|
def enabled?(name) = client.enabled?(name)
|
|
@@ -51,12 +70,14 @@ module CruFlags
|
|
|
51
70
|
end
|
|
52
71
|
|
|
53
72
|
# Test hook, not public API: closes and discards the singleton so the
|
|
54
|
-
# next call re-reads the environment
|
|
73
|
+
# next call re-reads the environment, and forgets any fallback_store
|
|
74
|
+
# assignment, so the Railtie's default applies again.
|
|
55
75
|
def reset!
|
|
56
76
|
@client_mutex.synchronize do
|
|
57
77
|
@client&.close
|
|
58
78
|
@client = nil
|
|
59
79
|
end
|
|
80
|
+
@fallback_store = UNSET
|
|
60
81
|
@flipper_adapter_mutex.synchronize { @flipper_adapter = nil }
|
|
61
82
|
end
|
|
62
83
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: cru-flags
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Cru
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-10-29 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: flipper
|