sbf-dm-transactions 1.3.0.beta

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,112 @@
1
+ module DataMapper
2
+ class Transaction
3
+
4
+ module DataObjectsAdapter
5
+ extend Chainable
6
+
7
+ # Produces a fresh transaction primitive for this Adapter
8
+ #
9
+ # Used by Transaction to perform its various tasks.
10
+ #
11
+ # @return [Object]
12
+ # a new Object that responds to :close, :begin, :commit,
13
+ # and :rollback,
14
+ #
15
+ # @api private
16
+ def transaction_primitive
17
+ if current_transaction && supports_savepoints?
18
+ DataObjects::SavePoint.create_for_uri(normalized_uri, current_connection)
19
+ else
20
+ DataObjects::Transaction.create_for_uri(normalized_uri)
21
+ end
22
+ end
23
+
24
+ # Pushes the given Transaction onto the per thread Transaction stack so
25
+ # that everything done by this Adapter is done within the context of said
26
+ # Transaction.
27
+ #
28
+ # @param [Transaction] transaction
29
+ # a Transaction to be the 'current' transaction until popped.
30
+ #
31
+ # @return [Array(Transaction)]
32
+ # the stack of active transactions for the current thread
33
+ #
34
+ # @api private
35
+ def push_transaction(transaction)
36
+ transactions << transaction
37
+ end
38
+
39
+ # Pop the 'current' Transaction from the per thread Transaction stack so
40
+ # that everything done by this Adapter is no longer necessarily within the
41
+ # context of said Transaction.
42
+ #
43
+ # @return [Transaction]
44
+ # the former 'current' transaction.
45
+ #
46
+ # @api private
47
+ def pop_transaction
48
+ transactions.pop
49
+ end
50
+
51
+ # Retrieve the current transaction for this Adapter.
52
+ #
53
+ # Everything done by this Adapter is done within the context of this
54
+ # Transaction.
55
+ #
56
+ # @return [Transaction]
57
+ # the 'current' transaction for this Adapter.
58
+ #
59
+ # @api private
60
+ def current_transaction
61
+ transactions.last
62
+ end
63
+
64
+ chainable do
65
+ protected
66
+
67
+ # @api semipublic
68
+ def open_connection
69
+ current_connection || super
70
+ end
71
+
72
+ # @api semipublic
73
+ def close_connection(connection)
74
+ super unless current_connection.equal?(connection)
75
+ end
76
+ end
77
+
78
+ private
79
+
80
+ # @api private
81
+ def transactions
82
+ Thread.current[:dm_transactions] ||= {}
83
+ Thread.current[:dm_transactions][object_id] ||= []
84
+ end
85
+
86
+ # Retrieve the current connection for this Adapter.
87
+ #
88
+ # @return [Transaction]
89
+ # the 'current' connection for this Adapter.
90
+ #
91
+ # @api private
92
+ def current_connection
93
+ if transaction = current_transaction
94
+ transaction.primitive_for(self).connection
95
+ end
96
+ end
97
+
98
+ # Indicate whether adapter supports transactional savepoints. Not all DO
99
+ # adapters do, so default to false.
100
+ #
101
+ # @return [Boolean]
102
+ # whether or not the adapter supports savepoints
103
+ #
104
+ # @api private
105
+ def supports_savepoints?
106
+ false
107
+ end
108
+
109
+ end # module DataObjectsAdapter
110
+
111
+ end # class Transaction
112
+ end # module DataMapper
@@ -0,0 +1,15 @@
1
+ require 'dm-transactions/adapters/dm-do-adapter'
2
+
3
+ module DataMapper
4
+ class Transaction
5
+
6
+ module MysqlAdapter
7
+ include DataObjectsAdapter
8
+
9
+ def supports_savepoints?
10
+ true
11
+ end
12
+ end
13
+
14
+ end
15
+ end
@@ -0,0 +1,11 @@
1
+ require 'dm-transactions/adapters/dm-do-adapter'
2
+
3
+ module DataMapper
4
+ class Transaction
5
+
6
+ module OracleAdapter
7
+ include DataObjectsAdapter
8
+ end
9
+
10
+ end
11
+ end
@@ -0,0 +1,15 @@
1
+ require 'dm-transactions/adapters/dm-do-adapter'
2
+
3
+ module DataMapper
4
+ class Transaction
5
+
6
+ module PostgresAdapter
7
+ include DataObjectsAdapter
8
+
9
+ def supports_savepoints?
10
+ true
11
+ end
12
+ end
13
+
14
+ end
15
+ end
@@ -0,0 +1,11 @@
1
+ require 'dm-transactions/adapters/dm-do-adapter'
2
+
3
+ module DataMapper
4
+ class Transaction
5
+
6
+ module SqliteAdapter
7
+ include DataObjectsAdapter
8
+ end
9
+
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ require 'dm-transactions/adapters/dm-do-adapter'
2
+
3
+ module DataMapper
4
+ class Transaction
5
+
6
+ module SqlserverAdapter
7
+ include DataObjectsAdapter
8
+ end
9
+
10
+ end
11
+ end
@@ -0,0 +1,5 @@
1
+ module DataMapper
2
+ module Transactions
3
+ VERSION = '1.3.0.beta'
4
+ end
5
+ end
@@ -0,0 +1,443 @@
1
+ require 'dm-core'
2
+
3
+ module DataMapper
4
+ class Transaction
5
+ extend Chainable
6
+
7
+ # @api private
8
+ attr_accessor :state
9
+
10
+ # @api private
11
+ def none?
12
+ state == :none
13
+ end
14
+
15
+ # @api private
16
+ def begin?
17
+ state == :begin
18
+ end
19
+
20
+ # @api private
21
+ def rollback?
22
+ state == :rollback
23
+ end
24
+
25
+ # @api private
26
+ def commit?
27
+ state == :commit
28
+ end
29
+
30
+ # Create a new Transaction
31
+ #
32
+ # @see Transaction#link
33
+ #
34
+ # In fact, it just calls #link with the given arguments at the end of the
35
+ # constructor.
36
+ #
37
+ # @api public
38
+ def initialize(*things)
39
+ @transaction_primitives = {}
40
+ self.state = :none
41
+ @adapters = {}
42
+ link(*things)
43
+ if block_given?
44
+ warn "Passing block to #{self.class.name}.new is deprecated (#{caller[0]})"
45
+ commit { |*block_args| yield(*block_args) }
46
+ end
47
+ end
48
+
49
+ # Associate this Transaction with some things.
50
+ #
51
+ # @param [Object] things
52
+ # the things you want this Transaction associated with:
53
+ #
54
+ # Adapters::AbstractAdapter subclasses will be added as
55
+ # adapters as is.
56
+ # Arrays will have their elements added.
57
+ # Repository will have it's own @adapters added.
58
+ # Resource subclasses will have all the repositories of all
59
+ # their properties added.
60
+ # Resource instances will have all repositories of all their
61
+ # properties added.
62
+ #
63
+ # @param [Proc] block
64
+ # a block (taking one argument, the Transaction) to execute within
65
+ # this transaction. The transaction will begin and commit around
66
+ # the block, and rollback if an exception is raised.
67
+ #
68
+ # @api private
69
+ def link(*things)
70
+ unless none?
71
+ raise "Illegal state for link: #{state}"
72
+ end
73
+
74
+ things.each do |thing|
75
+ case thing
76
+ when DataMapper::Adapters::AbstractAdapter
77
+ @adapters[thing] = :none
78
+ when DataMapper::Repository
79
+ link(thing.adapter)
80
+ when DataMapper::Model
81
+ link(*thing.repositories)
82
+ when DataMapper::Resource
83
+ link(thing.model)
84
+ when Array
85
+ link(*thing)
86
+ else
87
+ raise "Unknown argument to #{self.class}#link: #{thing.inspect} (#{thing.class})"
88
+ end
89
+ end
90
+
91
+ if block_given?
92
+ commit { |*block_args| yield(*block_args) }
93
+ else
94
+ self
95
+ end
96
+ end
97
+
98
+ # Begin the transaction
99
+ #
100
+ # Before #begin is called, the transaction is not valid and can not be used.
101
+ #
102
+ # @api private
103
+ def begin
104
+ unless none?
105
+ raise "Illegal state for begin: #{state}"
106
+ end
107
+
108
+ each_adapter(:connect_adapter, [:log_fatal_transaction_breakage])
109
+ each_adapter(:begin_adapter, [:rollback_and_close_adapter_if_begin, :close_adapter_if_none])
110
+ self.state = :begin
111
+ end
112
+
113
+ # Commit the transaction
114
+ #
115
+ # If no block is given, it will simply commit any changes made since the
116
+ # Transaction did #begin.
117
+ #
118
+ # @param block<Block> a block (taking the one argument, the Transaction) to
119
+ # execute within this transaction. The transaction will begin and commit
120
+ # around the block, and roll back if an exception is raised.
121
+ #
122
+ # @api private
123
+ def commit
124
+ if block_given?
125
+ unless none?
126
+ raise "Illegal state for commit with block: #{state}"
127
+ end
128
+
129
+ begin
130
+ self.begin
131
+ rval = within { |*block_args| yield(*block_args) }
132
+ rescue Exception => exception
133
+ if begin?
134
+ rollback
135
+ end
136
+ raise exception
137
+ ensure
138
+ unless exception
139
+ if begin?
140
+ commit
141
+ end
142
+ return rval
143
+ end
144
+ end
145
+ else
146
+ unless begin?
147
+ raise "Illegal state for commit without block: #{state}"
148
+ end
149
+ each_adapter(:commit_adapter, [:log_fatal_transaction_breakage])
150
+ each_adapter(:close_adapter, [:log_fatal_transaction_breakage])
151
+ self.state = :commit
152
+ end
153
+ end
154
+
155
+ # Rollback the transaction
156
+ #
157
+ # Will undo all changes made during the transaction.
158
+ #
159
+ # @api private
160
+ def rollback
161
+ unless begin?
162
+ raise "Illegal state for rollback: #{state}"
163
+ end
164
+ each_adapter(:rollback_adapter_if_begin, [:rollback_and_close_adapter_if_begin, :close_adapter_if_none])
165
+ each_adapter(:close_adapter_if_open, [:log_fatal_transaction_breakage])
166
+ self.state = :rollback
167
+ end
168
+
169
+ # Execute a block within this Transaction.
170
+ #
171
+ # No #begin, #commit or #rollback is performed in #within, but this
172
+ # Transaction will pushed on the per thread stack of transactions for each
173
+ # adapter it is associated with, and it will ensures that it will pop the
174
+ # Transaction away again after the block is finished.
175
+ #
176
+ # @param block<Block> the block of code to execute.
177
+ #
178
+ # @api private
179
+ def within
180
+ unless block_given?
181
+ raise 'No block provided'
182
+ end
183
+
184
+ unless begin?
185
+ raise "Illegal state for within: #{state}"
186
+ end
187
+
188
+ adapters = @adapters
189
+
190
+ adapters.each_key do |adapter|
191
+ adapter.push_transaction(self)
192
+ end
193
+
194
+ begin
195
+ yield self
196
+ ensure
197
+ adapters.each_key do |adapter|
198
+ adapter.pop_transaction
199
+ end
200
+ end
201
+ end
202
+
203
+ # @api private
204
+ def method_missing(method, *args, &block)
205
+ first_arg = args.first
206
+
207
+ return super unless args.size == 1 && first_arg.kind_of?(Adapters::AbstractAdapter)
208
+ return super unless match = method.to_s.match(/\A(.*)_(if|unless)_(none|begin|rollback|commit)\z/)
209
+
210
+ action, condition, expected_state = match.captures
211
+ return super unless respond_to?(action, true)
212
+
213
+ state = state_for(first_arg).to_s
214
+ execute = (condition == 'if') == (state == expected_state)
215
+
216
+ send(action, first_arg) if execute
217
+ end
218
+
219
+ # @api private
220
+ def primitive_for(adapter)
221
+ unless @adapters.include?(adapter)
222
+ raise "Unknown adapter #{adapter}"
223
+ end
224
+
225
+ unless @transaction_primitives.include?(adapter)
226
+ raise "No primitive for #{adapter}"
227
+ end
228
+
229
+ @transaction_primitives[adapter]
230
+ end
231
+
232
+ private
233
+
234
+ # @api private
235
+ def validate_primitive(primitive)
236
+ [:close, :begin, :rollback, :commit].each do |meth|
237
+ unless primitive.respond_to?(meth)
238
+ raise "Invalid primitive #{primitive}: doesnt respond_to?(#{meth.inspect})"
239
+ end
240
+ end
241
+
242
+ primitive
243
+ end
244
+
245
+ # @api private
246
+ def each_adapter(method, on_fail)
247
+ adapters = @adapters
248
+ begin
249
+ adapters.each_key do |adapter|
250
+ send(method, adapter)
251
+ end
252
+ rescue Exception => exception
253
+ adapters.each_key do |adapter|
254
+ on_fail.each do |fail_handler|
255
+ begin
256
+ send(fail_handler, adapter)
257
+ rescue Exception => inner_exception
258
+ DataMapper.logger.fatal("#{self}#each_adapter(#{method.inspect}, #{on_fail.inspect}) failed with #{exception.inspect}: #{exception.backtrace.join("\n")} - and when sending #{fail_handler} to #{adapter} we failed again with #{inner_exception.inspect}: #{inner_exception.backtrace.join("\n")}")
259
+ end
260
+ end
261
+ end
262
+ raise exception
263
+ end
264
+ end
265
+
266
+ # @api private
267
+ def state_for(adapter)
268
+ unless @adapters.include?(adapter)
269
+ raise "Unknown adapter #{adapter}"
270
+ end
271
+
272
+ @adapters[adapter]
273
+ end
274
+
275
+ # @api private
276
+ def do_adapter(adapter, what, prerequisite)
277
+ unless @transaction_primitives.include?(adapter)
278
+ raise "No primitive for #{adapter}"
279
+ end
280
+
281
+ state = state_for(adapter)
282
+
283
+ unless state == prerequisite
284
+ raise "Illegal state for #{what}: #{state}"
285
+ end
286
+
287
+ DataMapper.logger.debug("#{adapter.name}: #{what}")
288
+ @transaction_primitives[adapter].send(what)
289
+ @adapters[adapter] = what
290
+ end
291
+
292
+ # @api private
293
+ def log_fatal_transaction_breakage(adapter)
294
+ DataMapper.logger.fatal("#{self} experienced a totally broken transaction execution. Presenting member #{adapter.inspect}.")
295
+ end
296
+
297
+ # @api private
298
+ def connect_adapter(adapter)
299
+ if @transaction_primitives.key?(adapter)
300
+ raise "Already a primitive for adapter #{adapter}"
301
+ end
302
+
303
+ @transaction_primitives[adapter] = validate_primitive(adapter.transaction_primitive)
304
+ end
305
+
306
+ # @api private
307
+ def close_adapter_if_open(adapter)
308
+ if @transaction_primitives.include?(adapter)
309
+ close_adapter(adapter)
310
+ end
311
+ end
312
+
313
+ # @api private
314
+ def close_adapter(adapter)
315
+ unless @transaction_primitives.include?(adapter)
316
+ raise 'No primitive for adapter'
317
+ end
318
+
319
+ @transaction_primitives[adapter].close
320
+ @transaction_primitives.delete(adapter)
321
+ end
322
+
323
+ # @api private
324
+ def begin_adapter(adapter)
325
+ do_adapter(adapter, :begin, :none)
326
+ end
327
+
328
+ # @api private
329
+ def commit_adapter(adapter)
330
+ do_adapter(adapter, :commit, :begin)
331
+ end
332
+
333
+ # @api private
334
+ def rollback_adapter(adapter)
335
+ do_adapter(adapter, :rollback, :begin)
336
+ end
337
+
338
+ # @api private
339
+ def rollback_and_close_adapter(adapter)
340
+ rollback_adapter(adapter)
341
+ close_adapter(adapter)
342
+ end
343
+
344
+ module Repository
345
+
346
+ # Produce a new Transaction for this Repository
347
+ #
348
+ # @return [Adapters::Transaction]
349
+ # a new Transaction (in state :none) that can be used
350
+ # to execute code #with_transaction
351
+ #
352
+ # @api public
353
+ def transaction
354
+ Transaction.new(self)
355
+ end
356
+ end # module Repository
357
+
358
+ module Model
359
+ # @api private
360
+ def self.included(mod)
361
+ mod.descendants.each { |model| model.extend self }
362
+ end
363
+
364
+ # Produce a new Transaction for this Resource class
365
+ #
366
+ # @return <Adapters::Transaction
367
+ # a new Adapters::Transaction with all Repositories
368
+ # of the class of this Resource added.
369
+ #
370
+ # @api public
371
+ def transaction
372
+ transaction = Transaction.new(self)
373
+ transaction.commit { |block_args| yield(*block_args) }
374
+ end
375
+ end # module Model
376
+
377
+ module Resource
378
+
379
+ # Produce a new Transaction for the class of this Resource
380
+ #
381
+ # @return [Adapters::Transaction]
382
+ # a new Adapters::Transaction for the Repository
383
+ # of the class of this Resource added.
384
+ #
385
+ # @api public
386
+ def transaction
387
+ model.transaction { |*block_args| yield(*block_args) }
388
+ end
389
+ end # module Resource
390
+
391
+ def self.include_transaction_api
392
+ [ :Repository, :Model, :Resource ].each do |name|
393
+ DataMapper.const_get(name).send(:include, const_get(name))
394
+ end
395
+ Adapters::AbstractAdapter.descendants.each do |adapter_class|
396
+ Adapters.include_transaction_api(DataMapper::Inflector.demodulize(adapter_class.name))
397
+ end
398
+ end
399
+
400
+ end # class Transaction
401
+
402
+ module Adapters
403
+
404
+ def self.include_transaction_api(const_name)
405
+ require transaction_extensions(const_name)
406
+ if Transaction.const_defined?(const_name)
407
+ adapter = const_get(const_name)
408
+ adapter.send(:include, transaction_module(const_name))
409
+ end
410
+ rescue LoadError
411
+ # Silently ignore the fact that no adapter extensions could be required
412
+ # This means that the adapter in use doesn't support transactions
413
+ end
414
+
415
+ def self.transaction_module(const_name)
416
+ Transaction.const_get(const_name)
417
+ end
418
+
419
+ class << self
420
+ private
421
+
422
+ # @api private
423
+ def transaction_extensions(const_name)
424
+ name = adapter_name(const_name)
425
+ name = 'do' if name == 'dataobjects'
426
+ "dm-transactions/adapters/dm-#{name}-adapter"
427
+ end
428
+
429
+ end
430
+
431
+ extendable do
432
+ # @api private
433
+ def const_added(const_name)
434
+ include_transaction_api(const_name)
435
+ super
436
+ end
437
+ end
438
+
439
+ end # module Adapters
440
+
441
+ Transaction.include_transaction_api
442
+
443
+ end # module DataMapper
@@ -0,0 +1,17 @@
1
+ require 'rspec'
2
+ require_relative 'require_spec'
3
+ require 'dm-core/spec/setup'
4
+
5
+ # To really test this behavior, this spec needs to be run in isolation and not
6
+ # as part of the typical rake spec run, which requires dm-transactions upfront
7
+
8
+ if %w(postgres mysql sqlite oracle sqlserver).include?(ENV['ADAPTER'])
9
+ describe "require 'dm-transactions after calling DataMapper.setup" do
10
+ before(:all) do
11
+ @adapter = DataMapper::Spec.adapter
12
+ require 'dm-transactions'
13
+ end
14
+
15
+ it_behaves_like "require 'dm-transactions'"
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ require 'rspec'
2
+ require_relative 'require_spec'
3
+ require 'dm-core/spec/setup'
4
+
5
+ # To really test this behavior, this spec needs to be run in isolation and not
6
+ # as part of the typical rake spec run, which requires dm-transactions upfront
7
+
8
+ if %w(postgres mysql sqlite oracle sqlserver).include?(ENV['ADAPTER'])
9
+ describe "require 'dm-transactions' before calling DataMapper.setup" do
10
+ before(:all) do
11
+ require 'dm-transactions'
12
+ @adapter = DataMapper::Spec.adapter
13
+ end
14
+
15
+ it_behaves_like "require 'dm-transactions'"
16
+ end
17
+ end
@@ -0,0 +1,13 @@
1
+ shared_examples "require 'dm-transactions'" do
2
+ %w[ Repository Model Resource ].each do |name|
3
+ it "includes the transaction api in DataMapper::#{name}" do
4
+ expect((DataMapper.const_get(name) < DataMapper::Transaction.const_get(name))).to be(true)
5
+ end
6
+ end
7
+
8
+ it "includes the transaction api into the adapter" do
9
+ expect(@adapter.respond_to?(:push_transaction)).to be(true)
10
+ expect(@adapter.respond_to?(:pop_transaction)).to be(true)
11
+ expect(@adapter.respond_to?(:current_transaction)).to be(true)
12
+ end
13
+ end