redis-ick 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 7d2e7517bc9eba00b0c5744984d90717b87979b0
4
+ data.tar.gz: 9879cc1f153d6a9d8ec52a2604a50b13178453cf
5
+ SHA512:
6
+ metadata.gz: 636ea109ba2dbcc8691ab1de0250c5487c13523fd1963698742a664f5e367a23757cdaa55d38614f15b0909425984cffc8db44bfd2cad35b2813e4ae5312938a
7
+ data.tar.gz: f5f1c137aaa9f4663aeb9c844f6d39d50fb97ad1ed75e4850a10f43bcc43fce39630ff33ee922e18c1cc20fb6a50e8f1246fd1dfd4ee6647da958d409a00cda4
data/.gitignore ADDED
@@ -0,0 +1,51 @@
1
+ *.gem
2
+ *.rbc
3
+ /.config
4
+ /coverage/
5
+ /InstalledFiles
6
+ /pkg/
7
+ /spec/reports/
8
+ /spec/examples.txt
9
+ /test/tmp/
10
+ /test/version_tmp/
11
+ /tmp/
12
+
13
+ # Used by dotenv library to load environment variables.
14
+ # .env
15
+
16
+ ## Specific to RubyMotion:
17
+ .dat*
18
+ .repl_history
19
+ build/
20
+ *.bridgesupport
21
+ build-iPhoneOS/
22
+ build-iPhoneSimulator/
23
+
24
+ ## Specific to RubyMotion (use of CocoaPods):
25
+ #
26
+ # We recommend against adding the Pods directory to your .gitignore. However
27
+ # you should judge for yourself, the pros and cons are mentioned at:
28
+ # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
29
+ #
30
+ # vendor/Pods/
31
+
32
+ ## Documentation cache and generated files:
33
+ /.yardoc/
34
+ /_yardoc/
35
+ /doc/
36
+ /rdoc/
37
+
38
+ ## Environment normalization:
39
+ /.bundle/
40
+ /vendor/bundle
41
+ /lib/bundler/man/
42
+
43
+ # For a library or gem, you might want to ignore these files since the code is
44
+ # intended to run in multiple environments; otherwise, check them in:
45
+ #
46
+ Gemfile.lock
47
+ .ruby-version
48
+ .ruby-gemset
49
+
50
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
51
+ .rvmrc
data/.travis.yml ADDED
@@ -0,0 +1,10 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.1.6
5
+ before_install:
6
+ - gem install bundler -v 1.14.6
7
+ services:
8
+ - redis-server
9
+ script:
10
+ - bundle exec env REDIS_URL=redis://localhost:6379 rake test
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in redis-ick.gemspec
6
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2017 ProsperWorks
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,108 @@
1
+ # Redis::Ick ![TravisCI](https://travis-ci.org/ProsperWorks/redis-ick.svg?branch=master)
2
+
3
+ Ick: An Indexing QUeue.
4
+
5
+ Redis::Ick implements a priority queue in Redis with two-phase commit
6
+ and write-folding aka write-combining semantics.
7
+
8
+ Icks are well-suited for dirty lists in data sync systems.
9
+
10
+ A Redis-based queue-like data structure for message passing in a
11
+ many-producer/single-consumer pattern.
12
+
13
+ Ick is compatible with Redis Cluster and RedisLabs Enterprise Cluster.
14
+ Each Ick has a master key and any other Redis keys it uses use a
15
+ prescriptive hash based on the master key.
16
+
17
+ Ick offers write-folding semantics in which re-adding a member already
18
+ in queue does not increase the size of the queue. It may, or may not,
19
+ rearrange that member's position within the queue.
20
+
21
+ Ick is batchy on the consumer side with reliable delivery semantics
22
+ using a two-phase protocol: it supports for reserving batches and
23
+ later committing all, some, or none of them.
24
+
25
+ Note that members held in the reserve buffer by the consumer do *not*
26
+ write-fold against members being added by producers.
27
+
28
+ Ick offers atomicity among producer and consumer operations by virtue
29
+ of leveraging Lua-in-Redis.
30
+
31
+ Ick offers starvation-free semantics when scores are approximately the
32
+ current time. When Ick performs write-folding, it always preserves
33
+ the *lowest* score seen for a given message. Thus, in both the
34
+ producer set and the consumer set, entries never move further away
35
+ from the poppy end.
36
+
37
+ Ick supports only a single consumer: there is only one buffer for the
38
+ two-phase pop protocol. If you need more than one consumer, shard
39
+ messages across multiple Icks each of which routes to one consumer.
40
+
41
+ incept: 2015-10-01
42
+ arch: https://goo.gl/V1g9I8
43
+
44
+ ```ruby
45
+ gem 'redis-ick'
46
+ ```
47
+
48
+ And then execute:
49
+
50
+ $ bundle
51
+
52
+ Or install it yourself as:
53
+
54
+ $ gem install redis-ick
55
+
56
+ ## Usage
57
+
58
+ Usage example for producers:
59
+
60
+ # one producer
61
+ #
62
+ ick = Ick.new(redis)
63
+ ick.ickadd("mykey",123,"foo",20151001,"bar")
64
+
65
+ # another producer
66
+ #
67
+ ick = Ick.new(redis)
68
+ ick.ickadd("mykey",12.8,"foo")
69
+ ick.ickadd("mykey",123.4,"baz")
70
+
71
+ Usage example for consumer:
72
+
73
+ ick = Ick.new(redis)
74
+ batch = ick.ickreserve("mykey",BATCH_SIZE)
75
+ members = batch.map { |i| i[0] }
76
+ scores = batch.map { |i| i[1] }
77
+ members.each do |member|
78
+ something_with(member)
79
+ end
80
+ ick.ickcommit("mykey",*members)
81
+
82
+ Usage example for statistician:
83
+
84
+ ick = Ick.new(redis)
85
+ stats = ick.ickstats("mykey")
86
+ puts stats['ver'] # string, version of Ick data structure in Redis
87
+ puts stats['cset_size'] # integer, number of elements in consumer set
88
+ puts stats['pset_size'] # integer, number of elements in producer set
89
+ puts stats['total_size'] # integer, number of elements in all sets
90
+ puts stats # other stuff also maybe or in future
91
+
92
+ ## Development
93
+
94
+ After checking out the repo, run `bin/setup` to install
95
+ dependencies. Then, run `rake test` to run the tests. You can also run
96
+ `bin/console` for an interactive prompt that will allow you to
97
+ experiment.
98
+
99
+ To install this gem onto your local machine, run `bundle exec rake
100
+ install`. To release a new version, update the version number in
101
+ `version.rb`, and then run `bundle exec rake release`, which will
102
+ create a git tag for the version, push git commits and tags, and push
103
+ the `.gem` file to [rubygems.org](https://rubygems.org).
104
+
105
+ ## Contributing
106
+
107
+ Bug reports and pull requests are welcome on GitHub at
108
+ https://github.com/ProsperWorks/redis-ick.
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList["test/**/*_test.rb"]
8
+ end
9
+
10
+ task :default => :test
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "redis/ick"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/lib/redis/ick.rb ADDED
@@ -0,0 +1,626 @@
1
+ require 'redis/ick/version'
2
+
3
+ class Redis
4
+ class Ick
5
+
6
+ # TODO: test *everything* in pipelines
7
+ # TODO: redis-script_manager for eval
8
+ # TODO: rubocop
9
+ # TODO: rdoc
10
+
11
+ # Creates an Ick accessor.
12
+ #
13
+ # @param redis Redis
14
+ #
15
+ # @param statsd a stats proxy. May be nil, else expected to respond
16
+ # to :increment and :timing.
17
+ #
18
+ def initialize(redis, statsd: nil)
19
+ if !redis.is_a?(Redis)
20
+ raise ArgumentError, "not a Redis: #{redis}"
21
+ end
22
+ if statsd && !statsd.respond_to?(:increment)
23
+ raise ArgumentError, "no statsd.increment"
24
+ end
25
+ if statsd && !statsd.respond_to?(:timing)
26
+ raise ArgumentError, "no statsd.timeing"
27
+ end
28
+ if statsd && !statsd.respond_to?(:time)
29
+ raise ArgumentError, "no statsd.time"
30
+ end
31
+ @redis = redis
32
+ @statsd = statsd
33
+ end
34
+
35
+ attr_accessor :redis
36
+ attr_accessor :statsd
37
+
38
+ # Reports a single count on the requested metric to statsd (if any).
39
+ #
40
+ # @param metric String
41
+ #
42
+ def _statsd_increment(metric)
43
+ statsd.increment(metric) if statsd
44
+ end
45
+
46
+ # Reports the specified timing on the requested metric to statsd (if
47
+ # any).
48
+ #
49
+ # @param metric String
50
+ #
51
+ def _statsd_timing(metric,time)
52
+ statsd.timing(metric,time) if statsd
53
+ end
54
+
55
+ # Executes the block (if any) and reports its timing in milliseconds
56
+ # on the requested metric to statsd (if any).
57
+ #
58
+ # @param metric String
59
+ #
60
+ # @return the value of the block, or nil if none
61
+ #
62
+ def _statsd_time(metric)
63
+ if statsd
64
+ statsd.time(metric) do
65
+ return block_given? ? yield : nil
66
+ end
67
+ else
68
+ return block_given? ? yield : nil
69
+ end
70
+ end
71
+
72
+ # Removes all data associated with the Ick in Redis at key.
73
+ #
74
+ # Similar to DEL key, http://redis.io/commands/del, but may
75
+ # delete multiple keys which together implement the Ick data
76
+ # structure.
77
+ #
78
+ # @param ick_key String the base key for the Ick
79
+ #
80
+ # @return an integer, the number of Redis keys deleted, which will
81
+ # be >= 1 if an Ick existed at key.
82
+ #
83
+ def ickdel(ick_key)
84
+ if !ick_key.is_a?(String)
85
+ raise ArgumentError, "bogus non-String ick_key #{ick_key}"
86
+ end
87
+ _statsd_increment('profile.ick.ickdel.calls')
88
+ _statsd_time('profile.ick.ickdel.time') do
89
+ Ick._eval(redis,LUA_ICKDEL,ick_key)
90
+ end
91
+ end
92
+
93
+ # Fetches stats.
94
+ #
95
+ # @param ick_key String the base key for the Ick
96
+ #
97
+ # @return If called outside a Redis pipeline, a Hash with stats
98
+ # about the Ick at ick_key, if any, else nil. If called within a
99
+ # pipeline, returns a redis::Future whose value is a Hash or nil as
100
+ # before.
101
+ #
102
+ def ickstats(ick_key)
103
+ if !ick_key.is_a?(String)
104
+ raise ArgumentError, "bogus non-String ick_key #{ick_key}"
105
+ end
106
+ _statsd_increment('profile.ick.ickstats.calls')
107
+ raw_ickstats_results = nil
108
+ _statsd_time('profile.ick.time.ickstats') do
109
+ raw_ickstats_results = Ick._eval(redis,LUA_ICKSTATS,ick_key)
110
+ end
111
+ if raw_ickstats_results.is_a?(Redis::Future)
112
+ #
113
+ # We extend the Redis::Future with a continuation so we can add
114
+ # our own post-processing.
115
+ #
116
+ class << raw_ickstats_results
117
+ alias_method :original_value, :value
118
+ def value
119
+ ::Redis::Ick._postprocess_ickstats_results(original_value)
120
+ end
121
+ end
122
+ raw_ickstats_results
123
+ else
124
+ ::Redis::Ick._postprocess_ickstats_results(raw_ickstats_results)
125
+ end
126
+ end
127
+
128
+ def self._postprocess_ickstats_results(raw_ickstats_results)
129
+ return nil if !raw_ickstats_results
130
+ #
131
+ # LUA_ICKSTATS returned bulk data response [k,v,k,v,...]
132
+ #
133
+ stats = Hash[*raw_ickstats_results]
134
+ #
135
+ # From http://redis.io/commands/eval, the "Lua to Redis conversion
136
+ # table" states that:
137
+ #
138
+ # Lua number -> Redis integer reply (the number is converted
139
+ # into an integer)
140
+ #
141
+ # ...If you want to return a float from Lua you should return
142
+ # it as a string.
143
+ #
144
+ # LUA_ICKSTATS works around this by converting certain stats to
145
+ # strings. We reverse that conversion here.
146
+ #
147
+ stats.keys.select{|k|/_min$/ =~ k || /_max$/ =~ k}.each do |k|
148
+ next if !stats[k]
149
+ stats[k] = (/^\d+$/ =~ stats[k]) ? stats[k].to_i : stats[k].to_f
150
+ end
151
+ stats
152
+ end
153
+
154
+ # Adds all the specified members with the specified scores to the
155
+ # Ick stored at key.
156
+ #
157
+ # Entries are stored in order by score. Lower-scored entries will
158
+ # pop out in reserve before higher-scored entries. Re-adding an
159
+ # entry which already exists in the producer set with a new score
160
+ # results in the entry having the lowest of the old and new scores.
161
+ #
162
+ # Similar to http://redis.io/commands/zadd with a modified NX
163
+ # option, operating on the producer set.
164
+ #
165
+ # Usage:
166
+ #
167
+ # ick.ickadd(ick_key,score,member[,score,member]*)
168
+ #
169
+ # Suggested usage is for scores to be a Unix timestamp indicating
170
+ # when something became dirty.
171
+ #
172
+ # @param ick_key String the base key for the Ick
173
+ #
174
+ # @param score_member_pairs Array of Arrays of [score,message]
175
+ #
176
+ # @return a pair, the number of new values followed by the numer of
177
+ # changed scores.
178
+ #
179
+ def ickadd(ick_key,*score_member_pairs)
180
+ if !ick_key.is_a?(String)
181
+ raise ArgumentError, "bogus non-String ick_key #{ick_key}"
182
+ end
183
+ if score_member_pairs.size.odd?
184
+ raise ArgumentError, "bogus odd-numbered #{score_member_pairs}"
185
+ end
186
+ score_member_pairs.each_slice(2) do |slice|
187
+ score, member = slice
188
+ if ! score.is_a? Numeric
189
+ raise ArgumentError, "bogus non-Numeric score #{score}"
190
+ end
191
+ if ! member.is_a? String
192
+ raise ArgumentError, "bogus non-String member #{member}"
193
+ end
194
+ end
195
+ _statsd_increment('profile.ick.ickadd.calls')
196
+ _statsd_timing('profile.ick.ickadd.pairs',score_member_pairs.size / 2)
197
+ _statsd_time('profile.ick.time.ickadd') do
198
+ Ick._eval(redis,LUA_ICKADD,ick_key,*score_member_pairs)
199
+ end
200
+ end
201
+
202
+ # Tops up the consumer set up to max_size by shifting the
203
+ # lowest-scored elements from the producer set into the consumer set
204
+ # until the consumer set cardinality reaches max_size or the
205
+ # producer set is exhausted.
206
+ #
207
+ # The reserved elements are meant to represent consumer
208
+ # work-in-progress. If they are not committed, they will be
209
+ # returned again in future calls to ickreserve.
210
+ #
211
+ # Note that the Lua for ick is irritating like so:
212
+ #
213
+ # - you add in the pattern [ score_number, member_string, ... ]
214
+ # - you retrieve in the pattern [ member_string, score_string, ... ]
215
+ #
216
+ # Native ZADD and ZRANGE WITHSCORES exhibit this same irritating
217
+ # inconsistency: Ick is annoyance-compatible with Redis sorted sets.
218
+ #
219
+ # However, by analogy with redis-rb's Redis.current.zrange(), this
220
+ # Ruby wrapper method pairs up the results for you, and converts the
221
+ # string scores to floats.
222
+ #
223
+ # - you get from this method [[ member_string, score_number] , ... ]
224
+ #
225
+ # @param ick_key String the base key for the Ick
226
+ #
227
+ # @param max_size max number of messages to reserve
228
+ #
229
+ # @return a list of up to max_size pairs, similar to
230
+ # Redis.current.zrange() withscores: [ member_string, score_number ]
231
+ # representing the lowest-scored elements from the producer set.
232
+ #
233
+ def ickreserve(ick_key,max_size=0)
234
+ if !ick_key.is_a?(String)
235
+ raise ArgumentError, "bogus non-String ick_key #{ick_key}"
236
+ end
237
+ if !max_size.is_a?(Integer)
238
+ raise ArgumentError, "bogus non-Integer max_size #{max_size}"
239
+ end
240
+ if max_size < 0
241
+ raise ArgumentError, "bogus negative #{max_size}"
242
+ end
243
+ _statsd_increment('profile.ick.ickreserve.calls')
244
+ _statsd_timing('profile.ick.ickreserve.max_size',max_size)
245
+ results = nil
246
+ _statsd_time('profile.ick.time.ickreserve') do
247
+ results =
248
+ Ick._eval(
249
+ redis,
250
+ LUA_ICKRESERVE,
251
+ ick_key,
252
+ max_size
253
+ ).each_slice(2).map { |p|
254
+ [ p[0], Ick._floatify(p[1]) ]
255
+ }
256
+ end
257
+ _statsd_timing('profile.ick.ickreserve.num_results',results.size)
258
+ results
259
+ end
260
+
261
+ # Removes the indicated members from the producer set, if present.
262
+ #
263
+ # Similar to ZREM ick_key [member]*, per
264
+ # http://redis.io/commands/zrem, operating on the consumer set only.
265
+ #
266
+ # Usage:
267
+ #
268
+ # ick.ickcommit(ick_key,memberA,memberB,...)
269
+ #
270
+ # Committed elements are meant to represent consumer work-completed.
271
+ #
272
+ # @param ick_key String the base key for the Ick
273
+ #
274
+ # @param members members to be committed out pf the pset
275
+ #
276
+ # @return an integer, the number of members removed from the
277
+ # producer set, not including non existing members.
278
+ #
279
+ def ickcommit(ick_key,*members)
280
+ if !ick_key.is_a?(String)
281
+ raise ArgumentError, "bogus non-String ick_key #{ick_key}"
282
+ end
283
+ _statsd_increment('profile.ick.ickcommit.calls')
284
+ _statsd_timing('profile.ick.ickcommit.members',members.size)
285
+ _statsd_time('profile.ick.time.ickcommit') do
286
+ Ick._eval(redis,LUA_ICKCOMMIT,ick_key,*members)
287
+ end
288
+ end
289
+
290
+ # Converts a string str into a Float, and recognizes 'inf', '-inf',
291
+ # etc.
292
+ #
293
+ # So we can be certain of compatibility, this was stolen with tweaks
294
+ # from https://github.com/redis/redis-rb/blob/master/lib/redis.rb.
295
+ #
296
+ def self._floatify(str)
297
+ raise ArgumentError, "not String: #{str}" if !str.is_a?(String)
298
+ if (inf = str.match(/^(-)?inf/i))
299
+ (inf[1] ? -1.0 : 1.0) / 0.0
300
+ else
301
+ Float(str)
302
+ end
303
+ end
304
+
305
+ # Runs the specified lua in the redis against the specifified Ick.
306
+ #
307
+ def self._eval(redis,lua,ick_key,*args)
308
+ if !lua.is_a?(String)
309
+ raise ArgumentError, "bogus non-String lua #{lua}"
310
+ end
311
+ if !ick_key.is_a?(String)
312
+ raise ArgumentError, "bogus non-String ick_key #{ick_key}"
313
+ end
314
+ redis.eval(lua,[ick_key],args)
315
+ end
316
+
317
+ #######################################################################
318
+ #
319
+ # The Ick Data Model in Redis
320
+ #
321
+ # - At ick_key, we keep a simple manifest string. Currently, only
322
+ # 'ick.v1' is expected or supported. This is for future-proofing
323
+ #
324
+ # - At "#{ick_key}/ick/{#{ick_key}}/pset" we keep a sorted set.
325
+ # Ick, the "producer set", into which new messages are pushed by
326
+ # ickadd.
327
+ #
328
+ # - At "#{ick_key}/ick/{#{ick_key}}/cset" we keep another sorted
329
+ # set, the "consumer set", where messages are held between
330
+ # ickreserve and ickcommit.
331
+ #
332
+ # These name patterns were chosen carefully, so that under the Redis
333
+ # Cluster Specification, http://redis.io/topics/cluster-spec, all
334
+ # three keys will always be hashed to the same HASH_SLOT.
335
+ #
336
+ # Note that if ick_key contain a user-specified prescriptive hashing
337
+ # subsequence like "{foo}", then that {}-sequence appears at the
338
+ # front of the key, hence will serve as the prescriptive hash key
339
+ # for all the derived keys which use ick_keys a prefix.
340
+ #
341
+ # If ick_key does not contain a {}-sequence, then the portion of all
342
+ # derived keys ".../{#{ick_key}}/..." provides one which hashes to
343
+ # the same slot as ick_key.
344
+ #
345
+ # Thus, we know all keys will be present on these same shard at
346
+ # run-time.
347
+ #
348
+ # The ickadd op adds entries to the pset, but only if they do not
349
+ # already exist.
350
+ #
351
+ # The ickreserve op moves entries from the pset into the cset.
352
+ #
353
+ # The ickcommit op removes entries from the pset.
354
+ #
355
+ # WARNING: If ick_key itself contains an {}-expr, this hashslot
356
+ # matching algorithm will break in RedisLabs Enterprise Cluster due
357
+ # to the newly-discovered Yikes Curly Brace Surprise. See
358
+ # https://github.com/ProsperWorks/ALI/pull/1132 for details.
359
+ #
360
+ #######################################################################
361
+
362
+
363
+ #######################################################################
364
+ # LUA_ICK_PREFIX
365
+ #######################################################################
366
+ #
367
+ # A snippet of Lua code which is common to all the Ick scripts.
368
+ #
369
+ # For convenience and to avoid repeating code, we set up
370
+ # some computed key names.
371
+ #
372
+ # For safety, we check that the ick_ver, ick_pset, and ick_cset
373
+ # either do not exist or exit with the correct types and values to
374
+ # be identifiable as an Ick.
375
+ #
376
+ # All scripts in the LUA_ICK series expect only one KEYS, the root
377
+ # key of the Ick data structure. We expect a version flag as a
378
+ # string at this key. Keys for other data are computed from KEYS[1]
379
+ # in such a way as to guarantee they all hash to the same slot.
380
+ #
381
+ LUA_ICK_PREFIX = %{
382
+ local ick_key = KEYS[1]
383
+ local ick_ver = redis.call('GET',ick_key)
384
+ local ick_pset_key = ick_key .. '/ick/{' .. ick_key .. '}/pset'
385
+ local ick_cset_key = ick_key .. '/ick/{' .. ick_key .. '}/cset'
386
+ local ick_ver_type = redis.call('TYPE',ick_key).ok
387
+ local ick_pset_type = redis.call('TYPE',ick_pset_key).ok
388
+ local ick_cset_type = redis.call('TYPE',ick_cset_key).ok
389
+ if (false ~= ick_ver and 'ick.v1' ~= ick_ver) then
390
+ return redis.error_reply('unrecognized ick version ' .. ick_ver)
391
+ end
392
+ if ('none' ~= ick_ver_type and 'string' ~= ick_ver_type) then
393
+ return redis.error_reply('ick defense: expected string at ' ..
394
+ ick_ver_key .. ', found ' .. ick_ver_type)
395
+ end
396
+ if ('none' ~= ick_pset_type and 'zset' ~= ick_pset_type) then
397
+ return redis.error_reply('ick defense: expected string at ' ..
398
+ ick_pset_key .. ', found ' .. ick_pset_type)
399
+ end
400
+ if ('none' ~= ick_cset_type and 'zset' ~= ick_cset_type) then
401
+ return redis.error_reply('ick defense: expected string at ' ..
402
+ ick_cset_key .. ', found ' .. ick_cset_type)
403
+ end
404
+ if ('none' == ick_ver_type) then
405
+ if ('none' ~= ick_pset_type) then
406
+ return redis.error_reply('ick defense: no ver at ' .. ick_ver_key ..
407
+ ', but found pset at ' .. ick_pset_key)
408
+ end
409
+ if ('none' ~= ick_cset_type) then
410
+ return redis.error_reply('ick defense: no ver at ' .. ick_ver_key ..
411
+ ', but found cset at ' .. ick_cset_key)
412
+ end
413
+ end
414
+ }.freeze
415
+
416
+ #######################################################################
417
+ # LUA_ICKDEL
418
+ #######################################################################
419
+ #
420
+ # Removes all keys associated with the Ick at KEYS[1].
421
+ #
422
+ # @param uses no ARGV
423
+ #
424
+ # @return the number of Redis keys deleted, which will be 0 if and
425
+ # only if no Ick existed at KEYS[1]
426
+ #
427
+ LUA_ICKDEL = (LUA_ICK_PREFIX + %{
428
+ return redis.call('DEL',ick_key,ick_pset_key,ick_cset_key)
429
+ }).freeze
430
+
431
+ #######################################################################
432
+ # LUA_ICKSTATS
433
+ #######################################################################
434
+ #
435
+ # @param uses no ARGV
436
+ #
437
+ # @return a bulk data response with statistics about the Ick at
438
+ # KEYS[1], or nil if none.
439
+ #
440
+ # Note: At http://redis.io/commands/eval, the "Lua to Redis
441
+ # conversion table" stats:
442
+ #
443
+ # Lua number -> Redis integer reply (the number is converted
444
+ # into an integer)
445
+ #
446
+ # ...If you want to return a float from Lua you should return
447
+ # it as a string.
448
+ #
449
+ # We follow this recommendation in our Lua below where we convert
450
+ # our numeric responses to strings with "tostring(tonumber(n))".
451
+ #
452
+ LUA_ICKSTATS = (LUA_ICK_PREFIX + %{
453
+ if (false == ick_ver) then
454
+ return nil
455
+ end
456
+ local ick_pset_size = redis.call('ZCARD',ick_pset_key)
457
+ local ick_cset_size = redis.call('ZCARD',ick_cset_key)
458
+ local ick_stats = {
459
+ 'key', ick_key,
460
+ 'ver', ick_ver,
461
+ 'cset_size', ick_cset_size,
462
+ 'pset_size', ick_pset_size,
463
+ 'total_size', ick_cset_size + ick_pset_size,
464
+ }
465
+ local pset_min = nil
466
+ local pset_max = nil
467
+ if ick_pset_size > 0 then
468
+ pset_min = redis.call('ZRANGE',ick_pset_key, 0, 0,'WITHSCORES')[2]
469
+ table.insert(ick_stats, 'pset_min')
470
+ table.insert(ick_stats, tostring(tonumber(pset_min)))
471
+ pset_max = redis.call('ZRANGE',ick_pset_key,-1,-1,'WITHSCORES')[2]
472
+ table.insert(ick_stats, 'pset_max')
473
+ table.insert(ick_stats, tostring(tonumber(pset_max)))
474
+ end
475
+ local cset_min = nil
476
+ local cset_max = nil
477
+ if ick_cset_size > 0 then
478
+ cset_min = redis.call('ZRANGE',ick_cset_key, 0, 0,'WITHSCORES')[2]
479
+ table.insert(ick_stats, 'cset_min')
480
+ table.insert(ick_stats, tostring(tonumber(cset_min)))
481
+ cset_max = redis.call('ZRANGE',ick_cset_key,-1,-1,'WITHSCORES')[2]
482
+ table.insert(ick_stats, 'cset_max')
483
+ table.insert(ick_stats, tostring(tonumber(cset_max)))
484
+ end
485
+ local total_min = nil
486
+ if pset_min and cset_min then
487
+ total_min = math.min(cset_min,pset_min)
488
+ elseif pset_min then
489
+ total_min = pset_min
490
+ elseif cset_min then
491
+ total_min = cset_min
492
+ end
493
+ if total_min then
494
+ table.insert(ick_stats, 'total_min')
495
+ table.insert(ick_stats, tostring(tonumber(total_min)))
496
+ end
497
+ local total_max = nil
498
+ if pset_max and cset_max then
499
+ total_max = math.max(cset_max,pset_max)
500
+ elseif pset_max then
501
+ total_max = pset_max
502
+ elseif cset_max then
503
+ total_max = cset_max
504
+ end
505
+ if total_max then
506
+ table.insert(ick_stats, 'total_max')
507
+ table.insert(ick_stats, tostring(tonumber(total_max)))
508
+ end
509
+ return ick_stats
510
+ }).freeze
511
+
512
+ #######################################################################
513
+ # LUA_ICKADD
514
+ #######################################################################
515
+ #
516
+ # Adds members to the cset as per ZADD. Where a member is
517
+ # re-written, we always take the lowest score.
518
+ #
519
+ # Thus, scores are only allowed to move downward. changes to score.
520
+ #
521
+ # Creates the Ick if necessary.
522
+ #
523
+ # @param ARGV a sequence of score,member pairs as per Redis ZADD.
524
+ #
525
+ # @return a pair of numbers [num_new, num_changed]
526
+ #
527
+ LUA_ICKADD = (LUA_ICK_PREFIX + %{
528
+ local num_args = table.getn(ARGV)
529
+ if 1 == (num_args % 2) then
530
+ return redis.error_reply("odd number of arguments for 'ickadd' command")
531
+ end
532
+ local num_new = 0
533
+ local num_changed = 0
534
+ for i = 1,num_args,2 do
535
+ local score = tonumber(ARGV[i])
536
+ local member = ARGV[i+1]
537
+ local old_score = redis.call('ZSCORE',ick_pset_key,member)
538
+ if false == old_score then
539
+ redis.call('ZADD',ick_pset_key,score,member)
540
+ num_new = num_new + 1
541
+ elseif score < tonumber(old_score) then
542
+ redis.call('ZADD',ick_pset_key,score,member)
543
+ num_changed = num_changed + 1
544
+ end
545
+ end
546
+ redis.call('SETNX', ick_key, 'ick.v1')
547
+ return { num_new, num_changed }
548
+ }).freeze
549
+
550
+ #######################################################################
551
+ # LUA_ICKRESERVE
552
+ #######################################################################
553
+ #
554
+ # Tops up the cset to up to size ARGV[1] by shifting the
555
+ # lowest-scored members over from the pset.
556
+ #
557
+ # The cset might already be full, in which case we may shift fewer
558
+ # than ARGV[1] elements.
559
+ #
560
+ # The same score-folding happens as per ICKADD. Thus where there
561
+ # are duplicate messages, we may remove more members from the pset
562
+ # than we add to the cset.
563
+ #
564
+ # @param ARGV a single number, batch_size, the desired
565
+ # size for cset and to be returned
566
+ #
567
+ # @return a bulk response, up to ARGV[1] pairs [member,score,...]
568
+ #
569
+ LUA_ICKRESERVE = (LUA_ICK_PREFIX + %{
570
+ local target_cset_size = tonumber(ARGV[1])
571
+ while true do
572
+ local ick_cset_size = redis.call('ZCARD',ick_cset_key)
573
+ if ick_cset_size and target_cset_size <= ick_cset_size then
574
+ break
575
+ end
576
+ local first_in_pset = redis.call('ZRANGE',ick_pset_key,0,0,'WITHSCORES')
577
+ if 0 == table.getn(first_in_pset) then
578
+ break
579
+ end
580
+ local first_member = first_in_pset[1]
581
+ local first_score = tonumber(first_in_pset[2])
582
+ redis.call('ZREM',ick_pset_key,first_member)
583
+ local old_score = redis.call('ZSCORE',ick_cset_key,first_member)
584
+ if false == old_score or first_score < tonumber(old_score) then
585
+ redis.call('ZADD',ick_cset_key,first_score,first_member)
586
+ end
587
+ end
588
+ redis.call('SETNX', ick_key, 'ick.v1')
589
+ if target_cset_size <= 0 then
590
+ return {}
591
+ else
592
+ local max = target_cset_size - 1
593
+ return redis.call('ZRANGE',ick_cset_key,0,max,'WITHSCORES')
594
+ end
595
+ }).freeze
596
+
597
+ #######################################################################
598
+ # LUA_ICKCOMMIT
599
+ #######################################################################
600
+ #
601
+ # Removes specified members from the pset.
602
+ #
603
+ # @param ARGV a list of members to be removed from the cset
604
+ #
605
+ # @return the number of members removed
606
+ #
607
+ # Note: This this Lua unpacks ARGV with the iterator ipairs()
608
+ # instead of unpack() to avoid a "too many results to unpack"
609
+ # failure at 8000 args. However, the loop over many redis.call is
610
+ # regrettably heavy-weight. From a performance standpoint it
611
+ # would be preferable to call ZREM in larger batches.
612
+ #
613
+ LUA_ICKCOMMIT = (LUA_ICK_PREFIX + %{
614
+ redis.call('SETNX', ick_key, 'ick.v1')
615
+ if 0 == table.getn(ARGV) then
616
+ return 0
617
+ end
618
+ local num_removed = 0
619
+ for i,v in ipairs(ARGV) do
620
+ num_removed = num_removed + redis.call('ZREM',ick_cset_key,v)
621
+ end
622
+ return num_removed
623
+ }).freeze
624
+
625
+ end
626
+ end
@@ -0,0 +1,17 @@
1
+ class Redis
2
+ class Ick
3
+ #
4
+ # Version plan:
5
+ #
6
+ # 0.0.1 - still in Prosperworks/ALI/vendor/gems/redis-ick
7
+ #
8
+ # 0.0.2 - broke out into Prosperworks/redis-ick
9
+ #
10
+ # 0.1.0 - big README.md and Rdoc update, open repo
11
+ #
12
+ # 0.2.0 - solicit and incorporate initial feedback from select
13
+ # beta external users
14
+ #
15
+ VERSION = "0.0.2" # broken into standalone repo, not polished
16
+ end
17
+ end
data/redis-ick.gemspec ADDED
@@ -0,0 +1,30 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "redis/ick/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+
8
+ spec.name = "redis-ick"
9
+ spec.version = Redis::Ick::VERSION
10
+ spec.platform = Gem::Platform::RUBY
11
+
12
+ spec.authors = ["jhwillett"]
13
+ spec.email = ["jhw@prosperworks.com"]
14
+
15
+ spec.summary = 'Redis queues with two-phase commit and write-folding.'
16
+ spec.homepage = 'https://github.com/ProsperWorks/redis-ick'
17
+
18
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
19
+ f.match(%r{^(test|spec|features)/})
20
+ end
21
+ spec.bindir = "exe"
22
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
23
+ spec.require_paths = ["lib"]
24
+
25
+ spec.add_development_dependency 'bundler', '~> 1.14'
26
+ spec.add_development_dependency 'rake', '~> 10.0'
27
+ spec.add_development_dependency 'minitest', '~> 5.0'
28
+ spec.add_development_dependency 'redis', '~> 3.2'
29
+
30
+ end
metadata ADDED
@@ -0,0 +1,110 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: redis-ick
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: ruby
6
+ authors:
7
+ - jhwillett
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2017-08-29 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.14'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.14'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: minitest
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '5.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '5.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: redis
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.2'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.2'
69
+ description:
70
+ email:
71
+ - jhw@prosperworks.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - ".gitignore"
77
+ - ".travis.yml"
78
+ - Gemfile
79
+ - LICENSE
80
+ - README.md
81
+ - Rakefile
82
+ - bin/console
83
+ - bin/setup
84
+ - lib/redis/ick.rb
85
+ - lib/redis/ick/version.rb
86
+ - redis-ick.gemspec
87
+ homepage: https://github.com/ProsperWorks/redis-ick
88
+ licenses: []
89
+ metadata: {}
90
+ post_install_message:
91
+ rdoc_options: []
92
+ require_paths:
93
+ - lib
94
+ required_ruby_version: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ version: '0'
99
+ required_rubygems_version: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ requirements: []
105
+ rubyforge_project:
106
+ rubygems_version: 2.4.8
107
+ signing_key:
108
+ specification_version: 4
109
+ summary: Redis queues with two-phase commit and write-folding.
110
+ test_files: []