retry on exception 'database is locked' (#80538)
gitea/ants-hub/pipeline/head This commit looks good Details

This commit is contained in:
Benjamin Dauvergne 2023-08-24 17:27:07 +02:00
parent a988b26a41
commit 1927c358f7
3 changed files with 215 additions and 5 deletions

View File

@ -8,10 +8,13 @@ import functools
import json
import logging
import os
import time
import jsonschema
import requests
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import OperationalError
from django.db.models import Count
from django.db.transaction import atomic
from django.http import JsonResponse
@ -160,6 +163,7 @@ class RendezVousDisponibleView(View):
def handle_rdv_payload(self, lieu, rdv):
# cannot fail, as JSON schema is already validated
rdv = rdv.copy()
identifiant_predemande = rdv.pop('id').strip()
date = datetime.datetime.fromisoformat(rdv.pop('date'))
annule = bool(rdv.pop('annule', False))
@ -262,6 +266,7 @@ class RendezVousDisponibleView(View):
self.plage_deleted += count_by_model.get('data.Plage', 0)
def handle_lieu_payload(self, collectivite, payload):
payload = payload.copy()
source_id = payload.pop('id')
plages = payload.pop('plages', [])
rdvs = payload.pop('rdvs', [])
@ -289,6 +294,7 @@ class RendezVousDisponibleView(View):
return lieu
def handle_collectivite_payload(self, raccordement, payload):
payload = payload.copy()
source_id = payload.pop('id')
lieux_payload = payload.pop('lieux', [])
full = payload.pop('full', False)
@ -333,11 +339,31 @@ class RendezVousDisponibleView(View):
except jsonschema.ValidationError as e:
return JsonResponse({'err': 'invalid-json', 'detail': e.message}, status=400)
try:
with atomic():
# prevent concurrent updates to the data of the same raccordement
request.raccordement.lock()
for collectivite in payload.get('collectivites', []):
self.handle_collectivite_payload(request.raccordement, collectivite)
i = 0
while True:
i += 1
try:
with atomic():
# prevent concurrent updates to the data of the same raccordement
request.raccordement.lock()
for collectivite in payload.get('collectivites', []):
self.handle_collectivite_payload(request.raccordement, collectivite)
break
except OperationalError as e:
if 'database is locked' not in str(e):
raise
if i > 3:
logger.warning(
'rendez-vous-disponibles(%s) server is too busy "%s"', request.raccordement, e
)
return JsonResponse(
{
'err': 1,
'error': 'busy',
},
status=500,
)
time.sleep(settings.ANTS_HUB_BUSY_BACKOFF * 2**i)
except (ValueError, ValidationError) as e:
logger.warning('rendez-vous-disponibles(%s) received bad request "%s"', request.raccordement, e)
return JsonResponse(

View File

@ -116,6 +116,8 @@ ANTS_HUB_ADMIN_ROLE = None
# ANTS_HUB_API_URL
ANTS_HUB_API_URL = 'https://%s:@ants-hub.entrouvert.org/api/chrono/'
ANTS_HUB_BUSY_BACKOFF = 0.5
if 'ANTS_HUB_SETTINGS_FILE' in os.environ:
with open(os.environ['ANTS_HUB_SETTINGS_FILE']) as fd:

View File

@ -6,7 +6,9 @@ import zoneinfo
import pytest
import responses
import responses.matchers
from django.db import OperationalError
from ants_hub.api.views.chrono import RendezVousDisponibleView
from ants_hub.data.models import Collectivite, Config, Lieu, Plage, Raccordement, RendezVous
@ -1008,3 +1010,183 @@ def test_annulation_rdv_full(django_app, db):
assert rdv_response(response) == {
'rdv_deleted': 1,
}
def test_rendez_vous_disponibles_database_is_locked(django_app, db, monkeypatch, settings):
settings.ANTS_HUB_BUSY_BACKOFF = 0
Raccordement.objects.create(name='plateforme', apikey='abcd')
django_app.set_authorization(('Basic', ('abcd', '')))
assert django_app.get('/api/chrono/rendez-vous-disponibles/').json == {
'err': 0,
'collectivites': [],
}
assert Collectivite.objects.count() == 0
assert Lieu.objects.count() == 0
assert Plage.objects.count() == 0
assert RendezVous.objects.count() == 0
old_method = RendezVousDisponibleView.handle_collectivite_payload
call_count = 0
def new_method(self, raccordement, payload):
nonlocal call_count
call_count += 1
if call_count < 2:
raise OperationalError('database is locked')
return old_method(self, raccordement, payload)
monkeypatch.setattr(RendezVousDisponibleView, 'handle_collectivite_payload', new_method)
resp = django_app.post_json(
'/api/chrono/rendez-vous-disponibles/',
params={
'collectivites': [
{
'id': 'col1',
'nom': 'Saint-Didier',
'url': 'https://saint-didier.fr/rdv/',
'lieux': [
{
'id': 'lieu1',
'nom': 'Mairie de Saint-Didier',
'numero_rue': '2 rue du four',
'code_postal': '99999',
'ville': 'Saint-Didier',
'longitude': 1.5,
'latitude': 2.3,
'plages': [
{
'date': '2023-03-20',
'types_rdv': ['CNI', 'PASSPORT'],
'heure_debut': '08:00+02:00',
'heure_fin': '12:00+02:00',
'duree': 15,
'personnes': 1,
},
{
'date': '2023-03-21',
'types_rdv': ['CNI', 'PASSPORT'],
'heure_debut': '08:00+02:00',
'heure_fin': '12:00+02:00',
'duree': 15,
'personnes': 1,
},
{
'date': '2023-03-22',
'types_rdv': ['CNI', 'PASSPORT'],
'heure_debut': '08:00+02:00',
'heure_fin': '12:00+02:00',
'duree': 15,
'personnes': 1,
},
],
'rdvs': [
{
'id': 'abcd1',
'date': '2023-03-23T15:00:00+02:00',
},
{
'id': 'abcd2',
'date': '2023-03-24T15:00:00+02:00',
},
{
'id': 'abcd3',
'date': '2023-03-25T15:00:00+02:00',
},
],
},
{
'id': 'lieu2',
'nom': 'Mairie annexe de Saint-Didier',
'numero_rue': '3 rue du four',
'code_postal': '99999',
'ville': 'Saint-Didier',
'longitude': 1.5,
'latitude': 2.3,
'plages': [
{
'date': '2023-03-20',
'types_rdv': ['CNI', 'PASSPORT'],
'heure_debut': '08:00+02:00',
'heure_fin': '12:00+02:00',
'duree': 15,
'personnes': 1,
},
{
'date': '2023-03-21',
'types_rdv': ['CNI', 'PASSPORT'],
'heure_debut': '08:00+02:00',
'heure_fin': '12:00+02:00',
'duree': 15,
'personnes': 1,
},
{
'date': '2023-03-22',
'types_rdv': ['CNI', 'PASSPORT'],
'heure_debut': '08:00+02:00',
'heure_fin': '12:00+02:00',
'duree': 15,
'personnes': 1,
},
],
'rdvs': [
{
'id': 'abcd4',
'date': '2023-03-17T15:00:00+02:00',
},
{
'id': 'abcd5',
'date': '2023-03-18T15:00:00+02:00',
},
{
'id': 'abcd6',
'date': '2023-03-19T15:00:00+02:00',
},
],
},
],
}
]
},
)
assert call_count == 2
assert resp.json['err'] == 0
assert Collectivite.objects.count() == 1
assert Lieu.objects.count() == 2
assert Plage.objects.count() == 12
assert RendezVous.objects.count() == 6
def test_rendez_vous_disponibles_database_is_busy(django_app, db, monkeypatch, settings):
settings.ANTS_HUB_BUSY_BACKOFF = 0
Raccordement.objects.create(name='plateforme', apikey='abcd')
django_app.set_authorization(('Basic', ('abcd', '')))
call_count = 0
def new_method(self, raccordement, payload):
nonlocal call_count
call_count += 1
raise OperationalError('database is locked')
monkeypatch.setattr(RendezVousDisponibleView, 'handle_collectivite_payload', new_method)
resp = django_app.post_json(
'/api/chrono/rendez-vous-disponibles/',
params={
'collectivites': [
{
'id': 'col1',
'nom': 'Saint-Didier',
'url': 'https://saint-didier.fr/rdv/',
}
]
},
status=500,
)
assert call_count == 4
assert resp.json['err'] == 1
assert resp.json['error'] == 'busy'