Skip to content

TAN-7554 Email bug: Sending manual campaigns twice#13675

Merged
sebastienhoorens merged 2 commits intomasterfrom
TAN-7554-manual-email-sending-issues
Apr 23, 2026
Merged

TAN-7554 Email bug: Sending manual campaigns twice#13675
sebastienhoorens merged 2 commits intomasterfrom
TAN-7554-manual-email-sending-issues

Conversation

@sebastienhoorens
Copy link
Copy Markdown
Contributor

When sending to large recipient lists, the HTTP request can time out (504) while the backend continues processing. The frontend shows an error and still displays the Send button, allowing users to trigger a second send and resulting in duplicate emails to all recipients.

There are at least 5 issues:

  • The do_send endpoint can time out because emails are sent out synchronously.
  • The frontend cannot handle the timeout error well and crashes (front/app/containers/Admin/messaging/CustomEmails/Show/index.tsx#L302-L305).
  • The frontend allows sending the email campaign twice in this situation because the data is not refreshed in this situation.
  • The backend allows sending the email campaign twice because of a race condition. The policy check runs before the lock is acquired (which creates a transaction and that's why the second send does not see that the first send has happened). This is fixed in this PR.
  • The draft status is implicitly modelled as not having deliveries. Explicit modelling would have avoided this.

Changelog

Fixed

[TAN-7554] Initial fix to prevent sending manual campaigns twice.

When sending to large recipient lists, the HTTP request can time out
(504) while the backend continues processing. The frontend shows an
error and still displays the Send button, allowing users to trigger
a second send — resulting in duplicate emails to all recipients.

Introduce SendManualCampaignService that checks sent? inside a
pessimistic row lock, making the check-then-send atomic. The existing
policy check (CampaignPolicy#update?) runs before the lock is acquired,
so concurrent requests can both pass it while deliveries from the first
request are still uncommitted.

Also decouple do_send? from update? in CampaignPolicy so the
"prevent double send" concern lives only in the service, while
"prevent editing sent campaigns" stays in the policy.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@notion-workspace
Copy link
Copy Markdown

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@cl-dev-bot
Copy link
Copy Markdown
Collaborator

Messages
📖 Changelog provided 🎉
📖 Notion issue: TAN-7554
📖

Run the e2e tests

📖 Check translation progress

Generated by 🚫 dangerJS against 369d639

Copy link
Copy Markdown
Contributor

@jamesspeake jamesspeake left a comment

Choose a reason for hiding this comment

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

Looks reasonable to me

describe EmailCampaigns::SendManualCampaignService do
subject(:service) { described_class.new }

let(:user) { create(:admin) }
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.

Might be clearer that this is the triggering user

Suggested change
let(:user) { create(:admin) }
let(:current_user) { create(:admin) }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

In this case I tend to disagree. The service implements a "use case" (business logic) that is meant to be decoupled from controller logic. "current_user" is a name typically used in controllers and not in the rest of the code, that's why I would argue the name doesn't really belong here.

@sebastienhoorens sebastienhoorens modified the milestone: Quality Quest Apr 23, 2026
@sebastienhoorens sebastienhoorens merged commit 2870929 into master Apr 23, 2026
9 checks passed
@sebastienhoorens sebastienhoorens deleted the TAN-7554-manual-email-sending-issues branch April 23, 2026 11:13
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.

3 participants