Skip to content

Commit d18ec8b

Browse files
committed
MONGOID-5836 Don't repeat callbacks for embedded grandchildren. (mongodb#5933)
* MONGOID-5836 don't explicitly process grandchildren Grandchildren are already accounted for when this is invoked, because if called when callbacks are cascading, the input is the full list of all children and grandchildren. Processing grandchildren recursively here causes them to be processed twice. * specify python version to address failing mongo-orchestration * make sure we don't break update callbacks
1 parent c3a9ac9 commit d18ec8b

File tree

4 files changed

+138
-116
lines changed

4 files changed

+138
-116
lines changed

.github/workflows/test.yml

+7
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,13 @@ jobs:
184184
uses: actions/checkout@v2
185185
with:
186186
submodules: recursive
187+
188+
# the default python 3.8 doesn't cut it, and causes mongo-orchestration
189+
# to fail in mongodb-labs/drivers-evergreen-tools.
190+
- uses: actions/setup-python@v5
191+
with:
192+
python-version: '3.13'
193+
187194
- id: start-mongodb
188195
name: start mongodb
189196
uses: mongodb-labs/drivers-evergreen-tools@master

lib/mongoid/interceptable.rb

+6-5
Original file line numberDiff line numberDiff line change
@@ -141,9 +141,13 @@ def run_callbacks(kind, with_children: true, &block)
141141
# @api private
142142
def _mongoid_run_child_callbacks(kind, children: nil, &block)
143143
if Mongoid::Config.around_callbacks_for_embeds
144-
_mongoid_run_child_callbacks_with_around(kind, children: children, &block)
144+
_mongoid_run_child_callbacks_with_around(kind,
145+
children: children,
146+
&block)
145147
else
146-
_mongoid_run_child_callbacks_without_around(kind, children: children, &block)
148+
_mongoid_run_child_callbacks_without_around(kind,
149+
children: children,
150+
&block)
147151
end
148152
end
149153

@@ -219,9 +223,6 @@ def _mongoid_run_child_before_callbacks(kind, children: [], callback_list: [])
219223
return false if env.halted
220224
env.value = !env.halted
221225
callback_list << [next_sequence, env]
222-
if (grandchildren = child.send(:cascadable_children, kind))
223-
_mongoid_run_child_before_callbacks(kind, children: grandchildren, callback_list: callback_list)
224-
end
225226
end
226227
callback_list
227228
end

spec/mongoid/interceptable_spec.rb

+78
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,84 @@ class TestClass
388388
end
389389
end
390390
end
391+
392+
context 'with embedded grandchildren' do
393+
IS = InterceptableSpec
394+
395+
context 'when creating' do
396+
let(:registry) { IS::CallbackRegistry.new(only: %i[ before_save ]) }
397+
398+
let(:expected_calls) do
399+
[
400+
# the parent
401+
[ IS::CbParent, :before_save ],
402+
403+
# the immediate child of the parent
404+
[ IS::CbCascadedNode, :before_save ],
405+
406+
# the grandchild of the parent
407+
[ IS::CbCascadedNode, :before_save ],
408+
]
409+
end
410+
411+
let!(:parent) do
412+
parent = IS::CbParent.new(registry)
413+
child = IS::CbCascadedNode.new(registry)
414+
grandchild = IS::CbCascadedNode.new(registry)
415+
416+
child.cb_cascaded_nodes = [ grandchild ]
417+
parent.cb_cascaded_nodes = [ child ]
418+
419+
parent.tap(&:save)
420+
end
421+
422+
it 'should cascade callbacks to grandchildren' do
423+
expect(registry.calls).to be == expected_calls
424+
end
425+
end
426+
427+
context 'when updating' do
428+
let(:registry) { IS::CallbackRegistry.new(only: %i[ before_update ]) }
429+
430+
let(:expected_calls) do
431+
[
432+
# the parent
433+
[ IS::CbParent, :before_update ],
434+
435+
# the immediate child of the parent
436+
[ IS::CbCascadedNode, :before_update ],
437+
438+
# the grandchild of the parent
439+
[ IS::CbCascadedNode, :before_update ],
440+
]
441+
end
442+
443+
let!(:parent) do
444+
parent = IS::CbParent.new(nil)
445+
child = IS::CbCascadedNode.new(nil)
446+
grandchild = IS::CbCascadedNode.new(nil)
447+
448+
child.cb_cascaded_nodes = [ grandchild ]
449+
parent.cb_cascaded_nodes = [ child ]
450+
451+
parent.save
452+
453+
parent.callback_registry = registry
454+
child.callback_registry = registry
455+
grandchild.callback_registry = registry
456+
457+
parent.name = 'updated'
458+
child.name = 'updated'
459+
grandchild.name = 'updated'
460+
461+
parent.tap(&:save)
462+
end
463+
464+
it 'should cascade callbacks to grandchildren' do
465+
expect(registry.calls).to be == expected_calls
466+
end
467+
end
468+
end
391469
end
392470

393471
describe ".before_destroy" do

spec/mongoid/interceptable_spec_models.rb

+47-111
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
module InterceptableSpec
22
class CallbackRegistry
3-
def initialize
3+
def initialize(only: [])
44
@calls = []
5+
@only = only
56
end
67

78
def record_call(cls, cb)
9+
return unless @only.empty? || @only.any? { |pat| pat == cb }
810
@calls << [cls, cb]
911
end
1012

@@ -15,6 +17,8 @@ module CallbackTracking
1517
extend ActiveSupport::Concern
1618

1719
included do
20+
field :name, type: String
21+
1822
%i(
1923
validation save create update
2024
).each do |what|
@@ -34,196 +38,128 @@ module CallbackTracking
3438
end
3539
end
3640
end
37-
end
3841

39-
class CbHasOneParent
40-
include Mongoid::Document
41-
42-
has_one :child, autosave: true, class_name: "CbHasOneChild", inverse_of: :parent
42+
attr_accessor :callback_registry
4343

44-
def initialize(callback_registry)
44+
def initialize(callback_registry, *args, **kwargs)
4545
@callback_registry = callback_registry
46-
super()
46+
super(*args, **kwargs)
4747
end
48+
end
4849

49-
attr_accessor :callback_registry
50-
50+
module RootInsertable
5151
def insert_as_root
5252
@callback_registry&.record_call(self.class, :insert_into_database)
5353
super
5454
end
55+
end
5556

57+
class CbHasOneParent
58+
include Mongoid::Document
5659
include CallbackTracking
60+
include RootInsertable
61+
62+
has_one :child, autosave: true, class_name: "CbHasOneChild", inverse_of: :parent
5763
end
5864

5965
class CbHasOneChild
6066
include Mongoid::Document
67+
include CallbackTracking
6168

6269
belongs_to :parent, class_name: "CbHasOneParent", inverse_of: :child
63-
64-
def initialize(callback_registry)
65-
@callback_registry = callback_registry
66-
super()
67-
end
68-
69-
attr_accessor :callback_registry
70-
71-
include CallbackTracking
7270
end
7371

7472
class CbHasManyParent
7573
include Mongoid::Document
74+
include CallbackTracking
75+
include RootInsertable
7676

7777
has_many :children, autosave: true, class_name: "CbHasManyChild", inverse_of: :parent
78-
79-
def initialize(callback_registry)
80-
@callback_registry = callback_registry
81-
super()
82-
end
83-
84-
attr_accessor :callback_registry
85-
86-
def insert_as_root
87-
@callback_registry&.record_call(self.class, :insert_into_database)
88-
super
89-
end
90-
91-
include CallbackTracking
9278
end
9379

9480
class CbHasManyChild
9581
include Mongoid::Document
82+
include CallbackTracking
9683

9784
belongs_to :parent, class_name: "CbHasManyParent", inverse_of: :children
98-
99-
def initialize(callback_registry)
100-
@callback_registry = callback_registry
101-
super()
102-
end
103-
104-
attr_accessor :callback_registry
105-
106-
include CallbackTracking
10785
end
10886

10987
class CbEmbedsOneParent
11088
include Mongoid::Document
89+
include CallbackTracking
90+
include RootInsertable
11191

11292
field :name
11393

11494
embeds_one :child, cascade_callbacks: true, class_name: "CbEmbedsOneChild", inverse_of: :parent
115-
116-
def initialize(callback_registry)
117-
@callback_registry = callback_registry
118-
super()
119-
end
120-
121-
attr_accessor :callback_registry
122-
123-
def insert_as_root
124-
@callback_registry&.record_call(self.class, :insert_into_database)
125-
super
126-
end
127-
128-
include CallbackTracking
12995
end
13096

13197
class CbEmbedsOneChild
13298
include Mongoid::Document
99+
include CallbackTracking
133100

134101
field :age
135102

136103
embedded_in :parent, class_name: "CbEmbedsOneParent", inverse_of: :child
137-
138-
def initialize(callback_registry)
139-
@callback_registry = callback_registry
140-
super()
141-
end
142-
143-
attr_accessor :callback_registry
144-
145-
include CallbackTracking
146104
end
147105

148106
class CbEmbedsManyParent
149107
include Mongoid::Document
108+
include CallbackTracking
109+
include RootInsertable
150110

151111
embeds_many :children, cascade_callbacks: true, class_name: "CbEmbedsManyChild", inverse_of: :parent
152-
153-
def initialize(callback_registry)
154-
@callback_registry = callback_registry
155-
super()
156-
end
157-
158-
attr_accessor :callback_registry
159-
160-
def insert_as_root
161-
@callback_registry&.record_call(self.class, :insert_into_database)
162-
super
163-
end
164-
165-
include CallbackTracking
166112
end
167113

168114
class CbEmbedsManyChild
169115
include Mongoid::Document
116+
include CallbackTracking
170117

171118
embedded_in :parent, class_name: "CbEmbedsManyParent", inverse_of: :children
172-
173-
def initialize(callback_registry)
174-
@callback_registry = callback_registry
175-
super()
176-
end
177-
178-
attr_accessor :callback_registry
179-
180-
include CallbackTracking
181119
end
182120

183121
class CbParent
184122
include Mongoid::Document
185-
186-
def initialize(callback_registry)
187-
@callback_registry = callback_registry
188-
super()
189-
end
190-
191-
attr_accessor :callback_registry
123+
include CallbackTracking
192124

193125
embeds_many :cb_children
194126
embeds_many :cb_cascaded_children, cascade_callbacks: true
195-
196-
include CallbackTracking
127+
embeds_many :cb_cascaded_nodes, cascade_callbacks: true, as: :parent
197128
end
198129

199130
class CbChild
200131
include Mongoid::Document
132+
include CallbackTracking
201133

202134
embedded_in :cb_parent
203-
204-
def initialize(callback_registry, options)
205-
@callback_registry = callback_registry
206-
super(options)
207-
end
208-
209-
attr_accessor :callback_registry
210-
211-
include CallbackTracking
212135
end
213136

214137
class CbCascadedChild
215138
include Mongoid::Document
139+
include CallbackTracking
216140

217141
embedded_in :cb_parent
218142

219-
def initialize(callback_registry, options)
220-
@callback_registry = callback_registry
221-
super(options)
222-
end
143+
before_save :test_mongoid_state
223144

224-
attr_accessor :callback_registry
145+
private
146+
147+
# Helps test that cascading child callbacks have access to the Mongoid
148+
# state objects; if the implementation uses fiber-local (instead of truly
149+
# thread-local) variables, the related tests will fail because the
150+
# cascading child callbacks use fibers to linearize the recursion.
151+
def test_mongoid_state
152+
Mongoid::Threaded.stack('interceptable').push(self)
153+
end
154+
end
225155

156+
class CbCascadedNode
157+
include Mongoid::Document
226158
include CallbackTracking
159+
160+
embedded_in :parent, polymorphic: true
161+
162+
embeds_many :cb_cascaded_nodes, cascade_callbacks: true, as: :parent
227163
end
228164
end
229165

0 commit comments

Comments
 (0)