graph_mediator 0.2.2 → 0.2.3

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/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