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.
@@ -1,5 +1,6 @@
1
1
  require 'redis'
2
2
  require 'json'
3
+ require 'timeout'
3
4
  require 'acts_as_hashish'
4
5
  require 'acts_as_hashish/version'
5
6
  require 'acts_as_hashish/configuration'
@@ -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 rebuild_indexes
24
- get_list(:page_size => 0).each do |item|
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 flush!
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 size(category = '')
34
- Hashish.redis_connection.zcard("#{@options[:key_prefix]}:#{category}")
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(get_hashish_list.first)
81
+ key ||= get_hashish_key(hashish_list.first)
39
82
  prefix = @options[:key_prefix]
40
- Hashish.redis_connection.smembers("#{prefix}:status:*").each do |v|
41
- Hashish.redis_connection.zrem(v, "#{prefix}:#{key}")
42
- end
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, category = '', t = Time.now.to_i)
47
- data = o.to_json
48
- prefix = @options[:key_prefix]
49
- category += ':' unless category.empty?
50
- key = get_hashish_key(o)
51
-
52
- Hashish.redis_connection.zadd("#{prefix}:#{category}", t , "#{prefix}:#{key}")
53
- Hashish.redis_connection.zremrangebyrank("#{prefix}:#{category}", 0, -(@options[:max_size] + 1)) if @options[:max_size]
54
- Hashish.redis_connection.sadd("#{prefix}:status:*", "#{prefix}:#{category}")
55
- @options[:indexes].each do |index_field, index|
56
- index_value = index.is_a?(Proc) ? index.call(o) : o[index]
57
- if index_value.is_a?(Array)
58
- index_value.each do |i|
59
- Hashish.redis_connection.zadd("#{prefix}:#{index_field}:#{i}", t, "#{prefix}:#{key}") if i.is_a?(String)
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
- else
62
- Hashish.redis_connection.zadd("#{prefix}:#{index_field}:#{index_value}", t, "#{prefix}:#{key}")
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(*args)
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]}:result:seq")
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]}:union:#{key}:#{seq}"
145
+ union_key = "#{@options[:key_prefix]}#UNION##{key}##{seq}"
118
146
  value.each do |v|
119
- union << "#{@options[:key_prefix]}:#{key}:#{v}"
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]}:#{key}:#{value}"
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]}:#{category}"
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]}:all_items:#{seq}"
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]}:result:#{seq}"
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
- custom_sort = "#{@options[:key_prefix]}:custom_sort:#{seq}"
153
- Hashish.redis_connection.sort(result_key, :by => "*:#{sort_by}",:get => '*', :store => custom_sort, :order => sort_order)
154
- Hashish.redis_connection.expire(custom_sort, Hashish.redis_search_keys_ttl * 60)
155
- result = Hashish.redis_connection.lrange(custom_sort, offset, limit -1)
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?
@@ -1,3 +1,3 @@
1
1
  module Hashish
2
- VERSION = '0.2.0'
2
+ VERSION = '0.3.0'
3
3
  end
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.expects(:hashify)
8
- SampleClass.acts_as_hashish(:key => 'id')
9
- SampleClass.instance_variable_get(:@options)[:key_prefix].should eq(Hashish.redis_namespace + ":" + SampleClass.to_s)
10
- puts SampleClass.methods.sort
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
 
metadata CHANGED
@@ -2,7 +2,7 @@
2
2
  name: acts_as_hashish
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: 0.2.0
5
+ version: 0.3.0
6
6
  platform: ruby
7
7
  authors:
8
8
  - Schubert Cardozo