simple-module-background-tasks 0.0.41__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.
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/PKG-INFO +5 -5
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/__init__.py +6 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/_signal_support.py +29 -12
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/celery_app.py +2 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/constants.py +6 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/endpoints/api_status.py +26 -10
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/models.py +11 -1
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/outbox_dispatch.py +6 -2
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/retry_service.py +12 -1
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/settings.py +28 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/signals.py +22 -13
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/sync_db.py +15 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/task_heartbeat.py +2 -1
- simple_module_background_tasks-0.0.42/background_tasks/task_resource.py +55 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/tasks.py +2 -1
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/tenant_context.py +5 -3
- simple_module_background_tasks-0.0.42/background_tasks/tracking.py +55 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/package.json +5 -5
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/pyproject.toml +5 -5
- simple_module_background_tasks-0.0.42/tests/test_background_tasks_delivery_context.py +72 -0
- simple_module_background_tasks-0.0.42/tests/test_background_tasks_resource_status.py +145 -0
- simple_module_background_tasks-0.0.42/tests/test_background_tasks_resource_transport.py +129 -0
- simple_module_background_tasks-0.0.42/tests/test_background_tasks_tracking_modes.py +245 -0
- simple_module_background_tasks-0.0.42/tests/test_background_tasks_worker_tenancy.py +35 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_bg_service.py +1 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/.gitignore +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/LICENSE +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/README.md +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/_lifecycle.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/_signal_bus.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/after_commit.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/config_guards.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/contracts/__init__.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/contracts/events.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/contracts/logs.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/contracts/outbox.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/contracts/schemas.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/db_settings.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/deps.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/endpoints/__init__.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/endpoints/api_admin.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/endpoints/logs_api.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/endpoints/logs_views.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/endpoints/outbox_api.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/endpoints/outbox_views.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/endpoints/views.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/filters.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/health.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/invalidation.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/locales/de.json +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/locales/en.json +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/log_context.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/log_purge.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/log_service.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/log_sink.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/module.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/outbox_claim.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/outbox_clock.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/outbox_guard.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/outbox_loop.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/outbox_service.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/outbox_support.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/outbox_sweep.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/outbox_transport.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/outbox_worker.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/pages/Detail.tsx +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/pages/Index.tsx +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/pages/Logs.tsx +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/pages/Outbox.tsx +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/pages/Workers.tsx +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/pages/api.generated.ts +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/pages/api.ts +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/pages/components/DetailFacts.tsx +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/pages/components/ExecutionRow.tsx +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/pages/components/ExecutionsTable.tsx +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/pages/components/LogLevelSelect.tsx +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/pages/components/LogTable.tsx +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/pages/components/LogsPanel.tsx +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/pages/components/OutboxTable.tsx +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/pages/components/PayloadCards.tsx +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/pages/components/ProgressCard.tsx +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/pages/components/RetryAllDialog.tsx +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/pages/components/RetryConfirmDialog.tsx +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/pages/components/RoutesTable.tsx +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/pages/components/StatusPill.tsx +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/pages/components/StatusStrip.tsx +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/pages/components/TaskFilters.tsx +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/pages/components/TasksEmpty.tsx +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/pages/components/TracebackCard.tsx +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/pages/components/WorkerCard.tsx +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/pages/components/WorkerEmptyStates.tsx +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/pages/components/WorkerHealthBanner.tsx +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/pages/constants.ts +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/pages/logs.ts +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/pages/outbox.ts +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/pages/retry.ts +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/pages/worker-health.ts +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/py.typed +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/runner.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/service.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/services.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/settings_celery.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/settings_env_checks.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/settings_logs.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/task_routing.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/worker_inspector.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/worker_settings.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/background_tasks/workers_state.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/conftest.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_admin_api.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_background_tasks_after_commit.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_background_tasks_heartbeat.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_background_tasks_settings.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_background_tasks_tz_columns.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_beat_schedule.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_bg_config_guards.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_bg_db_timeouts.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_bg_index_has_executions.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_bg_index_worker_presence.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_bg_inprocess_sweep.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_bg_invalidation.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_bg_invalidation_degraded.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_bg_invalidation_redis.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_bg_list_item_payload.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_bg_log_api.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_bg_log_purge.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_bg_log_sink.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_bg_log_sink_loop.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_bg_log_sink_nul.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_bg_outbox_eager_platform.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_bg_outbox_eager_serialization.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_bg_outbox_fence_guard.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_bg_outbox_guard_savepoint.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_bg_outbox_loop_interrupt.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_bg_outbox_publish_scope.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_bg_outbox_retry_policy.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_bg_outbox_swallowed_cancel.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_bg_outbox_thread_loops.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_bg_outbox_worker_chain.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_bg_page_clamp.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_bg_pagination_bounds.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_bg_quiet_sweep_unrecorded.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_bg_redis_health.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_bg_redis_url.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_bg_routes_override_api.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_bg_runner_settings.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_bg_settings_env.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_bg_startup_log_masking.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_bg_task_progress.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_bg_task_progress_audit.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_bg_task_routing.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_bg_task_status_api.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_bg_users_ordering.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_bg_worker_audit.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_bg_worker_audit_bookkeeping.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_bg_worker_settings.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_detail_props.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_execution_tenancy.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_index_filters.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_log_context.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_outbox_admin.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_outbox_concurrency.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_outbox_delivery.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_outbox_tenancy.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_outbox_worker_task.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_retry_failed_bulk.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_retry_failed_guards.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_retry_sweep_ordering.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_signal_tenant_stamp.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_signals.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_sweep_stuck.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_sync_db.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_sync_db_fork.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_tenant_context.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_worker_inspector.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_worker_tenancy.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_workers_endpoints.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests/test_workers_uptime.py +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests-js/background_tasks_DetailPage.test.tsx +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests-js/background_tasks_IndexPage.test.tsx +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests-js/background_tasks_LogsTab.test.tsx +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests-js/background_tasks_OutboxPage.test.tsx +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests-js/background_tasks_WorkerHealthBanner.test.tsx +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests-js/outbox.test.ts +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests-js/retry.test.ts +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests-js/stub-fetch.ts +0 -0
- {simple_module_background_tasks-0.0.41 → simple_module_background_tasks-0.0.42}/tests-js/task-formatters.test.ts +0 -0
- {simple_module_background_tasks-0.0.41 → 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.
|
|
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.
|
|
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.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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
5
|
-
is marked ``Authenticated()`` (SM031); 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
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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
|
|
@@ -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
|
-
|
|
124
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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",
|