Aller au contenu

Envoi groupé (batch)

L'application Driver envoie ses requêtes d'écriture une par une tant que le réseau suit, et par lots quand une file s'est accumulée : positions, transitions d'arrêt, événements de service. Un lot part en un seul appel, que le serveur exécute requête après requête avant de répondre pour toutes à la fois.

Endpoint Rôle
POST /nav-data-processor-api/batch Exécute un lot de sous-requêtes et répond pour chacune

L'endpoint n'est pas versionné et n'existe que dans la spec interne : l'application Driver en est le seul client.

La requête

Le corps est en multipart/form-data ou en application/x-www-form-urlencoded. Chaque sous-requête est un groupe de champs qui partagent le même préfixe avant leur dernier point. L'application Driver nomme ses groupes request[0], request[1]… Le serveur exécute les groupes dans l'ordre où apparaît leur premier champ.

Champ Rôle
method Méthode HTTP de la sous-requête (obligatoire)
path Chemin complet, préfixe compris, par exemple /nav-data-processor-api/v1/groups/<group_id>/devices/<device_id>/location (obligatoire)
queryString Paramètres de requête, sans le ? initial
contentType, body Type et contenu du corps, en champ texte ou en partie fichier
id Identifiant repris dans la réponse ; par défaut, la position du groupe dans le lot, à partir de 0
continueOnError, continueOnErrorStatus Poursuivre le lot après une réponse hors 2xx : toujours (true), ou pour les statuts listés, séparés par des virgules
idempotencyKey Clé d'idempotence de la sous-requête, transmise à sa route comme l'en-tête Idempotency-Key (voir Les renvois)

Une sous-requête reprend les en-têtes du lot (Authorization, User-Agent…), sauf Content-Type, Content-Length, Connection, Idempotency-Key et x-pysae-api-request-id : ces deux derniers désignent une seule requête, et partagés par tout le lot ils feraient répondre la deuxième sous-requête avec la réponse de la première. Sa route cible l'autorise comme un appel direct, version minimale de Driver comprise : le lot n'exige aucun rôle par lui-même.

Un corps absent, illisible ou d'un autre type reçoit une 400 INVALID_BATCH_REQUEST, dont le champ accepted_content_types liste les types acceptés.

Le budget du lot

Le paramètre timeout donne le délai au-delà duquel le client abandonne l'appel, en secondes. Il vaut 60 par défaut, le délai de l'application Driver. Une valeur qui n'est pas un nombre positif fini retombe sur ce défaut.

Le serveur consacre 75 % de ce délai aux sous-requêtes, soit 45 s par défaut, et garde le reste pour renvoyer la réponse. Avant de lancer une sous-requête, il estime sa durée : la plus longue sous-requête du lot jusque-là, majorée de 50 %. Si le temps déjà écoulé plus cette estimation dépasse le budget, il ne lance ni cette sous-requête ni les suivantes. La première sous-requête est toujours exécutée, faute de mesure.

Une sous-requête lancée est attendue au plus jusqu'à la fin du budget. Si elle dure plus longtemps, le serveur répond sans elle et ne lance pas les suivantes. Elle n'est pas annulée pour autant : elle va jusqu'à son terme en arrière-plan, et son effet est appliqué une seule fois.

La réponse

La réponse est toujours un 200 en multipart/form-data, avec un groupe de champs par sous-requête, préfixé response[<id>].

Champ Rôle
id Identifiant de la sous-requête
status, contentType, body Réponse d'une sous-requête exécutée ; body est absent pour un statut sans corps (204, 304…)
skipped true pour une sous-requête dont le serveur ne rend pas la réponse : le client la renvoie
skipReason Cause d'une sous-requête skipped (voir ci-dessous)

Une sous-requête skipped ne porte ni status, ni contentType, ni body. Son champ skipReason en donne la cause :

skipReason Cause Appliquée ?
budget Non lancée, faute de budget Non
previous_error Non lancée, après une sous-requête précédente qui a répondu hors 2xx sans continueOnError ni son statut dans continueOnErrorStatus Non
interrupted Lancée, mais pas finie dans le budget Peut-être : elle se termine en arrière-plan

Dans les trois cas, le client garde la sous-requête en file et la renvoie au prochain envoi. Une sous-requête interrupted a peut-être été appliquée : une route qui déclare l'idempotence rend alors sa réponse d'origine au renvoi, ou une 503 IDEMPOTENCY_KEY_IN_PROGRESS si elle tourne encore (voir Les renvois).

Une réponse envoyée après le délai du client est journalisée en erreur côté serveur, avec le détail de chaque sous-requête. Le client ne la lit pas, et renvoie le lot entier.

Les renvois

Un lot renvoyé contient des sous-requêtes que le serveur a déjà appliquées. Les routes qui déclarent l'idempotence les reconnaissent et rendent leur réponse d'origine sans les retraiter : ce sont toutes les routes d'écriture que Driver met en file et rejoue.

Une requête est reconnue par sa clé :

  • la clé d'idempotence envoyée par le client, dans l'en-tête Idempotency-Key d'un appel direct, ou dans le champ idempotencyKey d'une sous-requête. Elle est générée une fois par requête, à sa mise en file, et reste la même d'une tentative à l'autre ;
  • à défaut, une clé que la route tire de la requête, quand elle en a une. Un horodatage y est ramené à la milliseconde.

Les routes de service à la demande acceptent aussi la clé d'idempotence dans l'en-tête x-pysae-api-request-id, lu quand Idempotency-Key est absent.

Préfixe des routes : /nav-data-processor-api/<v>/groups/<group_id>/devices/<device_id>.

Route Clé tirée de la requête
PUT …/location L'appareil et ts
POST …/passenger-count L'appareil, trip, stop_sequence et passenger_count : un recomptage avec d'autres chiffres est appliqué
POST …/duties L'appareil et l'identifiant _id de la prise de service, généré par Driver
POST …/duties/<duty_id>/checkout La prise de service et ts
POST …/trip-events L'appareil, plan_id, trip_id, trip_date, event_type, date et content, seulement quand le client envoie date
POST …/on-demand-stop-events L'appareil, route_id, stop_id, type et ts
POST …/on-demand-passenger-counts Aucune : la clé d'idempotence est obligatoire, deux comptages identiques au même passage étant légitimes
PUT et DELETE …/current-trip, PUT …/current-trip/details, PUT et DELETE …/trip-filter, PUT …/location-metadata, PUT …/system-metadata, PUT …/vehicle-load Aucune : leur contenu ne distingue pas un renvoi d'un retour à un état précédent. Sans clé d'idempotence, la requête s'exécute comme avant

Selon l'état de la clé, la route répond :

Cas Réponse
Clé nouvelle Celle de la route, qui s'exécute
Requête déjà exécutée avec succès Le statut et le corps d'origine, avec l'en-tête Idempotent-Replayed: true sur un appel direct
Requête en cours d'exécution 503 IDEMPOTENCY_KEY_IN_PROGRESS, avec un en-tête Retry-After : la renvoyer plus tard
Clé d'idempotence réutilisée avec un autre contenu 422 IDEMPOTENCY_KEY_MISMATCH

Seule une réponse 2xx est mémorisée : une requête qui a échoué, ou qui a été interrompue, s'exécute de nouveau à son renvoi. Une requête est reconnue pendant 24 h. Les clés vivent dans Redis seul : une copie qui arrive après la perte de sa clé s'exécute de nouveau.

L'en-tête Idempotent-Replayed n'apparaît pas dans la réponse d'une sous-requête : le lot ne transmet que son statut, son type et son corps.

Une position qui n'est pas plus récente que celle de l'appareil, qu'elle soit renvoyée après la perte de sa clé ou jamais vue, ne déplace pas l'appareil et ne compte aucun kilomètre.