taskito 0.5.0__tar.gz → 0.7.0__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.
- {taskito-0.5.0 → taskito-0.7.0}/Cargo.lock +16 -13
- {taskito-0.5.0 → taskito-0.7.0}/PKG-INFO +1 -1
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-core/Cargo.toml +1 -1
- taskito-0.7.0/crates/taskito-core/src/resilience/circuit_breaker.rs +313 -0
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-core/src/scheduler/maintenance.rs +2 -1
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-core/src/scheduler/mod.rs +55 -0
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-core/src/scheduler/poller.rs +33 -0
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-core/src/scheduler/result_handler.rs +52 -4
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-core/src/storage/diesel_common/jobs.rs +13 -0
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-core/src/storage/mod.rs +9 -0
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-core/src/storage/models.rs +18 -0
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-core/src/storage/postgres/mod.rs +28 -1
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-core/src/storage/redis_backend/jobs.rs +18 -0
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-core/src/storage/redis_backend/locks.rs +27 -12
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-core/src/storage/schema.rs +5 -0
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-core/src/storage/sqlite/dead_letter.rs +2 -2
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-core/src/storage/sqlite/mod.rs +28 -1
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-core/src/storage/sqlite/tests.rs +25 -0
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-core/src/storage/traits.rs +4 -0
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-core/tests/rust/storage_tests.rs +5 -0
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-python/Cargo.toml +3 -1
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-python/src/async_worker.rs +4 -3
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-python/src/py_config.rs +17 -1
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-python/src/py_queue/mod.rs +55 -7
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-python/src/py_queue/worker.rs +205 -12
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-python/src/py_worker.rs +11 -5
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/__init__.py +12 -1
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/_taskito.pyi +19 -0
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/app.py +183 -12
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/async_support/mixins.py +2 -1
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/async_support/result.py +4 -1
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/canvas.py +97 -1
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/contrib/django/admin.py +24 -5
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/contrib/django/apps.py +8 -2
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/contrib/django/management/commands/taskito_dashboard.py +14 -2
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/contrib/django/management/commands/taskito_info.py +7 -3
- taskito-0.7.0/py_src/taskito/contrib/fastapi.py +425 -0
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/contrib/flask.py +27 -10
- taskito-0.7.0/py_src/taskito/contrib/otel.py +131 -0
- taskito-0.7.0/py_src/taskito/contrib/prometheus.py +367 -0
- taskito-0.7.0/py_src/taskito/contrib/sentry.py +104 -0
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/events.py +7 -2
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/exceptions.py +4 -0
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/interception/registry.py +2 -1
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/middleware.py +16 -0
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/mixins.py +8 -0
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/resources/definition.py +2 -1
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/resources/pool.py +2 -1
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/resources/runtime.py +2 -1
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/resources/thread_local.py +2 -1
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/resources/toml_config.py +2 -1
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/result.py +19 -4
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/task.py +4 -1
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/webhooks.py +20 -5
- {taskito-0.5.0 → taskito-0.7.0}/pyproject.toml +2 -2
- taskito-0.5.0/crates/taskito-core/src/resilience/circuit_breaker.rs +0 -200
- taskito-0.5.0/py_src/taskito/contrib/fastapi.py +0 -322
- taskito-0.5.0/py_src/taskito/contrib/otel.py +0 -93
- taskito-0.5.0/py_src/taskito/contrib/prometheus.py +0 -325
- taskito-0.5.0/py_src/taskito/contrib/sentry.py +0 -68
- {taskito-0.5.0 → taskito-0.7.0}/Cargo.toml +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/LICENSE +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/README.md +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-core/src/error.rs +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-core/src/job.rs +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-core/src/lib.rs +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-core/src/periodic.rs +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-core/src/resilience/dlq.rs +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-core/src/resilience/mod.rs +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-core/src/resilience/rate_limiter.rs +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-core/src/resilience/retry.rs +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-core/src/storage/diesel_common/locks.rs +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-core/src/storage/diesel_common/logs.rs +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-core/src/storage/diesel_common/metrics.rs +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-core/src/storage/diesel_common/mod.rs +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-core/src/storage/postgres/archival.rs +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-core/src/storage/postgres/circuit_breakers.rs +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-core/src/storage/postgres/dead_letter.rs +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-core/src/storage/postgres/jobs.rs +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-core/src/storage/postgres/locks.rs +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-core/src/storage/postgres/logs.rs +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-core/src/storage/postgres/metrics.rs +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-core/src/storage/postgres/periodic.rs +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-core/src/storage/postgres/queue_state.rs +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-core/src/storage/postgres/rate_limits.rs +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-core/src/storage/postgres/trait_impl.rs +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-core/src/storage/postgres/workers.rs +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-core/src/storage/redis_backend/archival.rs +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-core/src/storage/redis_backend/circuit_breakers.rs +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-core/src/storage/redis_backend/dead_letter.rs +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-core/src/storage/redis_backend/logs.rs +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-core/src/storage/redis_backend/metrics.rs +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-core/src/storage/redis_backend/mod.rs +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-core/src/storage/redis_backend/periodic.rs +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-core/src/storage/redis_backend/queue_state.rs +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-core/src/storage/redis_backend/rate_limits.rs +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-core/src/storage/redis_backend/trait_impl.rs +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-core/src/storage/redis_backend/workers.rs +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-core/src/storage/sqlite/archival.rs +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-core/src/storage/sqlite/circuit_breakers.rs +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-core/src/storage/sqlite/jobs.rs +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-core/src/storage/sqlite/locks.rs +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-core/src/storage/sqlite/logs.rs +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-core/src/storage/sqlite/metrics.rs +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-core/src/storage/sqlite/periodic.rs +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-core/src/storage/sqlite/queue_state.rs +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-core/src/storage/sqlite/rate_limits.rs +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-core/src/storage/sqlite/trait_impl.rs +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-core/src/storage/sqlite/workers.rs +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-core/src/worker.rs +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-core/tests/rust.rs +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-python/src/lib.rs +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-python/src/py_job.rs +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/crates/taskito-python/src/py_queue/inspection.rs +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/async_support/__init__.py +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/async_support/context.py +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/async_support/executor.py +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/async_support/helpers.py +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/async_support/locks.py +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/cli.py +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/context.py +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/contrib/__init__.py +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/contrib/django/__init__.py +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/contrib/django/management/__init__.py +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/contrib/django/management/commands/__init__.py +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/contrib/django/management/commands/taskito_worker.py +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/contrib/django/settings.py +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/dashboard.py +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/health.py +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/inject.py +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/interception/__init__.py +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/interception/built_in.py +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/interception/converters.py +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/interception/errors.py +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/interception/interceptor.py +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/interception/metrics.py +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/interception/reconstruct.py +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/interception/strategy.py +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/interception/walker.py +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/locks.py +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/proxies/__init__.py +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/proxies/built_in.py +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/proxies/handler.py +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/proxies/handlers/__init__.py +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/proxies/handlers/boto3_client.py +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/proxies/handlers/file.py +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/proxies/handlers/gcs_client.py +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/proxies/handlers/httpx_client.py +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/proxies/handlers/logger.py +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/proxies/handlers/requests_session.py +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/proxies/metrics.py +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/proxies/no_proxy.py +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/proxies/reconstruct.py +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/proxies/registry.py +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/proxies/schema.py +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/proxies/signing.py +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/py.typed +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/resources/__init__.py +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/resources/frozen.py +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/resources/graph.py +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/resources/health.py +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/scaler.py +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/serializers.py +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/templates/dashboard.css +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/templates/dashboard.html +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/templates/js/actions.js +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/templates/js/app.js +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/templates/js/chart.js +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/templates/js/components.js +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/templates/js/utils.js +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/templates/js/views.js +0 -0
- {taskito-0.5.0 → taskito-0.7.0}/py_src/taskito/testing.py +0 -0
|
@@ -19,9 +19,9 @@ checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
|
|
19
19
|
|
|
20
20
|
[[package]]
|
|
21
21
|
name = "arc-swap"
|
|
22
|
-
version = "1.
|
|
22
|
+
version = "1.9.0"
|
|
23
23
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
24
|
-
checksum = "
|
|
24
|
+
checksum = "a07d1f37ff60921c83bdfc7407723bdefe89b44b98a9b772f225c8f9d67141a6"
|
|
25
25
|
dependencies = [
|
|
26
26
|
"rustversion",
|
|
27
27
|
]
|
|
@@ -69,9 +69,9 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
|
|
|
69
69
|
|
|
70
70
|
[[package]]
|
|
71
71
|
name = "cc"
|
|
72
|
-
version = "1.2.
|
|
72
|
+
version = "1.2.57"
|
|
73
73
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
74
|
-
checksum = "
|
|
74
|
+
checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423"
|
|
75
75
|
dependencies = [
|
|
76
76
|
"find-msvc-tools",
|
|
77
77
|
"shlex",
|
|
@@ -551,9 +551,9 @@ dependencies = [
|
|
|
551
551
|
|
|
552
552
|
[[package]]
|
|
553
553
|
name = "itoa"
|
|
554
|
-
version = "1.0.
|
|
554
|
+
version = "1.0.18"
|
|
555
555
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
556
|
-
checksum = "
|
|
556
|
+
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
|
557
557
|
|
|
558
558
|
[[package]]
|
|
559
559
|
name = "js-sys"
|
|
@@ -1201,10 +1201,11 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
|
|
|
1201
1201
|
|
|
1202
1202
|
[[package]]
|
|
1203
1203
|
name = "taskito-async"
|
|
1204
|
-
version = "0.
|
|
1204
|
+
version = "0.7.0"
|
|
1205
1205
|
dependencies = [
|
|
1206
1206
|
"async-trait",
|
|
1207
1207
|
"crossbeam-channel",
|
|
1208
|
+
"log",
|
|
1208
1209
|
"pyo3",
|
|
1209
1210
|
"taskito-core",
|
|
1210
1211
|
"tokio",
|
|
@@ -1212,7 +1213,7 @@ dependencies = [
|
|
|
1212
1213
|
|
|
1213
1214
|
[[package]]
|
|
1214
1215
|
name = "taskito-core"
|
|
1215
|
-
version = "0.
|
|
1216
|
+
version = "0.7.0"
|
|
1216
1217
|
dependencies = [
|
|
1217
1218
|
"async-trait",
|
|
1218
1219
|
"chrono",
|
|
@@ -1236,11 +1237,13 @@ dependencies = [
|
|
|
1236
1237
|
|
|
1237
1238
|
[[package]]
|
|
1238
1239
|
name = "taskito-python"
|
|
1239
|
-
version = "0.
|
|
1240
|
+
version = "0.7.0"
|
|
1240
1241
|
dependencies = [
|
|
1241
1242
|
"async-trait",
|
|
1242
1243
|
"crossbeam-channel",
|
|
1244
|
+
"log",
|
|
1243
1245
|
"pyo3",
|
|
1246
|
+
"serde_json",
|
|
1244
1247
|
"taskito-async",
|
|
1245
1248
|
"taskito-core",
|
|
1246
1249
|
"tokio",
|
|
@@ -1785,18 +1788,18 @@ dependencies = [
|
|
|
1785
1788
|
|
|
1786
1789
|
[[package]]
|
|
1787
1790
|
name = "zerocopy"
|
|
1788
|
-
version = "0.8.
|
|
1791
|
+
version = "0.8.47"
|
|
1789
1792
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1790
|
-
checksum = "
|
|
1793
|
+
checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87"
|
|
1791
1794
|
dependencies = [
|
|
1792
1795
|
"zerocopy-derive",
|
|
1793
1796
|
]
|
|
1794
1797
|
|
|
1795
1798
|
[[package]]
|
|
1796
1799
|
name = "zerocopy-derive"
|
|
1797
|
-
version = "0.8.
|
|
1800
|
+
version = "0.8.47"
|
|
1798
1801
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1799
|
-
checksum = "
|
|
1802
|
+
checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89"
|
|
1800
1803
|
dependencies = [
|
|
1801
1804
|
"proc-macro2",
|
|
1802
1805
|
"quote",
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
use crate::error::Result;
|
|
2
|
+
use crate::job::now_millis;
|
|
3
|
+
use crate::storage::models::CircuitBreakerRow;
|
|
4
|
+
use crate::storage::{Storage, StorageBackend};
|
|
5
|
+
|
|
6
|
+
/// Circuit breaker states.
|
|
7
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
8
|
+
#[repr(i32)]
|
|
9
|
+
pub enum CircuitState {
|
|
10
|
+
Closed = 0,
|
|
11
|
+
Open = 1,
|
|
12
|
+
HalfOpen = 2,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
impl CircuitState {
|
|
16
|
+
pub fn from_i32(v: i32) -> Self {
|
|
17
|
+
match v {
|
|
18
|
+
1 => Self::Open,
|
|
19
|
+
2 => Self::HalfOpen,
|
|
20
|
+
_ => Self::Closed,
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
pub fn as_str(&self) -> &'static str {
|
|
25
|
+
match self {
|
|
26
|
+
Self::Closed => "closed",
|
|
27
|
+
Self::Open => "open",
|
|
28
|
+
Self::HalfOpen => "half_open",
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/// Configuration for a task's circuit breaker.
|
|
34
|
+
#[derive(Debug, Clone)]
|
|
35
|
+
pub struct CircuitBreakerConfig {
|
|
36
|
+
pub threshold: i32,
|
|
37
|
+
pub window_ms: i64,
|
|
38
|
+
pub cooldown_ms: i64,
|
|
39
|
+
/// Number of probe requests allowed in HalfOpen state (default: 5).
|
|
40
|
+
pub half_open_max_probes: i32,
|
|
41
|
+
/// Required success rate (0.0–1.0) to close from HalfOpen (default: 0.8 = 80%).
|
|
42
|
+
pub half_open_success_rate: f64,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/// Circuit breaker manager backed by SQLite.
|
|
46
|
+
pub struct CircuitBreaker {
|
|
47
|
+
storage: StorageBackend,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
impl CircuitBreaker {
|
|
51
|
+
pub fn new(storage: StorageBackend) -> Self {
|
|
52
|
+
Self { storage }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/// Check if a task is allowed to execute. Returns true if allowed.
|
|
56
|
+
/// Transitions Open -> HalfOpen after cooldown.
|
|
57
|
+
pub fn allow(&self, task_name: &str) -> Result<bool> {
|
|
58
|
+
let row = match self.storage.get_circuit_breaker(task_name)? {
|
|
59
|
+
Some(r) => r,
|
|
60
|
+
None => return Ok(true), // No breaker configured = always allow
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
let now = now_millis();
|
|
64
|
+
let state = CircuitState::from_i32(row.state);
|
|
65
|
+
|
|
66
|
+
match state {
|
|
67
|
+
CircuitState::Closed => Ok(true),
|
|
68
|
+
CircuitState::Open => {
|
|
69
|
+
// Check if cooldown has elapsed
|
|
70
|
+
let opened = row.opened_at.unwrap_or(0);
|
|
71
|
+
if now.saturating_sub(opened) >= row.cooldown_ms {
|
|
72
|
+
// Transition to half-open: reset probe counters
|
|
73
|
+
let updated = CircuitBreakerRow {
|
|
74
|
+
state: CircuitState::HalfOpen as i32,
|
|
75
|
+
half_open_at: Some(now),
|
|
76
|
+
half_open_probe_count: 0,
|
|
77
|
+
half_open_success_count: 0,
|
|
78
|
+
half_open_failure_count: 0,
|
|
79
|
+
..row
|
|
80
|
+
};
|
|
81
|
+
self.storage.upsert_circuit_breaker(&updated)?;
|
|
82
|
+
Ok(true)
|
|
83
|
+
} else {
|
|
84
|
+
Ok(false) // Still in cooldown
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
CircuitState::HalfOpen => {
|
|
88
|
+
// Allow up to max_probes concurrent probes
|
|
89
|
+
if row.half_open_probe_count < row.half_open_max_probes {
|
|
90
|
+
let updated = CircuitBreakerRow {
|
|
91
|
+
half_open_probe_count: row.half_open_probe_count + 1,
|
|
92
|
+
..row
|
|
93
|
+
};
|
|
94
|
+
self.storage.upsert_circuit_breaker(&updated)?;
|
|
95
|
+
Ok(true)
|
|
96
|
+
} else {
|
|
97
|
+
// Check for timeout: if probes haven't completed within cooldown, re-open
|
|
98
|
+
let half_open_since = row.half_open_at.unwrap_or(0);
|
|
99
|
+
if now.saturating_sub(half_open_since) >= row.cooldown_ms {
|
|
100
|
+
let updated = CircuitBreakerRow {
|
|
101
|
+
state: CircuitState::Open as i32,
|
|
102
|
+
opened_at: Some(now),
|
|
103
|
+
half_open_at: None,
|
|
104
|
+
half_open_probe_count: 0,
|
|
105
|
+
half_open_success_count: 0,
|
|
106
|
+
half_open_failure_count: 0,
|
|
107
|
+
..row
|
|
108
|
+
};
|
|
109
|
+
self.storage.upsert_circuit_breaker(&updated)?;
|
|
110
|
+
}
|
|
111
|
+
Ok(false)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/// Record a task success. In HalfOpen, tracks probes and closes when
|
|
118
|
+
/// the success rate threshold is met.
|
|
119
|
+
pub fn record_success(&self, task_name: &str) -> Result<()> {
|
|
120
|
+
let row = match self.storage.get_circuit_breaker(task_name)? {
|
|
121
|
+
Some(r) => r,
|
|
122
|
+
None => return Ok(()),
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
let state = CircuitState::from_i32(row.state);
|
|
126
|
+
|
|
127
|
+
match state {
|
|
128
|
+
CircuitState::Closed if row.failure_count == 0 => Ok(()),
|
|
129
|
+
CircuitState::HalfOpen => {
|
|
130
|
+
let successes = row.half_open_success_count + 1;
|
|
131
|
+
let total = successes + row.half_open_failure_count;
|
|
132
|
+
|
|
133
|
+
if total >= row.half_open_max_probes {
|
|
134
|
+
let rate = successes as f64 / total as f64;
|
|
135
|
+
if rate >= row.half_open_success_rate {
|
|
136
|
+
// Threshold met — close the circuit
|
|
137
|
+
let updated = CircuitBreakerRow {
|
|
138
|
+
state: CircuitState::Closed as i32,
|
|
139
|
+
failure_count: 0,
|
|
140
|
+
opened_at: None,
|
|
141
|
+
half_open_at: None,
|
|
142
|
+
half_open_probe_count: 0,
|
|
143
|
+
half_open_success_count: 0,
|
|
144
|
+
half_open_failure_count: 0,
|
|
145
|
+
..row
|
|
146
|
+
};
|
|
147
|
+
self.storage.upsert_circuit_breaker(&updated)?;
|
|
148
|
+
} else {
|
|
149
|
+
// Threshold not met — re-open
|
|
150
|
+
let now = now_millis();
|
|
151
|
+
let updated = CircuitBreakerRow {
|
|
152
|
+
state: CircuitState::Open as i32,
|
|
153
|
+
opened_at: Some(now),
|
|
154
|
+
half_open_at: None,
|
|
155
|
+
half_open_probe_count: 0,
|
|
156
|
+
half_open_success_count: 0,
|
|
157
|
+
half_open_failure_count: 0,
|
|
158
|
+
..row
|
|
159
|
+
};
|
|
160
|
+
self.storage.upsert_circuit_breaker(&updated)?;
|
|
161
|
+
}
|
|
162
|
+
} else {
|
|
163
|
+
// Still collecting samples
|
|
164
|
+
let updated = CircuitBreakerRow {
|
|
165
|
+
half_open_success_count: successes,
|
|
166
|
+
..row
|
|
167
|
+
};
|
|
168
|
+
self.storage.upsert_circuit_breaker(&updated)?;
|
|
169
|
+
}
|
|
170
|
+
Ok(())
|
|
171
|
+
}
|
|
172
|
+
_ => {
|
|
173
|
+
// Closed with failures or Open — reset to clean Closed
|
|
174
|
+
let updated = CircuitBreakerRow {
|
|
175
|
+
state: CircuitState::Closed as i32,
|
|
176
|
+
failure_count: 0,
|
|
177
|
+
opened_at: None,
|
|
178
|
+
half_open_at: None,
|
|
179
|
+
half_open_probe_count: 0,
|
|
180
|
+
half_open_success_count: 0,
|
|
181
|
+
half_open_failure_count: 0,
|
|
182
|
+
..row
|
|
183
|
+
};
|
|
184
|
+
self.storage.upsert_circuit_breaker(&updated)?;
|
|
185
|
+
Ok(())
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/// Record a task failure. May trip the breaker open.
|
|
191
|
+
pub fn record_failure(&self, task_name: &str) -> Result<()> {
|
|
192
|
+
let now = now_millis();
|
|
193
|
+
|
|
194
|
+
let row = match self.storage.get_circuit_breaker(task_name)? {
|
|
195
|
+
Some(r) => r,
|
|
196
|
+
None => return Ok(()),
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
let state = CircuitState::from_i32(row.state);
|
|
200
|
+
|
|
201
|
+
match state {
|
|
202
|
+
CircuitState::HalfOpen => {
|
|
203
|
+
let failures = row.half_open_failure_count + 1;
|
|
204
|
+
let total = row.half_open_success_count + failures;
|
|
205
|
+
|
|
206
|
+
// Check if the success rate can still be met
|
|
207
|
+
let remaining = row.half_open_max_probes - total;
|
|
208
|
+
let best_possible_rate = if row.half_open_max_probes > 0 {
|
|
209
|
+
(row.half_open_success_count + remaining) as f64
|
|
210
|
+
/ row.half_open_max_probes as f64
|
|
211
|
+
} else {
|
|
212
|
+
0.0
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
if total >= row.half_open_max_probes
|
|
216
|
+
|| best_possible_rate < row.half_open_success_rate
|
|
217
|
+
{
|
|
218
|
+
// Either all samples collected and failed, or impossible to meet threshold
|
|
219
|
+
let updated = CircuitBreakerRow {
|
|
220
|
+
state: CircuitState::Open as i32,
|
|
221
|
+
failure_count: row.failure_count.saturating_add(1),
|
|
222
|
+
last_failure_at: Some(now),
|
|
223
|
+
opened_at: Some(now),
|
|
224
|
+
half_open_at: None,
|
|
225
|
+
half_open_probe_count: 0,
|
|
226
|
+
half_open_success_count: 0,
|
|
227
|
+
half_open_failure_count: 0,
|
|
228
|
+
..row
|
|
229
|
+
};
|
|
230
|
+
self.storage.upsert_circuit_breaker(&updated)?;
|
|
231
|
+
} else {
|
|
232
|
+
// Still collecting samples
|
|
233
|
+
let updated = CircuitBreakerRow {
|
|
234
|
+
failure_count: row.failure_count.saturating_add(1),
|
|
235
|
+
last_failure_at: Some(now),
|
|
236
|
+
half_open_failure_count: failures,
|
|
237
|
+
..row
|
|
238
|
+
};
|
|
239
|
+
self.storage.upsert_circuit_breaker(&updated)?;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
CircuitState::Closed => {
|
|
243
|
+
// Reset count if outside the window
|
|
244
|
+
let count = if let Some(last) = row.last_failure_at {
|
|
245
|
+
if now.saturating_sub(last) > row.window_ms {
|
|
246
|
+
1 // Window expired, start fresh
|
|
247
|
+
} else {
|
|
248
|
+
row.failure_count.saturating_add(1)
|
|
249
|
+
}
|
|
250
|
+
} else {
|
|
251
|
+
1
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
if count >= row.threshold {
|
|
255
|
+
// Trip to open
|
|
256
|
+
let updated = CircuitBreakerRow {
|
|
257
|
+
state: CircuitState::Open as i32,
|
|
258
|
+
failure_count: count,
|
|
259
|
+
last_failure_at: Some(now),
|
|
260
|
+
opened_at: Some(now),
|
|
261
|
+
half_open_at: None,
|
|
262
|
+
..row
|
|
263
|
+
};
|
|
264
|
+
self.storage.upsert_circuit_breaker(&updated)?;
|
|
265
|
+
} else {
|
|
266
|
+
let updated = CircuitBreakerRow {
|
|
267
|
+
failure_count: count,
|
|
268
|
+
last_failure_at: Some(now),
|
|
269
|
+
..row
|
|
270
|
+
};
|
|
271
|
+
self.storage.upsert_circuit_breaker(&updated)?;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
CircuitState::Open => {
|
|
275
|
+
// Already open, just update failure count
|
|
276
|
+
let updated = CircuitBreakerRow {
|
|
277
|
+
failure_count: row.failure_count.saturating_add(1),
|
|
278
|
+
last_failure_at: Some(now),
|
|
279
|
+
..row
|
|
280
|
+
};
|
|
281
|
+
self.storage.upsert_circuit_breaker(&updated)?;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
Ok(())
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/// Register a circuit breaker for a task (idempotent).
|
|
289
|
+
pub fn register(&self, task_name: &str, config: &CircuitBreakerConfig) -> Result<()> {
|
|
290
|
+
if self.storage.get_circuit_breaker(task_name)?.is_some() {
|
|
291
|
+
return Ok(());
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
let row = CircuitBreakerRow {
|
|
295
|
+
task_name: task_name.to_string(),
|
|
296
|
+
state: CircuitState::Closed as i32,
|
|
297
|
+
failure_count: 0,
|
|
298
|
+
last_failure_at: None,
|
|
299
|
+
opened_at: None,
|
|
300
|
+
half_open_at: None,
|
|
301
|
+
threshold: config.threshold,
|
|
302
|
+
window_ms: config.window_ms,
|
|
303
|
+
cooldown_ms: config.cooldown_ms,
|
|
304
|
+
half_open_max_probes: config.half_open_max_probes,
|
|
305
|
+
half_open_success_rate: config.half_open_success_rate,
|
|
306
|
+
half_open_probe_count: 0,
|
|
307
|
+
half_open_success_count: 0,
|
|
308
|
+
half_open_failure_count: 0,
|
|
309
|
+
};
|
|
310
|
+
self.storage.upsert_circuit_breaker(&row)?;
|
|
311
|
+
Ok(())
|
|
312
|
+
}
|
|
313
|
+
}
|
|
@@ -33,7 +33,7 @@ impl Scheduler {
|
|
|
33
33
|
|
|
34
34
|
for job in stale_jobs {
|
|
35
35
|
let error = format!("job timed out after {}ms", job.timeout_ms);
|
|
36
|
-
self.handle_result(JobResult::Failure {
|
|
36
|
+
let _ = self.handle_result(JobResult::Failure {
|
|
37
37
|
job_id: job.id.clone(),
|
|
38
38
|
error,
|
|
39
39
|
retry_count: job.retry_count,
|
|
@@ -41,6 +41,7 @@ impl Scheduler {
|
|
|
41
41
|
task_name: job.task_name.clone(),
|
|
42
42
|
wall_time_ns: 0,
|
|
43
43
|
should_retry: true,
|
|
44
|
+
timed_out: true,
|
|
44
45
|
})?;
|
|
45
46
|
}
|
|
46
47
|
|
|
@@ -60,6 +60,7 @@ pub enum JobResult {
|
|
|
60
60
|
task_name: String,
|
|
61
61
|
wall_time_ns: i64,
|
|
62
62
|
should_retry: bool,
|
|
63
|
+
timed_out: bool,
|
|
63
64
|
},
|
|
64
65
|
Cancelled {
|
|
65
66
|
job_id: String,
|
|
@@ -68,12 +69,51 @@ pub enum JobResult {
|
|
|
68
69
|
},
|
|
69
70
|
}
|
|
70
71
|
|
|
72
|
+
/// Outcome of processing a job result, returned to the caller for
|
|
73
|
+
/// Python-side middleware hook dispatch.
|
|
74
|
+
#[derive(Debug, Clone)]
|
|
75
|
+
pub enum ResultOutcome {
|
|
76
|
+
/// Task completed successfully.
|
|
77
|
+
Success { job_id: String, task_name: String },
|
|
78
|
+
/// Task failed and will be retried.
|
|
79
|
+
Retry {
|
|
80
|
+
job_id: String,
|
|
81
|
+
task_name: String,
|
|
82
|
+
queue: String,
|
|
83
|
+
error: String,
|
|
84
|
+
retry_count: i32,
|
|
85
|
+
timed_out: bool,
|
|
86
|
+
},
|
|
87
|
+
/// Task exhausted retries and moved to the dead-letter queue.
|
|
88
|
+
DeadLettered {
|
|
89
|
+
job_id: String,
|
|
90
|
+
task_name: String,
|
|
91
|
+
queue: String,
|
|
92
|
+
error: String,
|
|
93
|
+
timed_out: bool,
|
|
94
|
+
},
|
|
95
|
+
/// Task was cancelled during execution.
|
|
96
|
+
Cancelled {
|
|
97
|
+
job_id: String,
|
|
98
|
+
task_name: String,
|
|
99
|
+
queue: String,
|
|
100
|
+
},
|
|
101
|
+
}
|
|
102
|
+
|
|
71
103
|
/// Per-task configuration for retry, rate limiting, and circuit breaker.
|
|
72
104
|
#[derive(Debug, Clone)]
|
|
73
105
|
pub struct TaskConfig {
|
|
74
106
|
pub retry_policy: RetryPolicy,
|
|
75
107
|
pub rate_limit: Option<crate::resilience::rate_limiter::RateLimitConfig>,
|
|
76
108
|
pub circuit_breaker: Option<CircuitBreakerConfig>,
|
|
109
|
+
pub max_concurrent: Option<i32>,
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/// Per-queue configuration for rate limiting and concurrency caps.
|
|
113
|
+
#[derive(Debug, Clone)]
|
|
114
|
+
pub struct QueueConfig {
|
|
115
|
+
pub rate_limit: Option<crate::resilience::rate_limiter::RateLimitConfig>,
|
|
116
|
+
pub max_concurrent: Option<i32>,
|
|
77
117
|
}
|
|
78
118
|
|
|
79
119
|
/// The central scheduler that coordinates job dispatch, retries, rate limiting, and circuit breakers.
|
|
@@ -83,6 +123,7 @@ pub struct Scheduler {
|
|
|
83
123
|
dlq: DeadLetterQueue,
|
|
84
124
|
circuit_breaker: CircuitBreaker,
|
|
85
125
|
task_configs: HashMap<String, TaskConfig>,
|
|
126
|
+
queue_configs: HashMap<String, QueueConfig>,
|
|
86
127
|
queues: Vec<String>,
|
|
87
128
|
config: SchedulerConfig,
|
|
88
129
|
shutdown: Arc<Notify>,
|
|
@@ -109,6 +150,7 @@ impl Scheduler {
|
|
|
109
150
|
dlq,
|
|
110
151
|
circuit_breaker,
|
|
111
152
|
task_configs: HashMap::new(),
|
|
153
|
+
queue_configs: HashMap::new(),
|
|
112
154
|
queues,
|
|
113
155
|
config,
|
|
114
156
|
shutdown: Arc::new(Notify::new()),
|
|
@@ -124,6 +166,10 @@ impl Scheduler {
|
|
|
124
166
|
self.shutdown.clone()
|
|
125
167
|
}
|
|
126
168
|
|
|
169
|
+
pub fn register_queue_config(&mut self, queue_name: String, config: QueueConfig) {
|
|
170
|
+
self.queue_configs.insert(queue_name, config);
|
|
171
|
+
}
|
|
172
|
+
|
|
127
173
|
pub fn register_task(&mut self, task_name: String, config: TaskConfig) {
|
|
128
174
|
if let Some(ref cb_config) = config.circuit_breaker {
|
|
129
175
|
if let Err(e) = self.circuit_breaker.register(&task_name, cb_config) {
|
|
@@ -276,6 +322,7 @@ mod tests {
|
|
|
276
322
|
},
|
|
277
323
|
rate_limit: None,
|
|
278
324
|
circuit_breaker: None,
|
|
325
|
+
max_concurrent: None,
|
|
279
326
|
},
|
|
280
327
|
);
|
|
281
328
|
|
|
@@ -290,6 +337,7 @@ mod tests {
|
|
|
290
337
|
task_name: "retry_task".to_string(),
|
|
291
338
|
wall_time_ns: 500_000,
|
|
292
339
|
should_retry: true,
|
|
340
|
+
timed_out: false,
|
|
293
341
|
})
|
|
294
342
|
.unwrap();
|
|
295
343
|
|
|
@@ -312,6 +360,7 @@ mod tests {
|
|
|
312
360
|
task_name: "exhausted_task".to_string(),
|
|
313
361
|
wall_time_ns: 100,
|
|
314
362
|
should_retry: true,
|
|
363
|
+
timed_out: false,
|
|
315
364
|
})
|
|
316
365
|
.unwrap();
|
|
317
366
|
|
|
@@ -337,6 +386,7 @@ mod tests {
|
|
|
337
386
|
task_name: "no_retry_task".to_string(),
|
|
338
387
|
wall_time_ns: 100,
|
|
339
388
|
should_retry: false,
|
|
389
|
+
timed_out: false,
|
|
340
390
|
})
|
|
341
391
|
.unwrap();
|
|
342
392
|
|
|
@@ -373,6 +423,7 @@ mod tests {
|
|
|
373
423
|
refill_rate: 0.0,
|
|
374
424
|
}),
|
|
375
425
|
circuit_breaker: None,
|
|
426
|
+
max_concurrent: None,
|
|
376
427
|
},
|
|
377
428
|
);
|
|
378
429
|
|
|
@@ -407,6 +458,8 @@ mod tests {
|
|
|
407
458
|
threshold: 1,
|
|
408
459
|
window_ms: 60_000,
|
|
409
460
|
cooldown_ms: 300_000,
|
|
461
|
+
half_open_max_probes: 5,
|
|
462
|
+
half_open_success_rate: 0.8,
|
|
410
463
|
};
|
|
411
464
|
scheduler.register_task(
|
|
412
465
|
"cb_task".to_string(),
|
|
@@ -414,6 +467,7 @@ mod tests {
|
|
|
414
467
|
retry_policy: RetryPolicy::default(),
|
|
415
468
|
rate_limit: None,
|
|
416
469
|
circuit_breaker: Some(cb_config),
|
|
470
|
+
max_concurrent: None,
|
|
417
471
|
},
|
|
418
472
|
);
|
|
419
473
|
|
|
@@ -452,6 +506,7 @@ mod tests {
|
|
|
452
506
|
},
|
|
453
507
|
rate_limit: None,
|
|
454
508
|
circuit_breaker: None,
|
|
509
|
+
max_concurrent: None,
|
|
455
510
|
},
|
|
456
511
|
);
|
|
457
512
|
|
|
@@ -14,6 +14,9 @@ const CIRCUIT_BREAKER_RETRY_DELAY_MS: i64 = 5000;
|
|
|
14
14
|
/// Delay before re-scheduling a rate-limited job (ms).
|
|
15
15
|
const RATE_LIMIT_RETRY_DELAY_MS: i64 = 1000;
|
|
16
16
|
|
|
17
|
+
/// Delay before re-scheduling a concurrency-limited job (ms).
|
|
18
|
+
const CONCURRENCY_RETRY_DELAY_MS: i64 = 500;
|
|
19
|
+
|
|
17
20
|
impl Scheduler {
|
|
18
21
|
pub(super) fn try_dispatch(&self, job_tx: &tokio::sync::mpsc::Sender<Job>) -> Result<bool> {
|
|
19
22
|
let now = now_millis();
|
|
@@ -53,6 +56,26 @@ impl Scheduler {
|
|
|
53
56
|
None => return Ok(false),
|
|
54
57
|
};
|
|
55
58
|
|
|
59
|
+
// Check queue-level limits
|
|
60
|
+
if let Some(qcfg) = self.queue_configs.get(&job.queue) {
|
|
61
|
+
if let Some(ref rl_config) = qcfg.rate_limit {
|
|
62
|
+
let key = format!("queue:{}", job.queue);
|
|
63
|
+
if !self.rate_limiter.try_acquire(&key, rl_config)? {
|
|
64
|
+
self.storage
|
|
65
|
+
.retry(&job.id, now + RATE_LIMIT_RETRY_DELAY_MS)?;
|
|
66
|
+
return Ok(false);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if let Some(max_conc) = qcfg.max_concurrent {
|
|
70
|
+
let stats = self.storage.stats_by_queue(&job.queue)?;
|
|
71
|
+
if stats.running >= max_conc as i64 {
|
|
72
|
+
self.storage
|
|
73
|
+
.retry(&job.id, now + CONCURRENCY_RETRY_DELAY_MS)?;
|
|
74
|
+
return Ok(false);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
56
79
|
// Check circuit breaker for this task
|
|
57
80
|
if let Some(config) = self.task_configs.get(&job.task_name) {
|
|
58
81
|
if config.circuit_breaker.is_some() && !self.circuit_breaker.allow(&job.task_name)? {
|
|
@@ -68,6 +91,16 @@ impl Scheduler {
|
|
|
68
91
|
return Ok(false);
|
|
69
92
|
}
|
|
70
93
|
}
|
|
94
|
+
|
|
95
|
+
// Check per-task concurrency limit
|
|
96
|
+
if let Some(max_conc) = config.max_concurrent {
|
|
97
|
+
let running = self.storage.count_running_by_task(&job.task_name)?;
|
|
98
|
+
if running >= max_conc as i64 {
|
|
99
|
+
self.storage
|
|
100
|
+
.retry(&job.id, now + CONCURRENCY_RETRY_DELAY_MS)?;
|
|
101
|
+
return Ok(false);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
71
104
|
}
|
|
72
105
|
|
|
73
106
|
// Claim exactly-once execution
|