redis-bitops 0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,47 @@
1
+ require 'forwardable'
2
+
3
+ class Redis
4
+ module Bitops
5
+ module Queries
6
+
7
+ # Support for materializing expressions when one of the supported bitmap methods is called.
8
+ #
9
+ # Example
10
+ #
11
+ # a = $redis.sparse_bitmap("a")
12
+ # b = $redis.sparse_bitmap("b")
13
+ # a[0] = true
14
+ # result = a | b
15
+ # puts result[0] => true
16
+ #
17
+ module LazyEvaluation
18
+ extend Forwardable
19
+
20
+ def_delegators :dest, :bitcount, :[], :[]=, :<<, :delete!, :root_key
21
+
22
+ def dest
23
+ if @dest.nil?
24
+ @dest = temp_bitmap
25
+ do_evaluate(@dest)
26
+ end
27
+ @dest
28
+ end
29
+
30
+ # Optimizes the expression and materializes it into bitmap 'dest'.
31
+ #
32
+ def evaluate(dest_bitmap)
33
+ if @dest
34
+ @dest.copy_to(dest_bitmap)
35
+ else
36
+ do_evaluate(dest_bitmap)
37
+ end
38
+ end
39
+
40
+ protected def do_evaluate(dest_bitmap)
41
+ optimize!
42
+ materialize(dest_bitmap)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,53 @@
1
+ class Redis
2
+ module Bitops
3
+ module Queries
4
+
5
+ # Helpers for materialization, i.e. running a BITOP command or, possibly, another command
6
+ # and saving its results to a Redis database in 'intermediate'.
7
+ #
8
+ module MaterializationHelpers
9
+
10
+ # Materializes the operand 'o' saving the result in 'redis'.
11
+ # If the operand can be materialized, it does so storing the result in 'intermediate'
12
+ # unless the latter is nil. In that case, a temp intermediate bitmap is created to hold
13
+ # the result (and the bitmap is added to 'temp_intermediates').
14
+ #
15
+ def resolve_operand(o, intermediate, temp_intermediates)
16
+ if o.respond_to?(:materialize)
17
+ if intermediate.nil?
18
+ new_intermediate = temp_bitmap
19
+ temp_intermediates << new_intermediate
20
+ end
21
+ intermediate ||= new_intermediate
22
+ o.materialize(intermediate)
23
+ [intermediate, nil]
24
+ else
25
+ [o, intermediate]
26
+ end
27
+ end
28
+
29
+ # Creates a temp bitmap.
30
+ #
31
+ def temp_bitmap
32
+ bitmap = bitmap_factory.call(unique_key)
33
+ bitmap
34
+ end
35
+
36
+ # Generates a random unique key.
37
+ #
38
+ # TODO: The key _should_ be unique and not repeat in the
39
+ # database but this isn't guaranteed. Considering the intended usage though
40
+ # (creation of temporary intermediate bitmaps while materializing
41
+ # queries), it should be sufficient.
42
+ #
43
+ def unique_key
44
+ "redis:bitops:#{SecureRandom.hex(20)}"
45
+ end
46
+
47
+ def bitmap_factory
48
+ raise "Override in the class using the module to return the bitmap factory."
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,46 @@
1
+ class Redis
2
+ module Bitops
3
+ module Queries
4
+ # Helpers for expression tree building.
5
+ #
6
+ # Add new logical operators here as/if they become supported by Redis' BITOP command.
7
+ #
8
+ module TreeBuildingHelpers
9
+
10
+ # Logical AND operator.
11
+ #
12
+ def & (rhs)
13
+ BinaryOperator.new(:and, self, rhs)
14
+ end
15
+
16
+ # Logical OR operator.
17
+ #
18
+ def | (rhs)
19
+ BinaryOperator.new(:or, self, rhs)
20
+ end
21
+
22
+ # Logical XOR operator.
23
+ #
24
+ def ^ (rhs)
25
+ BinaryOperator.new(:xor, self, rhs)
26
+ end
27
+
28
+ # Logical NOT operator.
29
+ #
30
+ # IMPORTANT: It inverts bits padding with zeroes till the nearest byte boundary.
31
+ # It means that setting the left-most bit to 1 and inverting will result in 01111111 not 0.
32
+ #
33
+ # Corresponding Redis commands:
34
+ #
35
+ # SETBIT "a" 0 1
36
+ # BITOP NOT "b" "a"
37
+ # BITCOUNT "b"
38
+ # => (integer) 7
39
+ #
40
+ def ~
41
+ UnaryOperator.new(:not, self)
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,48 @@
1
+ class Redis
2
+ module Bitops
3
+ module Queries
4
+
5
+ # A unary bitwise operator. Currently only NOT is supported by redis.
6
+ #
7
+ class UnaryOperator
8
+ include MaterializationHelpers
9
+ include TreeBuildingHelpers
10
+ include LazyEvaluation
11
+
12
+ # Create a new bitwise operator 'op' with one argument 'arg'.
13
+ #
14
+ def initialize(op, arg)
15
+ @op = op
16
+ @arg = arg
17
+ end
18
+
19
+ # Runs the expression tree against the redis database, saving the results
20
+ # in bitmap 'dest'.
21
+ #
22
+ def materialize(dest)
23
+ temp_intermediates = []
24
+ result, = resolve_operand(@arg, dest, temp_intermediates)
25
+ result.bitop(@op, dest)
26
+ ensure
27
+ temp_intermediates.each(&:delete!)
28
+ end
29
+
30
+ # Optimizes the expression tree by combining operands for neighboring identical operators.
31
+ # Because NOT operator in Redis can only accept one operand, no optimization is made
32
+ # for the operand but the children are optimized recursively.
33
+ #
34
+ def optimize!(parent_op = nil)
35
+ @arg.optimize!(@op) if @arg.respond_to?(:optimize!)
36
+ self
37
+ end
38
+
39
+ # Finds the first bitmap factory in the expression tree.
40
+ # Required by LazyEvaluation and MaterializationHelpers.
41
+ #
42
+ def bitmap_factory
43
+ @arg.bitmap_factory or raise "Internal error. Cannot get redis connection."
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,125 @@
1
+ require 'set'
2
+
3
+ class Redis
4
+ module Bitops
5
+
6
+ # A sparse bitmap using multiple key to store its data to save memory.
7
+ #
8
+ # Note: When adding new public methods, revise the LazyEvaluation module.
9
+ #
10
+ class SparseBitmap < Bitmap
11
+
12
+ # Creates a new sparse bitmap stored in 'redis' under 'root_key'.
13
+ #
14
+ def initialize(root_key, redis, bytes_per_chunk = nil)
15
+ @bytes_per_chunk = bytes_per_chunk || Redis::Bitops.configuration.default_bytes_per_chunk
16
+ super(root_key, redis)
17
+ end
18
+
19
+ # Returns the number of set bits.
20
+ #
21
+ def bitcount
22
+ chunk_keys.map { |key| @redis.bitcount(key) }.reduce(:+) || 0
23
+ end
24
+
25
+ # Deletes the bitmap and all its keys.
26
+ #
27
+ def delete!
28
+ chunk_keys.each do |key|
29
+ @redis.del(key)
30
+ end
31
+ super
32
+ end
33
+
34
+ # Redis BITOP operator 'op' (one of :and, :or, :xor or :not) on operands
35
+ # (bitmaps). The result is stored in 'result'.
36
+ #
37
+ def bitop(op, *operands, result)
38
+ # TODO: Optimization is possible for AND. We can use an intersection of each operand
39
+ # chunk numbers to minimize the number of database accesses.
40
+
41
+ all_keys = self.chunk_keys + (operands.map(&:chunk_keys).flatten! || [])
42
+ unique_chunk_numbers = Set.new(chunk_numbers(all_keys))
43
+
44
+ maybe_multi(level: :bitmap, watch: all_keys) do
45
+ unique_chunk_numbers.each do |i|
46
+ @redis.bitop(op, result.chunk_key(i), self.chunk_key(i), *operands.map { |o| o.chunk_key(i) })
47
+ end
48
+ end
49
+ result
50
+ end
51
+
52
+ def chunk_keys
53
+ @redis.keys("#{@root_key}:chunk:*")
54
+ end
55
+
56
+ def chunk_key(i)
57
+ "#{@root_key}:chunk:#{i}"
58
+ end
59
+
60
+ # Returns lambda creating SparseBitmap objects using @redis as the connection.
61
+ #
62
+ def bitmap_factory
63
+ lambda { |key| @redis.sparse_bitmap(key, @bytes_per_chunk) }
64
+ end
65
+
66
+ # Copy this bitmap to 'dest' bitmap.
67
+ #
68
+ def copy_to(dest)
69
+
70
+ # Copies all source chunks to destination chunks and deletes remaining destination chunk keys.
71
+
72
+ source_keys = self.chunk_keys
73
+ dest_keys = dest.chunk_keys
74
+
75
+ maybe_multi(level: :bitmap, watch: source_keys + dest_keys) do
76
+ source_chunks = Set.new(chunk_numbers(source_keys))
77
+ source_chunks.each do |i|
78
+ copy(chunk_key(i), dest.chunk_key(i))
79
+ end
80
+ dest_chunks = Set.new(chunk_numbers(dest_keys))
81
+ (dest_chunks - source_chunks).each do |i|
82
+ @redis.del(dest.chunk_key(i))
83
+ end
84
+ end
85
+ end
86
+
87
+ protected
88
+
89
+ def bits_per_chunk
90
+ @bytes_per_chunk * 8
91
+ end
92
+
93
+ def key(pos)
94
+ chunk_key(chunk_number(pos))
95
+ end
96
+
97
+ def offset(pos)
98
+ pos.modulo bits_per_chunk
99
+ end
100
+
101
+ def chunk_number(pos)
102
+ (pos / bits_per_chunk).to_i
103
+ end
104
+
105
+ def chunk_numbers(keys)
106
+ keys.map { |key| key.split(":").last.to_i }
107
+ end
108
+
109
+ # Maybe pipeline/make atomic based on the configuration.
110
+ #
111
+ def maybe_multi(options = {}, &block)
112
+ current_level = options[:level] or raise "Specify the current transaction level."
113
+
114
+ if Redis::Bitops.configuration.transaction_level == current_level
115
+ watched_keys = options[:watch]
116
+ @redis.watch(watched_keys) if watched_keys
117
+ @redis.multi(&block)
118
+ else
119
+ block.call
120
+ end
121
+ end
122
+
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,9 @@
1
+ require 'spec_helper'
2
+
3
+ describe Redis::Bitops::Bitmap, redis_cleanup: true, redis_key_prefix: "rsb:" do
4
+ it_should_behave_like "a bitmap", :bitmap
5
+ end
6
+
7
+ describe "Redis#bitmap" do
8
+ it_should_behave_like "a bitmap factory method", :bitmap, Redis::Bitops::Bitmap
9
+ end
@@ -0,0 +1,24 @@
1
+ require 'spec_helper'
2
+
3
+ describe Redis::Bitops::Queries::BinaryOperator do
4
+ let(:a) { double("a") }
5
+ let(:b) { double("b") }
6
+ let(:c) { double("c") }
7
+ let(:d) { double("d") }
8
+ let(:e) { double("e") }
9
+
10
+ let(:redis) { double("redis", del: nil) }
11
+ let(:result) { double("output", redis: redis) }
12
+
13
+ it "optimizes the expression tree" do
14
+ a.should_receive(:bitop).with(:and, result, d, e, result)
15
+ b.should_receive(:bitop).with(:or, c, result)
16
+ expr = Redis::Bitops::Queries::BinaryOperator.new(:and,
17
+ a,
18
+ Redis::Bitops::Queries::BinaryOperator.new(:and,
19
+ Redis::Bitops::Queries::BinaryOperator.new(:or, b, c),
20
+ Redis::Bitops::Queries::BinaryOperator.new(:and, d, e)))
21
+ expr.optimize!
22
+ expr.materialize(result)
23
+ end
24
+ end
@@ -0,0 +1,27 @@
1
+ require 'spec_helper'
2
+
3
+ describe Redis::Bitops::Queries::UnaryOperator do
4
+ let(:a) { double("a") }
5
+ let(:b) { double("b") }
6
+ let(:c) { double("c") }
7
+ let(:d) { double("d") }
8
+ let(:e) { double("e") }
9
+
10
+ let(:redis) { double("redis", del: nil) }
11
+ let(:result) { double("result", redis: redis) }
12
+
13
+ it "optimizes the expression tree" do
14
+ a.should_receive(:bitop).with(:and, result, d, e, result)
15
+ b.should_receive(:bitop).with(:or, c, result)
16
+ result.should_receive(:bitop).with(:not, result)
17
+ expr =
18
+ Redis::Bitops::Queries::UnaryOperator.new(:not,
19
+ Redis::Bitops::Queries::BinaryOperator.new(:and,
20
+ a,
21
+ Redis::Bitops::Queries::BinaryOperator.new(:and,
22
+ Redis::Bitops::Queries::BinaryOperator.new(:or, b, c),
23
+ Redis::Bitops::Queries::BinaryOperator.new(:and, d, e))))
24
+ expr.optimize!
25
+ expr.materialize(result)
26
+ end
27
+ end
@@ -0,0 +1,99 @@
1
+ require 'spec_helper'
2
+
3
+ describe Redis::Bitops::SparseBitmap, redis_cleanup: true, redis_key_prefix: "rsb:" do
4
+ it_should_behave_like "a bitmap", :sparse_bitmap
5
+
6
+ let(:redis) { Redis.new }
7
+ let(:bytes_per_chunk) { 4 }
8
+ let(:bits_per_chunk) { bytes_per_chunk * 8 }
9
+ let(:a) { redis.sparse_bitmap("rsb:a", bytes_per_chunk) }
10
+ let(:b) { redis.sparse_bitmap("rsb:b", bytes_per_chunk) }
11
+ let(:c) { redis.sparse_bitmap("rsb:c", bytes_per_chunk) }
12
+ let(:result) { redis.sparse_bitmap("rsb:output", bytes_per_chunk) }
13
+
14
+ describe "edge cases" do
15
+ before do
16
+ a[0] = true
17
+ a[bits_per_chunk - 1] = true
18
+ a[bits_per_chunk] = true
19
+ a[2 * bits_per_chunk - 1] = true
20
+ a[2 * bits_per_chunk] = true
21
+
22
+ b[0] = true
23
+ b[bits_per_chunk] = true
24
+ b[2 * bits_per_chunk - 1] = true
25
+ end
26
+
27
+ describe "#[]" do
28
+ it "handles bits arround chunk boundaries" do
29
+ a.chunk_keys.should have(3).items
30
+ set_bits =
31
+ (0..(3 * bits_per_chunk)).inject([]) { |acc, i|
32
+ acc << i if a[i]
33
+ acc
34
+ }
35
+ set_bits.should match_array([
36
+ 0,
37
+ bits_per_chunk - 1,
38
+ bits_per_chunk,
39
+ 2 * bits_per_chunk - 1,
40
+ 2 * bits_per_chunk])
41
+ end
42
+ end
43
+
44
+ describe "#bitcount" do
45
+ it "handles bits around chunk boundaries" do
46
+ a.bitcount.should == 5
47
+ end
48
+ end
49
+
50
+ describe "#operator |" do
51
+ it "handles bits around chunk boundaries" do
52
+ result << (a | b)
53
+ result.bitcount.should == 5
54
+ end
55
+ end
56
+
57
+ describe "#operator &" do
58
+ it "handles bits around chunk boundaries" do
59
+ result << (a & b)
60
+ result.bitcount.should == 3
61
+ end
62
+ end
63
+
64
+ describe "#operator ~" do
65
+ it "handles bits around chunk boundaries" do
66
+ result << (~(a & b))
67
+ result[0].should be_false
68
+ result[1].should be_true
69
+ result[bits_per_chunk - 1].should be_true
70
+ result[bits_per_chunk].should be_false
71
+ result[bits_per_chunk + 1].should be_true
72
+ result[2 * bits_per_chunk - 2].should be_true
73
+ result[2 * bits_per_chunk - 1].should be_false
74
+ result[2 * bits_per_chunk].should be_true
75
+ end
76
+ end
77
+
78
+ describe "#operator ^" do
79
+ it "handles bits around chunk boundaries" do
80
+ result << (a ^ b)
81
+ result.bitcount.should == 2
82
+ end
83
+ end
84
+
85
+ describe "#copy_to" do
86
+ it "overrides all chunks in the target bitmap" do
87
+ # Fix expression with bits set using [] after evaluation doesn't materialize the newly set bits.
88
+ result[4*bits_per_chunk + 1] = true
89
+ a.copy_to(result)
90
+ result.bitcount.should == a.bitcount
91
+ result[4*bits_per_chunk + 1].should be_false
92
+ end
93
+ end
94
+ end
95
+ end
96
+
97
+ describe "Redis#sparse_bitmap" do
98
+ it_should_behave_like "a bitmap factory method", :sparse_bitmap, Redis::Bitops::SparseBitmap
99
+ end