kameleoon-client-ruby 3.21.0 → 3.22.1
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/kameleoon/client_readiness.rb +119 -31
- data/lib/kameleoon/configuration/custom_data_info.rb +1 -0
- data/lib/kameleoon/events/data_file_update_event.rb +37 -0
- data/lib/kameleoon/events/data_file_update_handler.rb +22 -0
- data/lib/kameleoon/events/event_handler.rb +14 -0
- data/lib/kameleoon/events/event_manager.rb +79 -0
- data/lib/kameleoon/events/event_type.rb +17 -0
- data/lib/kameleoon/events/http_request_failure.rb +63 -0
- data/lib/kameleoon/events/http_request_handler.rb +32 -0
- data/lib/kameleoon/events/request_type.rb +29 -0
- data/lib/kameleoon/exceptions.rb +16 -0
- data/lib/kameleoon/hybrid/manager.rb +1 -1
- data/lib/kameleoon/kameleoon_client.rb +140 -65
- data/lib/kameleoon/kameleoon_client_factory.rb +1 -1
- data/lib/kameleoon/managers/tracking/tracking_manager.rb +41 -3
- data/lib/kameleoon/managers/tracking/visitor_tracking_registry.rb +57 -57
- data/lib/kameleoon/network/access_token_source.rb +126 -52
- data/lib/kameleoon/network/network_manager.rb +33 -10
- data/lib/kameleoon/real_time/{real_time_configuration_service.rb → real_time_event_service.rb} +16 -7
- data/lib/kameleoon/targeting/conditions/unknown_condition.rb +3 -3
- data/lib/kameleoon/version.rb +1 -1
- metadata +11 -5
- data/lib/kameleoon/storage/cache.rb +0 -84
- data/lib/kameleoon/storage/cache_factory.rb +0 -23
|
@@ -10,6 +10,13 @@ require 'kameleoon/data/manager/assigned_variation'
|
|
|
10
10
|
require 'kameleoon/data/manager/forced_experiment_variation'
|
|
11
11
|
require 'kameleoon/data/manager/legal_consent'
|
|
12
12
|
require 'kameleoon/data/manager/visitor_manager'
|
|
13
|
+
require 'kameleoon/events/data_file_update_event'
|
|
14
|
+
require 'kameleoon/events/data_file_update_handler'
|
|
15
|
+
require 'kameleoon/events/event_manager'
|
|
16
|
+
require 'kameleoon/events/event_type'
|
|
17
|
+
require 'kameleoon/events/http_request_failure'
|
|
18
|
+
require 'kameleoon/events/http_request_handler'
|
|
19
|
+
require 'kameleoon/events/request_type'
|
|
13
20
|
require 'kameleoon/exceptions'
|
|
14
21
|
require 'kameleoon/hybrid/manager'
|
|
15
22
|
require 'kameleoon/managers/data/data_manager'
|
|
@@ -22,8 +29,7 @@ require 'kameleoon/network/activity_event'
|
|
|
22
29
|
require 'kameleoon/network/network_manager'
|
|
23
30
|
require 'kameleoon/network/url_provider'
|
|
24
31
|
require 'kameleoon/network/cookie/cookie_manager'
|
|
25
|
-
require 'kameleoon/real_time/
|
|
26
|
-
require 'kameleoon/storage/cache_factory'
|
|
32
|
+
require 'kameleoon/real_time/real_time_event_service'
|
|
27
33
|
require 'kameleoon/targeting/models'
|
|
28
34
|
require 'kameleoon/targeting/targeting_manager'
|
|
29
35
|
require 'kameleoon/types/variable'
|
|
@@ -58,7 +64,8 @@ module Kameleoon
|
|
|
58
64
|
@scheduler = Rufus::Scheduler.new
|
|
59
65
|
@site_code = site_code
|
|
60
66
|
@config = config
|
|
61
|
-
@
|
|
67
|
+
@disposed = false
|
|
68
|
+
@real_time_event_service = nil
|
|
62
69
|
@update_configuration_handler = nil
|
|
63
70
|
@fetch_configuration_update_job = nil
|
|
64
71
|
data_file = Configuration::DataFile.new(config.environment, nil)
|
|
@@ -67,11 +74,13 @@ module Kameleoon
|
|
|
67
74
|
@data_manager, config.session_duration_second, @scheduler
|
|
68
75
|
)
|
|
69
76
|
@hybrid_manager = Hybrid::ManagerImpl.new(HYBRID_EXPIRATION_TIME, @data_manager)
|
|
77
|
+
@event_manager = Events::EventManager.new
|
|
70
78
|
@network_manager = Network::NetworkManager.new(
|
|
71
79
|
config.environment,
|
|
72
80
|
config.default_timeout_millisecond,
|
|
73
81
|
Network::AccessTokenSourceFactory.new(config.client_id, config.client_secret),
|
|
74
|
-
Network::UrlProvider.new(site_code, config.network_domain)
|
|
82
|
+
Network::UrlProvider.new(site_code, config.network_domain),
|
|
83
|
+
@event_manager
|
|
75
84
|
)
|
|
76
85
|
@tracking_manager = Managers::Tracking::TrackingManager.new(
|
|
77
86
|
@data_manager, @network_manager, @visitor_manager, config.tracking_interval_second, @scheduler
|
|
@@ -81,7 +90,7 @@ module Kameleoon
|
|
|
81
90
|
@data_manager, @network_manager, @visitor_manager
|
|
82
91
|
)
|
|
83
92
|
@cookie_manager = Network::Cookie::CookieManager.new(@data_manager, @visitor_manager, config.top_level_domain)
|
|
84
|
-
@readiness = ClientReadiness.new
|
|
93
|
+
@readiness = ClientReadiness.new(site_code, config.environment)
|
|
85
94
|
@targeting_manager = Targeting::TargetingManager.new(@data_manager, @visitor_manager)
|
|
86
95
|
|
|
87
96
|
if @config.verbose_mode == true && Logging::KameleoonLogger.log_level == Logging::LogLevel::WARNING
|
|
@@ -95,25 +104,53 @@ module Kameleoon
|
|
|
95
104
|
##
|
|
96
105
|
# Block until the SDK has loaded its configuration and is ready to use.
|
|
97
106
|
#
|
|
98
|
-
#
|
|
99
|
-
#
|
|
100
|
-
#
|
|
101
|
-
#
|
|
107
|
+
# The method returns the result of the configuration fetch: `true` once the
|
|
108
|
+
# SDK has been initialized, or `false` as soon as the fetch has failed
|
|
109
|
+
# (without waiting for background retries) or when no fetch result is
|
|
110
|
+
# available within the timeout. The failure which prevented the SDK from
|
|
111
|
+
# initializing is reported to the log.
|
|
112
|
+
#
|
|
113
|
+
# An expired timeout or a failed fetch does not affect the SDK state: the
|
|
114
|
+
# client keeps retrying in the background, and once the SDK becomes ready,
|
|
115
|
+
# a subsequent call returns true.
|
|
102
116
|
#
|
|
103
117
|
# @param [Integer, nil] timeout_millisecond Maximum time to wait, in
|
|
104
118
|
# milliseconds. When nil or negative, config.default_timeout_millisecond
|
|
105
119
|
# is applied.
|
|
106
|
-
# @return [Boolean] true if the SDK is ready, false if the
|
|
120
|
+
# @return [Boolean] true if the SDK is ready, false if the fetch failed or
|
|
121
|
+
# the timeout elapsed first.
|
|
107
122
|
def wait_init(timeout_millisecond = nil)
|
|
108
123
|
Logging::KameleoonLogger.info('CALL: KameleoonClient.wait_init(timeout_millisecond: %s)', timeout_millisecond)
|
|
109
124
|
if timeout_millisecond.nil? || timeout_millisecond.negative?
|
|
110
125
|
timeout_millisecond = @config.default_timeout_millisecond
|
|
111
126
|
end
|
|
112
|
-
|
|
127
|
+
error = @readiness.wait_with_timeout(timeout_millisecond / 1000.0)
|
|
128
|
+
if error.nil?
|
|
129
|
+
Logging::KameleoonLogger.info('Kameleoon is initialized')
|
|
130
|
+
else
|
|
131
|
+
Logging::KameleoonLogger.error('Kameleoon failed to initialize due to error: %s', error.message)
|
|
132
|
+
end
|
|
133
|
+
result = error.nil?
|
|
113
134
|
Logging::KameleoonLogger.info('RETURN: KameleoonClient.wait_init -> (result: %s)', result)
|
|
114
135
|
result
|
|
115
136
|
end
|
|
116
137
|
|
|
138
|
+
##
|
|
139
|
+
# Indicates whether the SDK is ready for use, i.e. its configuration has been
|
|
140
|
+
# successfully loaded. Unlike `wait_init`, it returns immediately without blocking.
|
|
141
|
+
#
|
|
142
|
+
# It returns `true` if the SDK has been successfully initialized, `false` otherwise
|
|
143
|
+
# (including while the initialization is still pending or has failed).
|
|
144
|
+
#
|
|
145
|
+
# Readiness is monotonic: once the SDK becomes ready it stays ready.
|
|
146
|
+
#
|
|
147
|
+
# @return [Boolean] whether the SDK is ready for use.
|
|
148
|
+
def is_ready? # rubocop:disable Naming/PredicateName
|
|
149
|
+
ready = @readiness.ready?
|
|
150
|
+
Logging::KameleoonLogger.info('CALL/RETURN: KameleoonClient.is_ready? -> (ready: %s)', ready)
|
|
151
|
+
ready
|
|
152
|
+
end
|
|
153
|
+
|
|
117
154
|
##
|
|
118
155
|
# Obtain a visitor code.
|
|
119
156
|
#
|
|
@@ -712,6 +749,24 @@ module Kameleoon
|
|
|
712
749
|
map_active_features
|
|
713
750
|
end
|
|
714
751
|
|
|
752
|
+
##
|
|
753
|
+
# Sets the SDK event handler for the specified event type.
|
|
754
|
+
#
|
|
755
|
+
# The handler is called when the corresponding SDK event occurs. Supported event types are
|
|
756
|
+
# defined in `Kameleoon::Events::EventType`. Passing `nil` clears the handler for the
|
|
757
|
+
# specified event type.
|
|
758
|
+
#
|
|
759
|
+
# @param event_type [Symbol] The SDK event type to handle, one of the
|
|
760
|
+
# `Kameleoon::Events::EventType` constants.
|
|
761
|
+
# @param handler [Object, nil] The handler to register, or `nil` to remove the current handler.
|
|
762
|
+
# The handler must respond to the methods required by the selected event type
|
|
763
|
+
# (see `Kameleoon::Events::HttpRequestHandler` and `Kameleoon::Events::DataFileUpdateHandler`).
|
|
764
|
+
def set_event_handler(event_type, handler)
|
|
765
|
+
Logging::KameleoonLogger.info("CALL: KameleoonClient.set_event_handler(event_type: '%s', handler)", event_type)
|
|
766
|
+
@event_manager.set_event_handler(event_type, handler)
|
|
767
|
+
Logging::KameleoonLogger.info("RETURN: KameleoonClient.set_event_handler(event_type: '%s', handler)", event_type)
|
|
768
|
+
end
|
|
769
|
+
|
|
715
770
|
##
|
|
716
771
|
# The `on_update_configuration()` method allows you to handle the event when configuration
|
|
717
772
|
# has updated data. It takes one input parameter: callable **handler**. The handler
|
|
@@ -719,7 +774,13 @@ module Kameleoon
|
|
|
719
774
|
#
|
|
720
775
|
# @param handler [Callable | NilClass] The handler that will be called when the configuration
|
|
721
776
|
# is updated using a real-time configuration event.
|
|
777
|
+
#
|
|
778
|
+
# DEPRECATED. Please use `set_event_handler(Kameleoon::Events::EventType::DATAFILE_UPDATE, handler)` instead.
|
|
722
779
|
def on_update_configuration(handler)
|
|
780
|
+
Logging::KameleoonLogger.info(
|
|
781
|
+
'[DEPRECATION] `on_update_configuration` is deprecated. ' \
|
|
782
|
+
'Please use `set_event_handler(Kameleoon::Events::EventType::DATAFILE_UPDATE, handler)` instead.'
|
|
783
|
+
)
|
|
723
784
|
Logging::KameleoonLogger.info('CALL/RETURN: KameleoonClient.on_update_configuration(handler)')
|
|
724
785
|
@update_configuration_handler = handler
|
|
725
786
|
end
|
|
@@ -826,44 +887,41 @@ module Kameleoon
|
|
|
826
887
|
|
|
827
888
|
HYBRID_EXPIRATION_TIME = 5
|
|
828
889
|
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
rescue StandardError => e
|
|
837
|
-
Logging::KameleoonLogger.error('Initial configuration fetch failed: %s', e)
|
|
838
|
-
end
|
|
839
|
-
@readiness.set(ok)
|
|
840
|
-
real_time_update = ok && @data_manager.data_file.settings.real_time_update
|
|
841
|
-
manage_configuration_update(real_time_update)
|
|
842
|
-
end
|
|
843
|
-
end
|
|
844
|
-
|
|
890
|
+
# Fetches the configuration in a background thread; used for the initial fetch,
|
|
891
|
+
# the polling schedule, and the streaming events alike. The fetch outcome settles
|
|
892
|
+
# the client readiness, and a failed fetch always falls back to the polling mode:
|
|
893
|
+
# a streaming event means the configuration has changed, so staying in the
|
|
894
|
+
# streaming mode would leave the SDK serving the outdated configuration until
|
|
895
|
+
# the next event arrives. The server-provided settings are not mutated, so a
|
|
896
|
+
# later successful fetch switches back to streaming when requested.
|
|
845
897
|
def fetch_configuration_job(time_stamp = nil)
|
|
846
898
|
Thread.new do
|
|
899
|
+
cause = nil
|
|
847
900
|
ok = false
|
|
848
901
|
begin
|
|
849
902
|
ok = obtain_configuration(time_stamp)
|
|
850
903
|
rescue StandardError => e
|
|
904
|
+
cause = e
|
|
851
905
|
Logging::KameleoonLogger.error('Error occurred during configuration fetching: %s', e)
|
|
852
906
|
end
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
@readiness.set(ok)
|
|
856
|
-
real_time_update = @data_manager.data_file.settings.real_time_update
|
|
857
|
-
if !ok && real_time_update
|
|
858
|
-
@data_manager.data_file.settings.real_time_update = false
|
|
859
|
-
real_time_update = false
|
|
860
|
-
Logging::KameleoonLogger.warning('Switching to polling mode due to failed fetch')
|
|
861
|
-
end
|
|
862
|
-
manage_configuration_update(real_time_update)
|
|
907
|
+
settle_readiness(ok, cause)
|
|
908
|
+
manage_configuration_update(ok && @data_manager.data_file.settings.real_time_update)
|
|
863
909
|
end
|
|
864
910
|
end
|
|
865
911
|
|
|
866
|
-
|
|
912
|
+
# A successful fetch marks the SDK ready, a failed one reports the failure so
|
|
913
|
+
# `wait_init` fails fast instead of sleeping out its timeout. A failure never
|
|
914
|
+
# reverts an already-ready client.
|
|
915
|
+
def settle_readiness(ok, cause)
|
|
916
|
+
if ok
|
|
917
|
+
@readiness.mark_ready
|
|
918
|
+
else
|
|
919
|
+
cause ||= Exception::KameleoonError.new('configuration fetch failed')
|
|
920
|
+
@readiness.mark_not_ready(cause)
|
|
921
|
+
end
|
|
922
|
+
end
|
|
923
|
+
|
|
924
|
+
def start_polling_datafile_update
|
|
867
925
|
return unless @fetch_configuration_update_job.nil?
|
|
868
926
|
|
|
869
927
|
@fetch_configuration_update_job = @scheduler.schedule_every @config.refresh_interval_second do
|
|
@@ -872,7 +930,7 @@ module Kameleoon
|
|
|
872
930
|
end
|
|
873
931
|
end
|
|
874
932
|
|
|
875
|
-
def
|
|
933
|
+
def stop_polling_datafile_update
|
|
876
934
|
return if @fetch_configuration_update_job.nil?
|
|
877
935
|
|
|
878
936
|
@fetch_configuration_update_job&.unschedule
|
|
@@ -880,27 +938,34 @@ module Kameleoon
|
|
|
880
938
|
Logging::KameleoonLogger.info('Scheduled job to fetch configuration is stopped.')
|
|
881
939
|
end
|
|
882
940
|
|
|
883
|
-
def
|
|
884
|
-
return unless @
|
|
941
|
+
def start_real_time_event_service_if_needed
|
|
942
|
+
return unless @real_time_event_service.nil?
|
|
885
943
|
|
|
886
944
|
url = @network_manager.url_provider.make_real_time_url
|
|
887
945
|
fetch_func = proc { |real_time_event| fetch_configuration_job(real_time_event.time_stamp) }
|
|
888
|
-
@
|
|
889
|
-
Kameleoon::RealTime::
|
|
946
|
+
@real_time_event_service =
|
|
947
|
+
Kameleoon::RealTime::RealTimeEventService.new(url, fetch_func)
|
|
890
948
|
end
|
|
891
949
|
|
|
892
|
-
def
|
|
893
|
-
@
|
|
894
|
-
@
|
|
950
|
+
def stop_real_time_event_service_if_needed
|
|
951
|
+
@real_time_event_service&.close
|
|
952
|
+
@real_time_event_service = nil
|
|
895
953
|
end
|
|
896
954
|
|
|
955
|
+
# A disposed client must not (re)start the update services: a fetch which was
|
|
956
|
+
# in flight when `dispose` ran would otherwise resurrect them. `@disposed` is
|
|
957
|
+
# set before `dispose` stops the services, so a fetch completing after that
|
|
958
|
+
# point can only stop things.
|
|
897
959
|
def manage_configuration_update(is_real_time_update)
|
|
898
|
-
if
|
|
899
|
-
|
|
900
|
-
|
|
960
|
+
if @disposed
|
|
961
|
+
stop_polling_datafile_update
|
|
962
|
+
stop_real_time_event_service_if_needed
|
|
963
|
+
elsif is_real_time_update
|
|
964
|
+
stop_polling_datafile_update
|
|
965
|
+
start_real_time_event_service_if_needed
|
|
901
966
|
else
|
|
902
|
-
|
|
903
|
-
|
|
967
|
+
stop_real_time_event_service_if_needed
|
|
968
|
+
start_polling_datafile_update
|
|
904
969
|
end
|
|
905
970
|
end
|
|
906
971
|
|
|
@@ -927,34 +992,44 @@ module Kameleoon
|
|
|
927
992
|
if response.configuration
|
|
928
993
|
configuration = JSON.parse(response.configuration)
|
|
929
994
|
data_file = Configuration::DataFile.new(@config.environment, configuration, response.last_modified)
|
|
930
|
-
|
|
931
|
-
|
|
995
|
+
source = time_stamp.nil? ? Events::DataFileUpdateEvent::Source::POLLING
|
|
996
|
+
: Events::DataFileUpdateEvent::Source::STREAMING
|
|
997
|
+
apply_new_configuration(data_file, source)
|
|
932
998
|
Logging::KameleoonLogger.info('Feature flags are fetched: %s', response.inspect)
|
|
933
999
|
end
|
|
934
1000
|
true
|
|
935
1001
|
end
|
|
936
1002
|
|
|
937
|
-
def apply_new_configuration(data_file)
|
|
938
|
-
Logging::KameleoonLogger.debug('CALL: KameleoonClient.apply_new_configuration(data_file: %s)',
|
|
1003
|
+
def apply_new_configuration(data_file, source)
|
|
1004
|
+
Logging::KameleoonLogger.debug('CALL: KameleoonClient.apply_new_configuration(data_file: %s, source: %s)',
|
|
1005
|
+
data_file, source)
|
|
939
1006
|
@data_manager.data_file = data_file
|
|
940
1007
|
@network_manager.url_provider.apply_data_api_domain(data_file.settings.data_api_domain)
|
|
941
|
-
|
|
1008
|
+
@event_manager.fire_data_file_update(Events::DataFileUpdateEvent.new(source, data_file.date_modified))
|
|
1009
|
+
call_deprecated_update_handler if source == Events::DataFileUpdateEvent::Source::STREAMING
|
|
1010
|
+
Logging::KameleoonLogger.debug('RETURN: KameleoonClient.apply_new_configuration(data_file: %s, source: %s)',
|
|
1011
|
+
data_file, source)
|
|
942
1012
|
end
|
|
943
1013
|
|
|
944
1014
|
##
|
|
945
|
-
# Call the handler when configuration was updated
|
|
946
|
-
#
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
return if
|
|
950
|
-
|
|
951
|
-
|
|
1015
|
+
# Call the deprecated update configuration handler when configuration was updated
|
|
1016
|
+
# by a real-time (streaming) notification.
|
|
1017
|
+
def call_deprecated_update_handler
|
|
1018
|
+
handler = @update_configuration_handler
|
|
1019
|
+
return if handler.nil?
|
|
1020
|
+
|
|
1021
|
+
begin
|
|
1022
|
+
handler.call
|
|
1023
|
+
rescue StandardError => e
|
|
1024
|
+
Logging::KameleoonLogger.warning('Update configuration handler failed: %s', e)
|
|
1025
|
+
end
|
|
952
1026
|
end
|
|
953
1027
|
|
|
954
1028
|
def dispose(_object_id = nil)
|
|
955
1029
|
Logging::KameleoonLogger.debug('CALL: KameleoonClient.dispose')
|
|
956
|
-
|
|
957
|
-
|
|
1030
|
+
@disposed = true
|
|
1031
|
+
stop_polling_datafile_update
|
|
1032
|
+
stop_real_time_event_service_if_needed
|
|
958
1033
|
@visitor_manager.stop
|
|
959
1034
|
@tracking_manager.stop
|
|
960
1035
|
@scheduler.shutdown
|
|
@@ -24,7 +24,7 @@ module Kameleoon
|
|
|
24
24
|
key = get_client_key(site_code, config.environment)
|
|
25
25
|
client = @clients.compute_if_absent(key) do
|
|
26
26
|
client = KameleoonClient.new(site_code, config)
|
|
27
|
-
client.send(:
|
|
27
|
+
client.send(:fetch_configuration_job)
|
|
28
28
|
client
|
|
29
29
|
end
|
|
30
30
|
Logging::KameleoonLogger.info(
|
|
@@ -11,6 +11,7 @@ module Kameleoon
|
|
|
11
11
|
class TrackingManager
|
|
12
12
|
LINES_DELIMETER = "\n"
|
|
13
13
|
REQUEST_SIZE_LIMIT = 2560 * 1024 # 2.5 * 1024^2 characters
|
|
14
|
+
PROBE_MAX_VISITORS = 1
|
|
14
15
|
|
|
15
16
|
def initialize(
|
|
16
17
|
data_manager, network_manager, visitor_manager, track_interval_seconds, scheduler
|
|
@@ -19,7 +20,8 @@ module Kameleoon
|
|
|
19
20
|
'CALL: TrackingManager.new(data_manager, network_manager. visitor_manager, ' \
|
|
20
21
|
'track_interval_seconds: %s, scheduler)', track_interval_seconds
|
|
21
22
|
)
|
|
22
|
-
@tracking_visitors =
|
|
23
|
+
@tracking_visitors = ConcurrentVisitorTrackingRegistry.new(visitor_manager)
|
|
24
|
+
@probe_mode = false
|
|
23
25
|
@data_manager = data_manager
|
|
24
26
|
@network_manager = network_manager
|
|
25
27
|
@visitor_manager = visitor_manager
|
|
@@ -45,10 +47,19 @@ module Kameleoon
|
|
|
45
47
|
|
|
46
48
|
def track_all
|
|
47
49
|
Logging::KameleoonLogger.debug('CALL: TrackingManager.track_all')
|
|
48
|
-
|
|
50
|
+
if @probe_mode
|
|
51
|
+
track_probe
|
|
52
|
+
else
|
|
53
|
+
track(@tracking_visitors.extract)
|
|
54
|
+
end
|
|
49
55
|
Logging::KameleoonLogger.debug('RETURN: TrackingManager.track_all')
|
|
50
56
|
end
|
|
51
57
|
|
|
58
|
+
# Test purpose only
|
|
59
|
+
def probe_mode?
|
|
60
|
+
@probe_mode
|
|
61
|
+
end
|
|
62
|
+
|
|
52
63
|
def track_visitor(visitor_code)
|
|
53
64
|
Logging::KameleoonLogger.debug("CALL: TrackingManager.track_visitor(visitor_code: '%s')", visitor_code)
|
|
54
65
|
track([visitor_code])
|
|
@@ -57,6 +68,14 @@ module Kameleoon
|
|
|
57
68
|
|
|
58
69
|
private
|
|
59
70
|
|
|
71
|
+
# Sends one visitor's data; visitors without data are dropped (as on the normal path) until one is found.
|
|
72
|
+
def track_probe
|
|
73
|
+
until (visitor_code = @tracking_visitors.extract(PROBE_MAX_VISITORS)).empty?
|
|
74
|
+
return if track(visitor_code)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Returns whether a request was sent.
|
|
60
79
|
def track(visitor_codes)
|
|
61
80
|
builder = TrackingBuilder.new(visitor_codes, @data_manager.data_file, @visitor_manager, REQUEST_SIZE_LIMIT)
|
|
62
81
|
builder.build
|
|
@@ -71,8 +90,9 @@ module Kameleoon
|
|
|
71
90
|
perform_tracking_request(builder.visitor_codes_to_send, builder.unsent_visitor_data, builder.tracking_lines)
|
|
72
91
|
end
|
|
73
92
|
|
|
93
|
+
# Returns whether a request was sent.
|
|
74
94
|
def perform_tracking_request(visitor_codes, visitor_data, tracking_lines)
|
|
75
|
-
return if visitor_data.empty?
|
|
95
|
+
return false if visitor_data.empty?
|
|
76
96
|
|
|
77
97
|
lines = tracking_lines.join(LINES_DELIMETER)
|
|
78
98
|
# mark unsent data as transmitted
|
|
@@ -83,14 +103,32 @@ module Kameleoon
|
|
|
83
103
|
Logging::KameleoonLogger.info('Successful request for tracking visitors: %s, data: %s',
|
|
84
104
|
visitor_codes, visitor_data)
|
|
85
105
|
visitor_data.each { |d| d.mark_as_sent if d.is_a?(Kameleoon::Data) }
|
|
106
|
+
leave_probe_mode
|
|
86
107
|
else
|
|
87
108
|
Logging::KameleoonLogger.error('Tracking request failed')
|
|
88
109
|
Logging::KameleoonLogger.info('Failed request for tracking visitors: %s, data: %s',
|
|
89
110
|
visitor_codes, visitor_data)
|
|
90
111
|
visitor_data.each { |d| d.mark_as_unsent if d.is_a?(Kameleoon::Data) }
|
|
112
|
+
enter_probe_mode
|
|
91
113
|
@tracking_visitors.add_all(visitor_codes)
|
|
92
114
|
end
|
|
93
115
|
end
|
|
116
|
+
true
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def enter_probe_mode
|
|
120
|
+
return if @probe_mode
|
|
121
|
+
|
|
122
|
+
@probe_mode = true
|
|
123
|
+
Logging::KameleoonLogger.info("Tracking request failed: only one visitor's data per tracking request " \
|
|
124
|
+
'will be sent until a request succeeds')
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def leave_probe_mode
|
|
128
|
+
return unless @probe_mode
|
|
129
|
+
|
|
130
|
+
@probe_mode = false
|
|
131
|
+
Logging::KameleoonLogger.info('Tracking request succeeded: full-size tracking requests are restored')
|
|
94
132
|
end
|
|
95
133
|
end
|
|
96
134
|
end
|
|
@@ -1,91 +1,91 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'set'
|
|
4
|
-
|
|
5
3
|
module Kameleoon
|
|
6
4
|
module Managers
|
|
7
5
|
module Tracking
|
|
8
|
-
|
|
6
|
+
# Visitor codes waiting for a tracking request, in a Hash used as an insertion-ordered set.
|
|
7
|
+
#
|
|
8
|
+
# Lock-free on the hot path (MRI): `add` is a single Hash#[]=, which the GVL makes atomic (String keys hash
|
|
9
|
+
# in C, so no Ruby code runs inside it), and request threads never block on the registry. The Hash is never
|
|
10
|
+
# swapped out; `extract` removes with Hash#shift (O(1), oldest first) under a lock that only serializes
|
|
11
|
+
# extractors, so a concurrently added code is either returned now or kept for the next extraction, never
|
|
12
|
+
# lost. Nothing here iterates the live Hash: an `add` during iteration would raise in the request thread.
|
|
13
|
+
# Other engines run threads in parallel and take the lock on `add` as well: JRuby's Hash raises
|
|
14
|
+
# ConcurrencyError on unsynchronized concurrent writes; TruffleRuby's shared collections are thread-safe,
|
|
15
|
+
# but the lock-free path has not been measured there.
|
|
16
|
+
class ConcurrentVisitorTrackingRegistry
|
|
9
17
|
LIMITED_EXTRACTION_THRESHOLD_COEFFICIENT = 2
|
|
10
18
|
REMOVAL_FACTOR = 0.8
|
|
19
|
+
UNLIMITED_EXTRACTION = 2**62
|
|
20
|
+
LOCK_FREE_ADD = (RUBY_ENGINE == 'ruby')
|
|
11
21
|
|
|
12
22
|
def initialize(visitor_manager, storage_limit = 1_000_000, extraction_limit = 20_000)
|
|
13
23
|
@visitor_manager = visitor_manager
|
|
14
24
|
@storage_limit = storage_limit
|
|
15
25
|
@extraction_limit = extraction_limit
|
|
16
|
-
@visitors =
|
|
17
|
-
@
|
|
26
|
+
@visitors = {}
|
|
27
|
+
@extract_mutex = Mutex.new
|
|
18
28
|
end
|
|
19
29
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
30
|
+
if LOCK_FREE_ADD
|
|
31
|
+
def add(visitor_code)
|
|
32
|
+
@visitors[visitor_code] = true
|
|
33
|
+
end
|
|
23
34
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
@visitors.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
35
|
+
def add_all(visitor_codes)
|
|
36
|
+
visitor_codes.each { |vc| @visitors[vc] = true }
|
|
37
|
+
erase_to_storage_limit if @visitors.size > @storage_limit
|
|
38
|
+
end
|
|
39
|
+
else
|
|
40
|
+
def add(visitor_code)
|
|
41
|
+
@extract_mutex.synchronize { @visitors[visitor_code] = true }
|
|
31
42
|
end
|
|
43
|
+
|
|
44
|
+
def add_all(visitor_codes)
|
|
45
|
+
@extract_mutex.synchronize { visitor_codes.each { |vc| @visitors[vc] = true } }
|
|
46
|
+
erase_to_storage_limit if @visitors.size > @storage_limit # takes the (non-reentrant) mutex itself
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Removes and returns visitor codes, oldest first: all of them while the registry holds no more than
|
|
51
|
+
# `limit` codes and fewer than twice the extraction limit, otherwise `min(limit, extraction_limit)`.
|
|
52
|
+
def extract(limit = UNLIMITED_EXTRACTION)
|
|
53
|
+
@extract_mutex.synchronize { shift_visitors(extraction_count(@visitors.size, limit)) }
|
|
32
54
|
end
|
|
33
55
|
|
|
34
|
-
|
|
35
|
-
|
|
56
|
+
# Test purpose only
|
|
57
|
+
def visitors
|
|
58
|
+
@visitors.keys
|
|
36
59
|
end
|
|
37
60
|
|
|
38
61
|
private
|
|
39
62
|
|
|
40
|
-
def
|
|
41
|
-
|
|
42
|
-
end
|
|
63
|
+
def extraction_count(size, limit)
|
|
64
|
+
return size if (size <= limit) && (size < @extraction_limit * LIMITED_EXTRACTION_THRESHOLD_COEFFICIENT)
|
|
43
65
|
|
|
44
|
-
|
|
45
|
-
old_visitors = nil
|
|
46
|
-
new_visitors = Set.new
|
|
47
|
-
@mutex.synchronize do
|
|
48
|
-
old_visitors = @visitors
|
|
49
|
-
@visitors = new_visitors
|
|
50
|
-
end
|
|
51
|
-
old_visitors
|
|
66
|
+
[limit, @extraction_limit].min
|
|
52
67
|
end
|
|
53
68
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
end
|
|
61
|
-
i = 0
|
|
62
|
-
extracted = []
|
|
63
|
-
@visitors.each do |vc|
|
|
64
|
-
break if i >= @extraction_limit
|
|
69
|
+
# Fewer than `count` codes come back only if the registry runs empty meanwhile.
|
|
70
|
+
def shift_visitors(count)
|
|
71
|
+
extracted = []
|
|
72
|
+
while extracted.size < count
|
|
73
|
+
pair = @visitors.shift
|
|
74
|
+
break if pair.nil?
|
|
65
75
|
|
|
66
|
-
|
|
67
|
-
i += 1
|
|
68
|
-
@visitors.delete(vc)
|
|
69
|
-
end
|
|
76
|
+
extracted.push(pair[0])
|
|
70
77
|
end
|
|
71
78
|
extracted
|
|
72
79
|
end
|
|
73
80
|
|
|
74
|
-
# Not thread-safe
|
|
75
|
-
def erase_nonexistent_visitors
|
|
76
|
-
@visitors.delete_if { |vc| @visitor_manager.get_visitor(vc).nil? }
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
# Not thread-safe
|
|
80
81
|
def erase_to_storage_limit
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
visitors_to_remove_count
|
|
88
|
-
@visitors.delete(vc)
|
|
82
|
+
@extract_mutex.synchronize do
|
|
83
|
+
# Drain and re-insert instead of `delete_if`, so that concurrent adds never hit an iteration in progress.
|
|
84
|
+
# Codes added while draining stay in the live Hash.
|
|
85
|
+
drained = shift_visitors(@visitors.size)
|
|
86
|
+
drained.each { |vc| @visitors[vc] = true unless @visitor_manager.peek_visitor(vc).nil? }
|
|
87
|
+
visitors_to_remove_count = @visitors.size - (@storage_limit * REMOVAL_FACTOR).to_i
|
|
88
|
+
visitors_to_remove_count.times { @visitors.shift }
|
|
89
89
|
end
|
|
90
90
|
end
|
|
91
91
|
end
|