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/tranny.rb
ADDED
|
@@ -0,0 +1,650 @@
|
|
|
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 'bdb'
|
|
19
|
+
require 'pathname'
|
|
20
|
+
require 'drb'
|
|
21
|
+
require 'current'
|
|
22
|
+
require 'digest/sha1'
|
|
23
|
+
require 'disco'
|
|
24
|
+
|
|
25
|
+
module Archipelago
|
|
26
|
+
|
|
27
|
+
module Tranny
|
|
28
|
+
|
|
29
|
+
#
|
|
30
|
+
# Time before any Transaction will be automatically aborted.
|
|
31
|
+
#
|
|
32
|
+
TRANSACTION_TIMEOUT = 60 * 60
|
|
33
|
+
|
|
34
|
+
#
|
|
35
|
+
# If a member tries to join a transaction that it has allready joined
|
|
36
|
+
# it will receive this.
|
|
37
|
+
#
|
|
38
|
+
class JoinCountException < RuntimeError
|
|
39
|
+
def initialize(member, transaction)
|
|
40
|
+
super("#{member.inspect} has allready joined the transaction #{transaction.inspect}")
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
#
|
|
45
|
+
# If a member tries to commit a transaction not in :active state
|
|
46
|
+
# or abort a transaction in :commited state, or any other grave
|
|
47
|
+
# state offence
|
|
48
|
+
#
|
|
49
|
+
class IllegalOperationException < RuntimeError
|
|
50
|
+
def initialize(operation, transaction)
|
|
51
|
+
super("#{operation.inspect} is illegal for #{transaction.inspect}")
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
#
|
|
56
|
+
# If a member tries to get a transaction that the manager doesnt know about
|
|
57
|
+
# it gets this.
|
|
58
|
+
#
|
|
59
|
+
class UnknownTransactionException < RuntimeError
|
|
60
|
+
def initialize(transaction_id, manager)
|
|
61
|
+
super("#{manager.inspect} doesnt know about any transaction with id #{transaction_id.inspect}")
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
#
|
|
66
|
+
# If a member tries to report its state and the manager doesnt know about the
|
|
67
|
+
# member, it gets this.
|
|
68
|
+
#
|
|
69
|
+
class UnknownMemberException < RuntimeError
|
|
70
|
+
def initialize(member, transaction)
|
|
71
|
+
super("#{transaction.inspect} doesnt know about any member like #{member.inspect}")
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
#
|
|
76
|
+
# If a member misbehaves (or the Transaction is completely fucked up) this will be raised
|
|
77
|
+
#
|
|
78
|
+
class IllegalStateException < RuntimeError
|
|
79
|
+
def initialize(transaction)
|
|
80
|
+
super("#{transaction.inspect} is in a completely fucked up state")
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
#
|
|
85
|
+
# The manager itself.
|
|
86
|
+
#
|
|
87
|
+
# This will be the drb exported object that participants talk to,
|
|
88
|
+
# either directly or through a TransactionProxy.
|
|
89
|
+
#
|
|
90
|
+
# See also the TransactionProxy and Transaction classes.
|
|
91
|
+
#
|
|
92
|
+
class Manager
|
|
93
|
+
|
|
94
|
+
include DRb::DRbUndumped
|
|
95
|
+
include Archipelago::Disco::Publishable
|
|
96
|
+
|
|
97
|
+
attr_accessor :error_logger
|
|
98
|
+
attr_accessor :transaction_timeout
|
|
99
|
+
|
|
100
|
+
#
|
|
101
|
+
# Will use a BerkeleyHashishProvider using tranny_manager.db in the same dir to get its hashes
|
|
102
|
+
# if not <i>:persistence_provider</i> is given.
|
|
103
|
+
#
|
|
104
|
+
# Will create Transactions timing out after <i>:transaction_timeout</i> seconds or TRANSACTION_TIMEOUT
|
|
105
|
+
# if none is given.
|
|
106
|
+
#
|
|
107
|
+
# Will use Archipelago::Disco::Publishable by calling <b>initialize_publishable</b> with +options+.
|
|
108
|
+
#
|
|
109
|
+
def initialize(options = {})
|
|
110
|
+
@persistence_provider = options[:persistence_provider] || Archipelago::Hashish::BerkeleyHashishProvider.new(Pathname.new(File.expand_path(__FILE__)).parent.join("tranny_manager.db"))
|
|
111
|
+
|
|
112
|
+
initialize_publishable(options)
|
|
113
|
+
|
|
114
|
+
@transaction_timeout = options[:transaction_timeout] || TRANSACTION_TIMEOUT
|
|
115
|
+
|
|
116
|
+
@metadata = @persistence_provider.get_hashish("metadata")
|
|
117
|
+
@db = @persistence_provider.get_cached_hashish("db")
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
#
|
|
121
|
+
# Returns a proxy to a newly created Transaction.
|
|
122
|
+
#
|
|
123
|
+
# The Transaction will timeout after <i>:timeout</i> seconds
|
|
124
|
+
# or @transaction_timeout if none is given.
|
|
125
|
+
#
|
|
126
|
+
def begin(options = {})
|
|
127
|
+
options[:manager] = self
|
|
128
|
+
options[:timeout] ||= @transaction_timeout
|
|
129
|
+
return Transaction.new(options).proxy
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
#
|
|
133
|
+
# Used by transactions to notify this manager
|
|
134
|
+
# about an error. Delegates the actual logging
|
|
135
|
+
# to @error_logger.call(exception).
|
|
136
|
+
#
|
|
137
|
+
# Set @error_logger or override this method
|
|
138
|
+
# if you want to log any errors.
|
|
139
|
+
#
|
|
140
|
+
def log_error(exception)
|
|
141
|
+
self.error_logger.call(exception) if self.error_logger
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
#
|
|
145
|
+
# Used by a +transaction+ to store its state for future reference.
|
|
146
|
+
#
|
|
147
|
+
def store_transaction!(transaction)
|
|
148
|
+
@db[transaction.transaction_id] = transaction
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
#
|
|
152
|
+
# Used by a +transaction+ to remove its state when finished.
|
|
153
|
+
#
|
|
154
|
+
def remove_transaction!(transaction)
|
|
155
|
+
@db.delete(transaction.transaction_id)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
#
|
|
159
|
+
# Used by a transaction proxy to run +meth+ with +args+ on its +transaction_id+.
|
|
160
|
+
#
|
|
161
|
+
def call_instance_method(transaction_id, meth, *args)
|
|
162
|
+
transaction = @db[transaction_id]
|
|
163
|
+
if transaction
|
|
164
|
+
return transaction.send(meth, *args)
|
|
165
|
+
else
|
|
166
|
+
raise UnknownTransactionException.new(transaction_id, self)
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
#
|
|
176
|
+
# A proxy to a transaction managed by this manager.
|
|
177
|
+
#
|
|
178
|
+
# Used because standard DRbObjects only use object_id
|
|
179
|
+
# to identify objects, while we want the more unique transaction_id.
|
|
180
|
+
#
|
|
181
|
+
# Will also remember the state of the transaction when it detects
|
|
182
|
+
# methods that will terminate it (:commit | :abort).
|
|
183
|
+
#
|
|
184
|
+
class TransactionProxy
|
|
185
|
+
attr_accessor :transaction_id
|
|
186
|
+
def initialize(transaction)
|
|
187
|
+
@manager = transaction.manager
|
|
188
|
+
@transaction_id = transaction.transaction_id
|
|
189
|
+
@state = :unknown
|
|
190
|
+
end
|
|
191
|
+
#
|
|
192
|
+
# Return the cached state of the wrapped Archipelago::Tranny::Transaction
|
|
193
|
+
# if it is known, otherwise fetch it.
|
|
194
|
+
#
|
|
195
|
+
def state
|
|
196
|
+
if @state == :unknown
|
|
197
|
+
method_missing(:state)
|
|
198
|
+
else
|
|
199
|
+
@state
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
#
|
|
203
|
+
# Implemented to ensure that all TransactionProxies with the same
|
|
204
|
+
# transaction_id are hashwise equal.
|
|
205
|
+
#
|
|
206
|
+
def hash
|
|
207
|
+
@transaction_id.hash
|
|
208
|
+
end
|
|
209
|
+
#
|
|
210
|
+
# Implemented to ensure that all TransactionProxies with the same
|
|
211
|
+
# transaction_id are hashwise equal.
|
|
212
|
+
#
|
|
213
|
+
def eql?(o)
|
|
214
|
+
if TransactionProxy === o
|
|
215
|
+
o.transaction_id == @transaction_id
|
|
216
|
+
else
|
|
217
|
+
false
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
#
|
|
221
|
+
# Forwards everything to our Transaction and remembers
|
|
222
|
+
# returnvalue if necessary.
|
|
223
|
+
#
|
|
224
|
+
def method_missing(meth, *args) #:nodoc:
|
|
225
|
+
rval = @manager.call_instance_method(@transaction_id, meth, *args)
|
|
226
|
+
case meth
|
|
227
|
+
when :abort!
|
|
228
|
+
@state = :aborted
|
|
229
|
+
when :commit!
|
|
230
|
+
@state = rval
|
|
231
|
+
end
|
|
232
|
+
return rval
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
#
|
|
240
|
+
# A transaction managed by the manager.
|
|
241
|
+
#
|
|
242
|
+
# A transaction can have the following states:
|
|
243
|
+
#
|
|
244
|
+
# * :active - When it is first created.
|
|
245
|
+
# * This transaction can be commited or aborted.
|
|
246
|
+
#
|
|
247
|
+
# * :voting - When it has started the two-phase commit and the voting has begun.
|
|
248
|
+
# * This transaction can be aborted.
|
|
249
|
+
#
|
|
250
|
+
# * :commited - After everyone has voted and voted either :unchanged or :commit.
|
|
251
|
+
# * This transaction can not be changed.
|
|
252
|
+
#
|
|
253
|
+
# * :aborted - After someone has called abort! or voted :abort in a vote!
|
|
254
|
+
# * This transaction can not be changed.
|
|
255
|
+
#
|
|
256
|
+
# Anyone that wants to join the transaction must implement the following methods:
|
|
257
|
+
#
|
|
258
|
+
# * abort!(transaction): Abort the provided transaction.
|
|
259
|
+
# * commit!(transaction): Commit the provided transaction.
|
|
260
|
+
# * prepare!(transaction): Prepare for commiting the provided transaction.
|
|
261
|
+
#
|
|
262
|
+
# abort! and commit! should not return any values, but can raise exceptions
|
|
263
|
+
# if required.
|
|
264
|
+
#
|
|
265
|
+
# prepare! must return either :abort, :commit or :unchanged, depending on what
|
|
266
|
+
# the member is prepared to do. :unchanged is only when the member has not changed
|
|
267
|
+
# state during the transaction, and means that it does not require any further
|
|
268
|
+
# notification on the progress of the transaction.
|
|
269
|
+
#
|
|
270
|
+
# A member that has returned :commit on the prepare! must store the transaction
|
|
271
|
+
# proxy in a persistent manner to be able to connect to the manager
|
|
272
|
+
# and get a new copy of the transaction in case of communications failure.
|
|
273
|
+
#
|
|
274
|
+
# A member that reconnects to a crashed transaction manager should not abort! the
|
|
275
|
+
# transaction if the transaction is still in :active state, since the transaction will
|
|
276
|
+
# have been aborted on commit! anyway (since the manager will not be able to prepare! the
|
|
277
|
+
# disconnected member after either the manager or member having crashed) if needed.
|
|
278
|
+
# If the disconnect was just a temporary networking problem, the transaction will
|
|
279
|
+
# continue as planned.
|
|
280
|
+
#
|
|
281
|
+
# A member that reconnects to a crashed transaction manager where the transaction
|
|
282
|
+
# is in :voting state should just wait around and see if it gets prepare! called.
|
|
283
|
+
# In case of temporary network failure the transaction will continue as planned, otherwise
|
|
284
|
+
# it will abort! automatically.
|
|
285
|
+
#
|
|
286
|
+
# A member that reconnects to a disconnected transaction manager where the transaction
|
|
287
|
+
# is in :commited state should just commit its state. Then it must notify the transaction
|
|
288
|
+
# using report_commited! so that the transaction can disappear gracefully.
|
|
289
|
+
#
|
|
290
|
+
# A member that reconnects to a disconnected transaction manager that either doesnt know
|
|
291
|
+
# of the transaction or returns an :aborted transaction may safely abort the state change
|
|
292
|
+
# and forget about the transaction.
|
|
293
|
+
#
|
|
294
|
+
class Transaction
|
|
295
|
+
|
|
296
|
+
include Archipelago::Current::Synchronized
|
|
297
|
+
|
|
298
|
+
attr_reader :state, :transaction_id, :proxy, :manager
|
|
299
|
+
|
|
300
|
+
#
|
|
301
|
+
# Create a transaction managed by the provided +manager+.
|
|
302
|
+
#
|
|
303
|
+
# Will have <i>:manager</i> as TransactionManager, and will timeout
|
|
304
|
+
# after <i>:timeout</i> seconds.
|
|
305
|
+
#
|
|
306
|
+
def initialize(options)
|
|
307
|
+
super()
|
|
308
|
+
#
|
|
309
|
+
# A hash where members are keys and their state the values.
|
|
310
|
+
#
|
|
311
|
+
@members = {}
|
|
312
|
+
@members.extend(Archipelago::Current::Synchronized)
|
|
313
|
+
#
|
|
314
|
+
# We are alive!
|
|
315
|
+
#
|
|
316
|
+
@state = :active
|
|
317
|
+
#
|
|
318
|
+
# We have a timeout!
|
|
319
|
+
#
|
|
320
|
+
@timeout = options[:timeout]
|
|
321
|
+
#
|
|
322
|
+
# We are unique!
|
|
323
|
+
#
|
|
324
|
+
@transaction_id = "#{options[:manager].service_id}:#{Time.new.to_f}:#{self.object_id}:#{rand(1 << 32)}"
|
|
325
|
+
#
|
|
326
|
+
# We have a birth time!
|
|
327
|
+
#
|
|
328
|
+
@created_at = Time.now
|
|
329
|
+
#
|
|
330
|
+
# We have a manager!
|
|
331
|
+
#
|
|
332
|
+
self.manager = options[:manager]
|
|
333
|
+
|
|
334
|
+
store_us_for_future_reference!
|
|
335
|
+
|
|
336
|
+
start_timeout_thread
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
#
|
|
340
|
+
# Store this manager as ours and create a proxy that knows about it.
|
|
341
|
+
#
|
|
342
|
+
# Used by Archipelago::Tranny:Manager when restoring crashed
|
|
343
|
+
# Archipelago::Tranny:Transactions.
|
|
344
|
+
#
|
|
345
|
+
def manager=(manager)
|
|
346
|
+
#
|
|
347
|
+
# We have a manager!
|
|
348
|
+
#
|
|
349
|
+
@manager = DRbObject.new(manager)
|
|
350
|
+
#
|
|
351
|
+
# We have a proxy to send forth into the world!
|
|
352
|
+
#
|
|
353
|
+
@proxy = TransactionProxy.new(self)
|
|
354
|
+
nil
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
#
|
|
358
|
+
# Special dump call to NOT dump our manager or proxy,
|
|
359
|
+
# since DRbObjects dont take kindly to being restored
|
|
360
|
+
# in an environment where they are are known to be invalid.
|
|
361
|
+
#
|
|
362
|
+
def _dump(l)
|
|
363
|
+
return Marshal.dump([
|
|
364
|
+
@members,
|
|
365
|
+
@state,
|
|
366
|
+
@timeout,
|
|
367
|
+
@transaction_id,
|
|
368
|
+
@created_at
|
|
369
|
+
])
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def self._load(s)
|
|
373
|
+
members, state, timeout, transaction_id, created_at = Marshal.load(s)
|
|
374
|
+
rval = self.allocate
|
|
375
|
+
rval.instance_variable_set(:@members, members)
|
|
376
|
+
rval.instance_variable_set(:@state, state)
|
|
377
|
+
rval.instance_variable_set(:@timeout, timeout)
|
|
378
|
+
rval.instance_variable_set(:@transaction_id, transaction_id)
|
|
379
|
+
rval.instance_variable_set(:@created_at, created_at)
|
|
380
|
+
rval
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
#
|
|
384
|
+
# Starts the thread that will abort! us automatically
|
|
385
|
+
# after we have lived @timeout
|
|
386
|
+
#
|
|
387
|
+
def start_timeout_thread
|
|
388
|
+
Thread.new do
|
|
389
|
+
now = Time.now
|
|
390
|
+
die_at = @created_at + @timeout
|
|
391
|
+
if die_at > now
|
|
392
|
+
sleep(die_at - now)
|
|
393
|
+
end
|
|
394
|
+
abort!
|
|
395
|
+
end
|
|
396
|
+
nil
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
#
|
|
400
|
+
# What it sounds like.
|
|
401
|
+
#
|
|
402
|
+
def store_us_for_future_reference!
|
|
403
|
+
@manager.store_transaction!(self)
|
|
404
|
+
nil
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
#
|
|
408
|
+
# Remove ourselves, we are redundant
|
|
409
|
+
#
|
|
410
|
+
def remove_us_we_are_redundant!
|
|
411
|
+
@manager.remove_transaction!(self)
|
|
412
|
+
nil
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
#
|
|
416
|
+
# Used by members that failed during commit.
|
|
417
|
+
#
|
|
418
|
+
def report_commited!(member)
|
|
419
|
+
raise UnknownMemberException(member, self) unless @members.include?(member)
|
|
420
|
+
raise IllegalOperationException(:report_commited!, self) unless self.state == :commited
|
|
421
|
+
|
|
422
|
+
@members[member] = :commited
|
|
423
|
+
|
|
424
|
+
remove_us_if_all_are_commited!
|
|
425
|
+
nil
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
#
|
|
429
|
+
# Abort the transaction, sending all participants the abort! message.
|
|
430
|
+
#
|
|
431
|
+
def abort!
|
|
432
|
+
synchronize do
|
|
433
|
+
raise IllegalOperationException.new(:abort!, self) if [:commited, :aborted].include?(self.state)
|
|
434
|
+
|
|
435
|
+
#
|
|
436
|
+
# Set our state.
|
|
437
|
+
#
|
|
438
|
+
@state = :aborted
|
|
439
|
+
|
|
440
|
+
store_us_for_future_reference!
|
|
441
|
+
|
|
442
|
+
#
|
|
443
|
+
# Abort all members.
|
|
444
|
+
#
|
|
445
|
+
threads = []
|
|
446
|
+
@members.clone.each do |member, state|
|
|
447
|
+
raise RuntimeException.new("This is not supposed to be possible, but we are in abort! with member " +
|
|
448
|
+
"#{member.inspect} in state #{state.inspect}") if state == :commited
|
|
449
|
+
if state != :aborted
|
|
450
|
+
threads << Thread.new do
|
|
451
|
+
begin
|
|
452
|
+
member.abort!(self.proxy)
|
|
453
|
+
@members.synchronize do
|
|
454
|
+
@members[member] = :aborted
|
|
455
|
+
end
|
|
456
|
+
rescue Exception => e
|
|
457
|
+
@manager.log_error(e)
|
|
458
|
+
#
|
|
459
|
+
# We must not let the other members stop just
|
|
460
|
+
# because one member failed. No more Mr Nice Guy!
|
|
461
|
+
#
|
|
462
|
+
end
|
|
463
|
+
end
|
|
464
|
+
end
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
#
|
|
468
|
+
# Wait for all members to finish.
|
|
469
|
+
#
|
|
470
|
+
threads.each do |thread|
|
|
471
|
+
thread.join
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
#
|
|
475
|
+
# No use having aborted transactions lying about.
|
|
476
|
+
#
|
|
477
|
+
# NB: This means that disconnected members that cant
|
|
478
|
+
# find their old transactions will have to presume they
|
|
479
|
+
# have been aborted.
|
|
480
|
+
#
|
|
481
|
+
remove_us_we_are_redundant!
|
|
482
|
+
end
|
|
483
|
+
nil
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
#
|
|
487
|
+
# Commits the transaction, returning the new state (:aborted | :commited)
|
|
488
|
+
#
|
|
489
|
+
def commit!
|
|
490
|
+
synchronize do
|
|
491
|
+
raise IllegalOperationException.new(:commit!, self) unless self.state == :active
|
|
492
|
+
|
|
493
|
+
#
|
|
494
|
+
# Vote for the outcome and act on it
|
|
495
|
+
#
|
|
496
|
+
case vote!
|
|
497
|
+
when :abort
|
|
498
|
+
abort!
|
|
499
|
+
when :commit
|
|
500
|
+
_commit!
|
|
501
|
+
when :unchanged
|
|
502
|
+
@state = :commited
|
|
503
|
+
remove_us_we_are_redundant!
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
return @state
|
|
507
|
+
end
|
|
508
|
+
nil
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
#
|
|
512
|
+
# Join a +member+ to this transaction.
|
|
513
|
+
#
|
|
514
|
+
# Will raise a JoinCountException if the given member has allready
|
|
515
|
+
# joined this transaction.
|
|
516
|
+
#
|
|
517
|
+
def join(member)
|
|
518
|
+
@members.synchronize do
|
|
519
|
+
raise IllegalOperationException.new(:join, self) unless self.state == :active
|
|
520
|
+
raise JoinCountException.new(member, self) if @members.include?(member)
|
|
521
|
+
|
|
522
|
+
@members[member] = :active
|
|
523
|
+
end
|
|
524
|
+
store_us_for_future_reference!
|
|
525
|
+
nil
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
private
|
|
529
|
+
|
|
530
|
+
#
|
|
531
|
+
# Commit the transaction, sending all participants the commit message.
|
|
532
|
+
#
|
|
533
|
+
# The transaction is commited by all members having commit! called.
|
|
534
|
+
#
|
|
535
|
+
def _commit!
|
|
536
|
+
#
|
|
537
|
+
# Set our state.
|
|
538
|
+
#
|
|
539
|
+
@state = :commited
|
|
540
|
+
|
|
541
|
+
store_us_for_future_reference!
|
|
542
|
+
|
|
543
|
+
#
|
|
544
|
+
# Commit all members.
|
|
545
|
+
#
|
|
546
|
+
threads = []
|
|
547
|
+
@members.clone.each do |member, state|
|
|
548
|
+
raise RuntimeException.new("This is not supposed to be possible, but we are in _commit with member " +
|
|
549
|
+
"#{member.inspect} in state #{state.inspect}") unless [:prepared, :commited].include?(state)
|
|
550
|
+
threads << Thread.new do
|
|
551
|
+
begin
|
|
552
|
+
member.commit!(self.proxy)
|
|
553
|
+
@members.synchronize do
|
|
554
|
+
@members[member] = :commited
|
|
555
|
+
end
|
|
556
|
+
rescue Exception => e
|
|
557
|
+
@manager.log_error(e)
|
|
558
|
+
#
|
|
559
|
+
# We must not let the other members stop just
|
|
560
|
+
# because one member failed. No more Mr Nice Guy!
|
|
561
|
+
#
|
|
562
|
+
end
|
|
563
|
+
end
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
#
|
|
567
|
+
# Wait for all members to finish.
|
|
568
|
+
#
|
|
569
|
+
threads.each do |thread|
|
|
570
|
+
thread.join
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
remove_us_if_all_are_commited!
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
def remove_us_if_all_are_commited!
|
|
577
|
+
#
|
|
578
|
+
# Check to see if all members have been told about the decision.
|
|
579
|
+
#
|
|
580
|
+
all_have_commited = true
|
|
581
|
+
@members.each do |member, state|
|
|
582
|
+
all_have_commited = false unless state == :comitted
|
|
583
|
+
end
|
|
584
|
+
|
|
585
|
+
#
|
|
586
|
+
# If they have, remove ourselves from the manager.
|
|
587
|
+
#
|
|
588
|
+
remove_us_we_are_redundant! if all_have_commited
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
#
|
|
592
|
+
# Let the members of the transaction vote for its outcome.
|
|
593
|
+
#
|
|
594
|
+
# Members vote by having prepare! called.
|
|
595
|
+
#
|
|
596
|
+
# Valid returnvalues for the prepare! call are:
|
|
597
|
+
# :abort, if the member wants to abort the transaction
|
|
598
|
+
# :commit, if the member wants to commit the transaction
|
|
599
|
+
# :unchanged, if the member has not changed state during the transaction
|
|
600
|
+
#
|
|
601
|
+
def vote!
|
|
602
|
+
raise IllegalOperationException.new(:vote!, self) unless self.state == :active
|
|
603
|
+
|
|
604
|
+
@state = :voting
|
|
605
|
+
|
|
606
|
+
store_us_for_future_reference!
|
|
607
|
+
|
|
608
|
+
return_value = :commit
|
|
609
|
+
|
|
610
|
+
threads = []
|
|
611
|
+
@members.clone.each do |member, state|
|
|
612
|
+
raise RuntimeException.new("This is not supposed to be possible, but we are in vote! with member " +
|
|
613
|
+
"#{member.inspect} in state #{state.inspect}") unless state == :active
|
|
614
|
+
threads << Thread.new do
|
|
615
|
+
this_result = nil
|
|
616
|
+
begin
|
|
617
|
+
this_result = member.prepare!(self.proxy)
|
|
618
|
+
rescue Exception => e
|
|
619
|
+
@manager.log_error(e)
|
|
620
|
+
this_result = :abort
|
|
621
|
+
end
|
|
622
|
+
@members.synchronize do
|
|
623
|
+
case this_result
|
|
624
|
+
when :abort
|
|
625
|
+
@members[member] = :aborted
|
|
626
|
+
return_value = :abort
|
|
627
|
+
when :unchanged
|
|
628
|
+
@members.delete(member)
|
|
629
|
+
else
|
|
630
|
+
@members[member] = :prepared
|
|
631
|
+
end
|
|
632
|
+
end
|
|
633
|
+
end
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
threads.each do |thread|
|
|
637
|
+
thread.join
|
|
638
|
+
end
|
|
639
|
+
|
|
640
|
+
if @members.empty?
|
|
641
|
+
return_value = :unchanged
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
return_value
|
|
645
|
+
end
|
|
646
|
+
|
|
647
|
+
end
|
|
648
|
+
end
|
|
649
|
+
|
|
650
|
+
end
|