From 49e12f2ef15f746d4c0133d2202d91d212d9ea46 Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Fri, 23 Oct 2026 13:19:01 +0000 Subject: [PATCH 01/12] docs: stage Kubernetes SDK worker deployment and published acceptance --- .github/workflows/kubernetes-workers.yml | 53 +++++++ docs/deployment.md | 79 ++++++++++ tests/kubernetes-workers/artifacts.json | 10 ++ tests/kubernetes-workers/php/worker.php | 25 ++++ tests/kubernetes-workers/python/verify.py | 55 +++++++ tests/kubernetes-workers/python/worker.py | 49 +++++++ tests/kubernetes-workers/qualify.py | 171 ++++++++++++++++++++++ tests/kubernetes-workers/rust/Cargo.toml | 10 ++ tests/kubernetes-workers/rust/src/main.rs | 27 ++++ 9 files changed, 479 insertions(+) create mode 100644 .github/workflows/kubernetes-workers.yml create mode 100644 tests/kubernetes-workers/artifacts.json create mode 100644 tests/kubernetes-workers/php/worker.php create mode 100644 tests/kubernetes-workers/python/verify.py create mode 100644 tests/kubernetes-workers/python/worker.py create mode 100644 tests/kubernetes-workers/qualify.py create mode 100644 tests/kubernetes-workers/rust/Cargo.toml create mode 100644 tests/kubernetes-workers/rust/src/main.rs diff --git a/.github/workflows/kubernetes-workers.yml b/.github/workflows/kubernetes-workers.yml new file mode 100644 index 00000000000..9f3bdbbef63 --- /dev/null +++ b/.github/workflows/kubernetes-workers.yml @@ -0,0 +1,53 @@ +name: Kubernetes SDK worker guide + +on: + pull_request: + paths: + - docs/deployment.md + - tests/kubernetes-workers/** + - .github/workflows/kubernetes-workers.yml + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: kubernetes-worker-guide-${{ github.ref }} + cancel-in-progress: true + +jobs: + published-workers: + name: Published SDK workers on Kubernetes + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + with: + persist-credentials: false + - name: Install verified kind and kubectl binaries + shell: bash + run: | + install_dir="${RUNNER_TEMP}/worker-guide-tools" + mkdir -p "$install_dir" + curl -fsSLo "$install_dir/kind" https://kind.sigs.k8s.io/dl/v0.31.0/kind-linux-amd64 + curl -fsSLo "$install_dir/kind.sha256" https://kind.sigs.k8s.io/dl/v0.31.0/kind-linux-amd64.sha256sum + expected="$(cut -d ' ' -f 1 "$install_dir/kind.sha256")" + printf '%s %s\n' "$expected" "$install_dir/kind" | sha256sum --check + curl -fsSLo "$install_dir/kubectl" https://dl.k8s.io/release/v1.35.0/bin/linux/amd64/kubectl + curl -fsSLo "$install_dir/kubectl.sha256" https://dl.k8s.io/release/v1.35.0/bin/linux/amd64/kubectl.sha256 + expected="$(cat "$install_dir/kubectl.sha256")" + printf '%s %s\n' "$expected" "$install_dir/kubectl" | sha256sum --check + chmod +x "$install_dir/kind" "$install_dir/kubectl" + echo "$install_dir" >> "$GITHUB_PATH" + - name: Exercise the documented Deployment with published artifacts + env: + QUALIFICATION_OUTPUT: ${{ runner.temp }}/worker-guide-evidence + run: python3 tests/kubernetes-workers/qualify.py + - name: Retain bounded qualification evidence + if: always() + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: kubernetes-worker-guide-evidence + path: ${{ runner.temp }}/worker-guide-evidence + retention-days: 3 + if-no-files-found: ignore diff --git a/docs/deployment.md b/docs/deployment.md index 69a305c8215..b69b33c5321 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -505,6 +505,85 @@ support-led or tracked separately from the raw-manifest contract. Use the separately versioned Helm path above when you want Helm-managed installation and upgrades. +## Running SDK workers on Kubernetes {#sdk-workers-on-kubernetes} + +Deploy your application workers separately from the Server's Laravel queue worker. Build an image containing your registered PHP, Python or Rust handlers and its locked SDK dependencies. Use an exec-form entrypoint so the worker process receives `SIGTERM`. Replace the image below with that build's immutable digest. + +For workers in the same cluster, use the Server Service's actual name, namespace and port. The raw manifests expose `http://durable-workflow-server.durable-workflow.svc.cluster.local:8080`. A Helm release can use a different Service name. Workers outside the cluster need the reachable ingress or load balancer's HTTPS URL and normal certificate verification. Keep long-poll and transport budgets aligned with [the timeout guidance](#worker-long-poll-timeout-budgets). + +Create a Secret in the worker Pod's Kubernetes namespace from your existing worker credential. Keep the token out of the manifest and image. The Server namespace is a separate product boundary from the Kubernetes namespace. + +```bash +kubectl -n durable-workflow create secret generic orders-worker-auth --from-literal=token="$DW_WORKER_TOKEN" +``` + +Save this as `orders-worker.yaml`. These environment names are application configuration: your entrypoint must pass them to the SDK client and Worker. Give each live process its own worker ID and register the handlers for the configured queue. + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: orders-worker + namespace: durable-workflow +spec: + replicas: 2 + selector: + matchLabels: + app: orders-worker + strategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 0 + maxSurge: 1 + template: + metadata: + labels: + app: orders-worker + spec: + automountServiceAccountToken: false + terminationGracePeriodSeconds: 75 + containers: + - name: worker + image: registry.example.com/acme/orders-worker:release-id + env: + - name: DW_SERVER_URL + value: http://durable-workflow-server.durable-workflow.svc.cluster.local:8080 + - name: DW_NAMESPACE + value: default + - name: DW_TASK_QUEUE + value: orders + - name: DW_WORKER_ID + valueFrom: + fieldRef: + fieldPath: metadata.uid + - name: DW_WORKER_TOKEN + valueFrom: + secretKeyRef: + name: orders-worker-auth + key: token + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: "1" + memory: 512Mi +``` + +Replica and resource values are starting examples, not capacity guarantees. Tune them with actual task duration, memory use, downstream limits and [worker admission budgets](./polyglot/task-queue-admission.md). A Deployment becoming available does not prove SDK registration or useful work. Verify fresh worker registrations and the expected build/queue, then start and complete a representative workflow. An SDK worker does not acquire an HTTP health endpoint merely by running in Kubernetes. Use an application-owned probe when you have one, and do not use Server health as a worker liveness check. + +### Delivering SIGTERM and waiting for cleanup {#sdk-worker-pod-shutdown} + +| SDK | Worker process shutdown | +| --- | --- | +| PHP | Use the managed `Worker::run()` loop with `pcntl` installed. Its signal handler requests shutdown, then the active synchronous task reaches a boundary before managed cleanup and deregistration. A custom loop around `tick()` does not supply that lifecycle. See [PHP graceful shutdown](https://php.durable-workflow.com/operate/graceful-shutdown/). | +| Python | Wire `SIGTERM` to the same Worker's shutdown path and await both `Worker.stop()` and its run task. Set an explicit `shutdown_timeout`. Accepted work shares that drain deadline. A running synchronous thread cannot be forcibly stopped by cancelling its await, and an unconfirmed stop remains an error. Use the [separate-process example and signal wiring](https://python.durable-workflow.com/guides/separate-workers/) and [lifecycle contract](https://python.durable-workflow.com/reference/worker/). | +| Rust | Pass a future that resolves on Unix `SIGTERM` to `Worker::run_until(...)` and await its result. A Ctrl-C-only future does not handle the Pod's termination signal. Keep asynchronous handlers yielding and bound blocking work separately. See [Worker::run_until](https://rust.durable-workflow.com/durable_workflow/struct.Worker.html#method.run_until). | + +Give `terminationGracePeriodSeconds` enough time for the installed SDK's poll, accepted-task shutdown and HTTP/session/deregistration cleanup. The example's 75 seconds must be checked against your application's actual bounds. Kubernetes counts any `preStop` hook within that same grace period and force-stops remaining processes after expiry. A sleep hook alone neither asks the SDK to drain nor extends its deadline. See the [Kubernetes Pod termination flow](https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-termination-flow). + +Rehearse termination while idle and during bounded work. Treat cleanup errors, a retained registration or a forced kill as incomplete shutdown. A replacement worker continues durable work according to its recorded leases, retries and deadlines. Stopping a Pod does not cancel a workflow or undo an external side effect. Before changing builds, follow [worker build-ID rollout](./polyglot/worker-build-id-rollout.md) and retain its compatibility, replay and rollback checks. + ## Single-region high availability and failover Single-region HA failover is currently an **unverified, support-led** contract diff --git a/tests/kubernetes-workers/artifacts.json b/tests/kubernetes-workers/artifacts.json new file mode 100644 index 00000000000..1bd31d12427 --- /dev/null +++ b/tests/kubernetes-workers/artifacts.json @@ -0,0 +1,10 @@ +{ + "server": "durableworkflow/server@sha256:b29157eb2cf844916debbbb3c01b3257672e0dece778a9c6fa539043a3038f02", + "server_version": "2.11.0", + "php_sdk": "3.4.1", + "php_source": "cb1d11e57767e20e1fdb8081024f67057ff66714", + "python_sdk": "2.9.0", + "rust_sdk": "4.7.0", + "rust_checksum": "dc6c0353e940a068f4a71eef8dc516d920c88be34f9c68e205f05ec10c34e65b", + "kind_node": "kindest/node:v1.35.0@sha256:452d707d4862f52530247495d180205e029056831160e22870e37e3f6c1ac31f" +} diff --git a/tests/kubernetes-workers/php/worker.php b/tests/kubernetes-workers/php/worker.php new file mode 100644 index 00000000000..e8a3a336daf --- /dev/null +++ b/tests/kubernetes-workers/php/worker.php @@ -0,0 +1,25 @@ +registerWorkflow('website336.echo', function (WorkflowContext $context, string $name): mixed { + return $context->activity('website336.greet', [$name]); +}); +$worker->registerActivity('website336.greet', function (string $name): array { + return ['sdk' => 'php', 'name' => $name, 'worker_id' => getenv('DW_WORKER_ID')]; +}); +$worker->run(pollTimeoutSeconds: 1); +echo json_encode(['event' => 'worker_stopped', 'sdk' => 'php'], JSON_THROW_ON_ERROR).PHP_EOL; diff --git a/tests/kubernetes-workers/python/verify.py b/tests/kubernetes-workers/python/verify.py new file mode 100644 index 00000000000..b2a8b121391 --- /dev/null +++ b/tests/kubernetes-workers/python/verify.py @@ -0,0 +1,55 @@ +import asyncio +import json +import os +import sys +import uuid +from pathlib import Path + +from durable_workflow import Client + + +async def main(): + async with Client( + os.environ["DW_SERVER_URL"], + namespace=os.environ["DW_NAMESPACE"], + token="website336-control", + timeout=5, + ) as client: + if len(sys.argv) > 1 and sys.argv[1] == "retired": + registered = json.loads(Path('/tmp/website336-workers.json').read_text()) + async with asyncio.timeout(30): + while True: + present = {worker.worker_id for worker in (await client.list_workers()).workers} + if not (present & set(registered)): + break + await asyncio.sleep(0.2) + print(json.dumps({"deregistered_worker_ids": registered})) + return + receipts = [] + worker_ids = [] + for sdk in ["php", "python", "rust"]: + async with asyncio.timeout(40): + while True: + roster = await client.list_workers(task_queue="orders-" + sdk) + if len(roster.workers) == 2: + break + await asyncio.sleep(0.2) + ids = [worker.worker_id for worker in roster.workers] + assert len(set(ids)) == 2, ids + worker_ids.extend(ids) + workflow_id = "website336-" + sdk + "-" + uuid.uuid4().hex + handle = await client.start_workflow( + workflow_type="website336.echo", task_queue="orders-" + sdk, + workflow_id=workflow_id, input=["Kubernetes"], + run_timeout_seconds=60, execution_timeout_seconds=60, + ) + result = await handle.result(timeout=45) + assert result["sdk"] == sdk and result["name"] == "Kubernetes", result + assert result["worker_id"] in ids, result + receipts.append({"sdk": sdk, "registered_worker_ids": ids, "workflow_id": workflow_id, "run_id": handle.run_id, "result": result}) + Path('/tmp/website336-workers.json').write_text(json.dumps(worker_ids)) + print(json.dumps({"completed_workflows": receipts})) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/tests/kubernetes-workers/python/worker.py b/tests/kubernetes-workers/python/worker.py new file mode 100644 index 00000000000..f5456c9b5bc --- /dev/null +++ b/tests/kubernetes-workers/python/worker.py @@ -0,0 +1,49 @@ +import asyncio +import json +import os +import signal + +from durable_workflow import Client, Worker, activity, workflow + + +@activity.defn(name="website336.greet") +async def greet(name: str): + return {"sdk": "python", "name": name, "worker_id": os.environ["DW_WORKER_ID"]} + + +@workflow.defn(name="website336.echo") +class Echo: + def run(self, context, name: str): + return (yield context.schedule_activity("website336.greet", [name])) + + +async def main(): + async with Client( + os.environ["DW_SERVER_URL"], + namespace=os.environ["DW_NAMESPACE"], + worker_token=os.environ["DW_WORKER_TOKEN"], + timeout=5, + ) as client: + worker = Worker( + client, + task_queue=os.environ["DW_TASK_QUEUE"], + worker_id=os.environ["DW_WORKER_ID"], + workflows=[Echo], activities=[greet], poll_timeout=1, shutdown_timeout=10, + ) + shutdown = asyncio.Event() + loop = asyncio.get_running_loop() + for requested_signal in (signal.SIGTERM, signal.SIGINT): + loop.add_signal_handler(requested_signal, shutdown.set) + run_task = asyncio.create_task(worker.run()) + try: + await shutdown.wait() + finally: + try: + await worker.stop() + finally: + await run_task + print(json.dumps({"event": "worker_stopped", "sdk": "python"}), flush=True) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/tests/kubernetes-workers/qualify.py b/tests/kubernetes-workers/qualify.py new file mode 100644 index 00000000000..bf3c984a400 --- /dev/null +++ b/tests/kubernetes-workers/qualify.py @@ -0,0 +1,171 @@ +"""Exercise the guide's Deployment with ordinary published SDK installations.""" +import hashlib +import json +import os +from pathlib import Path +import subprocess +import tempfile +import time +import urllib.request + +ROOT = Path(__file__).resolve().parents[2] +FIXTURE = Path(__file__).resolve().parent +ARTIFACTS = json.loads((FIXTURE / 'artifacts.json').read_text()) +OUTPUT = Path(os.environ['QUALIFICATION_OUTPUT']) if 'QUALIFICATION_OUTPUT' in os.environ else Path(tempfile.mkdtemp(prefix='website336-')) +OUTPUT.mkdir(parents=True, exist_ok=True) +CLUSTER = 'website336-' + os.environ.get('GITHUB_RUN_ID', str(os.getpid())) +NAMESPACE = 'durable-workflow' + + +def run(args, *, label=None, timeout=180, stdin=None): + result = subprocess.run(args, input=stdin, text=True, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, timeout=timeout, check=False) + if label: + (OUTPUT / (label + '.txt')).write_text(result.stdout) + if result.returncode: + raise RuntimeError(f'{args[0:3]} exited {result.returncode}:\n{result.stdout[-6000:]}') + return result.stdout + + +def kubectl(*args, **kwargs): + return run(['kubectl', '--context', 'kind-' + CLUSTER, '-n', NAMESPACE, *args], **kwargs) + + +def apply(document): + return kubectl('apply', '-f', '-', stdin=json.dumps(document)) + + +def server(): + environment = { + 'APP_ENV': 'testing', 'APP_DEBUG': 'false', 'APP_VERSION': ARTIFACTS['server_version'], + 'DB_CONNECTION': 'sqlite', 'DB_DATABASE': '/data/database.sqlite', + 'QUEUE_CONNECTION': 'sync', 'CACHE_STORE': 'array', 'SESSION_DRIVER': 'array', + 'DW_AUTH_DRIVER': 'token', 'DW_AUTH_BACKWARD_COMPATIBLE': 'false', + 'DW_ADMIN_TOKEN': 'website336-control', 'DW_WORKER_TOKEN': 'website336-worker', + 'DW_TASK_DISPATCH_MODE': 'poll', + 'DW_SERVER_KEY': 'base64:' + 'd2Vic2l0ZTMzNi1maXh0dXJlLWtleS0wMDAwMDAwMDA=', + } + common = {'image': ARTIFACTS['server'], + 'env': [{'name': key, 'value': value} for key, value in environment.items()], + 'volumeMounts': [{'name': 'data', 'mountPath': '/data'}]} + api = {**common, 'name': 'server', 'ports': [{'containerPort': 8080}], + 'readinessProbe': {'httpGet': {'path': '/api/ready', 'port': 8080}, + 'periodSeconds': 2, 'failureThreshold': 60}} + # A real scheduler drives ordinary timeout/recovery work. This ephemeral + # SQLite fixture is not a production Server installation or HA rehearsal. + scheduler = {**common, 'name': 'scheduler', 'command': ['sh', '-ec', + 'while true; do php artisan schedule:evaluate --limit=100 --json; ' + 'php artisan workflow:v2:repair-pass --json; sleep 1; done']} + apply({'apiVersion': 'apps/v1', 'kind': 'Deployment', + 'metadata': {'name': 'durable-workflow-server', 'namespace': NAMESPACE}, + 'spec': {'replicas': 1, 'selector': {'matchLabels': {'app': 'fixture-server'}}, + 'template': {'metadata': {'labels': {'app': 'fixture-server'}}, + 'spec': {'initContainers': [{**common, 'name': 'bootstrap', + 'command': ['server-bootstrap']}], + 'containers': [api, scheduler], + 'volumes': [{'name': 'data', 'emptyDir': {}}]}}}}) + apply({'apiVersion': 'v1', 'kind': 'Service', + 'metadata': {'name': 'durable-workflow-server', 'namespace': NAMESPACE}, + 'spec': {'selector': {'app': 'fixture-server'}, + 'ports': [{'port': 8080, 'targetPort': 8080}]}}) + + +def collect(): + for kind in ['pods', 'deployments', 'events']: + try: + kubectl('get', kind, '-o', 'json', label=kind) + except RuntimeError: + pass + try: + pods = json.loads(kubectl('get', 'pods', '-o', 'json'))['items'] + for pod in pods: + for container in pod['spec'].get('initContainers', []) + pod['spec']['containers']: + try: + kubectl('logs', pod['metadata']['name'], '-c', container['name'], + label=pod['metadata']['name'] + '-' + container['name']) + except RuntimeError: + pass + except RuntimeError: + pass + + +def main(): + images = {} + for sdk in ['php', 'python', 'rust']: + image = 'website336-' + sdk + ':' + CLUSTER + run(['docker', 'build', '--build-arg', 'SDK_VERSION=' + ARTIFACTS[sdk + '_sdk'], + '--tag', image, str(FIXTURE / sdk)], label=sdk + '-build', timeout=900) + images[sdk] = image + php_lock = json.loads(run(['docker', 'run', '--rm', '--entrypoint', 'cat', images['php'], '/app/composer.lock'], label='php-lock')) + php_sdk = next(item for item in php_lock['packages'] if item['name'] == 'durable-workflow/sdk') + assert php_sdk['source']['reference'] == ARTIFACTS['php_source'] + assert php_sdk['dist']['reference'] == ARTIFACTS['php_source'] + rust_lock = run(['docker', 'run', '--rm', '--entrypoint', 'cat', images['rust'], '/app/Cargo.lock'], label='rust-lock') + assert ARTIFACTS['rust_checksum'] in rust_lock + run(['docker', 'run', '--rm', '--entrypoint', 'cat', images['python'], '/app/install-report.json'], label='python-install-report') + created = False + try: + created = True + run(['kind', 'create', 'cluster', '--name', CLUSTER, '--image', ARTIFACTS['kind_node'], + '--wait', '180s'], label='kind-create', timeout=300) + kubectl('create', 'namespace', NAMESPACE) + kubectl('create', 'secret', 'generic', 'orders-worker-auth', '--from-literal=token=website336-worker') + server() + kubectl('rollout', 'status', 'deployment/durable-workflow-server', '--timeout=180s', label='server-ready', timeout=200) + source = (ROOT / 'docs/deployment.md').read_text() + manifest = source.split('{#sdk-workers-on-kubernetes}', 1)[1].split('```yaml\n', 1)[1].split('```', 1)[0] + (OUTPUT / 'documented-worker.yaml').write_text(manifest) + template = json.loads(kubectl('create', '--dry-run=client', '-f', '-', '-o', 'json', stdin=manifest)) + for sdk, image in images.items(): + run(['kind', 'load', 'docker-image', '--name', CLUSTER, image], label=sdk + '-load', timeout=180) + document = json.loads(json.dumps(template)) + name = 'orders-' + sdk + document['metadata']['name'] = name + document['spec']['selector']['matchLabels']['app'] = name + document['spec']['template']['metadata']['labels']['app'] = name + container = document['spec']['template']['spec']['containers'][0] + container['image'] = image + for value in container['env']: + if value['name'] == 'DW_TASK_QUEUE': + value['value'] = name + apply(document) + kubectl('rollout', 'status', 'deployment/' + name, '--timeout=90s', timeout=110) + apply({'apiVersion': 'v1', 'kind': 'Pod', 'metadata': {'name': 'controller', 'namespace': NAMESPACE}, + 'spec': {'containers': [{'name': 'controller', 'image': images['python'], + 'command': ['sleep', 'infinity'], 'env': [ + {'name': 'DW_SERVER_URL', 'value': 'http://durable-workflow-server.durable-workflow.svc.cluster.local:8080'}, + {'name': 'DW_NAMESPACE', 'value': 'default'}]}]}}) + kubectl('wait', '--for=condition=Ready', 'pod/controller', '--timeout=60s') + receipt = json.loads(kubectl('exec', 'controller', '--', 'python', '/app/verify.py', label='completed-workflows', timeout=240)) + collect() + for sdk in images: + pods = json.loads(kubectl('get', 'pods', '-l', 'app=orders-' + sdk, '-o', 'json'))['items'] + streams = [] + for pod in pods: + logfile = OUTPUT / (pod['metadata']['name'] + '-shutdown.txt') + stream = logfile.open('w') + process = subprocess.Popen(['kubectl', '--context', 'kind-' + CLUSTER, '-n', NAMESPACE, + 'logs', '-f', pod['metadata']['name']], stdout=stream, stderr=subprocess.STDOUT) + streams.append((process, stream, logfile)) + time.sleep(1) + started = time.monotonic() + kubectl('scale', 'deployment/' + 'orders-' + sdk, '--replicas=0') + kubectl('wait', '--for=delete', 'pod', '-l', 'app=orders-' + sdk, '--timeout=90s', timeout=100) + for process, stream, logfile in streams: + process.wait(timeout=10) + stream.close() + assert '"worker_stopped"' in logfile.read_text(), logfile + receipt[sdk + '_idle_shutdown_seconds'] = time.monotonic() - started + receipt.update(json.loads(kubectl('exec', 'controller', '--', 'python', '/app/verify.py', 'retired', label='deregistered-workers', timeout=45))) + receipt['artifacts'] = ARTIFACTS + receipt['documented_manifest_sha256'] = hashlib.sha256(manifest.encode()).hexdigest() + (OUTPUT / 'receipt.json').write_text(json.dumps(receipt, indent=2) + '\n') + print(json.dumps(receipt)) + finally: + if created: + collect() + run(['kind', 'delete', 'cluster', '--name', CLUSTER], label='kind-delete', timeout=120) + + +if __name__ == '__main__': + main() diff --git a/tests/kubernetes-workers/rust/Cargo.toml b/tests/kubernetes-workers/rust/Cargo.toml new file mode 100644 index 00000000000..15682de658e --- /dev/null +++ b/tests/kubernetes-workers/rust/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "website-kubernetes-worker" +version = "0.1.0" +edition = "2021" +rust-version = "1.90" + +[dependencies] +durable-workflow = "=4.7.0" +serde_json = "1" +tokio = { version = "1", features = ["macros", "rt-multi-thread", "signal"] } diff --git a/tests/kubernetes-workers/rust/src/main.rs b/tests/kubernetes-workers/rust/src/main.rs new file mode 100644 index 00000000000..9eb1b76000f --- /dev/null +++ b/tests/kubernetes-workers/rust/src/main.rs @@ -0,0 +1,27 @@ +use std::{env, time::Duration}; + +use durable_workflow::{Client, Worker}; +use serde_json::json; +use tokio::signal::unix::{signal, SignalKind}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let client = Client::builder(env::var("DW_SERVER_URL")?) + .namespace(env::var("DW_NAMESPACE")?) + .worker_token(Some(env::var("DW_WORKER_TOKEN")?)) + .timeout(Duration::from_secs(5)) + .build()?; + let mut worker = Worker::new(client, env::var("DW_TASK_QUEUE")?) + .worker_id(env::var("DW_WORKER_ID")?) + .poll_timeout(Duration::from_secs(1)); + worker.register_workflow("website336.echo", |context, input| async move { + context.activity("website336.greet", input).await + }); + worker.register_activity("website336.greet", |_, input| async move { + Ok(json!({"sdk": "rust", "name": input[0], "worker_id": env::var("DW_WORKER_ID").unwrap()})) + }); + let mut terminate = signal(SignalKind::terminate())?; + worker.run_until(async { terminate.recv().await; }).await?; + println!("{}", json!({"event": "worker_stopped", "sdk": "rust"})); + Ok(()) +} From 2556189f58ec4efce2b7586c63412a843dfa877b Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Fri, 23 Oct 2026 13:19:45 +0000 Subject: [PATCH 02/12] test: include worker image build recipes in Kubernetes qualification --- tests/kubernetes-workers/php/Dockerfile | 10 ++++++++++ tests/kubernetes-workers/python/Dockerfile | 6 ++++++ tests/kubernetes-workers/rust/Dockerfile | 13 +++++++++++++ 3 files changed, 29 insertions(+) create mode 100644 tests/kubernetes-workers/php/Dockerfile create mode 100644 tests/kubernetes-workers/python/Dockerfile create mode 100644 tests/kubernetes-workers/rust/Dockerfile diff --git a/tests/kubernetes-workers/php/Dockerfile b/tests/kubernetes-workers/php/Dockerfile new file mode 100644 index 00000000000..95f4a1b7b71 --- /dev/null +++ b/tests/kubernetes-workers/php/Dockerfile @@ -0,0 +1,10 @@ +FROM php:8.3.35-cli-bookworm +RUN apt-get update && apt-get install -y --no-install-recommends unzip \ + && docker-php-ext-install pcntl \ + && rm -r /var/lib/apt/lists/* +COPY --from=composer:2.9 /usr/bin/composer /usr/bin/composer +WORKDIR /app +ARG SDK_VERSION +RUN composer require --no-interaction --no-progress --prefer-dist durable-workflow/sdk:${SDK_VERSION} +COPY worker.php . +ENTRYPOINT ["php", "/app/worker.php"] diff --git a/tests/kubernetes-workers/python/Dockerfile b/tests/kubernetes-workers/python/Dockerfile new file mode 100644 index 00000000000..810bb2a9cd0 --- /dev/null +++ b/tests/kubernetes-workers/python/Dockerfile @@ -0,0 +1,6 @@ +FROM python:3.12-slim-bookworm +WORKDIR /app +ARG SDK_VERSION +RUN python -m pip install --no-cache-dir --only-binary=:all: --report=/app/install-report.json durable-workflow==${SDK_VERSION} +COPY worker.py verify.py . +ENTRYPOINT ["python", "/app/worker.py"] diff --git a/tests/kubernetes-workers/rust/Dockerfile b/tests/kubernetes-workers/rust/Dockerfile new file mode 100644 index 00000000000..db68a2a4b3a --- /dev/null +++ b/tests/kubernetes-workers/rust/Dockerfile @@ -0,0 +1,13 @@ +FROM rust:1.90.0-slim-bookworm AS build +WORKDIR /app +COPY Cargo.toml . +COPY src ./src +RUN cargo build --release -j 2 + +FROM debian:bookworm-slim +RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates \ + && rm -r /var/lib/apt/lists/* +WORKDIR /app +COPY --from=build /app/target/release/website-kubernetes-worker /app/worker +COPY --from=build /app/Cargo.lock /app/Cargo.lock +ENTRYPOINT ["/app/worker"] From 66f7a67d1322c607ba5802b6b1563651db4d2740 Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Fri, 23 Oct 2026 13:25:35 +0000 Subject: [PATCH 03/12] docs: translate Kubernetes worker lifecycle guide in all eight locales --- .../current/deployment.md | 79 +++++++++++++++++++ i18n/de/source-hashes.json | 2 +- .../current/deployment.md | 79 +++++++++++++++++++ i18n/es/source-hashes.json | 2 +- .../current/deployment.md | 79 +++++++++++++++++++ i18n/fr/source-hashes.json | 2 +- .../current/deployment.md | 79 +++++++++++++++++++ i18n/ja/source-hashes.json | 2 +- .../current/deployment.md | 79 +++++++++++++++++++ i18n/pt-BR/source-hashes.json | 2 +- .../current/deployment.md | 79 +++++++++++++++++++ i18n/uk/source-hashes.json | 2 +- .../current/deployment.md | 79 +++++++++++++++++++ i18n/zh-Hans/source-hashes.json | 2 +- tests/kubernetes-workers/README.md | 33 ++++++++ tests/kubernetes-workers/qualify.py | 12 ++- 16 files changed, 603 insertions(+), 9 deletions(-) create mode 100644 tests/kubernetes-workers/README.md diff --git a/i18n/de/docusaurus-plugin-content-docs/current/deployment.md b/i18n/de/docusaurus-plugin-content-docs/current/deployment.md index c91446a7cd8..ac0268693f0 100644 --- a/i18n/de/docusaurus-plugin-content-docs/current/deployment.md +++ b/i18n/de/docusaurus-plugin-content-docs/current/deployment.md @@ -373,6 +373,85 @@ curl -H "Authorization: Bearer $DW_ADMIN_TOKEN" http://localhost:8080/api/cluste Einregionen-HA-Failover mit verwalteter Datenbank, verwaltetem Redis, API-Knotenverlust, Worker-Verlust und Schedulerneustart bleibt auf dem Rohmanifestweg ebenso wie im kleinen Cluster supportbegleitet, solange die Veröffentlichungsnachweisprüfung geschlossen ist. Der beabsichtigte Vertrag erfordert die Regeln für Bereitschaft, Singleton-Scheduler und gemeinsame Basissysteme unter [Hochverfügbarkeit und Failover in einer Region](#single-region-high-availability-and-failover). Providerspezifische Load Balancer, Speicherklassen, Netzwerkrichtlinien, Aktiv/Aktiv über mehrere Regionen und selbst gehostetes bedienungsfreies regionales Failover bleiben supportbegleitet oder werden separat vom Rohmanifestvertrag verfolgt. Verwenden Sie den separat versionierten Helm-Weg oben für Helm-verwaltete Installation und Upgrades. +## SDK-Worker auf Kubernetes ausführen {#sdk-workers-on-kubernetes} + +Stellen Sie die Worker Ihrer Anwendung getrennt vom Laravel-Queue-Worker des Servers bereit. Bauen Sie ein Image mit Ihren registrierten PHP-, Python- oder Rust-Handlern und den im Lockfile festgelegten SDK-Abhängigkeiten. Verwenden Sie einen Entrypoint in Exec-Form, damit der Worker-Prozess `SIGTERM` erhält. Ersetzen Sie das folgende Image durch den unveränderlichen Digest dieses Builds. + +Verwenden Sie für Worker im selben Cluster den tatsächlichen Namen, Namespace und Port des Server-Service. Die unbearbeiteten Manifeste stellen `http://durable-workflow-server.durable-workflow.svc.cluster.local:8080` bereit. Ein Helm-Release kann einen anderen Service-Namen verwenden. Worker außerhalb des Clusters benötigen die erreichbare HTTPS-URL des Ingress oder Load Balancers mit normaler Zertifikatsprüfung. Stimmen Sie Long-Poll- und Transportbudgets mit den [Timeout-Hinweisen](#worker-long-poll-timeout-budgets) ab. + +Erstellen Sie im Kubernetes-Namespace des Worker-Pods ein Secret aus dessen vorhandenen Zugangsdaten. Halten Sie den Token aus Manifest und Image heraus. Der Server-Namespace ist eine eigene Produktgrenze und vom Kubernetes-Namespace getrennt. + +```bash +kubectl -n durable-workflow create secret generic orders-worker-auth --from-literal=token="$DW_WORKER_TOKEN" +``` + +Speichern Sie dies als `orders-worker.yaml`. Diese Umgebungsvariablen sind Anwendungskonfiguration: Ihr Entrypoint muss sie an SDK-Client und Worker übergeben. Geben Sie jedem gleichzeitig laufenden Prozess eine eigene Worker-ID und registrieren Sie die Handler für die konfigurierte Queue. + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: orders-worker + namespace: durable-workflow +spec: + replicas: 2 + selector: + matchLabels: + app: orders-worker + strategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 0 + maxSurge: 1 + template: + metadata: + labels: + app: orders-worker + spec: + automountServiceAccountToken: false + terminationGracePeriodSeconds: 75 + containers: + - name: worker + image: registry.example.com/acme/orders-worker:release-id + env: + - name: DW_SERVER_URL + value: http://durable-workflow-server.durable-workflow.svc.cluster.local:8080 + - name: DW_NAMESPACE + value: default + - name: DW_TASK_QUEUE + value: orders + - name: DW_WORKER_ID + valueFrom: + fieldRef: + fieldPath: metadata.uid + - name: DW_WORKER_TOKEN + valueFrom: + secretKeyRef: + name: orders-worker-auth + key: token + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: "1" + memory: 512Mi +``` + +Replikazahlen und Ressourcen sind Ausgangsbeispiele, keine Kapazitätsgarantien. Passen Sie sie an tatsächliche Task-Dauer, Speicherbedarf, Grenzen nachgelagerter Dienste und die [Worker-Zulassungsbudgets](./polyglot/task-queue-admission.md) an. Ein verfügbares Deployment belegt weder die SDK-Registrierung noch erfolgreiche Arbeit. Prüfen Sie aktuelle Worker-Registrierungen sowie erwarteten Build und Queue und starten und beenden Sie anschließend einen repräsentativen Workflow. Ein SDK-Worker erhält durch Kubernetes nicht automatisch einen HTTP-Health-Endpunkt. Verwenden Sie eine anwendungseigene Probe, falls vorhanden, und nicht die Server-Gesundheit als Liveness-Prüfung des Workers. + +### SIGTERM zustellen und die Bereinigung abwarten {#sdk-worker-pod-shutdown} + +| SDK | Beenden des Worker-Prozesses | +| --- | --- | +| PHP | Verwenden Sie die verwaltete Schleife `Worker::run()` mit installiertem `pcntl`. Ihr Signal-Handler fordert das Herunterfahren an. Der aktive synchrone Task erreicht danach eine Grenze vor der verwalteten Bereinigung und Deregistrierung. Eine eigene Schleife um `tick()` stellt diesen Lebenszyklus nicht bereit. Siehe [geordnetes Herunterfahren in PHP](https://php.durable-workflow.com/operate/graceful-shutdown/). | +| Python | Verbinden Sie `SIGTERM` mit dem Shutdown-Pfad desselben Workers und warten Sie sowohl auf `Worker.stop()` als auch auf dessen Run-Task. Setzen Sie `shutdown_timeout` ausdrücklich. Angenommene Arbeit teilt sich diese Frist zum Leeren. Das Abbrechen eines Await kann keinen laufenden synchronen Thread zwangsweise stoppen. Ein unbestätigter Stopp bleibt ein Fehler. Verwenden Sie das [Beispiel mit getrennten Prozessen und Signalbehandlung](https://python.durable-workflow.com/guides/separate-workers/) und den [Lebenszyklusvertrag](https://python.durable-workflow.com/reference/worker/). | +| Rust | Übergeben Sie `Worker::run_until(...)` ein Future, das bei Unix-`SIGTERM` abgeschlossen wird, und warten Sie auf dessen Ergebnis. Ein Future nur für Strg+C behandelt das Beendigungssignal des Pods nicht. Asynchrone Handler müssen Kontrolle abgeben; begrenzen Sie blockierende Arbeit gesondert. Siehe [Worker::run_until](https://rust.durable-workflow.com/durable_workflow/struct.Worker.html#method.run_until). | + +Planen Sie in `terminationGracePeriodSeconds` genug Zeit für Polling, das Beenden angenommener Tasks und die HTTP-, Session- und Registrierungsbereinigung des installierten SDKs ein. Prüfen Sie die 75 Sekunden des Beispiels gegen die tatsächlichen Grenzen Ihrer Anwendung. Kubernetes rechnet jeden `preStop`-Hook auf dieselbe Frist an und beendet verbleibende Prozesse danach zwangsweise. Ein reiner Sleep-Hook fordert weder das Leeren laufender Arbeit an noch verlängert er die SDK-Frist. Siehe den [Ablauf der Pod-Beendigung in Kubernetes](https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-termination-flow). + +Erproben Sie die Beendigung im Leerlauf und während zeitlich begrenzter Arbeit. Behandeln Sie Bereinigungsfehler, eine verbleibende Registrierung oder erzwungenes Beenden als unvollständiges Herunterfahren. Ein Ersatz-Worker setzt dauerhafte Arbeit gemäß den aufgezeichneten Leases, Wiederholungen und Fristen fort. Das Stoppen eines Pods bricht keinen Workflow ab und macht keinen externen Seiteneffekt rückgängig. Folgen Sie vor einem Build-Wechsel dem [Rollout nach Worker-Build-ID](./polyglot/worker-build-id-rollout.md) und behalten Sie dessen Kompatibilitäts-, Replay- und Rollback-Prüfungen bei. + ## Hochverfügbarkeit und Failover in einer Region {#single-region-high-availability-and-failover} Einregionen-HA-Failover ist derzeit ein **unverifizierter, supportbegleiteter** Vertragskandidat auf Basis des kleinen Clusters und der unveränderten Kubernetes-Form. Er zielt auf Fehlermodi, für deren Überstehen die Engine **innerhalb einer Region** ausgelegt ist: Failover verwalteter Datenbanken wie RDS Multi-AZ, Aurora-Cluster, Cloud SQL HA oder Patroni-Hochstufung; Failover verwalteten Redis wie Sentinel, Elasticache-Replikationsgruppen oder Memorystore HA; API-Knotenverlust, Worker-Verlust und Neustart des Schedulers/Wartungsprozesses. Regionsübergreifende Aktiv/Passiv-Wiederherstellung ist eine andere supportbegleitete Evaluierung. Siehe [Aktiv/Passiv über mehrere Regionen](#activepassive-multi-region). diff --git a/i18n/de/source-hashes.json b/i18n/de/source-hashes.json index 19f0a3f0330..117d7708dbc 100644 --- a/i18n/de/source-hashes.json +++ b/i18n/de/source-hashes.json @@ -27,7 +27,7 @@ "defining-workflows/workflow-id.md": "91b94211e2f8f405895ad82cfe2ef9baadb7a2cbd06e9c1e7aa6362231e0d424", "defining-workflows/workflow-status.md": "8e8cb27ff44c890d489d89bf166375acfdfaecabd990a2235e106fab1592d529", "defining-workflows/workflows.md": "f467bc73de32aa5d722c61ce085065460382ae7dd44cea4625847d9fcdc4c97c", - "deployment.md": "aa76a132fd78a27428dd9b8d9ab6f81aa8661b3f302eb799877688b77f508596", + "deployment.md": "7ad56fc682a495fef95c0de365953b904ec2a82653d6f2e109dc22f8659b8d5d", "failures-and-recovery.md": "318f0cc183d26539c7d5b6bdf791429bb874b793b81cff4c6094ef1d11a796b2", "features/activity-execution-model.md": "e8c37520ae46a7efb30438f4fc06bf893c1df430cb1f92c48963697adaf09082", "features/cancel-and-terminate.md": "c678fc7a0c9b11f0e1bee295aff259de9803219c0aa6769fb801622799e7deb1", diff --git a/i18n/es/docusaurus-plugin-content-docs/current/deployment.md b/i18n/es/docusaurus-plugin-content-docs/current/deployment.md index 7dd41f7ab23..e7f2ee38eaa 100644 --- a/i18n/es/docusaurus-plugin-content-docs/current/deployment.md +++ b/i18n/es/docusaurus-plugin-content-docs/current/deployment.md @@ -478,6 +478,85 @@ activo/activo y conmutación regional sin intervención requieren soporte o un seguimiento separado. Usa la opción Helm versionada aparte para gestionar con Helm la instalación y las actualizaciones. +## Ejecutar workers de SDK en Kubernetes {#sdk-workers-on-kubernetes} + +Despliega los workers de tu aplicación por separado del worker de colas Laravel del Server. Crea una imagen con tus manejadores PHP, Python o Rust registrados y las dependencias del SDK fijadas en su archivo de bloqueo. Usa un entrypoint en formato exec para que el proceso worker reciba `SIGTERM`. Sustituye la imagen siguiente por el digest inmutable de esa compilación. + +Para workers del mismo clúster, usa el nombre, namespace y puerto reales del Service del Server. Los manifiestos sin procesar exponen `http://durable-workflow-server.durable-workflow.svc.cluster.local:8080`. Una versión de Helm puede usar otro nombre de Service. Los workers externos necesitan la URL HTTPS accesible del ingress o balanceador y la verificación normal del certificado. Coordina los presupuestos de long poll y transporte con [la guía de tiempos de espera](#worker-long-poll-timeout-budgets). + +Crea un Secret en el namespace de Kubernetes del Pod del worker con su credencial existente. Mantén el token fuera del manifiesto y de la imagen. El namespace del Server es un límite del producto distinto del namespace de Kubernetes. + +```bash +kubectl -n durable-workflow create secret generic orders-worker-auth --from-literal=token="$DW_WORKER_TOKEN" +``` + +Guarda esto como `orders-worker.yaml`. Estos nombres de variables de entorno son configuración de la aplicación: su entrypoint debe pasarlos al cliente del SDK y al Worker. Asigna a cada proceso activo un worker ID propio y registra los manejadores de la cola configurada. + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: orders-worker + namespace: durable-workflow +spec: + replicas: 2 + selector: + matchLabels: + app: orders-worker + strategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 0 + maxSurge: 1 + template: + metadata: + labels: + app: orders-worker + spec: + automountServiceAccountToken: false + terminationGracePeriodSeconds: 75 + containers: + - name: worker + image: registry.example.com/acme/orders-worker:release-id + env: + - name: DW_SERVER_URL + value: http://durable-workflow-server.durable-workflow.svc.cluster.local:8080 + - name: DW_NAMESPACE + value: default + - name: DW_TASK_QUEUE + value: orders + - name: DW_WORKER_ID + valueFrom: + fieldRef: + fieldPath: metadata.uid + - name: DW_WORKER_TOKEN + valueFrom: + secretKeyRef: + name: orders-worker-auth + key: token + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: "1" + memory: 512Mi +``` + +Las réplicas y los recursos son ejemplos iniciales, no garantías de capacidad. Ajústalos según la duración real de las tareas, la memoria, los límites de servicios dependientes y los [presupuestos de admisión del worker](./polyglot/task-queue-admission.md). Un Deployment disponible no demuestra que el SDK se haya registrado ni que procese trabajo útil. Comprueba registros recientes y la compilación y cola esperadas; después inicia y completa un workflow representativo. Ejecutarse en Kubernetes no proporciona automáticamente un endpoint HTTP de salud al worker del SDK. Usa una sonda de la aplicación cuando exista y no uses la salud del Server como sonda de actividad del worker. + +### Entregar SIGTERM y esperar la limpieza {#sdk-worker-pod-shutdown} + +| SDK | Cierre del proceso worker | +| --- | --- | +| PHP | Usa el bucle administrado `Worker::run()` con `pcntl` instalado. Su manejador de señales solicita el cierre; la tarea síncrona activa alcanza un límite antes de la limpieza y eliminación del registro administradas. Un bucle propio sobre `tick()` no proporciona ese ciclo de vida. Consulta el [cierre ordenado de PHP](https://php.durable-workflow.com/operate/graceful-shutdown/). | +| Python | Conecta `SIGTERM` al cierre del mismo Worker y espera tanto `Worker.stop()` como su tarea de ejecución. Configura `shutdown_timeout` explícitamente. El trabajo aceptado comparte ese plazo de drenaje. Cancelar un await no puede detener por la fuerza un hilo síncrono activo; una detención sin confirmar sigue siendo un error. Usa el [ejemplo de procesos separados y señales](https://python.durable-workflow.com/guides/separate-workers/) y el [contrato del ciclo de vida](https://python.durable-workflow.com/reference/worker/). | +| Rust | Pasa a `Worker::run_until(...)` un future que se resuelva con Unix `SIGTERM` y espera su resultado. Un future que solo maneje Ctrl-C no procesa la señal de terminación del Pod. Los manejadores asíncronos deben ceder el control; limita el trabajo bloqueante por separado. Consulta [Worker::run_until](https://rust.durable-workflow.com/durable_workflow/struct.Worker.html#method.run_until). | + +Da a `terminationGracePeriodSeconds` tiempo suficiente para el poll, el cierre de tareas aceptadas y la limpieza de HTTP, sesiones y registro del SDK instalado. Comprueba los 75 segundos del ejemplo frente a los límites reales de tu aplicación. Kubernetes incluye cualquier hook `preStop` en ese mismo período de gracia y fuerza la detención de los procesos restantes al agotarse. Un hook que solo duerme no solicita al SDK que drene ni amplía su plazo. Consulta el [flujo de terminación de Pods de Kubernetes](https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-termination-flow). + +Ensaya la terminación en reposo y durante trabajo con duración acotada. Considera los errores de limpieza, un registro retenido o una terminación forzada como un cierre incompleto. Un worker de reemplazo continúa el trabajo durable según sus leases, reintentos y plazos registrados. Detener un Pod no cancela un workflow ni revierte un efecto externo. Antes de cambiar de compilación, sigue el [despliegue por build ID del worker](./polyglot/worker-build-id-rollout.md) y conserva sus comprobaciones de compatibilidad, replay y reversión. + ## Alta disponibilidad y conmutación en una región {#single-region-high-availability-and-failover} La conmutación de alta disponibilidad regional es actualmente un contrato diff --git a/i18n/es/source-hashes.json b/i18n/es/source-hashes.json index 19f0a3f0330..117d7708dbc 100644 --- a/i18n/es/source-hashes.json +++ b/i18n/es/source-hashes.json @@ -27,7 +27,7 @@ "defining-workflows/workflow-id.md": "91b94211e2f8f405895ad82cfe2ef9baadb7a2cbd06e9c1e7aa6362231e0d424", "defining-workflows/workflow-status.md": "8e8cb27ff44c890d489d89bf166375acfdfaecabd990a2235e106fab1592d529", "defining-workflows/workflows.md": "f467bc73de32aa5d722c61ce085065460382ae7dd44cea4625847d9fcdc4c97c", - "deployment.md": "aa76a132fd78a27428dd9b8d9ab6f81aa8661b3f302eb799877688b77f508596", + "deployment.md": "7ad56fc682a495fef95c0de365953b904ec2a82653d6f2e109dc22f8659b8d5d", "failures-and-recovery.md": "318f0cc183d26539c7d5b6bdf791429bb874b793b81cff4c6094ef1d11a796b2", "features/activity-execution-model.md": "e8c37520ae46a7efb30438f4fc06bf893c1df430cb1f92c48963697adaf09082", "features/cancel-and-terminate.md": "c678fc7a0c9b11f0e1bee295aff259de9803219c0aa6769fb801622799e7deb1", diff --git a/i18n/fr/docusaurus-plugin-content-docs/current/deployment.md b/i18n/fr/docusaurus-plugin-content-docs/current/deployment.md index 4f2a64166f7..c1304442aa4 100644 --- a/i18n/fr/docusaurus-plugin-content-docs/current/deployment.md +++ b/i18n/fr/docusaurus-plugin-content-docs/current/deployment.md @@ -491,6 +491,85 @@ actif/actif et basculement régional sans intervention restent accompagnés ou suivis séparément. Utilisez le parcours Helm versionné séparément pour une installation et des mises à niveau gérées par Helm. +## Exécuter des workers SDK sur Kubernetes {#sdk-workers-on-kubernetes} + +Déployez les workers de votre application séparément du worker de file Laravel du Server. Construisez une image contenant vos gestionnaires PHP, Python ou Rust enregistrés et les dépendances SDK verrouillées. Utilisez un entrypoint au format exec pour que le processus worker reçoive `SIGTERM`. Remplacez l’image ci-dessous par le digest immuable de cette build. + +Pour les workers du même cluster, utilisez le nom, l’espace de noms et le port réels du Service du Server. Les manifestes bruts exposent `http://durable-workflow-server.durable-workflow.svc.cluster.local:8080`. Une version Helm peut employer un autre nom de Service. Les workers externes ont besoin de l’URL HTTPS accessible de l’ingress ou de l’équilibreur, avec la vérification normale du certificat. Alignez les budgets de long poll et de transport sur [les conseils de délai d’attente](#worker-long-poll-timeout-budgets). + +Créez un Secret dans l’espace de noms Kubernetes du Pod worker à partir de son identifiant d’accès existant. Gardez le jeton hors du manifeste et de l’image. L’espace de noms du Server constitue une frontière du produit distincte de l’espace de noms Kubernetes. + +```bash +kubectl -n durable-workflow create secret generic orders-worker-auth --from-literal=token="$DW_WORKER_TOKEN" +``` + +Enregistrez ceci dans `orders-worker.yaml`. Ces noms de variables d’environnement relèvent de la configuration de l’application : son entrypoint doit les transmettre au client SDK et au Worker. Attribuez à chaque processus actif son propre worker ID et enregistrez les gestionnaires de la file configurée. + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: orders-worker + namespace: durable-workflow +spec: + replicas: 2 + selector: + matchLabels: + app: orders-worker + strategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 0 + maxSurge: 1 + template: + metadata: + labels: + app: orders-worker + spec: + automountServiceAccountToken: false + terminationGracePeriodSeconds: 75 + containers: + - name: worker + image: registry.example.com/acme/orders-worker:release-id + env: + - name: DW_SERVER_URL + value: http://durable-workflow-server.durable-workflow.svc.cluster.local:8080 + - name: DW_NAMESPACE + value: default + - name: DW_TASK_QUEUE + value: orders + - name: DW_WORKER_ID + valueFrom: + fieldRef: + fieldPath: metadata.uid + - name: DW_WORKER_TOKEN + valueFrom: + secretKeyRef: + name: orders-worker-auth + key: token + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: "1" + memory: 512Mi +``` + +Les nombres de réplicas et les ressources sont des exemples de départ, pas des garanties de capacité. Ajustez-les selon la durée réelle des tâches, la mémoire, les limites des services en aval et les [budgets d’admission des workers](./polyglot/task-queue-admission.md). La disponibilité d’un Deployment ne prouve ni l’enregistrement du SDK ni un travail utile. Vérifiez des enregistrements récents ainsi que la build et la file attendues, puis démarrez et terminez un workflow représentatif. Exécuter un worker SDK sur Kubernetes ne lui fournit pas automatiquement un endpoint HTTP de santé. Utilisez une sonde propre à l’application si elle existe et non la santé du Server comme sonde de vie du worker. + +### Transmettre SIGTERM et attendre le nettoyage {#sdk-worker-pod-shutdown} + +| SDK | Arrêt du processus worker | +| --- | --- | +| PHP | Utilisez la boucle gérée `Worker::run()` avec `pcntl` installé. Son gestionnaire de signal demande l’arrêt ; la tâche synchrone active atteint ensuite une limite avant le nettoyage et la désinscription gérés. Une boucle personnalisée autour de `tick()` ne fournit pas ce cycle de vie. Voir l’[arrêt gracieux PHP](https://php.durable-workflow.com/operate/graceful-shutdown/). | +| Python | Reliez `SIGTERM` au chemin d’arrêt du même Worker et attendez à la fois `Worker.stop()` et sa tâche d’exécution. Définissez explicitement `shutdown_timeout`. Le travail accepté partage cette échéance de drainage. Annuler un await ne peut pas arrêter de force un thread synchrone actif ; un arrêt non confirmé reste une erreur. Utilisez l’[exemple de processus séparés avec gestion des signaux](https://python.durable-workflow.com/guides/separate-workers/) et le [contrat de cycle de vie](https://python.durable-workflow.com/reference/worker/). | +| Rust | Passez à `Worker::run_until(...)` un future qui se résout sur Unix `SIGTERM` et attendez son résultat. Un future limité à Ctrl-C ne gère pas le signal de terminaison du Pod. Les gestionnaires asynchrones doivent rendre la main ; bornez séparément le travail bloquant. Voir [Worker::run_until](https://rust.durable-workflow.com/durable_workflow/struct.Worker.html#method.run_until). | + +Prévoyez dans `terminationGracePeriodSeconds` assez de temps pour le poll, l’arrêt du travail accepté et le nettoyage HTTP, des sessions et de l’enregistrement du SDK installé. Comparez les 75 secondes de l’exemple aux limites réelles de votre application. Kubernetes compte tout hook `preStop` dans ce même délai de grâce et force l’arrêt des processus restants à son expiration. Un hook qui dort seulement ne demande pas au SDK de drainer le travail et ne prolonge pas son échéance. Voir le [déroulement de la terminaison d’un Pod Kubernetes](https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-termination-flow). + +Répétez la terminaison au repos et pendant un travail de durée bornée. Traitez une erreur de nettoyage, un enregistrement conservé ou un arrêt forcé comme un arrêt incomplet. Un worker de remplacement poursuit le travail durable selon les baux, tentatives et échéances enregistrés. Arrêter un Pod n’annule pas un workflow et ne défait pas un effet externe. Avant de changer de build, suivez le [déploiement par build ID du worker](./polyglot/worker-build-id-rollout.md) et conservez ses contrôles de compatibilité, de relecture et de retour arrière. + ## Haute disponibilité et basculement monorégion {#single-region-high-availability-and-failover} Ce basculement est actuellement un candidat de contrat **non vérifié et diff --git a/i18n/fr/source-hashes.json b/i18n/fr/source-hashes.json index 19f0a3f0330..117d7708dbc 100644 --- a/i18n/fr/source-hashes.json +++ b/i18n/fr/source-hashes.json @@ -27,7 +27,7 @@ "defining-workflows/workflow-id.md": "91b94211e2f8f405895ad82cfe2ef9baadb7a2cbd06e9c1e7aa6362231e0d424", "defining-workflows/workflow-status.md": "8e8cb27ff44c890d489d89bf166375acfdfaecabd990a2235e106fab1592d529", "defining-workflows/workflows.md": "f467bc73de32aa5d722c61ce085065460382ae7dd44cea4625847d9fcdc4c97c", - "deployment.md": "aa76a132fd78a27428dd9b8d9ab6f81aa8661b3f302eb799877688b77f508596", + "deployment.md": "7ad56fc682a495fef95c0de365953b904ec2a82653d6f2e109dc22f8659b8d5d", "failures-and-recovery.md": "318f0cc183d26539c7d5b6bdf791429bb874b793b81cff4c6094ef1d11a796b2", "features/activity-execution-model.md": "e8c37520ae46a7efb30438f4fc06bf893c1df430cb1f92c48963697adaf09082", "features/cancel-and-terminate.md": "c678fc7a0c9b11f0e1bee295aff259de9803219c0aa6769fb801622799e7deb1", diff --git a/i18n/ja/docusaurus-plugin-content-docs/current/deployment.md b/i18n/ja/docusaurus-plugin-content-docs/current/deployment.md index 5dc72ca5e07..e705139c9d3 100644 --- a/i18n/ja/docusaurus-plugin-content-docs/current/deployment.md +++ b/i18n/ja/docusaurus-plugin-content-docs/current/deployment.md @@ -351,6 +351,85 @@ curl -H "Authorization: Bearer $DW_ADMIN_TOKEN" http://localhost:8080/api/cluste 単一リージョン HA は、証拠ゲート閉鎖中は小規模クラスター同様サポート主導です。[以下の HA](#single-region-high-availability-and-failover)にある準備状態、単一スケジューラー、共有基盤のルールが必要です。プロバイダー固有 LB、storage class、network policy、アクティブ/アクティブ複数リージョン、無人切替はサポート主導または別追跡です。Helm 管理の導入・更新には、独立バージョンの Helm 経路を使います。 +## Kubernetes で SDK Worker を実行する {#sdk-workers-on-kubernetes} + +アプリケーションの Worker は、Server の Laravel キュー Worker と分けてデプロイします。登録済みの PHP、Python、Rust ハンドラーと、ロックした SDK 依存関係を含むイメージをビルドしてください。Worker プロセスが `SIGTERM` を受け取れるよう、exec 形式の entrypoint を使います。以下のイメージを、そのビルドの変更不能な digest に置き換えてください。 + +同じクラスター内の Worker では、Server Service の実際の名前、名前空間、ポートを使います。生のマニフェストは `http://durable-workflow-server.durable-workflow.svc.cluster.local:8080` を公開します。Helm リリースでは Service 名が異なる場合があります。クラスター外の Worker には、到達可能な ingress またはロードバランサーの HTTPS URL と、通常の証明書検証が必要です。long poll とトランスポートの時間枠は[タイムアウトの説明](#worker-long-poll-timeout-budgets)に合わせてください。 + +既存の Worker 認証情報から、Worker Pod の Kubernetes 名前空間に Secret を作成します。トークンはマニフェストやイメージに含めないでください。Server の名前空間は製品上の別の境界であり、Kubernetes の名前空間とは異なります。 + +```bash +kubectl -n durable-workflow create secret generic orders-worker-auth --from-literal=token="$DW_WORKER_TOKEN" +``` + +これを `orders-worker.yaml` として保存します。これらの環境変数名はアプリケーションの設定です。entrypoint から SDK クライアントと Worker に渡す必要があります。同時に動作する各プロセスに固有の worker ID を与え、設定したキューのハンドラーを登録してください。 + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: orders-worker + namespace: durable-workflow +spec: + replicas: 2 + selector: + matchLabels: + app: orders-worker + strategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 0 + maxSurge: 1 + template: + metadata: + labels: + app: orders-worker + spec: + automountServiceAccountToken: false + terminationGracePeriodSeconds: 75 + containers: + - name: worker + image: registry.example.com/acme/orders-worker:release-id + env: + - name: DW_SERVER_URL + value: http://durable-workflow-server.durable-workflow.svc.cluster.local:8080 + - name: DW_NAMESPACE + value: default + - name: DW_TASK_QUEUE + value: orders + - name: DW_WORKER_ID + valueFrom: + fieldRef: + fieldPath: metadata.uid + - name: DW_WORKER_TOKEN + valueFrom: + secretKeyRef: + name: orders-worker-auth + key: token + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: "1" + memory: 512Mi +``` + +レプリカ数とリソース値は出発点となる例であり、処理能力の保証ではありません。実際のタスク時間、メモリー使用量、下流サービスの制限、[Worker の受け入れ予算](./polyglot/task-queue-admission.md)に合わせて調整します。Deployment が利用可能になっても、SDK の登録や有用な処理の証明にはなりません。新鮮な Worker 登録と想定するビルド、キューを確認し、代表的なワークフローを開始して完了させてください。Kubernetes で実行するだけでは、SDK Worker に HTTP ヘルスエンドポイントは追加されません。アプリケーションに probe がある場合はそれを使い、Server のヘルスを Worker の liveness 検査に使わないでください。 + +### SIGTERM を届けて後処理を待つ {#sdk-worker-pod-shutdown} + +| SDK | Worker プロセスの終了 | +| --- | --- | +| PHP | `pcntl` をインストールし、管理対象の `Worker::run()` ループを使います。シグナルハンドラーが終了を要求し、実行中の同期タスクが区切りに達してから、管理対象の後処理と登録解除を行います。`tick()` を呼ぶ独自ループには、このライフサイクルはありません。[PHP の正常終了](https://php.durable-workflow.com/operate/graceful-shutdown/)を参照してください。 | +| Python | `SIGTERM` を同じ Worker の終了処理につなぎ、`Worker.stop()` と実行タスクの両方を await します。`shutdown_timeout` を明示的に設定してください。受け入れ済み処理は、この共通ドレイン期限を使います。await のキャンセルでは、実行中の同期スレッドを強制停止できません。停止を確認できない場合はエラーのままです。[プロセス分離とシグナル設定の例](https://python.durable-workflow.com/guides/separate-workers/)と[ライフサイクル契約](https://python.durable-workflow.com/reference/worker/)を使ってください。 | +| Rust | Unix の `SIGTERM` で完了する future を `Worker::run_until(...)` に渡し、その結果を await します。Ctrl-C だけを待つ future は Pod の終了シグナルを扱いません。非同期ハンドラーは実行権を譲る必要があり、ブロッキング処理には別の上限を設けます。[Worker::run_until](https://rust.durable-workflow.com/durable_workflow/struct.Worker.html#method.run_until)を参照してください。 | + +`terminationGracePeriodSeconds` には、インストール済み SDK の poll、受け入れ済みタスクの終了、HTTP、セッション、登録の後処理に十分な時間を確保します。例の 75 秒は、アプリケーションの実際の上限に照らして確認してください。Kubernetes は `preStop` hook も同じ猶予時間に含め、期限後に残ったプロセスを強制停止します。sleep だけの hook は SDK にドレインを要求せず、期限も延長しません。[Kubernetes の Pod 終了フロー](https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-termination-flow)を参照してください。 + +アイドル時と、所要時間を制限した処理の実行中に終了をリハーサルしてください。後処理エラー、残った登録、強制終了は、不完全な停止として扱います。代替 Worker は、記録済みのリース、リトライ、期限に従って永続処理を続行します。Pod の停止はワークフローのキャンセルや外部副作用の取り消しにはなりません。ビルドを変更する前に、[Worker の build ID ロールアウト](./polyglot/worker-build-id-rollout.md)に従い、互換性、リプレイ、ロールバックの確認を維持してください。 + ## 単一リージョンの高可用性とフェイルオーバー {#single-region-high-availability-and-failover} 現在は、小規模クラスター・生 Kubernetes 上に重ねる**未検証のサポート主導**契約候補です。**一つのリージョン内**で、管理 DB(RDS Multi-AZ、Aurora、Cloud SQL HA、Patroni 昇格等)・管理 Redis(Sentinel、Elasticache replication-group、Memorystore HA 等)の切替、API・Worker 喪失、スケジューラー・保守再起動への耐性を目標にします。リージョン間アクティブ/パッシブは別の[評価](#activepassive-multi-region)です。 diff --git a/i18n/ja/source-hashes.json b/i18n/ja/source-hashes.json index 19f0a3f0330..117d7708dbc 100644 --- a/i18n/ja/source-hashes.json +++ b/i18n/ja/source-hashes.json @@ -27,7 +27,7 @@ "defining-workflows/workflow-id.md": "91b94211e2f8f405895ad82cfe2ef9baadb7a2cbd06e9c1e7aa6362231e0d424", "defining-workflows/workflow-status.md": "8e8cb27ff44c890d489d89bf166375acfdfaecabd990a2235e106fab1592d529", "defining-workflows/workflows.md": "f467bc73de32aa5d722c61ce085065460382ae7dd44cea4625847d9fcdc4c97c", - "deployment.md": "aa76a132fd78a27428dd9b8d9ab6f81aa8661b3f302eb799877688b77f508596", + "deployment.md": "7ad56fc682a495fef95c0de365953b904ec2a82653d6f2e109dc22f8659b8d5d", "failures-and-recovery.md": "318f0cc183d26539c7d5b6bdf791429bb874b793b81cff4c6094ef1d11a796b2", "features/activity-execution-model.md": "e8c37520ae46a7efb30438f4fc06bf893c1df430cb1f92c48963697adaf09082", "features/cancel-and-terminate.md": "c678fc7a0c9b11f0e1bee295aff259de9803219c0aa6769fb801622799e7deb1", diff --git a/i18n/pt-BR/docusaurus-plugin-content-docs/current/deployment.md b/i18n/pt-BR/docusaurus-plugin-content-docs/current/deployment.md index e28bfc4c83b..efa1fea6ed7 100644 --- a/i18n/pt-BR/docusaurus-plugin-content-docs/current/deployment.md +++ b/i18n/pt-BR/docusaurus-plugin-content-docs/current/deployment.md @@ -505,6 +505,85 @@ intervenção sob sua administração continuam conduzidos pelo suporte ou trata separadamente do contrato de manifestos diretos. Use o caminho Helm com versão independente acima quando quiser instalação e atualizações gerenciadas pelo Helm. +## Executar workers dos SDKs no Kubernetes {#sdk-workers-on-kubernetes} + +Implante os workers da aplicação separadamente do worker de filas Laravel do Server. Crie uma imagem com seus handlers PHP, Python ou Rust registrados e as dependências do SDK fixadas no arquivo de lock. Use um entrypoint no formato exec para que o processo worker receba `SIGTERM`. Substitua a imagem abaixo pelo digest imutável dessa build. + +Para workers no mesmo cluster, use o nome, namespace e porta reais do Service do Server. Os manifestos brutos expõem `http://durable-workflow-server.durable-workflow.svc.cluster.local:8080`. Uma release Helm pode usar outro nome de Service. Workers fora do cluster precisam da URL HTTPS acessível do ingress ou balanceador, com verificação normal do certificado. Alinhe os orçamentos de long poll e transporte com [as orientações de timeout](#worker-long-poll-timeout-budgets). + +Crie um Secret no namespace Kubernetes do Pod do worker usando sua credencial existente. Mantenha o token fora do manifesto e da imagem. O namespace do Server é um limite do produto distinto do namespace Kubernetes. + +```bash +kubectl -n durable-workflow create secret generic orders-worker-auth --from-literal=token="$DW_WORKER_TOKEN" +``` + +Salve como `orders-worker.yaml`. Esses nomes de variáveis de ambiente são configuração da aplicação: o entrypoint deve passá-los ao cliente do SDK e ao Worker. Dê a cada processo ativo seu próprio worker ID e registre os handlers da fila configurada. + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: orders-worker + namespace: durable-workflow +spec: + replicas: 2 + selector: + matchLabels: + app: orders-worker + strategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 0 + maxSurge: 1 + template: + metadata: + labels: + app: orders-worker + spec: + automountServiceAccountToken: false + terminationGracePeriodSeconds: 75 + containers: + - name: worker + image: registry.example.com/acme/orders-worker:release-id + env: + - name: DW_SERVER_URL + value: http://durable-workflow-server.durable-workflow.svc.cluster.local:8080 + - name: DW_NAMESPACE + value: default + - name: DW_TASK_QUEUE + value: orders + - name: DW_WORKER_ID + valueFrom: + fieldRef: + fieldPath: metadata.uid + - name: DW_WORKER_TOKEN + valueFrom: + secretKeyRef: + name: orders-worker-auth + key: token + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: "1" + memory: 512Mi +``` + +Réplicas e recursos são exemplos iniciais, não garantias de capacidade. Ajuste-os conforme a duração real das tarefas, o uso de memória, os limites dos serviços dependentes e os [orçamentos de admissão do worker](./polyglot/task-queue-admission.md). Um Deployment disponível não comprova o registro do SDK nem trabalho útil. Verifique registros recentes e a build e fila esperadas; depois inicie e conclua um workflow representativo. Executar no Kubernetes não fornece automaticamente um endpoint HTTP de saúde ao worker do SDK. Use uma probe da aplicação quando houver uma e não use a saúde do Server como liveness do worker. + +### Entregar SIGTERM e aguardar a limpeza {#sdk-worker-pod-shutdown} + +| SDK | Encerramento do processo worker | +| --- | --- | +| PHP | Use o loop gerenciado `Worker::run()` com `pcntl` instalado. Seu handler de sinais solicita a parada; a tarefa síncrona ativa chega a um ponto de parada antes da limpeza e remoção do registro gerenciadas. Um loop próprio em torno de `tick()` não fornece esse ciclo de vida. Veja o [encerramento gracioso do PHP](https://php.durable-workflow.com/operate/graceful-shutdown/). | +| Python | Conecte `SIGTERM` ao encerramento do mesmo Worker e aguarde tanto `Worker.stop()` quanto sua tarefa de execução. Defina `shutdown_timeout` explicitamente. O trabalho aceito compartilha esse prazo de drenagem. Cancelar um await não força a parada de uma thread síncrona ativa; uma parada não confirmada continua sendo um erro. Use o [exemplo de processos separados e sinais](https://python.durable-workflow.com/guides/separate-workers/) e o [contrato do ciclo de vida](https://python.durable-workflow.com/reference/worker/). | +| Rust | Passe a `Worker::run_until(...)` um future que resolva com Unix `SIGTERM` e aguarde seu resultado. Um future que trate apenas Ctrl-C não processa o sinal de término do Pod. Os handlers assíncronos devem ceder o controle; limite o trabalho bloqueante separadamente. Veja [Worker::run_until](https://rust.durable-workflow.com/durable_workflow/struct.Worker.html#method.run_until). | + +Reserve em `terminationGracePeriodSeconds` tempo suficiente para o poll, o encerramento das tarefas aceitas e a limpeza de HTTP, sessões e registro do SDK instalado. Confira os 75 segundos do exemplo contra os limites reais da aplicação. O Kubernetes inclui qualquer hook `preStop` nesse mesmo período de tolerância e força a parada dos processos restantes após seu término. Um hook que apenas dorme não solicita drenagem ao SDK nem estende seu prazo. Veja o [fluxo de término de Pods do Kubernetes](https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-termination-flow). + +Ensaie o término em estado ocioso e durante trabalho com duração limitada. Trate erros de limpeza, registro retido ou encerramento forçado como parada incompleta. Um worker substituto continua o trabalho durável conforme seus leases, tentativas e prazos registrados. Parar um Pod não cancela um workflow nem desfaz um efeito externo. Antes de mudar de build, siga o [rollout por build ID do worker](./polyglot/worker-build-id-rollout.md) e mantenha suas verificações de compatibilidade, replay e rollback. + ## Alta disponibilidade e failover em região única {#single-region-high-availability-and-failover} O failover de alta disponibilidade em região única é atualmente um contrato diff --git a/i18n/pt-BR/source-hashes.json b/i18n/pt-BR/source-hashes.json index 19f0a3f0330..117d7708dbc 100644 --- a/i18n/pt-BR/source-hashes.json +++ b/i18n/pt-BR/source-hashes.json @@ -27,7 +27,7 @@ "defining-workflows/workflow-id.md": "91b94211e2f8f405895ad82cfe2ef9baadb7a2cbd06e9c1e7aa6362231e0d424", "defining-workflows/workflow-status.md": "8e8cb27ff44c890d489d89bf166375acfdfaecabd990a2235e106fab1592d529", "defining-workflows/workflows.md": "f467bc73de32aa5d722c61ce085065460382ae7dd44cea4625847d9fcdc4c97c", - "deployment.md": "aa76a132fd78a27428dd9b8d9ab6f81aa8661b3f302eb799877688b77f508596", + "deployment.md": "7ad56fc682a495fef95c0de365953b904ec2a82653d6f2e109dc22f8659b8d5d", "failures-and-recovery.md": "318f0cc183d26539c7d5b6bdf791429bb874b793b81cff4c6094ef1d11a796b2", "features/activity-execution-model.md": "e8c37520ae46a7efb30438f4fc06bf893c1df430cb1f92c48963697adaf09082", "features/cancel-and-terminate.md": "c678fc7a0c9b11f0e1bee295aff259de9803219c0aa6769fb801622799e7deb1", diff --git a/i18n/uk/docusaurus-plugin-content-docs/current/deployment.md b/i18n/uk/docusaurus-plugin-content-docs/current/deployment.md index ef960960af6..a5fb4f75584 100644 --- a/i18n/uk/docusaurus-plugin-content-docs/current/deployment.md +++ b/i18n/uk/docusaurus-plugin-content-docs/current/deployment.md @@ -492,6 +492,85 @@ curl -H "Authorization: Bearer $DW_ADMIN_TOKEN" http://localhost:8080/api/cluste підтримки або відстежуються окремо від контракту маніфестів. Для встановлення й оновлень під керуванням Helm використовуйте окремо версійований шлях вище. +## Запуск SDK workers у Kubernetes {#sdk-workers-on-kubernetes} + +Розгортайте workers вашого застосунку окремо від Laravel queue worker сервера. Зберіть образ із зареєстрованими обробниками PHP, Python або Rust і зафіксованими залежностями SDK. Використовуйте entrypoint у формі exec, щоб процес worker отримував `SIGTERM`. Замініть наведений образ незмінним digest цієї збірки. + +Для workers у тому самому кластері використовуйте фактичні назву, namespace і порт Service сервера. Сирі маніфести надають `http://durable-workflow-server.durable-workflow.svc.cluster.local:8080`. Випуск Helm може мати іншу назву Service. Workers поза кластером потребують доступної HTTPS-адреси ingress або балансувальника з нормальною перевіркою сертифіката. Узгодьте бюджети long poll і транспорту з [рекомендаціями щодо таймаутів](#worker-long-poll-timeout-budgets). + +Створіть Secret у Kubernetes namespace Pod worker, використовуючи наявні облікові дані worker. Не додавайте токен до маніфесту чи образу. Namespace сервера — окрема межа продукту, відмінна від namespace Kubernetes. + +```bash +kubectl -n durable-workflow create secret generic orders-worker-auth --from-literal=token="$DW_WORKER_TOKEN" +``` + +Збережіть це як `orders-worker.yaml`. Ці назви змінних середовища є конфігурацією застосунку: entrypoint має передати їх клієнту SDK і Worker. Надайте кожному одночасно активному процесу власний worker ID і зареєструйте обробники для налаштованої черги. + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: orders-worker + namespace: durable-workflow +spec: + replicas: 2 + selector: + matchLabels: + app: orders-worker + strategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 0 + maxSurge: 1 + template: + metadata: + labels: + app: orders-worker + spec: + automountServiceAccountToken: false + terminationGracePeriodSeconds: 75 + containers: + - name: worker + image: registry.example.com/acme/orders-worker:release-id + env: + - name: DW_SERVER_URL + value: http://durable-workflow-server.durable-workflow.svc.cluster.local:8080 + - name: DW_NAMESPACE + value: default + - name: DW_TASK_QUEUE + value: orders + - name: DW_WORKER_ID + valueFrom: + fieldRef: + fieldPath: metadata.uid + - name: DW_WORKER_TOKEN + valueFrom: + secretKeyRef: + name: orders-worker-auth + key: token + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: "1" + memory: 512Mi +``` + +Кількість реплік і ресурси — початкові приклади, а не гарантії місткості. Налаштуйте їх за фактичною тривалістю завдань, використанням пам’яті, лімітами залежних сервісів і [бюджетами допуску worker](./polyglot/task-queue-admission.md). Доступність Deployment не доводить реєстрацію SDK чи корисну роботу. Перевірте свіжі реєстрації workers та очікувані збірку й чергу, потім запустіть і завершіть показовий workflow. Запуск у Kubernetes сам собою не створює HTTP endpoint здоров’я для SDK worker. Використовуйте probe застосунку, якщо він існує, і не використовуйте здоров’я сервера як liveness-перевірку worker. + +### Передавання SIGTERM і очікування очищення {#sdk-worker-pod-shutdown} + +| SDK | Завершення процесу worker | +| --- | --- | +| PHP | Використовуйте керований цикл `Worker::run()` зі встановленим `pcntl`. Його signal handler запитує завершення, після чого активне синхронне завдання доходить до межі перед керованим очищенням і зняттям реєстрації. Власний цикл навколо `tick()` не надає цього lifecycle. Див. [коректне завершення PHP](https://php.durable-workflow.com/operate/graceful-shutdown/). | +| Python | Під’єднайте `SIGTERM` до шляху завершення того самого Worker та дочекайтеся і `Worker.stop()`, і його run task. Явно задайте `shutdown_timeout`. Прийняті завдання поділяють цей deadline спорожнення. Скасування await не може примусово зупинити активний синхронний потік, а непідтверджена зупинка залишається помилкою. Використовуйте [приклад окремих процесів і signal wiring](https://python.durable-workflow.com/guides/separate-workers/) та [контракт lifecycle](https://python.durable-workflow.com/reference/worker/). | +| Rust | Передайте до `Worker::run_until(...)` future, який завершується за Unix `SIGTERM`, і дочекайтеся результату. Future лише для Ctrl-C не обробляє signal завершення Pod. Асинхронні обробники мають віддавати керування, а блокувальну роботу потрібно обмежувати окремо. Див. [Worker::run_until](https://rust.durable-workflow.com/durable_workflow/struct.Worker.html#method.run_until). | + +Виділіть у `terminationGracePeriodSeconds` достатньо часу для poll, завершення прийнятих завдань і очищення HTTP/session/реєстрації у встановленому SDK. Приклад із 75 секундами потрібно звірити з фактичними межами вашого застосунку. Kubernetes зараховує будь-який hook `preStop` у той самий пільговий період і примусово зупиняє процеси, що залишилися після його спливу. Hook лише зі sleep не просить SDK спорожнити роботу й не подовжує його deadline. Див. [процес завершення Pod у Kubernetes](https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-termination-flow). + +Відрепетируйте завершення під час простою й обмеженої за часом роботи. Вважайте помилки очищення, збережену реєстрацію або примусове завершення неповним вимкненням. Worker на заміну продовжує durable-роботу за її записаними leases, retries і deadlines. Зупинка Pod не скасовує workflow і не відкочує зовнішній побічний ефект. Перед зміною збірок дотримуйтеся [розгортання за build ID worker](./polyglot/worker-build-id-rollout.md) і зберігайте його перевірки сумісності, replay та rollback. + ## Висока доступність і перемикання в одному регіоні {#single-region-high-availability-and-failover} Висока доступність і перемикання в одному регіоні наразі є **неперевіреним diff --git a/i18n/uk/source-hashes.json b/i18n/uk/source-hashes.json index 19f0a3f0330..117d7708dbc 100644 --- a/i18n/uk/source-hashes.json +++ b/i18n/uk/source-hashes.json @@ -27,7 +27,7 @@ "defining-workflows/workflow-id.md": "91b94211e2f8f405895ad82cfe2ef9baadb7a2cbd06e9c1e7aa6362231e0d424", "defining-workflows/workflow-status.md": "8e8cb27ff44c890d489d89bf166375acfdfaecabd990a2235e106fab1592d529", "defining-workflows/workflows.md": "f467bc73de32aa5d722c61ce085065460382ae7dd44cea4625847d9fcdc4c97c", - "deployment.md": "aa76a132fd78a27428dd9b8d9ab6f81aa8661b3f302eb799877688b77f508596", + "deployment.md": "7ad56fc682a495fef95c0de365953b904ec2a82653d6f2e109dc22f8659b8d5d", "failures-and-recovery.md": "318f0cc183d26539c7d5b6bdf791429bb874b793b81cff4c6094ef1d11a796b2", "features/activity-execution-model.md": "e8c37520ae46a7efb30438f4fc06bf893c1df430cb1f92c48963697adaf09082", "features/cancel-and-terminate.md": "c678fc7a0c9b11f0e1bee295aff259de9803219c0aa6769fb801622799e7deb1", diff --git a/i18n/zh-Hans/docusaurus-plugin-content-docs/current/deployment.md b/i18n/zh-Hans/docusaurus-plugin-content-docs/current/deployment.md index e6468f21ec6..63a7f31f3c0 100644 --- a/i18n/zh-Hans/docusaurus-plugin-content-docs/current/deployment.md +++ b/i18n/zh-Hans/docusaurus-plugin-content-docs/current/deployment.md @@ -339,6 +339,85 @@ curl -H "Authorization: Bearer $DW_ADMIN_TOKEN" http://localhost:8080/api/cluste 发布证据门槛关闭时,单区域高可用故障转移在原始清单路径上与小型集群路径一样,仍由支持团队指导。预期契约要求遵循下方[单区域高可用与故障转移](#single-region-high-availability-and-failover)中的就绪、单实例调度器和共享基础层规则。提供商特定负载均衡器、存储类、网络策略、双活多区域和自托管无人值守区域故障转移,仍由支持团队指导或在原始清单契约之外单独跟踪。需要 Helm 管理安装与升级时,使用上方独立版本化的 Helm 路径。 +## 在 Kubernetes 上运行 SDK Worker {#sdk-workers-on-kubernetes} + +将应用的 Worker 与 Server 的 Laravel 队列 Worker 分开部署。构建包含已注册 PHP、Python 或 Rust 处理函数及锁定 SDK 依赖的镜像。使用 exec 形式的 entrypoint,让 Worker 进程能够接收 `SIGTERM`。将下方镜像替换为该构建不可变的 digest。 + +对于同一集群内的 Worker,使用 Server Service 的实际名称、命名空间和端口。原始清单公开 `http://durable-workflow-server.durable-workflow.svc.cluster.local:8080`。Helm 发布可能使用不同的 Service 名称。集群外的 Worker 需要可访问的 ingress 或负载均衡器 HTTPS URL,并正常验证证书。按[超时指南](#worker-long-poll-timeout-budgets)协调长轮询和传输时间预算。 + +使用现有 Worker 凭据,在 Worker Pod 所属的 Kubernetes 命名空间中创建 Secret。不要把令牌写入清单或镜像。Server 命名空间是产品中的独立边界,与 Kubernetes 命名空间不同。 + +```bash +kubectl -n durable-workflow create secret generic orders-worker-auth --from-literal=token="$DW_WORKER_TOKEN" +``` + +将其保存为 `orders-worker.yaml`。这些环境变量名属于应用配置:entrypoint 必须将值传给 SDK 客户端和 Worker。为每个同时运行的进程分配独立 worker ID,并注册所配置队列的处理函数。 + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: orders-worker + namespace: durable-workflow +spec: + replicas: 2 + selector: + matchLabels: + app: orders-worker + strategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 0 + maxSurge: 1 + template: + metadata: + labels: + app: orders-worker + spec: + automountServiceAccountToken: false + terminationGracePeriodSeconds: 75 + containers: + - name: worker + image: registry.example.com/acme/orders-worker:release-id + env: + - name: DW_SERVER_URL + value: http://durable-workflow-server.durable-workflow.svc.cluster.local:8080 + - name: DW_NAMESPACE + value: default + - name: DW_TASK_QUEUE + value: orders + - name: DW_WORKER_ID + valueFrom: + fieldRef: + fieldPath: metadata.uid + - name: DW_WORKER_TOKEN + valueFrom: + secretKeyRef: + name: orders-worker-auth + key: token + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: "1" + memory: 512Mi +``` + +副本数和资源值只是起始示例,不保证容量。根据实际任务时长、内存使用、下游限制和 [Worker 准入预算](./polyglot/task-queue-admission.md)调整。Deployment 可用并不能证明 SDK 已注册或已完成有效工作。先验证新鲜的 Worker 注册、预期构建和队列,再启动并完成一个有代表性的工作流。仅在 Kubernetes 中运行不会自动为 SDK Worker 提供 HTTP 健康端点。如果应用有自己的探针,应使用该探针,不要把 Server 健康状态用作 Worker 存活检查。 + +### 传递 SIGTERM 并等待清理 {#sdk-worker-pod-shutdown} + +| SDK | Worker 进程关闭 | +| --- | --- | +| PHP | 安装 `pcntl` 并使用受管理的 `Worker::run()` 循环。信号处理函数请求关闭,活动同步任务到达边界后才执行受管理的清理和注销。围绕 `tick()` 的自定义循环不提供该生命周期。参见 [PHP 优雅关闭](https://php.durable-workflow.com/operate/graceful-shutdown/)。 | +| Python | 将 `SIGTERM` 接到同一 Worker 的关闭路径,同时 await `Worker.stop()` 及其运行任务。显式设置 `shutdown_timeout`。已接受工作共享该排空期限。取消 await 无法强制停止正在运行的同步线程,未确认停止仍是错误。使用[独立进程与信号连接示例](https://python.durable-workflow.com/guides/separate-workers/)和[生命周期契约](https://python.durable-workflow.com/reference/worker/)。 | +| Rust | 向 `Worker::run_until(...)` 传入一个在收到 Unix `SIGTERM` 时完成的 future,并 await 其结果。只等待 Ctrl-C 的 future 无法处理 Pod 终止信号。异步处理函数必须让出执行权,并单独限制阻塞工作。参见 [Worker::run_until](https://rust.durable-workflow.com/durable_workflow/struct.Worker.html#method.run_until)。 | + +在 `terminationGracePeriodSeconds` 中,为已安装 SDK 的轮询、已接受任务的关闭,以及 HTTP、会话和注销清理留出足够时间。必须根据应用的实际上限核对示例中的 75 秒。Kubernetes 将任何 `preStop` hook 计入同一宽限期,并在期限结束后强制停止剩余进程。仅执行 sleep 的 hook 不会要求 SDK 排空工作,也不会延长其期限。参见 [Kubernetes Pod 终止流程](https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-termination-flow)。 + +在空闲时和执行有时间上限的工作时演练终止。将清理错误、保留的注册或强制结束视为未完成关闭。替代 Worker 根据已记录的租约、重试和期限继续持久工作。停止 Pod 不会取消工作流,也不会撤销外部副作用。更换构建前,请遵循 [Worker build ID 发布流程](./polyglot/worker-build-id-rollout.md),保留其中的兼容性、重放和回滚检查。 + ## 单区域高可用与故障转移 {#single-region-high-availability-and-failover} 单区域高可用故障转移当前是在小型集群和原始 Kubernetes 形态之上叠加的**尚未验证、由支持团队指导**的候选契约。它面向引擎设计上能在**单一区域内**承受的故障模式:托管数据库故障转移(RDS Multi-AZ、Aurora 集群故障转移、Cloud SQL HA、Patroni 提升等)、托管 Redis 故障转移(Sentinel、Elasticache 复制组故障转移、Memorystore HA 等)、API 节点丢失、Worker 丢失,以及调度器/维护运行器重启。跨区域主备恢复是另一种由支持团队指导的评估,参见下方[主备多区域](#activepassive-multi-region)。 diff --git a/i18n/zh-Hans/source-hashes.json b/i18n/zh-Hans/source-hashes.json index 19f0a3f0330..117d7708dbc 100644 --- a/i18n/zh-Hans/source-hashes.json +++ b/i18n/zh-Hans/source-hashes.json @@ -27,7 +27,7 @@ "defining-workflows/workflow-id.md": "91b94211e2f8f405895ad82cfe2ef9baadb7a2cbd06e9c1e7aa6362231e0d424", "defining-workflows/workflow-status.md": "8e8cb27ff44c890d489d89bf166375acfdfaecabd990a2235e106fab1592d529", "defining-workflows/workflows.md": "f467bc73de32aa5d722c61ce085065460382ae7dd44cea4625847d9fcdc4c97c", - "deployment.md": "aa76a132fd78a27428dd9b8d9ab6f81aa8661b3f302eb799877688b77f508596", + "deployment.md": "7ad56fc682a495fef95c0de365953b904ec2a82653d6f2e109dc22f8659b8d5d", "failures-and-recovery.md": "318f0cc183d26539c7d5b6bdf791429bb874b793b81cff4c6094ef1d11a796b2", "features/activity-execution-model.md": "e8c37520ae46a7efb30438f4fc06bf893c1df430cb1f92c48963697adaf09082", "features/cancel-and-terminate.md": "c678fc7a0c9b11f0e1bee295aff259de9803219c0aa6769fb801622799e7deb1", diff --git a/tests/kubernetes-workers/README.md b/tests/kubernetes-workers/README.md new file mode 100644 index 00000000000..051183edac3 --- /dev/null +++ b/tests/kubernetes-workers/README.md @@ -0,0 +1,33 @@ +# Kubernetes SDK worker guide acceptance + +This fixture checks the worker Deployment in `docs/deployment.md`. It extracts +that YAML instead of maintaining another worker manifest. The PHP, Python and +Rust images install normal published SDK packages. There is no SDK or Server +source overlay. `artifacts.json` records this fixture's exact qualification +cohort; it does not update the site's selected compatibility tuple. + +The ordinary GitHub Actions workflow `kubernetes-workers.yml` builds the three +applications, verifies installed package identities, creates an isolated kind +cluster, and uses the immutable published Server image. It checks two distinct +Pod worker IDs per SDK, a completed workflow and remote activity per SDK, +idle Pod termination through SIGTERM, successful managed shutdown logs, and +removal of all six original registrations. Active-task interruption, forced +kill, recovery, provider networking, production database operations and HA +are separate controls and are not claimed by this fixture. + +The Server fixture uses ephemeral SQLite and a real scheduler. Its contents +are synthetic test data. The strings named `website336-worker` and +`website336-control` are test-only role credentials. The public guide keeps +actual credentials outside manifests and images. + +With Docker, kind, kubectl and Python 3.11 or newer available: + +```bash +QUALIFICATION_OUTPUT=/tmp/kubernetes-worker-evidence python3 tests/kubernetes-workers/qualify.py +``` + +Run in an isolated test runner with enough disk for a Rust application build +and kind. The script deletes only its uniquely named cluster in its cleanup +path. GitHub-hosted build resources expire with the bounded job. Evidence is +retained for three days in Actions; retain a sanitized outcome and exact +source/artifact links in the owning issue before claiming delivery. diff --git a/tests/kubernetes-workers/qualify.py b/tests/kubernetes-workers/qualify.py index bf3c984a400..5665c9c7591 100644 --- a/tests/kubernetes-workers/qualify.py +++ b/tests/kubernetes-workers/qualify.py @@ -6,6 +6,7 @@ import subprocess import tempfile import time +import tomllib import urllib.request ROOT = Path(__file__).resolve().parents[2] @@ -18,6 +19,8 @@ def run(args, *, label=None, timeout=180, stdin=None): + if label: + print(label + ': running', flush=True) result = subprocess.run(args, input=stdin, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=timeout, check=False) if label: @@ -100,9 +103,14 @@ def main(): php_sdk = next(item for item in php_lock['packages'] if item['name'] == 'durable-workflow/sdk') assert php_sdk['source']['reference'] == ARTIFACTS['php_source'] assert php_sdk['dist']['reference'] == ARTIFACTS['php_source'] + assert php_sdk['version'].lstrip('v') == ARTIFACTS['php_sdk'] rust_lock = run(['docker', 'run', '--rm', '--entrypoint', 'cat', images['rust'], '/app/Cargo.lock'], label='rust-lock') - assert ARTIFACTS['rust_checksum'] in rust_lock - run(['docker', 'run', '--rm', '--entrypoint', 'cat', images['python'], '/app/install-report.json'], label='python-install-report') + rust_sdk = next(item for item in tomllib.loads(rust_lock)['package'] if item['name'] == 'durable-workflow') + assert rust_sdk['version'] == ARTIFACTS['rust_sdk'] + assert rust_sdk['checksum'] == ARTIFACTS['rust_checksum'] + python_report = json.loads(run(['docker', 'run', '--rm', '--entrypoint', 'cat', images['python'], '/app/install-report.json'], label='python-install-report')) + python_sdk = next(item for item in python_report['install'] if item['metadata']['name'] == 'durable-workflow') + assert python_sdk['metadata']['version'] == ARTIFACTS['python_sdk'] created = False try: created = True From 75db95bedd8e2f7c773332fedb8145389f58b4f7 Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Fri, 23 Oct 2026 13:28:16 +0000 Subject: [PATCH 04/12] test: enforce HTTPS downloads and lock the published Rust worker graph --- .github/workflows/kubernetes-workers.yml | 8 +- tests/kubernetes-workers/rust/Cargo.lock | 1794 ++++++++++++++++++++++ tests/kubernetes-workers/rust/Cargo.toml | 1 + tests/kubernetes-workers/rust/Dockerfile | 4 +- 4 files changed, 1801 insertions(+), 6 deletions(-) create mode 100644 tests/kubernetes-workers/rust/Cargo.lock diff --git a/.github/workflows/kubernetes-workers.yml b/.github/workflows/kubernetes-workers.yml index 9f3bdbbef63..e328471064f 100644 --- a/.github/workflows/kubernetes-workers.yml +++ b/.github/workflows/kubernetes-workers.yml @@ -29,12 +29,12 @@ jobs: run: | install_dir="${RUNNER_TEMP}/worker-guide-tools" mkdir -p "$install_dir" - curl -fsSLo "$install_dir/kind" https://kind.sigs.k8s.io/dl/v0.31.0/kind-linux-amd64 - curl -fsSLo "$install_dir/kind.sha256" https://kind.sigs.k8s.io/dl/v0.31.0/kind-linux-amd64.sha256sum + curl --proto '=https' --proto-redir '=https' -fsSLo "$install_dir/kind" https://kind.sigs.k8s.io/dl/v0.31.0/kind-linux-amd64 + curl --proto '=https' --proto-redir '=https' -fsSLo "$install_dir/kind.sha256" https://kind.sigs.k8s.io/dl/v0.31.0/kind-linux-amd64.sha256sum expected="$(cut -d ' ' -f 1 "$install_dir/kind.sha256")" printf '%s %s\n' "$expected" "$install_dir/kind" | sha256sum --check - curl -fsSLo "$install_dir/kubectl" https://dl.k8s.io/release/v1.35.0/bin/linux/amd64/kubectl - curl -fsSLo "$install_dir/kubectl.sha256" https://dl.k8s.io/release/v1.35.0/bin/linux/amd64/kubectl.sha256 + curl --proto '=https' --proto-redir '=https' -fsSLo "$install_dir/kubectl" https://dl.k8s.io/release/v1.35.0/bin/linux/amd64/kubectl + curl --proto '=https' --proto-redir '=https' -fsSLo "$install_dir/kubectl.sha256" https://dl.k8s.io/release/v1.35.0/bin/linux/amd64/kubectl.sha256 expected="$(cat "$install_dir/kubectl.sha256")" printf '%s %s\n' "$expected" "$install_dir/kubectl" | sha256sum --check chmod +x "$install_dir/kind" "$install_dir/kubectl" diff --git a/tests/kubernetes-workers/rust/Cargo.lock b/tests/kubernetes-workers/rust/Cargo.lock new file mode 100644 index 00000000000..3a805f3b7ef --- /dev/null +++ b/tests/kubernetes-workers/rust/Cargo.lock @@ -0,0 +1,1794 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "apache-avro" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36fa98bc79671c7981272d91a8753a928ff6a1cd8e4f20a44c45bd5d313840bf" +dependencies = [ + "bigdecimal", + "bon", + "digest", + "log", + "miniz_oxide", + "num-bigint", + "quad-rand", + "rand 0.9.5", + "regex-lite", + "serde", + "serde_bytes", + "serde_json", + "strum", + "strum_macros", + "thiserror 2.0.21", + "uuid", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + +[[package]] +name = "bigdecimal" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fa3f3d8cbf4dffcfe4991de61d012bef509a409ecbe9dd41049bfe32b4d4653" +dependencies = [ + "autocfg", + "libm", + "num-bigint", + "num-integer", + "num-traits", + "serde", +] + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bon" +version = "3.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebeb9aaf9329dff6ceb65c689ca3db33dbf15f324909c60e4e5eef5701ce31b1" +dependencies = [ + "bon-macros", + "rustversion", +] + +[[package]] +name = "bon-macros" +version = "3.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77e9d642a7e3a318e37c2c9427b5a6a48aa1ad55dcd986f3034ab2239045a645" +dependencies = [ + "darling", + "ident_case", + "prettyplease", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.119", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cc" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86e055fa6e3e40ad6187ba25b6855eeb635d5271f8e0f0e1e9a5826b8d75bea0" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "rand_core 0.10.1", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "num-traits", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.119", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "durable-workflow" +version = "4.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6c0353e940a068f4a71eef8dc516d920c88be34f9c68e205f05ec10c34e65b" +dependencies = [ + "apache-avro", + "base64 0.22.1", + "bon", + "chrono", + "futures-util", + "icu_collections", + "icu_locale_core", + "icu_normalizer", + "icu_properties", + "icu_provider", + "reqwest", + "rustls-webpki", + "serde", + "serde-value", + "serde_json", + "sha2", + "thiserror 1.0.69", + "tokio", + "uuid", + "webpki-roots", + "yoke-derive", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-macro", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", + "wasm-bindgen", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c3e324da4c95177d6291d4c8730197c0d1822f8a9766814a4a44fa5ab797c9c" +dependencies = [ + "atomic-waker", + "bytes", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa8e654703247911e29c23fbeaa261834bd9bb74efba2f9acddc37bfb127f53" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-util" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff" +dependencies = [ + "base64 0.23.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "httparse", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "ipnet" +version = "2.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.191" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "lru-slab" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4050469837a6ff301cd14c1f8f24f88549e6d548f24f64e2148eb0f72cebc51f" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", + "serde", +] + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.119", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quad-rand" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a651516ddc9168ebd67b24afd085a718be02f8858fe406591b013d101ce2f40" + +[[package]] +name = "quinn" +version = "0.11.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4051e23e9185c255a7e33ef59cdbca87a22d359052eecd22fc6b901fb37d9d11" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.21", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e750cca55fe4f0439a15d0bb529da9651e79993e8e72c61a899a36d462befbe" +dependencies = [ + "bytes", + "getrandom 0.4.3", + "lru-slab", + "rand 0.10.3", + "rand_pcg", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.21", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af66907df18639dcf4db56ca65490cabc4b27a97dbadd96f2926cca73298f016" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha", + "rand_core 0.9.5", +] + +[[package]] +name = "rand" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", +] + +[[package]] +name = "regex-lite" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustls" +version = "0.23.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443b1b6d39d7c663139942cc4558262562d2c84123bb6282ef177009ca8b281" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float", + "serde", +] + +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "serde_json" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl 2.0.21", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" + +[[package]] +name = "tokio" +version = "1.54.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b52fd3255258d26fac7264ab5a76b53c13367136dc011d3d6ad4f8ed070396a3" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9cc2678c2cdd569ef8215e2afd7954ada2ae20b4fdd2c5fe6139a3b02d105db" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "want" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec4cdd0dd910afe868b7ef477227d8d538b46b3075031afee8a9f2acb0a2ed0b" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbab34de2d982e9b48e18d216d04c4a6f641066ff19ffb699980f591ee3610e" +dependencies = [ + "js-sys", + "tokio", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.9", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88261b9deccee56594c11a3460c462c41f58d148598fe70ad77070126a68aba4" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5f280bbb2b287bc8e7de7e3852945a0ff50d1bbcb5ed42b6c7a568c546df171" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "website-kubernetes-worker" +version = "0.1.0" +dependencies = [ + "durable-workflow", + "serde_json", + "tokio", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure 0.13.2", +] + +[[package]] +name = "zerocopy" +version = "0.8.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5fe1f8f1b06191a00962174c61aa5005e0bb391a6d80d07e24d115c01a92ed8" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863ad3ac83293fb4d740aedbfdc9240dd8d1a50c1099acd76ce80ce7c7230c7f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", + "synstructure 0.14.0", +] + +[[package]] +name = "zeroize" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879" + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3743ddbf47738d929cd1ae2e199d40d0162979dfe31aef8ae646d2bf00bf97a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/tests/kubernetes-workers/rust/Cargo.toml b/tests/kubernetes-workers/rust/Cargo.toml index 15682de658e..caa05b7df74 100644 --- a/tests/kubernetes-workers/rust/Cargo.toml +++ b/tests/kubernetes-workers/rust/Cargo.toml @@ -3,6 +3,7 @@ name = "website-kubernetes-worker" version = "0.1.0" edition = "2021" rust-version = "1.90" +resolver = "3" [dependencies] durable-workflow = "=4.7.0" diff --git a/tests/kubernetes-workers/rust/Dockerfile b/tests/kubernetes-workers/rust/Dockerfile index db68a2a4b3a..b2a16f6bbc6 100644 --- a/tests/kubernetes-workers/rust/Dockerfile +++ b/tests/kubernetes-workers/rust/Dockerfile @@ -1,8 +1,8 @@ FROM rust:1.90.0-slim-bookworm AS build WORKDIR /app -COPY Cargo.toml . +COPY Cargo.toml Cargo.lock ./ COPY src ./src -RUN cargo build --release -j 2 +RUN cargo build --release --locked -j 2 FROM debian:bookworm-slim RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates \ From 7e760debb078dca21eade6961e106107cdf44bcd Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Fri, 23 Oct 2026 13:39:22 +0000 Subject: [PATCH 05/12] test: expose Kubernetes worker diagnostics on acceptance failure --- tests/kubernetes-workers/python/verify.py | 17 +++++++++++------ tests/kubernetes-workers/qualify.py | 12 ++++++++---- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/tests/kubernetes-workers/python/verify.py b/tests/kubernetes-workers/python/verify.py index b2a8b121391..19c54f93da5 100644 --- a/tests/kubernetes-workers/python/verify.py +++ b/tests/kubernetes-workers/python/verify.py @@ -28,12 +28,17 @@ async def main(): receipts = [] worker_ids = [] for sdk in ["php", "python", "rust"]: - async with asyncio.timeout(40): - while True: - roster = await client.list_workers(task_queue="orders-" + sdk) - if len(roster.workers) == 2: - break - await asyncio.sleep(0.2) + observed = [] + try: + async with asyncio.timeout(40): + while True: + roster = await client.list_workers(task_queue="orders-" + sdk) + observed = [worker.worker_id for worker in roster.workers] + if len(observed) == 2: + break + await asyncio.sleep(0.2) + except TimeoutError as error: + raise AssertionError(f'{sdk}: expected two registered Pod workers, observed {observed}') from error ids = [worker.worker_id for worker in roster.workers] assert len(set(ids)) == 2, ids worker_ids.extend(ids) diff --git a/tests/kubernetes-workers/qualify.py b/tests/kubernetes-workers/qualify.py index 5665c9c7591..1a8e61b4f02 100644 --- a/tests/kubernetes-workers/qualify.py +++ b/tests/kubernetes-workers/qualify.py @@ -73,7 +73,7 @@ def server(): 'ports': [{'port': 8080, 'targetPort': 8080}]}}) -def collect(): +def collect(*, show_logs=False): for kind in ['pods', 'deployments', 'events']: try: kubectl('get', kind, '-o', 'json', label=kind) @@ -84,8 +84,10 @@ def collect(): for pod in pods: for container in pod['spec'].get('initContainers', []) + pod['spec']['containers']: try: - kubectl('logs', pod['metadata']['name'], '-c', container['name'], - label=pod['metadata']['name'] + '-' + container['name']) + logs = kubectl('logs', pod['metadata']['name'], '-c', container['name'], + label=pod['metadata']['name'] + '-' + container['name']) + if show_logs: + print('Pod ' + pod['metadata']['name'] + ' / ' + container['name'] + ':\n' + logs[-3000:], flush=True) except RuntimeError: pass except RuntimeError: @@ -112,6 +114,7 @@ def main(): python_sdk = next(item for item in python_report['install'] if item['metadata']['name'] == 'durable-workflow') assert python_sdk['metadata']['version'] == ARTIFACTS['python_sdk'] created = False + complete = False try: created = True run(['kind', 'create', 'cluster', '--name', CLUSTER, '--image', ARTIFACTS['kind_node'], @@ -169,9 +172,10 @@ def main(): receipt['documented_manifest_sha256'] = hashlib.sha256(manifest.encode()).hexdigest() (OUTPUT / 'receipt.json').write_text(json.dumps(receipt, indent=2) + '\n') print(json.dumps(receipt)) + complete = True finally: if created: - collect() + collect(show_logs=not complete) run(['kind', 'delete', 'cluster', '--name', CLUSTER], label='kind-delete', timeout=120) From 117391a581043400a6e7de213ce3c61bb7462dbb Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Fri, 23 Oct 2026 13:42:57 +0000 Subject: [PATCH 06/12] test: register the required PHP activity context and bounded transport --- tests/kubernetes-workers/php/Dockerfile | 4 ++-- tests/kubernetes-workers/php/worker.php | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/kubernetes-workers/php/Dockerfile b/tests/kubernetes-workers/php/Dockerfile index 95f4a1b7b71..df52f1a99e3 100644 --- a/tests/kubernetes-workers/php/Dockerfile +++ b/tests/kubernetes-workers/php/Dockerfile @@ -1,6 +1,6 @@ FROM php:8.3.35-cli-bookworm -RUN apt-get update && apt-get install -y --no-install-recommends unzip \ - && docker-php-ext-install pcntl \ +RUN apt-get update && apt-get install -y --no-install-recommends unzip libcurl4-openssl-dev \ + && docker-php-ext-install pcntl curl \ && rm -r /var/lib/apt/lists/* COPY --from=composer:2.9 /usr/bin/composer /usr/bin/composer WORKDIR /app diff --git a/tests/kubernetes-workers/php/worker.php b/tests/kubernetes-workers/php/worker.php index e8a3a336daf..57cd028a573 100644 --- a/tests/kubernetes-workers/php/worker.php +++ b/tests/kubernetes-workers/php/worker.php @@ -6,6 +6,7 @@ use DurableWorkflow\Client; use DurableWorkflow\Worker; +use DurableWorkflow\Worker\ActivityContext; use DurableWorkflow\Worker\WorkflowContext; $client = new Client( @@ -18,7 +19,7 @@ $worker->registerWorkflow('website336.echo', function (WorkflowContext $context, string $name): mixed { return $context->activity('website336.greet', [$name]); }); -$worker->registerActivity('website336.greet', function (string $name): array { +$worker->registerActivity('website336.greet', function (ActivityContext $context, string $name): array { return ['sdk' => 'php', 'name' => $name, 'worker_id' => getenv('DW_WORKER_ID')]; }); $worker->run(pollTimeoutSeconds: 1); From e4d816fc5e47808b3cb08b3a6d18c1f16941eef4 Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Fri, 23 Oct 2026 13:45:38 +0000 Subject: [PATCH 07/12] test: retain Server exceptions during worker acceptance --- tests/kubernetes-workers/qualify.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/kubernetes-workers/qualify.py b/tests/kubernetes-workers/qualify.py index 1a8e61b4f02..79011deb4d1 100644 --- a/tests/kubernetes-workers/qualify.py +++ b/tests/kubernetes-workers/qualify.py @@ -41,6 +41,7 @@ def apply(document): def server(): environment = { 'APP_ENV': 'testing', 'APP_DEBUG': 'false', 'APP_VERSION': ARTIFACTS['server_version'], + 'LOG_CHANNEL': 'stderr', 'LOG_LEVEL': 'warning', 'DB_CONNECTION': 'sqlite', 'DB_DATABASE': '/data/database.sqlite', 'QUEUE_CONNECTION': 'sync', 'CACHE_STORE': 'array', 'SESSION_DRIVER': 'array', 'DW_AUTH_DRIVER': 'token', 'DW_AUTH_BACKWARD_COMPATIBLE': 'false', From 6829a43b14b9e07c6b5ba995f42fc3cfd3a9ed0b Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Fri, 23 Oct 2026 13:46:05 +0000 Subject: [PATCH 08/12] test: preserve exception messages before truncated Kubernetes stacks --- tests/kubernetes-workers/qualify.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/kubernetes-workers/qualify.py b/tests/kubernetes-workers/qualify.py index 79011deb4d1..3e43e179da6 100644 --- a/tests/kubernetes-workers/qualify.py +++ b/tests/kubernetes-workers/qualify.py @@ -88,7 +88,10 @@ def collect(*, show_logs=False): logs = kubectl('logs', pod['metadata']['name'], '-c', container['name'], label=pod['metadata']['name'] + '-' + container['name']) if show_logs: - print('Pod ' + pod['metadata']['name'] + ' / ' + container['name'] + ':\n' + logs[-3000:], flush=True) + errors = [line[:3000] for line in logs.splitlines() + if any(token in line for token in ['ERROR:', 'CRITICAL:', 'SQLSTATE', 'Fatal error', 'Error:'])] + print('Pod ' + pod['metadata']['name'] + ' / ' + container['name'] + ':\n' + + '\n'.join(errors[:4]) + '\n' + logs[-3000:], flush=True) except RuntimeError: pass except RuntimeError: From 1bba95a7ce668c0f27128d115238becbf72a4cfd Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Fri, 23 Oct 2026 13:47:08 +0000 Subject: [PATCH 09/12] test: preserve published Server database ownership on the fixture volume --- tests/kubernetes-workers/qualify.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/kubernetes-workers/qualify.py b/tests/kubernetes-workers/qualify.py index 3e43e179da6..086539e5d40 100644 --- a/tests/kubernetes-workers/qualify.py +++ b/tests/kubernetes-workers/qualify.py @@ -65,7 +65,10 @@ def server(): 'spec': {'replicas': 1, 'selector': {'matchLabels': {'app': 'fixture-server'}}, 'template': {'metadata': {'labels': {'app': 'fixture-server'}}, 'spec': {'initContainers': [{**common, 'name': 'bootstrap', - 'command': ['server-bootstrap']}], + # EmptyDir masks the image's pre-owned database directory. + # Apache needs both the file and journal directory writable. + 'command': ['sh', '-ec', 'server-bootstrap && ' + 'chown -R www-data:www-data /data && chmod -R ug+rwX /data']}], 'containers': [api, scheduler], 'volumes': [{'name': 'data', 'emptyDir': {}}]}}}}) apply({'apiVersion': 'v1', 'kind': 'Service', From 51c2226f8b5a7bdf9e0caf2c84b000b1f4708529 Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Fri, 23 Oct 2026 13:57:45 +0000 Subject: [PATCH 10/12] test: use the published Server shared polling cache baseline --- tests/kubernetes-workers/qualify.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/kubernetes-workers/qualify.py b/tests/kubernetes-workers/qualify.py index 086539e5d40..bece9c41b11 100644 --- a/tests/kubernetes-workers/qualify.py +++ b/tests/kubernetes-workers/qualify.py @@ -43,7 +43,9 @@ def server(): 'APP_ENV': 'testing', 'APP_DEBUG': 'false', 'APP_VERSION': ARTIFACTS['server_version'], 'LOG_CHANNEL': 'stderr', 'LOG_LEVEL': 'warning', 'DB_CONNECTION': 'sqlite', 'DB_DATABASE': '/data/database.sqlite', - 'QUEUE_CONNECTION': 'sync', 'CACHE_STORE': 'array', 'SESSION_DRIVER': 'array', + # Match the image's shared file cache. A process-local array cache cannot + # coordinate long-poll wait slots across Apache workers. + 'QUEUE_CONNECTION': 'sync', 'CACHE_STORE': 'file', 'SESSION_DRIVER': 'array', 'DW_AUTH_DRIVER': 'token', 'DW_AUTH_BACKWARD_COMPATIBLE': 'false', 'DW_ADMIN_TOKEN': 'website336-control', 'DW_WORKER_TOKEN': 'website336-worker', 'DW_TASK_DISPATCH_MODE': 'poll', From b64027626c129797c36b19f11688cd756aa64c81 Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Fri, 23 Oct 2026 13:59:38 +0000 Subject: [PATCH 11/12] docs: include worker apply and rollout commands in all eight locales --- docs/deployment.md | 7 +++++++ .../docusaurus-plugin-content-docs/current/deployment.md | 7 +++++++ i18n/de/source-hashes.json | 2 +- .../docusaurus-plugin-content-docs/current/deployment.md | 7 +++++++ i18n/es/source-hashes.json | 2 +- .../docusaurus-plugin-content-docs/current/deployment.md | 7 +++++++ i18n/fr/source-hashes.json | 2 +- .../docusaurus-plugin-content-docs/current/deployment.md | 7 +++++++ i18n/ja/source-hashes.json | 2 +- .../docusaurus-plugin-content-docs/current/deployment.md | 7 +++++++ i18n/pt-BR/source-hashes.json | 2 +- .../docusaurus-plugin-content-docs/current/deployment.md | 7 +++++++ i18n/uk/source-hashes.json | 2 +- .../docusaurus-plugin-content-docs/current/deployment.md | 7 +++++++ i18n/zh-Hans/source-hashes.json | 2 +- 15 files changed, 63 insertions(+), 7 deletions(-) diff --git a/docs/deployment.md b/docs/deployment.md index b69b33c5321..961a5597033 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -570,6 +570,13 @@ spec: memory: 512Mi ``` +Apply the Deployment and wait for its rollout: + +```bash +kubectl apply -f orders-worker.yaml +kubectl -n durable-workflow rollout status deployment/orders-worker +``` + Replica and resource values are starting examples, not capacity guarantees. Tune them with actual task duration, memory use, downstream limits and [worker admission budgets](./polyglot/task-queue-admission.md). A Deployment becoming available does not prove SDK registration or useful work. Verify fresh worker registrations and the expected build/queue, then start and complete a representative workflow. An SDK worker does not acquire an HTTP health endpoint merely by running in Kubernetes. Use an application-owned probe when you have one, and do not use Server health as a worker liveness check. ### Delivering SIGTERM and waiting for cleanup {#sdk-worker-pod-shutdown} diff --git a/i18n/de/docusaurus-plugin-content-docs/current/deployment.md b/i18n/de/docusaurus-plugin-content-docs/current/deployment.md index ac0268693f0..6290ecee1e3 100644 --- a/i18n/de/docusaurus-plugin-content-docs/current/deployment.md +++ b/i18n/de/docusaurus-plugin-content-docs/current/deployment.md @@ -438,6 +438,13 @@ spec: memory: 512Mi ``` +Wenden Sie das Deployment an und warten Sie auf den Rollout: + +```bash +kubectl apply -f orders-worker.yaml +kubectl -n durable-workflow rollout status deployment/orders-worker +``` + Replikazahlen und Ressourcen sind Ausgangsbeispiele, keine Kapazitätsgarantien. Passen Sie sie an tatsächliche Task-Dauer, Speicherbedarf, Grenzen nachgelagerter Dienste und die [Worker-Zulassungsbudgets](./polyglot/task-queue-admission.md) an. Ein verfügbares Deployment belegt weder die SDK-Registrierung noch erfolgreiche Arbeit. Prüfen Sie aktuelle Worker-Registrierungen sowie erwarteten Build und Queue und starten und beenden Sie anschließend einen repräsentativen Workflow. Ein SDK-Worker erhält durch Kubernetes nicht automatisch einen HTTP-Health-Endpunkt. Verwenden Sie eine anwendungseigene Probe, falls vorhanden, und nicht die Server-Gesundheit als Liveness-Prüfung des Workers. ### SIGTERM zustellen und die Bereinigung abwarten {#sdk-worker-pod-shutdown} diff --git a/i18n/de/source-hashes.json b/i18n/de/source-hashes.json index 434cd147761..f96f8dd692a 100644 --- a/i18n/de/source-hashes.json +++ b/i18n/de/source-hashes.json @@ -27,7 +27,7 @@ "defining-workflows/workflow-id.md": "91b94211e2f8f405895ad82cfe2ef9baadb7a2cbd06e9c1e7aa6362231e0d424", "defining-workflows/workflow-status.md": "8e8cb27ff44c890d489d89bf166375acfdfaecabd990a2235e106fab1592d529", "defining-workflows/workflows.md": "f467bc73de32aa5d722c61ce085065460382ae7dd44cea4625847d9fcdc4c97c", - "deployment.md": "7ad56fc682a495fef95c0de365953b904ec2a82653d6f2e109dc22f8659b8d5d", + "deployment.md": "dd0b6bd184eda12e67922121a09c7f3c98346bde01be1d4c3217f168db5fd7ca", "failures-and-recovery.md": "318f0cc183d26539c7d5b6bdf791429bb874b793b81cff4c6094ef1d11a796b2", "features/activity-execution-model.md": "e8c37520ae46a7efb30438f4fc06bf893c1df430cb1f92c48963697adaf09082", "features/cancel-and-terminate.md": "c678fc7a0c9b11f0e1bee295aff259de9803219c0aa6769fb801622799e7deb1", diff --git a/i18n/es/docusaurus-plugin-content-docs/current/deployment.md b/i18n/es/docusaurus-plugin-content-docs/current/deployment.md index e7f2ee38eaa..6a959c6a55b 100644 --- a/i18n/es/docusaurus-plugin-content-docs/current/deployment.md +++ b/i18n/es/docusaurus-plugin-content-docs/current/deployment.md @@ -543,6 +543,13 @@ spec: memory: 512Mi ``` +Aplica el Deployment y espera a que termine el despliegue: + +```bash +kubectl apply -f orders-worker.yaml +kubectl -n durable-workflow rollout status deployment/orders-worker +``` + Las réplicas y los recursos son ejemplos iniciales, no garantías de capacidad. Ajústalos según la duración real de las tareas, la memoria, los límites de servicios dependientes y los [presupuestos de admisión del worker](./polyglot/task-queue-admission.md). Un Deployment disponible no demuestra que el SDK se haya registrado ni que procese trabajo útil. Comprueba registros recientes y la compilación y cola esperadas; después inicia y completa un workflow representativo. Ejecutarse en Kubernetes no proporciona automáticamente un endpoint HTTP de salud al worker del SDK. Usa una sonda de la aplicación cuando exista y no uses la salud del Server como sonda de actividad del worker. ### Entregar SIGTERM y esperar la limpieza {#sdk-worker-pod-shutdown} diff --git a/i18n/es/source-hashes.json b/i18n/es/source-hashes.json index 434cd147761..f96f8dd692a 100644 --- a/i18n/es/source-hashes.json +++ b/i18n/es/source-hashes.json @@ -27,7 +27,7 @@ "defining-workflows/workflow-id.md": "91b94211e2f8f405895ad82cfe2ef9baadb7a2cbd06e9c1e7aa6362231e0d424", "defining-workflows/workflow-status.md": "8e8cb27ff44c890d489d89bf166375acfdfaecabd990a2235e106fab1592d529", "defining-workflows/workflows.md": "f467bc73de32aa5d722c61ce085065460382ae7dd44cea4625847d9fcdc4c97c", - "deployment.md": "7ad56fc682a495fef95c0de365953b904ec2a82653d6f2e109dc22f8659b8d5d", + "deployment.md": "dd0b6bd184eda12e67922121a09c7f3c98346bde01be1d4c3217f168db5fd7ca", "failures-and-recovery.md": "318f0cc183d26539c7d5b6bdf791429bb874b793b81cff4c6094ef1d11a796b2", "features/activity-execution-model.md": "e8c37520ae46a7efb30438f4fc06bf893c1df430cb1f92c48963697adaf09082", "features/cancel-and-terminate.md": "c678fc7a0c9b11f0e1bee295aff259de9803219c0aa6769fb801622799e7deb1", diff --git a/i18n/fr/docusaurus-plugin-content-docs/current/deployment.md b/i18n/fr/docusaurus-plugin-content-docs/current/deployment.md index c1304442aa4..3b83077fff7 100644 --- a/i18n/fr/docusaurus-plugin-content-docs/current/deployment.md +++ b/i18n/fr/docusaurus-plugin-content-docs/current/deployment.md @@ -556,6 +556,13 @@ spec: memory: 512Mi ``` +Appliquez le Deployment et attendez son déploiement : + +```bash +kubectl apply -f orders-worker.yaml +kubectl -n durable-workflow rollout status deployment/orders-worker +``` + Les nombres de réplicas et les ressources sont des exemples de départ, pas des garanties de capacité. Ajustez-les selon la durée réelle des tâches, la mémoire, les limites des services en aval et les [budgets d’admission des workers](./polyglot/task-queue-admission.md). La disponibilité d’un Deployment ne prouve ni l’enregistrement du SDK ni un travail utile. Vérifiez des enregistrements récents ainsi que la build et la file attendues, puis démarrez et terminez un workflow représentatif. Exécuter un worker SDK sur Kubernetes ne lui fournit pas automatiquement un endpoint HTTP de santé. Utilisez une sonde propre à l’application si elle existe et non la santé du Server comme sonde de vie du worker. ### Transmettre SIGTERM et attendre le nettoyage {#sdk-worker-pod-shutdown} diff --git a/i18n/fr/source-hashes.json b/i18n/fr/source-hashes.json index 434cd147761..f96f8dd692a 100644 --- a/i18n/fr/source-hashes.json +++ b/i18n/fr/source-hashes.json @@ -27,7 +27,7 @@ "defining-workflows/workflow-id.md": "91b94211e2f8f405895ad82cfe2ef9baadb7a2cbd06e9c1e7aa6362231e0d424", "defining-workflows/workflow-status.md": "8e8cb27ff44c890d489d89bf166375acfdfaecabd990a2235e106fab1592d529", "defining-workflows/workflows.md": "f467bc73de32aa5d722c61ce085065460382ae7dd44cea4625847d9fcdc4c97c", - "deployment.md": "7ad56fc682a495fef95c0de365953b904ec2a82653d6f2e109dc22f8659b8d5d", + "deployment.md": "dd0b6bd184eda12e67922121a09c7f3c98346bde01be1d4c3217f168db5fd7ca", "failures-and-recovery.md": "318f0cc183d26539c7d5b6bdf791429bb874b793b81cff4c6094ef1d11a796b2", "features/activity-execution-model.md": "e8c37520ae46a7efb30438f4fc06bf893c1df430cb1f92c48963697adaf09082", "features/cancel-and-terminate.md": "c678fc7a0c9b11f0e1bee295aff259de9803219c0aa6769fb801622799e7deb1", diff --git a/i18n/ja/docusaurus-plugin-content-docs/current/deployment.md b/i18n/ja/docusaurus-plugin-content-docs/current/deployment.md index e705139c9d3..015aa5393eb 100644 --- a/i18n/ja/docusaurus-plugin-content-docs/current/deployment.md +++ b/i18n/ja/docusaurus-plugin-content-docs/current/deployment.md @@ -416,6 +416,13 @@ spec: memory: 512Mi ``` +Deployment を適用し、ロールアウトの完了を待ちます。 + +```bash +kubectl apply -f orders-worker.yaml +kubectl -n durable-workflow rollout status deployment/orders-worker +``` + レプリカ数とリソース値は出発点となる例であり、処理能力の保証ではありません。実際のタスク時間、メモリー使用量、下流サービスの制限、[Worker の受け入れ予算](./polyglot/task-queue-admission.md)に合わせて調整します。Deployment が利用可能になっても、SDK の登録や有用な処理の証明にはなりません。新鮮な Worker 登録と想定するビルド、キューを確認し、代表的なワークフローを開始して完了させてください。Kubernetes で実行するだけでは、SDK Worker に HTTP ヘルスエンドポイントは追加されません。アプリケーションに probe がある場合はそれを使い、Server のヘルスを Worker の liveness 検査に使わないでください。 ### SIGTERM を届けて後処理を待つ {#sdk-worker-pod-shutdown} diff --git a/i18n/ja/source-hashes.json b/i18n/ja/source-hashes.json index 434cd147761..f96f8dd692a 100644 --- a/i18n/ja/source-hashes.json +++ b/i18n/ja/source-hashes.json @@ -27,7 +27,7 @@ "defining-workflows/workflow-id.md": "91b94211e2f8f405895ad82cfe2ef9baadb7a2cbd06e9c1e7aa6362231e0d424", "defining-workflows/workflow-status.md": "8e8cb27ff44c890d489d89bf166375acfdfaecabd990a2235e106fab1592d529", "defining-workflows/workflows.md": "f467bc73de32aa5d722c61ce085065460382ae7dd44cea4625847d9fcdc4c97c", - "deployment.md": "7ad56fc682a495fef95c0de365953b904ec2a82653d6f2e109dc22f8659b8d5d", + "deployment.md": "dd0b6bd184eda12e67922121a09c7f3c98346bde01be1d4c3217f168db5fd7ca", "failures-and-recovery.md": "318f0cc183d26539c7d5b6bdf791429bb874b793b81cff4c6094ef1d11a796b2", "features/activity-execution-model.md": "e8c37520ae46a7efb30438f4fc06bf893c1df430cb1f92c48963697adaf09082", "features/cancel-and-terminate.md": "c678fc7a0c9b11f0e1bee295aff259de9803219c0aa6769fb801622799e7deb1", diff --git a/i18n/pt-BR/docusaurus-plugin-content-docs/current/deployment.md b/i18n/pt-BR/docusaurus-plugin-content-docs/current/deployment.md index efa1fea6ed7..1dc44f9a84a 100644 --- a/i18n/pt-BR/docusaurus-plugin-content-docs/current/deployment.md +++ b/i18n/pt-BR/docusaurus-plugin-content-docs/current/deployment.md @@ -570,6 +570,13 @@ spec: memory: 512Mi ``` +Aplique o Deployment e aguarde o rollout: + +```bash +kubectl apply -f orders-worker.yaml +kubectl -n durable-workflow rollout status deployment/orders-worker +``` + Réplicas e recursos são exemplos iniciais, não garantias de capacidade. Ajuste-os conforme a duração real das tarefas, o uso de memória, os limites dos serviços dependentes e os [orçamentos de admissão do worker](./polyglot/task-queue-admission.md). Um Deployment disponível não comprova o registro do SDK nem trabalho útil. Verifique registros recentes e a build e fila esperadas; depois inicie e conclua um workflow representativo. Executar no Kubernetes não fornece automaticamente um endpoint HTTP de saúde ao worker do SDK. Use uma probe da aplicação quando houver uma e não use a saúde do Server como liveness do worker. ### Entregar SIGTERM e aguardar a limpeza {#sdk-worker-pod-shutdown} diff --git a/i18n/pt-BR/source-hashes.json b/i18n/pt-BR/source-hashes.json index 434cd147761..f96f8dd692a 100644 --- a/i18n/pt-BR/source-hashes.json +++ b/i18n/pt-BR/source-hashes.json @@ -27,7 +27,7 @@ "defining-workflows/workflow-id.md": "91b94211e2f8f405895ad82cfe2ef9baadb7a2cbd06e9c1e7aa6362231e0d424", "defining-workflows/workflow-status.md": "8e8cb27ff44c890d489d89bf166375acfdfaecabd990a2235e106fab1592d529", "defining-workflows/workflows.md": "f467bc73de32aa5d722c61ce085065460382ae7dd44cea4625847d9fcdc4c97c", - "deployment.md": "7ad56fc682a495fef95c0de365953b904ec2a82653d6f2e109dc22f8659b8d5d", + "deployment.md": "dd0b6bd184eda12e67922121a09c7f3c98346bde01be1d4c3217f168db5fd7ca", "failures-and-recovery.md": "318f0cc183d26539c7d5b6bdf791429bb874b793b81cff4c6094ef1d11a796b2", "features/activity-execution-model.md": "e8c37520ae46a7efb30438f4fc06bf893c1df430cb1f92c48963697adaf09082", "features/cancel-and-terminate.md": "c678fc7a0c9b11f0e1bee295aff259de9803219c0aa6769fb801622799e7deb1", diff --git a/i18n/uk/docusaurus-plugin-content-docs/current/deployment.md b/i18n/uk/docusaurus-plugin-content-docs/current/deployment.md index a5fb4f75584..080fcba5a98 100644 --- a/i18n/uk/docusaurus-plugin-content-docs/current/deployment.md +++ b/i18n/uk/docusaurus-plugin-content-docs/current/deployment.md @@ -557,6 +557,13 @@ spec: memory: 512Mi ``` +Застосуйте Deployment і перевірте його розгортання: + +```bash +kubectl apply -f orders-worker.yaml +kubectl -n durable-workflow rollout status deployment/orders-worker +``` + Кількість реплік і ресурси — початкові приклади, а не гарантії місткості. Налаштуйте їх за фактичною тривалістю завдань, використанням пам’яті, лімітами залежних сервісів і [бюджетами допуску worker](./polyglot/task-queue-admission.md). Доступність Deployment не доводить реєстрацію SDK чи корисну роботу. Перевірте свіжі реєстрації workers та очікувані збірку й чергу, потім запустіть і завершіть показовий workflow. Запуск у Kubernetes сам собою не створює HTTP endpoint здоров’я для SDK worker. Використовуйте probe застосунку, якщо він існує, і не використовуйте здоров’я сервера як liveness-перевірку worker. ### Передавання SIGTERM і очікування очищення {#sdk-worker-pod-shutdown} diff --git a/i18n/uk/source-hashes.json b/i18n/uk/source-hashes.json index 434cd147761..f96f8dd692a 100644 --- a/i18n/uk/source-hashes.json +++ b/i18n/uk/source-hashes.json @@ -27,7 +27,7 @@ "defining-workflows/workflow-id.md": "91b94211e2f8f405895ad82cfe2ef9baadb7a2cbd06e9c1e7aa6362231e0d424", "defining-workflows/workflow-status.md": "8e8cb27ff44c890d489d89bf166375acfdfaecabd990a2235e106fab1592d529", "defining-workflows/workflows.md": "f467bc73de32aa5d722c61ce085065460382ae7dd44cea4625847d9fcdc4c97c", - "deployment.md": "7ad56fc682a495fef95c0de365953b904ec2a82653d6f2e109dc22f8659b8d5d", + "deployment.md": "dd0b6bd184eda12e67922121a09c7f3c98346bde01be1d4c3217f168db5fd7ca", "failures-and-recovery.md": "318f0cc183d26539c7d5b6bdf791429bb874b793b81cff4c6094ef1d11a796b2", "features/activity-execution-model.md": "e8c37520ae46a7efb30438f4fc06bf893c1df430cb1f92c48963697adaf09082", "features/cancel-and-terminate.md": "c678fc7a0c9b11f0e1bee295aff259de9803219c0aa6769fb801622799e7deb1", diff --git a/i18n/zh-Hans/docusaurus-plugin-content-docs/current/deployment.md b/i18n/zh-Hans/docusaurus-plugin-content-docs/current/deployment.md index 63a7f31f3c0..ba932e25875 100644 --- a/i18n/zh-Hans/docusaurus-plugin-content-docs/current/deployment.md +++ b/i18n/zh-Hans/docusaurus-plugin-content-docs/current/deployment.md @@ -404,6 +404,13 @@ spec: memory: 512Mi ``` +应用 Deployment 并等待发布完成: + +```bash +kubectl apply -f orders-worker.yaml +kubectl -n durable-workflow rollout status deployment/orders-worker +``` + 副本数和资源值只是起始示例,不保证容量。根据实际任务时长、内存使用、下游限制和 [Worker 准入预算](./polyglot/task-queue-admission.md)调整。Deployment 可用并不能证明 SDK 已注册或已完成有效工作。先验证新鲜的 Worker 注册、预期构建和队列,再启动并完成一个有代表性的工作流。仅在 Kubernetes 中运行不会自动为 SDK Worker 提供 HTTP 健康端点。如果应用有自己的探针,应使用该探针,不要把 Server 健康状态用作 Worker 存活检查。 ### 传递 SIGTERM 并等待清理 {#sdk-worker-pod-shutdown} diff --git a/i18n/zh-Hans/source-hashes.json b/i18n/zh-Hans/source-hashes.json index 434cd147761..f96f8dd692a 100644 --- a/i18n/zh-Hans/source-hashes.json +++ b/i18n/zh-Hans/source-hashes.json @@ -27,7 +27,7 @@ "defining-workflows/workflow-id.md": "91b94211e2f8f405895ad82cfe2ef9baadb7a2cbd06e9c1e7aa6362231e0d424", "defining-workflows/workflow-status.md": "8e8cb27ff44c890d489d89bf166375acfdfaecabd990a2235e106fab1592d529", "defining-workflows/workflows.md": "f467bc73de32aa5d722c61ce085065460382ae7dd44cea4625847d9fcdc4c97c", - "deployment.md": "7ad56fc682a495fef95c0de365953b904ec2a82653d6f2e109dc22f8659b8d5d", + "deployment.md": "dd0b6bd184eda12e67922121a09c7f3c98346bde01be1d4c3217f168db5fd7ca", "failures-and-recovery.md": "318f0cc183d26539c7d5b6bdf791429bb874b793b81cff4c6094ef1d11a796b2", "features/activity-execution-model.md": "e8c37520ae46a7efb30438f4fc06bf893c1df430cb1f92c48963697adaf09082", "features/cancel-and-terminate.md": "c678fc7a0c9b11f0e1bee295aff259de9803219c0aa6769fb801622799e7deb1", From 4faac223522656d9c6eb69a641214e41ba26ae8c Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Fri, 23 Oct 2026 14:19:08 +0000 Subject: [PATCH 12/12] test: retain actual durable queue state when Kubernetes acceptance fails --- tests/kubernetes-workers/qualify.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/kubernetes-workers/qualify.py b/tests/kubernetes-workers/qualify.py index bece9c41b11..8d956273b28 100644 --- a/tests/kubernetes-workers/qualify.py +++ b/tests/kubernetes-workers/qualify.py @@ -101,6 +101,33 @@ def collect(*, show_logs=False): pass except RuntimeError: pass + try: + # Read only synthetic fixture state. Keep queue/status identity, not + # credentials, serialized callbacks or complete payload bodies. + audit = r'''$db = new PDO('sqlite:/data/database.sqlite'); +$db->exec('PRAGMA query_only=ON'); +$known = ['workflow_instances','workflow_runs','workflow_tasks','workflow_history_events', + 'activity_executions','activity_attempts','activity_tasks','jobs','failed_jobs']; +$allowed = ['id','workflow_id','workflow_instance_id','workflow_run_id','task_queue','queue', + 'type','status','attempt_count','repair_count','activity_type','activity_execution_id', + 'activity_attempt_id','failure_reason','exception_class','exception_message', + 'sequence','event_type','command_type','command_id','namespace','worker_id', + 'lease_owner','available_at','created_at','started_at','completed_at','last_error']; +$result = []; +foreach ($known as $table) { + $columns = $db->query('PRAGMA table_info("'.$table.'")')->fetchAll(PDO::FETCH_ASSOC); + $selected = array_values(array_intersect(array_column($columns, 'name'), $allowed)); + if ($selected !== []) { + $result[$table] = $db->query('SELECT "'.implode('","', $selected).'" FROM "'.$table.'" LIMIT 20')->fetchAll(PDO::FETCH_ASSOC); + } +} +echo json_encode($result, JSON_THROW_ON_ERROR);''' + state = kubectl('exec', 'deployment/durable-workflow-server', '-c', 'scheduler', '--', + 'php', '-r', audit, label='durable-state', timeout=20) + if show_logs: + print('Synthetic durable state: ' + state, flush=True) + except RuntimeError: + pass def main():