Pulp Engine Document Rendering
Get started
Release v0.16.0

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

EndpointPreconditionMissing headerMalformedStale versionUnknown keySuccess
POST /templates (create)None required201 as before
PUT /templates/:keyIf-Match: "1.0.3"428400412404200 + ETag
DELETE /templates/:keyIf-Match: "1.0.3"428400412404204
POST /templates/:key/versions/:version/restoreIf-Match: "1.0.3"428400412404200 + 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

CodeMeaning
428 Precondition RequiredIf-Match header absent
400 Bad RequestIf-Match present but malformed (*, W/"...", multi-value, unquoted)
412 Precondition FailedVersion 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

BackendMechanism
FilePer-template in-process promise-queue mutex; read → check → write inside the lock
PostgreSQLupdateMany({ where: { key, currentVersion } }) — single atomic UPDATE with compound WHERE
SQL ServerUPDATE ... 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-Match with 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.