launchdarkly-server-sdk 5.8.2 → 6.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|