launchdarkly-server-sdk 8.17.0 → 8.18.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: 74715a28fdf3d75b188d4689034d3e297d90facb6084422f9fdc7dca4f007e81
4
- data.tar.gz: 51b24db08e38873412290d8ed0860ea2ae9c7004aa4368a2a943029c347ef5a5
3
+ metadata.gz: 911f611485b4aee247de0914c23169d847c130f4053669c363d55080d4c71ace
4
+ data.tar.gz: fed937b0ec2a0b265dc01049cff2c8453970f6480dbd21711dffece525d55ef1
5
5
  SHA512:
6
- metadata.gz: 1f1975f00b4a78aca6b008963654b6e1a6496e09b8634625bdd8c8af34a5d4b9a73788f3b107b076f751811e4b8f9209ffbc2203b0e8f098cccc4df629898122
7
- data.tar.gz: 7e55a4a3997de7f46c5e5157d354f35990878645d1ac957f673903fa96bb523342d7dd33a0e780000fc53828d2519cdc049df24b661c208bb1fbe0ffb8ac3ba0
6
+ metadata.gz: b77e71d79a713370c10733fee8b3d16808cc06635fdb6f63861e020687fe096f1f2befaec578d68d63878e1126c39098aa5b88abee3d92dab45ac1eb91cae0dd
7
+ data.tar.gz: f0f69217e0707543248265cc87f7186a97d0eadf1978420cf71b0b86df66f7960db765ba6414af5c5f9a62187e5de177bb5da94fe0a7f36b3c09dcd016d416a0
@@ -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(
@@ -47,6 +47,7 @@ module LaunchDarkly
47
47
  opts = {
48
48
  headers: headers,
49
49
  read_timeout: READ_TIMEOUT_SECONDS,
50
+ connect_timeout: @config.connect_timeout,
50
51
  logger: @config.logger,
51
52
  socket_factory: @config.socket_factory,
52
53
  reconnect_time: @config.initial_reconnect_delay,
@@ -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
@@ -473,15 +477,14 @@ module LaunchDarkly
473
477
  # Handle the update
474
478
  @store.apply(update.change_set, true) if update.change_set
475
479
 
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
480
+ valid = update.state == LaunchDarkly::Interfaces::DataSource::Status::VALID
481
+ record_environment_id(update.environment_id) if valid
481
482
 
482
483
  # Update status
483
484
  @data_source_status_provider.update_status(update.state, update.error)
484
485
 
486
+ @ready_event.set if valid
487
+
485
488
  return SyncResult::FDV1 if update.fallback_to_fdv1
486
489
 
487
490
  return SyncResult::REMOVE if update.state == LaunchDarkly::Interfaces::DataSource::Status::OFF
@@ -97,6 +97,7 @@ module LaunchDarkly
97
97
  opts = {
98
98
  headers: headers,
99
99
  read_timeout: STREAM_READ_TIMEOUT,
100
+ connect_timeout: @http_config.connect_timeout,
100
101
  logger: @logger,
101
102
  socket_factory: @http_config.socket_factory,
102
103
  reconnect_time: @initial_reconnect_delay,
@@ -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
@@ -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
@@ -81,6 +81,10 @@ module LaunchDarkly
81
81
  # The pid of the process in which the fork warning was last logged, or nil.
82
82
  @fork_warned_pid = Concurrent::AtomicReference.new(nil)
83
83
 
84
+ # Each flag lets the matching cached-data warning log once per client.
85
+ @cached_data_evaluation_warned = Concurrent::AtomicBoolean.new(false)
86
+ @cached_data_all_flags_warned = Concurrent::AtomicBoolean.new(false)
87
+
84
88
  start_up(wait_for_sec)
85
89
  end
86
90
 
@@ -629,7 +633,9 @@ module LaunchDarkly
629
633
 
630
634
  unless initialized?
631
635
  if @data_system.store.initialized?
632
- @config.logger.warn { "Called all_flags_state before client initialization; using last known values from data store" }
636
+ if @cached_data_all_flags_warned.make_true
637
+ @config.logger.warn { "Called all_flags_state before client initialization; using last known values from data store. This message is logged once." }
638
+ end
633
639
  else
634
640
  @config.logger.warn { "Called all_flags_state before client initialization. Data store not available; returning empty state" }
635
641
  return FeatureFlagsState.new(false)
@@ -765,7 +771,9 @@ module LaunchDarkly
765
771
 
766
772
  if @data_system.data_availability != Impl::DataSystem::DataAvailability::REFRESHED
767
773
  if @data_system.data_availability == Impl::DataSystem::DataAvailability::CACHED
768
- @config.logger.warn { "[LDClient] Client has not finished initializing; using last known values from feature store" }
774
+ if @cached_data_evaluation_warned.make_true
775
+ @config.logger.warn { "[LDClient] Client has not finished initializing; using last known values from feature store. This message is logged once." }
776
+ end
769
777
  else
770
778
  @config.logger.error { "[LDClient] Client has not finished initializing; feature store unavailable, returning default value" }
771
779
  detail = Evaluator.error_result(EvaluationReason::ERROR_CLIENT_NOT_READY, default)
@@ -1,3 +1,3 @@
1
1
  module LaunchDarkly
2
- VERSION = "8.17.0" # x-release-please-version
2
+ VERSION = "8.18.1" # x-release-please-version
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: launchdarkly-server-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 8.17.0
4
+ version: 8.18.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - LaunchDarkly
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-09-14 00:00:00.000000000 Z
11
+ date: 2026-10-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: aws-sdk-dynamodb
@@ -346,16 +346,22 @@ dependencies:
346
346
  name: json
347
347
  requirement: !ruby/object:Gem::Requirement
348
348
  requirements:
349
- - - "~>"
349
+ - - ">="
350
350
  - !ruby/object:Gem::Version
351
351
  version: '2.3'
352
+ - - "<"
353
+ - !ruby/object:Gem::Version
354
+ version: '4'
352
355
  type: :runtime
353
356
  prerelease: false
354
357
  version_requirements: !ruby/object:Gem::Requirement
355
358
  requirements:
356
- - - "~>"
359
+ - - ">="
357
360
  - !ruby/object:Gem::Version
358
361
  version: '2.3'
362
+ - - "<"
363
+ - !ruby/object:Gem::Version
364
+ version: '4'
359
365
  description: Official LaunchDarkly SDK for Ruby
360
366
  email:
361
367
  - team@launchdarkly.com