Skip to content

Batch requests

The Driver app sends its write requests one by one while the network keeps up, and in batches once a queue has built up: positions, stop transitions, service events. A batch goes out in a single call, which the server runs one sub-request after the other before answering all of them at once.

Endpoint Role
POST /nav-data-processor-api/batch Runs a batch of sub-requests and answers each of them

The endpoint is not versioned and exists only in the internal spec: the Driver app is its only client.

The request

The body is multipart/form-data or application/x-www-form-urlencoded. Each sub-request is a group of fields sharing the same prefix before their last dot. The Driver app names its groups request[0], request[1]… The server runs the groups in the order their first field appears.

Field Role
method HTTP method of the sub-request (required)
path Full path, prefix included, for example /nav-data-processor-api/v1/groups/<group_id>/devices/<device_id>/location (required)
queryString Query parameters, without the leading ?
contentType, body Type and content of the body, as a text field or a file part
id Identifier echoed in the response; defaults to the position of the group in the batch, starting at 0
continueOnError, continueOnErrorStatus Carry on with the batch after an answer outside 2xx: always (true), or for the listed statuses, comma-separated
idempotencyKey Idempotency key of the sub-request, passed to its route as the Idempotency-Key header (see Resent requests)

A sub-request inherits the headers of the batch (Authorization, User-Agent…), except Content-Type, Content-Length, Connection, Idempotency-Key and x-pysae-api-request-id: the last two name a single request, and shared by the whole batch they would answer the second sub-request with the response of the first. Its target route authorizes it like a direct call, minimum Driver version included: the batch itself requires no role.

A body that is missing, unreadable or of another type gets a 400 INVALID_BATCH_REQUEST, whose accepted_content_types field lists the accepted types.

The batch budget

The timeout parameter gives the delay after which the client gives up on the call, in seconds. It defaults to 60, the delay of the Driver app. A value that is not a finite positive number falls back to that default.

The server spends 75 % of that delay on the sub-requests, 45 s by default, and keeps the rest to send the response back. Before running a sub-request, it projects its duration: the longest sub-request of the batch so far, plus 50 %. When the time already spent plus that projection exceeds the budget, it runs neither this sub-request nor the next ones. The first sub-request always runs, as there is nothing to measure yet.

A sub-request that started is awaited at most until the end of the budget. When it takes longer, the server answers without it and starts none of the next ones. It is not cancelled for all that: it runs to its end in the background, and its effect is applied once.

The response

The response is always a 200 in multipart/form-data, with one group of fields per sub-request, prefixed response[<id>].

Field Role
id Identifier of the sub-request
status, contentType, body Answer of a sub-request that ran; body is absent for a status without a body (204, 304…)
skipped true for a sub-request the server does not answer for: the client sends it again
skipReason Why a sub-request is skipped (see below)

A skipped sub-request carries no status, contentType nor body. Its skipReason field gives the cause:

skipReason Cause Applied?
budget Not started, for lack of budget No
previous_error Not started, after an earlier sub-request answered outside 2xx without continueOnError nor its status in continueOnErrorStatus No
interrupted Started, but not finished within the budget Maybe: it ends in the background

In all three cases, the client keeps the sub-request queued and sends it again next time. An interrupted sub-request may have been applied: a route declaring idempotency then answers its resend with the original response, or with a 503 IDEMPOTENCY_KEY_IN_PROGRESS while it still runs (see Resent requests).

A response sent after the client delay is logged as an error by the server, with the detail of every sub-request. The client does not read it, and sends the whole batch again.

Resent requests

A resent batch holds sub-requests the server already applied. The routes declaring idempotency recognize them and answer their original response without running them again: they are all the write routes Driver queues and replays.

A request is recognized by its key:

  • the idempotency key sent by the client, in the Idempotency-Key header of a direct call, or in the idempotencyKey field of a sub-request. It is generated once per request, when it is queued, and stays the same from one attempt to the next;
  • otherwise, a key the route derives from the request, when it has one. A timestamp is reduced to the millisecond in it.

The on-demand service routes also accept the idempotency key in the x-pysae-api-request-id header, read when Idempotency-Key is absent.

Route prefix: /nav-data-processor-api/<v>/groups/<group_id>/devices/<device_id>.

Route Key derived from the request
PUT …/location The device and ts
POST …/passenger-count The device, trip, stop_sequence and passenger_count: a recount with other figures is applied
POST …/duties The device and the _id of the duty, generated by Driver
POST …/duties/<duty_id>/checkout The duty and ts
POST …/trip-events The device, plan_id, trip_id, trip_date, event_type, date and content, only when the client sends date
POST …/on-demand-stop-events The device, route_id, stop_id, type and ts
POST …/on-demand-passenger-counts None: the idempotency key is required, as two identical counts at one passage are legitimate
PUT and DELETE …/current-trip, PUT …/current-trip/details, PUT and DELETE …/trip-filter, PUT …/location-metadata, PUT …/system-metadata, PUT …/vehicle-load None: their content cannot tell a copy from a return to an earlier state. Without an idempotency key, the request runs as before

Depending on the state of the key, the route answers:

Case Response
New key The route's own, which runs
Request already run successfully The original status and body, with the Idempotent-Replayed: true header on a direct call
Request still running 503 IDEMPOTENCY_KEY_IN_PROGRESS, with a Retry-After header: send it again later
Idempotency key reused with another content 422 IDEMPOTENCY_KEY_MISMATCH

Only a 2xx response is remembered: a request that failed, or was interrupted, runs again when resent. A request is recognized for 24 h. The keys live in Redis only: a copy arriving after its key was lost runs again.

The Idempotent-Replayed header does not appear in the response of a sub-request: the batch only passes on its status, type and body.

A position no newer than the device's, whether resent after its key was lost or never seen before, does not move the device and counts no kilometre.