launchdarkly-server-sdk 8.18.1-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: 978ab174d6d432ede341e1a970e140a06ae4c07e1a377202649233e779c85bec
4
- data.tar.gz: ecca2bc7c0c0067c6c6b3b12b7cd018e0457eaa0d6ff694d65a5841d72bc3f5a
3
+ metadata.gz: c7079a31a9e896f54f3efa14e977ca139d2bf023458ffaa247b52c0d4216a96e
4
+ data.tar.gz: ecb670e91dcee356c5d8427dddcc071b377cdf5e5d36d965df29a3fc8fd936a6
5
5
  SHA512:
6
- metadata.gz: 52614f1147d38eaa8d2acf2fa2f4f72593a26520746bb3ed7ca8ccf5c1c13e29ef900b424e275e398867ad8590f30fb098bb40e8f51dab8bc5cac28bd8473935
7
- data.tar.gz: e37a04e7d825571da84756b4b62dfb33132c2e4116101aad8cb8163a5e0cfaee8c89fb4f64fe9ab05a444724a5f5a135ba549de589e685c022056b82d867a54e
6
+ metadata.gz: 919d9830ecd32c8e8e0e7656839d240897c94c8059ef9dfffc5769544e5e0c9bd4317ae601d7740713537c302bc16bcbaee1920a60c9b657928fbf68a3fe2af2
7
+ data.tar.gz: a1749cdd89a33e8dacb30efe4d334b072ebded9c5148a2bb49434878f5aa78a634318390a6a5abbc0a132bc0f6da89fb4b0aebea41ee118ffd4ef70657c77c40
@@ -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?
@@ -185,13 +186,16 @@ module LaunchDarkly
185
186
 
186
187
  def log_connection_started
187
188
  @connection_attempt_start_time = Impl::Util::current_time_millis
189
+ @connection_attempt_started_monotonic = Impl::Util.monotonic_seconds
188
190
  end
189
191
 
190
192
  def log_connection_result(is_success)
191
- if !@diagnostic_accumulator.nil? && @connection_attempt_start_time > 0
192
- @diagnostic_accumulator.record_stream_init(@connection_attempt_start_time, !is_success,
193
- Impl::Util::current_time_millis - @connection_attempt_start_time)
194
- @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
195
199
  end
196
200
  end
197
201
  end
@@ -463,10 +463,12 @@ module LaunchDarkly
463
463
  break if update == "quit"
464
464
 
465
465
  if update == "check"
466
- # Check condition periodically
467
- current_status = @data_source_status_provider.status
468
- return SyncResult::RECOVER if check_recovery && recovery_condition(current_status)
469
- 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)
470
472
  end
471
473
  next
472
474
  end
@@ -517,13 +519,14 @@ module LaunchDarkly
517
519
  # Determine if we should fallback to the next synchronizer.
518
520
  #
519
521
  # @param status [LaunchDarkly::Interfaces::DataSource::Status] Current data source status
522
+ # @param seconds_in_state [Float] monotonic seconds spent in status.state
520
523
  # @return [Boolean] true if fallback condition is met
521
524
  #
522
- def fallback_condition(status)
525
+ def fallback_condition(status, seconds_in_state)
523
526
  interrupted_at_runtime = status.state == LaunchDarkly::Interfaces::DataSource::Status::INTERRUPTED &&
524
- Time.now - status.state_since > 60 # 1 minute
527
+ seconds_in_state > 60 # 1 minute
525
528
  cannot_initialize = status.state == LaunchDarkly::Interfaces::DataSource::Status::INITIALIZING &&
526
- Time.now - status.state_since > 10 # 10 seconds
529
+ seconds_in_state > 10 # 10 seconds
527
530
 
528
531
  interrupted_at_runtime || cannot_initialize
529
532
  end
@@ -532,11 +535,12 @@ module LaunchDarkly
532
535
  # Determine if we should recover to the primary synchronizer.
533
536
  #
534
537
  # @param status [LaunchDarkly::Interfaces::DataSource::Status] Current data source status
538
+ # @param seconds_in_state [Float] monotonic seconds spent in status.state
535
539
  # @return [Boolean] true if recovery condition is met (healthy for too long)
536
540
  #
537
- def recovery_condition(status)
541
+ def recovery_condition(status, seconds_in_state)
538
542
  status.state == LaunchDarkly::Interfaces::DataSource::Status::VALID &&
539
- Time.now - status.state_since > 300 # 5 minutes
543
+ seconds_in_state > 300 # 5 minutes
540
544
  end
541
545
 
542
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
  #
@@ -119,7 +120,7 @@ module LaunchDarkly
119
120
  next unless update
120
121
 
121
122
  log_connection_result(true)
122
- @connection_attempt_start_time = 0
123
+ @connection_attempt_started_monotonic = nil
123
124
 
124
125
  yield update
125
126
 
@@ -367,16 +368,18 @@ module LaunchDarkly
367
368
 
368
369
  private def log_connection_started
369
370
  @connection_attempt_start_time = Impl::Util.current_time_millis
371
+ @connection_attempt_started_monotonic = Impl::Util.monotonic_seconds
370
372
  end
371
373
 
372
374
  private def log_connection_result(is_success)
373
375
  return unless @diagnostic_accumulator
374
- return unless @connection_attempt_start_time > 0
376
+ return if @connection_attempt_started_monotonic.nil?
375
377
 
376
- current_time = Impl::Util.current_time_millis
377
- elapsed = current_time - @connection_attempt_start_time
378
- @diagnostic_accumulator.record_stream_init(@connection_attempt_start_time, !is_success, elapsed >= 0 ? elapsed : 0)
379
- @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
380
383
  end
381
384
  end
382
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
@@ -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
@@ -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.1" # 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.1
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-10-06 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