launchdarkly-server-sdk 7.1.0 → 7.3.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.
@@ -1,3 +1,4 @@
1
+ require "ldclient-rb/interfaces"
1
2
  require "concurrent/atomics"
2
3
  require "json"
3
4
 
@@ -42,6 +43,14 @@ module LaunchDarkly
42
43
  @wrapper = LaunchDarkly::Integrations::Util::CachingStoreWrapper.new(core, opts)
43
44
  end
44
45
 
46
+ def monitoring_enabled?
47
+ true
48
+ end
49
+
50
+ def available?
51
+ @wrapper.available?
52
+ end
53
+
45
54
  #
46
55
  # Default value for the `redis_url` constructor parameter; points to an instance of Redis
47
56
  # running at `localhost` with its default port.
@@ -109,7 +118,7 @@ module LaunchDarkly
109
118
  @logger = opts[:logger] || Config.default_logger
110
119
  @test_hook = opts[:test_hook] # used for unit tests, deliberately undocumented
111
120
 
112
- @stopped = Concurrent::AtomicBoolean.new()
121
+ @stopped = Concurrent::AtomicBoolean.new
113
122
 
114
123
  with_connection do |redis|
115
124
  @logger.info("#{description}: using Redis instance at #{redis.connection[:host]}:#{redis.connection[:port]} and prefix: #{@prefix}")
@@ -154,6 +163,14 @@ module LaunchDarkly
154
163
  @test_hook = opts[:test_hook] # used for unit tests, deliberately undocumented
155
164
  end
156
165
 
166
+ def available?
167
+ # We don't care what the status is, only that we can connect
168
+ initialized_internal?
169
+ true
170
+ rescue
171
+ false
172
+ end
173
+
157
174
  def description
158
175
  "RedisFeatureStore"
159
176
  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
@@ -1,3 +1,4 @@
1
+ require "concurrent"
1
2
  require "ldclient-rb/interfaces"
2
3
  require "ldclient-rb/impl/store_data_set_sorter"
3
4
 
@@ -5,34 +6,45 @@ module LaunchDarkly
5
6
  module Impl
6
7
  #
7
8
  # Provides additional behavior that the client requires before or after feature store operations.
8
- # Currently this just means sorting the data set for init(). In the future we may also use this
9
- # to provide an update listener capability.
9
+ # This just means sorting the data set for init() and dealing with data store status listeners.
10
10
  #
11
11
  class FeatureStoreClientWrapper
12
12
  include Interfaces::FeatureStore
13
13
 
14
- def initialize(store)
14
+ def initialize(store, store_update_sink, logger)
15
+ # @type [LaunchDarkly::Interfaces::FeatureStore]
15
16
  @store = store
17
+
18
+ @monitoring_enabled = does_store_support_monitoring?
19
+
20
+ # @type [LaunchDarkly::Impl::DataStore::UpdateSink]
21
+ @store_update_sink = store_update_sink
22
+ @logger = logger
23
+
24
+ @mutex = Mutex.new # Covers the following variables
25
+ @last_available = true
26
+ # @type [LaunchDarkly::Impl::RepeatingTask, nil]
27
+ @poller = nil
16
28
  end
17
29
 
18
30
  def init(all_data)
19
- @store.init(FeatureStoreDataSetSorter.sort_all_collections(all_data))
31
+ wrapper { @store.init(FeatureStoreDataSetSorter.sort_all_collections(all_data)) }
20
32
  end
21
33
 
22
34
  def get(kind, key)
23
- @store.get(kind, key)
35
+ wrapper { @store.get(kind, key) }
24
36
  end
25
37
 
26
38
  def all(kind)
27
- @store.all(kind)
39
+ wrapper { @store.all(kind) }
28
40
  end
29
41
 
30
42
  def upsert(kind, item)
31
- @store.upsert(kind, item)
43
+ wrapper { @store.upsert(kind, item) }
32
44
  end
33
45
 
34
46
  def delete(kind, key, version)
35
- @store.delete(kind, key, version)
47
+ wrapper { @store.delete(kind, key, version) }
36
48
  end
37
49
 
38
50
  def initialized?
@@ -41,6 +53,88 @@ module LaunchDarkly
41
53
 
42
54
  def stop
43
55
  @store.stop
56
+ @mutex.synchronize do
57
+ return if @poller.nil?
58
+
59
+ @poller.stop
60
+ @poller = nil
61
+ end
62
+ end
63
+
64
+ def monitoring_enabled?
65
+ @monitoring_enabled
66
+ end
67
+
68
+ private def wrapper()
69
+ begin
70
+ yield
71
+ rescue => e
72
+ update_availability(false) if @monitoring_enabled
73
+ raise
74
+ end
75
+ end
76
+
77
+ private def update_availability(available)
78
+ @mutex.synchronize do
79
+ return if available == @last_available
80
+ @last_available = available
81
+ end
82
+
83
+ status = LaunchDarkly::Interfaces::DataStore::Status.new(available, false)
84
+
85
+ @logger.warn("Persistent store is available again") if available
86
+
87
+ @store_update_sink.update_status(status)
88
+
89
+ if available
90
+ @mutex.synchronize do
91
+ return if @poller.nil?
92
+
93
+ @poller.stop
94
+ @poller = nil
95
+ end
96
+
97
+ return
98
+ end
99
+
100
+ @logger.warn("Detected persistent store unavailability; updates will be cached until it recovers.")
101
+
102
+ task = Impl::RepeatingTask.new(0.5, 0, -> { self.check_availability }, @logger)
103
+
104
+ @mutex.synchronize do
105
+ @poller = task
106
+ @poller.start
107
+ end
108
+ end
109
+
110
+ private def check_availability
111
+ begin
112
+ update_availability(true) if @store.available?
113
+ rescue => e
114
+ @logger.error("Unexpected error from data store status function: #{e}")
115
+ end
116
+ end
117
+
118
+ # This methods determines whether the wrapped store can support enabling monitoring.
119
+ #
120
+ # The wrapped store must provide a monitoring_enabled method, which must
121
+ # be true. But this alone is not sufficient.
122
+ #
123
+ # Because this class wraps all interactions with a provided store, it can
124
+ # technically "monitor" any store. However, monitoring also requires that
125
+ # we notify listeners when the store is available again.
126
+ #
127
+ # We determine this by checking the store's `available?` method, so this
128
+ # is also a requirement for monitoring support.
129
+ #
130
+ # These extra checks won't be necessary once `available` becomes a part
131
+ # of the core interface requirements and this class no longer wraps every
132
+ # feature store.
133
+ private def does_store_support_monitoring?
134
+ return false unless @store.respond_to? :monitoring_enabled?
135
+ return false unless @store.respond_to? :available?
136
+
137
+ @store.monitoring_enabled?
44
138
  end
45
139
  end
46
140
  end
@@ -23,6 +23,9 @@ module LaunchDarkly
23
23
  priority: 0,
24
24
  }.freeze
25
25
 
26
+ # @private
27
+ ALL_KINDS = [FEATURES, SEGMENTS].freeze
28
+
26
29
  #
27
30
  # Default implementation of the LaunchDarkly client's feature store, using an in-memory
28
31
  # cache. This object holds feature flags and related data received from LaunchDarkly.
@@ -37,6 +40,10 @@ module LaunchDarkly
37
40
  @initialized = Concurrent::AtomicBoolean.new(false)
38
41
  end
39
42
 
43
+ def monitoring_enabled?
44
+ false
45
+ end
46
+
40
47
  def get(kind, key)
41
48
  @lock.with_read_lock do
42
49
  coll = @items[kind]
@@ -101,7 +101,7 @@ module LaunchDarkly
101
101
  #
102
102
  def self.data_source(options={})
103
103
  lambda { |sdk_key, config|
104
- Impl::Integrations::FileDataSourceImpl.new(config.feature_store, config.logger, options) }
104
+ Impl::Integrations::FileDataSourceImpl.new(config.feature_store, config.data_source_update_sink, config.logger, options) }
105
105
  end
106
106
  end
107
107
  end
@@ -90,7 +90,7 @@ module LaunchDarkly
90
90
  #
91
91
  def flag(key)
92
92
  existing_builder = @lock.with_read_lock { @flag_builders[key] }
93
- if existing_builder.nil? then
93
+ if existing_builder.nil?
94
94
  FlagBuilder.new(key).boolean_flag
95
95
  else
96
96
  existing_builder.clone
@@ -118,7 +118,7 @@ module LaunchDarkly
118
118
  @flag_builders[flag_builder.key] = flag_builder
119
119
  version = 0
120
120
  flag_key = flag_builder.key.to_sym
121
- if @current_flags[flag_key] then
121
+ if @current_flags[flag_key]
122
122
  version = @current_flags[flag_key][:version]
123
123
  end
124
124
  new_flag = Impl::Model.deserialize(FEATURES, flag_builder.build(version+1))
@@ -175,7 +175,7 @@ module LaunchDarkly
175
175
  key = item.key.to_sym
176
176
  @lock.with_write_lock do
177
177
  old_item = current[key]
178
- unless old_item.nil? then
178
+ unless old_item.nil?
179
179
  data = item.as_json
180
180
  data[:version] = old_item.version + 1
181
181
  item = Impl::Model.deserialize(kind, data)
@@ -43,6 +43,17 @@ module LaunchDarkly
43
43
  end
44
44
 
45
45
  @inited = Concurrent::AtomicBoolean.new(false)
46
+ @has_available_method = @core.respond_to? :available?
47
+ end
48
+
49
+ def monitoring_enabled?
50
+ @has_available_method
51
+ end
52
+
53
+ def available?
54
+ return false unless @has_available_method
55
+
56
+ @core.available?
46
57
  end
47
58
 
48
59
  def init(all_data)