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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 1c22eec5b65c61b1bc5ec5c19a88d21da264df33
4
- data.tar.gz: 0e7e7f500fc89fa0fb3394a8df7d4e8784c8e202
3
+ metadata.gz: cb3406be565b9f93bea296e10d6cb1d34c48707a
4
+ data.tar.gz: 92f6dd755dad1165b41dd2050547712124b0cbea
5
5
  SHA512:
6
- metadata.gz: 1f9e5dda5e632a926420c6ce7c5483edf53f757718622d2ae2d151c7c46105c5a46e1c7f77ce06804a56ee60703e4b151a616c36dab5cf634569e9c789a1c4f7
7
- data.tar.gz: d65b846e08e4c4ca477770c09ce9331fc6ed293971fb4b0bd5ef1139c6020e36515a10bcd4971c633f47b438489199d1fb2e3329b112f102f127528d71f6700c
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
- # I like a lot of the Lint tests, but not these.
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
- Performance/HashEachMethods:
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
- # As far as I'm concerned, "unless" is in poor taste because it means
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/TernaryParentheses:
58
+ Style:
142
59
  Enabled: false
143
60
 
144
- # I am a huge fan of using trailing commas when I break an argument
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
- Style/TrailingCommaInArguments:
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.14.6
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. If some document is
48
- dirtied a second time after the start of process_batch_slowly(), when
49
- process_batch_slowly() end we will drop that document from the queue.
50
- Thus, the document will be dirty but no longer in the queue!
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 some
85
- local set arithmetic which.
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 and the Forgotten Dirtiness Problem which it
88
- causes is still there, but is much smaller. In practice we have
89
- process_batch_slowly() taking minutes, but even in extreme situations
90
- this critical section never took more than 3 seconds.
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
- Ick.ickadd(redis,queue_key,Time.now.to_f,document_id)
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
- ICKADD. Unlike ZADD, which overwrites the old score when a message is
121
- re-added, or ZADD NX which always preserves the old score, ICKADD
122
- always takes the _min_ of the old and new scores. Thus, Ick tracks
123
- the first-known ditry time for a message even when there is time skew
124
- in the producers. The longer entries stay in the consumer set, the
125
- more they implicitly percolate toward the cold end regardless of how
126
- many updates they receive. Ditto in the consumer set. Provided that
127
- all producers make a best effort to use only current or future
128
- timestamps when they call ICKADD, the ICKRESERVE batch will always
129
- include the oldest entries and there will be no starvation.
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 very common failsafe queue pattern. I
135
- suppose we could think of the Redis sorted set as the coordinator and
136
- the consumer process as the (single) participant node and, generously,
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 5 operations on this data via Lua on Redis:
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 sources.
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
- app
174
- |
175
- +-- **ickadd** --> producer set
176
- |
177
- +-- **ickreserve** --> consumer set
178
- |
179
- +-- **ickcommit** --> forgotten
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 some very tricky things to compute the producer set and
190
- consumer set keys from the master key in a way which puts them all on
191
- the same slot in both Redis Cluster and with RLEC's default
192
- prescriptive hashing algorithm.
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. Thus, Ick by
221
- itself does not attempt to solve scalability.
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 ICKADD uses write-folding semantics over the producer set,
227
- ICKADD might or might not grow the total size of the queue.
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
- ICKRESERVE is not a read-only operation. It can mutate both the
230
- producer set and the consumer set. Because ICKRESERVE uses
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
- ICKRESERVE(N) might:
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
- * or anything in between
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, ICKADD can rearrange the order of the producer set and
241
- ICKRESERVE can rearrange the order of the consumer set in surprising
242
- ways.
243
-
244
- ICKADD write-folds in the producer set but not in the consumer set.
245
- Thus, one message can appear in both the producer set and the consumer
246
- set. At first this seems wrong and inefficient, but in fact it is a
247
- desirable property. When a message is in both sets, it means it was
248
- included in a batch by ICKRESERVE, then added by ICKADD, but has yet
249
- to be ICKCOMMITed. The interpretation for this is that the consumer
250
- is actively engaged in updating the downstream systems. But that
251
- means, at the Ick it is indeterminate whether the message is still
252
- dirty or has been cleaned. That is, being in both queues corresponds
253
- exactly to a message being in the critical section where a race
254
- condition is possible. Thus, we _want_ it to still be dirty and to
255
- appear in a future batch.
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
@@ -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
- # Binds Lua code to provide the Ick operations in Redis.
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
- raw_ickstats_results = nil
110
+ raw_results = nil
111
111
  _statsd_time('profile.ick.time.ickstats') do
112
- raw_ickstats_results = _eval(LUA_ICKSTATS,ick_key)
113
- end
114
- if raw_ickstats_results.is_a?(Redis::Future)
115
- #
116
- # We extend the Redis::Future with a continuation so we can add
117
- # our own post-processing.
118
- #
119
- class << raw_ickstats_results
120
- alias_method :original_value, :value
121
- def value
122
- ::Redis::Ick._postprocess_ickstats_results(original_value)
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
- raw_ickstats_results
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
- raw_ickreserve_results = nil
240
+ raw_results = nil
249
241
  _statsd_time('profile.ick.time.ickreserve') do
250
- raw_ickreserve_results =
251
- _eval(
252
- LUA_ICKRESERVE,
253
- ick_key,
254
- max_size
255
- )
256
- end
257
- if raw_ickreserve_results.is_a?(Redis::Future)
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(LUA_ICKCOMMIT,ick_key,*members)
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 https://github.com/redis/redis-rb/blob/master/lib/redis.rb.
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
- # LUA_ICKRESERVE
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 a single number, batch_size, the desired
586
- # size for cset and to be returned
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
- # @return a bulk response, up to ARGV[1] pairs [member,score,...]
736
+ # @param ARGV[3..N] messages to be removed from the cset before reserving
589
737
  #
590
- LUA_ICKRESERVE = (LUA_ICK_PREFIX + %{
591
- local target_cset_size = tonumber(ARGV[1])
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 ick_cset_size = redis.call('ZCARD',ick_cset_key)
594
- if ick_cset_size and target_cset_size <= ick_cset_size then
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 first_in_pset =
598
- redis.call('ZRANGE',ick_pset_key,0,0,'WITHSCORES')
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 = first_in_pset[1]
603
- local first_score = tonumber(first_in_pset[2])
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 = redis.call('ZSCORE',ick_cset_key,first_member)
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
- if target_cset_size <= 0 then
612
- return {}
613
- else
614
- local max = target_cset_size - 1
615
- return redis.call('ZRANGE',ick_cset_key,0,max,'WITHSCORES')
616
- end
617
- }).freeze
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 num_removed
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.14'
25
- spec.add_development_dependency 'rake', '~> 10.0'
26
- spec.add_development_dependency 'minitest', '~> 5.0'
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.50.0'
29
+ spec.add_development_dependency 'rubocop', '~> 0.54.0'
30
30
 
31
- spec.add_dependency 'redis', '~> 3.2'
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.5
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: 2017-09-20 00:00:00.000000000 Z
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: '1.14'
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: '1.14'
26
+ version: 1.16.1
27
27
  - !ruby/object:Gem::Dependency
28
- name: rake
28
+ name: minitest
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '10.0'
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: '10.0'
40
+ version: 5.11.3
41
41
  - !ruby/object:Gem::Dependency
42
- name: minitest
42
+ name: rake
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '5.0'
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: '5.0'
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.50.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.50.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