acts_as_hashish 0.2.0 → 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/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
|
|