redis-ick 0.0.2

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 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: []