python-platform 0.1.1__py3-none-any.whl
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.
- python_platform/__init__.py +445 -0
- python_platform/application/__init__.py +6 -0
- python_platform/application/build_spec.py +37 -0
- python_platform/application/builder.py +260 -0
- python_platform/application/composition.py +219 -0
- python_platform/application/runtime.py +546 -0
- python_platform/application/runtime_hooks.py +327 -0
- python_platform/application/state.py +216 -0
- python_platform/application_services/__init__.py +54 -0
- python_platform/application_services/catalog.py +500 -0
- python_platform/application_services/contracts.py +188 -0
- python_platform/application_services/dispatcher.py +423 -0
- python_platform/application_services/errors.py +89 -0
- python_platform/application_services/execution.py +199 -0
- python_platform/application_services/interceptors.py +44 -0
- python_platform/application_services/invocation.py +427 -0
- python_platform/application_services/policies.py +46 -0
- python_platform/application_services/seeding.py +72 -0
- python_platform/application_services/signature.py +49 -0
- python_platform/application_services/validation.py +84 -0
- python_platform/auditing/__init__.py +12 -0
- python_platform/auditing/contracts.py +39 -0
- python_platform/auditing/control.py +48 -0
- python_platform/auditing/sqlalchemy/__init__.py +6 -0
- python_platform/auditing/sqlalchemy/migrations/0001_platform_auditing.py +45 -0
- python_platform/auditing/sqlalchemy/migrations/__init__.py +1 -0
- python_platform/auditing/sqlalchemy/models.py +31 -0
- python_platform/auditing/sqlalchemy/module.py +32 -0
- python_platform/auditing/sqlalchemy/store.py +63 -0
- python_platform/authorization/__init__.py +38 -0
- python_platform/authorization/catalog.py +63 -0
- python_platform/authorization/contracts.py +241 -0
- python_platform/authorization/definition_discovery.py +81 -0
- python_platform/authorization/definitions.py +43 -0
- python_platform/authorization/errors.py +40 -0
- python_platform/background_execution/__init__.py +15 -0
- python_platform/background_execution/application.py +41 -0
- python_platform/background_execution/child.py +123 -0
- python_platform/background_execution/contracts.py +39 -0
- python_platform/background_execution/lifecycle.py +22 -0
- python_platform/background_execution/local.py +127 -0
- python_platform/background_execution/locks.py +31 -0
- python_platform/background_execution/management.py +17 -0
- python_platform/background_execution/module.py +10 -0
- python_platform/background_execution/permissions.py +14 -0
- python_platform/background_execution/processes.py +331 -0
- python_platform/background_jobs/__init__.py +31 -0
- python_platform/background_jobs/catalog.py +287 -0
- python_platform/background_jobs/contracts.py +202 -0
- python_platform/background_jobs/errors.py +28 -0
- python_platform/background_jobs/execution.py +201 -0
- python_platform/background_jobs/pgqueuer/UPSTREAM_LICENSE.txt +21 -0
- python_platform/background_jobs/pgqueuer/__init__.py +6 -0
- python_platform/background_jobs/pgqueuer/enqueue.py +82 -0
- python_platform/background_jobs/pgqueuer/migrations/0001_pgqueuer_1_3_2.py +63 -0
- python_platform/background_jobs/pgqueuer/migrations/__init__.py +1 -0
- python_platform/background_jobs/pgqueuer/module.py +102 -0
- python_platform/background_jobs/pgqueuer/options.py +70 -0
- python_platform/background_jobs/pgqueuer/runtime.py +479 -0
- python_platform/background_jobs/pgqueuer/sql/pgqueuer_1_3_2_install.sql +118 -0
- python_platform/background_jobs/pgqueuer/supervision.py +201 -0
- python_platform/background_workers/__init__.py +22 -0
- python_platform/background_workers/catalog.py +96 -0
- python_platform/background_workers/contracts.py +124 -0
- python_platform/background_workers/errors.py +20 -0
- python_platform/background_workers/execution.py +133 -0
- python_platform/background_workers/runtime.py +203 -0
- python_platform/caching/__init__.py +11 -0
- python_platform/caching/catalog.py +73 -0
- python_platform/caching/contracts.py +73 -0
- python_platform/caching/errors.py +20 -0
- python_platform/cli/__init__.py +87 -0
- python_platform/cli/errors.py +5 -0
- python_platform/cli/project.py +69 -0
- python_platform/cli/runtime.py +68 -0
- python_platform/configuration/__init__.py +21 -0
- python_platform/configuration/composition.py +65 -0
- python_platform/configuration/contracts.py +68 -0
- python_platform/configuration/dotenv_source.py +51 -0
- python_platform/configuration/environment_source.py +38 -0
- python_platform/configuration/immutability.py +124 -0
- python_platform/configuration/input_shape.py +58 -0
- python_platform/configuration/merge.py +103 -0
- python_platform/configuration/root.py +215 -0
- python_platform/configuration/sources.py +519 -0
- python_platform/configuration/values.py +427 -0
- python_platform/configuration/yaml_source.py +51 -0
- python_platform/developer_kit/__init__.py +1 -0
- python_platform/developer_kit/generation.py +182 -0
- python_platform/developer_kit/project_metadata.py +45 -0
- python_platform/developer_kit/source.py +53 -0
- python_platform/developer_kit/templates/module/cookiecutter.json +1 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/__init__.py.jinja +1 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application/__init__.py.jinja +1 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application/cache.py.jinja +10 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application/events.py.jinja +24 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application/integration.py.jinja +46 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application/module.py.jinja +29 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application/options.py.jinja +8 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application/orders.py.jinja +81 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application/tasks.py.jinja +86 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/__init__.py.jinja +1 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/module.py.jinja +10 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/orders.py.jinja +36 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/__init__.py.jinja +1 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/module.py.jinja +10 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/orders.py.jinja +57 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/repository.py.jinja +12 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/seeding.py.jinja +19 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/settings.py.jinja +16 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/__init__.py.jinja +1 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/definitions.py.jinja +24 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/module.py.jinja +7 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/permissions.py.jinja +15 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/__init__.py.jinja +1 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/files.py.jinja +74 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/module.py.jinja +58 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/realtime.py.jinja +37 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/__init__.py.jinja +1 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/migrations/__init__.py.jinja +1 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/models.py.jinja +26 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/module.py.jinja +32 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/repository.py.jinja +49 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/tests/test_domain.py.jinja +18 -0
- python_platform/developer_kit/templates/project/cookiecutter.json +6 -0
- python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/.dockerignore +8 -0
- python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/.env.jinja +13 -0
- python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/.gitignore +6 -0
- python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/.python-version +1 -0
- python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/Dockerfile +22 -0
- python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/README.md +86 -0
- python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/app.yaml.jinja +16 -0
- python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/compose.dev.yaml.jinja +24 -0
- python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/compose.production.yaml.jinja +13 -0
- python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/pyproject.toml.jinja +28 -0
- python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/src/host/__init__.py.jinja +1 -0
- python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/src/host/main.py.jinja +52 -0
- python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/src/host/module.py.jinja +38 -0
- python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/tests/conftest.py.jinja +8 -0
- python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/tests/host/test_http.py.jinja +101 -0
- python_platform/developer_kit/wiring.py +85 -0
- python_platform/diagnostics/__init__.py +23 -0
- python_platform/diagnostics/journal.py +174 -0
- python_platform/diagnostics/model.py +83 -0
- python_platform/diagnostics/source.py +25 -0
- python_platform/distributed_lock/__init__.py +6 -0
- python_platform/distributed_lock/contracts.py +20 -0
- python_platform/distributed_lock/options.py +19 -0
- python_platform/domain/__init__.py +13 -0
- python_platform/domain/aggregates.py +64 -0
- python_platform/domain/entities.py +33 -0
- python_platform/domain/errors.py +12 -0
- python_platform/domain/value_objects.py +31 -0
- python_platform/errors/__init__.py +75 -0
- python_platform/errors/base.py +13 -0
- python_platform/errors/business.py +53 -0
- python_platform/errors/configuration.py +119 -0
- python_platform/errors/diagnostics.py +12 -0
- python_platform/errors/lifecycle.py +164 -0
- python_platform/errors/modularity.py +100 -0
- python_platform/errors/services.py +100 -0
- python_platform/events/__init__.py +25 -0
- python_platform/events/aggregate.py +39 -0
- python_platform/events/catalog.py +136 -0
- python_platform/events/contracts.py +39 -0
- python_platform/events/contribution.py +128 -0
- python_platform/events/discovery.py +105 -0
- python_platform/events/errors.py +48 -0
- python_platform/events/runtime.py +192 -0
- python_platform/fastapi/__init__.py +49 -0
- python_platform/fastapi/action.py +175 -0
- python_platform/fastapi/adapter.py +381 -0
- python_platform/fastapi/application_services.py +647 -0
- python_platform/fastapi/background.py +42 -0
- python_platform/fastapi/contracts.py +278 -0
- python_platform/fastapi/errors.py +32 -0
- python_platform/fastapi/filters.py +111 -0
- python_platform/fastapi/health.py +44 -0
- python_platform/fastapi/http.py +55 -0
- python_platform/fastapi/http_router.py +212 -0
- python_platform/fastapi/manual_action.py +105 -0
- python_platform/fastapi/middleware.py +93 -0
- python_platform/fastapi/parameters.py +97 -0
- python_platform/fastapi/realtime/__init__.py +13 -0
- python_platform/fastapi/realtime/authentication.py +181 -0
- python_platform/fastapi/realtime/connection.py +166 -0
- python_platform/fastapi/realtime/module.py +41 -0
- python_platform/fastapi/realtime/options.py +50 -0
- python_platform/fastapi/realtime/runtime.py +423 -0
- python_platform/fastapi/request_context.py +244 -0
- python_platform/fastapi/route_integrity.py +88 -0
- python_platform/fastapi/routing.py +410 -0
- python_platform/fastapi/server.py +181 -0
- python_platform/fastapi/settings.py +40 -0
- python_platform/fastapi/tracing.py +62 -0
- python_platform/fastapi/transfer.py +128 -0
- python_platform/fastapi/upload_limits.py +63 -0
- python_platform/fastapi/uploads.py +94 -0
- python_platform/hosted_services/__init__.py +29 -0
- python_platform/hosted_services/bridge.py +420 -0
- python_platform/hosted_services/catalog.py +259 -0
- python_platform/hosted_services/contracts.py +55 -0
- python_platform/hosted_services/errors.py +53 -0
- python_platform/hosted_services/options.py +17 -0
- python_platform/hosted_services/runtime.py +267 -0
- python_platform/hosted_services/state.py +40 -0
- python_platform/hosting/__init__.py +4 -0
- python_platform/hosting/instance.py +40 -0
- python_platform/identity/__init__.py +73 -0
- python_platform/identity/application.py +287 -0
- python_platform/identity/contracts.py +283 -0
- python_platform/identity/errors.py +28 -0
- python_platform/identity/http_api.py +90 -0
- python_platform/identity/module.py +46 -0
- python_platform/identity/passwords.py +34 -0
- python_platform/identity/permissions.py +14 -0
- python_platform/identity/services.py +108 -0
- python_platform/identity/sqlalchemy/__init__.py +5 -0
- python_platform/identity/sqlalchemy/migrations/0001_platform_identity.py +151 -0
- python_platform/identity/sqlalchemy/migrations/0002_physical_delete.py +42 -0
- python_platform/identity/sqlalchemy/migrations/__init__.py +1 -0
- python_platform/identity/sqlalchemy/models.py +85 -0
- python_platform/identity/sqlalchemy/module.py +62 -0
- python_platform/identity/sqlalchemy/stores.py +526 -0
- python_platform/identity/tokens.py +106 -0
- python_platform/invocation/__init__.py +3 -0
- python_platform/invocation/callables.py +171 -0
- python_platform/invocation/contracts.py +33 -0
- python_platform/invocation/entries.py +54 -0
- python_platform/invocation/function_runtime.py +48 -0
- python_platform/invocation/interception.py +184 -0
- python_platform/lifecycle/__init__.py +25 -0
- python_platform/lifecycle/composition.py +35 -0
- python_platform/lifecycle/context.py +31 -0
- python_platform/lifecycle/runtime.py +43 -0
- python_platform/lifecycle/state.py +20 -0
- python_platform/modularity/__init__.py +15 -0
- python_platform/modularity/contracts.py +90 -0
- python_platform/modularity/discovery.py +143 -0
- python_platform/modularity/graph.py +334 -0
- python_platform/modularity/registry.py +100 -0
- python_platform/modularity/selection.py +16 -0
- python_platform/notifications/__init__.py +19 -0
- python_platform/notifications/catalog.py +46 -0
- python_platform/notifications/contracts.py +107 -0
- python_platform/observability/__init__.py +5 -0
- python_platform/observability/context.py +43 -0
- python_platform/observability/export.py +73 -0
- python_platform/observability/formatting.py +89 -0
- python_platform/observability/logging.py +118 -0
- python_platform/observability/options.py +45 -0
- python_platform/observability/tracing.py +80 -0
- python_platform/options/__init__.py +19 -0
- python_platform/options/aliases.py +331 -0
- python_platform/options/contribution.py +62 -0
- python_platform/options/immutability.py +48 -0
- python_platform/options/input_keys.py +214 -0
- python_platform/options/issues.py +335 -0
- python_platform/options/models.py +114 -0
- python_platform/options/registry.py +280 -0
- python_platform/options/schema.py +488 -0
- python_platform/options/validation.py +120 -0
- python_platform/py.typed +0 -0
- python_platform/realtime/__init__.py +12 -0
- python_platform/realtime/contracts.py +28 -0
- python_platform/realtime/diagnostics.py +37 -0
- python_platform/realtime/messages.py +113 -0
- python_platform/redis/__init__.py +6 -0
- python_platform/redis/distributed_lock.py +212 -0
- python_platform/redis/lease_lock.py +36 -0
- python_platform/redis/module.py +50 -0
- python_platform/redis/notification_runtime.py +182 -0
- python_platform/redis/notifications.py +25 -0
- python_platform/redis/options.py +35 -0
- python_platform/redis/runtime.py +116 -0
- python_platform/services/__init__.py +39 -0
- python_platform/services/application_bindings.py +221 -0
- python_platform/services/arbitration.py +381 -0
- python_platform/services/binding.py +102 -0
- python_platform/services/contribution.py +438 -0
- python_platform/services/convention.py +292 -0
- python_platform/services/convention_contracts.py +94 -0
- python_platform/services/exposure.py +23 -0
- python_platform/services/framework_provider.py +178 -0
- python_platform/services/native_graph.py +172 -0
- python_platform/services/provider.py +204 -0
- python_platform/services/registration.py +101 -0
- python_platform/services/runtime.py +298 -0
- python_platform/settings/__init__.py +30 -0
- python_platform/settings/application.py +95 -0
- python_platform/settings/binding.py +33 -0
- python_platform/settings/catalog.py +121 -0
- python_platform/settings/changes.py +14 -0
- python_platform/settings/contracts.py +115 -0
- python_platform/settings/definition_discovery.py +71 -0
- python_platform/settings/definitions.py +41 -0
- python_platform/settings/errors.py +22 -0
- python_platform/settings/handlers.py +43 -0
- python_platform/settings/management.py +57 -0
- python_platform/settings/manager.py +89 -0
- python_platform/settings/module.py +7 -0
- python_platform/settings/notifications.py +39 -0
- python_platform/settings/permissions.py +14 -0
- python_platform/settings/provider.py +74 -0
- python_platform/settings/refresh.py +140 -0
- python_platform/settings/refresh_module.py +45 -0
- python_platform/settings/sqlalchemy/__init__.py +5 -0
- python_platform/settings/sqlalchemy/migrations/0001_platform_settings.py +31 -0
- python_platform/settings/sqlalchemy/migrations/0002_physical_delete.py +32 -0
- python_platform/settings/sqlalchemy/migrations/0003_version_tokens.py +39 -0
- python_platform/settings/sqlalchemy/migrations/__init__.py +1 -0
- python_platform/settings/sqlalchemy/models.py +36 -0
- python_platform/settings/sqlalchemy/module.py +36 -0
- python_platform/settings/sqlalchemy/store.py +86 -0
- python_platform/settings/store.py +16 -0
- python_platform/settings/values.py +39 -0
- python_platform/sqlalchemy/__init__.py +51 -0
- python_platform/sqlalchemy/alembic_runtime/__init__.py +1 -0
- python_platform/sqlalchemy/alembic_runtime/env.py +27 -0
- python_platform/sqlalchemy/alembic_runtime/script.py.mako +14 -0
- python_platform/sqlalchemy/auditing.py +48 -0
- python_platform/sqlalchemy/errors.py +85 -0
- python_platform/sqlalchemy/metadata.py +538 -0
- python_platform/sqlalchemy/migration.py +346 -0
- python_platform/sqlalchemy/module.py +48 -0
- python_platform/sqlalchemy/module_migration.py +148 -0
- python_platform/sqlalchemy/options.py +66 -0
- python_platform/sqlalchemy/repository.py +33 -0
- python_platform/sqlalchemy/runtime.py +85 -0
- python_platform/sqlalchemy/session_provider.py +45 -0
- python_platform/sqlalchemy/unit_of_work.py +97 -0
- python_platform/testing/__init__.py +5 -0
- python_platform/testing/runtime.py +112 -0
- python_platform/unit_of_work/__init__.py +14 -0
- python_platform/unit_of_work/contracts.py +223 -0
- python_platform/unit_of_work/errors.py +27 -0
- python_platform/unit_of_work/manager.py +210 -0
- python_platform/unit_of_work/options.py +92 -0
- python_platform-0.1.1.dist-info/METADATA +122 -0
- python_platform-0.1.1.dist-info/RECORD +344 -0
- python_platform-0.1.1.dist-info/WHEEL +4 -0
- python_platform-0.1.1.dist-info/entry_points.txt +9 -0
- python_platform-0.1.1.dist-info/licenses/LICENSE +7 -0
- python_platform-0.1.1.dist-info/licenses/src/python_platform/background_jobs/pgqueuer/UPSTREAM_LICENSE.txt +21 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""PgQueuer manager readiness、fatal completion 与 task ownership。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from collections.abc import Awaitable, Callable
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
|
|
9
|
+
from ...errors.lifecycle import ApplicationRuntimeError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True, slots=True)
|
|
13
|
+
class _PgQueuerManagerBinding:
|
|
14
|
+
component: str
|
|
15
|
+
task_name: str
|
|
16
|
+
ready: asyncio.Event
|
|
17
|
+
run: Callable[[], Awaitable[None]]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True, slots=True)
|
|
21
|
+
class _PgQueuerManagerFailure:
|
|
22
|
+
component: str
|
|
23
|
+
error_type: str
|
|
24
|
+
_source: object = field(default_factory=object, repr=False, compare=False)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class _PgQueuerManagerError(ApplicationRuntimeError):
|
|
28
|
+
"""保留 provider 启动失败的类型边界;运行期事实使用已有 ApplicationRuntimeError 契约。"""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class _PgQueuerManagerSupervisor:
|
|
32
|
+
"""单个 provider runtime 的 manager owner;只发布脱敏失败事实。"""
|
|
33
|
+
|
|
34
|
+
__slots__ = (
|
|
35
|
+
"_failure",
|
|
36
|
+
"_reported_tasks",
|
|
37
|
+
"_startup_complete",
|
|
38
|
+
"_stopping",
|
|
39
|
+
"_task_components",
|
|
40
|
+
"_task_failures",
|
|
41
|
+
"_tasks",
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def __init__(self) -> None:
|
|
45
|
+
self._failure: asyncio.Future[_PgQueuerManagerFailure] | None = None
|
|
46
|
+
self._startup_complete = False
|
|
47
|
+
self._stopping = False
|
|
48
|
+
self._task_components: dict[asyncio.Task[None], str] = {}
|
|
49
|
+
self._tasks: tuple[asyncio.Task[None], ...] = ()
|
|
50
|
+
self._reported_tasks: set[asyncio.Task[None]] = set()
|
|
51
|
+
self._task_failures: dict[asyncio.Task[None], _PgQueuerManagerFailure] = {}
|
|
52
|
+
|
|
53
|
+
async def start(self, bindings: tuple[_PgQueuerManagerBinding, ...]) -> None:
|
|
54
|
+
if self._failure is not None or self._tasks:
|
|
55
|
+
raise RuntimeError("PgQueuer manager supervisor may only start once")
|
|
56
|
+
if not bindings:
|
|
57
|
+
raise RuntimeError("PgQueuer manager supervisor requires an enabled role")
|
|
58
|
+
|
|
59
|
+
self._failure = asyncio.get_running_loop().create_future()
|
|
60
|
+
tasks_by_binding = {
|
|
61
|
+
asyncio.create_task(self._run(binding), name=binding.task_name): binding
|
|
62
|
+
for binding in bindings
|
|
63
|
+
}
|
|
64
|
+
self._tasks = tuple(tasks_by_binding)
|
|
65
|
+
self._task_components = {
|
|
66
|
+
task: binding.component for task, binding in tasks_by_binding.items()
|
|
67
|
+
}
|
|
68
|
+
readiness = {
|
|
69
|
+
asyncio.create_task(
|
|
70
|
+
binding.ready.wait(),
|
|
71
|
+
name=f"{binding.task_name}-readiness",
|
|
72
|
+
): binding
|
|
73
|
+
for binding in bindings
|
|
74
|
+
}
|
|
75
|
+
try:
|
|
76
|
+
pending_readiness = set(readiness)
|
|
77
|
+
while pending_readiness:
|
|
78
|
+
done, _ = await asyncio.wait(
|
|
79
|
+
(*pending_readiness, *self._tasks),
|
|
80
|
+
return_when=asyncio.FIRST_COMPLETED,
|
|
81
|
+
)
|
|
82
|
+
failed = next((task for task in self._tasks if task in done), None)
|
|
83
|
+
if failed is not None:
|
|
84
|
+
binding = tasks_by_binding[failed]
|
|
85
|
+
raise self._manager_error(binding.component, failed)
|
|
86
|
+
pending_readiness.difference_update(done)
|
|
87
|
+
|
|
88
|
+
# readiness callback 返回只说明 bootstrap I/O 已成功;再让 manager 前进一步,
|
|
89
|
+
# 防止 callback 后立即失败却被误报为 Application 已启动。
|
|
90
|
+
self._startup_complete = True
|
|
91
|
+
await asyncio.sleep(0)
|
|
92
|
+
failed = next((task for task in self._tasks if task.done()), None)
|
|
93
|
+
if failed is not None:
|
|
94
|
+
self._startup_complete = False
|
|
95
|
+
binding = tasks_by_binding[failed]
|
|
96
|
+
raise self._manager_error(binding.component, failed)
|
|
97
|
+
except BaseException:
|
|
98
|
+
self._startup_complete = False
|
|
99
|
+
self.begin_stop()
|
|
100
|
+
self.cancel()
|
|
101
|
+
await self._collect_outcomes()
|
|
102
|
+
raise
|
|
103
|
+
finally:
|
|
104
|
+
for task in readiness:
|
|
105
|
+
task.cancel()
|
|
106
|
+
if readiness:
|
|
107
|
+
await asyncio.gather(*readiness, return_exceptions=True)
|
|
108
|
+
|
|
109
|
+
def begin_stop(self) -> None:
|
|
110
|
+
self._stopping = True
|
|
111
|
+
|
|
112
|
+
async def wait(self) -> None:
|
|
113
|
+
cancellation: asyncio.CancelledError | None = None
|
|
114
|
+
try:
|
|
115
|
+
outcomes = await self._collect_outcomes()
|
|
116
|
+
except asyncio.CancelledError as error:
|
|
117
|
+
# deadline 取消 gather 时,已失败的 peer 仍须进入终止聚合,不能被取消掩盖。
|
|
118
|
+
cancellation = error
|
|
119
|
+
outcomes = await self._collect_outcomes()
|
|
120
|
+
failures: list[BaseException] = []
|
|
121
|
+
for task, outcome in zip(self._tasks, outcomes, strict=True):
|
|
122
|
+
fact = self._task_failures.get(task)
|
|
123
|
+
if (
|
|
124
|
+
fact is not None
|
|
125
|
+
or (
|
|
126
|
+
isinstance(outcome, BaseException)
|
|
127
|
+
and not isinstance(outcome, asyncio.CancelledError)
|
|
128
|
+
)
|
|
129
|
+
) and task not in self._reported_tasks:
|
|
130
|
+
manager_error = _PgQueuerManagerError(
|
|
131
|
+
component=self._task_components[task],
|
|
132
|
+
error_type=fact.error_type if fact is not None else type(outcome).__name__,
|
|
133
|
+
)
|
|
134
|
+
if fact is not None:
|
|
135
|
+
manager_error._source = fact._source
|
|
136
|
+
if isinstance(outcome, BaseException):
|
|
137
|
+
manager_error.__cause__ = outcome
|
|
138
|
+
failures.append(manager_error)
|
|
139
|
+
self._reported_tasks.add(task)
|
|
140
|
+
if failures:
|
|
141
|
+
raise BaseExceptionGroup(
|
|
142
|
+
"PgQueuer manager drain failed", tuple(failures)
|
|
143
|
+
) from cancellation
|
|
144
|
+
if cancellation is not None:
|
|
145
|
+
raise cancellation
|
|
146
|
+
|
|
147
|
+
async def wait_for_failure(self) -> _PgQueuerManagerFailure:
|
|
148
|
+
failure = self._failure
|
|
149
|
+
if failure is None:
|
|
150
|
+
raise RuntimeError("PgQueuer manager supervisor is not started")
|
|
151
|
+
return await asyncio.shield(failure)
|
|
152
|
+
|
|
153
|
+
def cancel(self) -> None:
|
|
154
|
+
for task in self._tasks:
|
|
155
|
+
task.cancel()
|
|
156
|
+
|
|
157
|
+
async def close(self) -> None:
|
|
158
|
+
self.begin_stop()
|
|
159
|
+
# bootstrap 已报告的原始失败不会重复计数;取消 peer 时 finally 的失败仍须可观察。
|
|
160
|
+
await self.wait()
|
|
161
|
+
self._tasks = ()
|
|
162
|
+
self._task_components = {}
|
|
163
|
+
self._task_failures.clear()
|
|
164
|
+
self._reported_tasks.clear()
|
|
165
|
+
|
|
166
|
+
async def _run(self, binding: _PgQueuerManagerBinding) -> None:
|
|
167
|
+
try:
|
|
168
|
+
await binding.run()
|
|
169
|
+
except BaseException as original_error:
|
|
170
|
+
if self._startup_complete and not self._stopping:
|
|
171
|
+
self._publish_failure(binding.component, type(original_error).__name__)
|
|
172
|
+
raise
|
|
173
|
+
if self._startup_complete and not self._stopping:
|
|
174
|
+
self._publish_failure(binding.component, "UnexpectedCompletion")
|
|
175
|
+
|
|
176
|
+
def _publish_failure(self, component: str, error_type: str) -> None:
|
|
177
|
+
task = asyncio.current_task()
|
|
178
|
+
assert task is not None
|
|
179
|
+
fact = _PgQueuerManagerFailure(component=component, error_type=error_type)
|
|
180
|
+
self._task_failures[task] = fact
|
|
181
|
+
failure = self._failure
|
|
182
|
+
if failure is not None and not failure.done():
|
|
183
|
+
failure.set_result(fact)
|
|
184
|
+
# 发布不等于 Application 已接收;同源去重只能由最终终止聚合 owner 完成。
|
|
185
|
+
|
|
186
|
+
async def _collect_outcomes(self) -> tuple[object, ...]:
|
|
187
|
+
if not self._tasks:
|
|
188
|
+
return ()
|
|
189
|
+
return tuple(await asyncio.gather(*self._tasks, return_exceptions=True))
|
|
190
|
+
|
|
191
|
+
def _manager_error(self, component: str, task: asyncio.Task[None]) -> _PgQueuerManagerError:
|
|
192
|
+
self._reported_tasks.add(task)
|
|
193
|
+
if task.cancelled():
|
|
194
|
+
error_type = asyncio.CancelledError.__name__
|
|
195
|
+
else:
|
|
196
|
+
error = task.exception()
|
|
197
|
+
error_type = "UnexpectedCompletion" if error is None else type(error).__name__
|
|
198
|
+
failure = _PgQueuerManagerError(component=component, error_type=error_type)
|
|
199
|
+
if not task.cancelled():
|
|
200
|
+
failure.__cause__ = task.exception()
|
|
201
|
+
return failure
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Provider-neutral non-durable background worker capability。"""
|
|
2
|
+
|
|
3
|
+
from .catalog import BackgroundWorkerCatalog
|
|
4
|
+
from .contracts import (
|
|
5
|
+
BackgroundExecutionOptions,
|
|
6
|
+
BackgroundExecutionProfile,
|
|
7
|
+
BackgroundWorker,
|
|
8
|
+
BackgroundWorkerContext,
|
|
9
|
+
BackgroundWorkerDefinition,
|
|
10
|
+
)
|
|
11
|
+
from .errors import BackgroundWorkerDefinitionError, BackgroundWorkerRuntimeError
|
|
12
|
+
|
|
13
|
+
__all__ = (
|
|
14
|
+
"BackgroundExecutionOptions",
|
|
15
|
+
"BackgroundExecutionProfile",
|
|
16
|
+
"BackgroundWorker",
|
|
17
|
+
"BackgroundWorkerCatalog",
|
|
18
|
+
"BackgroundWorkerContext",
|
|
19
|
+
"BackgroundWorkerDefinition",
|
|
20
|
+
"BackgroundWorkerDefinitionError",
|
|
21
|
+
"BackgroundWorkerRuntimeError",
|
|
22
|
+
)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Module-owned worker metadata 的 Application-local immutable catalog。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
from collections.abc import Iterator, Mapping
|
|
8
|
+
from types import MappingProxyType
|
|
9
|
+
|
|
10
|
+
from ..modularity import AppModule, ModuleKey, ModuleRegistry
|
|
11
|
+
from .contracts import BackgroundWorkerDefinition
|
|
12
|
+
from .errors import BackgroundWorkerDefinitionError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BackgroundWorkerCatalog:
|
|
16
|
+
__slots__ = ("_declarations", "_definitions", "_fingerprint", "_owners")
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
definitions: tuple[BackgroundWorkerDefinition, ...],
|
|
21
|
+
*,
|
|
22
|
+
owners: Mapping[str, ModuleKey],
|
|
23
|
+
declarations: tuple[tuple[type[AppModule], tuple[BackgroundWorkerDefinition, ...]], ...],
|
|
24
|
+
) -> None:
|
|
25
|
+
self._definitions = MappingProxyType({item.name: item for item in definitions})
|
|
26
|
+
self._owners = MappingProxyType(dict(owners))
|
|
27
|
+
self._declarations = declarations
|
|
28
|
+
encoded = json.dumps(
|
|
29
|
+
[
|
|
30
|
+
{
|
|
31
|
+
"name": item.name,
|
|
32
|
+
"owner": str(self._owners[item.name]),
|
|
33
|
+
"interval_seconds": item.interval.total_seconds(),
|
|
34
|
+
"timeout_seconds": item.iteration_timeout.total_seconds(),
|
|
35
|
+
}
|
|
36
|
+
for item in definitions
|
|
37
|
+
],
|
|
38
|
+
sort_keys=True,
|
|
39
|
+
separators=(",", ":"),
|
|
40
|
+
).encode()
|
|
41
|
+
self._fingerprint = hashlib.sha256(encoded).hexdigest()
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def definitions(self) -> tuple[BackgroundWorkerDefinition, ...]:
|
|
45
|
+
return tuple(self._definitions.values())
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def fingerprint(self) -> str:
|
|
49
|
+
return self._fingerprint
|
|
50
|
+
|
|
51
|
+
def __iter__(self) -> Iterator[str]:
|
|
52
|
+
return iter(self._definitions)
|
|
53
|
+
|
|
54
|
+
def get(self, name: str) -> BackgroundWorkerDefinition | None:
|
|
55
|
+
return self._definitions.get(name)
|
|
56
|
+
|
|
57
|
+
def _assert_unchanged(self) -> None:
|
|
58
|
+
for module_type, declared in self._declarations:
|
|
59
|
+
if module_type.__dict__.get("background_worker_definitions", ()) is not declared:
|
|
60
|
+
raise BackgroundWorkerDefinitionError(
|
|
61
|
+
worker=f"{module_type.__module__}.{module_type.__qualname__}",
|
|
62
|
+
reason="Module background worker metadata changed after build",
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _build_background_worker_catalog(registry: ModuleRegistry) -> BackgroundWorkerCatalog:
|
|
67
|
+
definitions: list[BackgroundWorkerDefinition] = []
|
|
68
|
+
owners: dict[str, ModuleKey] = {}
|
|
69
|
+
declarations: list[tuple[type[AppModule], tuple[BackgroundWorkerDefinition, ...]]] = []
|
|
70
|
+
for descriptor in registry.dependency_order:
|
|
71
|
+
declared = descriptor.module_type.__dict__.get("background_worker_definitions", ())
|
|
72
|
+
if not isinstance(declared, tuple):
|
|
73
|
+
raise BackgroundWorkerDefinitionError(
|
|
74
|
+
worker=str(descriptor.key),
|
|
75
|
+
reason="Module background_worker_definitions must be an immutable tuple",
|
|
76
|
+
)
|
|
77
|
+
declarations.append((descriptor.module_type, declared))
|
|
78
|
+
for definition in declared:
|
|
79
|
+
if not isinstance(definition, BackgroundWorkerDefinition):
|
|
80
|
+
raise BackgroundWorkerDefinitionError(
|
|
81
|
+
worker=str(descriptor.key),
|
|
82
|
+
reason="Module background_worker_definitions contains an invalid item",
|
|
83
|
+
)
|
|
84
|
+
previous = owners.get(definition.name)
|
|
85
|
+
if previous is not None:
|
|
86
|
+
raise BackgroundWorkerDefinitionError(
|
|
87
|
+
worker=definition.name,
|
|
88
|
+
reason=f"worker is already owned by {previous}",
|
|
89
|
+
)
|
|
90
|
+
owners[definition.name] = descriptor.key
|
|
91
|
+
definitions.append(definition)
|
|
92
|
+
return BackgroundWorkerCatalog(
|
|
93
|
+
tuple(definitions),
|
|
94
|
+
owners=owners,
|
|
95
|
+
declarations=tuple(declarations),
|
|
96
|
+
)
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Non-durable worker contract 与 Host-owned execution options。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import inspect
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from collections.abc import Awaitable, Callable, Mapping
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from datetime import timedelta
|
|
11
|
+
from enum import Enum
|
|
12
|
+
from typing import cast
|
|
13
|
+
|
|
14
|
+
from pydantic import ConfigDict, Field, field_serializer
|
|
15
|
+
|
|
16
|
+
from ..background_execution import BackgroundTaskOptions
|
|
17
|
+
from ..invocation import BackgroundSystemCaller
|
|
18
|
+
from ..options import BaseOptions
|
|
19
|
+
from .errors import BackgroundWorkerDefinitionError
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class BackgroundExecutionProfile(str, Enum):
|
|
23
|
+
"""Host 对当前 Application background execution ownership 的显式选择。"""
|
|
24
|
+
|
|
25
|
+
DISABLED = "disabled"
|
|
26
|
+
EMBEDDED = "embedded"
|
|
27
|
+
DEDICATED = "dedicated"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class BackgroundExecutionOptions(BaseOptions):
|
|
31
|
+
"""Application 准入和关闭策略;任务声明仍由 Module catalog 唯一拥有。"""
|
|
32
|
+
|
|
33
|
+
model_config = ConfigDict(extra="forbid")
|
|
34
|
+
|
|
35
|
+
profile: BackgroundExecutionProfile = BackgroundExecutionProfile.EMBEDDED
|
|
36
|
+
workers: Mapping[str, BackgroundTaskOptions] = Field(default_factory=dict)
|
|
37
|
+
jobs: Mapping[str, BackgroundTaskOptions] = Field(default_factory=dict)
|
|
38
|
+
max_restarts: int = Field(default=3, ge=0)
|
|
39
|
+
restart_interval: timedelta = Field(default=timedelta(seconds=1), gt=timedelta(0))
|
|
40
|
+
graceful_shutdown_timeout: timedelta = Field(
|
|
41
|
+
default=timedelta(seconds=30),
|
|
42
|
+
gt=timedelta(0),
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
@field_serializer("workers", "jobs")
|
|
46
|
+
def _serialize_tasks(
|
|
47
|
+
self, value: Mapping[str, BackgroundTaskOptions]
|
|
48
|
+
) -> dict[str, BackgroundTaskOptions]:
|
|
49
|
+
# 发布后的 Mapping 已冻结;原生 serializer 接收普通 dict 后继续处理内部 typed Options。
|
|
50
|
+
return dict(value)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class BackgroundWorker(ABC):
|
|
54
|
+
"""一次 iteration 的实现边界;永久 loop 由 Application coordinator 独占。"""
|
|
55
|
+
|
|
56
|
+
@abstractmethod
|
|
57
|
+
async def run_iteration(self, context: BackgroundWorkerContext) -> None:
|
|
58
|
+
raise NotImplementedError
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True, slots=True)
|
|
62
|
+
class BackgroundWorkerContext:
|
|
63
|
+
name: str
|
|
64
|
+
system_caller: BackgroundSystemCaller
|
|
65
|
+
_stop_event: asyncio.Event = field(repr=False, compare=False)
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def stop_requested(self) -> bool:
|
|
69
|
+
return self._stop_event.is_set()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass(frozen=True, slots=True)
|
|
73
|
+
class BackgroundWorkerDefinition:
|
|
74
|
+
name: str
|
|
75
|
+
worker_type: type[BackgroundWorker] | Callable[..., Awaitable[None]]
|
|
76
|
+
interval: timedelta
|
|
77
|
+
iteration_timeout: timedelta
|
|
78
|
+
|
|
79
|
+
def __post_init__(self) -> None:
|
|
80
|
+
_validate_name(self.name)
|
|
81
|
+
is_class = isinstance(self.worker_type, type) and issubclass(
|
|
82
|
+
self.worker_type, BackgroundWorker
|
|
83
|
+
)
|
|
84
|
+
if not is_class and not inspect.iscoroutinefunction(self.worker_type):
|
|
85
|
+
raise BackgroundWorkerDefinitionError(
|
|
86
|
+
worker=self.name,
|
|
87
|
+
reason="worker_type must be a BackgroundWorker class or async function",
|
|
88
|
+
)
|
|
89
|
+
if isinstance(self.worker_type, type) and not inspect.iscoroutinefunction(
|
|
90
|
+
cast(type[BackgroundWorker], self.worker_type).run_iteration
|
|
91
|
+
):
|
|
92
|
+
raise BackgroundWorkerDefinitionError(
|
|
93
|
+
worker=self.name,
|
|
94
|
+
reason="run_iteration must be async",
|
|
95
|
+
)
|
|
96
|
+
if not isinstance(self.interval, timedelta) or self.interval.total_seconds() <= 0:
|
|
97
|
+
raise BackgroundWorkerDefinitionError(
|
|
98
|
+
worker=self.name,
|
|
99
|
+
reason="interval must be a positive timedelta",
|
|
100
|
+
)
|
|
101
|
+
if (
|
|
102
|
+
not isinstance(self.iteration_timeout, timedelta)
|
|
103
|
+
or self.iteration_timeout.total_seconds() <= 0
|
|
104
|
+
):
|
|
105
|
+
raise BackgroundWorkerDefinitionError(
|
|
106
|
+
worker=self.name,
|
|
107
|
+
reason="iteration_timeout must be a positive timedelta",
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _validate_name(name: str) -> None:
|
|
112
|
+
if (
|
|
113
|
+
not isinstance(name, str)
|
|
114
|
+
or not name
|
|
115
|
+
or name != name.strip()
|
|
116
|
+
or name.startswith(".")
|
|
117
|
+
or name.endswith(".")
|
|
118
|
+
or ".." in name
|
|
119
|
+
or any(not (character.isalnum() or character in "._-") for character in name)
|
|
120
|
+
):
|
|
121
|
+
raise BackgroundWorkerDefinitionError(
|
|
122
|
+
worker=name if isinstance(name, str) else repr(name),
|
|
123
|
+
reason="name must be a trimmed stable identifier",
|
|
124
|
+
)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Non-durable background worker definition 与 runtime failures。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ..errors.base import FrameworkError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BackgroundWorkerDefinitionError(FrameworkError):
|
|
9
|
+
def __init__(self, *, worker: str, reason: str) -> None:
|
|
10
|
+
self.worker = worker
|
|
11
|
+
self.reason = reason
|
|
12
|
+
super().__init__(f"Invalid background worker {worker!r}: {reason}")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BackgroundWorkerRuntimeError(FrameworkError):
|
|
16
|
+
def __init__(self, *, worker: str, operation: str, error_type: str) -> None:
|
|
17
|
+
self.worker = worker
|
|
18
|
+
self.operation = operation
|
|
19
|
+
self.error_type = error_type
|
|
20
|
+
super().__init__(f"Background worker {worker!r} {operation} failed with {error_type}")
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""单个 Worker 的 fixed-delay loop 与每次执行的原生作用域。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
from contextlib import AsyncExitStack
|
|
8
|
+
from uuid import uuid4
|
|
9
|
+
|
|
10
|
+
from dishka import AsyncContainer, Scope
|
|
11
|
+
|
|
12
|
+
from ..application_services.contracts import _DEFAULT_CONNECTION_NAME
|
|
13
|
+
from ..application_services.errors import ApplicationInvocationRejectedError
|
|
14
|
+
from ..application_services.invocation import _InvocationRuntime
|
|
15
|
+
from ..authorization import CurrentUser
|
|
16
|
+
from ..background_execution.lifecycle import _TaskExecution
|
|
17
|
+
from ..invocation import BackgroundSystemCaller
|
|
18
|
+
from .contracts import BackgroundWorkerContext, BackgroundWorkerDefinition
|
|
19
|
+
|
|
20
|
+
_LOGGER = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class _WorkerExecution:
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
definition: BackgroundWorkerDefinition,
|
|
27
|
+
container: AsyncContainer,
|
|
28
|
+
invocation_runtime: _InvocationRuntime,
|
|
29
|
+
caller: BackgroundSystemCaller,
|
|
30
|
+
) -> None:
|
|
31
|
+
self._definition = definition
|
|
32
|
+
self._container = container
|
|
33
|
+
self._invocation_runtime = invocation_runtime
|
|
34
|
+
self._caller = caller
|
|
35
|
+
self._stop_event = asyncio.Event()
|
|
36
|
+
self._task: asyncio.Task[None] | None = None
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def binding(self) -> _TaskExecution:
|
|
40
|
+
return _TaskExecution(self.start, self._stop_event.set, self.wait, None, self.cancel)
|
|
41
|
+
|
|
42
|
+
async def start(self) -> None:
|
|
43
|
+
self._stop_event.clear()
|
|
44
|
+
self._task = asyncio.create_task(
|
|
45
|
+
self._worker_loop(self._definition),
|
|
46
|
+
name=f"python-platform-worker:{self._definition.name}",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
async def wait(self) -> None:
|
|
50
|
+
if self._task is not None:
|
|
51
|
+
await self._task
|
|
52
|
+
|
|
53
|
+
def cancel(self) -> None:
|
|
54
|
+
if self._task is not None:
|
|
55
|
+
self._task.cancel()
|
|
56
|
+
|
|
57
|
+
async def _worker_loop(self, definition: BackgroundWorkerDefinition) -> None:
|
|
58
|
+
while not self._stop_event.is_set():
|
|
59
|
+
try:
|
|
60
|
+
await self._run_worker_iteration(definition)
|
|
61
|
+
except asyncio.CancelledError:
|
|
62
|
+
raise
|
|
63
|
+
except Exception as original_error:
|
|
64
|
+
if self._stop_event.is_set():
|
|
65
|
+
raise
|
|
66
|
+
# payload/secret 不进入日志;下一次 fixed-delay iteration 仍按 contract 执行。
|
|
67
|
+
_LOGGER.error(
|
|
68
|
+
"background worker iteration failed",
|
|
69
|
+
exc_info=original_error,
|
|
70
|
+
extra={
|
|
71
|
+
"background_worker": definition.name,
|
|
72
|
+
"background_exception_type": type(original_error).__name__,
|
|
73
|
+
},
|
|
74
|
+
)
|
|
75
|
+
if self._stop_event.is_set():
|
|
76
|
+
break
|
|
77
|
+
try:
|
|
78
|
+
await asyncio.wait_for(
|
|
79
|
+
self._stop_event.wait(),
|
|
80
|
+
timeout=definition.interval.total_seconds(),
|
|
81
|
+
)
|
|
82
|
+
except TimeoutError:
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
async def _run_worker_iteration(self, definition: BackgroundWorkerDefinition) -> None:
|
|
86
|
+
container = self._container
|
|
87
|
+
context = BackgroundWorkerContext(
|
|
88
|
+
name=definition.name,
|
|
89
|
+
system_caller=self._caller,
|
|
90
|
+
_stop_event=self._stop_event,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# 原生 timeout 在当前 Worker Task 内执行、取消并等待清理;finally 失败直接传播。
|
|
94
|
+
# 不创建额外迭代 Task,调用许可、caller 和业务作用域保持同一 owner。
|
|
95
|
+
async with (
|
|
96
|
+
asyncio.timeout(definition.iteration_timeout.total_seconds()),
|
|
97
|
+
AsyncExitStack() as scope,
|
|
98
|
+
):
|
|
99
|
+
# 关闭可能撤回尚未取得的许可;只处理 enter 的拒绝,不吞业务或 finally 异常。
|
|
100
|
+
try:
|
|
101
|
+
await scope.enter_async_context(self._invocation_runtime.outer_lease())
|
|
102
|
+
except ApplicationInvocationRejectedError:
|
|
103
|
+
return
|
|
104
|
+
token = self._invocation_runtime.bind_caller(CurrentUser.anonymous(), str(uuid4()))
|
|
105
|
+
try:
|
|
106
|
+
try:
|
|
107
|
+
async with container(scope=Scope.REQUEST) as request_container:
|
|
108
|
+
from ..invocation.interception import _EntryInterceptionCatalog
|
|
109
|
+
|
|
110
|
+
entries = await request_container.get(_EntryInterceptionCatalog)
|
|
111
|
+
await entries.invoke(
|
|
112
|
+
self._invocation_runtime,
|
|
113
|
+
request_container,
|
|
114
|
+
definition.worker_type,
|
|
115
|
+
"run_iteration",
|
|
116
|
+
(context,),
|
|
117
|
+
connection_name=_DEFAULT_CONNECTION_NAME,
|
|
118
|
+
)
|
|
119
|
+
except Exception as error:
|
|
120
|
+
if self._stop_event.is_set():
|
|
121
|
+
# 关闭中的异常可能来自被取消业务的 finally,不能按普通迭代失败吞掉。
|
|
122
|
+
raise
|
|
123
|
+
# lease/caller 尚有效;普通迭代失败在 owner 内记录,然后继续下一轮。
|
|
124
|
+
_LOGGER.error(
|
|
125
|
+
"background worker iteration failed",
|
|
126
|
+
exc_info=error,
|
|
127
|
+
extra={
|
|
128
|
+
"background_worker": definition.name,
|
|
129
|
+
"background_exception_type": type(error).__name__,
|
|
130
|
+
},
|
|
131
|
+
)
|
|
132
|
+
finally:
|
|
133
|
+
self._invocation_runtime.reset_caller(token)
|