graph_mediator 0.2.2 → 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -106,7 +106,7 @@ lock_version. So if another transaction updates a child, root.lock_version
106
106
  should increment, and the first transaction should raise a StaleObject error
107
107
  when it too tries to update the child.
108
108
 
109
- If you override super in the model hierarchy you are mediating, you must pass your
109
+ If you override save in the model hierarchy you are mediating, you must pass your
110
110
  override as a block to super or it will occur outside of mediation:
111
111
 
112
112
  def save
@@ -24,8 +24,10 @@ Gem::Specification.new do |s|
24
24
  s.test_files = `git ls-files spec/*`.split("\n")
25
25
 
26
26
  s.add_development_dependency(%q<rspec>, [">= 1.2.9"])
27
- s.add_runtime_dependency(%q<activerecord>, ["= 2.3.5"])
28
- s.add_runtime_dependency(%q<activesupport>, ["= 2.3.5"])
27
+ s.add_development_dependency(%q<diff-lcs>)
28
+ s.add_development_dependency(%q<sqlite3>)
29
+ s.add_runtime_dependency(%q<activerecord>, [">= 2.3.6", "< 3.0.0"])
30
+ s.add_runtime_dependency(%q<activesupport>, [">= 2.3.6", "< 3.0.0"])
29
31
  s.add_runtime_dependency(%q<aasm>, [">= 2.2.0"])
30
32
  end
31
33
 
@@ -80,6 +80,9 @@ module GraphMediator
80
80
  base.__send__(:class_inheritable_array, :graph_mediator_dependencies)
81
81
  base.graph_mediator_dependencies = []
82
82
  base.__send__(:_register_for_mediation, *(SAVE_METHODS.clone << { :track_changes => true }))
83
+ base.class_eval do
84
+ _alias_method_chain_ensuring_inheritability(:destroy, :flag)
85
+ end
83
86
  end
84
87
 
85
88
  # Inserts a new #{base}::MediatorProxy module with Proxy included.
@@ -104,10 +107,12 @@ module GraphMediator
104
107
  key = base.to_s.underscore.gsub('/','_').upcase
105
108
  hash_key = "GRAPH_MEDIATOR_#{key}_HASH_KEY"
106
109
  new_array_key = "GRAPH_MEDIATOR_#{key}_NEW_ARRAY_KEY"
110
+ being_destroyed_array_key = "GRAPH_MEDIATOR_#{key}_BEING_DESTROYED_ARRAY_KEY"
107
111
  eigen = base.instance_eval { class << self; self; end }
108
112
  eigen.class_eval do
109
113
  define_method(:mediator_hash_key) { hash_key }
110
114
  define_method(:mediator_new_array_key) { new_array_key }
115
+ define_method(:mediator_being_destroyed_array_key) { being_destroyed_array_key }
111
116
  end
112
117
 
113
118
  # Relies on ActiveSupport::Callbacks (which is included
@@ -189,27 +194,38 @@ module GraphMediator
189
194
  # (This is overwritten by GraphMediator._include_new_proxy)
190
195
  def mediator_new_array_key; end
191
196
 
197
+ # Unique key to access a thread local array of ids of instances that
198
+ # are currently in the process of being deleted.
199
+ def mediator_being_destroyed_array_key; end
200
+
192
201
  # The hash of Mediator instances active in this Thread for the Proxy's
193
202
  # base class.
194
203
  #
195
204
  # instance.id => Mediator of (instance)
196
205
  #
197
206
  def mediators
198
- unless Thread.current[mediator_hash_key]
199
- Thread.current[mediator_hash_key] = {}
200
- end
201
- Thread.current[mediator_hash_key]
207
+ _generate_thread_local(mediator_hash_key, Hash)
202
208
  end
203
209
 
204
210
  # An array of Mediator instances mediating new records in this Thread for
205
211
  # the Proxy's base class.
206
212
  def mediators_for_new_records
207
- unless Thread.current[mediator_new_array_key]
208
- Thread.current[mediator_new_array_key] = []
209
- end
210
- Thread.current[mediator_new_array_key]
213
+ _generate_thread_local(mediator_new_array_key, Array)
211
214
  end
212
215
 
216
+ # An array of instance ids currently being in the process of being destroyed.
217
+ def instances_being_destroyed
218
+ _generate_thread_local(mediator_being_destroyed_array_key, Array)
219
+ end
220
+
221
+ private
222
+
223
+ def _generate_thread_local(key, initial)
224
+ unless Thread.current[key]
225
+ Thread.current[key] = initial.kind_of?(Class) ? initial.new : initial
226
+ end
227
+ Thread.current[key]
228
+ end
213
229
  end
214
230
 
215
231
  # Wraps the given block in a transaction and begins mediation.
@@ -257,6 +273,42 @@ module GraphMediator
257
273
  @graph_mediator_mediation_disabled = false
258
274
  end
259
275
 
276
+ # True if this instance is currently in the middle of being destroyed. Set
277
+ # by code slipped around the core ActiveRecord::Base#destroy via
278
+ # destroy_with_flag.
279
+ #
280
+ # Used by dependents in the mediation process to check whether they should
281
+ # update their root (see notes under the +mediate+ method).
282
+ def being_destroyed?
283
+ instances_being_destroyed.include?(id)
284
+ end
285
+
286
+ # Surrounding the base destroy ensures that instance is marked in Thread
287
+ # before any other callbacks occur (notably the collection destroy
288
+ # dependents pushed into the before_destroy callback).
289
+ #
290
+ # If we instead relied on on the before_destroy, after_destroy callbacks,
291
+ # we would be at the mercy of declaration order in the class for the
292
+ # GraphMediator include versus the association macro.
293
+ def destroy_with_flag
294
+ _mark_being_destroyed
295
+ destroy_without_flag
296
+ ensure
297
+ _unmark_being_destroyed
298
+ end
299
+
300
+ private
301
+
302
+ def _mark_being_destroyed
303
+ instances_being_destroyed << id
304
+ end
305
+
306
+ def _unmark_being_destroyed
307
+ instances_being_destroyed.delete(id)
308
+ end
309
+
310
+ public
311
+
260
312
  # By default, every instance will be mediated and this will return true.
261
313
  # You can turn mediation on or off on an instance by instance basis with
262
314
  # calls to disable_mediation! or enable_mediation!.
@@ -295,6 +347,10 @@ module GraphMediator
295
347
  self.class.mediators_for_new_records
296
348
  end
297
349
 
350
+ def instances_being_destroyed
351
+ self.class.instances_being_destroyed
352
+ end
353
+
298
354
  # Accessor for the mediator associated with this instance's id, or nil if
299
355
  # we are not currently mediating.
300
356
  def current_mediator
@@ -335,7 +391,8 @@ module GraphMediator
335
391
  #
336
392
  # * options:
337
393
  # * :through => root node accessor that will be the target of the
338
- # mediated_transaction. By default self is assumed.
394
+ # mediated_transaction. By default self is assumed. This is used for
395
+ # tracking in the given root node the fact that dependents have changed.
339
396
  # * :track_changes => if true, the mediator will track changes such
340
397
  # that they can be reviewed after_mediation. The after_mediation
341
398
  # callbacks occur after dirty has completed and changes are normally
@@ -351,7 +408,7 @@ module GraphMediator
351
408
  _alias_method_chain_ensuring_inheritability(method, :mediation) do |aliased_target,punctuation|
352
409
  __send__(:define_method, "#{aliased_target}_with_mediation#{punctuation}") do |*args, &block|
353
410
  root_node = (root_node_accessor ? send(root_node_accessor) : self)
354
- unless root_node.nil?
411
+ unless root_node.nil? || root_node.being_destroyed?
355
412
  root_node.mediated_transaction do |mediator|
356
413
  mediator.debug("#{root_node} mediating #{aliased_target}#{punctuation} for #{self}")
357
414
  mediator.track_changes_for(self) if track_changes && saveing
@@ -485,7 +542,26 @@ module GraphMediator
485
542
  # Dependent classes have their save methods mediated as well. However, a
486
543
  # dependent class must provide an accessor for the root node, so that a
487
544
  # mediated_transaction can be begun in the root node when a dependent is
488
- # changed.
545
+ # changed. Dependent clases also have their destroy methods mediated so
546
+ # that destruction of a dependent also registers as a change to the
547
+ # graph.
548
+ #
549
+ # == Deletion and Dependents
550
+ #
551
+ # When a class participating in mediation assigns a dependent to mediation,
552
+ # destruction of that dependent class will cause an update to the parent's
553
+ # lock_version. This can cause a problem in Rails 2.3.6+ because
554
+ # ActiveRecord#destroy is wrapped with an optimistic locking check. When
555
+ # the dependent association is set to :dependent => :destroy, the
556
+ # dependents are automatically destroyed before the parent, which causes
557
+ # graph_mediator to update the lock_version of the parent, which then fails
558
+ # the optimistic locking check when it is sent for destruction in
559
+ # ActiveRecord::Locking::Optimistic#destroy_with_lock.
560
+ #
561
+ # To avoid this, GraphMediator causes an activerecord instance to flag when
562
+ # it is in the process of destroying itself. This flag is then checked by
563
+ # dependents so they can bypass touching the parent when they are being
564
+ # destroyed.
489
565
  #
490
566
  # = Versioning and Optimistic Locking
491
567
  #
@@ -1,3 +1,3 @@
1
1
  module GraphMediator
2
- VERSION = "0.2.2"
2
+ VERSION = "0.2.3"
3
3
  end
@@ -0,0 +1,9 @@
1
+ module Dependents
2
+ class Child < ActiveRecord::Base
3
+ belongs_to :parent
4
+ end
5
+
6
+ class ReverseChild < ActiveRecord::Base
7
+ belongs_to :reverse_parent
8
+ end
9
+ end
@@ -0,0 +1,20 @@
1
+ module Dependents
2
+ class Parent < ActiveRecord::Base
3
+ include GraphMediator
4
+ mediate :dependencies => Child
5
+
6
+ has_many :children, :dependent => :destroy
7
+ end
8
+
9
+ # To test whether the order of declaration impacts function
10
+ # For example, if we used before_destroy callbacks to mark a parent
11
+ # as being destroyed, the order matters, because associations register
12
+ # themselves for deletion when first declared using before_destroy as
13
+ # well.
14
+ class ReverseParent < ActiveRecord::Base
15
+ has_many :reverse_children, :dependent => :destroy
16
+
17
+ include GraphMediator
18
+ mediate :dependencies => ReverseChild
19
+ end
20
+ end
@@ -0,0 +1,23 @@
1
+ create_schema do |connection|
2
+ connection.create_table(:parents, :force => true) do |t|
3
+ t.string :name
4
+ t.integer :lock_version, :default => 0
5
+ t.timestamps
6
+ end
7
+
8
+ connection.create_table(:reverse_parents, :force => true) do |t|
9
+ t.string :name
10
+ t.integer :lock_version, :default => 0
11
+ t.timestamps
12
+ end
13
+
14
+ connection.create_table(:children, :force => true) do |t|
15
+ t.integer :parent_id
16
+ t.string :marker
17
+ end
18
+
19
+ connection.create_table(:reverse_children, :force => true) do |t|
20
+ t.integer :reverse_parent_id
21
+ t.string :marker
22
+ end
23
+ end
@@ -485,7 +485,11 @@ describe "GraphMediator" do
485
485
  it "should generate a unique mediator_new_array_key for each MediatorProxy" do
486
486
  @f.class.mediator_new_array_key.should == 'GRAPH_MEDIATOR_GRAPH_MEDIATOR_SPEC_FOO_NEW_ARRAY_KEY'
487
487
  end
488
-
488
+
489
+ it "should generate a unique mediator_being_destroyed_array_key for each MediatorProxy" do
490
+ @f.class.mediator_being_destroyed_array_key.should == 'GRAPH_MEDIATOR_GRAPH_MEDIATOR_SPEC_FOO_BEING_DESTROYED_ARRAY_KEY'
491
+ end
492
+
489
493
  it "should access an array of mediators for new records" do
490
494
  @f.__send__(:mediators_for_new_records).should == []
491
495
  end
@@ -494,6 +498,9 @@ describe "GraphMediator" do
494
498
  @f.__send__(:mediators).should == {}
495
499
  end
496
500
 
501
+ it "should access an array of ids for instances being destroyed" do
502
+ @f.__send__(:instances_being_destroyed).should == []
503
+ end
497
504
  end
498
505
  end
499
506
 
@@ -0,0 +1,84 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper'))
2
+
3
+ require 'dependents/child.rb'
4
+ require 'dependents/parent.rb'
5
+
6
+ module Dependents
7
+
8
+ describe "GraphMediator instances should flag that they are being destroyed" do
9
+ def load_track_destroy
10
+ create_schema do |conn|
11
+ conn.create_table(:track_destroys, :force => true) do |t|
12
+ t.string :name
13
+ t.integer :lock_version, :defaults => 0
14
+ t.tiimestamps
15
+ end
16
+ end
17
+
18
+ @destroy_callbacks = callbacks_ref = []
19
+ c = Class.new(ActiveRecord::Base)
20
+ Object.const_set(:TrackDestroy, c)
21
+ c.class_eval do
22
+ include GraphMediator
23
+
24
+ def destroy_without_callbacks
25
+ callbacks << being_destroyed?
26
+ super
27
+ end
28
+ define_method(:callbacks) { callbacks_ref }
29
+ end
30
+ end
31
+
32
+ before(:each) do
33
+ load_track_destroy
34
+ @t = TrackDestroy.create!
35
+ end
36
+
37
+ it "should note that it is being destroyed" do
38
+ @t.should_not be_being_destroyed
39
+ @t.destroy
40
+ @destroy_callbacks.should == [true]
41
+ @t.should_not be_being_destroyed
42
+ @t.should be_destroyed
43
+ end
44
+ end
45
+
46
+ describe "Tests ability to destroy objects with dependents despite optimistic locking" do
47
+
48
+ before(:all) do
49
+ load 'dependents/schema.rb'
50
+ end
51
+
52
+ before(:each) do
53
+ @p = Parent.create!(:name => 'foo')
54
+ @c1 = @p.children.create(:marker => 'bar')
55
+ @c2 = @p.children.create(:marker => 'baz')
56
+ end
57
+
58
+ it "should not raise a stale object error when deleting a parent with dependents that are automagically destroyed by activerecord" do
59
+ @p.should_not be_new_record
60
+ @c1.should_not be_new_record
61
+ @c2.should_not be_new_record
62
+ @p.reload
63
+ lambda { @p.destroy }.should_not raise_error(ActiveRecord::StaleObjectError)
64
+ end
65
+
66
+ it "does not matter whether the include or association is declared first" do
67
+ rp = ReverseParent.create(:name => 'foo')
68
+ c1 = rp.reverse_children.create(:marker => 'bar')
69
+ c2 = rp.reverse_children.create(:marker => 'baz')
70
+ rp.reload
71
+ lambda { rp.destroy }.should_not raise_error(ActiveRecord::StaleObjectError)
72
+ end
73
+
74
+ # This is a vague test that our changes to destroy workflow don't choke
75
+ # in the trivial case of an unstored instance.
76
+ it "should be okay if call destroy on new object" do
77
+ np = Parent.new(:name => 'foo')
78
+ cp = np.children.new(:marker => 'bar')
79
+ np.destroy.should == np # Nothing should be thrown
80
+ np.should be_destroyed
81
+ np.lock_version.should == 0
82
+ end
83
+ end
84
+ end
metadata CHANGED
@@ -1,12 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: graph_mediator
3
3
  version: !ruby/object:Gem::Version
4
+ hash: 17
4
5
  prerelease: false
5
6
  segments:
6
7
  - 0
7
8
  - 2
8
- - 2
9
- version: 0.2.2
9
+ - 3
10
+ version: 0.2.3
10
11
  platform: ruby
11
12
  authors:
12
13
  - Josh Partlow
@@ -14,16 +15,18 @@ autorequire:
14
15
  bindir: bin
15
16
  cert_chain: []
16
17
 
17
- date: 2010-12-08 00:00:00 -08:00
18
+ date: 2013-01-24 00:00:00 -08:00
18
19
  default_executable:
19
20
  dependencies:
20
21
  - !ruby/object:Gem::Dependency
21
22
  name: rspec
22
23
  prerelease: false
23
24
  requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
24
26
  requirements:
25
27
  - - ">="
26
28
  - !ruby/object:Gem::Version
29
+ hash: 13
27
30
  segments:
28
31
  - 1
29
32
  - 2
@@ -32,47 +35,97 @@ dependencies:
32
35
  type: :development
33
36
  version_requirements: *id001
34
37
  - !ruby/object:Gem::Dependency
35
- name: activerecord
38
+ name: diff-lcs
36
39
  prerelease: false
37
40
  requirement: &id002 !ruby/object:Gem::Requirement
41
+ none: false
38
42
  requirements:
39
- - - "="
43
+ - - ">="
40
44
  - !ruby/object:Gem::Version
45
+ hash: 3
46
+ segments:
47
+ - 0
48
+ version: "0"
49
+ type: :development
50
+ version_requirements: *id002
51
+ - !ruby/object:Gem::Dependency
52
+ name: sqlite3
53
+ prerelease: false
54
+ requirement: &id003 !ruby/object:Gem::Requirement
55
+ none: false
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ hash: 3
60
+ segments:
61
+ - 0
62
+ version: "0"
63
+ type: :development
64
+ version_requirements: *id003
65
+ - !ruby/object:Gem::Dependency
66
+ name: activerecord
67
+ prerelease: false
68
+ requirement: &id004 !ruby/object:Gem::Requirement
69
+ none: false
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ hash: 15
41
74
  segments:
42
75
  - 2
43
76
  - 3
44
- - 5
45
- version: 2.3.5
77
+ - 6
78
+ version: 2.3.6
79
+ - - <
80
+ - !ruby/object:Gem::Version
81
+ hash: 7
82
+ segments:
83
+ - 3
84
+ - 0
85
+ - 0
86
+ version: 3.0.0
46
87
  type: :runtime
47
- version_requirements: *id002
88
+ version_requirements: *id004
48
89
  - !ruby/object:Gem::Dependency
49
90
  name: activesupport
50
91
  prerelease: false
51
- requirement: &id003 !ruby/object:Gem::Requirement
92
+ requirement: &id005 !ruby/object:Gem::Requirement
93
+ none: false
52
94
  requirements:
53
- - - "="
95
+ - - ">="
54
96
  - !ruby/object:Gem::Version
97
+ hash: 15
55
98
  segments:
56
99
  - 2
57
100
  - 3
58
- - 5
59
- version: 2.3.5
101
+ - 6
102
+ version: 2.3.6
103
+ - - <
104
+ - !ruby/object:Gem::Version
105
+ hash: 7
106
+ segments:
107
+ - 3
108
+ - 0
109
+ - 0
110
+ version: 3.0.0
60
111
  type: :runtime
61
- version_requirements: *id003
112
+ version_requirements: *id005
62
113
  - !ruby/object:Gem::Dependency
63
114
  name: aasm
64
115
  prerelease: false
65
- requirement: &id004 !ruby/object:Gem::Requirement
116
+ requirement: &id006 !ruby/object:Gem::Requirement
117
+ none: false
66
118
  requirements:
67
119
  - - ">="
68
120
  - !ruby/object:Gem::Version
121
+ hash: 7
69
122
  segments:
70
123
  - 2
71
124
  - 2
72
125
  - 0
73
126
  version: 2.2.0
74
127
  type: :runtime
75
- version_requirements: *id004
128
+ version_requirements: *id006
76
129
  description: Mediates state changes between a set of interdependent ActiveRecord objects.
77
130
  email: jpartlow@glatisant.org
78
131
  executables: []
@@ -94,10 +147,14 @@ files:
94
147
  - lib/graph_mediator/mediator.rb
95
148
  - lib/graph_mediator/version.rb
96
149
  - spec/database.rb
150
+ - spec/dependents/child.rb
151
+ - spec/dependents/parent.rb
152
+ - spec/dependents/schema.rb
97
153
  - spec/examples/course_example_spec.rb
98
154
  - spec/examples/dingo_pen_example_spec.rb
99
155
  - spec/graph_mediator_spec.rb
100
156
  - spec/integration/changes_spec.rb
157
+ - spec/integration/destroy_dependents_locking_tests_spec.rb
101
158
  - spec/integration/locking_tests_spec.rb
102
159
  - spec/integration/nesting_spec.rb
103
160
  - spec/integration/threads_spec.rb
@@ -127,16 +184,20 @@ rdoc_options:
127
184
  require_paths:
128
185
  - lib
129
186
  required_ruby_version: !ruby/object:Gem::Requirement
187
+ none: false
130
188
  requirements:
131
189
  - - ">="
132
190
  - !ruby/object:Gem::Version
191
+ hash: 3
133
192
  segments:
134
193
  - 0
135
194
  version: "0"
136
195
  required_rubygems_version: !ruby/object:Gem::Requirement
196
+ none: false
137
197
  requirements:
138
198
  - - ">="
139
199
  - !ruby/object:Gem::Version
200
+ hash: 23
140
201
  segments:
141
202
  - 1
142
203
  - 3
@@ -145,16 +206,20 @@ required_rubygems_version: !ruby/object:Gem::Requirement
145
206
  requirements: []
146
207
 
147
208
  rubyforge_project:
148
- rubygems_version: 1.3.6
209
+ rubygems_version: 1.3.7
149
210
  signing_key:
150
211
  specification_version: 3
151
212
  summary: Mediates ActiveRecord state changes
152
213
  test_files:
153
214
  - spec/database.rb
215
+ - spec/dependents/child.rb
216
+ - spec/dependents/parent.rb
217
+ - spec/dependents/schema.rb
154
218
  - spec/examples/course_example_spec.rb
155
219
  - spec/examples/dingo_pen_example_spec.rb
156
220
  - spec/graph_mediator_spec.rb
157
221
  - spec/integration/changes_spec.rb
222
+ - spec/integration/destroy_dependents_locking_tests_spec.rb
158
223
  - spec/integration/locking_tests_spec.rb
159
224
  - spec/integration/nesting_spec.rb
160
225
  - spec/integration/threads_spec.rb