Skip to content

Commit bf943d4

Browse files
authored
feat: read backup file and bootstrap for streaming (#255)
1 parent 4184c8c commit bf943d4

File tree

4 files changed

+219
-21
lines changed

4 files changed

+219
-21
lines changed

lib/unleash/backup_file_reader.rb

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
require 'unleash/configuration'
2+
3+
module Unleash
4+
class BackupFileReader
5+
def self.read!
6+
Unleash.logger.debug "read!()"
7+
8+
backup_file = Unleash.configuration.backup_file
9+
return nil unless File.exist?(backup_file)
10+
11+
File.read(backup_file)
12+
rescue IOError => e
13+
# :nocov:
14+
Unleash.logger.error "Unable to read the backup_file: #{e}"
15+
# :nocov:
16+
nil
17+
rescue StandardError => e
18+
# :nocov:
19+
Unleash.logger.error "Unable to extract valid data from backup_file. Exception thrown: #{e}"
20+
# :nocov:
21+
nil
22+
end
23+
end
24+
end

lib/unleash/streaming_client_executor.rb

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
require 'unleash/streaming_event_processor'
2+
require 'unleash/bootstrap/handler'
3+
require 'unleash/backup_file_reader'
24
require 'unleash/util/event_source_wrapper'
35

46
module Unleash
@@ -10,6 +12,20 @@ def initialize(name, engine)
1012
self.event_source = nil
1113
self.event_processor = Unleash::StreamingEventProcessor.new(engine)
1214
self.running = false
15+
16+
begin
17+
# if bootstrap configuration is available, initialize. Otherwise read backup file
18+
if Unleash.configuration.use_bootstrap?
19+
bootstrap(engine)
20+
else
21+
read_backup_file!(engine)
22+
end
23+
rescue StandardError => e
24+
# fall back to reading the backup file
25+
Unleash.logger.warn "StreamingClientExecutor was unable to initialize, attempting to read from backup file."
26+
Unleash.logger.debug "Exception Caught: #{e}"
27+
read_backup_file!(engine)
28+
end
1329
end
1430

1531
def run(&_block)
@@ -81,5 +97,18 @@ def handle_event(event)
8197
Unleash.logger.error "Streaming client #{self.name} threw exception #{e.class}: '#{e}'"
8298
Unleash.logger.debug "stacktrace: #{e.backtrace}"
8399
end
100+
101+
def read_backup_file!(engine)
102+
backup_data = Unleash::BackupFileReader.read!
103+
engine.take_state(backup_data) if backup_data
104+
end
105+
106+
def bootstrap(engine)
107+
bootstrap_payload = Unleash::Bootstrap::Handler.new(Unleash.configuration.bootstrap_config).retrieve_toggles
108+
engine.take_state(bootstrap_payload)
109+
110+
# reset Unleash.configuration.bootstrap_data to free up memory, as we will never use it again
111+
Unleash.configuration.bootstrap_config = nil
112+
end
84113
end
85114
end

lib/unleash/toggle_fetcher.rb

Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
require 'unleash/configuration'
22
require 'unleash/bootstrap/handler'
33
require 'unleash/backup_file_writer'
4+
require 'unleash/backup_file_reader'
45
require 'net/http'
56
require 'json'
67
require 'yggdrasil_engine'
@@ -24,10 +25,10 @@ def initialize(engine)
2425
fetch
2526
end
2627
rescue StandardError => e
27-
# fail back to reading the backup file
28+
# fall back to reading the backup file
2829
Unleash.logger.warn "ToggleFetcher was unable to fetch from the network, attempting to read from backup file."
2930
Unleash.logger.debug "Exception Caught: #{e}"
30-
read!
31+
read_backup_file!
3132
end
3233

3334
# once initialized, somewhere else you will want to start a loop with fetch()
@@ -71,25 +72,9 @@ def update_engine_state!(toggle_data)
7172
Unleash.logger.error "Failed to hydrate state: #{e.backtrace}"
7273
end
7374

74-
def read!
75-
Unleash.logger.debug "read!()"
76-
backup_file = Unleash.configuration.backup_file
77-
return nil unless File.exist?(backup_file)
78-
79-
backup_data = File.read(backup_file)
80-
update_engine_state!(backup_data)
81-
rescue IOError => e
82-
# :nocov:
83-
Unleash.logger.error "Unable to read the backup_file: #{e}"
84-
# :nocov:
85-
rescue JSON::ParserError => e
86-
# :nocov:
87-
Unleash.logger.error "Unable to parse JSON from existing backup_file: #{e}"
88-
# :nocov:
89-
rescue StandardError => e
90-
# :nocov:
91-
Unleash.logger.error "Unable to extract valid data from backup_file. Exception thrown: #{e}"
92-
# :nocov:
75+
def read_backup_file!
76+
backup_data = Unleash::BackupFileReader.read!
77+
update_engine_state!(backup_data) if backup_data
9378
end
9479

9580
def bootstrap
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
RSpec.describe Unleash::StreamingClientExecutor do
2+
subject(:streaming_executor) { Unleash::StreamingClientExecutor.new(executor_name, engine) }
3+
4+
unless RUBY_ENGINE == 'jruby'
5+
before do
6+
Unleash.configure do |config|
7+
config.url = 'http://streaming-test-url/'
8+
config.app_name = 'streaming-test-app'
9+
config.instance_id = 'rspec/streaming'
10+
config.disable_metrics = true
11+
config.experimental_mode = { type: 'streaming' }
12+
end
13+
14+
WebMock.stub_request(:post, "http://streaming-test-url/client/register")
15+
.to_return(status: 200, body: "", headers: {})
16+
17+
Unleash.logger = Unleash.configuration.logger
18+
end
19+
20+
after do
21+
WebMock.reset!
22+
File.delete(Unleash.configuration.backup_file) if File.exist?(Unleash.configuration.backup_file)
23+
24+
# Reset configuration to prevent interference with other tests
25+
Unleash.configuration.bootstrap_config = nil
26+
Unleash.configuration.experimental_mode = nil
27+
Unleash.configuration.disable_metrics = false
28+
end
29+
30+
describe '.new' do
31+
let(:engine) { YggdrasilEngine.new }
32+
let(:executor_name) { 'streaming_client_executor_spec' }
33+
34+
context 'when there are problems connecting to streaming endpoint' do
35+
let(:backup_toggles) do
36+
{
37+
version: 1,
38+
features: [
39+
{
40+
name: "backup-feature",
41+
description: "Feature from backup",
42+
enabled: true,
43+
strategies: [{
44+
name: "default"
45+
}]
46+
}
47+
]
48+
}
49+
end
50+
51+
before do
52+
backup_file = Unleash.configuration.backup_file
53+
54+
# manually create a stub cache on disk, so we can test that we read it correctly later.
55+
File.open(backup_file, "w") do |file|
56+
file.write(backup_toggles.to_json)
57+
end
58+
59+
WebMock.stub_request(:get, "http://streaming-test-url/client/streaming")
60+
.to_return(status: 500, body: "Internal Server Error", headers: {})
61+
62+
streaming_executor
63+
end
64+
65+
it 'reads the backup file for values' do
66+
enabled = engine.enabled?('backup-feature', {})
67+
expect(enabled).to eq(true)
68+
end
69+
end
70+
71+
context 'when bootstrap is configured' do
72+
let(:bootstrap_data) do
73+
{
74+
version: 1,
75+
features: [
76+
{
77+
name: "bootstrap-feature",
78+
enabled: true,
79+
strategies: [{ name: "default" }]
80+
}
81+
]
82+
}
83+
end
84+
85+
let(:bootstrap_config) do
86+
Unleash::Bootstrap::Configuration.new({
87+
'data' => bootstrap_data.to_json
88+
})
89+
end
90+
91+
before do
92+
Unleash.configuration.bootstrap_config = bootstrap_config
93+
94+
WebMock.stub_request(:get, "http://streaming-test-url/client/streaming")
95+
.to_return(status: 200, body: "", headers: {})
96+
97+
streaming_executor
98+
end
99+
100+
after do
101+
Unleash.configuration.bootstrap_config = nil
102+
end
103+
104+
it 'uses bootstrap data on initialization' do
105+
enabled = engine.enabled?('bootstrap-feature', {})
106+
expect(enabled).to eq(true)
107+
end
108+
109+
it 'clears bootstrap config after use' do
110+
expect(Unleash.configuration.bootstrap_config).to be_nil
111+
end
112+
end
113+
114+
context 'when bootstrap fails and backup file exists' do
115+
let(:invalid_bootstrap_config) do
116+
Unleash::Bootstrap::Configuration.new({
117+
'data' => 'invalid json'
118+
})
119+
end
120+
121+
let(:fallback_toggles) do
122+
{
123+
version: 1,
124+
features: [
125+
{
126+
name: "fallback-feature",
127+
enabled: true,
128+
strategies: [{ name: "default" }]
129+
}
130+
]
131+
}
132+
end
133+
134+
before do
135+
backup_file = Unleash.configuration.backup_file
136+
137+
File.open(backup_file, "w") do |file|
138+
file.write(fallback_toggles.to_json)
139+
end
140+
141+
Unleash.configuration.bootstrap_config = invalid_bootstrap_config
142+
143+
WebMock.stub_request(:get, "http://streaming-test-url/client/streaming")
144+
.to_return(status: 500, body: "", headers: {})
145+
146+
streaming_executor
147+
end
148+
149+
after do
150+
Unleash.configuration.bootstrap_config = nil
151+
end
152+
153+
it 'falls back to reading backup file when bootstrap fails' do
154+
enabled = engine.enabled?('fallback-feature', {})
155+
expect(enabled).to eq(true)
156+
end
157+
end
158+
end
159+
end
160+
end

0 commit comments

Comments
 (0)