taskito 0.5.0__tar.gz → 0.6.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 (171) hide show
  1. {taskito-0.5.0 → taskito-0.6.0}/Cargo.lock +6 -5
  2. {taskito-0.5.0 → taskito-0.6.0}/PKG-INFO +1 -1
  3. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-core/Cargo.toml +1 -1
  4. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-core/src/scheduler/maintenance.rs +2 -1
  5. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-core/src/scheduler/mod.rs +47 -0
  6. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-core/src/scheduler/poller.rs +33 -0
  7. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-core/src/scheduler/result_handler.rs +31 -4
  8. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-core/src/storage/diesel_common/jobs.rs +13 -0
  9. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-core/src/storage/mod.rs +9 -0
  10. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-core/src/storage/redis_backend/jobs.rs +18 -0
  11. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-core/src/storage/sqlite/tests.rs +25 -0
  12. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-core/src/storage/traits.rs +4 -0
  13. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-python/Cargo.toml +2 -1
  14. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-python/src/async_worker.rs +1 -0
  15. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-python/src/py_config.rs +9 -1
  16. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-python/src/py_queue/mod.rs +11 -1
  17. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-python/src/py_queue/worker.rs +193 -11
  18. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-python/src/py_worker.rs +8 -2
  19. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/__init__.py +1 -1
  20. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/_taskito.pyi +10 -0
  21. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/app.py +122 -6
  22. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/contrib/django/admin.py +24 -5
  23. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/contrib/django/apps.py +8 -2
  24. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/contrib/django/management/commands/taskito_dashboard.py +14 -2
  25. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/contrib/django/management/commands/taskito_info.py +7 -3
  26. taskito-0.6.0/py_src/taskito/contrib/fastapi.py +425 -0
  27. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/contrib/flask.py +27 -10
  28. taskito-0.6.0/py_src/taskito/contrib/otel.py +130 -0
  29. taskito-0.6.0/py_src/taskito/contrib/prometheus.py +366 -0
  30. taskito-0.6.0/py_src/taskito/contrib/sentry.py +103 -0
  31. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/events.py +5 -1
  32. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/middleware.py +16 -0
  33. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/mixins.py +8 -0
  34. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/webhooks.py +20 -5
  35. {taskito-0.5.0 → taskito-0.6.0}/pyproject.toml +1 -1
  36. taskito-0.5.0/py_src/taskito/contrib/fastapi.py +0 -322
  37. taskito-0.5.0/py_src/taskito/contrib/otel.py +0 -93
  38. taskito-0.5.0/py_src/taskito/contrib/prometheus.py +0 -325
  39. taskito-0.5.0/py_src/taskito/contrib/sentry.py +0 -68
  40. {taskito-0.5.0 → taskito-0.6.0}/Cargo.toml +0 -0
  41. {taskito-0.5.0 → taskito-0.6.0}/LICENSE +0 -0
  42. {taskito-0.5.0 → taskito-0.6.0}/README.md +0 -0
  43. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-core/src/error.rs +0 -0
  44. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-core/src/job.rs +0 -0
  45. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-core/src/lib.rs +0 -0
  46. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-core/src/periodic.rs +0 -0
  47. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-core/src/resilience/circuit_breaker.rs +0 -0
  48. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-core/src/resilience/dlq.rs +0 -0
  49. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-core/src/resilience/mod.rs +0 -0
  50. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-core/src/resilience/rate_limiter.rs +0 -0
  51. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-core/src/resilience/retry.rs +0 -0
  52. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-core/src/storage/diesel_common/locks.rs +0 -0
  53. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-core/src/storage/diesel_common/logs.rs +0 -0
  54. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-core/src/storage/diesel_common/metrics.rs +0 -0
  55. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-core/src/storage/diesel_common/mod.rs +0 -0
  56. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-core/src/storage/models.rs +0 -0
  57. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-core/src/storage/postgres/archival.rs +0 -0
  58. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-core/src/storage/postgres/circuit_breakers.rs +0 -0
  59. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-core/src/storage/postgres/dead_letter.rs +0 -0
  60. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-core/src/storage/postgres/jobs.rs +0 -0
  61. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-core/src/storage/postgres/locks.rs +0 -0
  62. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-core/src/storage/postgres/logs.rs +0 -0
  63. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-core/src/storage/postgres/metrics.rs +0 -0
  64. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-core/src/storage/postgres/mod.rs +0 -0
  65. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-core/src/storage/postgres/periodic.rs +0 -0
  66. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-core/src/storage/postgres/queue_state.rs +0 -0
  67. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-core/src/storage/postgres/rate_limits.rs +0 -0
  68. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-core/src/storage/postgres/trait_impl.rs +0 -0
  69. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-core/src/storage/postgres/workers.rs +0 -0
  70. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-core/src/storage/redis_backend/archival.rs +0 -0
  71. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-core/src/storage/redis_backend/circuit_breakers.rs +0 -0
  72. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-core/src/storage/redis_backend/dead_letter.rs +0 -0
  73. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-core/src/storage/redis_backend/locks.rs +0 -0
  74. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-core/src/storage/redis_backend/logs.rs +0 -0
  75. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-core/src/storage/redis_backend/metrics.rs +0 -0
  76. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-core/src/storage/redis_backend/mod.rs +0 -0
  77. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-core/src/storage/redis_backend/periodic.rs +0 -0
  78. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-core/src/storage/redis_backend/queue_state.rs +0 -0
  79. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-core/src/storage/redis_backend/rate_limits.rs +0 -0
  80. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-core/src/storage/redis_backend/trait_impl.rs +0 -0
  81. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-core/src/storage/redis_backend/workers.rs +0 -0
  82. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-core/src/storage/schema.rs +0 -0
  83. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-core/src/storage/sqlite/archival.rs +0 -0
  84. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-core/src/storage/sqlite/circuit_breakers.rs +0 -0
  85. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-core/src/storage/sqlite/dead_letter.rs +0 -0
  86. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-core/src/storage/sqlite/jobs.rs +0 -0
  87. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-core/src/storage/sqlite/locks.rs +0 -0
  88. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-core/src/storage/sqlite/logs.rs +0 -0
  89. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-core/src/storage/sqlite/metrics.rs +0 -0
  90. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-core/src/storage/sqlite/mod.rs +0 -0
  91. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-core/src/storage/sqlite/periodic.rs +0 -0
  92. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-core/src/storage/sqlite/queue_state.rs +0 -0
  93. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-core/src/storage/sqlite/rate_limits.rs +0 -0
  94. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-core/src/storage/sqlite/trait_impl.rs +0 -0
  95. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-core/src/storage/sqlite/workers.rs +0 -0
  96. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-core/src/worker.rs +0 -0
  97. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-core/tests/rust/storage_tests.rs +0 -0
  98. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-core/tests/rust.rs +0 -0
  99. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-python/src/lib.rs +0 -0
  100. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-python/src/py_job.rs +0 -0
  101. {taskito-0.5.0 → taskito-0.6.0}/crates/taskito-python/src/py_queue/inspection.rs +0 -0
  102. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/async_support/__init__.py +0 -0
  103. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/async_support/context.py +0 -0
  104. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/async_support/executor.py +0 -0
  105. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/async_support/helpers.py +0 -0
  106. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/async_support/locks.py +0 -0
  107. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/async_support/mixins.py +0 -0
  108. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/async_support/result.py +0 -0
  109. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/canvas.py +0 -0
  110. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/cli.py +0 -0
  111. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/context.py +0 -0
  112. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/contrib/__init__.py +0 -0
  113. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/contrib/django/__init__.py +0 -0
  114. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/contrib/django/management/__init__.py +0 -0
  115. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/contrib/django/management/commands/__init__.py +0 -0
  116. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/contrib/django/management/commands/taskito_worker.py +0 -0
  117. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/contrib/django/settings.py +0 -0
  118. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/dashboard.py +0 -0
  119. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/exceptions.py +0 -0
  120. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/health.py +0 -0
  121. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/inject.py +0 -0
  122. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/interception/__init__.py +0 -0
  123. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/interception/built_in.py +0 -0
  124. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/interception/converters.py +0 -0
  125. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/interception/errors.py +0 -0
  126. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/interception/interceptor.py +0 -0
  127. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/interception/metrics.py +0 -0
  128. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/interception/reconstruct.py +0 -0
  129. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/interception/registry.py +0 -0
  130. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/interception/strategy.py +0 -0
  131. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/interception/walker.py +0 -0
  132. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/locks.py +0 -0
  133. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/proxies/__init__.py +0 -0
  134. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/proxies/built_in.py +0 -0
  135. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/proxies/handler.py +0 -0
  136. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/proxies/handlers/__init__.py +0 -0
  137. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/proxies/handlers/boto3_client.py +0 -0
  138. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/proxies/handlers/file.py +0 -0
  139. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/proxies/handlers/gcs_client.py +0 -0
  140. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/proxies/handlers/httpx_client.py +0 -0
  141. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/proxies/handlers/logger.py +0 -0
  142. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/proxies/handlers/requests_session.py +0 -0
  143. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/proxies/metrics.py +0 -0
  144. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/proxies/no_proxy.py +0 -0
  145. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/proxies/reconstruct.py +0 -0
  146. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/proxies/registry.py +0 -0
  147. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/proxies/schema.py +0 -0
  148. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/proxies/signing.py +0 -0
  149. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/py.typed +0 -0
  150. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/resources/__init__.py +0 -0
  151. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/resources/definition.py +0 -0
  152. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/resources/frozen.py +0 -0
  153. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/resources/graph.py +0 -0
  154. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/resources/health.py +0 -0
  155. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/resources/pool.py +0 -0
  156. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/resources/runtime.py +0 -0
  157. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/resources/thread_local.py +0 -0
  158. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/resources/toml_config.py +0 -0
  159. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/result.py +0 -0
  160. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/scaler.py +0 -0
  161. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/serializers.py +0 -0
  162. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/task.py +0 -0
  163. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/templates/dashboard.css +0 -0
  164. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/templates/dashboard.html +0 -0
  165. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/templates/js/actions.js +0 -0
  166. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/templates/js/app.js +0 -0
  167. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/templates/js/chart.js +0 -0
  168. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/templates/js/components.js +0 -0
  169. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/templates/js/utils.js +0 -0
  170. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/templates/js/views.js +0 -0
  171. {taskito-0.5.0 → taskito-0.6.0}/py_src/taskito/testing.py +0 -0
@@ -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",
@@ -1201,7 +1201,7 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
1201
1201
 
1202
1202
  [[package]]
1203
1203
  name = "taskito-async"
1204
- version = "0.5.0"
1204
+ version = "0.6.0"
1205
1205
  dependencies = [
1206
1206
  "async-trait",
1207
1207
  "crossbeam-channel",
@@ -1212,7 +1212,7 @@ dependencies = [
1212
1212
 
1213
1213
  [[package]]
1214
1214
  name = "taskito-core"
1215
- version = "0.5.0"
1215
+ version = "0.6.0"
1216
1216
  dependencies = [
1217
1217
  "async-trait",
1218
1218
  "chrono",
@@ -1236,11 +1236,12 @@ dependencies = [
1236
1236
 
1237
1237
  [[package]]
1238
1238
  name = "taskito-python"
1239
- version = "0.5.0"
1239
+ version = "0.6.0"
1240
1240
  dependencies = [
1241
1241
  "async-trait",
1242
1242
  "crossbeam-channel",
1243
1243
  "pyo3",
1244
+ "serde_json",
1244
1245
  "taskito-async",
1245
1246
  "taskito-core",
1246
1247
  "tokio",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: taskito
3
- Version: 0.5.0
3
+ Version: 0.6.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.6.0"
4
4
  edition = "2021"
5
5
 
6
6
  [features]
@@ -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,45 @@ 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
+ error: String,
83
+ retry_count: i32,
84
+ timed_out: bool,
85
+ },
86
+ /// Task exhausted retries and moved to the dead-letter queue.
87
+ DeadLettered {
88
+ job_id: String,
89
+ task_name: String,
90
+ error: String,
91
+ timed_out: bool,
92
+ },
93
+ /// Task was cancelled during execution.
94
+ Cancelled { job_id: String, task_name: String },
95
+ }
96
+
71
97
  /// Per-task configuration for retry, rate limiting, and circuit breaker.
72
98
  #[derive(Debug, Clone)]
73
99
  pub struct TaskConfig {
74
100
  pub retry_policy: RetryPolicy,
75
101
  pub rate_limit: Option<crate::resilience::rate_limiter::RateLimitConfig>,
76
102
  pub circuit_breaker: Option<CircuitBreakerConfig>,
103
+ pub max_concurrent: Option<i32>,
104
+ }
105
+
106
+ /// Per-queue configuration for rate limiting and concurrency caps.
107
+ #[derive(Debug, Clone)]
108
+ pub struct QueueConfig {
109
+ pub rate_limit: Option<crate::resilience::rate_limiter::RateLimitConfig>,
110
+ pub max_concurrent: Option<i32>,
77
111
  }
78
112
 
79
113
  /// The central scheduler that coordinates job dispatch, retries, rate limiting, and circuit breakers.
@@ -83,6 +117,7 @@ pub struct Scheduler {
83
117
  dlq: DeadLetterQueue,
84
118
  circuit_breaker: CircuitBreaker,
85
119
  task_configs: HashMap<String, TaskConfig>,
120
+ queue_configs: HashMap<String, QueueConfig>,
86
121
  queues: Vec<String>,
87
122
  config: SchedulerConfig,
88
123
  shutdown: Arc<Notify>,
@@ -109,6 +144,7 @@ impl Scheduler {
109
144
  dlq,
110
145
  circuit_breaker,
111
146
  task_configs: HashMap::new(),
147
+ queue_configs: HashMap::new(),
112
148
  queues,
113
149
  config,
114
150
  shutdown: Arc::new(Notify::new()),
@@ -124,6 +160,10 @@ impl Scheduler {
124
160
  self.shutdown.clone()
125
161
  }
126
162
 
163
+ pub fn register_queue_config(&mut self, queue_name: String, config: QueueConfig) {
164
+ self.queue_configs.insert(queue_name, config);
165
+ }
166
+
127
167
  pub fn register_task(&mut self, task_name: String, config: TaskConfig) {
128
168
  if let Some(ref cb_config) = config.circuit_breaker {
129
169
  if let Err(e) = self.circuit_breaker.register(&task_name, cb_config) {
@@ -276,6 +316,7 @@ mod tests {
276
316
  },
277
317
  rate_limit: None,
278
318
  circuit_breaker: None,
319
+ max_concurrent: None,
279
320
  },
280
321
  );
281
322
 
@@ -290,6 +331,7 @@ mod tests {
290
331
  task_name: "retry_task".to_string(),
291
332
  wall_time_ns: 500_000,
292
333
  should_retry: true,
334
+ timed_out: false,
293
335
  })
294
336
  .unwrap();
295
337
 
@@ -312,6 +354,7 @@ mod tests {
312
354
  task_name: "exhausted_task".to_string(),
313
355
  wall_time_ns: 100,
314
356
  should_retry: true,
357
+ timed_out: false,
315
358
  })
316
359
  .unwrap();
317
360
 
@@ -337,6 +380,7 @@ mod tests {
337
380
  task_name: "no_retry_task".to_string(),
338
381
  wall_time_ns: 100,
339
382
  should_retry: false,
383
+ timed_out: false,
340
384
  })
341
385
  .unwrap();
342
386
 
@@ -373,6 +417,7 @@ mod tests {
373
417
  refill_rate: 0.0,
374
418
  }),
375
419
  circuit_breaker: None,
420
+ max_concurrent: None,
376
421
  },
377
422
  );
378
423
 
@@ -414,6 +459,7 @@ mod tests {
414
459
  retry_policy: RetryPolicy::default(),
415
460
  rate_limit: None,
416
461
  circuit_breaker: Some(cb_config),
462
+ max_concurrent: None,
417
463
  },
418
464
  );
419
465
 
@@ -452,6 +498,7 @@ mod tests {
452
498
  },
453
499
  rate_limit: None,
454
500
  circuit_breaker: None,
501
+ max_concurrent: None,
455
502
  },
456
503
  );
457
504
 
@@ -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
@@ -3,11 +3,14 @@ use log::{error, warn};
3
3
  use crate::error::Result;
4
4
  use crate::storage::Storage;
5
5
 
6
- use super::{JobResult, Scheduler};
6
+ use super::{JobResult, ResultOutcome, Scheduler};
7
7
 
8
8
  impl Scheduler {
9
9
  /// Handle a completed or failed job result from a worker.
10
- pub fn handle_result(&self, result: JobResult) -> Result<()> {
10
+ ///
11
+ /// Returns a [`ResultOutcome`] describing the action taken, so the
12
+ /// caller can dispatch Python-side middleware hooks and events.
13
+ pub fn handle_result(&self, result: JobResult) -> Result<ResultOutcome> {
11
14
  match result {
12
15
  JobResult::Success {
13
16
  job_id,
@@ -32,6 +35,11 @@ impl Scheduler {
32
35
  if let Err(e) = self.circuit_breaker.record_success(task_name) {
33
36
  error!("circuit breaker error for {task_name}: {e}");
34
37
  }
38
+
39
+ Ok(ResultOutcome::Success {
40
+ job_id,
41
+ task_name: task_name.clone(),
42
+ })
35
43
  }
36
44
  JobResult::Failure {
37
45
  job_id,
@@ -41,6 +49,7 @@ impl Scheduler {
41
49
  task_name,
42
50
  wall_time_ns,
43
51
  should_retry,
52
+ timed_out,
44
53
  } => {
45
54
  // Clear execution claim so it can be retried
46
55
  if let Err(e) = self.storage.complete_execution(&job_id) {
@@ -68,7 +77,12 @@ impl Scheduler {
68
77
  Some(job) => self.dlq.move_to_dlq(&job, &error, None)?,
69
78
  None => warn!("job {job_id} disappeared before DLQ move"),
70
79
  }
71
- return Ok(());
80
+ return Ok(ResultOutcome::DeadLettered {
81
+ job_id,
82
+ task_name,
83
+ error,
84
+ timed_out,
85
+ });
72
86
  }
73
87
 
74
88
  let policy = self
@@ -86,12 +100,25 @@ impl Scheduler {
86
100
  if retry_count < effective_max {
87
101
  let next_at = policy.next_retry_at(retry_count);
88
102
  self.storage.retry(&job_id, next_at)?;
103
+ Ok(ResultOutcome::Retry {
104
+ job_id,
105
+ task_name,
106
+ error,
107
+ retry_count,
108
+ timed_out,
109
+ })
89
110
  } else {
90
111
  // Move to DLQ
91
112
  match self.storage.get_job(&job_id)? {
92
113
  Some(job) => self.dlq.move_to_dlq(&job, &error, None)?,
93
114
  None => warn!("job {job_id} disappeared before DLQ move"),
94
115
  }
116
+ Ok(ResultOutcome::DeadLettered {
117
+ job_id,
118
+ task_name,
119
+ error,
120
+ timed_out,
121
+ })
95
122
  }
96
123
  }
97
124
  JobResult::Cancelled {
@@ -113,8 +140,8 @@ impl Scheduler {
113
140
  {
114
141
  error!("failed to record metric for cancelled job {job_id}: {e}");
115
142
  }
143
+ Ok(ResultOutcome::Cancelled { job_id, task_name })
116
144
  }
117
145
  }
118
- Ok(())
119
146
  }
120
147
  }
@@ -916,6 +916,19 @@ macro_rules! impl_diesel_job_ops {
916
916
  Ok(affected as u64)
917
917
  }
918
918
 
919
+ /// Count running jobs for a specific task name (for per-task concurrency limiting).
920
+ pub fn count_running_by_task(&self, task_name: &str) -> Result<i64> {
921
+ let mut conn = self.conn()?;
922
+
923
+ let count: i64 = jobs::table
924
+ .filter(jobs::task_name.eq(task_name))
925
+ .filter(jobs::status.eq(JobStatus::Running as i32))
926
+ .count()
927
+ .get_result(&mut conn)?;
928
+
929
+ Ok(count)
930
+ }
931
+
919
932
  /// Purge job errors older than the given timestamp.
920
933
  pub fn purge_job_errors(&self, older_than_ms: i64) -> Result<u64> {
921
934
  let mut conn = self.conn()?;
@@ -444,6 +444,12 @@ macro_rules! impl_storage {
444
444
  ) -> $crate::error::Result<u64> {
445
445
  self.purge_execution_claims(older_than_ms)
446
446
  }
447
+ fn count_running_by_task(
448
+ &self,
449
+ task_name: &str,
450
+ ) -> $crate::error::Result<i64> {
451
+ self.count_running_by_task(task_name)
452
+ }
447
453
  fn stats_by_queue(
448
454
  &self,
449
455
  queue_name: &str,
@@ -792,6 +798,9 @@ impl Storage for StorageBackend {
792
798
  fn purge_execution_claims(&self, older_than_ms: i64) -> Result<u64> {
793
799
  delegate!(self, purge_execution_claims, older_than_ms)
794
800
  }
801
+ fn count_running_by_task(&self, task_name: &str) -> Result<i64> {
802
+ delegate!(self, count_running_by_task, task_name)
803
+ }
795
804
  fn stats_by_queue(&self, queue_name: &str) -> Result<QueueStats> {
796
805
  delegate!(self, stats_by_queue, queue_name)
797
806
  }
@@ -725,6 +725,24 @@ impl RedisStorage {
725
725
  Ok(stats)
726
726
  }
727
727
 
728
+ /// Count running jobs for a specific task name (for per-task concurrency limiting).
729
+ pub fn count_running_by_task(&self, task_name: &str) -> Result<i64> {
730
+ let mut conn = self.conn()?;
731
+ let by_task_key = self.key(&["jobs", "by_task", task_name]);
732
+ let job_ids: Vec<String> = conn.smembers(&by_task_key).map_err(map_err)?;
733
+
734
+ let mut count: i64 = 0;
735
+ for id in &job_ids {
736
+ if let Some(job) = self.load_job(&mut conn, id)? {
737
+ if job.status == JobStatus::Running {
738
+ count += 1;
739
+ }
740
+ }
741
+ }
742
+
743
+ Ok(count)
744
+ }
745
+
728
746
  pub fn stats_by_queue(&self, queue_name: &str) -> Result<QueueStats> {
729
747
  let mut conn = self.conn()?;
730
748
  let by_queue_key = self.key(&["jobs", "by_queue", queue_name]);
@@ -362,6 +362,31 @@ fn test_cascade_cancel_on_dlq() {
362
362
  assert!(b.error.unwrap().contains("dependency failed"));
363
363
  }
364
364
 
365
+ #[test]
366
+ fn test_count_running_by_task() {
367
+ let storage = test_storage();
368
+ storage.enqueue(make_job("task_a")).unwrap();
369
+ storage.enqueue(make_job("task_a")).unwrap();
370
+ storage.enqueue(make_job("task_b")).unwrap();
371
+
372
+ // No running jobs yet
373
+ assert_eq!(storage.count_running_by_task("task_a").unwrap(), 0);
374
+
375
+ let now = now_millis() + 1000;
376
+ // Dequeue one task_a (becomes running)
377
+ storage.dequeue("default", now).unwrap().unwrap();
378
+
379
+ assert_eq!(storage.count_running_by_task("task_a").unwrap(), 1);
380
+ assert_eq!(storage.count_running_by_task("task_b").unwrap(), 0);
381
+
382
+ // Dequeue second task_a
383
+ storage.dequeue("default", now).unwrap().unwrap();
384
+ assert_eq!(storage.count_running_by_task("task_a").unwrap(), 2);
385
+
386
+ // Nonexistent task should return 0
387
+ assert_eq!(storage.count_running_by_task("no_such_task").unwrap(), 0);
388
+ }
389
+
365
390
  #[test]
366
391
  fn test_enqueue_rejects_missing_dependency() {
367
392
  let storage = test_storage();
@@ -161,6 +161,10 @@ pub trait Storage: Send + Sync + Clone {
161
161
  fn complete_execution(&self, job_id: &str) -> Result<()>;
162
162
  fn purge_execution_claims(&self, older_than_ms: i64) -> Result<u64>;
163
163
 
164
+ // ── Per-task concurrency ──────────────────────────────────────
165
+
166
+ fn count_running_by_task(&self, task_name: &str) -> Result<i64>;
167
+
164
168
  // ── Per-queue stats ──────────────────────────────────────────
165
169
 
166
170
  fn stats_by_queue(&self, queue_name: &str) -> Result<QueueStats>;
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "taskito-python"
3
- version = "0.5.0"
3
+ version = "0.6.0"
4
4
  edition = "2021"
5
5
 
6
6
  [features]
@@ -22,3 +22,4 @@ crossbeam-channel = { workspace = true }
22
22
  uuid = { workspace = true }
23
23
  async-trait = { workspace = true }
24
24
  taskito-async = { path = "../taskito-async", optional = true }
25
+ serde_json = { workspace = true }
@@ -118,6 +118,7 @@ impl WorkerDispatcher for AsyncWorkerPool {
118
118
  task_name,
119
119
  wall_time_ns,
120
120
  should_retry,
121
+ timed_out: false,
121
122
  }
122
123
  }
123
124
  }
@@ -26,13 +26,17 @@ pub struct PyTaskConfig {
26
26
  pub circuit_breaker_cooldown: Option<i64>,
27
27
  #[pyo3(get, set)]
28
28
  pub retry_delays: Option<Vec<f64>>,
29
+ #[pyo3(get, set)]
30
+ pub max_retry_delay: Option<i64>,
31
+ #[pyo3(get, set)]
32
+ pub max_concurrent: Option<i32>,
29
33
  }
30
34
 
31
35
  #[pymethods]
32
36
  #[allow(clippy::too_many_arguments)]
33
37
  impl PyTaskConfig {
34
38
  #[new]
35
- #[pyo3(signature = (name, max_retries=3, retry_backoff=1.0, timeout=300, priority=0, rate_limit=None, queue="default".to_string(), circuit_breaker_threshold=None, circuit_breaker_window=None, circuit_breaker_cooldown=None, retry_delays=None))]
39
+ #[pyo3(signature = (name, max_retries=3, retry_backoff=1.0, timeout=300, priority=0, rate_limit=None, queue="default".to_string(), circuit_breaker_threshold=None, circuit_breaker_window=None, circuit_breaker_cooldown=None, retry_delays=None, max_retry_delay=None, max_concurrent=None))]
36
40
  pub fn new(
37
41
  name: String,
38
42
  max_retries: i32,
@@ -45,6 +49,8 @@ impl PyTaskConfig {
45
49
  circuit_breaker_window: Option<i64>,
46
50
  circuit_breaker_cooldown: Option<i64>,
47
51
  retry_delays: Option<Vec<f64>>,
52
+ max_retry_delay: Option<i64>,
53
+ max_concurrent: Option<i32>,
48
54
  ) -> Self {
49
55
  Self {
50
56
  name,
@@ -58,6 +64,8 @@ impl PyTaskConfig {
58
64
  circuit_breaker_window,
59
65
  circuit_breaker_cooldown,
60
66
  retry_delays,
67
+ max_retry_delay,
68
+ max_concurrent,
61
69
  }
62
70
  }
63
71
  }
@@ -33,6 +33,9 @@ pub struct PyQueue {
33
33
  pub(crate) default_priority: i32,
34
34
  pub(crate) shutdown_flag: Arc<AtomicBool>,
35
35
  pub(crate) result_ttl_ms: Option<i64>,
36
+ pub(crate) scheduler_poll_interval_ms: u64,
37
+ pub(crate) scheduler_reap_interval: u32,
38
+ pub(crate) scheduler_cleanup_interval: u32,
36
39
  }
37
40
 
38
41
  #[pymethods]
@@ -43,7 +46,8 @@ pub struct PyQueue {
43
46
  )]
44
47
  impl PyQueue {
45
48
  #[new]
46
- #[pyo3(signature = (db_path=".taskito/taskito.db", workers=0, default_retry=3, default_timeout=300, default_priority=0, result_ttl=None, backend="sqlite", db_url=None, schema="taskito", pool_size=None))]
49
+ #[pyo3(signature = (db_path=".taskito/taskito.db", workers=0, default_retry=3, default_timeout=300, default_priority=0, result_ttl=None, backend="sqlite", db_url=None, schema="taskito", pool_size=None, scheduler_poll_interval_ms=50, scheduler_reap_interval=100, scheduler_cleanup_interval=1200))]
50
+ #[allow(clippy::too_many_arguments)]
47
51
  pub fn new(
48
52
  db_path: &str,
49
53
  workers: usize,
@@ -55,6 +59,9 @@ impl PyQueue {
55
59
  db_url: Option<&str>,
56
60
  schema: &str,
57
61
  pool_size: Option<u32>,
62
+ scheduler_poll_interval_ms: u64,
63
+ scheduler_reap_interval: u32,
64
+ scheduler_cleanup_interval: u32,
58
65
  ) -> PyResult<Self> {
59
66
  let storage = match backend {
60
67
  "sqlite" => {
@@ -117,6 +124,9 @@ impl PyQueue {
117
124
  default_priority,
118
125
  shutdown_flag: Arc::new(AtomicBool::new(false)),
119
126
  result_ttl_ms: result_ttl.map(|s| s * 1000),
127
+ scheduler_poll_interval_ms,
128
+ scheduler_reap_interval,
129
+ scheduler_cleanup_interval,
120
130
  })
121
131
  }
122
132