launchdarkly-server-sdk 7.0.4 → 7.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a8b1b56252c5dc7a4247aff5a006492277e5823a802666e73a8a0727e09c1496
4
- data.tar.gz: aa6e8485356a7705e42541d8b26d2be322f5abe5135b3f50840d2cf065e34676
3
+ metadata.gz: 0b14010cc3e61f33c32a83dc937046b23bdd00eabe728cef7a139e4300810cf0
4
+ data.tar.gz: 21eaf262fe3279bd8d4db2169737d3d934d0471e308b97037473e8d84d81af51
5
5
  SHA512:
6
- metadata.gz: '049a06570111278a044ea8845f63bf593caa19df7ce7baf03c4aadce2238b1e9d8f52fd9571fc108207028db40a0ff94c71a1e27eb5538e47b490617a9bc85c5'
7
- data.tar.gz: 73d0d4ac6484bbf206e35f6d519fca17cbda0364593efc35515bab68bd7252fe632ec6b807053989990c4c1133e2cb851b989aa2ff8e071235886416fef44b74
6
+ metadata.gz: b59b63876011041a2f37b2bcfabd179318622b1daadb7a33c5eaa217efb68abc2d2cc3d224c7ff6ad1e7a6b8223b15844a318cee767612d155b5b29a6cc135e8
7
+ data.tar.gz: 6808eb4a6eca0d7680152022344a8aee930b864282a37e12da806b8d311fb9d6cbaac86fef2732e3abbaa75f025c3e67edfab7869d2b117e9a82b14f32f98b39
@@ -57,6 +57,7 @@ module LaunchDarkly
57
57
  # @option opts [#open] :socket_factory See {#socket_factory}.
58
58
  # @option opts [BigSegmentsConfig] :big_segments See {#big_segments}.
59
59
  # @option opts [Hash] :application See {#application}
60
+ # @option opts [String] :payload_filter_key See {#payload_filter_key}
60
61
  #
61
62
  def initialize(opts = {})
62
63
  @base_uri = (opts[:base_uri] || Config.default_base_uri).chomp("/")
@@ -88,8 +89,24 @@ module LaunchDarkly
88
89
  @socket_factory = opts[:socket_factory]
89
90
  @big_segments = opts[:big_segments] || BigSegmentsConfig.new(store: nil)
90
91
  @application = LaunchDarkly::Impl::Util.validate_application_info(opts[:application] || {}, @logger)
92
+ @payload_filter_key = opts[:payload_filter_key]
93
+ @data_source_update_sink = nil
91
94
  end
92
95
 
96
+ #
97
+ # Returns the component that allows a data source to push data into the SDK.
98
+ #
99
+ # This property should only be set by the SDK. Long term access of this
100
+ # property is not supported; it is temporarily being exposed to maintain
101
+ # backwards compatibility while the SDK structure is updated.
102
+ #
103
+ # Custom data source implementations should integrate with this sink if
104
+ # they want to provide support for data source status listeners.
105
+ #
106
+ # @private
107
+ #
108
+ attr_accessor :data_source_update_sink
109
+
93
110
  #
94
111
  # The base URL for the LaunchDarkly server. This is configurable mainly for testing
95
112
  # purposes; most users should use the default value.
@@ -330,6 +347,21 @@ module LaunchDarkly
330
347
  #
331
348
  attr_reader :application
332
349
 
350
+ #
351
+ # LaunchDarkly Server SDKs historically downloaded all flag configuration and segments for a particular environment
352
+ # during initialization.
353
+ #
354
+ # For some customers, this is an unacceptably large amount of data, and has contributed to performance issues within
355
+ # their products.
356
+ #
357
+ # Filtered environments aim to solve this problem. By allowing customers to specify subsets of an environment's
358
+ # flags using a filter key, SDKs will initialize faster and use less memory.
359
+ #
360
+ # This payload filter key only applies to the default streaming and polling data sources. It will not affect TestData or FileData
361
+ # data sources, nor will it be applied to any data source provided through the {#data_source} config property.
362
+ #
363
+ attr_reader :payload_filter_key
364
+
333
365
  #
334
366
  # Set to true to opt out of sending diagnostics data.
335
367
  #
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LaunchDarkly
4
+ module Impl
5
+
6
+ #
7
+ # A generic mechanism for registering event listeners and broadcasting
8
+ # events to them.
9
+ #
10
+ # The SDK maintains an instance of this for each available type of listener
11
+ # (flag change, data store status, etc.). They are all intended to share a
12
+ # single executor service; notifications are submitted individually to this
13
+ # service for each listener.
14
+ #
15
+ class Broadcaster
16
+ def initialize(executor, logger)
17
+ @listeners = Concurrent::Set.new
18
+ @executor = executor
19
+ @logger = logger
20
+ end
21
+
22
+ #
23
+ # Register a listener to this broadcaster.
24
+ #
25
+ # @param listener [#update]
26
+ #
27
+ def add_listener(listener)
28
+ unless listener.respond_to? :update
29
+ logger.warn("listener (#{listener.class}) does not respond to :update method. ignoring as registered listener")
30
+ return
31
+ end
32
+
33
+ listeners.add(listener)
34
+ end
35
+
36
+ #
37
+ # Removes a registered listener from this broadcaster.
38
+ #
39
+ def remove_listener(listener)
40
+ listeners.delete(listener)
41
+ end
42
+
43
+ def has_listeners?
44
+ !listeners.empty?
45
+ end
46
+
47
+ #
48
+ # Broadcast the provided event to all registered listeners.
49
+ #
50
+ # Each listener will be notified using the broadcasters executor. This
51
+ # method is non-blocking.
52
+ #
53
+ def broadcast(event)
54
+ listeners.each do |listener|
55
+ executor.post do
56
+ begin
57
+ listener.update(event)
58
+ rescue StandardError => e
59
+ logger.error("listener (#{listener.class}) raised exception (#{e}) processing event (#{event.class})")
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+
66
+ private
67
+
68
+ # @return [Concurrent::ThreadPoolExecutor]
69
+ attr_reader :executor
70
+
71
+ # @return [Logger]
72
+ attr_reader :logger
73
+
74
+ # @return [Concurrent::Set]
75
+ attr_reader :listeners
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,188 @@
1
+ require "concurrent"
2
+ require "forwardable"
3
+ require "ldclient-rb/impl/dependency_tracker"
4
+ require "ldclient-rb/interfaces"
5
+ require "set"
6
+
7
+ module LaunchDarkly
8
+ module Impl
9
+ module DataSource
10
+ class StatusProvider
11
+ include LaunchDarkly::Interfaces::DataSource::StatusProvider
12
+
13
+ extend Forwardable
14
+ def_delegators :@status_broadcaster, :add_listener, :remove_listener
15
+
16
+ def initialize(status_broadcaster, update_sink)
17
+ # @type [Broadcaster]
18
+ @status_broadcaster = status_broadcaster
19
+ # @type [UpdateSink]
20
+ @data_source_update_sink = update_sink
21
+ end
22
+
23
+ def status
24
+ @data_source_update_sink.current_status
25
+ end
26
+ end
27
+
28
+ class UpdateSink
29
+ include LaunchDarkly::Interfaces::DataSource::UpdateSink
30
+
31
+ # @return [LaunchDarkly::Interfaces::DataSource::Status]
32
+ attr_reader :current_status
33
+
34
+ def initialize(data_store, status_broadcaster, flag_change_broadcaster)
35
+ # @type [LaunchDarkly::Interfaces::FeatureStore]
36
+ @data_store = data_store
37
+ # @type [Broadcaster]
38
+ @status_broadcaster = status_broadcaster
39
+ # @type [Broadcaster]
40
+ @flag_change_broadcaster = flag_change_broadcaster
41
+ @dependency_tracker = LaunchDarkly::Impl::DependencyTracker.new
42
+
43
+ @mutex = Mutex.new
44
+ @current_status = LaunchDarkly::Interfaces::DataSource::Status.new(
45
+ LaunchDarkly::Interfaces::DataSource::Status::INITIALIZING,
46
+ Time.now,
47
+ nil)
48
+ end
49
+
50
+ def init(all_data)
51
+ old_data = nil
52
+ monitor_store_update do
53
+ if @flag_change_broadcaster.has_listeners?
54
+ old_data = {}
55
+ LaunchDarkly::ALL_KINDS.each do |kind|
56
+ old_data[kind] = @data_store.all(kind)
57
+ end
58
+ end
59
+
60
+ @data_store.init(all_data)
61
+ end
62
+
63
+ update_full_dependency_tracker(all_data)
64
+
65
+ return if old_data.nil?
66
+
67
+ send_change_events(
68
+ compute_changed_items_for_full_data_set(old_data, all_data)
69
+ )
70
+ end
71
+
72
+ def upsert(kind, item)
73
+ monitor_store_update { @data_store.upsert(kind, item) }
74
+
75
+ # TODO(sc-197908): We only want to do this if the store successfully
76
+ # updates the record.
77
+ @dependency_tracker.update_dependencies_from(kind, item[:key], item)
78
+ if @flag_change_broadcaster.has_listeners?
79
+ affected_items = Set.new
80
+ @dependency_tracker.add_affected_items(affected_items, {kind: kind, key: item[:key]})
81
+ send_change_events(affected_items)
82
+ end
83
+ end
84
+
85
+ def delete(kind, key, version)
86
+ monitor_store_update { @data_store.delete(kind, key, version) }
87
+
88
+ @dependency_tracker.update_dependencies_from(kind, key, nil)
89
+ if @flag_change_broadcaster.has_listeners?
90
+ affected_items = Set.new
91
+ @dependency_tracker.add_affected_items(affected_items, {kind: kind, key: key})
92
+ send_change_events(affected_items)
93
+ end
94
+ end
95
+
96
+ def update_status(new_state, new_error)
97
+ return if new_state.nil?
98
+
99
+ status_to_broadcast = nil
100
+
101
+ @mutex.synchronize do
102
+ old_status = @current_status
103
+
104
+ if new_state == LaunchDarkly::Interfaces::DataSource::Status::INTERRUPTED && old_status.state == LaunchDarkly::Interfaces::DataSource::Status::INITIALIZING
105
+ # See {LaunchDarkly::Interfaces::DataSource::UpdateSink#update_status} for more information
106
+ new_state = LaunchDarkly::Interfaces::DataSource::Status::INITIALIZING
107
+ end
108
+
109
+ unless new_state == old_status.state && new_error.nil?
110
+ @current_status = LaunchDarkly::Interfaces::DataSource::Status.new(
111
+ new_state,
112
+ new_state == current_status.state ? current_status.state_since : Time.now,
113
+ new_error.nil? ? current_status.last_error : new_error
114
+ )
115
+ status_to_broadcast = current_status
116
+ end
117
+ end
118
+
119
+ @status_broadcaster.broadcast(status_to_broadcast) unless status_to_broadcast.nil?
120
+ end
121
+
122
+ private def update_full_dependency_tracker(all_data)
123
+ @dependency_tracker.reset
124
+ all_data.each do |kind, items|
125
+ items.each do |key, item|
126
+ @dependency_tracker.update_dependencies_from(kind, item.key, item)
127
+ end
128
+ end
129
+ end
130
+
131
+
132
+ #
133
+ # Method to monitor updates to the store. You provide a block to update
134
+ # the store. This mthod wraps that block, catching and re-raising all
135
+ # errors, and notifying all status listeners of the error.
136
+ #
137
+ private def monitor_store_update
138
+ begin
139
+ yield
140
+ rescue => e
141
+ error_info = LaunchDarkly::Interfaces::DataSource::ErrorInfo.new(LaunchDarkly::Interfaces::DataSource::ErrorInfo::STORE_ERROR, 0, e.to_s, Time.now)
142
+ update_status(LaunchDarkly::Interfaces::DataSource::Status::INTERRUPTED, error_info)
143
+ raise
144
+ end
145
+ end
146
+
147
+
148
+ #
149
+ # @param [Hash] old_data
150
+ # @param [Hash] new_data
151
+ # @return [Set]
152
+ #
153
+ private def compute_changed_items_for_full_data_set(old_data, new_data)
154
+ affected_items = Set.new
155
+
156
+ LaunchDarkly::ALL_KINDS.each do |kind|
157
+ old_items = old_data[kind] || {}
158
+ new_items = new_data[kind] || {}
159
+
160
+ old_items.keys.concat(new_items.keys).each do |key|
161
+ old_item = old_items[key]
162
+ new_item = new_items[key]
163
+
164
+ next if old_item.nil? && new_item.nil?
165
+
166
+ if old_item.nil? || new_item.nil? || old_item[:version] < new_item[:version]
167
+ @dependency_tracker.add_affected_items(affected_items, {kind: kind, key: key.to_s})
168
+ end
169
+ end
170
+ end
171
+
172
+ affected_items
173
+ end
174
+
175
+ #
176
+ # @param affected_items [Set]
177
+ #
178
+ private def send_change_events(affected_items)
179
+ affected_items.each do |item|
180
+ if item[:kind] == LaunchDarkly::FEATURES
181
+ @flag_change_broadcaster.broadcast(LaunchDarkly::Interfaces::FlagChange.new(item[:key]))
182
+ end
183
+ end
184
+ end
185
+ end
186
+ end
187
+ end
188
+ end
@@ -0,0 +1,59 @@
1
+ require 'concurrent'
2
+ require "ldclient-rb/interfaces"
3
+
4
+ module LaunchDarkly
5
+ module Impl
6
+ module DataStore
7
+ class StatusProvider
8
+ include LaunchDarkly::Interfaces::DataStore::StatusProvider
9
+
10
+ def initialize(store, update_sink)
11
+ # @type [LaunchDarkly::Impl::FeatureStoreClientWrapper]
12
+ @store = store
13
+ # @type [UpdateSink]
14
+ @update_sink = update_sink
15
+ end
16
+
17
+ def status
18
+ @update_sink.last_status.get
19
+ end
20
+
21
+ def monitoring_enabled?
22
+ @store.monitoring_enabled?
23
+ end
24
+
25
+ def add_listener(listener)
26
+ @update_sink.broadcaster.add_listener(listener)
27
+ end
28
+
29
+ def remove_listener(listener)
30
+ @update_sink.broadcaster.remove_listener(listener)
31
+ end
32
+ end
33
+
34
+ class UpdateSink
35
+ include LaunchDarkly::Interfaces::DataStore::UpdateSink
36
+
37
+ # @return [LaunchDarkly::Impl::Broadcaster]
38
+ attr_reader :broadcaster
39
+
40
+ # @return [Concurrent::AtomicReference]
41
+ attr_reader :last_status
42
+
43
+ def initialize(broadcaster)
44
+ @broadcaster = broadcaster
45
+ @last_status = Concurrent::AtomicReference.new(
46
+ LaunchDarkly::Interfaces::DataStore::Status.new(true, false)
47
+ )
48
+ end
49
+
50
+ def update_status(status)
51
+ return if status.nil?
52
+
53
+ old_status = @last_status.get_and_set(status)
54
+ @broadcaster.broadcast(status) unless old_status == status
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,102 @@
1
+ module LaunchDarkly
2
+ module Impl
3
+ class DependencyTracker
4
+ def initialize
5
+ @from = {}
6
+ @to = {}
7
+ end
8
+
9
+ #
10
+ # Updates the dependency graph when an item has changed.
11
+ #
12
+ # @param from_kind [Object] the changed item's kind
13
+ # @param from_key [String] the changed item's key
14
+ # @param from_item [Object] the changed item
15
+ #
16
+ def update_dependencies_from(from_kind, from_key, from_item)
17
+ from_what = { kind: from_kind, key: from_key }
18
+ updated_dependencies = DependencyTracker.compute_dependencies_from(from_kind, from_item)
19
+
20
+ old_dependency_set = @from[from_what]
21
+ unless old_dependency_set.nil?
22
+ old_dependency_set.each do |kind_and_key|
23
+ deps_to_this_old_dep = @to[kind_and_key]
24
+ deps_to_this_old_dep&.delete(from_what)
25
+ end
26
+ end
27
+
28
+ @from[from_what] = updated_dependencies
29
+ updated_dependencies.each do |kind_and_key|
30
+ deps_to_this_new_dep = @to[kind_and_key]
31
+ if deps_to_this_new_dep.nil?
32
+ deps_to_this_new_dep = Set.new
33
+ @to[kind_and_key] = deps_to_this_new_dep
34
+ end
35
+ deps_to_this_new_dep.add(from_what)
36
+ end
37
+ end
38
+
39
+ def self.segment_keys_from_clauses(clauses)
40
+ clauses.flat_map do |clause|
41
+ if clause.op == :segmentMatch
42
+ clause.values.map { |value| {kind: LaunchDarkly::SEGMENTS, key: value }}
43
+ else
44
+ []
45
+ end
46
+ end
47
+ end
48
+
49
+ #
50
+ # @param from_kind [String]
51
+ # @param from_item [LaunchDarkly::Impl::Model::FeatureFlag, LaunchDarkly::Impl::Model::Segment]
52
+ # @return [Set]
53
+ #
54
+ def self.compute_dependencies_from(from_kind, from_item)
55
+ return Set.new if from_item.nil?
56
+
57
+ if from_kind == LaunchDarkly::FEATURES
58
+ prereq_keys = from_item.prerequisites.map { |prereq| {kind: from_kind, key: prereq.key} }
59
+ segment_keys = from_item.rules.flat_map { |rule| DependencyTracker.segment_keys_from_clauses(rule.clauses) }
60
+
61
+ results = Set.new(prereq_keys)
62
+ results.merge(segment_keys)
63
+ elsif from_kind == LaunchDarkly::SEGMENTS
64
+ kind_and_keys = from_item.rules.flat_map do |rule|
65
+ DependencyTracker.segment_keys_from_clauses(rule.clauses)
66
+ end
67
+ Set.new(kind_and_keys)
68
+ else
69
+ Set.new
70
+ end
71
+ end
72
+
73
+ #
74
+ # Clear any tracked dependencies and reset the tracking state to a clean slate.
75
+ #
76
+ def reset
77
+ @from.clear
78
+ @to.clear
79
+ end
80
+
81
+ #
82
+ # Populates the given set with the union of the initial item and all items that directly or indirectly
83
+ # depend on it (based on the current state of the dependency graph).
84
+ #
85
+ # @param items_out [Set]
86
+ # @param initial_modified_item [Object]
87
+ #
88
+ def add_affected_items(items_out, initial_modified_item)
89
+ return if items_out.include? initial_modified_item
90
+
91
+ items_out.add(initial_modified_item)
92
+ affected_items = @to[initial_modified_item]
93
+
94
+ return if affected_items.nil?
95
+
96
+ affected_items.each do |affected_item|
97
+ add_affected_items(items_out, affected_item)
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,58 @@
1
+ require "concurrent"
2
+ require "ldclient-rb/interfaces"
3
+ require "forwardable"
4
+
5
+ module LaunchDarkly
6
+ module Impl
7
+ class FlagTracker
8
+ include LaunchDarkly::Interfaces::FlagTracker
9
+
10
+ extend Forwardable
11
+ def_delegators :@broadcaster, :add_listener, :remove_listener
12
+
13
+ def initialize(broadcaster, eval_fn)
14
+ @broadcaster = broadcaster
15
+ @eval_fn = eval_fn
16
+ end
17
+
18
+ def add_flag_value_change_listener(key, context, listener)
19
+ flag_change_listener = FlagValueChangeAdapter.new(key, context, listener, @eval_fn)
20
+ add_listener(flag_change_listener)
21
+
22
+ flag_change_listener
23
+ end
24
+
25
+ #
26
+ # An adapter which turns a normal flag change listener into a flag value change listener.
27
+ #
28
+ class FlagValueChangeAdapter
29
+ # @param [Symbol] flag_key
30
+ # @param [LaunchDarkly::LDContext] context
31
+ # @param [#update] listener
32
+ # @param [#call] eval_fn
33
+ def initialize(flag_key, context, listener, eval_fn)
34
+ @flag_key = flag_key
35
+ @context = context
36
+ @listener = listener
37
+ @eval_fn = eval_fn
38
+ @value = Concurrent::AtomicReference.new(@eval_fn.call(@flag_key, @context))
39
+ end
40
+
41
+ #
42
+ # @param [LaunchDarkly::Interfaces::FlagChange] flag_change
43
+ #
44
+ def update(flag_change)
45
+ return unless flag_change.key == @flag_key
46
+
47
+ new_eval = @eval_fn.call(@flag_key, @context)
48
+ old_eval = @value.get_and_set(new_eval)
49
+
50
+ return if new_eval == old_eval
51
+
52
+ @listener.update(
53
+ LaunchDarkly::Interfaces::FlagValueChange.new(@flag_key, old_eval, new_eval))
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -119,6 +119,18 @@ module LaunchDarkly
119
119
  end
120
120
  end
121
121
 
122
+ def available?
123
+ # Most implementations use the initialized_internal? method as a
124
+ # proxy for this check. However, since `initialized_internal?`
125
+ # catches a KeyNotFound exception, and that exception can be raised
126
+ # when the server goes away, we have to modify our behavior
127
+ # slightly.
128
+ Diplomat::Kv.get(inited_key, {}, :return, :return)
129
+ true
130
+ rescue
131
+ false
132
+ end
133
+
122
134
  def stop
123
135
  # There's no Consul client instance to dispose of
124
136
  end
@@ -62,6 +62,14 @@ module LaunchDarkly
62
62
  "DynamoDBFeatureStore"
63
63
  end
64
64
 
65
+ def available?
66
+ resp = get_item_by_keys(inited_key, inited_key)
67
+ !resp.item.nil? && resp.item.length > 0
68
+ true
69
+ rescue
70
+ false
71
+ end
72
+
65
73
  def init_internal(all_data)
66
74
  # Start by reading the existing keys; we will later delete any of these that weren't in all_data.
67
75
  unused_old_keys = read_existing_keys(all_data.keys)
@@ -20,8 +20,15 @@ module LaunchDarkly
20
20
  rescue LoadError
21
21
  end
22
22
 
23
- def initialize(feature_store, logger, options={})
24
- @feature_store = feature_store
23
+ #
24
+ # @param data_store [LaunchDarkly::Interfaces::FeatureStore]
25
+ # @param data_source_update_sink [LaunchDarkly::Interfaces::DataSource::UpdateSink, nil] Might be nil for backwards compatibility reasons.
26
+ # @param logger [Logger]
27
+ # @param options [Hash]
28
+ #
29
+ def initialize(data_store, data_source_update_sink, logger, options={})
30
+ @data_store = data_source_update_sink || data_store
31
+ @data_source_update_sink = data_source_update_sink
25
32
  @logger = logger
26
33
  @paths = options[:paths] || []
27
34
  if @paths.is_a? String
@@ -80,10 +87,15 @@ module LaunchDarkly
80
87
  load_file(path, all_data)
81
88
  rescue => exn
82
89
  LaunchDarkly::Util.log_exception(@logger, "Unable to load flag data from \"#{path}\"", exn)
90
+ @data_source_update_sink&.update_status(
91
+ LaunchDarkly::Interfaces::DataSource::Status::INTERRUPTED,
92
+ LaunchDarkly::Interfaces::DataSource::ErrorInfo.new(LaunchDarkly::Interfaces::DataSource::ErrorInfo::INVALID_DATA, 0, exn.to_s, Time.now)
93
+ )
83
94
  return
84
95
  end
85
96
  end
86
- @feature_store.init(all_data)
97
+ @data_store.init(all_data)
98
+ @data_source_update_sink&.update_status(LaunchDarkly::Interfaces::DataSource::Status::VALID, nil)
87
99
  @initialized.make_true
88
100
  end
89
101
 
@@ -42,6 +42,14 @@ module LaunchDarkly
42
42
  @wrapper = LaunchDarkly::Integrations::Util::CachingStoreWrapper.new(core, opts)
43
43
  end
44
44
 
45
+ def monitoring_enabled?
46
+ true
47
+ end
48
+
49
+ def available?
50
+ @wrapper.available?
51
+ end
52
+
45
53
  #
46
54
  # Default value for the `redis_url` constructor parameter; points to an instance of Redis
47
55
  # running at `localhost` with its default port.
@@ -154,6 +162,14 @@ module LaunchDarkly
154
162
  @test_hook = opts[:test_hook] # used for unit tests, deliberately undocumented
155
163
  end
156
164
 
165
+ def available?
166
+ # We don't care what the status is, only that we can connect
167
+ initialized_internal?
168
+ true
169
+ rescue
170
+ false
171
+ end
172
+
157
173
  def description
158
174
  "RedisFeatureStore"
159
175
  end
@@ -16,9 +16,8 @@ module LaunchDarkly
16
16
 
17
17
  def start
18
18
  @worker = Thread.new do
19
- if @start_delay
20
- sleep(@start_delay)
21
- end
19
+ sleep(@start_delay) unless @start_delay.nil? || @start_delay == 0
20
+
22
21
  until @stopped.value do
23
22
  started_at = Time.now
24
23
  begin