launchdarkly-server-sdk 8.15.1 → 8.17.0

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: 06b22ffc1ba131d82969abf4c5e6b04ab80d18a5112393a41f59a3b7217e9633
4
- data.tar.gz: 52e8d3a1615a43d15d9fa548db9568456409c3cbfd716f558635c7377b35987f
3
+ metadata.gz: 74715a28fdf3d75b188d4689034d3e297d90facb6084422f9fdc7dca4f007e81
4
+ data.tar.gz: 51b24db08e38873412290d8ed0860ea2ae9c7004aa4368a2a943029c347ef5a5
5
5
  SHA512:
6
- metadata.gz: b632a4958bab5ad77398d87cf3b018e0fe35c37e53b5590dc330b6c395a6fe22f0fc50ef820182b197179ac9c933356bf1a718b8f5bdcd4d8b40d3daae8367b5
7
- data.tar.gz: 22284f8464984ed1de431109260354d8285ae2079a47429a62e2e8fb49abb954e4d75c220cf264a9409b20bcd0ddc2cb6ca167c8ddd481bf8d769e08b5642362
6
+ metadata.gz: 1f1975f00b4a78aca6b008963654b6e1a6496e09b8634625bdd8c8af34a5d4b9a73788f3b107b076f751811e4b8f9209ffbc2203b0e8f098cccc4df629898122
7
+ data.tar.gz: 7e55a4a3997de7f46c5e5157d354f35990878645d1ac957f673903fa96bb523342d7dd33a0e780000fc53828d2519cdc049df24b661c208bb1fbe0ffb8ac3ba0
@@ -362,6 +362,9 @@ module LaunchDarkly
362
362
  # This payload filter key only applies to the default streaming and polling data sources. It will not affect TestData or FileData
363
363
  # data sources, nor will it be applied to any data source provided through the {#data_source} config property.
364
364
  #
365
+ # Payload filtering is not supported with the FDv2 data system, so this key has no effect on requests made by FDv2
366
+ # data sources.
367
+ #
365
368
  attr_reader :payload_filter_key
366
369
 
367
370
  #
@@ -54,6 +54,11 @@ module LaunchDarkly
54
54
  @config = config
55
55
  @data_system_config = data_system_config
56
56
  @logger = config.logger
57
+
58
+ unless config.payload_filter_key.nil?
59
+ @logger.warn { "[LDClient] Payload filtering is not supported with the FDv2 data system; the configured payload filter has no effect on FDv2 requests" }
60
+ end
61
+
57
62
  @synchronizer_builders = data_system_config.synchronizers || []
58
63
  @fdv1_fallback_synchronizer_builder = data_system_config.fdv1_fallback_synchronizer
59
64
  @disabled = @config.offline?
@@ -4,22 +4,39 @@ require "concurrent/atomics"
4
4
 
5
5
  module LaunchDarkly
6
6
  module Impl
7
+ #
8
+ # Runs a task again and again on a worker thread, with a fixed interval
9
+ # between runs.
10
+ #
11
+ # The worker waits on an event instead of calling `sleep`, so `stop` can
12
+ # wake it at once even if `stop` runs before the worker starts waiting.
13
+ #
14
+ # @private
15
+ #
7
16
  class RepeatingTask
8
17
  attr_reader :name
9
18
 
19
+ #
20
+ # @param interval [Numeric] seconds between the start of one run and the start of the next
21
+ # @param start_delay [Numeric, nil] seconds to wait before the first run
22
+ # @param task [Proc] the code to run
23
+ # @param logger [Logger]
24
+ # @param name [String] the name given to the worker thread
25
+ #
10
26
  def initialize(interval, start_delay, task, logger, name)
11
27
  @interval = interval
12
28
  @start_delay = start_delay
13
29
  @task = task
14
30
  @logger = logger
15
31
  @stopped = Concurrent::AtomicBoolean.new(false)
32
+ @stop_event = Concurrent::Event.new
16
33
  @worker = nil
17
34
  @name = name
18
35
  end
19
36
 
20
37
  def start
21
38
  @worker = Thread.new do
22
- sleep(@start_delay) unless @start_delay.nil? || @start_delay == 0
39
+ @stop_event.wait(@start_delay) unless @start_delay.nil? || @start_delay == 0
23
40
 
24
41
  until @stopped.value do
25
42
  started_at = Time.now
@@ -29,19 +46,23 @@ module LaunchDarkly
29
46
  Impl::Util.log_exception(@logger, "Uncaught exception from repeating task", e)
30
47
  end
31
48
  delta = @interval - (Time.now - started_at)
32
- if delta > 0
33
- sleep(delta)
34
- end
49
+ @stop_event.wait(delta) if delta > 0
35
50
  end
36
51
  end
37
52
 
38
53
  @worker.name = @name
39
54
  end
40
55
 
56
+ #
57
+ # Stops the worker thread and waits for it to finish.
58
+ #
59
+ # This method is safe to call more than once, before `start`, and from
60
+ # inside the task itself.
61
+ #
41
62
  def stop
42
63
  if @stopped.make_true
64
+ @stop_event.set
43
65
  if @worker && @worker.alive? && @worker != Thread.current
44
- @worker.run # causes the thread to wake up if it's currently in a sleep
45
66
  @worker.join
46
67
  end
47
68
  end
@@ -487,7 +487,7 @@ module LaunchDarkly
487
487
  #
488
488
  class FlagRuleBuilder
489
489
  # @api private
490
- FlagRuleClause = Struct.new(:contextKind, :attribute, :op, :values, :negate, keyword_init: true) # rubocop:disable Naming/MethodName:
490
+ FlagRuleClause = Struct.new(:contextKind, :attribute, :op, :values, :negate, keyword_init: true) # rubocop:disable Naming/MethodName
491
491
 
492
492
  # @api private
493
493
  def initialize(flag_builder)
@@ -33,10 +33,6 @@ module LaunchDarkly
33
33
 
34
34
  def_delegators :@config, :logger
35
35
 
36
- # @!method flush
37
- # Delegates to {LaunchDarkly::EventProcessorMethods#flush}.
38
- def_delegator :@event_processor, :flush
39
-
40
36
  # @!method data_store_status_provider
41
37
  # Delegates to the data system {LaunchDarkly::Impl::DataSystem#data_store_status_provider}.
42
38
  # @return [LaunchDarkly::Interfaces::DataStore::StatusProvider]
@@ -79,6 +75,12 @@ module LaunchDarkly
79
75
  config.instance_id = SecureRandom.uuid
80
76
  @config = config
81
77
 
78
+ # The pid of the process that owns this client's background threads. A child process that
79
+ # inherits this client after a fork has a different pid. See #check_forked.
80
+ @owner_pid = Process.pid
81
+ # The pid of the process in which the fork warning was last logged, or nil.
82
+ @fork_warned_pid = Concurrent::AtomicReference.new(nil)
83
+
82
84
  start_up(wait_for_sec)
83
85
  end
84
86
 
@@ -97,6 +99,10 @@ module LaunchDarkly
97
99
  # reason, it is recommended that any listener or hook integrations be added postfork unless you are certain it can
98
100
  # survive the forking process.
99
101
  #
102
+ # If the SDK is used in a forked child process before this method is called, it logs a warning once per process.
103
+ # The warning says that flag updates and analytics events will not work until `postfork` is called. Calling this
104
+ # method clears that condition for the current process.
105
+ #
100
106
  # @param wait_for_sec [Float] maximum time (in seconds) to wait for initialization
101
107
  #
102
108
  def postfork(wait_for_sec = 5)
@@ -105,9 +111,51 @@ module LaunchDarkly
105
111
  @big_segment_store_manager = nil
106
112
  @flag_tracker = nil
107
113
 
114
+ @owner_pid = Process.pid
115
+ @fork_warned_pid.set(nil)
116
+
108
117
  start_up(wait_for_sec)
109
118
  end
110
119
 
120
+ FORK_WARNING_MESSAGE = "[LDClient] This process was forked after the LDClient was created. Background threads do not " +
121
+ "survive a fork, so flag updates and analytics events will not work in this process. Call LDClient#postfork after forking."
122
+ private_constant :FORK_WARNING_MESSAGE
123
+
124
+ #
125
+ # Log a warning once per process if this client is used in a process that was forked after the client was created.
126
+ #
127
+ # Ruby threads do not survive a fork. A child process that inherits a client has no stream thread, no event
128
+ # dispatcher thread, and no timer tasks, so flag updates and analytics events stop working. The SDK does not
129
+ # repair this by itself; the application must call {#postfork}.
130
+ #
131
+ # The check compares the current pid with the pid recorded in the constructor or in {#postfork}. It is a single
132
+ # `getpid` call, which is cheap compared to a flag evaluation. The "already warned" state is stored as the pid in
133
+ # which the warning was logged, not as a boolean. A boolean would be copied into every child on fork, so a
134
+ # grandchild process would inherit "already warned" from its parent and never get its own warning.
135
+ #
136
+ private def check_forked
137
+ pid = Process.pid
138
+ return if pid == @owner_pid
139
+ return unless fork_breaks_client?
140
+
141
+ if @fork_warned_pid.get_and_set(pid) != pid
142
+ @config.logger.warn { FORK_WARNING_MESSAGE }
143
+ end
144
+ end
145
+
146
+ #
147
+ # Return true if this configuration needs background threads. Offline mode has none. LDD mode with events
148
+ # disabled reads flags from the persistent store and sends nothing, so a fork does not break it.
149
+ #
150
+ # @return [Boolean]
151
+ #
152
+ private def fork_breaks_client?
153
+ return false if @config.offline?
154
+ return false if @config.use_ldd? && !@config.send_events
155
+
156
+ true
157
+ end
158
+
111
159
  private def start_up(wait_for_sec)
112
160
  environment_metadata = get_environment_metadata
113
161
  plugin_hooks = get_plugin_hooks(environment_metadata)
@@ -281,6 +329,7 @@ module LaunchDarkly
281
329
  # @return the variation for the provided context, or the default value if there's an error
282
330
  #
283
331
  def variation(key, context, default)
332
+ check_forked
284
333
  context = Impl::Context::make_context(context)
285
334
  result = evaluate_with_hooks(key, context, default, :variation) do
286
335
  detail, _, _ = variation_with_flag(key, context, default)
@@ -314,6 +363,7 @@ module LaunchDarkly
314
363
  # @return [EvaluationDetail] an object describing the result
315
364
  #
316
365
  def variation_detail(key, context, default)
366
+ check_forked
317
367
  context = Impl::Context::make_context(context)
318
368
  result = evaluate_with_hooks(key, context, default, :variation_detail) do
319
369
  detail, _, _ = evaluate_internal(key, context, default, true)
@@ -439,6 +489,7 @@ module LaunchDarkly
439
489
  # @return [Array<Symbol, Interfaces::Migrations::OpTracker>]
440
490
  #
441
491
  def migration_variation(key, context, default_stage)
492
+ check_forked
442
493
  unless Migrations::VALID_STAGES.include? default_stage
443
494
  @config.logger.error { "[LDClient] default_stage #{default_stage} is not a valid stage; continuing with 'off' as default" }
444
495
  default_stage = Migrations::STAGE_OFF
@@ -480,6 +531,7 @@ module LaunchDarkly
480
531
  # @return [void]
481
532
  #
482
533
  def identify(context)
534
+ check_forked
483
535
  context = LaunchDarkly::Impl::Context.make_context(context)
484
536
  unless context.valid?
485
537
  @config.logger.warn("Identify called with invalid context: #{context.error}")
@@ -516,6 +568,7 @@ module LaunchDarkly
516
568
  # @return [void]
517
569
  #
518
570
  def track(event_name, context, data = nil, metric_value = nil)
571
+ check_forked
519
572
  context = LaunchDarkly::Impl::Context.make_context(context)
520
573
  unless context.valid?
521
574
  @config.logger.warn("Track called with invalid context: #{context.error}")
@@ -536,6 +589,7 @@ module LaunchDarkly
536
589
  # @param tracker [LaunchDarkly::Interfaces::Migrations::OpTracker]
537
590
  #
538
591
  def track_migration_op(tracker)
592
+ check_forked
539
593
  unless tracker.is_a? LaunchDarkly::Interfaces::Migrations::OpTracker
540
594
  @config.logger.error { "invalid op tracker received in track_migration_op" }
541
595
  return
@@ -571,6 +625,8 @@ module LaunchDarkly
571
625
  def all_flags_state(context, options={})
572
626
  return FeatureFlagsState.new(false) if @config.offline?
573
627
 
628
+ check_forked
629
+
574
630
  unless initialized?
575
631
  if @data_system.store.initialized?
576
632
  @config.logger.warn { "Called all_flags_state before client initialization; using last known values from data store" }
@@ -628,11 +684,24 @@ module LaunchDarkly
628
684
  state
629
685
  end
630
686
 
687
+ #
688
+ # Tells the client that all pending analytics events should be delivered as soon as possible.
689
+ #
690
+ # Delegates to {LaunchDarkly::EventProcessorMethods#flush}.
691
+ #
692
+ # @return [void]
693
+ #
694
+ def flush
695
+ check_forked
696
+ @event_processor.flush
697
+ end
698
+
631
699
  #
632
700
  # Releases all network connections and other resources held by the client, making it no longer usable.
633
701
  #
634
702
  # @return [void]
635
703
  def close
704
+ check_forked
636
705
  @config.logger.info { "[LDClient] Closing LaunchDarkly client..." }
637
706
  @data_system.stop
638
707
  @event_processor.stop
@@ -1,3 +1,3 @@
1
1
  module LaunchDarkly
2
- VERSION = "8.15.1" # x-release-please-version
2
+ VERSION = "8.17.0" # 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.15.1
4
+ version: 8.17.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - LaunchDarkly
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-08-28 00:00:00.000000000 Z
11
+ date: 2026-09-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: aws-sdk-dynamodb