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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ea79d87e992d2f5bf9829c8082427c18251101be90f50c769b46242a1f4fc272
4
- data.tar.gz: 305d07e5adee2b86cf58acc2b01f8a47f4161daeb06efbc8239e7a585ec355cd
3
+ metadata.gz: 04efd3ab470f352395de3161c329086e07893f9a29d2e8d55085cc4ab00788c3
4
+ data.tar.gz: 62d498e4f19ec285358b29672d622e0476e70ae31cbf39bd480bf33dbe5b335b
5
5
  SHA512:
6
- metadata.gz: c3319b271cf4053f01e9fab40b0133cd609a800f55fd3de84f19b76209d2fcbe5438672aa24a42dc39c1dd4dade97641956c2cff1f5c5c0cee8ece0d80636ca8
7
- data.tar.gz: 10dfdd35cf97d0adb20ed4a866c4b6739be6ec11458c267a550bf02f6e5ae2542b282b2c30ac33ae94797e3e4a8ce8dd6187406ef1d85603c2d02d0b9f09d44f
6
+ metadata.gz: 0431f162cc71fb10db0c3920bef2a9d04c35713dfdfd467fc750ac16345c3cfd5965b8abb17c335ad8a7e7226d85c776a14d5d76030c7173969b00a61b309fd1
7
+ data.tar.gz: 683b9a9e65838df67344bc3b5ff05d8b04eb37fe5328ec4532f4d18b141d9957ca6709b3373adc22ce4f52791d3d0e90be8927693acfe159add2ba991812dc16
@@ -1,50 +1,138 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'concurrent'
4
+ require 'timeout'
5
+ require 'kameleoon/exceptions'
4
6
 
5
7
  module Kameleoon
6
- # ClientReadiness is a cross-thread latch signalling that the SDK has
7
- # successfully loaded its configuration.
8
+ # ClientReadiness tracks whether the SDK has successfully loaded its configuration
9
+ # and exposes that state to `KameleoonClient#wait_init` and `KameleoonClient#is_ready?`:
10
+ # - a successful fetch settles the state successfully;
11
+ # - a failed fetch settles it with an `Exception::Initialization` error.
8
12
  #
9
- # It is created on the thread that builds the client but released (`set`) from
10
- # the background thread(s) performing configuration fetches, so it relies on
11
- # Concurrent::Event (which allows cross-thread release) rather than a
12
- # read/write lock (whose write lock must be released by the acquiring thread).
13
+ # Readiness is monotonic: once the SDK is ready it stays ready, and a later failed fetch
14
+ # can never revert it. A failure reported before the first success does not prevent a
15
+ # subsequent successful retry from marking the SDK ready.
13
16
  #
14
- # A failed fetch does NOT release the latch: the client keeps retrying in the
15
- # background and the latch is released as soon as any fetch succeeds. Callers
16
- # should bound their `wait` with a timeout so they are never blocked forever
17
- # when the configuration can never be loaded.
17
+ # It is created on the thread that builds the client but settled from the background
18
+ # thread(s) performing configuration fetches, so it relies on Concurrent::Event
19
+ # (which allows cross-thread release).
18
20
  class ClientReadiness
19
- def initialize
20
- @success = false
21
- @event = Concurrent::Event.new
21
+ # State is a single settlement of the readiness state: `error` is written at most
22
+ # once, before the event is set, and never changes afterwards, so a waiter which
23
+ # obtained the state observes exactly the outcome it was registered for.
24
+ class State
25
+ attr_accessor :error
26
+
27
+ def initialize
28
+ @event = Concurrent::Event.new
29
+ @error = nil
30
+ end
31
+
32
+ def settle
33
+ @event.set
34
+ end
35
+
36
+ def settled?
37
+ @event.set?
38
+ end
39
+
40
+ # @return [Boolean] whether the state was settled before the timeout elapsed.
41
+ def wait(timeout_second)
42
+ @event.wait(timeout_second)
43
+ end
22
44
  end
45
+ private_constant :State
23
46
 
24
- # @return [Boolean] whether the SDK has become ready (configuration loaded).
25
- def success
26
- @success
47
+ def initialize(site_code, environment)
48
+ @mutex = Mutex.new
49
+ @state = State.new
50
+ @ready = false
51
+ @site_code = site_code
52
+ @environment = environment
27
53
  end
28
54
 
29
- # Record a fetch outcome. A successful fetch marks the client ready and
30
- # unblocks all waiting threads; a failed fetch is ignored so that a later
31
- # successful retry can still release the latch. Idempotent and safe to call
32
- # from any thread.
33
- def set(success)
34
- return unless success
55
+ # Marks the SDK ready and releases all waiters.
56
+ def mark_ready
57
+ @mutex.synchronize do
58
+ @ready = true
59
+ if @state.settled?
60
+ unless @state.error.nil?
61
+ # Recovery after a failure: the failed state is left settled for its waiters,
62
+ # and a fresh, ready state is published for the new ones.
63
+ state = State.new
64
+ state.settle
65
+ @state = state
66
+ end
67
+ else
68
+ @state.settle
69
+ end
70
+ end
71
+ end
72
+
73
+ # Reports a failed fetch with an `Exception::Initialization` error, but only while
74
+ # the SDK is not yet ready. A failure never reverts an already-ready client and is
75
+ # reported at most once, so the first reported cause is the one waiters observe.
76
+ #
77
+ # @param cause [StandardError, nil] the failure which prevented the SDK from
78
+ # loading its configuration.
79
+ def mark_not_ready(cause)
80
+ @mutex.synchronize do
81
+ unless @state.settled?
82
+ @state.error = Exception::Initialization.new(@site_code, @environment, cause)
83
+ @state.settle
84
+ end # else: already ready, or the failure was already reported
85
+ end
86
+ end
35
87
 
36
- @success = true
37
- @event.set
88
+ # @return [Boolean] `true` if the SDK has been successfully initialized, `false`
89
+ # otherwise (including while the initialization is still pending or has failed).
90
+ # It never blocks.
91
+ #
92
+ # This is the per-request hot path, so it is lock-free: `@ready` is only ever
93
+ # flipped false -> true (readiness is monotonic), making a plain ivar read safe.
94
+ # At worst a reader briefly observes `false` while `mark_ready` is completing,
95
+ # which is indistinguishable from calling a moment earlier.
96
+ def ready?
97
+ @ready
38
98
  end
39
99
 
40
- # Block until the SDK becomes ready or the timeout elapses.
100
+ # Blocks until the readiness state is settled, but no longer than `timeout_second`.
101
+ # Returns the result of the configuration fetch: nil once the SDK is ready, or the
102
+ # `Exception::Initialization` error the fetch failure was reported with (fail-fast -
103
+ # it does not wait for background retries). If no fetch result is available within
104
+ # the timeout, returns an `Exception::Initialization` error caused by the expired
105
+ # timeout. A non-positive timeout expires immediately unless a result is already
106
+ # available. An expired timeout does not settle the readiness state: once the SDK
107
+ # becomes ready, a subsequent call returns nil.
41
108
  #
42
- # @param timeout [Numeric, nil] maximum number of seconds to wait;
43
- # nil waits indefinitely.
44
- # @return [Boolean] whether the SDK is ready.
45
- def wait(timeout = nil)
46
- @event.wait(timeout)
47
- @success
109
+ # @param timeout_second [Numeric] maximum number of seconds to wait.
110
+ # @return [Kameleoon::Exception::Initialization, nil] nil if the SDK is ready.
111
+ def wait_with_timeout(timeout_second)
112
+ state = current_state
113
+ return state.error if state.settled?
114
+ return timeout_failure(timeout_second) if timeout_second <= 0
115
+
116
+ return state.error if state.wait(timeout_second)
117
+
118
+ timeout_failure(timeout_second)
119
+ end
120
+
121
+ private
122
+
123
+ # The timeout failure wraps a `Timeout::Error` cause (the counterpart of the
124
+ # timeout error types the other SDKs report), so a caller can distinguish an
125
+ # expired timeout from a failed fetch.
126
+ def timeout_failure(timeout_second)
127
+ cause = Timeout::Error.new(
128
+ "initialization did not complete within #{(timeout_second * 1000).round} ms"
129
+ )
130
+ Exception::Initialization.new(@site_code, @environment, cause)
131
+ end
132
+
133
+ # Synchronized for safe publication of the state swapped in by a recovery.
134
+ def current_state
135
+ @mutex.synchronize { @state }
48
136
  end
49
137
  end
50
138
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'set'
3
4
  require 'kameleoon/logging/kameleoon_logger'
4
5
 
5
6
  module Kameleoon
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kameleoon
4
+ module Events
5
+ ##
6
+ # Describes an update of the SDK data file (configuration) reported to `DataFileUpdateHandler`.
7
+ class DataFileUpdateEvent
8
+ ##
9
+ # Source of a data file update.
10
+ module Source
11
+ ##
12
+ # The data file was updated by periodic polling.
13
+ POLLING = :polling
14
+
15
+ ##
16
+ # The data file was updated by a real-time (streaming) notification.
17
+ STREAMING = :streaming
18
+ end
19
+
20
+ # @return [Symbol] The source of the data file update, one of the `Source` constants.
21
+ attr_reader :source
22
+
23
+ # @return [Integer] The date of the last modification of the data file,
24
+ # in Unix time milliseconds.
25
+ attr_reader :date_modified
26
+
27
+ def initialize(source, date_modified)
28
+ @source = source
29
+ @date_modified = date_modified
30
+ end
31
+
32
+ def to_s
33
+ "DataFileUpdateEvent{source:#{@source},date_modified:#{@date_modified}}"
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'kameleoon/events/event_handler'
4
+
5
+ module Kameleoon
6
+ module Events
7
+ ##
8
+ # Handler of `EventType::DATAFILE_UPDATE` SDK events.
9
+ #
10
+ # A handler must respond to the following method:
11
+ #
12
+ # # Called when the SDK data file (configuration) is updated.
13
+ # #
14
+ # # @param event [Kameleoon::Events::DataFileUpdateEvent] The data file update details.
15
+ # def on_update(event); end
16
+ #
17
+ # Including this module is optional and serves documentation purposes only.
18
+ module DataFileUpdateHandler
19
+ include EventHandler
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kameleoon
4
+ module Events
5
+ ##
6
+ # Base module for all SDK event handlers. See `EventType` for the supported event types
7
+ # and their corresponding handler modules.
8
+ #
9
+ # Including the handler modules is optional: any object which responds to the methods
10
+ # required by the selected event type is accepted by `KameleoonClient#set_event_handler`.
11
+ module EventHandler
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'kameleoon/events/data_file_update_event'
4
+ require 'kameleoon/events/event_type'
5
+ require 'kameleoon/events/http_request_failure'
6
+ require 'kameleoon/events/request_type'
7
+ require 'kameleoon/logging/kameleoon_logger'
8
+
9
+ module Kameleoon
10
+ module Events
11
+ ##
12
+ # EventManager stores SDK event handlers and fires SDK events.
13
+ #
14
+ # @api private
15
+ class EventManager
16
+ HANDLER_METHODS = {
17
+ EventType::HTTP_REQUEST => %i[on_request_succeeded on_request_failed],
18
+ EventType::DATAFILE_UPDATE => %i[on_update]
19
+ }.freeze
20
+
21
+ def initialize
22
+ @event_handlers = {}
23
+ end
24
+
25
+ def set_event_handler(event_type, handler)
26
+ handler_methods = HANDLER_METHODS[event_type]
27
+ if handler_methods.nil?
28
+ Logging::KameleoonLogger.error("Unknown event type '%s'", event_type)
29
+ return
30
+ end
31
+ unless handler.nil? || handler_methods.all? { |method| handler.respond_to?(method) }
32
+ Logging::KameleoonLogger.error(
33
+ "Handler for event type '%s' must respond to the following methods: %s",
34
+ event_type, handler_methods.join(', ')
35
+ )
36
+ return
37
+ end
38
+ if handler.nil?
39
+ @event_handlers.delete(event_type)
40
+ else
41
+ @event_handlers[event_type] = handler
42
+ end
43
+ end
44
+
45
+ def fire_http_request_succeeded(request_type, http_status, duration_millis)
46
+ handler = @event_handlers[EventType::HTTP_REQUEST]
47
+ return if handler.nil?
48
+
49
+ begin
50
+ handler.on_request_succeeded(request_type, http_status, duration_millis)
51
+ rescue StandardError => e
52
+ Logging::KameleoonLogger.warning('HTTP request event handler failed: %s', e)
53
+ end
54
+ end
55
+
56
+ def fire_http_request_failed(request_type, failure, duration_millis)
57
+ handler = @event_handlers[EventType::HTTP_REQUEST]
58
+ return if handler.nil?
59
+
60
+ begin
61
+ handler.on_request_failed(request_type, failure, duration_millis)
62
+ rescue StandardError => e
63
+ Logging::KameleoonLogger.warning('HTTP request event handler failed: %s', e)
64
+ end
65
+ end
66
+
67
+ def fire_data_file_update(event)
68
+ handler = @event_handlers[EventType::DATAFILE_UPDATE]
69
+ return if handler.nil?
70
+
71
+ begin
72
+ handler.on_update(event)
73
+ rescue StandardError => e
74
+ Logging::KameleoonLogger.warning('Data file update event handler failed: %s', e)
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kameleoon
4
+ module Events
5
+ ##
6
+ # SDK event types which can be handled with `KameleoonClient#set_event_handler`.
7
+ module EventType
8
+ ##
9
+ # HTTP request event. Requires a handler implementing the `HttpRequestHandler` methods.
10
+ HTTP_REQUEST = :http_request
11
+
12
+ ##
13
+ # Data file update event. Requires a handler implementing the `DataFileUpdateHandler` methods.
14
+ DATAFILE_UPDATE = :datafile_update
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kameleoon
4
+ module Events
5
+ ##
6
+ # Describes the failure of an SDK HTTP request reported to `HttpRequestHandler`.
7
+ class HttpRequestFailure
8
+ ##
9
+ # Reason of an SDK HTTP request failure.
10
+ module Reason
11
+ ##
12
+ # The request completed with an unexpected HTTP status code.
13
+ HTTP_STATUS = :http_status
14
+
15
+ ##
16
+ # The request failed with an error or exception (e.g. a network error).
17
+ EXCEPTION = :exception
18
+
19
+ ##
20
+ # The request was cancelled (for example, due to a timeout).
21
+ CANCELLED = :cancelled
22
+ end
23
+
24
+ # @return [Symbol] The reason of the failure, one of the `Reason` constants.
25
+ attr_reader :reason
26
+
27
+ # @return [Integer, nil] The HTTP status code of the response. Not `nil` only if
28
+ # `reason` is `Reason::HTTP_STATUS`.
29
+ attr_reader :http_status
30
+
31
+ # @return [Exception, nil] The exception caused the failure. Not `nil` only if
32
+ # `reason` is `Reason::EXCEPTION`.
33
+ attr_reader :cause
34
+
35
+ # @api private
36
+ def self.from_http_status(http_status)
37
+ new(Reason::HTTP_STATUS, http_status, nil)
38
+ end
39
+
40
+ # @api private
41
+ def self.from_exception(cause)
42
+ new(Reason::EXCEPTION, nil, cause)
43
+ end
44
+
45
+ # @api private
46
+ def self.of_cancellation
47
+ new(Reason::CANCELLED, nil, nil)
48
+ end
49
+
50
+ def to_s
51
+ "HttpRequestFailure{reason:#{@reason},http_status:#{@http_status || 'nil'}," \
52
+ "cause:#{@cause.nil? ? 'nil' : @cause.class}}"
53
+ end
54
+
55
+ def initialize(reason, http_status, cause)
56
+ @reason = reason
57
+ @http_status = http_status
58
+ @cause = cause
59
+ end
60
+ private_class_method :new
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'kameleoon/events/event_handler'
4
+
5
+ module Kameleoon
6
+ module Events
7
+ ##
8
+ # Handler of `EventType::HTTP_REQUEST` SDK events. The handler is called once per each
9
+ # actual HTTP request attempt, including retries.
10
+ #
11
+ # A handler must respond to the following methods:
12
+ #
13
+ # # Called when an SDK HTTP request completes successfully.
14
+ # #
15
+ # # @param request_type [Symbol] The type of the request, one of the `RequestType` constants.
16
+ # # @param http_status [Integer] The HTTP status code of the response.
17
+ # # @param duration_millis [Integer] The duration of the request in milliseconds.
18
+ # def on_request_succeeded(request_type, http_status, duration_millis); end
19
+ #
20
+ # # Called when an SDK HTTP request fails.
21
+ # #
22
+ # # @param request_type [Symbol] The type of the request, one of the `RequestType` constants.
23
+ # # @param failure [Kameleoon::Events::HttpRequestFailure] The failure details.
24
+ # # @param duration_millis [Integer] The duration of the request in milliseconds.
25
+ # def on_request_failed(request_type, failure, duration_millis); end
26
+ #
27
+ # Including this module is optional and serves documentation purposes only.
28
+ module HttpRequestHandler
29
+ include EventHandler
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kameleoon
4
+ module Events
5
+ ##
6
+ # Type of an HTTP request performed by the SDK, reported to `HttpRequestHandler`.
7
+ module RequestType
8
+ ##
9
+ # Fetching of the SDK configuration (data file).
10
+ DATAFILE = :datafile
11
+
12
+ ##
13
+ # Sending of tracking data.
14
+ TRACKING = :tracking
15
+
16
+ ##
17
+ # Fetching of remote visitor data.
18
+ REMOTE_VISITOR_DATA = :remote_visitor_data
19
+
20
+ ##
21
+ # Fetching of remote data.
22
+ REMOTE_DATA = :remote_data
23
+
24
+ ##
25
+ # Fetching of the Kameleoon API access token.
26
+ ACCESS_TOKEN = :access_token
27
+ end
28
+ end
29
+ end
@@ -70,6 +70,22 @@ module Kameleoon
70
70
  class SiteCodeIsEmpty < KameleoonError
71
71
  end
72
72
 
73
+ # SDK could not be initialized: its configuration failed to load or no
74
+ # initialization result was available within the requested timeout.
75
+ # The failure which prevented the initialization is available via `cause`;
76
+ # it is also reported in the message so a caller which only logs the
77
+ # message still learns why the SDK is not initialized.
78
+ class Initialization < KameleoonError
79
+ # The failure which prevented the SDK from loading its configuration.
80
+ attr_reader :cause
81
+
82
+ def initialize(site_code, environment, cause)
83
+ @cause = cause
84
+ super("SDK is not initialized for siteCode: '#{site_code}', environment: '#{environment}'. " \
85
+ "Reason: #{cause.nil? ? 'unknown' : cause.message}")
86
+ end
87
+ end
88
+
73
89
  # Visitor Code Not Valid (empty or length > 255)
74
90
  class VisitorCodeInvalid < KameleoonError
75
91
  def initialize(visitor_code)
@@ -17,7 +17,7 @@ module Kameleoon
17
17
  end
18
18
  end
19
19
 
20
- # Implementation of Cache with auto cleaning feature
20
+ # Implementation of Manager with auto-expiring visitor variations
21
21
  class ManagerImpl < Manager
22
22
  def initialize(expiration_time, data_manager)
23
23
  super()