Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 61 additions & 1 deletion app/Http/Controllers/Api/WorkerController.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
use Workflow\V2\Support\StickyExecution;
use Workflow\V2\Support\WorkerProtocolVersion;
use Workflow\V2\Support\WorkflowCommandNormalizer;
use Workflow\V2\Support\WorkflowExecutor;
use Workflow\V2\Support\WorkflowStepHistory;
use Workflow\V2\Support\WorkflowTaskOwnership;

Expand Down Expand Up @@ -3764,6 +3765,10 @@
return $outcome;
}

if ($outcome['reason'] === 'run_timed_out') {
return $this->workflowTaskTimeoutResponse($taskId, (int) $validated['workflow_task_attempt'], $outcome);
}

return WorkerProtocol::json([
'task_id' => $taskId,
'workflow_task_attempt' => (int) $validated['workflow_task_attempt'],
Expand All @@ -3784,21 +3789,30 @@
$taskId,
$validated,
): array|JsonResponse {
return DB::transaction(function () use (

Check warning on line 3792 in app/Http/Controllers/Api/WorkerController.php

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

This function has 4 returns, which is more than the 3 allowed.

See more on https://sonarcloud.io/project/issues?id=durable-workflow_server&issues=AaGYrEqJ1v_hAsJMwDg6&open=AaGYrEqJ1v_hAsJMwDg6&pullRequest=924
$bridge,
$request,
$namespace,
$taskId,
$validated,
): array|JsonResponse {
if (! NamespaceWorkflowScope::lockTaskForMutation($namespace, $taskId) instanceof WorkflowTask) {
$task = NamespaceWorkflowScope::lockTaskForMutation($namespace, $taskId);
if (! $task instanceof WorkflowTask) {
return $this->workflowTaskNotFound($taskId, (int) $validated['workflow_task_attempt']);
}
if ($response = $this->guardWorkflowTaskOwnership($request, $namespace, $taskId,
(int) $validated['workflow_task_attempt'], $validated['lease_owner'])) {
return $response;
}

$run = WorkflowRun::query()->lockForUpdate()->find($task->workflow_run_id);
if ($run instanceof WorkflowRun) {
$timeout = $this->workflowTaskDeadlineOutcome($run, $task);
if ($timeout !== null) {
return $timeout;
}
}

$outcome = $bridge->fail($taskId, $validated['failure']);
$nextTaskId = is_string($outcome['next_task_id'] ?? null)
? $outcome['next_task_id']
Expand Down Expand Up @@ -3845,6 +3859,10 @@
? $outcome['next_task_id']
: null;

if ($outcome['reason'] === 'run_timed_out') {
return $this->workflowTaskTimeoutResponse($taskId, (int) $validated['workflow_task_attempt'], $outcome);
}

return WorkerProtocol::json([
'task_id' => $taskId,
'workflow_task_attempt' => (int) $validated['workflow_task_attempt'],
Expand All @@ -3855,6 +3873,41 @@
], $this->workflowOutcomeStatus($outcome['reason']));
}

/** @param array<string, mixed> $outcome */
private function workflowTaskTimeoutResponse(string $taskId, int $attempt, array $outcome): JsonResponse
{
return WorkerProtocol::json([
'task_id' => $taskId,
'workflow_task_attempt' => $attempt,
'outcome' => 'completed',
'recorded' => false,
'run_id' => $outcome['run_id'],
'run_status' => $outcome['run_status'],
'created_task_ids' => [],
'reason' => 'run_timed_out',
], 409);
}

/** @return array<string, mixed>|null */
private function workflowTaskDeadlineOutcome(WorkflowRun $run, WorkflowTask $task): ?array
{
if ($task->task_type !== TaskType::Workflow || $run->status->isTerminal()) {
return null;
}
$now = now();
$expired = ($run->execution_deadline_at !== null && $now->gte($run->execution_deadline_at))
|| ($run->run_deadline_at !== null && $now->gte($run->run_deadline_at));

// Both callers hold the task/run locks after revalidating ownership.
// Avoid loading the full run history for ordinary, unexpired waits.
if ($expired && app(WorkflowExecutor::class)->timeoutIfDeadlineExpired($run, $task)) {
return ['recorded' => false, 'reason' => 'run_timed_out', 'next_task_id' => null,
'run_id' => $run->id, 'run_status' => $run->status->value];
}

return null;
}

/**
* @param array<string, mixed> $failure
*/
Expand Down Expand Up @@ -3968,6 +4021,11 @@
return ['recorded' => false, 'reason' => 'run_already_closed', 'next_task_id' => null];
}

$timeout = $this->workflowTaskDeadlineOutcome($run, $task);
if ($timeout !== null) {
return $timeout;
}

if ($this->workflowTaskResumesSignal($task)) {
$bridgeOutcome = $bridge->complete($taskId, []);

Expand All @@ -3978,6 +4036,8 @@
? $bridgeOutcome['reason']
: 'workflow_bridge_completion_failed',
'next_task_id' => null,
'run_id' => $bridgeOutcome['workflow_run_id'] ?? null,
'run_status' => $bridgeOutcome['run_status'] ?? null,
];
}

Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
},
"extra": {
"durable-workflow": {
"product-train": "2.11.8"
"product-train": "2.11.9"
},
"laravel": {
"dont-discover": []
Expand Down
2 changes: 1 addition & 1 deletion composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions docker-compose.dedicated-matching.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,13 @@ name: durable-workflow-server
# daemon reports `shape: dedicated`.

# Generated by scripts/ci/sync-source-release.mjs. Do not edit the fallback.
x-server-image: &server-image ${DW_SERVER_IMAGE:-durableworkflow/server:${DW_SERVER_TAG:-2.11.8}}
x-server-image: &server-image ${DW_SERVER_IMAGE:-durableworkflow/server:${DW_SERVER_TAG:-2.11.9}}

x-server-environment: &server-environment
APP_NAME: "Durable Workflow Server"
APP_ENV: ${APP_ENV:-local}
DW_SERVER_KEY: ${DW_SERVER_KEY:-}
APP_VERSION: ${APP_VERSION:-${DW_SERVER_TAG:-2.11.8}}
APP_VERSION: ${APP_VERSION:-${DW_SERVER_TAG:-2.11.9}}
APP_DEBUG: ${APP_DEBUG:-false}
DB_CONNECTION: mysql
DB_HOST: mysql
Expand Down
4 changes: 2 additions & 2 deletions docker-compose.memo-rolling.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,14 @@ services:
command: ["server-bootstrap"]
environment:
<<: *runtime-environment
APP_VERSION: ${APP_VERSION:-2.11.8}
APP_VERSION: ${APP_VERSION:-2.11.9}

successor:
image: ${DW_MEMO_SUCCESSOR_IMAGE:-durable-workflow/server-memo-rolling:local}
ports: !override []
environment:
<<: *runtime-environment
APP_VERSION: ${APP_VERSION:-2.11.8}
APP_VERSION: ${APP_VERSION:-2.11.9}
DW_SERVER_ID: memo-successor
DW_SERVER_TOPOLOGY_SHAPE: standalone_server
DW_SERVER_PROCESS_CLASS: server_http_node
Expand Down
4 changes: 2 additions & 2 deletions docker-compose.php.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ name: durable-workflow-server
# conformance runners that qualify PHP releases.
#
# Generated by scripts/ci/sync-source-release.mjs. Do not edit the fallback.
x-server-image: &server-image ${DW_SERVER_IMAGE:-durableworkflow/server:${DW_SERVER_TAG:-2.11.8}}
x-server-image: &server-image ${DW_SERVER_IMAGE:-durableworkflow/server:${DW_SERVER_TAG:-2.11.9}}

x-server-environment: &server-environment
APP_NAME: "Durable Workflow Server"
APP_ENV: ${APP_ENV:-local}
DW_SERVER_KEY: ${DW_SERVER_KEY:-}
APP_VERSION: ${APP_VERSION:-${DW_SERVER_TAG:-2.11.8}}
APP_VERSION: ${APP_VERSION:-${DW_SERVER_TAG:-2.11.9}}
APP_DEBUG: ${APP_DEBUG:-false}
LOG_CHANNEL: ${LOG_CHANNEL:-stderr}
LOG_LEVEL: ${LOG_LEVEL:-info}
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.small-cluster.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ x-server-build: &server-build
x-server-environment: &server-environment
APP_NAME: "Durable Workflow Server"
APP_ENV: testing
APP_VERSION: ${APP_VERSION:-2.11.8}
APP_VERSION: ${APP_VERSION:-2.11.9}
APP_DEBUG: "false"
DW_SERVER_KEY: ${DW_SERVER_KEY:-base64:5Zt4nUhlCm3DD0nLXZJQdHiwPfb56yGo9gNV/g3jYbY=}
DB_CONNECTION: ${DW_SMALL_CLUSTER_DB:-mysql}
Expand Down
8 changes: 4 additions & 4 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ services:
DW_SERVER_KEY: "${DW_SERVER_KEY:-}"
DW_SERVER_TOPOLOGY_SHAPE: standalone_server
DW_SERVER_PROCESS_CLASS: server_http_node
APP_VERSION: "${APP_VERSION:-2.11.8}"
APP_VERSION: "${APP_VERSION:-2.11.9}"
APP_DEBUG: "false"
DB_CONNECTION: mysql
DB_HOST: mysql
Expand Down Expand Up @@ -62,7 +62,7 @@ services:
APP_NAME: "Durable Workflow Server"
APP_ENV: local
DW_SERVER_KEY: "${DW_SERVER_KEY:-}"
APP_VERSION: "${APP_VERSION:-2.11.8}"
APP_VERSION: "${APP_VERSION:-2.11.9}"
APP_DEBUG: "false"
DB_CONNECTION: mysql
DB_HOST: mysql
Expand Down Expand Up @@ -124,7 +124,7 @@ services:
DW_SERVER_KEY: "${DW_SERVER_KEY:-}"
DW_SERVER_TOPOLOGY_SHAPE: standalone_server
DW_SERVER_PROCESS_CLASS: worker_node
APP_VERSION: "${APP_VERSION:-2.11.8}"
APP_VERSION: "${APP_VERSION:-2.11.9}"
DB_CONNECTION: mysql
DB_HOST: mysql
DB_PORT: 3306
Expand Down Expand Up @@ -178,7 +178,7 @@ services:
DW_SERVER_KEY: "${DW_SERVER_KEY:-}"
DW_SERVER_TOPOLOGY_SHAPE: standalone_server
DW_SERVER_PROCESS_CLASS: scheduler_node
APP_VERSION: "${APP_VERSION:-2.11.8}"
APP_VERSION: "${APP_VERSION:-2.11.9}"
DB_CONNECTION: mysql
DB_HOST: mysql
DB_PORT: 3306
Expand Down
31 changes: 31 additions & 0 deletions docs/contracts/nexus.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,37 @@ The execute, describe, and cancel surfaces require the operator role.
Catalog mutations require admin. The caller-history surface requires
operator and is read-only.

## Workflow bindings

PHP Server can bind catalog operations to an SDK-worker workflow without loading
that workflow's implementation into the Server process:

| Binding | Target |
|---|---|
| `start_workflow` | Workflow type, with optional `workflow_instance_id` and `queue` in `handler_binding`. |
| `signal_workflow` | Existing workflow instance and declared signal name. |
| `update_workflow` | Existing workflow instance and declared update name. |
| `query_workflow` | Existing workflow instance and declared query name. |

The start uses the same task-queue resolution and any promoted worker build as a
normal workflow start. Its durable command declarations come from that selected
namespace, queue, workflow type and build cohort. A different queue's handler
registration cannot supply the target run's update or query contract. Register
the target worker and its declarations before starting the workflow. See
[worker command declarations](../worker-command-contracts.md).

Use the SDK's Avro argument encoding. A completed query or update exposes its
value in `result_envelope`, including null, false and zero. An asynchronous
update may initially return a started call without a result. Retain its
`service_call_id` and use the describe route until the call is terminal; replay
the same idempotency key for the same intent rather than creating another call.

When its captured contract rejects an update argument, Nexus returns a typed handler failure with
`accepted: false` and HTTP 409, such as `invalid_update_arguments`. The rejected
command remains available for inspection. This differs from request validation,
such as an invalid top-level zero completion wait, which returns HTTP 422 before
admission. Use the call and rejected command to inspect the refusal.

## Durable record and retry semantics

Workflow task completion derives caller namespace and run/instance identity from
Expand Down
17 changes: 17 additions & 0 deletions docs/worker-command-contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,20 @@ selected build cohort. A later registration applies to new admissions and
preserves the original run's durable snapshot. Existing runs with missing
metadata remain unavailable until an explicit supported recovery can establish
their original contract.

Workflow-bound Nexus starts, updates and queries use the same declarations as
the ordinary workflow routes. [Nexus workflow bindings](contracts/nexus.md#workflow-bindings)
describe queue selection, result observation and typed refusals.

An update's durable identity and completion record distinguish it from a second
accepted update. SDKs that reconstruct workflow state may invoke its recorded
handler again during replay, including replay for a query. Keep replayed
handlers deterministic and use durable workflow operations for external
effects. A callback entry count alone does not prove duplicate admission or
exactly-once external execution.

When a failure or history-wait acknowledgment closes an expired run, Server
returns the normal timeout completion receipt: HTTP 409 `run_timed_out`, `outcome=completed`
and `recorded=false`, with the original task, run and attempt identities and
`run_status=failed`. Workers can recognize that matching terminal receipt and
continue processing other workflows.
10 changes: 5 additions & 5 deletions k8s/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ The checked-in manifests are synchronized with the repository's stable source
release and pin its Docker Hub tag:

```text
durableworkflow/server:2.11.8
durableworkflow/server:2.11.9
```

## Application logs
Expand Down Expand Up @@ -42,15 +42,15 @@ digest you intend to run:

```bash
kubectl set image -n durable-workflow deploy/durable-workflow-server \
server=durableworkflow/server:2.11.8
server=durableworkflow/server:2.11.9
kubectl set image -n durable-workflow deploy/durable-workflow-worker \
worker=durableworkflow/server:2.11.8
worker=durableworkflow/server:2.11.9
kubectl set image -n durable-workflow cronjob/durable-workflow-scheduler \
scheduler=durableworkflow/server:2.11.8
scheduler=durableworkflow/server:2.11.9
```

GitHub Container Registry publishes the same release line at
`ghcr.io/durable-workflow/server:2.11.8`. Digest pinning is preferred for strict
`ghcr.io/durable-workflow/server:2.11.9`. Digest pinning is preferred for strict
change control.

The manifests expect you to provide:
Expand Down
6 changes: 3 additions & 3 deletions k8s/helm/durable-workflow/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ type: application
# The chart's own semver version. Bumped on every chart release; treated as
# independent of the server image version (appVersion). Breaking-change rules
# for this version live in docs/UPGRADING.md alongside the chart.
version: 0.3.8
version: 0.3.9
# The immutable Durable Workflow Server identity this chart release packages.
# The onboarding default in values.yaml and appVersion are generated from the
# checked-in source release record.
appVersion: "2.11.8"
appVersion: "2.11.9"
kubeVersion: ">=1.27.0-0"
home: https://durable-workflow.github.io/docs/2.0/deployment
sources:
Expand All @@ -30,7 +30,7 @@ annotations:
# exact commit that most recently changed the packaged chart.
org.opencontainers.image.source: https://github.com/durable-workflow/server
dev.durable-workflow.source-revision: "unreleased"
dev.durable-workflow.image-reference: "docker.io/durableworkflow/server:2.11.8"
dev.durable-workflow.image-reference: "docker.io/durableworkflow/server:2.11.9"
artifacthub.io/license: MIT
artifacthub.io/category: integration-delivery
# Free-form changelog for the current chart release shown by Artifact Hub.
Expand Down
2 changes: 1 addition & 1 deletion k8s/helm/durable-workflow/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ helm install durable-workflow ./k8s/helm/durable-workflow \

```yaml
image:
tag: "2.11.8"
tag: "2.11.9"
# Pin a digest in production:
# digest: "sha256:abc123..."
# memoPayloadStorage: "raw-json-v1" # Required for a digest or custom image.
Expand Down
2 changes: 1 addition & 1 deletion k8s/helm/durable-workflow/ci/existing-secrets-values.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# CI fixture: GitOps / externally-managed-secret path. The chart consumes
# existing Secrets and renders no Secret resources of its own.
image:
tag: "2.11.8"
tag: "2.11.9"

externalDatabase:
connection: pgsql
Expand Down
2 changes: 1 addition & 1 deletion k8s/helm/durable-workflow/ci/ingress-and-hpa-values.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# CI fixture: ingress + autoscaling enabled. Exercises optional templates.
image:
tag: "2.11.8"
tag: "2.11.9"

externalDatabase:
connection: mysql
Expand Down
2 changes: 1 addition & 1 deletion k8s/helm/durable-workflow/ci/inline-secrets-values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# chart's render path is exercised end-to-end. Real deployments should use
# existingSecret instead.
image:
tag: "2.11.8"
tag: "2.11.9"

externalDatabase:
connection: mysql
Expand Down
2 changes: 1 addition & 1 deletion k8s/helm/durable-workflow/templates/_helpers.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ resolved by an explicit capability declaration or an existing workload marker.
{{- define "durable-workflow.memoPayloadStorageForImage" -}}
{{- $image := toString . -}}
{{- $normalized := regexReplaceAll "^index\\.docker\\.io/" $image "docker.io/" -}}
{{- if eq $normalized "docker.io/durableworkflow/server:2.11.8" -}}
{{- if eq $normalized "docker.io/durableworkflow/server:2.11.9" -}}
dual-v1
{{- else if regexMatch "^docker\\.io/durableworkflow/server:2\\.0\\.0-rc\\.[0-9]+$" $normalized -}}
{{- $releaseCandidate := atoi (regexFind "[0-9]+$" $normalized) -}}
Expand Down
2 changes: 1 addition & 1 deletion k8s/helm/durable-workflow/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ image:
registry: docker.io
repository: durableworkflow/server
# Generated by scripts/ci/sync-source-release.mjs. Do not edit this default.
tag: "2.11.8"
tag: "2.11.9"
# Optional digest pin. When set, takes precedence over tag for change control.
# Example: "sha256:abc123..."
digest: ""
Expand Down
Loading
Loading