redis-ick 0.0.5 → 0.1.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/.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
|