hyperactive 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,358 @@
1
+
2
+ require 'rubygems'
3
+ require 'archipelago'
4
+
5
+ #
6
+ # A utility module to provide the functionality required for example
7
+ # if you want to use archipelago in a Ruby on Rails project *hint hint*.
8
+ #
9
+ module Hyperactive
10
+
11
+ #
12
+ # The default database connector.
13
+ #
14
+ CAPTAIN = Archipelago::Pirate::Captain.new
15
+
16
+ #
17
+ # A tiny <b>call</b>able class that saves stuff in
18
+ # indexes depending on certain attributes.
19
+ #
20
+ class IndexBuilder
21
+ #
22
+ # Get the first part of the key, that depends on the +attributes+.
23
+ #
24
+ def self.get_attribute_key_part(attributes)
25
+ "Hyperactive::IndexBuilder::#{attributes.join(",")}"
26
+ end
27
+ #
28
+ # Get the last part of the key, that depends on the +values+.
29
+ #
30
+ def self.get_value_key_part(values)
31
+ "#{values.join(",")}"
32
+ end
33
+ #
34
+ # Initialize an IndexBuilder giving it an array of +attributes+
35
+ # that will be indexed.
36
+ #
37
+ def initialize(attributes)
38
+ @attributes = attributes
39
+ end
40
+ #
41
+ # Get the Tree for the given +record+.
42
+ #
43
+ def get_tree_for(record)
44
+ values = @attributes.collect do |att|
45
+ if record.respond_to?(att)
46
+ record.send(att)
47
+ else
48
+ nil
49
+ end
50
+ end
51
+ key = "#{self.class.get_attribute_key_part(@attributes)}::#{self.class.get_value_key_part(values)}"
52
+ return CAPTAIN[key] ||= Tree.get_instance
53
+ end
54
+ #
55
+ # Call this IndexBuilder and pass it a +block+.
56
+ #
57
+ # If the +argument+ is an Array then we know we are a save hook,
58
+ # otherwise we are a destroy hook.
59
+ #
60
+ def call(argument, &block)
61
+ yield
62
+
63
+ #
64
+ # If the argument is an Array (of old value, new value)
65
+ # then we are a save hook, otherwise a destroy hook.
66
+ #
67
+ if Array === argument
68
+ record = argument.last
69
+ old_record = argument.first
70
+ get_tree_for(old_record).delete(record.record_id)
71
+ get_tree_for(record)[record.record_id] = record
72
+ else
73
+ record = argument
74
+ get_tree_for(record).delete(record.record_id)
75
+ end
76
+ end
77
+ end
78
+
79
+ #
80
+ # A tiny <b>call</b>able class that saves stuff inside containers
81
+ # if they match certain criteria.
82
+ #
83
+ class MatchSaver
84
+ #
85
+ # Initialize this MatchSaver with a +key+, a <b>call</b>able +matcher+
86
+ # and a +mode+ (:select, :reject, :delete_if_match or :delete_unless_match).
87
+ #
88
+ def initialize(key, matcher, mode)
89
+ @key = key
90
+ @matcher = matcher
91
+ @mode = mode
92
+ end
93
+ #
94
+ # Depending on <i>@mode</i> and return value of <i>@matcher</i>.call
95
+ # may save record in the Hash-like container named <i>@key</i> in
96
+ # the main database after having yielded to +block+.
97
+ #
98
+ def call(argument, &block)
99
+ yield
100
+
101
+ record = argument
102
+ record = argument.last if Array === argument
103
+
104
+ case @mode
105
+ when :select
106
+ if @matcher.call(record)
107
+ CAPTAIN[@key][record.record_id] = record
108
+ else
109
+ CAPTAIN[@key].delete(record.record_id)
110
+ end
111
+ when :reject
112
+ if @matcher.call(record)
113
+ CAPTAIN[@key].delete(record.record_id)
114
+ else
115
+ CAPTAIN[@key][record.record_id] = record
116
+ end
117
+ when :delete_if_match
118
+ if @matcher.call(record)
119
+ CAPTAIN[@key].delete(record.record_id)
120
+ end
121
+ when :delete_unless_match
122
+ unless @matcher.call(record)
123
+ CAPTAIN[@key].delete(record.record_id)
124
+ end
125
+ end
126
+ end
127
+ end
128
+
129
+ #
130
+ # A convenient base class to inherit when you want the basic utility methods
131
+ # provided by for example ActiveRecord::Base *hint hint*.
132
+ #
133
+ # NB: After an instance is created, it will actually return a copy <b>within your local machine</b>
134
+ # which is not what is usually the case. Every other time you fetch it using a select or other
135
+ # method you will instead receive a proxy object to the database. This means that nothing you
136
+ # do to it at that point will be persistent or even necessarily have a defined result.
137
+ # Therefore: do not use <b>MyRecordSubclass.new</b> to <b>MyRecordSubclass#initialize</b> objects,
138
+ # instead use <b>MyRecordSubclass.get_instance</b>, since it will return a fresh proxy object
139
+ # instead of the devious original:
140
+ # my_instance = MyRecordSubclass.get_instance(*the_same_arguments_as_to_initialize)
141
+ #
142
+ class Record
143
+
144
+ @@create_hooks_by_class = {}
145
+ @@destroy_hooks_by_class = {}
146
+ @@save_hooks_by_class = {}
147
+
148
+ #
149
+ # The host we are running on.
150
+ #
151
+ HOST = "#{Socket::gethostbyname(Socket::gethostname)[0]}" rescue "localhost"
152
+
153
+ #
154
+ # Call this if you want to change the default database connector
155
+ # to something else.
156
+ #
157
+ def self.setup(options = {})
158
+ CAPTAIN.setup(options[:pirate_options])
159
+ end
160
+
161
+ #
162
+ # Return our create_hooks, which can then be treated as any old Array.
163
+ #
164
+ # These must be <b>call</b>able objects with an arity of 1
165
+ # that will be sent the instance about to be created (initial
166
+ # insertion into the database system) that take a block argument.
167
+ #
168
+ # The block argument will be a Proc that actually injects the
169
+ # instance into the database system.
170
+ #
171
+ # Use this to preprocess, validate and/or postprocess your
172
+ # instances upon creation.
173
+ #
174
+ def self.create_hooks
175
+ self.get_hook_array_by_class(@@create_hooks_by_class)
176
+ end
177
+
178
+ #
179
+ # Return our destroy_hooks, which can then be treated as any old Array.
180
+ #
181
+ # These must be <b>call</b>able objects with an arity of 1
182
+ # that will be sent the instance about to be destroyed (removal
183
+ # from the database system) that take a block argument.
184
+ #
185
+ # The block argument will be a Proc that actually removes the
186
+ # instance from the database system.
187
+ #
188
+ # Use this to preprocess, validate and/or postprocess your
189
+ # instances upon destruction.
190
+ #
191
+ def self.destroy_hooks
192
+ self.get_hook_array_by_class(@@destroy_hooks_by_class)
193
+ end
194
+
195
+ #
196
+ # Return our save_hooks, which can then be treated as any old Array.
197
+ #
198
+ # These must be <b>call</b>able objects with an arity of 1
199
+ # that will be sent [the old version, the new version] of the
200
+ # instance about to be saved (storage into the database system)
201
+ # along with a block argument.
202
+ #
203
+ # The block argument will be a Proc that actually saves the
204
+ # instance into the database system.
205
+ #
206
+ # Use this to preprocess, validate and/or postprocess your
207
+ # instances upon saving.
208
+ #
209
+ def self.save_hooks
210
+ self.get_hook_array_by_class(@@save_hooks_by_class)
211
+ end
212
+
213
+ def self.index_by(*attributes)
214
+ attribute_key_part = IndexBuilder.get_attribute_key_part(attributes)
215
+ self.class_eval <<END
216
+ def self.find_by_#{attributes.join("_and_")}(*args)
217
+ key = "#{attribute_key_part}::" + IndexBuilder.get_value_key_part(args)
218
+ CAPTAIN[key]
219
+ end
220
+ END
221
+ index_builder = IndexBuilder.new(attributes)
222
+ self.save_hooks << index_builder
223
+ self.destroy_hooks << index_builder
224
+ end
225
+
226
+ #
227
+ # Will define a method called +name+ that will include all
228
+ # existing instances of this class that when sent to +matcher+.call
229
+ # return true. Will only return instances saved after this selector
230
+ # is defined.
231
+ #
232
+ def self.select(name, matcher)
233
+ key = self.collection_key(name)
234
+ CAPTAIN[key] ||= Tree.get_instance
235
+ self.class_eval <<END
236
+ def self.#{name}
237
+ CAPTAIN["#{key}"]
238
+ end
239
+ END
240
+ self.save_hooks << MatchSaver.new(key, matcher, :select)
241
+ self.destroy_hooks << MatchSaver.new(key, matcher, :delete_if_match)
242
+ end
243
+
244
+ #
245
+ # Will define a method called +name+ that will include all
246
+ # existing instances of this class that when sent to +matcher+.call
247
+ # does not return true. Will only return instances saved after this
248
+ # rejector is defined.
249
+ #
250
+ def self.reject(name, matcher)
251
+ key = self.collection_key(name)
252
+ CAPTAIN[key] ||= Tree.get_instance
253
+ self.class_eval <<END
254
+ def self.#{name}
255
+ CAPTAIN["#{key}"]
256
+ end
257
+ END
258
+ self.save_hooks << MatchSaver.new(key, matcher, :reject)
259
+ self.destroy_hooks << MatchSaver.new(key, matcher, :delete_unless_match)
260
+ end
261
+
262
+ #
263
+ # Return the record with +record_id+.
264
+ #
265
+ def self.find(record_id)
266
+ CAPTAIN[record_id]
267
+ end
268
+
269
+ #
270
+ # Will execute +block+ within a transaction.
271
+ #
272
+ def self.transaction(&block)
273
+ CAPTAIN.transaction(&block)
274
+ end
275
+
276
+ #
277
+ # Use this method to get new instances of this class, since it will actually
278
+ # make sure it both resides in the database and return a proxy to the remote
279
+ # object.
280
+ #
281
+ def self.get_instance(*arguments)
282
+ instance = self.new(*arguments)
283
+ instance.instance_eval do
284
+ @record_id = Digest::SHA1.hexdigest("#{HOST}:#{Time.new.to_f}:#{self.object_id}:#{rand(1 << 32)}")
285
+ end
286
+
287
+ Hyperactive::Hooker.call_with_hooks(instance, *self.create_hooks) do
288
+ CAPTAIN[instance.record_id] = instance
289
+ end
290
+
291
+ proxy = CAPTAIN[instance.record_id]
292
+
293
+ return proxy
294
+ end
295
+
296
+ #
297
+ # Our semi-unique id.
298
+ #
299
+ attr_reader :record_id
300
+
301
+ #
302
+ # This will allow us to wrap any write of us to persistent storage
303
+ # in the @@save_hooks as long as the Archipelago::Hashish provider
304
+ # supports it. See Archipelago::Hashish::BerkeleyHashish for an example
305
+ # of Hashish providers that do this.
306
+ #
307
+ def save_hook(old_value, &block)
308
+ Hyperactive::Hooker.call_with_hooks([old_value, self], *self.class.save_hooks) do
309
+ yield
310
+ end
311
+ end
312
+
313
+ #
314
+ # Remove this instance from the database calling all the right hooks.
315
+ #
316
+ # Freezes this instance after having deleted it.
317
+ #
318
+ # Returns false without destroying anything if any of the @@pre_destroy_hooks
319
+ # returns false.
320
+ #
321
+ # Returns true otherwise.
322
+ #
323
+ def destroy
324
+ Hyperactive::Hooker.call_with_hooks(self, *self.class.destroy_hooks) do
325
+ CAPTAIN.delete(@record_id)
326
+ self.freeze
327
+ end
328
+ end
329
+
330
+ private
331
+
332
+ #
333
+ # The key used to store the collection with the given +sym+ as name.
334
+ #
335
+ def self.collection_key(sym)
336
+ "Hyperactive::Record::collection_key::#{sym}"
337
+ end
338
+
339
+ #
340
+ # Get an Array from +hash+ using <i>self</i> as
341
+ # key. If <i>self</i> doesnt exist in the +hash+
342
+ # it will recurse by calling the same method in the
343
+ # superclass until it has been called in Hyperactive::Record.
344
+ #
345
+ def self.get_hook_array_by_class(hash)
346
+ return hash[self] if hash.include?(self)
347
+
348
+ if self == Record
349
+ hash[self] = []
350
+ return self.get_hook_array_by_class(hash)
351
+ else
352
+ hash[self] = self.superclass.get_hook_array_by_class(hash).clone
353
+ return self.get_hook_array_by_class(hash)
354
+ end
355
+ end
356
+
357
+ end
358
+ end
@@ -0,0 +1,216 @@
1
+
2
+ require 'rubygems'
3
+ require 'archipelago'
4
+ require 'rbtree'
5
+
6
+ module Hyperactive
7
+
8
+ #
9
+ # A class suitable for storing large and often-changing datasets in
10
+ # an Archipelago environment.
11
+ #
12
+ # Is constructed like a set of nested Hashes that automatically create
13
+ # new children on demand, and will thusly only have to check the path from
14
+ # the root node to the leaf for changes when method calls return (see Archipelago::Treasure::Chest)
15
+ # and will only have to actually store into database the leaf itself if it
16
+ # has changed.
17
+ #
18
+ class Tree < Record
19
+
20
+ attr_accessor :elements, :subtrees
21
+
22
+ WIDTH = 1 << 3
23
+
24
+ #
25
+ # Dont call this! Call <i>Tree.get_instance(options)</i> instead!
26
+ #
27
+ def initialize(options = {})
28
+ @width = options[:width] || WIDTH
29
+ @elements = {}
30
+ @subtrees = nil
31
+ end
32
+
33
+ #
34
+ # Deletes +key+ in this Tree.
35
+ #
36
+ def delete(key)
37
+ if @elements
38
+ @elements.delete(key)
39
+ else
40
+ subtree_for(key).delete(key)
41
+ end
42
+ end
43
+
44
+ #
45
+ # Returns the size of this Tree.
46
+ #
47
+ def size
48
+ if @elements
49
+ @elements.size
50
+ else
51
+ @subtrees.t_collect do |tree_id, tree|
52
+ tree.size
53
+ end.inject(0) do |sum, size|
54
+ sum + size
55
+ end
56
+ end
57
+ end
58
+
59
+ #
60
+ # Returns all keys and values returning true for +callable+.call(key, value) in this Tree.
61
+ #
62
+ def select(callable)
63
+ if @elements
64
+ @elements.select do |k,v|
65
+ callable.call(k,v)
66
+ end
67
+ else
68
+ @subtrees.t_collect do |tree_id, tree|
69
+ tree.select(callable)
70
+ end.inject([]) do |sum, match|
71
+ sum + match
72
+ end
73
+ end
74
+ end
75
+
76
+ #
77
+ # Returns all keys and values returning false for +callable+.call(key, value) in this Tree.
78
+ #
79
+ def reject(callable)
80
+ if @elements
81
+ @elements.reject do |k,v|
82
+ callable.call(k,v)
83
+ end
84
+ else
85
+ @subtrees.t_collect do |tree_id, tree|
86
+ tree.reject(callable)
87
+ end.inject([]) do |sum, match|
88
+ sum + match
89
+ end
90
+ end
91
+ end
92
+
93
+ #
94
+ # Puts +value+ under +key+ in this Tree.
95
+ #
96
+ def []=(key, value)
97
+ if @elements
98
+ if @elements.size < @width
99
+ @elements[key] = value
100
+ else
101
+ split!
102
+ subtree_for(key)[key] = value
103
+ end
104
+ else
105
+ subtree_for(key)[key] = value
106
+ end
107
+ return value
108
+ end
109
+
110
+ #
111
+ # Returns the value for +key+ in this Tree.
112
+ #
113
+ def [](key)
114
+ if @elements
115
+ return @elements[key]
116
+ else
117
+ return subtree_for(key)[key]
118
+ end
119
+ end
120
+
121
+ #
122
+ # Returns an Array containing +callable+.call(key, value) from all values in this Tree.
123
+ #
124
+ def collect(callable)
125
+ rval = []
126
+ self.each(Proc.new do |k,v|
127
+ rval << callable.call(k,v)
128
+ end)
129
+ return rval
130
+ end
131
+
132
+ #
133
+ # Does +callable+.call(key, value) on all values in this Tree.
134
+ #
135
+ def each(callable)
136
+ if @elements
137
+ @elements.each do |key, value|
138
+ callable.call(key, value)
139
+ end
140
+ else
141
+ @subtrees.t_each do |tree_id, tree|
142
+ tree.each(callable)
143
+ end
144
+ nil
145
+ end
146
+ end
147
+
148
+ #
149
+ # Clear everything from this Tree and destroy it.
150
+ #
151
+ def destroy
152
+ if @elements
153
+ @elements.each do |key, value|
154
+ value.destroy if value.respond_to?(:destroy)
155
+ end
156
+ else
157
+ @subtrees.each do |tree_id, tree|
158
+ tree.destroy
159
+ end
160
+ end
161
+ freeze
162
+ super
163
+ end
164
+
165
+ #
166
+ # Clear everything from this Tree.
167
+ #
168
+ def clear
169
+ unless @elements
170
+ @subtrees.each do |tree_id, tree|
171
+ tree.clear
172
+ end
173
+ @subtrees = nil
174
+ end
175
+ @elements = {}
176
+ end
177
+
178
+ private
179
+
180
+ #
181
+ # Finds the subtree responsible for +key+.
182
+ #
183
+ # Does it in this ugly way cause the nice way of just doing modulo gave
184
+ # really odd results since all hashes seem to give some modulo values
185
+ # a lot more often than expected.
186
+ #
187
+ def subtree_for(key)
188
+ key_id = Digest::SHA1.new("#{key.hash}#{self.record_id}").to_s
189
+ @subtrees.each do |tree_id, tree|
190
+ return tree if tree_id > key_id
191
+ end
192
+ return @subtrees.values.first
193
+ end
194
+
195
+ #
196
+ # Split this Tree by creating @subtrees,
197
+ # then putting all @elements in them and then emptying @elements.
198
+ #
199
+ def split!
200
+ raise "Cant split twice!" unless @elements
201
+
202
+ @subtrees = RBTree.new
203
+ @subtrees.extend(Archipelago::Current::ThreadedCollection)
204
+ step = (1 << 160) / @width
205
+ 0.upto(@width - 1) do |n|
206
+ @subtrees["%x" % (n * step)] = Tree.get_instance(:width => @width)
207
+ end
208
+ @elements.each do |key, value|
209
+ subtree_for(key)[key] = value
210
+ end
211
+ @elements = nil
212
+ end
213
+
214
+ end
215
+
216
+ end
@@ -0,0 +1,6 @@
1
+
2
+ $: << File.dirname(__FILE__)
3
+
4
+ require 'hyperactive/hooker'
5
+ require 'hyperactive/record'
6
+ require 'hyperactive/tree'