-
Notifications
You must be signed in to change notification settings - Fork 5.3k
fix: prevent duplicate in-app notifications on task retry #9614
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: preview
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -187,6 +187,21 @@ def create_mention_notification(project, notification_comment, issue, actor_id, | |
| ) | ||
|
|
||
|
|
||
| def _notification_dedup_key(notification): | ||
| """ | ||
| Build a key that identifies a "logically the same" notification so we can | ||
| dedupe both within a single task run and against notifications already | ||
| persisted by a prior (retried/redelivered) execution of this same task. | ||
| """ | ||
| issue_activity = (notification.data or {}).get("issue_activity", {}) or {} | ||
| return ( | ||
| notification.receiver_id, | ||
| notification.sender, | ||
| notification.entity_identifier, | ||
| issue_activity.get("id"), | ||
| ) | ||
|
|
||
|
|
||
| @shared_task | ||
| def notifications( | ||
| type, | ||
|
|
@@ -665,10 +680,50 @@ def notifications( | |
| new_mentions=new_mentions, | ||
| removed_mention=removed_mention, | ||
| ) | ||
|
|
||
| # --- Deduplicate notifications before bulk_create ----------------------------------------- | ||
| # Guards against duplicate in-app notifications when this task executes more than once | ||
| # for the same event (Celery retries, broker redelivery, worker restarts). This is an | ||
| # app-level (non-atomic) safeguard - see notification.py for the alternative DB-constraint | ||
| # based approach, which is race-safe and preferred long-term. | ||
|
|
||
| # 1. Dedupe within this run's own batch (the code above can independently | ||
| # append near-identical notifications for the same receiver/activity, | ||
| # e.g. via both the general subscriber loop and the mention loops). | ||
| seen_in_batch = set() | ||
| deduped_batch = [] | ||
| for notification in bulk_notifications: | ||
| key = _notification_dedup_key(notification) | ||
| if key in seen_in_batch: | ||
| continue | ||
| seen_in_batch.add(key) | ||
| deduped_batch.append(notification) | ||
|
|
||
| # 2. Dedupe against notifications already persisted for this issue | ||
| # (covers retries / redelivery of the same task execution). | ||
| if deduped_batch: | ||
| receiver_ids = {notification.receiver_id for notification in deduped_batch} | ||
| existing_keys = set( | ||
| Notification.objects.filter( | ||
| entity_identifier=issue_id, | ||
| receiver_id__in=receiver_ids, | ||
| ).values_list( | ||
| "receiver_id", "sender", "entity_identifier", "data__issue_activity__id" | ||
| ) | ||
| ) | ||
| final_notifications = [ | ||
| notification | ||
| for notification in deduped_batch | ||
| if _notification_dedup_key(notification) not in existing_keys | ||
| ] | ||
| else: | ||
| final_notifications = [] | ||
|
|
||
| # Bulk create notifications | ||
| Notification.objects.bulk_create(bulk_notifications, batch_size=100) | ||
| if final_notifications: | ||
| Notification.objects.bulk_create(final_notifications, batch_size=100) | ||
|
Comment on lines
+702
to
+724
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- target function context ---'
sed -n '650,745p' apps/api/plane/bgtasks/notification_task.py
printf '%s\n' '--- Notification model definitions and constraints ---'
rg -n -C 4 'class Notification|UniqueConstraint|unique_together|entity_identifier|issue_activity' apps/api/plane | head -n 240
printf '%s\n' '--- notification creation paths ---'
rg -n -C 3 'Notification\.objects\.(create|bulk_create)|Notification\(' apps/api/plane | head -n 260Repository: makeplane/plane Length of output: 29485 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- Notification model ---'
cat -n apps/api/plane/db/models/notification.py
printf '%s\n' '--- referenced alternative and migrations ---'
fd -i 'notification.py' apps/api
rg -n -C 5 'Notification|notification_dedup|dedup_key|UniqueConstraint|unique_together' apps/api/plane/db/migrations apps/api/plane/db/models apps/api/plane/bgtasks/notification_task.py
printf '%s\n' '--- complete notification construction regions ---'
sed -n '145,215p' apps/api/plane/bgtasks/notification_task.py
sed -n '360,420p' apps/api/plane/bgtasks/notification_task.py
sed -n '525,575p' apps/api/plane/bgtasks/notification_task.pyRepository: makeplane/plane Length of output: 50372 🏁 Script executed: #!/bin/bash
set -eu
log=/tmp/coderabbit-shell-logs/shell-output-UrrPT9
printf '%s\n' '--- model and focused search output ---'
sed -n '1,220p' "$log"
printf '%s\n' '--- exact Notification references excluding migrations ---'
rg -n -C 3 'class Notification|Notification\.objects|Notification\(' apps/api/plane/db/models apps/api/plane/bgtasks apps/api/plane/api --glob '!migrations/**' --glob '*.py'
printf '%s\n' '--- exact notification migration references ---'
rg -n 'model_name="notification"|name="notification"|Notification' apps/api/plane/db/migrations | tail -n 80Repository: makeplane/plane Length of output: 17845 Make persisted notification deduplication race-safe. The pre-query and Add an immutable event-key field, populate it in every notification construction path, and add a migration with a database uniqueness constraint for the canonical notification identity. Then use 🤖 Prompt for AI AgentsSource: Learnings |
||
| EmailNotificationLog.objects.bulk_create(bulk_email_logs, batch_size=100, ignore_conflicts=True) | ||
| return | ||
| except Exception as e: | ||
| print(e) | ||
| return | ||
| return | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: makeplane/plane
Length of output: 50371
🏁 Script executed:
Repository: makeplane/plane
Length of output: 25619
🏁 Script executed:
Repository: makeplane/plane
Length of output: 20219
Canonicalize UUID values before notification deduplication.
Mention notifications use string
mention_idandissue_id, whileexisting_keyscan contain UUID objects. The tuples can differ, so retries can insert duplicate notifications. Normalizereceiver_id,entity_identifier, and the activity ID in both key paths. Add a regression test for a mention retry.🤖 Prompt for AI Agents