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.
Files changed (158) hide show
  1. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/PKG-INFO +1 -1
  2. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/pyproject.toml +6 -6
  3. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/__version__.py +1 -1
  4. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/core/job_manager/base_job_manager.py +6 -0
  5. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/core/job_manager/single_job_manager.py +11 -6
  6. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/core/job_manager/taskiq_broker.py +52 -0
  7. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/core/job_manager/taskiq_job_manager.py +212 -94
  8. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/core/task_manager/task_session.py +2 -0
  9. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/grpc_servers/module_server.py +10 -0
  10. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/grpc_servers/module_servicer.py +22 -2
  11. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/models/module/module_context.py +2 -1
  12. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/modules/_base_module.py +10 -3
  13. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/storage/storage_strategy.py +31 -5
  14. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin.egg-info/PKG-INFO +1 -1
  15. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/LICENSE +0 -0
  16. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/README.md +0 -0
  17. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/examples/base_server/__init__.py +0 -0
  18. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/examples/base_server/mock/__init__.py +0 -0
  19. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/examples/base_server/mock/mock_pb2.py +0 -0
  20. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/examples/base_server/mock/mock_pb2_grpc.py +0 -0
  21. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/examples/base_server/server_async_insecure.py +0 -0
  22. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/examples/base_server/server_async_secure.py +0 -0
  23. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/examples/base_server/server_sync_insecure.py +0 -0
  24. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/examples/base_server/server_sync_secure.py +0 -0
  25. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/examples/modules/__init__.py +0 -0
  26. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/examples/modules/archetype_with_tools_module.py +0 -0
  27. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/examples/modules/cpu_intensive_module.py +0 -0
  28. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/examples/modules/dynamic_setup_module.py +0 -0
  29. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/examples/modules/minimal_llm_module.py +0 -0
  30. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/examples/modules/text_transform_module.py +0 -0
  31. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/examples/monitoring/digitalkin_observability/__init__.py +0 -0
  32. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/examples/monitoring/digitalkin_observability/http_server.py +0 -0
  33. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/examples/monitoring/digitalkin_observability/interceptors.py +0 -0
  34. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/examples/monitoring/digitalkin_observability/metrics.py +0 -0
  35. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/examples/monitoring/digitalkin_observability/prometheus.py +0 -0
  36. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/examples/monitoring/tests/test_metrics.py +0 -0
  37. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/examples/services/filesystem_module.py +0 -0
  38. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/examples/services/storage_module.py +0 -0
  39. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/setup.cfg +0 -0
  40. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/__init__.py +0 -0
  41. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/core/__init__.py +0 -0
  42. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/core/common/__init__.py +0 -0
  43. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/core/common/factories.py +0 -0
  44. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/core/job_manager/__init__.py +0 -0
  45. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/core/profiling/__init__.py +0 -0
  46. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/core/profiling/asyncio_monitor.py +0 -0
  47. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/core/profiling/task_profiler.py +0 -0
  48. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/core/task_manager/__init__.py +0 -0
  49. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/core/task_manager/base_task_manager.py +0 -0
  50. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/core/task_manager/local_task_manager.py +0 -0
  51. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/core/task_manager/remote_task_manager.py +0 -0
  52. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/core/task_manager/task_executor.py +0 -0
  53. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/grpc_servers/__init__.py +0 -0
  54. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/grpc_servers/_base_server.py +0 -0
  55. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/grpc_servers/utils/__init__.py +0 -0
  56. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/grpc_servers/utils/exceptions.py +0 -0
  57. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/grpc_servers/utils/grpc_client_wrapper.py +0 -0
  58. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/grpc_servers/utils/grpc_error_handler.py +0 -0
  59. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/grpc_servers/utils/utility_schema_extender.py +0 -0
  60. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/logger.py +0 -0
  61. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/mixins/__init__.py +0 -0
  62. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/mixins/base_mixin.py +0 -0
  63. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/mixins/callback_mixin.py +0 -0
  64. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/mixins/chat_history_mixin.py +0 -0
  65. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/mixins/cost_mixin.py +0 -0
  66. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/mixins/file_history_mixin.py +0 -0
  67. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/mixins/filesystem_mixin.py +0 -0
  68. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/mixins/logger_mixin.py +0 -0
  69. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/mixins/storage_mixin.py +0 -0
  70. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/models/__init__.py +0 -0
  71. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/models/core/__init__.py +0 -0
  72. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/models/core/job_manager_models.py +0 -0
  73. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/models/core/task_monitor.py +0 -0
  74. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/models/grpc_servers/__init__.py +0 -0
  75. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/models/grpc_servers/models.py +0 -0
  76. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/models/grpc_servers/types.py +0 -0
  77. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/models/module/__init__.py +0 -0
  78. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/models/module/base_types.py +0 -0
  79. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/models/module/module.py +0 -0
  80. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/models/module/module_types.py +0 -0
  81. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/models/module/request_metadata.py +0 -0
  82. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/models/module/select_schema.py +0 -0
  83. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/models/module/setup_types.py +0 -0
  84. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/models/module/tool_cache.py +0 -0
  85. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/models/module/tool_reference.py +0 -0
  86. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/models/module/utility.py +0 -0
  87. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/models/services/__init__.py +0 -0
  88. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/models/services/cost.py +0 -0
  89. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/models/services/registry.py +0 -0
  90. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/models/services/storage.py +0 -0
  91. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/modules/__init__.py +0 -0
  92. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/modules/archetype_module.py +0 -0
  93. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/modules/tool_module.py +0 -0
  94. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/modules/trigger_handler.py +0 -0
  95. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/modules/triggers/__init__.py +0 -0
  96. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/modules/triggers/healthcheck_ping_trigger.py +0 -0
  97. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/modules/triggers/healthcheck_services_trigger.py +0 -0
  98. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/modules/triggers/healthcheck_status_trigger.py +0 -0
  99. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/py.typed +0 -0
  100. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/__init__.py +0 -0
  101. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/agent/__init__.py +0 -0
  102. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/agent/agent_strategy.py +0 -0
  103. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/agent/default_agent.py +0 -0
  104. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/base_strategy.py +0 -0
  105. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/communication/__init__.py +0 -0
  106. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/communication/communication_strategy.py +0 -0
  107. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/communication/default_communication.py +0 -0
  108. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/communication/grpc_communication.py +0 -0
  109. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/cost/__init__.py +0 -0
  110. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/cost/cost_strategy.py +0 -0
  111. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/cost/default_cost.py +0 -0
  112. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/cost/grpc_cost.py +0 -0
  113. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/filesystem/__init__.py +0 -0
  114. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/filesystem/default_filesystem.py +0 -0
  115. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/filesystem/filesystem_strategy.py +0 -0
  116. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/filesystem/grpc_filesystem.py +0 -0
  117. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/identity/__init__.py +0 -0
  118. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/identity/default_identity.py +0 -0
  119. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/identity/identity_strategy.py +0 -0
  120. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/registry/__init__.py +0 -0
  121. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/registry/default_registry.py +0 -0
  122. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/registry/exceptions.py +0 -0
  123. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/registry/grpc_registry.py +0 -0
  124. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/registry/registry_models.py +0 -0
  125. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/registry/registry_strategy.py +0 -0
  126. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/services_config.py +0 -0
  127. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/services_models.py +0 -0
  128. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/setup/__init__.py +0 -0
  129. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/setup/default_setup.py +0 -0
  130. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/setup/grpc_setup.py +0 -0
  131. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/setup/setup_strategy.py +0 -0
  132. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/snapshot/__init__.py +0 -0
  133. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/snapshot/default_snapshot.py +0 -0
  134. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/snapshot/snapshot_strategy.py +0 -0
  135. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/storage/__init__.py +0 -0
  136. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/storage/default_storage.py +0 -0
  137. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/storage/grpc_storage.py +0 -0
  138. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/task_manager/__init__.py +0 -0
  139. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/task_manager/default_task_manager.py +0 -0
  140. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/task_manager/grpc_task_manager.py +0 -0
  141. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/task_manager/task_manager_strategy.py +0 -0
  142. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/user_profile/__init__.py +0 -0
  143. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/user_profile/default_user_profile.py +0 -0
  144. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/user_profile/grpc_user_profile.py +0 -0
  145. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/services/user_profile/user_profile_strategy.py +0 -0
  146. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/utils/__init__.py +0 -0
  147. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/utils/arg_parser.py +0 -0
  148. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/utils/conditional_schema.py +0 -0
  149. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/utils/development_mode_action.py +0 -0
  150. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/utils/dynamic_schema.py +0 -0
  151. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/utils/llm_ready_schema.py +0 -0
  152. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/utils/package_discover.py +0 -0
  153. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/utils/proto_utils.py +0 -0
  154. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin/utils/schema_splitter.py +0 -0
  155. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin.egg-info/SOURCES.txt +0 -0
  156. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin.egg-info/dependency_links.txt +0 -0
  157. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin.egg-info/requires.txt +0 -0
  158. {digitalkin-0.3.3.dev6 → digitalkin-0.3.3.dev8}/src/digitalkin.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: digitalkin
3
- Version: 0.3.3.dev6
3
+ Version: 0.3.3.dev8
4
4
  Summary: SDK to build kin used in DigitalKin
5
5
  Author-email: "DigitalKin.ai" <contact@digitalkin.ai>
6
6
  License: Attribution-NonCommercial-ShareAlike 4.0 International
@@ -34,7 +34,7 @@
34
34
  "grpcio-status==1.78.0",
35
35
  "pydantic==2.12.5",
36
36
  ]
37
- version = "0.3.3.dev6"
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.5",
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.2",
89
+ "mkdocs-literate-nav==0.6.3",
90
90
  "mkdocs-llmstxt==0.5.0",
91
- "mkdocs-material[imaging]==9.7.5",
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.10",
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.0.0",
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",
@@ -5,4 +5,4 @@ from importlib.metadata import PackageNotFoundError, version
5
5
  try:
6
6
  __version__ = version("digitalkin")
7
7
  except PackageNotFoundError:
8
- __version__ = "0.3.3.dev6"
8
+ __version__ = "0.3.3.dev8"
@@ -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
- logger.debug(
95
- "Config setup response retrieved",
96
- extra={"job_id": job_id, "queue_empty": session.queue.empty()},
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
- # Remove the module from the manager in case of an error.
136
- del self.tasks_sessions[job_id]
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:
@@ -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(data.get("output_data"))
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
- run_consumer_with_error_handling(),
211
+ self._run_consumer_with_restart(),
132
212
  name="stream_consumer_task",
133
213
  )
134
214
 
135
- async def _stop(self) -> None:
136
- """Stop the TaskiqJobManager and clean up all resources."""
137
- # Signal the consumer to stop
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
- # Clean up job queues
239
+ # 5. Clear job queues
145
240
  queue_count = len(self.job_queues)
146
241
  self.job_queues.clear()
147
- logger.info("TaskiqJobManager: Cleared %d job queues", queue_count)
242
+ logger.info("TaskiqJobManager stopped: cleared %d queues", queue_count)
148
243
 
149
- # Call global cleanup for producer and broker
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
- queue = QueueFactory.create_bounded_queue(maxsize=self.max_queue_size)
196
- self.job_queues[job_id] = queue
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
- # Create module instance for metadata
264
- module = self.module_class(
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
- # Register task in TaskManager (remote mode)
273
- async def _dummy_coro() -> None:
274
- """Dummy coroutine - actual execution happens in worker."""
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
- await self.create_task(
277
- job_id,
278
- mission_id,
279
- module,
280
- _dummy_coro(),
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, waiting for initial result", job_id)
284
- result = await running_task.wait_result(timeout=10)
285
- logger.info("Job %s with data %s", job_id, result)
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
- queue = QueueFactory.create_bounded_queue(maxsize=self.max_queue_size)
299
- self.job_queues[job_id] = queue
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
- status = await self.get_module_status(job_id)
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
- if status in {"cancelled", "failed"}:
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
- # Create module instance for metadata
421
- module = self.module_class(
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
- # Register task in TaskManager (remote mode)
430
- async def _dummy_coro() -> None:
431
- """Dummy coroutine - actual execution happens in worker."""
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
- await self.create_task(
434
- job_id,
435
- mission_id,
436
- module,
437
- _dummy_coro(),
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, waiting for initial result", job_id)
441
- result = await running_task.wait_result(timeout=10)
442
- logger.debug("Job %s with data %s", job_id, result)
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 by polling its status.
588
+ """Wait for a task to complete via stream-closed event.
462
589
 
463
- Uses adaptive polling: starts at 50ms for fast jobs, doubles up to 500ms
464
- for long-running tasks to reduce CPU overhead while maintaining low latency.
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
- if job_id not in self.tasks_sessions:
475
- msg = f"Job {job_id} not found"
476
- raise KeyError(msg)
477
-
478
- terminal_states = {"completed", "failed", "cancelled"}
479
- poll_interval = 0.05
480
- elapsed = 0.0
481
- while True:
482
- session = self.tasks_sessions.get(job_id)
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.
@@ -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
 
@@ -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: