retry on exception 'database is locked' (#80538)
gitea/ants-hub/pipeline/head This commit looks good
Details
gitea/ants-hub/pipeline/head This commit looks good
Details
This commit is contained in:
parent
a988b26a41
commit
1927c358f7
|
@ -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(
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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'
|
||||
|
|
Loading…
Reference in New Issue