Skip to content

Conversation

@stefanotroncaro
Copy link
Contributor

@stefanotroncaro stefanotroncaro commented Oct 24, 2025

  • Added a CeleryTaskEmailClient, which schedules an email sending task instead of sending the email outright.
  • Configured app container to use the CeleryTaskEmailClient as default client
  • Configured celery worker container to use other email client implementations (that synchronously send emails)
  • Added configurable email sending retry logic by specific methods on the EmailService, through env variables.

@stefanotroncaro stefanotroncaro marked this pull request as ready for review October 27, 2025 21:07
@stefanotroncaro stefanotroncaro requested review from DanTcheche and jsarrolt and removed request for jsarrolt October 27, 2025 21:14
@stefanotroncaro stefanotroncaro changed the title feat: celery task email client default on api container Add email client to send emails via celery tasks Oct 27, 2025
except ExternalProviderException as exc:
if email.context:
countdown_in_seconds = email.context.backoff_in_seconds * (
2**self.request.retries
Copy link
Contributor

Choose a reason for hiding this comment

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

Why X 2? If necessary, maybe can be put in a constant to avoid the magic number

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is a loose implementation of the retry_backoff algorithm that was used in the previous definition of the task. It's not exactly the same because I did not implement neither the jitter randomization nor the backoff max value, but the main feature, the exponential growth, is there.

2 ** self.request.retries is a power of 2 that represents the exponential growth of backoff_in_seconds. So the first time it is retried, it will wait 1x the backoff value, the second time 2x, the third time 4x, and so on.

I could define the 2 as a constant like BACKOFF_EXPONENTIAL_GROWTH_BASE (or something like that) at the top of the file, and here the code would be left as:

Suggested change
2**self.request.retries
multiplicator = BACKOFF_EXPONENTIAL_GROWTH_BASE ** self.request.retries
countdown_in_seconds = email.context.backoff_in_seconds * multiplicator

Do you approve of this change? It seemed kinda overkill to me at first, but after reviewing it I think it certainly is easier to understand this way.

from app.emails.schema.email import Email, EmailContext


class CeleryTaskEmailClient(BaseEmailClient):
Copy link
Contributor

@jsarrolt jsarrolt Oct 28, 2025

Choose a reason for hiding this comment

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

It makes me noice the celery name in here. I think client shouldn't know about Celery directly in here, since i can use SES with celery in theory.
We are mixing the domain/application layer with a specific infrastructure tool (Celery).
Maybe abstracting the name to decouple implementation?

Copy link
Contributor Author

@stefanotroncaro stefanotroncaro Oct 28, 2025

Choose a reason for hiding this comment

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

I understand the concern.

The separation between layers is here, because the application layer should always use the EmailService, not an EmailClient instance directly, and the EmailService uses by default the email client that was configured during app initialization (although injection is possible).

For the example you mentioned (using SES with celery):

  • the app would be configured to use the CeleryEmailClient, and the celery worker to use the SESClient,
  • the application would use the EmailService to send emails,
  • the EmailService would use the configured CeleryEmailClient to enqueue emails via celery,
  • the celery worker would use the configured SESClient (or similar class) to send the emails via SES.

The configuration happens in main.py, where the set_client call is made to set up the default client for each container/environment.

So, the layers are decoupled 😄

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