launchdarkly-server-sdk 5.5.7
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 +7 -0
- data/.circleci/config.yml +134 -0
- data/.github/ISSUE_TEMPLATE/bug_report.md +37 -0
- data/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
- data/.gitignore +15 -0
- data/.hound.yml +2 -0
- data/.rspec +2 -0
- data/.rubocop.yml +600 -0
- data/.simplecov +4 -0
- data/.yardopts +9 -0
- data/CHANGELOG.md +261 -0
- data/CODEOWNERS +1 -0
- data/CONTRIBUTING.md +37 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +102 -0
- data/LICENSE.txt +13 -0
- data/README.md +56 -0
- data/Rakefile +5 -0
- data/azure-pipelines.yml +51 -0
- data/ext/mkrf_conf.rb +11 -0
- data/launchdarkly-server-sdk.gemspec +40 -0
- data/lib/ldclient-rb.rb +29 -0
- data/lib/ldclient-rb/cache_store.rb +45 -0
- data/lib/ldclient-rb/config.rb +411 -0
- data/lib/ldclient-rb/evaluation.rb +455 -0
- data/lib/ldclient-rb/event_summarizer.rb +55 -0
- data/lib/ldclient-rb/events.rb +468 -0
- data/lib/ldclient-rb/expiring_cache.rb +77 -0
- data/lib/ldclient-rb/file_data_source.rb +312 -0
- data/lib/ldclient-rb/flags_state.rb +76 -0
- data/lib/ldclient-rb/impl.rb +13 -0
- data/lib/ldclient-rb/impl/integrations/consul_impl.rb +158 -0
- data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +228 -0
- data/lib/ldclient-rb/impl/integrations/redis_impl.rb +155 -0
- data/lib/ldclient-rb/impl/store_client_wrapper.rb +47 -0
- data/lib/ldclient-rb/impl/store_data_set_sorter.rb +55 -0
- data/lib/ldclient-rb/in_memory_store.rb +100 -0
- data/lib/ldclient-rb/integrations.rb +55 -0
- data/lib/ldclient-rb/integrations/consul.rb +38 -0
- data/lib/ldclient-rb/integrations/dynamodb.rb +47 -0
- data/lib/ldclient-rb/integrations/redis.rb +55 -0
- data/lib/ldclient-rb/integrations/util/store_wrapper.rb +230 -0
- data/lib/ldclient-rb/interfaces.rb +153 -0
- data/lib/ldclient-rb/ldclient.rb +424 -0
- data/lib/ldclient-rb/memoized_value.rb +32 -0
- data/lib/ldclient-rb/newrelic.rb +17 -0
- data/lib/ldclient-rb/non_blocking_thread_pool.rb +46 -0
- data/lib/ldclient-rb/polling.rb +78 -0
- data/lib/ldclient-rb/redis_store.rb +87 -0
- data/lib/ldclient-rb/requestor.rb +101 -0
- data/lib/ldclient-rb/simple_lru_cache.rb +25 -0
- data/lib/ldclient-rb/stream.rb +141 -0
- data/lib/ldclient-rb/user_filter.rb +51 -0
- data/lib/ldclient-rb/util.rb +50 -0
- data/lib/ldclient-rb/version.rb +3 -0
- data/scripts/gendocs.sh +11 -0
- data/scripts/release.sh +27 -0
- data/spec/config_spec.rb +63 -0
- data/spec/evaluation_spec.rb +739 -0
- data/spec/event_summarizer_spec.rb +63 -0
- data/spec/events_spec.rb +642 -0
- data/spec/expiring_cache_spec.rb +76 -0
- data/spec/feature_store_spec_base.rb +213 -0
- data/spec/file_data_source_spec.rb +255 -0
- data/spec/fixtures/feature.json +37 -0
- data/spec/fixtures/feature1.json +36 -0
- data/spec/fixtures/user.json +9 -0
- data/spec/flags_state_spec.rb +81 -0
- data/spec/http_util.rb +109 -0
- data/spec/in_memory_feature_store_spec.rb +12 -0
- data/spec/integrations/consul_feature_store_spec.rb +42 -0
- data/spec/integrations/dynamodb_feature_store_spec.rb +105 -0
- data/spec/integrations/store_wrapper_spec.rb +276 -0
- data/spec/ldclient_spec.rb +471 -0
- data/spec/newrelic_spec.rb +5 -0
- data/spec/polling_spec.rb +120 -0
- data/spec/redis_feature_store_spec.rb +95 -0
- data/spec/requestor_spec.rb +214 -0
- data/spec/segment_store_spec_base.rb +95 -0
- data/spec/simple_lru_cache_spec.rb +24 -0
- data/spec/spec_helper.rb +9 -0
- data/spec/store_spec.rb +10 -0
- data/spec/stream_spec.rb +60 -0
- data/spec/user_filter_spec.rb +91 -0
- data/spec/util_spec.rb +17 -0
- data/spec/version_spec.rb +7 -0
- metadata +375 -0
@@ -0,0 +1,32 @@
|
|
1
|
+
|
2
|
+
module LaunchDarkly
|
3
|
+
# Simple implementation of a thread-safe memoized value whose generator function will never be
|
4
|
+
# run more than once, and whose value can be overridden by explicit assignment.
|
5
|
+
# Note that we no longer use this class and it will be removed in a future version.
|
6
|
+
# @private
|
7
|
+
class MemoizedValue
|
8
|
+
def initialize(&generator)
|
9
|
+
@generator = generator
|
10
|
+
@mutex = Mutex.new
|
11
|
+
@inited = false
|
12
|
+
@value = nil
|
13
|
+
end
|
14
|
+
|
15
|
+
def get
|
16
|
+
@mutex.synchronize do
|
17
|
+
if !@inited
|
18
|
+
@value = @generator.call
|
19
|
+
@inited = true
|
20
|
+
end
|
21
|
+
end
|
22
|
+
@value
|
23
|
+
end
|
24
|
+
|
25
|
+
def set(value)
|
26
|
+
@mutex.synchronize do
|
27
|
+
@value = value
|
28
|
+
@inited = true
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module LaunchDarkly
|
2
|
+
# @private
|
3
|
+
class LDNewRelic
|
4
|
+
begin
|
5
|
+
require "newrelic_rpm"
|
6
|
+
NR_ENABLED = defined?(::NewRelic::Agent.add_custom_parameters)
|
7
|
+
rescue ScriptError, StandardError
|
8
|
+
NR_ENABLED = false
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.annotate_transaction(key, value)
|
12
|
+
if NR_ENABLED
|
13
|
+
::NewRelic::Agent.add_custom_parameters(key.to_s => value.to_s)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require "concurrent"
|
2
|
+
require "concurrent/atomics"
|
3
|
+
require "concurrent/executors"
|
4
|
+
require "thread"
|
5
|
+
|
6
|
+
module LaunchDarkly
|
7
|
+
# Simple wrapper for a FixedThreadPool that rejects new jobs if all the threads are busy, rather
|
8
|
+
# than blocking. Also provides a way to wait for all jobs to finish without shutting down.
|
9
|
+
# @private
|
10
|
+
class NonBlockingThreadPool
|
11
|
+
def initialize(capacity)
|
12
|
+
@capacity = capacity
|
13
|
+
@pool = Concurrent::FixedThreadPool.new(capacity)
|
14
|
+
@semaphore = Concurrent::Semaphore.new(capacity)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Attempts to submit a job, but only if a worker is available. Unlike the regular post method,
|
18
|
+
# this returns a value: true if the job was submitted, false if all workers are busy.
|
19
|
+
def post
|
20
|
+
if !@semaphore.try_acquire(1)
|
21
|
+
return
|
22
|
+
end
|
23
|
+
@pool.post do
|
24
|
+
begin
|
25
|
+
yield
|
26
|
+
ensure
|
27
|
+
@semaphore.release(1)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Waits until no jobs are executing, without shutting down the pool.
|
33
|
+
def wait_all
|
34
|
+
@semaphore.acquire(@capacity)
|
35
|
+
@semaphore.release(@capacity)
|
36
|
+
end
|
37
|
+
|
38
|
+
def shutdown
|
39
|
+
@pool.shutdown
|
40
|
+
end
|
41
|
+
|
42
|
+
def wait_for_termination
|
43
|
+
@pool.wait_for_termination
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require "concurrent/atomics"
|
2
|
+
require "thread"
|
3
|
+
|
4
|
+
module LaunchDarkly
|
5
|
+
# @private
|
6
|
+
class PollingProcessor
|
7
|
+
def initialize(config, requestor)
|
8
|
+
@config = config
|
9
|
+
@requestor = requestor
|
10
|
+
@initialized = Concurrent::AtomicBoolean.new(false)
|
11
|
+
@started = Concurrent::AtomicBoolean.new(false)
|
12
|
+
@stopped = Concurrent::AtomicBoolean.new(false)
|
13
|
+
@ready = Concurrent::Event.new
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialized?
|
17
|
+
@initialized.value
|
18
|
+
end
|
19
|
+
|
20
|
+
def start
|
21
|
+
return @ready unless @started.make_true
|
22
|
+
@config.logger.info { "[LDClient] Initializing polling connection" }
|
23
|
+
create_worker
|
24
|
+
@ready
|
25
|
+
end
|
26
|
+
|
27
|
+
def stop
|
28
|
+
if @stopped.make_true
|
29
|
+
if @worker && @worker.alive? && @worker != Thread.current
|
30
|
+
@worker.run # causes the thread to wake up if it's currently in a sleep
|
31
|
+
@worker.join
|
32
|
+
end
|
33
|
+
@config.logger.info { "[LDClient] Polling connection stopped" }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def poll
|
38
|
+
all_data = @requestor.request_all_data
|
39
|
+
if all_data
|
40
|
+
@config.feature_store.init({
|
41
|
+
FEATURES => all_data[:flags],
|
42
|
+
SEGMENTS => all_data[:segments]
|
43
|
+
})
|
44
|
+
if @initialized.make_true
|
45
|
+
@config.logger.info { "[LDClient] Polling connection initialized" }
|
46
|
+
@ready.set
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def create_worker
|
52
|
+
@worker = Thread.new do
|
53
|
+
@config.logger.debug { "[LDClient] Starting polling worker" }
|
54
|
+
while !@stopped.value do
|
55
|
+
started_at = Time.now
|
56
|
+
begin
|
57
|
+
poll
|
58
|
+
rescue UnexpectedResponseError => e
|
59
|
+
message = Util.http_error_message(e.status, "polling request", "will retry")
|
60
|
+
@config.logger.error { "[LDClient] #{message}" };
|
61
|
+
if !Util.http_error_recoverable?(e.status)
|
62
|
+
@ready.set # if client was waiting on us, make it stop waiting - has no effect if already set
|
63
|
+
stop
|
64
|
+
end
|
65
|
+
rescue StandardError => exn
|
66
|
+
Util.log_exception(@config.logger, "Exception while polling", exn)
|
67
|
+
end
|
68
|
+
delta = @config.poll_interval - (Time.now - started_at)
|
69
|
+
if delta > 0
|
70
|
+
sleep(delta)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
private :poll, :create_worker
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require "ldclient-rb/interfaces"
|
2
|
+
require "ldclient-rb/impl/integrations/redis_impl"
|
3
|
+
|
4
|
+
module LaunchDarkly
|
5
|
+
#
|
6
|
+
# An implementation of the LaunchDarkly client's feature store that uses a Redis
|
7
|
+
# instance. This object holds feature flags and related data received from the
|
8
|
+
# streaming API. Feature data can also be further cached in memory to reduce overhead
|
9
|
+
# of calls to Redis.
|
10
|
+
#
|
11
|
+
# To use this class, you must first have the `redis` and `connection-pool` gems
|
12
|
+
# installed. Then, create an instance and store it in the `feature_store` property
|
13
|
+
# of your client configuration.
|
14
|
+
#
|
15
|
+
# @deprecated Use the factory method in {LaunchDarkly::Integrations::Redis} instead. This specific
|
16
|
+
# implementation class may be changed or removed in the future.
|
17
|
+
#
|
18
|
+
class RedisFeatureStore
|
19
|
+
include LaunchDarkly::Interfaces::FeatureStore
|
20
|
+
|
21
|
+
# Note that this class is now just a facade around CachingStoreWrapper, which is in turn delegating
|
22
|
+
# to RedisFeatureStoreCore where the actual database logic is. This class was retained for historical
|
23
|
+
# reasons, so that existing code can still call RedisFeatureStore.new. In the future, we will migrate
|
24
|
+
# away from exposing these concrete classes and use factory methods instead.
|
25
|
+
|
26
|
+
#
|
27
|
+
# Constructor for a RedisFeatureStore instance.
|
28
|
+
#
|
29
|
+
# @param opts [Hash] the configuration options
|
30
|
+
# @option opts [String] :redis_url URL of the Redis instance (shortcut for omitting redis_opts)
|
31
|
+
# @option opts [Hash] :redis_opts options to pass to the Redis constructor (if you want to specify more than just redis_url)
|
32
|
+
# @option opts [String] :prefix namespace prefix to add to all hash keys used by LaunchDarkly
|
33
|
+
# @option opts [Logger] :logger a `Logger` instance; defaults to `Config.default_logger`
|
34
|
+
# @option opts [Integer] :max_connections size of the Redis connection pool
|
35
|
+
# @option opts [Integer] :expiration expiration time for the in-memory cache, in seconds; 0 for no local caching
|
36
|
+
# @option opts [Integer] :capacity maximum number of feature flags (or related objects) to cache locally
|
37
|
+
# @option opts [Object] :pool custom connection pool, if desired
|
38
|
+
#
|
39
|
+
def initialize(opts = {})
|
40
|
+
core = LaunchDarkly::Impl::Integrations::Redis::RedisFeatureStoreCore.new(opts)
|
41
|
+
@wrapper = LaunchDarkly::Integrations::Util::CachingStoreWrapper.new(core, opts)
|
42
|
+
end
|
43
|
+
|
44
|
+
#
|
45
|
+
# Default value for the `redis_url` constructor parameter; points to an instance of Redis
|
46
|
+
# running at `localhost` with its default port.
|
47
|
+
#
|
48
|
+
def self.default_redis_url
|
49
|
+
LaunchDarkly::Integrations::Redis::default_redis_url
|
50
|
+
end
|
51
|
+
|
52
|
+
#
|
53
|
+
# Default value for the `prefix` constructor parameter.
|
54
|
+
#
|
55
|
+
def self.default_prefix
|
56
|
+
LaunchDarkly::Integrations::Redis::default_prefix
|
57
|
+
end
|
58
|
+
|
59
|
+
def get(kind, key)
|
60
|
+
@wrapper.get(kind, key)
|
61
|
+
end
|
62
|
+
|
63
|
+
def all(kind)
|
64
|
+
@wrapper.all(kind)
|
65
|
+
end
|
66
|
+
|
67
|
+
def delete(kind, key, version)
|
68
|
+
@wrapper.delete(kind, key, version)
|
69
|
+
end
|
70
|
+
|
71
|
+
def init(all_data)
|
72
|
+
@wrapper.init(all_data)
|
73
|
+
end
|
74
|
+
|
75
|
+
def upsert(kind, item)
|
76
|
+
@wrapper.upsert(kind, item)
|
77
|
+
end
|
78
|
+
|
79
|
+
def initialized?
|
80
|
+
@wrapper.initialized?
|
81
|
+
end
|
82
|
+
|
83
|
+
def stop
|
84
|
+
@wrapper.stop
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require "concurrent/atomics"
|
2
|
+
require "json"
|
3
|
+
require "uri"
|
4
|
+
|
5
|
+
module LaunchDarkly
|
6
|
+
# @private
|
7
|
+
class UnexpectedResponseError < StandardError
|
8
|
+
def initialize(status)
|
9
|
+
@status = status
|
10
|
+
super("HTTP error #{status}")
|
11
|
+
end
|
12
|
+
|
13
|
+
def status
|
14
|
+
@status
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# @private
|
19
|
+
class Requestor
|
20
|
+
CacheEntry = Struct.new(:etag, :body)
|
21
|
+
|
22
|
+
def initialize(sdk_key, config)
|
23
|
+
@sdk_key = sdk_key
|
24
|
+
@config = config
|
25
|
+
@client = Util.new_http_client(@config.base_uri, @config)
|
26
|
+
@cache = @config.cache_store
|
27
|
+
end
|
28
|
+
|
29
|
+
def request_flag(key)
|
30
|
+
make_request("/sdk/latest-flags/" + key)
|
31
|
+
end
|
32
|
+
|
33
|
+
def request_segment(key)
|
34
|
+
make_request("/sdk/latest-segments/" + key)
|
35
|
+
end
|
36
|
+
|
37
|
+
def request_all_data()
|
38
|
+
make_request("/sdk/latest-all")
|
39
|
+
end
|
40
|
+
|
41
|
+
def stop
|
42
|
+
begin
|
43
|
+
@client.finish
|
44
|
+
rescue
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def make_request(path)
|
51
|
+
@client.start if !@client.started?
|
52
|
+
uri = URI(@config.base_uri + path)
|
53
|
+
req = Net::HTTP::Get.new(uri)
|
54
|
+
req["Authorization"] = @sdk_key
|
55
|
+
req["User-Agent"] = "RubyClient/" + LaunchDarkly::VERSION
|
56
|
+
req["Connection"] = "keep-alive"
|
57
|
+
cached = @cache.read(uri)
|
58
|
+
if !cached.nil?
|
59
|
+
req["If-None-Match"] = cached.etag
|
60
|
+
end
|
61
|
+
res = @client.request(req)
|
62
|
+
status = res.code.to_i
|
63
|
+
@config.logger.debug { "[LDClient] Got response from uri: #{uri}\n\tstatus code: #{status}\n\theaders: #{res.to_hash}\n\tbody: #{res.body}" }
|
64
|
+
|
65
|
+
if status == 304 && !cached.nil?
|
66
|
+
body = cached.body
|
67
|
+
else
|
68
|
+
@cache.delete(uri)
|
69
|
+
if status < 200 || status >= 300
|
70
|
+
raise UnexpectedResponseError.new(status)
|
71
|
+
end
|
72
|
+
body = fix_encoding(res.body, res["content-type"])
|
73
|
+
etag = res["etag"]
|
74
|
+
@cache.write(uri, CacheEntry.new(etag, body)) if !etag.nil?
|
75
|
+
end
|
76
|
+
JSON.parse(body, symbolize_names: true)
|
77
|
+
end
|
78
|
+
|
79
|
+
def fix_encoding(body, content_type)
|
80
|
+
return body if content_type.nil?
|
81
|
+
media_type, charset = parse_content_type(content_type)
|
82
|
+
return body if charset.nil?
|
83
|
+
body.force_encoding(Encoding::find(charset)).encode(Encoding::UTF_8)
|
84
|
+
end
|
85
|
+
|
86
|
+
def parse_content_type(value)
|
87
|
+
return [nil, nil] if value.nil? || value == ''
|
88
|
+
parts = value.split(/; */)
|
89
|
+
return [value, nil] if parts.count < 2
|
90
|
+
charset = nil
|
91
|
+
parts.each do |part|
|
92
|
+
fields = part.split('=')
|
93
|
+
if fields.count >= 2 && fields[0] == 'charset'
|
94
|
+
charset = fields[1]
|
95
|
+
break
|
96
|
+
end
|
97
|
+
end
|
98
|
+
return [parts[0], charset]
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
|
2
|
+
module LaunchDarkly
|
3
|
+
# A non-thread-safe implementation of a LRU cache set with only add and reset methods.
|
4
|
+
# Based on https://github.com/SamSaffron/lru_redux/blob/master/lib/lru_redux/cache.rb
|
5
|
+
# @private
|
6
|
+
class SimpleLRUCacheSet
|
7
|
+
def initialize(capacity)
|
8
|
+
@values = {}
|
9
|
+
@capacity = capacity
|
10
|
+
end
|
11
|
+
|
12
|
+
# Adds a value to the cache or marks it recent if it was already there. Returns true if already there.
|
13
|
+
def add(value)
|
14
|
+
found = true
|
15
|
+
@values.delete(value) { found = false }
|
16
|
+
@values[value] = true
|
17
|
+
@values.shift if @values.length > @capacity
|
18
|
+
found
|
19
|
+
end
|
20
|
+
|
21
|
+
def clear
|
22
|
+
@values = {}
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
require "concurrent/atomics"
|
2
|
+
require "json"
|
3
|
+
require "ld-eventsource"
|
4
|
+
|
5
|
+
module LaunchDarkly
|
6
|
+
# @private
|
7
|
+
PUT = :put
|
8
|
+
# @private
|
9
|
+
PATCH = :patch
|
10
|
+
# @private
|
11
|
+
DELETE = :delete
|
12
|
+
# @private
|
13
|
+
INDIRECT_PUT = :'indirect/put'
|
14
|
+
# @private
|
15
|
+
INDIRECT_PATCH = :'indirect/patch'
|
16
|
+
# @private
|
17
|
+
READ_TIMEOUT_SECONDS = 300 # 5 minutes; the stream should send a ping every 3 minutes
|
18
|
+
|
19
|
+
# @private
|
20
|
+
KEY_PATHS = {
|
21
|
+
FEATURES => "/flags/",
|
22
|
+
SEGMENTS => "/segments/"
|
23
|
+
}
|
24
|
+
|
25
|
+
# @private
|
26
|
+
class StreamProcessor
|
27
|
+
def initialize(sdk_key, config, requestor)
|
28
|
+
@sdk_key = sdk_key
|
29
|
+
@config = config
|
30
|
+
@feature_store = config.feature_store
|
31
|
+
@requestor = requestor
|
32
|
+
@initialized = Concurrent::AtomicBoolean.new(false)
|
33
|
+
@started = Concurrent::AtomicBoolean.new(false)
|
34
|
+
@stopped = Concurrent::AtomicBoolean.new(false)
|
35
|
+
@ready = Concurrent::Event.new
|
36
|
+
end
|
37
|
+
|
38
|
+
def initialized?
|
39
|
+
@initialized.value
|
40
|
+
end
|
41
|
+
|
42
|
+
def start
|
43
|
+
return @ready unless @started.make_true
|
44
|
+
|
45
|
+
@config.logger.info { "[LDClient] Initializing stream connection" }
|
46
|
+
|
47
|
+
headers = {
|
48
|
+
'Authorization' => @sdk_key,
|
49
|
+
'User-Agent' => 'RubyClient/' + LaunchDarkly::VERSION
|
50
|
+
}
|
51
|
+
opts = {
|
52
|
+
headers: headers,
|
53
|
+
read_timeout: READ_TIMEOUT_SECONDS,
|
54
|
+
logger: @config.logger
|
55
|
+
}
|
56
|
+
@es = SSE::Client.new(@config.stream_uri + "/all", **opts) do |conn|
|
57
|
+
conn.on_event { |event| process_message(event) }
|
58
|
+
conn.on_error { |err|
|
59
|
+
case err
|
60
|
+
when SSE::Errors::HTTPStatusError
|
61
|
+
status = err.status
|
62
|
+
message = Util.http_error_message(status, "streaming connection", "will retry")
|
63
|
+
@config.logger.error { "[LDClient] #{message}" }
|
64
|
+
if !Util.http_error_recoverable?(status)
|
65
|
+
@ready.set # if client was waiting on us, make it stop waiting - has no effect if already set
|
66
|
+
stop
|
67
|
+
end
|
68
|
+
end
|
69
|
+
}
|
70
|
+
end
|
71
|
+
|
72
|
+
@ready
|
73
|
+
end
|
74
|
+
|
75
|
+
def stop
|
76
|
+
if @stopped.make_true
|
77
|
+
@es.close
|
78
|
+
@config.logger.info { "[LDClient] Stream connection stopped" }
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def process_message(message)
|
85
|
+
method = message.type
|
86
|
+
@config.logger.debug { "[LDClient] Stream received #{method} message: #{message.data}" }
|
87
|
+
if method == PUT
|
88
|
+
message = JSON.parse(message.data, symbolize_names: true)
|
89
|
+
@feature_store.init({
|
90
|
+
FEATURES => message[:data][:flags],
|
91
|
+
SEGMENTS => message[:data][:segments]
|
92
|
+
})
|
93
|
+
@initialized.make_true
|
94
|
+
@config.logger.info { "[LDClient] Stream initialized" }
|
95
|
+
@ready.set
|
96
|
+
elsif method == PATCH
|
97
|
+
data = JSON.parse(message.data, symbolize_names: true)
|
98
|
+
for kind in [FEATURES, SEGMENTS]
|
99
|
+
key = key_for_path(kind, data[:path])
|
100
|
+
if key
|
101
|
+
@feature_store.upsert(kind, data[:data])
|
102
|
+
break
|
103
|
+
end
|
104
|
+
end
|
105
|
+
elsif method == DELETE
|
106
|
+
data = JSON.parse(message.data, symbolize_names: true)
|
107
|
+
for kind in [FEATURES, SEGMENTS]
|
108
|
+
key = key_for_path(kind, data[:path])
|
109
|
+
if key
|
110
|
+
@feature_store.delete(kind, key, data[:version])
|
111
|
+
break
|
112
|
+
end
|
113
|
+
end
|
114
|
+
elsif method == INDIRECT_PUT
|
115
|
+
all_data = @requestor.request_all_data
|
116
|
+
@feature_store.init({
|
117
|
+
FEATURES => all_data[:flags],
|
118
|
+
SEGMENTS => all_data[:segments]
|
119
|
+
})
|
120
|
+
@initialized.make_true
|
121
|
+
@config.logger.info { "[LDClient] Stream initialized (via indirect message)" }
|
122
|
+
elsif method == INDIRECT_PATCH
|
123
|
+
key = key_for_path(FEATURES, message.data)
|
124
|
+
if key
|
125
|
+
@feature_store.upsert(FEATURES, @requestor.request_flag(key))
|
126
|
+
else
|
127
|
+
key = key_for_path(SEGMENTS, message.data)
|
128
|
+
if key
|
129
|
+
@feature_store.upsert(SEGMENTS, @requestor.request_segment(key))
|
130
|
+
end
|
131
|
+
end
|
132
|
+
else
|
133
|
+
@config.logger.warn { "[LDClient] Unknown message received: #{method}" }
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def key_for_path(kind, path)
|
138
|
+
path.start_with?(KEY_PATHS[kind]) ? path[KEY_PATHS[kind].length..-1] : nil
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|