Pulp Engine v0.16.0 — Release Notes
Optimistic concurrency protection for template mutations
Summary
Prior to this release, all template mutation endpoints (PUT, DELETE, and version restore) used last-write-wins semantics. Two concurrent editors could each load the same template, modify their copies, and both save successfully — the second write would silently overwrite the first with no indication that a conflict had occurred.
v0.16.0 introduces an If-Match precondition on all mutating endpoints, making concurrent write conflicts explicit and recoverable. The version check is performed atomically as part of the write path — there is no TOCTOU window.
Concurrency matrix
| Endpoint | Precondition | Missing header | Malformed | Stale version | Unknown key | Success |
|---|---|---|---|---|---|---|
POST /templates (create) | None required | — | — | — | — | 201 as before |
PUT /templates/:key | If-Match: "1.0.3" | 428 | 400 | 412 | 404 | 200 + ETag |
DELETE /templates/:key | If-Match: "1.0.3" | 428 | 400 | 412 | 404 | 204 |
POST /templates/:key/versions/:version/restore | If-Match: "1.0.3" | 428 | 400 | 412 | 404 | 200 + ETag |
New response headers
GET /templates/:key now returns:
ETag: "1.0.3"
PUT /templates/:key and POST /templates/:key/versions/:version/restore now return an ETag header reflecting the new version after a successful write.
Status codes
| Code | Meaning |
|---|---|
428 Precondition Required | If-Match header absent |
400 Bad Request | If-Match present but malformed (*, W/"...", multi-value, unquoted) |
412 Precondition Failed | Version mismatch — another writer has already modified this template |
412 response body:
{
"error": "Precondition Failed",
"message": "Template \"loan-approval-letter\" was modified after you last loaded it (expected v1.0.3, current v1.0.5). Reload and re-apply your changes."
}
Breaking change
PUT /templates/:key, DELETE /templates/:key, and POST /templates/:key/versions/:version/restore now require an If-Match header.
Existing callers that omit the header will receive 428 Precondition Required instead of proceeding.
Migration for API consumers
Before this release:
PUT /templates/my-template
Content-Type: application/json
{ ... }
After this release:
GET /templates/my-template
# Response: ETag: "1.0.3"
PUT /templates/my-template
Content-Type: application/json
If-Match: "1.0.3"
{ ... }
# Response: 200 OK, ETag: "1.0.4"
The pattern is: GET first (or retain the ETag / currentVersion from your previous successful write), then PUT with If-Match. The value to use is the ETag from GET, or the currentVersion field in any response body.
If the write returns 412, another writer has advanced the version. Fetch the latest state, re-apply your changes, and retry.
Atomicity guarantees
| Backend | Mechanism |
|---|---|
| File | Per-template in-process promise-queue mutex; read → check → write inside the lock |
| PostgreSQL | updateMany({ where: { key, currentVersion } }) — single atomic UPDATE with compound WHERE |
| SQL Server | UPDATE ... WHERE [key] = @key AND current_version = @expectedVersion + SELECT @@ROWCOUNT |
Soft-delete consistency fix
DELETE /templates/:key previously returned 204 silently for unknown keys in file and SQL Server modes (Postgres already returned 404). All three backends now return 404 Not Found for an unknown key, consistent with the concurrency matrix.
Editor changes
The visual editor has been updated automatically:
- Save (Update): the editor now sends
If-Matchwith the version loaded from the API. No change to the user workflow. - Save conflict: if another user writes the template between your load and your save, the editor shows:
Conflict: this template was modified by someone else. Reload from the API to see the latest version. - See docs/editor-guide.md for conflict recovery steps.
Internal-only callers
replaceDefinition (used by the seed script only, not exposed via HTTP) is unaffected. The expectedVersion parameter is optional on all ITemplateStore methods — existing internal callers that do not supply it continue to work without precondition checking.