featureflip 2.8.0 → 2.9.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/lib/featureflip/data_source/streaming.rb +23 -5
- data/lib/featureflip/shared_core.rb +25 -3
- data/lib/featureflip/version.rb +1 -1
- 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: 4dfdc2e9de3795a36a917d2a748184853bbd1c30362dd313ac70e386a4d0758c
|
|
4
|
+
data.tar.gz: e878ef6a553b2948435fc004b905f84015433b42625721d41311c5cb55e87654
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 871ae6a42bfcc7663a805a70935e2ac6b5a746a879df937343239f5f833c00be0990890f0ec47684a1beca0507160439274e26a606ef957510a3aa400d442929
|
|
7
|
+
data.tar.gz: d793281fb6b00d5cfeb5226c89c035fa17d6fad5d7af1876eac1c7d8c4c1290452c2067b6b9c8106795e4511451e43ef4debc399e660b245b3f7685507c494af
|
|
@@ -36,7 +36,7 @@ module Featureflip
|
|
|
36
36
|
RECONNECT_BASE_DELAY_SECONDS = 1
|
|
37
37
|
MAX_BACKOFF_SECONDS = 30
|
|
38
38
|
|
|
39
|
-
def initialize(sdk_key:, config:, http_client:, on_flag_updated:, on_flag_deleted:, on_segment_updated:, on_error:, on_sync: nil,
|
|
39
|
+
def initialize(sdk_key:, config:, http_client:, on_flag_updated:, on_flag_deleted:, on_segment_updated:, on_error:, on_sync: nil, on_fallback_to_polling: nil, on_recovered: nil)
|
|
40
40
|
@sdk_key = sdk_key
|
|
41
41
|
@config = config
|
|
42
42
|
@http_client = http_client
|
|
@@ -45,10 +45,14 @@ module Featureflip
|
|
|
45
45
|
@on_segment_updated = on_segment_updated
|
|
46
46
|
@on_error = on_error
|
|
47
47
|
@on_sync = on_sync
|
|
48
|
-
@
|
|
48
|
+
@on_fallback_to_polling = on_fallback_to_polling
|
|
49
|
+
@on_recovered = on_recovered
|
|
49
50
|
@stop_flag = false
|
|
50
51
|
@thread = nil
|
|
51
52
|
@retry_count = 0
|
|
53
|
+
# True between arming the polling fallback and the next delivered frame.
|
|
54
|
+
# Only ever touched from the streaming thread.
|
|
55
|
+
@fallback_active = false
|
|
52
56
|
@current_event_type = nil
|
|
53
57
|
@current_data = nil
|
|
54
58
|
@line_buffer = String.new # ASCII-8BIT: raw read_body bytes concatenate safely
|
|
@@ -60,6 +64,7 @@ module Featureflip
|
|
|
60
64
|
def start
|
|
61
65
|
@stop_flag = false
|
|
62
66
|
@retry_count = 0
|
|
67
|
+
@fallback_active = false
|
|
63
68
|
@thread = Thread.new { run }
|
|
64
69
|
end
|
|
65
70
|
|
|
@@ -112,9 +117,14 @@ module Featureflip
|
|
|
112
117
|
# purposes — otherwise an accept-then-close server never accumulates
|
|
113
118
|
# toward max_stream_retries and never degrades to polling.
|
|
114
119
|
@retry_count += 1
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
120
|
+
# The fallback is ADDITIVE, never terminal (#3071). Polling covers the
|
|
121
|
+
# outage; this loop keeps retrying the stream underneath at the capped
|
|
122
|
+
# backoff, and the next delivered frame retires the poller. Breaking out
|
|
123
|
+
# here left the process polling — and blind to real-time updates — until
|
|
124
|
+
# it restarted, after only ~31s of unreachability.
|
|
125
|
+
if @retry_count > @config.max_stream_retries && !@fallback_active
|
|
126
|
+
@fallback_active = true
|
|
127
|
+
@on_fallback_to_polling&.call
|
|
118
128
|
end
|
|
119
129
|
end
|
|
120
130
|
|
|
@@ -189,6 +199,14 @@ module Featureflip
|
|
|
189
199
|
@current_data = @current_data.nil? ? fragment : "#{@current_data}\n#{fragment}"
|
|
190
200
|
elsif line.empty? && @current_event_type && @current_data
|
|
191
201
|
@delivered_frame = true
|
|
202
|
+
# Retire the fallback poller HERE rather than when run() next comes round:
|
|
203
|
+
# connect() blocks in read_body for the whole lifetime of a healthy stream,
|
|
204
|
+
# so a reap on return would leave the poller alive that entire time, and its
|
|
205
|
+
# periodic whole-store replaces would revert deltas applied by this stream.
|
|
206
|
+
if @fallback_active
|
|
207
|
+
@fallback_active = false
|
|
208
|
+
@on_recovered&.call
|
|
209
|
+
end
|
|
192
210
|
handle_event(@current_event_type, @current_data)
|
|
193
211
|
@current_event_type = nil
|
|
194
212
|
@current_data = nil
|
|
@@ -276,6 +276,11 @@ module Featureflip
|
|
|
276
276
|
@streaming_handler&.stop
|
|
277
277
|
@polling_handler&.stop
|
|
278
278
|
@event_processor&.stop
|
|
279
|
+
# Clear both before restarting: start_polling refuses to start a second
|
|
280
|
+
# poller while one is referenced, so a stopped-but-referenced handler here
|
|
281
|
+
# would leave a restarted non-streaming core with no data source at all.
|
|
282
|
+
@streaming_handler = nil
|
|
283
|
+
@polling_handler = nil
|
|
279
284
|
|
|
280
285
|
if @config.streaming
|
|
281
286
|
start_streaming
|
|
@@ -361,18 +366,35 @@ module Featureflip
|
|
|
361
366
|
on_segment_updated: ->(flags, segments) { @store.init(flags, segments) },
|
|
362
367
|
on_sync: ->(flags, segments) { @store.init(flags, segments) },
|
|
363
368
|
on_error: ->(_err) { },
|
|
364
|
-
|
|
369
|
+
on_fallback_to_polling: -> { fallback_to_polling },
|
|
370
|
+
on_recovered: -> { stop_fallback_polling }
|
|
365
371
|
)
|
|
366
372
|
@streaming_handler.start
|
|
367
373
|
end
|
|
368
374
|
|
|
369
375
|
def fallback_to_polling
|
|
370
|
-
@config.logger&.warn(
|
|
371
|
-
|
|
376
|
+
@config.logger&.warn(
|
|
377
|
+
"Featureflip: streaming retries exhausted, falling back to polling while the stream keeps retrying"
|
|
378
|
+
)
|
|
379
|
+
# The handler is NOT cleared: it is still running and still trying to reopen
|
|
380
|
+
# the stream (#3071). Clearing it here is what used to make the fallback
|
|
381
|
+
# permanent — nothing would have restarted streaming.
|
|
372
382
|
start_polling
|
|
373
383
|
end
|
|
374
384
|
|
|
385
|
+
def stop_fallback_polling
|
|
386
|
+
return if @polling_handler.nil?
|
|
387
|
+
|
|
388
|
+
@config.logger&.info("Featureflip: stream recovered, stopping polling fallback")
|
|
389
|
+
@polling_handler.stop
|
|
390
|
+
@polling_handler = nil
|
|
391
|
+
end
|
|
392
|
+
|
|
375
393
|
def start_polling
|
|
394
|
+
# Reachable from both start_data_source and the streaming fallback; a second
|
|
395
|
+
# call must not leak a second polling thread.
|
|
396
|
+
return if @polling_handler
|
|
397
|
+
|
|
376
398
|
@polling_handler = DataSource::PollingHandler.new(
|
|
377
399
|
http_client: @http_client,
|
|
378
400
|
config: @config,
|
data/lib/featureflip/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: featureflip
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.9.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Featureflip
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09-
|
|
11
|
+
date: 2026-09-18 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: logger
|