launchdarkly-server-sdk 7.1.0 → 7.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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)