Skip to content

feat(notifications): add EDA-based idempotent processing and DLQ for chapter/event updates#4454

Open
Shofikul-Isl4m wants to merge 24 commits intoOWASP:mainfrom
Shofikul-Isl4m:feature/entity-subscriptions
Open

feat(notifications): add EDA-based idempotent processing and DLQ for chapter/event updates#4454
Shofikul-Isl4m wants to merge 24 commits intoOWASP:mainfrom
Shofikul-Isl4m:feature/entity-subscriptions

Conversation

@Shofikul-Isl4m
Copy link
Copy Markdown
Contributor

Proposed change

This PR depends on #3701 please rebase before reviewing/merging.
Resolves #3900
Implements a robust Event-Driven Architecture (EDA) notification system with idempotent processing and manual DLQ management.

Screenshot from 2026-03-10 08-36-07

This implementation uses an event-driven approach:

  1. Entity changes (create/update) → emit events to Redis Stream

  2. Worker consumes events asynchronously → processes notifications

  3. On failure → exponential backoff retry (5 attempts)

  4. Still failing → stored in DLQ for manual handling

  5. Idempotency ensures no duplicate notifications

Changes

  1. Deadline Reminder Emails (Events)
  • Add days_remaining in Redis message and email content (7, 3, 1 days left)
  1. Meaningful Change Detection (Chapter & Event)
  • Added pre_save signals to capture previous values before save
  • Detect changes in meaningful fields:
    • Event: name, start_date, end_date, suggested_location, url, description
    • Chapter: name, country, region, suggested_location, description
  • Format changes in email: "Start Date: 2026-04-01 → 2026-06-20"
  • Only send notification if meaningful fields change
  1. Idempotent Processing
  • Message-based deduplication using Notification table
  • Different updates (different changes) → sends notification
  • Same message reprocessed → finds Notification → skips
  • Prevents duplicate emails on worker restart/crash
  1. Manual DLQ Management
  • Disabled automatic DLQ processing (best practice)
  • Created owasp_notification_dlq management command:
    • list - Show all failed notifications
    • retry - Retry failed notifications
    • remove - Remove failed notifications from DLQ

Checklist

  • Required: I followed the contributing workflow
  • Required: I verified that my code works as intended and resolves the issue as described
  • Required: I ran make check-test locally: all warnings addressed, tests passed
  • I used AI for code, documentation, tests, or communication related to this PR

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 1, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds entity subscription and notification features: new Subscription and Notification models with migrations, signal handlers that enqueue Redis-stream notifications for snapshots/chapters/events, a Redis-backed worker with retry/DLQ logic, management commands (worker, deadline checker, DLQ manager), admin registrations, settings, cron entry, and tests.

Changes

Cohort / File(s) Summary
Makefile Targets
backend/Makefile, backend/apps/owasp/Makefile
Added run-notification-worker and owasp-check-event-deadlines Make targets to run new management commands (interactive and non-interactive).
Admin & App Init
backend/apps/owasp/admin/notification.py, backend/apps/owasp/apps.py
Registered Notification and Subscription in Django admin; added OwaspConfig.ready() to import signal modules on app startup.
Models & Migrations
backend/apps/owasp/models/notification.py, backend/apps/owasp/models/__init__.py, backend/apps/owasp/migrations/0073_notification_subscription.py, .../0074_remove_subscription_owasp_subsc_user_id_33ae6d_idx_and_more.py, .../0075_alter_subscription_object_id.py
Added Notification and Subscription models, exported them, and included migrations to create models, remove an index, alter related_link default, and change object_id to PositiveBigIntegerField.
Signals
backend/apps/owasp/signals/chapter.py, backend/apps/owasp/signals/event.py, backend/apps/owasp/signals/snapshot.py
Added pre_save/post_save handlers capturing previous state and publishing notification payloads (created/updated/published) via transaction.on_commit.
Notification Utilities
backend/apps/owasp/utils/notifications.py
Added Redis stream publishers (STREAM_KEY="owasp_notifications") and send_notification which deduplicates, sends email, and persists Notification rows.
Worker & DLQ Commands
backend/apps/owasp/management/commands/owasp_run_notification_worker.py, backend/apps/owasp/management/commands/owasp_notification_dlq.py, backend/apps/owasp/management/commands/owasp_check_event_deadlines.py
Added long-running notification worker with consumer-group recovery, per-message routing, retry/backoff and DLQ writes; DLQ management command (list/retry/remove); and deadline-reminder command that queues deadline notifications.
Settings
backend/settings/base.py
Added Base.EMAIL_BACKEND env-driven setting (default: console backend).
Tests
backend/tests/apps/owasp/...
Added unit tests for signals, publish utilities, management commands, and worker behaviors (routing, recovery, DLQ, subscriber selection, deadline reminders).
Cron & Spellcheck
cron/production, cspell/custom-dict.txt
Added cron entry to run the deadline checker daily and extended cspell dictionary with Redis/stream terms (DLQ, xack, xautoclaim, etc.).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested labels

nestbot

Suggested reviewers

  • kasya
  • arkid15r
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'feat(notifications): add EDA-based idempotent processing and DLQ for chapter/event updates' clearly and concisely describes the main change: implementing an event-driven architecture for notifications with idempotent processing and dead letter queue management.
Description check ✅ Passed The PR description is comprehensive and directly related to the changeset, covering proposed changes, implementation details, dependencies, and a completed checklist.
Linked Issues check ✅ Passed All key requirements from issue #3900 are met: Signal handlers for Chapter/Event detect create vs update, deadline reminders at 7/3/1 days via management command and cron, Redis Stream queueing, worker with retry/DLQ support, idempotent processing, and comprehensive tests.
Out of Scope Changes check ✅ Passed All changes are directly aligned with implementing entity-level subscriptions and notifications. Additional items like cron scheduling, email backend configuration, and DLQ management command are necessary supporting implementations within scope.
Docstring Coverage ✅ Passed Docstring coverage is 86.76% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

15 issues found across 26 files

Confidence score: 2/5

  • There is clear reliability risk in backend/apps/owasp/management/commands/owasp_run_notification_worker.py: broad exception swallowing can acknowledge failed events, causing permanent event loss (high severity/high confidence).
  • Multiple concrete duplicate/loss paths exist across notification flow (owasp_run_notification_worker.py, owasp_notification_dlq.py, and migrations/0073_notification_subscription.py): non-atomic idempotency, missing DB uniqueness, and DLQ retry behavior can resend emails or drop retry records.
  • post_save emission before commit in backend/apps/owasp/signals/snapshot.py and backend/apps/owasp/signals/event.py can publish phantom notifications for rolled-back transactions, creating user-visible inconsistency.
  • Pay close attention to backend/apps/owasp/management/commands/owasp_run_notification_worker.py, backend/apps/owasp/management/commands/owasp_notification_dlq.py, backend/apps/owasp/signals/snapshot.py, backend/apps/owasp/signals/event.py, backend/apps/owasp/migrations/0073_notification_subscription.py, backend/settings/base.py - notification delivery, dedupe, and transaction-ordering issues are the primary merge risks.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="backend/apps/owasp/signals/snapshot.py">

<violation number="1" location="backend/apps/owasp/signals/snapshot.py:27">
P1: `post_save` triggers external notification before transaction commit, which can publish phantom events when the surrounding DB transaction rolls back.</violation>
</file>

<file name="backend/tests/apps/owasp/utils/notifications_test.py">

<violation number="1" location="backend/tests/apps/owasp/utils/notifications_test.py:120">
P2: Deadline reminder publisher test misses `days_remaining` assertion, so regressions in required reminder payload serialization can pass undetected.</violation>
</file>

<file name="backend/apps/owasp/management/commands/owasp_run_notification_worker.py">

<violation number="1" location="backend/apps/owasp/management/commands/owasp_run_notification_worker.py:67">
P1: Broad exception swallowing in entity handler causes failed events to be acknowledged and dropped from the stream.</violation>

<violation number="2" location="backend/apps/owasp/management/commands/owasp_run_notification_worker.py:68">
P2: Main-loop processing failures are only logged, leaving stream entries pending (unacked and not DLQ’d), which can stall messages until a later recovery/restart.</violation>

<violation number="3" location="backend/apps/owasp/management/commands/owasp_run_notification_worker.py:168">
P1: Idempotency is non-atomic: email is sent before dedup state is persisted, allowing duplicate sends on crash/partial failure or replay.</violation>
</file>

<file name="backend/tests/apps/owasp/signals/event_test.py">

<violation number="1" location="backend/tests/apps/owasp/signals/event_test.py:19">
P2: Test name/docstring claim updated-notification publish coverage, but assertions validate the no-change/no-publish path instead.</violation>
</file>

<file name="backend/apps/owasp/migrations/0073_notification_subscription.py">

<violation number="1" location="backend/apps/owasp/migrations/0073_notification_subscription.py:40">
P1: `Notification` lacks a database-level idempotency/uniqueness constraint, so current read-then-insert dedupe is race-prone and can create duplicates under concurrent processing.</violation>

<violation number="2" location="backend/apps/owasp/migrations/0073_notification_subscription.py:54">
P2: `Subscription.object_id` is too narrow (`PositiveIntegerField`) for bigint primary keys and can overflow for polymorphic targets.</violation>
</file>

<file name="backend/apps/owasp/management/commands/owasp_notification_dlq.py">

<violation number="1" location="backend/apps/owasp/management/commands/owasp_notification_dlq.py:113">
P1: DLQ retry bypasses the Notification idempotency guard, so repeated/manual retries can send duplicate emails.</violation>

<violation number="2" location="backend/apps/owasp/management/commands/owasp_notification_dlq.py:120">
P1: Retry error handling can lose DLQ messages because it deletes the old entry before successfully adding the updated one.</violation>
</file>

<file name="backend/tests/apps/owasp/signals/chapter_test.py">

<violation number="1" location="backend/tests/apps/owasp/signals/chapter_test.py:19">
P2: The test named as verifying updated notifications actually tests the no-change branch, leaving meaningful-change update notification behavior uncovered.</violation>
</file>

<file name="backend/Makefile">

<violation number="1" location="backend/Makefile:160">
P2: New notification worker target runs through an interactive exec helper that suppresses stderr, which can hide worker failures and break non-interactive execution.</violation>
</file>

<file name="backend/settings/base.py">

<violation number="1" location="backend/settings/base.py:234">
P1: Global `Base` settings default email backend to console output, so missing `EMAIL_BACKEND` in staging/production silently disables real email delivery.</violation>
</file>

<file name="backend/apps/owasp/signals/event.py">

<violation number="1" location="backend/apps/owasp/signals/event.py:36">
P1: Event notifications are emitted in `post_save` before transaction commit, so rolled-back DB writes can still produce Redis events.</violation>

<violation number="2" location="backend/apps/owasp/signals/event.py:46">
P2: Changed-field serialization uses truthiness and incorrectly converts empty-string values to `None`, causing inaccurate update diff payloads.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (7)
backend/settings/base.py (1)

232-235: Remove redundant environ_name parameter.

When the setting name matches the environ_name value, the parameter is redundant. values.Value() will automatically use the setting name (EMAIL_BACKEND) as the environment variable key (with DJANGO_ prefix applied).

♻️ Suggested fix
-    EMAIL_BACKEND = values.Value(
-        environ_name="EMAIL_BACKEND", default="django.core.mail.backends.console.EmailBackend"
-    )
+    EMAIL_BACKEND = values.Value(default="django.core.mail.backends.console.EmailBackend")

Based on learnings: "when a setting name matches the environ_name value, the environ_name parameter is redundant."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/settings/base.py` around lines 232 - 235, The EMAIL_BACKEND setting
currently passes an explicit environ_name to values.Value redundantly; update
the EMAIL_BACKEND declaration to call values.Value(...) without the environ_name
argument so it relies on the default behavior (values.Value uses the setting
name as the env key), keeping the same default
"django.core.mail.backends.console.EmailBackend" and the values.Value call
otherwise unchanged.
backend/apps/owasp/apps.py (1)

11-15: Consider adding noqa: F401 to all signal imports for consistency.

All three signal modules are imported purely for side effects (registering @receiver decorators). Currently only snapshot has the suppression comment.

♻️ Suggested fix
     def ready(self):
         """Import signals when app is ready."""
-        import apps.owasp.signals.chapter
-        import apps.owasp.signals.event
+        import apps.owasp.signals.chapter  # noqa: F401
+        import apps.owasp.signals.event  # noqa: F401
         import apps.owasp.signals.snapshot  # noqa: F401
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/apps/owasp/apps.py` around lines 11 - 15, The ready() method imports
signal modules for side effects but only apps.owasp.signals.snapshot has the
noqa suppression; add "# noqa: F401" to the other two imports
(apps.owasp.signals.chapter and apps.owasp.signals.event) so all three
side-effect-only imports in ready() are consistently ignored by the
unused-import linter.
backend/tests/apps/owasp/signals/event_test.py (1)

18-40: Misleading docstring - test verifies no notification when no fields change.

The docstring says "Test that updating an event publishes an 'updated' notification" but the test actually verifies the opposite: when no meaningful fields change, no notification is published. Consider:

  1. Fixing the docstring to match the actual behavior being tested
  2. Adding a separate test for when fields DO change and notification IS published
♻️ Suggested fix and additional test
     `@patch`("apps.owasp.signals.event.publish_event_notification")
-    def test_event_updated_publishes_updated_notification(self, mock_publish):
-        """Test that updating an event publishes an 'updated' notification."""
+    def test_event_updated_no_changes_does_not_publish(self, mock_publish):
+        """Test that updating an event without meaningful changes does not publish."""
         event = MagicMock()
-        # Set up previous values that match current values - no changes, no notification
+        # Set up previous values that match current values
         event._previous_values = {

Add a test for actual changes:

`@patch`("apps.owasp.signals.event.publish_event_notification")
def test_event_updated_with_changes_publishes_notification(self, mock_publish):
    """Test that updating an event with meaningful changes publishes notification."""
    event = MagicMock()
    event._previous_values = {"name": "Old Name", ...}
    event.name = "New Name"
    # ... other fields same as previous
    event_post_save(sender=None, instance=event, created=False)
    mock_publish.assert_called_once()
    assert mock_publish.call_args[0][1] == "updated"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/tests/apps/owasp/signals/event_test.py` around lines 18 - 40, Update
the misleading docstring in test_event_updated_publishes_updated_notification to
state it verifies that no notification is published when no meaningful fields
change, and add a new test named
test_event_updated_with_changes_publishes_notification that uses MagicMock with
event._previous_values different from current values, calls
event_post_save(sender=None, instance=event, created=False), and asserts the
patched publish_event_notification (mock_publish) is called once and that its
call args include the "updated" notification; reference event_post_save,
publish_event_notification, mock_publish, and event._previous_values to locate
the relevant code.
backend/tests/apps/owasp/utils/notifications_test.py (1)

112-124: Consider testing days_remaining in deadline reminder notification.

The test verifies the notification type but doesn't pass or assert days_remaining, which is mentioned in the PR objectives as part of the deadline reminder payload. The actual command in owasp_check_event_deadlines.py calls publish_event_notification(event, "deadline_reminder", days_remaining=days).

♻️ Suggested enhancement
     `@patch`("apps.owasp.utils.notifications.get_redis_connection")
     def test_publishes_deadline_reminder_notification(self, mock_redis):
         """Test that event deadline reminder notification is published."""
         mock_conn = MagicMock()
         mock_redis.return_value = mock_conn
         event = MagicMock()
         event.id = 10
 
-        publish_event_notification(event, "deadline_reminder")
+        publish_event_notification(event, "deadline_reminder", days_remaining=3)
 
         call_args = mock_conn.xadd.call_args
         assert call_args[0][1]["type"] == "event_deadline_reminder"
+        assert call_args[0][1]["days_remaining"] == "3"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/tests/apps/owasp/utils/notifications_test.py` around lines 112 - 124,
The test test_publishes_deadline_reminder_notification does not verify the
days_remaining field; update the test to call publish_event_notification(event,
"deadline_reminder", days_remaining=expected_days) and assert that
mock_conn.xadd was called with a payload containing "days_remaining" equal to
the expected value (in addition to the existing type check) so the deadline
reminder payload matches the usage in owasp_check_event_deadlines.py and
publish_event_notification.
backend/apps/owasp/signals/chapter.py (1)

37-40: Empty string handling may cause inconsistent display.

The condition if old_value else None treats empty strings as falsey, converting them to None. This could cause display inconsistencies where an empty field shows as None → "new value" instead of "" → "new value".

This is a minor edge case and may be intentional for cleaner display.

♻️ Alternative for explicit None handling
                 changed_fields[field] = {
-                    "old": str(old_value) if old_value else None,
-                    "new": str(new_value) if new_value else None,
+                    "old": str(old_value) if old_value is not None else None,
+                    "new": str(new_value) if new_value is not None else None,
                 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/apps/owasp/signals/chapter.py` around lines 37 - 40, The current
conversion uses truthiness and turns empty strings into None causing
inconsistent display; update the assignment to only treat actual None as None by
changing the conditions to explicit None checks (e.g., use "old": str(old_value)
if old_value is not None else None and "new": str(new_value) if new_value is not
None else None) in the changed_fields[field] construction so empty strings
remain "" while real None values stay None.
backend/apps/owasp/management/commands/owasp_run_notification_worker.py (1)

287-334: Fix the 1-day reminder copy.

days_info always uses "days", so the 1-day reminder renders "1 days left" in both subject and body.

💬 Proposed copy fix
             if days_bytes:
                 days = days_bytes.decode()
-                days_info = f" ({days} days left)"
+                unit = "day" if days == "1" else "days"
+                days_info = f" ({days} {unit} left)"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/apps/owasp/management/commands/owasp_run_notification_worker.py`
around lines 287 - 334, The reminder text always shows "days" because days_info
is built from the decoded string without pluralization; change the days handling
(where days_bytes is decoded into days and days_info is set) to parse the days
value as an integer and choose the unit ("day" vs "days") accordingly (e.g.,
when days_int == 1 use "day") and then set days_info = f" ({days_int} {unit}
left)"; this will ensure the subject/body templates referencing days_info (see
days_bytes, days, days_info and the "event_deadline_reminder" entry in
titles/entity_messages) render "1 day left" correctly.
backend/tests/apps/owasp/signals/chapter_test.py (1)

18-38: Add the actual updated-notification path to this suite.

This test only exercises the no-op branch, so a regression in meaningful-field detection would still pass. Either rename it to match the current assertion or add a case where one tracked field changes and assert the "updated" publish call.

🧪 Suggested positive-path test
+    `@patch`("apps.owasp.signals.chapter.publish_chapter_notification")
+    def test_chapter_updated_with_meaningful_change_publishes_updated_notification(
+        self, mock_publish
+    ):
+        chapter = MagicMock()
+        chapter._previous_values = {
+            "name": "Old Chapter",
+            "country": "Test Country",
+            "region": "Test Region",
+            "suggested_location": "Test Location",
+            "description": "Test description",
+        }
+        chapter.name = "New Chapter"
+        chapter.country = "Test Country"
+        chapter.region = "Test Region"
+        chapter.suggested_location = "Test Location"
+        chapter.description = "Test description"
+
+        chapter_post_save(sender=None, instance=chapter, created=False)
+
+        mock_publish.assert_called_once_with(
+            chapter,
+            "updated",
+            {"name": {"old": "Old Chapter", "new": "New Chapter"}},
+        )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/tests/apps/owasp/signals/chapter_test.py` around lines 18 - 38, The
test only covers the no-op branch; add a positive-path case in the same test
file that mutates one tracked field and asserts publish_chapter_notification is
called with an "updated" notification: call chapter_post_save(sender=None,
instance=chapter, created=False) after changing one property (e.g., change
chapter.name or chapter.description from its _previous_values), and use the
patched mock_publish to assert it was invoked (e.g.,
mock_publish.assert_called_once_with(...) or check call args for an "updated"
action). Keep the existing no-change assertion or split into two tests (rename
the current to reflect "no changes") and add a new test method like
test_chapter_updated_publishes_updated_notification_on_field_change referencing
chapter_post_save and publish_chapter_notification.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/apps/owasp/management/commands/owasp_notification_dlq.py`:
- Around line 103-123: The DLQ retry branch currently sends the email via
send_mail and deletes the stream entry with redis_conn.xdel but never persists a
Notification record or uses the worker's idempotency path; modify this branch
(the block using self._get_value, send_mail, redis_conn.xdel, and
self.stdout.write) to route the delivery through the same
persistence/idempotency routine the live worker uses (i.e., call the
service/function that creates Notification rows and enforces deduplication
instead of directly sending mail), then only delete the DLQ entry and increment
success_count after that routine returns success so retried messages are
recorded in-app and deduped.

In `@backend/apps/owasp/management/commands/owasp_run_notification_worker.py`:
- Around line 156-183: The send_notification function is racy because it checks
Notification.objects.filter(...).exists() then does send_mail() then
Notification.objects.create(), which allows duplicate mails when concurrent
workers run; fix by creating or claiming an idempotency record transactionally
before the side effect: add a unique constraint (e.g., on
recipient/type/related_link/message or an idempotency_key) and in
send_notification use a DB transaction/atomic block to insert a pending
Notification (or upsert/get_or_create) to reserve the notification, only proceed
to call send_mail() if the insert succeeded (i.e., you were the creator), and
then update that Notification to sent/complete; alternatively, implement a
separate Idempotency model and claim it (create unique row) inside an atomic
block before calling send_mail(), ensuring only the holder sends the email.
- Around line 62-68: The main loop currently logs exceptions from
process_message() but leaves the message in the PEL until restart; change the
except block so failed messages are retried or dead-lettered instead of being
stranded: on exception, read a retry_count field from the message (or add one),
increment it, and if retry_count < MAX_RETRIES requeue the message back onto the
stream (XADD) with the updated retry_count and then XACK the original
message_id; if retry_count >= MAX_RETRIES XADD the message to a DLQ stream
(e.g., stream_key + ":dlq") and XACK the original; keep logging the failure and
include message_id and retry_count; reference process_message,
recover_pending_messages, stream_key, group_name, message_id, and
introduce/configure MAX_RETRY_ATTEMPTS.
- Around line 392-395: The generic "except Exception:" block in the worker
swallows failures from _handle_entity_notification() so the message gets XACKed
as if processed; remove or change that catch so failures are re-raised (or
re-thrown) after logging instead of returning silently. Keep the specific
model_class.DoesNotExist handling, but for the generic exception either remove
the except entirely or call logger.exception(...) and then raise to let the
outer worker logic treat it (retry/DLQ) — reference the logger.exception call,
the _handle_entity_notification invocation, and the
notification_type/model_class symbols to locate the code to update.
- Around line 417-427: The DLQ entry created in the exception branch for
recovery failures only records message_id and error, which prevents the DLQ
retry command from reconstructing and resending the notification; update the
redis_conn.xadd call in the except Exception as exc block (the one that logs via
logger.exception and uses self.DLQ_STREAM_KEY, stream_key, group_name,
message_id) to include the original notification payload fields (e.g.,
user_email, title, message, notification_type) along with
type="recovery_failed", message_id and error so the owasp_notification_dlq retry
command can read and resend the message.

In `@backend/apps/owasp/utils/notifications.py`:
- Around line 20-33: The current publish block around
get_redis_connection/redis_conn.xadd(STREAM_KEY, message) catches all exceptions
and only logs them, which drops notifications; change this by persisting failed
messages to a durable outbox/retry store or re-raising for caller-managed retry
so failures aren't lost: implement (or call) a durable fallback path when xadd
fails (e.g., write the same message object to your DB outbox table or enqueue to
a persistent task queue) and/or re-raise the exception instead of swallowing it;
update the snapshot publish code (the try/except surrounding
get_redis_connection, STREAM_KEY, xadd and logger.exception) and apply the same
durable-fallback pattern to the other publish helpers referenced (lines ~48-65
and ~84-103) so all publish failures are persisted or propagated for retry.

---

Nitpick comments:
In `@backend/apps/owasp/apps.py`:
- Around line 11-15: The ready() method imports signal modules for side effects
but only apps.owasp.signals.snapshot has the noqa suppression; add "# noqa:
F401" to the other two imports (apps.owasp.signals.chapter and
apps.owasp.signals.event) so all three side-effect-only imports in ready() are
consistently ignored by the unused-import linter.

In `@backend/apps/owasp/management/commands/owasp_run_notification_worker.py`:
- Around line 287-334: The reminder text always shows "days" because days_info
is built from the decoded string without pluralization; change the days handling
(where days_bytes is decoded into days and days_info is set) to parse the days
value as an integer and choose the unit ("day" vs "days") accordingly (e.g.,
when days_int == 1 use "day") and then set days_info = f" ({days_int} {unit}
left)"; this will ensure the subject/body templates referencing days_info (see
days_bytes, days, days_info and the "event_deadline_reminder" entry in
titles/entity_messages) render "1 day left" correctly.

In `@backend/apps/owasp/signals/chapter.py`:
- Around line 37-40: The current conversion uses truthiness and turns empty
strings into None causing inconsistent display; update the assignment to only
treat actual None as None by changing the conditions to explicit None checks
(e.g., use "old": str(old_value) if old_value is not None else None and "new":
str(new_value) if new_value is not None else None) in the changed_fields[field]
construction so empty strings remain "" while real None values stay None.

In `@backend/settings/base.py`:
- Around line 232-235: The EMAIL_BACKEND setting currently passes an explicit
environ_name to values.Value redundantly; update the EMAIL_BACKEND declaration
to call values.Value(...) without the environ_name argument so it relies on the
default behavior (values.Value uses the setting name as the env key), keeping
the same default "django.core.mail.backends.console.EmailBackend" and the
values.Value call otherwise unchanged.

In `@backend/tests/apps/owasp/signals/chapter_test.py`:
- Around line 18-38: The test only covers the no-op branch; add a positive-path
case in the same test file that mutates one tracked field and asserts
publish_chapter_notification is called with an "updated" notification: call
chapter_post_save(sender=None, instance=chapter, created=False) after changing
one property (e.g., change chapter.name or chapter.description from its
_previous_values), and use the patched mock_publish to assert it was invoked
(e.g., mock_publish.assert_called_once_with(...) or check call args for an
"updated" action). Keep the existing no-change assertion or split into two tests
(rename the current to reflect "no changes") and add a new test method like
test_chapter_updated_publishes_updated_notification_on_field_change referencing
chapter_post_save and publish_chapter_notification.

In `@backend/tests/apps/owasp/signals/event_test.py`:
- Around line 18-40: Update the misleading docstring in
test_event_updated_publishes_updated_notification to state it verifies that no
notification is published when no meaningful fields change, and add a new test
named test_event_updated_with_changes_publishes_notification that uses MagicMock
with event._previous_values different from current values, calls
event_post_save(sender=None, instance=event, created=False), and asserts the
patched publish_event_notification (mock_publish) is called once and that its
call args include the "updated" notification; reference event_post_save,
publish_event_notification, mock_publish, and event._previous_values to locate
the relevant code.

In `@backend/tests/apps/owasp/utils/notifications_test.py`:
- Around line 112-124: The test test_publishes_deadline_reminder_notification
does not verify the days_remaining field; update the test to call
publish_event_notification(event, "deadline_reminder",
days_remaining=expected_days) and assert that mock_conn.xadd was called with a
payload containing "days_remaining" equal to the expected value (in addition to
the existing type check) so the deadline reminder payload matches the usage in
owasp_check_event_deadlines.py and publish_event_notification.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 85e9f11a-aeb9-4be1-8b50-527887c5a95e

📥 Commits

Reviewing files that changed from the base of the PR and between 365492c and 4eadda6.

📒 Files selected for processing (26)
  • backend/Makefile
  • backend/apps/owasp/Makefile
  • backend/apps/owasp/admin/notification.py
  • backend/apps/owasp/apps.py
  • backend/apps/owasp/management/commands/owasp_check_event_deadlines.py
  • backend/apps/owasp/management/commands/owasp_notification_dlq.py
  • backend/apps/owasp/management/commands/owasp_run_notification_worker.py
  • backend/apps/owasp/migrations/0073_notification_subscription.py
  • backend/apps/owasp/migrations/0074_remove_subscription_owasp_subsc_user_id_33ae6d_idx_and_more.py
  • backend/apps/owasp/migrations/0075_alter_subscription_object_id.py
  • backend/apps/owasp/models/__init__.py
  • backend/apps/owasp/models/notification.py
  • backend/apps/owasp/signals/__init__.py
  • backend/apps/owasp/signals/chapter.py
  • backend/apps/owasp/signals/event.py
  • backend/apps/owasp/signals/snapshot.py
  • backend/apps/owasp/utils/notifications.py
  • backend/settings/base.py
  • backend/tests/apps/owasp/management/commands/owasp_check_event_deadlines_test.py
  • backend/tests/apps/owasp/management/commands/owasp_run_notification_worker_test.py
  • backend/tests/apps/owasp/signals/__init__.py
  • backend/tests/apps/owasp/signals/chapter_test.py
  • backend/tests/apps/owasp/signals/event_test.py
  • backend/tests/apps/owasp/utils/notifications_test.py
  • cron/production
  • cspell/custom-dict.txt

- Main loop now sends failed messages directly to DLQ with full
  original data instead of only logging and leaving them stranded
  in PEL
- Recovery DLQ entries now include original stream payload for
  observability and future retries
- Re-raise DoesNotExist and generic Exception in
  _handle_entity_notification so failures propagate to the main
  loop DLQ routing instead of being silently swallowed and ACKed
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 1 file (changes from recent commits).

Requires human review: Auto-approval blocked by 13 unresolved issues from previous reviews.

- Extracted send_notification business logic out of the worker class
  and moved it to utils/notifications.py to serve as a shared utility
- Updated DLQ retry command to use the shared send_notification utility
  so that manually retried messages go through the exact same
  deduplication checks and Notification model persistence as the worker
- Updated worker and related unit tests to use the new shared utilit sonarcloud security warning for hardcoded http:// URLs in event test
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
backend/apps/owasp/utils/notifications.py (1)

108-135: ⚠️ Potential issue | 🟠 Major

Race condition in idempotency check remains unaddressed.

The exists()send_mail()create() sequence is not atomic. With multiple concurrent workers, two can both pass the exists() check before either commits the Notification row, resulting in duplicate emails. Additionally, if send_mail() succeeds but create() fails (DB error), retries will resend the email.

The Notification model (context snippet 1) has no unique_together constraint on (recipient, type, related_link, message), so the DB cannot enforce uniqueness as a fallback.

Consider using get_or_create or a transactional "claim" pattern:

Proposed atomic idempotency fix
 def send_notification(*, user, title, message, notification_type, related_link):
     """Send notification to user and persist to DB."""
-    if Notification.objects.filter(
-        recipient_id=user.id,
-        type=notification_type,
-        related_link=related_link,
-        message=message,
-    ).exists():
+    from django.db import IntegrityError, transaction
+
+    # Attempt to claim the notification slot first (idempotency key)
+    try:
+        with transaction.atomic():
+            notification, created = Notification.objects.get_or_create(
+                recipient_id=user.id,
+                type=notification_type,
+                related_link=related_link,
+                message=message,
+                defaults={"title": title},
+            )
+    except IntegrityError:
+        # Another worker won the race
+        logger.info("Already notified %s for %s, skipping", user.email, notification_type)
+        return
+
+    if not created:
         logger.info("Already notified %s for %s, skipping", user.email, notification_type)
         return
 
     full_message = f"{message}\n\nView: {related_link}" if related_link else message
     send_mail(
         subject=title,
         message=full_message,
         from_email="noreply@owasp.org",
         recipient_list=[user.email],
         fail_silently=False,
     )
     logger.info("Sent %s email to %s", notification_type, user.email)
-
-    Notification.objects.create(
-        recipient=user,
-        type=notification_type,
-        title=title,
-        message=message,
-        related_link=related_link,
-    )

Note: This fix also requires adding a unique constraint on the Notification model for (recipient, type, related_link, message) to make get_or_create race-safe.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/apps/owasp/utils/notifications.py` around lines 108 - 135, The
current exists() → send_mail() → create() flow in send_notification is racy;
change send_notification to perform an atomic get_or_create on the Notification
model (using Notification.objects.get_or_create(recipient=user,
type=notification_type, related_link=related_link, message=message,
defaults={'title': title})) and only call send_mail if get_or_create returned
created=True; additionally add a DB-level unique constraint on (recipient, type,
related_link, message) in the Notification model (and create a migration) so
get_or_create is race-safe across concurrent workers; ensure you reference the
send_notification function and the Notification model when making these changes
and run migrations.
🧹 Nitpick comments (3)
backend/apps/owasp/management/commands/owasp_run_notification_worker.py (1)

127-173: Unreachable return False at line 173.

The function always returns from within the while loop: either return True on success (line 171) or return False after exceeding MAX_RETRIES (line 163). Line 173 is dead code.

Remove unreachable code
                     logger.info(
                         "Email to %s succeeded after %d retries",
                         user.email,
                         retry_count,
                     )
                 return True
-
-        return False
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/apps/owasp/management/commands/owasp_run_notification_worker.py`
around lines 127 - 173, The final "return False" is unreachable in
send_notification_with_retry because all control paths inside the while loop
already return; remove the trailing "return False" at the end of the function so
there is no dead code; keep the existing logic that returns True on success and
returns False when retry_count exceeds self.MAX_RETRIES (logger.exception path),
and leave constants MAX_RETRIES, BASE_DELAY, and DELAY_MULTIPLIER unchanged.
backend/tests/apps/owasp/management/commands/owasp_run_notification_worker_test.py (2)

56-74: Unused command fixture in idempotency test.

Same as above — command is passed but not used.

Remove unused fixture
     `@mock.patch`("apps.owasp.utils.notifications.send_mail")
     `@mock.patch`("apps.owasp.utils.notifications.Notification")
     def test_send_notification_idempotency(
-        self, mock_notification, mock_send_mail, command, mock_user, mock_snapshot
+        self, mock_notification, mock_send_mail, mock_user, mock_snapshot
     ):
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/tests/apps/owasp/management/commands/owasp_run_notification_worker_test.py`
around lines 56 - 74, The test function test_send_notification_idempotency
declares an unused fixture parameter named command; remove the unused parameter
from the test signature so it reads def test_send_notification_idempotency(self,
mock_notification, mock_send_mail, mock_user, mock_snapshot): and run the tests
to ensure no other references to command exist—this cleans up the unused fixture
without changing behavior of send_notification assertions.

30-54: Unused command fixture in test method.

The command fixture is passed to test_send_notification_success but never used — this test exercises the send_notification utility function directly, not the Command class.

Remove unused fixture
     `@mock.patch`("apps.owasp.utils.notifications.send_mail")
     `@mock.patch`("apps.owasp.utils.notifications.Notification")
     def test_send_notification_success(
-        self, mock_notification, mock_send_mail, command, mock_user, mock_snapshot
+        self, mock_notification, mock_send_mail, mock_user, mock_snapshot
     ):
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/tests/apps/owasp/management/commands/owasp_run_notification_worker_test.py`
around lines 30 - 54, Remove the unused `command` fixture from the test method
signature in test_send_notification_success; update the parameter list to (self,
mock_notification, mock_send_mail, mock_user, mock_snapshot) so it matches the
`@mock.patch` decoration order, leaving the test body unchanged (it calls
send_notification and asserts on mock_send_mail and
mock_notification.objects.create).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@backend/apps/owasp/utils/notifications.py`:
- Around line 108-135: The current exists() → send_mail() → create() flow in
send_notification is racy; change send_notification to perform an atomic
get_or_create on the Notification model (using
Notification.objects.get_or_create(recipient=user, type=notification_type,
related_link=related_link, message=message, defaults={'title': title})) and only
call send_mail if get_or_create returned created=True; additionally add a
DB-level unique constraint on (recipient, type, related_link, message) in the
Notification model (and create a migration) so get_or_create is race-safe across
concurrent workers; ensure you reference the send_notification function and the
Notification model when making these changes and run migrations.

---

Nitpick comments:
In `@backend/apps/owasp/management/commands/owasp_run_notification_worker.py`:
- Around line 127-173: The final "return False" is unreachable in
send_notification_with_retry because all control paths inside the while loop
already return; remove the trailing "return False" at the end of the function so
there is no dead code; keep the existing logic that returns True on success and
returns False when retry_count exceeds self.MAX_RETRIES (logger.exception path),
and leave constants MAX_RETRIES, BASE_DELAY, and DELAY_MULTIPLIER unchanged.

In
`@backend/tests/apps/owasp/management/commands/owasp_run_notification_worker_test.py`:
- Around line 56-74: The test function test_send_notification_idempotency
declares an unused fixture parameter named command; remove the unused parameter
from the test signature so it reads def test_send_notification_idempotency(self,
mock_notification, mock_send_mail, mock_user, mock_snapshot): and run the tests
to ensure no other references to command exist—this cleans up the unused fixture
without changing behavior of send_notification assertions.
- Around line 30-54: Remove the unused `command` fixture from the test method
signature in test_send_notification_success; update the parameter list to (self,
mock_notification, mock_send_mail, mock_user, mock_snapshot) so it matches the
`@mock.patch` decoration order, leaving the test body unchanged (it calls
send_notification and asserts on mock_send_mail and
mock_notification.objects.create).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e1d647b5-a5c2-472c-a7a0-df3767088f09

📥 Commits

Reviewing files that changed from the base of the PR and between 2de837a and f74a9d4.

📒 Files selected for processing (5)
  • backend/apps/owasp/management/commands/owasp_notification_dlq.py
  • backend/apps/owasp/management/commands/owasp_run_notification_worker.py
  • backend/apps/owasp/utils/notifications.py
  • backend/tests/apps/owasp/management/commands/owasp_run_notification_worker_test.py
  • backend/tests/apps/owasp/signals/event_test.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • backend/tests/apps/owasp/signals/event_test.py
  • backend/apps/owasp/management/commands/owasp_notification_dlq.py

coderabbitai[bot]
coderabbitai bot previously approved these changes Apr 6, 2026
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 5 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="backend/apps/owasp/management/commands/owasp_notification_dlq.py">

<violation number="1" location="backend/apps/owasp/management/commands/owasp_notification_dlq.py:131">
P2: `User.DoesNotExist` retry failures bypass `dlq_retries` increment, causing inconsistent DLQ retry accounting.</violation>
</file>

<file name="backend/apps/owasp/utils/notifications.py">

<violation number="1" location="backend/apps/owasp/utils/notifications.py:110">
P1: Idempotency check is non-atomic and persisted after sending email, allowing duplicate emails during concurrency or DB-write-after-send failures.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

- Wrapped all post_save publishers (snapshot, chapter, event) in
  transaction.on_commit() to prevent phantom Redis messages during rollbacks
- Fixed falsy dictionary parsing in signals so empty strings aren't converted to None
- Updated 0073 migration to use PositiveBigIntegerField for GenericForeignKey
  object_id to prevent overflow with large integer keys
- Added missing days_remaining payload assertion in deadline reminder test
- Fixed chapter and event signal tests to properly cover the meaningful-update
  publish path instead of just the no-change skip path
- Added transaction.on_commit mock decorators to test classes to pass Pytest strict
  database-access rules
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/apps/owasp/signals/event.py`:
- Around line 36-55: The lambdas passed to transaction.on_commit capture
instance and changed_fields by reference which can be mutated before commit;
change them to bind current values as default args so the snapshot is
captured—e.g., for the created branch bind instance (lambda instance=instance:
publish_event_notification(instance, "created")) and for the updated branch bind
both instance and changed_fields (lambda instance=instance,
changed_fields=changed_fields: publish_event_notification(instance, "updated",
changed_fields=changed_fields)); update the usages around transaction.on_commit
and publish_event_notification in the event handling code that inspects
MEANINGFUL_FIELDS.

In `@backend/tests/apps/owasp/utils/notifications_test.py`:
- Around line 112-124: The test passes a string for days_remaining but
publish_event_notification declares days_remaining: int | None; update the
test_publishes_deadline_reminder_notification to pass an integer (5) instead of
the string "5" so the argument type matches the function signature and adjust
the assertion accordingly (still check mock_conn.xadd.call_args for
["days_remaining"] value).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d48a3890-3708-4138-8dcb-ee053eb629ab

📥 Commits

Reviewing files that changed from the base of the PR and between f74a9d4 and 5ca4224.

📒 Files selected for processing (8)
  • backend/apps/owasp/migrations/0073_notification_subscription.py
  • backend/apps/owasp/signals/chapter.py
  • backend/apps/owasp/signals/event.py
  • backend/apps/owasp/signals/snapshot.py
  • backend/tests/apps/owasp/management/commands/owasp_run_notification_worker_test.py
  • backend/tests/apps/owasp/signals/chapter_test.py
  • backend/tests/apps/owasp/signals/event_test.py
  • backend/tests/apps/owasp/utils/notifications_test.py
✅ Files skipped from review due to trivial changes (1)
  • backend/tests/apps/owasp/management/commands/owasp_run_notification_worker_test.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • backend/apps/owasp/signals/chapter.py
  • backend/tests/apps/owasp/signals/chapter_test.py

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 8 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="backend/tests/apps/owasp/signals/event_test.py">

<violation number="1" location="backend/tests/apps/owasp/signals/event_test.py:13">
P2: Tests patch `transaction.on_commit` but never assert it was used, so regressions that publish notifications outside commit scheduling may go undetected.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

- Patched lambda late-binding bug in all signals by using default
  argument binding (inst=instance) to capture immutable snapshots
- Hardened DLQ retry logic to explicitly delete messages for missing
  users, preventing infinite 'Zombie' retry loops in Redis
- Added explicit mock_on_commit assertions to chapter and event tests
  to ensure notifications are always scheduled within transactions
- Standardized deadline reminder tests to pass integer days_remaining
  while verifying stringified Redis payloads
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 7 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="backend/apps/owasp/management/commands/owasp_notification_dlq.py">

<violation number="1" location="backend/apps/owasp/management/commands/owasp_notification_dlq.py:131">
P2: Failed DLQ retries are deleted on `User.DoesNotExist`, causing failed notifications to be dropped instead of retained for manual recovery.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
backend/apps/owasp/signals/event.py (1)

20-30: Consider setting _previous_values when the DB record is not found.

When instance.pk is truthy but db_instance is None (e.g., concurrent deletion or manually set pk for a non-existent record), _previous_values is not set. The post_save handler's getattr(..., {}) default handles this gracefully, but explicitly setting an empty dict in this edge case would make the intent clearer.

Optional: Make the edge case explicit
 `@receiver`(pre_save, sender=Event)
 def event_pre_save(sender, instance, **kwargs):  # noqa: ARG001
     """Store the previous values before saving."""
     if instance.pk:
         db_instance = Event.objects.filter(pk=instance.pk).values(*MEANINGFUL_FIELDS).first()
         if db_instance:
             instance._previous_values = {  # noqa: SLF001
                 field: db_instance.get(field) for field in MEANINGFUL_FIELDS
             }
+        else:
+            instance._previous_values = {}  # noqa: SLF001
     else:
         instance._previous_values = {}  # noqa: SLF001
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/apps/owasp/signals/event.py` around lines 20 - 30, event_pre_save may
leave instance._previous_values unset when instance.pk is truthy but db_instance
is None; ensure we explicitly set instance._previous_values = {} in that branch
so the post_save handler can rely on it. Locate the pre_save receiver function
event_pre_save and, after fetching db_instance via
Event.objects.filter(...).values(*MEANINGFUL_FIELDS).first(), set
instance._previous_values = {} when db_instance is falsy (otherwise keep the
dict built from MEANINGFUL_FIELDS); this makes the edge case explicit and
consistent with the non-pk branch.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/apps/owasp/management/commands/owasp_notification_dlq.py`:
- Around line 106-150: The loop currently only checks for
user_email/title/message and silently skips other DLQ shapes; fix by reading the
record type (use self._get_value(data, b"type")) and add an explicit branch for
b"processing_failed" that replays the entry through the same worker path used by
owasp_run_notification_worker (call the worker's message handler or re-publish
to the main notification stream), or if unsupported, log a clear error and
increment dlq_retries before deleting the DLQ entry; modify the block around
self._get_value, DLQ_STREAM_KEY, redis_conn.xdel, send_notification and
send_mail to handle the new branch and ensure success_count/error_count and
message deletion are updated appropriately.
- Around line 27-35: Make the CLI flags mutually exclusive by updating the
command's argument setup: in the add_arguments (or where parser is defined)
replace the two separate parser.add_argument calls for "--id" and "--all" with a
mutually exclusive group (e.g., group = parser.add_mutually_exclusive_group())
and add the "--id" and "--all" arguments to that group, or alternatively keep
the args but add an explicit check in handle() that raises a CommandError if
both options are provided; reference the parser.add_argument calls for "--id"
and "--all" to locate the changes.
- Around line 48-55: Replace non-idiomatic exits and silent returns with
Django's CommandError: in the action validation block where you currently call
sys.exit(1) for the "retry" branch, raise CommandError("Error: --id or --all is
required for retry"); similarly change the early returns in retry_dlq (method
retry_dlq) to raise CommandError with the same descriptive message instead of
returning, and do the same for remove_dlq (method remove_dlq) and the "remove"
action validation (raise CommandError("Error: --id or --all is required for
remove") instead of printing and returning); this ensures the management command
exits with code 1 and matches the rest of the codebase.

---

Nitpick comments:
In `@backend/apps/owasp/signals/event.py`:
- Around line 20-30: event_pre_save may leave instance._previous_values unset
when instance.pk is truthy but db_instance is None; ensure we explicitly set
instance._previous_values = {} in that branch so the post_save handler can rely
on it. Locate the pre_save receiver function event_pre_save and, after fetching
db_instance via Event.objects.filter(...).values(*MEANINGFUL_FIELDS).first(),
set instance._previous_values = {} when db_instance is falsy (otherwise keep the
dict built from MEANINGFUL_FIELDS); this makes the edge case explicit and
consistent with the non-pk branch.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 66f9fdc2-44f5-4915-b1ac-f472ea52c9ab

📥 Commits

Reviewing files that changed from the base of the PR and between 5ca4224 and beb6da7.

📒 Files selected for processing (7)
  • backend/apps/owasp/management/commands/owasp_notification_dlq.py
  • backend/apps/owasp/signals/chapter.py
  • backend/apps/owasp/signals/event.py
  • backend/apps/owasp/signals/snapshot.py
  • backend/tests/apps/owasp/signals/chapter_test.py
  • backend/tests/apps/owasp/signals/event_test.py
  • backend/tests/apps/owasp/utils/notifications_test.py
✅ Files skipped from review due to trivial changes (3)
  • backend/tests/apps/owasp/utils/notifications_test.py
  • backend/tests/apps/owasp/signals/chapter_test.py
  • backend/tests/apps/owasp/signals/event_test.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • backend/apps/owasp/signals/snapshot.py
  • backend/apps/owasp/signals/chapter.py

- Standardized DLQ command to use Django's CommandError for idiomatic
  error reporting and correct terminal exit codes
- Patched DLQ command arguments with mutually exclusive groups to
  prevent accidental data loss from overlapping --id and --all flags
- Improved DLQ logging to distinguish between retriable email failures
  and system-level worker failures requiring manual admin review
- Implemented early-binding snapshots (inst=instance) for all signal lambdas
  to fix the Python late-binding closure bug in transactions
- Strengthened test suite with mandatory on_commit mock assertions
  and resolved payload type mismatches in deadline reminder tests
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 1 file (changes from recent commits).

Requires human review: Auto-approval blocked by 1 unresolved issue from previous reviews.

coderabbitai[bot]
coderabbitai bot previously approved these changes Apr 8, 2026
- Fixed TRY003 and EM101 errors in owasp_notification_dlq.py by assigning
  exception string literals to variables before raising CommandError
- Ensured compliance with project linting rules for better error
  traceability and cleaner exception handling
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 1 file (changes from recent commits).

@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud bot commented Apr 8, 2026

@Shofikul-Isl4m
Copy link
Copy Markdown
Contributor Author

@arkid15r this is ready for review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement Entity-Level Subscriptions for Chapters and Events

1 participant