launchdarkly-server-sdk 6.2.5 → 6.3.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +0 -1
- data/lib/ldclient-rb/config.rb +95 -1
- data/lib/ldclient-rb/evaluation_detail.rb +67 -8
- data/lib/ldclient-rb/file_data_source.rb +9 -300
- data/lib/ldclient-rb/flags_state.rb +23 -12
- data/lib/ldclient-rb/impl/big_segments.rb +117 -0
- data/lib/ldclient-rb/impl/evaluator.rb +80 -28
- data/lib/ldclient-rb/impl/evaluator_operators.rb +1 -1
- data/lib/ldclient-rb/impl/event_factory.rb +9 -12
- data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +82 -18
- data/lib/ldclient-rb/impl/integrations/file_data_source.rb +212 -0
- data/lib/ldclient-rb/impl/integrations/redis_impl.rb +84 -31
- data/lib/ldclient-rb/impl/integrations/test_data/test_data_source.rb +40 -0
- data/lib/ldclient-rb/impl/repeating_task.rb +47 -0
- data/lib/ldclient-rb/impl/util.rb +4 -1
- data/lib/ldclient-rb/integrations/consul.rb +8 -1
- data/lib/ldclient-rb/integrations/dynamodb.rb +47 -2
- data/lib/ldclient-rb/integrations/file_data.rb +108 -0
- data/lib/ldclient-rb/integrations/redis.rb +41 -1
- data/lib/ldclient-rb/integrations/test_data/flag_builder.rb +438 -0
- data/lib/ldclient-rb/integrations/test_data.rb +209 -0
- data/lib/ldclient-rb/integrations/util/store_wrapper.rb +5 -0
- data/lib/ldclient-rb/integrations.rb +2 -51
- data/lib/ldclient-rb/interfaces.rb +151 -1
- data/lib/ldclient-rb/ldclient.rb +43 -9
- data/lib/ldclient-rb/polling.rb +22 -41
- data/lib/ldclient-rb/stream.rb +2 -1
- data/lib/ldclient-rb/util.rb +10 -1
- data/lib/ldclient-rb/version.rb +1 -1
- metadata +14 -7
@@ -16,21 +16,32 @@ module LaunchDarkly
|
|
16
16
|
|
17
17
|
# Used internally to build the state map.
|
18
18
|
# @private
|
19
|
-
def add_flag(
|
20
|
-
key =
|
21
|
-
@flag_values[key] = value
|
19
|
+
def add_flag(flag_state, with_reasons, details_only_if_tracked)
|
20
|
+
key = flag_state[:key]
|
21
|
+
@flag_values[key] = flag_state[:value]
|
22
22
|
meta = {}
|
23
|
-
|
24
|
-
|
25
|
-
|
23
|
+
|
24
|
+
omit_details = false
|
25
|
+
if details_only_if_tracked
|
26
|
+
if !flag_state[:trackEvents] && !flag_state[:trackReason] && !(flag_state[:debugEventsUntilDate] && flag_state[:debugEventsUntilDate] > Impl::Util::current_time_millis)
|
27
|
+
omit_details = true
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
reason = (!with_reasons and !flag_state[:trackReason]) ? nil : flag_state[:reason]
|
32
|
+
|
33
|
+
if !reason.nil? && !omit_details
|
34
|
+
meta[:reason] = reason
|
26
35
|
end
|
27
|
-
|
28
|
-
|
29
|
-
meta[:
|
36
|
+
|
37
|
+
if !omit_details
|
38
|
+
meta[:version] = flag_state[:version]
|
30
39
|
end
|
31
|
-
|
32
|
-
meta[:
|
33
|
-
meta[:
|
40
|
+
|
41
|
+
meta[:variation] = flag_state[:variation] if !flag_state[:variation].nil?
|
42
|
+
meta[:trackEvents] = true if flag_state[:trackEvents]
|
43
|
+
meta[:trackReason] = true if flag_state[:trackReason]
|
44
|
+
meta[:debugEventsUntilDate] = flag_state[:debugEventsUntilDate] if flag_state[:debugEventsUntilDate]
|
34
45
|
@flag_metadata[key] = meta
|
35
46
|
end
|
36
47
|
|
@@ -0,0 +1,117 @@
|
|
1
|
+
require "ldclient-rb/config"
|
2
|
+
require "ldclient-rb/expiring_cache"
|
3
|
+
require "ldclient-rb/impl/repeating_task"
|
4
|
+
require "ldclient-rb/interfaces"
|
5
|
+
require "ldclient-rb/util"
|
6
|
+
|
7
|
+
require "digest"
|
8
|
+
|
9
|
+
module LaunchDarkly
|
10
|
+
module Impl
|
11
|
+
BigSegmentMembershipResult = Struct.new(:membership, :status)
|
12
|
+
|
13
|
+
class BigSegmentStoreManager
|
14
|
+
# use this as a singleton whenever a membership query returns nil; it's safe to reuse it because
|
15
|
+
# we will never modify the membership properties after they're queried
|
16
|
+
EMPTY_MEMBERSHIP = {}
|
17
|
+
|
18
|
+
def initialize(big_segments_config, logger)
|
19
|
+
@store = big_segments_config.store
|
20
|
+
@stale_after_millis = big_segments_config.stale_after * 1000
|
21
|
+
@status_provider = BigSegmentStoreStatusProviderImpl.new(-> { get_status })
|
22
|
+
@logger = logger
|
23
|
+
@last_status = nil
|
24
|
+
|
25
|
+
if !@store.nil?
|
26
|
+
@cache = ExpiringCache.new(big_segments_config.user_cache_size, big_segments_config.user_cache_time)
|
27
|
+
@poll_worker = RepeatingTask.new(big_segments_config.status_poll_interval, 0, -> { poll_store_and_update_status }, logger)
|
28
|
+
@poll_worker.start
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
attr_reader :status_provider
|
33
|
+
|
34
|
+
def stop
|
35
|
+
@poll_worker.stop if !@poll_worker.nil?
|
36
|
+
@store.stop if !@store.nil?
|
37
|
+
end
|
38
|
+
|
39
|
+
def get_user_membership(user_key)
|
40
|
+
return nil if !@store
|
41
|
+
membership = @cache[user_key]
|
42
|
+
if !membership
|
43
|
+
begin
|
44
|
+
membership = @store.get_membership(BigSegmentStoreManager.hash_for_user_key(user_key))
|
45
|
+
membership = EMPTY_MEMBERSHIP if membership.nil?
|
46
|
+
@cache[user_key] = membership
|
47
|
+
rescue => e
|
48
|
+
LaunchDarkly::Util.log_exception(@logger, "Big Segment store membership query returned error", e)
|
49
|
+
return BigSegmentMembershipResult.new(nil, BigSegmentsStatus::STORE_ERROR)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
poll_store_and_update_status if !@last_status
|
53
|
+
if !@last_status.available
|
54
|
+
return BigSegmentMembershipResult.new(membership, BigSegmentsStatus::STORE_ERROR)
|
55
|
+
end
|
56
|
+
BigSegmentMembershipResult.new(membership, @last_status.stale ? BigSegmentsStatus::STALE : BigSegmentsStatus::HEALTHY)
|
57
|
+
end
|
58
|
+
|
59
|
+
def get_status
|
60
|
+
@last_status || poll_store_and_update_status
|
61
|
+
end
|
62
|
+
|
63
|
+
def poll_store_and_update_status
|
64
|
+
new_status = Interfaces::BigSegmentStoreStatus.new(false, false) # default to "unavailable" if we don't get a new status below
|
65
|
+
if !@store.nil?
|
66
|
+
begin
|
67
|
+
metadata = @store.get_metadata
|
68
|
+
new_status = Interfaces::BigSegmentStoreStatus.new(true, !metadata || is_stale(metadata.last_up_to_date))
|
69
|
+
rescue => e
|
70
|
+
LaunchDarkly::Util.log_exception(@logger, "Big Segment store status query returned error", e)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
@last_status = new_status
|
74
|
+
@status_provider.update_status(new_status)
|
75
|
+
|
76
|
+
new_status
|
77
|
+
end
|
78
|
+
|
79
|
+
def is_stale(timestamp)
|
80
|
+
!timestamp || ((Impl::Util.current_time_millis - timestamp) >= @stale_after_millis)
|
81
|
+
end
|
82
|
+
|
83
|
+
def self.hash_for_user_key(user_key)
|
84
|
+
Digest::SHA256.base64digest(user_key)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
#
|
89
|
+
# Default implementation of the BigSegmentStoreStatusProvider interface.
|
90
|
+
#
|
91
|
+
# There isn't much to this because the real implementation is in BigSegmentStoreManager - we pass in a lambda
|
92
|
+
# that allows us to get the current status from that class. Also, the standard Observer methods such as
|
93
|
+
# add_observer are provided for us because BigSegmentStoreStatusProvider mixes in Observer, so all we need to
|
94
|
+
# to do make notifications happen is to call the Observer methods "changed" and "notify_observers".
|
95
|
+
#
|
96
|
+
class BigSegmentStoreStatusProviderImpl
|
97
|
+
include LaunchDarkly::Interfaces::BigSegmentStoreStatusProvider
|
98
|
+
|
99
|
+
def initialize(status_fn)
|
100
|
+
@status_fn = status_fn
|
101
|
+
@last_status = nil
|
102
|
+
end
|
103
|
+
|
104
|
+
def status
|
105
|
+
@status_fn.call
|
106
|
+
end
|
107
|
+
|
108
|
+
def update_status(new_status)
|
109
|
+
if !@last_status || new_status != @last_status
|
110
|
+
@last_status = new_status
|
111
|
+
changed
|
112
|
+
notify_observers(new_status)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -16,16 +16,28 @@ module LaunchDarkly
|
|
16
16
|
# flag data - or nil if the flag is unknown or deleted
|
17
17
|
# @param get_segment [Function] similar to `get_flag`, but is used to query a user segment.
|
18
18
|
# @param logger [Logger] the client's logger
|
19
|
-
def initialize(get_flag, get_segment, logger)
|
19
|
+
def initialize(get_flag, get_segment, get_big_segments_membership, logger)
|
20
20
|
@get_flag = get_flag
|
21
21
|
@get_segment = get_segment
|
22
|
+
@get_big_segments_membership = get_big_segments_membership
|
22
23
|
@logger = logger
|
23
24
|
end
|
24
25
|
|
25
|
-
# Used internally to hold an evaluation result and
|
26
|
-
#
|
27
|
-
#
|
28
|
-
|
26
|
+
# Used internally to hold an evaluation result and additional state that may be accumulated during an
|
27
|
+
# evaluation. It's simpler and a bit more efficient to represent these as mutable properties rather than
|
28
|
+
# trying to use a pure functional approach, and since we're not exposing this object to any application code
|
29
|
+
# or retaining it anywhere, we don't have to be quite as strict about immutability.
|
30
|
+
#
|
31
|
+
# The big_segments_status and big_segments_membership properties are not used by the caller; they are used
|
32
|
+
# during an evaluation to cache the result of any Big Segments query that we've done for this user, because
|
33
|
+
# we don't want to do multiple queries for the same user if multiple Big Segments are referenced in the same
|
34
|
+
# evaluation.
|
35
|
+
EvalResult = Struct.new(
|
36
|
+
:detail, # the EvaluationDetail representing the evaluation result
|
37
|
+
:events, # an array of evaluation events generated by prerequisites, or nil
|
38
|
+
:big_segments_status,
|
39
|
+
:big_segments_membership
|
40
|
+
)
|
29
41
|
|
30
42
|
# Helper function used internally to construct an EvaluationDetail for an error result.
|
31
43
|
def self.error_result(errorKind, value = nil)
|
@@ -42,30 +54,38 @@ module LaunchDarkly
|
|
42
54
|
# evaluated; the caller is responsible for constructing the feature event for the top-level evaluation
|
43
55
|
# @return [EvalResult] the evaluation result
|
44
56
|
def evaluate(flag, user, event_factory)
|
57
|
+
result = EvalResult.new
|
45
58
|
if user.nil? || user[:key].nil?
|
46
|
-
|
59
|
+
result.detail = Evaluator.error_result(EvaluationReason::ERROR_USER_NOT_SPECIFIED)
|
60
|
+
return result
|
47
61
|
end
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
62
|
+
|
63
|
+
detail = eval_internal(flag, user, result, event_factory)
|
64
|
+
if !result.big_segments_status.nil?
|
65
|
+
# If big_segments_status is non-nil at the end of the evaluation, it means a query was done at
|
66
|
+
# some point and we will want to include the status in the evaluation reason.
|
67
|
+
detail = EvaluationDetail.new(detail.value, detail.variation_index,
|
68
|
+
detail.reason.with_big_segments_status(result.big_segments_status))
|
55
69
|
end
|
70
|
+
result.detail = detail
|
71
|
+
return result
|
72
|
+
end
|
56
73
|
|
57
|
-
|
58
|
-
|
74
|
+
def self.make_big_segment_ref(segment) # method is visible for testing
|
75
|
+
# The format of Big Segment references is independent of what store implementation is being
|
76
|
+
# used; the store implementation receives only this string and does not know the details of
|
77
|
+
# the data model. The Relay Proxy will use the same format when writing to the store.
|
78
|
+
"#{segment[:key]}.g#{segment[:generation]}"
|
59
79
|
end
|
60
80
|
|
61
81
|
private
|
62
82
|
|
63
|
-
def eval_internal(flag, user,
|
83
|
+
def eval_internal(flag, user, state, event_factory)
|
64
84
|
if !flag[:on]
|
65
85
|
return get_off_value(flag, EvaluationReason::off)
|
66
86
|
end
|
67
87
|
|
68
|
-
prereq_failure_reason = check_prerequisites(flag, user,
|
88
|
+
prereq_failure_reason = check_prerequisites(flag, user, state, event_factory)
|
69
89
|
if !prereq_failure_reason.nil?
|
70
90
|
return get_off_value(flag, prereq_failure_reason)
|
71
91
|
end
|
@@ -83,7 +103,7 @@ module LaunchDarkly
|
|
83
103
|
rules = flag[:rules] || []
|
84
104
|
rules.each_index do |i|
|
85
105
|
rule = rules[i]
|
86
|
-
if rule_match_user(rule, user)
|
106
|
+
if rule_match_user(rule, user, state)
|
87
107
|
reason = rule[:_reason] # try to use cached reason for this rule
|
88
108
|
reason = EvaluationReason::rule_match(i, rule[:id]) if reason.nil?
|
89
109
|
return get_value_for_variation_or_rollout(flag, rule, user, reason)
|
@@ -98,7 +118,7 @@ module LaunchDarkly
|
|
98
118
|
return EvaluationDetail.new(nil, nil, EvaluationReason::fallthrough)
|
99
119
|
end
|
100
120
|
|
101
|
-
def check_prerequisites(flag, user,
|
121
|
+
def check_prerequisites(flag, user, state, event_factory)
|
102
122
|
(flag[:prerequisites] || []).each do |prerequisite|
|
103
123
|
prereq_ok = true
|
104
124
|
prereq_key = prerequisite[:key]
|
@@ -109,14 +129,15 @@ module LaunchDarkly
|
|
109
129
|
prereq_ok = false
|
110
130
|
else
|
111
131
|
begin
|
112
|
-
prereq_res = eval_internal(prereq_flag, user,
|
132
|
+
prereq_res = eval_internal(prereq_flag, user, state, event_factory)
|
113
133
|
# Note that if the prerequisite flag is off, we don't consider it a match no matter what its
|
114
134
|
# off variation was. But we still need to evaluate it in order to generate an event.
|
115
135
|
if !prereq_flag[:on] || prereq_res.variation_index != prerequisite[:variation]
|
116
136
|
prereq_ok = false
|
117
137
|
end
|
118
138
|
event = event_factory.new_eval_event(prereq_flag, user, prereq_res, nil, flag)
|
119
|
-
events.
|
139
|
+
state.events = [] if state.events.nil?
|
140
|
+
state.events.push(event)
|
120
141
|
rescue => exn
|
121
142
|
Util.log_exception(@logger, "Error evaluating prerequisite flag \"#{prereq_key}\" for flag \"#{flag[:key]}\"", exn)
|
122
143
|
prereq_ok = false
|
@@ -130,23 +151,23 @@ module LaunchDarkly
|
|
130
151
|
nil
|
131
152
|
end
|
132
153
|
|
133
|
-
def rule_match_user(rule, user)
|
154
|
+
def rule_match_user(rule, user, state)
|
134
155
|
return false if !rule[:clauses]
|
135
156
|
|
136
157
|
(rule[:clauses] || []).each do |clause|
|
137
|
-
return false if !clause_match_user(clause, user)
|
158
|
+
return false if !clause_match_user(clause, user, state)
|
138
159
|
end
|
139
160
|
|
140
161
|
return true
|
141
162
|
end
|
142
163
|
|
143
|
-
def clause_match_user(clause, user)
|
164
|
+
def clause_match_user(clause, user, state)
|
144
165
|
# In the case of a segment match operator, we check if the user is in any of the segments,
|
145
166
|
# and possibly negate
|
146
167
|
if clause[:op].to_sym == :segmentMatch
|
147
168
|
result = (clause[:values] || []).any? { |v|
|
148
169
|
segment = @get_segment.call(v)
|
149
|
-
!segment.nil? && segment_match_user(segment, user)
|
170
|
+
!segment.nil? && segment_match_user(segment, user, state)
|
150
171
|
}
|
151
172
|
clause[:negate] ? !result : result
|
152
173
|
else
|
@@ -168,11 +189,42 @@ module LaunchDarkly
|
|
168
189
|
clause[:negate] ? !result : result
|
169
190
|
end
|
170
191
|
|
171
|
-
def segment_match_user(segment, user)
|
192
|
+
def segment_match_user(segment, user, state)
|
172
193
|
return false unless user[:key]
|
194
|
+
segment[:unbounded] ? big_segment_match_user(segment, user, state) : simple_segment_match_user(segment, user, true)
|
195
|
+
end
|
173
196
|
|
174
|
-
|
175
|
-
|
197
|
+
def big_segment_match_user(segment, user, state)
|
198
|
+
if !segment[:generation]
|
199
|
+
# Big segment queries can only be done if the generation is known. If it's unset,
|
200
|
+
# that probably means the data store was populated by an older SDK that doesn't know
|
201
|
+
# about the generation property and therefore dropped it from the JSON data. We'll treat
|
202
|
+
# that as a "not configured" condition.
|
203
|
+
state.big_segments_status = BigSegmentsStatus::NOT_CONFIGURED
|
204
|
+
return false
|
205
|
+
end
|
206
|
+
if !state.big_segments_status
|
207
|
+
result = @get_big_segments_membership.nil? ? nil : @get_big_segments_membership.call(user[:key])
|
208
|
+
if result
|
209
|
+
state.big_segments_membership = result.membership
|
210
|
+
state.big_segments_status = result.status
|
211
|
+
else
|
212
|
+
state.big_segments_membership = nil
|
213
|
+
state.big_segments_status = BigSegmentsStatus::NOT_CONFIGURED
|
214
|
+
end
|
215
|
+
end
|
216
|
+
segment_ref = Evaluator.make_big_segment_ref(segment)
|
217
|
+
membership = state.big_segments_membership
|
218
|
+
included = membership.nil? ? nil : membership[segment_ref]
|
219
|
+
return included if !included.nil?
|
220
|
+
simple_segment_match_user(segment, user, false)
|
221
|
+
end
|
222
|
+
|
223
|
+
def simple_segment_match_user(segment, user, use_includes_and_excludes)
|
224
|
+
if use_includes_and_excludes
|
225
|
+
return true if segment[:included].include?(user[:key])
|
226
|
+
return false if segment[:excluded].include?(user[:key])
|
227
|
+
end
|
176
228
|
|
177
229
|
(segment[:rules] || []).each do |r|
|
178
230
|
return true if segment_rule_match_user(r, user, segment[:key], segment[:salt])
|
@@ -89,7 +89,7 @@ module LaunchDarkly
|
|
89
89
|
|
90
90
|
private
|
91
91
|
|
92
|
-
BUILTINS = Set[:key, :ip, :country, :email, :firstName, :lastName, :avatar, :name, :anonymous]
|
92
|
+
BUILTINS = Set[:key, :secondary, :ip, :country, :email, :firstName, :lastName, :avatar, :name, :anonymous]
|
93
93
|
NUMERIC_VERSION_COMPONENTS_REGEX = Regexp.new("^[0-9.]*")
|
94
94
|
|
95
95
|
private_constant :BUILTINS
|
@@ -13,7 +13,7 @@ module LaunchDarkly
|
|
13
13
|
end
|
14
14
|
|
15
15
|
def new_eval_event(flag, user, detail, default_value, prereq_of_flag = nil)
|
16
|
-
add_experiment_data = is_experiment(flag, detail.reason)
|
16
|
+
add_experiment_data = self.class.is_experiment(flag, detail.reason)
|
17
17
|
e = {
|
18
18
|
kind: 'feature',
|
19
19
|
key: flag[:key],
|
@@ -91,17 +91,7 @@ module LaunchDarkly
|
|
91
91
|
e
|
92
92
|
end
|
93
93
|
|
94
|
-
|
95
|
-
|
96
|
-
def context_to_context_kind(user)
|
97
|
-
if !user.nil? && user[:anonymous]
|
98
|
-
return "anonymousUser"
|
99
|
-
else
|
100
|
-
return "user"
|
101
|
-
end
|
102
|
-
end
|
103
|
-
|
104
|
-
def is_experiment(flag, reason)
|
94
|
+
def self.is_experiment(flag, reason)
|
105
95
|
return false if !reason
|
106
96
|
|
107
97
|
if reason.in_experiment
|
@@ -121,6 +111,13 @@ module LaunchDarkly
|
|
121
111
|
false
|
122
112
|
end
|
123
113
|
|
114
|
+
private def context_to_context_kind(user)
|
115
|
+
if !user.nil? && user[:anonymous]
|
116
|
+
return "anonymousUser"
|
117
|
+
else
|
118
|
+
return "user"
|
119
|
+
end
|
120
|
+
end
|
124
121
|
end
|
125
122
|
end
|
126
123
|
end
|
@@ -4,10 +4,7 @@ module LaunchDarkly
|
|
4
4
|
module Impl
|
5
5
|
module Integrations
|
6
6
|
module DynamoDB
|
7
|
-
|
8
|
-
# Internal implementation of the DynamoDB feature store, intended to be used with CachingStoreWrapper.
|
9
|
-
#
|
10
|
-
class DynamoDBFeatureStoreCore
|
7
|
+
class DynamoDBStoreImplBase
|
11
8
|
begin
|
12
9
|
require "aws-sdk-dynamodb"
|
13
10
|
AWS_SDK_ENABLED = true
|
@@ -19,29 +16,50 @@ module LaunchDarkly
|
|
19
16
|
AWS_SDK_ENABLED = false
|
20
17
|
end
|
21
18
|
end
|
22
|
-
|
19
|
+
|
23
20
|
PARTITION_KEY = "namespace"
|
24
21
|
SORT_KEY = "key"
|
25
22
|
|
26
|
-
VERSION_ATTRIBUTE = "version"
|
27
|
-
ITEM_JSON_ATTRIBUTE = "item"
|
28
|
-
|
29
23
|
def initialize(table_name, opts)
|
30
24
|
if !AWS_SDK_ENABLED
|
31
|
-
raise RuntimeError.new("can't use
|
25
|
+
raise RuntimeError.new("can't use #{description} without the aws-sdk or aws-sdk-dynamodb gem")
|
32
26
|
end
|
33
|
-
|
27
|
+
|
34
28
|
@table_name = table_name
|
35
|
-
@prefix = opts[:prefix]
|
29
|
+
@prefix = opts[:prefix] ? (opts[:prefix] + ":") : ""
|
36
30
|
@logger = opts[:logger] || Config.default_logger
|
37
|
-
|
31
|
+
|
38
32
|
if !opts[:existing_client].nil?
|
39
33
|
@client = opts[:existing_client]
|
40
34
|
else
|
41
35
|
@client = Aws::DynamoDB::Client.new(opts[:dynamodb_opts] || {})
|
42
36
|
end
|
37
|
+
|
38
|
+
@logger.info("#{description}: using DynamoDB table \"#{table_name}\"")
|
39
|
+
end
|
40
|
+
|
41
|
+
def stop
|
42
|
+
# AWS client doesn't seem to have a close method
|
43
|
+
end
|
43
44
|
|
44
|
-
|
45
|
+
protected def description
|
46
|
+
"DynamoDB"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
#
|
51
|
+
# Internal implementation of the DynamoDB feature store, intended to be used with CachingStoreWrapper.
|
52
|
+
#
|
53
|
+
class DynamoDBFeatureStoreCore < DynamoDBStoreImplBase
|
54
|
+
VERSION_ATTRIBUTE = "version"
|
55
|
+
ITEM_JSON_ATTRIBUTE = "item"
|
56
|
+
|
57
|
+
def initialize(table_name, opts)
|
58
|
+
super(table_name, opts)
|
59
|
+
end
|
60
|
+
|
61
|
+
def description
|
62
|
+
"DynamoDBFeatureStore"
|
45
63
|
end
|
46
64
|
|
47
65
|
def init_internal(all_data)
|
@@ -124,14 +142,10 @@ module LaunchDarkly
|
|
124
142
|
!resp.item.nil? && resp.item.length > 0
|
125
143
|
end
|
126
144
|
|
127
|
-
def stop
|
128
|
-
# AWS client doesn't seem to have a close method
|
129
|
-
end
|
130
|
-
|
131
145
|
private
|
132
146
|
|
133
147
|
def prefixed_namespace(base_str)
|
134
|
-
|
148
|
+
@prefix + base_str
|
135
149
|
end
|
136
150
|
|
137
151
|
def namespace_for_kind(kind)
|
@@ -208,6 +222,56 @@ module LaunchDarkly
|
|
208
222
|
end
|
209
223
|
end
|
210
224
|
|
225
|
+
class DynamoDBBigSegmentStore < DynamoDBStoreImplBase
|
226
|
+
KEY_METADATA = 'big_segments_metadata';
|
227
|
+
KEY_USER_DATA = 'big_segments_user';
|
228
|
+
ATTR_SYNC_TIME = 'synchronizedOn';
|
229
|
+
ATTR_INCLUDED = 'included';
|
230
|
+
ATTR_EXCLUDED = 'excluded';
|
231
|
+
|
232
|
+
def initialize(table_name, opts)
|
233
|
+
super(table_name, opts)
|
234
|
+
end
|
235
|
+
|
236
|
+
def description
|
237
|
+
"DynamoDBBigSegmentStore"
|
238
|
+
end
|
239
|
+
|
240
|
+
def get_metadata
|
241
|
+
key = @prefix + KEY_METADATA
|
242
|
+
data = @client.get_item(
|
243
|
+
table_name: @table_name,
|
244
|
+
key: {
|
245
|
+
PARTITION_KEY => key,
|
246
|
+
SORT_KEY => key
|
247
|
+
}
|
248
|
+
)
|
249
|
+
timestamp = data.item && data.item[ATTR_SYNC_TIME] ?
|
250
|
+
data.item[ATTR_SYNC_TIME] : nil
|
251
|
+
LaunchDarkly::Interfaces::BigSegmentStoreMetadata.new(timestamp)
|
252
|
+
end
|
253
|
+
|
254
|
+
def get_membership(user_hash)
|
255
|
+
data = @client.get_item(
|
256
|
+
table_name: @table_name,
|
257
|
+
key: {
|
258
|
+
PARTITION_KEY => @prefix + KEY_USER_DATA,
|
259
|
+
SORT_KEY => user_hash
|
260
|
+
})
|
261
|
+
return nil if !data.item
|
262
|
+
excluded_refs = data.item[ATTR_EXCLUDED] || []
|
263
|
+
included_refs = data.item[ATTR_INCLUDED] || []
|
264
|
+
if excluded_refs.empty? && included_refs.empty?
|
265
|
+
nil
|
266
|
+
else
|
267
|
+
membership = {}
|
268
|
+
excluded_refs.each { |ref| membership[ref] = false }
|
269
|
+
included_refs.each { |ref| membership[ref] = true }
|
270
|
+
membership
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
211
275
|
class DynamoDBUtil
|
212
276
|
#
|
213
277
|
# Calls client.batch_write_item as many times as necessary to submit all of the given requests.
|