simple-module-background-tasks 0.0.40__tar.gz → 0.0.42__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 (188) hide show
  1. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/PKG-INFO +5 -5
  2. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/__init__.py +6 -0
  3. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/_signal_support.py +29 -12
  4. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/celery_app.py +2 -0
  5. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/constants.py +6 -0
  6. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/endpoints/api_status.py +26 -10
  7. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/models.py +11 -1
  8. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/outbox_dispatch.py +6 -2
  9. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/outbox_transport.py +5 -2
  10. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/retry_service.py +12 -1
  11. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/settings.py +28 -0
  12. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/signals.py +22 -13
  13. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/sync_db.py +15 -0
  14. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/task_heartbeat.py +2 -1
  15. simple_module_background_tasks-0.0.42/background_tasks/task_resource.py +55 -0
  16. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/tasks.py +2 -1
  17. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/tenant_context.py +5 -3
  18. simple_module_background_tasks-0.0.42/background_tasks/tracking.py +55 -0
  19. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/package.json +5 -5
  20. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/pyproject.toml +5 -5
  21. simple_module_background_tasks-0.0.42/tests/test_background_tasks_delivery_context.py +72 -0
  22. simple_module_background_tasks-0.0.42/tests/test_background_tasks_resource_status.py +145 -0
  23. simple_module_background_tasks-0.0.42/tests/test_background_tasks_resource_transport.py +129 -0
  24. simple_module_background_tasks-0.0.42/tests/test_background_tasks_tracking_modes.py +245 -0
  25. simple_module_background_tasks-0.0.42/tests/test_background_tasks_worker_tenancy.py +35 -0
  26. simple_module_background_tasks-0.0.42/tests/test_bg_outbox_eager_serialization.py +55 -0
  27. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_bg_service.py +1 -0
  28. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/.gitignore +0 -0
  29. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/LICENSE +0 -0
  30. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/README.md +0 -0
  31. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/_lifecycle.py +0 -0
  32. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/_signal_bus.py +0 -0
  33. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/after_commit.py +0 -0
  34. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/config_guards.py +0 -0
  35. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/contracts/__init__.py +0 -0
  36. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/contracts/events.py +0 -0
  37. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/contracts/logs.py +0 -0
  38. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/contracts/outbox.py +0 -0
  39. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/contracts/schemas.py +0 -0
  40. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/db_settings.py +0 -0
  41. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/deps.py +0 -0
  42. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/endpoints/__init__.py +0 -0
  43. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/endpoints/api_admin.py +0 -0
  44. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/endpoints/logs_api.py +0 -0
  45. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/endpoints/logs_views.py +0 -0
  46. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/endpoints/outbox_api.py +0 -0
  47. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/endpoints/outbox_views.py +0 -0
  48. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/endpoints/views.py +0 -0
  49. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/filters.py +0 -0
  50. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/health.py +0 -0
  51. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/invalidation.py +0 -0
  52. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/locales/de.json +0 -0
  53. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/locales/en.json +0 -0
  54. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/log_context.py +0 -0
  55. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/log_purge.py +0 -0
  56. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/log_service.py +0 -0
  57. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/log_sink.py +0 -0
  58. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/module.py +0 -0
  59. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/outbox_claim.py +0 -0
  60. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/outbox_clock.py +0 -0
  61. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/outbox_guard.py +0 -0
  62. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/outbox_loop.py +0 -0
  63. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/outbox_service.py +0 -0
  64. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/outbox_support.py +0 -0
  65. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/outbox_sweep.py +0 -0
  66. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/outbox_worker.py +0 -0
  67. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/pages/Detail.tsx +0 -0
  68. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/pages/Index.tsx +0 -0
  69. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/pages/Logs.tsx +0 -0
  70. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/pages/Outbox.tsx +0 -0
  71. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/pages/Workers.tsx +0 -0
  72. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/pages/api.generated.ts +0 -0
  73. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/pages/api.ts +0 -0
  74. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/pages/components/DetailFacts.tsx +0 -0
  75. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/pages/components/ExecutionRow.tsx +0 -0
  76. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/pages/components/ExecutionsTable.tsx +0 -0
  77. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/pages/components/LogLevelSelect.tsx +0 -0
  78. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/pages/components/LogTable.tsx +0 -0
  79. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/pages/components/LogsPanel.tsx +0 -0
  80. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/pages/components/OutboxTable.tsx +0 -0
  81. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/pages/components/PayloadCards.tsx +0 -0
  82. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/pages/components/ProgressCard.tsx +0 -0
  83. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/pages/components/RetryAllDialog.tsx +0 -0
  84. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/pages/components/RetryConfirmDialog.tsx +0 -0
  85. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/pages/components/RoutesTable.tsx +0 -0
  86. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/pages/components/StatusPill.tsx +0 -0
  87. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/pages/components/StatusStrip.tsx +0 -0
  88. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/pages/components/TaskFilters.tsx +0 -0
  89. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/pages/components/TasksEmpty.tsx +0 -0
  90. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/pages/components/TracebackCard.tsx +0 -0
  91. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/pages/components/WorkerCard.tsx +0 -0
  92. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/pages/components/WorkerEmptyStates.tsx +0 -0
  93. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/pages/components/WorkerHealthBanner.tsx +0 -0
  94. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/pages/constants.ts +0 -0
  95. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/pages/logs.ts +0 -0
  96. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/pages/outbox.ts +0 -0
  97. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/pages/retry.ts +0 -0
  98. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/pages/worker-health.ts +0 -0
  99. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/py.typed +0 -0
  100. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/runner.py +0 -0
  101. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/service.py +0 -0
  102. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/services.py +0 -0
  103. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/settings_celery.py +0 -0
  104. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/settings_env_checks.py +0 -0
  105. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/settings_logs.py +0 -0
  106. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/task_routing.py +0 -0
  107. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/worker_inspector.py +0 -0
  108. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/worker_settings.py +0 -0
  109. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/background_tasks/workers_state.py +0 -0
  110. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/conftest.py +0 -0
  111. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_admin_api.py +0 -0
  112. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_background_tasks_after_commit.py +0 -0
  113. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_background_tasks_heartbeat.py +0 -0
  114. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_background_tasks_settings.py +0 -0
  115. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_background_tasks_tz_columns.py +0 -0
  116. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_beat_schedule.py +0 -0
  117. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_bg_config_guards.py +0 -0
  118. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_bg_db_timeouts.py +0 -0
  119. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_bg_index_has_executions.py +0 -0
  120. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_bg_index_worker_presence.py +0 -0
  121. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_bg_inprocess_sweep.py +0 -0
  122. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_bg_invalidation.py +0 -0
  123. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_bg_invalidation_degraded.py +0 -0
  124. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_bg_invalidation_redis.py +0 -0
  125. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_bg_list_item_payload.py +0 -0
  126. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_bg_log_api.py +0 -0
  127. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_bg_log_purge.py +0 -0
  128. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_bg_log_sink.py +0 -0
  129. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_bg_log_sink_loop.py +0 -0
  130. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_bg_log_sink_nul.py +0 -0
  131. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_bg_outbox_eager_platform.py +0 -0
  132. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_bg_outbox_fence_guard.py +0 -0
  133. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_bg_outbox_guard_savepoint.py +0 -0
  134. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_bg_outbox_loop_interrupt.py +0 -0
  135. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_bg_outbox_publish_scope.py +0 -0
  136. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_bg_outbox_retry_policy.py +0 -0
  137. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_bg_outbox_swallowed_cancel.py +0 -0
  138. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_bg_outbox_thread_loops.py +0 -0
  139. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_bg_outbox_worker_chain.py +0 -0
  140. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_bg_page_clamp.py +0 -0
  141. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_bg_pagination_bounds.py +0 -0
  142. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_bg_quiet_sweep_unrecorded.py +0 -0
  143. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_bg_redis_health.py +0 -0
  144. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_bg_redis_url.py +0 -0
  145. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_bg_routes_override_api.py +0 -0
  146. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_bg_runner_settings.py +0 -0
  147. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_bg_settings_env.py +0 -0
  148. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_bg_startup_log_masking.py +0 -0
  149. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_bg_task_progress.py +0 -0
  150. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_bg_task_progress_audit.py +0 -0
  151. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_bg_task_routing.py +0 -0
  152. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_bg_task_status_api.py +0 -0
  153. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_bg_users_ordering.py +0 -0
  154. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_bg_worker_audit.py +0 -0
  155. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_bg_worker_audit_bookkeeping.py +0 -0
  156. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_bg_worker_settings.py +0 -0
  157. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_detail_props.py +0 -0
  158. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_execution_tenancy.py +0 -0
  159. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_index_filters.py +0 -0
  160. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_log_context.py +0 -0
  161. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_outbox_admin.py +0 -0
  162. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_outbox_concurrency.py +0 -0
  163. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_outbox_delivery.py +0 -0
  164. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_outbox_tenancy.py +0 -0
  165. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_outbox_worker_task.py +0 -0
  166. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_retry_failed_bulk.py +0 -0
  167. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_retry_failed_guards.py +0 -0
  168. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_retry_sweep_ordering.py +0 -0
  169. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_signal_tenant_stamp.py +0 -0
  170. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_signals.py +0 -0
  171. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_sweep_stuck.py +0 -0
  172. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_sync_db.py +0 -0
  173. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_sync_db_fork.py +0 -0
  174. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_tenant_context.py +0 -0
  175. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_worker_inspector.py +0 -0
  176. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_worker_tenancy.py +0 -0
  177. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_workers_endpoints.py +0 -0
  178. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests/test_workers_uptime.py +0 -0
  179. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests-js/background_tasks_DetailPage.test.tsx +0 -0
  180. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests-js/background_tasks_IndexPage.test.tsx +0 -0
  181. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests-js/background_tasks_LogsTab.test.tsx +0 -0
  182. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests-js/background_tasks_OutboxPage.test.tsx +0 -0
  183. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests-js/background_tasks_WorkerHealthBanner.test.tsx +0 -0
  184. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests-js/outbox.test.ts +0 -0
  185. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests-js/retry.test.ts +0 -0
  186. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests-js/stub-fetch.ts +0 -0
  187. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tests-js/task-formatters.test.ts +0 -0
  188. {simple_module_background_tasks-0.0.40 → simple_module_background_tasks-0.0.42}/tsconfig.json +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: simple_module_background_tasks
3
- Version: 0.0.40
3
+ Version: 0.0.42
4
4
  Summary: Celery + Redis task queue with admin UI for monitoring and retrying failed/stuck tasks
5
5
  Project-URL: Homepage, https://github.com/antosubash/simple_module_python
6
6
  Project-URL: Repository, https://github.com/antosubash/simple_module_python
@@ -23,10 +23,10 @@ Classifier: Typing :: Typed
23
23
  Requires-Python: >=3.12
24
24
  Requires-Dist: celery[redis]<6,>=5.4
25
25
  Requires-Dist: redis<7,>=5
26
- Requires-Dist: simple-module-core==0.0.40
27
- Requires-Dist: simple-module-db==0.0.40
28
- Requires-Dist: simple-module-hosting==0.0.40
29
- Requires-Dist: simple-module-settings==0.0.40
26
+ Requires-Dist: simple-module-core==0.0.42
27
+ Requires-Dist: simple-module-db==0.0.42
28
+ Requires-Dist: simple-module-hosting==0.0.42
29
+ Requires-Dist: simple-module-settings==0.0.42
30
30
  Description-Content-Type: text/markdown
31
31
 
32
32
  # simple_module_background_tasks
@@ -3,11 +3,14 @@
3
3
  from typing import TYPE_CHECKING, Any
4
4
 
5
5
  from background_tasks.after_commit import enqueue_after_commit
6
+ from background_tasks.constants import SM_TRACKING_HEADER
6
7
  from background_tasks.log_context import (
7
8
  bind_task_context,
8
9
  get_log_context,
9
10
  install_log_filter,
10
11
  )
12
+ from background_tasks.task_resource import with_task_resource
13
+ from background_tasks.tracking import TrackingMode
11
14
 
12
15
  if TYPE_CHECKING:
13
16
  from background_tasks.task_heartbeat import heartbeat
@@ -30,10 +33,13 @@ def __getattr__(name: str) -> Any:
30
33
 
31
34
 
32
35
  __all__ = [
36
+ "SM_TRACKING_HEADER",
37
+ "TrackingMode",
33
38
  "bind_task_context",
34
39
  "enqueue_after_commit",
35
40
  "get_log_context",
36
41
  "heartbeat",
37
42
  "install_log_filter",
38
43
  "settings_for",
44
+ "with_task_resource",
39
45
  ]
@@ -13,13 +13,16 @@ from collections.abc import Callable
13
13
  from datetime import UTC, datetime
14
14
  from typing import Any
15
15
 
16
+ from celery import current_app
16
17
  from sqlalchemy import select
17
18
  from sqlalchemy.orm import Session
18
19
  from sqlmodel import col
19
20
 
20
- from background_tasks.constants import INTERNAL_TASK_SWEEP_OUTBOX, TaskStatus
21
21
  from background_tasks.models import TaskExecution
22
22
  from background_tasks.sync_db import sync_session
23
+ from background_tasks.task_resource import message_resource
24
+ from background_tasks.tenant_context import message_owner
25
+ from background_tasks.tracking import records_signal, tracking_mode
23
26
 
24
27
  logger = logging.getLogger(__name__)
25
28
 
@@ -112,13 +115,6 @@ def render_traceback(einfo: Any, exception: BaseException | None) -> str | None:
112
115
  return None
113
116
 
114
117
 
115
- # Beat ticks that fire every few seconds and are a no-op almost every time;
116
- # recording each run buried real work (QA, GH #459). Only a run that went wrong
117
- # is worth a row.
118
- _UNRECORDED_WHEN_QUIET = frozenset({INTERNAL_TASK_SWEEP_OUTBOX})
119
- _QUIET_STATUSES = frozenset({None, TaskStatus.PENDING, TaskStatus.RUNNING, TaskStatus.SUCCESS})
120
-
121
-
122
118
  def apply_signal(
123
119
  handler: str,
124
120
  *,
@@ -127,6 +123,9 @@ def apply_signal(
127
123
  after: Callable[[TaskExecution], None] | None = None,
128
124
  tenant_id: str | None = None,
129
125
  owner_user_id: str | None = None,
126
+ task: Any = None,
127
+ request: Any = None,
128
+ headers: dict | None = None,
130
129
  ) -> TaskExecution | None:
131
130
  """Open a sync session, upsert by celery_task_id, log on failure.
132
131
 
@@ -136,12 +135,30 @@ def apply_signal(
136
135
  ``tenant_id`` and ``owner_user_id`` (the message's) are written only when
137
136
  known, so a signal whose message carries neither never blanks a row already
138
137
  stamped.
138
+
139
+ The task's tracking mode (:mod:`.tracking`) decides first whether this
140
+ signal is recorded at all. When it is, the row also takes args, kwargs,
141
+ queue, owner and resource from the message when ``defaults`` lack them, so
142
+ a worker can create the row itself when the publisher never did.
139
143
  """
140
- if (
141
- defaults.get("task_name") in _UNRECORDED_WHEN_QUIET
142
- and defaults.get("status") in _QUIET_STATUSES
143
- ):
144
+ name = defaults.get("task_name", "")
145
+ mode = tracking_mode(task=task, request=request, headers=headers, name=name)
146
+ app = getattr(task, "app", None) or current_app
147
+ if not records_signal(mode, handler, defaults.get("status"), app=app):
144
148
  return None
149
+ request = request if request is not None else getattr(task, "request", None)
150
+ if request is not None:
151
+ metadata = {}
152
+ if getattr(request, "args", None) is not None:
153
+ metadata["args"] = list(request.args)
154
+ if getattr(request, "kwargs", None) is not None:
155
+ metadata["kwargs"] = dict(request.kwargs)
156
+ delivery = getattr(request, "delivery_info", None) or {}
157
+ if delivery.get("routing_key"):
158
+ metadata["queue"] = delivery["routing_key"]
159
+ defaults = {**metadata, **defaults}
160
+ owner_user_id = owner_user_id or message_owner(request=request)
161
+ defaults = {**message_resource(headers=headers, request=request), **defaults}
145
162
  if tenant_id:
146
163
  defaults = {**defaults, "tenant_id": tenant_id}
147
164
  if owner_user_id:
@@ -174,6 +174,8 @@ def build_celery(settings: BackgroundTasksSettings) -> Celery:
174
174
  beat_schedule[name] = entry
175
175
 
176
176
  celery.conf.update(
177
+ sm_execution_tracking=settings.execution_tracking,
178
+ sm_track_publish=settings.track_publish,
177
179
  broker_url=settings.broker_url,
178
180
  result_backend=settings.result_backend,
179
181
  task_default_queue=settings.task_default_queue,
@@ -199,3 +199,9 @@ DEFAULT_LOG_RETENTION_DAYS = 7
199
199
  LOG_PER_PAGE = 50
200
200
  # Level names the admin filter offers, lowest first.
201
201
  LOG_LEVEL_NAMES = ("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL")
202
+
203
+ # Message metadata shared by publishers and workers.
204
+ SM_TRACKING_HEADER = "sm_tracking"
205
+ SM_RESOURCE_TYPE_HEADER = "sm_resource_type"
206
+ SM_RESOURCE_ID_HEADER = "sm_resource_id"
207
+ RESOURCE_REF_MAX_LENGTH = 255
@@ -1,8 +1,8 @@
1
- """Owner-scoped task status (#536).
1
+ """Owner- and resource-scoped task status (#536, #709).
2
2
 
3
3
  Mounted apart from the admin router because it carries no ``PERM_VIEW`` gate:
4
- the person who started a job may follow it without being a task operator. It
5
- is marked ``Authenticated()`` (SM031); the ownership check lives in the
4
+ owners and resource viewers may follow a job without being task operators. It
5
+ is marked ``Authenticated()`` (SM031); the visibility check lives in the
6
6
  handler, and every refusal is a 404 so the route does not reveal which task
7
7
  ids exist.
8
8
  """
@@ -13,9 +13,12 @@ import re
13
13
 
14
14
  from fastapi import APIRouter, Depends, HTTPException, Request
15
15
  from simple_module_core.permissions import grants
16
+ from simple_module_core.resources import ACTION_VIEW
16
17
  from simple_module_db import RequestSession, current_tenant_id
17
18
  from simple_module_db.deps import get_db
19
+ from simple_module_db.resource_policy import load_authorized
18
20
  from simple_module_hosting.permissions import ensure_resolved_permissions
21
+ from simple_module_hosting.resource_deps import resource_principal
19
22
  from simple_module_hosting.token_guard import Authenticated
20
23
  from sqlalchemy import select
21
24
  from sqlmodel import col
@@ -30,15 +33,28 @@ _TYPE_NAME = re.compile(r"[A-Za-z_][A-Za-z0-9_.]{0,254}")
30
33
  _NOT_FOUND = "Task not found"
31
34
 
32
35
 
33
- async def _visible(request: Request, row: TaskExecution, user_id: str) -> bool:
36
+ async def _visible(request: Request, row: TaskExecution, user_id: str, db: RequestSession) -> bool:
34
37
  if grants(await ensure_resolved_permissions(request), PERM_VIEW):
35
38
  return True
36
- if not row.owner_user_id or row.owner_user_id != user_id:
37
- return False
38
- if not bool(getattr(request.app.state.sm.settings, "multi_tenant", False)):
39
+ if bool(getattr(request.app.state.sm.settings, "multi_tenant", False)):
40
+ tenant_id = current_tenant_id.get()
41
+ if tenant_id is None or row.tenant_id != tenant_id:
42
+ return False
43
+ if row.owner_user_id and row.owner_user_id == user_id:
39
44
  return True
40
- tenant_id = current_tenant_id.get()
41
- return tenant_id is not None and row.tenant_id == tenant_id
45
+ if not row.resource_type or not row.resource_id:
46
+ return False
47
+ resource = request.app.state.sm.resource_types.get(row.resource_type)
48
+ if resource is None:
49
+ return False
50
+ try:
51
+ stmt = resource.select_by_id(row.resource_id)
52
+ except ValueError:
53
+ return False
54
+ obj, allowed = await load_authorized(
55
+ db, resource.policy, await resource_principal(request), ACTION_VIEW, stmt
56
+ )
57
+ return obj is not None and allowed
42
58
 
43
59
 
44
60
  @router.get(
@@ -62,7 +78,7 @@ async def get_task_status(
62
78
  .limit(1)
63
79
  )
64
80
  ).scalar_one_or_none()
65
- if row is None or not await _visible(request, row, str(user.id)):
81
+ if row is None or not await _visible(request, row, str(user.id), db):
66
82
  raise HTTPException(status_code=404, detail=_NOT_FOUND)
67
83
  error = row.exception_type
68
84
  if error and not _TYPE_NAME.fullmatch(error):
@@ -9,7 +9,7 @@ from typing import Any, ClassVar
9
9
  from simple_module_db.base import create_module_base
10
10
  from simple_module_db.mixins import AuditMixin
11
11
  from simple_module_db.types import AwareDateTime
12
- from sqlalchemy import JSON, Column, Index, String
12
+ from sqlalchemy import JSON, CheckConstraint, Column, Index, String
13
13
  from sqlmodel import Field
14
14
 
15
15
  from background_tasks.constants import (
@@ -22,6 +22,7 @@ from background_tasks.constants import (
22
22
  OWNER_ID_MAX_LENGTH,
23
23
  PROGRESS_MESSAGE_MAX_LENGTH,
24
24
  PROGRESS_PHASE_MAX_LENGTH,
25
+ RESOURCE_REF_MAX_LENGTH,
25
26
  TABLE_LOG_RECORD,
26
27
  TABLE_OUTBOX,
27
28
  TABLE_TASK_EXECUTION,
@@ -86,6 +87,11 @@ class TaskExecution(Base, AuditMixin, table=True): # ty: ignore[unsupported-bas
86
87
  # publishes. It scopes who may read the task's status (#536).
87
88
  owner_user_id: str | None = Field(default=None, index=True, max_length=OWNER_ID_MAX_LENGTH)
88
89
 
90
+ # Optional resource this task is about (#709); lets viewers of that resource
91
+ # read the task's status. Both set or both null (check constraint below).
92
+ resource_type: str | None = Field(default=None, max_length=RESOURCE_REF_MAX_LENGTH)
93
+ resource_id: str | None = Field(default=None, max_length=RESOURCE_REF_MAX_LENGTH)
94
+
89
95
  # Progress the task body reports through ``heartbeat(progress=...)``.
90
96
  progress_current: int | None = None
91
97
  progress_total: int | None = None
@@ -118,6 +124,10 @@ class TaskExecution(Base, AuditMixin, table=True): # ty: ignore[unsupported-bas
118
124
  heartbeat_at: datetime | None = None
119
125
 
120
126
  __table_args__ = (
127
+ CheckConstraint(
128
+ "(resource_type IS NULL) = (resource_id IS NULL)",
129
+ name="task_resource_pair",
130
+ ),
121
131
  Index(
122
132
  "ix_background_tasks_task_execution_status_queued",
123
133
  "status",
@@ -41,7 +41,7 @@ from collections.abc import Sequence
41
41
  from datetime import datetime, timedelta
42
42
  from typing import Any
43
43
 
44
- from simple_module_core.durable_events import DurableContext, deserialize_event
44
+ from simple_module_core.durable_events import IN_DELIVERY, DurableContext, deserialize_event
45
45
  from simple_module_core.events import EventBus
46
46
  from simple_module_db import is_valid_tenant_id
47
47
  from simple_module_db.callbacks import discard_on_commit_callbacks, run_on_commit_callbacks
@@ -166,7 +166,11 @@ async def deliver_one(
166
166
  async with deadline:
167
167
  with handler_guard(session):
168
168
  ctx = DurableContext(db=session, attempt=row.attempts)
169
- await sub.handler(event, ctx)
169
+ token = IN_DELIVERY.set(True)
170
+ try:
171
+ await sub.handler(event, ctx)
172
+ finally:
173
+ IN_DELIVERY.reset(token)
170
174
  except TimeoutError as exc:
171
175
  if not deadline.expired():
172
176
  raise # the handler's own timeout, not ours
@@ -132,6 +132,8 @@ class OutboxTransport(_OutboxWriter):
132
132
  # Eager in-process deliveries still running (kept so they aren't
133
133
  # garbage-collected mid-flight, and so ``drain`` can await them).
134
134
  self._tasks: set[asyncio.Task[Any]] = set()
135
+ # Chained eager publishes must not share a StaticPool transaction (#655).
136
+ self._dispatch_lock = asyncio.Lock()
135
137
 
136
138
  def policy(self) -> OutboxPolicy:
137
139
  s = self._services.settings
@@ -155,8 +157,9 @@ class OutboxTransport(_OutboxWriter):
155
157
  id_strs = [str(i) for i in id_list]
156
158
  after_commit(db, [lambda: self._send(id_strs)])
157
159
 
158
- def _dispatch(self, ids: list[uuid.UUID]) -> Any:
159
- return dispatch(self._db_state.session_factory, self._bus, self.policy(), ids=ids)
160
+ async def _dispatch(self, ids: list[uuid.UUID]) -> Any:
161
+ async with self._dispatch_lock:
162
+ return await dispatch(self._db_state.session_factory, self._bus, self.policy(), ids=ids)
160
163
 
161
164
  def _dispatch_soon(self, ids: list[uuid.UUID]) -> None:
162
165
  """Eager delivery from a session's ``after_commit`` event.
@@ -20,13 +20,15 @@ from sqlmodel import col
20
20
 
21
21
  from background_tasks._signal_support import now_utc
22
22
  from background_tasks.after_commit import after_commit
23
- from background_tasks.constants import RETRY_ALL_BATCH, TaskStatus
23
+ from background_tasks.constants import RETRY_ALL_BATCH, SM_TRACKING_HEADER, TaskStatus
24
24
  from background_tasks.contracts.events import TaskRetried
25
25
  from background_tasks.contracts.schemas import RetryFailedResult
26
26
  from background_tasks.filters import Conditions, bulk_retry_conditions
27
27
  from background_tasks.models import TaskExecution
28
28
  from background_tasks.sync_db import sync_session
29
+ from background_tasks.task_resource import resource_headers
29
30
  from background_tasks.tenant_context import publish_as_owner
31
+ from background_tasks.tracking import TrackingMode
30
32
 
31
33
  if TYPE_CHECKING:
32
34
  from celery import Celery
@@ -196,6 +198,13 @@ class RetryCoordinator:
196
198
  "task_id": task_id,
197
199
  }
198
200
 
201
+ # The retry already reserved a ``pending`` row; a reduced tracking
202
+ # mode would never move it on, so an operator's retry is tracked fully.
203
+ headers: dict[str, str] = {SM_TRACKING_HEADER: TrackingMode.FULL.value}
204
+ if row.resource_type and row.resource_id:
205
+ headers.update(resource_headers(row.resource_type, row.resource_id))
206
+ options["headers"] = headers
207
+
199
208
  def publish() -> str:
200
209
  try:
201
210
  return _send()
@@ -233,6 +242,8 @@ class RetryCoordinator:
233
242
  queue=row.queue,
234
243
  tenant_id=row.tenant_id,
235
244
  owner_user_id=row.owner_user_id,
245
+ resource_type=row.resource_type,
246
+ resource_id=row.resource_id,
236
247
  args=list(row.args or []),
237
248
  kwargs=dict(row.kwargs or {}),
238
249
  retried_from_id=row.id,
@@ -53,6 +53,7 @@ from background_tasks.settings_env_checks import (
53
53
  forbid_localhost_broker_in_production,
54
54
  warn_on_legacy_redis_vars,
55
55
  )
56
+ from background_tasks.tracking import TrackingMode
56
57
 
57
58
  __all__ = ["DEFAULT_RETENTION_DAYS", "ENV_PREFIX", "BackgroundTasksSettings"]
58
59
 
@@ -62,6 +63,33 @@ logger = logging.getLogger(__name__)
62
63
  class BackgroundTasksSettings(CelerySettingsFields):
63
64
  """Configuration for the Celery + Redis task runner."""
64
65
 
66
+ execution_tracking: TrackingMode = Field(
67
+ default=TrackingMode.FULL, json_schema_extra=_CELERY_RESTART
68
+ )
69
+ track_publish: bool = Field(default=True, json_schema_extra=_CELERY_RESTART)
70
+
71
+ @classmethod
72
+ def settings_customise_sources(
73
+ cls, settings_cls, init_settings, env_settings, dotenv_settings, file_secret_settings
74
+ ):
75
+ # Tracking controls are DB-backed; preserve env wiring for older fields.
76
+ def db_only(source):
77
+ def read():
78
+ return {
79
+ k: v
80
+ for k, v in source().items()
81
+ if k not in {"execution_tracking", "track_publish"}
82
+ }
83
+
84
+ return read
85
+
86
+ return (
87
+ init_settings,
88
+ db_only(env_settings),
89
+ db_only(dotenv_settings),
90
+ db_only(file_secret_settings),
91
+ )
92
+
65
93
  # ── Durable event outbox (GH #459) ──────────────────────
66
94
  outbox_mode: Literal["celery", "inprocess"] = Field(
67
95
  default="celery", json_schema_extra=_CELERY_RESTART
@@ -42,7 +42,6 @@ from background_tasks.log_context import signal_task_finished, signal_task_start
42
42
  from background_tasks.models import TaskExecution
43
43
  from background_tasks.task_heartbeat import forget_heartbeat
44
44
  from background_tasks.tenant_context import (
45
- message_owner,
46
45
  message_tenant,
47
46
  published_owner,
48
47
  published_tenant,
@@ -86,6 +85,7 @@ def on_task_publish(
86
85
  }
87
86
  apply_signal(
88
87
  "on_task_publish",
88
+ headers=headers,
89
89
  celery_task_id=task_id,
90
90
  defaults=defaults,
91
91
  tenant_id=published_tenant(headers),
@@ -106,6 +106,7 @@ def on_task_prerun(
106
106
  **_k: Any,
107
107
  ) -> None:
108
108
  """Flip the row to ``running``, start the heartbeat, bind log context."""
109
+ restore_tenant(task_id=task_id, task=task or sender)
109
110
  args_n, kwargs_n = coerce_args_kwargs(args, kwargs)
110
111
  now = now_utc()
111
112
  name = task_name_of(sender, task)
@@ -120,11 +121,10 @@ def on_task_prerun(
120
121
  "started_at": now,
121
122
  "heartbeat_at": now,
122
123
  },
123
- tenant_id=message_tenant(task=task),
124
- owner_user_id=message_owner(task=task),
124
+ task=task or sender,
125
+ tenant_id=message_tenant(task=task or sender),
125
126
  )
126
127
  signal_task_started(task_id=task_id, task_name=name)
127
- restore_tenant(task_id=task_id, task=task)
128
128
 
129
129
 
130
130
  @signals.task_postrun.connect
@@ -140,15 +140,18 @@ def on_task_postrun(
140
140
  ``task_retry`` which fire *before* postrun; postrun only refreshes the
141
141
  heartbeat so the sweep doesn't immediately flip a just-finished row.
142
142
  """
143
- apply_signal(
144
- "on_task_postrun",
145
- celery_task_id=task_id,
146
- defaults={"task_name": task_name_of(sender, task), "heartbeat_at": now_utc()},
147
- tenant_id=message_tenant(task=task),
148
- )
149
- release_tenant(task_id=task_id)
150
- forget_heartbeat(task_id)
151
- signal_task_finished(task_id=task_id)
143
+ try:
144
+ apply_signal(
145
+ "on_task_postrun",
146
+ celery_task_id=task_id,
147
+ defaults={"task_name": task_name_of(sender, task), "heartbeat_at": now_utc()},
148
+ task=task or sender,
149
+ tenant_id=message_tenant(task=task or sender),
150
+ )
151
+ finally:
152
+ release_tenant(task_id=task_id)
153
+ forget_heartbeat(task_id)
154
+ signal_task_finished(task_id=task_id)
152
155
 
153
156
 
154
157
  @signals.task_success.connect
@@ -164,6 +167,7 @@ def on_task_success(sender: Any = None, result: Any = None, **_k: Any) -> None:
164
167
  "traceback": None,
165
168
  "exception_type": None,
166
169
  },
170
+ task=sender,
167
171
  tenant_id=message_tenant(task=sender),
168
172
  )
169
173
 
@@ -189,6 +193,7 @@ def on_task_failure(
189
193
  "exception_type": exception_type,
190
194
  "finished_at": now_utc(),
191
195
  },
196
+ task=sender,
192
197
  tenant_id=message_tenant(task=sender),
193
198
  )
194
199
 
@@ -225,6 +230,8 @@ def on_task_retry(
225
230
  "heartbeat_at": now_utc(),
226
231
  },
227
232
  after=_stamp_reason,
233
+ task=sender,
234
+ request=request,
228
235
  tenant_id=message_tenant(request=request),
229
236
  )
230
237
 
@@ -239,6 +246,8 @@ def on_task_revoked(sender: Any = None, request: Any = None, **_k: Any) -> None:
239
246
  "status": TaskStatus.REVOKED,
240
247
  "finished_at": now_utc(),
241
248
  },
249
+ task=sender,
250
+ request=request,
242
251
  tenant_id=message_tenant(request=request),
243
252
  )
244
253
 
@@ -28,6 +28,7 @@ from simple_module_db.query_filter import EngineTenancy, bind_engine_policy
28
28
  from sqlalchemy import create_engine
29
29
  from sqlalchemy.engine import Engine
30
30
  from sqlalchemy.orm import Session, sessionmaker
31
+ from sqlmodel import SQLModel
31
32
 
32
33
  logger = logging.getLogger(__name__)
33
34
 
@@ -43,6 +44,20 @@ _tenant_strict: bool = False
43
44
  _default_tenant: str | None = None
44
45
 
45
46
 
47
+ class WorkerTenancy(SQLModel, frozen=True):
48
+ """Snapshot of the host tenancy configured for this worker (#685)."""
49
+
50
+ multi_tenant: bool
51
+ default_tenant: str | None
52
+
53
+
54
+ def worker_tenancy() -> WorkerTenancy | None:
55
+ """Return configured tenancy, or ``None`` when no database URL is pinned."""
56
+ if _url_override is None:
57
+ return None
58
+ return WorkerTenancy(multi_tenant=_tenant_strict, default_tenant=_default_tenant)
59
+
60
+
46
61
  class WorkerSession(Session):
47
62
  """This engine's own session class, so the entity listeners attached to it
48
63
  never double up with a ``DatabaseState``'s (each state has its own
@@ -35,6 +35,7 @@ from background_tasks.constants import (
35
35
  )
36
36
  from background_tasks.models import TaskExecution
37
37
  from background_tasks.sync_db import sync_session
38
+ from background_tasks.tracking import TrackingMode, tracking_mode
38
39
 
39
40
  logger = logging.getLogger(__name__)
40
41
 
@@ -99,7 +100,7 @@ def heartbeat(
99
100
  reporting on.
100
101
  """
101
102
  task_id = _current_task_id()
102
- if task_id is None:
103
+ if task_id is None or tracking_mode(task=current_task) != TrackingMode.FULL:
103
104
  return False
104
105
  try:
105
106
  reported = _progress_values(progress, message, phase)
@@ -0,0 +1,55 @@
1
+ """Explicit resource references on task messages (#709)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from typing import TYPE_CHECKING, Any
7
+
8
+ from background_tasks.constants import (
9
+ RESOURCE_REF_MAX_LENGTH,
10
+ SM_RESOURCE_ID_HEADER,
11
+ SM_RESOURCE_TYPE_HEADER,
12
+ )
13
+ from background_tasks.tenant_context import header_of_request
14
+
15
+ if TYPE_CHECKING:
16
+ from celery.canvas import Signature
17
+
18
+ _TYPE_NAME = re.compile(r"[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+")
19
+
20
+
21
+ def resource_headers(resource_type: str, resource_id: str) -> dict[str, str]:
22
+ """Validate before publishing; never silently truncate an identity."""
23
+ if (
24
+ not isinstance(resource_type, str)
25
+ or len(resource_type) > RESOURCE_REF_MAX_LENGTH
26
+ or not _TYPE_NAME.fullmatch(resource_type)
27
+ or not isinstance(resource_id, str)
28
+ or not resource_id.strip()
29
+ or len(resource_id) > RESOURCE_REF_MAX_LENGTH
30
+ or any(ord(c) < 32 or ord(c) == 127 for c in resource_id)
31
+ ):
32
+ raise ValueError("Invalid task resource reference")
33
+ return {SM_RESOURCE_TYPE_HEADER: resource_type, SM_RESOURCE_ID_HEADER: resource_id}
34
+
35
+
36
+ def with_task_resource(signature: Signature, *, resource_type: str, resource_id: str) -> Signature:
37
+ """Clone a signature, preserving its other options and headers."""
38
+ headers = resource_headers(resource_type, resource_id)
39
+ return signature.clone().set(headers={**(signature.options.get("headers") or {}), **headers})
40
+
41
+
42
+ def message_resource(*, headers: dict | None = None, request: Any = None) -> dict[str, str]:
43
+ """Accept a validated pair only; malformed external messages grant no access."""
44
+ kind = (headers or {}).get(SM_RESOURCE_TYPE_HEADER)
45
+ key = (headers or {}).get(SM_RESOURCE_ID_HEADER)
46
+ if headers is None:
47
+ kind = header_of_request(request, SM_RESOURCE_TYPE_HEADER)
48
+ key = header_of_request(request, SM_RESOURCE_ID_HEADER)
49
+ if not isinstance(kind, str) or not isinstance(key, str):
50
+ return {}
51
+ try:
52
+ resource_headers(kind, key)
53
+ except ValueError:
54
+ return {}
55
+ return {"resource_type": kind, "resource_id": key}
@@ -38,6 +38,7 @@ from background_tasks.log_purge import purge_log_records
38
38
  from background_tasks.models import OutboxMessage, TaskExecution
39
39
  from background_tasks.settings import BackgroundTasksSettings
40
40
  from background_tasks.sync_db import sync_session
41
+ from background_tasks.tracking import TrackingMode
41
42
  from background_tasks.worker_settings import load_runner_settings
42
43
 
43
44
  logger = logging.getLogger(__name__)
@@ -174,7 +175,7 @@ def dispatch_outbox(ids: list[str]) -> dict[str, int]:
174
175
  return run_dispatch(list(ids))
175
176
 
176
177
 
177
- @shared_task(name=INTERNAL_TASK_SWEEP_OUTBOX)
178
+ @shared_task(name=INTERNAL_TASK_SWEEP_OUTBOX, sm_tracking=TrackingMode.FAILURES)
178
179
  def sweep_outbox() -> dict[str, int]:
179
180
  """Deliver every due outbox row and reclaim expired leases (beat, every 30 s)."""
180
181
  from background_tasks.outbox_worker import run_dispatch
@@ -119,7 +119,8 @@ def _tenant_of(task: Any) -> str | None:
119
119
  return _tenant_of_request(getattr(task, "request", None))
120
120
 
121
121
 
122
- def _header_of_request(request: Any, name: str) -> Any:
122
+ def header_of_request(request: Any, name: str) -> Any:
123
+ """A message header off a running task's request (``Request`` or publish-side object)."""
123
124
  value = getattr(request, name, None)
124
125
  # A worker ``Request`` (revoked signal) keeps headers in a dict instead.
125
126
  for attr in ("headers", "request_dict"):
@@ -133,14 +134,14 @@ def message_owner(*, task: Any = None, request: Any = None) -> str | None:
133
134
  request = request if request is not None else getattr(task, "request", None)
134
135
  if request is None:
135
136
  return None
136
- value = _header_of_request(request, OWNER_HEADER)
137
+ value = header_of_request(request, OWNER_HEADER)
137
138
  return str(value)[:OWNER_ID_MAX_LENGTH] if value else None
138
139
 
139
140
 
140
141
  def _tenant_of_request(request: Any) -> str | None:
141
142
  if request is None:
142
143
  return None
143
- value = _header_of_request(request, TENANT_HEADER)
144
+ value = header_of_request(request, TENANT_HEADER)
144
145
  if not value:
145
146
  return None
146
147
  if not is_valid_tenant_id(value):
@@ -199,6 +200,7 @@ __all__ = [
199
200
  "OWNER_HEADER",
200
201
  "TENANT_HEADER",
201
202
  "default_tenant",
203
+ "header_of_request",
202
204
  "message_owner",
203
205
  "message_tenant",
204
206
  "publish_as_owner",