redis-ick 0.0.5 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +6 -94
- data/.travis.yml +5 -2
- data/CHANGELOG.md +31 -0
- data/README.md +94 -67
- data/lib/redis/ick/version.rb +1 -34
- data/lib/redis/ick.rb +277 -122
- data/redis-ick.gemspec +5 -5
- metadata +13 -12
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cb3406be565b9f93bea296e10d6cb1d34c48707a
|
4
|
+
data.tar.gz: 92f6dd755dad1165b41dd2050547712124b0cbea
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d811a5bc57c88d9833ca04c0e1715e1dbeb2a6be1de01bc6a41dec3a06d009a11a9e5884b0a8ec01b75c0e6cdaacdaeb284c5b3d678ebf071bc54812691a1935
|
7
|
+
data.tar.gz: 1bd851eadaff3a9fe3dc1996eb38654121dd4785857ea3fc14c9e2cabfe7b815273091f1b677ae9720e78265ee3a382534cf739c6666a3d34bcb86421bbeafbb
|
data/.rubocop.yml
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
|
2
1
|
AllCops:
|
3
2
|
Include:
|
4
3
|
- Rakefile
|
@@ -50,103 +49,16 @@ Metrics/MethodLength:
|
|
50
49
|
Layout:
|
51
50
|
Enabled: false
|
52
51
|
|
53
|
-
#
|
54
|
-
#
|
55
|
-
Lint/AmbiguousBlockAssociation: # obnoxiously rejects idiomatic Ruby
|
56
|
-
Enabled: false
|
57
|
-
|
58
|
-
# Ick#ickreserve returns an Array of Arrays.
|
59
|
-
#
|
60
|
-
# Performance/HashEachMethods complains that:
|
61
|
-
#
|
62
|
-
# ick.ickreserve('key',num).each do |message,score|
|
63
|
-
# ...
|
64
|
-
# end
|
65
|
-
#
|
66
|
-
# would be more efficient as a each_key loop.
|
67
|
-
#
|
68
|
-
# This cop inappropriately requires us to change working code to
|
69
|
-
# broken code. This is a known issue which the authors of Rubocop do
|
70
|
-
# not intend to change:
|
71
|
-
#
|
72
|
-
# https://github.com/bbatsov/rubocop/issues/4732
|
73
|
-
#
|
74
|
-
# My reaction is to blacklist this cop.
|
52
|
+
# As a group, the Style cops are bewilderingly opiniated.
|
75
53
|
#
|
76
|
-
|
77
|
-
Enabled: false
|
78
|
-
|
79
|
-
# This does no more than insist I type "format" instead of "sprintf",
|
80
|
-
# where the two are aliases.
|
81
|
-
#
|
82
|
-
Style/FormatString:
|
83
|
-
Enabled: false
|
84
|
-
|
85
|
-
# There is nothing wrong with Ruby 1.9 Hash syntax.
|
86
|
-
#
|
87
|
-
Style/HashSyntax:
|
88
|
-
Enabled: false
|
89
|
-
|
90
|
-
# No. Indeed, postfix if can often drive a line over 80 columns wide.
|
91
|
-
#
|
92
|
-
Style/IfUnlessModifier:
|
93
|
-
Enabled: false
|
94
|
-
|
95
|
-
# No. There is nothing wrong with "if !foo".
|
54
|
+
# In some cases IMO they are harmful e.g. Style/TernaryParentheses.
|
96
55
|
#
|
97
|
-
#
|
98
|
-
# I have to think in English in two different logical senses - and
|
99
|
-
# English is a poor language for logical senses.
|
100
|
-
#
|
101
|
-
Style/NegatedIf:
|
102
|
-
Enabled: false
|
103
|
-
|
104
|
-
# "Do not use semicolons to terminate expressions."
|
105
|
-
#
|
106
|
-
# That's great when I terminate a single-line expression with a
|
107
|
-
# redundant semicolo because I forget I'm not using C.
|
108
|
-
#
|
109
|
-
# But when I'm using a semicolon to separate two expressions there is
|
110
|
-
# no other choice. So this really ought to be Style/OneExprPerLine,
|
111
|
-
# which I reject.
|
112
|
-
#
|
113
|
-
Style/Semicolon:
|
114
|
-
Enabled: false
|
115
|
-
|
116
|
-
# No.
|
117
|
-
#
|
118
|
-
# Some lines must have '\"'. It is ugly to use a mix of '"' and '\''
|
119
|
-
# in LoCs which are close to one another. Therefore, banning '"' if
|
120
|
-
# '"' is not strictly necessary drives visual inconsistency.
|
121
|
-
#
|
122
|
-
Style/StringLiterals:
|
123
|
-
Enabled: false
|
124
|
-
|
125
|
-
# This is the same kind of obnoxious pedantry which drove Hungarian
|
126
|
-
# Notation.
|
127
|
-
#
|
128
|
-
# The [] literal syntax is perfectly servicable and there is no point
|
129
|
-
# _tightly_ coupling it to the content of the array. That's why we
|
130
|
-
# have context-free grammers!
|
131
|
-
#
|
132
|
-
Style/SymbolArray:
|
133
|
-
Enabled: false
|
134
|
-
|
135
|
-
# Shockingly, this cop requires us to *OMIT*, not *INCLUDE* parens in
|
136
|
-
# ternery conditons.
|
137
|
-
#
|
138
|
-
# IMO this one is actively harmful in that it discourages attention to
|
139
|
-
# clarity and avoiding some nasty precedence surprises.
|
56
|
+
# I reject these cops.
|
140
57
|
#
|
141
|
-
Style
|
58
|
+
Style:
|
142
59
|
Enabled: false
|
143
60
|
|
144
|
-
# I
|
145
|
-
# list down one-per line.
|
146
|
-
#
|
147
|
-
# As such, I reject these tests.
|
61
|
+
# I like a lot of the Lint tests, but not these.
|
148
62
|
#
|
149
|
-
|
150
|
-
Enabled: false
|
151
|
-
Style/TrailingCommaInLiteral:
|
63
|
+
Lint/AmbiguousBlockAssociation: # obnoxiously rejects idiomatic Ruby
|
152
64
|
Enabled: false
|
data/.travis.yml
CHANGED
@@ -10,7 +10,7 @@
|
|
10
10
|
sudo: true
|
11
11
|
language: ruby
|
12
12
|
before_install:
|
13
|
-
- gem install bundler -v 1.
|
13
|
+
- gem install bundler -v 1.16.1
|
14
14
|
- sudo add-apt-repository -y ppa:twemproxy/stable
|
15
15
|
- sudo apt-get update -y
|
16
16
|
- sudo apt-get install -y twemproxy
|
@@ -18,6 +18,9 @@ services:
|
|
18
18
|
- redis-server
|
19
19
|
rvm:
|
20
20
|
- 2.1.6
|
21
|
+
- 2.2.9
|
22
|
+
- 2.4.3
|
23
|
+
- 2.5.0
|
21
24
|
before_script:
|
22
25
|
- bundle exec rubocop --version
|
23
26
|
- nutcracker --version
|
@@ -25,6 +28,6 @@ before_script:
|
|
25
28
|
- nutcracker --conf-file=.travis/nutcracker.yml &
|
26
29
|
- sleep 0.3
|
27
30
|
script:
|
28
|
-
- bundle exec rubocop
|
31
|
+
- bundle exec rubocop --display-cop-names --display-style-guide
|
29
32
|
- bundle exec env REDIS_URL=redis://localhost:6379 rake test
|
30
33
|
- bundle exec env REDIS_URL=redis://localhost:22121 rake test
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
## 0.1.0 (2018-03-20)
|
2
|
+
|
3
|
+
- Added ickexchange which combines ickcommit+ickreserve.
|
4
|
+
- Introduced backwash to ickreserve and ickexchange.
|
5
|
+
- Expanded .travis.yml to cover more rvm versions.
|
6
|
+
- Shrink Rubocop coverage to exclude `Style/*`.
|
7
|
+
- Moves version history out into CHANGELOG.md.
|
8
|
+
|
9
|
+
## 0.0.5 (2017-09-20)
|
10
|
+
|
11
|
+
- Rework ickstats so it no longer angers Twemproxy, per https://github.com/ProsperWorks/redis-ick/issues/3, by producing a nested Array response.
|
12
|
+
|
13
|
+
## 0.0.4 (2017-09-12)
|
14
|
+
|
15
|
+
- Imported text from original design doc to README.md, polish.
|
16
|
+
- Rubocop polish and defiance.
|
17
|
+
- Development dependency on [redis-key_hash](https://github.com/ProsperWorks/redis-key_hash) to test prescriptive hash claims.
|
18
|
+
- Identified limits of prescriptive hash robustness.
|
19
|
+
|
20
|
+
## 0.0.3 (2017-08-29)
|
21
|
+
- Got .travis.yml working with a live redis-server.
|
22
|
+
- Runtime dependency on redis-script_manager for Ick._eval.
|
23
|
+
- Initial Rubocop integration.
|
24
|
+
- Misc cleanup.
|
25
|
+
|
26
|
+
## 0.0.2 (2017-08-29)
|
27
|
+
- Broke out into Prosperworks/redis-ick, make public.
|
28
|
+
|
29
|
+
## 0.0.1 (prehistory)
|
30
|
+
- Still in Prosperworks/ALI/vendor/gems/redis-ick.
|
31
|
+
|
data/README.md
CHANGED
@@ -44,10 +44,10 @@ of dirty _documents_ per unit time, not in the number of dirty
|
|
44
44
|
_operations_ per unit time. In a sense, the more we fall behind the
|
45
45
|
slower we fall.
|
46
46
|
|
47
|
-
**Big Problem**: The Forgotten Dirtiness Problem.
|
48
|
-
dirtied a second time
|
49
|
-
|
50
|
-
|
47
|
+
**Big Problem**: The Forgotten Dirtiness Problem. A document could be
|
48
|
+
dirtied a second time while process_batch_slowly() is processing it.
|
49
|
+
When we we drop that document from the queue, we may end up with the
|
50
|
+
document still dirty but no longer in the queue.
|
51
51
|
|
52
52
|
**Small Problem**: The Hot Data Starvation Problem. Because we score
|
53
53
|
by time-of-dirtiness and we use ZRANGEBYRANK starting at 0, each batch
|
@@ -81,13 +81,13 @@ taken after process_batch_slowly(). Only documents whose timestamps
|
|
81
81
|
did not change between the two snapshots are removed from the queue.
|
82
82
|
|
83
83
|
Notice how the critical section no longer includes
|
84
|
-
process_batch_slowly(). Instead it only spans two Redis ops and
|
85
|
-
local set arithmetic
|
84
|
+
process_batch_slowly(). Instead it only spans two Redis ops and a bit
|
85
|
+
of local set arithmetic.
|
86
86
|
|
87
|
-
The critical section
|
88
|
-
|
89
|
-
process_batch_slowly()
|
90
|
-
this critical section never
|
87
|
+
The critical section which causes the Forgotten Dirtiness Problem is
|
88
|
+
still there, but is much smaller. In practice we see
|
89
|
+
process_batch_slowly() take minutes, but even in extreme situations
|
90
|
+
this smaller critical section never takes more than 3 seconds.
|
91
91
|
|
92
92
|
## Proposal: The Ick Pattern
|
93
93
|
|
@@ -96,14 +96,17 @@ identified the Hot Data Starvation Problem. We developed Ick and
|
|
96
96
|
switched to this almost familiar pattern:
|
97
97
|
|
98
98
|
# in any process: whenever a document is dirtied
|
99
|
-
|
100
|
-
|
101
|
-
# in the indexer process:
|
102
|
-
batch = Ick.ickreserve(redis,queue_key,batch_size)
|
103
|
-
process_batch_slowly(batch)
|
104
|
-
# burn down the queue only if the batch succeeded
|
105
|
-
Ick.ickcommit(redis,queue_key,*members_of(batch)) # critical section gone
|
99
|
+
#
|
100
|
+
Ick.new(redis).ickadd(queue_key,Time.now.to_f,document_id)
|
106
101
|
|
102
|
+
# in the indexer process, burn down the queue only if the batch succeeded:
|
103
|
+
#
|
104
|
+
ick = Ick.new(redis)
|
105
|
+
while still_going() do
|
106
|
+
batch = ick.ickreserve(queue_key,batch_size)
|
107
|
+
process_batch_slowly(batch)
|
108
|
+
ick.ickcommit(queue_key,*members_of(batch)) # critical section gone
|
109
|
+
end
|
107
110
|
|
108
111
|
Ick solves for failover via a two phase commit protocol between
|
109
112
|
**ickreserve** and **ickcommit**. If there is a failure during
|
@@ -117,23 +120,36 @@ only ever called from the indexer and producers do not mutate the
|
|
117
120
|
consumer set.
|
118
121
|
|
119
122
|
Ick solves the Hot Data Starvation Problem by a subtle change in
|
120
|
-
|
121
|
-
re-added, or ZADD NX which always preserves the old score,
|
122
|
-
always takes the _min_ of the old and new scores. Thus,
|
123
|
-
the first-known ditry time for a message even when there is
|
124
|
-
in the producers. The longer entries stay in the consumer
|
125
|
-
more they implicitly percolate toward the cold end regardless
|
126
|
-
many updates they receive. Ditto in the consumer set.
|
127
|
-
all producers make a best effort to use only current or
|
128
|
-
timestamps when they call
|
129
|
-
include the oldest entries and there will be no
|
123
|
+
**ickadd**. Unlike ZADD, which overwrites the old score when a
|
124
|
+
message is re-added, or ZADD NX which always preserves the old score,
|
125
|
+
**ickadd** always takes the _min_ of the old and new scores. Thus,
|
126
|
+
Ick tracks the first-known ditry time for a message even when there is
|
127
|
+
time skew in the producers. The longer entries stay in the consumer
|
128
|
+
set, the more they implicitly percolate toward the cold end regardless
|
129
|
+
of how many updates they receive. Ditto in the consumer set.
|
130
|
+
Provided that all producers make a best effort to use only current or
|
131
|
+
future timestamps when they call **ickadd*, the **ickreserve** batch
|
132
|
+
will always include the oldest entries and there will be no
|
133
|
+
starvation.
|
134
|
+
|
135
|
+
To reduce Redis round-trips, Ick also supports an operation
|
136
|
+
**ickexchange** which combines **ickcommit** with **ickreserve**
|
137
|
+
|
138
|
+
ick = Ick.new(redis)
|
139
|
+
batch = []
|
140
|
+
while still_going() do
|
141
|
+
batch = ick.ickexchange(queue_key,batch_size,*batch) # commit + reserve
|
142
|
+
process_batch_slowly(batch)
|
143
|
+
end
|
144
|
+
ick.ickexchange(queue_key,0,*batch) # commit final batch
|
145
|
+
|
130
146
|
|
131
147
|
Apology: I know that [Two-Phase
|
132
148
|
Commit](https://en.wikipedia.org/wiki/Two-phase_commit_protocol) has a
|
133
149
|
different technical meaning than what Ick does. Unfortunately I can't
|
134
|
-
find a better name for this
|
135
|
-
|
136
|
-
|
150
|
+
find a better name for this common fail-safe pattern. I suppose we
|
151
|
+
could think of the Redis sorted set as the coordinator and the
|
152
|
+
consumer process as the (single) participant node and, generously,
|
137
153
|
Two-Phase Commit might be taken to describe Ick.
|
138
154
|
|
139
155
|
|
@@ -146,7 +162,7 @@ An Ick is a collection of three Redis keys which all live on the same
|
|
146
162
|
* producer set, a sorted set into which we flag keys as dirty with timestamps
|
147
163
|
* consumer set, a sorted set from which the indexer pulls batches to index
|
148
164
|
|
149
|
-
### Ick defines
|
165
|
+
### Ick defines 6 operations on this data via Lua on Redis:
|
150
166
|
|
151
167
|
* **ickdel**: removes all keys associated with a given Ick structure
|
152
168
|
* **ickstats**: returns a hash of stats including version and size
|
@@ -159,24 +175,32 @@ An Ick is a collection of three Redis keys which all live on the same
|
|
159
175
|
* when a member-is re-added it takes the lowest of 2 scores
|
160
176
|
* returns the results as an array
|
161
177
|
* **ickcommit**: deletes members from the consumer set
|
178
|
+
* **ickexchange**: combines **ickcommit** and **ickreserve** in one op
|
162
179
|
|
163
180
|
Reminder: With few exceptions, all Redis commands are atomic and
|
164
181
|
transactional. This includes any Lua scripts such as those which
|
165
182
|
implement Ick. This atomicity guarantee is important to the
|
166
183
|
correctness of Ick, but because it is inherent in Redis/Lua, does not
|
167
|
-
appear explicitly in any of the Ick
|
184
|
+
appear explicitly in any of the Ick source.
|
168
185
|
|
169
186
|
## Fabulous Diagram
|
170
187
|
|
171
188
|
Here is a coarse dataflow for members moving through an Ick.
|
172
189
|
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
190
|
+
**ickadd** --> producer set --+
|
191
|
+
|
|
192
|
+
**ickreserve** consumer set -----+--> consumer set --+
|
193
|
+
|
|
194
|
+
**ickcommit** +--> forgotten
|
195
|
+
|
196
|
+
Here is the dataflow for members moving through an Ick with backwash.
|
197
|
+
|
198
|
+
**ickadd** --> producer set --+--+
|
199
|
+
| |
|
200
|
+
**ickreserve** consumer set --+ +--> consumer set --+
|
201
|
+
|
|
202
|
+
**ickcommit** +--> forgotten
|
203
|
+
|
180
204
|
|
181
205
|
## Miscellanea
|
182
206
|
|
@@ -186,10 +210,10 @@ Even though one Ick uses three Redis keys, Ick is compatible with
|
|
186
210
|
Redis Cluster. At ProsperWorks we use it with RedisLabs Enterprise
|
187
211
|
Cluster.
|
188
212
|
|
189
|
-
Ick does
|
190
|
-
|
191
|
-
|
192
|
-
|
213
|
+
Ick does tricky things to compute the producer set and consumer set
|
214
|
+
keys from the master key in a way which puts them all on the same slot
|
215
|
+
in both Redis Cluster and with RLEC's default prescriptive hashing
|
216
|
+
algorithm.
|
193
217
|
|
194
218
|
See [redis-key_hash](https://github.com/ProsperWorks/redis-key_hash)
|
195
219
|
for how test this.
|
@@ -217,42 +241,45 @@ when one consumer halts? How do we prevent the cold end of the
|
|
217
241
|
producer set from getting clogged up with messages destined for the
|
218
242
|
idle consumer?
|
219
243
|
|
220
|
-
We prefer handling those issues in higher-level code.
|
221
|
-
|
244
|
+
We prefer handling those issues in higher-level code. Ick by itself
|
245
|
+
does not attempt to solve scalability.
|
222
246
|
|
223
247
|
|
224
248
|
### Some Surprises Which Can Be Gotchas in Test
|
225
249
|
|
226
|
-
Because
|
227
|
-
|
250
|
+
Because **ickadd** uses write-folding semantics over the producer set,
|
251
|
+
**ickadd** might or might not grow the total size of the queue.
|
228
252
|
|
229
|
-
|
230
|
-
producer set and the consumer set. Because
|
253
|
+
**ickreserve** is not a read-only operation. It can mutate both the
|
254
|
+
producer set and the consumer set. Because **ickreserve** uses
|
231
255
|
write-folding semantics between the producer set and the consumer set,
|
232
|
-
|
256
|
+
`ickreserve(ick_key,N)` might:
|
233
257
|
|
234
258
|
* shrink the producer set by N and grow the consumer set by N
|
235
259
|
* shrink the producer set by 0 and grow the consumer set by 0
|
236
260
|
* shrink the producer set by N and grow the consumer set by 0
|
237
|
-
*
|
261
|
+
* anything where the producer set shrinks at least as much as consumer set grows
|
238
262
|
|
239
263
|
Because Ick always uses the min when multiple scores are present for
|
240
|
-
one message,
|
241
|
-
|
242
|
-
ways.
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
is
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
264
|
+
one message, **ickadd** can rearrange the order of the producer set
|
265
|
+
and **ickreserve** can rearrange the order of the consumer set in
|
266
|
+
surprising ways.
|
267
|
+
|
268
|
+
With backwash enabled, **ickreserve** can result in a complete
|
269
|
+
exchange of entries between the producer set and the consumer set.
|
270
|
+
|
271
|
+
**ickadd** write-folds in the producer set but not in the consumer
|
272
|
+
set. Thus, one message can appear in both the producer set and the
|
273
|
+
consumer set. At first this seems wrong and inefficient, but in fact
|
274
|
+
it is a desirable property. When a message is in both sets, it means
|
275
|
+
it was included in a batch by **ickreserve**, then added by
|
276
|
+
**ickadd**, but has yet to be **ickcommit**-ed. The interpretation
|
277
|
+
for this is that the consumer is actively engaged in updating the
|
278
|
+
downstream systems. But that means, at the Ick it is indeterminate
|
279
|
+
whether the message is still dirty or has been cleaned. That is,
|
280
|
+
being in both queues corresponds exactly to a message being in the
|
281
|
+
critical section where a race condition is possible. Thus, we _want_
|
282
|
+
it to still be dirty and to appear in a future batch.
|
256
283
|
|
257
284
|
None of these surprises is a bug in Ick: they are all consistent with
|
258
285
|
the design and the intent. But they are surprises nonetheless and can
|
data/lib/redis/ick/version.rb
CHANGED
@@ -1,38 +1,5 @@
|
|
1
1
|
class Redis
|
2
2
|
class Ick
|
3
|
-
|
4
|
-
# Version plan/history:
|
5
|
-
#
|
6
|
-
# 0.0.1 - Still in Prosperworks/ALI/vendor/gems/redis-ick.
|
7
|
-
#
|
8
|
-
# 0.0.2 - Broke out into Prosperworks/redis-ick, make public.
|
9
|
-
#
|
10
|
-
# 0.0.3 - Got .travis.yml working with a live redis-server.
|
11
|
-
#
|
12
|
-
# Runtime dependency on redis-script_manager for
|
13
|
-
# Ick._eval.
|
14
|
-
#
|
15
|
-
# Initial Rubocop integration.
|
16
|
-
#
|
17
|
-
# Misc cleanup.
|
18
|
-
#
|
19
|
-
# 0.0.4 - Imported text from original design doc to README.md, polish.
|
20
|
-
#
|
21
|
-
# Rubocop polish and defiance.
|
22
|
-
#
|
23
|
-
# Development dependency on redis-key_hash to test
|
24
|
-
# prescriptive hash claims. Identified limits of
|
25
|
-
# prescriptive hash robustness.
|
26
|
-
#
|
27
|
-
# 0.0.5 - Rework ickstats so it no longer angers Twemproxy, per
|
28
|
-
# https://github.com/ProsperWorks/redis-ick/issues/3,
|
29
|
-
# by producing a nested Array response.
|
30
|
-
#
|
31
|
-
# 0.1.0 - (future) Big README.md and Rdoc update, solicit feedback
|
32
|
-
# from select external beta users.
|
33
|
-
#
|
34
|
-
# 0.2.0 - (future) Incorporate feedback, announce.
|
35
|
-
#
|
36
|
-
VERSION = '0.0.5'.freeze
|
3
|
+
VERSION = '0.1.0'.freeze
|
37
4
|
end
|
38
5
|
end
|
data/lib/redis/ick.rb
CHANGED
@@ -3,7 +3,7 @@ require 'redis/script_manager'
|
|
3
3
|
|
4
4
|
class Redis
|
5
5
|
|
6
|
-
#
|
6
|
+
# Accessor for Ick data structures in Redis.
|
7
7
|
#
|
8
8
|
class Ick
|
9
9
|
|
@@ -107,51 +107,38 @@ class Redis
|
|
107
107
|
raise ArgumentError, "bogus non-String ick_key #{ick_key}"
|
108
108
|
end
|
109
109
|
_statsd_increment('profile.ick.ickstats.calls')
|
110
|
-
|
110
|
+
raw_results = nil
|
111
111
|
_statsd_time('profile.ick.time.ickstats') do
|
112
|
-
|
113
|
-
end
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
112
|
+
raw_results = _eval(LUA_ICKSTATS,ick_key)
|
113
|
+
end
|
114
|
+
_postprocess(
|
115
|
+
raw_results,
|
116
|
+
lambda do |results|
|
117
|
+
return nil if !results
|
118
|
+
#
|
119
|
+
# LUA_ICKSTATS returned bulk data response [k,v,k,v,...]
|
120
|
+
#
|
121
|
+
stats = Hash[*results]
|
122
|
+
#
|
123
|
+
# From http://redis.io/commands/eval, the "Lua to Redis conversion
|
124
|
+
# table" states that:
|
125
|
+
#
|
126
|
+
# Lua number -> Redis integer reply (the number is converted
|
127
|
+
# into an integer)
|
128
|
+
#
|
129
|
+
# ...If you want to return a float from Lua you should return
|
130
|
+
# it as a string.
|
131
|
+
#
|
132
|
+
# LUA_ICKSTATS works around this by converting certain stats to
|
133
|
+
# strings. We reverse that conversion here.
|
134
|
+
#
|
135
|
+
stats.keys.select{|k|/_min$/ =~ k || /_max$/ =~ k}.each do |k|
|
136
|
+
next if !stats[k]
|
137
|
+
stats[k] = (/^\d+$/ =~ stats[k]) ? stats[k].to_i : stats[k].to_f
|
123
138
|
end
|
139
|
+
stats
|
124
140
|
end
|
125
|
-
|
126
|
-
else
|
127
|
-
::Redis::Ick._postprocess_ickstats_results(raw_ickstats_results)
|
128
|
-
end
|
129
|
-
end
|
130
|
-
|
131
|
-
def self._postprocess_ickstats_results(raw_ickstats_results)
|
132
|
-
return nil if !raw_ickstats_results
|
133
|
-
#
|
134
|
-
# LUA_ICKSTATS returned bulk data response [k,v,k,v,...]
|
135
|
-
#
|
136
|
-
stats = Hash[*raw_ickstats_results]
|
137
|
-
#
|
138
|
-
# From http://redis.io/commands/eval, the "Lua to Redis conversion
|
139
|
-
# table" states that:
|
140
|
-
#
|
141
|
-
# Lua number -> Redis integer reply (the number is converted
|
142
|
-
# into an integer)
|
143
|
-
#
|
144
|
-
# ...If you want to return a float from Lua you should return
|
145
|
-
# it as a string.
|
146
|
-
#
|
147
|
-
# LUA_ICKSTATS works around this by converting certain stats to
|
148
|
-
# strings. We reverse that conversion here.
|
149
|
-
#
|
150
|
-
stats.keys.select{|k|/_min$/ =~ k || /_max$/ =~ k}.each do |k|
|
151
|
-
next if !stats[k]
|
152
|
-
stats[k] = (/^\d+$/ =~ stats[k]) ? stats[k].to_i : stats[k].to_f
|
153
|
-
end
|
154
|
-
stats
|
141
|
+
)
|
155
142
|
end
|
156
143
|
|
157
144
|
# Adds all the specified members with the specified scores to the
|
@@ -229,11 +216,16 @@ class Redis
|
|
229
216
|
#
|
230
217
|
# @param max_size max number of messages to reserve
|
231
218
|
#
|
219
|
+
# @param backwash if true, in the reserve function cset members
|
220
|
+
# with high scores are swapped out for pset members with lower
|
221
|
+
# scores. Otherwise cset members remain in the cset until
|
222
|
+
# committed regardless of how low scores in the pset might be.
|
223
|
+
#
|
232
224
|
# @return a list of up to max_size pairs, similar to
|
233
225
|
# Redis.current.zrange() withscores: [ member_string, score_number ]
|
234
226
|
# representing the lowest-scored elements from the producer set.
|
235
227
|
#
|
236
|
-
def ickreserve(ick_key,max_size=0)
|
228
|
+
def ickreserve(ick_key,max_size=0,backwash: false)
|
237
229
|
if !ick_key.is_a?(String)
|
238
230
|
raise ArgumentError, "bogus non-String ick_key #{ick_key}"
|
239
231
|
end
|
@@ -245,36 +237,16 @@ class Redis
|
|
245
237
|
end
|
246
238
|
_statsd_increment('profile.ick.ickreserve.calls')
|
247
239
|
_statsd_timing('profile.ick.ickreserve.max_size',max_size)
|
248
|
-
|
240
|
+
raw_results = nil
|
249
241
|
_statsd_time('profile.ick.time.ickreserve') do
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
end
|
257
|
-
|
258
|
-
#
|
259
|
-
# We extend the Redis::Future with a continuation so we can
|
260
|
-
# add our own post-processing.
|
261
|
-
#
|
262
|
-
class << raw_ickreserve_results
|
263
|
-
alias_method :original_value, :value
|
264
|
-
def value
|
265
|
-
original_value.each_slice(2).map do |p|
|
266
|
-
[ p[0], ::Redis::Ick._floatify(p[1]) ]
|
267
|
-
end
|
268
|
-
end
|
269
|
-
end
|
270
|
-
raw_ickreserve_results
|
271
|
-
else
|
272
|
-
results = raw_ickreserve_results.each_slice(2).map do |p|
|
273
|
-
[ p[0], ::Redis::Ick._floatify(p[1]) ]
|
274
|
-
end
|
275
|
-
_statsd_timing('profile.ick.ickreserve.num_results',results.size)
|
276
|
-
results
|
277
|
-
end
|
242
|
+
raw_results = _eval(
|
243
|
+
LUA_ICKEXCHANGE,
|
244
|
+
ick_key,
|
245
|
+
max_size,
|
246
|
+
backwash ? 'backwash' : false,
|
247
|
+
)
|
248
|
+
end
|
249
|
+
_postprocess(raw_results,Skip0ThenFloatifyPairs)
|
278
250
|
end
|
279
251
|
|
280
252
|
# Removes the indicated members from the producer set, if present.
|
@@ -301,8 +273,169 @@ class Redis
|
|
301
273
|
end
|
302
274
|
_statsd_increment('profile.ick.ickcommit.calls')
|
303
275
|
_statsd_timing('profile.ick.ickcommit.members',members.size)
|
276
|
+
raw_results = nil
|
304
277
|
_statsd_time('profile.ick.time.ickcommit') do
|
305
|
-
_eval(
|
278
|
+
raw_results = _eval(
|
279
|
+
LUA_ICKEXCHANGE,
|
280
|
+
ick_key,
|
281
|
+
0,
|
282
|
+
false, # backwash not relevant in ickcommit
|
283
|
+
*members
|
284
|
+
)
|
285
|
+
end
|
286
|
+
#
|
287
|
+
# raw_results are num_committed followed by 0 message-and-score
|
288
|
+
# pairs.
|
289
|
+
#
|
290
|
+
# We just capture the num_committed.
|
291
|
+
#
|
292
|
+
_postprocess(raw_results,lambda { |results| results[0] })
|
293
|
+
end
|
294
|
+
|
295
|
+
# ickexchange combines several functions in one Redis round-trip.
|
296
|
+
#
|
297
|
+
# 1. As ickcommit, removes consumed members from the consumer set.
|
298
|
+
#
|
299
|
+
# 2. As ickreserve, tops up the consumer set from the producer and
|
300
|
+
# returns the requested new consumer members, if any.
|
301
|
+
#
|
302
|
+
# @param ick_key String the base key for the Ick
|
303
|
+
#
|
304
|
+
# @param reserve_size Integer max number of messages to reserve.
|
305
|
+
#
|
306
|
+
# @param commit_members Array members to be committed.
|
307
|
+
#
|
308
|
+
# @param backwash if true, in the reserve function cset members
|
309
|
+
# with high scores are swapped out for pset members with lower
|
310
|
+
# scores. Otherwise cset members remain in the cset until
|
311
|
+
# committed regardless of how low scores in the pset might be.
|
312
|
+
#
|
313
|
+
# @return a list of up to reserve_size pairs, similar to
|
314
|
+
# Redis.current.zrange() withscores: [ message, score ]
|
315
|
+
# representing the lowest-scored elements from the producer set
|
316
|
+
# after the commit and reserve operations.
|
317
|
+
#
|
318
|
+
def ickexchange(ick_key,reserve_size,*commit_members,backwash: false)
|
319
|
+
if !ick_key.is_a?(String)
|
320
|
+
raise ArgumentError, "bogus non-String ick_key #{ick_key}"
|
321
|
+
end
|
322
|
+
if !reserve_size.is_a?(Integer)
|
323
|
+
raise ArgumentError, "bogus non-Integer reserve_size #{reserve_size}"
|
324
|
+
end
|
325
|
+
if reserve_size < 0
|
326
|
+
raise ArgumentError, "bogus negative reserve_size #{reserve_size}"
|
327
|
+
end
|
328
|
+
_statsd_increment('profile.ick.ickexchange.calls')
|
329
|
+
_statsd_timing('profile.ick.ickexchange.reserve_size',reserve_size)
|
330
|
+
_statsd_timing(
|
331
|
+
'profile.ick.ickexchange.commit_members',
|
332
|
+
commit_members.size
|
333
|
+
)
|
334
|
+
raw_results = nil
|
335
|
+
_statsd_time('profile.ick.time.ickexchange') do
|
336
|
+
raw_results = _eval(
|
337
|
+
LUA_ICKEXCHANGE,
|
338
|
+
ick_key,
|
339
|
+
reserve_size,
|
340
|
+
backwash ? 'backwash' : false,
|
341
|
+
commit_members
|
342
|
+
)
|
343
|
+
end
|
344
|
+
_postprocess(raw_results,Skip0ThenFloatifyPairs)
|
345
|
+
end
|
346
|
+
|
347
|
+
# Postprocessing done on the LUA_ICKEXCHANGE results for both
|
348
|
+
# ickreserve and ickexchange.
|
349
|
+
#
|
350
|
+
# results are num_committed followed by N message-and-score
|
351
|
+
# pairs.
|
352
|
+
#
|
353
|
+
# We do results[1..-1] to skip the first element, num_committed.
|
354
|
+
#
|
355
|
+
# On the rest, we floatify the scores to convert from Redis
|
356
|
+
# number-as-string limitation to Ruby Floats.
|
357
|
+
#
|
358
|
+
# This is similar to to Redis::FloatifyPairs:
|
359
|
+
#
|
360
|
+
# https://github.com/redis/redis-rb/blob/master/lib/redis.rb#L2887-L2896
|
361
|
+
#
|
362
|
+
Skip0ThenFloatifyPairs = lambda do |results|
|
363
|
+
results[1..-1].each_slice(2).map do |m_and_s|
|
364
|
+
[ m_and_s[0], ::Redis::Ick._floatify(m_and_s[1]) ]
|
365
|
+
end
|
366
|
+
end
|
367
|
+
|
368
|
+
# Calls back to block with the results.
|
369
|
+
#
|
370
|
+
# If raw_results is a Redis::Future, callback will be deferred
|
371
|
+
# until the future is expanded.
|
372
|
+
#
|
373
|
+
# Otherwise, callback will happen immediately.
|
374
|
+
#
|
375
|
+
def _postprocess(raw_results,callback)
|
376
|
+
if raw_results.is_a?(Redis::Future)
|
377
|
+
#
|
378
|
+
# Redis::Future have a built-in mechanism for calling a
|
379
|
+
# transformation on the raw results.
|
380
|
+
#
|
381
|
+
# Here, we monkey-patch not the Redis::Future class, but just
|
382
|
+
# this one raw_results object. We give ourselves a door to
|
383
|
+
# set the post-processing transformation.
|
384
|
+
#
|
385
|
+
# The transformation will be called only once when the real
|
386
|
+
# results are materialized.
|
387
|
+
#
|
388
|
+
class << raw_results
|
389
|
+
def transformation=(transformation)
|
390
|
+
raise "transformation collision" if @transformation
|
391
|
+
@transformation = transformation
|
392
|
+
end
|
393
|
+
end
|
394
|
+
raw_results.transformation = callback
|
395
|
+
raw_results
|
396
|
+
else
|
397
|
+
#
|
398
|
+
# If not Redis::Future, we invoke the callback immediately.
|
399
|
+
#
|
400
|
+
callback.call(raw_results)
|
401
|
+
end
|
402
|
+
end
|
403
|
+
|
404
|
+
# A deferred computation which allows us to perform post-processing
|
405
|
+
# on results which come back from redis pipelines.
|
406
|
+
#
|
407
|
+
# The idea is to regain some measure of composability by allowing
|
408
|
+
# utility methods to respond polymorphically depending on whether
|
409
|
+
# they are called in a pipeline.
|
410
|
+
#
|
411
|
+
# TODO: Where this utility lives in the code is not very well
|
412
|
+
# thought-out. This is more broadly applicable than just for
|
413
|
+
# Icks. This probably belongs in its own file, or in RedisUtil,
|
414
|
+
# or as a monkey-patch into redis-rb. This is intended for use
|
415
|
+
# with Redis::Futures, but has zero Redis-specific code. This is
|
416
|
+
# more broadly applicable, maybe, than Redis. This is in class Ick
|
417
|
+
# for the time being only because Ick.ickstats() is where I first
|
418
|
+
# needed this and it isn't otherwise obvious where to put this.
|
419
|
+
#
|
420
|
+
class FutureContinuation
|
421
|
+
#
|
422
|
+
# The first (and only the first) time :value is called on this
|
423
|
+
# FutureContinuation, conversion will be called.
|
424
|
+
#
|
425
|
+
def initialize(continuation)
|
426
|
+
@continuation = continuation
|
427
|
+
@result = nil
|
428
|
+
end
|
429
|
+
#
|
430
|
+
# Force the computation. :value is chosen as the name of this
|
431
|
+
# method to be duck-typing compatible with Redis::Future.
|
432
|
+
#
|
433
|
+
def value
|
434
|
+
if @continuation
|
435
|
+
@result = @continuation.call
|
436
|
+
@continuation = nil
|
437
|
+
end
|
438
|
+
@result
|
306
439
|
end
|
307
440
|
end
|
308
441
|
|
@@ -310,7 +443,9 @@ class Redis
|
|
310
443
|
# etc.
|
311
444
|
#
|
312
445
|
# So we can be certain of compatibility, this was stolen with tweaks
|
313
|
-
# from
|
446
|
+
# from:
|
447
|
+
#
|
448
|
+
# https://github.com/redis/redis-rb/blob/master/lib/redis.rb#L2876-L2885
|
314
449
|
#
|
315
450
|
def self._floatify(str)
|
316
451
|
raise ArgumentError, "not String: #{str}" if !str.is_a?(String)
|
@@ -569,9 +704,20 @@ class Redis
|
|
569
704
|
}).freeze
|
570
705
|
|
571
706
|
#######################################################################
|
572
|
-
#
|
707
|
+
# LUA_ICKEXCHANGE: commit then reserve
|
573
708
|
#######################################################################
|
574
709
|
#
|
710
|
+
# Commit Function
|
711
|
+
#
|
712
|
+
# Removes specified members in ARGV[2..N] from the pset, then tops
|
713
|
+
# up the cset to up to size ARGV[1] by shifting the lowest-scored
|
714
|
+
# members over from the pset.
|
715
|
+
#
|
716
|
+
# The cset might already be full, in which case we may shift fewer
|
717
|
+
# than ARGV[1] elements.
|
718
|
+
#
|
719
|
+
# Reserve Function
|
720
|
+
#
|
575
721
|
# Tops up the cset to up to size ARGV[1] by shifting the
|
576
722
|
# lowest-scored members over from the pset.
|
577
723
|
#
|
@@ -582,66 +728,75 @@ class Redis
|
|
582
728
|
# are duplicate messages, we may remove more members from the pset
|
583
729
|
# than we add to the cset.
|
584
730
|
#
|
585
|
-
# @param ARGV
|
586
|
-
#
|
731
|
+
# @param ARGV[1] single number, batch_size, the desired size for
|
732
|
+
# cset and to be returned
|
733
|
+
#
|
734
|
+
# @param ARGV[2] string, 'backwash' for backwash
|
587
735
|
#
|
588
|
-
# @
|
736
|
+
# @param ARGV[3..N] messages to be removed from the cset before reserving
|
589
737
|
#
|
590
|
-
|
591
|
-
|
738
|
+
# @return a bulk response, the number of members removed from the
|
739
|
+
# cset by the commit function followed by up to ARGV[1] pairs
|
740
|
+
# [member,score,...] from the reserve funciton.
|
741
|
+
#
|
742
|
+
# Note: This Lua unpacks ARGV with the iterator ipairs() instead
|
743
|
+
# of unpack() to avoid a "too many results to unpack" failure at
|
744
|
+
# 8000 args. However, the loop over many redis.call is
|
745
|
+
# regrettably heavy-weight. From a performance standpoint it
|
746
|
+
# would be preferable to call ZREM in larger batches.
|
747
|
+
#
|
748
|
+
LUA_ICKEXCHANGE = (LUA_ICK_PREFIX + %{
|
749
|
+
local reserve_size = tonumber(ARGV[1])
|
750
|
+
local backwash = ARGV[2]
|
751
|
+
local argc = table.getn(ARGV)
|
752
|
+
local num_committed = 0
|
753
|
+
for i = 3,argc,1 do
|
754
|
+
local num_zrem = redis.call('ZREM',ick_cset_key,ARGV[i])
|
755
|
+
num_committed = num_committed + num_zrem
|
756
|
+
end
|
757
|
+
if 'backwash' == backwash then
|
758
|
+
local cset_all = redis.call('ZRANGE',ick_cset_key,0,-1,'WITHSCORES')
|
759
|
+
local cset_size = table.getn(cset_all)
|
760
|
+
for i = 1,cset_size,2 do
|
761
|
+
local member = cset_all[i]
|
762
|
+
local score = cset_all[i+1]
|
763
|
+
local old_score = redis.call('ZSCORE',ick_pset_key,member)
|
764
|
+
if false == old_score then
|
765
|
+
redis.call('ZADD',ick_pset_key,score,member)
|
766
|
+
elseif score < tonumber(old_score) then
|
767
|
+
redis.call('ZADD',ick_pset_key,score,member)
|
768
|
+
end
|
769
|
+
end
|
770
|
+
redis.call('DEL',ick_cset_key)
|
771
|
+
end
|
592
772
|
while true do
|
593
|
-
local
|
594
|
-
if
|
773
|
+
local cset_size = redis.call('ZCARD',ick_cset_key)
|
774
|
+
if cset_size and reserve_size <= cset_size then
|
595
775
|
break
|
596
776
|
end
|
597
|
-
local
|
598
|
-
|
599
|
-
if 0 == table.getn(first_in_pset) then
|
777
|
+
local first_pset = redis.call('ZRANGE',ick_pset_key,0,0,'WITHSCORES')
|
778
|
+
if 0 == table.getn(first_pset) then
|
600
779
|
break
|
601
780
|
end
|
602
|
-
local first_member
|
603
|
-
local first_score
|
781
|
+
local first_member = first_pset[1]
|
782
|
+
local first_score = tonumber(first_pset[2])
|
604
783
|
redis.call('ZREM',ick_pset_key,first_member)
|
605
|
-
local old_score
|
784
|
+
local old_score = redis.call('ZSCORE',ick_cset_key,first_member)
|
606
785
|
if false == old_score or first_score < tonumber(old_score) then
|
607
786
|
redis.call('ZADD',ick_cset_key,first_score,first_member)
|
608
787
|
end
|
609
788
|
end
|
610
789
|
redis.call('SETNX', ick_key, 'ick.v1')
|
611
|
-
|
612
|
-
|
613
|
-
|
614
|
-
local
|
615
|
-
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
#######################################################################
|
620
|
-
# LUA_ICKCOMMIT
|
621
|
-
#######################################################################
|
622
|
-
#
|
623
|
-
# Removes specified members from the pset.
|
624
|
-
#
|
625
|
-
# @param ARGV a list of members to be removed from the cset
|
626
|
-
#
|
627
|
-
# @return the number of members removed
|
628
|
-
#
|
629
|
-
# Note: This this Lua unpacks ARGV with the iterator ipairs()
|
630
|
-
# instead of unpack() to avoid a "too many results to unpack"
|
631
|
-
# failure at 8000 args. However, the loop over many redis.call is
|
632
|
-
# regrettably heavy-weight. From a performance standpoint it
|
633
|
-
# would be preferable to call ZREM in larger batches.
|
634
|
-
#
|
635
|
-
LUA_ICKCOMMIT = (LUA_ICK_PREFIX + %{
|
636
|
-
redis.call('SETNX', ick_key, 'ick.v1')
|
637
|
-
if 0 == table.getn(ARGV) then
|
638
|
-
return 0
|
639
|
-
end
|
640
|
-
local num_removed = 0
|
641
|
-
for i,v in ipairs(ARGV) do
|
642
|
-
num_removed = num_removed + redis.call('ZREM',ick_cset_key,v)
|
790
|
+
local result = { num_committed }
|
791
|
+
if reserve_size > 0 then
|
792
|
+
local max = reserve_size - 1
|
793
|
+
local cset_batch =
|
794
|
+
redis.call('ZRANGE',ick_cset_key,0,max,'WITHSCORES')
|
795
|
+
for _i,v in ipairs(cset_batch) do
|
796
|
+
table.insert(result,v)
|
797
|
+
end
|
643
798
|
end
|
644
|
-
return
|
799
|
+
return result
|
645
800
|
}).freeze
|
646
801
|
|
647
802
|
end
|
data/redis-ick.gemspec
CHANGED
@@ -21,14 +21,14 @@ Gem::Specification.new do |spec|
|
|
21
21
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
22
22
|
spec.require_paths = ['lib']
|
23
23
|
|
24
|
-
spec.add_development_dependency 'bundler', '~> 1.
|
25
|
-
spec.add_development_dependency '
|
26
|
-
spec.add_development_dependency '
|
24
|
+
spec.add_development_dependency 'bundler', '~> 1.16.1'
|
25
|
+
spec.add_development_dependency 'minitest', '~> 5.11.3'
|
26
|
+
spec.add_development_dependency 'rake', '~> 12.3.1'
|
27
27
|
spec.add_development_dependency 'redis-key_hash', '~> 0.0.4'
|
28
28
|
spec.add_development_dependency 'redis-namespace', '~> 1.5'
|
29
|
-
spec.add_development_dependency 'rubocop', '~> 0.
|
29
|
+
spec.add_development_dependency 'rubocop', '~> 0.54.0'
|
30
30
|
|
31
|
-
spec.
|
31
|
+
spec.add_runtime_dependency 'redis', '~> 3.2'
|
32
32
|
spec.add_runtime_dependency 'redis-script_manager', '~> 0.0.2'
|
33
33
|
|
34
34
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: redis-ick
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- jhwillett
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2018-03-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -16,42 +16,42 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version:
|
19
|
+
version: 1.16.1
|
20
20
|
type: :development
|
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:
|
26
|
+
version: 1.16.1
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name:
|
28
|
+
name: minitest
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version:
|
33
|
+
version: 5.11.3
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version:
|
40
|
+
version: 5.11.3
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
|
-
name:
|
42
|
+
name: rake
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
45
|
- - "~>"
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version:
|
47
|
+
version: 12.3.1
|
48
48
|
type: :development
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version:
|
54
|
+
version: 12.3.1
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
56
|
name: redis-key_hash
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -86,14 +86,14 @@ dependencies:
|
|
86
86
|
requirements:
|
87
87
|
- - "~>"
|
88
88
|
- !ruby/object:Gem::Version
|
89
|
-
version: 0.
|
89
|
+
version: 0.54.0
|
90
90
|
type: :development
|
91
91
|
prerelease: false
|
92
92
|
version_requirements: !ruby/object:Gem::Requirement
|
93
93
|
requirements:
|
94
94
|
- - "~>"
|
95
95
|
- !ruby/object:Gem::Version
|
96
|
-
version: 0.
|
96
|
+
version: 0.54.0
|
97
97
|
- !ruby/object:Gem::Dependency
|
98
98
|
name: redis
|
99
99
|
requirement: !ruby/object:Gem::Requirement
|
@@ -133,6 +133,7 @@ files:
|
|
133
133
|
- ".rubocop.yml"
|
134
134
|
- ".travis.yml"
|
135
135
|
- ".travis/nutcracker.yml"
|
136
|
+
- CHANGELOG.md
|
136
137
|
- Gemfile
|
137
138
|
- LICENSE
|
138
139
|
- README.md
|