launchdarkly-server-sdk 8.18.0-java → 8.18.2-java

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: b4f54af862047f472205f2c74c5ddcd68307cf16ad7a670a00936657f4904aba
4
- data.tar.gz: 1f3da02fe1782c8bead177dbc7b9dfcd5a432df508f6e3f0b834708883a651e9
3
+ metadata.gz: c7079a31a9e896f54f3efa14e977ca139d2bf023458ffaa247b52c0d4216a96e
4
+ data.tar.gz: ecb670e91dcee356c5d8427dddcc071b377cdf5e5d36d965df29a3fc8fd936a6
5
5
  SHA512:
6
- metadata.gz: cef3a3c3ac786a60563ad0a580b1ed93bf404a3d92c32b0a5c9812f8ba69d8298a6fd6fc69c0005a5d7c1f0c5338855483966e7b05e2b9fbf1ac949d0d648035
7
- data.tar.gz: 0fce5e4c5eb2928b44aecdb2e449a8dbb217dadb783ec1b9e2aa40bdfd82ba499079a4155160bb1b5434bb809ba7050f8e33f4c2d123ec484384cf0006e8c6e6
6
+ metadata.gz: 919d9830ecd32c8e8e0e7656839d240897c94c8059ef9dfffc5769544e5e0c9bd4317ae601d7740713537c302bc16bcbaee1920a60c9b657928fbf68a3fe2af2
7
+ data.tar.gz: a1749cdd89a33e8dacb30efe4d334b072ebded9c5148a2bb49434878f5aa78a634318390a6a5abbc0a132bc0f6da89fb4b0aebea41ee118ffd4ef70657c77c40
@@ -21,6 +21,7 @@ module LaunchDarkly
21
21
  @status_provider = BigSegmentStoreStatusProviderImpl.new(-> { get_status })
22
22
  @logger = logger
23
23
  @last_status = nil
24
+ @poll_lock = Mutex.new
24
25
 
25
26
  unless @store.nil?
26
27
  @cache = ExpiringCache.new(big_segments_config.context_cache_size, big_segments_config.context_cache_time)
@@ -49,18 +50,41 @@ module LaunchDarkly
49
50
  return BigSegmentMembershipResult.new(nil, BigSegmentsStatus::STORE_ERROR)
50
51
  end
51
52
  end
52
- poll_store_and_update_status unless @last_status
53
- unless @last_status.available
53
+ status = get_status
54
+ unless status.available
54
55
  return BigSegmentMembershipResult.new(membership, BigSegmentsStatus::STORE_ERROR)
55
56
  end
56
- BigSegmentMembershipResult.new(membership, @last_status.stale ? BigSegmentsStatus::STALE : BigSegmentsStatus::HEALTHY)
57
+ BigSegmentMembershipResult.new(membership, status.stale ? BigSegmentsStatus::STALE : BigSegmentsStatus::HEALTHY)
57
58
  end
58
59
 
59
60
  def get_status
60
- @last_status || poll_store_and_update_status
61
+ status = @last_status
62
+ return status if status
63
+
64
+ new_status = @poll_lock.synchronize do
65
+ # Another caller may have finished a poll while we waited for the lock.
66
+ status = @last_status
67
+ return status if status
68
+
69
+ query_store_status
70
+ end
71
+ @status_provider.update_status(new_status)
72
+
73
+ new_status
61
74
  end
62
75
 
63
76
  def poll_store_and_update_status
77
+ new_status = @poll_lock.synchronize { query_store_status }
78
+ @status_provider.update_status(new_status)
79
+
80
+ new_status
81
+ end
82
+
83
+ #
84
+ # Queries the store and caches the result. Callers hold @poll_lock, so this must not notify
85
+ # observers - a listener that calls back into the manager would deadlock on the mutex.
86
+ #
87
+ private def query_store_status
64
88
  new_status = Interfaces::BigSegmentStoreStatus.new(false, false) # default to "unavailable" if we don't get a new status below
65
89
  unless @store.nil?
66
90
  begin
@@ -71,9 +95,6 @@ module LaunchDarkly
71
95
  end
72
96
  end
73
97
  @last_status = new_status
74
- @status_provider.update_status(new_status)
75
-
76
- new_status
77
98
  end
78
99
 
79
100
  def stale?(timestamp)
@@ -38,14 +38,17 @@ module LaunchDarkly
38
38
  begin
39
39
  all_data, headers = request_all_data
40
40
  DataSource.record_environment_id(@config.data_source_update_sink, headers)
41
+ newly_initialized = false
41
42
  if all_data
42
43
  update_sink_or_data_store.init(all_data)
43
- if @initialized.make_true
44
- @config.logger.info { "[LDClient] Polling connection initialized" }
45
- @ready.set
46
- end
44
+ newly_initialized = @initialized.make_true
47
45
  end
48
46
  @config.data_source_update_sink&.update_status(LaunchDarkly::Interfaces::DataSource::Status::VALID, nil)
47
+
48
+ if newly_initialized
49
+ @config.logger.info { "[LDClient] Polling connection initialized" }
50
+ @ready.set
51
+ end
49
52
  rescue JSON::ParserError => e
50
53
  @config.logger.error { "[LDClient] JSON parsing failed for polling response." }
51
54
  error_info = LaunchDarkly::Interfaces::DataSource::ErrorInfo.new(
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "concurrent"
4
4
  require "forwardable"
5
+ require "ldclient-rb/impl/util"
5
6
  require "ldclient-rb/interfaces"
6
7
 
7
8
  module LaunchDarkly
@@ -19,18 +20,24 @@ module LaunchDarkly
19
20
  extend Forwardable
20
21
  def_delegators :@status_broadcaster, :add_listener, :remove_listener
21
22
 
23
+ MONOTONIC_CLOCK = -> { Impl::Util.monotonic_seconds }
24
+ private_constant :MONOTONIC_CLOCK
25
+
22
26
  #
23
27
  # Creates a new status provider.
24
28
  #
25
29
  # @param status_broadcaster [LaunchDarkly::Impl::Broadcaster] Broadcaster for status changes
30
+ # @param clock [#call] returns seconds on a monotonic clock; injectable for tests
26
31
  #
27
- def initialize(status_broadcaster)
32
+ def initialize(status_broadcaster, clock: MONOTONIC_CLOCK)
28
33
  @status_broadcaster = status_broadcaster
34
+ @clock = clock
29
35
  @status = LaunchDarkly::Interfaces::DataSource::Status.new(
30
36
  LaunchDarkly::Interfaces::DataSource::Status::INITIALIZING,
31
37
  Time.now,
32
38
  nil
33
39
  )
40
+ @state_since_monotonic = @clock.call
34
41
  @lock = Concurrent::ReadWriteLock.new
35
42
  end
36
43
 
@@ -41,8 +48,29 @@ module LaunchDarkly
41
48
  end
42
49
  end
43
50
 
51
+ #
52
+ # The current status together with the seconds the data source has spent
53
+ # in its current state, read atomically under one lock acquisition so the
54
+ # duration can never be paired with a stale state.
55
+ #
56
+ # The duration is measured on the monotonic clock, so a wall-clock step
57
+ # cannot distort it. `status.state_since` remains the wall-clock time
58
+ # reported to applications; this duration is the value to use when it
59
+ # decides behavior.
60
+ #
61
+ # @private
62
+ # @return [Array(LaunchDarkly::Interfaces::DataSource::Status, Float)]
63
+ #
64
+ def status_and_seconds_in_state
65
+ @lock.with_read_lock do
66
+ [@status, @clock.call - @state_since_monotonic]
67
+ end
68
+ end
69
+
44
70
  # (see LaunchDarkly::Interfaces::DataSource::UpdateSink#update_status)
45
71
  def update_status(new_state, new_error)
72
+ return if new_state.nil?
73
+
46
74
  status_to_broadcast = nil
47
75
 
48
76
  @lock.with_write_lock do
@@ -62,7 +90,12 @@ module LaunchDarkly
62
90
  # No change if state is the same and no error
63
91
  return if new_state == old_status.state && new_error.nil?
64
92
 
65
- new_since = new_state == old_status.state ? @status.state_since : Time.now
93
+ if new_state == old_status.state
94
+ new_since = @status.state_since
95
+ else
96
+ new_since = Time.now
97
+ @state_since_monotonic = @clock.call
98
+ end
66
99
  new_error = @status.last_error if new_error.nil?
67
100
 
68
101
  @status = LaunchDarkly::Interfaces::DataSource::Status.new(
@@ -32,6 +32,7 @@ module LaunchDarkly
32
32
  @stopped = Concurrent::AtomicBoolean.new(false)
33
33
  @ready = Concurrent::Event.new
34
34
  @connection_attempt_start_time = 0
35
+ @connection_attempt_started_monotonic = nil
35
36
  end
36
37
 
37
38
  def initialized?
@@ -47,6 +48,7 @@ module LaunchDarkly
47
48
  opts = {
48
49
  headers: headers,
49
50
  read_timeout: READ_TIMEOUT_SECONDS,
51
+ connect_timeout: @config.connect_timeout,
50
52
  logger: @config.logger,
51
53
  socket_factory: @config.socket_factory,
52
54
  reconnect_time: @config.initial_reconnect_delay,
@@ -184,13 +186,16 @@ module LaunchDarkly
184
186
 
185
187
  def log_connection_started
186
188
  @connection_attempt_start_time = Impl::Util::current_time_millis
189
+ @connection_attempt_started_monotonic = Impl::Util.monotonic_seconds
187
190
  end
188
191
 
189
192
  def log_connection_result(is_success)
190
- if !@diagnostic_accumulator.nil? && @connection_attempt_start_time > 0
191
- @diagnostic_accumulator.record_stream_init(@connection_attempt_start_time, !is_success,
192
- Impl::Util::current_time_millis - @connection_attempt_start_time)
193
- @connection_attempt_start_time = 0
193
+ if !@diagnostic_accumulator.nil? && !@connection_attempt_started_monotonic.nil?
194
+ # The monotonic stamp is both the "attempt in flight" sentinel and the
195
+ # duration base; the wall-clock value is only the reported timestamp.
196
+ duration_millis = ((Impl::Util.monotonic_seconds - @connection_attempt_started_monotonic) * 1_000).to_i
197
+ @diagnostic_accumulator.record_stream_init(@connection_attempt_start_time, !is_success, duration_millis)
198
+ @connection_attempt_started_monotonic = nil
194
199
  end
195
200
  end
196
201
  end
@@ -292,6 +292,10 @@ module LaunchDarkly
292
292
 
293
293
  # Set ready event if and only if a selector is defined for the changeset.
294
294
  if basis.change_set.selector && basis.change_set.selector.defined?
295
+ @data_source_status_provider.update_status(
296
+ LaunchDarkly::Interfaces::DataSource::Status::VALID,
297
+ nil
298
+ )
295
299
  @ready_event.set
296
300
  return fallback
297
301
  end
@@ -459,10 +463,12 @@ module LaunchDarkly
459
463
  break if update == "quit"
460
464
 
461
465
  if update == "check"
462
- # Check condition periodically
463
- current_status = @data_source_status_provider.status
464
- return SyncResult::RECOVER if check_recovery && recovery_condition(current_status)
465
- return SyncResult::FALLBACK if fallback_condition(current_status)
466
+ # Check condition periodically. One atomic read pairs the status with
467
+ # its monotonic time-in-state, so a verdict can never pair a stale
468
+ # state with a fresh duration.
469
+ current_status, seconds_in_state = @data_source_status_provider.status_and_seconds_in_state
470
+ return SyncResult::RECOVER if check_recovery && recovery_condition(current_status, seconds_in_state)
471
+ return SyncResult::FALLBACK if fallback_condition(current_status, seconds_in_state)
466
472
  end
467
473
  next
468
474
  end
@@ -473,15 +479,14 @@ module LaunchDarkly
473
479
  # Handle the update
474
480
  @store.apply(update.change_set, true) if update.change_set
475
481
 
476
- # Set ready event on valid update
477
- if update.state == LaunchDarkly::Interfaces::DataSource::Status::VALID
478
- @ready_event.set
479
- record_environment_id(update.environment_id)
480
- end
482
+ valid = update.state == LaunchDarkly::Interfaces::DataSource::Status::VALID
483
+ record_environment_id(update.environment_id) if valid
481
484
 
482
485
  # Update status
483
486
  @data_source_status_provider.update_status(update.state, update.error)
484
487
 
488
+ @ready_event.set if valid
489
+
485
490
  return SyncResult::FDV1 if update.fallback_to_fdv1
486
491
 
487
492
  return SyncResult::REMOVE if update.state == LaunchDarkly::Interfaces::DataSource::Status::OFF
@@ -514,13 +519,14 @@ module LaunchDarkly
514
519
  # Determine if we should fallback to the next synchronizer.
515
520
  #
516
521
  # @param status [LaunchDarkly::Interfaces::DataSource::Status] Current data source status
522
+ # @param seconds_in_state [Float] monotonic seconds spent in status.state
517
523
  # @return [Boolean] true if fallback condition is met
518
524
  #
519
- def fallback_condition(status)
525
+ def fallback_condition(status, seconds_in_state)
520
526
  interrupted_at_runtime = status.state == LaunchDarkly::Interfaces::DataSource::Status::INTERRUPTED &&
521
- Time.now - status.state_since > 60 # 1 minute
527
+ seconds_in_state > 60 # 1 minute
522
528
  cannot_initialize = status.state == LaunchDarkly::Interfaces::DataSource::Status::INITIALIZING &&
523
- Time.now - status.state_since > 10 # 10 seconds
529
+ seconds_in_state > 10 # 10 seconds
524
530
 
525
531
  interrupted_at_runtime || cannot_initialize
526
532
  end
@@ -529,11 +535,12 @@ module LaunchDarkly
529
535
  # Determine if we should recover to the primary synchronizer.
530
536
  #
531
537
  # @param status [LaunchDarkly::Interfaces::DataSource::Status] Current data source status
538
+ # @param seconds_in_state [Float] monotonic seconds spent in status.state
532
539
  # @return [Boolean] true if recovery condition is met (healthy for too long)
533
540
  #
534
- def recovery_condition(status)
541
+ def recovery_condition(status, seconds_in_state)
535
542
  status.state == LaunchDarkly::Interfaces::DataSource::Status::VALID &&
536
- Time.now - status.state_since > 300 # 5 minutes
543
+ seconds_in_state > 300 # 5 minutes
537
544
  end
538
545
 
539
546
  #
@@ -47,6 +47,7 @@ module LaunchDarkly
47
47
  @stopped = Concurrent::Event.new
48
48
  @diagnostic_accumulator = nil
49
49
  @connection_attempt_start_time = 0
50
+ @connection_attempt_started_monotonic = nil
50
51
  end
51
52
 
52
53
  #
@@ -97,6 +98,7 @@ module LaunchDarkly
97
98
  opts = {
98
99
  headers: headers,
99
100
  read_timeout: STREAM_READ_TIMEOUT,
101
+ connect_timeout: @http_config.connect_timeout,
100
102
  logger: @logger,
101
103
  socket_factory: @http_config.socket_factory,
102
104
  reconnect_time: @initial_reconnect_delay,
@@ -118,7 +120,7 @@ module LaunchDarkly
118
120
  next unless update
119
121
 
120
122
  log_connection_result(true)
121
- @connection_attempt_start_time = 0
123
+ @connection_attempt_started_monotonic = nil
122
124
 
123
125
  yield update
124
126
 
@@ -366,16 +368,18 @@ module LaunchDarkly
366
368
 
367
369
  private def log_connection_started
368
370
  @connection_attempt_start_time = Impl::Util.current_time_millis
371
+ @connection_attempt_started_monotonic = Impl::Util.monotonic_seconds
369
372
  end
370
373
 
371
374
  private def log_connection_result(is_success)
372
375
  return unless @diagnostic_accumulator
373
- return unless @connection_attempt_start_time > 0
376
+ return if @connection_attempt_started_monotonic.nil?
374
377
 
375
- current_time = Impl::Util.current_time_millis
376
- elapsed = current_time - @connection_attempt_start_time
377
- @diagnostic_accumulator.record_stream_init(@connection_attempt_start_time, !is_success, elapsed >= 0 ? elapsed : 0)
378
- @connection_attempt_start_time = 0
378
+ # The monotonic stamp is both the "attempt in flight" sentinel and the
379
+ # duration base; the wall-clock value is only the reported timestamp.
380
+ elapsed = ((Impl::Util.monotonic_seconds - @connection_attempt_started_monotonic) * 1_000).to_i
381
+ @diagnostic_accumulator.record_stream_init(@connection_attempt_start_time, !is_success, elapsed)
382
+ @connection_attempt_started_monotonic = nil
379
383
  end
380
384
  end
381
385
 
@@ -1,3 +1,4 @@
1
+ require "ldclient-rb/impl/util"
1
2
 
2
3
  module LaunchDarkly
3
4
  module Impl
@@ -7,10 +8,17 @@ module LaunchDarkly
7
8
  # * made thread-safe
8
9
  # * removed many unused methods
9
10
  # * reading a key does not reset its expiration time, only writing
11
+ # * expiration is measured on a monotonic clock, so wall-clock steps cannot
12
+ # retain entries past their TTL or evict them early
10
13
  class ExpiringCache
11
- def initialize(max_size, ttl)
14
+ MONOTONIC_CLOCK = -> { Impl::Util.monotonic_seconds }
15
+ private_constant :MONOTONIC_CLOCK
16
+
17
+ # @param clock [#call] returns seconds on a monotonic clock; injectable for tests
18
+ def initialize(max_size, ttl, clock: MONOTONIC_CLOCK)
12
19
  @max_size = max_size
13
20
  @ttl = ttl
21
+ @clock = clock
14
22
  @data_lru = {}
15
23
  @data_ttl = {}
16
24
  @lock = Mutex.new
@@ -31,7 +39,7 @@ module LaunchDarkly
31
39
  @data_ttl.delete(key)
32
40
 
33
41
  @data_lru[key] = val
34
- @data_ttl[key] = Time.now.to_f
42
+ @data_ttl[key] = @clock.call
35
43
 
36
44
  if @data_lru.size > @max_size
37
45
  key, _ = @data_lru.first # hashes have a FIFO ordering in Ruby
@@ -63,7 +71,7 @@ module LaunchDarkly
63
71
  private
64
72
 
65
73
  def ttl_evict
66
- ttl_horizon = Time.now.to_f - @ttl
74
+ ttl_horizon = @clock.call - @ttl
67
75
  key, time = @data_ttl.first
68
76
 
69
77
  until time.nil? || time > ttl_horizon
@@ -68,13 +68,15 @@ module LaunchDarkly
68
68
 
69
69
  def get_all_internal(kind)
70
70
  items_out = {}
71
- results = Diplomat::Kv.get(kind_key(kind), { recurse: true }, :return)
72
- (results == "" ? [] : results).each do |result|
71
+ prefix = kind_key(kind)
72
+ Diplomat::Kv.get_all(prefix, {}, :return).each do |result|
73
73
  value = result[:value]
74
- unless value.nil?
75
- item = Model.deserialize(kind, value)
76
- items_out[item[:key].to_sym] = item
77
- end
74
+ next if value.nil?
75
+ db_key = result[:key].to_s
76
+ next unless db_key.start_with?(prefix)
77
+ # Use the key that the item is stored under, not the key inside the item. A deleted
78
+ # item (a "tombstone") is not guaranteed to carry a key of its own.
79
+ items_out[db_key[prefix.length..].to_sym] = Model.deserialize(kind, value)
78
80
  end
79
81
  items_out
80
82
  end
@@ -113,7 +113,10 @@ module LaunchDarkly
113
113
  resp = @client.query(req)
114
114
  resp.items.each do |item|
115
115
  item_out = unmarshal_item(kind, item)
116
- items_out[item_out[:key].to_sym] = item_out
116
+ next if item_out.nil?
117
+ # Use the sort key that the item is stored under, not the key inside the item. A
118
+ # deleted item (a "tombstone") is not guaranteed to carry a key of its own.
119
+ items_out[item[SORT_KEY].to_sym] = item_out
117
120
  end
118
121
  break if resp.last_evaluated_key.nil? || resp.last_evaluated_key.length == 0
119
122
  req.exclusive_start_key = resp.last_evaluated_key
@@ -270,7 +270,7 @@ module LaunchDarkly
270
270
  # @return [LaunchDarkly::Migrations::OperationResult]
271
271
  #
272
272
  def run()
273
- start = Time.now
273
+ start = Impl::Util.monotonic_seconds
274
274
 
275
275
  begin
276
276
  result = @fn.call(@payload)
@@ -279,7 +279,7 @@ module LaunchDarkly
279
279
  result = LaunchDarkly::Result.fail("'#{origin}' operation raised an exception", e)
280
280
  end
281
281
 
282
- @tracker.latency(@origin, (Time.now - start) * 1_000) if @measure_latency
282
+ @tracker.latency(@origin, (Impl::Util.monotonic_seconds - start) * 1_000) if @measure_latency
283
283
  @tracker.error(@origin) if @measure_errors && !result.success?
284
284
  @tracker.invoked(@origin)
285
285
 
@@ -39,13 +39,15 @@ module LaunchDarkly
39
39
  @stop_event.wait(@start_delay) unless @start_delay.nil? || @start_delay == 0
40
40
 
41
41
  until @stopped.value do
42
- started_at = Time.now
42
+ started_at = Impl::Util.monotonic_seconds
43
43
  begin
44
44
  @task.call
45
45
  rescue => e
46
46
  Impl::Util.log_exception(@logger, "Uncaught exception from repeating task", e)
47
47
  end
48
- delta = @interval - (Time.now - started_at)
48
+ # The run must be measured on the monotonic clock: a wall-clock step would
49
+ # make this wait negative (immediate re-run) or stretch it by the step size.
50
+ delta = @interval - (Impl::Util.monotonic_seconds - started_at)
49
51
  @stop_event.wait(delta) if delta > 0
50
52
  end
51
53
  end
@@ -23,6 +23,7 @@ module LaunchDarkly
23
23
 
24
24
  @mutex = Mutex.new # Covers the following variables
25
25
  @last_available = true
26
+ @stopped = false
26
27
  # @type [LaunchDarkly::Impl::RepeatingTask, nil]
27
28
  @poller = nil
28
29
  end
@@ -53,12 +54,14 @@ module LaunchDarkly
53
54
 
54
55
  def stop
55
56
  @store.stop
56
- @mutex.synchronize do
57
- return if @poller.nil?
58
57
 
59
- @poller.stop
58
+ poller = @mutex.synchronize do
59
+ @stopped = true
60
+ task = @poller
60
61
  @poller = nil
62
+ task
61
63
  end
64
+ poller&.stop
62
65
  end
63
66
 
64
67
  def monitoring_enabled?
@@ -87,12 +90,12 @@ module LaunchDarkly
87
90
  @store_update_sink.update_status(status)
88
91
 
89
92
  if available
90
- @mutex.synchronize do
91
- return if @poller.nil?
92
-
93
- @poller.stop
93
+ poller = @mutex.synchronize do
94
+ task = @poller
94
95
  @poller = nil
96
+ task
95
97
  end
98
+ poller&.stop
96
99
 
97
100
  return
98
101
  end
@@ -102,6 +105,9 @@ module LaunchDarkly
102
105
  task = Impl::RepeatingTask.new(0.5, 0, -> { self.check_availability }, @logger, 'LD/StoreWrapper#check_availability')
103
106
 
104
107
  @mutex.synchronize do
108
+ # A read can fail after stop, and a poller started then would never be stopped.
109
+ next if @stopped
110
+
105
111
  @poller = task
106
112
  @poller.start
107
113
  end
@@ -12,6 +12,17 @@ module LaunchDarkly
12
12
  (Time.now.to_f * 1000).to_i
13
13
  end
14
14
 
15
+ #
16
+ # Seconds on the monotonic clock. Use this for measuring intervals and
17
+ # durations; use the wall clock only for values reported outside the
18
+ # process.
19
+ #
20
+ # @return [Float]
21
+ #
22
+ def self.monotonic_seconds
23
+ Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_second)
24
+ end
25
+
15
26
  def self.default_http_headers(sdk_key, config)
16
27
  ret = { "Authorization" => sdk_key, "User-Agent" => "RubyClient/" + LaunchDarkly::VERSION }
17
28
 
@@ -1,3 +1,3 @@
1
1
  module LaunchDarkly
2
- VERSION = "8.18.0" # x-release-please-version
2
+ VERSION = "8.18.2" # x-release-please-version
3
3
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: launchdarkly-server-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 8.18.0
4
+ version: 8.18.2
5
5
  platform: java
6
6
  authors:
7
7
  - LaunchDarkly
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-09-16 00:00:00.000000000 Z
10
+ date: 2026-10-07 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: aws-sdk-dynamodb