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,479 @@
|
|
|
1
|
+
"""PgQueuer 1.3 runtime ownership、registration 与 graceful recovery policy。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from datetime import timedelta
|
|
9
|
+
from typing import Any, cast
|
|
10
|
+
|
|
11
|
+
from pgqueuer import PgQueuer, RetryRequested
|
|
12
|
+
from pgqueuer.adapters.drivers.asyncpg import AsyncpgDriver, AsyncpgPoolDriver
|
|
13
|
+
from pgqueuer.adapters.persistence.qb import (
|
|
14
|
+
DBSettings,
|
|
15
|
+
QueryBuilderEnvironment,
|
|
16
|
+
QueryQueueBuilder,
|
|
17
|
+
QuerySchedulerBuilder,
|
|
18
|
+
)
|
|
19
|
+
from pgqueuer.adapters.persistence.queries import Queries
|
|
20
|
+
from pgqueuer.core.executors import (
|
|
21
|
+
DatabaseRetryEntrypointExecutor,
|
|
22
|
+
EntrypointExecutorParameters,
|
|
23
|
+
)
|
|
24
|
+
from pgqueuer.domain.models import CronExpressionEntrypoint
|
|
25
|
+
from pgqueuer.domain.types import QueueExecutionMode
|
|
26
|
+
from pgqueuer.models import Channel, Context, Job, Schedule
|
|
27
|
+
from pydantic import BaseModel
|
|
28
|
+
|
|
29
|
+
from ...background_execution.lifecycle import (
|
|
30
|
+
_BackgroundRuntimeFailure,
|
|
31
|
+
_TaskExecution,
|
|
32
|
+
)
|
|
33
|
+
from ...options import Options
|
|
34
|
+
from ..catalog import BackgroundJobCatalog
|
|
35
|
+
from ..contracts import BackgroundJobDefinition, BackgroundJobSchedule
|
|
36
|
+
from ..errors import (
|
|
37
|
+
BackgroundJobDefinitionError,
|
|
38
|
+
BackgroundJobExecutionError,
|
|
39
|
+
BackgroundJobPayloadError,
|
|
40
|
+
)
|
|
41
|
+
from ..execution import _BackgroundJobExecutor, _encode_scheduled_envelope
|
|
42
|
+
from .options import PgQueuerBackgroundJobOptions
|
|
43
|
+
from .supervision import (
|
|
44
|
+
_PgQueuerManagerBinding,
|
|
45
|
+
_PgQueuerManagerError,
|
|
46
|
+
_PgQueuerManagerSupervisor,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class _PgQueuerRuntimeError(RuntimeError):
|
|
51
|
+
def __init__(self, *, operation: str, error_type: str) -> None:
|
|
52
|
+
self.operation = operation
|
|
53
|
+
self.error_type = error_type
|
|
54
|
+
super().__init__(f"PgQueuer {operation} failed with {error_type}")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class _ReadinessAwareAsyncpgPoolDriver(AsyncpgPoolDriver):
|
|
58
|
+
"""以 PgQueuer 已有 listener bootstrap 作为 executor ready authority。"""
|
|
59
|
+
|
|
60
|
+
def __init__(self, pool: Any, ready: asyncio.Event) -> None:
|
|
61
|
+
super().__init__(pool)
|
|
62
|
+
self._python_platform_ready = ready
|
|
63
|
+
|
|
64
|
+
async def add_listener(
|
|
65
|
+
self,
|
|
66
|
+
channel: str,
|
|
67
|
+
callback: Callable[[str | bytes | bytearray], None],
|
|
68
|
+
) -> None:
|
|
69
|
+
await super().add_listener(channel, callback)
|
|
70
|
+
self._python_platform_ready.set()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class _ReadinessAwareQueries(Queries):
|
|
74
|
+
"""以 PgQueuer 已有 schedule insert 作为 scheduler ready authority。"""
|
|
75
|
+
|
|
76
|
+
def __init__(
|
|
77
|
+
self,
|
|
78
|
+
driver: _ReadinessAwareAsyncpgPoolDriver,
|
|
79
|
+
*,
|
|
80
|
+
qbe: QueryBuilderEnvironment,
|
|
81
|
+
qbq: QueryQueueBuilder,
|
|
82
|
+
qbs: QuerySchedulerBuilder,
|
|
83
|
+
ready: asyncio.Event,
|
|
84
|
+
) -> None:
|
|
85
|
+
super().__init__(driver, qbe=qbe, qbq=qbq, qbs=qbs)
|
|
86
|
+
self._python_platform_ready = ready
|
|
87
|
+
|
|
88
|
+
async def insert_schedule(
|
|
89
|
+
self,
|
|
90
|
+
schedules: dict[CronExpressionEntrypoint, timedelta],
|
|
91
|
+
) -> None:
|
|
92
|
+
await super().insert_schedule(schedules)
|
|
93
|
+
self._python_platform_ready.set()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclass
|
|
97
|
+
class _ShutdownAwareRetryExecutor(DatabaseRetryEntrypointExecutor):
|
|
98
|
+
deadline_cancellation: asyncio.Event = field(default_factory=asyncio.Event)
|
|
99
|
+
|
|
100
|
+
async def execute(self, job: Job, context: Context) -> None:
|
|
101
|
+
try:
|
|
102
|
+
await super().execute(job, context)
|
|
103
|
+
except RetryRequested as retry:
|
|
104
|
+
# Envelope/version/schema failures are permanent facts;重试只会重复同一失败。
|
|
105
|
+
permanent_failure = retry.__cause__
|
|
106
|
+
if isinstance(
|
|
107
|
+
permanent_failure,
|
|
108
|
+
(BackgroundJobDefinitionError, BackgroundJobPayloadError),
|
|
109
|
+
):
|
|
110
|
+
raise permanent_failure from None
|
|
111
|
+
raise
|
|
112
|
+
except asyncio.CancelledError:
|
|
113
|
+
if self.deadline_cancellation.is_set():
|
|
114
|
+
raise RetryRequested(
|
|
115
|
+
delay=timedelta(0),
|
|
116
|
+
reason="application shutdown deadline",
|
|
117
|
+
) from None
|
|
118
|
+
raise
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class _PgQueuerRuntime:
|
|
122
|
+
__slots__ = (
|
|
123
|
+
"_active_deliveries",
|
|
124
|
+
"_catalog",
|
|
125
|
+
"_cleanup_failure",
|
|
126
|
+
"_deadline_cancellation",
|
|
127
|
+
"_executor",
|
|
128
|
+
"_executor_ready",
|
|
129
|
+
"_job_name",
|
|
130
|
+
"_options",
|
|
131
|
+
"_pgqueuer",
|
|
132
|
+
"_pool",
|
|
133
|
+
"_scheduler_ready",
|
|
134
|
+
"_stop_event",
|
|
135
|
+
"_supervisor",
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
def __init__(
|
|
139
|
+
self,
|
|
140
|
+
pool: Any,
|
|
141
|
+
options: Options[PgQueuerBackgroundJobOptions],
|
|
142
|
+
catalog: BackgroundJobCatalog,
|
|
143
|
+
executor: _BackgroundJobExecutor,
|
|
144
|
+
job_name: str,
|
|
145
|
+
) -> None:
|
|
146
|
+
self._options = options.value
|
|
147
|
+
self._catalog = catalog
|
|
148
|
+
self._executor = executor
|
|
149
|
+
self._job_name = job_name
|
|
150
|
+
self._pool = pool
|
|
151
|
+
self._pgqueuer: PgQueuer | None = None
|
|
152
|
+
self._cleanup_failure: BaseException | None = None
|
|
153
|
+
self._active_deliveries: set[asyncio.Task[object]] = set()
|
|
154
|
+
self._stop_event = asyncio.Event()
|
|
155
|
+
self._deadline_cancellation = asyncio.Event()
|
|
156
|
+
self._executor_ready = asyncio.Event()
|
|
157
|
+
self._scheduler_ready = asyncio.Event()
|
|
158
|
+
self._supervisor = _PgQueuerManagerSupervisor()
|
|
159
|
+
|
|
160
|
+
@property
|
|
161
|
+
def binding(self) -> _TaskExecution:
|
|
162
|
+
return _TaskExecution(
|
|
163
|
+
start=self.start,
|
|
164
|
+
stop_claiming=self.stop_claiming,
|
|
165
|
+
wait=self.wait,
|
|
166
|
+
wait_for_failure=self.wait_for_failure,
|
|
167
|
+
cancel_for_deadline=self.cancel_for_deadline,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
async def initialize(self) -> None:
|
|
171
|
+
if self._cleanup_failure is not None:
|
|
172
|
+
raise self._cleanup_failure
|
|
173
|
+
self._stop_event.clear()
|
|
174
|
+
self._deadline_cancellation.clear()
|
|
175
|
+
self._executor_ready.clear()
|
|
176
|
+
self._scheduler_ready.clear()
|
|
177
|
+
self._supervisor = _PgQueuerManagerSupervisor()
|
|
178
|
+
try:
|
|
179
|
+
queries = _queries_for_pool(
|
|
180
|
+
self._pool,
|
|
181
|
+
self._options,
|
|
182
|
+
executor_ready=self._executor_ready,
|
|
183
|
+
scheduler_ready=self._scheduler_ready,
|
|
184
|
+
)
|
|
185
|
+
settings = queries.qbe.settings
|
|
186
|
+
pgqueuer = PgQueuer(
|
|
187
|
+
connection=queries.driver,
|
|
188
|
+
channel=Channel(settings.channel),
|
|
189
|
+
queries=queries,
|
|
190
|
+
)
|
|
191
|
+
self._pgqueuer = pgqueuer
|
|
192
|
+
self._register_entrypoints(pgqueuer)
|
|
193
|
+
self._register_schedules(pgqueuer)
|
|
194
|
+
# scheduler 也会 enqueue;任一执行角色启动前都必须验证完整 queue schema。
|
|
195
|
+
await pgqueuer.qm.verify_structure()
|
|
196
|
+
if self._options.scheduler_enabled and not await queries.has_table(
|
|
197
|
+
settings.schedules_table
|
|
198
|
+
):
|
|
199
|
+
raise RuntimeError("required PgQueuer schedule table is missing")
|
|
200
|
+
except BaseException as original_error:
|
|
201
|
+
await self._cleanup_failed_start(original_error)
|
|
202
|
+
if not isinstance(original_error, Exception):
|
|
203
|
+
raise
|
|
204
|
+
raise _PgQueuerRuntimeError(
|
|
205
|
+
operation="startup verification",
|
|
206
|
+
error_type=type(original_error).__name__,
|
|
207
|
+
) from original_error
|
|
208
|
+
|
|
209
|
+
async def start(self) -> None:
|
|
210
|
+
if self._cleanup_failure is not None:
|
|
211
|
+
raise self._cleanup_failure
|
|
212
|
+
if self._pgqueuer is None:
|
|
213
|
+
await self.initialize()
|
|
214
|
+
pgqueuer = self._require_pgqueuer()
|
|
215
|
+
bindings: list[_PgQueuerManagerBinding] = []
|
|
216
|
+
if self._options.executor_enabled:
|
|
217
|
+
|
|
218
|
+
async def run_executor() -> None:
|
|
219
|
+
await pgqueuer.qm.run(
|
|
220
|
+
dequeue_timeout=self._options.poll_interval,
|
|
221
|
+
batch_size=self._options.batch_size,
|
|
222
|
+
mode=QueueExecutionMode.continuous,
|
|
223
|
+
max_concurrent_tasks=self._options.max_concurrent_jobs,
|
|
224
|
+
heartbeat_timeout=self._options.heartbeat_timeout,
|
|
225
|
+
log_aggregation_interval=self._options.log_aggregation_interval,
|
|
226
|
+
shutdown_on_listener_failure=True,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
bindings.append(
|
|
230
|
+
_PgQueuerManagerBinding(
|
|
231
|
+
component="pgqueuer-executor",
|
|
232
|
+
task_name="python-platform-pgqueuer-executor",
|
|
233
|
+
ready=self._executor_ready,
|
|
234
|
+
run=run_executor,
|
|
235
|
+
)
|
|
236
|
+
)
|
|
237
|
+
if self._options.scheduler_enabled:
|
|
238
|
+
|
|
239
|
+
async def run_scheduler() -> None:
|
|
240
|
+
await pgqueuer.sm.run()
|
|
241
|
+
|
|
242
|
+
bindings.append(
|
|
243
|
+
_PgQueuerManagerBinding(
|
|
244
|
+
component="pgqueuer-scheduler",
|
|
245
|
+
task_name="python-platform-pgqueuer-scheduler",
|
|
246
|
+
ready=self._scheduler_ready,
|
|
247
|
+
run=run_scheduler,
|
|
248
|
+
)
|
|
249
|
+
)
|
|
250
|
+
try:
|
|
251
|
+
await self._supervisor.start(tuple(bindings))
|
|
252
|
+
except BaseException as original_error:
|
|
253
|
+
await self._cleanup_failed_start(original_error)
|
|
254
|
+
if not isinstance(original_error, _PgQueuerManagerError):
|
|
255
|
+
raise
|
|
256
|
+
raise _PgQueuerRuntimeError(
|
|
257
|
+
operation=f"{original_error.component} bootstrap",
|
|
258
|
+
error_type=original_error.error_type,
|
|
259
|
+
) from original_error
|
|
260
|
+
|
|
261
|
+
def stop_claiming(self) -> None:
|
|
262
|
+
self._supervisor.begin_stop()
|
|
263
|
+
self._stop_event.set()
|
|
264
|
+
pgqueuer = self._pgqueuer
|
|
265
|
+
if pgqueuer is not None:
|
|
266
|
+
pgqueuer.shutdown.set()
|
|
267
|
+
|
|
268
|
+
async def wait(self) -> None:
|
|
269
|
+
try:
|
|
270
|
+
await self._supervisor.wait()
|
|
271
|
+
finally:
|
|
272
|
+
await self.close()
|
|
273
|
+
|
|
274
|
+
async def wait_for_failure(self) -> _BackgroundRuntimeFailure:
|
|
275
|
+
failure = await self._supervisor.wait_for_failure()
|
|
276
|
+
return _BackgroundRuntimeFailure(
|
|
277
|
+
component=failure.component,
|
|
278
|
+
error_type=failure.error_type,
|
|
279
|
+
_source=failure._source,
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
def cancel_for_deadline(self) -> None:
|
|
283
|
+
self._deadline_cancellation.set()
|
|
284
|
+
for task in tuple(self._active_deliveries):
|
|
285
|
+
task.cancel()
|
|
286
|
+
self._supervisor.cancel()
|
|
287
|
+
|
|
288
|
+
async def close(self) -> None:
|
|
289
|
+
self.stop_claiming()
|
|
290
|
+
try:
|
|
291
|
+
await self._supervisor.close()
|
|
292
|
+
except BaseException as error:
|
|
293
|
+
if self._cleanup_failure is None:
|
|
294
|
+
self._cleanup_failure = error
|
|
295
|
+
if self._cleanup_failure is not None:
|
|
296
|
+
raise self._cleanup_failure
|
|
297
|
+
self._pgqueuer = None
|
|
298
|
+
|
|
299
|
+
async def _cleanup_failed_start(self, original_error: BaseException) -> None:
|
|
300
|
+
try:
|
|
301
|
+
await self.close()
|
|
302
|
+
except BaseException as cleanup_error:
|
|
303
|
+
# 只有本逻辑 Job 的所有 manager 成功收口才能替换实例;共享 pool 仍由 Module 拥有。
|
|
304
|
+
self._cleanup_failure = BaseExceptionGroup(
|
|
305
|
+
"PgQueuer startup and cleanup failed", (original_error, cleanup_error)
|
|
306
|
+
)
|
|
307
|
+
raise self._cleanup_failure from None
|
|
308
|
+
|
|
309
|
+
def _register_entrypoints(self, pgqueuer: PgQueuer) -> None:
|
|
310
|
+
for definition in self._catalog.definitions:
|
|
311
|
+
if definition.name != self._job_name:
|
|
312
|
+
continue
|
|
313
|
+
executor_factory = self._executor_factory(definition)
|
|
314
|
+
|
|
315
|
+
@pgqueuer.entrypoint(
|
|
316
|
+
_entrypoint_name(definition.name, definition.version),
|
|
317
|
+
on_failure="hold",
|
|
318
|
+
executor_factory=executor_factory,
|
|
319
|
+
)
|
|
320
|
+
async def execute(
|
|
321
|
+
job: Job,
|
|
322
|
+
*,
|
|
323
|
+
_definition: BackgroundJobDefinition[BaseModel] = definition,
|
|
324
|
+
_runtime: _PgQueuerRuntime = self,
|
|
325
|
+
) -> None:
|
|
326
|
+
await _runtime._execute_job(job, _definition)
|
|
327
|
+
|
|
328
|
+
def _register_schedules(self, pgqueuer: PgQueuer) -> None:
|
|
329
|
+
if not self._options.scheduler_enabled:
|
|
330
|
+
return
|
|
331
|
+
for schedule in self._catalog.schedules:
|
|
332
|
+
if schedule.job_name != self._job_name:
|
|
333
|
+
continue
|
|
334
|
+
definition = self._catalog.get(schedule.job_name, schedule.job_version)
|
|
335
|
+
assert definition is not None
|
|
336
|
+
envelope = _encode_scheduled_envelope(definition, schedule.payload)
|
|
337
|
+
target_entrypoint = _entrypoint_name(definition.name, definition.version)
|
|
338
|
+
|
|
339
|
+
@pgqueuer.schedule(
|
|
340
|
+
_schedule_entrypoint_name(schedule),
|
|
341
|
+
schedule.cron,
|
|
342
|
+
clean_old=False,
|
|
343
|
+
)
|
|
344
|
+
async def enqueue_scheduled(
|
|
345
|
+
_schedule: Schedule,
|
|
346
|
+
*,
|
|
347
|
+
_envelope: bytes = envelope,
|
|
348
|
+
_entrypoint: str = target_entrypoint,
|
|
349
|
+
_runtime: _PgQueuerRuntime = self,
|
|
350
|
+
) -> None:
|
|
351
|
+
queries = _runtime._require_queries()
|
|
352
|
+
await queries.enqueue(_entrypoint, _envelope)
|
|
353
|
+
|
|
354
|
+
def _executor_factory(
|
|
355
|
+
self,
|
|
356
|
+
definition: BackgroundJobDefinition[BaseModel],
|
|
357
|
+
) -> Callable[[EntrypointExecutorParameters], _ShutdownAwareRetryExecutor]:
|
|
358
|
+
policy = definition.retry_policy
|
|
359
|
+
|
|
360
|
+
def create(parameters: EntrypointExecutorParameters) -> _ShutdownAwareRetryExecutor:
|
|
361
|
+
return _ShutdownAwareRetryExecutor(
|
|
362
|
+
parameters=parameters,
|
|
363
|
+
max_attempts=policy.max_attempts,
|
|
364
|
+
initial_delay=policy.initial_delay,
|
|
365
|
+
max_delay=policy.max_delay,
|
|
366
|
+
backoff_multiplier=policy.backoff_multiplier,
|
|
367
|
+
deadline_cancellation=self._deadline_cancellation,
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
return create
|
|
371
|
+
|
|
372
|
+
async def _execute_job(
|
|
373
|
+
self,
|
|
374
|
+
job: Job,
|
|
375
|
+
definition: BackgroundJobDefinition[BaseModel],
|
|
376
|
+
) -> None:
|
|
377
|
+
task = asyncio.current_task()
|
|
378
|
+
if task is None:
|
|
379
|
+
raise RuntimeError("PgQueuer delivery requires an asyncio Task")
|
|
380
|
+
owned_task = cast(asyncio.Task[object], task)
|
|
381
|
+
self._active_deliveries.add(owned_task)
|
|
382
|
+
try:
|
|
383
|
+
execution_error: BackgroundJobExecutionError | None = None
|
|
384
|
+
try:
|
|
385
|
+
await self._executor.execute(
|
|
386
|
+
job.payload,
|
|
387
|
+
job_id=str(job.id),
|
|
388
|
+
attempt=job.attempts,
|
|
389
|
+
scheduled_at=job.execute_after,
|
|
390
|
+
cancellation_event=self._stop_event,
|
|
391
|
+
connection_name=self._options.connection_name,
|
|
392
|
+
)
|
|
393
|
+
except (
|
|
394
|
+
BackgroundJobDefinitionError,
|
|
395
|
+
BackgroundJobExecutionError,
|
|
396
|
+
BackgroundJobPayloadError,
|
|
397
|
+
):
|
|
398
|
+
raise
|
|
399
|
+
except Exception as original_error:
|
|
400
|
+
# PgQueuer 会持久化 traceback/reason;边界前只允许结构化类型事实离开 handler。
|
|
401
|
+
execution_error = BackgroundJobExecutionError(
|
|
402
|
+
job=definition.name,
|
|
403
|
+
version=definition.version,
|
|
404
|
+
error_type=type(original_error).__name__,
|
|
405
|
+
)
|
|
406
|
+
if execution_error is not None:
|
|
407
|
+
# 离开 except suite 后再抛出,确保 wrapper 连隐式 __context__ 也不保留。
|
|
408
|
+
raise execution_error from None
|
|
409
|
+
finally:
|
|
410
|
+
self._active_deliveries.discard(owned_task)
|
|
411
|
+
|
|
412
|
+
def _require_pgqueuer(self) -> PgQueuer:
|
|
413
|
+
if self._pgqueuer is None:
|
|
414
|
+
raise RuntimeError("PgQueuer runtime is not initialized")
|
|
415
|
+
return self._pgqueuer
|
|
416
|
+
|
|
417
|
+
def _require_queries(self) -> Queries:
|
|
418
|
+
pgqueuer = self._require_pgqueuer()
|
|
419
|
+
queries = pgqueuer.queries
|
|
420
|
+
if not isinstance(queries, Queries):
|
|
421
|
+
raise RuntimeError("PgQueuer runtime does not own Queries")
|
|
422
|
+
return queries
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def _entrypoint_name(name: str, version: int) -> str:
|
|
426
|
+
return f"{name}.v{version}"
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def _schedule_entrypoint_name(schedule: BackgroundJobSchedule) -> str:
|
|
430
|
+
return f"schedule.{schedule.name}"
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def _database_settings(options: PgQueuerBackgroundJobOptions) -> DBSettings:
|
|
434
|
+
prefix = options.prefix
|
|
435
|
+
# 每个字段都显式传入,防止 PgQueuer BaseSettings 从 process environment 建立第二权威。
|
|
436
|
+
return DBSettings(
|
|
437
|
+
prefix=prefix,
|
|
438
|
+
db_schema=options.schema_name,
|
|
439
|
+
channel=f"{prefix}ch_pgqueuer",
|
|
440
|
+
function=f"{prefix}fn_pgqueuer_changed",
|
|
441
|
+
statistics_table=f"{prefix}pgqueuer_statistics",
|
|
442
|
+
queue_status_type=f"{prefix}pgqueuer_status",
|
|
443
|
+
queue_table=f"{prefix}pgqueuer",
|
|
444
|
+
queue_table_log=f"{prefix}pgqueuer_log",
|
|
445
|
+
trigger=f"{prefix}tg_pgqueuer_changed",
|
|
446
|
+
schedules_table=f"{prefix}pgqueuer_schedules",
|
|
447
|
+
durability="durable",
|
|
448
|
+
widen_id=True,
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def _queries_for_pool(
|
|
453
|
+
pool: Any,
|
|
454
|
+
options: PgQueuerBackgroundJobOptions,
|
|
455
|
+
*,
|
|
456
|
+
executor_ready: asyncio.Event,
|
|
457
|
+
scheduler_ready: asyncio.Event,
|
|
458
|
+
) -> Queries:
|
|
459
|
+
settings = _database_settings(options)
|
|
460
|
+
return _ReadinessAwareQueries(
|
|
461
|
+
_ReadinessAwareAsyncpgPoolDriver(pool, executor_ready),
|
|
462
|
+
qbe=QueryBuilderEnvironment(settings=settings),
|
|
463
|
+
qbq=QueryQueueBuilder(settings=settings),
|
|
464
|
+
qbs=QuerySchedulerBuilder(settings=settings),
|
|
465
|
+
ready=scheduler_ready,
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def _queries_for_connection(
|
|
470
|
+
connection: Any,
|
|
471
|
+
options: PgQueuerBackgroundJobOptions,
|
|
472
|
+
) -> Queries:
|
|
473
|
+
settings = _database_settings(options)
|
|
474
|
+
return Queries(
|
|
475
|
+
AsyncpgDriver(connection),
|
|
476
|
+
qbe=QueryBuilderEnvironment(settings=settings),
|
|
477
|
+
qbq=QueryQueueBuilder(settings=settings),
|
|
478
|
+
qbs=QuerySchedulerBuilder(settings=settings),
|
|
479
|
+
)
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
CREATE SCHEMA IF NOT EXISTS python_platform_jobs;
|
|
2
|
+
CREATE TYPE python_platform_jobs.pgqueuer_status AS ENUM ('queued', 'picked', 'successful', 'exception', 'canceled', 'deleted', 'failed');
|
|
3
|
+
CREATE TABLE python_platform_jobs.pgqueuer (
|
|
4
|
+
id BIGSERIAL PRIMARY KEY,
|
|
5
|
+
priority INT NOT NULL,
|
|
6
|
+
queue_manager_id UUID,
|
|
7
|
+
created TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
|
|
8
|
+
updated TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
|
|
9
|
+
heartbeat TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
|
|
10
|
+
execute_after TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
|
|
11
|
+
status python_platform_jobs.pgqueuer_status NOT NULL,
|
|
12
|
+
entrypoint TEXT NOT NULL,
|
|
13
|
+
dedupe_key TEXT,
|
|
14
|
+
payload BYTEA,
|
|
15
|
+
headers JSONB,
|
|
16
|
+
attempts INT NOT NULL DEFAULT 0
|
|
17
|
+
);
|
|
18
|
+
CREATE INDEX pgqueuer_priority_id_id1_idx ON python_platform_jobs.pgqueuer (priority ASC, id DESC)
|
|
19
|
+
INCLUDE (id) WHERE status = 'queued';
|
|
20
|
+
CREATE INDEX pgqueuer_updated_id_id1_idx ON python_platform_jobs.pgqueuer (updated ASC, id DESC)
|
|
21
|
+
INCLUDE (id) WHERE status = 'picked';
|
|
22
|
+
CREATE INDEX pgqueuer_queue_manager_id_idx ON python_platform_jobs.pgqueuer (queue_manager_id)
|
|
23
|
+
WHERE queue_manager_id IS NOT NULL;
|
|
24
|
+
CREATE INDEX pgqueuer_ep_prio_id_idx ON python_platform_jobs.pgqueuer (entrypoint, priority DESC, id ASC)
|
|
25
|
+
WHERE status = 'queued';
|
|
26
|
+
CREATE INDEX pgqueuer_ep_ea_idx ON python_platform_jobs.pgqueuer (entrypoint, execute_after)
|
|
27
|
+
WHERE status = 'queued';
|
|
28
|
+
CREATE UNIQUE INDEX IF NOT EXISTS pgqueuer_unique_dedupe_key ON
|
|
29
|
+
python_platform_jobs.pgqueuer (dedupe_key) WHERE ((status IN ('queued', 'picked') AND dedupe_key IS NOT NULL));
|
|
30
|
+
|
|
31
|
+
CREATE TABLE python_platform_jobs.pgqueuer_log (
|
|
32
|
+
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
|
33
|
+
created TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
|
|
34
|
+
job_id BIGINT NOT NULL,
|
|
35
|
+
status python_platform_jobs.pgqueuer_status NOT NULL,
|
|
36
|
+
priority INT NOT NULL,
|
|
37
|
+
entrypoint TEXT NOT NULL,
|
|
38
|
+
traceback JSONB DEFAULT NULL,
|
|
39
|
+
aggregated BOOLEAN DEFAULT FALSE
|
|
40
|
+
);
|
|
41
|
+
CREATE INDEX pgqueuer_log_not_aggregated ON python_platform_jobs.pgqueuer_log ((1)) WHERE not aggregated;
|
|
42
|
+
CREATE INDEX pgqueuer_log_created ON python_platform_jobs.pgqueuer_log (created);
|
|
43
|
+
CREATE INDEX pgqueuer_log_status ON python_platform_jobs.pgqueuer_log (status);
|
|
44
|
+
CREATE INDEX pgqueuer_log_job_id_status ON python_platform_jobs.pgqueuer_log (job_id, created DESC);
|
|
45
|
+
|
|
46
|
+
CREATE TABLE python_platform_jobs.pgqueuer_statistics (
|
|
47
|
+
id BIGSERIAL PRIMARY KEY,
|
|
48
|
+
created TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT DATE_TRUNC('sec', NOW() at time zone 'UTC'),
|
|
49
|
+
count BIGINT NOT NULL,
|
|
50
|
+
priority INT NOT NULL,
|
|
51
|
+
status python_platform_jobs.pgqueuer_status NOT NULL,
|
|
52
|
+
entrypoint TEXT NOT NULL
|
|
53
|
+
);
|
|
54
|
+
CREATE UNIQUE INDEX pgqueuer_statistics_unique_count ON python_platform_jobs.pgqueuer_statistics (
|
|
55
|
+
priority,
|
|
56
|
+
DATE_TRUNC('sec', created at time zone 'UTC'),
|
|
57
|
+
status,
|
|
58
|
+
entrypoint
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
CREATE TABLE python_platform_jobs.pgqueuer_schedules (
|
|
62
|
+
id BIGSERIAL PRIMARY KEY,
|
|
63
|
+
expression TEXT NOT NULL, -- Crontab-like schedule definition (e.g., '* * * * *')
|
|
64
|
+
entrypoint TEXT NOT NULL,
|
|
65
|
+
heartbeat TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
|
|
66
|
+
created TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
|
|
67
|
+
updated TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
|
|
68
|
+
next_run TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
|
|
69
|
+
last_run TIMESTAMP WITH TIME ZONE,
|
|
70
|
+
status python_platform_jobs.pgqueuer_status DEFAULT 'queued',
|
|
71
|
+
UNIQUE (expression, entrypoint)
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
CREATE FUNCTION python_platform_jobs.fn_pgqueuer_changed() RETURNS TRIGGER AS $$
|
|
75
|
+
DECLARE
|
|
76
|
+
to_emit BOOLEAN := false; -- Flag to decide whether to emit a notification
|
|
77
|
+
BEGIN
|
|
78
|
+
-- Check operation type and set the emit flag accordingly
|
|
79
|
+
IF TG_OP = 'UPDATE' AND OLD IS DISTINCT FROM NEW THEN
|
|
80
|
+
to_emit := true;
|
|
81
|
+
ELSIF TG_OP = 'DELETE' THEN
|
|
82
|
+
to_emit := true;
|
|
83
|
+
ELSIF TG_OP = 'INSERT' THEN
|
|
84
|
+
to_emit := true;
|
|
85
|
+
ELSIF TG_OP = 'TRUNCATE' THEN
|
|
86
|
+
to_emit := true;
|
|
87
|
+
END IF;
|
|
88
|
+
|
|
89
|
+
-- Perform notification if the emit flag is set
|
|
90
|
+
IF to_emit THEN
|
|
91
|
+
PERFORM pg_notify(
|
|
92
|
+
'ch_pgqueuer',
|
|
93
|
+
json_build_object(
|
|
94
|
+
'channel', 'ch_pgqueuer',
|
|
95
|
+
'operation', lower(TG_OP),
|
|
96
|
+
'sent_at', NOW(),
|
|
97
|
+
'table', TG_TABLE_NAME,
|
|
98
|
+
'type', 'table_changed_event'
|
|
99
|
+
)::text
|
|
100
|
+
);
|
|
101
|
+
END IF;
|
|
102
|
+
|
|
103
|
+
-- Return appropriate value based on the operation
|
|
104
|
+
IF TG_OP IN ('INSERT', 'UPDATE') THEN
|
|
105
|
+
RETURN NEW;
|
|
106
|
+
ELSIF TG_OP = 'DELETE' THEN
|
|
107
|
+
RETURN OLD;
|
|
108
|
+
ELSE
|
|
109
|
+
RETURN NULL; -- For TRUNCATE and other non-row-specific contexts
|
|
110
|
+
END IF;
|
|
111
|
+
|
|
112
|
+
END;
|
|
113
|
+
$$ LANGUAGE plpgsql;
|
|
114
|
+
|
|
115
|
+
CREATE TRIGGER tg_pgqueuer_changed
|
|
116
|
+
AFTER INSERT OR UPDATE OR DELETE OR TRUNCATE ON python_platform_jobs.pgqueuer
|
|
117
|
+
EXECUTE FUNCTION python_platform_jobs.fn_pgqueuer_changed();
|
|
118
|
+
|