Skip to content

feat: auto-pause campaign if initial bounce rate exceeds threshold (#… - #644

Open
Bheemeswari497 wants to merge 2 commits into
Kuldeeep18:mainfrom
Bheemeswari497:feature/auto-pause-campaign-480
Open

feat: auto-pause campaign if initial bounce rate exceeds threshold (#…#644
Bheemeswari497 wants to merge 2 commits into
Kuldeeep18:mainfrom
Bheemeswari497:feature/auto-pause-campaign-480

Conversation

@Bheemeswari497

@Bheemeswari497 Bheemeswari497 commented Jul 11, 2026

Copy link
Copy Markdown

Related Issue

Closes #480

Summary

Implemented automatic campaign pausing when the initial bounce rate exceeds 10% after at least 50 emails have been sent.

Changes

  • Added automatic campaign health check after bounce events.
  • Auto-pauses campaigns when bounce rate exceeds the configured threshold.
  • Prevents COMPLETED from overriding PAUSED status.
  • Uses database locking to reduce race conditions during pause evaluation.
  • Sends notification to organization admins when a campaign is auto-paused.
  • Added backend tests covering:
    • Less than 50 sent emails
    • Threshold reached
    • Already paused campaigns
    • Pause priority over completion

Type of Change

  • New feature

Testing

  • Backend tests executed
  • Auto-pause behavior verified
  • Existing functionality remains unchanged

Summary by CodeRabbit

  • New Features
    • Campaigns now auto-pause when at least 50 messages have been sent and the bounce rate exceeds 10%.
    • When a campaign is auto-paused, a pause notification is emitted (only once per pause event).
  • Bug Fixes
    • Paused campaigns no longer receive additional completion processing.
    • Bounce events that occur after a campaign is already paused won’t trigger duplicate pause notifications.
    • Pausing now takes priority over completion-related outcomes.
  • Tests
    • Added automated test coverage for bounce-rate-driven auto-pause behavior and priority rules.

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b5881027-6dbf-47d9-b4ff-0867b2010cee

📥 Commits

Reviewing files that changed from the base of the PR and between d171a8f and cfc7ea2.

📒 Files selected for processing (2)
  • backend/campaigns/tasks.py
  • backend/campaigns/tests.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • backend/campaigns/tasks.py
  • backend/campaigns/tests.py

📝 Walkthrough

Walkthrough

Campaign bounce processing now performs an atomic health check after recording bounces. Campaigns with at least 50 sent emails and bounce rates above 10% are paused and generate a notification. Completion logic skips paused campaigns, with tests covering thresholds and precedence.

Changes

Campaign auto-pause

Layer / File(s) Summary
Add locked bounce health evaluation
backend/campaigns/tasks.py
Adds atomic, row-locked bounce-rate evaluation, pauses campaigns exceeding the threshold, sends campaign_paused notifications after commit, and treats paused campaigns as terminal.
Validate pause thresholds and precedence
backend/campaigns/tests.py
Tests the 50-send threshold, automatic pausing, notification de-duplication, and pause precedence over completion.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant BounceProcessor
  participant CampaignHealthCheck
  participant Campaign
  participant NotificationService
  BounceProcessor->>CampaignHealthCheck: evaluate campaign after recording bounce
  CampaignHealthCheck->>Campaign: lock row and inspect sent and bounced counts
  CampaignHealthCheck->>Campaign: set status to PAUSED when bounce rate exceeds 10%
  CampaignHealthCheck->>NotificationService: schedule campaign_paused notification on commit
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: auto-pausing campaigns when bounce rates exceed the threshold.
Linked Issues check ✅ Passed The PR implements the requested 50-sent, 10% bounce-rate auto-pause and admin notification behavior in the bounce task.
Out of Scope Changes check ✅ Passed The changes stay within the campaign auto-pause scope, with tests and completion-guard updates supporting the same feature.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ 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.

@Bheemeswari497

Copy link
Copy Markdown
Author

Hi @Kuldeeep18,

I have completed the implementation for Issue #480.

The feature has been implemented along with backend tests covering the required scenarios. I would appreciate it if you could review the PR when you have time.

Thank you!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
backend/campaigns/tests.py (1)

2059-2079: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

This test doesn't actually exercise pause-over-completion precedence.

The lead is created with status='ACTIVE', so in _maybe_mark_campaign_completed the has_unfinished check is True and the function returns before reaching the .exclude(status='PAUSED').update(...) logic. The campaign stays PAUSED trivially, not because of the precedence guard. To truly validate that pause wins over completion, the enrolled lead should be in a terminal state (e.g. BOUNCED) so completion would otherwise fire.

💚 Make the lead terminal so completion is a real contender
         lead = Lead.objects.create(organization=self.organization, email='b4@acme.test')
         clead = CampaignLead.objects.create(
             organization=self.organization,
             campaign=self.campaign,
             lead=lead,
             current_step=self.step,
-            status='ACTIVE'
+            status='BOUNCED'
         )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/campaigns/tests.py` around lines 2059 - 2079, Update
test_pause_priority_over_completion so the CampaignLead created for the
precedence scenario uses a terminal status such as BOUNCED instead of ACTIVE.
Keep the existing health-check and _maybe_mark_campaign_completed calls,
ensuring completion would otherwise be eligible while the assertion verifies the
campaign remains PAUSED.
backend/campaigns/tasks.py (1)

155-162: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Move the notification out of the locked transaction.

send_notification performs external Firestore I/O while the select_for_update() row lock is still held inside transaction.atomic(). A slow or hanging network call keeps the campaign row locked, blocking concurrent bounce processing for the same campaign. Prefer persisting the status change, then emitting the notification after the transaction commits.

♻️ Send notification after commit
     with transaction.atomic():
         try:
             campaign = Campaign.objects.select_for_update().get(id=campaign_id)
         except Campaign.DoesNotExist:
             return
 
         if campaign.status == 'PAUSED':
             return
 
+        paused = False
         if campaign.sent_count >= 50:
             bounce_rate = campaign.bounced_count / campaign.sent_count
             if bounce_rate > 0.10:
                 campaign.status = 'PAUSED'
                 campaign.save(update_fields=['status'])
                 logger.info(f"Campaign {campaign.id} auto-paused due to high bounce rate.")
-                send_notification(
-                    campaign.organization_id,
-                    'campaign_paused',
-                    {
-                        'message': 'Campaign Auto-Paused due to high bounce rates',
-                        'campaign_id': str(campaign.id)
-                    }
-                )
+                paused = True
+                org_id, cid = campaign.organization_id, str(campaign.id)
+
+    if paused:
+        send_notification(
+            org_id,
+            'campaign_paused',
+            {
+                'message': 'Campaign Auto-Paused due to high bounce rates',
+                'campaign_id': cid,
+            },
+        )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/campaigns/tasks.py` around lines 155 - 162, Move the
send_notification call out of the transaction.atomic/select_for_update block in
the campaign auto-pause flow, while preserving the status update within the
transaction. Register or perform the campaign_paused notification only after the
transaction successfully commits, using the existing campaign organization_id
and id values.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@backend/campaigns/tasks.py`:
- Around line 155-162: Move the send_notification call out of the
transaction.atomic/select_for_update block in the campaign auto-pause flow,
while preserving the status update within the transaction. Register or perform
the campaign_paused notification only after the transaction successfully
commits, using the existing campaign organization_id and id values.

In `@backend/campaigns/tests.py`:
- Around line 2059-2079: Update test_pause_priority_over_completion so the
CampaignLead created for the precedence scenario uses a terminal status such as
BOUNCED instead of ACTIVE. Keep the existing health-check and
_maybe_mark_campaign_completed calls, ensuring completion would otherwise be
eligible while the assertion verifies the campaign remains PAUSED.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: acfda693-f11f-4530-b7e8-0d641b1708e2

📥 Commits

Reviewing files that changed from the base of the PR and between 4a33158 and d171a8f.

📒 Files selected for processing (2)
  • backend/campaigns/tasks.py
  • backend/campaigns/tests.py

@Bheemeswari497

Copy link
Copy Markdown
Author

Hi @Kuldeeep18,

I have addressed all the CodeRabbit review comments and updated the PR accordingly.

Changes made:

  • Deferred notification using transaction.on_commit() to avoid external I/O while holding database locks.
  • Updated the test to properly verify that PAUSED takes precedence over COMPLETED.

All checks are now passing successfully. Kindly review the PR when you have time.

Thank you!

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

LO-114 [Intermediate]: Auto-Pause Campaign if Initial Bounce Rate Exceeds Threshold

1 participant