launchdarkly-server-sdk 5.5.7
Sign up to get free protection for your applications and to get access to all the features.
- 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,158 @@
|
|
1
|
+
require "json"
|
2
|
+
|
3
|
+
module LaunchDarkly
|
4
|
+
module Impl
|
5
|
+
module Integrations
|
6
|
+
module Consul
|
7
|
+
#
|
8
|
+
# Internal implementation of the Consul feature store, intended to be used with CachingStoreWrapper.
|
9
|
+
#
|
10
|
+
class ConsulFeatureStoreCore
|
11
|
+
begin
|
12
|
+
require "diplomat"
|
13
|
+
CONSUL_ENABLED = true
|
14
|
+
rescue ScriptError, StandardError
|
15
|
+
CONSUL_ENABLED = false
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize(opts)
|
19
|
+
if !CONSUL_ENABLED
|
20
|
+
raise RuntimeError.new("can't use Consul feature store without the 'diplomat' gem")
|
21
|
+
end
|
22
|
+
|
23
|
+
@prefix = (opts[:prefix] || LaunchDarkly::Integrations::Consul.default_prefix) + '/'
|
24
|
+
@logger = opts[:logger] || Config.default_logger
|
25
|
+
Diplomat.configuration = opts[:consul_config] if !opts[:consul_config].nil?
|
26
|
+
Diplomat.configuration.url = opts[:url] if !opts[:url].nil?
|
27
|
+
@logger.info("ConsulFeatureStore: using Consul host at #{Diplomat.configuration.url}")
|
28
|
+
end
|
29
|
+
|
30
|
+
def init_internal(all_data)
|
31
|
+
# Start by reading the existing keys; we will later delete any of these that weren't in all_data.
|
32
|
+
unused_old_keys = Set.new
|
33
|
+
keys = Diplomat::Kv.get(@prefix, { keys: true, recurse: true }, :return)
|
34
|
+
unused_old_keys.merge(keys) if keys != ""
|
35
|
+
|
36
|
+
ops = []
|
37
|
+
num_items = 0
|
38
|
+
|
39
|
+
# Insert or update every provided item
|
40
|
+
all_data.each do |kind, items|
|
41
|
+
items.values.each do |item|
|
42
|
+
value = item.to_json
|
43
|
+
key = item_key(kind, item[:key])
|
44
|
+
ops.push({ 'KV' => { 'Verb' => 'set', 'Key' => key, 'Value' => value } })
|
45
|
+
unused_old_keys.delete(key)
|
46
|
+
num_items = num_items + 1
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Now delete any previously existing items whose keys were not in the current data
|
51
|
+
unused_old_keys.each do |key|
|
52
|
+
ops.push({ 'KV' => { 'Verb' => 'delete', 'Key' => key } })
|
53
|
+
end
|
54
|
+
|
55
|
+
# Now set the special key that we check in initialized_internal?
|
56
|
+
ops.push({ 'KV' => { 'Verb' => 'set', 'Key' => inited_key, 'Value' => '' } })
|
57
|
+
|
58
|
+
ConsulUtil.batch_operations(ops)
|
59
|
+
|
60
|
+
@logger.info { "Initialized database with #{num_items} items" }
|
61
|
+
end
|
62
|
+
|
63
|
+
def get_internal(kind, key)
|
64
|
+
value = Diplomat::Kv.get(item_key(kind, key), {}, :return) # :return means "don't throw an error if not found"
|
65
|
+
(value.nil? || value == "") ? nil : JSON.parse(value, symbolize_names: true)
|
66
|
+
end
|
67
|
+
|
68
|
+
def get_all_internal(kind)
|
69
|
+
items_out = {}
|
70
|
+
results = Diplomat::Kv.get(kind_key(kind), { recurse: true }, :return)
|
71
|
+
(results == "" ? [] : results).each do |result|
|
72
|
+
value = result[:value]
|
73
|
+
if !value.nil?
|
74
|
+
item = JSON.parse(value, symbolize_names: true)
|
75
|
+
items_out[item[:key].to_sym] = item
|
76
|
+
end
|
77
|
+
end
|
78
|
+
items_out
|
79
|
+
end
|
80
|
+
|
81
|
+
def upsert_internal(kind, new_item)
|
82
|
+
key = item_key(kind, new_item[:key])
|
83
|
+
json = new_item.to_json
|
84
|
+
|
85
|
+
# We will potentially keep retrying indefinitely until someone's write succeeds
|
86
|
+
while true
|
87
|
+
old_value = Diplomat::Kv.get(key, { decode_values: true }, :return)
|
88
|
+
if old_value.nil? || old_value == ""
|
89
|
+
mod_index = 0
|
90
|
+
else
|
91
|
+
old_item = JSON.parse(old_value[0]["Value"], symbolize_names: true)
|
92
|
+
# Check whether the item is stale. If so, don't do the update (and return the existing item to
|
93
|
+
# FeatureStoreWrapper so it can be cached)
|
94
|
+
if old_item[:version] >= new_item[:version]
|
95
|
+
return old_item
|
96
|
+
end
|
97
|
+
mod_index = old_value[0]["ModifyIndex"]
|
98
|
+
end
|
99
|
+
|
100
|
+
# Otherwise, try to write. We will do a compare-and-set operation, so the write will only succeed if
|
101
|
+
# the key's ModifyIndex is still equal to the previous value. If the previous ModifyIndex was zero,
|
102
|
+
# it means the key did not previously exist and the write will only succeed if it still doesn't exist.
|
103
|
+
success = Diplomat::Kv.put(key, json, cas: mod_index)
|
104
|
+
return new_item if success
|
105
|
+
|
106
|
+
# If we failed, retry the whole shebang
|
107
|
+
@logger.debug { "Concurrent modification detected, retrying" }
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def initialized_internal?
|
112
|
+
# Unfortunately we need to use exceptions here, instead of the :return parameter, because with
|
113
|
+
# :return there's no way to distinguish between a missing value and an empty string.
|
114
|
+
begin
|
115
|
+
Diplomat::Kv.get(inited_key, {})
|
116
|
+
true
|
117
|
+
rescue Diplomat::KeyNotFound
|
118
|
+
false
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def stop
|
123
|
+
# There's no Consul client instance to dispose of
|
124
|
+
end
|
125
|
+
|
126
|
+
private
|
127
|
+
|
128
|
+
def item_key(kind, key)
|
129
|
+
kind_key(kind) + key.to_s
|
130
|
+
end
|
131
|
+
|
132
|
+
def kind_key(kind)
|
133
|
+
@prefix + kind[:namespace] + '/'
|
134
|
+
end
|
135
|
+
|
136
|
+
def inited_key
|
137
|
+
@prefix + '$inited'
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
class ConsulUtil
|
142
|
+
#
|
143
|
+
# Submits as many transactions as necessary to submit all of the given operations.
|
144
|
+
# The ops array is consumed.
|
145
|
+
#
|
146
|
+
def self.batch_operations(ops)
|
147
|
+
batch_size = 64 # Consul can only do this many at a time
|
148
|
+
while true
|
149
|
+
chunk = ops.shift(batch_size)
|
150
|
+
break if chunk.empty?
|
151
|
+
Diplomat::Kv.txn(chunk)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
@@ -0,0 +1,228 @@
|
|
1
|
+
require "json"
|
2
|
+
|
3
|
+
module LaunchDarkly
|
4
|
+
module Impl
|
5
|
+
module Integrations
|
6
|
+
module DynamoDB
|
7
|
+
#
|
8
|
+
# Internal implementation of the DynamoDB feature store, intended to be used with CachingStoreWrapper.
|
9
|
+
#
|
10
|
+
class DynamoDBFeatureStoreCore
|
11
|
+
begin
|
12
|
+
require "aws-sdk-dynamodb"
|
13
|
+
AWS_SDK_ENABLED = true
|
14
|
+
rescue ScriptError, StandardError
|
15
|
+
begin
|
16
|
+
require "aws-sdk"
|
17
|
+
AWS_SDK_ENABLED = true
|
18
|
+
rescue ScriptError, StandardError
|
19
|
+
AWS_SDK_ENABLED = false
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
PARTITION_KEY = "namespace"
|
24
|
+
SORT_KEY = "key"
|
25
|
+
|
26
|
+
VERSION_ATTRIBUTE = "version"
|
27
|
+
ITEM_JSON_ATTRIBUTE = "item"
|
28
|
+
|
29
|
+
def initialize(table_name, opts)
|
30
|
+
if !AWS_SDK_ENABLED
|
31
|
+
raise RuntimeError.new("can't use DynamoDB feature store without the aws-sdk or aws-sdk-dynamodb gem")
|
32
|
+
end
|
33
|
+
|
34
|
+
@table_name = table_name
|
35
|
+
@prefix = opts[:prefix]
|
36
|
+
@logger = opts[:logger] || Config.default_logger
|
37
|
+
|
38
|
+
if !opts[:existing_client].nil?
|
39
|
+
@client = opts[:existing_client]
|
40
|
+
else
|
41
|
+
@client = Aws::DynamoDB::Client.new(opts[:dynamodb_opts] || {})
|
42
|
+
end
|
43
|
+
|
44
|
+
@logger.info("DynamoDBFeatureStore: using DynamoDB table \"#{table_name}\"")
|
45
|
+
end
|
46
|
+
|
47
|
+
def init_internal(all_data)
|
48
|
+
# Start by reading the existing keys; we will later delete any of these that weren't in all_data.
|
49
|
+
unused_old_keys = read_existing_keys(all_data.keys)
|
50
|
+
|
51
|
+
requests = []
|
52
|
+
num_items = 0
|
53
|
+
|
54
|
+
# Insert or update every provided item
|
55
|
+
all_data.each do |kind, items|
|
56
|
+
items.values.each do |item|
|
57
|
+
requests.push({ put_request: { item: marshal_item(kind, item) } })
|
58
|
+
unused_old_keys.delete([ namespace_for_kind(kind), item[:key] ])
|
59
|
+
num_items = num_items + 1
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Now delete any previously existing items whose keys were not in the current data
|
64
|
+
unused_old_keys.each do |tuple|
|
65
|
+
del_item = make_keys_hash(tuple[0], tuple[1])
|
66
|
+
requests.push({ delete_request: { key: del_item } })
|
67
|
+
end
|
68
|
+
|
69
|
+
# Now set the special key that we check in initialized_internal?
|
70
|
+
inited_item = make_keys_hash(inited_key, inited_key)
|
71
|
+
requests.push({ put_request: { item: inited_item } })
|
72
|
+
|
73
|
+
DynamoDBUtil.batch_write_requests(@client, @table_name, requests)
|
74
|
+
|
75
|
+
@logger.info { "Initialized table #{@table_name} with #{num_items} items" }
|
76
|
+
end
|
77
|
+
|
78
|
+
def get_internal(kind, key)
|
79
|
+
resp = get_item_by_keys(namespace_for_kind(kind), key)
|
80
|
+
unmarshal_item(resp.item)
|
81
|
+
end
|
82
|
+
|
83
|
+
def get_all_internal(kind)
|
84
|
+
items_out = {}
|
85
|
+
req = make_query_for_kind(kind)
|
86
|
+
while true
|
87
|
+
resp = @client.query(req)
|
88
|
+
resp.items.each do |item|
|
89
|
+
item_out = unmarshal_item(item)
|
90
|
+
items_out[item_out[:key].to_sym] = item_out
|
91
|
+
end
|
92
|
+
break if resp.last_evaluated_key.nil? || resp.last_evaluated_key.length == 0
|
93
|
+
req.exclusive_start_key = resp.last_evaluated_key
|
94
|
+
end
|
95
|
+
items_out
|
96
|
+
end
|
97
|
+
|
98
|
+
def upsert_internal(kind, new_item)
|
99
|
+
encoded_item = marshal_item(kind, new_item)
|
100
|
+
begin
|
101
|
+
@client.put_item({
|
102
|
+
table_name: @table_name,
|
103
|
+
item: encoded_item,
|
104
|
+
condition_expression: "attribute_not_exists(#namespace) or attribute_not_exists(#key) or :version > #version",
|
105
|
+
expression_attribute_names: {
|
106
|
+
"#namespace" => PARTITION_KEY,
|
107
|
+
"#key" => SORT_KEY,
|
108
|
+
"#version" => VERSION_ATTRIBUTE
|
109
|
+
},
|
110
|
+
expression_attribute_values: {
|
111
|
+
":version" => new_item[:version]
|
112
|
+
}
|
113
|
+
})
|
114
|
+
new_item
|
115
|
+
rescue Aws::DynamoDB::Errors::ConditionalCheckFailedException
|
116
|
+
# The item was not updated because there's a newer item in the database.
|
117
|
+
# We must now read the item that's in the database and return it, so CachingStoreWrapper can cache it.
|
118
|
+
get_internal(kind, new_item[:key])
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def initialized_internal?
|
123
|
+
resp = get_item_by_keys(inited_key, inited_key)
|
124
|
+
!resp.item.nil? && resp.item.length > 0
|
125
|
+
end
|
126
|
+
|
127
|
+
def stop
|
128
|
+
# AWS client doesn't seem to have a close method
|
129
|
+
end
|
130
|
+
|
131
|
+
private
|
132
|
+
|
133
|
+
def prefixed_namespace(base_str)
|
134
|
+
(@prefix.nil? || @prefix == "") ? base_str : "#{@prefix}:#{base_str}"
|
135
|
+
end
|
136
|
+
|
137
|
+
def namespace_for_kind(kind)
|
138
|
+
prefixed_namespace(kind[:namespace])
|
139
|
+
end
|
140
|
+
|
141
|
+
def inited_key
|
142
|
+
prefixed_namespace("$inited")
|
143
|
+
end
|
144
|
+
|
145
|
+
def make_keys_hash(namespace, key)
|
146
|
+
{
|
147
|
+
PARTITION_KEY => namespace,
|
148
|
+
SORT_KEY => key
|
149
|
+
}
|
150
|
+
end
|
151
|
+
|
152
|
+
def make_query_for_kind(kind)
|
153
|
+
{
|
154
|
+
table_name: @table_name,
|
155
|
+
consistent_read: true,
|
156
|
+
key_conditions: {
|
157
|
+
PARTITION_KEY => {
|
158
|
+
comparison_operator: "EQ",
|
159
|
+
attribute_value_list: [ namespace_for_kind(kind) ]
|
160
|
+
}
|
161
|
+
}
|
162
|
+
}
|
163
|
+
end
|
164
|
+
|
165
|
+
def get_item_by_keys(namespace, key)
|
166
|
+
@client.get_item({
|
167
|
+
table_name: @table_name,
|
168
|
+
key: make_keys_hash(namespace, key)
|
169
|
+
})
|
170
|
+
end
|
171
|
+
|
172
|
+
def read_existing_keys(kinds)
|
173
|
+
keys = Set.new
|
174
|
+
kinds.each do |kind|
|
175
|
+
req = make_query_for_kind(kind).merge({
|
176
|
+
projection_expression: "#namespace, #key",
|
177
|
+
expression_attribute_names: {
|
178
|
+
"#namespace" => PARTITION_KEY,
|
179
|
+
"#key" => SORT_KEY
|
180
|
+
}
|
181
|
+
})
|
182
|
+
while true
|
183
|
+
resp = @client.query(req)
|
184
|
+
resp.items.each do |item|
|
185
|
+
namespace = item[PARTITION_KEY]
|
186
|
+
key = item[SORT_KEY]
|
187
|
+
keys.add([ namespace, key ])
|
188
|
+
end
|
189
|
+
break if resp.last_evaluated_key.nil? || resp.last_evaluated_key.length == 0
|
190
|
+
req.exclusive_start_key = resp.last_evaluated_key
|
191
|
+
end
|
192
|
+
end
|
193
|
+
keys
|
194
|
+
end
|
195
|
+
|
196
|
+
def marshal_item(kind, item)
|
197
|
+
make_keys_hash(namespace_for_kind(kind), item[:key]).merge({
|
198
|
+
VERSION_ATTRIBUTE => item[:version],
|
199
|
+
ITEM_JSON_ATTRIBUTE => item.to_json
|
200
|
+
})
|
201
|
+
end
|
202
|
+
|
203
|
+
def unmarshal_item(item)
|
204
|
+
return nil if item.nil? || item.length == 0
|
205
|
+
json_attr = item[ITEM_JSON_ATTRIBUTE]
|
206
|
+
raise RuntimeError.new("DynamoDB map did not contain expected item string") if json_attr.nil?
|
207
|
+
JSON.parse(json_attr, symbolize_names: true)
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
class DynamoDBUtil
|
212
|
+
#
|
213
|
+
# Calls client.batch_write_item as many times as necessary to submit all of the given requests.
|
214
|
+
# The requests array is consumed.
|
215
|
+
#
|
216
|
+
def self.batch_write_requests(client, table, requests)
|
217
|
+
batch_size = 25
|
218
|
+
while true
|
219
|
+
chunk = requests.shift(batch_size)
|
220
|
+
break if chunk.empty?
|
221
|
+
client.batch_write_item({ request_items: { table => chunk } })
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
@@ -0,0 +1,155 @@
|
|
1
|
+
require "concurrent/atomics"
|
2
|
+
require "json"
|
3
|
+
|
4
|
+
module LaunchDarkly
|
5
|
+
module Impl
|
6
|
+
module Integrations
|
7
|
+
module Redis
|
8
|
+
#
|
9
|
+
# Internal implementation of the Redis feature store, intended to be used with CachingStoreWrapper.
|
10
|
+
#
|
11
|
+
class RedisFeatureStoreCore
|
12
|
+
begin
|
13
|
+
require "redis"
|
14
|
+
require "connection_pool"
|
15
|
+
REDIS_ENABLED = true
|
16
|
+
rescue ScriptError, StandardError
|
17
|
+
REDIS_ENABLED = false
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize(opts)
|
21
|
+
if !REDIS_ENABLED
|
22
|
+
raise RuntimeError.new("can't use Redis feature store because one of these gems is missing: redis, connection_pool")
|
23
|
+
end
|
24
|
+
|
25
|
+
@redis_opts = opts[:redis_opts] || Hash.new
|
26
|
+
if opts[:redis_url]
|
27
|
+
@redis_opts[:url] = opts[:redis_url]
|
28
|
+
end
|
29
|
+
if !@redis_opts.include?(:url)
|
30
|
+
@redis_opts[:url] = LaunchDarkly::Integrations::Redis::default_redis_url
|
31
|
+
end
|
32
|
+
max_connections = opts[:max_connections] || 16
|
33
|
+
@pool = opts[:pool] || ConnectionPool.new(size: max_connections) do
|
34
|
+
::Redis.new(@redis_opts)
|
35
|
+
end
|
36
|
+
@prefix = opts[:prefix] || LaunchDarkly::Integrations::Redis::default_prefix
|
37
|
+
@logger = opts[:logger] || Config.default_logger
|
38
|
+
@test_hook = opts[:test_hook] # used for unit tests, deliberately undocumented
|
39
|
+
|
40
|
+
@stopped = Concurrent::AtomicBoolean.new(false)
|
41
|
+
|
42
|
+
with_connection do |redis|
|
43
|
+
@logger.info("RedisFeatureStore: using Redis instance at #{redis.connection[:host]}:#{redis.connection[:port]} \
|
44
|
+
and prefix: #{@prefix}")
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def init_internal(all_data)
|
49
|
+
count = 0
|
50
|
+
with_connection do |redis|
|
51
|
+
redis.multi do |multi|
|
52
|
+
all_data.each do |kind, items|
|
53
|
+
multi.del(items_key(kind))
|
54
|
+
count = count + items.count
|
55
|
+
items.each do |key, item|
|
56
|
+
multi.hset(items_key(kind), key, item.to_json)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
multi.set(inited_key, inited_key)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
@logger.info { "RedisFeatureStore: initialized with #{count} items" }
|
63
|
+
end
|
64
|
+
|
65
|
+
def get_internal(kind, key)
|
66
|
+
with_connection do |redis|
|
67
|
+
get_redis(redis, kind, key)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def get_all_internal(kind)
|
72
|
+
fs = {}
|
73
|
+
with_connection do |redis|
|
74
|
+
hashfs = redis.hgetall(items_key(kind))
|
75
|
+
hashfs.each do |k, json_item|
|
76
|
+
f = JSON.parse(json_item, symbolize_names: true)
|
77
|
+
fs[k.to_sym] = f
|
78
|
+
end
|
79
|
+
end
|
80
|
+
fs
|
81
|
+
end
|
82
|
+
|
83
|
+
def upsert_internal(kind, new_item)
|
84
|
+
base_key = items_key(kind)
|
85
|
+
key = new_item[:key]
|
86
|
+
try_again = true
|
87
|
+
final_item = new_item
|
88
|
+
while try_again
|
89
|
+
try_again = false
|
90
|
+
with_connection do |redis|
|
91
|
+
redis.watch(base_key) do
|
92
|
+
old_item = get_redis(redis, kind, key)
|
93
|
+
before_update_transaction(base_key, key)
|
94
|
+
if old_item.nil? || old_item[:version] < new_item[:version]
|
95
|
+
result = redis.multi do |multi|
|
96
|
+
multi.hset(base_key, key, new_item.to_json)
|
97
|
+
end
|
98
|
+
if result.nil?
|
99
|
+
@logger.debug { "RedisFeatureStore: concurrent modification detected, retrying" }
|
100
|
+
try_again = true
|
101
|
+
end
|
102
|
+
else
|
103
|
+
final_item = old_item
|
104
|
+
action = new_item[:deleted] ? "delete" : "update"
|
105
|
+
@logger.warn { "RedisFeatureStore: attempted to #{action} #{key} version: #{old_item[:version]} \
|
106
|
+
in '#{kind[:namespace]}' with a version that is the same or older: #{new_item[:version]}" }
|
107
|
+
end
|
108
|
+
redis.unwatch
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
final_item
|
113
|
+
end
|
114
|
+
|
115
|
+
def initialized_internal?
|
116
|
+
with_connection { |redis| redis.exists(inited_key) }
|
117
|
+
end
|
118
|
+
|
119
|
+
def stop
|
120
|
+
if @stopped.make_true
|
121
|
+
@pool.shutdown { |redis| redis.close }
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
private
|
126
|
+
|
127
|
+
def before_update_transaction(base_key, key)
|
128
|
+
@test_hook.before_update_transaction(base_key, key) if !@test_hook.nil?
|
129
|
+
end
|
130
|
+
|
131
|
+
def items_key(kind)
|
132
|
+
@prefix + ":" + kind[:namespace]
|
133
|
+
end
|
134
|
+
|
135
|
+
def cache_key(kind, key)
|
136
|
+
kind[:namespace] + ":" + key.to_s
|
137
|
+
end
|
138
|
+
|
139
|
+
def inited_key
|
140
|
+
@prefix + ":$inited"
|
141
|
+
end
|
142
|
+
|
143
|
+
def with_connection
|
144
|
+
@pool.with { |redis| yield(redis) }
|
145
|
+
end
|
146
|
+
|
147
|
+
def get_redis(redis, kind, key)
|
148
|
+
json_item = redis.hget(items_key(kind), key)
|
149
|
+
json_item.nil? ? nil : JSON.parse(json_item, symbolize_names: true)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|