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.
Files changed (257) hide show
  1. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/PKG-INFO +18 -18
  2. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/docs/guides/configuration.mdx +3 -0
  3. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyproject.toml +18 -18
  4. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/__init__.py +1 -1
  5. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/celery/app.py +22 -31
  6. pyworkflow_engine-0.4.1/pyworkflow/celery/reschedule.py +178 -0
  7. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/celery/tasks.py +184 -9
  8. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow_engine.egg-info/SOURCES.txt +2 -1
  9. pyworkflow_engine-0.4.1/tests/integration/_reschedule_e2e_app.py +97 -0
  10. pyworkflow_engine-0.4.1/tests/integration/test_reschedule_e2e.py +270 -0
  11. pyworkflow_engine-0.4.1/tests/unit/test_child_redelivery.py +222 -0
  12. pyworkflow_engine-0.4.1/tests/unit/test_interrupted_delivery_recovery.py +156 -0
  13. pyworkflow_engine-0.4.1/tests/unit/test_reschedule.py +176 -0
  14. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/test_singleton.py +4 -5
  15. pyworkflow_engine-0.4.0/pyworkflow/celery/reschedule.py +0 -339
  16. pyworkflow_engine-0.4.0/tests/integration/_reschedule_e2e_app.py +0 -41
  17. pyworkflow_engine-0.4.0/tests/integration/test_reschedule.py +0 -133
  18. pyworkflow_engine-0.4.0/tests/integration/test_reschedule_e2e.py +0 -200
  19. pyworkflow_engine-0.4.0/tests/unit/test_reschedule.py +0 -323
  20. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/CLAUDE.md +0 -0
  21. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/DISTRIBUTED.md +0 -0
  22. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/LICENSE +0 -0
  23. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/MANIFEST.in +0 -0
  24. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/README.md +0 -0
  25. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/RELEASING.md +0 -0
  26. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/docs/architecture.md +0 -0
  27. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/docs/concepts/cancellation.mdx +0 -0
  28. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/docs/concepts/continue-as-new.mdx +0 -0
  29. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/docs/concepts/events.mdx +0 -0
  30. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/docs/concepts/fault-tolerance.mdx +0 -0
  31. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/docs/concepts/hooks.mdx +0 -0
  32. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/docs/concepts/limitations.mdx +0 -0
  33. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/docs/concepts/schedules.mdx +0 -0
  34. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/docs/concepts/sleep.mdx +0 -0
  35. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/docs/concepts/step-context.mdx +0 -0
  36. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/docs/concepts/step-hooks-and-checkpointing.mdx +0 -0
  37. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/docs/concepts/steps.mdx +0 -0
  38. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/docs/concepts/stream-steps.mdx +0 -0
  39. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/docs/concepts/stream-workflows.mdx +0 -0
  40. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/docs/concepts/workflows.mdx +0 -0
  41. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/docs/conventions.md +0 -0
  42. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/docs/guides/brokers.mdx +0 -0
  43. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/docs/guides/cli.mdx +0 -0
  44. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/docs/harness-gaps.md +0 -0
  45. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/docs/introduction.mdx +0 -0
  46. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/docs/layers.md +0 -0
  47. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/docs/quickstart.mdx +0 -0
  48. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/__init__.py +0 -0
  49. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/celery/__init__.py +0 -0
  50. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/celery/durable/docker-compose.yml +0 -0
  51. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/celery/durable/pyworkflow.config.yaml +0 -0
  52. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/celery/durable/workflows/__init__.py +0 -0
  53. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/celery/durable/workflows/basic.py +0 -0
  54. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/celery/durable/workflows/batch_processing.py +0 -0
  55. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/celery/durable/workflows/cancellation.py +0 -0
  56. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/celery/durable/workflows/child_workflow_from_step.py +0 -0
  57. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/celery/durable/workflows/child_workflow_patterns.py +0 -0
  58. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/celery/durable/workflows/child_workflows.py +0 -0
  59. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/celery/durable/workflows/continue_as_new.py +0 -0
  60. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/celery/durable/workflows/fault_tolerance.py +0 -0
  61. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/celery/durable/workflows/hooks.py +0 -0
  62. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/celery/durable/workflows/idempotency.py +0 -0
  63. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/celery/durable/workflows/long_running.py +0 -0
  64. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/celery/durable/workflows/retries.py +0 -0
  65. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/celery/durable/workflows/schedules.py +0 -0
  66. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/celery/durable/workflows/sleep_in_step.py +0 -0
  67. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/celery/durable/workflows/step_context.py +0 -0
  68. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/celery/transient/01_basic_workflow.py +0 -0
  69. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/celery/transient/02_fault_tolerance.py +0 -0
  70. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/celery/transient/__init__.py +0 -0
  71. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/celery/transient/pyworkflow.config.yaml +0 -0
  72. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/local/__init__.py +0 -0
  73. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/local/durable/01_basic_workflow.py +0 -0
  74. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/local/durable/02_file_storage.py +0 -0
  75. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/local/durable/03_retries.py +0 -0
  76. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/local/durable/04_long_running.py +0 -0
  77. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/local/durable/05_event_log.py +0 -0
  78. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/local/durable/06_idempotency.py +0 -0
  79. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/local/durable/07_hooks.py +0 -0
  80. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/local/durable/08_cancellation.py +0 -0
  81. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/local/durable/09_child_workflows.py +0 -0
  82. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/local/durable/10_child_workflow_patterns.py +0 -0
  83. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/local/durable/11_continue_as_new.py +0 -0
  84. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/local/durable/12_schedules.py +0 -0
  85. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/local/durable/13_step_context.py +0 -0
  86. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/local/durable/14_child_workflow_from_step.py +0 -0
  87. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/local/durable/__init__.py +0 -0
  88. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/local/transient/01_quick_tasks.py +0 -0
  89. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/local/transient/02_retries.py +0 -0
  90. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/local/transient/03_sleep.py +0 -0
  91. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/examples/local/transient/__init__.py +0 -0
  92. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/aws/__init__.py +0 -0
  93. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/aws/context.py +0 -0
  94. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/aws/handler.py +0 -0
  95. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/aws/testing.py +0 -0
  96. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/celery/__init__.py +0 -0
  97. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/celery/loop.py +0 -0
  98. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/celery/scheduler.py +0 -0
  99. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/celery/singleton.py +0 -0
  100. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/cli/__init__.py +0 -0
  101. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/cli/__main__.py +0 -0
  102. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/cli/commands/__init__.py +0 -0
  103. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/cli/commands/hooks.py +0 -0
  104. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/cli/commands/quickstart.py +0 -0
  105. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/cli/commands/runs.py +0 -0
  106. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/cli/commands/scheduler.py +0 -0
  107. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/cli/commands/schedules.py +0 -0
  108. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/cli/commands/setup.py +0 -0
  109. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/cli/commands/worker.py +0 -0
  110. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/cli/commands/workflows.py +0 -0
  111. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/cli/output/__init__.py +0 -0
  112. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/cli/output/formatters.py +0 -0
  113. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/cli/output/styles.py +0 -0
  114. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/cli/utils/__init__.py +0 -0
  115. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/cli/utils/async_helpers.py +0 -0
  116. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/cli/utils/config.py +0 -0
  117. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/cli/utils/config_generator.py +0 -0
  118. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/cli/utils/discovery.py +0 -0
  119. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/cli/utils/docker_manager.py +0 -0
  120. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/cli/utils/interactive.py +0 -0
  121. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/cli/utils/storage.py +0 -0
  122. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/config.py +0 -0
  123. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/context/__init__.py +0 -0
  124. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/context/aws.py +0 -0
  125. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/context/base.py +0 -0
  126. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/context/local.py +0 -0
  127. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/context/mock.py +0 -0
  128. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/context/step_context.py +0 -0
  129. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/core/__init__.py +0 -0
  130. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/core/exceptions.py +0 -0
  131. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/core/registry.py +0 -0
  132. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/core/scheduled.py +0 -0
  133. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/core/step.py +0 -0
  134. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/core/strategy.py +0 -0
  135. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/core/validation.py +0 -0
  136. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/core/workflow.py +0 -0
  137. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/discovery.py +0 -0
  138. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/engine/__init__.py +0 -0
  139. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/engine/events.py +0 -0
  140. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/engine/executor.py +0 -0
  141. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/engine/replay.py +0 -0
  142. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/observability/__init__.py +0 -0
  143. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/observability/logging.py +0 -0
  144. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/primitives/__init__.py +0 -0
  145. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/primitives/child_handle.py +0 -0
  146. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/primitives/child_workflow.py +0 -0
  147. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/primitives/continue_as_new.py +0 -0
  148. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/primitives/define_hook.py +0 -0
  149. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/primitives/hooks.py +0 -0
  150. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/primitives/resume_hook.py +0 -0
  151. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/primitives/schedule.py +0 -0
  152. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/primitives/shield.py +0 -0
  153. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/primitives/sleep.py +0 -0
  154. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/primitives/step_checkpoint.py +0 -0
  155. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/primitives/step_hook.py +0 -0
  156. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/runtime/__init__.py +0 -0
  157. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/runtime/base.py +0 -0
  158. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/runtime/celery.py +0 -0
  159. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/runtime/factory.py +0 -0
  160. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/runtime/local.py +0 -0
  161. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/scheduler/__init__.py +0 -0
  162. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/scheduler/local.py +0 -0
  163. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/serialization/__init__.py +0 -0
  164. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/serialization/decoder.py +0 -0
  165. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/serialization/encoder.py +0 -0
  166. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/storage/__init__.py +0 -0
  167. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/storage/base.py +0 -0
  168. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/storage/cassandra.py +0 -0
  169. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/storage/citus.py +0 -0
  170. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/storage/config.py +0 -0
  171. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/storage/dynamodb.py +0 -0
  172. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/storage/file.py +0 -0
  173. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/storage/memory.py +0 -0
  174. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/storage/migrations/__init__.py +0 -0
  175. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/storage/migrations/base.py +0 -0
  176. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/storage/mysql.py +0 -0
  177. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/storage/postgres.py +0 -0
  178. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/storage/schemas.py +0 -0
  179. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/storage/sqlite.py +0 -0
  180. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/streams/__init__.py +0 -0
  181. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/streams/checkpoint.py +0 -0
  182. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/streams/consumer.py +0 -0
  183. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/streams/context.py +0 -0
  184. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/streams/decorator.py +0 -0
  185. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/streams/dispatcher.py +0 -0
  186. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/streams/emit.py +0 -0
  187. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/streams/registry.py +0 -0
  188. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/streams/runtime.py +0 -0
  189. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/streams/signal.py +0 -0
  190. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/streams/step_context.py +0 -0
  191. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/tracing/__init__.py +0 -0
  192. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/tracing/base.py +0 -0
  193. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/tracing/factory.py +0 -0
  194. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/tracing/langfuse.py +0 -0
  195. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/tracing/types.py +0 -0
  196. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/utils/__init__.py +0 -0
  197. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/utils/duration.py +0 -0
  198. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/utils/helpers.py +0 -0
  199. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/pyworkflow/utils/schedule.py +0 -0
  200. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/setup.cfg +0 -0
  201. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/integration/__init__.py +0 -0
  202. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/integration/_run_strategy_e2e_app.py +0 -0
  203. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/integration/_run_strategy_e2e_driver.py +0 -0
  204. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/integration/test_cancellation.py +0 -0
  205. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/integration/test_cassandra_storage.py +0 -0
  206. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/integration/test_child_workflows.py +0 -0
  207. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/integration/test_continue_as_new.py +0 -0
  208. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/integration/test_dynamodb_storage.py +0 -0
  209. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/integration/test_fault_tolerance.py +0 -0
  210. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/integration/test_list_runs_pagination.py +0 -0
  211. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/integration/test_run_strategy_e2e.py +0 -0
  212. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/integration/test_schedule_storage.py +0 -0
  213. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/integration/test_schema_migrations.py +0 -0
  214. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/integration/test_singleton.py +0 -0
  215. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/integration/test_step_hook_inline_suspension_e2e.py +0 -0
  216. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/integration/test_stream_e2e.py +0 -0
  217. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/integration/test_workflow_suspended.py +0 -0
  218. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/__init__.py +0 -0
  219. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/backends/__init__.py +0 -0
  220. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/backends/test_cassandra_storage.py +0 -0
  221. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/backends/test_citus_storage.py +0 -0
  222. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/backends/test_dynamodb_storage.py +0 -0
  223. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/backends/test_postgres_storage.py +0 -0
  224. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/backends/test_sqlite_storage.py +0 -0
  225. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/conftest.py +0 -0
  226. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/storage/__init__.py +0 -0
  227. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/storage/test_migrations.py +0 -0
  228. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/test_cancellation.py +0 -0
  229. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/test_child_completion_notify.py +0 -0
  230. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/test_child_workflows.py +0 -0
  231. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/test_cli_worker.py +0 -0
  232. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/test_continue_as_new.py +0 -0
  233. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/test_emit.py +0 -0
  234. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/test_event_limits.py +0 -0
  235. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/test_executor.py +0 -0
  236. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/test_fault_tolerance.py +0 -0
  237. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/test_force_local.py +0 -0
  238. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/test_hooks.py +0 -0
  239. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/test_parent_run_id.py +0 -0
  240. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/test_primitives_from_steps.py +0 -0
  241. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/test_registry.py +0 -0
  242. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/test_replay.py +0 -0
  243. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/test_retention.py +0 -0
  244. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/test_schedule_schemas.py +0 -0
  245. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/test_schedule_utils.py +0 -0
  246. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/test_scheduled_workflow.py +0 -0
  247. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/test_signal.py +0 -0
  248. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/test_step.py +0 -0
  249. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/test_step_checkpoint.py +0 -0
  250. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/test_step_context.py +0 -0
  251. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/test_step_hook.py +0 -0
  252. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/test_stream_storage.py +0 -0
  253. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/test_stream_workflow.py +0 -0
  254. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/test_validation.py +0 -0
  255. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/test_workflow.py +0 -0
  256. {pyworkflow_engine-0.4.0 → pyworkflow_engine-0.4.1}/tests/unit/test_workflow_run_strategy.py +0 -0
  257. {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.0
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.0.0
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>=3.32.4
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.29.0; extra == "postgres"
41
+ Requires-Dist: asyncpg>=0.32.0; extra == "postgres"
42
42
  Provides-Extra: aws
43
- Requires-Dist: aws-durable-execution-sdk-python>=1.6.0; extra == "aws"
43
+ Requires-Dist: aws-durable-execution-sdk-python>=2.0.1; extra == "aws"
44
44
  Provides-Extra: dynamodb
45
- Requires-Dist: aiobotocore>=3.9.0; extra == "dynamodb"
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.29.0; extra == "all"
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>=1.6.0; extra == "all"
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.12.0; extra == "dev"
63
- Requires-Dist: moto[dynamodb]>=5.2.3; extra == "dev"
64
- Requires-Dist: black>=26.5.1; extra == "dev"
65
- Requires-Dist: ruff>=0.16.4; extra == "dev"
66
- Requires-Dist: mypy>=2.3.1; extra == "dev"
67
- Requires-Dist: pre-commit>=4.6.2; extra == "dev"
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.20260807; extra == "dev"
70
- Requires-Dist: types-PyYAML>=6.0.12.20260815; extra == "dev"
71
- Requires-Dist: flower>=2.1.0; extra == "dev"
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.29.0; extra == "dev"
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.0"
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.0.0,<3.0.0",
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>=3.32.4",
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.29.0",
62
+ "asyncpg>=0.32.0",
63
63
  ]
64
64
 
65
65
  # AWS runtime support
66
66
  aws = [
67
- "aws-durable-execution-sdk-python>=1.6.0",
67
+ "aws-durable-execution-sdk-python>=2.0.1",
68
68
  ]
69
69
  dynamodb = [
70
- "aiobotocore>=3.9.0",
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.29.0",
85
+ "asyncpg>=0.32.0",
86
86
  "aiomysql>=0.2.0",
87
- "aws-durable-execution-sdk-python>=1.6.0",
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.12.0",
99
- "moto[dynamodb]>=5.2.3", # AWS mocking for DynamoDB tests
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.5.1",
103
- "ruff>=0.16.4",
104
- "mypy>=2.3.1",
105
- "pre-commit>=4.6.2",
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.20260807",
110
- "types-PyYAML>=6.0.12.20260815",
109
+ "types-python-dateutil>=2.9.0.20261021",
110
+ "types-PyYAML>=6.0.12.20260906",
111
111
 
112
112
  # Monitoring (optional)
113
- "flower>=2.1.0", # Celery web UI
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.29.0",
118
+ "asyncpg>=0.32.0",
119
119
  ]
120
120
 
121
121
  [project.urls]
@@ -29,7 +29,7 @@ Quick Start:
29
29
  >>> run_id = await start(my_workflow, "Alice")
30
30
  """
31
31
 
32
- __version__ = "0.4.0"
32
+ __version__ = "0.4.1"
33
33
 
34
34
  # Configuration
35
35
  from pyworkflow.config import (
@@ -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: Re-enqueue in-flight tasks immediately when a worker is shut down
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. Implemented as a consumer bootstep; see pyworkflow/celery/reschedule.py.
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
- @task_prerun.connect
577
- def on_task_prerun(task_id=None, task=None, args=None, kwargs=None, **_):
578
+ @worker_ready.connect
579
+ def on_worker_ready(sender=None, **_):
578
580
  """
579
- Track the task now executing in this child process so it can be re-enqueued
580
- if the worker is shut down (SIGTERM) before the task finishes.
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 track_task_start
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
- with contextlib.suppress(Exception):
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