pydocket 0.26.1__tar.gz → 0.26.2__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 (214) hide show
  1. {pydocket-0.26.1 → pydocket-0.26.2}/.gitignore +2 -0
  2. {pydocket-0.26.1 → pydocket-0.26.2}/PKG-INFO +2 -2
  3. {pydocket-0.26.1 → pydocket-0.26.2}/README.md +1 -1
  4. {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/lua/acquire_or_park.lua +3 -1
  5. {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/lua/schedule.lua +19 -0
  6. {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/worker.py +19 -2
  7. {pydocket-0.26.1 → pydocket-0.26.2}/tests/_rotating_provider.py +4 -7
  8. pydocket-0.26.2/tests/concurrency_limits/test_parked_status.py +40 -0
  9. {pydocket-0.26.1 → pydocket-0.26.2}/tests/concurrency_limits/test_waiter_queue.py +11 -21
  10. pydocket-0.26.2/tests/concurrency_limits/waiters.py +21 -0
  11. {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_credential_rotation.py +45 -19
  12. {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_results_storage.py +1 -1
  13. pydocket-0.26.2/tests/test_reused_keys.py +113 -0
  14. pydocket-0.26.2/tests/worker/test_strikes_at_startup.py +68 -0
  15. {pydocket-0.26.1 → pydocket-0.26.2}/.coveragerc-cli +0 -0
  16. {pydocket-0.26.1 → pydocket-0.26.2}/.coveragerc-core +0 -0
  17. {pydocket-0.26.1 → pydocket-0.26.2}/.pre-commit-config.yaml +0 -0
  18. {pydocket-0.26.1 → pydocket-0.26.2}/LICENSE +0 -0
  19. {pydocket-0.26.1 → pydocket-0.26.2}/pyproject.toml +0 -0
  20. {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/__init__.py +0 -0
  21. {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/__main__.py +0 -0
  22. {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/_cancellation.py +0 -0
  23. {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/_docket_snapshot.py +0 -0
  24. {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/_execution_progress.py +0 -0
  25. {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/_execution_scripts.py +0 -0
  26. {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/_lua.py +0 -0
  27. {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/_prometheus_exporter.py +0 -0
  28. {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/_redelivery.py +0 -0
  29. {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/_redis.py +0 -0
  30. {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/_redis_credentials.py +0 -0
  31. {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/_redis_memory.py +0 -0
  32. {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/_redis_sentinel.py +0 -0
  33. {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/_result_store.py +0 -0
  34. {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/_telemetry.py +0 -0
  35. {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/_uuid7.py +0 -0
  36. {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/agenda.py +0 -0
  37. {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/annotations.py +0 -0
  38. {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/cli/__init__.py +0 -0
  39. {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/cli/_support.py +0 -0
  40. {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/dependencies/__init__.py +0 -0
  41. {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/dependencies/_base.py +0 -0
  42. {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/dependencies/_concurrency.py +0 -0
  43. {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/dependencies/_contextual.py +0 -0
  44. {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/dependencies/_cooldown.py +0 -0
  45. {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/dependencies/_cron.py +0 -0
  46. {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/dependencies/_debounce.py +0 -0
  47. {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/dependencies/_functional.py +0 -0
  48. {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/dependencies/_perpetual.py +0 -0
  49. {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/dependencies/_progress.py +0 -0
  50. {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/dependencies/_ratelimit.py +0 -0
  51. {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/dependencies/_resolution.py +0 -0
  52. {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/dependencies/_retry.py +0 -0
  53. {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/dependencies/_timeout.py +0 -0
  54. {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/docket.py +0 -0
  55. {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/execution.py +0 -0
  56. {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/instrumentation.py +0 -0
  57. {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/lua/cancel_cleanup.lua +0 -0
  58. {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/lua/cancel_task.lua +0 -0
  59. {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/lua/claim.lua +0 -0
  60. {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/lua/debounce.lua +0 -0
  61. {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/lua/progress_write.lua +0 -0
  62. {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/lua/ratelimit.lua +0 -0
  63. {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/lua/refresh_lease.lua +0 -0
  64. {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/lua/release_and_wake.lua +0 -0
  65. {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/lua/scavenge_and_wake.lua +0 -0
  66. {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/lua/stream_due_tasks.lua +0 -0
  67. {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/lua/terminal.lua +0 -0
  68. {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/py.typed +0 -0
  69. {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/strikelist.py +0 -0
  70. {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/tasks.py +0 -0
  71. {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/testing.py +0 -0
  72. {pydocket-0.26.1 → pydocket-0.26.2}/tests/__init__.py +0 -0
  73. {pydocket-0.26.1 → pydocket-0.26.2}/tests/_container.py +0 -0
  74. {pydocket-0.26.1 → pydocket-0.26.2}/tests/_key_leak_checker.py +0 -0
  75. {pydocket-0.26.1 → pydocket-0.26.2}/tests/cli/__init__.py +0 -0
  76. {pydocket-0.26.1 → pydocket-0.26.2}/tests/cli/run.py +0 -0
  77. {pydocket-0.26.1 → pydocket-0.26.2}/tests/cli/test_clear.py +0 -0
  78. {pydocket-0.26.1 → pydocket-0.26.2}/tests/cli/test_iterate_with_timeout.py +0 -0
  79. {pydocket-0.26.1 → pydocket-0.26.2}/tests/cli/test_module.py +0 -0
  80. {pydocket-0.26.1 → pydocket-0.26.2}/tests/cli/test_parsing.py +0 -0
  81. {pydocket-0.26.1 → pydocket-0.26.2}/tests/cli/test_snapshot.py +0 -0
  82. {pydocket-0.26.1 → pydocket-0.26.2}/tests/cli/test_striking.py +0 -0
  83. {pydocket-0.26.1 → pydocket-0.26.2}/tests/cli/test_tasks.py +0 -0
  84. {pydocket-0.26.1 → pydocket-0.26.2}/tests/cli/test_url_validation.py +0 -0
  85. {pydocket-0.26.1 → pydocket-0.26.2}/tests/cli/test_version.py +0 -0
  86. {pydocket-0.26.1 → pydocket-0.26.2}/tests/cli/test_watch.py +0 -0
  87. {pydocket-0.26.1 → pydocket-0.26.2}/tests/cli/test_worker.py +0 -0
  88. {pydocket-0.26.1 → pydocket-0.26.2}/tests/cli/test_workers.py +0 -0
  89. {pydocket-0.26.1 → pydocket-0.26.2}/tests/cli/waiting.py +0 -0
  90. {pydocket-0.26.1 → pydocket-0.26.2}/tests/cluster/Dockerfile +0 -0
  91. {pydocket-0.26.1 → pydocket-0.26.2}/tests/cluster/entrypoint.sh +0 -0
  92. {pydocket-0.26.1 → pydocket-0.26.2}/tests/cluster/start-cluster.sh +0 -0
  93. {pydocket-0.26.1 → pydocket-0.26.2}/tests/concurrency_limits/__init__.py +0 -0
  94. {pydocket-0.26.1 → pydocket-0.26.2}/tests/concurrency_limits/overlap.py +0 -0
  95. {pydocket-0.26.1 → pydocket-0.26.2}/tests/concurrency_limits/test_annotated.py +0 -0
  96. {pydocket-0.26.1 → pydocket-0.26.2}/tests/concurrency_limits/test_basic.py +0 -0
  97. {pydocket-0.26.1 → pydocket-0.26.2}/tests/concurrency_limits/test_errors_and_resilience.py +0 -0
  98. {pydocket-0.26.1 → pydocket-0.26.2}/tests/concurrency_limits/test_execution_patterns.py +0 -0
  99. {pydocket-0.26.1 → pydocket-0.26.2}/tests/concurrency_limits/test_redelivery.py +0 -0
  100. {pydocket-0.26.1 → pydocket-0.26.2}/tests/concurrency_limits/test_waiter_safeguard.py +0 -0
  101. {pydocket-0.26.1 → pydocket-0.26.2}/tests/concurrency_limits/test_worker_mechanics.py +0 -0
  102. {pydocket-0.26.1 → pydocket-0.26.2}/tests/conftest.py +0 -0
  103. {pydocket-0.26.1 → pydocket-0.26.2}/tests/fundamentals/AGENTS.md +0 -0
  104. {pydocket-0.26.1 → pydocket-0.26.2}/tests/fundamentals/CLAUDE.md +0 -0
  105. {pydocket-0.26.1 → pydocket-0.26.2}/tests/fundamentals/conftest.py +0 -0
  106. {pydocket-0.26.1 → pydocket-0.26.2}/tests/fundamentals/test_async_dependencies.py +0 -0
  107. {pydocket-0.26.1 → pydocket-0.26.2}/tests/fundamentals/test_builtin_tasks.py +0 -0
  108. {pydocket-0.26.1 → pydocket-0.26.2}/tests/fundamentals/test_call_arguments.py +0 -0
  109. {pydocket-0.26.1 → pydocket-0.26.2}/tests/fundamentals/test_cancellation.py +0 -0
  110. {pydocket-0.26.1 → pydocket-0.26.2}/tests/fundamentals/test_context_injection.py +0 -0
  111. {pydocket-0.26.1 → pydocket-0.26.2}/tests/fundamentals/test_cron.py +0 -0
  112. {pydocket-0.26.1 → pydocket-0.26.2}/tests/fundamentals/test_errors.py +0 -0
  113. {pydocket-0.26.1 → pydocket-0.26.2}/tests/fundamentals/test_idempotency.py +0 -0
  114. {pydocket-0.26.1 → pydocket-0.26.2}/tests/fundamentals/test_logging.py +0 -0
  115. {pydocket-0.26.1 → pydocket-0.26.2}/tests/fundamentals/test_perpetual.py +0 -0
  116. {pydocket-0.26.1 → pydocket-0.26.2}/tests/fundamentals/test_progress_state.py +0 -0
  117. {pydocket-0.26.1 → pydocket-0.26.2}/tests/fundamentals/test_results.py +0 -0
  118. {pydocket-0.26.1 → pydocket-0.26.2}/tests/fundamentals/test_retries.py +0 -0
  119. {pydocket-0.26.1 → pydocket-0.26.2}/tests/fundamentals/test_scheduling.py +0 -0
  120. {pydocket-0.26.1 → pydocket-0.26.2}/tests/fundamentals/test_self_perpetuation.py +0 -0
  121. {pydocket-0.26.1 → pydocket-0.26.2}/tests/fundamentals/test_shared_dependencies.py +0 -0
  122. {pydocket-0.26.1 → pydocket-0.26.2}/tests/fundamentals/test_striking.py +0 -0
  123. {pydocket-0.26.1 → pydocket-0.26.2}/tests/fundamentals/test_sync_dependencies.py +0 -0
  124. {pydocket-0.26.1 → pydocket-0.26.2}/tests/fundamentals/test_timeouts.py +0 -0
  125. {pydocket-0.26.1 → pydocket-0.26.2}/tests/instrumentation/test_counters.py +0 -0
  126. {pydocket-0.26.1 → pydocket-0.26.2}/tests/instrumentation/test_export.py +0 -0
  127. {pydocket-0.26.1 → pydocket-0.26.2}/tests/instrumentation/test_tracing.py +0 -0
  128. {pydocket-0.26.1 → pydocket-0.26.2}/tests/lua/calc.lua +0 -0
  129. {pydocket-0.26.1 → pydocket-0.26.2}/tests/lua/count_argv.lua +0 -0
  130. {pydocket-0.26.1 → pydocket-0.26.2}/tests/lua/echo_bool.lua +0 -0
  131. {pydocket-0.26.1 → pydocket-0.26.2}/tests/lua/echo_described_key.lua +0 -0
  132. {pydocket-0.26.1 → pydocket-0.26.2}/tests/lua/echo_dict.lua +0 -0
  133. {pydocket-0.26.1 → pydocket-0.26.2}/tests/lua/echo_keys_and_args.lua +0 -0
  134. {pydocket-0.26.1 → pydocket-0.26.2}/tests/lua/echo_list.lua +0 -0
  135. {pydocket-0.26.1 → pydocket-0.26.2}/tests/lua/echo_numbers.lua +0 -0
  136. {pydocket-0.26.1 → pydocket-0.26.2}/tests/lua/echo_tuple.lua +0 -0
  137. {pydocket-0.26.1 → pydocket-0.26.2}/tests/lua/extra_binding.lua +0 -0
  138. {pydocket-0.26.1 → pydocket-0.26.2}/tests/lua/join_variadic.lua +0 -0
  139. {pydocket-0.26.1 → pydocket-0.26.2}/tests/lua/misdecoded_flag.lua +0 -0
  140. {pydocket-0.26.1 → pydocket-0.26.2}/tests/lua/noscript_echo.lua +0 -0
  141. {pydocket-0.26.1 → pydocket-0.26.2}/tests/sitecustomize.py +0 -0
  142. {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_agenda.py +0 -0
  143. {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_annotated_dependencies.py +0 -0
  144. {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_automatic_perpetual_recovery.py +0 -0
  145. {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_batch_scheduling.py +0 -0
  146. {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_call_arguments.py +0 -0
  147. {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_cancel_queued_task.py +0 -0
  148. {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_cancellation.py +0 -0
  149. {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_claim_supersedes.py +0 -0
  150. {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_cooldown.py +0 -0
  151. {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_credential_provider.py +0 -0
  152. {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_debounce.py +0 -0
  153. {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_dependencies_advanced.py +0 -0
  154. {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_dependencies_core.py +0 -0
  155. {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_dependency_uniqueness.py +0 -0
  156. {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_depends_typing.py +0 -0
  157. {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_docket_clear.py +0 -0
  158. {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_docket_execution.py +0 -0
  159. {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_docket_isolation.py +0 -0
  160. {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_docket_keys.py +0 -0
  161. {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_docket_registration.py +0 -0
  162. {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_docket_snapshot.py +0 -0
  163. {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_execution.py +0 -0
  164. {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_execution_state.py +0 -0
  165. {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_exponential_retry_overflow.py +0 -0
  166. {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_fallback_task.py +0 -0
  167. {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_get_result_cancellation.py +0 -0
  168. {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_handler_semantics.py +0 -0
  169. {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_immediate_retry_state.py +0 -0
  170. {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_interrupted_tasks.py +0 -0
  171. {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_key_leak_protection.py +0 -0
  172. {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_lease_renewal.py +0 -0
  173. {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_lua_decorator.py +0 -0
  174. {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_memory_backend.py +0 -0
  175. {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_payload_escaping.py +0 -0
  176. {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_perpetual_handoff.py +0 -0
  177. {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_perpetual_race.py +0 -0
  178. {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_perpetual_reliability.py +0 -0
  179. {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_perpetual_reschedule.py +0 -0
  180. {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_perpetual_state.py +0 -0
  181. {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_progress_basics.py +0 -0
  182. {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_progress_clobber.py +0 -0
  183. {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_progress_pubsub.py +0 -0
  184. {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_pubsub_resp_version.py +0 -0
  185. {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_ratelimit.py +0 -0
  186. {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_redelivery.py +0 -0
  187. {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_redelivery_sweep.py +0 -0
  188. {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_redis_connection.py +0 -0
  189. {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_reschedule_clears_worker.py +0 -0
  190. {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_results_retrieval.py +0 -0
  191. {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_sentinel_urls.py +0 -0
  192. {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_socket_timeout.py +0 -0
  193. {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_strikelist.py +0 -0
  194. {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_striking.py +0 -0
  195. {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_task_cycle_round_trips.py +0 -0
  196. {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_terminal_acked.py +0 -0
  197. {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_testing.py +0 -0
  198. {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_uncalled_for_contract.py +0 -0
  199. {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_uuid7.py +0 -0
  200. {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_worker_cancellation.py +0 -0
  201. {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_worker_dependencies.py +0 -0
  202. {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_worker_single_dependencies.py +0 -0
  203. {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_worker_task_deps_interaction.py +0 -0
  204. {pydocket-0.26.1 → pydocket-0.26.2}/tests/worker/test_bootstrap.py +0 -0
  205. {pydocket-0.26.1 → pydocket-0.26.2}/tests/worker/test_core.py +0 -0
  206. {pydocket-0.26.1 → pydocket-0.26.2}/tests/worker/test_crash.py +0 -0
  207. {pydocket-0.26.1 → pydocket-0.26.2}/tests/worker/test_delivery_batch.py +0 -0
  208. {pydocket-0.26.1 → pydocket-0.26.2}/tests/worker/test_invariants.py +0 -0
  209. {pydocket-0.26.1 → pydocket-0.26.2}/tests/worker/test_lifecycle.py +0 -0
  210. {pydocket-0.26.1 → pydocket-0.26.2}/tests/worker/test_liveness.py +0 -0
  211. {pydocket-0.26.1 → pydocket-0.26.2}/tests/worker/test_reconnection.py +0 -0
  212. {pydocket-0.26.1 → pydocket-0.26.2}/tests/worker/test_scheduling.py +0 -0
  213. {pydocket-0.26.1 → pydocket-0.26.2}/tests/worker/test_task_factory.py +0 -0
  214. {pydocket-0.26.1 → pydocket-0.26.2}/tests/worker/test_ttl_zero.py +0 -0
@@ -16,3 +16,5 @@ wheels/
16
16
  .claude/scheduled_tasks.lock
17
17
  coverage.xml
18
18
  junit.xml
19
+ target/
20
+ target-*/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pydocket
3
- Version: 0.26.1
3
+ Version: 0.26.2
4
4
  Summary: A distributed background task system for Python functions
5
5
  Project-URL: Homepage, https://docket.lol/
6
6
  Project-URL: Documentation, https://docket.lol/en/latest/
@@ -52,7 +52,7 @@ on the scheduling of future work as seamlessly and efficiently as immediate work
52
52
 
53
53
  [![PyPI - Version](https://img.shields.io/pypi/v/pydocket)](https://pypi.org/project/pydocket/)
54
54
  [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pydocket)](https://pypi.org/project/pydocket/)
55
- [![GitHub main checks](https://img.shields.io/github/check-runs/chrisguidry/docket/main)](https://github.com/chrisguidry/docket/actions/workflows/ci.yml)
55
+ [![GitHub main checks](https://img.shields.io/github/check-runs/chrisguidry/docket/main)](https://github.com/chrisguidry/docket/actions)
56
56
  [![Codecov](https://img.shields.io/codecov/c/github/chrisguidry/docket)](https://app.codecov.io/gh/chrisguidry/docket)
57
57
  [![PyPI - License](https://img.shields.io/pypi/l/pydocket)](https://github.com/chrisguidry/docket/blob/main/LICENSE)
58
58
  [![Documentation](https://img.shields.io/badge/docs-latest-blue.svg)](https://docket.lol/)
@@ -3,7 +3,7 @@ on the scheduling of future work as seamlessly and efficiently as immediate work
3
3
 
4
4
  [![PyPI - Version](https://img.shields.io/pypi/v/pydocket)](https://pypi.org/project/pydocket/)
5
5
  [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pydocket)](https://pypi.org/project/pydocket/)
6
- [![GitHub main checks](https://img.shields.io/github/check-runs/chrisguidry/docket/main)](https://github.com/chrisguidry/docket/actions/workflows/ci.yml)
6
+ [![GitHub main checks](https://img.shields.io/github/check-runs/chrisguidry/docket/main)](https://github.com/chrisguidry/docket/actions)
7
7
  [![Codecov](https://img.shields.io/codecov/c/github/chrisguidry/docket)](https://app.codecov.io/gh/chrisguidry/docket)
8
8
  [![PyPI - License](https://img.shields.io/pypi/l/pydocket)](https://github.com/chrisguidry/docket/blob/main/LICENSE)
9
9
  [![Documentation](https://img.shields.io/badge/docs-latest-blue.svg)](https://docket.lol/)
@@ -88,7 +88,9 @@ redis.call('HSET', runs_key,
88
88
  'args', args_data,
89
89
  'kwargs', kwargs_data
90
90
  )
91
- redis.call('HDEL', runs_key, 'stream_id')
91
+ -- The claim before admission wrote a worker and a start time, and a parked
92
+ -- task has neither until a worker claims it again.
93
+ redis.call('HDEL', runs_key, 'stream_id', 'worker', 'started_at')
92
94
 
93
95
  redis.call('PUBLISH', state_channel, state_payload)
94
96
 
@@ -154,12 +154,31 @@ else
154
154
  end
155
155
  end
156
156
 
157
+ -- A key used again after its run ended: terminal.lua gave that run's
158
+ -- record an expiry of execution_ttl, which the new run would inherit, so
159
+ -- the record could expire before a worker claims the new run (it is then
160
+ -- refused as superseded) or while it runs (it then can't be seen, and the
161
+ -- key no longer refuses a duplicate). The new run starts a record of its
162
+ -- own, keeping only the generation counter.
163
+ local previous_state = redis.call('HGET', runs_key, 'state')
164
+ if previous_state == 'completed' or previous_state == 'failed' or previous_state == 'cancelled' then
165
+ local previous_generation = redis.call('HGET', runs_key, 'generation')
166
+ redis.call('DEL', runs_key)
167
+ if previous_generation then
168
+ redis.call('HSET', runs_key, 'generation', previous_generation)
169
+ end
170
+ end
171
+
157
172
  -- Increment generation counter
158
173
  local new_gen = redis.call('HINCRBY', runs_key, 'generation', 1)
159
174
  if generation_index then
160
175
  message[generation_index] = tostring(new_gen)
161
176
  end
162
177
 
178
+ -- A new run starts without the previous run's ending, so a reused key
179
+ -- never shows the old worker, error, completion time, or result.
180
+ redis.call('HDEL', runs_key, 'worker', 'started_at', 'completed_at', 'error', 'result_key')
181
+
163
182
  if is_immediate then
164
183
  -- Add to stream for immediate execution
165
184
  local message_id = redis.call('XADD', stream_key, '*', unpack(message))
@@ -755,6 +755,20 @@ class Worker:
755
755
  await _wait_for_event(session.cancellation_ready, 0.1)
756
756
  if stopping.is_set():
757
757
  return
758
+ # A strike written before the worker started must stop
759
+ # its tasks, so nothing runs until the strike stream is
760
+ # read. That takes as long as Redis is unreachable, so
761
+ # a stopping worker does not wait for it.
762
+ strikes_loaded = asyncio.ensure_future(
763
+ self.docket.wait_for_strikes_loaded()
764
+ )
765
+ try:
766
+ while not strikes_loaded.done() and not stopping.is_set():
767
+ await asyncio.wait([strikes_loaded], timeout=0.1)
768
+ finally:
769
+ strikes_loaded.cancel()
770
+ if stopping.is_set():
771
+ return
758
772
  if self.schedule_automatic_tasks:
759
773
  try:
760
774
  await self._schedule_all_automatic_perpetual_tasks()
@@ -1137,7 +1151,10 @@ class Worker:
1137
1151
  encoded_result = base64.b64encode(pickled_result).decode(
1138
1152
  "ascii"
1139
1153
  )
1140
- result_key = execution.key
1154
+ # Each run stores under its own generation, so a replaced run
1155
+ # that finishes last writes where nobody reads: terminal.lua
1156
+ # leaves a superseded run's result_key out of the run state.
1157
+ result_key = f"{execution.key}:{execution.generation}"
1141
1158
  ttl_seconds = int(self.docket.execution_ttl.total_seconds())
1142
1159
  await self.docket.result_storage.put(
1143
1160
  result_key, {"data": encoded_result}, ttl=ttl_seconds
@@ -1225,7 +1242,7 @@ class Worker:
1225
1242
  encoded_exception = base64.b64encode(pickled_exception).decode(
1226
1243
  "ascii"
1227
1244
  )
1228
- result_key = execution.key
1245
+ result_key = f"{execution.key}:{execution.generation}"
1229
1246
  ttl_seconds = int(self.docket.execution_ttl.total_seconds())
1230
1247
  await self.docket.result_storage.put(
1231
1248
  result_key, {"data": encoded_exception}, ttl=ttl_seconds
@@ -8,7 +8,6 @@ connections get, and pushes them to that one callback as an AUTH token whose
8
8
  "oid" claim is the username.
9
9
  """
10
10
 
11
- import inspect
12
11
  from typing import Any, Callable
13
12
 
14
13
  from redis.auth.token import SimpleToken
@@ -20,9 +19,8 @@ class RotatingProvider(StreamingCredentialProvider):
20
19
  self.credentials = (username, password)
21
20
  self._on_next: Callable[[Any], Any] | None = None
22
21
 
23
- def get_credentials(self) -> tuple[str, str]:
24
- return self.credentials
25
-
22
+ # Only the async method: docket's clients are all asyncio, so redis-py
23
+ # never asks for the credentials synchronously.
26
24
  async def get_credentials_async(self) -> tuple[str, str]:
27
25
  return self.credentials
28
26
 
@@ -40,6 +38,5 @@ class RotatingProvider(StreamingCredentialProvider):
40
38
  if self._on_next is None:
41
39
  return
42
40
  token = SimpleToken(password, -1, -1, {"oid": username})
43
- result = self._on_next(token)
44
- if inspect.isawaitable(result):
45
- await result
41
+ # redis-py's asyncio clients register a coroutine function.
42
+ await self._on_next(token)
@@ -0,0 +1,40 @@
1
+ """A task parked on a concurrency limit shows no worker and no start time."""
2
+
3
+ import asyncio
4
+
5
+ from docket import ConcurrencyLimit, Docket, Worker
6
+ from docket.execution import ExecutionState
7
+
8
+ from tests.concurrency_limits.waiters import wait_for_xlen
9
+
10
+
11
+ async def test_a_parked_task_has_no_worker_or_start_time(
12
+ docket: Docket, worker: Worker
13
+ ):
14
+ started = asyncio.Event()
15
+ hold = asyncio.Event()
16
+
17
+ async def holder(
18
+ customer_id: int,
19
+ concurrency: ConcurrencyLimit = ConcurrencyLimit(
20
+ "customer_id", max_concurrent=1
21
+ ),
22
+ ):
23
+ started.set()
24
+ await hold.wait()
25
+
26
+ # The waiter goes in only after the holder has its slot, because the
27
+ # worker may start either of two tasks it reads together.
28
+ await docket.add(holder)(customer_id=1)
29
+ worker_task = asyncio.create_task(worker.run_until_finished())
30
+ await started.wait()
31
+ waiter = await docket.add(holder)(customer_id=1)
32
+ await wait_for_xlen(docket, f"{docket.prefix}:concurrency:customer_id:1:waiters", 1)
33
+
34
+ await waiter.sync()
35
+ parked = (waiter.state, waiter.worker, waiter.started_at)
36
+
37
+ hold.set()
38
+ await worker_task
39
+
40
+ assert parked == (ExecutionState.SCHEDULED, None, None)
@@ -4,7 +4,6 @@ frees up, rather than polling via the future queue).
4
4
  """
5
5
 
6
6
  import asyncio
7
- import time
8
7
  from datetime import datetime, timezone
9
8
 
10
9
  from docket import (
@@ -13,18 +12,7 @@ from docket import (
13
12
  Worker,
14
13
  )
15
14
 
16
-
17
- async def _wait_for_xlen(docket: Docket, key: str, target: int) -> None:
18
- deadline = time.monotonic() + 2.0
19
- while time.monotonic() < deadline:
20
- async with docket.redis() as redis:
21
- size = await redis.xlen(key)
22
- if size == target:
23
- return
24
- await asyncio.sleep(0.01)
25
- raise AssertionError( # pragma: no cover
26
- f"XLEN({key}) did not reach {target} in time"
27
- )
15
+ from tests.concurrency_limits.waiters import wait_for_xlen
28
16
 
29
17
 
30
18
  async def test_blocked_task_parks_on_waiter_stream(docket: Docket, worker: Worker):
@@ -51,7 +39,7 @@ async def test_blocked_task_parks_on_waiter_stream(docket: Docket, worker: Worke
51
39
  await started.wait()
52
40
 
53
41
  waiters_stream = f"{docket.prefix}:concurrency:customer_id:1:waiters"
54
- await _wait_for_xlen(docket, waiters_stream, 1)
42
+ await wait_for_xlen(docket, waiters_stream, 1)
55
43
 
56
44
  async with docket.redis() as redis:
57
45
  entries = await redis.xrange(waiters_stream, "-", "+")
@@ -95,7 +83,7 @@ async def test_release_wakes_oldest_waiter_first(docket: Docket, worker: Worker)
95
83
  waiters_stream = f"{docket.prefix}:concurrency:customer_id:1:waiters"
96
84
  for tid in (1, 2, 3):
97
85
  await docket.add(task)(customer_id=1, task_id=tid)
98
- await _wait_for_xlen(docket, waiters_stream, tid)
86
+ await wait_for_xlen(docket, waiters_stream, tid)
99
87
 
100
88
  release_holder.set()
101
89
  await worker_task
@@ -146,7 +134,7 @@ async def test_blocked_task_makes_no_polling_retries(docket: Docket, worker: Wor
146
134
  # Wait for the blocked task to actually park on the waiter stream
147
135
  # before releasing the holder.
148
136
  waiters_stream = f"{docket.prefix}:concurrency:customer_id:1:waiters"
149
- await _wait_for_xlen(docket, waiters_stream, 1)
137
+ await wait_for_xlen(docket, waiters_stream, 1)
150
138
 
151
139
  release_holder.set()
152
140
  await worker_task
@@ -198,7 +186,7 @@ async def test_safeguard_wakes_waiter_when_holder_died_without_releasing(
198
186
  runner = asyncio.create_task(worker.run_forever())
199
187
 
200
188
  # Waiter parks because the (live-looking) fake slot is full.
201
- await _wait_for_xlen(docket, waiters_stream, 1)
189
+ await wait_for_xlen(docket, waiters_stream, 1)
202
190
 
203
191
  # Now forge a stale-holder state. No lease renewer is touching
204
192
  # this slot, so the timestamp stays at 0 until the safeguard
@@ -262,7 +250,7 @@ async def test_cancel_of_parked_task_prevents_wake_and_run(
262
250
 
263
251
  # Make sure `blocked` is actually parked on the waiter stream
264
252
  waiters_stream = f"{docket.prefix}:concurrency:customer_id:1:waiters"
265
- await _wait_for_xlen(docket, waiters_stream, 1)
253
+ await wait_for_xlen(docket, waiters_stream, 1)
266
254
 
267
255
  # Cancel the parked task; release the holder; verify blocked never ran
268
256
  await docket.cancel(blocked_exec.key)
@@ -310,10 +298,10 @@ async def test_cancel_of_parked_task_removes_waiter_entry_before_release(
310
298
 
311
299
  parked = await docket.add(holder)(customer_id=1)
312
300
  waiters_stream = f"{docket.prefix}:concurrency:customer_id:1:waiters"
313
- await _wait_for_xlen(docket, waiters_stream, 1)
301
+ await wait_for_xlen(docket, waiters_stream, 1)
314
302
 
315
303
  await docket.cancel(parked.key)
316
- await _wait_for_xlen(docket, waiters_stream, 0)
304
+ await wait_for_xlen(docket, waiters_stream, 0)
317
305
 
318
306
  release_holder.set()
319
307
  await asyncio.wait_for(worker_task, timeout=5)
@@ -326,7 +314,9 @@ async def test_cancel_cleanup_script_drains_waiter_entry(docket: Docket):
326
314
  ``ConcurrencyLimit._cleanup_cancelled_waiter`` when a parked task is
327
315
  cancelled. Driving the script directly tests it without a worker.
328
316
  """
329
- from docket.dependencies._concurrency import _cancel_cleanup # pyright: ignore[reportPrivateUsage]
317
+ from docket.dependencies._concurrency import (
318
+ _cancel_cleanup, # pyright: ignore[reportPrivateUsage]
319
+ )
330
320
 
331
321
  waiter_stream = f"{docket.prefix}:concurrency:cleanup-direct:waiters"
332
322
  task_key = "cleanup-direct"
@@ -0,0 +1,21 @@
1
+ """Waits on the waiter streams that concurrency limits park tasks on."""
2
+
3
+ import asyncio
4
+
5
+ from docket import Docket
6
+
7
+
8
+ async def wait_for_xlen(docket: Docket, key: str, target: int) -> None:
9
+ # Sleeping before each read, not after a failed one, runs every line on
10
+ # every call, so coverage does not depend on how fast the stream fills.
11
+ # The loop-again branch happens only when the stream is behind on a read,
12
+ # so it is excluded from branch coverage.
13
+ # asyncio.timeout needs Python 3.11, so wait_for bounds the loop instead.
14
+ async def poll() -> None:
15
+ while True:
16
+ await asyncio.sleep(0.01)
17
+ async with docket.redis() as redis:
18
+ if await redis.xlen(key) == target: # pragma: no branch
19
+ return
20
+
21
+ await asyncio.wait_for(poll(), timeout=2.0)
@@ -9,7 +9,7 @@ which user each connection is authenticated as.
9
9
 
10
10
  import asyncio
11
11
  import gc
12
- from contextlib import asynccontextmanager
12
+ from contextlib import asynccontextmanager, suppress
13
13
  from datetime import timedelta
14
14
  from typing import Any, AsyncGenerator, Callable, NamedTuple
15
15
  from uuid import uuid4
@@ -147,10 +147,11 @@ async def reading(pubsub: Any) -> AsyncGenerator[list[bytes], None]:
147
147
  try:
148
148
  yield received
149
149
  finally:
150
- if reader.done():
151
- reader.result() # the reader's own error, if it failed
150
+ # Cancelling a reader that already failed does nothing, so awaiting it
151
+ # raises the reader's own error.
152
152
  reader.cancel()
153
- await asyncio.gather(reader, return_exceptions=True)
153
+ with suppress(asyncio.CancelledError):
154
+ await reader
154
155
 
155
156
 
156
157
  async def test_a_rotation_leaves_a_reading_subscriber_alone(
@@ -160,23 +161,43 @@ async def test_a_rotation_leaves_a_reading_subscriber_alone(
160
161
  rotation leaves it alone: one connection, still getting messages."""
161
162
  old, new = users
162
163
  provider = RotatingProvider(*old)
163
- async with RedisConnection(credential_less_url, provider) as connection:
164
- async with connection.pubsub() as pubsub:
165
- await pubsub.subscribe("rotation")
166
- async with reading(pubsub) as received:
167
- await wait_until(
168
- lambda: subscribed_as(admin, users, old),
169
- description="the subscriber to authenticate as the old user",
170
- )
164
+ # One statement for both, because coverage.py's sys.monitoring core reports
165
+ # a false partial branch on the innermost of three nested with statements.
166
+ async with (
167
+ RedisConnection(credential_less_url, provider) as connection,
168
+ connection.pubsub() as pubsub,
169
+ ):
170
+ await pubsub.subscribe("rotation")
171
+ async with reading(pubsub) as received:
172
+ await wait_until(
173
+ lambda: subscribed_as(admin, users, old),
174
+ description="the subscriber to authenticate as the old user",
175
+ )
171
176
 
172
- await provider.rotate(*new)
173
- await admin.publish("rotation", "after") # type: ignore[reportUnknownMemberType]
174
- await wait_until(
175
- lambda: b"after" in received,
176
- description="a message published after the rotation",
177
- )
177
+ await provider.rotate(*new)
178
+ await admin.publish("rotation", "after") # type: ignore[reportUnknownMemberType]
179
+ await wait_until(
180
+ lambda: b"after" in received,
181
+ description="a message published after the rotation",
182
+ )
178
183
 
179
- assert await subscribers(admin, users) == [old.name]
184
+ assert await subscribers(admin, users) == [old.name]
185
+
186
+
187
+ async def test_a_rotation_before_connecting_reaches_the_first_connection(
188
+ credential_less_url: str, admin: Redis, users: tuple[User, User]
189
+ ):
190
+ """A token refresh can land before docket opens any connection, and the
191
+ first connection must authenticate with the refreshed token."""
192
+ old, new = users
193
+ provider = RotatingProvider(*old)
194
+
195
+ await provider.rotate(*new)
196
+
197
+ async with RedisConnection(credential_less_url, provider) as connection:
198
+ async with connection.client() as r:
199
+ await r.exists("rotation")
200
+ assert await authenticated_as(admin, users, subscribed=False) == {new.name}
180
201
 
181
202
 
182
203
  async def test_a_rotation_after_close_opens_no_connections(
@@ -237,3 +258,8 @@ async def test_a_worker_keeps_working_when_the_old_user_is_revoked(
237
258
  after = await docket.add(add)(3, 4)
238
259
  await worker.run_until_finished()
239
260
  assert await after.get_result() == 7
261
+
262
+
263
+ def test_the_provider_streams():
264
+ """It says it streams, as redis-entraid's provider does."""
265
+ assert RotatingProvider("user", "secret").is_streaming()
@@ -189,7 +189,7 @@ async def test_result_key_stored_in_execution_record(docket: Docket, worker: Wor
189
189
 
190
190
  # Sync and check result field
191
191
  await execution.sync()
192
- assert execution.result_key == execution.key
192
+ assert execution.result_key == f"{execution.key}:1"
193
193
 
194
194
 
195
195
  async def test_result_storage_uses_provided_or_default(docket: Docket):
@@ -0,0 +1,113 @@
1
+ """A key used again, or replaced while it runs, shows only its current run:
2
+ its result, its error, and its ending."""
3
+
4
+ import asyncio
5
+ from datetime import datetime, timedelta, timezone
6
+ from typing import Callable
7
+
8
+ from docket import Docket, Worker
9
+ from docket.execution import ExecutionState
10
+
11
+
12
+ async def test_a_replaced_run_that_finishes_last_keeps_its_successors_result(
13
+ docket: Docket, worker: Worker
14
+ ):
15
+ started = asyncio.Event()
16
+ release = asyncio.Event()
17
+
18
+ async def echo(text: str) -> str:
19
+ if text == "old":
20
+ started.set()
21
+ await release.wait()
22
+ return text
23
+
24
+ docket.register(echo)
25
+ await docket.add(echo, key="swapped")("old")
26
+ running = asyncio.create_task(worker.run_until_finished())
27
+ await started.wait()
28
+
29
+ replacement = await docket.replace(
30
+ echo, when=datetime.now(timezone.utc), key="swapped"
31
+ )("new")
32
+ while replacement.state != ExecutionState.COMPLETED:
33
+ await asyncio.sleep(0.02)
34
+ await replacement.sync()
35
+ release.set()
36
+ await running
37
+
38
+ assert await replacement.get_result() == "new"
39
+
40
+
41
+ async def test_a_reused_key_does_not_return_the_previous_runs_result(
42
+ docket: Docket, worker: Worker
43
+ ):
44
+ async def maybe(text: str | None) -> str | None:
45
+ return text
46
+
47
+ docket.register(maybe)
48
+ first = await docket.add(maybe, key="reused")("first")
49
+ await worker.run_until_finished()
50
+ assert await first.get_result() == "first"
51
+
52
+ second = await docket.add(maybe, key="reused")(None)
53
+ await worker.run_until_finished()
54
+
55
+ assert await second.get_result() is None
56
+
57
+
58
+ async def test_a_reused_key_does_not_show_the_previous_runs_ending(
59
+ docket: Docket, worker: Worker
60
+ ):
61
+ async def echo(text: str) -> str:
62
+ if text == "fail":
63
+ raise ValueError("the first run failed")
64
+ return text
65
+
66
+ docket.register(echo)
67
+ await docket.add(echo, key="again")("fail")
68
+ await worker.run_until_finished()
69
+
70
+ later = datetime.now(timezone.utc) + timedelta(minutes=1)
71
+ second = await docket.add(echo, when=later, key="again")("fine")
72
+ await second.sync()
73
+ assert (second.state, second.error, second.completed_at, second.worker) == (
74
+ ExecutionState.SCHEDULED,
75
+ None,
76
+ None,
77
+ None,
78
+ )
79
+
80
+ await docket.replace(echo, when=datetime.now(timezone.utc), key="again")("fine")
81
+ await worker.run_until_finished()
82
+ await second.sync()
83
+ assert (second.state, second.error) == (ExecutionState.COMPLETED, None)
84
+
85
+
86
+ async def test_a_key_used_again_soon_after_its_run_ended_keeps_its_record(
87
+ redis_url: str, make_docket_name: Callable[[], str]
88
+ ):
89
+ """An ended run's record expires execution_ttl after the ending. A run added
90
+ under the same key before then has a record of its own: it waits to be claimed
91
+ and runs, however long it waits."""
92
+ ran: list[int] = []
93
+
94
+ async def count(n: int) -> None:
95
+ ran.append(n)
96
+
97
+ async with Docket(
98
+ name=make_docket_name(), url=redis_url, execution_ttl=timedelta(seconds=1)
99
+ ) as docket:
100
+ docket.register(count)
101
+ await docket.add(count, key="again")(1)
102
+ async with Worker(docket) as worker:
103
+ await worker.run_until_finished()
104
+
105
+ second = await docket.add(count, key="again")(2)
106
+ await asyncio.sleep(1.5) # the ended run's record would have expired by now
107
+ await second.sync()
108
+ assert second.state == ExecutionState.QUEUED
109
+
110
+ async with Worker(docket) as worker:
111
+ await worker.run_until_finished()
112
+
113
+ assert ran == [1, 2]
@@ -0,0 +1,68 @@
1
+ """A worker reads the strikes that exist when it starts before it runs
2
+ anything, so a strike written earlier is never missed."""
3
+
4
+ import asyncio
5
+ from datetime import timedelta
6
+ from typing import Any
7
+ from unittest.mock import AsyncMock
8
+
9
+ import pytest
10
+
11
+ from docket import Docket, Worker
12
+ from docket.execution import ExecutionState
13
+ from docket.strikelist import StrikeList
14
+
15
+
16
+ def block_strike_reads(monkeypatch: pytest.MonkeyPatch) -> asyncio.Event:
17
+ """Holds every strike monitor's reads until the returned event is set."""
18
+ loading = asyncio.Event()
19
+ read_strikes = StrikeList._read_strikes # pyright: ignore[reportPrivateUsage]
20
+
21
+ async def slow_read_strikes(self: StrikeList, *args: Any) -> Any:
22
+ await loading.wait()
23
+ return await read_strikes(self, *args)
24
+
25
+ monkeypatch.setattr(StrikeList, "_read_strikes", slow_read_strikes)
26
+ return loading
27
+
28
+
29
+ async def test_a_worker_waits_for_the_strikes_before_it_runs_anything(
30
+ docket: Docket, the_task: AsyncMock, monkeypatch: pytest.MonkeyPatch
31
+ ):
32
+ docket.register(the_task)
33
+ execution = await docket.add(the_task)()
34
+ await docket.strike(the_task)
35
+
36
+ # A second connection to the docket, whose strike monitor cannot read
37
+ # the stream until the test lets it, the way it can be slow after a
38
+ # restart.
39
+ loading = block_strike_reads(monkeypatch)
40
+
41
+ async with Docket(name=docket.name, url=docket.url) as second:
42
+ second.register(the_task)
43
+ async with Worker(
44
+ second,
45
+ schedule_automatic_tasks=False,
46
+ minimum_check_interval=timedelta(milliseconds=5),
47
+ ) as worker:
48
+ running = asyncio.create_task(worker.run_until_finished())
49
+ await asyncio.sleep(0.5)
50
+ loading.set()
51
+ await running
52
+
53
+ the_task.assert_not_awaited()
54
+ await execution.sync()
55
+ assert execution.state == ExecutionState.CANCELLED
56
+
57
+
58
+ async def test_a_worker_stops_while_it_waits_for_the_strikes(
59
+ docket: Docket, monkeypatch: pytest.MonkeyPatch
60
+ ):
61
+ block_strike_reads(monkeypatch)
62
+
63
+ async with Docket(name=docket.name, url=docket.url) as second:
64
+ async with Worker(second, schedule_automatic_tasks=False) as worker:
65
+ running = asyncio.create_task(worker.run_forever())
66
+ await asyncio.sleep(0.2)
67
+ # Leaving the worker stops it, though the strikes never loaded.
68
+ await asyncio.wait_for(running, timeout=5)
File without changes
File without changes
File without changes
File without changes
File without changes