redis_recipes 0.3.0

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.
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")