From 80dc00e11c8c720a6c04688934818c06f3fa352b Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Fri, 13 Nov 2026 06:28:04 +0000 Subject: [PATCH] Prepare native 3.9.0 release notes and Compose default Release notes in 3.8.0's format: the schema upgrade for dw_command_intents on every backend, upgrade and backup-only rollback, the two behavior changes (request_id reuse with different input is a 409 request_id_conflict, unlike PHP 2.x; a run's own close settles its open activities and timers), storage admission, the other changes and the SDK releases. The published Compose default selects 3.9.0. Co-Authored-By: Claude Opus 5.5 --- docker-compose.published.yml | 2 +- docs/releases/3.9.0.md | 192 +++++++++++++++++++++++++++++++++++ 2 files changed, 193 insertions(+), 1 deletion(-) create mode 100644 docs/releases/3.9.0.md diff --git a/docker-compose.published.yml b/docker-compose.published.yml index 738229ec0..3282f6e50 100644 --- a/docker-compose.published.yml +++ b/docker-compose.published.yml @@ -16,7 +16,7 @@ name: durable-workflow-server # release whose default names another image). Set DW_SERVER_IMAGE to run # any other native image, by tag or digest; pin a digest for production # change control. -x-server-image: &server-image ${DW_SERVER_IMAGE:-durableworkflow/server:3.8.0} +x-server-image: &server-image ${DW_SERVER_IMAGE:-durableworkflow/server:3.9.0} services: server: diff --git a/docs/releases/3.9.0.md b/docs/releases/3.9.0.md new file mode 100644 index 000000000..80712d271 --- /dev/null +++ b/docs/releases/3.9.0.md @@ -0,0 +1,192 @@ +# Durable Workflow Server 3.9.0 (native) + +Server 3.9.0 makes a signal or update `request_id` a durable command key, has +a run's own close settle the activities and timers it left open, and honors +storage admission. Two of these change behavior a client can observe; see +[Behavior changes](#behavior-changes). Its exact images are +`durableworkflow/server:3.9.0` and `ghcr.io/durable-workflow/server:3.9.0`; +the published Compose default selects 3.9.0. Rolling aliases move separately +after published-image qualification. + +## Upgrade + +**Schema upgrade required on every backend.** This release adds one +native-only table, `dw_command_intents`, which is created empty: + +| Backend | Schema | +|------------|---------| +| SQLite | 12 → 13 | +| PostgreSQL | 13 → 14 | +| MariaDB | 13 → 14 | +| MySQL | 14 → 15 | + +The step only creates the table; it rewrites no rows and takes seconds. A +server never upgrades its schema at startup. Started on the earlier schema, +3.9.0 refuses with `native_schema_upgrade_required`. + +For the single-node Compose deployment: + +1. Stop the Server and leave MySQL running: + + ```bash + docker compose --env-file durable-workflow.prod.env \ + -f docker-compose.published.yml stop server + ``` +2. Back up MySQL and `server_storage`, and record the current image + reference. This backup is the only way back to 3.8.x. +3. Change `DW_SERVER_IMAGE` to the 3.9.0 image, pull it and run + `schema-upgrade` once: + + ```bash + docker compose --env-file durable-workflow.prod.env \ + -f docker-compose.published.yml pull server + docker compose --env-file durable-workflow.prod.env \ + -f docker-compose.published.yml run --rm --no-deps server schema-upgrade + ``` + + From 3.8.x on MySQL it reports + `"from_schema_version":14,"schema_version":15,"status":"upgraded"`. Run + again, it reports `"status":"current"`. +4. Start the Server with + `docker compose --env-file durable-workflow.prod.env -f docker-compose.published.yml up -d --wait`, + then confirm `/api/ready`, `/api/cluster/info` (`version` is `3.9.0`) and + worker registration. + +With several Servers on one database, stop every one of them before step 3 +and start them all on 3.9.0 afterwards. On MySQL and MariaDB, an interrupted +upgrade resumes when you run `schema-upgrade` again; SQLite and PostgreSQL +upgrade in one transaction. + +**Rolling back to 3.8.x requires restoring the backup.** 3.8.0 refuses the +upgraded database with `native_schema_newer_than_binary`. + +**From earlier releases,** the 3.9.0 image's `schema-upgrade` applies every +earlier step in the same run. From 3.7.x that includes the +[3.8.0 step](3.8.0.md#upgrade). On MySQL from 3.6.x or earlier (schema 12 or +below), it includes the [3.7.0 JSON rebuild](3.7.0.md#upgrade), with its +downtime of about 2 minutes per million history rows. + +On Kubernetes, scale the Server to zero, back up, run `helm upgrade` with +`native.schemaUpgrade.enabled=true` so the 3.9.0 image runs `schema-upgrade` +once, then scale back up. Chart 1.7.0, which defaults to Server 3.9.0, adds +its notes to the chart's +[upgrade notes](../../k8s/helm/durable-workflow/docs/UPGRADING.md). + +The worker protocol stays at `1.20` and the control plane at version `2`. + +## Behavior changes + +### A signal or update `request_id` is a durable command key + +A signal or update `request_id` now names one command on its workflow +instance ([#934](https://github.com/durable-workflow/server/issues/934)). +Cluster info advertises `capabilities.command_request_keys: true`. + +- A retry with the same command intent returns the original receipt, + including its `command_id`, `run_id` and, for an update, `update_id`, and + writes nothing. The receipt is found before any run gate, so a retry after + the run closed or continued as new still gets it. The intent is Core's + CommandIntent v1 digest over the command type, name, typed arguments and + explicit run target, so `1`, `1.0` and `"1"` differ and map key order does + not. +- The same key with a different name, arguments or run target is refused with + `409`, `reason: "request_id_conflict"` and + `outcome: "rejected_request_id_conflict"`. The response names the differing + part in `conflict` and the original command in `retained_command_id`. + Nothing is written. **Do not retry this 409 unchanged:** it means the key + was reused for different input. Send the original input to recover the + receipt, or use a new key. +- The key is stored hashed and the intent as its digest, never the arguments. + Both live as long as the command; retention removes them with the run's + details. + +**This differs from PHP 2.x,** which recorded a second signal for a repeated +signal `request_id` and returned the original update receipt for a retry with +different arguments. Clients that reuse one `request_id` for different input +get the 409 on native. sdk-php 3.17.0 surfaces it as a typed conflict +(`SignalFailed::conflict()` / `UpdateFailed::conflict()`), and the coming +sdk-rust 4.15.0 reports the key it sent; none of the SDKs resend on 409. See +[Behavior that differs from PHP 2.x](../rust-server-upgrade-takeover.md#behavior-that-differs-from-php-2x). + +### A run's own close settles its open activities and timers + +When a run's own code closes it with `complete_workflow`, `fail_workflow` or +`continue_as_new`, the close now cancels the activities and timers it still +has open, in the same transaction and before the terminal event, as Workflow +2.28.0 does ([#1193](https://github.com/durable-workflow/server/issues/1193), +Core workflow#1117). + +- Each open activity, such as a `select()` loser the workflow never awaited, + gets `ActivityCancelled` with `cancellation_reason: "run_closed"`. Each + pending timer, a wait's timeout timer included, gets `TimerCancelled`. + Nothing is appended after the terminal event. +- A late heartbeat or lease renewal for such an activity answers + `reason: "run_closed"` with `can_continue: false`. A late `complete` or + `fail` gets `409` with `outcome: "ignored"`, `recorded: false`, + `reason: "run_closed"` and `activity_status: "cancelled"`. Workers treat it + as final. +- Replay ignores an `ActivityCancelled` whose `cancellation_reason` is + `run_closed`; the workflow never observed it. sdk-php 3.17.0 does this, as do + sdk-python 2.17.0 and sdk-rust 4.15.0 (in release). +- Native refuses the `abandon` cancellation policy, so it never keeps an + activity running past its run's close. Children are unchanged: they follow + their parent-close policy. + +PHP 2.x left this work open, so the activity still ran and its history was +appended after the terminal event. **A workflow that needs a loser's effect +must await it before returning.** + +## Storage admission + +Native now honors the operator storage-pressure observation that PHP Server +documents ([#951](https://github.com/durable-workflow/server/issues/951)). +It is off unless `DW_STORAGE_ADMISSION_FILE`, `DW_STORAGE_ADMISSION_SOURCE` and +`DW_STORAGE_ADMISSION_MAX_AGE_SECONDS` (1 to 300) are set. + +- `normal` admits everything. `draining` refuses starts, signals, updates, + queries, registrations, uploads, polls and other new work with `503`, + `Retry-After: 5` and `storage_pressure`, while existing leases still + complete, fail, heartbeat and renew. `fenced`, and any missing, malformed, + stale, wrong-source or untrusted observation, keeps only reads. +- A long poll that is already waiting answers the poll refusal at its next + probe. The maintenance loop starts no pass under pressure. +- `/api/ready` reports pressure as not ready; `/api/health` keeps its + database meaning. Refusals never expose the file path or source. +- The file is opened without following symlinks and checked on the same + descriptor. Native does not yet serve the draining completion-upload + allowance, so an upload is refused like other new work; a worker keeps its + lease and uploads after pressure clears. + +See [Storage Admission](../storage-admission.md#native-server). + +## Other changes + +- Worker command names longer than their database columns are refused with + the typed `422 validation_failed` naming `commands.N.`, before any + write ([#607](https://github.com/durable-workflow/server/issues/607)): + `activity_type`, `signal_name`, `condition_key` and + `condition_definition_fingerprint` at 191 characters; `queue`, + `connection` and `workflow_type` at 255; a child's `workflow_type` and + `queue` at 191. Before, MySQL and PostgreSQL failed such a completion with a + database error and the task retried, while **SQLite stored the over-long + name**; on SQLite such a completion is now refused. +- `DW_MAX_HISTORY_EVENTS` has no effect, and the reference now says so + ([#504](https://github.com/durable-workflow/server/issues/504)). The Server + does not send `invocable_http` activity requests, and the reference now says + so too ([#527](https://github.com/durable-workflow/server/issues/527)). +- Native GitHub Releases carry the tagged release notes after the identity + table ([#1111](https://github.com/durable-workflow/server/issues/1111)). +- Tests now cover numeric-string names, selected-build routing and bounded + observation for declared worker command contracts + ([#436](https://github.com/durable-workflow/server/issues/436)). +- The two unproved APT paths in conformance and performance images run with + APT retries off (durable-workflow/.github#295). + +## SDKs + +- sdk-php 3.17.0 is out: typed `request_id` conflicts, replay that ignores + `run_closed` cancellations, and a one-time structural-limit failure for an + oversized completion. +- sdk-python 2.17.0 and sdk-rust 4.15.0 (in release) add the same handling: + `run_closed` replay, the structural-limit failure, and, in sdk-rust, the + key it sent on a `request_id` conflict.