Skip to content

Commit a182209

Browse files
fix: add unique constraint on conference to prevent duplicate split (#21)
* fix: add unique constraint on conference (conference_id, app) to prevent duplicates When two participants join the same session simultaneously, both call POST /v1/initialize with the same conferenceId. Without a unique constraint, both get_or_create calls create separate conference records (race condition). This splits participants across different conference pages. Fix: - Migration 0002: merges existing duplicates (keeps oldest, moves all related events/sessions/connections/issues to it) - Migration 0003: adds unique_together on (conference_id, app) After this, concurrent get_or_create calls will correctly find the existing conference instead of creating duplicates. * fix: address review feedback on conference unique constraint migration - Moved imports to top of file/function - Wrapped each duplicate group in transaction.atomic() - Added Summary model to reassignment (OneToOne FK to Conference) - Added safety check on dup.delete() to catch unexpected cascades * fix: address P1 and P2 review feedback on migration P1: Removed over-strict cascade-delete guard. Deleting a duplicate conference normally cascades to M2M join table rows (participant through table), which is expected. The guard was raising on this. P2: Replaced hasattr(keeper, 'summary') with a local boolean flag (keeper_has_summary) that updates after the first summary is moved. Avoids stale cached reverse OneToOne lookups across iterations.
1 parent b5ad74e commit a182209

3 files changed

Lines changed: 83 additions & 0 deletions

File tree

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# Generated by Django 2.2.28 on 2026-04-09 13:49
2+
3+
from django.db import migrations
4+
from django.db.models import Count, Min
5+
6+
7+
def merge_duplicate_conferences(apps, schema_editor):
8+
"""
9+
Before adding the unique constraint, merge duplicate conferences
10+
(same conference_id + app_id). Keep the oldest one, move all
11+
related data to it, and delete the rest.
12+
"""
13+
from django.db import transaction
14+
15+
Conference = apps.get_model('app', 'Conference')
16+
GenericEvent = apps.get_model('app', 'GenericEvent')
17+
Connection = apps.get_model('app', 'Connection')
18+
Session = apps.get_model('app', 'Session')
19+
Issue = apps.get_model('app', 'Issue')
20+
Summary = apps.get_model('app', 'Summary')
21+
22+
dupes = (Conference.objects
23+
.values('conference_id', 'app_id')
24+
.annotate(count=Count('id'), earliest=Min('created_at'))
25+
.filter(count__gt=1))
26+
27+
for dupe in dupes:
28+
with transaction.atomic():
29+
conferences = Conference.objects.filter(
30+
conference_id=dupe['conference_id'],
31+
app_id=dupe['app_id'],
32+
).order_by('created_at')
33+
34+
keeper = conferences.first()
35+
duplicates = conferences.exclude(pk=keeper.pk)
36+
keeper_has_summary = Summary.objects.filter(conference=keeper).exists()
37+
38+
for dup in duplicates:
39+
GenericEvent.objects.filter(conference=dup).update(conference=keeper)
40+
Connection.objects.filter(conference=dup).update(conference=keeper)
41+
Session.objects.filter(conference=dup).update(conference=keeper)
42+
Issue.objects.filter(conference=dup).update(conference=keeper)
43+
44+
# Summary is OneToOne — keep the first one, delete the rest
45+
if not keeper_has_summary:
46+
Summary.objects.filter(conference=dup).update(conference=keeper)
47+
keeper_has_summary = True
48+
else:
49+
Summary.objects.filter(conference=dup).delete()
50+
51+
for participant in dup.participants.all():
52+
keeper.participants.add(participant)
53+
54+
dup.delete()
55+
56+
57+
class Migration(migrations.Migration):
58+
59+
dependencies = [
60+
('app', '0001_initial'),
61+
]
62+
63+
operations = [
64+
migrations.RunPython(merge_duplicate_conferences, migrations.RunPython.noop),
65+
]
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Generated by Django 2.2.28 on 2026-04-09 13:49
2+
3+
from django.db import migrations
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('app', '0002_conference_unique_conference_id_app'),
10+
]
11+
12+
operations = [
13+
migrations.AlterUniqueTogether(
14+
name='conference',
15+
unique_together={('conference_id', 'app')},
16+
),
17+
]

app/models/conference.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ class Conference(BaseModel):
3737

3838
class Meta:
3939
db_table = 'conference'
40+
unique_together = (('conference_id', 'app'),)
4041

4142
cache_keys = (
4243
sorted(('id',)),

0 commit comments

Comments
 (0)