archipelago 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.
data/lib/treasure.rb ADDED
@@ -0,0 +1,679 @@
1
+ # Archipelago - a distributed computing toolkit for ruby
2
+ # Copyright (C) 2006 Martin Kihlgren <zond at troja dot ath dot cx>
3
+ #
4
+ # This program is free software; you can redistribute it and/or
5
+ # modify it under the terms of the GNU General Public License
6
+ # as published by the Free Software Foundation; either version 2
7
+ # of the License, or (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program; if not, write to the Free Software
16
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
+
18
+ require 'drb'
19
+ require 'disco'
20
+ require 'bdb'
21
+ require 'pathname'
22
+ require 'digest/sha1'
23
+ require 'pp'
24
+ require 'set'
25
+ require 'hashish'
26
+ require 'tranny'
27
+
28
+ module Archipelago
29
+
30
+ module Treasure
31
+
32
+ #
33
+ # Minimum time between trying to recover
34
+ # crashed transactions.
35
+ #
36
+ TRANSACTION_RECOVERY_INTERVAL = 30
37
+
38
+ #
39
+ # Raised whenever the optimistic locking of the serializable transaction isolation level
40
+ # was proved wrong.
41
+ #
42
+ class RollbackException < RuntimeError
43
+ def initialize(chest, transaction)
44
+ super("#{chest} has been modified during #{transaction}")
45
+ end
46
+ end
47
+
48
+ #
49
+ # Raised whenever a method is called on an object that we dont know about.
50
+ #
51
+ class UnknownObjectException < RuntimeError
52
+ def initialize(chest, key, transaction)
53
+ super("#{chest} does not contain #{key} under #{transaction}")
54
+ end
55
+ end
56
+
57
+ #
58
+ # Raised if a Dubloon or Chest doesnt know what transaction you are talking about.
59
+ #
60
+ class UnknownTransactionException < RuntimeError
61
+ def initialize(source, transaction)
62
+ super("#{source} is not a part of #{transaction}")
63
+ end
64
+ end
65
+
66
+ #
67
+ # Raised if anyone tries to commit a non-prepared transaction.
68
+ #
69
+ class IllegalCommitException < RuntimeError
70
+ def initialize(source, transaction)
71
+ super("#{transaction} is not prepared in #{source}")
72
+ end
73
+ end
74
+
75
+ #
76
+ # Raised if someone tries to join us to a non-active transaction.
77
+ #
78
+ class IllegalJoinException < RuntimeError
79
+ def initialize(transaction)
80
+ super("#{transaction} is not active")
81
+ end
82
+ end
83
+
84
+ #
85
+ # A proxy to something in the chest.
86
+ #
87
+ class Dubloon
88
+ #
89
+ # Remove all methods so that we look like our target.
90
+ #
91
+ instance_methods.each do |method|
92
+ undef_method method unless method =~ /^__/
93
+ end
94
+ #
95
+ # Initialize us with knowledge of our +chest+, the +key+ to our
96
+ # target in the +chest+, the known +public_methods+ of our target
97
+ # and any +transaction+ we are associated with.
98
+ #
99
+ def initialize(key, chest, transaction, chest_id, public_methods)
100
+ @key = key
101
+ @chest = chest
102
+ @transaction = transaction
103
+ @chest_id = chest_id
104
+ @public_methods = public_methods
105
+ end
106
+ #
107
+ # The public_methods of our target.
108
+ #
109
+ def public_methods
110
+ return @public_methods.clone
111
+ end
112
+ #
113
+ # Return a clone of myself that is joined to
114
+ # the +transaction+.
115
+ #
116
+ def join(transaction)
117
+ @chest.join!(transaction) if transaction
118
+ return Dubloon.new(@key, @chest, transaction, @chest_id, @public_methods)
119
+ end
120
+ #
121
+ # Raises exception if the given +transaction+
122
+ # is not the same as our own.
123
+ #
124
+ def assert_transaction(transaction)
125
+ raise UnknownTransactionException.new(self, transaction) unless transaction == @transaction
126
+ end
127
+ #
128
+ # The object_id of our chest-held target.
129
+ #
130
+ def object_id
131
+ id = "#{@chest_id}:#{@key}"
132
+ id << ":#{@transaction.transaction_id}" if @transaction
133
+ return id
134
+ end
135
+ #
136
+ # Does our target respond to +meth+?
137
+ #
138
+ def respond_to?(meth)
139
+ return @public_methods.include?(meth.to_sym) || @public_methods.include?(meth.to_s)
140
+ end
141
+ #
142
+ # Call +meth+ with +args+ and +block+ on our target if it responds to
143
+ # it.
144
+ #
145
+ def method_missing(meth, *args, &block)
146
+ if respond_to?(meth)
147
+ return @chest.call_instance_method(@key, meth, @transaction, *args, &block)
148
+ else
149
+ return super(meth, *args)
150
+ end
151
+ end
152
+
153
+ end
154
+
155
+ #
156
+ # A possibly remote database that only returns proxies to its
157
+ # contents, and thus runs all methods on its contents itself.
158
+ #
159
+ # Has support for optimistically locked distributed serializable transactions.
160
+ #
161
+ # TODO: Test the transaction recovery mechanism.
162
+ #
163
+ class Chest
164
+
165
+ #
166
+ # The Chest never leaves its host.
167
+ #
168
+ include DRb::DRbUndumped
169
+
170
+ #
171
+ # The Chest can be published.
172
+ #
173
+ include Archipelago::Disco::Publishable
174
+
175
+ #
176
+ # Initialize a Chest
177
+ #
178
+ # Will use a BerkeleyHashishProvider using treasure_chest.db in the same dir to get its hashes
179
+ # if not <i>:persistence_provider</i> is given.
180
+ #
181
+ # Will try to recover crashed transaction every <i>:transaction_recovery_interval</i> seconds
182
+ # or TRANSACTION_RECOVERY_INTERVAL if none is given.
183
+ #
184
+ # Will use Archipelago::Disco::Publishable by calling <b>initialize_publishable</b> with +options+.
185
+ #
186
+ def initialize(options = {})
187
+ #
188
+ # The provider of happy magic persistent hashes of different kinds.
189
+ #
190
+ @persistence_provider = options[:persistence_provider] || Archipelago::Hashish::BerkeleyHashishProvider.new(Pathname.new(File.expand_path(__FILE__)).parent.join("treasure_chest.db"))
191
+
192
+ #
193
+ # Use the given options to initialize the publishable
194
+ # instance variables.
195
+ #
196
+ initialize_publishable(options)
197
+
198
+ #
199
+ # [transaction => [key => instance]]
200
+ # To know what stuff is visible to a transaction.
201
+ #
202
+ @snapshot_by_transaction = {}
203
+ @snapshot_by_transaction.extend(Archipelago::Current::Synchronized)
204
+
205
+ #
206
+ # [transaction => [key => when the key was read/updated/deleted]
207
+ # To know if a transaction is ok to prepare and commit.
208
+ #
209
+ @timestamp_by_key_by_transaction = {}
210
+
211
+ #
212
+ # [transaction => [key => instance]]
213
+ # To know what transactions were prepared but not
214
+ # properly finished last run.
215
+ #
216
+ @crashed = Set.new
217
+
218
+ #
219
+ # The magical persistent map that defines how we actually
220
+ # store our data.
221
+ #
222
+ @db = @persistence_provider.get_cached_hashish("db")
223
+
224
+ initialize_prepared(options[:transaction_recovery_interval] || TRANSACTION_RECOVERY_INTERVAL)
225
+
226
+ end
227
+
228
+ #
229
+ # The transactions active in this Chest.
230
+ #
231
+ def active_transactions
232
+ @snapshot_by_transaction.keys.clone
233
+ end
234
+
235
+ #
236
+ # Return the contents of this chest using a given +key+ and +transaction+.
237
+ #
238
+ def [](key, transaction = nil)
239
+ join!(transaction)
240
+
241
+ instance = ensure_instance_with_transaction(key, transaction)
242
+ return nil unless instance
243
+
244
+ if Dubloon === instance
245
+ return instance.join(transaction)
246
+ else
247
+ return Dubloon.new(key, DRbObject.new(self), transaction, self.service_id, instance.public_methods)
248
+ end
249
+ end
250
+
251
+ #
252
+ # Delete the value of +key+ within +transaction+.
253
+ #
254
+ def delete(key, transaction = nil)
255
+ join!(transaction)
256
+
257
+ rval = nil
258
+
259
+ if transaction
260
+ #
261
+ # If we have a transaction we must note that it is deleted in a
262
+ # separate space for that transaction.
263
+ #
264
+ snapshot = @snapshot_by_transaction[transaction]
265
+ snapshot.synchronize do
266
+
267
+ rval = snapshot[key]
268
+
269
+ snapshot[key] = :deleted
270
+ #
271
+ # Make sure we remember when it was last changed according to our main db.
272
+ #
273
+ timestamps = @timestamp_by_key_by_transaction[transaction]
274
+ timestamps[key] = @db.timestamp(key) unless timestamps.include?(key)
275
+
276
+ end
277
+ else
278
+ #
279
+ # Otherwise just ask our persistence provider to delete it.
280
+ #
281
+ @db.delete(key)
282
+ end
283
+
284
+ rval.freeze
285
+
286
+ return rval
287
+ end
288
+
289
+ #
290
+ # Put something into this chest with a given +key+, +value+ and
291
+ # +transaction+.
292
+ #
293
+ def []=(key, p1, p2 = nil)
294
+ if p2
295
+ value = p2
296
+ transaction = p1
297
+ else
298
+ value = p1
299
+ transaction = nil
300
+ end
301
+
302
+ return set(key, value, transaction)
303
+ end
304
+
305
+ #
306
+ # Call an instance +method+ on whatever this chest holds at +key+
307
+ # with any +transaction+ and +args+.
308
+ #
309
+ def call_instance_method(key, method, transaction, *arguments, &block)
310
+ if transaction
311
+ return call_with_transaction(key, method, transaction, *arguments, &block)
312
+ else
313
+ return call_without_transaction(key, method, *arguments, &block)
314
+ end
315
+ end
316
+
317
+ #
318
+ # Abort +transaction+ in this Chest.
319
+ #
320
+ def abort!(transaction)
321
+ assert_transaction(transaction)
322
+
323
+ snapshot = @snapshot_by_transaction[transaction]
324
+ #
325
+ # Make sure nobody can modify this transaction while we
326
+ # are aborting it.
327
+ #
328
+ snapshot.synchronize do
329
+ serialized_transaction = Marshal.dump(transaction)
330
+
331
+ #
332
+ # If this transaction was successfully prepared
333
+ #
334
+ if @snapshot_by_transaction_db.include?(serialized_transaction)
335
+ #
336
+ # Unlock the keys that are part of it.
337
+ #
338
+ snapshot.each do |key, value|
339
+ @db.unlock_on(key)
340
+ end
341
+ #
342
+ # And remove it from persistent storage.
343
+ #
344
+ @snapshot_by_transaction_db[serialized_transaction] = nil
345
+ #
346
+ # And remove its timestamps from persistent storage.
347
+ #
348
+ @timestamp_by_key_by_transaction_db[serialized_transaction] = nil
349
+ end
350
+ #
351
+ # Finally delete it from the snapshots.
352
+ #
353
+ @snapshot_by_transaction.delete(transaction)
354
+ #
355
+ # And from the timestamps.
356
+ #
357
+ @timestamp_by_key_by_transaction.delete(transaction)
358
+ end
359
+ end
360
+
361
+ #
362
+ # Prepares +transaction+ in this Chest.
363
+ #
364
+ # NB: This will cause any update of the data within
365
+ # this transaction to block until it is either aborted
366
+ # or commited!
367
+ #
368
+ # TODO: Make us aware about whether transactions have affected us
369
+ # 'for real' ie check whether the instance before the call
370
+ # differs from the instance after the call. Preferably
371
+ # without incurring performance lossage.
372
+ #
373
+ def prepare!(transaction)
374
+ assert_transaction(transaction)
375
+
376
+ #
377
+ # If we dont know about this transaction then it can't very well
378
+ # affect us.
379
+ #
380
+ return :unchanged unless @snapshot_by_transaction.include?(transaction)
381
+
382
+ snapshot = @snapshot_by_transaction[transaction]
383
+ #
384
+ # Make sure nobody can modify this transaction while we are
385
+ # preparing it.
386
+ #
387
+ snapshot.synchronize do
388
+
389
+ #
390
+ # Remember what locks we acquire so that we can
391
+ # unlock them in case of failure.
392
+ #
393
+ locks = []
394
+ timestamp_by_key = @timestamp_by_key_by_transaction[transaction]
395
+ #
396
+ # Acquire a lock on each key in the transaction
397
+ #
398
+ snapshot.each do |key, value|
399
+ if @db.timestamp(key) == timestamp_by_key[key]
400
+ @db.lock_on(key)
401
+ locks << key
402
+ else
403
+ locks.each do |key|
404
+ @db.unlock_on(key)
405
+ end
406
+ return :abort
407
+ end
408
+ end
409
+ serialized_transaction = Marshal.dump(transaction)
410
+
411
+ #
412
+ # Dump its state to persistent storage.
413
+ #
414
+ @snapshot_by_transaction_db[serialized_transaction] = Marshal.dump(snapshot)
415
+ #
416
+ # And dump its timestamps to persistent storage
417
+ #
418
+ @timestamp_by_key_by_transaction_db[serialized_transaction] = Marshal.dump(timestamp_by_key)
419
+ return :prepared
420
+ end
421
+ end
422
+
423
+ #
424
+ # Commits +transaction+ in this Chest.
425
+ #
426
+ # NB: Transaction must be prepared before commit is called.
427
+ #
428
+ def commit!(transaction)
429
+ assert_transaction(transaction)
430
+ raise IllegalCommitException.new(self, transaction) unless @snapshot_by_transaction_db.include?(Marshal.dump(transaction))
431
+
432
+ snapshot = @snapshot_by_transaction[transaction]
433
+ #
434
+ # Make sure nobody can modify this transaction while we are
435
+ # commiting it.
436
+ #
437
+ snapshot.synchronize do
438
+
439
+ #
440
+ # Copy each key and value from our private space to the real space
441
+ #
442
+ snapshot.each do |key, value|
443
+ if value == :deleted
444
+ @db.delete(key)
445
+ else
446
+ @db[key] = value
447
+ end
448
+ end
449
+
450
+ #
451
+ # Call abort! to clean up after the transaction.
452
+ #
453
+ abort!(transaction)
454
+
455
+ end
456
+ end
457
+
458
+ private
459
+
460
+ #
461
+ # Allocates space for this +transaction+.
462
+ #
463
+ # Will also call +transaction+.join to make sure
464
+ # it is aware of us.
465
+ #
466
+ def join!(transaction)
467
+ if transaction
468
+ if transaction.state == :active
469
+ @snapshot_by_transaction.synchronize do
470
+ unless @snapshot_by_transaction.include?(transaction)
471
+ @snapshot_by_transaction[transaction] = {}
472
+ @snapshot_by_transaction[transaction].extend(Archipelago::Current::Synchronized)
473
+ @timestamp_by_key_by_transaction[transaction] = {}
474
+ transaction.join(DRbObject.new(self))
475
+ end
476
+ end
477
+ else
478
+ raise IllegalJoinException.new(transaction)
479
+ end
480
+ end
481
+ end
482
+
483
+ #
484
+ # Raises if we are not in this transaction.
485
+ #
486
+ def assert_transaction(transaction)
487
+ raise UnknownTransactionException.new(self, transaction) unless @snapshot_by_transaction.include?(transaction)
488
+ end
489
+
490
+ #
491
+ # Call a method within a transaction.
492
+ #
493
+ def call_with_transaction(key, method, transaction, *arguments, &block)
494
+ assert_transaction(transaction)
495
+
496
+ #
497
+ # Fetch our instance from the snapshot.
498
+ #
499
+ snapshot = @snapshot_by_transaction[transaction]
500
+ instance = snapshot[key]
501
+ instance = nil if instance == :deleted
502
+
503
+ raise UnknownObjectException.new(self, key, transaction) unless instance
504
+
505
+ begin
506
+ return execute(instance, method, *arguments, &block)
507
+ ensure
508
+ #
509
+ # Make sure we remember when this object was last changed according
510
+ # to the main db.
511
+ #
512
+ snapshot.synchronize do
513
+ timestamps = @timestamp_by_key_by_transaction[transaction]
514
+ timestamps[key] = @db.timestamp(key) unless timestamps.include?(key)
515
+ end
516
+ end
517
+ end
518
+
519
+ #
520
+ # Execute +m+ with arguments +a+ and block +b+ on +o+.
521
+ #
522
+ def execute(o, m, *a, &b)
523
+ if b
524
+ return o.send(m, *a, &b)
525
+ else
526
+ return o.send(m, *a)
527
+ end
528
+ end
529
+
530
+ #
531
+ # Call a method outside any transaction (ie inside a transaction of its own).
532
+ #
533
+ def call_without_transaction(key, method, *arguments, &block)
534
+ instance = @db[key]
535
+
536
+ raise UnknownObjectException(self, key, transaction) unless instance
537
+
538
+ begin
539
+ return execute(instance, method, *arguments, &block)
540
+ ensure
541
+ @db.store_if_changed(key)
542
+ end
543
+ end
544
+
545
+ #
546
+ # Initializes our storage of prepared transactions.
547
+ #
548
+ def initialize_prepared(transaction_recovery_interval)
549
+ #
550
+ # Load stored timestamps for our transaction from db.
551
+ #
552
+ @timestamp_by_key_by_transaction_db = @persistence_provider.get_hashish("prepared_timestamps")
553
+ @timestamp_by_key_by_transaction_db.each do |serialized_transaction, serialized_timestamps|
554
+ @timestamp_by_key_by_transaction[Marshal.load(serialized_transaction)] = Marshal.load(serialized_timestamps)
555
+ end
556
+
557
+ #
558
+ # Load stored snapshots for our transaction from db.
559
+ #
560
+ @snapshot_by_transaction_db = @persistence_provider.get_hashish("prepared")
561
+ @snapshot_by_transaction_db.each do |serialized_transaction, serialized_snapshot|
562
+ transaction = Marshal.load(transaction)
563
+
564
+ @crashed << transaction
565
+ @snapshot_by_transaction[transaction] = Marshal.load(serialized_snapshot)
566
+ end
567
+ start_recovery_thread(transaction_recovery_interval)
568
+ end
569
+
570
+ #
571
+ # Starts the thread that will keep trying to recover
572
+ # our crashed transactions.
573
+ #
574
+ def start_recovery_thread(transaction_recovery_interval)
575
+ Thread.new do
576
+ loop do
577
+ begin
578
+ @crashed.clone.each do |transaction|
579
+ begin
580
+ case transaction.state
581
+ when :commited
582
+ commit!(transaction)
583
+ @crashed.delete(transaction)
584
+ when :aborted
585
+ abort!(transaction)
586
+ @crashed.delete(transaction)
587
+ end
588
+ rescue Archipelago::Tranny::UnknownTransactionException => e
589
+ abort!(transaction)
590
+ @crashed.delete(transaction)
591
+ end
592
+ end
593
+ sleep(transaction_recovery_interval)
594
+ rescue Exception => e
595
+ puts e
596
+ pp e.backtrace
597
+ end
598
+ end
599
+ end
600
+ end
601
+
602
+ #
603
+ # Insert +value+ under +key+ and +transaction+
604
+ # in this chest.
605
+ #
606
+ def set(key, value, transaction)
607
+ join!(transaction)
608
+ value.assert_transaction(transaction) if Dubloon === value
609
+
610
+ if transaction
611
+ snapshot = @snapshot_by_transaction[transaction]
612
+
613
+ #
614
+ # If we have a transaction we must put it in a
615
+ # separate space for that transaction.
616
+ #
617
+ snapshot.synchronize do
618
+
619
+ snapshot[key] = value
620
+ #
621
+ # Make sure we remember the last time this was changed according to
622
+ # our main db.
623
+ #
624
+ timestamps = @timestamp_by_key_by_transaction[transaction]
625
+ timestamps[key] = @db.timestamp(key) unless timestamps.include?(key)
626
+
627
+ end
628
+ else
629
+ @db[key] = value
630
+ end
631
+
632
+ return value if Dubloon === value
633
+
634
+ return Dubloon.new(key, DRbObject.new(self), transaction, service_id, value.public_methods)
635
+ end
636
+
637
+ #
638
+ # Try to fetch the data of +key+ from the private space
639
+ # of +transaction+ and put it there if it was not there
640
+ # already.
641
+ #
642
+ def ensure_instance_with_transaction(key, transaction)
643
+ if transaction
644
+ snapshot = @snapshot_by_transaction[transaction]
645
+ snapshot.synchronize do
646
+
647
+ #
648
+ # If we dont have this key in the snapshot.
649
+ #
650
+ unless snapshot.include?(key)
651
+ #
652
+ # Fetch the new value for the snapshot
653
+ #
654
+ new_value = @db.get_deep_clone(key)
655
+ #
656
+ # If it exists then copy it to the snapshot
657
+ # otherwise remove the transaction hash if it is empty.
658
+ #
659
+ if new_value
660
+ snapshot[key] = new_value
661
+ else
662
+ @snapshot_by_transaction.delete(transaction) if snapshot.empty?
663
+ end
664
+ end
665
+
666
+ rval = snapshot[key]
667
+ return rval == :deleted ? nil : rval
668
+
669
+ end
670
+ else
671
+ return @db[key]
672
+ end
673
+ end
674
+
675
+ end
676
+
677
+ end
678
+
679
+ end
@@ -0,0 +1,19 @@
1
+
2
+ require File.join(File.dirname(__FILE__), 'profile_helper')
3
+ require 'treasure'
4
+ require 'drb'
5
+
6
+ DRb.start_service
7
+ @c = TestChest.new
8
+
9
+ k = "hej"
10
+ v = "oj"
11
+ t = TestTransaction.new
12
+ 1000.times do |n|
13
+ @c[k, t] = v
14
+ @c.prepare!(t)
15
+ @c.commit!(t)
16
+ end
17
+
18
+ @c.persistence_provider.unlink
19
+ DRb.stop_service
@@ -0,0 +1,19 @@
1
+
2
+ require File.join(File.dirname(__FILE__), 'profile_helper')
3
+ require 'treasure'
4
+ require 'drb'
5
+
6
+ DRb.start_service
7
+ @c = TestChest.new
8
+ @tm = TestManager.new
9
+
10
+ tr = @tm.begin
11
+ k = "hej"
12
+ v = "oj"
13
+ 1000.times do |n|
14
+ @c[k,tr] = v
15
+ end
16
+
17
+ @c.persistence_provider.unlink
18
+ File.unlink(@tm.db.filename)
19
+ DRb.stop_service