mirror of
https://github.com/pacnpal/django-anymail.git
synced 2025-12-20 03:41:05 -05:00
Allow requests session customization, document use for automatic retries
* Refactor create_session() out of AnymailRequestsBackend * Document automatic retries with Requests
This commit is contained in:
@@ -30,6 +30,13 @@ vNext
|
||||
|
||||
*Unreleased changes*
|
||||
|
||||
Features
|
||||
~~~~~~~~
|
||||
|
||||
* Support customizing the requests.Session for requests-based backends,
|
||||
and document how this can be used to mount an adapter that simplifies
|
||||
automatic retry logic. (Thanks to `@dgilmanAIDENTIFIED`_.)
|
||||
|
||||
Fixes
|
||||
~~~~~
|
||||
|
||||
@@ -1340,6 +1347,7 @@ Features
|
||||
.. _@costela: https://github.com/costela
|
||||
.. _@coupa-anya: https://github.com/coupa-anya
|
||||
.. _@decibyte: https://github.com/decibyte
|
||||
.. _@dgilmanAIDENTIFIED: https://github.com/dgilmanAIDENTIFIED
|
||||
.. _@dominik-lekse: https://github.com/dominik-lekse
|
||||
.. _@erikdrums: https://github.com/erikdrums
|
||||
.. _@ewingrj: https://github.com/ewingrj
|
||||
|
||||
@@ -25,17 +25,12 @@ class AnymailRequestsBackend(AnymailBaseBackend):
|
||||
return False # already exists
|
||||
|
||||
try:
|
||||
self.session = requests.Session()
|
||||
except requests.RequestException:
|
||||
self.session = self.create_session()
|
||||
except Exception:
|
||||
if not self.fail_silently:
|
||||
raise
|
||||
else:
|
||||
self.session.headers["User-Agent"] = "django-anymail/{version}-{esp} {orig}".format(
|
||||
esp=self.esp_name.lower(), version=__version__,
|
||||
orig=self.session.headers.get("User-Agent", ""))
|
||||
if self.debug_api_requests:
|
||||
self.session.hooks['response'].append(self._dump_api_request)
|
||||
return True
|
||||
|
||||
return True
|
||||
|
||||
def close(self):
|
||||
if self.session is None:
|
||||
@@ -57,6 +52,23 @@ class AnymailRequestsBackend(AnymailBaseBackend):
|
||||
"or you are incorrectly calling _send directly.)".format(class_name=class_name))
|
||||
return super()._send(message)
|
||||
|
||||
def create_session(self):
|
||||
"""Create the requests.Session object used by this instance of the backend.
|
||||
If subclassed, you can modify the Session returned from super() to give
|
||||
it your own configuration.
|
||||
|
||||
This must return an instance of requests.Session."""
|
||||
session = requests.Session()
|
||||
|
||||
session.headers["User-Agent"] = "django-anymail/{version}-{esp} {orig}".format(
|
||||
esp=self.esp_name.lower(), version=__version__,
|
||||
orig=session.headers.get("User-Agent", ""))
|
||||
|
||||
if self.debug_api_requests:
|
||||
session.hooks['response'].append(self._dump_api_request)
|
||||
|
||||
return session
|
||||
|
||||
def post_to_esp(self, payload, message):
|
||||
"""Post payload to ESP send API endpoint, and return the raw response.
|
||||
|
||||
|
||||
@@ -276,6 +276,7 @@ intersphinx_mapping = {
|
||||
'python': ('https://docs.python.org/3.10', None),
|
||||
'django': ('https://docs.djangoproject.com/en/stable/', 'https://docs.djangoproject.com/en/stable/_objects/'),
|
||||
'requests': ('https://requests.readthedocs.io/en/stable/', None),
|
||||
'urllib3': ('https://urllib3.readthedocs.io/en/stable/', None),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -23,3 +23,46 @@ transient ESP errors depends on your Django project:
|
||||
In addition to handling connectivity issues, either of these approaches also has the advantage
|
||||
of moving email sending to a background thread. This is a best practice for sending email from
|
||||
Django, as it allows your web views to respond faster.
|
||||
|
||||
Automatic retries
|
||||
-----------------
|
||||
|
||||
Backends that use :pypi:`requests` for network calls can configure its built-in retry
|
||||
functionality. Subclass the Anymail backend and mount instances of
|
||||
:class:`~requests.adapters.HTTPAdapter` and :class:`~urllib3.util.Retry` configured with
|
||||
your settings on the :class:`~requests.Session` object in `create_session()`.
|
||||
|
||||
Automatic retries aren't a substitute for sending emails in a background thread, they're
|
||||
a way to simplify your retry logic within the worker. Be aware that retrying `read` and `other`
|
||||
failures may result in sending duplicate emails. Requests will only attempt to retry idempotent
|
||||
HTTP verbs by default, you may need to whitelist the verbs used by your backend's API in
|
||||
`allowed_methods` to actually get any retries. It can also automatically retry error HTTP
|
||||
status codes for you but you may need to configure `status_forcelist` with the error HTTP status
|
||||
codes used by your backend provider.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import anymail.backends.mandrill
|
||||
from django.conf import settings
|
||||
import requests.adapters
|
||||
|
||||
|
||||
class RetryableMandrillEmailBackend(anymail.backends.mandrill.EmailBackend):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
retry = requests.adapters.Retry(
|
||||
total=settings.EMAIL_TOTAL_RETRIES,
|
||||
connect=settings.EMAIL_CONNECT_RETRIES,
|
||||
read=settings.EMAIL_READ_RETRIES,
|
||||
status=settings.EMAIL_HTTP_STATUS_RETRIES,
|
||||
other=settings.EMAIL_OTHER_RETRIES,
|
||||
allowed_methods=False, # Retry all HTTP verbs
|
||||
status_forcelist=settings.EMAIL_HTTP_STATUS_RETRYABLE,
|
||||
backoff_factor=settings.EMAIL_RETRY_BACKOFF_FACTOR,
|
||||
)
|
||||
self.retryable_adapter = requests.adapters.HTTPAdapter(max_retries=retry)
|
||||
|
||||
def create_session(self):
|
||||
session = super().create_session()
|
||||
session.mount("https://", self.retryable_adapter)
|
||||
return session
|
||||
|
||||
Reference in New Issue
Block a user