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.
Files changed (168) hide show
  1. {taskito-0.6.0 → taskito-0.7.0}/Cargo.lock +13 -11
  2. {taskito-0.6.0 → taskito-0.7.0}/PKG-INFO +1 -1
  3. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/Cargo.toml +1 -1
  4. taskito-0.7.0/crates/taskito-core/src/resilience/circuit_breaker.rs +313 -0
  5. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/scheduler/mod.rs +9 -1
  6. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/scheduler/result_handler.rs +22 -1
  7. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/models.rs +18 -0
  8. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/postgres/mod.rs +28 -1
  9. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/redis_backend/locks.rs +27 -12
  10. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/schema.rs +5 -0
  11. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/sqlite/dead_letter.rs +2 -2
  12. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/sqlite/mod.rs +28 -1
  13. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/tests/rust/storage_tests.rs +5 -0
  14. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-python/Cargo.toml +2 -1
  15. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-python/src/async_worker.rs +3 -3
  16. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-python/src/py_config.rs +9 -1
  17. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-python/src/py_queue/mod.rs +44 -6
  18. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-python/src/py_queue/worker.rs +22 -11
  19. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-python/src/py_worker.rs +3 -3
  20. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/__init__.py +12 -1
  21. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/_taskito.pyi +9 -0
  22. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/app.py +62 -7
  23. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/async_support/mixins.py +2 -1
  24. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/async_support/result.py +4 -1
  25. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/canvas.py +97 -1
  26. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/contrib/fastapi.py +2 -2
  27. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/contrib/otel.py +2 -1
  28. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/contrib/prometheus.py +2 -1
  29. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/contrib/sentry.py +2 -1
  30. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/events.py +2 -1
  31. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/exceptions.py +4 -0
  32. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/interception/registry.py +2 -1
  33. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/resources/definition.py +2 -1
  34. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/resources/pool.py +2 -1
  35. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/resources/runtime.py +2 -1
  36. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/resources/thread_local.py +2 -1
  37. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/resources/toml_config.py +2 -1
  38. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/result.py +19 -4
  39. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/task.py +4 -1
  40. {taskito-0.6.0 → taskito-0.7.0}/pyproject.toml +2 -2
  41. taskito-0.6.0/crates/taskito-core/src/resilience/circuit_breaker.rs +0 -200
  42. {taskito-0.6.0 → taskito-0.7.0}/Cargo.toml +0 -0
  43. {taskito-0.6.0 → taskito-0.7.0}/LICENSE +0 -0
  44. {taskito-0.6.0 → taskito-0.7.0}/README.md +0 -0
  45. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/error.rs +0 -0
  46. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/job.rs +0 -0
  47. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/lib.rs +0 -0
  48. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/periodic.rs +0 -0
  49. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/resilience/dlq.rs +0 -0
  50. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/resilience/mod.rs +0 -0
  51. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/resilience/rate_limiter.rs +0 -0
  52. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/resilience/retry.rs +0 -0
  53. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/scheduler/maintenance.rs +0 -0
  54. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/scheduler/poller.rs +0 -0
  55. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/diesel_common/jobs.rs +0 -0
  56. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/diesel_common/locks.rs +0 -0
  57. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/diesel_common/logs.rs +0 -0
  58. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/diesel_common/metrics.rs +0 -0
  59. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/diesel_common/mod.rs +0 -0
  60. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/mod.rs +0 -0
  61. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/postgres/archival.rs +0 -0
  62. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/postgres/circuit_breakers.rs +0 -0
  63. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/postgres/dead_letter.rs +0 -0
  64. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/postgres/jobs.rs +0 -0
  65. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/postgres/locks.rs +0 -0
  66. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/postgres/logs.rs +0 -0
  67. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/postgres/metrics.rs +0 -0
  68. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/postgres/periodic.rs +0 -0
  69. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/postgres/queue_state.rs +0 -0
  70. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/postgres/rate_limits.rs +0 -0
  71. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/postgres/trait_impl.rs +0 -0
  72. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/postgres/workers.rs +0 -0
  73. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/redis_backend/archival.rs +0 -0
  74. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/redis_backend/circuit_breakers.rs +0 -0
  75. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/redis_backend/dead_letter.rs +0 -0
  76. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/redis_backend/jobs.rs +0 -0
  77. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/redis_backend/logs.rs +0 -0
  78. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/redis_backend/metrics.rs +0 -0
  79. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/redis_backend/mod.rs +0 -0
  80. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/redis_backend/periodic.rs +0 -0
  81. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/redis_backend/queue_state.rs +0 -0
  82. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/redis_backend/rate_limits.rs +0 -0
  83. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/redis_backend/trait_impl.rs +0 -0
  84. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/redis_backend/workers.rs +0 -0
  85. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/sqlite/archival.rs +0 -0
  86. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/sqlite/circuit_breakers.rs +0 -0
  87. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/sqlite/jobs.rs +0 -0
  88. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/sqlite/locks.rs +0 -0
  89. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/sqlite/logs.rs +0 -0
  90. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/sqlite/metrics.rs +0 -0
  91. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/sqlite/periodic.rs +0 -0
  92. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/sqlite/queue_state.rs +0 -0
  93. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/sqlite/rate_limits.rs +0 -0
  94. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/sqlite/tests.rs +0 -0
  95. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/sqlite/trait_impl.rs +0 -0
  96. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/sqlite/workers.rs +0 -0
  97. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/storage/traits.rs +0 -0
  98. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/src/worker.rs +0 -0
  99. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-core/tests/rust.rs +0 -0
  100. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-python/src/lib.rs +0 -0
  101. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-python/src/py_job.rs +0 -0
  102. {taskito-0.6.0 → taskito-0.7.0}/crates/taskito-python/src/py_queue/inspection.rs +0 -0
  103. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/async_support/__init__.py +0 -0
  104. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/async_support/context.py +0 -0
  105. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/async_support/executor.py +0 -0
  106. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/async_support/helpers.py +0 -0
  107. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/async_support/locks.py +0 -0
  108. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/cli.py +0 -0
  109. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/context.py +0 -0
  110. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/contrib/__init__.py +0 -0
  111. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/contrib/django/__init__.py +0 -0
  112. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/contrib/django/admin.py +0 -0
  113. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/contrib/django/apps.py +0 -0
  114. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/contrib/django/management/__init__.py +0 -0
  115. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/contrib/django/management/commands/__init__.py +0 -0
  116. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/contrib/django/management/commands/taskito_dashboard.py +0 -0
  117. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/contrib/django/management/commands/taskito_info.py +0 -0
  118. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/contrib/django/management/commands/taskito_worker.py +0 -0
  119. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/contrib/django/settings.py +0 -0
  120. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/contrib/flask.py +0 -0
  121. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/dashboard.py +0 -0
  122. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/health.py +0 -0
  123. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/inject.py +0 -0
  124. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/interception/__init__.py +0 -0
  125. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/interception/built_in.py +0 -0
  126. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/interception/converters.py +0 -0
  127. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/interception/errors.py +0 -0
  128. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/interception/interceptor.py +0 -0
  129. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/interception/metrics.py +0 -0
  130. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/interception/reconstruct.py +0 -0
  131. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/interception/strategy.py +0 -0
  132. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/interception/walker.py +0 -0
  133. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/locks.py +0 -0
  134. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/middleware.py +0 -0
  135. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/mixins.py +0 -0
  136. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/proxies/__init__.py +0 -0
  137. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/proxies/built_in.py +0 -0
  138. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/proxies/handler.py +0 -0
  139. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/proxies/handlers/__init__.py +0 -0
  140. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/proxies/handlers/boto3_client.py +0 -0
  141. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/proxies/handlers/file.py +0 -0
  142. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/proxies/handlers/gcs_client.py +0 -0
  143. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/proxies/handlers/httpx_client.py +0 -0
  144. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/proxies/handlers/logger.py +0 -0
  145. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/proxies/handlers/requests_session.py +0 -0
  146. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/proxies/metrics.py +0 -0
  147. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/proxies/no_proxy.py +0 -0
  148. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/proxies/reconstruct.py +0 -0
  149. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/proxies/registry.py +0 -0
  150. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/proxies/schema.py +0 -0
  151. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/proxies/signing.py +0 -0
  152. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/py.typed +0 -0
  153. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/resources/__init__.py +0 -0
  154. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/resources/frozen.py +0 -0
  155. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/resources/graph.py +0 -0
  156. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/resources/health.py +0 -0
  157. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/scaler.py +0 -0
  158. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/serializers.py +0 -0
  159. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/templates/dashboard.css +0 -0
  160. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/templates/dashboard.html +0 -0
  161. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/templates/js/actions.js +0 -0
  162. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/templates/js/app.js +0 -0
  163. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/templates/js/chart.js +0 -0
  164. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/templates/js/components.js +0 -0
  165. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/templates/js/utils.js +0 -0
  166. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/templates/js/views.js +0 -0
  167. {taskito-0.6.0 → taskito-0.7.0}/py_src/taskito/testing.py +0 -0
  168. {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.8.2"
22
+ version = "1.9.0"
23
23
  source = "registry+https://github.com/rust-lang/crates.io-index"
24
- checksum = "f9f3647c145568cec02c42054e07bdf9a5a698e15b466fb2341bfc393cd24aa5"
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.17"
554
+ version = "1.0.18"
555
555
  source = "registry+https://github.com/rust-lang/crates.io-index"
556
- checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
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.6.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.6.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.6.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.42"
1791
+ version = "0.8.47"
1790
1792
  source = "registry+https://github.com/rust-lang/crates.io-index"
1791
- checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3"
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.42"
1800
+ version = "0.8.47"
1799
1801
  source = "registry+https://github.com/rust-lang/crates.io-index"
1800
- checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f"
1802
+ checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89"
1801
1803
  dependencies = [
1802
1804
  "proc-macro2",
1803
1805
  "quote",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: taskito
3
- Version: 0.6.0
3
+ Version: 0.7.0
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "taskito-core"
3
- version = "0.6.0"
3
+ version = "0.7.0"
4
4
  edition = "2021"
5
5
 
6
6
  [features]
@@ -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 { job_id: String, task_name: String },
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
- Ok(ResultOutcome::Cancelled { job_id, task_name })
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
- for key in keys {
137
- let expires_at: Option<i64> = conn.hget(&key, "expires_at").map_err(map_err)?;
138
- if let Some(exp) = expires_at {
139
- if exp <= now {
140
- conn.del::<_, ()>(&key).map_err(map_err)?;
141
- count += 1;
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
- eprintln!(
58
- "[taskito] WARNING: cascade_cancel failed for job {}: {}. Dependent jobs may be left pending.",
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
  }