diff --git a/lib/unleash/backup_file_writer.rb b/lib/unleash/backup_file_writer.rb new file mode 100644 index 0000000..459e901 --- /dev/null +++ b/lib/unleash/backup_file_writer.rb @@ -0,0 +1,21 @@ +require 'unleash/configuration' + +module Unleash + class BackupFileWriter + def self.save!(toggle_data) + Unleash.logger.debug "Will save toggles to disk now" + + backup_file = Unleash.configuration.backup_file + backup_file_tmp = "#{backup_file}.tmp" + + File.open(backup_file_tmp, "w") do |file| + file.write(toggle_data) + end + File.rename(backup_file_tmp, backup_file) + rescue StandardError => e + # This is not really the end of the world. Swallowing the exception. + Unleash.logger.error "Unable to save backup file. Exception thrown #{e.class}:'#{e}'" + Unleash.logger.error "stacktrace: #{e.backtrace}" + end + end +end diff --git a/lib/unleash/streaming_event_processor.rb b/lib/unleash/streaming_event_processor.rb index e07c2c1..3825cc4 100644 --- a/lib/unleash/streaming_event_processor.rb +++ b/lib/unleash/streaming_event_processor.rb @@ -1,4 +1,5 @@ require 'json' +require 'unleash/backup_file_writer' module Unleash class StreamingEventProcessor @@ -41,7 +42,8 @@ def handle_connected_event(event) def handle_updated_event(event) handle_delta_event(event.data) - # TODO: update backup file + full_state = @toggle_engine.get_state + Unleash::BackupFileWriter.save!(full_state) rescue JSON::ParserError => e Unleash.logger.error "Unable to parse JSON from streaming event data. Exception thrown #{e.class}: '#{e}'" Unleash.logger.debug "stacktrace: #{e.backtrace}" diff --git a/lib/unleash/toggle_fetcher.rb b/lib/unleash/toggle_fetcher.rb index 1c881df..ebcb3be 100755 --- a/lib/unleash/toggle_fetcher.rb +++ b/lib/unleash/toggle_fetcher.rb @@ -1,5 +1,6 @@ require 'unleash/configuration' require 'unleash/bootstrap/handler' +require 'unleash/backup_file_writer' require 'net/http' require 'json' require 'yggdrasil_engine' @@ -54,25 +55,7 @@ def fetch # always synchronize with the local cache when fetching: update_engine_state!(response.body) - save! response.body - end - - def save!(toggle_data) - Unleash.logger.debug "Will save toggles to disk now" - - backup_file = Unleash.configuration.backup_file - backup_file_tmp = "#{backup_file}.tmp" - - self.toggle_lock.synchronize do - File.open(backup_file_tmp, "w") do |file| - file.write(toggle_data) - end - File.rename(backup_file_tmp, backup_file) - end - rescue StandardError => e - # This is not really the end of the world. Swallowing the exception. - Unleash.logger.error "Unable to save backup file. Exception thrown #{e.class}:'#{e}'" - Unleash.logger.error "stacktrace: #{e.backtrace}" + Unleash::BackupFileWriter.save! response.body end private diff --git a/spec/unleash/streaming_event_processor_spec.rb b/spec/unleash/streaming_event_processor_spec.rb new file mode 100644 index 0000000..7c406be --- /dev/null +++ b/spec/unleash/streaming_event_processor_spec.rb @@ -0,0 +1,81 @@ +RSpec.describe Unleash::StreamingEventProcessor do + let(:engine) { YggdrasilEngine.new } + let(:processor) { Unleash::StreamingEventProcessor.new(engine) } + let(:backup_file) { Unleash.configuration.backup_file } + + before do + Unleash.configure do |config| + config.url = 'http://test-url/' + config.app_name = 'test-app' + end + Unleash.logger = Unleash.configuration.logger + end + + after do + File.delete(backup_file) if File.exist?(backup_file) + end + + class TestEvent + attr_reader :type, :data + + def initialize(type, data) + @type = type + @data = data + end + end + + def feature_event(name, enabled = true) + { + "events": [{ + "type": "feature-updated", + "eventId": 1, + "feature": { + "name": name, + "enabled": enabled, + "strategies": [{ "name": "default" }] + } + }] + }.to_json + end + + def backup_contains_feature?(name) + return false unless File.exist?(backup_file) + + parsed = JSON.parse(File.read(backup_file)) + feature_names = parsed['features'].map { |f| f['name'] } + feature_names.include?(name) + end + + describe '#process_event' do + it 'processes valid events and saves full engine state' do + event = TestEvent.new('unleash-updated', feature_event('test-feature')) + processor.process_event(event) + + expect(engine.enabled?('test-feature', {})).to eq(true) + expect(backup_contains_feature?('test-feature')).to eq(true) + end + + it 'ignores unknown event types' do + event = TestEvent.new('unknown-event', feature_event('test-feature')) + processor.process_event(event) + + expect(File.exist?(backup_file)).to eq(false) + expect(engine.enabled?('test-feature', {})).to be_falsy + end + + it 'saves full engine state, not partial event data' do + processor.process_event(TestEvent.new('unleash-updated', feature_event('first-feature', true))) + processor.process_event(TestEvent.new('unleash-updated', feature_event('second-feature', false))) + + expect(backup_contains_feature?('first-feature')).to eq(true) + expect(backup_contains_feature?('second-feature')).to eq(true) + end + + it 'handles invalid JSON gracefully without creating backup' do + event = TestEvent.new('unleash-updated', 'invalid json') + + expect { processor.process_event(event) }.not_to raise_error + expect(File.exist?(backup_file)).to eq(false) + end + end +end