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