acts_as_hashish 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/acts_as_hashish.rb +1 -0
- data/lib/acts_as_hashish/hashish.rb +91 -63
- data/lib/acts_as_hashish/version.rb +1 -1
- data/spec/hashish_spec.rb +113 -4
- metadata +1 -1
data/lib/acts_as_hashish.rb
CHANGED
@@ -19,77 +19,105 @@ module Hashish
|
|
19
19
|
key_value = key.is_a?(Proc) ? key.call(item) : item[key]
|
20
20
|
end
|
21
21
|
|
22
|
+
def remove_hashish_metadata(key)
|
23
|
+
prefix = @options[:key_prefix]
|
24
|
+
Hashish.redis_connection.smembers("#{prefix}#INDEXES").each do |member|
|
25
|
+
Hashish.redis_connection.zrem(member, "#{prefix}@#{key}")
|
26
|
+
Hashish.redis_connection.srem("#{prefix}#INDEXES", member) if Hashish.redis_connection.zcard(member) == 0
|
27
|
+
end
|
28
|
+
Hashish.redis_connection.smembers("#{prefix}#SORTERS").each do |member|
|
29
|
+
if member =~ /^#{prefix}\$[\w]+@#{key}$/
|
30
|
+
Hashish.redis_connection.del(member)
|
31
|
+
Hashish.redis_connection.srem("#{prefix}#SORTERS", member)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def add_hashish_metadata(name, category)
|
37
|
+
prefix = @options[:key_prefix]
|
38
|
+
Hashish.redis_connection.sadd("#{prefix}##{category}", name)
|
39
|
+
end
|
40
|
+
|
41
|
+
def redis_keys
|
42
|
+
Hashish.redis_connection.keys("#{@options[:key_prefix]}*").inject({}){|h,x| h[x] = Hashish.redis_connection.type(x);h}
|
43
|
+
end
|
44
|
+
|
45
|
+
def wait_on_lock(timeout = 0)
|
46
|
+
lock_key = "#{@options[:key_prefix]}#MUTEX_LOCK"
|
47
|
+
begin
|
48
|
+
Timeout::timeout(timeout) do
|
49
|
+
while Hashish.redis_connection.incr(lock_key) != 1
|
50
|
+
sleep(1)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
yield if block_given?
|
54
|
+
ensure
|
55
|
+
Hashish.redis_connection.del(lock_key)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
22
59
|
public
|
23
|
-
def
|
24
|
-
|
60
|
+
def hashish_rebuild
|
61
|
+
temp = nil
|
62
|
+
wait_on_lock do
|
63
|
+
temp = hashish_list(:page_size => 0)
|
64
|
+
hashish_flush!
|
65
|
+
end
|
66
|
+
yield if block_given?
|
67
|
+
temp.each do |item|
|
25
68
|
hashish_insert(item)
|
26
69
|
end
|
27
70
|
end
|
28
|
-
|
29
|
-
def
|
71
|
+
|
72
|
+
def hashish_flush!
|
30
73
|
Hashish.redis_connection.keys("#{@options[:key_prefix]}*").each{|x| Hashish.redis_connection.del(x)}
|
31
74
|
end
|
32
75
|
|
33
|
-
def
|
34
|
-
Hashish.redis_connection.zcard("#{@options[:key_prefix]}
|
76
|
+
def hashish_length
|
77
|
+
Hashish.redis_connection.zcard("#{@options[:key_prefix]}*")
|
35
78
|
end
|
36
79
|
|
37
80
|
def hashish_delete(key = nil)
|
38
|
-
key ||= get_hashish_key(
|
81
|
+
key ||= get_hashish_key(hashish_list.first)
|
39
82
|
prefix = @options[:key_prefix]
|
40
|
-
Hashish.redis_connection.
|
41
|
-
|
42
|
-
|
43
|
-
Hashish.redis_connection.del("#{prefix}:#{key}")
|
83
|
+
Hashish.redis_connection.zrem("#{prefix}*", "#{prefix}@#{key}")
|
84
|
+
remove_hashish_metadata("#{key}")
|
85
|
+
Hashish.redis_connection.del("#{prefix}@#{key}")
|
44
86
|
end
|
45
87
|
|
46
|
-
def hashish_insert(o,
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
88
|
+
def hashish_insert(o, t = Time.now.to_i)
|
89
|
+
wait_on_lock do
|
90
|
+
data = o.to_json
|
91
|
+
prefix = @options[:key_prefix]
|
92
|
+
key = get_hashish_key(o)
|
93
|
+
raise "Error: Computed data key as '#{key}'. Only alphanumeric characters allowed!" unless key.to_s =~ /^[\w]+$/
|
94
|
+
Hashish.redis_connection.multi do
|
95
|
+
Hashish.redis_connection.zadd("#{prefix}*", t , "#{prefix}@#{key}")
|
96
|
+
# Hashish.redis_connection.zremrangebyrank("#{prefix}:", 0, -(@options[:max_size] + 1)) if @options[:max_size]
|
97
|
+
@options[:indexes].each do |index_field, index|
|
98
|
+
index_value = index.is_a?(Proc) ? index.call(o) : o[index]
|
99
|
+
if index_value.is_a?(Array)
|
100
|
+
index_value.each do |i|
|
101
|
+
Hashish.redis_connection.zadd("#{prefix}!#{index_field}=#{i}", t, "#{prefix}@#{key}") if i.is_a?(String)
|
102
|
+
add_hashish_metadata("#{prefix}!#{index_field}=#{i}", 'INDEXES')
|
103
|
+
end
|
104
|
+
else
|
105
|
+
Hashish.redis_connection.zadd("#{prefix}!#{index_field}=#{index_value}", t, "#{prefix}@#{key}")
|
106
|
+
add_hashish_metadata("#{prefix}!#{index_field}=#{index_value}", 'INDEXES')
|
107
|
+
end
|
60
108
|
end
|
61
|
-
|
62
|
-
|
109
|
+
@options[:sorters].each do |sort_field, sort|
|
110
|
+
sort_value = sort.is_a?(Proc) ? sort.call(o) : o[sort]
|
111
|
+
Hashish.redis_connection.set("#{prefix}$#{sort_field}@#{key}", sort_value)
|
112
|
+
add_hashish_metadata("#{prefix}$#{sort_field}@#{key}", 'SORTERS')
|
113
|
+
end
|
114
|
+
Hashish.redis_connection.set("#{prefix}@#{key}", data)
|
63
115
|
end
|
64
116
|
end
|
65
|
-
@options[:sorters].each do |sort_field, sort|
|
66
|
-
sort_value = sort.is_a?(Proc) ? sort.call(o) : o[sort]
|
67
|
-
Hashish.redis_connection.set("#{prefix}:#{key}:#{sort_field}", sort_value)
|
68
|
-
end
|
69
|
-
Hashish.redis_connection.set("#{prefix}:#{key}", data)
|
70
117
|
true
|
71
118
|
end
|
72
119
|
|
73
|
-
def hashish_list(
|
74
|
-
category = nil
|
75
|
-
|
76
|
-
options = {}
|
77
|
-
|
78
|
-
# handle all kinds of argument structures that make sense
|
79
|
-
if args.length == 1
|
80
|
-
if args[0].is_a?(Hash)
|
81
|
-
options = args[0]
|
82
|
-
elsif args[0].is_a?(String)
|
83
|
-
category = args[0]
|
84
|
-
else
|
85
|
-
raise
|
86
|
-
end
|
87
|
-
elsif args.length > 1
|
88
|
-
category = args[0]
|
89
|
-
options = args[1]
|
90
|
-
end
|
91
|
-
|
92
|
-
category += ':' if category
|
120
|
+
def hashish_list(options = {})
|
93
121
|
page_no = options[:page_no] || 1
|
94
122
|
page_size = options[:page_size] || 10
|
95
123
|
|
@@ -104,7 +132,7 @@ module Hashish
|
|
104
132
|
limit = offset + page_size
|
105
133
|
|
106
134
|
# get the next seq no for search operations for this search instance (perhaps this entire section shld be oops based stuff but what the heck :E mayb later)
|
107
|
-
seq = Hashish.redis_connection.incr("#{@options[:key_prefix]}
|
135
|
+
seq = Hashish.redis_connection.incr("#{@options[:key_prefix]}#SEQUENCER")
|
108
136
|
|
109
137
|
# search
|
110
138
|
inter = []
|
@@ -114,28 +142,28 @@ module Hashish
|
|
114
142
|
filters.each do |key, value|
|
115
143
|
if value.is_a?(Array)
|
116
144
|
union = []
|
117
|
-
union_key = "#{@options[:key_prefix]}
|
145
|
+
union_key = "#{@options[:key_prefix]}#UNION##{key}##{seq}"
|
118
146
|
value.each do |v|
|
119
|
-
union << "#{@options[:key_prefix]}
|
147
|
+
union << "#{@options[:key_prefix]}!#{key}=#{v}"
|
120
148
|
end
|
121
149
|
Hashish.redis_connection.zunionstore(union_key, union.uniq)
|
122
150
|
Hashish.redis_connection.expire(union_key, Hashish.redis_search_keys_ttl)
|
123
151
|
inter << union_key
|
124
152
|
else
|
125
|
-
inter << "#{@options[:key_prefix]}
|
153
|
+
inter << "#{@options[:key_prefix]}!#{key}=#{value}"
|
126
154
|
end
|
127
155
|
end
|
128
156
|
|
129
157
|
end
|
130
158
|
|
131
|
-
full_list = "#{@options[:key_prefix]}
|
159
|
+
full_list = "#{@options[:key_prefix]}*"
|
132
160
|
|
133
161
|
# is the user askin for a cropped set of data (min/max date/time of nQ)
|
134
162
|
if min_time == '-inf' and max_time == '+inf'
|
135
163
|
inter << full_list
|
136
164
|
else
|
137
165
|
# copy the full list to different temp set
|
138
|
-
all_items_key = "#{@options[:key_prefix]}
|
166
|
+
all_items_key = "#{@options[:key_prefix]}#ALL_ITEMS##{seq}"
|
139
167
|
Hashish.redis_connection.zunionstore(all_items_key, [full_list])
|
140
168
|
Hashish.redis_connection.expire(all_items_key, Hashish.redis_search_keys_ttl * 60)
|
141
169
|
# crop the set based on min/max (wish we had a 1 step zcroprangebyscore)
|
@@ -144,15 +172,15 @@ module Hashish
|
|
144
172
|
inter << all_items_key
|
145
173
|
end
|
146
174
|
|
147
|
-
result_key = "#{@options[:key_prefix]}
|
175
|
+
result_key = "#{@options[:key_prefix]}#RESULT##{seq}"
|
148
176
|
Hashish.redis_connection.zinterstore(result_key, inter, :aggregate => 'max')
|
149
177
|
Hashish.redis_connection.expire(result_key, Hashish.redis_search_keys_ttl * 60)
|
150
178
|
result = nil
|
151
179
|
if sort_by
|
152
|
-
|
153
|
-
Hashish.redis_connection.sort(result_key, :by => "*:#{sort_by}",:get => '*', :store =>
|
154
|
-
Hashish.redis_connection.expire(
|
155
|
-
result = Hashish.redis_connection.lrange(
|
180
|
+
custom_sort_key = "#{@options[:key_prefix]}#CUSTOM_SORT##{seq}"
|
181
|
+
Hashish.redis_connection.sort(result_key, :by => "*:#{sort_by}",:get => '*', :store => custom_sort_key, :order => sort_order)
|
182
|
+
Hashish.redis_connection.expire(custom_sort_key, Hashish.redis_search_keys_ttl * 60)
|
183
|
+
result = Hashish.redis_connection.lrange(custom_sort_key, offset, limit -1)
|
156
184
|
else
|
157
185
|
res_keys = Hashish.redis_connection.send("z#{(sort_order == 'DESC' ? 'rev' : '')}range".to_sym, result_key, offset, limit - 1)
|
158
186
|
if res_keys.empty?
|
data/spec/hashish_spec.rb
CHANGED
@@ -1,14 +1,123 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
describe Hashish do
|
4
|
+
|
5
|
+
before :each do
|
6
|
+
@sample_data_1 = {'id' => 1, 'name' => 'Joe'}
|
7
|
+
@sample_data_2 = {'id' => 3, 'name' => 'Doe'}
|
8
|
+
@sample_data_3 = {'id' => 2, 'name' => 'Moe'}
|
9
|
+
@sample_data_4 = {'id' => 4, 'name' => 'Loe'}
|
10
|
+
|
11
|
+
@primary_key = 'id'
|
12
|
+
|
13
|
+
@index_name = '_name'
|
14
|
+
@index_value = 'name'
|
15
|
+
|
16
|
+
@proc_index_name = 'id_name'
|
17
|
+
@proc_index_value = Proc.new{|x| "#{x['id']}=>#{x['name']}"}
|
18
|
+
|
19
|
+
@sorter_name = '_id'
|
20
|
+
@sorter_value = 'id'
|
21
|
+
|
22
|
+
SampleClass.acts_as_hashish(:key => @primary_key, :indexes => {@index_name => @index_value, @proc_index_name => @proc_index_value}, :sorters => {@sorter_name => @sorter_value})
|
23
|
+
|
24
|
+
@options = SampleClass.instance_variable_get(:@options)
|
25
|
+
end
|
4
26
|
|
5
27
|
describe ".acts_as_hashish" do
|
6
28
|
it "should hashify the class" do
|
7
|
-
SampleClass.
|
8
|
-
|
9
|
-
|
10
|
-
|
29
|
+
@options = SampleClass.instance_variable_get(:@options)
|
30
|
+
@options[:key_prefix].should eq(Hashish.redis_namespace + ":" + SampleClass.to_s)
|
31
|
+
[:hashish_rebuild, :hashish_flush!, :hashish_length, :hashish_insert, :hashish_delete, :hashish_list].each do |m|
|
32
|
+
SampleClass.public_methods.include?(m).should be_true
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe ".hashish_insert" do
|
38
|
+
it "should insert the data into the list" do
|
39
|
+
SampleClass.hashish_insert(@sample_data_1)
|
40
|
+
Hashish.redis_connection.zscore("#{@options[:key_prefix]}*", "#{@options[:key_prefix]}@#{SampleClass.send(:get_hashish_key, @sample_data_1)}").should_not be_nil
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should create appropriate indexes" do
|
44
|
+
SampleClass.hashish_insert(@sample_data_1)
|
45
|
+
Hashish.redis_connection.zscore("#{@options[:key_prefix]}!#{@index_name}=#{@sample_data_1[@index_value]}", "#{@options[:key_prefix]}@#{SampleClass.send(:get_hashish_key, @sample_data_1)}").should_not be_nil
|
46
|
+
Hashish.redis_connection.zscore("#{@options[:key_prefix]}!#{@proc_index_name}=#{@proc_index_value.call(@sample_data_1)}", "#{@options[:key_prefix]}@#{SampleClass.send(:get_hashish_key, @sample_data_1)}").should_not be_nil
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should create appropriate sorters" do
|
50
|
+
SampleClass.hashish_insert(@sample_data_1)
|
51
|
+
Hashish.redis_connection.get("#{@options[:key_prefix]}$#{@sorter_name}@#{SampleClass.send(:get_hashish_key, @sample_data_1)}").should == @sample_data_1[@sorter_value].to_s
|
11
52
|
end
|
12
53
|
end
|
54
|
+
|
55
|
+
describe ".hashish_delete" do
|
56
|
+
it "should delete the data from the list" do
|
57
|
+
SampleClass.hashish_insert(@sample_data_1)
|
58
|
+
SampleClass.hashish_delete(@sample_data_1[@primary_key])
|
59
|
+
Hashish.redis_connection.zscore("#{@options[:key_prefix]}:", "#{@options[:key_prefix]}:#{SampleClass.send(:get_hashish_key, @sample_data_1)}").should be_nil
|
60
|
+
Hashish.redis_connection.exists("#{@options[:key_prefix]}:#{SampleClass.send(:get_hashish_key, @sample_data_1)}").should be_false
|
61
|
+
end
|
62
|
+
|
63
|
+
it "should delete appropriate sorters and indexes" do
|
64
|
+
SampleClass.hashish_insert(@sample_data_1)
|
65
|
+
SampleClass.hashish_delete(@sample_data_1[@primary_key])
|
66
|
+
SampleClass.send(:redis_keys).should == {}
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
describe ".hashish_list" do
|
71
|
+
it "should list the items from the list"
|
72
|
+
end
|
73
|
+
|
74
|
+
describe ".hashish_flush!" do
|
75
|
+
it "should delete all keys pertaining to this class" do
|
76
|
+
SampleClass.hashish_insert(@sample_data_1)
|
77
|
+
SampleClass.hashish_insert(@sample_data_2)
|
78
|
+
SampleClass.hashish_insert(@sample_data_3)
|
79
|
+
SampleClass.hashish_flush!
|
80
|
+
SampleClass.send(:redis_keys).should == {}
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
describe ".hashish_length" do
|
85
|
+
it "should return the length of the list" do
|
86
|
+
SampleClass.hashish_insert(@sample_data_1)
|
87
|
+
SampleClass.hashish_insert(@sample_data_2)
|
88
|
+
SampleClass.hashish_length.should == 2
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
describe ".hashish_rebuild" do
|
93
|
+
it "should rebuild the list correctly" do
|
94
|
+
SampleClass.hashish_insert(@sample_data_1)
|
95
|
+
SampleClass.hashish_insert(@sample_data_2)
|
96
|
+
SampleClass.hashish_insert(@sample_data_3)
|
97
|
+
before = SampleClass.send(:redis_keys)
|
98
|
+
SampleClass.hashish_rebuild
|
99
|
+
after = SampleClass.send(:redis_keys)
|
100
|
+
before.should == after
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
describe ".wait_on_lock" do
|
105
|
+
it "should avoid concurrency issues" do
|
106
|
+
x = 0
|
107
|
+
threads = []
|
108
|
+
(0..1).each do |i|
|
109
|
+
threads << Thread.new do
|
110
|
+
SampleClass.send(:wait_on_lock) do
|
111
|
+
a = x + 1
|
112
|
+
sleep(5)
|
113
|
+
x = a
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
threads.each{|t|t.join}
|
118
|
+
x.should == 2
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
13
122
|
end
|
14
123
|
|