hyperactive 0.1.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.
@@ -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'