pyworkflow-engine 0.4.0__tar.gz → 0.4.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/PKG-INFO +18 -18
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/docs/guides/configuration.mdx +3 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyproject.toml +18 -18
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/__init__.py +1 -1
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/celery/app.py +22 -31
- pyworkflow_engine-0.4.1/pyworkflow/celery/reschedule.py +178 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/celery/tasks.py +184 -9
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow_engine.egg-info/SOURCES.txt +2 -1
- pyworkflow_engine-0.4.1/tests/integration/_reschedule_e2e_app.py +97 -0
- pyworkflow_engine-0.4.1/tests/integration/test_reschedule_e2e.py +270 -0
- pyworkflow_engine-0.4.1/tests/unit/test_child_redelivery.py +222 -0
- pyworkflow_engine-0.4.1/tests/unit/test_interrupted_delivery_recovery.py +156 -0
- pyworkflow_engine-0.4.1/tests/unit/test_reschedule.py +176 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/test_singleton.py +4 -5
- pyworkflow_engine-0.4.0/pyworkflow/celery/reschedule.py +0 -339
- pyworkflow_engine-0.4.0/tests/integration/_reschedule_e2e_app.py +0 -41
- pyworkflow_engine-0.4.0/tests/integration/test_reschedule.py +0 -133
- pyworkflow_engine-0.4.0/tests/integration/test_reschedule_e2e.py +0 -200
- pyworkflow_engine-0.4.0/tests/unit/test_reschedule.py +0 -323
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/CLAUDE.md +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/DISTRIBUTED.md +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/LICENSE +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/MANIFEST.in +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/README.md +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/RELEASING.md +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/docs/architecture.md +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/docs/concepts/cancellation.mdx +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/docs/concepts/continue-as-new.mdx +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/docs/concepts/events.mdx +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/docs/concepts/fault-tolerance.mdx +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/docs/concepts/hooks.mdx +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/docs/concepts/limitations.mdx +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/docs/concepts/schedules.mdx +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/docs/concepts/sleep.mdx +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/docs/concepts/step-context.mdx +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/docs/concepts/step-hooks-and-checkpointing.mdx +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/docs/concepts/steps.mdx +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/docs/concepts/stream-steps.mdx +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/docs/concepts/stream-workflows.mdx +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/docs/concepts/workflows.mdx +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/docs/conventions.md +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/docs/guides/brokers.mdx +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/docs/guides/cli.mdx +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/docs/harness-gaps.md +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/docs/introduction.mdx +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/docs/layers.md +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/docs/quickstart.mdx +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/__init__.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/celery/__init__.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/celery/durable/docker-compose.yml +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/celery/durable/pyworkflow.config.yaml +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/celery/durable/workflows/__init__.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/celery/durable/workflows/basic.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/celery/durable/workflows/batch_processing.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/celery/durable/workflows/cancellation.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/celery/durable/workflows/child_workflow_from_step.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/celery/durable/workflows/child_workflow_patterns.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/celery/durable/workflows/child_workflows.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/celery/durable/workflows/continue_as_new.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/celery/durable/workflows/fault_tolerance.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/celery/durable/workflows/hooks.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/celery/durable/workflows/idempotency.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/celery/durable/workflows/long_running.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/celery/durable/workflows/retries.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/celery/durable/workflows/schedules.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/celery/durable/workflows/sleep_in_step.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/celery/durable/workflows/step_context.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/celery/transient/01_basic_workflow.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/celery/transient/02_fault_tolerance.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/celery/transient/__init__.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/celery/transient/pyworkflow.config.yaml +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/local/__init__.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/local/durable/01_basic_workflow.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/local/durable/02_file_storage.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/local/durable/03_retries.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/local/durable/04_long_running.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/local/durable/05_event_log.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/local/durable/06_idempotency.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/local/durable/07_hooks.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/local/durable/08_cancellation.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/local/durable/09_child_workflows.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/local/durable/10_child_workflow_patterns.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/local/durable/11_continue_as_new.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/local/durable/12_schedules.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/local/durable/13_step_context.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/local/durable/14_child_workflow_from_step.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/local/durable/__init__.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/local/transient/01_quick_tasks.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/local/transient/02_retries.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/local/transient/03_sleep.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/local/transient/__init__.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/aws/__init__.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/aws/context.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/aws/handler.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/aws/testing.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/celery/__init__.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/celery/loop.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/celery/scheduler.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/celery/singleton.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/cli/__init__.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/cli/__main__.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/cli/commands/__init__.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/cli/commands/hooks.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/cli/commands/quickstart.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/cli/commands/runs.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/cli/commands/scheduler.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/cli/commands/schedules.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/cli/commands/setup.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/cli/commands/worker.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/cli/commands/workflows.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/cli/output/__init__.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/cli/output/formatters.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/cli/output/styles.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/cli/utils/__init__.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/cli/utils/async_helpers.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/cli/utils/config.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/cli/utils/config_generator.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/cli/utils/discovery.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/cli/utils/docker_manager.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/cli/utils/interactive.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/cli/utils/storage.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/config.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/context/__init__.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/context/aws.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/context/base.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/context/local.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/context/mock.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/context/step_context.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/core/__init__.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/core/exceptions.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/core/registry.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/core/scheduled.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/core/step.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/core/strategy.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/core/validation.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/core/workflow.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/discovery.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/engine/__init__.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/engine/events.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/engine/executor.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/engine/replay.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/observability/__init__.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/observability/logging.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/primitives/__init__.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/primitives/child_handle.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/primitives/child_workflow.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/primitives/continue_as_new.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/primitives/define_hook.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/primitives/hooks.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/primitives/resume_hook.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/primitives/schedule.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/primitives/shield.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/primitives/sleep.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/primitives/step_checkpoint.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/primitives/step_hook.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/runtime/__init__.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/runtime/base.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/runtime/celery.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/runtime/factory.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/runtime/local.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/scheduler/__init__.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/scheduler/local.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/serialization/__init__.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/serialization/decoder.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/serialization/encoder.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/storage/__init__.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/storage/base.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/storage/cassandra.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/storage/citus.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/storage/config.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/storage/dynamodb.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/storage/file.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/storage/memory.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/storage/migrations/__init__.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/storage/migrations/base.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/storage/mysql.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/storage/postgres.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/storage/schemas.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/storage/sqlite.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/streams/__init__.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/streams/checkpoint.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/streams/consumer.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/streams/context.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/streams/decorator.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/streams/dispatcher.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/streams/emit.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/streams/registry.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/streams/runtime.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/streams/signal.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/streams/step_context.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/tracing/__init__.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/tracing/base.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/tracing/factory.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/tracing/langfuse.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/tracing/types.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/utils/__init__.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/utils/duration.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/utils/helpers.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/utils/schedule.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/setup.cfg +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/integration/__init__.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/integration/_run_strategy_e2e_app.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/integration/_run_strategy_e2e_driver.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/integration/test_cancellation.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/integration/test_cassandra_storage.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/integration/test_child_workflows.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/integration/test_continue_as_new.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/integration/test_dynamodb_storage.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/integration/test_fault_tolerance.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/integration/test_list_runs_pagination.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/integration/test_run_strategy_e2e.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/integration/test_schedule_storage.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/integration/test_schema_migrations.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/integration/test_singleton.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/integration/test_step_hook_inline_suspension_e2e.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/integration/test_stream_e2e.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/integration/test_workflow_suspended.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/__init__.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/backends/__init__.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/backends/test_cassandra_storage.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/backends/test_citus_storage.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/backends/test_dynamodb_storage.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/backends/test_postgres_storage.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/backends/test_sqlite_storage.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/conftest.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/storage/__init__.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/storage/test_migrations.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/test_cancellation.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/test_child_completion_notify.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/test_child_workflows.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/test_cli_worker.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/test_continue_as_new.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/test_emit.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/test_event_limits.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/test_executor.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/test_fault_tolerance.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/test_force_local.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/test_hooks.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/test_parent_run_id.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/test_primitives_from_steps.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/test_registry.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/test_replay.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/test_retention.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/test_schedule_schemas.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/test_schedule_utils.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/test_scheduled_workflow.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/test_signal.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/test_step.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/test_step_checkpoint.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/test_step_context.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/test_step_hook.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/test_stream_storage.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/test_stream_workflow.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/test_validation.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/test_workflow.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/test_workflow_run_strategy.py +0 -0
- {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/test_workflow_suspended.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyworkflow-engine
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: A Python implementation of durable, event-sourced workflows inspired by Vercel Workflow
|
|
5
5
|
Author: PyWorkflow Contributors
|
|
6
6
|
License: MIT
|
|
@@ -24,13 +24,13 @@ Description-Content-Type: text/markdown
|
|
|
24
24
|
License-File: LICENSE
|
|
25
25
|
Requires-Dist: celery<6.0.0,>=5.3.0
|
|
26
26
|
Requires-Dist: cloudpickle>=3.0.0
|
|
27
|
-
Requires-Dist: pydantic<3.0.0,>=2.
|
|
27
|
+
Requires-Dist: pydantic<3.0.0,>=2.14.1
|
|
28
28
|
Requires-Dist: loguru>=0.7.0
|
|
29
29
|
Requires-Dist: click>=8.4.2
|
|
30
30
|
Requires-Dist: inquirerpy>=0.3.4; python_version < "4.0"
|
|
31
31
|
Requires-Dist: httpx>=0.25.0
|
|
32
32
|
Requires-Dist: python-dateutil>=2.8.0
|
|
33
|
-
Requires-Dist: filelock>=
|
|
33
|
+
Requires-Dist: filelock>=4.2.4
|
|
34
34
|
Requires-Dist: pyyaml>=6.0.0
|
|
35
35
|
Requires-Dist: croniter>=6.2.4
|
|
36
36
|
Provides-Extra: redis
|
|
@@ -38,11 +38,11 @@ Requires-Dist: redis>=8.1.0; extra == "redis"
|
|
|
38
38
|
Provides-Extra: sqlite
|
|
39
39
|
Requires-Dist: aiosqlite>=0.19.0; extra == "sqlite"
|
|
40
40
|
Provides-Extra: postgres
|
|
41
|
-
Requires-Dist: asyncpg>=0.
|
|
41
|
+
Requires-Dist: asyncpg>=0.32.0; extra == "postgres"
|
|
42
42
|
Provides-Extra: aws
|
|
43
|
-
Requires-Dist: aws-durable-execution-sdk-python>=
|
|
43
|
+
Requires-Dist: aws-durable-execution-sdk-python>=2.0.1; extra == "aws"
|
|
44
44
|
Provides-Extra: dynamodb
|
|
45
|
-
Requires-Dist: aiobotocore>=3.9.
|
|
45
|
+
Requires-Dist: aiobotocore>=3.9.2; extra == "dynamodb"
|
|
46
46
|
Provides-Extra: cassandra
|
|
47
47
|
Requires-Dist: cassandra-driver>=3.30.1; extra == "cassandra"
|
|
48
48
|
Provides-Extra: mysql
|
|
@@ -50,28 +50,28 @@ Requires-Dist: aiomysql>=0.2.0; extra == "mysql"
|
|
|
50
50
|
Provides-Extra: all
|
|
51
51
|
Requires-Dist: redis>=8.1.0; extra == "all"
|
|
52
52
|
Requires-Dist: aiosqlite>=0.19.0; extra == "all"
|
|
53
|
-
Requires-Dist: asyncpg>=0.
|
|
53
|
+
Requires-Dist: asyncpg>=0.32.0; extra == "all"
|
|
54
54
|
Requires-Dist: aiomysql>=0.2.0; extra == "all"
|
|
55
|
-
Requires-Dist: aws-durable-execution-sdk-python>=
|
|
55
|
+
Requires-Dist: aws-durable-execution-sdk-python>=2.0.1; extra == "all"
|
|
56
56
|
Requires-Dist: cassandra-driver>=3.30.1; extra == "all"
|
|
57
57
|
Provides-Extra: dev
|
|
58
58
|
Requires-Dist: pytest>=9.1.1; extra == "dev"
|
|
59
59
|
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
60
60
|
Requires-Dist: pytest-cov>=4.1.0; extra == "dev"
|
|
61
61
|
Requires-Dist: pytest-celery>=0.0.0; extra == "dev"
|
|
62
|
-
Requires-Dist: pytest-mock>=3.
|
|
63
|
-
Requires-Dist: moto[dynamodb]>=5.2.
|
|
64
|
-
Requires-Dist: black>=26.
|
|
65
|
-
Requires-Dist: ruff>=0.
|
|
66
|
-
Requires-Dist: mypy>=2.
|
|
67
|
-
Requires-Dist: pre-commit>=4.
|
|
62
|
+
Requires-Dist: pytest-mock>=3.17.0; extra == "dev"
|
|
63
|
+
Requires-Dist: moto[dynamodb]>=5.2.4; extra == "dev"
|
|
64
|
+
Requires-Dist: black>=26.10.1; extra == "dev"
|
|
65
|
+
Requires-Dist: ruff>=0.17.2; extra == "dev"
|
|
66
|
+
Requires-Dist: mypy>=2.4.0; extra == "dev"
|
|
67
|
+
Requires-Dist: pre-commit>=4.7.0; extra == "dev"
|
|
68
68
|
Requires-Dist: types-redis>=4.6.0; extra == "dev"
|
|
69
|
-
Requires-Dist: types-python-dateutil>=2.9.0.
|
|
70
|
-
Requires-Dist: types-PyYAML>=6.0.12.
|
|
71
|
-
Requires-Dist: flower>=2.
|
|
69
|
+
Requires-Dist: types-python-dateutil>=2.9.0.20261021; extra == "dev"
|
|
70
|
+
Requires-Dist: types-PyYAML>=6.0.12.20260906; extra == "dev"
|
|
71
|
+
Requires-Dist: flower>=2.2.0; extra == "dev"
|
|
72
72
|
Requires-Dist: redis>=8.1.0; extra == "dev"
|
|
73
73
|
Requires-Dist: aiosqlite>=0.19.0; extra == "dev"
|
|
74
|
-
Requires-Dist: asyncpg>=0.
|
|
74
|
+
Requires-Dist: asyncpg>=0.32.0; extra == "dev"
|
|
75
75
|
Dynamic: license-file
|
|
76
76
|
|
|
77
77
|
# PyWorkflow
|
|
@@ -392,6 +392,9 @@ Environment variables provide deployment flexibility:
|
|
|
392
392
|
| `PYWORKFLOW_CELERY_BROKER` | Celery broker URL |
|
|
393
393
|
| `PYWORKFLOW_CELERY_RESULT_BACKEND` | Celery result backend URL |
|
|
394
394
|
| `PYWORKFLOW_DISCOVER` | Modules to import for workflow discovery |
|
|
395
|
+
| `PYWORKFLOW_RESCHEDULE_ON_SIGTERM` | Hand in-flight tasks off to another worker when a worker gets SIGTERM (default on; `0` falls back to Celery's warm shutdown) |
|
|
396
|
+
| `PYWORKFLOW_SOFT_SHUTDOWN_TIMEOUT` | Seconds a SIGTERMed worker lets running tasks finish before terminating them and letting the broker redeliver each once (default `20`). Keep it below your orchestrator's grace period (Kubernetes `terminationGracePeriodSeconds`, 30s by default), or a SIGKILL during the wait leaves tasks unacked until the broker's `visibility_timeout` |
|
|
397
|
+
| `PYWORKFLOW_CANCEL_TASKS_ON_CONNECTION_LOSS` | Terminate running tasks when the worker loses its broker connection (Redis restart, Sentinel failover), because their messages are redelivered to another worker (default on). `0` keeps them running, and the redelivered copy then runs at the same time |
|
|
395
398
|
|
|
396
399
|
```bash
|
|
397
400
|
# Example: Production deployment with environment variables
|
|
@@ -7,7 +7,7 @@ packages = [{include = "pyworkflow"}]
|
|
|
7
7
|
|
|
8
8
|
[project]
|
|
9
9
|
name = "pyworkflow-engine"
|
|
10
|
-
version = "0.4.
|
|
10
|
+
version = "0.4.1"
|
|
11
11
|
description = "A Python implementation of durable, event-sourced workflows inspired by Vercel Workflow"
|
|
12
12
|
readme = "README.md"
|
|
13
13
|
requires-python = ">=3.11"
|
|
@@ -33,7 +33,7 @@ dependencies = [
|
|
|
33
33
|
# Core dependencies
|
|
34
34
|
"celery>=5.3.0,<6.0.0",
|
|
35
35
|
"cloudpickle>=3.0.0",
|
|
36
|
-
"pydantic>=2.
|
|
36
|
+
"pydantic>=2.14.1,<3.0.0",
|
|
37
37
|
"loguru>=0.7.0",
|
|
38
38
|
|
|
39
39
|
# CLI dependencies
|
|
@@ -43,7 +43,7 @@ dependencies = [
|
|
|
43
43
|
# Utilities
|
|
44
44
|
"httpx>=0.25.0",
|
|
45
45
|
"python-dateutil>=2.8.0",
|
|
46
|
-
"filelock>=
|
|
46
|
+
"filelock>=4.2.4",
|
|
47
47
|
"pyyaml>=6.0.0",
|
|
48
48
|
"croniter>=6.2.4",
|
|
49
49
|
]
|
|
@@ -59,15 +59,15 @@ sqlite = [
|
|
|
59
59
|
]
|
|
60
60
|
|
|
61
61
|
postgres = [
|
|
62
|
-
"asyncpg>=0.
|
|
62
|
+
"asyncpg>=0.32.0",
|
|
63
63
|
]
|
|
64
64
|
|
|
65
65
|
# AWS runtime support
|
|
66
66
|
aws = [
|
|
67
|
-
"aws-durable-execution-sdk-python>=
|
|
67
|
+
"aws-durable-execution-sdk-python>=2.0.1",
|
|
68
68
|
]
|
|
69
69
|
dynamodb = [
|
|
70
|
-
"aiobotocore>=3.9.
|
|
70
|
+
"aiobotocore>=3.9.2",
|
|
71
71
|
]
|
|
72
72
|
|
|
73
73
|
cassandra = [
|
|
@@ -82,9 +82,9 @@ mysql = [
|
|
|
82
82
|
all = [
|
|
83
83
|
"redis>=8.1.0",
|
|
84
84
|
"aiosqlite>=0.19.0",
|
|
85
|
-
"asyncpg>=0.
|
|
85
|
+
"asyncpg>=0.32.0",
|
|
86
86
|
"aiomysql>=0.2.0",
|
|
87
|
-
"aws-durable-execution-sdk-python>=
|
|
87
|
+
"aws-durable-execution-sdk-python>=2.0.1",
|
|
88
88
|
"cassandra-driver>=3.30.1",
|
|
89
89
|
]
|
|
90
90
|
|
|
@@ -95,27 +95,27 @@ dev = [
|
|
|
95
95
|
"pytest-asyncio>=0.21.0",
|
|
96
96
|
"pytest-cov>=4.1.0",
|
|
97
97
|
"pytest-celery>=0.0.0",
|
|
98
|
-
"pytest-mock>=3.
|
|
99
|
-
"moto[dynamodb]>=5.2.
|
|
98
|
+
"pytest-mock>=3.17.0",
|
|
99
|
+
"moto[dynamodb]>=5.2.4", # AWS mocking for DynamoDB tests
|
|
100
100
|
|
|
101
101
|
# Code quality
|
|
102
|
-
"black>=26.
|
|
103
|
-
"ruff>=0.
|
|
104
|
-
"mypy>=2.
|
|
105
|
-
"pre-commit>=4.
|
|
102
|
+
"black>=26.10.1",
|
|
103
|
+
"ruff>=0.17.2",
|
|
104
|
+
"mypy>=2.4.0",
|
|
105
|
+
"pre-commit>=4.7.0",
|
|
106
106
|
|
|
107
107
|
# Type stubs
|
|
108
108
|
"types-redis>=4.6.0",
|
|
109
|
-
"types-python-dateutil>=2.9.0.
|
|
110
|
-
"types-PyYAML>=6.0.12.
|
|
109
|
+
"types-python-dateutil>=2.9.0.20261021",
|
|
110
|
+
"types-PyYAML>=6.0.12.20260906",
|
|
111
111
|
|
|
112
112
|
# Monitoring (optional)
|
|
113
|
-
"flower>=2.
|
|
113
|
+
"flower>=2.2.0", # Celery web UI
|
|
114
114
|
|
|
115
115
|
# Include all storage backends for development
|
|
116
116
|
"redis>=8.1.0",
|
|
117
117
|
"aiosqlite>=0.19.0",
|
|
118
|
-
"asyncpg>=0.
|
|
118
|
+
"asyncpg>=0.32.0",
|
|
119
119
|
]
|
|
120
120
|
|
|
121
121
|
[project.urls]
|
|
@@ -12,17 +12,15 @@ warnings in Celery logs. This is a known compatibility issue between Python 3.13
|
|
|
12
12
|
garbage collector and Celery's saferepr module. It does not affect functionality.
|
|
13
13
|
"""
|
|
14
14
|
|
|
15
|
-
import contextlib
|
|
16
15
|
import os
|
|
17
16
|
from datetime import timedelta
|
|
18
17
|
from typing import Any
|
|
19
18
|
|
|
20
19
|
from celery import Celery
|
|
21
20
|
from celery.signals import (
|
|
22
|
-
task_postrun,
|
|
23
|
-
task_prerun,
|
|
24
21
|
worker_init,
|
|
25
22
|
worker_process_init,
|
|
23
|
+
worker_ready,
|
|
26
24
|
worker_shutdown,
|
|
27
25
|
)
|
|
28
26
|
from kombu import Exchange, Queue
|
|
@@ -288,9 +286,13 @@ def create_celery_app(
|
|
|
288
286
|
PYWORKFLOW_CELERY_SENTINEL_MASTER: Sentinel master name (used if sentinel_master_name param not provided)
|
|
289
287
|
PYWORKFLOW_WORKER_MAX_MEMORY: Max memory per worker child (KB) (used if worker_max_memory_per_child param not provided)
|
|
290
288
|
PYWORKFLOW_WORKER_MAX_TASKS: Max tasks per worker child (used if worker_max_tasks_per_child param not provided)
|
|
291
|
-
PYWORKFLOW_RESCHEDULE_ON_SIGTERM:
|
|
289
|
+
PYWORKFLOW_RESCHEDULE_ON_SIGTERM: Hand in-flight tasks off to another worker when a worker is shut down
|
|
292
290
|
(SIGTERM / spot reclaim), instead of waiting out the broker visibility_timeout. Default on; set to
|
|
293
|
-
"0"/"false"/"no" to disable.
|
|
291
|
+
"0"/"false"/"no" to disable. See pyworkflow/celery/reschedule.py.
|
|
292
|
+
PYWORKFLOW_SOFT_SHUTDOWN_TIMEOUT: Seconds a SIGTERMed worker lets running tasks finish before handing
|
|
293
|
+
them off (default 20). Must be lower than the orchestrator's grace period.
|
|
294
|
+
PYWORKFLOW_CANCEL_TASKS_ON_CONNECTION_LOSS: Terminate running tasks when the broker connection is lost,
|
|
295
|
+
since their messages are redelivered (default on; "0" keeps them running at the risk of duplicates).
|
|
294
296
|
|
|
295
297
|
Examples:
|
|
296
298
|
# Default configuration (uses env vars if set, otherwise localhost Redis)
|
|
@@ -461,6 +463,14 @@ def create_celery_app(
|
|
|
461
463
|
}
|
|
462
464
|
)
|
|
463
465
|
|
|
466
|
+
# SIGTERM handoff: let running tasks finish for a bounded time, then terminate
|
|
467
|
+
# them and let the broker redeliver each one once. On a broker reconnect,
|
|
468
|
+
# terminate running tasks whose messages are being redelivered. See reschedule.py.
|
|
469
|
+
from pyworkflow.celery.reschedule import connection_loss_config, soft_shutdown_config
|
|
470
|
+
|
|
471
|
+
config_dict.update(soft_shutdown_config())
|
|
472
|
+
config_dict.update(connection_loss_config())
|
|
473
|
+
|
|
464
474
|
# Configure Celery
|
|
465
475
|
app.conf.update(config_dict)
|
|
466
476
|
|
|
@@ -481,14 +491,6 @@ def create_celery_app(
|
|
|
481
491
|
# to ensure proper initialization AFTER process forking.
|
|
482
492
|
# See on_worker_init() and on_worker_process_init() below.
|
|
483
493
|
|
|
484
|
-
# Re-enqueue in-flight tasks on worker shutdown (SIGTERM / spot reclaim) so a
|
|
485
|
-
# live worker resumes them within seconds instead of waiting out the broker
|
|
486
|
-
# visibility_timeout. Hooked as a consumer bootstep so it fires exactly as the
|
|
487
|
-
# consumer is torn down (event-based). See pyworkflow/celery/reschedule.py.
|
|
488
|
-
from pyworkflow.celery.reschedule import RescheduleConsumerStep
|
|
489
|
-
|
|
490
|
-
app.steps["consumer"].add(RescheduleConsumerStep)
|
|
491
|
-
|
|
492
494
|
# Auto-discover workflows from environment variable or configured modules
|
|
493
495
|
discover_workflows()
|
|
494
496
|
|
|
@@ -573,27 +575,16 @@ def on_worker_shutdown(**kwargs):
|
|
|
573
575
|
close_worker_loop()
|
|
574
576
|
|
|
575
577
|
|
|
576
|
-
@
|
|
577
|
-
def
|
|
578
|
+
@worker_ready.connect
|
|
579
|
+
def on_worker_ready(sender=None, **_):
|
|
578
580
|
"""
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
Wrapped defensively: tracking must never interfere with task execution.
|
|
581
|
+
Make SIGTERM hand in-flight tasks off to another worker (soft + cold
|
|
582
|
+
shutdown) instead of Celery's default warm shutdown. Installed here, after
|
|
583
|
+
Celery has set up its own signal handlers. See pyworkflow/celery/reschedule.py.
|
|
583
584
|
"""
|
|
584
|
-
from pyworkflow.celery.reschedule import
|
|
585
|
-
|
|
586
|
-
with contextlib.suppress(Exception):
|
|
587
|
-
track_task_start(task, task_id, args, kwargs)
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
@task_postrun.connect
|
|
591
|
-
def on_task_postrun(task_id=None, **_):
|
|
592
|
-
"""Clear the in-flight task once it has finished (clean completion)."""
|
|
593
|
-
from pyworkflow.celery.reschedule import track_task_end
|
|
585
|
+
from pyworkflow.celery.reschedule import install_sigterm_handoff
|
|
594
586
|
|
|
595
|
-
|
|
596
|
-
track_task_end(task_id)
|
|
587
|
+
install_sigterm_handoff(getattr(sender, "controller", None))
|
|
597
588
|
|
|
598
589
|
|
|
599
590
|
def get_celery_app() -> Celery:
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Hand in-flight tasks off to another worker when a worker is shut down (SIGTERM).
|
|
3
|
+
|
|
4
|
+
When a Celery worker pod is reclaimed (e.g. an AWS spot node drain), it receives
|
|
5
|
+
SIGTERM. Celery's default *warm* shutdown waits for running tasks to finish, and
|
|
6
|
+
if the pod is SIGKILLed first, the task's message (``task_acks_late=True``) stays
|
|
7
|
+
unacked until the Redis ``visibility_timeout`` (an hour by default). The parent
|
|
8
|
+
workflow stays SUSPENDED that whole time, so the user-facing flow silently hangs.
|
|
9
|
+
|
|
10
|
+
How the handoff works
|
|
11
|
+
---------------------
|
|
12
|
+
SIGTERM runs Celery's *cold* shutdown with a *soft* phase first (Celery >= 5.5):
|
|
13
|
+
|
|
14
|
+
1. Soft phase: the worker waits ``worker_soft_shutdown_timeout`` seconds, so a
|
|
15
|
+
task that finishes in time acks normally and nothing is handed off.
|
|
16
|
+
2. Cold phase: Celery cancels the task consumer, terminates the pool processes
|
|
17
|
+
still running a task, and closes the broker connection. Kombu restores the
|
|
18
|
+
unacked messages to their queues, and a live worker picks them up within
|
|
19
|
+
seconds.
|
|
20
|
+
|
|
21
|
+
Each in-flight message goes back to the queue exactly once, and only after the
|
|
22
|
+
process that was running it is dead.
|
|
23
|
+
|
|
24
|
+
Why not re-enqueue at the start of shutdown (#733)
|
|
25
|
+
--------------------------------------------------
|
|
26
|
+
The previous design re-published every in-flight task as soon as the consumer
|
|
27
|
+
stopped. Warm shutdown then let the original keep running, and closing the
|
|
28
|
+
consumer connection made kombu restore the original message too, so every
|
|
29
|
+
in-flight task ran three times at once: the original plus two copies. The
|
|
30
|
+
completion checks in the tasks can't prevent that, because nothing is completed
|
|
31
|
+
while all three are still running.
|
|
32
|
+
|
|
33
|
+
Celery does the same thing when ``REMAP_SIGTERM=SIGQUIT`` is set, but billiard
|
|
34
|
+
reads that variable at import time, which is too early to rely on for every way
|
|
35
|
+
a worker can be launched. So we install the handler ourselves once the worker is
|
|
36
|
+
ready; when ``REMAP_SIGTERM`` is set, Celery already handles SIGTERM and we
|
|
37
|
+
leave it alone. Only the prefork pool is supported: terminating a running task
|
|
38
|
+
requires a separate process.
|
|
39
|
+
|
|
40
|
+
The timeout must be lower than the orchestrator's grace period (Kubernetes
|
|
41
|
+
``terminationGracePeriodSeconds``, 30s by default). A SIGKILL during the soft
|
|
42
|
+
phase leaves the message unacked until the ``visibility_timeout`` again.
|
|
43
|
+
|
|
44
|
+
Broker connection loss
|
|
45
|
+
----------------------
|
|
46
|
+
Losing the broker connection (a Redis restart, a Sentinel failover) runs the
|
|
47
|
+
same kombu restore: Celery collects the old connection and the unacked messages
|
|
48
|
+
go back to the queue. By default Celery leaves the running tasks alive, so the
|
|
49
|
+
original and the redelivered copy run at the same time. We enable Celery's
|
|
50
|
+
``worker_cancel_long_running_tasks_on_connection_loss`` (the default from
|
|
51
|
+
Celery 6) so the original is terminated as its message is restored.
|
|
52
|
+
|
|
53
|
+
Gates:
|
|
54
|
+
- ``PYWORKFLOW_RESCHEDULE_ON_SIGTERM`` (default on; set to ``0``/``false``/``no``
|
|
55
|
+
to fall back to Celery's plain warm shutdown).
|
|
56
|
+
- ``PYWORKFLOW_SOFT_SHUTDOWN_TIMEOUT`` (seconds, default 20). ``0`` hands tasks
|
|
57
|
+
off immediately without waiting for them.
|
|
58
|
+
- ``PYWORKFLOW_CANCEL_TASKS_ON_CONNECTION_LOSS`` (default on; ``0`` keeps
|
|
59
|
+
running tasks alive across a broker reconnect, at the risk of a duplicate).
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
import os
|
|
63
|
+
import threading
|
|
64
|
+
from typing import Any
|
|
65
|
+
|
|
66
|
+
from loguru import logger
|
|
67
|
+
|
|
68
|
+
DEFAULT_SOFT_SHUTDOWN_TIMEOUT = 20.0
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _flag(name: str) -> bool:
|
|
72
|
+
return os.getenv(name, "1").strip().lower() not in ("0", "false", "no")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _enabled() -> bool:
|
|
76
|
+
return _flag("PYWORKFLOW_RESCHEDULE_ON_SIGTERM")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def soft_shutdown_timeout() -> float:
|
|
80
|
+
"""Seconds a SIGTERMed worker waits for running tasks before handing them off."""
|
|
81
|
+
raw = os.getenv("PYWORKFLOW_SOFT_SHUTDOWN_TIMEOUT", "").strip()
|
|
82
|
+
if not raw:
|
|
83
|
+
return DEFAULT_SOFT_SHUTDOWN_TIMEOUT
|
|
84
|
+
try:
|
|
85
|
+
return max(float(raw), 0.0)
|
|
86
|
+
except ValueError:
|
|
87
|
+
logger.warning(
|
|
88
|
+
f"reschedule: invalid PYWORKFLOW_SOFT_SHUTDOWN_TIMEOUT={raw!r}, "
|
|
89
|
+
f"using {DEFAULT_SOFT_SHUTDOWN_TIMEOUT}s"
|
|
90
|
+
)
|
|
91
|
+
return DEFAULT_SOFT_SHUTDOWN_TIMEOUT
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def soft_shutdown_config() -> dict[str, Any]:
|
|
95
|
+
"""Celery settings for the SIGTERM handoff (empty when disabled)."""
|
|
96
|
+
if not _enabled():
|
|
97
|
+
return {}
|
|
98
|
+
return {"worker_soft_shutdown_timeout": soft_shutdown_timeout()}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def connection_loss_config() -> dict[str, Any]:
|
|
102
|
+
"""Celery settings that stop a task before a broker reconnect redelivers it."""
|
|
103
|
+
return {
|
|
104
|
+
"worker_cancel_long_running_tasks_on_connection_loss": _flag(
|
|
105
|
+
"PYWORKFLOW_CANCEL_TASKS_ON_CONNECTION_LOSS"
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _is_worker_stopping() -> bool:
|
|
111
|
+
"""True once the worker has started shutting down (warm or cold)."""
|
|
112
|
+
try:
|
|
113
|
+
from celery.worker import state
|
|
114
|
+
|
|
115
|
+
# Identity checks (matching celery.worker.state): the flags are set to the
|
|
116
|
+
# exit code, which can be 0 (EX_OK) -- `0 == False`.
|
|
117
|
+
def _set(v: Any) -> bool:
|
|
118
|
+
return v is not None and v is not False
|
|
119
|
+
|
|
120
|
+
return _set(state.should_stop) or _set(state.should_terminate)
|
|
121
|
+
except Exception:
|
|
122
|
+
return False
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _is_prefork(worker: Any) -> bool:
|
|
126
|
+
from celery.concurrency.prefork import TaskPool
|
|
127
|
+
|
|
128
|
+
return isinstance(getattr(worker, "pool", None), TaskPool)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _on_sigterm(worker: Any) -> None:
|
|
132
|
+
"""Run Celery's soft + cold shutdown, as it does for ``REMAP_SIGTERM=SIGQUIT``."""
|
|
133
|
+
from celery.apps import worker as celery_worker
|
|
134
|
+
|
|
135
|
+
# A second SIGTERM must not start a second soft wait: treat it like a repeated
|
|
136
|
+
# SIGQUIT (cancel the tasks now, keep waiting for the cold shutdown).
|
|
137
|
+
celery_worker.install_worker_term_hard_handler(
|
|
138
|
+
worker, sig="SIGTERM", callback=celery_worker.during_soft_shutdown
|
|
139
|
+
)
|
|
140
|
+
logger.warning(
|
|
141
|
+
"reschedule: SIGTERM received, handing off in-flight tasks after "
|
|
142
|
+
f"{worker.app.conf.worker_soft_shutdown_timeout}s"
|
|
143
|
+
)
|
|
144
|
+
celery_worker.on_cold_shutdown(worker)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def install_sigterm_handoff(worker: Any) -> bool:
|
|
148
|
+
"""
|
|
149
|
+
Make SIGTERM hand in-flight tasks off via Celery's soft + cold shutdown.
|
|
150
|
+
|
|
151
|
+
Called once the worker is ready (after Celery installed its own signal
|
|
152
|
+
handlers). Returns True if the handler was installed. Never raises.
|
|
153
|
+
"""
|
|
154
|
+
if not _enabled() or worker is None or _is_worker_stopping():
|
|
155
|
+
return False
|
|
156
|
+
try:
|
|
157
|
+
from celery.apps import worker as celery_worker
|
|
158
|
+
|
|
159
|
+
if not all(hasattr(celery_worker, n) for n in ("on_cold_shutdown", "during_soft_shutdown")):
|
|
160
|
+
logger.warning(
|
|
161
|
+
"reschedule: SIGTERM handoff needs Celery >= 5.5 (soft shutdown), "
|
|
162
|
+
"keeping Celery's warm shutdown"
|
|
163
|
+
)
|
|
164
|
+
return False
|
|
165
|
+
if getattr(celery_worker, "REMAP_SIGTERM", None):
|
|
166
|
+
return False # Celery already maps SIGTERM to the cold shutdown.
|
|
167
|
+
if not _is_prefork(worker):
|
|
168
|
+
logger.info("reschedule: SIGTERM handoff requires the prefork pool, skipping")
|
|
169
|
+
return False
|
|
170
|
+
if threading.current_thread() is not threading.main_thread():
|
|
171
|
+
logger.warning("reschedule: not on the main thread, cannot install SIGTERM handler")
|
|
172
|
+
return False
|
|
173
|
+
|
|
174
|
+
celery_worker.install_worker_term_hard_handler(worker, sig="SIGTERM", callback=_on_sigterm)
|
|
175
|
+
return True
|
|
176
|
+
except Exception as exc:
|
|
177
|
+
logger.warning(f"reschedule: failed to install SIGTERM handoff: {exc}")
|
|
178
|
+
return False
|