redis_recipes 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --colour
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source :rubygems
2
+
3
+ gemspec
@@ -0,0 +1,28 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ redis_recipes (0.3.0)
5
+
6
+ GEM
7
+ remote: http://rubygems.org/
8
+ specs:
9
+ diff-lcs (1.1.3)
10
+ rake (0.9.2.2)
11
+ redis (3.0.2)
12
+ rspec (2.11.0)
13
+ rspec-core (~> 2.11.0)
14
+ rspec-expectations (~> 2.11.0)
15
+ rspec-mocks (~> 2.11.0)
16
+ rspec-core (2.11.1)
17
+ rspec-expectations (2.11.3)
18
+ diff-lcs (~> 1.1.3)
19
+ rspec-mocks (2.11.3)
20
+
21
+ PLATFORMS
22
+ ruby
23
+
24
+ DEPENDENCIES
25
+ rake
26
+ redis (~> 3.0.0)
27
+ redis_recipes!
28
+ rspec
@@ -0,0 +1,26 @@
1
+ # Redis Recipes
2
+
3
+ Redis LUA recipes. Require Redis 2.6.0 or higher.
4
+
5
+ ## License
6
+
7
+ Copyright (c) 2012 Black Square Media Ltd
8
+
9
+ Permission is hereby granted, free of charge, to any person obtaining
10
+ a copy of this software and associated documentation files (the
11
+ "Software"), to deal in the Software without restriction, including
12
+ without limitation the rights to use, copy, modify, merge, publish,
13
+ distribute, sublicense, and/or sell copies of the Software, and to
14
+ permit persons to whom the Software is furnished to do so, subject to
15
+ the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be
18
+ included in all copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
21
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
22
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
23
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
24
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
25
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
26
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,8 @@
1
+ require 'rake'
2
+
3
+ require 'rspec/mocks/version'
4
+ require 'rspec/core/rake_task'
5
+ RSpec::Core::RakeTask.new(:spec)
6
+
7
+ desc 'Default: run specs.'
8
+ task :default => :spec
@@ -0,0 +1,62 @@
1
+ # Redis Recipes: Range Lookup
2
+
3
+ Data structure, optimised for fast lookups of (possibly overlapping) ranges
4
+ containing a given value. Works with any numericly representable ranges, e.g.
5
+ dates, times, IP addresses, etc. Inspired by: http://stackoverflow.com/questions/8622816/redis-or-mongo-for-determining-if-a-number-falls-within-ranges/8624231#8624231
6
+
7
+ ### Example / Internals:
8
+
9
+ Consider the following use case: "Holiday System"
10
+
11
+ * [A]lice is on vacation from the 4th until the 11th
12
+ * [B]ob leaves an 7th and returns on the 21st
13
+ * [C]olin is also away from 4th, but returns only on the 18th
14
+ * [D]eborah's holidays are from 16th to the 23th
15
+
16
+ Our numeric ranges would therefore be:
17
+
18
+ A 4-11
19
+ B 7-21
20
+ C 4-18
21
+ D 16-23
22
+
23
+ To identify the people away for any specific date quickly, we create a
24
+ flattened index:
25
+
26
+ 4 [A C]
27
+ 7 [A B C]
28
+ 11 [B C]
29
+ 16 [B C D]
30
+ 18 [B D]
31
+ 21 [D]
32
+ 23 []
33
+
34
+ For any given day, we can match the list (set) of members that are on vacation,
35
+ by:
36
+
37
+ 1. Just reading the members for exact hits. For example: on the 18th [B C D]
38
+ match.
39
+ 2. Creating an intersection between the preceeding and the following set. For
40
+ example: on the 19th INTER([B C D], [B D]) -> [B D] match.
41
+
42
+ ### Usage:
43
+
44
+ Add ranges:
45
+
46
+ redis-cli --eval <path/to/add.lua> holidays , A 4 11
47
+ redis-cli --eval <path/to/add.lua> holidays , B 7 21
48
+ redis-cli --eval <path/to/add.lua> holidays , C 4 18
49
+ redis-cli --eval <path/to/add.lua> holidays , D 16 23
50
+ redis-cli --eval <path/to/add.lua> holidays , X 5 15
51
+
52
+ Remove a range:
53
+
54
+ redis-cli --eval <path/to/remove.lua> holidays , X 5 15
55
+
56
+ Lookup range:
57
+
58
+ redis-cli --eval <path/to/lookup.lua> holidays , 18 # => 1) "B"
59
+ # 2) "C"
60
+ # 3) "D"
61
+ redis-cli --eval <path/to/lookup.lua> holidays , 19 # => 1) "B"
62
+ # 2) "D"
@@ -0,0 +1,66 @@
1
+ if #KEYS ~= 1 or #ARGV ~= 3 then
2
+ return redis.error_reply("wrong number of arguments")
3
+ end
4
+
5
+ local prefix = KEYS[1]
6
+ local member = ARGV[1]
7
+ local min = tonumber(ARGV[2])
8
+ local max = tonumber(ARGV[3])
9
+
10
+ if min == nil or max == nil or min > max then
11
+ return redis.error_reply("min/max are not numeric or out of range")
12
+ end
13
+
14
+ local index = prefix .. ":~"
15
+ local minscr = redis.call('zscore', index, min)
16
+ local maxscr = redis.call('zscore', index, max)
17
+ local minwrp = {}
18
+ local maxwrp = {}
19
+ local window = redis.call('zrangebyscore', index, "(" .. min, "(" .. max)
20
+
21
+ -- Find existing members to be included in the new min
22
+ if not minscr then
23
+ local before = redis.call('zrevrangebyscore', index, "(" .. min, "-inf", "limit", 0, 1)[1]
24
+ if before then
25
+ local after = redis.call('zrangebyscore', index, "(" .. min, "inf", "limit", 0, 1)[1]
26
+ if after then
27
+ minwrp = redis.call('sinter', prefix .. ":" .. before, prefix .. ":" .. after)
28
+ end
29
+ end
30
+ end
31
+
32
+ -- Find existing members to be included in the new max
33
+ if not maxscr then
34
+ local after = redis.call('zrangebyscore', index, "(" .. max, "inf", "limit", 0, 1)[1]
35
+ if after then
36
+ local before = redis.call('zrevrangebyscore', index, "(" .. max, "-inf", "limit", 0, 1)[1]
37
+ if before then
38
+ maxwrp = redis.call('sinter', prefix .. ":" .. before, prefix .. ":" .. after)
39
+ end
40
+ end
41
+ end
42
+
43
+ -- Store members in min & max sets
44
+ redis.call('sadd', prefix .. ":" .. min, member)
45
+ redis.call('sadd', prefix .. ":" .. max, member)
46
+
47
+ -- Store new min & max indices
48
+ if not minscr then redis.call('zadd', index, min, min) end
49
+ if not maxscr then redis.call('zadd', index, max, max) end
50
+
51
+ -- Store member in all existing sets between min & max
52
+ for _,key in pairs(window) do
53
+ redis.call('sadd', prefix .. ":" .. key, member)
54
+ end
55
+
56
+ -- Merge existing members into min
57
+ if #minwrp > 0 then
58
+ redis.call('sadd', prefix .. ":" .. min, unpack(minwrp))
59
+ end
60
+
61
+ -- Merge existing members into max
62
+ if #maxwrp > 0 then
63
+ redis.call('sadd', prefix .. ":" .. max, unpack(maxwrp))
64
+ end
65
+
66
+ return redis.status_reply("OK")
@@ -0,0 +1,27 @@
1
+ if #KEYS ~= 1 or #ARGV ~= 1 then
2
+ return redis.error_reply("wrong number of arguments")
3
+ end
4
+
5
+ local prefix = KEYS[1]
6
+ local value = tonumber(ARGV[1])
7
+
8
+ if value == nil then
9
+ return redis.error_reply("value is not numeric or out of range")
10
+ end
11
+
12
+ local members = {}
13
+ local score = redis.call('zscore', prefix .. ":~", value)
14
+
15
+ if score then -- Do we have an exact match?
16
+ members = redis.call('smembers', prefix .. ":" .. score)
17
+ else
18
+ local before = redis.call('zrevrangebyscore', prefix .. ":~", value, "-inf", "limit", 0, 1)[1]
19
+ if before then
20
+ local after = redis.call('zrangebyscore', prefix .. ":~", value, "inf", "limit", 0, 1)[1]
21
+ if after then
22
+ members = redis.call('sinter', prefix .. ":" .. before, prefix .. ":" .. after)
23
+ end
24
+ end
25
+ end
26
+
27
+ return members
@@ -0,0 +1,62 @@
1
+ if #KEYS ~= 1 or #ARGV ~= 3 then
2
+ return redis.error_reply("wrong number of arguments")
3
+ end
4
+
5
+ local prefix = KEYS[1]
6
+ local member = ARGV[1]
7
+ local min = tonumber(ARGV[2])
8
+ local max = tonumber(ARGV[3])
9
+
10
+ if min == nil or max == nil or min > max then
11
+ return redis.error_reply("min/max are not numeric or out of range")
12
+ end
13
+
14
+ local index = prefix .. ":~"
15
+ local window = redis.call('zrangebyscore', index, min, max)
16
+ local before = nil
17
+ local after = nil
18
+ local minwrp = {}
19
+ local maxwrp = {}
20
+
21
+ -- Remove the member from all sets between min & max
22
+ for _, val in pairs(window) do
23
+ redis.call('srem', prefix .. ":" .. val, member)
24
+ end
25
+
26
+ -- Identify members wrapped in min
27
+ before = redis.call('zrevrangebyscore', index, "(" .. min, "-inf", "limit", 0, 1)[1]
28
+ if before then
29
+ after = redis.call('zrangebyscore', index, "(" .. min, "inf", "limit", 0, 1)[1]
30
+ if after then
31
+ minwrp = redis.call('sinter', prefix .. ":" .. before, prefix .. ":" .. after)
32
+ end
33
+ end
34
+
35
+ -- Identify members wrapped in max
36
+ after = redis.call('zrangebyscore', index, "(" .. max, "inf", "limit", 0, 1)[1]
37
+ if after then
38
+ before = redis.call('zrevrangebyscore', index, "(" .. max, "-inf", "limit", 0, 1)[1]
39
+ if before then
40
+ maxwrp = redis.call('sinter', prefix .. ":" .. before, prefix .. ":" .. after)
41
+ end
42
+ end
43
+
44
+ -- Remove existing wrapped members from min
45
+ if #minwrp > 0 then
46
+ redis.call('srem', prefix .. ":" .. min, unpack(minwrp))
47
+ end
48
+
49
+ -- Remove existing wrapped members from max
50
+ if #maxwrp > 0 then
51
+ redis.call('srem', prefix .. ":" .. max, unpack(maxwrp))
52
+ end
53
+
54
+ -- Remove the min index, if no more items are left in the min set
55
+ local minlen = redis.call('scard', prefix .. ":" .. min)
56
+ if minlen == 0 then redis.call('zrem', index, min) end
57
+
58
+ -- Remove the max index, if no more items are left in the max set
59
+ local maxlen = redis.call('scard', prefix .. ":" .. max)
60
+ if maxlen == 0 then redis.call('zrem', index, max) end
61
+
62
+ return redis.status_reply("OK")
@@ -0,0 +1,59 @@
1
+ # Redis Recipes: Range Lookup X
2
+
3
+ A data structure, very similar to the "normal" Range Lookup, except that it
4
+ stores ranges that exclude the end value.
5
+
6
+ ### Example / Internals:
7
+
8
+ Consider the following use case: "Holiday System"
9
+
10
+ * [A]lice is on vacation from the 4th until the 11th
11
+ * [B]ob leaves an 7th and returns on the 21st
12
+ * [C]olin is also away from 4th, but returns only on the 18th
13
+ * [D]eborah's holidays are from 16th to the 23th
14
+
15
+ Our numeric ranges would therefore be:
16
+
17
+ A 4...12
18
+ B 7...22
19
+ C 4...19
20
+ D 16...24
21
+
22
+ To identify the people away for any specific date quickly, we create a
23
+ flattened index:
24
+
25
+ 4 [A C]
26
+ 7 [A B C]
27
+ 12 [B C]
28
+ 16 [B C D]
29
+ 19 [B D]
30
+ 22 [D]
31
+ 24 []
32
+
33
+ For any given day, we can match the list (set) of members that are on vacation,
34
+ by:
35
+
36
+ 1. Finding the index <= the given value. For example: a search for the 18th would return 16.
37
+ 2. Reading the members on the index. For example: for the 16th, [B C D] would be returned.
38
+
39
+ ### Usage:
40
+
41
+ Add ranges:
42
+
43
+ redis-cli --eval <path/to/add.lua> holidays , A 4 12
44
+ redis-cli --eval <path/to/add.lua> holidays , B 7 22
45
+ redis-cli --eval <path/to/add.lua> holidays , C 4 19
46
+ redis-cli --eval <path/to/add.lua> holidays , D 16 24
47
+ redis-cli --eval <path/to/add.lua> holidays , X 5 16
48
+
49
+ Remove a range:
50
+
51
+ redis-cli --eval <path/to/remove.lua> holidays , X 5 16
52
+
53
+ Lookup range:
54
+
55
+ redis-cli --eval <path/to/lookup.lua> holidays , 18 # => 1) "B"
56
+ # 2) "C"
57
+ # 3) "D"
58
+ redis-cli --eval <path/to/lookup.lua> holidays , 19 # => 1) "B"
59
+ # 2) "D"
@@ -0,0 +1,59 @@
1
+ if #KEYS ~= 1 or #ARGV ~= 3 then
2
+ return redis.error_reply("wrong number of arguments")
3
+ end
4
+
5
+ local prefix = KEYS[1]
6
+ local member = ARGV[1]
7
+ local min = tonumber(ARGV[2])
8
+ local max = tonumber(ARGV[3])
9
+
10
+ if min == nil or max == nil or min >= max then
11
+ return redis.error_reply("min/max are not numeric or out of range")
12
+ end
13
+
14
+ local index = prefix .. ":~"
15
+ local minscr = redis.call('zscore', index, min)
16
+ local maxscr = redis.call('zscore', index, max)
17
+ local minwrp = {}
18
+ local maxwrp = {}
19
+ local window = redis.call('zrangebyscore', index, "(" .. min, "(" .. max)
20
+
21
+ -- Find existing members to be included in the new min
22
+ if not minscr then
23
+ local before = redis.call('zrevrangebyscore', index, "(" .. min, "-inf", "limit", 0, 1)[1]
24
+ if before then
25
+ minwrp = redis.call('smembers', prefix .. ":" .. before)
26
+ end
27
+ end
28
+
29
+ -- Find existing members to be included in the new max
30
+ if not maxscr then
31
+ local before = redis.call('zrevrangebyscore', index, "(" .. max, "-inf", "limit", 0, 1)[1]
32
+ if before then
33
+ maxwrp = redis.call('smembers', prefix .. ":" .. before)
34
+ end
35
+ end
36
+
37
+ -- Store members in min set
38
+ redis.call('sadd', prefix .. ":" .. min, member)
39
+
40
+ -- Store new min & max indices
41
+ if not minscr then redis.call('zadd', index, min, min) end
42
+ if not maxscr then redis.call('zadd', index, max, max) end
43
+
44
+ -- Store member in all existing sets between min & max
45
+ for _,key in pairs(window) do
46
+ redis.call('sadd', prefix .. ":" .. key, member)
47
+ end
48
+
49
+ -- Merge existing members into min
50
+ if #minwrp > 0 then
51
+ redis.call('sadd', prefix .. ":" .. min, unpack(minwrp))
52
+ end
53
+
54
+ -- Merge existing members into max
55
+ if #maxwrp > 0 then
56
+ redis.call('sadd', prefix .. ":" .. max, unpack(maxwrp))
57
+ end
58
+
59
+ return redis.status_reply("OK")
@@ -0,0 +1,19 @@
1
+ if #KEYS ~= 1 or #ARGV ~= 1 then
2
+ return redis.error_reply("wrong number of arguments")
3
+ end
4
+
5
+ local prefix = KEYS[1]
6
+ local value = tonumber(ARGV[1])
7
+
8
+ if value == nil then
9
+ return redis.error_reply("value is not numeric or out of range")
10
+ end
11
+
12
+ local members = {}
13
+ local pos = redis.call('zrevrangebyscore', prefix .. ":~", value, "-inf", "limit", 0, 1)[1]
14
+
15
+ if pos then
16
+ members = redis.call('smembers', prefix .. ":" .. pos)
17
+ end
18
+
19
+ return members
@@ -0,0 +1,51 @@
1
+ if #KEYS ~= 1 or #ARGV ~= 3 then
2
+ return redis.error_reply("wrong number of arguments")
3
+ end
4
+
5
+ local prefix = KEYS[1]
6
+ local member = ARGV[1]
7
+ local min = tonumber(ARGV[2])
8
+ local max = tonumber(ARGV[3])
9
+
10
+ if min == nil or max == nil or min >= max then
11
+ return redis.error_reply("min/max are not numeric or out of range")
12
+ end
13
+
14
+ local index = prefix .. ":~"
15
+ local window = redis.call('zrangebyscore', index, min, "(" .. max)
16
+
17
+ local minlen = redis.call('scard', prefix .. ":" .. min)
18
+ local maxlen = redis.call('scard', prefix .. ":" .. max)
19
+
20
+ -- Calculate cardinality of the set before min
21
+ local befmin = redis.call('zrevrangebyscore', index, "(" .. min, "-inf", "limit", 0, 1)[1]
22
+ local bminlen = 0
23
+ if befmin then
24
+ bminlen = redis.call('scard', prefix .. ":" .. befmin)
25
+ end
26
+
27
+ -- Calculate cardinality of the set before max
28
+ local befmax = redis.call('zrevrangebyscore', index, "(" .. max, "-inf", "limit", 0, 1)[1]
29
+ local bmaxlen = 0
30
+ if befmax then
31
+ bmaxlen = redis.call('scard', prefix .. ":" .. befmax)
32
+ end
33
+
34
+ -- Remove min if the cardinality between min and the set before min differs by 1
35
+ if minlen - bminlen == 1 then
36
+ redis.call('del', prefix .. ":" .. min)
37
+ redis.call('zrem', index, min)
38
+ end
39
+
40
+ -- Remove max if the cardinality between max and the set before max differs by -1
41
+ if bmaxlen - maxlen == 1 then
42
+ redis.call('del', prefix .. ":" .. max)
43
+ redis.call('zrem', index, max)
44
+ end
45
+
46
+ -- Remove the member from all sets between min & max
47
+ for _, val in pairs(window) do
48
+ redis.call('srem', prefix .. ":" .. val, member)
49
+ end
50
+
51
+ return redis.status_reply("OK")