launchdarkly-server-sdk 5.8.2 → 6.0.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.
- checksums.yaml +4 -4
- data/.circleci/config.yml +28 -122
- data/.ldrelease/circleci/linux/execute.sh +18 -0
- data/.ldrelease/circleci/mac/execute.sh +18 -0
- data/.ldrelease/circleci/template/build.sh +29 -0
- data/.ldrelease/circleci/template/publish.sh +23 -0
- data/.ldrelease/circleci/template/set-gem-home.sh +7 -0
- data/.ldrelease/circleci/template/test.sh +10 -0
- data/.ldrelease/circleci/template/update-version.sh +8 -0
- data/.ldrelease/circleci/windows/execute.ps1 +19 -0
- data/.ldrelease/config.yml +7 -3
- data/CHANGELOG.md +9 -0
- data/CONTRIBUTING.md +1 -1
- data/Gemfile.lock +69 -42
- data/README.md +2 -2
- data/azure-pipelines.yml +1 -1
- data/launchdarkly-server-sdk.gemspec +16 -16
- data/lib/ldclient-rb.rb +0 -1
- data/lib/ldclient-rb/config.rb +15 -3
- data/lib/ldclient-rb/evaluation_detail.rb +293 -0
- data/lib/ldclient-rb/events.rb +1 -4
- data/lib/ldclient-rb/file_data_source.rb +1 -1
- data/lib/ldclient-rb/impl/evaluator.rb +225 -0
- data/lib/ldclient-rb/impl/evaluator_bucketing.rb +74 -0
- data/lib/ldclient-rb/impl/evaluator_operators.rb +160 -0
- data/lib/ldclient-rb/impl/event_sender.rb +56 -40
- data/lib/ldclient-rb/impl/integrations/consul_impl.rb +5 -5
- data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +5 -5
- data/lib/ldclient-rb/impl/integrations/redis_impl.rb +5 -9
- data/lib/ldclient-rb/impl/model/serialization.rb +62 -0
- data/lib/ldclient-rb/impl/unbounded_pool.rb +34 -0
- data/lib/ldclient-rb/ldclient.rb +14 -9
- data/lib/ldclient-rb/polling.rb +1 -4
- data/lib/ldclient-rb/requestor.rb +25 -15
- data/lib/ldclient-rb/stream.rb +9 -6
- data/lib/ldclient-rb/util.rb +12 -8
- data/lib/ldclient-rb/version.rb +1 -1
- data/spec/evaluation_detail_spec.rb +135 -0
- data/spec/event_sender_spec.rb +20 -2
- data/spec/http_util.rb +11 -1
- data/spec/impl/evaluator_bucketing_spec.rb +111 -0
- data/spec/impl/evaluator_clause_spec.rb +55 -0
- data/spec/impl/evaluator_operators_spec.rb +141 -0
- data/spec/impl/evaluator_rule_spec.rb +96 -0
- data/spec/impl/evaluator_segment_spec.rb +125 -0
- data/spec/impl/evaluator_spec.rb +305 -0
- data/spec/impl/evaluator_spec_base.rb +75 -0
- data/spec/impl/model/serialization_spec.rb +41 -0
- data/spec/launchdarkly-server-sdk_spec.rb +1 -1
- data/spec/ldclient_end_to_end_spec.rb +34 -0
- data/spec/ldclient_spec.rb +10 -8
- data/spec/polling_spec.rb +2 -2
- data/spec/redis_feature_store_spec.rb +2 -2
- data/spec/requestor_spec.rb +11 -11
- metadata +89 -46
- data/lib/ldclient-rb/evaluation.rb +0 -462
- data/spec/evaluation_spec.rb +0 -789
@@ -0,0 +1,74 @@
|
|
1
|
+
|
2
|
+
module LaunchDarkly
|
3
|
+
module Impl
|
4
|
+
# Encapsulates the logic for percentage rollouts.
|
5
|
+
module EvaluatorBucketing
|
6
|
+
# Applies either a fixed variation or a rollout for a rule (or the fallthrough rule).
|
7
|
+
#
|
8
|
+
# @param flag [Object] the feature flag
|
9
|
+
# @param rule [Object] the rule
|
10
|
+
# @param user [Object] the user properties
|
11
|
+
# @return [Number] the variation index, or nil if there is an error
|
12
|
+
def self.variation_index_for_user(flag, rule, user)
|
13
|
+
variation = rule[:variation]
|
14
|
+
return variation if !variation.nil? # fixed variation
|
15
|
+
rollout = rule[:rollout]
|
16
|
+
return nil if rollout.nil?
|
17
|
+
variations = rollout[:variations]
|
18
|
+
if !variations.nil? && variations.length > 0 # percentage rollout
|
19
|
+
rollout = rule[:rollout]
|
20
|
+
bucket_by = rollout[:bucketBy].nil? ? "key" : rollout[:bucketBy]
|
21
|
+
bucket = bucket_user(user, flag[:key], bucket_by, flag[:salt])
|
22
|
+
sum = 0;
|
23
|
+
variations.each do |variate|
|
24
|
+
sum += variate[:weight].to_f / 100000.0
|
25
|
+
if bucket < sum
|
26
|
+
return variate[:variation]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
# The user's bucket value was greater than or equal to the end of the last bucket. This could happen due
|
30
|
+
# to a rounding error, or due to the fact that we are scaling to 100000 rather than 99999, or the flag
|
31
|
+
# data could contain buckets that don't actually add up to 100000. Rather than returning an error in
|
32
|
+
# this case (or changing the scaling, which would potentially change the results for *all* users), we
|
33
|
+
# will simply put the user in the last bucket.
|
34
|
+
variations[-1][:variation]
|
35
|
+
else # the rule isn't well-formed
|
36
|
+
nil
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Returns a user's bucket value as a floating-point value in `[0, 1)`.
|
41
|
+
#
|
42
|
+
# @param user [Object] the user properties
|
43
|
+
# @param key [String] the feature flag key (or segment key, if this is for a segment rule)
|
44
|
+
# @param bucket_by [String|Symbol] the name of the user attribute to be used for bucketing
|
45
|
+
# @param salt [String] the feature flag's or segment's salt value
|
46
|
+
# @return [Number] the bucket value, from 0 inclusive to 1 exclusive
|
47
|
+
def self.bucket_user(user, key, bucket_by, salt)
|
48
|
+
return nil unless user[:key]
|
49
|
+
|
50
|
+
id_hash = bucketable_string_value(EvaluatorOperators.user_value(user, bucket_by))
|
51
|
+
if id_hash.nil?
|
52
|
+
return 0.0
|
53
|
+
end
|
54
|
+
|
55
|
+
if user[:secondary]
|
56
|
+
id_hash += "." + user[:secondary].to_s
|
57
|
+
end
|
58
|
+
|
59
|
+
hash_key = "%s.%s.%s" % [key, salt, id_hash]
|
60
|
+
|
61
|
+
hash_val = (Digest::SHA1.hexdigest(hash_key))[0..14]
|
62
|
+
hash_val.to_i(16) / Float(0xFFFFFFFFFFFFFFF)
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def self.bucketable_string_value(value)
|
68
|
+
return value if value.is_a? String
|
69
|
+
return value.to_s if value.is_a? Integer
|
70
|
+
nil
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,160 @@
|
|
1
|
+
require "date"
|
2
|
+
require "semantic"
|
3
|
+
require "set"
|
4
|
+
|
5
|
+
module LaunchDarkly
|
6
|
+
module Impl
|
7
|
+
# Defines the behavior of all operators that can be used in feature flag rules and segment rules.
|
8
|
+
module EvaluatorOperators
|
9
|
+
# Applies an operator to produce a boolean result.
|
10
|
+
#
|
11
|
+
# @param op [Symbol] one of the supported LaunchDarkly operators, as a symbol
|
12
|
+
# @param user_value the value of the user attribute that is referenced in the current clause (left-hand
|
13
|
+
# side of the expression)
|
14
|
+
# @param clause_value the constant value that `user_value` is being compared to (right-hand side of the
|
15
|
+
# expression)
|
16
|
+
# @return [Boolean] true if the expression should be considered a match; false if it is not a match, or
|
17
|
+
# if the values cannot be compared because they are of the wrong types, or if the operator is unknown
|
18
|
+
def self.apply(op, user_value, clause_value)
|
19
|
+
case op
|
20
|
+
when :in
|
21
|
+
user_value == clause_value
|
22
|
+
when :startsWith
|
23
|
+
string_op(user_value, clause_value, lambda { |a, b| a.start_with? b })
|
24
|
+
when :endsWith
|
25
|
+
string_op(user_value, clause_value, lambda { |a, b| a.end_with? b })
|
26
|
+
when :contains
|
27
|
+
string_op(user_value, clause_value, lambda { |a, b| a.include? b })
|
28
|
+
when :matches
|
29
|
+
string_op(user_value, clause_value, lambda { |a, b|
|
30
|
+
begin
|
31
|
+
re = Regexp.new b
|
32
|
+
!re.match(a).nil?
|
33
|
+
rescue
|
34
|
+
false
|
35
|
+
end
|
36
|
+
})
|
37
|
+
when :lessThan
|
38
|
+
numeric_op(user_value, clause_value, lambda { |a, b| a < b })
|
39
|
+
when :lessThanOrEqual
|
40
|
+
numeric_op(user_value, clause_value, lambda { |a, b| a <= b })
|
41
|
+
when :greaterThan
|
42
|
+
numeric_op(user_value, clause_value, lambda { |a, b| a > b })
|
43
|
+
when :greaterThanOrEqual
|
44
|
+
numeric_op(user_value, clause_value, lambda { |a, b| a >= b })
|
45
|
+
when :before
|
46
|
+
date_op(user_value, clause_value, lambda { |a, b| a < b })
|
47
|
+
when :after
|
48
|
+
date_op(user_value, clause_value, lambda { |a, b| a > b })
|
49
|
+
when :semVerEqual
|
50
|
+
semver_op(user_value, clause_value, lambda { |a, b| a == b })
|
51
|
+
when :semVerLessThan
|
52
|
+
semver_op(user_value, clause_value, lambda { |a, b| a < b })
|
53
|
+
when :semVerGreaterThan
|
54
|
+
semver_op(user_value, clause_value, lambda { |a, b| a > b })
|
55
|
+
when :segmentMatch
|
56
|
+
# We should never reach this; it can't be evaluated based on just two parameters, because it requires
|
57
|
+
# looking up the segment from the data store. Instead, we special-case this operator in clause_match_user.
|
58
|
+
false
|
59
|
+
else
|
60
|
+
false
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Retrieves the value of a user attribute by name.
|
65
|
+
#
|
66
|
+
# Built-in attributes correspond to top-level properties in the user object. They are treated as strings and
|
67
|
+
# non-string values are coerced to strings, except for `anonymous` which is meant to be a boolean if present
|
68
|
+
# and is not currently coerced. This behavior is consistent with earlier versions of the Ruby SDK, but is not
|
69
|
+
# guaranteed to be consistent with other SDKs, since the evaluator specification is based on the strongly-typed
|
70
|
+
# SDKs where it is not possible for an attribute to have the wrong type.
|
71
|
+
#
|
72
|
+
# Custom attributes correspond to properties within the `custom` property, if any, and can be of any type.
|
73
|
+
#
|
74
|
+
# @param user [Object] the user properties
|
75
|
+
# @param attribute [String|Symbol] the attribute to get, for instance `:key` or `:name` or `:some_custom_attr`
|
76
|
+
# @return the attribute value, or nil if the attribute is unknown
|
77
|
+
def self.user_value(user, attribute)
|
78
|
+
attribute = attribute.to_sym
|
79
|
+
if BUILTINS.include? attribute
|
80
|
+
value = user[attribute]
|
81
|
+
return nil if value.nil?
|
82
|
+
(attribute == :anonymous) ? value : value.to_s
|
83
|
+
elsif !user[:custom].nil?
|
84
|
+
user[:custom][attribute]
|
85
|
+
else
|
86
|
+
nil
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
BUILTINS = Set[:key, :ip, :country, :email, :firstName, :lastName, :avatar, :name, :anonymous]
|
93
|
+
NUMERIC_VERSION_COMPONENTS_REGEX = Regexp.new("^[0-9.]*")
|
94
|
+
|
95
|
+
private_constant :BUILTINS
|
96
|
+
private_constant :NUMERIC_VERSION_COMPONENTS_REGEX
|
97
|
+
|
98
|
+
def self.string_op(user_value, clause_value, fn)
|
99
|
+
(user_value.is_a? String) && (clause_value.is_a? String) && fn.call(user_value, clause_value)
|
100
|
+
end
|
101
|
+
|
102
|
+
def self.numeric_op(user_value, clause_value, fn)
|
103
|
+
(user_value.is_a? Numeric) && (clause_value.is_a? Numeric) && fn.call(user_value, clause_value)
|
104
|
+
end
|
105
|
+
|
106
|
+
def self.date_op(user_value, clause_value, fn)
|
107
|
+
ud = to_date(user_value)
|
108
|
+
if !ud.nil?
|
109
|
+
cd = to_date(clause_value)
|
110
|
+
!cd.nil? && fn.call(ud, cd)
|
111
|
+
else
|
112
|
+
false
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def self.semver_op(user_value, clause_value, fn)
|
117
|
+
uv = to_semver(user_value)
|
118
|
+
if !uv.nil?
|
119
|
+
cv = to_semver(clause_value)
|
120
|
+
!cv.nil? && fn.call(uv, cv)
|
121
|
+
else
|
122
|
+
false
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def self.to_date(value)
|
127
|
+
if value.is_a? String
|
128
|
+
begin
|
129
|
+
DateTime.rfc3339(value).strftime("%Q").to_i
|
130
|
+
rescue => e
|
131
|
+
nil
|
132
|
+
end
|
133
|
+
elsif value.is_a? Numeric
|
134
|
+
value
|
135
|
+
else
|
136
|
+
nil
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def self.to_semver(value)
|
141
|
+
if value.is_a? String
|
142
|
+
for _ in 0..2 do
|
143
|
+
begin
|
144
|
+
return Semantic::Version.new(value)
|
145
|
+
rescue ArgumentError
|
146
|
+
value = add_zero_version_component(value)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
nil
|
151
|
+
end
|
152
|
+
|
153
|
+
def self.add_zero_version_component(v)
|
154
|
+
NUMERIC_VERSION_COMPONENTS_REGEX.match(v) { |m|
|
155
|
+
m[0] + ".0" + v[m[0].length..-1]
|
156
|
+
}
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
@@ -1,4 +1,7 @@
|
|
1
|
+
require "ldclient-rb/impl/unbounded_pool"
|
2
|
+
|
1
3
|
require "securerandom"
|
4
|
+
require "http"
|
2
5
|
|
3
6
|
module LaunchDarkly
|
4
7
|
module Impl
|
@@ -9,62 +12,75 @@ module LaunchDarkly
|
|
9
12
|
DEFAULT_RETRY_INTERVAL = 1
|
10
13
|
|
11
14
|
def initialize(sdk_key, config, http_client = nil, retry_interval = DEFAULT_RETRY_INTERVAL)
|
12
|
-
@client = http_client ? http_client : LaunchDarkly::Util.new_http_client(config.events_uri, config)
|
13
15
|
@sdk_key = sdk_key
|
14
16
|
@config = config
|
15
17
|
@events_uri = config.events_uri + "/bulk"
|
16
18
|
@diagnostic_uri = config.events_uri + "/diagnostic"
|
17
19
|
@logger = config.logger
|
18
20
|
@retry_interval = retry_interval
|
21
|
+
@http_client_pool = UnboundedPool.new(
|
22
|
+
lambda { LaunchDarkly::Util.new_http_client(@config.events_uri, @config) },
|
23
|
+
lambda { |client| client.close })
|
24
|
+
end
|
25
|
+
|
26
|
+
def stop
|
27
|
+
@http_client_pool.dispose_all()
|
19
28
|
end
|
20
29
|
|
21
30
|
def send_event_data(event_data, description, is_diagnostic)
|
22
31
|
uri = is_diagnostic ? @diagnostic_uri : @events_uri
|
23
32
|
payload_id = is_diagnostic ? nil : SecureRandom.uuid
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
@client.start if !@client.started?
|
32
|
-
@logger.debug { "[LDClient] sending #{description}: #{event_data}" }
|
33
|
-
req = Net::HTTP::Post.new(uri)
|
34
|
-
req.content_type = "application/json"
|
35
|
-
req.body = event_data
|
36
|
-
Impl::Util.default_http_headers(@sdk_key, @config).each { |k, v| req[k] = v }
|
37
|
-
if !is_diagnostic
|
38
|
-
req["X-LaunchDarkly-Event-Schema"] = CURRENT_SCHEMA_VERSION.to_s
|
39
|
-
req["X-LaunchDarkly-Payload-ID"] = payload_id
|
33
|
+
begin
|
34
|
+
http_client = @http_client_pool.acquire()
|
35
|
+
response = nil
|
36
|
+
(0..1).each do |attempt|
|
37
|
+
if attempt > 0
|
38
|
+
@logger.warn { "[LDClient] Will retry posting events after #{@retry_interval} second" }
|
39
|
+
sleep(@retry_interval)
|
40
40
|
end
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
res_time = nil
|
50
|
-
if !res["date"].nil?
|
51
|
-
begin
|
52
|
-
res_time = Time.httpdate(res["date"])
|
53
|
-
rescue ArgumentError
|
41
|
+
begin
|
42
|
+
@logger.debug { "[LDClient] sending #{description}: #{event_data}" }
|
43
|
+
headers = {}
|
44
|
+
headers["content-type"] = "application/json"
|
45
|
+
Impl::Util.default_http_headers(@sdk_key, @config).each { |k, v| headers[k] = v }
|
46
|
+
if !is_diagnostic
|
47
|
+
headers["X-LaunchDarkly-Event-Schema"] = CURRENT_SCHEMA_VERSION.to_s
|
48
|
+
headers["X-LaunchDarkly-Payload-ID"] = payload_id
|
54
49
|
end
|
50
|
+
response = http_client.request("POST", uri, {
|
51
|
+
headers: headers,
|
52
|
+
body: event_data
|
53
|
+
})
|
54
|
+
rescue StandardError => exn
|
55
|
+
@logger.warn { "[LDClient] Error sending events: #{exn.inspect}." }
|
56
|
+
next
|
57
|
+
end
|
58
|
+
status = response.status.code
|
59
|
+
# must fully read body for persistent connections
|
60
|
+
body = response.to_s
|
61
|
+
if status >= 200 && status < 300
|
62
|
+
res_time = nil
|
63
|
+
if !response.headers["date"].nil?
|
64
|
+
begin
|
65
|
+
res_time = Time.httpdate(response.headers["date"])
|
66
|
+
rescue ArgumentError
|
67
|
+
end
|
68
|
+
end
|
69
|
+
return EventSenderResult.new(true, false, res_time)
|
70
|
+
end
|
71
|
+
must_shutdown = !LaunchDarkly::Util.http_error_recoverable?(status)
|
72
|
+
can_retry = !must_shutdown && attempt == 0
|
73
|
+
message = LaunchDarkly::Util.http_error_message(status, "event delivery", can_retry ? "will retry" : "some events were dropped")
|
74
|
+
@logger.error { "[LDClient] #{message}" }
|
75
|
+
if must_shutdown
|
76
|
+
return EventSenderResult.new(false, true, nil)
|
55
77
|
end
|
56
|
-
return EventSenderResult.new(true, false, res_time)
|
57
|
-
end
|
58
|
-
must_shutdown = !LaunchDarkly::Util.http_error_recoverable?(status)
|
59
|
-
can_retry = !must_shutdown && attempt == 0
|
60
|
-
message = LaunchDarkly::Util.http_error_message(status, "event delivery", can_retry ? "will retry" : "some events were dropped")
|
61
|
-
@logger.error { "[LDClient] #{message}" }
|
62
|
-
if must_shutdown
|
63
|
-
return EventSenderResult.new(false, true, nil)
|
64
78
|
end
|
79
|
+
# used up our retries
|
80
|
+
return EventSenderResult.new(false, false, nil)
|
81
|
+
ensure
|
82
|
+
@http_client_pool.release(http_client)
|
65
83
|
end
|
66
|
-
# used up our retries
|
67
|
-
return EventSenderResult.new(false, false, nil)
|
68
84
|
end
|
69
85
|
end
|
70
86
|
end
|
@@ -39,7 +39,7 @@ module LaunchDarkly
|
|
39
39
|
# Insert or update every provided item
|
40
40
|
all_data.each do |kind, items|
|
41
41
|
items.values.each do |item|
|
42
|
-
value = item
|
42
|
+
value = Model.serialize(kind, item)
|
43
43
|
key = item_key(kind, item[:key])
|
44
44
|
ops.push({ 'KV' => { 'Verb' => 'set', 'Key' => key, 'Value' => value } })
|
45
45
|
unused_old_keys.delete(key)
|
@@ -62,7 +62,7 @@ module LaunchDarkly
|
|
62
62
|
|
63
63
|
def get_internal(kind, key)
|
64
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 :
|
65
|
+
(value.nil? || value == "") ? nil : Model.deserialize(kind, value)
|
66
66
|
end
|
67
67
|
|
68
68
|
def get_all_internal(kind)
|
@@ -71,7 +71,7 @@ module LaunchDarkly
|
|
71
71
|
(results == "" ? [] : results).each do |result|
|
72
72
|
value = result[:value]
|
73
73
|
if !value.nil?
|
74
|
-
item =
|
74
|
+
item = Model.deserialize(kind, value)
|
75
75
|
items_out[item[:key].to_sym] = item
|
76
76
|
end
|
77
77
|
end
|
@@ -80,7 +80,7 @@ module LaunchDarkly
|
|
80
80
|
|
81
81
|
def upsert_internal(kind, new_item)
|
82
82
|
key = item_key(kind, new_item[:key])
|
83
|
-
json = new_item
|
83
|
+
json = Model.serialize(kind, new_item)
|
84
84
|
|
85
85
|
# We will potentially keep retrying indefinitely until someone's write succeeds
|
86
86
|
while true
|
@@ -88,7 +88,7 @@ module LaunchDarkly
|
|
88
88
|
if old_value.nil? || old_value == ""
|
89
89
|
mod_index = 0
|
90
90
|
else
|
91
|
-
old_item =
|
91
|
+
old_item = Model.deserialize(kind, old_value[0]["Value"])
|
92
92
|
# Check whether the item is stale. If so, don't do the update (and return the existing item to
|
93
93
|
# FeatureStoreWrapper so it can be cached)
|
94
94
|
if old_item[:version] >= new_item[:version]
|
@@ -77,7 +77,7 @@ module LaunchDarkly
|
|
77
77
|
|
78
78
|
def get_internal(kind, key)
|
79
79
|
resp = get_item_by_keys(namespace_for_kind(kind), key)
|
80
|
-
unmarshal_item(resp.item)
|
80
|
+
unmarshal_item(kind, resp.item)
|
81
81
|
end
|
82
82
|
|
83
83
|
def get_all_internal(kind)
|
@@ -86,7 +86,7 @@ module LaunchDarkly
|
|
86
86
|
while true
|
87
87
|
resp = @client.query(req)
|
88
88
|
resp.items.each do |item|
|
89
|
-
item_out = unmarshal_item(item)
|
89
|
+
item_out = unmarshal_item(kind, item)
|
90
90
|
items_out[item_out[:key].to_sym] = item_out
|
91
91
|
end
|
92
92
|
break if resp.last_evaluated_key.nil? || resp.last_evaluated_key.length == 0
|
@@ -196,15 +196,15 @@ module LaunchDarkly
|
|
196
196
|
def marshal_item(kind, item)
|
197
197
|
make_keys_hash(namespace_for_kind(kind), item[:key]).merge({
|
198
198
|
VERSION_ATTRIBUTE => item[:version],
|
199
|
-
ITEM_JSON_ATTRIBUTE => item
|
199
|
+
ITEM_JSON_ATTRIBUTE => Model.serialize(kind, item)
|
200
200
|
})
|
201
201
|
end
|
202
202
|
|
203
|
-
def unmarshal_item(item)
|
203
|
+
def unmarshal_item(kind, item)
|
204
204
|
return nil if item.nil? || item.length == 0
|
205
205
|
json_attr = item[ITEM_JSON_ATTRIBUTE]
|
206
206
|
raise RuntimeError.new("DynamoDB map did not contain expected item string") if json_attr.nil?
|
207
|
-
|
207
|
+
Model.deserialize(kind, json_attr)
|
208
208
|
end
|
209
209
|
end
|
210
210
|
|
@@ -55,7 +55,7 @@ module LaunchDarkly
|
|
55
55
|
multi.del(items_key(kind))
|
56
56
|
count = count + items.count
|
57
57
|
items.each do |key, item|
|
58
|
-
multi.hset(items_key(kind), key, item
|
58
|
+
multi.hset(items_key(kind), key, Model.serialize(kind,item))
|
59
59
|
end
|
60
60
|
end
|
61
61
|
multi.set(inited_key, inited_key)
|
@@ -75,8 +75,7 @@ module LaunchDarkly
|
|
75
75
|
with_connection do |redis|
|
76
76
|
hashfs = redis.hgetall(items_key(kind))
|
77
77
|
hashfs.each do |k, json_item|
|
78
|
-
|
79
|
-
fs[k.to_sym] = f
|
78
|
+
fs[k.to_sym] = Model.deserialize(kind, json_item)
|
80
79
|
end
|
81
80
|
end
|
82
81
|
fs
|
@@ -95,7 +94,7 @@ module LaunchDarkly
|
|
95
94
|
before_update_transaction(base_key, key)
|
96
95
|
if old_item.nil? || old_item[:version] < new_item[:version]
|
97
96
|
result = redis.multi do |multi|
|
98
|
-
multi.hset(base_key, key, new_item
|
97
|
+
multi.hset(base_key, key, Model.serialize(kind, new_item))
|
99
98
|
end
|
100
99
|
if result.nil?
|
101
100
|
@logger.debug { "RedisFeatureStore: concurrent modification detected, retrying" }
|
@@ -115,9 +114,7 @@ module LaunchDarkly
|
|
115
114
|
end
|
116
115
|
|
117
116
|
def initialized_internal?
|
118
|
-
with_connection
|
119
|
-
redis.respond_to?(:exists?) ? redis.exists?(inited_key) : redis.exists(inited_key)
|
120
|
-
end
|
117
|
+
with_connection { |redis| redis.exists?(inited_key) }
|
121
118
|
end
|
122
119
|
|
123
120
|
def stop
|
@@ -150,8 +147,7 @@ module LaunchDarkly
|
|
150
147
|
end
|
151
148
|
|
152
149
|
def get_redis(redis, kind, key)
|
153
|
-
|
154
|
-
json_item.nil? ? nil : JSON.parse(json_item, symbolize_names: true)
|
150
|
+
Model.deserialize(kind, redis.hget(items_key(kind), key))
|
155
151
|
end
|
156
152
|
end
|
157
153
|
end
|