digitalkin 1.0.0.dev18__tar.gz → 1.0.0.dev20__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-1.0.0.dev18 → digitalkin-1.0.0.dev20}/PKG-INFO +1 -1
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/pyproject.toml +1 -1
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/__version__.py +1 -1
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/core/task_manager/module_runner.py +12 -1
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/grpc_servers/gateway_servicer.py +230 -84
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/grpc_servers/utils/circuit_breaker.py +16 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/grpc_servers/utils/grpc_client_wrapper.py +62 -53
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/models/grpc_servers/stream_error_codes.py +1 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/models/settings/gateway.py +22 -1
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin.egg-info/PKG-INFO +1 -1
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/LICENSE +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/README.md +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/examples/base_server/__init__.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/examples/base_server/mock/__init__.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/examples/base_server/mock/mock_pb2.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/examples/base_server/mock/mock_pb2_grpc.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/examples/base_server/server_async_insecure.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/examples/base_server/server_async_secure.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/examples/base_server/server_sync_insecure.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/examples/base_server/server_sync_secure.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/examples/bench_module/__init__.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/examples/bench_module/echo_module.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/examples/bench_module/models/__init__.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/examples/bench_module/models/input.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/examples/bench_module/models/output.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/examples/bench_module/models/secret.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/examples/bench_module/models/setup.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/examples/bench_module/server.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/examples/bench_module/triggers/__init__.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/examples/bench_module/triggers/message_trigger.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/examples/modules/__init__.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/examples/modules/archetype_with_tools_module.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/examples/modules/cpu_intensive_module.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/examples/modules/dynamic_setup_module.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/examples/modules/minimal_llm_module.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/examples/modules/text_transform_module.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/examples/monitoring/digitalkin_observability/__init__.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/examples/monitoring/digitalkin_observability/http_server.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/examples/monitoring/digitalkin_observability/interceptors.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/examples/monitoring/digitalkin_observability/metrics.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/examples/monitoring/digitalkin_observability/prometheus.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/examples/monitoring/tests/test_metrics.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/examples/redis_demo/client.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/examples/redis_demo/echo_module.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/examples/redis_demo/models/__init__.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/examples/redis_demo/models/input.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/examples/redis_demo/models/output.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/examples/redis_demo/models/secret.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/examples/redis_demo/models/setup.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/examples/redis_demo/server.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/examples/redis_demo/triggers/__init__.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/examples/redis_demo/triggers/message_trigger.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/examples/services/filesystem_module.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/examples/services/storage_module.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/setup.cfg +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/__init__.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/community/__init__.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/community/agno/__init__.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/community/agno/agno_adapter.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/community/agno/agui_tools.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/community/agno/hitl.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/community/agno/models.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/core/__init__.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/core/common/__init__.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/core/common/factories.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/core/exceptions.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/core/job_manager/__init__.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/core/job_manager/base_job_manager.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/core/job_manager/single_job_manager.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/core/profiling/__init__.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/core/profiling/step_timer.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/core/profiling/task_profiler.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/core/resilience/__init__.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/core/resilience/bulkhead.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/core/resilience/task_supervisor.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/core/task_manager/__init__.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/core/task_manager/base_task_manager.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/core/task_manager/local_task_manager.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/core/task_manager/redis/__init__.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/core/task_manager/redis/proto_streams.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/core/task_manager/redis/redis_client.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/core/task_manager/redis/redis_idempotency.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/core/task_manager/redis/redis_signal.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/core/task_manager/redis/redis_state.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/core/task_manager/remote_task_manager.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/core/task_manager/task_executor.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/core/task_manager/task_session.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/exceptions.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/grpc_servers/__init__.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/grpc_servers/_base_server.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/grpc_servers/exceptions.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/grpc_servers/interceptors/__init__.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/grpc_servers/interceptors/request_ids.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/grpc_servers/m2m_call_registry.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/grpc_servers/module_server.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/grpc_servers/module_servicer.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/grpc_servers/stream_registry.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/grpc_servers/stream_session.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/grpc_servers/utils/__init__.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/grpc_servers/utils/grpc_error_handler.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/grpc_servers/utils/utility_schema_extender.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/grpc_servers/utils/validators.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/logger.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/mixins/__init__.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/mixins/agui_mixin.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/mixins/base_mixin.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/mixins/cost_mixin.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/mixins/file_history_mixin.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/mixins/filesystem_mixin.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/mixins/logger_mixin.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/mixins/storage_mixin.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/models/__init__.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/models/core/__init__.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/models/core/job_manager_models.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/models/core/redis.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/models/core/task_monitor.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/models/events/__init__.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/models/events/agent_events.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/models/grpc_servers/__init__.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/models/grpc_servers/circuit_breaker.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/models/grpc_servers/m2m.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/models/grpc_servers/models.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/models/grpc_servers/types.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/models/module/__init__.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/models/module/ag_ui.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/models/module/base_types.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/models/module/module.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/models/module/module_context.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/models/module/module_types.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/models/module/request_metadata.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/models/module/select_schema.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/models/module/setup_types.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/models/module/tool_cache.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/models/module/tool_reference.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/models/module/utility.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/models/services/__init__.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/models/services/cost.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/models/services/registry.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/models/services/services.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/models/services/storage.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/models/settings/__init__.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/models/settings/grpc_client.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/models/settings/log.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/models/settings/module.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/models/settings/profiling.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/models/settings/queue.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/models/settings/redis.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/models/settings/resilience.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/models/settings/server/__init__.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/models/settings/server/channel.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/models/settings/server/grpc.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/models/settings/server/server.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/models/settings/server/servicer.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/models/settings/task_manager.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/models/settings/utils/__init__.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/models/settings/utils/channel.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/models/utils/__init__.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/models/utils/dynamic_schema.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/modules/__init__.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/modules/_base_module.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/modules/archetype_module.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/modules/tool_module.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/modules/trigger_handler.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/modules/triggers/__init__.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/modules/triggers/healthcheck_ping_trigger.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/modules/triggers/healthcheck_services_trigger.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/modules/triggers/healthcheck_status_trigger.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/py.typed +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/services/__init__.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/services/base_strategy.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/services/communication/__init__.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/services/communication/communication_strategy.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/services/communication/default_communication.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/services/communication/exceptions.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/services/communication/grpc_communication.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/services/cost/__init__.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/services/cost/cost_strategy.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/services/cost/default_cost.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/services/cost/exceptions.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/services/cost/grpc_cost.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/services/filesystem/__init__.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/services/filesystem/default_filesystem.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/services/filesystem/exceptions.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/services/filesystem/filesystem_strategy.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/services/filesystem/grpc_filesystem.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/services/identity/__init__.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/services/identity/default_identity.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/services/identity/identity_strategy.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/services/registry/__init__.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/services/registry/default_registry.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/services/registry/exceptions.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/services/registry/grpc_registry.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/services/registry/registry_models.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/services/registry/registry_strategy.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/services/services_config.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/services/services_models.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/services/setup/__init__.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/services/setup/default_setup.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/services/setup/exceptions.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/services/setup/grpc_setup.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/services/setup/setup_strategy.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/services/storage/__init__.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/services/storage/default_storage.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/services/storage/exceptions.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/services/storage/grpc_storage.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/services/storage/storage_strategy.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/services/task_manager/__init__.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/services/task_manager/default_task_manager.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/services/task_manager/exceptions.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/services/task_manager/redis_task_manager.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/services/task_manager/task_manager_strategy.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/services/user_profile/__init__.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/services/user_profile/default_user_profile.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/services/user_profile/exceptions.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/services/user_profile/grpc_user_profile.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/services/user_profile/user_profile_strategy.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/utils/__init__.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/utils/arg_parser.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/utils/conditional_schema.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/utils/development_mode_action.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/utils/dynamic_schema.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/utils/exceptions.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/utils/llm_ready_schema.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/utils/package_discover.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/utils/proto_utils.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/utils/schema_splitter.py +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin.egg-info/SOURCES.txt +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin.egg-info/dependency_links.txt +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin.egg-info/requires.txt +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin.egg-info/top_level.txt +0 -0
- {digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/tests/test_exceptions.py +0 -0
{digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/core/task_manager/module_runner.py
RENAMED
|
@@ -8,6 +8,7 @@ from typing import TYPE_CHECKING, Any
|
|
|
8
8
|
|
|
9
9
|
from google.protobuf import json_format, struct_pb2
|
|
10
10
|
from pydantic import ValidationError
|
|
11
|
+
from redis.exceptions import RedisError
|
|
11
12
|
|
|
12
13
|
from digitalkin.core.exceptions import BackpressureTimeoutError
|
|
13
14
|
from digitalkin.core.profiling.step_timer import StepTimer
|
|
@@ -41,7 +42,7 @@ class ModuleRunner:
|
|
|
41
42
|
self._redis_client = redis_client
|
|
42
43
|
self._servicer = servicer
|
|
43
44
|
|
|
44
|
-
async def run( # noqa: PLR0914, PLR0915
|
|
45
|
+
async def run( # noqa: C901, PLR0914, PLR0915
|
|
45
46
|
self,
|
|
46
47
|
query: struct_pb2.Struct,
|
|
47
48
|
*,
|
|
@@ -215,6 +216,16 @@ class ModuleRunner:
|
|
|
215
216
|
except BackpressureTimeoutError as exc:
|
|
216
217
|
logger.exception("ModuleRunner: backpressure timeout", extra=log_extra)
|
|
217
218
|
await on_fatal(StreamErrorCode.BACKPRESSURE_TIMEOUT.value, str(exc))
|
|
219
|
+
except RedisError as exc:
|
|
220
|
+
logger.error(
|
|
221
|
+
"[VALIDATE R2] ModuleRunner: Redis unavailable writing output: task_id=%s",
|
|
222
|
+
task_id,
|
|
223
|
+
extra=log_extra,
|
|
224
|
+
) # TODO(validate): remove after prod validation
|
|
225
|
+
await on_fatal(
|
|
226
|
+
StreamErrorCode.REDIS_UNAVAILABLE.value,
|
|
227
|
+
f"redis unavailable: {type(exc).__name__}: {exc}",
|
|
228
|
+
)
|
|
218
229
|
except Exception as exc:
|
|
219
230
|
logger.exception("ModuleRunner: module job failed", extra=log_extra)
|
|
220
231
|
await on_fatal(
|
{digitalkin-1.0.0.dev18 → digitalkin-1.0.0.dev20}/src/digitalkin/grpc_servers/gateway_servicer.py
RENAMED
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import asyncio
|
|
6
6
|
import contextlib
|
|
7
7
|
import json
|
|
8
|
+
import random
|
|
8
9
|
import time
|
|
9
10
|
from datetime import datetime, timezone
|
|
10
11
|
from typing import TYPE_CHECKING, Any
|
|
@@ -34,7 +35,7 @@ from digitalkin.services.communication.exceptions import InvalidConsumerAddressE
|
|
|
34
35
|
from digitalkin.services.communication.grpc_communication import GrpcCommunication
|
|
35
36
|
|
|
36
37
|
if TYPE_CHECKING:
|
|
37
|
-
from collections.abc import AsyncGenerator, AsyncIterator
|
|
38
|
+
from collections.abc import AsyncGenerator, AsyncIterator, Callable
|
|
38
39
|
|
|
39
40
|
from digitalkin.core.task_manager.module_runner import ModuleRunner
|
|
40
41
|
from digitalkin.core.task_manager.redis.redis_client import RedisClient
|
|
@@ -208,61 +209,36 @@ class GatewayServicer:
|
|
|
208
209
|
return gateway_pb2.StartStreamResponse(accepted=False, task_id=task_id)
|
|
209
210
|
|
|
210
211
|
if self._registry.get(task_id) is not None:
|
|
211
|
-
logger.
|
|
212
|
-
|
|
212
|
+
logger.info(
|
|
213
|
+
"[VALIDATE RD1] StartStream refused: a dial is already live for task_id=%s",
|
|
214
|
+
task_id,
|
|
215
|
+
extra=log_extra,
|
|
216
|
+
) # TODO(validate): remove after prod validation
|
|
217
|
+
return gateway_pb2.StartStreamResponse(accepted=False, task_id=task_id)
|
|
213
218
|
timer.mark("dedup_check")
|
|
214
219
|
|
|
215
220
|
# Durable at-most-once guard: survives session teardown and spans replicas.
|
|
216
|
-
#
|
|
217
|
-
#
|
|
218
|
-
|
|
221
|
+
# Reconnection is server-driven (the live dial-back auto re-dials the same
|
|
222
|
+
# consumer), so a re-issued StartStream for an already-claimed/running task is
|
|
223
|
+
# REFUSED — one task_id maps to exactly one dial.
|
|
224
|
+
try:
|
|
225
|
+
claim = await self._idempotency.claim(task_id, SharedRedisListener.PROCESS_ID)
|
|
226
|
+
except RedisError:
|
|
227
|
+
logger.warning(
|
|
228
|
+
"[VALIDATE R2] StartStream claim Redis failure → accepted=False",
|
|
229
|
+
extra=log_extra,
|
|
230
|
+
exc_info=True,
|
|
231
|
+
) # TODO(validate): remove after prod validation
|
|
232
|
+
return gateway_pb2.StartStreamResponse(accepted=False, task_id=task_id)
|
|
219
233
|
timer.mark("idempotency_claim")
|
|
220
234
|
if claim is not ClaimResult.CLAIMED:
|
|
221
|
-
# Re-issued StartStream for an already-claimed task. The prior dial-back
|
|
222
|
-
# is dead (the dedup branch above returned if its session were still live),
|
|
223
|
-
# so re-dial in resume mode to re-attach the restarted consumer from its
|
|
224
|
-
# cursor — as long as the durable stream is still retained.
|
|
225
|
-
stream_len = await self._redis_client.xlen(f"task:{task_id}:stream")
|
|
226
|
-
if stream_len == 0:
|
|
227
|
-
logger.info(
|
|
228
|
-
"[VALIDATE IDEM1] no live stream to resume (TTL expired) task_id=%s (%s)",
|
|
229
|
-
task_id,
|
|
230
|
-
claim.name,
|
|
231
|
-
extra=log_extra,
|
|
232
|
-
) # TODO(validate): remove after prod validation
|
|
233
|
-
return gateway_pb2.StartStreamResponse(accepted=False, task_id=task_id)
|
|
234
|
-
# Atomic guard: recheck + register + spawn with no await that yields
|
|
235
|
-
# between them, so a concurrent re-poke falls into the dedup branch and acks.
|
|
236
|
-
if self._registry.get(task_id) is not None:
|
|
237
|
-
logger.debug("resume-dial already scheduled, reusing", extra=log_extra)
|
|
238
|
-
return gateway_pb2.StartStreamResponse(accepted=True, task_id=task_id)
|
|
239
|
-
session = StreamSession(task_id=task_id)
|
|
240
|
-
registered = await self._registry.register(
|
|
241
|
-
session,
|
|
242
|
-
setup_id=request.setup_id,
|
|
243
|
-
mission_id=request.mission_id,
|
|
244
|
-
)
|
|
245
|
-
if not registered:
|
|
246
|
-
logger.warning("Resume-dial rejected (capacity)", extra=log_extra)
|
|
247
|
-
return gateway_pb2.StartStreamResponse(accepted=False, task_id=task_id)
|
|
248
235
|
logger.info(
|
|
249
|
-
"[VALIDATE
|
|
236
|
+
"[VALIDATE RD1] StartStream refused: task already claimed/running task_id=%s (%s)",
|
|
250
237
|
task_id,
|
|
251
238
|
claim.name,
|
|
252
|
-
stream_len,
|
|
253
239
|
extra=log_extra,
|
|
254
240
|
) # TODO(validate): remove after prod validation
|
|
255
|
-
|
|
256
|
-
self._dial_consumer(
|
|
257
|
-
task_id=task_id,
|
|
258
|
-
mission_id=request.mission_id,
|
|
259
|
-
setup_id=request.setup_id,
|
|
260
|
-
address=client_address,
|
|
261
|
-
resume=True,
|
|
262
|
-
),
|
|
263
|
-
name=f"dial_consumer_{task_id}",
|
|
264
|
-
)
|
|
265
|
-
return gateway_pb2.StartStreamResponse(accepted=True, task_id=task_id)
|
|
241
|
+
return gateway_pb2.StartStreamResponse(accepted=False, task_id=task_id)
|
|
266
242
|
|
|
267
243
|
session = StreamSession(task_id=task_id)
|
|
268
244
|
accepted = await self._registry.register(
|
|
@@ -274,7 +250,8 @@ class GatewayServicer:
|
|
|
274
250
|
if not accepted:
|
|
275
251
|
logger.warning("Session rejected (capacity)", extra=log_extra)
|
|
276
252
|
# Release the claim so this task can be retried once capacity frees up.
|
|
277
|
-
|
|
253
|
+
with contextlib.suppress(RedisError):
|
|
254
|
+
await self._idempotency.release(task_id)
|
|
278
255
|
return gateway_pb2.StartStreamResponse(accepted=False, task_id=task_id)
|
|
279
256
|
|
|
280
257
|
# Seed stream.start so the consumer's first XREAD finds data immediately.
|
|
@@ -289,10 +266,22 @@ class GatewayServicer:
|
|
|
289
266
|
},
|
|
290
267
|
})
|
|
291
268
|
timer.mark("build_start_info")
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
269
|
+
try:
|
|
270
|
+
await self._redis_client.xadd(
|
|
271
|
+
f"task:{task_id}:stream",
|
|
272
|
+
{"pb": start_info.SerializeToString(), "seq": "0"},
|
|
273
|
+
)
|
|
274
|
+
except RedisError:
|
|
275
|
+
logger.warning(
|
|
276
|
+
"[VALIDATE R2] StartStream xadd(stream.start) Redis failure → accepted=False",
|
|
277
|
+
extra=log_extra,
|
|
278
|
+
exc_info=True,
|
|
279
|
+
) # TODO(validate): remove after prod validation
|
|
280
|
+
# Claimed + registered but the stream couldn't be seeded: undo both so a retry re-runs.
|
|
281
|
+
with contextlib.suppress(RedisError):
|
|
282
|
+
await self._idempotency.release(task_id)
|
|
283
|
+
await self._registry.unregister(task_id)
|
|
284
|
+
return gateway_pb2.StartStreamResponse(accepted=False, task_id=task_id)
|
|
296
285
|
timer.mark("xadd_stream_start")
|
|
297
286
|
|
|
298
287
|
logger.info("→ Dial-back scheduled to consumer %s", client_address, extra=log_extra)
|
|
@@ -424,7 +413,14 @@ class GatewayServicer:
|
|
|
424
413
|
|
|
425
414
|
# Late client: session finished but data still in Redis.
|
|
426
415
|
if session is None:
|
|
427
|
-
|
|
416
|
+
try:
|
|
417
|
+
stream_len = await self._redis_client.xlen(f"task:{task_id}:stream")
|
|
418
|
+
except RedisError:
|
|
419
|
+
async for out in self._fatal_close(
|
|
420
|
+
task_id, StreamErrorCode.REDIS_UNAVAILABLE.value, "redis unavailable"
|
|
421
|
+
):
|
|
422
|
+
yield out
|
|
423
|
+
return
|
|
428
424
|
if stream_len > 0:
|
|
429
425
|
async for resp in self._consume_guarded(task_id, from_seq):
|
|
430
426
|
yield resp
|
|
@@ -436,10 +432,17 @@ class GatewayServicer:
|
|
|
436
432
|
# First message's data is the query.
|
|
437
433
|
input_key = f"task:{task_id}:input"
|
|
438
434
|
if first_msg.data and len(first_msg.data.fields) > 0:
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
435
|
+
try:
|
|
436
|
+
await self._redis_client.xadd(
|
|
437
|
+
input_key,
|
|
438
|
+
{"pb": first_msg.data.SerializeToString()},
|
|
439
|
+
)
|
|
440
|
+
except RedisError:
|
|
441
|
+
async for out in self._fatal_close(
|
|
442
|
+
task_id, StreamErrorCode.REDIS_UNAVAILABLE.value, "redis unavailable"
|
|
443
|
+
):
|
|
444
|
+
yield out
|
|
445
|
+
return
|
|
443
446
|
|
|
444
447
|
upstream_task = self._spawn(
|
|
445
448
|
self._read_peer_upstream(request_iterator, task_id, session),
|
|
@@ -703,37 +706,150 @@ class GatewayServicer:
|
|
|
703
706
|
)
|
|
704
707
|
yield self._sentinel(0, task_id, "stream.end")
|
|
705
708
|
return
|
|
709
|
+
except RedisError:
|
|
710
|
+
logger.warning(
|
|
711
|
+
"[VALIDATE R2] stream read Redis failure — closing stream: task_id=%s",
|
|
712
|
+
task_id,
|
|
713
|
+
) # TODO(validate): remove after prod validation
|
|
714
|
+
yield self._sentinel(
|
|
715
|
+
0,
|
|
716
|
+
task_id,
|
|
717
|
+
"stream.error",
|
|
718
|
+
code=StreamErrorCode.REDIS_UNAVAILABLE.value,
|
|
719
|
+
message="redis unavailable during stream read",
|
|
720
|
+
fatal=True,
|
|
721
|
+
)
|
|
722
|
+
yield self._sentinel(0, task_id, "stream.end")
|
|
723
|
+
return
|
|
706
724
|
yield resp
|
|
707
725
|
|
|
708
|
-
async def _dial_consumer( # noqa: C901
|
|
726
|
+
async def _dial_consumer( # noqa: C901
|
|
709
727
|
self,
|
|
710
728
|
task_id: str,
|
|
711
729
|
mission_id: str,
|
|
712
730
|
setup_id: str,
|
|
713
731
|
address: str,
|
|
714
|
-
*,
|
|
715
|
-
resume: bool = False,
|
|
716
732
|
) -> None:
|
|
717
|
-
"""Dial the consumer's GatewayService.Stream
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
733
|
+
"""Dial the consumer's GatewayService.Stream, with server-side auto-reconnect.
|
|
734
|
+
|
|
735
|
+
First attempt is a fresh dial (``stream.init`` → consumer query →
|
|
736
|
+
``ModuleRunner`` → drain from seq 0). If the BiDi dies before the stream
|
|
737
|
+
is fully delivered and the module is still producing, re-dial the SAME
|
|
738
|
+
address in resume mode (``stream.resume`` → consumer replies with its
|
|
739
|
+
``from_seq`` → drain from that cursor, deduping the Redis queue) with
|
|
740
|
+
jittered backoff until the client returns or the reconnect window
|
|
741
|
+
(``DIGITALKIN_GATEWAY_DIAL_BACK_RECONNECT_WINDOW_S``) elapses. The module
|
|
742
|
+
runs as a separate task, spawned exactly once, so it keeps writing to
|
|
743
|
+
Redis across reconnects.
|
|
725
744
|
|
|
726
745
|
Args:
|
|
727
746
|
task_id: Task to push.
|
|
728
747
|
mission_id: Mission ID (logging context).
|
|
729
748
|
setup_id: Setup ID (logging context).
|
|
730
749
|
address: ``host:port`` of the consumer's GatewayService.
|
|
731
|
-
resume: Re-attach to an existing task's output instead of starting it.
|
|
732
750
|
"""
|
|
733
751
|
log_extra = {"task_id": task_id, "setup_id": setup_id, "mission_id": mission_id}
|
|
734
752
|
# Bind IDs so the dial-back BiDi + logs carry them (task-local spawned context).
|
|
735
753
|
RequestContext.bind(task_id=task_id, setup_id=setup_id, mission_id=mission_id)
|
|
736
754
|
|
|
755
|
+
module_spawned = False
|
|
756
|
+
|
|
757
|
+
def _mark_spawned() -> None:
|
|
758
|
+
nonlocal module_spawned
|
|
759
|
+
module_spawned = True
|
|
760
|
+
|
|
761
|
+
reconnect = get_gateway_settings().dial_reconnect
|
|
762
|
+
deadline: float | None = None
|
|
763
|
+
attempt = 0
|
|
764
|
+
resume = False
|
|
765
|
+
try:
|
|
766
|
+
while True:
|
|
767
|
+
disconnected = await self._run_dial_attempt(
|
|
768
|
+
task_id=task_id,
|
|
769
|
+
mission_id=mission_id,
|
|
770
|
+
setup_id=setup_id,
|
|
771
|
+
address=address,
|
|
772
|
+
resume=resume,
|
|
773
|
+
on_runner_spawn=_mark_spawned,
|
|
774
|
+
)
|
|
775
|
+
if not disconnected:
|
|
776
|
+
break
|
|
777
|
+
# Client BiDi died mid-stream. Re-dial only while there is a
|
|
778
|
+
# retained stream to resume and the reboot window hasn't elapsed.
|
|
779
|
+
if not module_spawned:
|
|
780
|
+
break
|
|
781
|
+
session = self._registry.get(task_id)
|
|
782
|
+
if session is None or session._stop_event.is_set(): # noqa: SLF001
|
|
783
|
+
break
|
|
784
|
+
try:
|
|
785
|
+
stream_len = await self._redis_client.xlen(f"task:{task_id}:stream")
|
|
786
|
+
except RedisError:
|
|
787
|
+
break
|
|
788
|
+
if stream_len == 0:
|
|
789
|
+
break
|
|
790
|
+
now = time.monotonic()
|
|
791
|
+
if deadline is None:
|
|
792
|
+
deadline = now + reconnect.window_s
|
|
793
|
+
if now >= deadline:
|
|
794
|
+
logger.info(
|
|
795
|
+
"[VALIDATE RD1] dial-back gave up after %.0fs reconnect window: task_id=%s attempts=%d",
|
|
796
|
+
reconnect.window_s,
|
|
797
|
+
task_id,
|
|
798
|
+
attempt,
|
|
799
|
+
) # TODO(validate): remove after prod validation
|
|
800
|
+
break
|
|
801
|
+
attempt += 1
|
|
802
|
+
delay = min(random.uniform(reconnect.backoff_base_s, reconnect.backoff_max_s), deadline - now) # noqa: S311
|
|
803
|
+
logger.info(
|
|
804
|
+
"[VALIDATE RD1] dial-back disconnected — re-dial attempt %d in %.1fs (resume): task_id=%s",
|
|
805
|
+
attempt,
|
|
806
|
+
delay,
|
|
807
|
+
task_id,
|
|
808
|
+
) # TODO(validate): remove after prod validation
|
|
809
|
+
await asyncio.sleep(max(0.0, delay))
|
|
810
|
+
resume = True
|
|
811
|
+
finally:
|
|
812
|
+
# End-of-stream cleanup (once). Output stream is left intact for replay.
|
|
813
|
+
try:
|
|
814
|
+
removed = await self._registry.unregister(task_id)
|
|
815
|
+
if removed is not None:
|
|
816
|
+
await removed.teardown()
|
|
817
|
+
except Exception:
|
|
818
|
+
logger.exception("end-of-stream unregister failed", extra=log_extra)
|
|
819
|
+
|
|
820
|
+
async def _run_dial_attempt( # noqa: C901, PLR0912, PLR0914, PLR0915
|
|
821
|
+
self,
|
|
822
|
+
*,
|
|
823
|
+
task_id: str,
|
|
824
|
+
mission_id: str,
|
|
825
|
+
setup_id: str,
|
|
826
|
+
address: str,
|
|
827
|
+
resume: bool,
|
|
828
|
+
on_runner_spawn: Callable[[], None],
|
|
829
|
+
) -> bool:
|
|
830
|
+
"""Run one dial-back BiDi attempt (fresh or resume).
|
|
831
|
+
|
|
832
|
+
Fresh (``resume=False``): sends ``stream.init``, spawns the
|
|
833
|
+
``ModuleRunner`` on the consumer's first reply (calling
|
|
834
|
+
``on_runner_spawn``), drains from seq 0. Resume: sends ``stream.resume``,
|
|
835
|
+
reads the consumer's cursor from the first reply's ``from_seq``, skips
|
|
836
|
+
the runner, drains from that cursor. Releases the channel before
|
|
837
|
+
returning; does NOT unregister the session (the caller owns lifecycle).
|
|
838
|
+
|
|
839
|
+
Args:
|
|
840
|
+
task_id: Task to push.
|
|
841
|
+
mission_id: Mission ID (logging context).
|
|
842
|
+
setup_id: Setup ID (logging context).
|
|
843
|
+
address: ``host:port`` of the consumer's GatewayService.
|
|
844
|
+
resume: Re-attach to an existing task's output instead of starting it.
|
|
845
|
+
on_runner_spawn: Called once, when this attempt spawns the module runner.
|
|
846
|
+
|
|
847
|
+
Returns:
|
|
848
|
+
True if the BiDi died before delivering ``stream.end`` (a re-dial
|
|
849
|
+
candidate); False on clean completion or a terminal failure.
|
|
850
|
+
"""
|
|
851
|
+
log_extra = {"task_id": task_id, "setup_id": setup_id, "mission_id": mission_id}
|
|
852
|
+
|
|
737
853
|
async def _fail(code: str, message: str) -> None:
|
|
738
854
|
"""Emit a fatal to Redis (fresh dial) or log only (resume).
|
|
739
855
|
|
|
@@ -758,7 +874,7 @@ class GatewayServicer:
|
|
|
758
874
|
StreamErrorCode.DIAL_BACK_INTERNAL.value,
|
|
759
875
|
"gateway has no client_config to dial back with",
|
|
760
876
|
)
|
|
761
|
-
return
|
|
877
|
+
return False
|
|
762
878
|
# setup_version_id is unused on the dial-back path.
|
|
763
879
|
comm = GrpcCommunication(
|
|
764
880
|
mission_id=mission_id,
|
|
@@ -780,14 +896,14 @@ class GatewayServicer:
|
|
|
780
896
|
StreamErrorCode.DIAL_BACK_UNREACHABLE.value,
|
|
781
897
|
f"dial-back channel build failed: {exc}",
|
|
782
898
|
)
|
|
783
|
-
return
|
|
899
|
+
return False
|
|
784
900
|
except OSError as exc:
|
|
785
901
|
logger.exception("dial_consumer: channel build failed addr=%s", address, extra=log_extra)
|
|
786
902
|
await _fail(
|
|
787
903
|
StreamErrorCode.DIAL_BACK_UNREACHABLE.value,
|
|
788
904
|
f"dial-back channel build failed: {type(exc).__name__}: {exc}",
|
|
789
905
|
)
|
|
790
|
-
return
|
|
906
|
+
return False
|
|
791
907
|
t_stub = time.perf_counter_ns()
|
|
792
908
|
logger.info("→ Dial-back channel ready to %s", address, extra=log_extra)
|
|
793
909
|
|
|
@@ -820,7 +936,7 @@ class GatewayServicer:
|
|
|
820
936
|
extra=log_extra,
|
|
821
937
|
)
|
|
822
938
|
await release()
|
|
823
|
-
return
|
|
939
|
+
return False
|
|
824
940
|
|
|
825
941
|
# Outbound is StreamServer, inbound is StreamClient — both share
|
|
826
942
|
# field tags so re-wrapping ``_consume_from_redis`` output is a rename.
|
|
@@ -835,8 +951,11 @@ class GatewayServicer:
|
|
|
835
951
|
outgoing_done = asyncio.Event()
|
|
836
952
|
# Consumer's resume cursor, captured from the first reply's ``from_seq``.
|
|
837
953
|
resume_cursor = 0
|
|
954
|
+
# Set once the reader reaches EOS and stream.end is delivered to the consumer.
|
|
955
|
+
delivered_eos = False
|
|
838
956
|
|
|
839
957
|
async def _outgoing() -> AsyncGenerator:
|
|
958
|
+
nonlocal delivered_eos
|
|
840
959
|
try:
|
|
841
960
|
yield init_server
|
|
842
961
|
logger.info(
|
|
@@ -857,6 +976,7 @@ class GatewayServicer:
|
|
|
857
976
|
try:
|
|
858
977
|
cli_msg = await asyncio.wait_for(anext(reader_iter), timeout=idle_timeout)
|
|
859
978
|
except StopAsyncIteration:
|
|
979
|
+
delivered_eos = True
|
|
860
980
|
break
|
|
861
981
|
except asyncio.TimeoutError:
|
|
862
982
|
logger.warning(
|
|
@@ -869,6 +989,25 @@ class GatewayServicer:
|
|
|
869
989
|
f"dial-back idle timeout: no module output in {idle_timeout:.0f}s",
|
|
870
990
|
)
|
|
871
991
|
return
|
|
992
|
+
except RedisError:
|
|
993
|
+
logger.warning(
|
|
994
|
+
"[VALIDATE R2] dial-back stream read Redis failure — closing BiDi: task_id=%s",
|
|
995
|
+
task_id,
|
|
996
|
+
extra=log_extra,
|
|
997
|
+
) # TODO(validate): remove after prod validation
|
|
998
|
+
for sc in (
|
|
999
|
+
self._sentinel(
|
|
1000
|
+
0,
|
|
1001
|
+
task_id,
|
|
1002
|
+
"stream.error",
|
|
1003
|
+
code=StreamErrorCode.REDIS_UNAVAILABLE.value,
|
|
1004
|
+
message="redis unavailable during stream read",
|
|
1005
|
+
fatal=True,
|
|
1006
|
+
),
|
|
1007
|
+
self._sentinel(0, task_id, "stream.end"),
|
|
1008
|
+
):
|
|
1009
|
+
yield gateway_pb2.StreamServer(task_id=task_id, seq=sc.from_seq, data=sc.data)
|
|
1010
|
+
return
|
|
872
1011
|
yield gateway_pb2.StreamServer(
|
|
873
1012
|
task_id=task_id,
|
|
874
1013
|
seq=cli_msg.from_seq,
|
|
@@ -880,6 +1019,7 @@ class GatewayServicer:
|
|
|
880
1019
|
async def _runner_fatal(code: str, message: str) -> None:
|
|
881
1020
|
await self._emit_fatal_to_redis(task_id, code=code, message=message, log_extra=log_extra)
|
|
882
1021
|
|
|
1022
|
+
retriable = False
|
|
883
1023
|
t_pre_stream = time.perf_counter_ns()
|
|
884
1024
|
logger.info(
|
|
885
1025
|
"[dial-debug] pre_stream dt_since_ready=%.3fms ch_state=%s",
|
|
@@ -953,7 +1093,7 @@ class GatewayServicer:
|
|
|
953
1093
|
"gateway has no ModuleRunner configured",
|
|
954
1094
|
)
|
|
955
1095
|
output_started.set()
|
|
956
|
-
return
|
|
1096
|
+
return False
|
|
957
1097
|
self._spawn(
|
|
958
1098
|
self._module_runner.run(
|
|
959
1099
|
upstream.data,
|
|
@@ -964,14 +1104,23 @@ class GatewayServicer:
|
|
|
964
1104
|
),
|
|
965
1105
|
name=f"module_runner_{task_id}",
|
|
966
1106
|
)
|
|
1107
|
+
on_runner_spawn()
|
|
967
1108
|
output_started.set()
|
|
968
1109
|
first = False
|
|
969
1110
|
continue
|
|
970
1111
|
# Follow-up multi-turn input → task's input stream.
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
1112
|
+
try:
|
|
1113
|
+
await self._redis_client.xadd(
|
|
1114
|
+
f"task:{task_id}:input",
|
|
1115
|
+
{"pb": upstream.data.SerializeToString()},
|
|
1116
|
+
)
|
|
1117
|
+
except RedisError:
|
|
1118
|
+
logger.warning(
|
|
1119
|
+
"[VALIDATE R2] dropped follow-up input (redis unavailable): task_id=%s data=%s",
|
|
1120
|
+
task_id,
|
|
1121
|
+
str(upstream.data)[:512],
|
|
1122
|
+
extra=log_extra,
|
|
1123
|
+
) # TODO(validate): remove after prod validation
|
|
975
1124
|
except grpc.aio.AioRpcError as exc:
|
|
976
1125
|
code_name = exc.code().name
|
|
977
1126
|
details = exc.details() or ""
|
|
@@ -997,6 +1146,8 @@ class GatewayServicer:
|
|
|
997
1146
|
)
|
|
998
1147
|
# Suppress DIAL_BACK_NO_QUERY in finally — RPC error already emitted.
|
|
999
1148
|
output_started.set()
|
|
1149
|
+
# Client BiDi died before the stream finished → re-dial candidate.
|
|
1150
|
+
retriable = not delivered_eos
|
|
1000
1151
|
except _GrpcUsageError:
|
|
1001
1152
|
t_fail = time.perf_counter_ns()
|
|
1002
1153
|
logger.warning(
|
|
@@ -1013,6 +1164,7 @@ class GatewayServicer:
|
|
|
1013
1164
|
"dial-back channel closed before BiDi could start",
|
|
1014
1165
|
)
|
|
1015
1166
|
output_started.set()
|
|
1167
|
+
retriable = not delivered_eos
|
|
1016
1168
|
except (RuntimeError, AssertionError, ValueError):
|
|
1017
1169
|
logger.exception("dial_consumer unexpected error", extra=log_extra)
|
|
1018
1170
|
if not output_started.is_set():
|
|
@@ -1035,10 +1187,4 @@ class GatewayServicer:
|
|
|
1035
1187
|
# Unblock _outgoing if consumer never replied.
|
|
1036
1188
|
output_started.set()
|
|
1037
1189
|
await release()
|
|
1038
|
-
|
|
1039
|
-
try:
|
|
1040
|
-
removed = await self._registry.unregister(task_id)
|
|
1041
|
-
if removed is not None:
|
|
1042
|
-
await removed.teardown()
|
|
1043
|
-
except Exception:
|
|
1044
|
-
logger.exception("end-of-stream unregister failed", extra=log_extra)
|
|
1190
|
+
return retriable
|
|
@@ -134,6 +134,22 @@ class CircuitBreaker:
|
|
|
134
134
|
self._failure_count,
|
|
135
135
|
)
|
|
136
136
|
|
|
137
|
+
def release_probe(self) -> bool:
|
|
138
|
+
"""Release a half-open probe slot without recording an outcome.
|
|
139
|
+
|
|
140
|
+
Called when an in-flight probe is abandoned (e.g. the caller was
|
|
141
|
+
cancelled) so the breaker cannot wedge with the lock held. A cancelled
|
|
142
|
+
probe is neither success nor failure — the slot simply frees for the
|
|
143
|
+
next caller. No-op unless a probe lock is currently held.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
True if a held probe lock was released; False if none was held.
|
|
147
|
+
"""
|
|
148
|
+
if self._state == CBState.HALF_OPEN and self._half_open_lock:
|
|
149
|
+
self._half_open_lock = False
|
|
150
|
+
return True
|
|
151
|
+
return False
|
|
152
|
+
|
|
137
153
|
def reset(self) -> None:
|
|
138
154
|
"""Force reset to CLOSED state."""
|
|
139
155
|
self._state = CBState.CLOSED
|