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-Keyheader of a direct call, or in theidempotencyKeyfield 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.