What is the simplest way to retry a Django view upon an exception being raised?
Question:
I want to rerun a Django view function if a certain exception is raised (in my case a serialization error in the underlying database). I want it to run with exactly the same parameters, including the same request
object – as if the client had re-requested the URL.
There are many database queries in the view, and the exception could be raised on any one of them – and it won’t work to rerun just one of the queries in isolation, so I think I need to wrap the entire view function in a try/except block and loop until success.
But, I have several view functions which might raise such exceptions, so I want a generic solution. I also want to be able to retry up to a certain number of times and then fail.
Is there any simple solution?
Answers:
You could achieve this by writing a decorator:
def retry_on_exception(view):
def wrapper(*args, **kwargs):
while True:
try:
return view(*args, **kwargs):
except (TheExceptions, IWant, ToCatch):
pass
return wrapper
And use this on the view:
@retry_on_exception
def my_view(request, foo, bar):
return HttpResponse("My stuff")
Obviously, this will retry indefinitely, so a lot of logic could be improved there. You could also write the decorator to accept the exceptions it wants to look out for, so you can customise it per view.
You can use retry()
below to retry a view 0 or more times with 0 or more interval seconds:
# "store/views.py"
from django.db import DatabaseError, transaction, IntegrityError
from time import sleep
from .models import Person
from django.http import HttpResponse
def retry(count, interval=1):
def _retry(func):
def core(*args, **kwargs):
nonlocal count
if callable(count):
count = 2
for _ in range(count+1):
try:
return func(*args, **kwargs)
except DatabaseError as e:
print(e)
sleep(interval)
return core
if callable(count):
return _retry(count)
return _retry
# ...
For example, I have Person
model below. *I use PostgreSQL:
# "store/models.py"
class Person(models.Model):
name = models.CharField(max_length=30)
Then, test()
view with @retry(4, 2)
below can retry 4 times with 2 interval seconds in addition to a normal try each time IntegrityError
exception occurs as shown below:
# "store/views.py"
from django.db import DatabaseError, transaction, IntegrityError
from time import sleep
from .models import Person
from django.http import HttpResponse
def retry(count, interval=1):
# ...
@retry(4, 2) # Here
@transaction.atomic
def test(request):
qs = Person.objects.get(name="John")
qs.name = "David"
qs.save()
raise IntegrityError("Exception occurs")
return HttpResponse("Test")
As you can see, test
view is retried 4 times in addition to a normal try, then finally ValueError
exception occurs because None
is returned:
Exception occurs # Normal try
Exception occurs # 1st retry
Exception occurs # 2nd retry
Exception occurs # 3rd retry
Exception occurs # 4th retry
Internal Server Error: /store/test/
Traceback (most recent call last):
File "C:UserskaiAppDataLocalProgramsPythonPython39libsite-packagesdjangocorehandlersexception.py", line 47, in inner
response = get_response(request)
File "C:UserskaiAppDataLocalProgramsPythonPython39libsite-packagesdjangocorehandlersbase.py", line 188, in _get_response
self.check_response(response, callback)
File "C:UserskaiAppDataLocalProgramsPythonPython39libsite-packagesdjangocorehandlersbase.py", line 309, in check_response
raise ValueError(
ValueError: The view store.views.core didn't return an HttpResponse object. It returned None instead.
[28/Dec/2022 16:42:41] "GET /store/test/ HTTP/1.1" 500 88950
And, transaction is run 5 times (The light blue one is a normal try and the yellow ones are 4 retries) according to the PostgreSQL query logs below. *You can check how to log PostgreSQL queries:
Be careful, if the order of the decorators is @transaction.atomic
then @retry(4, 2)
as shown below:
# "store/views.py"
# ...
@transaction.atomic # Here
@retry(4, 2) # Here
def test(request):
# ...
The error below occurs:
store.models.Person.DoesNotExist: Person matching query does not exist.
So, the order of the decorators must be @retry(4, 2)
then @transaction.atomic
as shown below:
# "store/views.py"
# ...
@retry(4, 2) # Here
@transaction.atomic # Here
def test(request):
# ...
And by default, @retry
retries test()
view 2 times with 1 interval second in addition to a normal try each time IntegrityError
exception occurs as shown below:
# "store/views.py"
# ...
@retry # Here
@transaction.atomic
def test(request):
# ...
As you can see, test
view is retried 2 times in addition to a normal try, then finally ValueError
exception occurs because None
is returned:
Exception occurs # Normal try
Exception occurs # 1st retry
Exception occurs # 2nd retry
Internal Server Error: /store/test/
Traceback (most recent call last):
File "C:UserskaiAppDataLocalProgramsPythonPython39libsite-packagesdjangocorehandlersexception.py", line 47, in inner
response = get_response(request)
File "C:UserskaiAppDataLocalProgramsPythonPython39libsite-packagesdjangocorehandlersbase.py", line 188, in _get_response
self.check_response(response, callback)
File "C:UserskaiAppDataLocalProgramsPythonPython39libsite-packagesdjangocorehandlersbase.py", line 309, in check_response
raise ValueError(
ValueError: The view store.views.core didn't return an HttpResponse object. It returned None instead.
[28/Dec/2022 19:08:07] "GET /store/test/ HTTP/1.1" 500 88950
And, transaction is run 3 times (The light blue one is a normal try and the yellow ones are 2 retries) according to the PostgreSQL query logs below:
In addition, if you want to retry test()
view 3 times with 0.5 interval seconds by default with @retry
, you need to change count = 2
to count = 3
and interval=1
to interval=0.5
respectively as shown below:
# "store/views.py"
# ...
# ↓ Here
def retry(count, interval=0.5):
def _retry(func):
def core(*args, **kwargs):
nonlocal count
if callable(count):
count = 3 # <= Here
for _ in range(count+1):
try:
return func(*args, **kwargs)
except DatabaseError as e:
print(e)
sleep(interval)
return core
if callable(count):
return _retry(count)
return _retry
I want to rerun a Django view function if a certain exception is raised (in my case a serialization error in the underlying database). I want it to run with exactly the same parameters, including the same request
object – as if the client had re-requested the URL.
There are many database queries in the view, and the exception could be raised on any one of them – and it won’t work to rerun just one of the queries in isolation, so I think I need to wrap the entire view function in a try/except block and loop until success.
But, I have several view functions which might raise such exceptions, so I want a generic solution. I also want to be able to retry up to a certain number of times and then fail.
Is there any simple solution?
You could achieve this by writing a decorator:
def retry_on_exception(view):
def wrapper(*args, **kwargs):
while True:
try:
return view(*args, **kwargs):
except (TheExceptions, IWant, ToCatch):
pass
return wrapper
And use this on the view:
@retry_on_exception
def my_view(request, foo, bar):
return HttpResponse("My stuff")
Obviously, this will retry indefinitely, so a lot of logic could be improved there. You could also write the decorator to accept the exceptions it wants to look out for, so you can customise it per view.
You can use retry()
below to retry a view 0 or more times with 0 or more interval seconds:
# "store/views.py"
from django.db import DatabaseError, transaction, IntegrityError
from time import sleep
from .models import Person
from django.http import HttpResponse
def retry(count, interval=1):
def _retry(func):
def core(*args, **kwargs):
nonlocal count
if callable(count):
count = 2
for _ in range(count+1):
try:
return func(*args, **kwargs)
except DatabaseError as e:
print(e)
sleep(interval)
return core
if callable(count):
return _retry(count)
return _retry
# ...
For example, I have Person
model below. *I use PostgreSQL:
# "store/models.py"
class Person(models.Model):
name = models.CharField(max_length=30)
Then, test()
view with @retry(4, 2)
below can retry 4 times with 2 interval seconds in addition to a normal try each time IntegrityError
exception occurs as shown below:
# "store/views.py"
from django.db import DatabaseError, transaction, IntegrityError
from time import sleep
from .models import Person
from django.http import HttpResponse
def retry(count, interval=1):
# ...
@retry(4, 2) # Here
@transaction.atomic
def test(request):
qs = Person.objects.get(name="John")
qs.name = "David"
qs.save()
raise IntegrityError("Exception occurs")
return HttpResponse("Test")
As you can see, test
view is retried 4 times in addition to a normal try, then finally ValueError
exception occurs because None
is returned:
Exception occurs # Normal try
Exception occurs # 1st retry
Exception occurs # 2nd retry
Exception occurs # 3rd retry
Exception occurs # 4th retry
Internal Server Error: /store/test/
Traceback (most recent call last):
File "C:UserskaiAppDataLocalProgramsPythonPython39libsite-packagesdjangocorehandlersexception.py", line 47, in inner
response = get_response(request)
File "C:UserskaiAppDataLocalProgramsPythonPython39libsite-packagesdjangocorehandlersbase.py", line 188, in _get_response
self.check_response(response, callback)
File "C:UserskaiAppDataLocalProgramsPythonPython39libsite-packagesdjangocorehandlersbase.py", line 309, in check_response
raise ValueError(
ValueError: The view store.views.core didn't return an HttpResponse object. It returned None instead.
[28/Dec/2022 16:42:41] "GET /store/test/ HTTP/1.1" 500 88950
And, transaction is run 5 times (The light blue one is a normal try and the yellow ones are 4 retries) according to the PostgreSQL query logs below. *You can check how to log PostgreSQL queries:
Be careful, if the order of the decorators is @transaction.atomic
then @retry(4, 2)
as shown below:
# "store/views.py"
# ...
@transaction.atomic # Here
@retry(4, 2) # Here
def test(request):
# ...
The error below occurs:
store.models.Person.DoesNotExist: Person matching query does not exist.
So, the order of the decorators must be @retry(4, 2)
then @transaction.atomic
as shown below:
# "store/views.py"
# ...
@retry(4, 2) # Here
@transaction.atomic # Here
def test(request):
# ...
And by default, @retry
retries test()
view 2 times with 1 interval second in addition to a normal try each time IntegrityError
exception occurs as shown below:
# "store/views.py"
# ...
@retry # Here
@transaction.atomic
def test(request):
# ...
As you can see, test
view is retried 2 times in addition to a normal try, then finally ValueError
exception occurs because None
is returned:
Exception occurs # Normal try
Exception occurs # 1st retry
Exception occurs # 2nd retry
Internal Server Error: /store/test/
Traceback (most recent call last):
File "C:UserskaiAppDataLocalProgramsPythonPython39libsite-packagesdjangocorehandlersexception.py", line 47, in inner
response = get_response(request)
File "C:UserskaiAppDataLocalProgramsPythonPython39libsite-packagesdjangocorehandlersbase.py", line 188, in _get_response
self.check_response(response, callback)
File "C:UserskaiAppDataLocalProgramsPythonPython39libsite-packagesdjangocorehandlersbase.py", line 309, in check_response
raise ValueError(
ValueError: The view store.views.core didn't return an HttpResponse object. It returned None instead.
[28/Dec/2022 19:08:07] "GET /store/test/ HTTP/1.1" 500 88950
And, transaction is run 3 times (The light blue one is a normal try and the yellow ones are 2 retries) according to the PostgreSQL query logs below:
In addition, if you want to retry test()
view 3 times with 0.5 interval seconds by default with @retry
, you need to change count = 2
to count = 3
and interval=1
to interval=0.5
respectively as shown below:
# "store/views.py"
# ...
# ↓ Here
def retry(count, interval=0.5):
def _retry(func):
def core(*args, **kwargs):
nonlocal count
if callable(count):
count = 3 # <= Here
for _ in range(count+1):
try:
return func(*args, **kwargs)
except DatabaseError as e:
print(e)
sleep(interval)
return core
if callable(count):
return _retry(count)
return _retry