simple-module-background-tasks 0.0.37__tar.gz → 0.0.38__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.
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/.gitignore +1 -2
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/PKG-INFO +5 -5
- simple_module_background_tasks-0.0.38/background_tasks/__init__.py +39 -0
- simple_module_background_tasks-0.0.38/background_tasks/_lifecycle.py +174 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/_signal_bus.py +2 -1
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/_signal_support.py +52 -1
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/after_commit.py +12 -1
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/constants.py +30 -4
- simple_module_background_tasks-0.0.38/background_tasks/contracts/logs.py +31 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/contracts/outbox.py +1 -1
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/contracts/schemas.py +52 -3
- simple_module_background_tasks-0.0.38/background_tasks/db_settings.py +34 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/deps.py +9 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/endpoints/api_admin.py +5 -4
- simple_module_background_tasks-0.0.38/background_tasks/endpoints/api_status.py +69 -0
- simple_module_background_tasks-0.0.38/background_tasks/endpoints/logs_api.py +62 -0
- simple_module_background_tasks-0.0.38/background_tasks/endpoints/logs_views.py +44 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/endpoints/outbox_api.py +4 -3
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/endpoints/outbox_views.py +6 -2
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/endpoints/views.py +4 -2
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/filters.py +3 -3
- simple_module_background_tasks-0.0.38/background_tasks/locales/de.json +222 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/locales/en.json +31 -6
- simple_module_background_tasks-0.0.38/background_tasks/log_purge.py +16 -0
- simple_module_background_tasks-0.0.38/background_tasks/log_service.py +85 -0
- simple_module_background_tasks-0.0.38/background_tasks/log_sink.py +136 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/models.py +69 -1
- simple_module_background_tasks-0.0.38/background_tasks/module.py +180 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/outbox_claim.py +3 -3
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/outbox_loop.py +2 -2
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/outbox_service.py +16 -20
- simple_module_background_tasks-0.0.38/background_tasks/outbox_sweep.py +58 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/outbox_transport.py +10 -5
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/outbox_worker.py +17 -5
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/pages/Detail.tsx +28 -11
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/pages/Index.tsx +12 -14
- simple_module_background_tasks-0.0.38/background_tasks/pages/Logs.tsx +92 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/pages/Outbox.tsx +2 -4
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/pages/Workers.tsx +17 -24
- simple_module_background_tasks-0.0.38/background_tasks/pages/api.generated.ts +965 -0
- simple_module_background_tasks-0.0.38/background_tasks/pages/api.ts +9 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/pages/components/ExecutionsTable.tsx +2 -33
- simple_module_background_tasks-0.0.38/background_tasks/pages/components/LogLevelSelect.tsx +29 -0
- simple_module_background_tasks-0.0.38/background_tasks/pages/components/LogTable.tsx +76 -0
- simple_module_background_tasks-0.0.38/background_tasks/pages/components/LogsPanel.tsx +46 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/pages/components/OutboxTable.tsx +7 -26
- simple_module_background_tasks-0.0.38/background_tasks/pages/components/ProgressCard.tsx +43 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/pages/components/WorkerHealthBanner.tsx +12 -10
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/pages/constants.ts +5 -0
- simple_module_background_tasks-0.0.38/background_tasks/pages/logs.ts +53 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/pages/outbox.ts +23 -32
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/pages/retry.ts +38 -41
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/retry_service.py +12 -6
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/runner.py +16 -2
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/services.py +4 -0
- simple_module_background_tasks-0.0.38/background_tasks/settings.py +147 -0
- simple_module_background_tasks-0.0.37/background_tasks/settings.py → simple_module_background_tasks-0.0.38/background_tasks/settings_celery.py +14 -106
- simple_module_background_tasks-0.0.38/background_tasks/settings_logs.py +45 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/signals.py +14 -55
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/sync_db.py +48 -3
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/task_heartbeat.py +55 -6
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/tasks.py +12 -13
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/tenant_context.py +80 -6
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/worker_settings.py +1 -1
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/package.json +2 -1
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/pyproject.toml +5 -5
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/tests/conftest.py +18 -1
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/tests/test_background_tasks_settings.py +3 -3
- simple_module_background_tasks-0.0.38/tests/test_bg_db_timeouts.py +95 -0
- simple_module_background_tasks-0.0.38/tests/test_bg_inprocess_sweep.py +187 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/tests/test_bg_invalidation.py +1 -1
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/tests/test_bg_invalidation_degraded.py +12 -8
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/tests/test_bg_invalidation_redis.py +43 -31
- simple_module_background_tasks-0.0.38/tests/test_bg_log_api.py +118 -0
- simple_module_background_tasks-0.0.38/tests/test_bg_log_purge.py +45 -0
- simple_module_background_tasks-0.0.38/tests/test_bg_log_sink.py +102 -0
- simple_module_background_tasks-0.0.38/tests/test_bg_log_sink_loop.py +60 -0
- simple_module_background_tasks-0.0.38/tests/test_bg_log_sink_nul.py +26 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/tests/test_bg_outbox_eager_platform.py +4 -11
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/tests/test_bg_outbox_loop_interrupt.py +1 -1
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/tests/test_bg_outbox_retry_policy.py +4 -2
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/tests/test_bg_outbox_swallowed_cancel.py +1 -1
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/tests/test_bg_outbox_thread_loops.py +19 -1
- simple_module_background_tasks-0.0.38/tests/test_bg_pagination_bounds.py +51 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/tests/test_bg_redis_url.py +3 -3
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/tests/test_bg_runner_settings.py +7 -1
- simple_module_background_tasks-0.0.38/tests/test_bg_startup_log_masking.py +26 -0
- simple_module_background_tasks-0.0.38/tests/test_bg_task_progress.py +139 -0
- simple_module_background_tasks-0.0.38/tests/test_bg_task_progress_audit.py +53 -0
- simple_module_background_tasks-0.0.38/tests/test_bg_task_status_api.py +122 -0
- simple_module_background_tasks-0.0.38/tests/test_bg_users_ordering.py +30 -0
- simple_module_background_tasks-0.0.38/tests/test_bg_worker_audit.py +129 -0
- simple_module_background_tasks-0.0.38/tests/test_bg_worker_audit_bookkeeping.py +96 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/tests/test_bg_worker_settings.py +1 -1
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/tests/test_log_context.py +5 -5
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/tests/test_outbox_concurrency.py +4 -1
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/tests/test_worker_inspector.py +4 -1
- simple_module_background_tasks-0.0.38/tests-js/background_tasks_DetailPage.test.tsx +158 -0
- simple_module_background_tasks-0.0.38/tests-js/background_tasks_IndexPage.test.tsx +180 -0
- simple_module_background_tasks-0.0.38/tests-js/background_tasks_LogsTab.test.tsx +56 -0
- simple_module_background_tasks-0.0.38/tests-js/background_tasks_OutboxPage.test.tsx +177 -0
- simple_module_background_tasks-0.0.38/tests-js/background_tasks_WorkerHealthBanner.test.tsx +63 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/tests-js/outbox.test.ts +9 -16
- simple_module_background_tasks-0.0.38/tests-js/stub-fetch.ts +18 -0
- simple_module_background_tasks-0.0.37/background_tasks/__init__.py +0 -19
- simple_module_background_tasks-0.0.37/background_tasks/module.py +0 -287
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/LICENSE +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/README.md +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/celery_app.py +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/config_guards.py +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/contracts/__init__.py +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/contracts/events.py +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/endpoints/__init__.py +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/health.py +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/invalidation.py +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/log_context.py +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/outbox_clock.py +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/outbox_dispatch.py +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/outbox_guard.py +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/outbox_support.py +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/pages/components/DetailFacts.tsx +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/pages/components/ExecutionRow.tsx +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/pages/components/PayloadCards.tsx +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/pages/components/RetryAllDialog.tsx +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/pages/components/RetryConfirmDialog.tsx +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/pages/components/RoutesTable.tsx +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/pages/components/StatusPill.tsx +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/pages/components/StatusStrip.tsx +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/pages/components/TaskFilters.tsx +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/pages/components/TasksEmpty.tsx +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/pages/components/TracebackCard.tsx +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/pages/components/WorkerCard.tsx +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/pages/components/WorkerEmptyStates.tsx +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/pages/worker-health.ts +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/py.typed +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/service.py +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/settings_env_checks.py +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/task_routing.py +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/worker_inspector.py +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/background_tasks/workers_state.py +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/tests/test_admin_api.py +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/tests/test_background_tasks_after_commit.py +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/tests/test_background_tasks_heartbeat.py +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/tests/test_background_tasks_tz_columns.py +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/tests/test_beat_schedule.py +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/tests/test_bg_config_guards.py +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/tests/test_bg_index_has_executions.py +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/tests/test_bg_index_worker_presence.py +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/tests/test_bg_list_item_payload.py +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/tests/test_bg_outbox_fence_guard.py +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/tests/test_bg_outbox_guard_savepoint.py +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/tests/test_bg_outbox_publish_scope.py +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/tests/test_bg_outbox_worker_chain.py +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/tests/test_bg_page_clamp.py +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/tests/test_bg_quiet_sweep_unrecorded.py +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/tests/test_bg_redis_health.py +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/tests/test_bg_routes_override_api.py +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/tests/test_bg_service.py +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/tests/test_bg_settings_env.py +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/tests/test_bg_task_routing.py +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/tests/test_detail_props.py +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/tests/test_execution_tenancy.py +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/tests/test_index_filters.py +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/tests/test_outbox_admin.py +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/tests/test_outbox_delivery.py +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/tests/test_outbox_tenancy.py +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/tests/test_outbox_worker_task.py +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/tests/test_retry_failed_bulk.py +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/tests/test_retry_failed_guards.py +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/tests/test_retry_sweep_ordering.py +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/tests/test_signal_tenant_stamp.py +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/tests/test_signals.py +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/tests/test_sweep_stuck.py +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/tests/test_sync_db.py +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/tests/test_sync_db_fork.py +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/tests/test_tenant_context.py +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/tests/test_worker_tenancy.py +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/tests/test_workers_endpoints.py +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/tests/test_workers_uptime.py +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/tests-js/retry.test.ts +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/tests-js/task-formatters.test.ts +0 -0
- {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.38}/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.
|
|
3
|
+
Version: 0.0.38
|
|
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]>=5.4
|
|
25
25
|
Requires-Dist: redis>=5
|
|
26
|
-
Requires-Dist: simple-module-core==0.0.
|
|
27
|
-
Requires-Dist: simple-module-db==0.0.
|
|
28
|
-
Requires-Dist: simple-module-hosting==0.0.
|
|
29
|
-
Requires-Dist: simple-module-settings==0.0.
|
|
26
|
+
Requires-Dist: simple-module-core==0.0.38
|
|
27
|
+
Requires-Dist: simple-module-db==0.0.38
|
|
28
|
+
Requires-Dist: simple-module-hosting==0.0.38
|
|
29
|
+
Requires-Dist: simple-module-settings==0.0.38
|
|
30
30
|
Description-Content-Type: text/markdown
|
|
31
31
|
|
|
32
32
|
# simple_module_background_tasks
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""BackgroundTasks module — Celery + Redis task queue with admin UI."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
from background_tasks.after_commit import enqueue_after_commit
|
|
6
|
+
from background_tasks.log_context import (
|
|
7
|
+
bind_task_context,
|
|
8
|
+
get_log_context,
|
|
9
|
+
install_log_filter,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from background_tasks.task_heartbeat import heartbeat
|
|
14
|
+
from background_tasks.worker_settings import settings_for
|
|
15
|
+
|
|
16
|
+
_LAZY_HEARTBEAT = "heartbeat"
|
|
17
|
+
_LAZY_SETTINGS = "settings_for"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def __getattr__(name: str) -> Any:
|
|
21
|
+
if name == _LAZY_HEARTBEAT:
|
|
22
|
+
from background_tasks.task_heartbeat import heartbeat
|
|
23
|
+
|
|
24
|
+
return heartbeat
|
|
25
|
+
if name == _LAZY_SETTINGS:
|
|
26
|
+
from background_tasks.worker_settings import settings_for
|
|
27
|
+
|
|
28
|
+
return settings_for
|
|
29
|
+
raise AttributeError(name)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"bind_task_context",
|
|
34
|
+
"enqueue_after_commit",
|
|
35
|
+
"get_log_context",
|
|
36
|
+
"heartbeat",
|
|
37
|
+
"install_log_filter",
|
|
38
|
+
"settings_for",
|
|
39
|
+
]
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""Startup/shutdown wiring for the BackgroundTasks module.
|
|
2
|
+
|
|
3
|
+
``BackgroundTasksModule`` keeps the ``on_startup`` / ``on_shutdown`` hooks and
|
|
4
|
+
delegates here, so the module file stays a declaration of what the module
|
|
5
|
+
contributes and this file owns the order things are installed and torn down.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from fastapi import FastAPI
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
async def startup(app: FastAPI, module_name: str) -> None:
|
|
20
|
+
"""Build the Celery app, install signal handlers, hand-off to worker."""
|
|
21
|
+
import asyncio
|
|
22
|
+
|
|
23
|
+
from simple_module_hosting.log_sinks import redact
|
|
24
|
+
|
|
25
|
+
from background_tasks.celery_app import build_celery
|
|
26
|
+
from background_tasks.db_settings import worker_database_options
|
|
27
|
+
from background_tasks.log_sink import install_log_sink
|
|
28
|
+
from background_tasks.signals import bind_event_bus
|
|
29
|
+
from background_tasks.sync_db import configure_database, set_database_url
|
|
30
|
+
from background_tasks.tenant_context import set_default_tenant
|
|
31
|
+
|
|
32
|
+
services = app.state.background_tasks
|
|
33
|
+
# Pin the sync engine to the same URL the host's async settings
|
|
34
|
+
# resolved — pydantic-settings reads ``.env`` but never propagates
|
|
35
|
+
# to ``os.environ``, so signals would otherwise fall back to the
|
|
36
|
+
# SQLite default and silently drop ``TaskExecution`` rows.
|
|
37
|
+
host = app.state.sm.settings
|
|
38
|
+
multi = bool(getattr(host, "multi_tenant", False))
|
|
39
|
+
default_tenant = None if multi else getattr(host, "default_tenant", "") or None
|
|
40
|
+
set_database_url(host.database_url, tenant_strict=multi, default_tenant=default_tenant)
|
|
41
|
+
configure_database(worker_database_options(host, services.settings))
|
|
42
|
+
set_default_tenant(default_tenant)
|
|
43
|
+
# build_celery imports `signals` for side effects and runs
|
|
44
|
+
# `autodiscover_tasks` across every installed module.
|
|
45
|
+
services.celery = build_celery(services.settings)
|
|
46
|
+
# Let signal handlers hop onto the API loop to publish events.
|
|
47
|
+
# In a standalone worker process this bind never runs, so signals
|
|
48
|
+
# stay a silent no-op — that's the documented cross-process limit.
|
|
49
|
+
bind_event_bus(app.state.sm.event_bus, asyncio.get_running_loop())
|
|
50
|
+
install_durable_transport(app)
|
|
51
|
+
install_log_sink(services.settings)
|
|
52
|
+
logger.info(
|
|
53
|
+
"BackgroundTasks: Celery app ready (broker=%s, queue=%s)",
|
|
54
|
+
redact(services.settings.broker_url),
|
|
55
|
+
services.settings.task_default_queue,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
await start_invalidation_transport(app)
|
|
59
|
+
|
|
60
|
+
# Registered here, not in register_health_checks: the Celery app does
|
|
61
|
+
# not exist until this hook builds it, and the check must follow later
|
|
62
|
+
# settings changes rather than pinning the boot-time instance.
|
|
63
|
+
from simple_module_core.health import HealthCheck
|
|
64
|
+
|
|
65
|
+
from background_tasks.health import CHECK_REDIS, build_redis_check
|
|
66
|
+
|
|
67
|
+
app.state.sm.health_registry.add(
|
|
68
|
+
HealthCheck(
|
|
69
|
+
name=CHECK_REDIS,
|
|
70
|
+
check=build_redis_check(app),
|
|
71
|
+
module=module_name,
|
|
72
|
+
# On demand only, like the mailer and storage checks. An
|
|
73
|
+
# earlier version had this on the readiness probe, reasoning
|
|
74
|
+
# that Redis is infrastructure this app owns rather than a
|
|
75
|
+
# third party. That conflates two different questions: whether
|
|
76
|
+
# this process can serve requests, and whether the system is
|
|
77
|
+
# fully functional. Failing readiness pulls the web tier out of
|
|
78
|
+
# the load balancer, turning "background jobs are backed up"
|
|
79
|
+
# into "the site is down" — and it made /health/ready report
|
|
80
|
+
# unhealthy on every deployment without Redis, which CI caught.
|
|
81
|
+
# The wizard's connection step and the settings screen's "Test
|
|
82
|
+
# connection" both run it explicitly, which is what it is for.
|
|
83
|
+
probe=False,
|
|
84
|
+
)
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def install_durable_transport(app: FastAPI) -> None:
|
|
89
|
+
"""Make ``bus.publish_durable`` write to this module's outbox (GH #459)."""
|
|
90
|
+
from background_tasks.constants import OutboxMode
|
|
91
|
+
from background_tasks.outbox_sweep import start_sweep
|
|
92
|
+
from background_tasks.outbox_transport import OutboxTransport
|
|
93
|
+
from background_tasks.outbox_worker import use_bus
|
|
94
|
+
|
|
95
|
+
bus = app.state.sm.event_bus
|
|
96
|
+
services = app.state.background_tasks
|
|
97
|
+
mode = (
|
|
98
|
+
OutboxMode.INPROCESS
|
|
99
|
+
if services.celery.conf.task_always_eager
|
|
100
|
+
else OutboxMode(services.settings.outbox_mode)
|
|
101
|
+
)
|
|
102
|
+
transport = OutboxTransport(services=services, bus=bus, db_state=app.state.sm.db, mode=mode)
|
|
103
|
+
bus.durable_transport = transport
|
|
104
|
+
if mode == OutboxMode.INPROCESS:
|
|
105
|
+
start_sweep(services, app.state.sm.db.session_factory, bus, transport.policy)
|
|
106
|
+
# An eager in-process dispatch delivers with the app's bus, which already
|
|
107
|
+
# carries every module's durable subscriptions.
|
|
108
|
+
use_bus(bus)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
async def start_invalidation_transport(app: FastAPI) -> None:
|
|
112
|
+
"""Hand the framework bus the Redis connection this module configures.
|
|
113
|
+
|
|
114
|
+
The transport is installed here rather than in a registration hook
|
|
115
|
+
because it needs a running event loop for its listener task, and it is
|
|
116
|
+
installed by *this* module because it owns the Redis URL — a cache-owning
|
|
117
|
+
module opening its own connection is the duplication GH #318 exists to
|
|
118
|
+
avoid.
|
|
119
|
+
|
|
120
|
+
A worker process that runs Celery without the FastAPI app never reaches
|
|
121
|
+
this, so it publishes nothing and hears nothing. That is the same
|
|
122
|
+
documented cross-process limit as ``bind_event_bus`` above: a task that
|
|
123
|
+
invalidates a cache should do it through an API call or accept the TTL.
|
|
124
|
+
"""
|
|
125
|
+
from background_tasks.invalidation import RedisInvalidationTransport
|
|
126
|
+
|
|
127
|
+
services = app.state.background_tasks
|
|
128
|
+
s = services.settings
|
|
129
|
+
if not s.broadcast_invalidations:
|
|
130
|
+
logger.info(
|
|
131
|
+
"BackgroundTasks: cross-worker cache invalidation disabled "
|
|
132
|
+
"(broadcast_invalidations=false) — per-process caches expire on their own TTL"
|
|
133
|
+
)
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
bus = app.state.sm.invalidation
|
|
137
|
+
transport = RedisInvalidationTransport(bus, s.broker_url, s.invalidation_channel)
|
|
138
|
+
await transport.start()
|
|
139
|
+
bus.set_transport(transport)
|
|
140
|
+
services.invalidation_transport = transport
|
|
141
|
+
logger.info(
|
|
142
|
+
"BackgroundTasks: invalidation transport on %r for channel(s) %s",
|
|
143
|
+
s.invalidation_channel,
|
|
144
|
+
", ".join(bus.channels) or "(none subscribed)",
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
async def shutdown(app: FastAPI) -> None:
|
|
149
|
+
from background_tasks.log_sink import uninstall_log_sink
|
|
150
|
+
from background_tasks.outbox_sweep import stop_sweep
|
|
151
|
+
from background_tasks.outbox_worker import reset_worker_state
|
|
152
|
+
from background_tasks.signals import unbind_event_bus
|
|
153
|
+
from background_tasks.sync_db import dispose_sync_engine
|
|
154
|
+
|
|
155
|
+
await stop_sweep(app.state.background_tasks)
|
|
156
|
+
bus = getattr(app.state.sm, "event_bus", None)
|
|
157
|
+
if bus is not None:
|
|
158
|
+
drain = getattr(bus.durable_transport, "drain", None)
|
|
159
|
+
if drain is not None:
|
|
160
|
+
# Eager deliveries still running on this loop.
|
|
161
|
+
await drain()
|
|
162
|
+
bus.durable_transport = None
|
|
163
|
+
reset_worker_state()
|
|
164
|
+
services = app.state.background_tasks
|
|
165
|
+
if services.invalidation_transport is not None:
|
|
166
|
+
app.state.sm.invalidation.clear_transport()
|
|
167
|
+
await services.invalidation_transport.stop()
|
|
168
|
+
services.invalidation_transport = None
|
|
169
|
+
if services.celery is not None:
|
|
170
|
+
services.celery.close()
|
|
171
|
+
unbind_event_bus()
|
|
172
|
+
# Before the engine goes: the final flush still needs it.
|
|
173
|
+
uninstall_log_sink()
|
|
174
|
+
dispose_sync_engine()
|
|
@@ -7,6 +7,7 @@ re-exports :func:`bind_event_bus` / :func:`unbind_event_bus`.
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
import asyncio
|
|
10
|
+
import concurrent.futures
|
|
10
11
|
import logging
|
|
11
12
|
from typing import Any
|
|
12
13
|
|
|
@@ -53,7 +54,7 @@ def publish_from_signal(event: Any) -> None:
|
|
|
53
54
|
future.add_done_callback(_log_publish_failure)
|
|
54
55
|
|
|
55
56
|
|
|
56
|
-
def _log_publish_failure(future:
|
|
57
|
+
def _log_publish_failure(future: concurrent.futures.Future[Any]) -> None:
|
|
57
58
|
if future.cancelled():
|
|
58
59
|
return
|
|
59
60
|
exc = future.exception()
|
|
@@ -7,14 +7,20 @@ These helpers are intentionally side-effect-free except for the DB write in
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
+
import logging
|
|
10
11
|
import traceback as tb_mod
|
|
12
|
+
from collections.abc import Callable
|
|
11
13
|
from datetime import UTC, datetime
|
|
12
14
|
from typing import Any
|
|
13
15
|
|
|
14
16
|
from sqlalchemy import select
|
|
15
17
|
from sqlalchemy.orm import Session
|
|
16
18
|
|
|
19
|
+
from background_tasks.constants import INTERNAL_TASK_SWEEP_OUTBOX, TaskStatus
|
|
17
20
|
from background_tasks.models import TaskExecution
|
|
21
|
+
from background_tasks.sync_db import sync_session
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
18
24
|
|
|
19
25
|
|
|
20
26
|
def now_utc() -> datetime:
|
|
@@ -47,7 +53,7 @@ def upsert_by_celery_id(
|
|
|
47
53
|
"""
|
|
48
54
|
row: TaskExecution | None = None
|
|
49
55
|
if celery_task_id:
|
|
50
|
-
stmt = select(TaskExecution).where(TaskExecution.celery_task_id == celery_task_id)
|
|
56
|
+
stmt = select(TaskExecution).where(TaskExecution.celery_task_id == celery_task_id) # ty: ignore[invalid-argument-type]
|
|
51
57
|
row = session.execute(stmt).scalar_one_or_none()
|
|
52
58
|
if row is None:
|
|
53
59
|
row = TaskExecution(celery_task_id=celery_task_id, **defaults)
|
|
@@ -103,3 +109,48 @@ def render_traceback(einfo: Any, exception: BaseException | None) -> str | None:
|
|
|
103
109
|
if exception is not None:
|
|
104
110
|
return "".join(tb_mod.format_exception(type(exception), exception, exception.__traceback__))
|
|
105
111
|
return None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# Beat ticks that fire every few seconds and are a no-op almost every time;
|
|
115
|
+
# recording each run buried real work (QA, GH #459). Only a run that went wrong
|
|
116
|
+
# is worth a row.
|
|
117
|
+
_UNRECORDED_WHEN_QUIET = frozenset({INTERNAL_TASK_SWEEP_OUTBOX})
|
|
118
|
+
_QUIET_STATUSES = frozenset({None, TaskStatus.PENDING, TaskStatus.RUNNING, TaskStatus.SUCCESS})
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def apply_signal(
|
|
122
|
+
handler: str,
|
|
123
|
+
*,
|
|
124
|
+
celery_task_id: str | None,
|
|
125
|
+
defaults: dict[str, Any],
|
|
126
|
+
after: Callable[[TaskExecution], None] | None = None,
|
|
127
|
+
tenant_id: str | None = None,
|
|
128
|
+
owner_user_id: str | None = None,
|
|
129
|
+
) -> TaskExecution | None:
|
|
130
|
+
"""Open a sync session, upsert by celery_task_id, log on failure.
|
|
131
|
+
|
|
132
|
+
Returns the upserted row (or ``None`` if the session raised) so callers
|
|
133
|
+
can read DB-assigned fields like ``id`` without a second round trip.
|
|
134
|
+
|
|
135
|
+
``tenant_id`` and ``owner_user_id`` (the message's) are written only when
|
|
136
|
+
known, so a signal whose message carries neither never blanks a row already
|
|
137
|
+
stamped.
|
|
138
|
+
"""
|
|
139
|
+
if (
|
|
140
|
+
defaults.get("task_name") in _UNRECORDED_WHEN_QUIET
|
|
141
|
+
and defaults.get("status") in _QUIET_STATUSES
|
|
142
|
+
):
|
|
143
|
+
return None
|
|
144
|
+
if tenant_id:
|
|
145
|
+
defaults = {**defaults, "tenant_id": tenant_id}
|
|
146
|
+
if owner_user_id:
|
|
147
|
+
defaults = {**defaults, "owner_user_id": owner_user_id}
|
|
148
|
+
try:
|
|
149
|
+
with sync_session() as session:
|
|
150
|
+
row = upsert_by_celery_id(session, celery_task_id=celery_task_id, defaults=defaults)
|
|
151
|
+
if after is not None:
|
|
152
|
+
after(row)
|
|
153
|
+
return row
|
|
154
|
+
except Exception:
|
|
155
|
+
logger.exception("%s failed for task_id=%s", handler, celery_task_id)
|
|
156
|
+
return None
|
|
@@ -30,13 +30,24 @@ import uuid
|
|
|
30
30
|
from collections.abc import Callable, Sequence
|
|
31
31
|
from typing import Any
|
|
32
32
|
|
|
33
|
-
from celery import current_app
|
|
34
33
|
from simple_module_db import RequestSession, bind_current_tenant, mark_written
|
|
35
34
|
from simple_module_db.transaction import SESSION_START_KEY
|
|
36
35
|
from sqlalchemy import event
|
|
37
36
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
38
37
|
from sqlalchemy.orm import Session, SessionTransaction
|
|
39
38
|
|
|
39
|
+
|
|
40
|
+
class _LazyCurrentApp:
|
|
41
|
+
"""``celery.current_app``, imported on first use so importing this module stays light."""
|
|
42
|
+
|
|
43
|
+
def __getattr__(self, name: str) -> Any:
|
|
44
|
+
from celery import current_app as _current_app
|
|
45
|
+
|
|
46
|
+
return getattr(_current_app, name)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
current_app: Any = _LazyCurrentApp()
|
|
50
|
+
|
|
40
51
|
logger = logging.getLogger(__name__)
|
|
41
52
|
|
|
42
53
|
type Publish = Callable[[], object]
|
|
@@ -18,9 +18,8 @@ TABLE_TASK_EXECUTION = f"{TABLE_PREFIX}task_execution"
|
|
|
18
18
|
TABLE_OUTBOX = f"{TABLE_PREFIX}outbox"
|
|
19
19
|
|
|
20
20
|
# ── Module dependencies ─────────────────────────────────────────
|
|
21
|
-
|
|
22
|
-
#
|
|
23
|
-
# which reads app.state.settings — so Settings must register first.
|
|
21
|
+
# Sorted after Settings for its admin editor and reload path; registering
|
|
22
|
+
# settings itself no longer needs it (the framework owns the registry, #500).
|
|
24
23
|
_MODULE_SETTINGS = "Settings"
|
|
25
24
|
|
|
26
25
|
# ── Env / settings ──────────────────────────────────────────────
|
|
@@ -42,10 +41,14 @@ API_PREFIX = "/api/background_tasks"
|
|
|
42
41
|
VIEW_PREFIX = "/admin/background-tasks"
|
|
43
42
|
ADMIN_ROUTER_PREFIX = "/admin"
|
|
44
43
|
|
|
44
|
+
STATUS_ROUTER_PREFIX = "/tasks"
|
|
45
|
+
|
|
45
46
|
# ── Menu ────────────────────────────────────────────────────────
|
|
46
47
|
MENU_LABEL = "Background tasks"
|
|
47
48
|
MENU_ICON = "activity"
|
|
48
49
|
MENU_ORDER = 120
|
|
50
|
+
LOGS_MENU_LABEL = "Logs"
|
|
51
|
+
LOGS_MENU_ICON = "scroll-text"
|
|
49
52
|
|
|
50
53
|
# ── Page identifiers ────────────────────────────────────────────
|
|
51
54
|
# Kept as literals at the call site (see endpoints/views.py) so
|
|
@@ -141,7 +144,7 @@ class OutboxScope(StrEnum):
|
|
|
141
144
|
|
|
142
145
|
|
|
143
146
|
DEFAULT_OUTBOX_MAX_ATTEMPTS = 8
|
|
144
|
-
DEFAULT_OUTBOX_SWEEP_INTERVAL_SECONDS =
|
|
147
|
+
DEFAULT_OUTBOX_SWEEP_INTERVAL_SECONDS = 5
|
|
145
148
|
# How long a claimed row stays ``running`` before another worker may reclaim it.
|
|
146
149
|
# Durable handlers are cut off ``outbox_lease_margin`` before it, so no run can
|
|
147
150
|
# outlive its claim and overlap the rerun a reclaim would start.
|
|
@@ -173,3 +176,26 @@ OUTBOX_KEY_MAX_LENGTH = 255
|
|
|
173
176
|
TENANT_ID_MAX_LENGTH = 50
|
|
174
177
|
# Filter value meaning "executions with no tenant" (beat / platform publishes).
|
|
175
178
|
PLATFORM_TENANT_FILTER = "__platform__"
|
|
179
|
+
|
|
180
|
+
OWNER_ID_MAX_LENGTH = 64
|
|
181
|
+
PROGRESS_MESSAGE_MAX_LENGTH = 255
|
|
182
|
+
PROGRESS_PHASE_MAX_LENGTH = 64
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class OutboxMode(StrEnum):
|
|
186
|
+
CELERY = "celery"
|
|
187
|
+
INPROCESS = "inprocess"
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
# ── Retained logs (GH #537) ─────────────────────────────────────
|
|
191
|
+
TABLE_LOG_RECORD = f"{TABLE_PREFIX}log_record"
|
|
192
|
+
LOG_MESSAGE_MAX_LENGTH = 4096
|
|
193
|
+
LOG_NAME_MAX_LENGTH = 255
|
|
194
|
+
LOG_ID_MAX_LENGTH = 64
|
|
195
|
+
LOG_EXTRA_MAX_KEYS = 20
|
|
196
|
+
LOG_EXTRA_VALUE_MAX_LENGTH = 1000
|
|
197
|
+
DEFAULT_LOG_SINK_LEVEL = "INFO"
|
|
198
|
+
DEFAULT_LOG_RETENTION_DAYS = 7
|
|
199
|
+
LOG_PER_PAGE = 50
|
|
200
|
+
# Level names the admin filter offers, lowest first.
|
|
201
|
+
LOG_LEVEL_NAMES = ("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL")
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""DTOs for the retained-log screens (GH #537)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import uuid
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from simple_module_db.types import AwareDateTime
|
|
10
|
+
from sqlmodel import Field, SQLModel
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class LogItem(SQLModel):
|
|
14
|
+
id: uuid.UUID
|
|
15
|
+
ts: datetime = Field(sa_type=AwareDateTime())
|
|
16
|
+
level: int
|
|
17
|
+
level_name: str
|
|
18
|
+
logger: str
|
|
19
|
+
message: str
|
|
20
|
+
task_id: str | None = None
|
|
21
|
+
job_id: str | None = None
|
|
22
|
+
tenant_id: str | None = None
|
|
23
|
+
correlation_id: str | None = None
|
|
24
|
+
extra: dict[str, Any] = Field(default_factory=dict)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class LogListResponse(SQLModel):
|
|
28
|
+
items: list[LogItem]
|
|
29
|
+
total: int
|
|
30
|
+
page: int
|
|
31
|
+
per_page: int
|
|
@@ -15,7 +15,7 @@ from background_tasks.constants import OutboxStatus
|
|
|
15
15
|
class OutboxItem(SQLModel):
|
|
16
16
|
"""One (event, durable handler) delivery as the admin screen shows it."""
|
|
17
17
|
|
|
18
|
-
model_config = ConfigDict(from_attributes=True)
|
|
18
|
+
model_config = ConfigDict(from_attributes=True) # ty: ignore[invalid-assignment]
|
|
19
19
|
|
|
20
20
|
id: uuid.UUID
|
|
21
21
|
event_key: str
|
|
@@ -6,7 +6,7 @@ import uuid
|
|
|
6
6
|
from datetime import datetime
|
|
7
7
|
from typing import Any
|
|
8
8
|
|
|
9
|
-
from pydantic import ConfigDict
|
|
9
|
+
from pydantic import ConfigDict, field_validator
|
|
10
10
|
from sqlmodel import SQLModel
|
|
11
11
|
|
|
12
12
|
from background_tasks.constants import TaskStatus
|
|
@@ -21,7 +21,7 @@ class TaskExecutionListItem(SQLModel):
|
|
|
21
21
|
the operator needs to read it *before* confirming, not on a second page.
|
|
22
22
|
"""
|
|
23
23
|
|
|
24
|
-
model_config = ConfigDict(from_attributes=True)
|
|
24
|
+
model_config = ConfigDict(from_attributes=True) # ty: ignore[invalid-assignment]
|
|
25
25
|
|
|
26
26
|
id: uuid.UUID
|
|
27
27
|
celery_task_id: str | None = None
|
|
@@ -39,11 +39,21 @@ class TaskExecutionListItem(SQLModel):
|
|
|
39
39
|
exception_type: str | None = None
|
|
40
40
|
retried_from_id: uuid.UUID | None = None
|
|
41
41
|
|
|
42
|
+
@field_validator("args", mode="before")
|
|
43
|
+
@classmethod
|
|
44
|
+
def _args_none(cls, v: Any) -> Any:
|
|
45
|
+
return [] if v is None else v
|
|
46
|
+
|
|
47
|
+
@field_validator("kwargs", mode="before")
|
|
48
|
+
@classmethod
|
|
49
|
+
def _kwargs_none(cls, v: Any) -> Any:
|
|
50
|
+
return {} if v is None else v
|
|
51
|
+
|
|
42
52
|
|
|
43
53
|
class TaskExecutionDetail(SQLModel):
|
|
44
54
|
"""Full row for the detail page (adds args/kwargs/result/traceback)."""
|
|
45
55
|
|
|
46
|
-
model_config = ConfigDict(from_attributes=True)
|
|
56
|
+
model_config = ConfigDict(from_attributes=True) # ty: ignore[invalid-assignment]
|
|
47
57
|
|
|
48
58
|
id: uuid.UUID
|
|
49
59
|
celery_task_id: str | None = None
|
|
@@ -59,6 +69,45 @@ class TaskExecutionDetail(SQLModel):
|
|
|
59
69
|
worker: str | None = None
|
|
60
70
|
retries: int = 0
|
|
61
71
|
retried_from_id: uuid.UUID | None = None
|
|
72
|
+
owner_user_id: str | None = None
|
|
73
|
+
progress_current: int | None = None
|
|
74
|
+
progress_total: int | None = None
|
|
75
|
+
progress_message: str | None = None
|
|
76
|
+
progress_phase: str | None = None
|
|
77
|
+
queued_at: datetime | None = None
|
|
78
|
+
started_at: datetime | None = None
|
|
79
|
+
finished_at: datetime | None = None
|
|
80
|
+
heartbeat_at: datetime | None = None
|
|
81
|
+
|
|
82
|
+
@field_validator("args", mode="before")
|
|
83
|
+
@classmethod
|
|
84
|
+
def _args_none(cls, v: Any) -> Any:
|
|
85
|
+
return [] if v is None else v
|
|
86
|
+
|
|
87
|
+
@field_validator("kwargs", mode="before")
|
|
88
|
+
@classmethod
|
|
89
|
+
def _kwargs_none(cls, v: Any) -> Any:
|
|
90
|
+
return {} if v is None else v
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class TaskStatusDTO(SQLModel):
|
|
94
|
+
"""What the owner of a task may see of it (#536).
|
|
95
|
+
|
|
96
|
+
Deliberately narrower than :class:`TaskExecutionDetail`: no args, kwargs,
|
|
97
|
+
result or traceback, and the error is the exception's *type name* only —
|
|
98
|
+
a message or traceback can carry other users' data or server internals.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
model_config = ConfigDict(from_attributes=True) # ty: ignore[invalid-assignment]
|
|
102
|
+
|
|
103
|
+
celery_task_id: str
|
|
104
|
+
task_name: str
|
|
105
|
+
status: TaskStatus
|
|
106
|
+
progress_current: int | None = None
|
|
107
|
+
progress_total: int | None = None
|
|
108
|
+
progress_message: str | None = None
|
|
109
|
+
progress_phase: str | None = None
|
|
110
|
+
exception_type: str | None = None
|
|
62
111
|
queued_at: datetime | None = None
|
|
63
112
|
started_at: datetime | None = None
|
|
64
113
|
finished_at: datetime | None = None
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Restart-required worker engine settings."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from pydantic import Field
|
|
6
|
+
from pydantic_settings import BaseSettings
|
|
7
|
+
|
|
8
|
+
_RESTART = {"requires_restart": True, "group": "Database"}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class WorkerDatabaseSettings(BaseSettings):
|
|
12
|
+
"""Inherited by BackgroundTasksSettings with its SM_BG_TASKS_ env prefix."""
|
|
13
|
+
|
|
14
|
+
db_pool_size: int = Field(default=2, ge=1, json_schema_extra=_RESTART)
|
|
15
|
+
db_max_overflow: int = Field(default=3, ge=0, json_schema_extra=_RESTART)
|
|
16
|
+
db_recycle: int = Field(default=1800, ge=0, json_schema_extra=_RESTART)
|
|
17
|
+
db_statement_timeout_ms: int | None = Field(default=None, ge=0, json_schema_extra=_RESTART)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def worker_database_options(host: Any, worker: WorkerDatabaseSettings) -> dict[str, int]:
|
|
21
|
+
"""Use host timeouts unless the worker has an explicit statement limit."""
|
|
22
|
+
return {
|
|
23
|
+
"pool_size": worker.db_pool_size,
|
|
24
|
+
"max_overflow": worker.db_max_overflow,
|
|
25
|
+
"pool_recycle": worker.db_recycle,
|
|
26
|
+
"pool_timeout": host.db_pool_timeout,
|
|
27
|
+
"connect_timeout": host.db_connect_timeout,
|
|
28
|
+
"statement_timeout_ms": (
|
|
29
|
+
host.db_statement_timeout_ms
|
|
30
|
+
if worker.db_statement_timeout_ms is None
|
|
31
|
+
else worker.db_statement_timeout_ms
|
|
32
|
+
),
|
|
33
|
+
"idle_in_tx_timeout_ms": host.db_idle_in_tx_timeout_ms,
|
|
34
|
+
}
|
|
@@ -8,6 +8,7 @@ from simple_module_core.permissions import is_admin
|
|
|
8
8
|
from simple_module_db import RequestSession, current_tenant_id
|
|
9
9
|
from simple_module_db.deps import get_db
|
|
10
10
|
|
|
11
|
+
from background_tasks.log_service import LogScope, LogService
|
|
11
12
|
from background_tasks.outbox_service import OutboxScope, OutboxService
|
|
12
13
|
from background_tasks.outbox_transport import OutboxTransport
|
|
13
14
|
from background_tasks.service import BackgroundTaskService
|
|
@@ -49,3 +50,11 @@ def outbox_scope(request: Request) -> OutboxScope:
|
|
|
49
50
|
return OutboxScope(tenant_id=tenant_id)
|
|
50
51
|
multi_tenant = bool(getattr(request.app.state.sm.settings, "multi_tenant", False))
|
|
51
52
|
return OutboxScope(everything=not multi_tenant)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
async def get_log_service(
|
|
56
|
+
request: Request,
|
|
57
|
+
db: RequestSession = Depends(get_db),
|
|
58
|
+
) -> LogService:
|
|
59
|
+
scope = outbox_scope(request)
|
|
60
|
+
return LogService(db=db, scope=LogScope(everything=scope.everything, tenant_id=scope.tenant_id))
|
|
@@ -5,6 +5,8 @@ from __future__ import annotations
|
|
|
5
5
|
import uuid
|
|
6
6
|
|
|
7
7
|
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
|
8
|
+
from simple_module_db.pagination import PageParams
|
|
9
|
+
from simple_module_hosting.pagination import page_params
|
|
8
10
|
from simple_module_hosting.permissions import RequiresPermission
|
|
9
11
|
|
|
10
12
|
from background_tasks.constants import (
|
|
@@ -38,8 +40,7 @@ async def list_executions(
|
|
|
38
40
|
task_name: str | None = Query(default=None, deprecated=True),
|
|
39
41
|
queue: str | None = Query(default=None),
|
|
40
42
|
tenant_id: str | None = Query(default=None),
|
|
41
|
-
|
|
42
|
-
per_page: int = Query(default=20, ge=1, le=200),
|
|
43
|
+
params: PageParams = Depends(page_params(20)),
|
|
43
44
|
service: BackgroundTaskService = Depends(get_background_task_service),
|
|
44
45
|
) -> TaskExecutionListResponse:
|
|
45
46
|
"""List executions, filtered exactly as the page filters them.
|
|
@@ -55,8 +56,8 @@ async def list_executions(
|
|
|
55
56
|
task_name=q or task_name,
|
|
56
57
|
queue=queue,
|
|
57
58
|
tenant_id=tenant_id,
|
|
58
|
-
page=page,
|
|
59
|
-
per_page=per_page,
|
|
59
|
+
page=params.page,
|
|
60
|
+
per_page=params.per_page,
|
|
60
61
|
)
|
|
61
62
|
|
|
62
63
|
|