ldclient-rb 5.4.3 → 5.5.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 +33 -6
- data/CHANGELOG.md +19 -0
- data/CONTRIBUTING.md +0 -12
- data/Gemfile.lock +22 -3
- data/README.md +41 -35
- data/ldclient-rb.gemspec +4 -3
- data/lib/ldclient-rb.rb +9 -1
- data/lib/ldclient-rb/cache_store.rb +1 -0
- data/lib/ldclient-rb/config.rb +201 -90
- data/lib/ldclient-rb/evaluation.rb +56 -8
- data/lib/ldclient-rb/event_summarizer.rb +3 -0
- data/lib/ldclient-rb/events.rb +16 -0
- data/lib/ldclient-rb/expiring_cache.rb +1 -0
- data/lib/ldclient-rb/file_data_source.rb +18 -13
- data/lib/ldclient-rb/flags_state.rb +3 -2
- 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 +15 -4
- 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 +135 -77
- data/lib/ldclient-rb/memoized_value.rb +2 -0
- data/lib/ldclient-rb/newrelic.rb +1 -0
- data/lib/ldclient-rb/non_blocking_thread_pool.rb +3 -3
- data/lib/ldclient-rb/polling.rb +1 -0
- data/lib/ldclient-rb/redis_store.rb +24 -190
- data/lib/ldclient-rb/requestor.rb +3 -2
- data/lib/ldclient-rb/simple_lru_cache.rb +1 -0
- data/lib/ldclient-rb/stream.rb +22 -10
- data/lib/ldclient-rb/user_filter.rb +1 -0
- data/lib/ldclient-rb/util.rb +1 -0
- data/lib/ldclient-rb/version.rb +1 -1
- data/scripts/gendocs.sh +12 -0
- data/spec/feature_store_spec_base.rb +173 -72
- data/spec/file_data_source_spec.rb +2 -2
- data/spec/http_util.rb +103 -0
- data/spec/in_memory_feature_store_spec.rb +1 -1
- data/spec/integrations/consul_feature_store_spec.rb +41 -0
- data/spec/integrations/dynamodb_feature_store_spec.rb +104 -0
- data/spec/integrations/store_wrapper_spec.rb +276 -0
- data/spec/ldclient_spec.rb +83 -4
- data/spec/redis_feature_store_spec.rb +25 -16
- data/spec/requestor_spec.rb +44 -38
- data/spec/stream_spec.rb +18 -18
- metadata +55 -33
- data/lib/sse_client.rb +0 -4
- data/lib/sse_client/backoff.rb +0 -38
- data/lib/sse_client/sse_client.rb +0 -171
- data/lib/sse_client/sse_events.rb +0 -67
- data/lib/sse_client/streaming_http.rb +0 -199
- data/spec/sse_client/sse_client_spec.rb +0 -177
- data/spec/sse_client/sse_events_spec.rb +0 -100
- data/spec/sse_client/sse_shared.rb +0 -82
- data/spec/sse_client/streaming_http_spec.rb +0 -263
@@ -2,7 +2,7 @@ require "date"
|
|
2
2
|
require "semantic"
|
3
3
|
|
4
4
|
module LaunchDarkly
|
5
|
-
# An object returned by
|
5
|
+
# An object returned by {LDClient#variation_detail}, combining the result of a flag evaluation with
|
6
6
|
# an explanation of how it was calculated.
|
7
7
|
class EvaluationDetail
|
8
8
|
def initialize(value, variation_index, reason)
|
@@ -11,19 +11,66 @@ module LaunchDarkly
|
|
11
11
|
@reason = reason
|
12
12
|
end
|
13
13
|
|
14
|
-
#
|
15
|
-
#
|
14
|
+
#
|
15
|
+
# The result of the flag evaluation. This will be either one of the flag's variations, or the
|
16
|
+
# default value that was passed to {LDClient#variation_detail}. It is the same as the return
|
17
|
+
# value of {LDClient#variation}.
|
18
|
+
#
|
19
|
+
# @return [Object]
|
20
|
+
#
|
16
21
|
attr_reader :value
|
17
22
|
|
18
|
-
#
|
19
|
-
#
|
23
|
+
#
|
24
|
+
# The index of the returned value within the flag's list of variations. The first variation is
|
25
|
+
# 0, the second is 1, etc. This is `nil` if the default value was returned.
|
26
|
+
#
|
27
|
+
# @return [int|nil]
|
28
|
+
#
|
20
29
|
attr_reader :variation_index
|
21
30
|
|
22
|
-
#
|
31
|
+
#
|
32
|
+
# An object describing the main factor that influenced the flag evaluation value.
|
33
|
+
#
|
34
|
+
# This object is currently represented as a Hash, which may have the following keys:
|
35
|
+
#
|
36
|
+
# `:kind`: The general category of reason. Possible values:
|
37
|
+
#
|
38
|
+
# * `'OFF'`: the flag was off and therefore returned its configured off value
|
39
|
+
# * `'FALLTHROUGH'`: the flag was on but the user did not match any targets or rules
|
40
|
+
# * `'TARGET_MATCH'`: the user key was specifically targeted for this flag
|
41
|
+
# * `'RULE_MATCH'`: the user matched one of the flag's rules
|
42
|
+
# * `'PREREQUISITE_FAILED`': the flag was considered off because it had at least one
|
43
|
+
# prerequisite flag that either was off or did not return the desired variation
|
44
|
+
# * `'ERROR'`: the flag could not be evaluated, so the default value was returned
|
45
|
+
#
|
46
|
+
# `:ruleIndex`: If the kind was `RULE_MATCH`, this is the positional index of the
|
47
|
+
# matched rule (0 for the first rule).
|
48
|
+
#
|
49
|
+
# `:ruleId`: If the kind was `RULE_MATCH`, this is the rule's unique identifier.
|
50
|
+
#
|
51
|
+
# `:prerequisiteKey`: If the kind was `PREREQUISITE_FAILED`, this is the flag key of
|
52
|
+
# the prerequisite flag that failed.
|
53
|
+
#
|
54
|
+
# `:errorKind`: If the kind was `ERROR`, this indicates the type of error:
|
55
|
+
#
|
56
|
+
# * `'CLIENT_NOT_READY'`: the caller tried to evaluate a flag before the client had
|
57
|
+
# successfully initialized
|
58
|
+
# * `'FLAG_NOT_FOUND'`: the caller provided a flag key that did not match any known flag
|
59
|
+
# * `'MALFORMED_FLAG'`: there was an internal inconsistency in the flag data, e.g. a
|
60
|
+
# rule specified a nonexistent variation
|
61
|
+
# * `'USER_NOT_SPECIFIED'`: the user object or user key was not provied
|
62
|
+
# * `'EXCEPTION'`: an unexpected exception stopped flag evaluation
|
63
|
+
#
|
64
|
+
# @return [Hash]
|
65
|
+
#
|
23
66
|
attr_reader :reason
|
24
67
|
|
25
|
-
#
|
26
|
-
#
|
68
|
+
#
|
69
|
+
# Tests whether the flag evaluation returned a default value. This is the same as checking
|
70
|
+
# whether {#variation_index} is nil.
|
71
|
+
#
|
72
|
+
# @return [Boolean]
|
73
|
+
#
|
27
74
|
def default_value?
|
28
75
|
variation_index.nil?
|
29
76
|
end
|
@@ -33,6 +80,7 @@ module LaunchDarkly
|
|
33
80
|
end
|
34
81
|
end
|
35
82
|
|
83
|
+
# @private
|
36
84
|
module Evaluation
|
37
85
|
BUILTINS = [:key, :ip, :country, :email, :firstName, :lastName, :avatar, :name, :anonymous]
|
38
86
|
|
@@ -1,11 +1,14 @@
|
|
1
1
|
|
2
2
|
module LaunchDarkly
|
3
|
+
# @private
|
3
4
|
EventSummary = Struct.new(:start_date, :end_date, :counters)
|
4
5
|
|
5
6
|
# Manages the state of summarizable information for the EventProcessor, including the
|
6
7
|
# event counters and user deduplication. Note that the methods of this class are
|
7
8
|
# deliberately not thread-safe; the EventProcessor is responsible for enforcing
|
8
9
|
# synchronization across both the summarizer and the event queue.
|
10
|
+
#
|
11
|
+
# @private
|
9
12
|
class EventSummarizer
|
10
13
|
def initialize
|
11
14
|
clear
|
data/lib/ldclient-rb/events.rb
CHANGED
@@ -9,6 +9,10 @@ module LaunchDarkly
|
|
9
9
|
MAX_FLUSH_WORKERS = 5
|
10
10
|
CURRENT_SCHEMA_VERSION = 3
|
11
11
|
|
12
|
+
private_constant :MAX_FLUSH_WORKERS
|
13
|
+
private_constant :CURRENT_SCHEMA_VERSION
|
14
|
+
|
15
|
+
# @private
|
12
16
|
class NullEventProcessor
|
13
17
|
def add_event(event)
|
14
18
|
end
|
@@ -20,6 +24,7 @@ module LaunchDarkly
|
|
20
24
|
end
|
21
25
|
end
|
22
26
|
|
27
|
+
# @private
|
23
28
|
class EventMessage
|
24
29
|
def initialize(event)
|
25
30
|
@event = event
|
@@ -27,12 +32,15 @@ module LaunchDarkly
|
|
27
32
|
attr_reader :event
|
28
33
|
end
|
29
34
|
|
35
|
+
# @private
|
30
36
|
class FlushMessage
|
31
37
|
end
|
32
38
|
|
39
|
+
# @private
|
33
40
|
class FlushUsersMessage
|
34
41
|
end
|
35
42
|
|
43
|
+
# @private
|
36
44
|
class SynchronousMessage
|
37
45
|
def initialize
|
38
46
|
@reply = Concurrent::Semaphore.new(0)
|
@@ -47,12 +55,15 @@ module LaunchDarkly
|
|
47
55
|
end
|
48
56
|
end
|
49
57
|
|
58
|
+
# @private
|
50
59
|
class TestSyncMessage < SynchronousMessage
|
51
60
|
end
|
52
61
|
|
62
|
+
# @private
|
53
63
|
class StopMessage < SynchronousMessage
|
54
64
|
end
|
55
65
|
|
66
|
+
# @private
|
56
67
|
class EventProcessor
|
57
68
|
def initialize(sdk_key, config, client = nil)
|
58
69
|
@queue = Queue.new
|
@@ -99,6 +110,7 @@ module LaunchDarkly
|
|
99
110
|
end
|
100
111
|
end
|
101
112
|
|
113
|
+
# @private
|
102
114
|
class EventDispatcher
|
103
115
|
def initialize(queue, sdk_key, config, client)
|
104
116
|
@sdk_key = sdk_key
|
@@ -252,8 +264,10 @@ module LaunchDarkly
|
|
252
264
|
end
|
253
265
|
end
|
254
266
|
|
267
|
+
# @private
|
255
268
|
FlushPayload = Struct.new(:events, :summary)
|
256
269
|
|
270
|
+
# @private
|
257
271
|
class EventBuffer
|
258
272
|
def initialize(capacity, logger)
|
259
273
|
@capacity = capacity
|
@@ -290,6 +304,7 @@ module LaunchDarkly
|
|
290
304
|
end
|
291
305
|
end
|
292
306
|
|
307
|
+
# @private
|
293
308
|
class EventPayloadSendTask
|
294
309
|
def run(sdk_key, config, client, payload, formatter)
|
295
310
|
events_out = formatter.make_output_events(payload.events, payload.summary)
|
@@ -327,6 +342,7 @@ module LaunchDarkly
|
|
327
342
|
end
|
328
343
|
end
|
329
344
|
|
345
|
+
# @private
|
330
346
|
class EventOutputFormatter
|
331
347
|
def initialize(config)
|
332
348
|
@inline_users = config.inline_users_in_events
|
@@ -7,12 +7,15 @@ module LaunchDarkly
|
|
7
7
|
# To avoid pulling in 'listen' and its transitive dependencies for people who aren't using the
|
8
8
|
# file data source or who don't need auto-updating, we only enable auto-update if the 'listen'
|
9
9
|
# gem has been provided by the host app.
|
10
|
+
# @private
|
10
11
|
@@have_listen = false
|
11
12
|
begin
|
12
13
|
require 'listen'
|
13
14
|
@@have_listen = true
|
14
15
|
rescue LoadError
|
15
16
|
end
|
17
|
+
|
18
|
+
# @private
|
16
19
|
def self.have_listen?
|
17
20
|
@@have_listen
|
18
21
|
end
|
@@ -22,30 +25,32 @@ module LaunchDarkly
|
|
22
25
|
# used in a test environment, to operate using a predetermined feature flag state without an
|
23
26
|
# actual LaunchDarkly connection.
|
24
27
|
#
|
25
|
-
# To use this component, call
|
26
|
-
#
|
28
|
+
# To use this component, call {FileDataSource#factory}, and store its return value in the
|
29
|
+
# {Config#data_source} property of your LaunchDarkly client configuration. In the options
|
27
30
|
# to `factory`, set `paths` to the file path(s) of your data file(s):
|
28
31
|
#
|
29
|
-
#
|
30
|
-
# config = LaunchDarkly::Config.new(
|
32
|
+
# file_source = FileDataSource.factory(paths: [ myFilePath ])
|
33
|
+
# config = LaunchDarkly::Config.new(data_source: file_source)
|
31
34
|
#
|
32
35
|
# This will cause the client not to connect to LaunchDarkly to get feature flags. The
|
33
36
|
# client may still make network connections to send analytics events, unless you have disabled
|
34
|
-
# this with Config
|
37
|
+
# this with {Config#send_events} or {Config#offline?}.
|
35
38
|
#
|
36
39
|
# Flag data files can be either JSON or YAML. They contain an object with three possible
|
37
40
|
# properties:
|
38
41
|
#
|
39
|
-
# -
|
40
|
-
# -
|
41
|
-
# -
|
42
|
+
# - `flags`: Feature flag definitions.
|
43
|
+
# - `flagValues`: Simplified feature flags that contain only a value.
|
44
|
+
# - `segments`: User segment definitions.
|
42
45
|
#
|
43
|
-
# The format of the data in
|
46
|
+
# The format of the data in `flags` and `segments` is defined by the LaunchDarkly application
|
44
47
|
# and is subject to change. Rather than trying to construct these objects yourself, it is simpler
|
45
48
|
# to request existing flags directly from the LaunchDarkly server in JSON format, and use this
|
46
49
|
# output as the starting point for your file. In Linux you would do this:
|
47
50
|
#
|
48
|
-
#
|
51
|
+
# ```
|
52
|
+
# curl -H "Authorization: YOUR_SDK_KEY" https://app.launchdarkly.com/sdk/latest-all
|
53
|
+
# ```
|
49
54
|
#
|
50
55
|
# The output will look something like this (but with many more properties):
|
51
56
|
#
|
@@ -108,14 +113,14 @@ module LaunchDarkly
|
|
108
113
|
# @option options [Float] :poll_interval The minimum interval, in seconds, between checks for
|
109
114
|
# file modifications - used only if auto_update is true, and if the native file-watching
|
110
115
|
# mechanism from 'listen' is not being used. The default value is 1 second.
|
116
|
+
# @return an object that can be stored in {Config#data_source}
|
111
117
|
#
|
112
118
|
def self.factory(options={})
|
113
|
-
return
|
114
|
-
FileDataSourceImpl.new(config.feature_store, config.logger, options)
|
115
|
-
end
|
119
|
+
return lambda { |sdk_key, config| FileDataSourceImpl.new(config.feature_store, config.logger, options) }
|
116
120
|
end
|
117
121
|
end
|
118
122
|
|
123
|
+
# @private
|
119
124
|
class FileDataSourceImpl
|
120
125
|
def initialize(feature_store, logger, options={})
|
121
126
|
@feature_store = feature_store
|
@@ -3,8 +3,8 @@ require 'json'
|
|
3
3
|
module LaunchDarkly
|
4
4
|
#
|
5
5
|
# A snapshot of the state of all feature flags with regard to a specific user, generated by
|
6
|
-
# calling the
|
7
|
-
# JSON.generate (or the to_json method) will produce the appropriate data structure for
|
6
|
+
# calling the {LDClient#all_flags_state}. Serializing this object to JSON using
|
7
|
+
# `JSON.generate` (or the `to_json` method) will produce the appropriate data structure for
|
8
8
|
# bootstrapping the LaunchDarkly JavaScript client.
|
9
9
|
#
|
10
10
|
class FeatureFlagsState
|
@@ -15,6 +15,7 @@ module LaunchDarkly
|
|
15
15
|
end
|
16
16
|
|
17
17
|
# Used internally to build the state map.
|
18
|
+
# @private
|
18
19
|
def add_flag(flag, value, variation, reason = nil, details_only_if_tracked = false)
|
19
20
|
key = flag[:key]
|
20
21
|
@flag_values[key] = value
|
@@ -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
|