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.
- {pydocket-0.26.1 → pydocket-0.26.2}/.gitignore +2 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/PKG-INFO +2 -2
- {pydocket-0.26.1 → pydocket-0.26.2}/README.md +1 -1
- {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/lua/acquire_or_park.lua +3 -1
- {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/lua/schedule.lua +19 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/worker.py +19 -2
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/_rotating_provider.py +4 -7
- pydocket-0.26.2/tests/concurrency_limits/test_parked_status.py +40 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/concurrency_limits/test_waiter_queue.py +11 -21
- pydocket-0.26.2/tests/concurrency_limits/waiters.py +21 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_credential_rotation.py +45 -19
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_results_storage.py +1 -1
- pydocket-0.26.2/tests/test_reused_keys.py +113 -0
- pydocket-0.26.2/tests/worker/test_strikes_at_startup.py +68 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/.coveragerc-cli +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/.coveragerc-core +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/.pre-commit-config.yaml +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/LICENSE +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/pyproject.toml +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/__init__.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/__main__.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/_cancellation.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/_docket_snapshot.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/_execution_progress.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/_execution_scripts.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/_lua.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/_prometheus_exporter.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/_redelivery.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/_redis.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/_redis_credentials.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/_redis_memory.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/_redis_sentinel.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/_result_store.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/_telemetry.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/_uuid7.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/agenda.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/annotations.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/cli/__init__.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/cli/_support.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/dependencies/__init__.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/dependencies/_base.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/dependencies/_concurrency.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/dependencies/_contextual.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/dependencies/_cooldown.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/dependencies/_cron.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/dependencies/_debounce.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/dependencies/_functional.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/dependencies/_perpetual.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/dependencies/_progress.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/dependencies/_ratelimit.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/dependencies/_resolution.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/dependencies/_retry.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/dependencies/_timeout.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/docket.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/execution.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/instrumentation.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/lua/cancel_cleanup.lua +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/lua/cancel_task.lua +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/lua/claim.lua +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/lua/debounce.lua +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/lua/progress_write.lua +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/lua/ratelimit.lua +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/lua/refresh_lease.lua +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/lua/release_and_wake.lua +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/lua/scavenge_and_wake.lua +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/lua/stream_due_tasks.lua +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/lua/terminal.lua +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/py.typed +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/strikelist.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/tasks.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/src/docket/testing.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/__init__.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/_container.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/_key_leak_checker.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/cli/__init__.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/cli/run.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/cli/test_clear.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/cli/test_iterate_with_timeout.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/cli/test_module.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/cli/test_parsing.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/cli/test_snapshot.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/cli/test_striking.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/cli/test_tasks.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/cli/test_url_validation.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/cli/test_version.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/cli/test_watch.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/cli/test_worker.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/cli/test_workers.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/cli/waiting.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/cluster/Dockerfile +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/cluster/entrypoint.sh +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/cluster/start-cluster.sh +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/concurrency_limits/__init__.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/concurrency_limits/overlap.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/concurrency_limits/test_annotated.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/concurrency_limits/test_basic.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/concurrency_limits/test_errors_and_resilience.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/concurrency_limits/test_execution_patterns.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/concurrency_limits/test_redelivery.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/concurrency_limits/test_waiter_safeguard.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/concurrency_limits/test_worker_mechanics.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/conftest.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/fundamentals/AGENTS.md +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/fundamentals/CLAUDE.md +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/fundamentals/conftest.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/fundamentals/test_async_dependencies.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/fundamentals/test_builtin_tasks.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/fundamentals/test_call_arguments.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/fundamentals/test_cancellation.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/fundamentals/test_context_injection.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/fundamentals/test_cron.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/fundamentals/test_errors.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/fundamentals/test_idempotency.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/fundamentals/test_logging.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/fundamentals/test_perpetual.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/fundamentals/test_progress_state.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/fundamentals/test_results.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/fundamentals/test_retries.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/fundamentals/test_scheduling.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/fundamentals/test_self_perpetuation.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/fundamentals/test_shared_dependencies.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/fundamentals/test_striking.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/fundamentals/test_sync_dependencies.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/fundamentals/test_timeouts.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/instrumentation/test_counters.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/instrumentation/test_export.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/instrumentation/test_tracing.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/lua/calc.lua +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/lua/count_argv.lua +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/lua/echo_bool.lua +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/lua/echo_described_key.lua +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/lua/echo_dict.lua +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/lua/echo_keys_and_args.lua +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/lua/echo_list.lua +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/lua/echo_numbers.lua +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/lua/echo_tuple.lua +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/lua/extra_binding.lua +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/lua/join_variadic.lua +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/lua/misdecoded_flag.lua +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/lua/noscript_echo.lua +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/sitecustomize.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_agenda.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_annotated_dependencies.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_automatic_perpetual_recovery.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_batch_scheduling.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_call_arguments.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_cancel_queued_task.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_cancellation.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_claim_supersedes.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_cooldown.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_credential_provider.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_debounce.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_dependencies_advanced.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_dependencies_core.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_dependency_uniqueness.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_depends_typing.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_docket_clear.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_docket_execution.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_docket_isolation.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_docket_keys.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_docket_registration.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_docket_snapshot.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_execution.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_execution_state.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_exponential_retry_overflow.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_fallback_task.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_get_result_cancellation.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_handler_semantics.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_immediate_retry_state.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_interrupted_tasks.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_key_leak_protection.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_lease_renewal.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_lua_decorator.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_memory_backend.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_payload_escaping.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_perpetual_handoff.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_perpetual_race.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_perpetual_reliability.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_perpetual_reschedule.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_perpetual_state.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_progress_basics.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_progress_clobber.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_progress_pubsub.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_pubsub_resp_version.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_ratelimit.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_redelivery.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_redelivery_sweep.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_redis_connection.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_reschedule_clears_worker.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_results_retrieval.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_sentinel_urls.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_socket_timeout.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_strikelist.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_striking.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_task_cycle_round_trips.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_terminal_acked.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_testing.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_uncalled_for_contract.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_uuid7.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_worker_cancellation.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_worker_dependencies.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_worker_single_dependencies.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/test_worker_task_deps_interaction.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/worker/test_bootstrap.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/worker/test_core.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/worker/test_crash.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/worker/test_delivery_batch.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/worker/test_invariants.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/worker/test_lifecycle.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/worker/test_liveness.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/worker/test_reconnection.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/worker/test_scheduling.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/worker/test_task_factory.py +0 -0
- {pydocket-0.26.1 → pydocket-0.26.2}/tests/worker/test_ttl_zero.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pydocket
|
|
3
|
-
Version: 0.26.
|
|
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
|
[](https://pypi.org/project/pydocket/)
|
|
54
54
|
[](https://pypi.org/project/pydocket/)
|
|
55
|
-
[](https://github.com/chrisguidry/docket/actions
|
|
55
|
+
[](https://github.com/chrisguidry/docket/actions)
|
|
56
56
|
[](https://app.codecov.io/gh/chrisguidry/docket)
|
|
57
57
|
[](https://github.com/chrisguidry/docket/blob/main/LICENSE)
|
|
58
58
|
[](https://docket.lol/)
|
|
@@ -3,7 +3,7 @@ on the scheduling of future work as seamlessly and efficiently as immediate work
|
|
|
3
3
|
|
|
4
4
|
[](https://pypi.org/project/pydocket/)
|
|
5
5
|
[](https://pypi.org/project/pydocket/)
|
|
6
|
-
[](https://github.com/chrisguidry/docket/actions
|
|
6
|
+
[](https://github.com/chrisguidry/docket/actions)
|
|
7
7
|
[](https://app.codecov.io/gh/chrisguidry/docket)
|
|
8
8
|
[](https://github.com/chrisguidry/docket/blob/main/LICENSE)
|
|
9
9
|
[](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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
24
|
-
|
|
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
|
-
|
|
44
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
301
|
+
await wait_for_xlen(docket, waiters_stream, 1)
|
|
314
302
|
|
|
315
303
|
await docket.cancel(parked.key)
|
|
316
|
-
await
|
|
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
|
|
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
|
-
|
|
151
|
-
|
|
150
|
+
# Cancelling a reader that already failed does nothing, so awaiting it
|
|
151
|
+
# raises the reader's own error.
|
|
152
152
|
reader.cancel()
|
|
153
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|