Skip to content

Commit 3558907

Browse files
rjarrystephenfin
authored andcommitted
parsemail: fix SeriesReference race with concurrent delivery
When multiple parsemail processes run in parallel (e.g. postfix delivering several messages from the same series at once), two processes can try to create a SeriesReference for the same msgid simultaneously. The second one fails with an IntegrityError: django.db.utils.IntegrityError: duplicate key value violates unique constraint "patchwork_seriesreference_project_id_msgid_..." DETAIL: Key (project_id, msgid)=(2, <...>) already exists. This can result in incomplete series that never reach the "received_all" state because the failed parsemail invocation prevents one of the patches from being recorded. The existing get/create pattern has a classic TOCTOU race: the get succeeds (no reference found), but by the time create runs, another process has already inserted the row. Replace both the try/get/ except/create block and the bare create call with get_or_create which handles the race atomically at the database level. Signed-off-by: Robin Jarry <robin@jarry.cc> Reviwed-by: Stephen Finucane <stephen@that.guru>
1 parent 66e9108 commit 3558907

2 files changed

Lines changed: 19 additions & 16 deletions

File tree

‎patchwork/parser.py‎

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1340,20 +1340,18 @@ def parse_mail(mail, list_id=None):
13401340
# later one.
13411341
for ref in refs + [msgid]:
13421342
ref = ref[:255]
1343-
# we don't want duplicates
1344-
try:
1345-
# we could have a ref to a previous series.
1346-
# (For example, a series sent in reply to
1347-
# another series.) That should not create a
1348-
# series ref for this series, so check for the
1349-
# msg-id only, not the msg-id/series pair.
1350-
SeriesReference.objects.get(
1351-
msgid=ref, project=project
1352-
)
1353-
except SeriesReference.DoesNotExist:
1354-
SeriesReference.objects.create(
1355-
msgid=ref, project=project, series=series
1356-
)
1343+
# We could have a ref to a previous series. (For
1344+
# example, a series sent in reply to another
1345+
# series.) That should not create a series ref for
1346+
# this series, so check for the msg-id only, not
1347+
# the msg-id/series pair. Use get_or_create to
1348+
# avoid races when multiple parsemail processes run
1349+
# in parallel.
1350+
SeriesReference.objects.get_or_create(
1351+
msgid=ref,
1352+
project=project,
1353+
defaults={'series': series},
1354+
)
13571355

13581356
# attempt to pull the series in again, raising an
13591357
# exception if we lost the race when creating a series
@@ -1440,8 +1438,8 @@ def parse_mail(mail, list_id=None):
14401438
# we don't save the in-reply-to or references fields
14411439
# for a cover letter, as they can't refer to the same
14421440
# series
1443-
SeriesReference.objects.create(
1444-
msgid=msgid, project=project, series=series
1441+
SeriesReference.objects.get_or_create(
1442+
msgid=msgid, project=project, defaults={'series': series}
14451443
)
14461444

14471445
with transaction.atomic():
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
fixes:
3+
- |
4+
Fix a race condition in SeriesReference creation during concurrent
5+
email delivery.

0 commit comments

Comments
 (0)