From 49ead7a0e59b165d888189874ec4b81903277826 Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Wed, 7 Oct 2026 12:22:33 +0000 Subject: [PATCH] Ship parent history query authority in Server 2.5.8 --- composer.json | 4 +- composer.lock | 16 +-- docker-compose.dedicated-matching.yml | 4 +- docker-compose.memo-rolling.yml | 4 +- docker-compose.published.yml | 4 +- docker-compose.small-cluster.yml | 2 +- docker-compose.yml | 8 +- k8s/README.md | 10 +- k8s/helm/durable-workflow/Chart.yaml | 6 +- k8s/helm/durable-workflow/README.md | 2 +- .../ci/existing-secrets-values.yaml | 2 +- .../ci/ingress-and-hpa-values.yaml | 2 +- .../ci/inline-secrets-values.yaml | 2 +- .../durable-workflow/templates/_helpers.tpl | 2 +- k8s/helm/durable-workflow/values.yaml | 2 +- k8s/helm/examples/values-dev.yaml | 2 +- .../values-external-secrets-operator.yaml | 2 +- .../values-production-existing-secrets.yaml | 2 +- k8s/migration-job.yaml | 2 +- k8s/scheduler-cronjob.yaml | 2 +- k8s/secret.yaml | 2 +- k8s/server-deployment.yaml | 2 +- k8s/worker-deployment.yaml | 2 +- resources/release/source-release.json | 4 +- scripts/k8s-kind-smoke.sh | 2 +- tests/Feature/ParallelChildQueryTest.php | 130 ++++++++++++++++++ tests/Fixtures/ParallelChildQueryWorkflow.php | 40 ++++++ 27 files changed, 216 insertions(+), 46 deletions(-) create mode 100644 tests/Feature/ParallelChildQueryTest.php create mode 100644 tests/Fixtures/ParallelChildQueryWorkflow.php diff --git a/composer.json b/composer.json index f3b041ec..d4dda200 100644 --- a/composer.json +++ b/composer.json @@ -6,7 +6,7 @@ "require": { "php": "^8.2", "apache/avro": "^1.12", - "durable-workflow/workflow": "2.5.1", + "durable-workflow/workflow": "2.5.2", "laravel/framework": "^13.30", "laravel/tinker": "^3.0", "league/flysystem-aws-s3-v3": "^3.35.3" @@ -48,7 +48,7 @@ }, "extra": { "durable-workflow": { - "product-train": "2.5.7" + "product-train": "2.5.8" }, "laravel": { "dont-discover": [] diff --git a/composer.lock b/composer.lock index 497c1b26..3c46f482 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "e96e99a7af4513634a0c20bcd2f001bd", + "content-hash": "1fb68ebe344f62cc8f2277555b1c227d", "packages": [ { "name": "apache/avro", @@ -655,16 +655,16 @@ }, { "name": "durable-workflow/workflow", - "version": "2.5.1", + "version": "2.5.2", "source": { "type": "git", "url": "https://github.com/durable-workflow/workflow.git", - "reference": "13927608be9d4a366678a4beb477d1a3ee27a549" + "reference": "8b7519d330e517b95d824afa28286e4529fbf0cd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/durable-workflow/workflow/zipball/13927608be9d4a366678a4beb477d1a3ee27a549", - "reference": "13927608be9d4a366678a4beb477d1a3ee27a549", + "url": "https://api.github.com/repos/durable-workflow/workflow/zipball/8b7519d330e517b95d824afa28286e4529fbf0cd", + "reference": "8b7519d330e517b95d824afa28286e4529fbf0cd", "shasum": "" }, "require": { @@ -697,7 +697,7 @@ "dev-main": "2.0.x-dev" }, "durable-workflow": { - "product-train": "2.5.1", + "product-train": "2.5.2", "laravel-embedded-upgrade-contract": "resources/laravel-embedded-upgrade-contract.json", "laravel-dependency-security-policy": "resources/laravel-dependency-security-policy.json" } @@ -724,9 +724,9 @@ "description": "Embedded durable workflow runtime and orchestration engine for Laravel applications.", "support": { "issues": "https://github.com/durable-workflow/workflow/issues", - "source": "https://github.com/durable-workflow/workflow/tree/2.5.1" + "source": "https://github.com/durable-workflow/workflow/tree/2.5.2" }, - "time": "2026-10-07T09:18:09+00:00" + "time": "2026-10-07T12:03:09+00:00" }, { "name": "egulias/email-validator", diff --git a/docker-compose.dedicated-matching.yml b/docker-compose.dedicated-matching.yml index cb0e2cdd..86c5b13c 100644 --- a/docker-compose.dedicated-matching.yml +++ b/docker-compose.dedicated-matching.yml @@ -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.5.7}} +x-server-image: &server-image ${DW_SERVER_IMAGE:-durableworkflow/server:${DW_SERVER_TAG:-2.5.8}} 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.5.7}} + APP_VERSION: ${APP_VERSION:-${DW_SERVER_TAG:-2.5.8}} APP_DEBUG: ${APP_DEBUG:-false} DB_CONNECTION: mysql DB_HOST: mysql diff --git a/docker-compose.memo-rolling.yml b/docker-compose.memo-rolling.yml index e1190f7a..94f1d783 100644 --- a/docker-compose.memo-rolling.yml +++ b/docker-compose.memo-rolling.yml @@ -49,14 +49,14 @@ services: command: ["server-bootstrap"] environment: <<: *runtime-environment - APP_VERSION: ${APP_VERSION:-2.5.7} + APP_VERSION: ${APP_VERSION:-2.5.8} successor: image: ${DW_MEMO_SUCCESSOR_IMAGE:-durable-workflow/server-memo-rolling:local} ports: !override [] environment: <<: *runtime-environment - APP_VERSION: ${APP_VERSION:-2.5.7} + APP_VERSION: ${APP_VERSION:-2.5.8} DW_SERVER_ID: memo-successor DW_SERVER_TOPOLOGY_SHAPE: standalone_server DW_SERVER_PROCESS_CLASS: server_http_node diff --git a/docker-compose.published.yml b/docker-compose.published.yml index 1a468e82..76cb12d3 100644 --- a/docker-compose.published.yml +++ b/docker-compose.published.yml @@ -1,13 +1,13 @@ name: durable-workflow-server # 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.5.7}} +x-server-image: &server-image ${DW_SERVER_IMAGE:-durableworkflow/server:${DW_SERVER_TAG:-2.5.8}} 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.5.7}} + APP_VERSION: ${APP_VERSION:-${DW_SERVER_TAG:-2.5.8}} APP_DEBUG: ${APP_DEBUG:-false} LOG_CHANNEL: ${LOG_CHANNEL:-stderr} LOG_LEVEL: ${LOG_LEVEL:-info} diff --git a/docker-compose.small-cluster.yml b/docker-compose.small-cluster.yml index a9ee2747..0eb7e38a 100644 --- a/docker-compose.small-cluster.yml +++ b/docker-compose.small-cluster.yml @@ -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.5.7} + APP_VERSION: ${APP_VERSION:-2.5.8} APP_DEBUG: "false" DW_SERVER_KEY: ${DW_SERVER_KEY:-base64:5Zt4nUhlCm3DD0nLXZJQdHiwPfb56yGo9gNV/g3jYbY=} DB_CONNECTION: ${DW_SMALL_CLUSTER_DB:-mysql} diff --git a/docker-compose.yml b/docker-compose.yml index 2fe2694f..be5237f4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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.5.7}" + APP_VERSION: "${APP_VERSION:-2.5.8}" APP_DEBUG: "false" DB_CONNECTION: mysql DB_HOST: mysql @@ -62,7 +62,7 @@ services: APP_NAME: "Durable Workflow Server" APP_ENV: local DW_SERVER_KEY: "${DW_SERVER_KEY:-}" - APP_VERSION: "${APP_VERSION:-2.5.7}" + APP_VERSION: "${APP_VERSION:-2.5.8}" APP_DEBUG: "false" DB_CONNECTION: mysql DB_HOST: mysql @@ -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.5.7}" + APP_VERSION: "${APP_VERSION:-2.5.8}" DB_CONNECTION: mysql DB_HOST: mysql DB_PORT: 3306 @@ -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.5.7}" + APP_VERSION: "${APP_VERSION:-2.5.8}" DB_CONNECTION: mysql DB_HOST: mysql DB_PORT: 3306 diff --git a/k8s/README.md b/k8s/README.md index 1463390f..7e21cdcd 100644 --- a/k8s/README.md +++ b/k8s/README.md @@ -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.5.7 +durableworkflow/server:2.5.8 ``` Before production use, patch every workload image to the exact published tag or @@ -21,15 +21,15 @@ digest you intend to run: ```bash kubectl set image -n durable-workflow deploy/durable-workflow-server \ - server=durableworkflow/server:2.5.7 + server=durableworkflow/server:2.5.8 kubectl set image -n durable-workflow deploy/durable-workflow-worker \ - worker=durableworkflow/server:2.5.7 + worker=durableworkflow/server:2.5.8 kubectl set image -n durable-workflow cronjob/durable-workflow-scheduler \ - scheduler=durableworkflow/server:2.5.7 + scheduler=durableworkflow/server:2.5.8 ``` GitHub Container Registry publishes the same release line at -`ghcr.io/durable-workflow/server:2.5.7`. Digest pinning is preferred for strict +`ghcr.io/durable-workflow/server:2.5.8`. Digest pinning is preferred for strict change control. The manifests expect you to provide: diff --git a/k8s/helm/durable-workflow/Chart.yaml b/k8s/helm/durable-workflow/Chart.yaml index 81fcac2f..95a5a236 100644 --- a/k8s/helm/durable-workflow/Chart.yaml +++ b/k8s/helm/durable-workflow/Chart.yaml @@ -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/helm-upgrading.md alongside the chart. -version: 0.1.144 +version: 0.1.145 # 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.5.7" +appVersion: "2.5.8" kubeVersion: ">=1.27.0-0" home: https://durable-workflow.github.io/docs/2.0/deployment sources: @@ -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.5.7" + dev.durable-workflow.image-reference: "docker.io/durableworkflow/server:2.5.8" artifacthub.io/license: MIT artifacthub.io/category: integration-delivery # Free-form changelog for the current chart release shown by Artifact Hub. diff --git a/k8s/helm/durable-workflow/README.md b/k8s/helm/durable-workflow/README.md index f7dfee05..d6810cc6 100644 --- a/k8s/helm/durable-workflow/README.md +++ b/k8s/helm/durable-workflow/README.md @@ -63,7 +63,7 @@ helm install durable-workflow ./k8s/helm/durable-workflow \ ```yaml image: - tag: "2.5.7" + tag: "2.5.8" # Pin a digest in production: # digest: "sha256:abc123..." # memoPayloadStorage: "raw-json-v1" # Required for a digest or custom image. diff --git a/k8s/helm/durable-workflow/ci/existing-secrets-values.yaml b/k8s/helm/durable-workflow/ci/existing-secrets-values.yaml index 6b5256aa..4fbb41c8 100644 --- a/k8s/helm/durable-workflow/ci/existing-secrets-values.yaml +++ b/k8s/helm/durable-workflow/ci/existing-secrets-values.yaml @@ -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.5.7" + tag: "2.5.8" externalDatabase: connection: pgsql diff --git a/k8s/helm/durable-workflow/ci/ingress-and-hpa-values.yaml b/k8s/helm/durable-workflow/ci/ingress-and-hpa-values.yaml index 5214e798..ae8ef15a 100644 --- a/k8s/helm/durable-workflow/ci/ingress-and-hpa-values.yaml +++ b/k8s/helm/durable-workflow/ci/ingress-and-hpa-values.yaml @@ -1,6 +1,6 @@ # CI fixture: ingress + autoscaling enabled. Exercises optional templates. image: - tag: "2.5.7" + tag: "2.5.8" externalDatabase: connection: mysql diff --git a/k8s/helm/durable-workflow/ci/inline-secrets-values.yaml b/k8s/helm/durable-workflow/ci/inline-secrets-values.yaml index 5f455cb7..ba64496f 100644 --- a/k8s/helm/durable-workflow/ci/inline-secrets-values.yaml +++ b/k8s/helm/durable-workflow/ci/inline-secrets-values.yaml @@ -2,7 +2,7 @@ # chart's render path is exercised end-to-end. Real deployments should use # existingSecret instead. image: - tag: "2.5.7" + tag: "2.5.8" externalDatabase: connection: mysql diff --git a/k8s/helm/durable-workflow/templates/_helpers.tpl b/k8s/helm/durable-workflow/templates/_helpers.tpl index 66b94b26..af5d4da2 100644 --- a/k8s/helm/durable-workflow/templates/_helpers.tpl +++ b/k8s/helm/durable-workflow/templates/_helpers.tpl @@ -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.5.7" -}} +{{- if eq $normalized "docker.io/durableworkflow/server:2.5.8" -}} dual-v1 {{- else if regexMatch "^docker\\.io/durableworkflow/server:2\\.0\\.0-rc\\.[0-9]+$" $normalized -}} {{- $releaseCandidate := atoi (regexFind "[0-9]+$" $normalized) -}} diff --git a/k8s/helm/durable-workflow/values.yaml b/k8s/helm/durable-workflow/values.yaml index ddb7db3a..bba4c108 100644 --- a/k8s/helm/durable-workflow/values.yaml +++ b/k8s/helm/durable-workflow/values.yaml @@ -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.5.7" + tag: "2.5.8" # Optional digest pin. When set, takes precedence over tag for change control. # Example: "sha256:abc123..." digest: "" diff --git a/k8s/helm/examples/values-dev.yaml b/k8s/helm/examples/values-dev.yaml index e2b7d8c4..46420b03 100644 --- a/k8s/helm/examples/values-dev.yaml +++ b/k8s/helm/examples/values-dev.yaml @@ -3,7 +3,7 @@ # shape in production. image: - tag: "2.5.7" + tag: "2.5.8" externalDatabase: connection: mysql diff --git a/k8s/helm/examples/values-external-secrets-operator.yaml b/k8s/helm/examples/values-external-secrets-operator.yaml index 02c8f76f..2758183b 100644 --- a/k8s/helm/examples/values-external-secrets-operator.yaml +++ b/k8s/helm/examples/values-external-secrets-operator.yaml @@ -5,7 +5,7 @@ # concern. image: - tag: "2.5.7" + tag: "2.5.8" externalDatabase: connection: pgsql diff --git a/k8s/helm/examples/values-production-existing-secrets.yaml b/k8s/helm/examples/values-production-existing-secrets.yaml index 0607b3cd..15dac4eb 100644 --- a/k8s/helm/examples/values-production-existing-secrets.yaml +++ b/k8s/helm/examples/values-production-existing-secrets.yaml @@ -10,7 +10,7 @@ image: repository: durable-workflow/server # Pin a digest in production for change-control auditability. digest: "" # e.g. "sha256:abc123..." - tag: "2.5.7" + tag: "2.5.8" externalDatabase: connection: pgsql diff --git a/k8s/migration-job.yaml b/k8s/migration-job.yaml index 05b5c29a..9e9efce9 100644 --- a/k8s/migration-job.yaml +++ b/k8s/migration-job.yaml @@ -13,7 +13,7 @@ spec: restartPolicy: OnFailure containers: - name: migrate - image: durableworkflow/server:2.5.7 + image: durableworkflow/server:2.5.8 command: ["server-entrypoint"] args: ["server-bootstrap"] envFrom: diff --git a/k8s/scheduler-cronjob.yaml b/k8s/scheduler-cronjob.yaml index 759db594..ae87a4f6 100644 --- a/k8s/scheduler-cronjob.yaml +++ b/k8s/scheduler-cronjob.yaml @@ -24,7 +24,7 @@ spec: restartPolicy: Never containers: - name: scheduler - image: durableworkflow/server:2.5.7 + image: durableworkflow/server:2.5.8 command: ["server-entrypoint"] args: - sh diff --git a/k8s/secret.yaml b/k8s/secret.yaml index 2924d638..39951fff 100644 --- a/k8s/secret.yaml +++ b/k8s/secret.yaml @@ -12,7 +12,7 @@ metadata: app.kubernetes.io/name: durable-workflow data: APP_NAME: "Durable Workflow Server" - APP_VERSION: "2.5.7" + APP_VERSION: "2.5.8" APP_ENV: production APP_DEBUG: "false" DB_CONNECTION: mysql diff --git a/k8s/server-deployment.yaml b/k8s/server-deployment.yaml index db896715..f2d2613b 100644 --- a/k8s/server-deployment.yaml +++ b/k8s/server-deployment.yaml @@ -23,7 +23,7 @@ spec: spec: containers: - name: server - image: durableworkflow/server:2.5.7 + image: durableworkflow/server:2.5.8 ports: - containerPort: 8080 name: http diff --git a/k8s/worker-deployment.yaml b/k8s/worker-deployment.yaml index a83c6d46..6fd74789 100644 --- a/k8s/worker-deployment.yaml +++ b/k8s/worker-deployment.yaml @@ -19,7 +19,7 @@ spec: spec: containers: - name: worker - image: durableworkflow/server:2.5.7 + image: durableworkflow/server:2.5.8 command: ["server-entrypoint"] args: ["php", "artisan", "queue:work", "--sleep=1", "--tries=3", "--max-time=3600"] envFrom: diff --git a/resources/release/source-release.json b/resources/release/source-release.json index ee52db2f..ae3223d6 100644 --- a/resources/release/source-release.json +++ b/resources/release/source-release.json @@ -1,9 +1,9 @@ { "schema": "durable-workflow.server.source-release/v1", "server": { - "version": "2.5.7" + "version": "2.5.8" }, "helm_chart": { - "version": "0.1.144" + "version": "0.1.145" } } diff --git a/scripts/k8s-kind-smoke.sh b/scripts/k8s-kind-smoke.sh index 10395fc4..566b9327 100755 --- a/scripts/k8s-kind-smoke.sh +++ b/scripts/k8s-kind-smoke.sh @@ -7,7 +7,7 @@ cluster="${K8S_SMOKE_CLUSTER:-durable-workflow-server-smoke}" image="${K8S_SMOKE_IMAGE:-durableworkflow/server:k8s-smoke}" # Generated by scripts/ci/sync-source-release.mjs so the smoke replaces the # same default shipped by the public manifests. -manifest_image="durableworkflow/server:2.5.7" +manifest_image="durableworkflow/server:2.5.8" kind_node_image="${K8S_SMOKE_KIND_NODE_IMAGE:-kindest/node:v1.29.4}" artifact_dir="${K8S_SMOKE_ARTIFACT_DIR:-/tmp/durable-workflow-k8s-kind-smoke-artifacts}" rendered_dir="${artifact_dir}/rendered-manifests" diff --git a/tests/Feature/ParallelChildQueryTest.php b/tests/Feature/ParallelChildQueryTest.php new file mode 100644 index 00000000..2d47f66c --- /dev/null +++ b/tests/Feature/ParallelChildQueryTest.php @@ -0,0 +1,130 @@ +set('workflows.v2.types.workflows', [ + 'tests.parallel-child-query-workflow' => ParallelChildQueryWorkflow::class, + 'tests.internal-child-workflow' => InternalChildWorkflow::class, + ]); + WorkflowNamespace::query()->updateOrCreate(['name' => 'default'], [ + 'description' => 'Parallel child query regression', + 'retention_days' => 30, + 'status' => 'active', + ]); + $headers = ['X-Namespace' => 'default', 'X-Durable-Workflow-Control-Plane-Version' => '2']; + $start = $this->withHeaders($headers)->postJson('/api/workflows', [ + 'workflow_id' => 'parallel-child-query', + 'workflow_type' => 'tests.parallel-child-query-workflow', + ])->assertCreated(); + $runId = (string) $start->json('run_id'); + $taskId = WorkflowTask::query()->where('workflow_run_id', $runId) + ->where('task_type', 'workflow')->where('status', 'ready')->value('id'); + $this->assertIsString($taskId); + (new RunWorkflowTask($taskId))->handle(app(WorkflowExecutor::class)); + $run = WorkflowRun::query()->findOrFail($runId); + $this->assertSame(RunStatus::Waiting, $run->status); + $scheduled = WorkflowHistoryEvent::query()->where('workflow_run_id', $runId) + ->where('event_type', HistoryEventType::ChildWorkflowScheduled->value) + ->orderBy('sequence')->get(); + $this->assertCount(2, $scheduled); + foreach ($scheduled as $event) { + WorkflowRun::query()->findOrFail($event->payload['child_workflow_run_id'])->forceFill([ + 'status' => $status, + 'closed_at' => now(), + 'closed_reason' => $status, + 'output' => Serializer::serializeWithCodec($run->payload_codec, 'uncommitted-child'), + 'output_payload_codec' => $run->payload_codec, + ])->save(); + } + $before = $this->snapshot(); + foreach ([1, 2] as $repetition) { + $this->withHeaders($headers)->postJson('/api/workflows/parallel-child-query/query/currentState') + ->assertOk()->assertJsonPath('run_id', $runId) + ->assertJsonPath('result.stage', 'waiting-for-children') + ->assertJsonPath('result.value', null)->assertJsonPath('result.failure', null); + $this->assertSame($before, $this->snapshot()); + } + $resolution = match (RunStatus::from($status)) { + RunStatus::Completed => HistoryEventType::ChildRunCompleted, + RunStatus::Failed => HistoryEventType::ChildRunFailed, + RunStatus::Cancelled => HistoryEventType::ChildRunCancelled, + default => HistoryEventType::ChildRunTerminated, + }; + $resolutionBase = now()->subMinute(); + foreach ($scheduled->reverse() as $event) { + $sequence = $event->payload['sequence']; + $run->increment('last_history_sequence'); + WorkflowHistoryEvent::query()->create([ + 'workflow_run_id' => $runId, + 'sequence' => $run->last_history_sequence, + 'event_type' => $resolution->value, + 'payload' => $event->payload + [ + 'child_status' => $status, + 'output' => Serializer::serializeWithCodec($run->payload_codec, 'committed-child-'.$sequence), + 'payload_codec' => $run->payload_codec, + 'exception_class' => \RuntimeException::class, + 'message' => 'committed-child-'.$sequence.'-failure', + ], + 'recorded_at' => $resolutionBase->copy()->addSeconds($run->last_history_sequence), + ]); + } + $expectedValue = $status === RunStatus::Completed->value ? ['committed-child-1', 'committed-child-2'] : null; + $expectedFailure = match (RunStatus::from($status)) { + RunStatus::Completed => null, + RunStatus::Failed => 'committed-child-2-failure', + default => 'Child workflow '.$scheduled->last()->payload['child_workflow_run_id'].' closed as '.$status.'.', + }; + $before = $this->snapshot(); + foreach ([1, 2] as $repetition) { + $this->withHeaders($headers)->postJson('/api/workflows/parallel-child-query/query/currentState') + ->assertOk()->assertJsonPath('run_id', $runId) + ->assertJsonPath('result.stage', 'waiting-for-finish') + ->assertJsonPath('result.value', $expectedValue)->assertJsonPath('result.failure', $expectedFailure); + $this->assertSame($before, $this->snapshot()); + } + } + + public static function terminalChildren(): array + { + return array_combine( + array_map(static fn (RunStatus $status): string => $status->value, [ + RunStatus::Completed, RunStatus::Failed, RunStatus::Cancelled, RunStatus::Terminated, + ]), + array_map(static fn (RunStatus $status): array => [$status->value], [ + RunStatus::Completed, RunStatus::Failed, RunStatus::Cancelled, RunStatus::Terminated, + ]) + ); + } + + private function snapshot(): array + { + return array_map(static fn (string $model): array => $model::query()->orderBy('id')->get()->toArray(), [ + WorkflowRun::class, WorkflowHistoryEvent::class, WorkflowTask::class, WorkflowLink::class, + ]); + } +} diff --git a/tests/Fixtures/ParallelChildQueryWorkflow.php b/tests/Fixtures/ParallelChildQueryWorkflow.php new file mode 100644 index 00000000..e53db758 --- /dev/null +++ b/tests/Fixtures/ParallelChildQueryWorkflow.php @@ -0,0 +1,40 @@ +value = Workflow::all([ + static fn () => Workflow::child(InternalChildWorkflow::class, 'first'), + static fn () => Workflow::child(InternalChildWorkflow::class, 'second'), + ]); + } catch (Throwable $failure) { + $this->failure = $failure->getMessage(); + } + $this->stage = 'waiting-for-finish'; + Workflow::awaitSignal('finish'); + + return $this->currentState(); + } + + #[QueryMethod] + public function currentState(): array + { + return ['stage' => $this->stage, 'value' => $this->value, 'failure' => $this->failure]; + } +}