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