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/GPL-2 +339 -0
- data/README +39 -0
- data/TODO +3 -0
- data/lib/archipelago.rb +24 -0
- data/lib/current.rb +128 -0
- data/lib/disco.rb +548 -0
- data/lib/hashish.rb +199 -0
- data/lib/pirate.rb +205 -0
- data/lib/tranny.rb +650 -0
- data/lib/treasure.rb +679 -0
- data/profiles/1000xChest#join!-prepare!-commit!.rb +19 -0
- data/profiles/1000xDubloon#[]=(t).rb +19 -0
- data/profiles/1000xDubloon#method_missing(t).rb +21 -0
- data/profiles/README +3 -0
- data/profiles/profile_helper.rb +25 -0
- data/scripts/chest.rb +20 -0
- data/scripts/console +3 -0
- data/scripts/pirate.rb +6 -0
- data/scripts/tranny.rb +20 -0
- data/tests/current_test.rb +28 -0
- data/tests/disco_test.rb +179 -0
- data/tests/pirate_test.rb +75 -0
- data/tests/test_helper.rb +60 -0
- data/tests/tranny_test.rb +70 -0
- data/tests/treasure_test.rb +257 -0
- metadata +69 -0
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
|