Skip to content

Commit 982d6c7

Browse files
authored
Fix automation deletion deadlock by enforcing consistent lock ordering (#19369)
1 parent a597841 commit 982d6c7

File tree

2 files changed

+55
-0
lines changed

2 files changed

+55
-0
lines changed

src/prefect/server/events/models/automations.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,25 @@ async def delete_automation(
233233
if not automation:
234234
return False
235235

236+
# Delete child tables in a consistent order to prevent deadlocks
237+
# when multiple automations are deleted concurrently
238+
await session.execute(
239+
sa.delete(db.AutomationBucket).where(
240+
db.AutomationBucket.automation_id == automation_id,
241+
)
242+
)
243+
await session.execute(
244+
sa.delete(db.AutomationRelatedResource).where(
245+
db.AutomationRelatedResource.automation_id == automation_id,
246+
)
247+
)
248+
await session.execute(
249+
sa.delete(db.CompositeTriggerChildFiring).where(
250+
db.CompositeTriggerChildFiring.automation_id == automation_id,
251+
)
252+
)
253+
254+
# Now delete the parent automation
236255
await session.execute(
237256
sa.delete(db.Automation).where(
238257
db.Automation.id == automation_id,
@@ -252,6 +271,14 @@ async def delete_automations_for_workspace(
252271
automations = await read_automations_for_workspace(
253272
session,
254273
)
274+
275+
# Delete child tables in a consistent order to prevent deadlocks
276+
# when multiple workspace deletions occur concurrently
277+
await session.execute(sa.delete(db.AutomationBucket))
278+
await session.execute(sa.delete(db.AutomationRelatedResource))
279+
await session.execute(sa.delete(db.CompositeTriggerChildFiring))
280+
281+
# Now delete all automations
255282
result = await session.execute(sa.delete(db.Automation))
256283
for automation in automations:
257284
await _notify(session, automation, "deleted")

tests/test_automations.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,3 +237,31 @@ async def test_find_automation(automation: Automation):
237237
# Test finding nonexistent name returns None
238238
found = await client.find_automation("nonexistent_name")
239239
assert found is None
240+
241+
242+
async def test_concurrent_automation_deletion():
243+
"""Test that concurrent automation deletions don't deadlock."""
244+
import asyncio
245+
246+
automations_to_create = [
247+
Automation(
248+
name=f"concurrent-test-{i}",
249+
trigger=EventTrigger(
250+
expect={"prefect.flow-run.Completed"},
251+
posture=Posture.Reactive,
252+
threshold=1,
253+
),
254+
actions=[DoNothing()],
255+
)
256+
for i in range(10)
257+
]
258+
259+
created_automations = await asyncio.gather(
260+
*[automation.create() for automation in automations_to_create]
261+
)
262+
263+
await asyncio.gather(*[automation.adelete() for automation in created_automations])
264+
265+
for automation in created_automations:
266+
with pytest.raises(ValueError):
267+
await Automation.read(id=automation.id)

0 commit comments

Comments
 (0)