redis-cluster-client 0.3.15 → 0.4.1
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
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3edb3d83fd54184b6e3069f156944fb1e51459dea9f72f6a149a2fba1fe1cfd8
|
4
|
+
data.tar.gz: 02fbd7531bcfcca5363351b3be60c832c43df320748f8641a39073ef6c08490a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9a7aece18852c1b31fad6f4d3fdd43d0201496439ce2bd5a1eea96682d916cfa99217983e96067e29dfc41001c0d35690c2c8054e9dbf299520a564d80568470
|
7
|
+
data.tar.gz: 8ba145cc9c9d017c768ae15c44483512d033ac24256bdb46953ec25a9d28d404a6d9a3297cdb31445448ee0d82cda2036328d674db6dc35fab8fe14037ebced1
|
@@ -43,7 +43,7 @@ class RedisClient
|
|
43
43
|
clients.each_slice(::RedisClient::Cluster::Node::MAX_THREADS).each_with_object({}) do |chuncked_clients, acc|
|
44
44
|
threads = chuncked_clients.map do |k, v|
|
45
45
|
Thread.new(k, v) do |node_key, client|
|
46
|
-
Thread.current
|
46
|
+
Thread.current[:node_key] = node_key
|
47
47
|
|
48
48
|
min = DUMMY_LATENCY_NSEC
|
49
49
|
MEASURE_ATTEMPT_COUNT.times do
|
@@ -53,15 +53,15 @@ class RedisClient
|
|
53
53
|
min = duration if duration < min
|
54
54
|
end
|
55
55
|
|
56
|
-
Thread.current
|
56
|
+
Thread.current[:latency] = min
|
57
57
|
rescue StandardError
|
58
|
-
Thread.current
|
58
|
+
Thread.current[:latency] = DUMMY_LATENCY_NSEC
|
59
59
|
end
|
60
60
|
end
|
61
61
|
|
62
62
|
threads.each do |t|
|
63
63
|
t.join
|
64
|
-
acc[t
|
64
|
+
acc[t[:node_key]] = t[:latency]
|
65
65
|
end
|
66
66
|
end
|
67
67
|
end
|
@@ -18,7 +18,6 @@ class RedisClient
|
|
18
18
|
MAX_STARTUP_SAMPLE = 37
|
19
19
|
MAX_THREADS = Integer(ENV.fetch('REDIS_CLIENT_MAX_THREADS', 5))
|
20
20
|
IGNORE_GENERIC_CONFIG_KEYS = %i[url host port path].freeze
|
21
|
-
SLOT_OPTIMIZATION_STRING = '0' * SLOT_SIZE
|
22
21
|
|
23
22
|
ReloadNeeded = Class.new(::RedisClient::Error)
|
24
23
|
|
@@ -37,27 +36,36 @@ class RedisClient
|
|
37
36
|
end
|
38
37
|
end
|
39
38
|
|
40
|
-
|
39
|
+
class CharArray
|
40
|
+
BASE = ''
|
41
|
+
PADDING = '0'
|
42
|
+
|
43
|
+
def initialize(size, elements)
|
44
|
+
@elements = elements
|
45
|
+
@string = String.new(BASE, encoding: Encoding::BINARY, capacity: size)
|
46
|
+
size.times { @string << PADDING }
|
47
|
+
end
|
48
|
+
|
41
49
|
def [](index)
|
42
50
|
raise IndexError if index < 0
|
43
|
-
return if index >= string.bytesize
|
51
|
+
return if index >= @string.bytesize
|
44
52
|
|
45
|
-
elements[string.getbyte(index)]
|
53
|
+
@elements[@string.getbyte(index)]
|
46
54
|
end
|
47
55
|
|
48
56
|
def []=(index, element)
|
49
57
|
raise IndexError if index < 0
|
50
|
-
return if index >= string.bytesize
|
58
|
+
return if index >= @string.bytesize
|
51
59
|
|
52
|
-
pos = elements.find_index(element) # O(N)
|
60
|
+
pos = @elements.find_index(element) # O(N)
|
53
61
|
if pos.nil?
|
54
|
-
raise(RangeError, 'full of elements') if elements.size >= 256
|
62
|
+
raise(RangeError, 'full of elements') if @elements.size >= 256
|
55
63
|
|
56
|
-
pos = elements.size
|
57
|
-
elements << element
|
64
|
+
pos = @elements.size
|
65
|
+
@elements << element
|
58
66
|
end
|
59
67
|
|
60
|
-
string.setbyte(index, pos)
|
68
|
+
@string.setbyte(index, pos)
|
61
69
|
end
|
62
70
|
end
|
63
71
|
|
@@ -85,11 +93,11 @@ class RedisClient
|
|
85
93
|
startup_nodes.each_slice(MAX_THREADS).with_index do |chuncked_startup_nodes, chuncked_idx|
|
86
94
|
threads = chuncked_startup_nodes.each_with_index.map do |raw_client, idx|
|
87
95
|
Thread.new(raw_client, (MAX_THREADS * chuncked_idx) + idx) do |cli, i|
|
88
|
-
Thread.current
|
96
|
+
Thread.current[:index] = i
|
89
97
|
reply = cli.call('CLUSTER', 'NODES')
|
90
|
-
Thread.current
|
98
|
+
Thread.current[:info] = parse_cluster_node_reply(reply)
|
91
99
|
rescue StandardError => e
|
92
|
-
Thread.current
|
100
|
+
Thread.current[:error] = e
|
93
101
|
ensure
|
94
102
|
cli&.close
|
95
103
|
end
|
@@ -97,12 +105,12 @@ class RedisClient
|
|
97
105
|
|
98
106
|
threads.each do |t|
|
99
107
|
t.join
|
100
|
-
if t.
|
108
|
+
if t.key?(:info)
|
101
109
|
node_info_list ||= Array.new(startup_size)
|
102
|
-
node_info_list[t
|
103
|
-
elsif t.
|
110
|
+
node_info_list[t[:index]] = t[:info]
|
111
|
+
elsif t.key?(:error)
|
104
112
|
errors ||= Array.new(startup_size)
|
105
|
-
errors[t
|
113
|
+
errors[t[:index]] = t[:error]
|
106
114
|
end
|
107
115
|
end
|
108
116
|
end
|
@@ -268,10 +276,8 @@ class RedisClient
|
|
268
276
|
def make_array_for_slot_node_mappings(node_info_list)
|
269
277
|
return Array.new(SLOT_SIZE) if node_info_list.count(&:primary?) > 256
|
270
278
|
|
271
|
-
|
272
|
-
|
273
|
-
elements: node_info_list.select(&:primary?).map(&:node_key)
|
274
|
-
)
|
279
|
+
primary_node_keys = node_info_list.select(&:primary?).map(&:node_key)
|
280
|
+
::RedisClient::Cluster::Node::CharArray.new(SLOT_SIZE, primary_node_keys)
|
275
281
|
end
|
276
282
|
|
277
283
|
def build_replication_mappings(node_info_list) # rubocop:disable Metrics/AbcSize
|
@@ -303,22 +309,22 @@ class RedisClient
|
|
303
309
|
clients.each_slice(MAX_THREADS) do |chuncked_clients|
|
304
310
|
threads = chuncked_clients.map do |k, v|
|
305
311
|
Thread.new(k, v) do |node_key, client|
|
306
|
-
Thread.current
|
312
|
+
Thread.current[:node_key] = node_key
|
307
313
|
reply = yield(node_key, client)
|
308
|
-
Thread.current
|
314
|
+
Thread.current[:result] = reply
|
309
315
|
rescue StandardError => e
|
310
|
-
Thread.current
|
316
|
+
Thread.current[:error] = e
|
311
317
|
end
|
312
318
|
end
|
313
319
|
|
314
320
|
threads.each do |t|
|
315
321
|
t.join
|
316
|
-
if t.
|
322
|
+
if t.key?(:result)
|
317
323
|
results ||= {}
|
318
|
-
results[t
|
319
|
-
elsif t.
|
324
|
+
results[t[:node_key]] = t[:result]
|
325
|
+
elsif t.key?(:error)
|
320
326
|
errors ||= {}
|
321
|
-
errors[t
|
327
|
+
errors[t[:node_key]] = t[:error]
|
322
328
|
end
|
323
329
|
end
|
324
330
|
end
|
@@ -150,34 +150,34 @@ class RedisClient
|
|
150
150
|
@pipelines&.each_slice(MAX_THREADS) do |chuncked_pipelines|
|
151
151
|
threads = chuncked_pipelines.map do |node_key, pipeline|
|
152
152
|
Thread.new(node_key, pipeline) do |nk, pl|
|
153
|
-
Thread.current
|
153
|
+
Thread.current[:node_key] = nk
|
154
154
|
replies = do_pipelining(@router.find_node(nk), pl)
|
155
155
|
raise ReplySizeError, "commands: #{pl._size}, replies: #{replies.size}" if pl._size != replies.size
|
156
156
|
|
157
|
-
Thread.current
|
157
|
+
Thread.current[:replies] = replies
|
158
158
|
rescue ::RedisClient::Cluster::Pipeline::RedirectionNeeded => e
|
159
|
-
Thread.current
|
159
|
+
Thread.current[:redirection_needed] = e
|
160
160
|
rescue StandardError => e
|
161
|
-
Thread.current
|
161
|
+
Thread.current[:error] = e
|
162
162
|
end
|
163
163
|
end
|
164
164
|
|
165
165
|
threads.each(&:join)
|
166
166
|
threads.each do |t|
|
167
|
-
if t.
|
167
|
+
if t.key?(:replies)
|
168
168
|
all_replies ||= Array.new(@size)
|
169
|
-
@pipelines[t
|
169
|
+
@pipelines[t[:node_key]]
|
170
170
|
.outer_indices
|
171
|
-
.each_with_index { |outer, inner| all_replies[outer] = t
|
172
|
-
elsif t.
|
171
|
+
.each_with_index { |outer, inner| all_replies[outer] = t[:replies][inner] }
|
172
|
+
elsif t.key?(:redirection_needed)
|
173
173
|
all_replies ||= Array.new(@size)
|
174
|
-
pipeline = @pipelines[t
|
175
|
-
err = t
|
174
|
+
pipeline = @pipelines[t[:node_key]]
|
175
|
+
err = t[:redirection_needed]
|
176
176
|
err.indices.each { |i| err.replies[i] = handle_redirection(err.replies[i], pipeline, i) }
|
177
177
|
pipeline.outer_indices.each_with_index { |outer, inner| all_replies[outer] = err.replies[inner] }
|
178
|
-
elsif t.
|
178
|
+
elsif t.key?(:error)
|
179
179
|
errors ||= {}
|
180
|
-
errors[t
|
180
|
+
errors[t[:node_key]] = t[:error]
|
181
181
|
end
|
182
182
|
end
|
183
183
|
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'redis_client'
|
4
|
+
require 'redis_client/circuit_breaker'
|
4
5
|
require 'redis_client/cluster/command'
|
5
6
|
require 'redis_client/cluster/errors'
|
6
7
|
require 'redis_client/cluster/key_slot_converter'
|
@@ -13,6 +14,7 @@ class RedisClient
|
|
13
14
|
class Router
|
14
15
|
ZERO_CURSOR_FOR_SCAN = '0'
|
15
16
|
METHODS_FOR_BLOCKING_CMD = %i[blocking_call_v blocking_call].freeze
|
17
|
+
TSF = ->(b, s) { b.nil? ? s : b.call(s) }.curry
|
16
18
|
|
17
19
|
attr_reader :node
|
18
20
|
|
@@ -29,38 +31,43 @@ class RedisClient
|
|
29
31
|
def send_command(method, command, *args, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
30
32
|
cmd = ::RedisClient::Cluster::NormalizedCmdName.instance.get_by_command(command)
|
31
33
|
case cmd
|
32
|
-
when '
|
33
|
-
@node.call_all(method, command, args, &block).first
|
34
|
-
when 'flushall', 'flushdb'
|
35
|
-
@node.call_primaries(method, command, args, &block).first
|
36
|
-
when 'ping' then @node.send_ping(method, command, args, &block).first
|
34
|
+
when 'ping' then @node.send_ping(method, command, args).first.then(&TSF.call(block))
|
37
35
|
when 'wait' then send_wait_command(method, command, args, &block)
|
38
|
-
when 'keys' then @node.call_replicas(method, command, args
|
39
|
-
when 'dbsize' then @node.call_replicas(method, command, args
|
36
|
+
when 'keys' then @node.call_replicas(method, command, args).flatten.sort_by(&:to_s).then(&TSF.call(block))
|
37
|
+
when 'dbsize' then @node.call_replicas(method, command, args).select { |e| e.is_a?(Integer) }.sum.then(&TSF.call(block))
|
40
38
|
when 'scan' then scan(command, seed: 1)
|
41
|
-
when 'lastsave' then @node.call_all(method, command, args
|
39
|
+
when 'lastsave' then @node.call_all(method, command, args).sort_by(&:to_i).then(&TSF.call(block))
|
42
40
|
when 'role' then @node.call_all(method, command, args, &block)
|
43
41
|
when 'config' then send_config_command(method, command, args, &block)
|
44
42
|
when 'client' then send_client_command(method, command, args, &block)
|
45
43
|
when 'cluster' then send_cluster_command(method, command, args, &block)
|
46
|
-
when 'readonly', 'readwrite', 'shutdown'
|
47
|
-
raise ::RedisClient::Cluster::OrchestrationCommandNotSupported, cmd
|
48
44
|
when 'memory' then send_memory_command(method, command, args, &block)
|
49
45
|
when 'script' then send_script_command(method, command, args, &block)
|
50
46
|
when 'pubsub' then send_pubsub_command(method, command, args, &block)
|
47
|
+
when 'acl', 'auth', 'bgrewriteaof', 'bgsave', 'quit', 'save'
|
48
|
+
@node.call_all(method, command, args).first.then(&TSF.call(block))
|
49
|
+
when 'flushall', 'flushdb'
|
50
|
+
@node.call_primaries(method, command, args).first.then(&TSF.call(block))
|
51
|
+
when 'readonly', 'readwrite', 'shutdown'
|
52
|
+
raise ::RedisClient::Cluster::OrchestrationCommandNotSupported, cmd
|
51
53
|
when 'discard', 'exec', 'multi', 'unwatch'
|
52
54
|
raise ::RedisClient::Cluster::AmbiguousNodeError, cmd
|
53
55
|
else
|
54
56
|
node = assign_node(command)
|
55
57
|
try_send(node, method, command, args, &block)
|
56
58
|
end
|
59
|
+
rescue ::RedisClient::CircuitBreaker::OpenCircuitError
|
60
|
+
raise
|
57
61
|
rescue ::RedisClient::Cluster::Node::ReloadNeeded
|
58
62
|
update_cluster_info!
|
59
63
|
raise ::RedisClient::Cluster::NodeMightBeDown
|
60
64
|
rescue ::RedisClient::Cluster::ErrorCollection => e
|
65
|
+
raise if e.errors.any?(::RedisClient::CircuitBreaker::OpenCircuitError)
|
66
|
+
|
61
67
|
update_cluster_info! if e.errors.values.any? do |err|
|
62
68
|
err.message.start_with?('CLUSTERDOWN Hash slot not served')
|
63
69
|
end
|
70
|
+
|
64
71
|
raise
|
65
72
|
end
|
66
73
|
|
@@ -72,6 +79,8 @@ class RedisClient
|
|
72
79
|
else
|
73
80
|
node.public_send(method, *args, command, &block)
|
74
81
|
end
|
82
|
+
rescue ::RedisClient::CircuitBreaker::OpenCircuitError
|
83
|
+
raise
|
75
84
|
rescue ::RedisClient::CommandError => e
|
76
85
|
raise if retry_count <= 0
|
77
86
|
|
@@ -102,6 +111,8 @@ class RedisClient
|
|
102
111
|
|
103
112
|
def try_delegate(node, method, *args, retry_count: 3, **kwargs, &block) # rubocop:disable Metrics/AbcSize
|
104
113
|
node.public_send(method, *args, **kwargs, &block)
|
114
|
+
rescue ::RedisClient::CircuitBreaker::OpenCircuitError
|
115
|
+
raise
|
105
116
|
rescue ::RedisClient::CommandError => e
|
106
117
|
raise if retry_count <= 0
|
107
118
|
|
@@ -197,9 +208,10 @@ class RedisClient
|
|
197
208
|
|
198
209
|
private
|
199
210
|
|
200
|
-
def send_wait_command(method, command, args, retry_count: 3, &block)
|
201
|
-
@node.call_primaries(method, command, args
|
211
|
+
def send_wait_command(method, command, args, retry_count: 3, &block) # rubocop:disable Metrics/AbcSize
|
212
|
+
@node.call_primaries(method, command, args).select { |r| r.is_a?(Integer) }.sum.then(&TSF.call(block))
|
202
213
|
rescue ::RedisClient::Cluster::ErrorCollection => e
|
214
|
+
raise if e.errors.any?(::RedisClient::CircuitBreaker::OpenCircuitError)
|
203
215
|
raise if retry_count <= 0
|
204
216
|
raise if e.errors.values.none? do |err|
|
205
217
|
err.message.include?('WAIT cannot be used with replica instances')
|
@@ -213,7 +225,7 @@ class RedisClient
|
|
213
225
|
def send_config_command(method, command, args, &block)
|
214
226
|
case ::RedisClient::Cluster::NormalizedCmdName.instance.get_by_subcommand(command)
|
215
227
|
when 'resetstat', 'rewrite', 'set'
|
216
|
-
@node.call_all(method, command, args
|
228
|
+
@node.call_all(method, command, args).first.then(&TSF.call(block))
|
217
229
|
else assign_node(command).public_send(method, *args, command, &block)
|
218
230
|
end
|
219
231
|
end
|
@@ -221,16 +233,16 @@ class RedisClient
|
|
221
233
|
def send_memory_command(method, command, args, &block)
|
222
234
|
case ::RedisClient::Cluster::NormalizedCmdName.instance.get_by_subcommand(command)
|
223
235
|
when 'stats' then @node.call_all(method, command, args, &block)
|
224
|
-
when 'purge' then @node.call_all(method, command, args
|
236
|
+
when 'purge' then @node.call_all(method, command, args).first.then(&TSF.call(block))
|
225
237
|
else assign_node(command).public_send(method, *args, command, &block)
|
226
238
|
end
|
227
239
|
end
|
228
240
|
|
229
241
|
def send_client_command(method, command, args, &block)
|
230
242
|
case ::RedisClient::Cluster::NormalizedCmdName.instance.get_by_subcommand(command)
|
231
|
-
when 'list' then @node.call_all(method, command, args
|
243
|
+
when 'list' then @node.call_all(method, command, args).flatten.then(&TSF.call(block))
|
232
244
|
when 'pause', 'reply', 'setname'
|
233
|
-
@node.call_all(method, command, args
|
245
|
+
@node.call_all(method, command, args).first.then(&TSF.call(block))
|
234
246
|
else assign_node(command).public_send(method, *args, command, &block)
|
235
247
|
end
|
236
248
|
end
|
@@ -240,7 +252,7 @@ class RedisClient
|
|
240
252
|
when 'addslots', 'delslots', 'failover', 'forget', 'meet', 'replicate',
|
241
253
|
'reset', 'set-config-epoch', 'setslot'
|
242
254
|
raise ::RedisClient::Cluster::OrchestrationCommandNotSupported, ['cluster', subcommand]
|
243
|
-
when 'saveconfig' then @node.call_all(method, command, args
|
255
|
+
when 'saveconfig' then @node.call_all(method, command, args).first.then(&TSF.call(block))
|
244
256
|
when 'getkeysinslot'
|
245
257
|
raise ArgumentError, command.join(' ') if command.size != 4
|
246
258
|
|
@@ -249,25 +261,25 @@ class RedisClient
|
|
249
261
|
end
|
250
262
|
end
|
251
263
|
|
252
|
-
def send_script_command(method, command, args, &block)
|
264
|
+
def send_script_command(method, command, args, &block) # rubocop:disable Metrics/AbcSize
|
253
265
|
case ::RedisClient::Cluster::NormalizedCmdName.instance.get_by_subcommand(command)
|
254
266
|
when 'debug', 'kill'
|
255
|
-
@node.call_all(method, command, args
|
267
|
+
@node.call_all(method, command, args).first.then(&TSF.call(block))
|
256
268
|
when 'flush', 'load'
|
257
|
-
@node.call_primaries(method, command, args
|
269
|
+
@node.call_primaries(method, command, args).first.then(&TSF.call(block))
|
258
270
|
when 'exists'
|
259
|
-
@node.call_all(method, command, args
|
271
|
+
@node.call_all(method, command, args).transpose.map { |arr| arr.any?(&:zero?) ? 0 : 1 }.then(&TSF.call(block))
|
260
272
|
else assign_node(command).public_send(method, *args, command, &block)
|
261
273
|
end
|
262
274
|
end
|
263
275
|
|
264
276
|
def send_pubsub_command(method, command, args, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
265
277
|
case ::RedisClient::Cluster::NormalizedCmdName.instance.get_by_subcommand(command)
|
266
|
-
when 'channels' then @node.call_all(method, command, args
|
278
|
+
when 'channels' then @node.call_all(method, command, args).flatten.uniq.sort_by(&:to_s).then(&TSF.call(block))
|
267
279
|
when 'numsub'
|
268
|
-
@node.call_all(method, command, args
|
269
|
-
.reduce({}) { |a, e| a.merge(e) { |_, v1, v2| v1 + v2 } }
|
270
|
-
when 'numpat' then @node.call_all(method, command, args
|
280
|
+
@node.call_all(method, command, args).reject(&:empty?).map { |e| Hash[*e] }
|
281
|
+
.reduce({}) { |a, e| a.merge(e) { |_, v1, v2| v1 + v2 } }.then(&TSF.call(block))
|
282
|
+
when 'numpat' then @node.call_all(method, command, args).select { |e| e.is_a?(Integer) }.sum.then(&TSF.call(block))
|
271
283
|
else assign_node(command).public_send(method, *args, command, &block)
|
272
284
|
end
|
273
285
|
end
|
@@ -98,7 +98,7 @@ class RedisClient
|
|
98
98
|
|
99
99
|
def build_node_configs(addrs)
|
100
100
|
configs = Array[addrs].flatten.filter_map { |addr| parse_node_addr(addr) }
|
101
|
-
raise InvalidClientConfigError, '`nodes` option is empty' if configs.
|
101
|
+
raise InvalidClientConfigError, '`nodes` option is empty' if configs.empty?
|
102
102
|
|
103
103
|
configs
|
104
104
|
end
|
@@ -150,7 +150,7 @@ class RedisClient
|
|
150
150
|
end
|
151
151
|
|
152
152
|
def merge_generic_config(client_config, node_configs)
|
153
|
-
return client_config if node_configs.
|
153
|
+
return client_config if node_configs.empty?
|
154
154
|
|
155
155
|
cfg = node_configs.first
|
156
156
|
MERGE_CONFIG_KEYS.each { |k| client_config[k] = cfg[k] if cfg.key?(k) }
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: redis-cluster-client
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Taishi Kasuga
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2023-01-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: redis-client
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '0.
|
19
|
+
version: '0.12'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '0.
|
26
|
+
version: '0.12'
|
27
27
|
description:
|
28
28
|
email:
|
29
29
|
- proxy0721@gmail.com
|