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 +7 -0
- data/.gitignore +51 -0
- data/.travis.yml +10 -0
- data/Gemfile +6 -0
- data/LICENSE +21 -0
- data/README.md +108 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/redis/ick.rb +626 -0
- data/lib/redis/ick/version.rb +17 -0
- data/redis-ick.gemspec +30 -0
- metadata +110 -0
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
data/Gemfile
ADDED
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 
|
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
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
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: []
|