digitalkin 0.3.3.dev6__tar.gz → 0.3.3.dev8__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.
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/PKG-INFO +1 -1
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/pyproject.toml +6 -6
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/__version__.py +1 -1
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/core/job_manager/base_job_manager.py +6 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/core/job_manager/single_job_manager.py +11 -6
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/core/job_manager/taskiq_broker.py +52 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/core/job_manager/taskiq_job_manager.py +212 -94
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/core/task_manager/task_session.py +2 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/grpc_servers/module_server.py +10 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/grpc_servers/module_servicer.py +22 -2
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/models/module/module_context.py +2 -1
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/modules/_base_module.py +10 -3
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/storage/storage_strategy.py +31 -5
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin.egg-info/PKG-INFO +1 -1
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/LICENSE +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/README.md +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/examples/base_server/__init__.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/examples/base_server/mock/__init__.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/examples/base_server/mock/mock_pb2.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/examples/base_server/mock/mock_pb2_grpc.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/examples/base_server/server_async_insecure.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/examples/base_server/server_async_secure.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/examples/base_server/server_sync_insecure.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/examples/base_server/server_sync_secure.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/examples/modules/__init__.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/examples/modules/archetype_with_tools_module.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/examples/modules/cpu_intensive_module.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/examples/modules/dynamic_setup_module.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/examples/modules/minimal_llm_module.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/examples/modules/text_transform_module.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/examples/monitoring/digitalkin_observability/__init__.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/examples/monitoring/digitalkin_observability/http_server.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/examples/monitoring/digitalkin_observability/interceptors.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/examples/monitoring/digitalkin_observability/metrics.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/examples/monitoring/digitalkin_observability/prometheus.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/examples/monitoring/tests/test_metrics.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/examples/services/filesystem_module.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/examples/services/storage_module.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/setup.cfg +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/__init__.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/core/__init__.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/core/common/__init__.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/core/common/factories.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/core/job_manager/__init__.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/core/profiling/__init__.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/core/profiling/asyncio_monitor.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/core/profiling/task_profiler.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/core/task_manager/__init__.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/core/task_manager/base_task_manager.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/core/task_manager/local_task_manager.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/core/task_manager/remote_task_manager.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/core/task_manager/task_executor.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/grpc_servers/__init__.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/grpc_servers/_base_server.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/grpc_servers/utils/__init__.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/grpc_servers/utils/exceptions.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/grpc_servers/utils/grpc_client_wrapper.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/grpc_servers/utils/grpc_error_handler.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/grpc_servers/utils/utility_schema_extender.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/logger.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/mixins/__init__.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/mixins/base_mixin.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/mixins/callback_mixin.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/mixins/chat_history_mixin.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/mixins/cost_mixin.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/mixins/file_history_mixin.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/mixins/filesystem_mixin.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/mixins/logger_mixin.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/mixins/storage_mixin.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/models/__init__.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/models/core/__init__.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/models/core/job_manager_models.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/models/core/task_monitor.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/models/grpc_servers/__init__.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/models/grpc_servers/models.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/models/grpc_servers/types.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/models/module/__init__.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/models/module/base_types.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/models/module/module.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/models/module/module_types.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/models/module/request_metadata.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/models/module/select_schema.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/models/module/setup_types.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/models/module/tool_cache.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/models/module/tool_reference.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/models/module/utility.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/models/services/__init__.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/models/services/cost.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/models/services/registry.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/models/services/storage.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/modules/__init__.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/modules/archetype_module.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/modules/tool_module.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/modules/trigger_handler.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/modules/triggers/__init__.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/modules/triggers/healthcheck_ping_trigger.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/modules/triggers/healthcheck_services_trigger.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/modules/triggers/healthcheck_status_trigger.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/py.typed +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/__init__.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/agent/__init__.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/agent/agent_strategy.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/agent/default_agent.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/base_strategy.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/communication/__init__.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/communication/communication_strategy.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/communication/default_communication.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/communication/grpc_communication.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/cost/__init__.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/cost/cost_strategy.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/cost/default_cost.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/cost/grpc_cost.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/filesystem/__init__.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/filesystem/default_filesystem.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/filesystem/filesystem_strategy.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/filesystem/grpc_filesystem.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/identity/__init__.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/identity/default_identity.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/identity/identity_strategy.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/registry/__init__.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/registry/default_registry.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/registry/exceptions.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/registry/grpc_registry.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/registry/registry_models.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/registry/registry_strategy.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/services_config.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/services_models.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/setup/__init__.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/setup/default_setup.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/setup/grpc_setup.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/setup/setup_strategy.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/snapshot/__init__.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/snapshot/default_snapshot.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/snapshot/snapshot_strategy.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/storage/__init__.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/storage/default_storage.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/storage/grpc_storage.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/task_manager/__init__.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/task_manager/default_task_manager.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/task_manager/grpc_task_manager.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/task_manager/task_manager_strategy.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/user_profile/__init__.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/user_profile/default_user_profile.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/user_profile/grpc_user_profile.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/user_profile/user_profile_strategy.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/utils/__init__.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/utils/arg_parser.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/utils/conditional_schema.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/utils/development_mode_action.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/utils/dynamic_schema.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/utils/llm_ready_schema.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/utils/package_discover.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/utils/proto_utils.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/utils/schema_splitter.py +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin.egg-info/SOURCES.txt +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin.egg-info/dependency_links.txt +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin.egg-info/requires.txt +0 -0
- {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin.egg-info/top_level.txt +0 -0
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"grpcio-status==1.78.0",
|
|
35
35
|
"pydantic==2.12.5",
|
|
36
36
|
]
|
|
37
|
-
version = "0.3.3.
|
|
37
|
+
version = "0.3.3.dev8"
|
|
38
38
|
|
|
39
39
|
[project.optional-dependencies]
|
|
40
40
|
profiling = [
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"mypy==1.19.1",
|
|
67
67
|
"pre-commit==4.5.1",
|
|
68
68
|
"pyright==1.1.408",
|
|
69
|
-
"ruff==0.15.
|
|
69
|
+
"ruff==0.15.7",
|
|
70
70
|
"twine==6.2.0",
|
|
71
71
|
"types-grpcio-health-checking==1.0.0.20250506",
|
|
72
72
|
"types-grpcio-reflection==1.0.0.20250506",
|
|
@@ -86,13 +86,13 @@
|
|
|
86
86
|
"mkdocs-git-revision-date-localized-plugin==1.5.1",
|
|
87
87
|
"mkdocs-glightbox==0.5.2",
|
|
88
88
|
"mkdocs-include-markdown-plugin==7.2.1",
|
|
89
|
-
"mkdocs-literate-nav==0.6.
|
|
89
|
+
"mkdocs-literate-nav==0.6.3",
|
|
90
90
|
"mkdocs-llmstxt==0.5.0",
|
|
91
|
-
"mkdocs-material[imaging]==9.7.
|
|
91
|
+
"mkdocs-material[imaging]==9.7.6",
|
|
92
92
|
"mkdocs-minify-plugin==0.8.0",
|
|
93
93
|
"mkdocs-open-in-new-tab==1.0.8",
|
|
94
94
|
"mkdocs-redirects==1.2.2",
|
|
95
|
-
"mkdocs-section-index==0.3.
|
|
95
|
+
"mkdocs-section-index==0.3.11",
|
|
96
96
|
"mkdocs==1.6.1",
|
|
97
97
|
"mkdocstrings-python==2.0.3",
|
|
98
98
|
"mkdocstrings==1.0.3",
|
|
@@ -104,7 +104,7 @@
|
|
|
104
104
|
"hdrhistogram==0.10.3",
|
|
105
105
|
"psutil==7.2.2",
|
|
106
106
|
"pytest-asyncio==1.3.0",
|
|
107
|
-
"pytest-cov==7.
|
|
107
|
+
"pytest-cov==7.1.0",
|
|
108
108
|
"pytest-html==4.2.0",
|
|
109
109
|
"pytest-json-report==1.5.0",
|
|
110
110
|
"pytest-timeout==2.4.0",
|
{digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/core/job_manager/base_job_manager.py
RENAMED
|
@@ -130,6 +130,12 @@ class BaseJobManager(abc.ABC, Generic[InputModelT, OutputModelT, SetupModelT]):
|
|
|
130
130
|
required for the job manager to function.
|
|
131
131
|
"""
|
|
132
132
|
|
|
133
|
+
async def stop(self) -> None:
|
|
134
|
+
"""Stop the job manager and clean up resources.
|
|
135
|
+
|
|
136
|
+
Default no-op. Subclasses with external connections override.
|
|
137
|
+
"""
|
|
138
|
+
|
|
133
139
|
@staticmethod
|
|
134
140
|
async def job_specific_callback(
|
|
135
141
|
callback: Callable[[str, DataModel | ModuleCodeModel], Coroutine[Any, Any, None]],
|
|
@@ -91,10 +91,11 @@ class SingleJobManager(BaseJobManager[InputModelT, OutputModelT, SetupModelT]):
|
|
|
91
91
|
message=f"Module {job_id} did not respond within {self._config_setup_timeout} seconds",
|
|
92
92
|
)
|
|
93
93
|
finally:
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
94
|
+
self.tasks_sessions.pop(job_id, None)
|
|
95
|
+
try:
|
|
96
|
+
await session.cleanup()
|
|
97
|
+
except Exception:
|
|
98
|
+
logger.exception("Config setup session cleanup failed", extra={"job_id": job_id})
|
|
98
99
|
|
|
99
100
|
async def create_config_setup_instance_job(
|
|
100
101
|
self,
|
|
@@ -132,8 +133,12 @@ class SingleJobManager(BaseJobManager[InputModelT, OutputModelT, SetupModelT]):
|
|
|
132
133
|
)
|
|
133
134
|
logger.debug("Module %s (%s) started successfully", job_id, module.name)
|
|
134
135
|
except Exception:
|
|
135
|
-
|
|
136
|
-
|
|
136
|
+
session = self.tasks_sessions.pop(job_id, None)
|
|
137
|
+
if session is not None:
|
|
138
|
+
try:
|
|
139
|
+
await session.cleanup()
|
|
140
|
+
except Exception:
|
|
141
|
+
logger.debug("Session cleanup failed during error handling", exc_info=True)
|
|
137
142
|
logger.exception("Failed to start module", extra={"job_id": job_id})
|
|
138
143
|
raise
|
|
139
144
|
else:
|
{digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/core/job_manager/taskiq_broker.py
RENAMED
|
@@ -11,8 +11,10 @@ from rstream import Producer
|
|
|
11
11
|
from rstream.exceptions import PreconditionFailed
|
|
12
12
|
from taskiq import Context, TaskiqDepends, TaskiqMessage
|
|
13
13
|
from taskiq.abc.formatter import TaskiqFormatter
|
|
14
|
+
from taskiq.abc.middleware import TaskiqMiddleware
|
|
14
15
|
from taskiq.compat import model_validate
|
|
15
16
|
from taskiq.message import BrokerMessage
|
|
17
|
+
from taskiq.result import TaskiqResult
|
|
16
18
|
from taskiq_aio_pika import AioPikaBroker
|
|
17
19
|
|
|
18
20
|
from digitalkin.core.common import ModuleFactory
|
|
@@ -175,6 +177,11 @@ class TaskiqBrokerConfig:
|
|
|
175
177
|
startup=[TaskiqBrokerConfig.init_rstream],
|
|
176
178
|
)
|
|
177
179
|
broker.formatter = PickleFormatter()
|
|
180
|
+
redis_url = os.environ.get("DIGITALKIN_TASKIQ_RESULT_BACKEND_URL")
|
|
181
|
+
if redis_url:
|
|
182
|
+
from taskiq_redis import RedisAsyncResultBackend
|
|
183
|
+
|
|
184
|
+
broker.with_result_backend(RedisAsyncResultBackend(redis_url))
|
|
178
185
|
return broker
|
|
179
186
|
|
|
180
187
|
@staticmethod
|
|
@@ -224,9 +231,54 @@ class TaskiqBrokerConfig:
|
|
|
224
231
|
await RSTREAM_PRODUCER.send(stream=TaskiqBrokerConfig.STREAM, message=body)
|
|
225
232
|
|
|
226
233
|
|
|
234
|
+
class TaskiqLifecycleMiddleware(TaskiqMiddleware):
|
|
235
|
+
"""Lifecycle middleware for structured logging and safety-net EndOfStreamOutput."""
|
|
236
|
+
|
|
237
|
+
async def pre_execute(self, message: TaskiqMessage) -> TaskiqMessage: # noqa: PLR6301
|
|
238
|
+
"""Log task start.
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
The unmodified message.
|
|
242
|
+
"""
|
|
243
|
+
logger.info("Taskiq task starting: %s (task_name=%s)", message.task_id, message.task_name)
|
|
244
|
+
return message
|
|
245
|
+
|
|
246
|
+
async def post_execute(self, message: TaskiqMessage, result: TaskiqResult) -> None: # noqa: PLR6301
|
|
247
|
+
"""Log task completion."""
|
|
248
|
+
log_fn = logger.info if not result.is_err else logger.error
|
|
249
|
+
log_fn(
|
|
250
|
+
"Taskiq task finished: %s (task_name=%s, is_err=%s, exec_time=%.3fs)",
|
|
251
|
+
message.task_id,
|
|
252
|
+
message.task_name,
|
|
253
|
+
result.is_err,
|
|
254
|
+
result.execution_time,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
async def on_error( # noqa: PLR6301
|
|
258
|
+
self,
|
|
259
|
+
message: TaskiqMessage,
|
|
260
|
+
result: TaskiqResult, # noqa: ARG002
|
|
261
|
+
exception: BaseException,
|
|
262
|
+
) -> None:
|
|
263
|
+
"""Safety net: send EndOfStreamOutput if worker task failed to."""
|
|
264
|
+
logger.error("Taskiq task error: %s (task_name=%s, error=%s)", message.task_id, message.task_name, exception)
|
|
265
|
+
try:
|
|
266
|
+
await TaskiqBrokerConfig.send_message_to_stream(
|
|
267
|
+
message.task_id,
|
|
268
|
+
ModuleCodeModel(code="WorkerCrash", short_description="Middleware safety net", message=str(exception)),
|
|
269
|
+
)
|
|
270
|
+
await TaskiqBrokerConfig.send_message_to_stream(
|
|
271
|
+
message.task_id,
|
|
272
|
+
DataModel(root=EndOfStreamOutput()),
|
|
273
|
+
)
|
|
274
|
+
except Exception:
|
|
275
|
+
logger.exception("Middleware safety net failed for %s", message.task_id)
|
|
276
|
+
|
|
277
|
+
|
|
227
278
|
# Module-level globals required by Taskiq framework (decorator needs broker at import time)
|
|
228
279
|
RSTREAM_PRODUCER = TaskiqBrokerConfig.define_producer()
|
|
229
280
|
TASKIQ_BROKER = TaskiqBrokerConfig.define_broker()
|
|
281
|
+
TASKIQ_BROKER.add_middlewares(TaskiqLifecycleMiddleware())
|
|
230
282
|
|
|
231
283
|
|
|
232
284
|
@TASKIQ_BROKER.task(task_name="__discarded__")
|
|
@@ -9,6 +9,7 @@ except ImportError:
|
|
|
9
9
|
|
|
10
10
|
import asyncio
|
|
11
11
|
import contextlib
|
|
12
|
+
import datetime
|
|
12
13
|
import json
|
|
13
14
|
import os
|
|
14
15
|
from collections.abc import AsyncGenerator, AsyncIterator
|
|
@@ -37,6 +38,9 @@ class TaskiqJobManager(BaseJobManager[InputModelT, OutputModelT, SetupModelT]):
|
|
|
37
38
|
"""Taskiq job manager for running modules in Taskiq tasks."""
|
|
38
39
|
|
|
39
40
|
services_mode: ServicesMode
|
|
41
|
+
stream_consumer: Consumer
|
|
42
|
+
stream_consumer_task: asyncio.Task[None]
|
|
43
|
+
_reaper_task: asyncio.Task[None]
|
|
40
44
|
|
|
41
45
|
@staticmethod
|
|
42
46
|
async def _on_consumer_closed(reason: Any) -> None:
|
|
@@ -86,8 +90,95 @@ class TaskiqJobManager(BaseJobManager[InputModelT, OutputModelT, SetupModelT]):
|
|
|
86
90
|
job_id = data.get("job_id")
|
|
87
91
|
if not job_id:
|
|
88
92
|
return
|
|
93
|
+
output_data = data.get("output_data")
|
|
89
94
|
if queue := self.job_queues.get(job_id):
|
|
90
|
-
await queue.put(
|
|
95
|
+
await queue.put(output_data)
|
|
96
|
+
|
|
97
|
+
# Bridge session status from RStream terminal markers
|
|
98
|
+
session = self.tasks_sessions.get(job_id)
|
|
99
|
+
if session is None or not isinstance(output_data, dict):
|
|
100
|
+
return
|
|
101
|
+
if "code" in output_data:
|
|
102
|
+
if session.status not in {"cancelled", "failed"}:
|
|
103
|
+
session.status = "failed"
|
|
104
|
+
logger.info("Job %s marked failed from RStream error (code=%s)", job_id, output_data.get("code"))
|
|
105
|
+
elif isinstance(output_data.get("root"), dict) and output_data["root"].get("protocol") == "end_of_stream":
|
|
106
|
+
if session.status not in {"cancelled", "failed"}:
|
|
107
|
+
session.status = "completed"
|
|
108
|
+
logger.info("Job %s marked completed from RStream end_of_stream", job_id)
|
|
109
|
+
session.close_stream()
|
|
110
|
+
|
|
111
|
+
async def _run_consumer_with_restart(self) -> None:
|
|
112
|
+
"""Run the RStream consumer with automatic restart on failure.
|
|
113
|
+
|
|
114
|
+
Raises:
|
|
115
|
+
CancelledError: If the task is cancelled.
|
|
116
|
+
"""
|
|
117
|
+
max_retries = int(os.environ.get("DIGITALKIN_RSTREAM_MAX_RETRIES", "10"))
|
|
118
|
+
base_delay = 1.0
|
|
119
|
+
max_delay = 60.0
|
|
120
|
+
attempt = 0
|
|
121
|
+
|
|
122
|
+
while True:
|
|
123
|
+
try:
|
|
124
|
+
await self.stream_consumer.run()
|
|
125
|
+
break # Normal exit (consumer closed gracefully)
|
|
126
|
+
except asyncio.CancelledError:
|
|
127
|
+
raise
|
|
128
|
+
except Exception:
|
|
129
|
+
attempt += 1
|
|
130
|
+
if attempt > max_retries:
|
|
131
|
+
logger.exception("Stream consumer failed after %d retries, giving up", max_retries)
|
|
132
|
+
for session in list(self.tasks_sessions.values()):
|
|
133
|
+
if session.status == "pending":
|
|
134
|
+
session.status = "failed"
|
|
135
|
+
session.close_stream()
|
|
136
|
+
break
|
|
137
|
+
delay = min(base_delay * (2 ** (attempt - 1)), max_delay)
|
|
138
|
+
logger.exception(
|
|
139
|
+
"Stream consumer failed (attempt %d/%d), restarting in %.1fs", attempt, max_retries, delay
|
|
140
|
+
)
|
|
141
|
+
await asyncio.sleep(delay)
|
|
142
|
+
# Reconnect
|
|
143
|
+
self.stream_consumer = self._define_consumer()
|
|
144
|
+
await self.stream_consumer.create_stream(
|
|
145
|
+
TaskiqBrokerConfig.STREAM,
|
|
146
|
+
exists_ok=True,
|
|
147
|
+
arguments={"max-length-bytes": TaskiqBrokerConfig.STREAM_RETENTION},
|
|
148
|
+
)
|
|
149
|
+
await self.stream_consumer.start()
|
|
150
|
+
await self.stream_consumer.subscribe(
|
|
151
|
+
stream=TaskiqBrokerConfig.STREAM,
|
|
152
|
+
subscriber_name=f"""subscriber_{os.environ.get("SERVER_NAME", "module_servicer")}""",
|
|
153
|
+
callback=self._on_message, # type: ignore[arg-type]
|
|
154
|
+
offset_specification=ConsumerOffsetSpecification(OffsetType.LAST),
|
|
155
|
+
initial_credit=int(os.environ.get("DIGITALKIN_RSTREAM_INITIAL_CREDIT", "50")),
|
|
156
|
+
)
|
|
157
|
+
logger.info("Stream consumer reconnected (attempt %d)", attempt)
|
|
158
|
+
|
|
159
|
+
async def _reap_orphan_sessions(self) -> None:
|
|
160
|
+
"""Mark sessions stuck in pending beyond timeout as failed.
|
|
161
|
+
|
|
162
|
+
Handles hard worker crashes where no EndOfStreamOutput arrives.
|
|
163
|
+
"""
|
|
164
|
+
orphan_timeout = float(os.environ.get("DIGITALKIN_ORPHAN_SESSION_TIMEOUT", "600.0"))
|
|
165
|
+
check_interval = float(os.environ.get("DIGITALKIN_ORPHAN_CHECK_INTERVAL", "60.0"))
|
|
166
|
+
|
|
167
|
+
while True:
|
|
168
|
+
try:
|
|
169
|
+
await asyncio.sleep(check_interval)
|
|
170
|
+
except asyncio.CancelledError:
|
|
171
|
+
return
|
|
172
|
+
now = datetime.datetime.now(datetime.timezone.utc)
|
|
173
|
+
for task_id, session in list(self.tasks_sessions.items()):
|
|
174
|
+
if session.status != "pending":
|
|
175
|
+
continue
|
|
176
|
+
elapsed = (now - session.created_at).total_seconds()
|
|
177
|
+
if elapsed > orphan_timeout:
|
|
178
|
+
logger.warning("Orphan session: %s (pending %.0fs)", task_id, elapsed)
|
|
179
|
+
session.status = "failed"
|
|
180
|
+
session.close_stream()
|
|
181
|
+
await self._task_manager._cleanup_task(task_id, session.mission_id) # noqa: SLF001
|
|
91
182
|
|
|
92
183
|
async def start(self) -> None:
|
|
93
184
|
"""Start the TaskiqJobManager (no-op for external connections)."""
|
|
@@ -116,37 +207,41 @@ class TaskiqJobManager(BaseJobManager[InputModelT, OutputModelT, SetupModelT]):
|
|
|
116
207
|
initial_credit=int(os.environ.get("DIGITALKIN_RSTREAM_INITIAL_CREDIT", "50")),
|
|
117
208
|
)
|
|
118
209
|
|
|
119
|
-
# Wrap the consumer task with error handling
|
|
120
|
-
async def run_consumer_with_error_handling() -> None:
|
|
121
|
-
try:
|
|
122
|
-
await self.stream_consumer.run()
|
|
123
|
-
except asyncio.CancelledError:
|
|
124
|
-
logger.debug("Stream consumer task cancelled")
|
|
125
|
-
raise
|
|
126
|
-
except Exception:
|
|
127
|
-
logger.exception("Stream consumer task failed")
|
|
128
|
-
raise
|
|
129
|
-
|
|
130
210
|
self.stream_consumer_task = asyncio.create_task(
|
|
131
|
-
|
|
211
|
+
self._run_consumer_with_restart(),
|
|
132
212
|
name="stream_consumer_task",
|
|
133
213
|
)
|
|
134
214
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
215
|
+
self._reaper_task = asyncio.create_task(self._reap_orphan_sessions(), name="orphan_session_reaper")
|
|
216
|
+
|
|
217
|
+
async def stop(self) -> None:
|
|
218
|
+
"""Stop the TaskiqJobManager, cancel workers, and clean up all resources."""
|
|
219
|
+
# 1. Cancel reaper
|
|
220
|
+
self._reaper_task.cancel()
|
|
221
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
222
|
+
await self._reaper_task
|
|
223
|
+
|
|
224
|
+
# 2. Cancel all running modules (sends cancel signals to workers)
|
|
225
|
+
await self.stop_all_modules()
|
|
226
|
+
|
|
227
|
+
# 3. Clean remaining sessions (releases semaphore slots)
|
|
228
|
+
for task_id in list(self.tasks_sessions.keys()):
|
|
229
|
+
session = self.tasks_sessions.get(task_id)
|
|
230
|
+
if session is not None:
|
|
231
|
+
await self._task_manager._cleanup_task(task_id, session.mission_id) # noqa: SLF001
|
|
232
|
+
|
|
233
|
+
# 4. Close RStream consumer
|
|
138
234
|
await self.stream_consumer.close()
|
|
139
|
-
# Cancel the background task
|
|
140
235
|
self.stream_consumer_task.cancel()
|
|
141
236
|
with contextlib.suppress(asyncio.CancelledError):
|
|
142
237
|
await self.stream_consumer_task
|
|
143
238
|
|
|
144
|
-
#
|
|
239
|
+
# 5. Clear job queues
|
|
145
240
|
queue_count = len(self.job_queues)
|
|
146
241
|
self.job_queues.clear()
|
|
147
|
-
logger.info("TaskiqJobManager:
|
|
242
|
+
logger.info("TaskiqJobManager stopped: cleared %d queues", queue_count)
|
|
148
243
|
|
|
149
|
-
#
|
|
244
|
+
# 6. Close producer and broker
|
|
150
245
|
await TaskiqBrokerConfig.cleanup_global_resources()
|
|
151
246
|
|
|
152
247
|
def __init__(
|
|
@@ -192,11 +287,11 @@ class TaskiqJobManager(BaseJobManager[InputModelT, OutputModelT, SetupModelT]):
|
|
|
192
287
|
Raises:
|
|
193
288
|
asyncio.TimeoutError: If waiting for the setup response times out.
|
|
194
289
|
"""
|
|
195
|
-
|
|
196
|
-
|
|
290
|
+
if job_id not in self.job_queues:
|
|
291
|
+
self.job_queues[job_id] = QueueFactory.create_bounded_queue(maxsize=self.max_queue_size)
|
|
292
|
+
queue = self.job_queues[job_id]
|
|
197
293
|
|
|
198
294
|
try:
|
|
199
|
-
# Add timeout to prevent indefinite blocking
|
|
200
295
|
item = await asyncio.wait_for(queue.get(), timeout=self._config_setup_timeout)
|
|
201
296
|
except asyncio.TimeoutError:
|
|
202
297
|
logger.error(
|
|
@@ -207,8 +302,9 @@ class TaskiqJobManager(BaseJobManager[InputModelT, OutputModelT, SetupModelT]):
|
|
|
207
302
|
queue.task_done()
|
|
208
303
|
return item
|
|
209
304
|
finally:
|
|
210
|
-
logger.info("generate_config_setup_module_response: job_id=%s: %s", job_id, self.job_queues[job_id].empty())
|
|
211
305
|
self.job_queues.pop(job_id, None)
|
|
306
|
+
if (session := self.tasks_sessions.get(job_id)) is not None:
|
|
307
|
+
await self._task_manager._cleanup_task(job_id, session.mission_id) # noqa: SLF001
|
|
212
308
|
|
|
213
309
|
async def create_config_setup_instance_job(
|
|
214
310
|
self,
|
|
@@ -260,33 +356,45 @@ class TaskiqJobManager(BaseJobManager[InputModelT, OutputModelT, SetupModelT]):
|
|
|
260
356
|
|
|
261
357
|
job_id = running_task.task_id
|
|
262
358
|
|
|
263
|
-
#
|
|
264
|
-
|
|
265
|
-
job_id,
|
|
266
|
-
mission_id=mission_id,
|
|
267
|
-
setup_id=setup_id,
|
|
268
|
-
setup_version_id=setup_version_id,
|
|
269
|
-
request_metadata=request_metadata,
|
|
270
|
-
)
|
|
359
|
+
# Pre-create queue to avoid message drop race
|
|
360
|
+
self.job_queues[job_id] = QueueFactory.create_bounded_queue(maxsize=self.max_queue_size)
|
|
271
361
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
362
|
+
try:
|
|
363
|
+
# Create module instance for metadata
|
|
364
|
+
module = self.module_class(
|
|
365
|
+
job_id,
|
|
366
|
+
mission_id=mission_id,
|
|
367
|
+
setup_id=setup_id,
|
|
368
|
+
setup_version_id=setup_version_id,
|
|
369
|
+
request_metadata=request_metadata,
|
|
370
|
+
)
|
|
275
371
|
|
|
276
|
-
|
|
277
|
-
job_id
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
372
|
+
# Wire RStream callback so stop() can send EndOfStreamOutput
|
|
373
|
+
callback = await self.job_specific_callback(TaskiqBrokerConfig.send_message_to_stream, job_id)
|
|
374
|
+
module.context.callbacks.send_message = callback
|
|
375
|
+
|
|
376
|
+
# Register task in TaskManager (remote mode)
|
|
377
|
+
async def _dummy_coro() -> None:
|
|
378
|
+
"""Dummy coroutine - actual execution happens in worker."""
|
|
379
|
+
|
|
380
|
+
await self.create_task(
|
|
381
|
+
job_id,
|
|
382
|
+
mission_id,
|
|
383
|
+
module,
|
|
384
|
+
_dummy_coro(),
|
|
385
|
+
)
|
|
386
|
+
except Exception:
|
|
387
|
+
self.job_queues.pop(job_id, None)
|
|
388
|
+
raise
|
|
282
389
|
|
|
283
|
-
logger.info("Registered config task: %s
|
|
284
|
-
|
|
285
|
-
|
|
390
|
+
logger.info("Registered config task: %s", job_id)
|
|
391
|
+
if os.environ.get("DIGITALKIN_TASKIQ_RESULT_BACKEND_URL"):
|
|
392
|
+
result = await running_task.wait_result(timeout=10)
|
|
393
|
+
logger.debug("Job %s config result: %s", job_id, result)
|
|
286
394
|
return job_id
|
|
287
395
|
|
|
288
396
|
@asynccontextmanager
|
|
289
|
-
async def generate_stream_consumer(self, job_id: str) -> AsyncIterator[AsyncGenerator[dict[str, Any], None]]:
|
|
397
|
+
async def generate_stream_consumer(self, job_id: str) -> AsyncIterator[AsyncGenerator[dict[str, Any], None]]: # noqa: C901, PLR0915
|
|
290
398
|
"""Generate a stream consumer for the RStream stream.
|
|
291
399
|
|
|
292
400
|
Args:
|
|
@@ -295,10 +403,11 @@ class TaskiqJobManager(BaseJobManager[InputModelT, OutputModelT, SetupModelT]):
|
|
|
295
403
|
Yields:
|
|
296
404
|
messages: The stream messages from the associated module.
|
|
297
405
|
"""
|
|
298
|
-
|
|
299
|
-
|
|
406
|
+
if job_id not in self.job_queues:
|
|
407
|
+
self.job_queues[job_id] = QueueFactory.create_bounded_queue(maxsize=self.max_queue_size)
|
|
408
|
+
queue = self.job_queues[job_id]
|
|
300
409
|
|
|
301
|
-
async def _stream() -> AsyncGenerator[dict[str, Any], Any]:
|
|
410
|
+
async def _stream() -> AsyncGenerator[dict[str, Any], Any]: # noqa: C901
|
|
302
411
|
"""Generate the stream with batch-drain optimization.
|
|
303
412
|
|
|
304
413
|
Yields:
|
|
@@ -352,16 +461,22 @@ class TaskiqJobManager(BaseJobManager[InputModelT, OutputModelT, SetupModelT]):
|
|
|
352
461
|
logger.info("Job %s no longer registered, ending stream", job_id)
|
|
353
462
|
break
|
|
354
463
|
|
|
355
|
-
|
|
464
|
+
session = self.tasks_sessions[job_id]
|
|
465
|
+
if session.stream_closed:
|
|
466
|
+
logger.info("Job %s stream closed, draining queue and ending stream", job_id)
|
|
467
|
+
while not queue.empty():
|
|
468
|
+
item = queue.get_nowait()
|
|
469
|
+
queue.task_done()
|
|
470
|
+
yield item
|
|
471
|
+
break
|
|
356
472
|
|
|
357
|
-
|
|
473
|
+
status = await self.get_module_status(job_id)
|
|
474
|
+
if status in {"cancelled", "failed", "completed"}:
|
|
358
475
|
logger.info("Job %s has terminal status %s, draining queue and ending stream", job_id, status)
|
|
359
|
-
|
|
360
476
|
while not queue.empty():
|
|
361
477
|
item = queue.get_nowait()
|
|
362
478
|
queue.task_done()
|
|
363
479
|
yield item
|
|
364
|
-
|
|
365
480
|
break
|
|
366
481
|
|
|
367
482
|
try:
|
|
@@ -417,29 +532,41 @@ class TaskiqJobManager(BaseJobManager[InputModelT, OutputModelT, SetupModelT]):
|
|
|
417
532
|
)
|
|
418
533
|
job_id = running_task.task_id
|
|
419
534
|
|
|
420
|
-
#
|
|
421
|
-
|
|
422
|
-
job_id,
|
|
423
|
-
mission_id=mission_id,
|
|
424
|
-
setup_id=setup_id,
|
|
425
|
-
setup_version_id=setup_version_id,
|
|
426
|
-
request_metadata=request_metadata,
|
|
427
|
-
)
|
|
535
|
+
# Pre-create queue to avoid message drop race
|
|
536
|
+
self.job_queues[job_id] = QueueFactory.create_bounded_queue(maxsize=self.max_queue_size)
|
|
428
537
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
538
|
+
try:
|
|
539
|
+
# Create module instance for metadata
|
|
540
|
+
module = self.module_class(
|
|
541
|
+
job_id,
|
|
542
|
+
mission_id=mission_id,
|
|
543
|
+
setup_id=setup_id,
|
|
544
|
+
setup_version_id=setup_version_id,
|
|
545
|
+
request_metadata=request_metadata,
|
|
546
|
+
)
|
|
432
547
|
|
|
433
|
-
|
|
434
|
-
job_id
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
548
|
+
# Wire RStream callback so stop() can send EndOfStreamOutput
|
|
549
|
+
callback = await self.job_specific_callback(TaskiqBrokerConfig.send_message_to_stream, job_id)
|
|
550
|
+
module.context.callbacks.send_message = callback
|
|
551
|
+
|
|
552
|
+
# Register task in TaskManager (remote mode)
|
|
553
|
+
async def _dummy_coro() -> None:
|
|
554
|
+
"""Dummy coroutine - actual execution happens in worker."""
|
|
555
|
+
|
|
556
|
+
await self.create_task(
|
|
557
|
+
job_id,
|
|
558
|
+
mission_id,
|
|
559
|
+
module,
|
|
560
|
+
_dummy_coro(),
|
|
561
|
+
)
|
|
562
|
+
except Exception:
|
|
563
|
+
self.job_queues.pop(job_id, None)
|
|
564
|
+
raise
|
|
439
565
|
|
|
440
|
-
logger.info("Registered remote task: %s
|
|
441
|
-
|
|
442
|
-
|
|
566
|
+
logger.info("Registered remote task: %s", job_id)
|
|
567
|
+
if os.environ.get("DIGITALKIN_TASKIQ_RESULT_BACKEND_URL"):
|
|
568
|
+
result = await running_task.wait_result(timeout=10)
|
|
569
|
+
logger.debug("Job %s result: %s", job_id, result)
|
|
443
570
|
return job_id
|
|
444
571
|
|
|
445
572
|
async def get_module_status(self, job_id: str) -> str:
|
|
@@ -458,37 +585,28 @@ class TaskiqJobManager(BaseJobManager[InputModelT, OutputModelT, SetupModelT]):
|
|
|
458
585
|
return session.status
|
|
459
586
|
|
|
460
587
|
async def wait_for_completion(self, job_id: str, max_wait: float = 600.0) -> None:
|
|
461
|
-
"""Wait for a task to complete
|
|
588
|
+
"""Wait for a task to complete via stream-closed event.
|
|
462
589
|
|
|
463
|
-
|
|
464
|
-
|
|
590
|
+
Relies on ``_on_message`` setting ``_stream_closed`` when
|
|
591
|
+
``end_of_stream`` arrives from RStream. Falls back to ``max_wait``
|
|
592
|
+
timeout for crash scenarios.
|
|
465
593
|
|
|
466
594
|
Args:
|
|
467
595
|
job_id: The unique identifier of the job to wait for.
|
|
468
596
|
max_wait: Maximum time in seconds to wait before giving up.
|
|
469
597
|
|
|
470
598
|
Raises:
|
|
471
|
-
KeyError: If the job_id is not found in tasks_sessions.
|
|
472
599
|
asyncio.TimeoutError: If max_wait is exceeded.
|
|
473
600
|
"""
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
if session is None or session.status in terminal_states:
|
|
484
|
-
logger.debug("Job %s reached terminal state: %s", job_id, session.status if session else "removed")
|
|
485
|
-
break
|
|
486
|
-
if elapsed >= max_wait:
|
|
487
|
-
logger.error("Job %s: max wait time (%.1fs) exceeded, giving up", job_id, max_wait)
|
|
488
|
-
raise asyncio.TimeoutError
|
|
489
|
-
await asyncio.sleep(poll_interval)
|
|
490
|
-
elapsed += poll_interval
|
|
491
|
-
poll_interval = min(poll_interval * 2, 0.5)
|
|
601
|
+
session = self.tasks_sessions.get(job_id)
|
|
602
|
+
if session is None:
|
|
603
|
+
return
|
|
604
|
+
try:
|
|
605
|
+
await asyncio.wait_for(session._stream_closed.wait(), timeout=max_wait) # noqa: SLF001
|
|
606
|
+
except asyncio.TimeoutError:
|
|
607
|
+
logger.error("Job %s: max wait time (%.1fs) exceeded", job_id, max_wait)
|
|
608
|
+
raise
|
|
609
|
+
logger.debug("Job %s: stream closed, completion detected (status=%s)", job_id, session.status)
|
|
492
610
|
|
|
493
611
|
async def stop_module(self, job_id: str) -> bool:
|
|
494
612
|
"""Stop a running module using TaskManager.
|
{digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/core/task_manager/task_session.py
RENAMED
|
@@ -31,6 +31,7 @@ class TaskSession:
|
|
|
31
31
|
task_id: str
|
|
32
32
|
mission_id: str
|
|
33
33
|
|
|
34
|
+
created_at: datetime.datetime
|
|
34
35
|
started_at: datetime.datetime | None
|
|
35
36
|
completed_at: datetime.datetime | None
|
|
36
37
|
|
|
@@ -73,6 +74,7 @@ class TaskSession:
|
|
|
73
74
|
self.task_id = task_id
|
|
74
75
|
self.mission_id = mission_id
|
|
75
76
|
|
|
77
|
+
self.created_at = datetime.datetime.now(datetime.timezone.utc)
|
|
76
78
|
self.started_at = None
|
|
77
79
|
self.completed_at = None
|
|
78
80
|
|
{digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/grpc_servers/module_server.py
RENAMED
|
@@ -155,6 +155,16 @@ class ModuleServer(BaseServer):
|
|
|
155
155
|
except Exception:
|
|
156
156
|
logger.exception("Failed to shutdown module servicer resources")
|
|
157
157
|
|
|
158
|
+
try:
|
|
159
|
+
await self.module_servicer.job_manager.stop_all_modules()
|
|
160
|
+
except Exception:
|
|
161
|
+
logger.exception("Failed to stop all modules during shutdown")
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
await self.module_servicer.job_manager.stop()
|
|
165
|
+
except Exception:
|
|
166
|
+
logger.exception("Failed to stop job manager during shutdown")
|
|
167
|
+
|
|
158
168
|
# Close server-level registry channel
|
|
159
169
|
if isinstance(self.registry, GrpcRegistry):
|
|
160
170
|
try:
|