redis_object 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. data/.coveralls.yml +1 -0
  2. data/.gitignore +6 -0
  3. data/.travis.yml +5 -0
  4. data/Gemfile +8 -0
  5. data/README.markdown +179 -0
  6. data/Rakefile +10 -0
  7. data/lib/redis_object.rb +47 -0
  8. data/lib/redis_object/base.rb +408 -0
  9. data/lib/redis_object/collection.rb +388 -0
  10. data/lib/redis_object/defaults.rb +42 -0
  11. data/lib/redis_object/experimental/history.rb +49 -0
  12. data/lib/redis_object/ext/benchmark.rb +34 -0
  13. data/lib/redis_object/ext/cleaner.rb +14 -0
  14. data/lib/redis_object/ext/filters.rb +68 -0
  15. data/lib/redis_object/ext/script_cache.rb +92 -0
  16. data/lib/redis_object/ext/shardable.rb +18 -0
  17. data/lib/redis_object/ext/triggers.rb +101 -0
  18. data/lib/redis_object/ext/view_caching.rb +258 -0
  19. data/lib/redis_object/ext/views.rb +102 -0
  20. data/lib/redis_object/external_index.rb +25 -0
  21. data/lib/redis_object/indices.rb +97 -0
  22. data/lib/redis_object/inheritance_tracking.rb +23 -0
  23. data/lib/redis_object/keys.rb +37 -0
  24. data/lib/redis_object/storage.rb +93 -0
  25. data/lib/redis_object/storage/adapter.rb +46 -0
  26. data/lib/redis_object/storage/aws.rb +71 -0
  27. data/lib/redis_object/storage/mysql.rb +47 -0
  28. data/lib/redis_object/storage/redis.rb +119 -0
  29. data/lib/redis_object/timestamps.rb +74 -0
  30. data/lib/redis_object/tpl.rb +17 -0
  31. data/lib/redis_object/types.rb +276 -0
  32. data/lib/redis_object/validation.rb +89 -0
  33. data/lib/redis_object/version.rb +5 -0
  34. data/redis_object.gemspec +26 -0
  35. data/spec/adapter_spec.rb +43 -0
  36. data/spec/base_spec.rb +90 -0
  37. data/spec/benchmark_spec.rb +46 -0
  38. data/spec/collections_spec.rb +144 -0
  39. data/spec/defaults_spec.rb +56 -0
  40. data/spec/filters_spec.rb +29 -0
  41. data/spec/indices_spec.rb +45 -0
  42. data/spec/rename_class_spec.rb +96 -0
  43. data/spec/spec_helper.rb +38 -0
  44. data/spec/timestamp_spec.rb +28 -0
  45. data/spec/trigger_spec.rb +51 -0
  46. data/spec/types_spec.rb +103 -0
  47. data/spec/view_caching_spec.rb +130 -0
  48. data/spec/views_spec.rb +72 -0
  49. metadata +172 -0
@@ -0,0 +1,34 @@
1
+ module Seabright
2
+ module Benchmark
3
+
4
+ module ClassMethods
5
+
6
+ def benchmark(*method_names)
7
+ method_names.each do |method_name|
8
+ original_method = instance_method(method_name)
9
+ define_method(method_name) do |*args,&blk|
10
+ st = Time.now
11
+ out = original_method.bind(self).call(*args,&blk)
12
+ self.class.benchmark_out(method_name,args,Time.now - st)
13
+ out
14
+ end
15
+ end
16
+ end
17
+
18
+ def benchmark_out(method,args,time)
19
+ puts "[RedisObject::Benchmark] #{method}(#{args.join(",")}): #{time}"
20
+ end
21
+
22
+ def benchmark!
23
+ benchmark :set, :get, :<<
24
+ end
25
+
26
+ end
27
+
28
+ def self.included(base)
29
+ base.extend(ClassMethods)
30
+ end
31
+
32
+ end
33
+
34
+ end
@@ -0,0 +1,14 @@
1
+ module Seabright
2
+ module RedisObjectCleaner
3
+ def self.clean!
4
+ RedisObject.store.keys("*:collections").each do |key|
5
+ if obj = RedisObject.find_by_key(key.gsub(/:collections$/,''))
6
+ obj.collections.each do |nm,col|
7
+ puts "Cleaning: #{nm} #{col.class} #{col.inspect}" if DEBUG
8
+ col.cleanup!
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,68 @@
1
+ module Seabright
2
+ module Filters
3
+
4
+ module ClassMethods
5
+
6
+ def intercept_for_filters!
7
+ return if @intercept_for_filters
8
+ self.class_eval do
9
+
10
+ def filtered_method_call(method,*args)
11
+ if filters = self.class.filters_for(method)
12
+ filters.each do |f|
13
+ args = send(f,*args)
14
+ end
15
+ end
16
+ send("unfiltered_#{method.to_s}".to_sym,*args)
17
+ end
18
+
19
+ alias_method :unfiltered_get, :get unless method_defined?(:unfiltered_get)
20
+ def get(k)
21
+ filtered_method_call(:get,k)
22
+ end
23
+
24
+ alias_method :unfiltered_set, :set unless method_defined?(:unfiltered_set)
25
+ def set(k,v)
26
+ filtered_method_call(:set,k,v)
27
+ end
28
+
29
+ alias_method :unfiltered_setnx, :setnx unless method_defined?(:unfiltered_setnx)
30
+ def setnx(k,v)
31
+ filtered_method_call(:setnx,k,v)
32
+ end
33
+
34
+ end
35
+ @intercept_for_filters = true
36
+ end
37
+
38
+ def set_filter(filter)
39
+ filter_method(:set,filter)
40
+ filter_method(:setnx,filter)
41
+ end
42
+
43
+ def get_filter(filter)
44
+ filter_method(:get,filter)
45
+ end
46
+
47
+ def filter_method(method, filter)
48
+ method_filters[method.to_sym] ||= []
49
+ method_filters[method.to_sym] << filter.to_sym unless method_filters[method.to_sym].include?(filter.to_sym)
50
+ intercept_for_filters!
51
+ end
52
+
53
+ def method_filters
54
+ @method_filters ||= {}
55
+ end
56
+
57
+ def filters_for(method)
58
+ method_filters[method.to_sym]
59
+ end
60
+
61
+ end
62
+
63
+ def self.included(base)
64
+ base.extend(ClassMethods)
65
+ end
66
+
67
+ end
68
+ end
@@ -0,0 +1,92 @@
1
+ $ScriptSHAMap = {}
2
+
3
+ module Seabright
4
+
5
+ class RedisObject
6
+ module ScriptSources; end
7
+ end
8
+
9
+ module CachedScripts
10
+
11
+ def run_script(name,keys=[],args=[],source=nil)
12
+ self.class.run_script(name,keys,args,source,store)
13
+ end
14
+
15
+ module ClassMethods
16
+
17
+ NoScriptError = "NOSCRIPT No matching script. Please use EVAL.".freeze
18
+
19
+ def run_script(name,keys=[],args=[],source=nil,stor=nil)
20
+ @tmp_store = stor if stor
21
+ @rescue_recurse ||= 0
22
+ begin
23
+ out = (@tmp_store || store).evalsha(get_script_sha(name,source),keys,args)
24
+ rescue Redis::CommandError => e
25
+ if e.message == NoScriptError && @rescue_recurse < 3
26
+ puts "Rescuing NOSCRIPT error for #{name} - running again..." if DEBUG
27
+ untrack_script name
28
+ @rescue_recurse += 1
29
+ out = (@tmp_store || store).evalsha(get_script_sha(name,source),keys,args)
30
+ else
31
+ @rescue_recurse = 0
32
+ raise e
33
+ end
34
+ end
35
+ @rescue_recurse = 0
36
+ remove_instance_variable(:@tmp_store) if @tmp_store
37
+ out
38
+ end
39
+
40
+ def get_script_sha(name,source=nil)
41
+ $ScriptSHAMap[name] ||= (script_sha_from_key(name) || store_script(name,source))
42
+ end
43
+
44
+ def script_sha_from_key(name)
45
+ (@tmp_store || store).get(script_sha_key(name))
46
+ end
47
+
48
+ class SourceNotFoundError < RuntimeError
49
+ def initialize
50
+ super("Could not locate script source")
51
+ end
52
+ end
53
+
54
+ def store_script(name,source=nil)
55
+ source ||= script_source_from_const(name)
56
+ raise SourceNotFoundError unless source
57
+ sha = (@tmp_store || store).script(:load,source)
58
+ (@tmp_store || store).set(script_sha_key(name),sha)
59
+ sha
60
+ end
61
+
62
+ def untrack_script(name)
63
+ ScriptSHAMap.delete name
64
+ (@tmp_store || store).del(script_sha_key(name))
65
+ end
66
+
67
+ def script_source_from_const(name)
68
+ (self.const_defined?(name.to_sym) && self.const_get(name.to_sym)) || (RedisObject::ScriptSources.const_defined?(name.to_sym) && RedisObject::ScriptSources.const_get(name.to_sym)) || nil
69
+ end
70
+
71
+ SCRIPT_KEY_PREFIX = "ScriptCache::SHA::".freeze
72
+
73
+ def expire_all_script_shas(store_name=nil)
74
+ store_obj = store_name.is_a?(String) || store_name.is_a?(Symbol) ? store(store_name.to_sym) : store_name
75
+ store_obj.keys(script_sha_key("*")).each do |k|
76
+ store_obj.del k
77
+ end
78
+ end
79
+
80
+ def script_sha_key(name)
81
+ "#{SCRIPT_KEY_PREFIX}#{name.to_s}"
82
+ end
83
+
84
+ end
85
+
86
+ def self.included(base)
87
+ base.extend(ClassMethods)
88
+ end
89
+
90
+ end
91
+
92
+ end
@@ -0,0 +1,18 @@
1
+ module Seabright
2
+ module Shardable
3
+
4
+ # Intention is to override any methods needed so that the underlying data can be safely sharded
5
+
6
+ module ClassMethods
7
+
8
+ # same here
9
+
10
+ end
11
+
12
+ def self.included(base)
13
+ base.extend(ClassMethods)
14
+ end
15
+
16
+ end
17
+ RedisObject.send(:include,Shardable)
18
+ end
@@ -0,0 +1,101 @@
1
+ module Seabright
2
+ module Triggers
3
+
4
+ module ClassMethods
5
+
6
+ def trigger_on_set(fld,actn)
7
+ field_triggers[fld.to_sym] = actn.to_sym
8
+ intercept_sets_for_triggers!
9
+ end
10
+
11
+ def intercept_sets_for_triggers!
12
+ return if @intercepted_sets_for_triggers
13
+ self.class_eval do
14
+ alias_method :untriggered_set, :set unless method_defined?(:untriggered_set)
15
+ def set(k,v)
16
+ untriggered_set(k,v)
17
+ unless self.class.untriggerables.include?(k)
18
+ begin
19
+ self.class.untriggerables << k
20
+ if self.class.field_triggers[k.to_sym]
21
+ send(self.class.field_triggers[k.to_sym],k,v)
22
+ end
23
+ self.class.update_triggers.each do |actn|
24
+ send(actn.to_sym,k,v)
25
+ end
26
+ ensure
27
+ self.class.untriggerables.delete k
28
+ end
29
+ end
30
+ end
31
+ alias_method :untriggered_setnx, :setnx unless method_defined?(:untriggered_setnx)
32
+ def setnx(k,v)
33
+ ret = untriggered_setnx(k,v)
34
+ unless self.class.untriggerables.include?(k)
35
+ begin
36
+ self.class.untriggerables << k
37
+ if self.class.field_triggers[k.to_sym]
38
+ send(self.class.field_triggers[k.to_sym],k,v)
39
+ end
40
+ self.class.update_triggers.each do |actn|
41
+ send(actn.to_sym,k,v)
42
+ end
43
+ ensure
44
+ self.class.untriggerables.delete k
45
+ end
46
+ end
47
+ ret
48
+ end
49
+
50
+ end
51
+ @intercepted_sets_for_triggers = true
52
+ end
53
+
54
+ def intercept_reference_for_triggers!
55
+ return if @intercepted_reference_for_triggers
56
+ self.class_eval do
57
+ alias_method :untriggered_reference, :reference unless method_defined?(:untriggered_reference)
58
+ def reference(obj)
59
+ untriggered_reference(obj)
60
+ self.class.reference_triggers.each do |actn|
61
+ send(actn.to_sym,obj)
62
+ end
63
+ end
64
+ end
65
+ @intercepted_reference_for_triggers = true
66
+ end
67
+
68
+ def untriggerables
69
+ @untriggerables ||= [:updated_at,:created_at]
70
+ end
71
+
72
+ def field_triggers
73
+ @field_triggers ||= {}
74
+ end
75
+
76
+ def trigger_on_update(actn)
77
+ update_triggers << actn.to_sym
78
+ intercept_sets_for_triggers!
79
+ end
80
+
81
+ def update_triggers
82
+ @update_triggers ||= Set.new
83
+ end
84
+
85
+ def trigger_on_reference(actn)
86
+ reference_triggers << actn.to_sym
87
+ intercept_reference_for_triggers!
88
+ end
89
+
90
+ def reference_triggers
91
+ @reference_triggers ||= Set.new
92
+ end
93
+
94
+ end
95
+
96
+ def self.included(base)
97
+ base.extend(ClassMethods)
98
+ end
99
+
100
+ end
101
+ end
@@ -0,0 +1,258 @@
1
+ # View Caching
2
+ #
3
+ # Cache a named view:
4
+ # cache_named_view :admin_view
5
+ #
6
+ # Invalidate all cached views when the I receive notification that something I reference or have been referenced by has changed:
7
+ # invalidate_caches_from_upstream_updates!
8
+ #
9
+ # Notify some objects that reference me when I am updated: (objects of these classes)
10
+ # invalidate_upstream Property, Application, PaymentRequest, PaymentResponse
11
+ #
12
+ # Notify some objects in my collections when I am updated: (objects in these collections)
13
+ # invalidate_downstream :properties, :applications, :payment_requests, :payment_responses
14
+ #
15
+ # You can also set up hooks for when this object is updated by certain types of objects by defining the following:
16
+ # def invalidated_by(obj,chain)
17
+ # invalidate_cached_view :blah
18
+ # end
19
+ #
20
+ # def invalidated_by_user(obj,chain)
21
+ # invalidate_cached_view :users
22
+ # end
23
+ #
24
+
25
+ module Seabright
26
+ module ViewCaching
27
+
28
+ CachedViewInvalidator = "
29
+ for i=1,#ARGV do
30
+ redis.call('HDEL', KEYS[1], ARGV[i])
31
+ end".gsub(/\t/,'').freeze
32
+
33
+ module ClassMethods
34
+
35
+ def cache_view(name,opts=true)
36
+ cached_views[name.to_sym] = opts
37
+ intercept_views_for_caching!
38
+ set_up_invalidation!
39
+ end
40
+ alias_method :cache_named_view, :cache_view
41
+
42
+ def intercept_views_for_caching!
43
+ return if @cached_views_intercepted
44
+ self.class_eval do
45
+
46
+ alias_method :uncached_view_as_hash, :view_as_hash unless method_defined?(:uncached_view_as_hash)
47
+ def view_as_hash(name)
48
+ return uncached_view_as_hash(name) unless self.class.view_should_be_cached?(name)
49
+ if v = view_from_cache(name)
50
+ puts " Got view from cache: #{name}" if DEBUG
51
+ Yajl::Parser.parse(v)
52
+ else
53
+ puts " View cache miss: #{name}" if DEBUG
54
+ cache_view_content(name)[0]
55
+ end
56
+ end
57
+
58
+ alias_method :uncached_view_as_json, :view_as_json unless method_defined?(:uncached_view_as_json)
59
+ def view_as_json(name)
60
+ return uncached_view_as_json(name) unless self.class.view_should_be_cached?(name)
61
+ if v = view_from_cache(name)
62
+ puts " Got view from cache: #{name}" if DEBUG
63
+ v
64
+ else
65
+ puts " View cache miss: #{name}" if DEBUG
66
+ cache_view_content(name)[1]
67
+ end
68
+ end
69
+
70
+ def cache_view_content(name,content=nil)
71
+ content ||= uncached_view_as_hash(name)
72
+ json = Yajl::Encoder.encode(content)
73
+ store.hset(cached_view_key,name,json)
74
+ [content,json]
75
+ end
76
+
77
+ def view_from_cache(name)
78
+ if v = store.hget(cached_view_key,name)
79
+ v
80
+ else
81
+ nil
82
+ end
83
+ end
84
+
85
+ def view_is_cached?(name)
86
+ store.hexists(cached_view_key, name)
87
+ end
88
+
89
+ def cached_view_key
90
+ "#{hkey}::ViewCache"
91
+ end
92
+
93
+ def regenerate_cached_views(*names)
94
+ names.each do |name|
95
+ cache_view_content name
96
+ end
97
+ end
98
+
99
+ def regenerate_cached_views!
100
+ regenerate_cached_views(*self.class.cached_views.map {|name,opts| name })
101
+ end
102
+
103
+ end
104
+ @cached_views_intercepted = true
105
+ end
106
+
107
+ def set_up_invalidation!
108
+ return if @invalidation_set_up
109
+ self.class_eval do
110
+
111
+ def invalidate_cached_views(*names)
112
+ puts "Invalidating cached views: #{names.join(", ")}" if Debug.verbose?
113
+ run_script(:CachedViewInvalidator, [cached_view_key], names, CachedViewInvalidator)
114
+ self.class.cache_invalidation_hooks do |hook|
115
+ hook.call(names)
116
+ end
117
+ end
118
+ alias_method :invalidate_cached_view, :invalidate_cached_views
119
+
120
+ def invalidate_cached_views!
121
+ invalidate_cached_views(*self.class.cached_views.map {|name,opts| name })
122
+ end
123
+
124
+ def invalidate_downstream!
125
+ return unless self.class.downstream_invalidations && (self.class.downstream_invalidations.size > 0)
126
+ puts "Invalidating downstream: #{self.class.downstream_invalidations.inspect}" if Debug.verbose?
127
+ self.class.downstream_invalidations.each do |col|
128
+ if has_collection?(col) && (colctn = get_collection(col))
129
+ colctn.each do |obj|
130
+ obj.invalidated_by_other(self,invalidation_chain + [self.hkey])
131
+ end
132
+ end
133
+ end
134
+ end
135
+
136
+ def invalidate_upstream!
137
+ return unless self.class.upstream_invalidations && (self.class.upstream_invalidations.size > 0)
138
+ puts "Invalidating upstream: #{self.class.upstream_invalidations.inspect}" if Debug.verbose?
139
+ backreferences.each do |obj|
140
+ next unless self.class.upstream_invalidations.include?(obj.class.name.split("::").last.to_sym) #|| self.class.invalidate_everything_upstream?
141
+ obj.invalidated_by_other(self,invalidation_chain + [self.hkey]) if obj.respond_to?(:invalidated_by_other)
142
+ end
143
+ end
144
+
145
+ def invalidated_by_update!(*args)
146
+ Thread.new do
147
+ invalidate_cached_views!
148
+ invalidate_up_and_down!
149
+ end
150
+ end
151
+
152
+ def invalidated_by_reference!(*args)
153
+ invalidated_by_update!
154
+ end
155
+
156
+ def invalidation_chain
157
+ @invalidation_chain ||= []
158
+ end
159
+
160
+ def invalidate_up_and_down!
161
+ unless invalidation_chain.include?(self)
162
+ invalidate_downstream!
163
+ invalidate_upstream!
164
+ end
165
+ end
166
+
167
+ def invalidated_by_other(obj,chain)
168
+ unless chain.include?(self.hkey)
169
+ puts "#{self.class.name}:#{self.id}'s view caches were invalidated by upstream object: #{obj.class.name}:#{obj.id} (chain:#{chain.inspect})" if Debug.verbose?
170
+ @invalidation_chain = chain
171
+ [:invalidated_by,"invalidated_by_#{obj.class.name.underscore}".to_sym].each do |meth_sym|
172
+ send(meth_sym,obj,chain) if respond_to?(meth_sym)
173
+ end
174
+ invalidate_up_and_down!
175
+ end
176
+ end
177
+
178
+ trigger_on_update :invalidated_by_update!
179
+ trigger_on_reference :invalidated_by_reference!
180
+
181
+ end
182
+ @invalidation_set_up = true
183
+ end
184
+
185
+ def invalidate_caches_from_upstream_updates!
186
+ self.class_eval do
187
+
188
+ def invalidated_by(obj,chain)
189
+ invalidate_cached_views!
190
+ end
191
+
192
+ end
193
+ end
194
+
195
+ def on_cache_invalidation(&block)
196
+ cache_invalidation_hooks << block
197
+ end
198
+
199
+ def cache_invalidation_hooks
200
+ @cache_invalidation_hooks ||= []
201
+ end
202
+
203
+ def invalidate_upstream(*args)
204
+ @upstream_invalidations = (@upstream_invalidations || []) + args
205
+ set_up_invalidation!
206
+ end
207
+
208
+ def upstream_invalidations
209
+ @upstream_invalidations ||= []
210
+ end
211
+
212
+ # def invalidate_everything_upstream!
213
+ # @invalidate_everything_upstream = true
214
+ # end
215
+ #
216
+ # def invalidate_everything_upstream?
217
+ # @invalidate_everything_upstream
218
+ # end
219
+
220
+ def invalidate_downstream(*args)
221
+ @downstream_invalidations = (@downstream_invalidations || []) + args
222
+ set_up_invalidation!
223
+ end
224
+
225
+ def downstream_invalidations
226
+ @downstream_invalidations ||= []
227
+ end
228
+
229
+ # def invalidate_everything_downstream!
230
+ # @invalidate_everything_downstream = true
231
+ # end
232
+ #
233
+ # def invalidate_everything_downstream?
234
+ # @invalidate_everything_downstream
235
+ # end
236
+
237
+ def view_should_be_cached?(name)
238
+ !!cached_views[name.to_sym]
239
+ end
240
+
241
+ def cached_views
242
+ @cached_view ||= {}
243
+ end
244
+
245
+ def cache_named_views!
246
+ named_views.each do |name,view|
247
+ cache_view name
248
+ end
249
+ end
250
+
251
+ end
252
+
253
+ def self.included(base)
254
+ base.extend(ClassMethods)
255
+ end
256
+
257
+ end
258
+ end