redis-bitops 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.
@@ -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