diff --git a/lib/splitclient-rb.rb b/lib/splitclient-rb.rb index 67688324..5a50b64f 100644 --- a/lib/splitclient-rb.rb +++ b/lib/splitclient-rb.rb @@ -67,7 +67,9 @@ require 'splitclient-rb/engine/common/impressions_manager' require 'splitclient-rb/engine/common/noop_impressions_counter' require 'splitclient-rb/engine/events/events_manager_config.rb' +require 'splitclient-rb/engine/events/events_manager.rb' require 'splitclient-rb/engine/events/events_task.rb' +require 'splitclient-rb/engine/events/events_delivery.rb' require 'splitclient-rb/engine/parser/condition' require 'splitclient-rb/engine/parser/partition' require 'splitclient-rb/engine/parser/evaluator' @@ -119,6 +121,8 @@ require 'splitclient-rb/engine/models/sdk_event.rb' require 'splitclient-rb/engine/models/sdk_internal_event.rb' require 'splitclient-rb/engine/models/sdk_internal_event_notification.rb' +require 'splitclient-rb/engine/models/valid_sdk_event.rb' +require 'splitclient-rb/engine/models/event_active_subscriptions.rb' require 'splitclient-rb/engine/auth_api_client' require 'splitclient-rb/engine/back_off' require 'splitclient-rb/engine/fallback_treatment_calculator.rb' diff --git a/lib/splitclient-rb/engine/events/events_delivery.rb b/lib/splitclient-rb/engine/events/events_delivery.rb new file mode 100644 index 00000000..dd2a093b --- /dev/null +++ b/lib/splitclient-rb/engine/events/events_delivery.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module SplitIoClient + module Engine + module Events + class EventsDelivery + def initialize(config) + @config = config + end + + def deliver(sdk_event, event_metadata, event_handler) + event_handler.call(event_metadata) + rescue StandardError => e + @config.logger.error("Exception when calling handler for Sdk Event #{sdk_event}") + @config.log_found_exception(__method__.to_s, e) + end + end + end + end +end diff --git a/lib/splitclient-rb/engine/events/events_manager.rb b/lib/splitclient-rb/engine/events/events_manager.rb new file mode 100644 index 00000000..ae4e9abc --- /dev/null +++ b/lib/splitclient-rb/engine/events/events_manager.rb @@ -0,0 +1,187 @@ +# frozen_string_literal: true + +module SplitIoClient + module Engine + module Events + class EventsManager + def initialize(events_manager_config, events_delivery, config) + @manager_config = events_manager_config + @events_delivery = events_delivery + @active_subscriptions = {} + @internal_events_status = {} + @mutex = Mutex.new + @config = config + end + + def register(sdk_event, event_handler) + return unless !@active_subscriptions.key?(sdk_event) || get_event_handler(sdk_event).nil? + + @mutex.synchronize do + # SDK ready already fired + if sdk_event == SplitIoClient::Engine::Models::SdkEvent::SDK_READY && event_already_triggered(sdk_event) + @active_subscriptions[sdk_event] = SplitIoClient::Engine::Models::EventActiveSubscriptions.new(true, event_handler) + @config.logger.debug('EventsManager: Firing SDK_READY event for new subscription') if @config.debug_enabled + fire_sdk_event(sdk_event, nil) + return + end + + @active_subscriptions[sdk_event] = SplitIoClient::Engine::Models::EventActiveSubscriptions.new(false, event_handler) + end + end + + def unregister(sdk_event) + return unless @active_subscriptions.key?(sdk_event) + + @mutex.synchronize do + @active_subscriptions.delete(sdk_event) + end + end + + def notify_internal_event(sdk_internal_event, event_metadata) + @mutex.synchronize do + update_internal_event_status(sdk_internal_event, true) + @manager_config.evaluation_order.each do |sorted_event| + if get_sdk_event_if_applicable(sdk_internal_event).include?(sorted_event) && + !get_event_handler(sorted_event).nil? + fire_sdk_event(sorted_event, event_metadata) + end + + # if client is not subscribed to SDK_READY + if sorted_event == SplitIoClient::Engine::Models::SdkEvent::SDK_READY && get_event_handler(sorted_event).nil? + @config.logger.debug('EventsManager: Registering SDK_READY event as fired') if @config.debug_enabled + @active_subscriptions[Engine::Models::SdkEvent::SDK_READY] = Engine::Models::EventActiveSubscriptions.new(true, nil) + end + end + end + end + + def destroy + @mutex.synchronize do + @active_subscriptions = {} + @internal_events_status = {} + end + end + + private + + def fire_sdk_event(sdk_event, event_metadata) + @config.logger.debug("EventsManager: Firing Sdk event: #{sdk_event}") if @config.debug_enabled + @config.threads[:sdk_event_notify] = Thread.new do + @events_delivery.deliver(sdk_event, event_metadata, get_event_handler(sdk_event)) + end + sdk_event_triggered(sdk_event) + end + + def event_already_triggered(sdk_event) + return @active_subscriptions[sdk_event].triggered if @active_subscriptions.key?(sdk_event) + + false + end + + def get_internal_event_status(sdk_internal_event) + return @internal_events_status[sdk_internal_event] if @internal_events_status.key?(sdk_internal_event) + + false + end + + def update_internal_event_status(sdk_internal_event, status) + @internal_events_status[sdk_internal_event] = status + end + + def sdk_event_triggered(sdk_event) + return unless @active_subscriptions.key?(sdk_event) + + return if @active_subscriptions[sdk_event].triggered + + @active_subscriptions[sdk_event].triggered = true + end + + def get_event_handler(sdk_event) + return nil unless @active_subscriptions.key?(sdk_event) + + @active_subscriptions[sdk_event].handler + end + + def get_sdk_event_if_applicable(sdk_internal_event) + final_sdk_event = SplitIoClient::Engine::Models::ValidSdkEvent.new(nil, false) + + events_to_fire = [] + require_any_sdk_event = check_require_any(sdk_internal_event) + if require_any_sdk_event.valid + if (!event_already_triggered(require_any_sdk_event.sdk_event) && + execution_limit(require_any_sdk_event.sdk_event) == 1) || + execution_limit(require_any_sdk_event.sdk_event) == -1 + final_sdk_event = SplitIoClient::Engine::Models::ValidSdkEvent.new( + require_any_sdk_event.sdk_event, + check_prerequisites(require_any_sdk_event.sdk_event) && + check_suppressed_by(require_any_sdk_event.sdk_event) + ) + end + events_to_fire.push(final_sdk_event.sdk_event) if final_sdk_event.valid + end + check_require_all.each { |sdk_event| events_to_fire.push(sdk_event) } + + events_to_fire + end + + def check_require_all + events = [] + @manager_config.require_all.each do |require_name, require_value| + final_status = true + require_value.each { |val| final_status &= get_internal_event_status(val) } + events.push(require_name) if check_event_eligible_conditions(final_status, require_name, require_value) + end + + events + end + + def check_event_eligible_conditions(final_status, require_name, require_value) + final_status && + check_prerequisites(require_name) && + ((!event_already_triggered(require_name) && + execution_limit(require_name) == 1) || + execution_limit(require_name) == -1) && + require_value.length.positive? + end + + def check_prerequisites(sdk_event) + @manager_config.prerequisites.each do |name, value| + value.each do |val| + return false if name == sdk_event && !event_already_triggered(val) + end + end + + true + end + + def check_suppressed_by(sdk_event) + @manager_config.suppressed_by.each do |name, value| + value.each do |val| + return false if name == sdk_event && event_already_triggered(val) + end + end + + true + end + + def execution_limit(sdk_event) + return -1 unless @manager_config.execution_limits.key?(sdk_event) + + @manager_config.execution_limits[sdk_event] + end + + def check_require_any(sdk_internal_event) + valid_sdk_event = SplitIoClient::Engine::Models::ValidSdkEvent.new(nil, false) + @manager_config.require_any.each do |name, val| + if val.include?(sdk_internal_event) + valid_sdk_event = SplitIoClient::Engine::Models::ValidSdkEvent.new(name, true) + return valid_sdk_event + end + end + + valid_sdk_event + end + end + end + end +end diff --git a/lib/splitclient-rb/engine/events/events_task.rb b/lib/splitclient-rb/engine/events/events_task.rb index 42bc736b..f1cdb245 100644 --- a/lib/splitclient-rb/engine/events/events_task.rb +++ b/lib/splitclient-rb/engine/events/events_task.rb @@ -16,7 +16,7 @@ def initialize(notify_internal_events, internal_events_queue, config) def start return if @running - @config.logger.info('Starting Internal Events Task.') if @config.debug_enabled + @config.logger.info('Starting Internal Events Task.') @running = true @config.threads[:internal_events_task] = Thread.new do worker_thread @@ -26,7 +26,7 @@ def start def stop return unless @running - @config.logger.info('Stopping Internal Events Task.') if @config.debug_enabled + @config.logger.info('Stopping Internal Events Task.') @running = false end @@ -36,7 +36,7 @@ def worker_thread while (event = @internal_events_queue.pop) break unless @running - @config.logger.info("Processing sdk internal event: #{event.internal_event}") if @config.debug_enabled + @config.logger.debug("Processing sdk internal event: #{event.internal_event}") if @config.debug_enabled begin @notify_internal_events.call(event.internal_event, event.metadata) rescue StandardError => e diff --git a/lib/splitclient-rb/engine/models/event_active_subscriptions.rb b/lib/splitclient-rb/engine/models/event_active_subscriptions.rb new file mode 100644 index 00000000..3a0d4909 --- /dev/null +++ b/lib/splitclient-rb/engine/models/event_active_subscriptions.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: false + +module SplitIoClient + module Engine::Models + class EventActiveSubscriptions + attr_accessor :triggered, :handler + + def initialize(triggered, handler) + @triggered = triggered + @handler = handler + end + end + end +end diff --git a/lib/splitclient-rb/engine/models/valid_sdk_event.rb b/lib/splitclient-rb/engine/models/valid_sdk_event.rb new file mode 100644 index 00000000..0a0cbcd0 --- /dev/null +++ b/lib/splitclient-rb/engine/models/valid_sdk_event.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: false + +module SplitIoClient + module Engine::Models + class ValidSdkEvent + attr_reader :sdk_event, :valid + + def initialize(sdk_event, valid) + @sdk_event = sdk_event + @valid = valid + end + end + end +end diff --git a/lib/splitclient-rb/engine/status_manager.rb b/lib/splitclient-rb/engine/status_manager.rb index bfe35f9b..e5ed19d1 100644 --- a/lib/splitclient-rb/engine/status_manager.rb +++ b/lib/splitclient-rb/engine/status_manager.rb @@ -22,7 +22,8 @@ def ready! @config.logger.info('SplitIO SDK is ready') @internal_events_queue.push( SplitIoClient::Engine::Models::SdkInternalEventNotification.new( - SplitIoClient::Engine::Models::SdkInternalEvent::SDK_READY, nil) + SplitIoClient::Engine::Models::SdkInternalEvent::SDK_READY, nil + ) ) end diff --git a/spec/engine/events/events_delivery_spec.rb b/spec/engine/events/events_delivery_spec.rb new file mode 100644 index 00000000..e851d20d --- /dev/null +++ b/spec/engine/events/events_delivery_spec.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe SplitIoClient::Engine::Events::EventsDelivery do + subject { SplitIoClient::Engine::Events::EventsDelivery } + + it 'calls handler successfully' do + config = SplitIoClient::SplitConfig.new(logger: Logger.new(StringIO.new)) + delivery = subject.new(config) + + delivery.deliver( + SplitIoClient::Engine::Models::SdkInternalEvent::FLAGS_UPDATED, + SplitIoClient::Engine::Models::EventsMetadata.new(SplitIoClient::Engine::Models::SdkEventType::FLAG_UPDATE), + method(:call_back) + ) + sleep 0.5 + expect(@metadata.type).to be(SplitIoClient::Engine::Models::SdkEventType::FLAG_UPDATE) + end + + it 'handles exception when calling handler' do + log = StringIO.new + config = SplitIoClient::SplitConfig.new(logger: Logger.new(log)) + delivery = subject.new(config) + + delivery.deliver( + SplitIoClient::Engine::Models::SdkInternalEvent::FLAGS_UPDATED, + SplitIoClient::Engine::Models::EventsMetadata.new(SplitIoClient::Engine::Models::SdkEventType::FLAG_UPDATE), + method(:call_with_exception) + ) + sleep 0.5 + expect(log.string).to include('Exception when calling handler for Sdk Event') + end + + it 'logs the sdk event name when handler raises exception' do + log = StringIO.new + config = SplitIoClient::SplitConfig.new(logger: Logger.new(log)) + delivery = subject.new(config) + + delivery.deliver( + SplitIoClient::Engine::Models::SdkInternalEvent::SDK_READY, + nil, + method(:call_with_exception) + ) + sleep 0.5 + expect(log.string).to include('Exception when calling handler for Sdk Event') + expect(log.string).to include(SplitIoClient::Engine::Models::SdkInternalEvent::SDK_READY.to_s) + end + + it 'calls handler with correct metadata' do + config = SplitIoClient::SplitConfig.new(logger: Logger.new(StringIO.new)) + delivery = subject.new(config) + metadata = SplitIoClient::Engine::Models::EventsMetadata.new( + SplitIoClient::Engine::Models::SdkEventType::FLAG_UPDATE, + ['feature1', 'feature2'] + ) + + delivery.deliver( + SplitIoClient::Engine::Models::SdkInternalEvent::FLAGS_UPDATED, + metadata, + method(:call_back) + ) + sleep 0.5 + expect(@metadata).to eq(metadata) + end + + def call_back(metadata) + @metadata = metadata + end + + def call_with_exception(_metadata) + raise StandardError, 'call exception' + end +end diff --git a/spec/engine/events/events_manager_spec.rb b/spec/engine/events/events_manager_spec.rb new file mode 100644 index 00000000..0ba9c43d --- /dev/null +++ b/spec/engine/events/events_manager_spec.rb @@ -0,0 +1,159 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe SplitIoClient::Engine::Events::EventsManager do + subject { SplitIoClient::Engine::Events::EventsManager } + let(:metadata) { nil } + let(:sdk_ready) { false } + let(:sdk_update) { false } + let(:first_event) { nil } + + + it 'test_firing_events' do + config = SplitIoClient::SplitConfig.new(logger: Logger.new(StringIO.new)) + manager = subject.new(SplitIoClient::Engine::Events::EventsManagerConfig.new, + SplitIoClient::Engine::Events::EventsDelivery.new(config), + config) + manager.register(SplitIoClient::Engine::Models::SdkEvent::SDK_READY, method(:ready_call_back)) + manager.register(SplitIoClient::Engine::Models::SdkEvent::SDK_UPDATE, method(:update_call_back)) + meta = SplitIoClient::Engine::Models::EventsMetadata.new(SplitIoClient::Engine::Models::SdkEventType::FLAG_UPDATE, ["feature1"]) + + reset_flags + manager.notify_internal_event(SplitIoClient::Engine::Models::SdkInternalEvent::SDK_READY, nil) + sleep 0.5 + expect(@metadata).to be(nil) + expect(@sdk_ready).to be(true) + expect(@sdk_update).to be(false) + + reset_flags + manager.notify_internal_event(SplitIoClient::Engine::Models::SdkInternalEvent::FLAGS_UPDATED, meta) + sleep 0.5 + expect(@metadata).to eq(meta) + expect(@sdk_update).to be(true) + expect(@sdk_ready).to be(false) + + reset_flags + manager.notify_internal_event(SplitIoClient::Engine::Models::SdkInternalEvent::FLAG_KILLED_NOTIFICATION, meta) + sleep 0.5 + expect(@metadata).to eq(meta) + expect(@sdk_update).to be(true) + expect(@sdk_ready).to be(false) + + reset_flags + manager.notify_internal_event(SplitIoClient::Engine::Models::SdkInternalEvent::SEGMENTS_UPDATED, meta) + sleep 0.5 + expect(@metadata).to eq(meta) + expect(@sdk_update).to be(true) + expect(@sdk_ready).to be(false) + + reset_flags + manager.notify_internal_event(SplitIoClient::Engine::Models::SdkInternalEvent::RB_SEGMENTS_UPDATED, meta) + sleep 0.5 + expect(@metadata).to eq(meta) + expect(@sdk_update).to be(true) + expect(@sdk_ready).to be(false) + end + + it 'events fire only after register' do + config = SplitIoClient::SplitConfig.new(logger: Logger.new(StringIO.new)) + manager = subject.new(SplitIoClient::Engine::Events::EventsManagerConfig.new, + SplitIoClient::Engine::Events::EventsDelivery.new(config), + config) + meta = SplitIoClient::Engine::Models::EventsMetadata.new(SplitIoClient::Engine::Models::SdkEventType::FLAG_UPDATE, ["feature1"]) + + reset_flags + manager.notify_internal_event(SplitIoClient::Engine::Models::SdkInternalEvent::SDK_READY, nil) + sleep 0.5 + expect(@metadata).to be(nil) + expect(@sdk_ready).to be(false) + expect(@sdk_update).to be(false) + + manager.register(SplitIoClient::Engine::Models::SdkEvent::SDK_READY, method(:ready_call_back)) + manager.notify_internal_event(SplitIoClient::Engine::Models::SdkInternalEvent::SDK_READY, nil) + sleep 0.5 + expect(@metadata).to be(nil) + expect(@sdk_ready).to be(true) + expect(@sdk_update).to be(false) + + reset_flags + manager.notify_internal_event(SplitIoClient::Engine::Models::SdkInternalEvent::FLAGS_UPDATED, meta) + sleep 0.5 + expect(@metadata).to eq(nil) + expect(@sdk_update).to be(false) + expect(@sdk_ready).to be(false) + + manager.register(SplitIoClient::Engine::Models::SdkEvent::SDK_UPDATE, method(:update_call_back)) + manager.notify_internal_event(SplitIoClient::Engine::Models::SdkInternalEvent::FLAGS_UPDATED, meta) + sleep 0.5 + expect(@metadata).to eq(meta) + expect(@sdk_update).to be(true) + expect(@sdk_ready).to be(false) + end + + it 'update fires only after ready events' do + config = SplitIoClient::SplitConfig.new(logger: Logger.new(StringIO.new)) + manager = subject.new(SplitIoClient::Engine::Events::EventsManagerConfig.new, + SplitIoClient::Engine::Events::EventsDelivery.new(config), + config) + manager.register(SplitIoClient::Engine::Models::SdkEvent::SDK_READY, method(:ready_call_back)) + manager.register(SplitIoClient::Engine::Models::SdkEvent::SDK_UPDATE, method(:update_call_back)) + meta = SplitIoClient::Engine::Models::EventsMetadata.new(SplitIoClient::Engine::Models::SdkEventType::FLAG_UPDATE, ["feature1"]) + + reset_flags + manager.notify_internal_event(SplitIoClient::Engine::Models::SdkInternalEvent::FLAGS_UPDATED, meta) + sleep 0.5 + expect(@metadata).to eq(nil) + expect(@sdk_update).to be(false) + expect(@sdk_ready).to be(false) + + reset_flags + manager.notify_internal_event(SplitIoClient::Engine::Models::SdkInternalEvent::SDK_READY, nil) + sleep 0.5 + expect(@metadata).to be(nil) + expect(@sdk_ready).to be(true) + expect(@sdk_update).to be(false) + + reset_flags + manager.notify_internal_event(SplitIoClient::Engine::Models::SdkInternalEvent::FLAGS_UPDATED, meta) + sleep 0.5 + expect(@metadata).to eq(meta) + expect(@sdk_update).to be(true) + expect(@sdk_ready).to be(false) + end + + it 'event ordered correctly' do + config = SplitIoClient::SplitConfig.new(logger: Logger.new(StringIO.new)) + manager = subject.new(SplitIoClient::Engine::Events::EventsManagerConfig.new, + SplitIoClient::Engine::Events::EventsDelivery.new(config), + config) + manager.register(SplitIoClient::Engine::Models::SdkEvent::SDK_READY, method(:ready_call_back)) + manager.register(SplitIoClient::Engine::Models::SdkEvent::SDK_UPDATE, method(:update_call_back)) + meta = SplitIoClient::Engine::Models::EventsMetadata.new(SplitIoClient::Engine::Models::SdkEventType::FLAG_UPDATE, ["feature1"]) + + reset_flags + @first_event = nil + manager.notify_internal_event(SplitIoClient::Engine::Models::SdkInternalEvent::SDK_READY, nil) + manager.notify_internal_event(SplitIoClient::Engine::Models::SdkInternalEvent::FLAGS_UPDATED, meta) + sleep 0.5 + expect(@first_event).to be("ready") + end + + def ready_call_back(metadata) + @sdk_ready = true + @metadata = metadata + @first_event = "ready" if @first_event.nil? + end + + def update_call_back(metadata) + @sdk_update = true + @metadata = metadata + @first_event = "update" if @first_event.nil? + end + + def reset_flags + @sdk_ready = false + @sdk_update = false + @metadata = nil + end +end