taskito 0.6.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.6.0 → taskito-0.7.0}/Cargo.lock +13 -11
- {taskito-0.6.0 → taskito-0.7.0}/PKG-INFO +1 -1
- {taskito-0.6.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.6.0 → taskito-0.7.0}/crates/taskito-core/src/scheduler/mod.rs +9 -1
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/scheduler/result_handler.rs +22 -1
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/models.rs +18 -0
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/postgres/mod.rs +28 -1
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/redis_backend/locks.rs +27 -12
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/schema.rs +5 -0
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/sqlite/dead_letter.rs +2 -2
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/sqlite/mod.rs +28 -1
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/tests/rust/storage_tests.rs +5 -0
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-python/Cargo.toml +2 -1
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-python/src/async_worker.rs +3 -3
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-python/src/py_config.rs +9 -1
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-python/src/py_queue/mod.rs +44 -6
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-python/src/py_queue/worker.rs +22 -11
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-python/src/py_worker.rs +3 -3
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/__init__.py +12 -1
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/_taskito.pyi +9 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/app.py +62 -7
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/async_support/mixins.py +2 -1
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/async_support/result.py +4 -1
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/canvas.py +97 -1
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/contrib/fastapi.py +2 -2
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/contrib/otel.py +2 -1
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/contrib/prometheus.py +2 -1
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/contrib/sentry.py +2 -1
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/events.py +2 -1
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/exceptions.py +4 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/interception/registry.py +2 -1
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/resources/definition.py +2 -1
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/resources/pool.py +2 -1
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/resources/runtime.py +2 -1
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/resources/thread_local.py +2 -1
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/resources/toml_config.py +2 -1
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/result.py +19 -4
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/task.py +4 -1
- {taskito-0.6.0 → taskito-0.7.0}/pyproject.toml +2 -2
- taskito-0.6.0/crates/taskito-core/src/resilience/circuit_breaker.rs +0 -200
- {taskito-0.6.0 → taskito-0.7.0}/Cargo.toml +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/LICENSE +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/README.md +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/error.rs +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/job.rs +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/lib.rs +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/periodic.rs +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/resilience/dlq.rs +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/resilience/mod.rs +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/resilience/rate_limiter.rs +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/resilience/retry.rs +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/scheduler/maintenance.rs +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/scheduler/poller.rs +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/diesel_common/jobs.rs +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/diesel_common/locks.rs +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/diesel_common/logs.rs +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/diesel_common/metrics.rs +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/diesel_common/mod.rs +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/mod.rs +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/postgres/archival.rs +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/postgres/circuit_breakers.rs +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/postgres/dead_letter.rs +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/postgres/jobs.rs +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/postgres/locks.rs +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/postgres/logs.rs +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/postgres/metrics.rs +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/postgres/periodic.rs +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/postgres/queue_state.rs +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/postgres/rate_limits.rs +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/postgres/trait_impl.rs +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/postgres/workers.rs +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/redis_backend/archival.rs +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/redis_backend/circuit_breakers.rs +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/redis_backend/dead_letter.rs +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/redis_backend/jobs.rs +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/redis_backend/logs.rs +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/redis_backend/metrics.rs +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/redis_backend/mod.rs +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/redis_backend/periodic.rs +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/redis_backend/queue_state.rs +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/redis_backend/rate_limits.rs +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/redis_backend/trait_impl.rs +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/redis_backend/workers.rs +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/sqlite/archival.rs +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/sqlite/circuit_breakers.rs +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/sqlite/jobs.rs +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/sqlite/locks.rs +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/sqlite/logs.rs +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/sqlite/metrics.rs +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/sqlite/periodic.rs +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/sqlite/queue_state.rs +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/sqlite/rate_limits.rs +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/sqlite/tests.rs +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/sqlite/trait_impl.rs +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/sqlite/workers.rs +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/traits.rs +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/worker.rs +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/tests/rust.rs +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-python/src/lib.rs +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-python/src/py_job.rs +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-python/src/py_queue/inspection.rs +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/async_support/__init__.py +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/async_support/context.py +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/async_support/executor.py +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/async_support/helpers.py +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/async_support/locks.py +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/cli.py +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/context.py +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/contrib/__init__.py +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/contrib/django/__init__.py +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/contrib/django/admin.py +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/contrib/django/apps.py +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/contrib/django/management/__init__.py +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/contrib/django/management/commands/__init__.py +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/contrib/django/management/commands/taskito_dashboard.py +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/contrib/django/management/commands/taskito_info.py +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/contrib/django/management/commands/taskito_worker.py +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/contrib/django/settings.py +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/contrib/flask.py +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/dashboard.py +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/health.py +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/inject.py +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/interception/__init__.py +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/interception/built_in.py +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/interception/converters.py +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/interception/errors.py +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/interception/interceptor.py +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/interception/metrics.py +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/interception/reconstruct.py +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/interception/strategy.py +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/interception/walker.py +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/locks.py +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/middleware.py +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/mixins.py +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/proxies/__init__.py +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/proxies/built_in.py +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/proxies/handler.py +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/proxies/handlers/__init__.py +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/proxies/handlers/boto3_client.py +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/proxies/handlers/file.py +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/proxies/handlers/gcs_client.py +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/proxies/handlers/httpx_client.py +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/proxies/handlers/logger.py +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/proxies/handlers/requests_session.py +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/proxies/metrics.py +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/proxies/no_proxy.py +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/proxies/reconstruct.py +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/proxies/registry.py +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/proxies/schema.py +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/proxies/signing.py +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/py.typed +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/resources/__init__.py +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/resources/frozen.py +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/resources/graph.py +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/resources/health.py +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/scaler.py +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/serializers.py +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/templates/dashboard.css +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/templates/dashboard.html +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/templates/js/actions.js +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/templates/js/app.js +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/templates/js/chart.js +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/templates/js/components.js +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/templates/js/utils.js +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/templates/js/views.js +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/testing.py +0 -0
- {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/webhooks.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
|
]
|
|
@@ -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,10 +1237,11 @@ 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",
|
|
1244
1246
|
"serde_json",
|
|
1245
1247
|
"taskito-async",
|
|
@@ -1786,18 +1788,18 @@ dependencies = [
|
|
|
1786
1788
|
|
|
1787
1789
|
[[package]]
|
|
1788
1790
|
name = "zerocopy"
|
|
1789
|
-
version = "0.8.
|
|
1791
|
+
version = "0.8.47"
|
|
1790
1792
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1791
|
-
checksum = "
|
|
1793
|
+
checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87"
|
|
1792
1794
|
dependencies = [
|
|
1793
1795
|
"zerocopy-derive",
|
|
1794
1796
|
]
|
|
1795
1797
|
|
|
1796
1798
|
[[package]]
|
|
1797
1799
|
name = "zerocopy-derive"
|
|
1798
|
-
version = "0.8.
|
|
1800
|
+
version = "0.8.47"
|
|
1799
1801
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1800
|
-
checksum = "
|
|
1802
|
+
checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89"
|
|
1801
1803
|
dependencies = [
|
|
1802
1804
|
"proc-macro2",
|
|
1803
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
|
+
}
|
|
@@ -79,6 +79,7 @@ pub enum ResultOutcome {
|
|
|
79
79
|
Retry {
|
|
80
80
|
job_id: String,
|
|
81
81
|
task_name: String,
|
|
82
|
+
queue: String,
|
|
82
83
|
error: String,
|
|
83
84
|
retry_count: i32,
|
|
84
85
|
timed_out: bool,
|
|
@@ -87,11 +88,16 @@ pub enum ResultOutcome {
|
|
|
87
88
|
DeadLettered {
|
|
88
89
|
job_id: String,
|
|
89
90
|
task_name: String,
|
|
91
|
+
queue: String,
|
|
90
92
|
error: String,
|
|
91
93
|
timed_out: bool,
|
|
92
94
|
},
|
|
93
95
|
/// Task was cancelled during execution.
|
|
94
|
-
Cancelled {
|
|
96
|
+
Cancelled {
|
|
97
|
+
job_id: String,
|
|
98
|
+
task_name: String,
|
|
99
|
+
queue: String,
|
|
100
|
+
},
|
|
95
101
|
}
|
|
96
102
|
|
|
97
103
|
/// Per-task configuration for retry, rate limiting, and circuit breaker.
|
|
@@ -452,6 +458,8 @@ mod tests {
|
|
|
452
458
|
threshold: 1,
|
|
453
459
|
window_ms: 60_000,
|
|
454
460
|
cooldown_ms: 300_000,
|
|
461
|
+
half_open_max_probes: 5,
|
|
462
|
+
half_open_success_rate: 0.8,
|
|
455
463
|
};
|
|
456
464
|
scheduler.register_task(
|
|
457
465
|
"cb_task".to_string(),
|
|
@@ -71,6 +71,14 @@ impl Scheduler {
|
|
|
71
71
|
log::error!("circuit breaker error for {task_name}: {e}");
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
// Look up the job to get the queue name for middleware context
|
|
75
|
+
let queue = self
|
|
76
|
+
.storage
|
|
77
|
+
.get_job(&job_id)?
|
|
78
|
+
.as_ref()
|
|
79
|
+
.map(|j| j.queue.clone())
|
|
80
|
+
.unwrap_or_default();
|
|
81
|
+
|
|
74
82
|
// If should_retry is false (exception filtering), skip straight to DLQ
|
|
75
83
|
if !should_retry {
|
|
76
84
|
match self.storage.get_job(&job_id)? {
|
|
@@ -80,6 +88,7 @@ impl Scheduler {
|
|
|
80
88
|
return Ok(ResultOutcome::DeadLettered {
|
|
81
89
|
job_id,
|
|
82
90
|
task_name,
|
|
91
|
+
queue,
|
|
83
92
|
error,
|
|
84
93
|
timed_out,
|
|
85
94
|
});
|
|
@@ -103,6 +112,7 @@ impl Scheduler {
|
|
|
103
112
|
Ok(ResultOutcome::Retry {
|
|
104
113
|
job_id,
|
|
105
114
|
task_name,
|
|
115
|
+
queue,
|
|
106
116
|
error,
|
|
107
117
|
retry_count,
|
|
108
118
|
timed_out,
|
|
@@ -116,6 +126,7 @@ impl Scheduler {
|
|
|
116
126
|
Ok(ResultOutcome::DeadLettered {
|
|
117
127
|
job_id,
|
|
118
128
|
task_name,
|
|
129
|
+
queue,
|
|
119
130
|
error,
|
|
120
131
|
timed_out,
|
|
121
132
|
})
|
|
@@ -140,7 +151,17 @@ impl Scheduler {
|
|
|
140
151
|
{
|
|
141
152
|
error!("failed to record metric for cancelled job {job_id}: {e}");
|
|
142
153
|
}
|
|
143
|
-
|
|
154
|
+
let queue = self
|
|
155
|
+
.storage
|
|
156
|
+
.get_job(&job_id)?
|
|
157
|
+
.as_ref()
|
|
158
|
+
.map(|j| j.queue.clone())
|
|
159
|
+
.unwrap_or_default();
|
|
160
|
+
Ok(ResultOutcome::Cancelled {
|
|
161
|
+
job_id,
|
|
162
|
+
task_name,
|
|
163
|
+
queue,
|
|
164
|
+
})
|
|
144
165
|
}
|
|
145
166
|
}
|
|
146
167
|
}
|
|
@@ -272,6 +272,24 @@ pub struct CircuitBreakerRow {
|
|
|
272
272
|
pub threshold: i32,
|
|
273
273
|
pub window_ms: i64,
|
|
274
274
|
pub cooldown_ms: i64,
|
|
275
|
+
#[serde(default = "default_max_probes")]
|
|
276
|
+
pub half_open_max_probes: i32,
|
|
277
|
+
#[serde(default = "default_success_rate")]
|
|
278
|
+
pub half_open_success_rate: f64,
|
|
279
|
+
#[serde(default)]
|
|
280
|
+
pub half_open_probe_count: i32,
|
|
281
|
+
#[serde(default)]
|
|
282
|
+
pub half_open_success_count: i32,
|
|
283
|
+
#[serde(default)]
|
|
284
|
+
pub half_open_failure_count: i32,
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
fn default_max_probes() -> i32 {
|
|
288
|
+
5
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
fn default_success_rate() -> f64 {
|
|
292
|
+
0.8
|
|
275
293
|
}
|
|
276
294
|
|
|
277
295
|
// ── Workers ──────────────────────────────────────────────────────
|
|
@@ -319,7 +319,12 @@ impl PostgresStorage {
|
|
|
319
319
|
half_open_at BIGINT,
|
|
320
320
|
threshold INTEGER NOT NULL DEFAULT 5,
|
|
321
321
|
window_ms BIGINT NOT NULL DEFAULT 60000,
|
|
322
|
-
cooldown_ms BIGINT NOT NULL DEFAULT 300000
|
|
322
|
+
cooldown_ms BIGINT NOT NULL DEFAULT 300000,
|
|
323
|
+
half_open_max_probes INTEGER NOT NULL DEFAULT 5,
|
|
324
|
+
half_open_success_rate DOUBLE PRECISION NOT NULL DEFAULT 0.8,
|
|
325
|
+
half_open_probe_count INTEGER NOT NULL DEFAULT 0,
|
|
326
|
+
half_open_success_count INTEGER NOT NULL DEFAULT 0,
|
|
327
|
+
half_open_failure_count INTEGER NOT NULL DEFAULT 0
|
|
323
328
|
)",
|
|
324
329
|
)
|
|
325
330
|
.execute(&mut conn)?;
|
|
@@ -407,6 +412,28 @@ impl PostgresStorage {
|
|
|
407
412
|
"ALTER TABLE jobs ADD COLUMN IF NOT EXISTS namespace TEXT",
|
|
408
413
|
);
|
|
409
414
|
|
|
415
|
+
// Migration: add sample-based half-open probes to circuit breakers
|
|
416
|
+
migration_alter(
|
|
417
|
+
&mut conn,
|
|
418
|
+
"ALTER TABLE circuit_breakers ADD COLUMN IF NOT EXISTS half_open_max_probes INTEGER NOT NULL DEFAULT 5",
|
|
419
|
+
);
|
|
420
|
+
migration_alter(
|
|
421
|
+
&mut conn,
|
|
422
|
+
"ALTER TABLE circuit_breakers ADD COLUMN IF NOT EXISTS half_open_success_rate DOUBLE PRECISION NOT NULL DEFAULT 0.8",
|
|
423
|
+
);
|
|
424
|
+
migration_alter(
|
|
425
|
+
&mut conn,
|
|
426
|
+
"ALTER TABLE circuit_breakers ADD COLUMN IF NOT EXISTS half_open_probe_count INTEGER NOT NULL DEFAULT 0",
|
|
427
|
+
);
|
|
428
|
+
migration_alter(
|
|
429
|
+
&mut conn,
|
|
430
|
+
"ALTER TABLE circuit_breakers ADD COLUMN IF NOT EXISTS half_open_success_count INTEGER NOT NULL DEFAULT 0",
|
|
431
|
+
);
|
|
432
|
+
migration_alter(
|
|
433
|
+
&mut conn,
|
|
434
|
+
"ALTER TABLE circuit_breakers ADD COLUMN IF NOT EXISTS half_open_failure_count INTEGER NOT NULL DEFAULT 0",
|
|
435
|
+
);
|
|
436
|
+
|
|
410
437
|
// ── Distributed Locks ─────────────────────────────
|
|
411
438
|
diesel::sql_query(
|
|
412
439
|
"CREATE TABLE IF NOT EXISTS distributed_locks (
|
|
@@ -127,20 +127,32 @@ impl RedisStorage {
|
|
|
127
127
|
let mut conn = self.conn()?;
|
|
128
128
|
let pattern = self.key(&["lock", "*"]);
|
|
129
129
|
|
|
130
|
-
let keys: Vec<String> = redis::cmd("KEYS")
|
|
131
|
-
.arg(&pattern)
|
|
132
|
-
.query(&mut conn)
|
|
133
|
-
.map_err(map_err)?;
|
|
134
|
-
|
|
135
130
|
let mut count = 0u64;
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
131
|
+
let mut cursor: u64 = 0;
|
|
132
|
+
loop {
|
|
133
|
+
let (next_cursor, keys): (u64, Vec<String>) = redis::cmd("SCAN")
|
|
134
|
+
.arg(cursor)
|
|
135
|
+
.arg("MATCH")
|
|
136
|
+
.arg(&pattern)
|
|
137
|
+
.arg("COUNT")
|
|
138
|
+
.arg(100)
|
|
139
|
+
.query(&mut conn)
|
|
140
|
+
.map_err(map_err)?;
|
|
141
|
+
|
|
142
|
+
for key in keys {
|
|
143
|
+
let expires_at: Option<i64> = conn.hget(&key, "expires_at").map_err(map_err)?;
|
|
144
|
+
if let Some(exp) = expires_at {
|
|
145
|
+
if exp <= now {
|
|
146
|
+
conn.del::<_, ()>(&key).map_err(map_err)?;
|
|
147
|
+
count += 1;
|
|
148
|
+
}
|
|
142
149
|
}
|
|
143
150
|
}
|
|
151
|
+
|
|
152
|
+
cursor = next_cursor;
|
|
153
|
+
if cursor == 0 {
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
144
156
|
}
|
|
145
157
|
|
|
146
158
|
Ok(count)
|
|
@@ -151,11 +163,14 @@ impl RedisStorage {
|
|
|
151
163
|
let now = now_millis();
|
|
152
164
|
let ckey = self.key(&["exec_claim", job_id]);
|
|
153
165
|
|
|
154
|
-
// NX: set only if not exists
|
|
166
|
+
// NX: set only if not exists. PX: auto-expire after 24 hours so
|
|
167
|
+
// orphaned claims from dead workers don't block re-execution forever.
|
|
155
168
|
let result: bool = redis::cmd("SET")
|
|
156
169
|
.arg(&ckey)
|
|
157
170
|
.arg(format!("{worker_id}:{now}"))
|
|
158
171
|
.arg("NX")
|
|
172
|
+
.arg("PX")
|
|
173
|
+
.arg(86_400_000i64) // 24 hours in milliseconds
|
|
159
174
|
.query(&mut conn)
|
|
160
175
|
.map_err(map_err)?;
|
|
161
176
|
|
|
@@ -134,6 +134,11 @@ diesel::table! {
|
|
|
134
134
|
threshold -> Integer,
|
|
135
135
|
window_ms -> BigInt,
|
|
136
136
|
cooldown_ms -> BigInt,
|
|
137
|
+
half_open_max_probes -> Integer,
|
|
138
|
+
half_open_success_rate -> Double,
|
|
139
|
+
half_open_probe_count -> Integer,
|
|
140
|
+
half_open_success_count -> Integer,
|
|
141
|
+
half_open_failure_count -> Integer,
|
|
137
142
|
}
|
|
138
143
|
}
|
|
139
144
|
|
|
@@ -54,8 +54,8 @@ impl SqliteStorage {
|
|
|
54
54
|
// Cascade cancel dependents — log warning on failure since the DLQ
|
|
55
55
|
// transaction already committed and we can't roll it back.
|
|
56
56
|
if let Err(e) = self.cascade_cancel(&job_id, "dependency failed") {
|
|
57
|
-
|
|
58
|
-
"[taskito]
|
|
57
|
+
log::warn!(
|
|
58
|
+
"[taskito] cascade_cancel failed for job {}: {}. Dependent jobs may be left pending.",
|
|
59
59
|
job_id, e
|
|
60
60
|
);
|
|
61
61
|
}
|