ldclient-rb 5.4.3 → 5.5.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 +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
|