Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions lib/unleash/backup_file_reader.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
require 'unleash/configuration'
require 'json'

module Unleash
class BackupFileReader
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is extracted from toggle_fetcher

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, I really like this

def self.read!
Unleash.logger.debug "read!()"

backup_file = Unleash.configuration.backup_file
return nil unless File.exist?(backup_file)

File.read(backup_file)
rescue IOError => e
# :nocov:
Unleash.logger.error "Unable to read the backup_file: #{e}"
# :nocov:
nil
rescue JSON::ParserError => e
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually don't think this case can happen since we no longer parse on read. I realise this is a hangover from the old implementation (my bad, should have dropped this when I ported to Ygg). If we don't remove it in this PR, no biggie, I'll do it in a future one

# :nocov:
Unleash.logger.error "Unable to parse JSON from existing backup_file: #{e}"
# :nocov:
nil
rescue StandardError => e
# :nocov:
Unleash.logger.error "Unable to extract valid data from backup_file. Exception thrown: #{e}"
# :nocov:
nil
end
end
end
29 changes: 29 additions & 0 deletions lib/unleash/streaming_client_executor.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
require 'unleash/streaming_event_processor'
require 'unleash/bootstrap/handler'
require 'unleash/backup_file_reader'
require 'unleash/util/event_source_wrapper'

module Unleash
Expand All @@ -10,6 +12,20 @@ def initialize(name, engine)
self.event_source = nil
self.event_processor = Unleash::StreamingEventProcessor.new(engine)
self.running = false

begin
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this was missing

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

similar setup to toggle fetcher in polling mode but we don't make first request to the server and instead go straight to either bootstrap or a backup file. Then streaming starts and takes over from there

# if bootstrap configuration is available, initialize. Otherwise read backup file
if Unleash.configuration.use_bootstrap?
bootstrap(engine)
else
read_backup_file!(engine)
end
rescue StandardError => e
# fail back to reading the backup file
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also a hangover from the old code haha, but may as well fix it while we're here

Suggested change
# fail back to reading the backup file
# fall back to reading the backup file

Unleash.logger.warn "StreamingClientExecutor was unable to initialize, attempting to read from backup file."
Unleash.logger.debug "Exception Caught: #{e}"
read_backup_file!(engine)
end
end

def run(&_block)
Expand Down Expand Up @@ -81,5 +97,18 @@ def handle_event(event)
Unleash.logger.error "Streaming client #{self.name} threw exception #{e.class}: '#{e}'"
Unleash.logger.debug "stacktrace: #{e.backtrace}"
end

def read_backup_file!(engine)
backup_data = Unleash::BackupFileReader.read!
engine.take_state(backup_data) if backup_data
end

def bootstrap(engine)
bootstrap_payload = Unleash::Bootstrap::Handler.new(Unleash.configuration.bootstrap_config).retrieve_toggles
engine.take_state(bootstrap_payload)

# reset Unleash.configuration.bootstrap_data to free up memory, as we will never use it again
Unleash.configuration.bootstrap_config = nil
end
end
end
25 changes: 5 additions & 20 deletions lib/unleash/toggle_fetcher.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
require 'unleash/configuration'
require 'unleash/bootstrap/handler'
require 'unleash/backup_file_writer'
require 'unleash/backup_file_reader'
require 'net/http'
require 'json'
require 'yggdrasil_engine'
Expand All @@ -27,7 +28,7 @@ def initialize(engine)
# fail back to reading the backup file
Unleash.logger.warn "ToggleFetcher was unable to fetch from the network, attempting to read from backup file."
Unleash.logger.debug "Exception Caught: #{e}"
read!
read_backup_file!
end

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

def read!
Unleash.logger.debug "read!()"
backup_file = Unleash.configuration.backup_file
return nil unless File.exist?(backup_file)

backup_data = File.read(backup_file)
update_engine_state!(backup_data)
rescue IOError => e
# :nocov:
Unleash.logger.error "Unable to read the backup_file: #{e}"
# :nocov:
rescue JSON::ParserError => e
# :nocov:
Unleash.logger.error "Unable to parse JSON from existing backup_file: #{e}"
# :nocov:
rescue StandardError => e
# :nocov:
Unleash.logger.error "Unable to extract valid data from backup_file. Exception thrown: #{e}"
# :nocov:
def read_backup_file!
backup_data = Unleash::BackupFileReader.read!
update_engine_state!(backup_data) if backup_data
end

def bootstrap
Expand Down
160 changes: 160 additions & 0 deletions spec/unleash/streaming_client_executor_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
RSpec.describe Unleash::StreamingClientExecutor do
subject(:streaming_executor) { Unleash::StreamingClientExecutor.new(executor_name, engine) }

unless RUBY_ENGINE == 'jruby'
before do
Unleash.configure do |config|
config.url = 'http://streaming-test-url/'
config.app_name = 'streaming-test-app'
config.instance_id = 'rspec/streaming'
config.disable_metrics = true
config.experimental_mode = { type: 'streaming' }
end

WebMock.stub_request(:post, "http://streaming-test-url/client/register")
.to_return(status: 200, body: "", headers: {})

Unleash.logger = Unleash.configuration.logger
end

after do
WebMock.reset!
File.delete(Unleash.configuration.backup_file) if File.exist?(Unleash.configuration.backup_file)

# Reset configuration to prevent interference with other tests
Unleash.configuration.bootstrap_config = nil
Unleash.configuration.experimental_mode = nil
Unleash.configuration.disable_metrics = false
end

describe '.new' do
let(:engine) { YggdrasilEngine.new }
let(:executor_name) { 'streaming_client_executor_spec' }

context 'when there are problems connecting to streaming endpoint' do
let(:backup_toggles) do
{
version: 1,
features: [
{
name: "backup-feature",
description: "Feature from backup",
enabled: true,
strategies: [{
name: "default"
}]
}
]
}
end

before do
backup_file = Unleash.configuration.backup_file

# manually create a stub cache on disk, so we can test that we read it correctly later.
File.open(backup_file, "w") do |file|
file.write(backup_toggles.to_json)
end

WebMock.stub_request(:get, "http://streaming-test-url/client/streaming")
.to_return(status: 500, body: "Internal Server Error", headers: {})

streaming_executor
end

it 'reads the backup file for values' do
enabled = engine.enabled?('backup-feature', {})
expect(enabled).to eq(true)
end
end

context 'when bootstrap is configured' do
let(:bootstrap_data) do
{
version: 1,
features: [
{
name: "bootstrap-feature",
enabled: true,
strategies: [{ name: "default" }]
}
]
}
end

let(:bootstrap_config) do
Unleash::Bootstrap::Configuration.new({
'data' => bootstrap_data.to_json
})
end

before do
Unleash.configuration.bootstrap_config = bootstrap_config

WebMock.stub_request(:get, "http://streaming-test-url/client/streaming")
.to_return(status: 200, body: "", headers: {})

streaming_executor
end

after do
Unleash.configuration.bootstrap_config = nil
end

it 'uses bootstrap data on initialization' do
enabled = engine.enabled?('bootstrap-feature', {})
expect(enabled).to eq(true)
end

it 'clears bootstrap config after use' do
expect(Unleash.configuration.bootstrap_config).to be_nil
end
end

context 'when bootstrap fails and backup file exists' do
let(:invalid_bootstrap_config) do
Unleash::Bootstrap::Configuration.new({
'data' => 'invalid json'
})
end

let(:fallback_toggles) do
{
version: 1,
features: [
{
name: "fallback-feature",
enabled: true,
strategies: [{ name: "default" }]
}
]
}
end

before do
backup_file = Unleash.configuration.backup_file

File.open(backup_file, "w") do |file|
file.write(fallback_toggles.to_json)
end

Unleash.configuration.bootstrap_config = invalid_bootstrap_config

WebMock.stub_request(:get, "http://streaming-test-url/client/streaming")
.to_return(status: 500, body: "", headers: {})

streaming_executor
end

after do
Unleash.configuration.bootstrap_config = nil
end

it 'falls back to reading backup file when bootstrap fails' do
enabled = engine.enabled?('fallback-feature', {})
expect(enabled).to eq(true)
end
end
end
end
end
Loading