omnibase_infra 0.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- omnibase_infra/__init__.py +101 -0
- omnibase_infra/cli/__init__.py +1 -0
- omnibase_infra/cli/commands.py +216 -0
- omnibase_infra/clients/__init__.py +0 -0
- omnibase_infra/contracts/handlers/filesystem/handler_contract.yaml +261 -0
- omnibase_infra/contracts/handlers/mcp/handler_contract.yaml +138 -0
- omnibase_infra/decorators/__init__.py +29 -0
- omnibase_infra/decorators/allow_any.py +109 -0
- omnibase_infra/dlq/__init__.py +90 -0
- omnibase_infra/dlq/constants_dlq.py +57 -0
- omnibase_infra/dlq/models/__init__.py +26 -0
- omnibase_infra/dlq/models/enum_replay_status.py +37 -0
- omnibase_infra/dlq/models/model_dlq_replay_record.py +135 -0
- omnibase_infra/dlq/models/model_dlq_tracking_config.py +184 -0
- omnibase_infra/dlq/service_dlq_tracking.py +611 -0
- omnibase_infra/enums/__init__.py +123 -0
- omnibase_infra/enums/enum_any_type_violation.py +104 -0
- omnibase_infra/enums/enum_backend_type.py +27 -0
- omnibase_infra/enums/enum_capture_outcome.py +42 -0
- omnibase_infra/enums/enum_capture_state.py +88 -0
- omnibase_infra/enums/enum_chain_violation_type.py +119 -0
- omnibase_infra/enums/enum_circuit_state.py +51 -0
- omnibase_infra/enums/enum_confirmation_event_type.py +27 -0
- omnibase_infra/enums/enum_contract_type.py +84 -0
- omnibase_infra/enums/enum_dedupe_strategy.py +46 -0
- omnibase_infra/enums/enum_dispatch_status.py +191 -0
- omnibase_infra/enums/enum_environment.py +46 -0
- omnibase_infra/enums/enum_execution_shape_violation.py +103 -0
- omnibase_infra/enums/enum_handler_error_type.py +101 -0
- omnibase_infra/enums/enum_handler_loader_error.py +178 -0
- omnibase_infra/enums/enum_handler_source_type.py +87 -0
- omnibase_infra/enums/enum_handler_type.py +77 -0
- omnibase_infra/enums/enum_handler_type_category.py +61 -0
- omnibase_infra/enums/enum_infra_transport_type.py +73 -0
- omnibase_infra/enums/enum_introspection_reason.py +154 -0
- omnibase_infra/enums/enum_message_category.py +213 -0
- omnibase_infra/enums/enum_node_archetype.py +74 -0
- omnibase_infra/enums/enum_node_output_type.py +185 -0
- omnibase_infra/enums/enum_non_retryable_error_category.py +224 -0
- omnibase_infra/enums/enum_policy_type.py +32 -0
- omnibase_infra/enums/enum_registration_state.py +261 -0
- omnibase_infra/enums/enum_registration_status.py +33 -0
- omnibase_infra/enums/enum_registry_response_status.py +28 -0
- omnibase_infra/enums/enum_response_status.py +26 -0
- omnibase_infra/enums/enum_retry_error_category.py +98 -0
- omnibase_infra/enums/enum_security_rule_id.py +103 -0
- omnibase_infra/enums/enum_selection_strategy.py +91 -0
- omnibase_infra/enums/enum_topic_standard.py +42 -0
- omnibase_infra/enums/enum_validation_severity.py +78 -0
- omnibase_infra/errors/__init__.py +156 -0
- omnibase_infra/errors/error_architecture_violation.py +152 -0
- omnibase_infra/errors/error_chain_propagation.py +188 -0
- omnibase_infra/errors/error_compute_registry.py +92 -0
- omnibase_infra/errors/error_consul.py +132 -0
- omnibase_infra/errors/error_container_wiring.py +243 -0
- omnibase_infra/errors/error_event_bus_registry.py +102 -0
- omnibase_infra/errors/error_infra.py +608 -0
- omnibase_infra/errors/error_message_type_registry.py +101 -0
- omnibase_infra/errors/error_policy_registry.py +112 -0
- omnibase_infra/errors/error_vault.py +123 -0
- omnibase_infra/event_bus/__init__.py +72 -0
- omnibase_infra/event_bus/configs/kafka_event_bus_config.yaml +86 -0
- omnibase_infra/event_bus/event_bus_inmemory.py +743 -0
- omnibase_infra/event_bus/event_bus_kafka.py +1658 -0
- omnibase_infra/event_bus/mixin_kafka_broadcast.py +184 -0
- omnibase_infra/event_bus/mixin_kafka_dlq.py +765 -0
- omnibase_infra/event_bus/models/__init__.py +29 -0
- omnibase_infra/event_bus/models/config/__init__.py +20 -0
- omnibase_infra/event_bus/models/config/model_kafka_event_bus_config.py +725 -0
- omnibase_infra/event_bus/models/model_dlq_event.py +206 -0
- omnibase_infra/event_bus/models/model_dlq_metrics.py +304 -0
- omnibase_infra/event_bus/models/model_event_headers.py +115 -0
- omnibase_infra/event_bus/models/model_event_message.py +60 -0
- omnibase_infra/event_bus/topic_constants.py +376 -0
- omnibase_infra/handlers/__init__.py +75 -0
- omnibase_infra/handlers/filesystem/__init__.py +48 -0
- omnibase_infra/handlers/filesystem/enum_file_system_operation.py +35 -0
- omnibase_infra/handlers/filesystem/model_file_system_request.py +298 -0
- omnibase_infra/handlers/filesystem/model_file_system_result.py +166 -0
- omnibase_infra/handlers/handler_consul.py +787 -0
- omnibase_infra/handlers/handler_db.py +1039 -0
- omnibase_infra/handlers/handler_filesystem.py +1478 -0
- omnibase_infra/handlers/handler_graph.py +1154 -0
- omnibase_infra/handlers/handler_http.py +920 -0
- omnibase_infra/handlers/handler_manifest_persistence.contract.yaml +184 -0
- omnibase_infra/handlers/handler_manifest_persistence.py +1539 -0
- omnibase_infra/handlers/handler_mcp.py +748 -0
- omnibase_infra/handlers/handler_qdrant.py +1076 -0
- omnibase_infra/handlers/handler_vault.py +422 -0
- omnibase_infra/handlers/mcp/__init__.py +19 -0
- omnibase_infra/handlers/mcp/adapter_onex_to_mcp.py +446 -0
- omnibase_infra/handlers/mcp/protocols.py +178 -0
- omnibase_infra/handlers/mcp/transport_streamable_http.py +352 -0
- omnibase_infra/handlers/mixins/__init__.py +42 -0
- omnibase_infra/handlers/mixins/mixin_consul_initialization.py +349 -0
- omnibase_infra/handlers/mixins/mixin_consul_kv.py +337 -0
- omnibase_infra/handlers/mixins/mixin_consul_service.py +277 -0
- omnibase_infra/handlers/mixins/mixin_vault_initialization.py +338 -0
- omnibase_infra/handlers/mixins/mixin_vault_retry.py +412 -0
- omnibase_infra/handlers/mixins/mixin_vault_secrets.py +450 -0
- omnibase_infra/handlers/mixins/mixin_vault_token.py +365 -0
- omnibase_infra/handlers/models/__init__.py +286 -0
- omnibase_infra/handlers/models/consul/__init__.py +81 -0
- omnibase_infra/handlers/models/consul/enum_consul_operation_type.py +57 -0
- omnibase_infra/handlers/models/consul/model_consul_deregister_payload.py +51 -0
- omnibase_infra/handlers/models/consul/model_consul_handler_config.py +153 -0
- omnibase_infra/handlers/models/consul/model_consul_handler_payload.py +89 -0
- omnibase_infra/handlers/models/consul/model_consul_kv_get_found_payload.py +55 -0
- omnibase_infra/handlers/models/consul/model_consul_kv_get_not_found_payload.py +49 -0
- omnibase_infra/handlers/models/consul/model_consul_kv_get_recurse_payload.py +50 -0
- omnibase_infra/handlers/models/consul/model_consul_kv_item.py +33 -0
- omnibase_infra/handlers/models/consul/model_consul_kv_put_payload.py +41 -0
- omnibase_infra/handlers/models/consul/model_consul_register_payload.py +53 -0
- omnibase_infra/handlers/models/consul/model_consul_retry_config.py +66 -0
- omnibase_infra/handlers/models/consul/model_payload_consul.py +66 -0
- omnibase_infra/handlers/models/consul/registry_payload_consul.py +214 -0
- omnibase_infra/handlers/models/graph/__init__.py +35 -0
- omnibase_infra/handlers/models/graph/enum_graph_operation_type.py +20 -0
- omnibase_infra/handlers/models/graph/model_graph_execute_payload.py +38 -0
- omnibase_infra/handlers/models/graph/model_graph_handler_config.py +54 -0
- omnibase_infra/handlers/models/graph/model_graph_handler_payload.py +44 -0
- omnibase_infra/handlers/models/graph/model_graph_query_payload.py +40 -0
- omnibase_infra/handlers/models/graph/model_graph_record.py +22 -0
- omnibase_infra/handlers/models/http/__init__.py +50 -0
- omnibase_infra/handlers/models/http/enum_http_operation_type.py +29 -0
- omnibase_infra/handlers/models/http/model_http_body_content.py +45 -0
- omnibase_infra/handlers/models/http/model_http_get_payload.py +88 -0
- omnibase_infra/handlers/models/http/model_http_handler_payload.py +90 -0
- omnibase_infra/handlers/models/http/model_http_post_payload.py +88 -0
- omnibase_infra/handlers/models/http/model_payload_http.py +66 -0
- omnibase_infra/handlers/models/http/registry_payload_http.py +212 -0
- omnibase_infra/handlers/models/mcp/__init__.py +23 -0
- omnibase_infra/handlers/models/mcp/enum_mcp_operation_type.py +24 -0
- omnibase_infra/handlers/models/mcp/model_mcp_handler_config.py +40 -0
- omnibase_infra/handlers/models/mcp/model_mcp_tool_call.py +32 -0
- omnibase_infra/handlers/models/mcp/model_mcp_tool_result.py +45 -0
- omnibase_infra/handlers/models/model_consul_handler_response.py +96 -0
- omnibase_infra/handlers/models/model_db_describe_response.py +83 -0
- omnibase_infra/handlers/models/model_db_query_payload.py +95 -0
- omnibase_infra/handlers/models/model_db_query_response.py +60 -0
- omnibase_infra/handlers/models/model_filesystem_config.py +98 -0
- omnibase_infra/handlers/models/model_filesystem_delete_payload.py +54 -0
- omnibase_infra/handlers/models/model_filesystem_delete_result.py +77 -0
- omnibase_infra/handlers/models/model_filesystem_directory_entry.py +75 -0
- omnibase_infra/handlers/models/model_filesystem_ensure_directory_payload.py +54 -0
- omnibase_infra/handlers/models/model_filesystem_ensure_directory_result.py +60 -0
- omnibase_infra/handlers/models/model_filesystem_list_directory_payload.py +60 -0
- omnibase_infra/handlers/models/model_filesystem_list_directory_result.py +68 -0
- omnibase_infra/handlers/models/model_filesystem_read_payload.py +62 -0
- omnibase_infra/handlers/models/model_filesystem_read_result.py +61 -0
- omnibase_infra/handlers/models/model_filesystem_write_payload.py +70 -0
- omnibase_infra/handlers/models/model_filesystem_write_result.py +55 -0
- omnibase_infra/handlers/models/model_graph_handler_response.py +98 -0
- omnibase_infra/handlers/models/model_handler_response.py +103 -0
- omnibase_infra/handlers/models/model_http_handler_response.py +101 -0
- omnibase_infra/handlers/models/model_manifest_metadata.py +75 -0
- omnibase_infra/handlers/models/model_manifest_persistence_config.py +62 -0
- omnibase_infra/handlers/models/model_manifest_query_payload.py +90 -0
- omnibase_infra/handlers/models/model_manifest_query_result.py +97 -0
- omnibase_infra/handlers/models/model_manifest_retrieve_payload.py +44 -0
- omnibase_infra/handlers/models/model_manifest_retrieve_result.py +98 -0
- omnibase_infra/handlers/models/model_manifest_store_payload.py +47 -0
- omnibase_infra/handlers/models/model_manifest_store_result.py +67 -0
- omnibase_infra/handlers/models/model_operation_context.py +187 -0
- omnibase_infra/handlers/models/model_qdrant_handler_response.py +98 -0
- omnibase_infra/handlers/models/model_retry_state.py +162 -0
- omnibase_infra/handlers/models/model_vault_handler_response.py +98 -0
- omnibase_infra/handlers/models/qdrant/__init__.py +44 -0
- omnibase_infra/handlers/models/qdrant/enum_qdrant_operation_type.py +26 -0
- omnibase_infra/handlers/models/qdrant/model_qdrant_collection_payload.py +42 -0
- omnibase_infra/handlers/models/qdrant/model_qdrant_delete_payload.py +36 -0
- omnibase_infra/handlers/models/qdrant/model_qdrant_handler_config.py +42 -0
- omnibase_infra/handlers/models/qdrant/model_qdrant_handler_payload.py +54 -0
- omnibase_infra/handlers/models/qdrant/model_qdrant_search_payload.py +42 -0
- omnibase_infra/handlers/models/qdrant/model_qdrant_search_result.py +30 -0
- omnibase_infra/handlers/models/qdrant/model_qdrant_upsert_payload.py +36 -0
- omnibase_infra/handlers/models/vault/__init__.py +69 -0
- omnibase_infra/handlers/models/vault/enum_vault_operation_type.py +35 -0
- omnibase_infra/handlers/models/vault/model_payload_vault.py +66 -0
- omnibase_infra/handlers/models/vault/model_vault_delete_payload.py +57 -0
- omnibase_infra/handlers/models/vault/model_vault_handler_config.py +148 -0
- omnibase_infra/handlers/models/vault/model_vault_handler_payload.py +101 -0
- omnibase_infra/handlers/models/vault/model_vault_list_payload.py +58 -0
- omnibase_infra/handlers/models/vault/model_vault_renew_token_payload.py +67 -0
- omnibase_infra/handlers/models/vault/model_vault_retry_config.py +66 -0
- omnibase_infra/handlers/models/vault/model_vault_secret_payload.py +106 -0
- omnibase_infra/handlers/models/vault/model_vault_write_payload.py +66 -0
- omnibase_infra/handlers/models/vault/registry_payload_vault.py +213 -0
- omnibase_infra/handlers/registration_storage/__init__.py +43 -0
- omnibase_infra/handlers/registration_storage/handler_registration_storage_mock.py +392 -0
- omnibase_infra/handlers/registration_storage/handler_registration_storage_postgres.py +915 -0
- omnibase_infra/handlers/registration_storage/models/__init__.py +23 -0
- omnibase_infra/handlers/registration_storage/models/model_delete_registration_request.py +58 -0
- omnibase_infra/handlers/registration_storage/models/model_update_registration_request.py +73 -0
- omnibase_infra/handlers/registration_storage/protocol_registration_persistence.py +191 -0
- omnibase_infra/handlers/service_discovery/__init__.py +43 -0
- omnibase_infra/handlers/service_discovery/handler_service_discovery_consul.py +747 -0
- omnibase_infra/handlers/service_discovery/handler_service_discovery_mock.py +258 -0
- omnibase_infra/handlers/service_discovery/models/__init__.py +22 -0
- omnibase_infra/handlers/service_discovery/models/model_discovery_result.py +64 -0
- omnibase_infra/handlers/service_discovery/models/model_registration_result.py +138 -0
- omnibase_infra/handlers/service_discovery/models/model_service_info.py +99 -0
- omnibase_infra/handlers/service_discovery/protocol_discovery_operations.py +170 -0
- omnibase_infra/idempotency/__init__.py +94 -0
- omnibase_infra/idempotency/models/__init__.py +43 -0
- omnibase_infra/idempotency/models/model_idempotency_check_result.py +85 -0
- omnibase_infra/idempotency/models/model_idempotency_guard_config.py +130 -0
- omnibase_infra/idempotency/models/model_idempotency_record.py +86 -0
- omnibase_infra/idempotency/models/model_idempotency_store_health_check_result.py +81 -0
- omnibase_infra/idempotency/models/model_idempotency_store_metrics.py +140 -0
- omnibase_infra/idempotency/models/model_postgres_idempotency_store_config.py +299 -0
- omnibase_infra/idempotency/protocol_idempotency_store.py +184 -0
- omnibase_infra/idempotency/store_inmemory.py +265 -0
- omnibase_infra/idempotency/store_postgres.py +923 -0
- omnibase_infra/infrastructure/__init__.py +0 -0
- omnibase_infra/mixins/__init__.py +71 -0
- omnibase_infra/mixins/mixin_async_circuit_breaker.py +655 -0
- omnibase_infra/mixins/mixin_dict_like_accessors.py +146 -0
- omnibase_infra/mixins/mixin_envelope_extraction.py +119 -0
- omnibase_infra/mixins/mixin_node_introspection.py +2465 -0
- omnibase_infra/mixins/mixin_retry_execution.py +386 -0
- omnibase_infra/mixins/protocol_circuit_breaker_aware.py +133 -0
- omnibase_infra/models/__init__.py +136 -0
- omnibase_infra/models/corpus/__init__.py +17 -0
- omnibase_infra/models/corpus/model_capture_config.py +133 -0
- omnibase_infra/models/corpus/model_capture_result.py +86 -0
- omnibase_infra/models/discovery/__init__.py +42 -0
- omnibase_infra/models/discovery/model_dependency_spec.py +319 -0
- omnibase_infra/models/discovery/model_discovered_capabilities.py +50 -0
- omnibase_infra/models/discovery/model_introspection_config.py +311 -0
- omnibase_infra/models/discovery/model_introspection_performance_metrics.py +169 -0
- omnibase_infra/models/discovery/model_introspection_task_config.py +116 -0
- omnibase_infra/models/dispatch/__init__.py +147 -0
- omnibase_infra/models/dispatch/model_dispatch_context.py +439 -0
- omnibase_infra/models/dispatch/model_dispatch_error.py +336 -0
- omnibase_infra/models/dispatch/model_dispatch_log_context.py +400 -0
- omnibase_infra/models/dispatch/model_dispatch_metadata.py +228 -0
- omnibase_infra/models/dispatch/model_dispatch_metrics.py +496 -0
- omnibase_infra/models/dispatch/model_dispatch_outcome.py +317 -0
- omnibase_infra/models/dispatch/model_dispatch_outputs.py +231 -0
- omnibase_infra/models/dispatch/model_dispatch_result.py +436 -0
- omnibase_infra/models/dispatch/model_dispatch_route.py +279 -0
- omnibase_infra/models/dispatch/model_dispatcher_metrics.py +275 -0
- omnibase_infra/models/dispatch/model_dispatcher_registration.py +352 -0
- omnibase_infra/models/dispatch/model_parsed_topic.py +135 -0
- omnibase_infra/models/dispatch/model_topic_parser.py +725 -0
- omnibase_infra/models/dispatch/model_tracing_context.py +285 -0
- omnibase_infra/models/errors/__init__.py +45 -0
- omnibase_infra/models/errors/model_handler_validation_error.py +594 -0
- omnibase_infra/models/errors/model_infra_error_context.py +99 -0
- omnibase_infra/models/errors/model_message_type_registry_error_context.py +71 -0
- omnibase_infra/models/errors/model_timeout_error_context.py +110 -0
- omnibase_infra/models/handlers/__init__.py +37 -0
- omnibase_infra/models/handlers/model_contract_discovery_result.py +80 -0
- omnibase_infra/models/handlers/model_handler_descriptor.py +185 -0
- omnibase_infra/models/handlers/model_handler_identifier.py +215 -0
- omnibase_infra/models/health/__init__.py +9 -0
- omnibase_infra/models/health/model_health_check_result.py +40 -0
- omnibase_infra/models/lifecycle/__init__.py +39 -0
- omnibase_infra/models/logging/__init__.py +51 -0
- omnibase_infra/models/logging/model_log_context.py +756 -0
- omnibase_infra/models/model_retry_error_classification.py +78 -0
- omnibase_infra/models/projection/__init__.py +43 -0
- omnibase_infra/models/projection/model_capability_fields.py +112 -0
- omnibase_infra/models/projection/model_registration_projection.py +434 -0
- omnibase_infra/models/projection/model_registration_snapshot.py +322 -0
- omnibase_infra/models/projection/model_sequence_info.py +182 -0
- omnibase_infra/models/projection/model_snapshot_topic_config.py +590 -0
- omnibase_infra/models/projectors/__init__.py +41 -0
- omnibase_infra/models/projectors/model_projector_column.py +289 -0
- omnibase_infra/models/projectors/model_projector_discovery_result.py +65 -0
- omnibase_infra/models/projectors/model_projector_index.py +270 -0
- omnibase_infra/models/projectors/model_projector_schema.py +415 -0
- omnibase_infra/models/projectors/model_projector_validation_error.py +63 -0
- omnibase_infra/models/projectors/util_sql_identifiers.py +115 -0
- omnibase_infra/models/registration/__init__.py +59 -0
- omnibase_infra/models/registration/commands/__init__.py +15 -0
- omnibase_infra/models/registration/commands/model_node_registration_acked.py +108 -0
- omnibase_infra/models/registration/events/__init__.py +56 -0
- omnibase_infra/models/registration/events/model_node_became_active.py +103 -0
- omnibase_infra/models/registration/events/model_node_liveness_expired.py +103 -0
- omnibase_infra/models/registration/events/model_node_registration_accepted.py +98 -0
- omnibase_infra/models/registration/events/model_node_registration_ack_received.py +98 -0
- omnibase_infra/models/registration/events/model_node_registration_ack_timed_out.py +112 -0
- omnibase_infra/models/registration/events/model_node_registration_initiated.py +107 -0
- omnibase_infra/models/registration/events/model_node_registration_rejected.py +104 -0
- omnibase_infra/models/registration/model_introspection_metrics.py +253 -0
- omnibase_infra/models/registration/model_node_capabilities.py +179 -0
- omnibase_infra/models/registration/model_node_heartbeat_event.py +126 -0
- omnibase_infra/models/registration/model_node_introspection_event.py +175 -0
- omnibase_infra/models/registration/model_node_metadata.py +79 -0
- omnibase_infra/models/registration/model_node_registration.py +162 -0
- omnibase_infra/models/registration/model_node_registration_record.py +162 -0
- omnibase_infra/models/registry/__init__.py +29 -0
- omnibase_infra/models/registry/model_domain_constraint.py +202 -0
- omnibase_infra/models/registry/model_message_type_entry.py +271 -0
- omnibase_infra/models/resilience/__init__.py +9 -0
- omnibase_infra/models/resilience/model_circuit_breaker_config.py +227 -0
- omnibase_infra/models/routing/__init__.py +25 -0
- omnibase_infra/models/routing/model_routing_entry.py +52 -0
- omnibase_infra/models/routing/model_routing_subcontract.py +70 -0
- omnibase_infra/models/runtime/__init__.py +40 -0
- omnibase_infra/models/runtime/model_contract_security_config.py +41 -0
- omnibase_infra/models/runtime/model_discovery_error.py +81 -0
- omnibase_infra/models/runtime/model_discovery_result.py +162 -0
- omnibase_infra/models/runtime/model_discovery_warning.py +74 -0
- omnibase_infra/models/runtime/model_failed_plugin_load.py +63 -0
- omnibase_infra/models/runtime/model_handler_contract.py +280 -0
- omnibase_infra/models/runtime/model_loaded_handler.py +120 -0
- omnibase_infra/models/runtime/model_plugin_load_context.py +93 -0
- omnibase_infra/models/runtime/model_plugin_load_summary.py +124 -0
- omnibase_infra/models/security/__init__.py +50 -0
- omnibase_infra/models/security/classification_levels.py +99 -0
- omnibase_infra/models/security/model_environment_policy.py +145 -0
- omnibase_infra/models/security/model_handler_security_policy.py +107 -0
- omnibase_infra/models/security/model_security_error.py +81 -0
- omnibase_infra/models/security/model_security_validation_result.py +328 -0
- omnibase_infra/models/security/model_security_warning.py +67 -0
- omnibase_infra/models/snapshot/__init__.py +27 -0
- omnibase_infra/models/snapshot/model_field_change.py +65 -0
- omnibase_infra/models/snapshot/model_snapshot.py +270 -0
- omnibase_infra/models/snapshot/model_snapshot_diff.py +203 -0
- omnibase_infra/models/snapshot/model_subject_ref.py +81 -0
- omnibase_infra/models/types/__init__.py +71 -0
- omnibase_infra/models/validation/__init__.py +89 -0
- omnibase_infra/models/validation/model_any_type_validation_result.py +118 -0
- omnibase_infra/models/validation/model_any_type_violation.py +141 -0
- omnibase_infra/models/validation/model_category_match_result.py +345 -0
- omnibase_infra/models/validation/model_chain_violation.py +166 -0
- omnibase_infra/models/validation/model_coverage_metrics.py +316 -0
- omnibase_infra/models/validation/model_execution_shape_rule.py +159 -0
- omnibase_infra/models/validation/model_execution_shape_validation.py +208 -0
- omnibase_infra/models/validation/model_execution_shape_validation_result.py +294 -0
- omnibase_infra/models/validation/model_execution_shape_violation.py +122 -0
- omnibase_infra/models/validation/model_localhandler_validation_result.py +139 -0
- omnibase_infra/models/validation/model_localhandler_violation.py +100 -0
- omnibase_infra/models/validation/model_output_validation_params.py +74 -0
- omnibase_infra/models/validation/model_validate_and_raise_params.py +84 -0
- omnibase_infra/models/validation/model_validation_error_params.py +84 -0
- omnibase_infra/models/validation/model_validation_outcome.py +287 -0
- omnibase_infra/nodes/__init__.py +48 -0
- omnibase_infra/nodes/architecture_validator/__init__.py +79 -0
- omnibase_infra/nodes/architecture_validator/contract.yaml +252 -0
- omnibase_infra/nodes/architecture_validator/contract_architecture_validator.yaml +208 -0
- omnibase_infra/nodes/architecture_validator/mixins/__init__.py +16 -0
- omnibase_infra/nodes/architecture_validator/mixins/mixin_file_path_rule.py +92 -0
- omnibase_infra/nodes/architecture_validator/models/__init__.py +36 -0
- omnibase_infra/nodes/architecture_validator/models/model_architecture_validation_request.py +56 -0
- omnibase_infra/nodes/architecture_validator/models/model_architecture_validation_result.py +311 -0
- omnibase_infra/nodes/architecture_validator/models/model_architecture_violation.py +163 -0
- omnibase_infra/nodes/architecture_validator/models/model_rule_check_result.py +265 -0
- omnibase_infra/nodes/architecture_validator/models/model_validation_request.py +105 -0
- omnibase_infra/nodes/architecture_validator/models/model_validation_result.py +314 -0
- omnibase_infra/nodes/architecture_validator/node.py +262 -0
- omnibase_infra/nodes/architecture_validator/node_architecture_validator.py +383 -0
- omnibase_infra/nodes/architecture_validator/protocols/__init__.py +9 -0
- omnibase_infra/nodes/architecture_validator/protocols/protocol_architecture_rule.py +225 -0
- omnibase_infra/nodes/architecture_validator/registry/__init__.py +28 -0
- omnibase_infra/nodes/architecture_validator/registry/registry_infra_architecture_validator.py +99 -0
- omnibase_infra/nodes/architecture_validator/validators/__init__.py +104 -0
- omnibase_infra/nodes/architecture_validator/validators/validator_no_direct_dispatch.py +422 -0
- omnibase_infra/nodes/architecture_validator/validators/validator_no_handler_publishing.py +481 -0
- omnibase_infra/nodes/architecture_validator/validators/validator_no_orchestrator_fsm.py +491 -0
- omnibase_infra/nodes/effects/README.md +358 -0
- omnibase_infra/nodes/effects/__init__.py +26 -0
- omnibase_infra/nodes/effects/contract.yaml +172 -0
- omnibase_infra/nodes/effects/models/__init__.py +32 -0
- omnibase_infra/nodes/effects/models/model_backend_result.py +190 -0
- omnibase_infra/nodes/effects/models/model_effect_idempotency_config.py +92 -0
- omnibase_infra/nodes/effects/models/model_registry_request.py +132 -0
- omnibase_infra/nodes/effects/models/model_registry_response.py +263 -0
- omnibase_infra/nodes/effects/protocol_consul_client.py +89 -0
- omnibase_infra/nodes/effects/protocol_effect_idempotency_store.py +143 -0
- omnibase_infra/nodes/effects/protocol_postgres_adapter.py +96 -0
- omnibase_infra/nodes/effects/registry_effect.py +525 -0
- omnibase_infra/nodes/effects/store_effect_idempotency_inmemory.py +425 -0
- omnibase_infra/nodes/node_registration_orchestrator/README.md +542 -0
- omnibase_infra/nodes/node_registration_orchestrator/__init__.py +120 -0
- omnibase_infra/nodes/node_registration_orchestrator/contract.yaml +475 -0
- omnibase_infra/nodes/node_registration_orchestrator/dispatchers/__init__.py +53 -0
- omnibase_infra/nodes/node_registration_orchestrator/dispatchers/dispatcher_node_introspected.py +376 -0
- omnibase_infra/nodes/node_registration_orchestrator/dispatchers/dispatcher_node_registration_acked.py +376 -0
- omnibase_infra/nodes/node_registration_orchestrator/dispatchers/dispatcher_runtime_tick.py +373 -0
- omnibase_infra/nodes/node_registration_orchestrator/handlers/__init__.py +62 -0
- omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_node_heartbeat.py +376 -0
- omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_node_introspected.py +609 -0
- omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_node_registration_acked.py +458 -0
- omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_runtime_tick.py +364 -0
- omnibase_infra/nodes/node_registration_orchestrator/introspection_event_router.py +544 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/__init__.py +75 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_consul_intent_payload.py +194 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_consul_registration_intent.py +67 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_intent_execution_result.py +50 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_node_liveness_expired.py +107 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_orchestrator_config.py +67 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_orchestrator_input.py +41 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_orchestrator_output.py +166 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_postgres_intent_payload.py +235 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_postgres_upsert_intent.py +68 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_reducer_execution_result.py +384 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_reducer_state.py +60 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_registration_intent.py +177 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_registry_intent.py +247 -0
- omnibase_infra/nodes/node_registration_orchestrator/node.py +195 -0
- omnibase_infra/nodes/node_registration_orchestrator/plugin.py +909 -0
- omnibase_infra/nodes/node_registration_orchestrator/protocols.py +439 -0
- omnibase_infra/nodes/node_registration_orchestrator/registry/__init__.py +41 -0
- omnibase_infra/nodes/node_registration_orchestrator/registry/registry_infra_node_registration_orchestrator.py +525 -0
- omnibase_infra/nodes/node_registration_orchestrator/timeout_coordinator.py +392 -0
- omnibase_infra/nodes/node_registration_orchestrator/wiring.py +742 -0
- omnibase_infra/nodes/node_registration_reducer/__init__.py +15 -0
- omnibase_infra/nodes/node_registration_reducer/contract.yaml +301 -0
- omnibase_infra/nodes/node_registration_reducer/models/__init__.py +38 -0
- omnibase_infra/nodes/node_registration_reducer/models/model_validation_result.py +113 -0
- omnibase_infra/nodes/node_registration_reducer/node.py +139 -0
- omnibase_infra/nodes/node_registration_reducer/registry/__init__.py +9 -0
- omnibase_infra/nodes/node_registration_reducer/registry/registry_infra_node_registration_reducer.py +79 -0
- omnibase_infra/nodes/node_registration_storage_effect/__init__.py +41 -0
- omnibase_infra/nodes/node_registration_storage_effect/contract.yaml +225 -0
- omnibase_infra/nodes/node_registration_storage_effect/models/__init__.py +44 -0
- omnibase_infra/nodes/node_registration_storage_effect/models/model_delete_result.py +132 -0
- omnibase_infra/nodes/node_registration_storage_effect/models/model_registration_record.py +199 -0
- omnibase_infra/nodes/node_registration_storage_effect/models/model_registration_update.py +155 -0
- omnibase_infra/nodes/node_registration_storage_effect/models/model_storage_health_check_details.py +123 -0
- omnibase_infra/nodes/node_registration_storage_effect/models/model_storage_health_check_result.py +117 -0
- omnibase_infra/nodes/node_registration_storage_effect/models/model_storage_query.py +100 -0
- omnibase_infra/nodes/node_registration_storage_effect/models/model_storage_result.py +136 -0
- omnibase_infra/nodes/node_registration_storage_effect/models/model_upsert_result.py +127 -0
- omnibase_infra/nodes/node_registration_storage_effect/node.py +109 -0
- omnibase_infra/nodes/node_registration_storage_effect/protocols/__init__.py +22 -0
- omnibase_infra/nodes/node_registration_storage_effect/protocols/protocol_registration_persistence.py +333 -0
- omnibase_infra/nodes/node_registration_storage_effect/registry/__init__.py +23 -0
- omnibase_infra/nodes/node_registration_storage_effect/registry/registry_infra_registration_storage.py +194 -0
- omnibase_infra/nodes/node_registry_effect/__init__.py +85 -0
- omnibase_infra/nodes/node_registry_effect/contract.yaml +682 -0
- omnibase_infra/nodes/node_registry_effect/handlers/__init__.py +70 -0
- omnibase_infra/nodes/node_registry_effect/handlers/handler_consul_deregister.py +211 -0
- omnibase_infra/nodes/node_registry_effect/handlers/handler_consul_register.py +212 -0
- omnibase_infra/nodes/node_registry_effect/handlers/handler_partial_retry.py +416 -0
- omnibase_infra/nodes/node_registry_effect/handlers/handler_postgres_deactivate.py +215 -0
- omnibase_infra/nodes/node_registry_effect/handlers/handler_postgres_upsert.py +208 -0
- omnibase_infra/nodes/node_registry_effect/models/__init__.py +43 -0
- omnibase_infra/nodes/node_registry_effect/models/model_partial_retry_request.py +92 -0
- omnibase_infra/nodes/node_registry_effect/node.py +165 -0
- omnibase_infra/nodes/node_registry_effect/registry/__init__.py +27 -0
- omnibase_infra/nodes/node_registry_effect/registry/registry_infra_registry_effect.py +196 -0
- omnibase_infra/nodes/node_service_discovery_effect/__init__.py +111 -0
- omnibase_infra/nodes/node_service_discovery_effect/contract.yaml +246 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/__init__.py +67 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/enum_health_status.py +72 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/enum_service_discovery_operation.py +58 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/model_discovery_query.py +99 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/model_discovery_result.py +98 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/model_health_check_config.py +121 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/model_query_metadata.py +63 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/model_registration_result.py +130 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/model_service_discovery_health_check_details.py +111 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/model_service_discovery_health_check_result.py +119 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/model_service_info.py +106 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/model_service_registration.py +121 -0
- omnibase_infra/nodes/node_service_discovery_effect/node.py +111 -0
- omnibase_infra/nodes/node_service_discovery_effect/protocols/__init__.py +14 -0
- omnibase_infra/nodes/node_service_discovery_effect/protocols/protocol_discovery_operations.py +279 -0
- omnibase_infra/nodes/node_service_discovery_effect/registry/__init__.py +13 -0
- omnibase_infra/nodes/node_service_discovery_effect/registry/registry_infra_service_discovery.py +214 -0
- omnibase_infra/nodes/reducers/__init__.py +30 -0
- omnibase_infra/nodes/reducers/models/__init__.py +32 -0
- omnibase_infra/nodes/reducers/models/model_payload_consul_register.py +76 -0
- omnibase_infra/nodes/reducers/models/model_payload_postgres_upsert_registration.py +60 -0
- omnibase_infra/nodes/reducers/models/model_registration_confirmation.py +166 -0
- omnibase_infra/nodes/reducers/models/model_registration_state.py +433 -0
- omnibase_infra/nodes/reducers/registration_reducer.py +1137 -0
- omnibase_infra/observability/__init__.py +143 -0
- omnibase_infra/observability/constants_metrics.py +91 -0
- omnibase_infra/observability/factory_observability_sink.py +525 -0
- omnibase_infra/observability/handlers/__init__.py +118 -0
- omnibase_infra/observability/handlers/handler_logging_structured.py +967 -0
- omnibase_infra/observability/handlers/handler_metrics_prometheus.py +1120 -0
- omnibase_infra/observability/handlers/model_logging_handler_config.py +71 -0
- omnibase_infra/observability/handlers/model_logging_handler_response.py +77 -0
- omnibase_infra/observability/handlers/model_metrics_handler_config.py +172 -0
- omnibase_infra/observability/handlers/model_metrics_handler_payload.py +135 -0
- omnibase_infra/observability/handlers/model_metrics_handler_response.py +101 -0
- omnibase_infra/observability/hooks/__init__.py +74 -0
- omnibase_infra/observability/hooks/hook_observability.py +1223 -0
- omnibase_infra/observability/models/__init__.py +30 -0
- omnibase_infra/observability/models/enum_required_log_context_key.py +77 -0
- omnibase_infra/observability/models/model_buffered_log_entry.py +117 -0
- omnibase_infra/observability/models/model_logging_sink_config.py +73 -0
- omnibase_infra/observability/models/model_metrics_sink_config.py +156 -0
- omnibase_infra/observability/sinks/__init__.py +69 -0
- omnibase_infra/observability/sinks/sink_logging_structured.py +809 -0
- omnibase_infra/observability/sinks/sink_metrics_prometheus.py +710 -0
- omnibase_infra/plugins/__init__.py +27 -0
- omnibase_infra/plugins/examples/__init__.py +28 -0
- omnibase_infra/plugins/examples/plugin_json_normalizer.py +271 -0
- omnibase_infra/plugins/examples/plugin_json_normalizer_error_handling.py +210 -0
- omnibase_infra/plugins/models/__init__.py +21 -0
- omnibase_infra/plugins/models/model_plugin_context.py +76 -0
- omnibase_infra/plugins/models/model_plugin_input_data.py +58 -0
- omnibase_infra/plugins/models/model_plugin_output_data.py +62 -0
- omnibase_infra/plugins/plugin_compute_base.py +435 -0
- omnibase_infra/projectors/__init__.py +30 -0
- omnibase_infra/projectors/contracts/__init__.py +63 -0
- omnibase_infra/projectors/contracts/registration_projector.yaml +370 -0
- omnibase_infra/projectors/projection_reader_registration.py +1559 -0
- omnibase_infra/projectors/snapshot_publisher_registration.py +1329 -0
- omnibase_infra/protocols/__init__.py +99 -0
- omnibase_infra/protocols/protocol_capability_projection.py +253 -0
- omnibase_infra/protocols/protocol_capability_query.py +251 -0
- omnibase_infra/protocols/protocol_event_bus_like.py +127 -0
- omnibase_infra/protocols/protocol_event_projector.py +96 -0
- omnibase_infra/protocols/protocol_idempotency_store.py +142 -0
- omnibase_infra/protocols/protocol_message_dispatcher.py +247 -0
- omnibase_infra/protocols/protocol_message_type_registry.py +306 -0
- omnibase_infra/protocols/protocol_plugin_compute.py +368 -0
- omnibase_infra/protocols/protocol_projector_schema_validator.py +82 -0
- omnibase_infra/protocols/protocol_registry_metrics.py +215 -0
- omnibase_infra/protocols/protocol_snapshot_publisher.py +396 -0
- omnibase_infra/protocols/protocol_snapshot_store.py +567 -0
- omnibase_infra/runtime/__init__.py +296 -0
- omnibase_infra/runtime/binding_config_resolver.py +2706 -0
- omnibase_infra/runtime/chain_aware_dispatch.py +467 -0
- omnibase_infra/runtime/contract_handler_discovery.py +582 -0
- omnibase_infra/runtime/contract_loaders/__init__.py +42 -0
- omnibase_infra/runtime/contract_loaders/handler_routing_loader.py +464 -0
- omnibase_infra/runtime/dispatch_context_enforcer.py +427 -0
- omnibase_infra/runtime/enums/__init__.py +18 -0
- omnibase_infra/runtime/enums/enum_config_ref_scheme.py +33 -0
- omnibase_infra/runtime/enums/enum_scheduler_status.py +170 -0
- omnibase_infra/runtime/envelope_validator.py +179 -0
- omnibase_infra/runtime/handler_contract_source.py +669 -0
- omnibase_infra/runtime/handler_plugin_loader.py +2029 -0
- omnibase_infra/runtime/handler_registry.py +321 -0
- omnibase_infra/runtime/invocation_security_enforcer.py +427 -0
- omnibase_infra/runtime/kernel.py +40 -0
- omnibase_infra/runtime/mixin_policy_validation.py +522 -0
- omnibase_infra/runtime/mixin_semver_cache.py +378 -0
- omnibase_infra/runtime/mixins/__init__.py +17 -0
- omnibase_infra/runtime/mixins/mixin_projector_sql_operations.py +757 -0
- omnibase_infra/runtime/models/__init__.py +192 -0
- omnibase_infra/runtime/models/model_batch_lifecycle_result.py +217 -0
- omnibase_infra/runtime/models/model_binding_config.py +168 -0
- omnibase_infra/runtime/models/model_binding_config_cache_stats.py +135 -0
- omnibase_infra/runtime/models/model_binding_config_resolver_config.py +329 -0
- omnibase_infra/runtime/models/model_cached_secret.py +138 -0
- omnibase_infra/runtime/models/model_compute_key.py +138 -0
- omnibase_infra/runtime/models/model_compute_registration.py +97 -0
- omnibase_infra/runtime/models/model_config_cache_entry.py +61 -0
- omnibase_infra/runtime/models/model_config_ref.py +331 -0
- omnibase_infra/runtime/models/model_config_ref_parse_result.py +125 -0
- omnibase_infra/runtime/models/model_domain_plugin_config.py +92 -0
- omnibase_infra/runtime/models/model_domain_plugin_result.py +270 -0
- omnibase_infra/runtime/models/model_duplicate_response.py +54 -0
- omnibase_infra/runtime/models/model_enabled_protocols_config.py +61 -0
- omnibase_infra/runtime/models/model_event_bus_config.py +54 -0
- omnibase_infra/runtime/models/model_failed_component.py +55 -0
- omnibase_infra/runtime/models/model_health_check_response.py +168 -0
- omnibase_infra/runtime/models/model_health_check_result.py +228 -0
- omnibase_infra/runtime/models/model_lifecycle_result.py +245 -0
- omnibase_infra/runtime/models/model_logging_config.py +42 -0
- omnibase_infra/runtime/models/model_optional_correlation_id.py +167 -0
- omnibase_infra/runtime/models/model_optional_string.py +94 -0
- omnibase_infra/runtime/models/model_optional_uuid.py +110 -0
- omnibase_infra/runtime/models/model_policy_context.py +100 -0
- omnibase_infra/runtime/models/model_policy_key.py +138 -0
- omnibase_infra/runtime/models/model_policy_registration.py +139 -0
- omnibase_infra/runtime/models/model_policy_result.py +103 -0
- omnibase_infra/runtime/models/model_policy_type_filter.py +157 -0
- omnibase_infra/runtime/models/model_projector_plugin_loader_config.py +47 -0
- omnibase_infra/runtime/models/model_protocol_registration_config.py +65 -0
- omnibase_infra/runtime/models/model_retry_policy.py +105 -0
- omnibase_infra/runtime/models/model_runtime_config.py +150 -0
- omnibase_infra/runtime/models/model_runtime_scheduler_config.py +624 -0
- omnibase_infra/runtime/models/model_runtime_scheduler_metrics.py +233 -0
- omnibase_infra/runtime/models/model_runtime_tick.py +193 -0
- omnibase_infra/runtime/models/model_secret_cache_stats.py +82 -0
- omnibase_infra/runtime/models/model_secret_mapping.py +63 -0
- omnibase_infra/runtime/models/model_secret_resolver_config.py +107 -0
- omnibase_infra/runtime/models/model_secret_resolver_metrics.py +111 -0
- omnibase_infra/runtime/models/model_secret_source_info.py +72 -0
- omnibase_infra/runtime/models/model_secret_source_spec.py +66 -0
- omnibase_infra/runtime/models/model_shutdown_batch_result.py +75 -0
- omnibase_infra/runtime/models/model_shutdown_config.py +94 -0
- omnibase_infra/runtime/projector_plugin_loader.py +1462 -0
- omnibase_infra/runtime/projector_schema_manager.py +565 -0
- omnibase_infra/runtime/projector_shell.py +1102 -0
- omnibase_infra/runtime/protocol_contract_descriptor.py +92 -0
- omnibase_infra/runtime/protocol_contract_source.py +92 -0
- omnibase_infra/runtime/protocol_domain_plugin.py +474 -0
- omnibase_infra/runtime/protocol_handler_discovery.py +221 -0
- omnibase_infra/runtime/protocol_handler_plugin_loader.py +327 -0
- omnibase_infra/runtime/protocol_lifecycle_executor.py +435 -0
- omnibase_infra/runtime/protocol_policy.py +366 -0
- omnibase_infra/runtime/protocols/__init__.py +27 -0
- omnibase_infra/runtime/protocols/protocol_runtime_scheduler.py +468 -0
- omnibase_infra/runtime/registry/__init__.py +93 -0
- omnibase_infra/runtime/registry/mixin_message_type_query.py +326 -0
- omnibase_infra/runtime/registry/mixin_message_type_registration.py +354 -0
- omnibase_infra/runtime/registry/registry_event_bus_binding.py +268 -0
- omnibase_infra/runtime/registry/registry_message_type.py +542 -0
- omnibase_infra/runtime/registry/registry_protocol_binding.py +444 -0
- omnibase_infra/runtime/registry_compute.py +1143 -0
- omnibase_infra/runtime/registry_dispatcher.py +678 -0
- omnibase_infra/runtime/registry_policy.py +1502 -0
- omnibase_infra/runtime/runtime_scheduler.py +1070 -0
- omnibase_infra/runtime/secret_resolver.py +2110 -0
- omnibase_infra/runtime/security_metadata_validator.py +776 -0
- omnibase_infra/runtime/service_kernel.py +1573 -0
- omnibase_infra/runtime/service_message_dispatch_engine.py +1805 -0
- omnibase_infra/runtime/service_runtime_host_process.py +2260 -0
- omnibase_infra/runtime/util_container_wiring.py +1123 -0
- omnibase_infra/runtime/util_validation.py +314 -0
- omnibase_infra/runtime/util_version.py +98 -0
- omnibase_infra/runtime/util_wiring.py +566 -0
- omnibase_infra/schemas/schema_registration_projection.sql +320 -0
- omnibase_infra/services/__init__.py +68 -0
- omnibase_infra/services/corpus_capture.py +678 -0
- omnibase_infra/services/service_capability_query.py +945 -0
- omnibase_infra/services/service_health.py +897 -0
- omnibase_infra/services/service_node_selector.py +530 -0
- omnibase_infra/services/service_timeout_emitter.py +682 -0
- omnibase_infra/services/service_timeout_scanner.py +390 -0
- omnibase_infra/services/snapshot/__init__.py +31 -0
- omnibase_infra/services/snapshot/service_snapshot.py +647 -0
- omnibase_infra/services/snapshot/store_inmemory.py +637 -0
- omnibase_infra/services/snapshot/store_postgres.py +1279 -0
- omnibase_infra/shared/__init__.py +8 -0
- omnibase_infra/testing/__init__.py +10 -0
- omnibase_infra/testing/utils.py +23 -0
- omnibase_infra/types/__init__.py +48 -0
- omnibase_infra/types/type_cache_info.py +49 -0
- omnibase_infra/types/type_dsn.py +173 -0
- omnibase_infra/types/type_infra_aliases.py +60 -0
- omnibase_infra/types/typed_dict/__init__.py +21 -0
- omnibase_infra/types/typed_dict/typed_dict_introspection_cache.py +128 -0
- omnibase_infra/types/typed_dict/typed_dict_performance_metrics_cache.py +140 -0
- omnibase_infra/types/typed_dict_capabilities.py +64 -0
- omnibase_infra/utils/__init__.py +89 -0
- omnibase_infra/utils/correlation.py +208 -0
- omnibase_infra/utils/util_datetime.py +372 -0
- omnibase_infra/utils/util_dsn_validation.py +333 -0
- omnibase_infra/utils/util_env_parsing.py +264 -0
- omnibase_infra/utils/util_error_sanitization.py +457 -0
- omnibase_infra/utils/util_pydantic_validators.py +477 -0
- omnibase_infra/utils/util_semver.py +233 -0
- omnibase_infra/validation/__init__.py +307 -0
- omnibase_infra/validation/enums/__init__.py +11 -0
- omnibase_infra/validation/enums/enum_contract_violation_severity.py +13 -0
- omnibase_infra/validation/infra_validators.py +1486 -0
- omnibase_infra/validation/linter_contract.py +907 -0
- omnibase_infra/validation/mixin_any_type_classification.py +120 -0
- omnibase_infra/validation/mixin_any_type_exemption.py +580 -0
- omnibase_infra/validation/mixin_any_type_reporting.py +106 -0
- omnibase_infra/validation/mixin_execution_shape_violation_checks.py +596 -0
- omnibase_infra/validation/mixin_node_archetype_detection.py +254 -0
- omnibase_infra/validation/models/__init__.py +15 -0
- omnibase_infra/validation/models/model_contract_lint_result.py +101 -0
- omnibase_infra/validation/models/model_contract_violation.py +41 -0
- omnibase_infra/validation/service_validation_aggregator.py +395 -0
- omnibase_infra/validation/validation_exemptions.yaml +1710 -0
- omnibase_infra/validation/validator_any_type.py +715 -0
- omnibase_infra/validation/validator_chain_propagation.py +839 -0
- omnibase_infra/validation/validator_execution_shape.py +465 -0
- omnibase_infra/validation/validator_localhandler.py +261 -0
- omnibase_infra/validation/validator_registration_security.py +410 -0
- omnibase_infra/validation/validator_routing_coverage.py +1020 -0
- omnibase_infra/validation/validator_runtime_shape.py +915 -0
- omnibase_infra/validation/validator_security.py +410 -0
- omnibase_infra/validation/validator_topic_category.py +1152 -0
- omnibase_infra-0.2.1.dist-info/METADATA +197 -0
- omnibase_infra-0.2.1.dist-info/RECORD +675 -0
- omnibase_infra-0.2.1.dist-info/WHEEL +4 -0
- omnibase_infra-0.2.1.dist-info/entry_points.txt +4 -0
- omnibase_infra-0.2.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,2260 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 OmniNode Team
|
|
3
|
+
"""Runtime Host Process implementation for ONEX Infrastructure.
|
|
4
|
+
|
|
5
|
+
This module implements the RuntimeHostProcess class, which is responsible for:
|
|
6
|
+
- Owning and managing an event bus instance (EventBusInmemory or EventBusKafka)
|
|
7
|
+
- Registering handlers via the wiring module
|
|
8
|
+
- Subscribing to event bus topics and routing envelopes to handlers
|
|
9
|
+
- Handling errors by producing success=False response envelopes
|
|
10
|
+
- Processing envelopes sequentially (no parallelism in MVP)
|
|
11
|
+
- Basic shutdown (no graceful drain in MVP)
|
|
12
|
+
|
|
13
|
+
The RuntimeHostProcess is the central coordinator for infrastructure runtime,
|
|
14
|
+
bridging event-driven message routing with protocol handlers.
|
|
15
|
+
|
|
16
|
+
Event Bus Support:
|
|
17
|
+
The RuntimeHostProcess supports two event bus implementations:
|
|
18
|
+
- EventBusInmemory: For local development and testing
|
|
19
|
+
- EventBusKafka: For production use with Kafka/Redpanda
|
|
20
|
+
|
|
21
|
+
The event bus can be injected via constructor or auto-created based on config.
|
|
22
|
+
|
|
23
|
+
Example Usage:
|
|
24
|
+
```python
|
|
25
|
+
from omnibase_infra.runtime import RuntimeHostProcess
|
|
26
|
+
|
|
27
|
+
async def main() -> None:
|
|
28
|
+
process = RuntimeHostProcess()
|
|
29
|
+
await process.start()
|
|
30
|
+
try:
|
|
31
|
+
# Process handles messages via event bus subscription
|
|
32
|
+
await asyncio.sleep(60)
|
|
33
|
+
finally:
|
|
34
|
+
await process.stop()
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Integration with Handlers:
|
|
38
|
+
Handlers are registered during start() via the wiring module. Each handler
|
|
39
|
+
processes envelopes for a specific protocol type (e.g., "http", "db").
|
|
40
|
+
The handler_type field in envelopes determines routing.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
from __future__ import annotations
|
|
44
|
+
|
|
45
|
+
import asyncio
|
|
46
|
+
import json
|
|
47
|
+
import logging
|
|
48
|
+
from collections.abc import Awaitable, Callable
|
|
49
|
+
from pathlib import Path
|
|
50
|
+
from typing import TYPE_CHECKING
|
|
51
|
+
from uuid import UUID, uuid4
|
|
52
|
+
|
|
53
|
+
from pydantic import BaseModel
|
|
54
|
+
|
|
55
|
+
from omnibase_infra.enums import EnumInfraTransportType
|
|
56
|
+
from omnibase_infra.errors import (
|
|
57
|
+
EnvelopeValidationError,
|
|
58
|
+
ModelInfraErrorContext,
|
|
59
|
+
ProtocolConfigurationError,
|
|
60
|
+
RuntimeHostError,
|
|
61
|
+
UnknownHandlerTypeError,
|
|
62
|
+
)
|
|
63
|
+
from omnibase_infra.event_bus.event_bus_inmemory import EventBusInmemory
|
|
64
|
+
from omnibase_infra.event_bus.event_bus_kafka import EventBusKafka
|
|
65
|
+
from omnibase_infra.runtime.envelope_validator import (
|
|
66
|
+
normalize_correlation_id,
|
|
67
|
+
validate_envelope,
|
|
68
|
+
)
|
|
69
|
+
from omnibase_infra.runtime.handler_registry import RegistryProtocolBinding
|
|
70
|
+
from omnibase_infra.runtime.models import ModelDuplicateResponse
|
|
71
|
+
from omnibase_infra.runtime.protocol_lifecycle_executor import ProtocolLifecycleExecutor
|
|
72
|
+
from omnibase_infra.runtime.util_wiring import wire_default_handlers
|
|
73
|
+
from omnibase_infra.utils.util_env_parsing import parse_env_float
|
|
74
|
+
|
|
75
|
+
if TYPE_CHECKING:
|
|
76
|
+
from omnibase_core.container import ModelONEXContainer
|
|
77
|
+
from omnibase_infra.event_bus.models import ModelEventMessage
|
|
78
|
+
from omnibase_infra.idempotency import ModelIdempotencyGuardConfig
|
|
79
|
+
from omnibase_infra.idempotency.protocol_idempotency_store import (
|
|
80
|
+
ProtocolIdempotencyStore,
|
|
81
|
+
)
|
|
82
|
+
from omnibase_infra.nodes.architecture_validator import ProtocolArchitectureRule
|
|
83
|
+
from omnibase_infra.runtime.contract_handler_discovery import (
|
|
84
|
+
ContractHandlerDiscovery,
|
|
85
|
+
)
|
|
86
|
+
from omnibase_spi.protocols.handlers.protocol_handler import ProtocolHandler
|
|
87
|
+
|
|
88
|
+
from omnibase_infra.models.types import JsonDict
|
|
89
|
+
|
|
90
|
+
# Expose wire_default_handlers as wire_handlers for test patching compatibility
|
|
91
|
+
# Tests patch "omnibase_infra.runtime.service_runtime_host_process.wire_handlers"
|
|
92
|
+
wire_handlers = wire_default_handlers
|
|
93
|
+
|
|
94
|
+
logger = logging.getLogger(__name__)
|
|
95
|
+
|
|
96
|
+
# Default configuration values
|
|
97
|
+
DEFAULT_INPUT_TOPIC = "requests"
|
|
98
|
+
DEFAULT_OUTPUT_TOPIC = "responses"
|
|
99
|
+
DEFAULT_GROUP_ID = "runtime-host"
|
|
100
|
+
|
|
101
|
+
# Health check timeout bounds (per ModelLifecycleSubcontract)
|
|
102
|
+
MIN_HEALTH_CHECK_TIMEOUT = 1.0
|
|
103
|
+
MAX_HEALTH_CHECK_TIMEOUT = 60.0
|
|
104
|
+
DEFAULT_HEALTH_CHECK_TIMEOUT: float = parse_env_float(
|
|
105
|
+
"ONEX_HEALTH_CHECK_TIMEOUT",
|
|
106
|
+
5.0,
|
|
107
|
+
min_value=MIN_HEALTH_CHECK_TIMEOUT,
|
|
108
|
+
max_value=MAX_HEALTH_CHECK_TIMEOUT,
|
|
109
|
+
transport_type=EnumInfraTransportType.RUNTIME,
|
|
110
|
+
service_name="runtime_host_process",
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Drain timeout bounds for graceful shutdown (OMN-756)
|
|
114
|
+
# Controls how long to wait for in-flight messages to complete before shutdown
|
|
115
|
+
MIN_DRAIN_TIMEOUT_SECONDS = 1.0
|
|
116
|
+
MAX_DRAIN_TIMEOUT_SECONDS = 300.0
|
|
117
|
+
DEFAULT_DRAIN_TIMEOUT_SECONDS: float = parse_env_float(
|
|
118
|
+
"ONEX_DRAIN_TIMEOUT",
|
|
119
|
+
30.0,
|
|
120
|
+
min_value=MIN_DRAIN_TIMEOUT_SECONDS,
|
|
121
|
+
max_value=MAX_DRAIN_TIMEOUT_SECONDS,
|
|
122
|
+
transport_type=EnumInfraTransportType.RUNTIME,
|
|
123
|
+
service_name="runtime_host_process",
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class RuntimeHostProcess:
|
|
128
|
+
"""Runtime host process that owns event bus and coordinates handlers.
|
|
129
|
+
|
|
130
|
+
The RuntimeHostProcess is the central coordinator for ONEX infrastructure
|
|
131
|
+
runtime. It owns an event bus instance (EventBusInmemory or EventBusKafka),
|
|
132
|
+
registers handlers via the wiring module, and routes incoming envelopes to
|
|
133
|
+
appropriate handlers.
|
|
134
|
+
|
|
135
|
+
Container Integration:
|
|
136
|
+
RuntimeHostProcess now accepts a ModelONEXContainer parameter for
|
|
137
|
+
dependency injection. The container provides access to:
|
|
138
|
+
- RegistryProtocolBinding: Handler registry for protocol routing
|
|
139
|
+
|
|
140
|
+
This follows ONEX container-based DI patterns for better testability
|
|
141
|
+
and lifecycle management. The legacy singleton pattern is deprecated
|
|
142
|
+
in favor of container resolution.
|
|
143
|
+
|
|
144
|
+
Attributes:
|
|
145
|
+
event_bus: The owned event bus instance (EventBusInmemory or EventBusKafka)
|
|
146
|
+
is_running: Whether the process is currently running
|
|
147
|
+
input_topic: Topic to subscribe to for incoming envelopes
|
|
148
|
+
output_topic: Topic to publish responses to
|
|
149
|
+
group_id: Consumer group identifier
|
|
150
|
+
|
|
151
|
+
Example:
|
|
152
|
+
```python
|
|
153
|
+
from omnibase_core.container import ModelONEXContainer
|
|
154
|
+
from omnibase_infra.runtime.util_container_wiring import wire_infrastructure_services
|
|
155
|
+
|
|
156
|
+
# Container-based initialization (preferred)
|
|
157
|
+
container = ModelONEXContainer()
|
|
158
|
+
wire_infrastructure_services(container)
|
|
159
|
+
process = RuntimeHostProcess(container=container)
|
|
160
|
+
await process.start()
|
|
161
|
+
health = await process.health_check()
|
|
162
|
+
await process.stop()
|
|
163
|
+
|
|
164
|
+
# Direct initialization (without container)
|
|
165
|
+
process = RuntimeHostProcess() # Uses singleton registries
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Graceful Shutdown:
|
|
169
|
+
The stop() method implements graceful shutdown with a configurable drain
|
|
170
|
+
period. After unsubscribing from topics, it waits for in-flight messages
|
|
171
|
+
to complete before shutting down handlers and closing the event bus.
|
|
172
|
+
See stop() docstring for configuration details.
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
def __init__(
|
|
176
|
+
self,
|
|
177
|
+
container: ModelONEXContainer | None = None,
|
|
178
|
+
event_bus: EventBusInmemory | EventBusKafka | None = None,
|
|
179
|
+
input_topic: str = DEFAULT_INPUT_TOPIC,
|
|
180
|
+
output_topic: str = DEFAULT_OUTPUT_TOPIC,
|
|
181
|
+
config: dict[str, object] | None = None,
|
|
182
|
+
handler_registry: RegistryProtocolBinding | None = None,
|
|
183
|
+
architecture_rules: tuple[ProtocolArchitectureRule, ...] | None = None,
|
|
184
|
+
contract_paths: list[str] | None = None,
|
|
185
|
+
) -> None:
|
|
186
|
+
"""Initialize the runtime host process.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
container: Optional ONEX dependency injection container. When provided,
|
|
190
|
+
the runtime host can resolve dependencies from the container if they
|
|
191
|
+
are not explicitly provided. This follows the ONEX container-based
|
|
192
|
+
DI pattern for better testability and explicit dependency management.
|
|
193
|
+
|
|
194
|
+
Container Resolution (during async start()):
|
|
195
|
+
- If handler_registry is None and container is provided, resolves
|
|
196
|
+
RegistryProtocolBinding from container.service_registry
|
|
197
|
+
- Event bus must be provided explicitly or defaults to EventBusInmemory
|
|
198
|
+
(required immediately during __init__)
|
|
199
|
+
|
|
200
|
+
Usage:
|
|
201
|
+
```python
|
|
202
|
+
from omnibase_core.container import ModelONEXContainer
|
|
203
|
+
from omnibase_infra.runtime.util_container_wiring import wire_infrastructure_services
|
|
204
|
+
|
|
205
|
+
container = ModelONEXContainer()
|
|
206
|
+
await wire_infrastructure_services(container)
|
|
207
|
+
process = RuntimeHostProcess(container=container)
|
|
208
|
+
await process.start()
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
event_bus: Optional event bus instance (EventBusInmemory or EventBusKafka).
|
|
212
|
+
If None, creates EventBusInmemory.
|
|
213
|
+
input_topic: Topic to subscribe to for incoming envelopes.
|
|
214
|
+
output_topic: Topic to publish responses to.
|
|
215
|
+
config: Optional configuration dict that can override topics and group_id.
|
|
216
|
+
Supported keys:
|
|
217
|
+
- input_topic: Override input topic
|
|
218
|
+
- output_topic: Override output topic
|
|
219
|
+
- group_id: Override consumer group identifier
|
|
220
|
+
- health_check_timeout_seconds: Timeout for individual handler
|
|
221
|
+
health checks (default: 5.0 seconds, valid range: 1-60 per
|
|
222
|
+
ModelLifecycleSubcontract). Values outside this range are
|
|
223
|
+
clamped to the nearest bound with a warning logged.
|
|
224
|
+
Invalid string values fall back to the default with a warning.
|
|
225
|
+
- drain_timeout_seconds: Maximum time to wait for in-flight
|
|
226
|
+
messages to complete during graceful shutdown (default: 30.0
|
|
227
|
+
seconds, valid range: 1-300). Values outside this range are
|
|
228
|
+
clamped to the nearest bound with a warning logged.
|
|
229
|
+
handler_registry: Optional RegistryProtocolBinding instance for handler lookup.
|
|
230
|
+
Type: RegistryProtocolBinding | None
|
|
231
|
+
|
|
232
|
+
Purpose:
|
|
233
|
+
Provides the registry that maps handler_type strings (e.g., "http", "db")
|
|
234
|
+
to their corresponding ProtocolHandler classes. The registry is queried
|
|
235
|
+
during start() to instantiate and initialize all registered handlers.
|
|
236
|
+
|
|
237
|
+
Resolution Order:
|
|
238
|
+
1. If handler_registry is provided, uses this pre-resolved registry
|
|
239
|
+
2. If container is provided, resolves from container.service_registry
|
|
240
|
+
3. If None, falls back to singleton via get_handler_registry()
|
|
241
|
+
|
|
242
|
+
Container Integration:
|
|
243
|
+
When using container-based DI (recommended), resolve the registry from
|
|
244
|
+
the container and pass it to RuntimeHostProcess:
|
|
245
|
+
|
|
246
|
+
```python
|
|
247
|
+
container = ModelONEXContainer()
|
|
248
|
+
wire_infrastructure_services(container)
|
|
249
|
+
registry = container.service_registry.resolve_service(RegistryProtocolBinding)
|
|
250
|
+
process = RuntimeHostProcess(handler_registry=registry)
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
This follows ONEX container-based DI patterns for better testability
|
|
254
|
+
and explicit dependency management.
|
|
255
|
+
|
|
256
|
+
container: Optional ONEX container for dependency injection. Required for
|
|
257
|
+
architecture validation. If None and architecture validation is requested,
|
|
258
|
+
a minimal container will be created.
|
|
259
|
+
|
|
260
|
+
architecture_rules: Optional tuple of architecture rules to validate at startup.
|
|
261
|
+
Type: tuple[ProtocolArchitectureRule, ...] | None
|
|
262
|
+
|
|
263
|
+
Purpose:
|
|
264
|
+
Architecture rules are validated BEFORE the runtime starts. Violations
|
|
265
|
+
with ERROR severity will prevent startup. Violations with WARNING
|
|
266
|
+
severity are logged but don't block startup.
|
|
267
|
+
|
|
268
|
+
Rules implementing ProtocolArchitectureRule can be:
|
|
269
|
+
- Custom rules specific to your application
|
|
270
|
+
- Standard rules from OMN-1099 validators
|
|
271
|
+
|
|
272
|
+
Example:
|
|
273
|
+
```python
|
|
274
|
+
from my_rules import NoHandlerPublishingRule, NoAnyTypesRule
|
|
275
|
+
|
|
276
|
+
process = RuntimeHostProcess(
|
|
277
|
+
container=container,
|
|
278
|
+
architecture_rules=(
|
|
279
|
+
NoHandlerPublishingRule(),
|
|
280
|
+
NoAnyTypesRule(),
|
|
281
|
+
),
|
|
282
|
+
)
|
|
283
|
+
await process.start() # Validates architecture first
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
contract_paths: Optional list of paths to scan for handler contracts.
|
|
287
|
+
Type: list[str] | None
|
|
288
|
+
|
|
289
|
+
Purpose:
|
|
290
|
+
Enables contract-based handler discovery. When provided, the runtime
|
|
291
|
+
will auto-discover and register handlers from these paths during
|
|
292
|
+
start() instead of using wire_default_handlers().
|
|
293
|
+
|
|
294
|
+
Paths can be:
|
|
295
|
+
- Directories: Recursively scanned for handler contracts
|
|
296
|
+
- Files: Directly loaded as contract files
|
|
297
|
+
|
|
298
|
+
Behavior:
|
|
299
|
+
- If contract_paths is provided: Uses ContractHandlerDiscovery
|
|
300
|
+
to auto-discover and register handlers from the specified paths.
|
|
301
|
+
- If contract_paths is None or empty: Falls back to the existing
|
|
302
|
+
wire_default_handlers() behavior.
|
|
303
|
+
|
|
304
|
+
Error Handling:
|
|
305
|
+
Discovery errors are logged but do not block startup. This enables
|
|
306
|
+
graceful degradation where some handlers can be registered even
|
|
307
|
+
if others fail to load.
|
|
308
|
+
|
|
309
|
+
Example:
|
|
310
|
+
```python
|
|
311
|
+
# Contract-based handler discovery
|
|
312
|
+
process = RuntimeHostProcess(
|
|
313
|
+
contract_paths=["src/nodes/handlers", "plugins/"]
|
|
314
|
+
)
|
|
315
|
+
await process.start()
|
|
316
|
+
|
|
317
|
+
# Or with explicit file paths
|
|
318
|
+
process = RuntimeHostProcess(
|
|
319
|
+
contract_paths=[
|
|
320
|
+
"handlers/auth/handler_contract.yaml",
|
|
321
|
+
"handlers/db/handler_contract.yaml",
|
|
322
|
+
]
|
|
323
|
+
)
|
|
324
|
+
```
|
|
325
|
+
"""
|
|
326
|
+
# Store container reference for dependency resolution
|
|
327
|
+
self._container: ModelONEXContainer | None = container
|
|
328
|
+
# Handler registry (container-based DI or singleton fallback)
|
|
329
|
+
self._handler_registry: RegistryProtocolBinding | None = handler_registry
|
|
330
|
+
|
|
331
|
+
# Architecture rules for startup validation
|
|
332
|
+
self._architecture_rules: tuple[ProtocolArchitectureRule, ...] = (
|
|
333
|
+
architecture_rules or ()
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
# Contract paths for handler discovery (OMN-1133)
|
|
337
|
+
# Convert strings to Path objects for consistent filesystem operations
|
|
338
|
+
self._contract_paths: list[Path] = (
|
|
339
|
+
[Path(p) for p in contract_paths] if contract_paths else []
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
# Handler discovery service (lazy-created if contract_paths provided)
|
|
343
|
+
self._handler_discovery: ContractHandlerDiscovery | None = None
|
|
344
|
+
|
|
345
|
+
# Create or use provided event bus
|
|
346
|
+
self._event_bus: EventBusInmemory | EventBusKafka = (
|
|
347
|
+
event_bus or EventBusInmemory()
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
# Extract configuration with defaults
|
|
351
|
+
config = config or {}
|
|
352
|
+
|
|
353
|
+
# Topic configuration (config overrides constructor args)
|
|
354
|
+
self._input_topic: str = str(config.get("input_topic", input_topic))
|
|
355
|
+
self._output_topic: str = str(config.get("output_topic", output_topic))
|
|
356
|
+
# Note: ModelRuntimeConfig uses field name "consumer_group" with alias "group_id".
|
|
357
|
+
# When config.model_dump() is called, it outputs "consumer_group" by default.
|
|
358
|
+
# We check both keys to support either field name or alias.
|
|
359
|
+
# Empty strings and whitespace-only strings fall through to the next option.
|
|
360
|
+
consumer_group = config.get("consumer_group")
|
|
361
|
+
group_id = config.get("group_id")
|
|
362
|
+
self._group_id: str = str(
|
|
363
|
+
(consumer_group if consumer_group and str(consumer_group).strip() else None)
|
|
364
|
+
or (group_id if group_id and str(group_id).strip() else None)
|
|
365
|
+
or DEFAULT_GROUP_ID
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
# Health check configuration (from lifecycle subcontract pattern)
|
|
369
|
+
# Default: 5.0 seconds, valid range: 1-60 seconds per ModelLifecycleSubcontract
|
|
370
|
+
# Values outside bounds are clamped with a warning
|
|
371
|
+
_timeout_raw = config.get("health_check_timeout_seconds")
|
|
372
|
+
timeout_value: float = DEFAULT_HEALTH_CHECK_TIMEOUT
|
|
373
|
+
if isinstance(_timeout_raw, int | float):
|
|
374
|
+
timeout_value = float(_timeout_raw)
|
|
375
|
+
elif isinstance(_timeout_raw, str):
|
|
376
|
+
try:
|
|
377
|
+
timeout_value = float(_timeout_raw)
|
|
378
|
+
except ValueError:
|
|
379
|
+
logger.warning(
|
|
380
|
+
"Invalid health_check_timeout_seconds string value, using default",
|
|
381
|
+
extra={
|
|
382
|
+
"invalid_value": _timeout_raw,
|
|
383
|
+
"default_value": DEFAULT_HEALTH_CHECK_TIMEOUT,
|
|
384
|
+
},
|
|
385
|
+
)
|
|
386
|
+
timeout_value = DEFAULT_HEALTH_CHECK_TIMEOUT
|
|
387
|
+
|
|
388
|
+
# Validate bounds and clamp if necessary
|
|
389
|
+
if (
|
|
390
|
+
timeout_value < MIN_HEALTH_CHECK_TIMEOUT
|
|
391
|
+
or timeout_value > MAX_HEALTH_CHECK_TIMEOUT
|
|
392
|
+
):
|
|
393
|
+
logger.warning(
|
|
394
|
+
"health_check_timeout_seconds out of valid range, clamping",
|
|
395
|
+
extra={
|
|
396
|
+
"original_value": timeout_value,
|
|
397
|
+
"min_value": MIN_HEALTH_CHECK_TIMEOUT,
|
|
398
|
+
"max_value": MAX_HEALTH_CHECK_TIMEOUT,
|
|
399
|
+
"clamped_value": max(
|
|
400
|
+
MIN_HEALTH_CHECK_TIMEOUT,
|
|
401
|
+
min(timeout_value, MAX_HEALTH_CHECK_TIMEOUT),
|
|
402
|
+
),
|
|
403
|
+
},
|
|
404
|
+
)
|
|
405
|
+
timeout_value = max(
|
|
406
|
+
MIN_HEALTH_CHECK_TIMEOUT,
|
|
407
|
+
min(timeout_value, MAX_HEALTH_CHECK_TIMEOUT),
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
self._health_check_timeout_seconds: float = timeout_value
|
|
411
|
+
|
|
412
|
+
# Drain timeout configuration for graceful shutdown (OMN-756)
|
|
413
|
+
# Default: 30.0 seconds, valid range: 1-300 seconds
|
|
414
|
+
# Values outside bounds are clamped with a warning
|
|
415
|
+
_drain_timeout_raw = config.get("drain_timeout_seconds")
|
|
416
|
+
drain_timeout_value: float = DEFAULT_DRAIN_TIMEOUT_SECONDS
|
|
417
|
+
if isinstance(_drain_timeout_raw, int | float):
|
|
418
|
+
drain_timeout_value = float(_drain_timeout_raw)
|
|
419
|
+
elif isinstance(_drain_timeout_raw, str):
|
|
420
|
+
try:
|
|
421
|
+
drain_timeout_value = float(_drain_timeout_raw)
|
|
422
|
+
except ValueError:
|
|
423
|
+
logger.warning(
|
|
424
|
+
"Invalid drain_timeout_seconds string value, using default",
|
|
425
|
+
extra={
|
|
426
|
+
"invalid_value": _drain_timeout_raw,
|
|
427
|
+
"default_value": DEFAULT_DRAIN_TIMEOUT_SECONDS,
|
|
428
|
+
},
|
|
429
|
+
)
|
|
430
|
+
drain_timeout_value = DEFAULT_DRAIN_TIMEOUT_SECONDS
|
|
431
|
+
|
|
432
|
+
# Validate drain timeout bounds and clamp if necessary
|
|
433
|
+
if (
|
|
434
|
+
drain_timeout_value < MIN_DRAIN_TIMEOUT_SECONDS
|
|
435
|
+
or drain_timeout_value > MAX_DRAIN_TIMEOUT_SECONDS
|
|
436
|
+
):
|
|
437
|
+
logger.warning(
|
|
438
|
+
"drain_timeout_seconds out of valid range, clamping",
|
|
439
|
+
extra={
|
|
440
|
+
"original_value": drain_timeout_value,
|
|
441
|
+
"min_value": MIN_DRAIN_TIMEOUT_SECONDS,
|
|
442
|
+
"max_value": MAX_DRAIN_TIMEOUT_SECONDS,
|
|
443
|
+
"clamped_value": max(
|
|
444
|
+
MIN_DRAIN_TIMEOUT_SECONDS,
|
|
445
|
+
min(drain_timeout_value, MAX_DRAIN_TIMEOUT_SECONDS),
|
|
446
|
+
),
|
|
447
|
+
},
|
|
448
|
+
)
|
|
449
|
+
drain_timeout_value = max(
|
|
450
|
+
MIN_DRAIN_TIMEOUT_SECONDS,
|
|
451
|
+
min(drain_timeout_value, MAX_DRAIN_TIMEOUT_SECONDS),
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
self._drain_timeout_seconds: float = drain_timeout_value
|
|
455
|
+
|
|
456
|
+
# Handler executor for lifecycle operations (shutdown, health check)
|
|
457
|
+
self._lifecycle_executor = ProtocolLifecycleExecutor(
|
|
458
|
+
health_check_timeout_seconds=self._health_check_timeout_seconds
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
# Store full config for handler initialization
|
|
462
|
+
self._config: dict[str, object] | None = config
|
|
463
|
+
|
|
464
|
+
# Runtime state
|
|
465
|
+
self._is_running: bool = False
|
|
466
|
+
|
|
467
|
+
# Subscription handle (callable to unsubscribe)
|
|
468
|
+
self._subscription: Callable[[], Awaitable[None]] | None = None
|
|
469
|
+
|
|
470
|
+
# Handler registry (handler_type -> handler instance)
|
|
471
|
+
# This will be populated from the singleton registry during start()
|
|
472
|
+
self._handlers: dict[str, ProtocolHandler] = {}
|
|
473
|
+
|
|
474
|
+
# Track failed handler instantiations (handler_type -> error message)
|
|
475
|
+
# Used by health_check() to report degraded state
|
|
476
|
+
self._failed_handlers: dict[str, str] = {}
|
|
477
|
+
|
|
478
|
+
# Pending message tracking for graceful shutdown (OMN-756)
|
|
479
|
+
# Tracks count of in-flight messages currently being processed
|
|
480
|
+
self._pending_message_count: int = 0
|
|
481
|
+
self._pending_lock: asyncio.Lock = asyncio.Lock()
|
|
482
|
+
|
|
483
|
+
# Drain state tracking for graceful shutdown (OMN-756)
|
|
484
|
+
# True when stop() has been called and we're waiting for messages to drain
|
|
485
|
+
self._is_draining: bool = False
|
|
486
|
+
|
|
487
|
+
# Idempotency guard for duplicate message detection (OMN-945)
|
|
488
|
+
# None = disabled, otherwise points to configured store
|
|
489
|
+
self._idempotency_store: ProtocolIdempotencyStore | None = None
|
|
490
|
+
self._idempotency_config: ModelIdempotencyGuardConfig | None = None
|
|
491
|
+
|
|
492
|
+
logger.debug(
|
|
493
|
+
"RuntimeHostProcess initialized",
|
|
494
|
+
extra={
|
|
495
|
+
"input_topic": self._input_topic,
|
|
496
|
+
"output_topic": self._output_topic,
|
|
497
|
+
"group_id": self._group_id,
|
|
498
|
+
"health_check_timeout_seconds": self._health_check_timeout_seconds,
|
|
499
|
+
"drain_timeout_seconds": self._drain_timeout_seconds,
|
|
500
|
+
"has_container": self._container is not None,
|
|
501
|
+
"has_handler_registry": self._handler_registry is not None,
|
|
502
|
+
"has_contract_paths": len(self._contract_paths) > 0,
|
|
503
|
+
"contract_path_count": len(self._contract_paths),
|
|
504
|
+
},
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
@property
|
|
508
|
+
def container(self) -> ModelONEXContainer | None:
|
|
509
|
+
"""Return the optional ONEX dependency injection container.
|
|
510
|
+
|
|
511
|
+
Returns:
|
|
512
|
+
The container if provided during initialization, None otherwise.
|
|
513
|
+
"""
|
|
514
|
+
return self._container
|
|
515
|
+
|
|
516
|
+
@property
|
|
517
|
+
def event_bus(self) -> EventBusInmemory | EventBusKafka:
|
|
518
|
+
"""Return the owned event bus instance.
|
|
519
|
+
|
|
520
|
+
Returns:
|
|
521
|
+
The event bus instance managed by this process.
|
|
522
|
+
"""
|
|
523
|
+
return self._event_bus
|
|
524
|
+
|
|
525
|
+
@property
|
|
526
|
+
def is_running(self) -> bool:
|
|
527
|
+
"""Return True if runtime is started.
|
|
528
|
+
|
|
529
|
+
Returns:
|
|
530
|
+
Boolean indicating whether the process is running.
|
|
531
|
+
"""
|
|
532
|
+
return self._is_running
|
|
533
|
+
|
|
534
|
+
@property
|
|
535
|
+
def input_topic(self) -> str:
|
|
536
|
+
"""Return the input topic for envelope subscription.
|
|
537
|
+
|
|
538
|
+
Returns:
|
|
539
|
+
The topic name to subscribe to for incoming envelopes.
|
|
540
|
+
"""
|
|
541
|
+
return self._input_topic
|
|
542
|
+
|
|
543
|
+
@property
|
|
544
|
+
def output_topic(self) -> str:
|
|
545
|
+
"""Return the output topic for response publishing.
|
|
546
|
+
|
|
547
|
+
Returns:
|
|
548
|
+
The topic name to publish responses to.
|
|
549
|
+
"""
|
|
550
|
+
return self._output_topic
|
|
551
|
+
|
|
552
|
+
@property
|
|
553
|
+
def group_id(self) -> str:
|
|
554
|
+
"""Return the consumer group identifier.
|
|
555
|
+
|
|
556
|
+
Returns:
|
|
557
|
+
The consumer group ID for this process.
|
|
558
|
+
"""
|
|
559
|
+
return self._group_id
|
|
560
|
+
|
|
561
|
+
@property
|
|
562
|
+
def is_draining(self) -> bool:
|
|
563
|
+
"""Return True if the process is draining pending messages during shutdown.
|
|
564
|
+
|
|
565
|
+
This property indicates whether the runtime host is in the graceful shutdown
|
|
566
|
+
drain period - the phase where stop() has been called, new messages are no
|
|
567
|
+
longer being accepted, and the process is waiting for in-flight messages to
|
|
568
|
+
complete before shutting down handlers and the event bus.
|
|
569
|
+
|
|
570
|
+
Drain State Transitions:
|
|
571
|
+
- False: Normal operation (accepting and processing messages)
|
|
572
|
+
- True: Drain period active (stop() called, waiting for pending messages)
|
|
573
|
+
- False: After drain completes and shutdown finishes
|
|
574
|
+
|
|
575
|
+
Use Cases:
|
|
576
|
+
- Health check reporting (indicate service is shutting down)
|
|
577
|
+
- Load balancer integration (remove from rotation during drain)
|
|
578
|
+
- Monitoring dashboards (show lifecycle state)
|
|
579
|
+
- Debugging shutdown behavior
|
|
580
|
+
|
|
581
|
+
Returns:
|
|
582
|
+
True if currently in drain period during graceful shutdown, False otherwise.
|
|
583
|
+
"""
|
|
584
|
+
return self._is_draining
|
|
585
|
+
|
|
586
|
+
@property
|
|
587
|
+
def pending_message_count(self) -> int:
|
|
588
|
+
"""Return the current count of in-flight messages being processed.
|
|
589
|
+
|
|
590
|
+
This property provides visibility into how many messages are currently
|
|
591
|
+
being processed by the runtime host. Used for graceful shutdown to
|
|
592
|
+
determine when it's safe to complete the shutdown process.
|
|
593
|
+
|
|
594
|
+
Atomicity Guarantees:
|
|
595
|
+
This property returns the raw counter value WITHOUT acquiring the
|
|
596
|
+
async lock (_pending_lock). This is safe because:
|
|
597
|
+
|
|
598
|
+
1. Single int read is atomic under CPython's GIL - reading a single
|
|
599
|
+
integer value cannot be interrupted mid-operation
|
|
600
|
+
2. The value is only used for observability/monitoring purposes
|
|
601
|
+
where exact precision is not required
|
|
602
|
+
3. The slight possibility of reading a stale value during concurrent
|
|
603
|
+
increment/decrement is acceptable for monitoring use cases
|
|
604
|
+
|
|
605
|
+
Thread Safety Considerations:
|
|
606
|
+
While the read itself is atomic, the value may be approximate if
|
|
607
|
+
read occurs during concurrent message processing:
|
|
608
|
+
- Another coroutine may be in the middle of incrementing/decrementing
|
|
609
|
+
- The value represents a point-in-time snapshot, not a synchronized view
|
|
610
|
+
- For observability, this approximation is acceptable and avoids
|
|
611
|
+
lock contention that would impact performance
|
|
612
|
+
|
|
613
|
+
Use Cases (appropriate for this property):
|
|
614
|
+
- Logging current message count for debugging
|
|
615
|
+
- Metrics/observability dashboards
|
|
616
|
+
- Approximate health status reporting
|
|
617
|
+
- Monitoring drain progress during shutdown
|
|
618
|
+
|
|
619
|
+
When to use shutdown_ready() instead:
|
|
620
|
+
For shutdown decisions requiring precise count, use the async
|
|
621
|
+
shutdown_ready() method which acquires the lock to ensure no
|
|
622
|
+
race condition with in-flight message processing. The stop()
|
|
623
|
+
method uses shutdown_ready() internally for this reason.
|
|
624
|
+
|
|
625
|
+
Returns:
|
|
626
|
+
Current count of messages being processed. May be approximate
|
|
627
|
+
if reads occur during concurrent increment/decrement operations.
|
|
628
|
+
"""
|
|
629
|
+
return self._pending_message_count
|
|
630
|
+
|
|
631
|
+
async def shutdown_ready(self) -> bool:
|
|
632
|
+
"""Check if process is ready for shutdown (no pending messages).
|
|
633
|
+
|
|
634
|
+
This method acquires the pending message lock to ensure an accurate
|
|
635
|
+
count of in-flight messages. Use this method during graceful shutdown
|
|
636
|
+
to determine when all pending messages have been processed.
|
|
637
|
+
|
|
638
|
+
Returns:
|
|
639
|
+
True if no messages are currently being processed, False otherwise.
|
|
640
|
+
"""
|
|
641
|
+
async with self._pending_lock:
|
|
642
|
+
return self._pending_message_count == 0
|
|
643
|
+
|
|
644
|
+
async def start(self) -> None:
|
|
645
|
+
"""Start the runtime host.
|
|
646
|
+
|
|
647
|
+
Performs the following steps:
|
|
648
|
+
1. Validate architecture compliance (if rules configured) - OMN-1138
|
|
649
|
+
2. Start event bus (if not already started)
|
|
650
|
+
3. Discover/wire handlers:
|
|
651
|
+
- If contract_paths provided: Auto-discover handlers from contracts (OMN-1133)
|
|
652
|
+
- Otherwise: Wire default handlers via wiring module
|
|
653
|
+
4. Populate self._handlers from singleton registry (instantiate and initialize)
|
|
654
|
+
5. Subscribe to input topic
|
|
655
|
+
|
|
656
|
+
Architecture Validation (OMN-1138):
|
|
657
|
+
If architecture_rules were provided at init, validation runs FIRST
|
|
658
|
+
before any other startup logic. This ensures:
|
|
659
|
+
- Violations are caught before resources are allocated
|
|
660
|
+
- Fast feedback for CI/CD pipelines
|
|
661
|
+
- Clean startup/failure without partial state
|
|
662
|
+
|
|
663
|
+
ERROR severity violations block startup by raising
|
|
664
|
+
ArchitectureViolationError. WARNING/INFO violations are logged
|
|
665
|
+
but don't block startup.
|
|
666
|
+
|
|
667
|
+
Contract-Based Handler Discovery (OMN-1133):
|
|
668
|
+
If contract_paths were provided at init, the runtime will auto-discover
|
|
669
|
+
handlers from these paths instead of using wire_default_handlers().
|
|
670
|
+
|
|
671
|
+
Discovery errors are logged but do not block startup, enabling
|
|
672
|
+
graceful degradation where some handlers can be registered even
|
|
673
|
+
if others fail to load.
|
|
674
|
+
|
|
675
|
+
This method is idempotent - calling start() on an already started
|
|
676
|
+
process is safe and has no effect.
|
|
677
|
+
|
|
678
|
+
Raises:
|
|
679
|
+
ArchitectureViolationError: If architecture validation fails with
|
|
680
|
+
blocking violations (ERROR severity).
|
|
681
|
+
"""
|
|
682
|
+
if self._is_running:
|
|
683
|
+
logger.debug("RuntimeHostProcess already started, skipping")
|
|
684
|
+
return
|
|
685
|
+
|
|
686
|
+
logger.info(
|
|
687
|
+
"Starting RuntimeHostProcess",
|
|
688
|
+
extra={
|
|
689
|
+
"input_topic": self._input_topic,
|
|
690
|
+
"output_topic": self._output_topic,
|
|
691
|
+
"group_id": self._group_id,
|
|
692
|
+
"has_contract_paths": len(self._contract_paths) > 0,
|
|
693
|
+
},
|
|
694
|
+
)
|
|
695
|
+
|
|
696
|
+
# Step 1: Validate architecture compliance FIRST (OMN-1138)
|
|
697
|
+
# This runs before event bus starts or handlers are wired to ensure
|
|
698
|
+
# clean failure without partial state if validation fails
|
|
699
|
+
await self._validate_architecture()
|
|
700
|
+
|
|
701
|
+
# Step 2: Start event bus
|
|
702
|
+
await self._event_bus.start()
|
|
703
|
+
|
|
704
|
+
# Step 3: Discover/wire handlers (OMN-1133)
|
|
705
|
+
# If contract_paths provided, use ContractHandlerDiscovery to auto-discover
|
|
706
|
+
# handlers from contract files. Otherwise, fall back to wire_default_handlers().
|
|
707
|
+
await self._discover_or_wire_handlers()
|
|
708
|
+
|
|
709
|
+
# Step 4: Populate self._handlers from singleton registry
|
|
710
|
+
# The wiring/discovery step registers handler classes, so we need to:
|
|
711
|
+
# - Get each registered handler class from the singleton registry
|
|
712
|
+
# - Instantiate the handler class
|
|
713
|
+
# - Call initialize() on each handler instance with config
|
|
714
|
+
# - Store the handler instance in self._handlers for routing
|
|
715
|
+
await self._populate_handlers_from_registry()
|
|
716
|
+
|
|
717
|
+
# Step 4.1: FAIL-FAST validation - runtime MUST have at least one handler
|
|
718
|
+
# A runtime with no handlers cannot process any events and is misconfigured.
|
|
719
|
+
# This catches configuration issues early rather than silently starting a
|
|
720
|
+
# runtime that cannot do anything useful.
|
|
721
|
+
if not self._handlers:
|
|
722
|
+
correlation_id = uuid4()
|
|
723
|
+
context = ModelInfraErrorContext(
|
|
724
|
+
transport_type=EnumInfraTransportType.RUNTIME,
|
|
725
|
+
operation="validate_handlers",
|
|
726
|
+
target_name="runtime_host_process",
|
|
727
|
+
correlation_id=correlation_id,
|
|
728
|
+
)
|
|
729
|
+
|
|
730
|
+
# Build informative error message with context about what was attempted
|
|
731
|
+
contract_paths_info = (
|
|
732
|
+
f" * contract_paths provided: {[str(p) for p in self._contract_paths]}\n"
|
|
733
|
+
if self._contract_paths
|
|
734
|
+
else " * contract_paths: NOT PROVIDED (using ONEX_CONTRACTS_DIR env var)\n"
|
|
735
|
+
)
|
|
736
|
+
|
|
737
|
+
# Get registry count for additional context
|
|
738
|
+
handler_registry = await self._get_handler_registry()
|
|
739
|
+
registry_protocol_count = len(handler_registry.list_protocols())
|
|
740
|
+
|
|
741
|
+
# Build additional diagnostic info
|
|
742
|
+
failed_handlers_detail = ""
|
|
743
|
+
if self._failed_handlers:
|
|
744
|
+
failed_handlers_detail = "FAILED HANDLERS (check these first):\n"
|
|
745
|
+
for handler_type, error_msg in self._failed_handlers.items():
|
|
746
|
+
failed_handlers_detail += f" * {handler_type}: {error_msg}\n"
|
|
747
|
+
failed_handlers_detail += "\n"
|
|
748
|
+
|
|
749
|
+
raise ProtocolConfigurationError(
|
|
750
|
+
"No handlers registered. The runtime cannot start without at least one handler.\n\n"
|
|
751
|
+
"CURRENT CONFIGURATION:\n"
|
|
752
|
+
f"{contract_paths_info}"
|
|
753
|
+
f" * Registry protocol count: {registry_protocol_count}\n"
|
|
754
|
+
f" * Failed handlers: {len(self._failed_handlers)}\n"
|
|
755
|
+
f" * Correlation ID: {correlation_id}\n\n"
|
|
756
|
+
f"{failed_handlers_detail}"
|
|
757
|
+
"TROUBLESHOOTING STEPS:\n"
|
|
758
|
+
" 1. Verify ONEX_CONTRACTS_DIR points to a valid contracts directory:\n"
|
|
759
|
+
" - Run: echo $ONEX_CONTRACTS_DIR && ls -la $ONEX_CONTRACTS_DIR\n"
|
|
760
|
+
" - Expected: Directory containing handler_contract.yaml or contract.yaml files\n\n"
|
|
761
|
+
" 2. Check for handler contract files:\n"
|
|
762
|
+
" - Run: find $ONEX_CONTRACTS_DIR -name 'handler_contract.yaml' -o -name 'contract.yaml'\n"
|
|
763
|
+
" - If empty: No contracts found - create handler contracts or set correct path\n\n"
|
|
764
|
+
" 3. Verify handler contracts have required fields:\n"
|
|
765
|
+
" - Required: handler_name, handler_class, handler_type\n"
|
|
766
|
+
" - Example:\n"
|
|
767
|
+
" handler_name: my_handler\n"
|
|
768
|
+
" handler_class: mymodule.handlers.MyHandler\n"
|
|
769
|
+
" handler_type: http\n\n"
|
|
770
|
+
" 4. Verify handler modules are importable:\n"
|
|
771
|
+
" - Run: python -c 'from mymodule.handlers import MyHandler; print(MyHandler)'\n"
|
|
772
|
+
" - Check PYTHONPATH includes your handler module paths\n\n"
|
|
773
|
+
" 5. Check application logs for loader errors:\n"
|
|
774
|
+
" - Look for: MODULE_NOT_FOUND (HANDLER_LOADER_010)\n"
|
|
775
|
+
" - Look for: CLASS_NOT_FOUND (HANDLER_LOADER_011)\n"
|
|
776
|
+
" - Look for: IMPORT_ERROR (HANDLER_LOADER_012)\n"
|
|
777
|
+
" - Look for: AMBIGUOUS_CONTRACT (HANDLER_LOADER_040)\n\n"
|
|
778
|
+
" 6. If using wire_handlers() manually:\n"
|
|
779
|
+
" - Ensure wire_handlers() is called before RuntimeHostProcess.start()\n"
|
|
780
|
+
" - Check that handlers implement ProtocolHandler interface\n\n"
|
|
781
|
+
" 7. Docker/container environment:\n"
|
|
782
|
+
" - Verify volume mounts include handler contract directories\n"
|
|
783
|
+
" - Check ONEX_CONTRACTS_DIR is set in docker-compose.yml/Dockerfile\n"
|
|
784
|
+
" - Run: docker exec <container> ls $ONEX_CONTRACTS_DIR\n\n"
|
|
785
|
+
"For verbose handler discovery logging, set LOG_LEVEL=DEBUG.",
|
|
786
|
+
context=context,
|
|
787
|
+
registered_handler_count=0,
|
|
788
|
+
failed_handler_count=len(self._failed_handlers),
|
|
789
|
+
failed_handlers=list(self._failed_handlers.keys()),
|
|
790
|
+
contract_paths=[str(p) for p in self._contract_paths],
|
|
791
|
+
registry_protocol_count=registry_protocol_count,
|
|
792
|
+
)
|
|
793
|
+
|
|
794
|
+
# Step 4.5: Initialize idempotency store if configured (OMN-945)
|
|
795
|
+
await self._initialize_idempotency_store()
|
|
796
|
+
|
|
797
|
+
# Step 5: Subscribe to input topic
|
|
798
|
+
self._subscription = await self._event_bus.subscribe(
|
|
799
|
+
topic=self._input_topic,
|
|
800
|
+
group_id=self._group_id,
|
|
801
|
+
on_message=self._on_message,
|
|
802
|
+
)
|
|
803
|
+
|
|
804
|
+
self._is_running = True
|
|
805
|
+
|
|
806
|
+
logger.info(
|
|
807
|
+
"RuntimeHostProcess started successfully",
|
|
808
|
+
extra={
|
|
809
|
+
"input_topic": self._input_topic,
|
|
810
|
+
"output_topic": self._output_topic,
|
|
811
|
+
"group_id": self._group_id,
|
|
812
|
+
"registered_handlers": list(self._handlers.keys()),
|
|
813
|
+
},
|
|
814
|
+
)
|
|
815
|
+
|
|
816
|
+
async def stop(self) -> None:
|
|
817
|
+
"""Stop the runtime host with graceful drain period.
|
|
818
|
+
|
|
819
|
+
Performs the following steps:
|
|
820
|
+
1. Unsubscribe from topics (stop receiving new messages)
|
|
821
|
+
2. Wait for in-flight messages to drain (up to drain_timeout_seconds)
|
|
822
|
+
3. Shutdown all registered handlers by priority (release resources)
|
|
823
|
+
4. Close event bus
|
|
824
|
+
|
|
825
|
+
This method is idempotent - calling stop() on an already stopped
|
|
826
|
+
process is safe and has no effect.
|
|
827
|
+
|
|
828
|
+
Drain Period:
|
|
829
|
+
After unsubscribing from topics, the process waits for in-flight
|
|
830
|
+
messages to complete processing. The drain period is controlled by
|
|
831
|
+
the drain_timeout_seconds configuration parameter (default: 30.0
|
|
832
|
+
seconds, valid range: 1-300).
|
|
833
|
+
|
|
834
|
+
During the drain period:
|
|
835
|
+
- No new messages are received (unsubscribed from topics)
|
|
836
|
+
- Messages currently being processed are allowed to complete
|
|
837
|
+
- shutdown_ready() is polled every 100ms to check completion
|
|
838
|
+
- If timeout is exceeded, shutdown proceeds with a warning
|
|
839
|
+
|
|
840
|
+
Handler Shutdown Order:
|
|
841
|
+
Handlers are shutdown in priority order, with higher priority handlers
|
|
842
|
+
shutting down first. Within the same priority level, handlers are
|
|
843
|
+
shutdown in parallel for performance.
|
|
844
|
+
|
|
845
|
+
Priority is determined by the handler's shutdown_priority() method:
|
|
846
|
+
- Higher values = shutdown first
|
|
847
|
+
- Handlers without shutdown_priority() get default priority of 0
|
|
848
|
+
|
|
849
|
+
Recommended Priority Scheme:
|
|
850
|
+
- 100: Consumers (stop receiving before stopping producers)
|
|
851
|
+
- 80: Active connections (close before closing pools)
|
|
852
|
+
- 50: Producers (stop producing before closing pools)
|
|
853
|
+
- 40: Connection pools (close last)
|
|
854
|
+
- 0: Default for handlers without explicit priority
|
|
855
|
+
|
|
856
|
+
This ensures dependency-based ordering:
|
|
857
|
+
- Consumers shutdown before producers
|
|
858
|
+
- Connections shutdown before connection pools
|
|
859
|
+
- Downstream resources shutdown before upstream resources
|
|
860
|
+
"""
|
|
861
|
+
if not self._is_running:
|
|
862
|
+
logger.debug("RuntimeHostProcess already stopped, skipping")
|
|
863
|
+
return
|
|
864
|
+
|
|
865
|
+
logger.info("Stopping RuntimeHostProcess")
|
|
866
|
+
|
|
867
|
+
# Step 1: Unsubscribe from topics (stop receiving new messages)
|
|
868
|
+
if self._subscription is not None:
|
|
869
|
+
await self._subscription()
|
|
870
|
+
self._subscription = None
|
|
871
|
+
|
|
872
|
+
# Step 1.5: Wait for in-flight messages to drain (OMN-756)
|
|
873
|
+
# This allows messages currently being processed to complete
|
|
874
|
+
loop = asyncio.get_running_loop()
|
|
875
|
+
drain_start = loop.time()
|
|
876
|
+
drain_deadline = drain_start + self._drain_timeout_seconds
|
|
877
|
+
last_progress_log = drain_start
|
|
878
|
+
|
|
879
|
+
# Mark drain state for health check visibility (OMN-756)
|
|
880
|
+
self._is_draining = True
|
|
881
|
+
|
|
882
|
+
# Log drain start for observability
|
|
883
|
+
logger.info(
|
|
884
|
+
"Starting drain period",
|
|
885
|
+
extra={
|
|
886
|
+
"pending_messages": self._pending_message_count,
|
|
887
|
+
"drain_timeout_seconds": self._drain_timeout_seconds,
|
|
888
|
+
},
|
|
889
|
+
)
|
|
890
|
+
|
|
891
|
+
while not await self.shutdown_ready():
|
|
892
|
+
remaining = drain_deadline - loop.time()
|
|
893
|
+
if remaining <= 0:
|
|
894
|
+
logger.warning(
|
|
895
|
+
"Drain timeout exceeded, forcing shutdown",
|
|
896
|
+
extra={
|
|
897
|
+
"pending_messages": self._pending_message_count,
|
|
898
|
+
"drain_timeout_seconds": self._drain_timeout_seconds,
|
|
899
|
+
"metric.drain_timeout_exceeded": True,
|
|
900
|
+
"metric.pending_at_timeout": self._pending_message_count,
|
|
901
|
+
},
|
|
902
|
+
)
|
|
903
|
+
break
|
|
904
|
+
|
|
905
|
+
# Wait a short interval before checking again
|
|
906
|
+
await asyncio.sleep(min(0.1, remaining))
|
|
907
|
+
|
|
908
|
+
# Log progress every 5 seconds during long drains for observability
|
|
909
|
+
elapsed = loop.time() - drain_start
|
|
910
|
+
if elapsed - (last_progress_log - drain_start) >= 5.0:
|
|
911
|
+
logger.info(
|
|
912
|
+
"Drain in progress",
|
|
913
|
+
extra={
|
|
914
|
+
"pending_messages": self._pending_message_count,
|
|
915
|
+
"elapsed_seconds": round(elapsed, 2),
|
|
916
|
+
"remaining_seconds": round(remaining, 2),
|
|
917
|
+
},
|
|
918
|
+
)
|
|
919
|
+
last_progress_log = loop.time()
|
|
920
|
+
|
|
921
|
+
# Clear drain state after drain period completes
|
|
922
|
+
self._is_draining = False
|
|
923
|
+
|
|
924
|
+
logger.info(
|
|
925
|
+
"Drain period completed",
|
|
926
|
+
extra={
|
|
927
|
+
"drain_duration_seconds": loop.time() - drain_start,
|
|
928
|
+
"pending_messages": self._pending_message_count,
|
|
929
|
+
"metric.drain_duration": loop.time() - drain_start,
|
|
930
|
+
"metric.forced_shutdown": self._pending_message_count > 0,
|
|
931
|
+
},
|
|
932
|
+
)
|
|
933
|
+
|
|
934
|
+
# Step 2: Shutdown all handlers by priority (release resources like DB/Kafka connections)
|
|
935
|
+
# Delegates to ProtocolLifecycleExecutor which handles:
|
|
936
|
+
# - Grouping handlers by priority (higher priority first)
|
|
937
|
+
# - Parallel shutdown within priority groups for performance
|
|
938
|
+
if self._handlers:
|
|
939
|
+
shutdown_result = (
|
|
940
|
+
await self._lifecycle_executor.shutdown_handlers_by_priority(
|
|
941
|
+
self._handlers
|
|
942
|
+
)
|
|
943
|
+
)
|
|
944
|
+
|
|
945
|
+
# Log summary (ProtocolLifecycleExecutor already logs detailed info)
|
|
946
|
+
logger.info(
|
|
947
|
+
"Handler shutdown completed",
|
|
948
|
+
extra={
|
|
949
|
+
"succeeded_handlers": shutdown_result.succeeded_handlers,
|
|
950
|
+
"failed_handlers": [
|
|
951
|
+
f.handler_type for f in shutdown_result.failed_handlers
|
|
952
|
+
],
|
|
953
|
+
"total_handlers": shutdown_result.total_count,
|
|
954
|
+
"success_count": shutdown_result.success_count,
|
|
955
|
+
"failure_count": shutdown_result.failure_count,
|
|
956
|
+
},
|
|
957
|
+
)
|
|
958
|
+
|
|
959
|
+
# Step 2.5: Cleanup idempotency store if initialized (OMN-945)
|
|
960
|
+
await self._cleanup_idempotency_store()
|
|
961
|
+
|
|
962
|
+
# Step 3: Close event bus
|
|
963
|
+
await self._event_bus.close()
|
|
964
|
+
|
|
965
|
+
self._is_running = False
|
|
966
|
+
|
|
967
|
+
logger.info("RuntimeHostProcess stopped successfully")
|
|
968
|
+
|
|
969
|
+
async def _discover_or_wire_handlers(self) -> None:
|
|
970
|
+
"""Discover handlers from contracts or wire default handlers.
|
|
971
|
+
|
|
972
|
+
This method implements the handler discovery/wiring step (Step 3) of the
|
|
973
|
+
start() sequence. It supports two modes:
|
|
974
|
+
|
|
975
|
+
Contract-Based Discovery (OMN-1133):
|
|
976
|
+
If contract_paths were provided at init, uses ContractHandlerDiscovery
|
|
977
|
+
to auto-discover and register handlers from the specified paths.
|
|
978
|
+
|
|
979
|
+
Discovery errors are logged but do not block startup, enabling
|
|
980
|
+
graceful degradation where some handlers can be registered even
|
|
981
|
+
if others fail to load.
|
|
982
|
+
|
|
983
|
+
Default Handler Wiring (Fallback):
|
|
984
|
+
If no contract_paths were provided, falls back to wire_default_handlers()
|
|
985
|
+
which registers the standard set of handlers (HTTP, DB, Consul, Vault).
|
|
986
|
+
|
|
987
|
+
The discovery/wiring step registers handler CLASSES with the handler registry.
|
|
988
|
+
The subsequent _populate_handlers_from_registry() step instantiates and
|
|
989
|
+
initializes these handler classes.
|
|
990
|
+
"""
|
|
991
|
+
if self._contract_paths:
|
|
992
|
+
# Contract-based handler discovery (OMN-1133)
|
|
993
|
+
await self._discover_handlers_from_contracts()
|
|
994
|
+
else:
|
|
995
|
+
# Fallback to default handler wiring (existing behavior)
|
|
996
|
+
wire_handlers()
|
|
997
|
+
|
|
998
|
+
async def _discover_handlers_from_contracts(self) -> None:
|
|
999
|
+
"""Discover and register handlers from contract files.
|
|
1000
|
+
|
|
1001
|
+
This method implements contract-based handler discovery as part of OMN-1133.
|
|
1002
|
+
It creates a ContractHandlerDiscovery service, discovers handlers from the
|
|
1003
|
+
configured contract_paths, and registers them with the handler registry.
|
|
1004
|
+
|
|
1005
|
+
Error Handling:
|
|
1006
|
+
Discovery errors are logged but do not block startup. This enables
|
|
1007
|
+
graceful degradation where some handlers can be registered even if
|
|
1008
|
+
others fail to load.
|
|
1009
|
+
|
|
1010
|
+
The discovery service tracks:
|
|
1011
|
+
- handlers_discovered: Number of handlers found in contracts
|
|
1012
|
+
- handlers_registered: Number successfully registered
|
|
1013
|
+
- errors: List of individual discovery/registration failures
|
|
1014
|
+
|
|
1015
|
+
Related:
|
|
1016
|
+
- ContractHandlerDiscovery: Discovery service implementation
|
|
1017
|
+
- HandlerPluginLoader: Contract file parsing and validation
|
|
1018
|
+
- ModelDiscoveryResult: Result model with error tracking
|
|
1019
|
+
"""
|
|
1020
|
+
from omnibase_infra.runtime.contract_handler_discovery import (
|
|
1021
|
+
ContractHandlerDiscovery,
|
|
1022
|
+
)
|
|
1023
|
+
from omnibase_infra.runtime.handler_plugin_loader import HandlerPluginLoader
|
|
1024
|
+
|
|
1025
|
+
logger.info(
|
|
1026
|
+
"Starting contract-based handler discovery",
|
|
1027
|
+
extra={
|
|
1028
|
+
"contract_paths": [str(p) for p in self._contract_paths],
|
|
1029
|
+
"path_count": len(self._contract_paths),
|
|
1030
|
+
},
|
|
1031
|
+
)
|
|
1032
|
+
|
|
1033
|
+
# Create handler discovery service if not already created
|
|
1034
|
+
# Uses the handler_registry from init or falls back to singleton
|
|
1035
|
+
handler_registry = await self._get_handler_registry()
|
|
1036
|
+
|
|
1037
|
+
self._handler_discovery = ContractHandlerDiscovery(
|
|
1038
|
+
plugin_loader=HandlerPluginLoader(),
|
|
1039
|
+
handler_registry=handler_registry,
|
|
1040
|
+
)
|
|
1041
|
+
|
|
1042
|
+
# Discover and register handlers from contract paths
|
|
1043
|
+
discovery_result = await self._handler_discovery.discover_and_register(
|
|
1044
|
+
contract_paths=self._contract_paths,
|
|
1045
|
+
)
|
|
1046
|
+
|
|
1047
|
+
# Log discovery results
|
|
1048
|
+
if discovery_result.has_errors:
|
|
1049
|
+
logger.warning(
|
|
1050
|
+
"Handler discovery completed with errors",
|
|
1051
|
+
extra={
|
|
1052
|
+
"handlers_discovered": discovery_result.handlers_discovered,
|
|
1053
|
+
"handlers_registered": discovery_result.handlers_registered,
|
|
1054
|
+
"error_count": len(discovery_result.errors),
|
|
1055
|
+
},
|
|
1056
|
+
)
|
|
1057
|
+
# Log individual errors for debugging
|
|
1058
|
+
for error in discovery_result.errors:
|
|
1059
|
+
logger.error(
|
|
1060
|
+
"Handler discovery error: %s",
|
|
1061
|
+
error.message,
|
|
1062
|
+
extra={
|
|
1063
|
+
"error_code": error.error_code,
|
|
1064
|
+
"handler_name": error.handler_name,
|
|
1065
|
+
"contract_path": str(error.contract_path)
|
|
1066
|
+
if error.contract_path
|
|
1067
|
+
else None,
|
|
1068
|
+
},
|
|
1069
|
+
)
|
|
1070
|
+
else:
|
|
1071
|
+
logger.info(
|
|
1072
|
+
"Handler discovery completed successfully",
|
|
1073
|
+
extra={
|
|
1074
|
+
"handlers_discovered": discovery_result.handlers_discovered,
|
|
1075
|
+
"handlers_registered": discovery_result.handlers_registered,
|
|
1076
|
+
},
|
|
1077
|
+
)
|
|
1078
|
+
|
|
1079
|
+
async def _populate_handlers_from_registry(self) -> None:
|
|
1080
|
+
"""Populate self._handlers from handler registry (container or singleton).
|
|
1081
|
+
|
|
1082
|
+
This method bridges the gap between the wiring module (which registers
|
|
1083
|
+
handler CLASSES to the registry) and the RuntimeHostProcess
|
|
1084
|
+
(which needs handler INSTANCES in self._handlers for routing).
|
|
1085
|
+
|
|
1086
|
+
Registry Resolution:
|
|
1087
|
+
- If handler_registry provided: Uses pre-resolved registry
|
|
1088
|
+
- If no handler_registry: Falls back to singleton get_handler_registry()
|
|
1089
|
+
|
|
1090
|
+
For each registered handler type in the registry:
|
|
1091
|
+
1. Skip if handler type is already registered (e.g., by tests)
|
|
1092
|
+
2. Get the handler class from the registry
|
|
1093
|
+
3. Instantiate the handler class
|
|
1094
|
+
4. Call initialize() on the handler instance with self._config
|
|
1095
|
+
5. Store the handler instance in self._handlers
|
|
1096
|
+
|
|
1097
|
+
This ensures that after start() is called, self._handlers contains
|
|
1098
|
+
fully initialized handler instances ready for envelope routing.
|
|
1099
|
+
|
|
1100
|
+
Note: Handlers already in self._handlers (e.g., injected by tests via
|
|
1101
|
+
register_handler() or patch.object()) are preserved and not overwritten.
|
|
1102
|
+
"""
|
|
1103
|
+
# Get handler registry (pre-resolved, container, or singleton)
|
|
1104
|
+
handler_registry = await self._get_handler_registry()
|
|
1105
|
+
registered_types = handler_registry.list_protocols()
|
|
1106
|
+
|
|
1107
|
+
logger.debug(
|
|
1108
|
+
"Populating handlers from registry",
|
|
1109
|
+
extra={
|
|
1110
|
+
"registered_types": registered_types,
|
|
1111
|
+
"existing_handlers": list(self._handlers.keys()),
|
|
1112
|
+
},
|
|
1113
|
+
)
|
|
1114
|
+
|
|
1115
|
+
for handler_type in registered_types:
|
|
1116
|
+
# Skip if handler is already registered (e.g., by tests or explicit registration)
|
|
1117
|
+
if handler_type in self._handlers:
|
|
1118
|
+
logger.debug(
|
|
1119
|
+
"Handler already registered, skipping",
|
|
1120
|
+
extra={
|
|
1121
|
+
"handler_type": handler_type,
|
|
1122
|
+
"existing_handler_class": type(
|
|
1123
|
+
self._handlers[handler_type]
|
|
1124
|
+
).__name__,
|
|
1125
|
+
},
|
|
1126
|
+
)
|
|
1127
|
+
continue
|
|
1128
|
+
|
|
1129
|
+
try:
|
|
1130
|
+
# Get handler class from singleton registry
|
|
1131
|
+
handler_cls: type[ProtocolHandler] = handler_registry.get(handler_type)
|
|
1132
|
+
|
|
1133
|
+
# Instantiate the handler
|
|
1134
|
+
handler_instance: ProtocolHandler = handler_cls()
|
|
1135
|
+
|
|
1136
|
+
# Call initialize() if the handler has this method
|
|
1137
|
+
# Handlers may require async initialization with config
|
|
1138
|
+
if hasattr(handler_instance, "initialize"):
|
|
1139
|
+
await handler_instance.initialize(self._config)
|
|
1140
|
+
|
|
1141
|
+
# Store the handler instance for routing
|
|
1142
|
+
self._handlers[handler_type] = handler_instance
|
|
1143
|
+
|
|
1144
|
+
logger.debug(
|
|
1145
|
+
"Handler instantiated and initialized",
|
|
1146
|
+
extra={
|
|
1147
|
+
"handler_type": handler_type,
|
|
1148
|
+
"handler_class": handler_cls.__name__,
|
|
1149
|
+
},
|
|
1150
|
+
)
|
|
1151
|
+
|
|
1152
|
+
except Exception as e:
|
|
1153
|
+
# Track the failure for health_check() reporting
|
|
1154
|
+
self._failed_handlers[handler_type] = str(e)
|
|
1155
|
+
|
|
1156
|
+
# Log error but continue with other handlers
|
|
1157
|
+
# This allows partial handler availability
|
|
1158
|
+
correlation_id = uuid4()
|
|
1159
|
+
context = ModelInfraErrorContext(
|
|
1160
|
+
transport_type=EnumInfraTransportType.RUNTIME,
|
|
1161
|
+
operation="populate_handlers",
|
|
1162
|
+
target_name=handler_type,
|
|
1163
|
+
correlation_id=correlation_id,
|
|
1164
|
+
)
|
|
1165
|
+
infra_error = RuntimeHostError(
|
|
1166
|
+
f"Failed to instantiate handler for type {handler_type}: {e}",
|
|
1167
|
+
context=context,
|
|
1168
|
+
)
|
|
1169
|
+
infra_error.__cause__ = e
|
|
1170
|
+
|
|
1171
|
+
logger.warning(
|
|
1172
|
+
"Failed to instantiate handler, skipping",
|
|
1173
|
+
extra={
|
|
1174
|
+
"handler_type": handler_type,
|
|
1175
|
+
"error": str(e),
|
|
1176
|
+
"correlation_id": str(correlation_id),
|
|
1177
|
+
},
|
|
1178
|
+
)
|
|
1179
|
+
|
|
1180
|
+
logger.info(
|
|
1181
|
+
"Handlers populated from registry",
|
|
1182
|
+
extra={
|
|
1183
|
+
"populated_handlers": list(self._handlers.keys()),
|
|
1184
|
+
"total_count": len(self._handlers),
|
|
1185
|
+
},
|
|
1186
|
+
)
|
|
1187
|
+
|
|
1188
|
+
async def _get_handler_registry(self) -> RegistryProtocolBinding:
|
|
1189
|
+
"""Get handler registry (pre-resolved, container, or singleton).
|
|
1190
|
+
|
|
1191
|
+
Resolution order:
|
|
1192
|
+
1. If handler_registry was provided to __init__, uses it (cached)
|
|
1193
|
+
2. If container was provided and has RegistryProtocolBinding, resolves from container
|
|
1194
|
+
3. Falls back to singleton via get_handler_registry()
|
|
1195
|
+
|
|
1196
|
+
Caching Behavior:
|
|
1197
|
+
The resolved registry is cached after the first successful resolution.
|
|
1198
|
+
Subsequent calls return the cached instance without re-resolving from
|
|
1199
|
+
the container or re-fetching the singleton. This ensures consistent
|
|
1200
|
+
registry usage throughout the runtime's lifecycle and avoids redundant
|
|
1201
|
+
resolution operations.
|
|
1202
|
+
|
|
1203
|
+
Returns:
|
|
1204
|
+
RegistryProtocolBinding instance.
|
|
1205
|
+
"""
|
|
1206
|
+
if self._handler_registry is not None:
|
|
1207
|
+
# Use pre-resolved registry from constructor
|
|
1208
|
+
return self._handler_registry
|
|
1209
|
+
|
|
1210
|
+
# Try to resolve from container if provided
|
|
1211
|
+
if self._container is not None and self._container.service_registry is not None:
|
|
1212
|
+
try:
|
|
1213
|
+
resolved_registry: RegistryProtocolBinding = (
|
|
1214
|
+
await self._container.service_registry.resolve_service(
|
|
1215
|
+
RegistryProtocolBinding
|
|
1216
|
+
)
|
|
1217
|
+
)
|
|
1218
|
+
# Cache the resolved registry for subsequent calls
|
|
1219
|
+
self._handler_registry = resolved_registry
|
|
1220
|
+
logger.debug(
|
|
1221
|
+
"Handler registry resolved from container",
|
|
1222
|
+
extra={"registry_type": type(resolved_registry).__name__},
|
|
1223
|
+
)
|
|
1224
|
+
return resolved_registry
|
|
1225
|
+
except (
|
|
1226
|
+
RuntimeError,
|
|
1227
|
+
ValueError,
|
|
1228
|
+
KeyError,
|
|
1229
|
+
AttributeError,
|
|
1230
|
+
LookupError,
|
|
1231
|
+
) as e:
|
|
1232
|
+
# Container resolution failed, fall through to singleton
|
|
1233
|
+
logger.debug(
|
|
1234
|
+
"Container registry resolution failed, falling back to singleton",
|
|
1235
|
+
extra={"error": str(e)},
|
|
1236
|
+
)
|
|
1237
|
+
|
|
1238
|
+
# Graceful degradation: fall back to singleton pattern when container unavailable
|
|
1239
|
+
from omnibase_infra.runtime.handler_registry import get_handler_registry
|
|
1240
|
+
|
|
1241
|
+
singleton_registry = get_handler_registry()
|
|
1242
|
+
# Cache for consistency with container resolution path
|
|
1243
|
+
self._handler_registry = singleton_registry
|
|
1244
|
+
logger.debug(
|
|
1245
|
+
"Handler registry resolved from singleton",
|
|
1246
|
+
extra={"registry_type": type(singleton_registry).__name__},
|
|
1247
|
+
)
|
|
1248
|
+
return singleton_registry
|
|
1249
|
+
|
|
1250
|
+
async def _on_message(self, message: ModelEventMessage) -> None:
|
|
1251
|
+
"""Handle incoming message from event bus subscription.
|
|
1252
|
+
|
|
1253
|
+
This is the callback invoked by the event bus when a message arrives
|
|
1254
|
+
on the input topic. It deserializes the envelope and routes it.
|
|
1255
|
+
|
|
1256
|
+
The method tracks pending messages for graceful shutdown support (OMN-756).
|
|
1257
|
+
The pending message count is incremented at the start of processing and
|
|
1258
|
+
decremented when processing completes (success or failure).
|
|
1259
|
+
|
|
1260
|
+
Args:
|
|
1261
|
+
message: The event message containing the envelope payload.
|
|
1262
|
+
"""
|
|
1263
|
+
# Increment pending message count (OMN-756: graceful shutdown tracking)
|
|
1264
|
+
async with self._pending_lock:
|
|
1265
|
+
self._pending_message_count += 1
|
|
1266
|
+
|
|
1267
|
+
try:
|
|
1268
|
+
# Deserialize envelope from message value
|
|
1269
|
+
envelope = json.loads(message.value.decode("utf-8"))
|
|
1270
|
+
await self._handle_envelope(envelope)
|
|
1271
|
+
except json.JSONDecodeError as e:
|
|
1272
|
+
# Create infrastructure error context for tracing
|
|
1273
|
+
correlation_id = uuid4()
|
|
1274
|
+
context = ModelInfraErrorContext(
|
|
1275
|
+
transport_type=EnumInfraTransportType.RUNTIME,
|
|
1276
|
+
operation="decode_envelope",
|
|
1277
|
+
target_name=message.topic,
|
|
1278
|
+
correlation_id=correlation_id,
|
|
1279
|
+
)
|
|
1280
|
+
# Chain the error with infrastructure context
|
|
1281
|
+
infra_error = RuntimeHostError(
|
|
1282
|
+
f"Failed to decode JSON envelope from message: {e}",
|
|
1283
|
+
context=context,
|
|
1284
|
+
)
|
|
1285
|
+
infra_error.__cause__ = e # Proper error chaining
|
|
1286
|
+
|
|
1287
|
+
logger.exception(
|
|
1288
|
+
"Failed to decode envelope from message",
|
|
1289
|
+
extra={
|
|
1290
|
+
"error": str(e),
|
|
1291
|
+
"topic": message.topic,
|
|
1292
|
+
"offset": message.offset,
|
|
1293
|
+
"correlation_id": str(correlation_id),
|
|
1294
|
+
},
|
|
1295
|
+
)
|
|
1296
|
+
# Publish error response for malformed messages
|
|
1297
|
+
error_response = self._create_error_response(
|
|
1298
|
+
error=f"Invalid JSON in message: {e}",
|
|
1299
|
+
correlation_id=correlation_id,
|
|
1300
|
+
)
|
|
1301
|
+
await self._publish_envelope_safe(error_response, self._output_topic)
|
|
1302
|
+
finally:
|
|
1303
|
+
# Decrement pending message count (OMN-756: graceful shutdown tracking)
|
|
1304
|
+
async with self._pending_lock:
|
|
1305
|
+
self._pending_message_count -= 1
|
|
1306
|
+
|
|
1307
|
+
async def _handle_envelope(self, envelope: dict[str, object]) -> None:
|
|
1308
|
+
"""Route envelope to appropriate handler.
|
|
1309
|
+
|
|
1310
|
+
Validates envelope before dispatch and routes it to the appropriate
|
|
1311
|
+
registered handler. Publishes the response to the output topic.
|
|
1312
|
+
|
|
1313
|
+
Validation (performed before dispatch):
|
|
1314
|
+
1. Operation presence and type validation
|
|
1315
|
+
2. Handler prefix validation against registry
|
|
1316
|
+
3. Payload requirement validation for specific operations
|
|
1317
|
+
4. Correlation ID normalization to UUID
|
|
1318
|
+
|
|
1319
|
+
Args:
|
|
1320
|
+
envelope: Dict with 'operation', 'payload', optional 'correlation_id',
|
|
1321
|
+
and 'handler_type'.
|
|
1322
|
+
"""
|
|
1323
|
+
# Pre-validation: Get correlation_id for error responses if validation fails
|
|
1324
|
+
# This handles the case where validation itself throws before normalizing
|
|
1325
|
+
pre_validation_correlation_id = normalize_correlation_id(
|
|
1326
|
+
envelope.get("correlation_id")
|
|
1327
|
+
)
|
|
1328
|
+
|
|
1329
|
+
# Step 1: Validate envelope BEFORE dispatch
|
|
1330
|
+
# This validates operation, prefix, payload requirements, and normalizes correlation_id
|
|
1331
|
+
try:
|
|
1332
|
+
validate_envelope(envelope, await self._get_handler_registry())
|
|
1333
|
+
except EnvelopeValidationError as e:
|
|
1334
|
+
# Validation failed - missing operation or payload
|
|
1335
|
+
error_response = self._create_error_response(
|
|
1336
|
+
error=str(e),
|
|
1337
|
+
correlation_id=pre_validation_correlation_id,
|
|
1338
|
+
)
|
|
1339
|
+
await self._publish_envelope_safe(error_response, self._output_topic)
|
|
1340
|
+
logger.warning(
|
|
1341
|
+
"Envelope validation failed",
|
|
1342
|
+
extra={
|
|
1343
|
+
"error": str(e),
|
|
1344
|
+
"correlation_id": str(pre_validation_correlation_id),
|
|
1345
|
+
"error_type": "EnvelopeValidationError",
|
|
1346
|
+
},
|
|
1347
|
+
)
|
|
1348
|
+
return
|
|
1349
|
+
except UnknownHandlerTypeError as e:
|
|
1350
|
+
# Unknown handler prefix - hard failure
|
|
1351
|
+
error_response = self._create_error_response(
|
|
1352
|
+
error=str(e),
|
|
1353
|
+
correlation_id=pre_validation_correlation_id,
|
|
1354
|
+
)
|
|
1355
|
+
await self._publish_envelope_safe(error_response, self._output_topic)
|
|
1356
|
+
logger.warning(
|
|
1357
|
+
"Unknown handler type in envelope",
|
|
1358
|
+
extra={
|
|
1359
|
+
"error": str(e),
|
|
1360
|
+
"correlation_id": str(pre_validation_correlation_id),
|
|
1361
|
+
"error_type": "UnknownHandlerTypeError",
|
|
1362
|
+
},
|
|
1363
|
+
)
|
|
1364
|
+
return
|
|
1365
|
+
|
|
1366
|
+
# After validation, correlation_id is guaranteed to be a UUID
|
|
1367
|
+
correlation_id = envelope.get("correlation_id")
|
|
1368
|
+
if not isinstance(correlation_id, UUID):
|
|
1369
|
+
correlation_id = pre_validation_correlation_id
|
|
1370
|
+
|
|
1371
|
+
# Step 2: Check idempotency before handler dispatch (OMN-945)
|
|
1372
|
+
# This prevents duplicate processing under at-least-once delivery
|
|
1373
|
+
if not await self._check_idempotency(envelope, correlation_id):
|
|
1374
|
+
# Duplicate detected - response already published, return early
|
|
1375
|
+
return
|
|
1376
|
+
|
|
1377
|
+
# Extract operation (validated to exist and be a string)
|
|
1378
|
+
operation = str(envelope.get("operation"))
|
|
1379
|
+
|
|
1380
|
+
# Determine handler_type from envelope
|
|
1381
|
+
# If handler_type not explicit, extract from operation (e.g., "http.get" -> "http")
|
|
1382
|
+
handler_type = envelope.get("handler_type")
|
|
1383
|
+
if handler_type is None:
|
|
1384
|
+
handler_type = operation.split(".")[0]
|
|
1385
|
+
|
|
1386
|
+
# Get handler from registry
|
|
1387
|
+
handler = self._handlers.get(str(handler_type))
|
|
1388
|
+
|
|
1389
|
+
if handler is None:
|
|
1390
|
+
# Handler not instantiated (different from unknown prefix - validation already passed)
|
|
1391
|
+
# This can happen if handler registration failed during start()
|
|
1392
|
+
context = ModelInfraErrorContext(
|
|
1393
|
+
transport_type=EnumInfraTransportType.RUNTIME,
|
|
1394
|
+
operation=str(operation),
|
|
1395
|
+
target_name=str(handler_type),
|
|
1396
|
+
correlation_id=correlation_id,
|
|
1397
|
+
)
|
|
1398
|
+
|
|
1399
|
+
# Create structured error for logging and tracking
|
|
1400
|
+
routing_error = RuntimeHostError(
|
|
1401
|
+
f"Handler type {handler_type!r} is registered but not instantiated",
|
|
1402
|
+
context=context,
|
|
1403
|
+
)
|
|
1404
|
+
|
|
1405
|
+
# Publish error response for envelope-based error handling
|
|
1406
|
+
error_response = self._create_error_response(
|
|
1407
|
+
error=str(routing_error),
|
|
1408
|
+
correlation_id=correlation_id,
|
|
1409
|
+
)
|
|
1410
|
+
await self._publish_envelope_safe(error_response, self._output_topic)
|
|
1411
|
+
|
|
1412
|
+
# Log with structured error
|
|
1413
|
+
logger.warning(
|
|
1414
|
+
"Handler registered but not instantiated",
|
|
1415
|
+
extra={
|
|
1416
|
+
"handler_type": handler_type,
|
|
1417
|
+
"correlation_id": str(correlation_id),
|
|
1418
|
+
"operation": operation,
|
|
1419
|
+
"registered_handlers": list(self._handlers.keys()),
|
|
1420
|
+
"error": str(routing_error),
|
|
1421
|
+
},
|
|
1422
|
+
)
|
|
1423
|
+
return
|
|
1424
|
+
|
|
1425
|
+
# Execute handler
|
|
1426
|
+
try:
|
|
1427
|
+
# Handler expected to have async execute(envelope) method
|
|
1428
|
+
# NOTE: MVP adapters use legacy execute(envelope: dict) signature.
|
|
1429
|
+
# TODO(OMN-40): Migrate handlers to new protocol signature execute(request, operation_config)
|
|
1430
|
+
response = await handler.execute(envelope) # type: ignore[call-arg] # NOTE: legacy signature
|
|
1431
|
+
|
|
1432
|
+
# Ensure response has correlation_id
|
|
1433
|
+
# Make a copy to avoid mutating handler's internal state
|
|
1434
|
+
if isinstance(response, dict):
|
|
1435
|
+
response = dict(response)
|
|
1436
|
+
if "correlation_id" not in response:
|
|
1437
|
+
response["correlation_id"] = correlation_id
|
|
1438
|
+
|
|
1439
|
+
await self._publish_envelope_safe(response, self._output_topic)
|
|
1440
|
+
|
|
1441
|
+
logger.debug(
|
|
1442
|
+
"Handler executed successfully",
|
|
1443
|
+
extra={
|
|
1444
|
+
"handler_type": handler_type,
|
|
1445
|
+
"correlation_id": str(correlation_id),
|
|
1446
|
+
"operation": operation,
|
|
1447
|
+
},
|
|
1448
|
+
)
|
|
1449
|
+
|
|
1450
|
+
except Exception as e:
|
|
1451
|
+
# Create infrastructure error context for handler execution failure
|
|
1452
|
+
context = ModelInfraErrorContext(
|
|
1453
|
+
transport_type=EnumInfraTransportType.RUNTIME,
|
|
1454
|
+
operation="handler_execution",
|
|
1455
|
+
target_name=str(handler_type),
|
|
1456
|
+
correlation_id=correlation_id,
|
|
1457
|
+
)
|
|
1458
|
+
# Chain the error with infrastructure context
|
|
1459
|
+
infra_error = RuntimeHostError(
|
|
1460
|
+
f"Handler execution failed for {handler_type}: {e}",
|
|
1461
|
+
context=context,
|
|
1462
|
+
)
|
|
1463
|
+
infra_error.__cause__ = e # Proper error chaining
|
|
1464
|
+
|
|
1465
|
+
# Handler execution failed - produce failure envelope
|
|
1466
|
+
error_response = self._create_error_response(
|
|
1467
|
+
error=str(e),
|
|
1468
|
+
correlation_id=correlation_id,
|
|
1469
|
+
)
|
|
1470
|
+
await self._publish_envelope_safe(error_response, self._output_topic)
|
|
1471
|
+
|
|
1472
|
+
logger.exception(
|
|
1473
|
+
"Handler execution failed",
|
|
1474
|
+
extra={
|
|
1475
|
+
"handler_type": handler_type,
|
|
1476
|
+
"correlation_id": str(correlation_id),
|
|
1477
|
+
"operation": operation,
|
|
1478
|
+
"error": str(e),
|
|
1479
|
+
"infra_error": str(infra_error),
|
|
1480
|
+
},
|
|
1481
|
+
)
|
|
1482
|
+
|
|
1483
|
+
def _create_error_response(
|
|
1484
|
+
self,
|
|
1485
|
+
error: str,
|
|
1486
|
+
correlation_id: UUID | None,
|
|
1487
|
+
) -> dict[str, object]:
|
|
1488
|
+
"""Create a standardized error response envelope.
|
|
1489
|
+
|
|
1490
|
+
Args:
|
|
1491
|
+
error: Error message to include.
|
|
1492
|
+
correlation_id: Correlation ID to preserve for tracking.
|
|
1493
|
+
|
|
1494
|
+
Returns:
|
|
1495
|
+
Error response dict with success=False and error details.
|
|
1496
|
+
"""
|
|
1497
|
+
# Use correlation_id or generate a new one, keeping as UUID for internal use
|
|
1498
|
+
final_correlation_id = correlation_id or uuid4()
|
|
1499
|
+
return {
|
|
1500
|
+
"success": False,
|
|
1501
|
+
"status": "error",
|
|
1502
|
+
"error": error,
|
|
1503
|
+
"correlation_id": final_correlation_id,
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
def _serialize_envelope(
|
|
1507
|
+
self, envelope: dict[str, object] | BaseModel
|
|
1508
|
+
) -> dict[str, object]:
|
|
1509
|
+
"""Recursively convert UUID objects to strings for JSON serialization.
|
|
1510
|
+
|
|
1511
|
+
Handles both dict envelopes and Pydantic models (e.g., ModelDuplicateResponse).
|
|
1512
|
+
|
|
1513
|
+
Args:
|
|
1514
|
+
envelope: Envelope dict or Pydantic model that may contain UUID objects.
|
|
1515
|
+
|
|
1516
|
+
Returns:
|
|
1517
|
+
New dict with all UUIDs converted to strings.
|
|
1518
|
+
"""
|
|
1519
|
+
# Convert Pydantic models to dict first, ensuring type safety
|
|
1520
|
+
envelope_dict: JsonDict = (
|
|
1521
|
+
envelope.model_dump() if isinstance(envelope, BaseModel) else envelope
|
|
1522
|
+
)
|
|
1523
|
+
|
|
1524
|
+
def convert_value(value: object) -> object:
|
|
1525
|
+
if isinstance(value, UUID):
|
|
1526
|
+
return str(value)
|
|
1527
|
+
elif isinstance(value, dict):
|
|
1528
|
+
return {k: convert_value(v) for k, v in value.items()}
|
|
1529
|
+
elif isinstance(value, list):
|
|
1530
|
+
return [convert_value(item) for item in value]
|
|
1531
|
+
return value
|
|
1532
|
+
|
|
1533
|
+
return {k: convert_value(v) for k, v in envelope_dict.items()}
|
|
1534
|
+
|
|
1535
|
+
async def _publish_envelope_safe(
|
|
1536
|
+
self, envelope: dict[str, object] | BaseModel, topic: str
|
|
1537
|
+
) -> None:
|
|
1538
|
+
"""Publish envelope with UUID serialization support.
|
|
1539
|
+
|
|
1540
|
+
Converts any UUID objects to strings before publishing to ensure
|
|
1541
|
+
JSON serialization works correctly.
|
|
1542
|
+
|
|
1543
|
+
Args:
|
|
1544
|
+
envelope: Envelope dict or Pydantic model (may contain UUID objects).
|
|
1545
|
+
topic: Target topic to publish to.
|
|
1546
|
+
"""
|
|
1547
|
+
# Always serialize UUIDs upfront - single code path
|
|
1548
|
+
json_safe_envelope = self._serialize_envelope(envelope)
|
|
1549
|
+
await self._event_bus.publish_envelope(json_safe_envelope, topic)
|
|
1550
|
+
|
|
1551
|
+
async def health_check(self) -> dict[str, object]:
|
|
1552
|
+
"""Return health check status.
|
|
1553
|
+
|
|
1554
|
+
Returns:
|
|
1555
|
+
Dictionary with health status information:
|
|
1556
|
+
- healthy: Overall health status (True only if running,
|
|
1557
|
+
event bus healthy, no handlers failed to instantiate,
|
|
1558
|
+
all registered handlers are healthy, AND at least one
|
|
1559
|
+
handler is registered - a runtime without handlers is useless)
|
|
1560
|
+
- degraded: True when process is running but some handlers
|
|
1561
|
+
failed to instantiate. Indicates partial functionality -
|
|
1562
|
+
the system is operational but not at full capacity.
|
|
1563
|
+
- is_running: Whether the process is running
|
|
1564
|
+
- is_draining: Whether the process is in graceful shutdown drain
|
|
1565
|
+
period, waiting for in-flight messages to complete (OMN-756).
|
|
1566
|
+
Load balancers can use this to remove the service from rotation
|
|
1567
|
+
before the container becomes unhealthy.
|
|
1568
|
+
- pending_message_count: Number of messages currently being
|
|
1569
|
+
processed. Useful for monitoring drain progress and determining
|
|
1570
|
+
when the service is ready for shutdown.
|
|
1571
|
+
- event_bus: Event bus health status (if running)
|
|
1572
|
+
- event_bus_healthy: Boolean indicating event bus health
|
|
1573
|
+
- failed_handlers: Dict of handler_type -> error message for
|
|
1574
|
+
handlers that failed to instantiate during start()
|
|
1575
|
+
- registered_handlers: List of successfully registered handler types
|
|
1576
|
+
- handlers: Dict of handler_type -> health status for each
|
|
1577
|
+
registered handler
|
|
1578
|
+
- no_handlers_registered: True if no handlers are registered.
|
|
1579
|
+
This indicates a critical configuration issue - the runtime
|
|
1580
|
+
cannot process any events without handlers (OMN-1317).
|
|
1581
|
+
|
|
1582
|
+
Health State Matrix:
|
|
1583
|
+
- healthy=True, degraded=False: Fully operational
|
|
1584
|
+
- healthy=False, degraded=True: Running with reduced functionality
|
|
1585
|
+
- healthy=False, degraded=False: Not running, event bus unhealthy,
|
|
1586
|
+
or no handlers registered (critical configuration issue)
|
|
1587
|
+
- healthy=False, no_handlers_registered=True: Configuration error,
|
|
1588
|
+
runtime cannot process events
|
|
1589
|
+
|
|
1590
|
+
Drain State:
|
|
1591
|
+
When is_draining=True, the service is shutting down gracefully:
|
|
1592
|
+
- New messages are no longer being accepted
|
|
1593
|
+
- In-flight messages are being allowed to complete
|
|
1594
|
+
- Health status may still show healthy during drain
|
|
1595
|
+
- Load balancers should remove the service from rotation
|
|
1596
|
+
|
|
1597
|
+
Note:
|
|
1598
|
+
Handler health checks are performed concurrently using asyncio.gather()
|
|
1599
|
+
with individual timeouts (configurable via health_check_timeout_seconds
|
|
1600
|
+
config, default: 5.0 seconds) to prevent slow handlers from blocking.
|
|
1601
|
+
"""
|
|
1602
|
+
# Get event bus health if available
|
|
1603
|
+
event_bus_health: dict[str, object] = {}
|
|
1604
|
+
event_bus_healthy = False
|
|
1605
|
+
|
|
1606
|
+
try:
|
|
1607
|
+
event_bus_health = await self._event_bus.health_check()
|
|
1608
|
+
# Explicit type guard (not assert) for production safety
|
|
1609
|
+
# health_check() returns dict per contract
|
|
1610
|
+
if not isinstance(event_bus_health, dict):
|
|
1611
|
+
context = ModelInfraErrorContext(
|
|
1612
|
+
transport_type=EnumInfraTransportType.RUNTIME,
|
|
1613
|
+
operation="health_check",
|
|
1614
|
+
)
|
|
1615
|
+
raise ProtocolConfigurationError(
|
|
1616
|
+
f"health_check() must return dict, got {type(event_bus_health).__name__}",
|
|
1617
|
+
context=context,
|
|
1618
|
+
)
|
|
1619
|
+
event_bus_healthy = bool(event_bus_health.get("healthy", False))
|
|
1620
|
+
except Exception as e:
|
|
1621
|
+
# Create infrastructure error context for health check failure
|
|
1622
|
+
correlation_id = uuid4()
|
|
1623
|
+
context = ModelInfraErrorContext(
|
|
1624
|
+
transport_type=EnumInfraTransportType.RUNTIME,
|
|
1625
|
+
operation="health_check",
|
|
1626
|
+
target_name="event_bus",
|
|
1627
|
+
correlation_id=correlation_id,
|
|
1628
|
+
)
|
|
1629
|
+
# Chain the error with infrastructure context
|
|
1630
|
+
infra_error = RuntimeHostError(
|
|
1631
|
+
f"Event bus health check failed: {e}",
|
|
1632
|
+
context=context,
|
|
1633
|
+
)
|
|
1634
|
+
infra_error.__cause__ = e # Proper error chaining
|
|
1635
|
+
|
|
1636
|
+
logger.warning(
|
|
1637
|
+
"Event bus health check failed",
|
|
1638
|
+
extra={
|
|
1639
|
+
"error": str(e),
|
|
1640
|
+
"correlation_id": str(correlation_id),
|
|
1641
|
+
"infra_error": str(infra_error),
|
|
1642
|
+
},
|
|
1643
|
+
exc_info=True,
|
|
1644
|
+
)
|
|
1645
|
+
event_bus_health = {"error": str(e), "correlation_id": str(correlation_id)}
|
|
1646
|
+
event_bus_healthy = False
|
|
1647
|
+
|
|
1648
|
+
# Check handler health for all registered handlers concurrently
|
|
1649
|
+
# Delegates to ProtocolLifecycleExecutor with configured timeout to prevent blocking
|
|
1650
|
+
handler_health_results: dict[str, object] = {}
|
|
1651
|
+
handlers_all_healthy = True
|
|
1652
|
+
|
|
1653
|
+
if self._handlers:
|
|
1654
|
+
# Run all handler health checks concurrently using asyncio.gather()
|
|
1655
|
+
health_check_tasks = [
|
|
1656
|
+
self._lifecycle_executor.check_handler_health(handler_type, handler)
|
|
1657
|
+
for handler_type, handler in self._handlers.items()
|
|
1658
|
+
]
|
|
1659
|
+
results = await asyncio.gather(*health_check_tasks)
|
|
1660
|
+
|
|
1661
|
+
# Process results and build the results dict
|
|
1662
|
+
for health_result in results:
|
|
1663
|
+
handler_health_results[health_result.handler_type] = (
|
|
1664
|
+
health_result.details
|
|
1665
|
+
)
|
|
1666
|
+
if not health_result.healthy:
|
|
1667
|
+
handlers_all_healthy = False
|
|
1668
|
+
|
|
1669
|
+
# Check for failed handlers - any failures indicate degraded state
|
|
1670
|
+
has_failed_handlers = len(self._failed_handlers) > 0
|
|
1671
|
+
|
|
1672
|
+
# Check for no handlers registered - critical configuration issue
|
|
1673
|
+
# A runtime with no handlers cannot process any events and should be unhealthy
|
|
1674
|
+
no_handlers_registered = len(self._handlers) == 0
|
|
1675
|
+
|
|
1676
|
+
# Degraded state: process is running but some handlers failed to instantiate
|
|
1677
|
+
# This means the system is operational but with reduced functionality
|
|
1678
|
+
degraded = self._is_running and has_failed_handlers
|
|
1679
|
+
|
|
1680
|
+
# Overall health is True only if running, event bus is healthy,
|
|
1681
|
+
# no handlers failed to instantiate, all registered handlers are healthy,
|
|
1682
|
+
# AND at least one handler is registered (runtime without handlers is useless)
|
|
1683
|
+
healthy = (
|
|
1684
|
+
self._is_running
|
|
1685
|
+
and event_bus_healthy
|
|
1686
|
+
and not has_failed_handlers
|
|
1687
|
+
and handlers_all_healthy
|
|
1688
|
+
and not no_handlers_registered
|
|
1689
|
+
)
|
|
1690
|
+
|
|
1691
|
+
return {
|
|
1692
|
+
"healthy": healthy,
|
|
1693
|
+
"degraded": degraded,
|
|
1694
|
+
"is_running": self._is_running,
|
|
1695
|
+
"is_draining": self._is_draining,
|
|
1696
|
+
"pending_message_count": self._pending_message_count,
|
|
1697
|
+
"event_bus": event_bus_health,
|
|
1698
|
+
"event_bus_healthy": event_bus_healthy,
|
|
1699
|
+
"failed_handlers": self._failed_handlers,
|
|
1700
|
+
"registered_handlers": list(self._handlers.keys()),
|
|
1701
|
+
"handlers": handler_health_results,
|
|
1702
|
+
"no_handlers_registered": no_handlers_registered,
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
def register_handler(self, handler_type: str, handler: ProtocolHandler) -> None:
|
|
1706
|
+
"""Register a handler for a specific type.
|
|
1707
|
+
|
|
1708
|
+
Args:
|
|
1709
|
+
handler_type: Protocol type identifier (e.g., "http", "db").
|
|
1710
|
+
handler: Handler instance implementing the ProtocolHandler protocol.
|
|
1711
|
+
"""
|
|
1712
|
+
self._handlers[handler_type] = handler
|
|
1713
|
+
logger.debug(
|
|
1714
|
+
"Handler registered",
|
|
1715
|
+
extra={
|
|
1716
|
+
"handler_type": handler_type,
|
|
1717
|
+
"handler_class": type(handler).__name__,
|
|
1718
|
+
},
|
|
1719
|
+
)
|
|
1720
|
+
|
|
1721
|
+
def get_handler(self, handler_type: str) -> ProtocolHandler | None:
|
|
1722
|
+
"""Get handler for type, returns None if not registered.
|
|
1723
|
+
|
|
1724
|
+
Args:
|
|
1725
|
+
handler_type: Protocol type identifier.
|
|
1726
|
+
|
|
1727
|
+
Returns:
|
|
1728
|
+
Handler instance if registered, None otherwise.
|
|
1729
|
+
"""
|
|
1730
|
+
return self._handlers.get(handler_type)
|
|
1731
|
+
|
|
1732
|
+
# =========================================================================
|
|
1733
|
+
# Architecture Validation Methods (OMN-1138)
|
|
1734
|
+
# =========================================================================
|
|
1735
|
+
|
|
1736
|
+
async def _validate_architecture(self) -> None:
|
|
1737
|
+
"""Validate architecture compliance before starting runtime.
|
|
1738
|
+
|
|
1739
|
+
This method is called at the beginning of start() to validate nodes
|
|
1740
|
+
and handlers against registered architecture rules. If any violations
|
|
1741
|
+
with ERROR severity are detected, startup is blocked.
|
|
1742
|
+
|
|
1743
|
+
Validation occurs BEFORE:
|
|
1744
|
+
- Event bus starts
|
|
1745
|
+
- Handlers are wired
|
|
1746
|
+
- Subscription begins
|
|
1747
|
+
|
|
1748
|
+
Validation Behavior:
|
|
1749
|
+
- ERROR severity violations: Block startup, raise ArchitectureViolationError
|
|
1750
|
+
- WARNING severity violations: Log warning, continue startup
|
|
1751
|
+
- INFO severity violations: Log info, continue startup
|
|
1752
|
+
|
|
1753
|
+
Raises:
|
|
1754
|
+
ArchitectureViolationError: If blocking violations (ERROR severity)
|
|
1755
|
+
are detected. Contains all blocking violations for inspection.
|
|
1756
|
+
|
|
1757
|
+
Example:
|
|
1758
|
+
>>> # Validation is automatic in start()
|
|
1759
|
+
>>> try:
|
|
1760
|
+
... await runtime.start()
|
|
1761
|
+
... except ArchitectureViolationError as e:
|
|
1762
|
+
... print(f"Startup blocked: {len(e.violations)} violations")
|
|
1763
|
+
... for v in e.violations:
|
|
1764
|
+
... print(v.format_for_logging())
|
|
1765
|
+
|
|
1766
|
+
Note:
|
|
1767
|
+
Validation only runs if architecture_rules were provided at init.
|
|
1768
|
+
If no rules are configured, this method returns immediately.
|
|
1769
|
+
|
|
1770
|
+
Related:
|
|
1771
|
+
- OMN-1138: Architecture Validator for omnibase_infra
|
|
1772
|
+
- OMN-1099: Validators implementing ProtocolArchitectureRule
|
|
1773
|
+
"""
|
|
1774
|
+
# Skip validation if no rules configured
|
|
1775
|
+
if not self._architecture_rules:
|
|
1776
|
+
logger.debug("No architecture rules configured, skipping validation")
|
|
1777
|
+
return
|
|
1778
|
+
|
|
1779
|
+
logger.info(
|
|
1780
|
+
"Validating architecture compliance",
|
|
1781
|
+
extra={
|
|
1782
|
+
"rule_count": len(self._architecture_rules),
|
|
1783
|
+
"rule_ids": tuple(r.rule_id for r in self._architecture_rules),
|
|
1784
|
+
},
|
|
1785
|
+
)
|
|
1786
|
+
|
|
1787
|
+
# Import architecture validator components
|
|
1788
|
+
from omnibase_infra.errors import ArchitectureViolationError
|
|
1789
|
+
from omnibase_infra.nodes.architecture_validator import (
|
|
1790
|
+
ModelArchitectureValidationRequest,
|
|
1791
|
+
NodeArchitectureValidatorCompute,
|
|
1792
|
+
)
|
|
1793
|
+
|
|
1794
|
+
# Create or get container
|
|
1795
|
+
container = self._get_or_create_container()
|
|
1796
|
+
|
|
1797
|
+
# Instantiate validator with rules
|
|
1798
|
+
validator = NodeArchitectureValidatorCompute(
|
|
1799
|
+
container=container,
|
|
1800
|
+
rules=self._architecture_rules,
|
|
1801
|
+
)
|
|
1802
|
+
|
|
1803
|
+
# Build validation request
|
|
1804
|
+
# Note: At this point, handlers haven't been instantiated yet (that happens
|
|
1805
|
+
# after validation in _populate_handlers_from_registry). We validate the
|
|
1806
|
+
# handler CLASSES from the registry, not handler instances.
|
|
1807
|
+
handler_registry = await self._get_handler_registry()
|
|
1808
|
+
handler_classes: list[type[ProtocolHandler]] = []
|
|
1809
|
+
for handler_type in handler_registry.list_protocols():
|
|
1810
|
+
try:
|
|
1811
|
+
handler_cls = handler_registry.get(handler_type)
|
|
1812
|
+
handler_classes.append(handler_cls)
|
|
1813
|
+
except Exception as e:
|
|
1814
|
+
# If a handler class can't be retrieved, skip it for validation
|
|
1815
|
+
# (it will fail later during instantiation anyway)
|
|
1816
|
+
logger.debug(
|
|
1817
|
+
"Skipping handler class for architecture validation",
|
|
1818
|
+
extra={
|
|
1819
|
+
"handler_type": handler_type,
|
|
1820
|
+
"error": str(e),
|
|
1821
|
+
},
|
|
1822
|
+
)
|
|
1823
|
+
|
|
1824
|
+
request = ModelArchitectureValidationRequest(
|
|
1825
|
+
nodes=(), # Nodes not yet available at this point
|
|
1826
|
+
handlers=tuple(handler_classes),
|
|
1827
|
+
)
|
|
1828
|
+
|
|
1829
|
+
# Execute validation
|
|
1830
|
+
result = validator.compute(request)
|
|
1831
|
+
|
|
1832
|
+
# Separate blocking and non-blocking violations
|
|
1833
|
+
blocking_violations = tuple(v for v in result.violations if v.blocks_startup())
|
|
1834
|
+
warning_violations = tuple(
|
|
1835
|
+
v for v in result.violations if not v.blocks_startup()
|
|
1836
|
+
)
|
|
1837
|
+
|
|
1838
|
+
# Log warnings but don't block
|
|
1839
|
+
for violation in warning_violations:
|
|
1840
|
+
# Note: We can't use to_structured_dict() directly because 'message'
|
|
1841
|
+
# is a reserved key in Python logging's extra parameter.
|
|
1842
|
+
# We use format_for_logging() instead for the log message.
|
|
1843
|
+
logger.warning(
|
|
1844
|
+
"Architecture warning: %s",
|
|
1845
|
+
violation.format_for_logging(),
|
|
1846
|
+
extra={
|
|
1847
|
+
"rule_id": violation.rule_id,
|
|
1848
|
+
"severity": violation.severity.value,
|
|
1849
|
+
"target_type": violation.target_type,
|
|
1850
|
+
"target_name": violation.target_name,
|
|
1851
|
+
},
|
|
1852
|
+
)
|
|
1853
|
+
|
|
1854
|
+
# Block startup on ERROR violations
|
|
1855
|
+
if blocking_violations:
|
|
1856
|
+
logger.error(
|
|
1857
|
+
"Architecture validation failed",
|
|
1858
|
+
extra={
|
|
1859
|
+
"blocking_violation_count": len(blocking_violations),
|
|
1860
|
+
"warning_violation_count": len(warning_violations),
|
|
1861
|
+
"blocking_rule_ids": tuple(v.rule_id for v in blocking_violations),
|
|
1862
|
+
},
|
|
1863
|
+
)
|
|
1864
|
+
raise ArchitectureViolationError(
|
|
1865
|
+
message=f"Architecture validation failed with {len(blocking_violations)} blocking violations",
|
|
1866
|
+
violations=blocking_violations,
|
|
1867
|
+
)
|
|
1868
|
+
|
|
1869
|
+
logger.info(
|
|
1870
|
+
"Architecture validation passed",
|
|
1871
|
+
extra={
|
|
1872
|
+
"rules_checked": result.rules_checked,
|
|
1873
|
+
"handlers_checked": result.handlers_checked,
|
|
1874
|
+
"warning_count": len(warning_violations),
|
|
1875
|
+
},
|
|
1876
|
+
)
|
|
1877
|
+
|
|
1878
|
+
def _get_or_create_container(self) -> ModelONEXContainer:
|
|
1879
|
+
"""Get the injected container or create a new one.
|
|
1880
|
+
|
|
1881
|
+
Returns:
|
|
1882
|
+
ModelONEXContainer instance for architecture validation.
|
|
1883
|
+
|
|
1884
|
+
Note:
|
|
1885
|
+
If no container was provided at init, a new container is created.
|
|
1886
|
+
This container provides basic infrastructure for node execution
|
|
1887
|
+
but may not have all services wired.
|
|
1888
|
+
"""
|
|
1889
|
+
if self._container is not None:
|
|
1890
|
+
return self._container
|
|
1891
|
+
|
|
1892
|
+
# Create container for validation
|
|
1893
|
+
from omnibase_core.models.container.model_onex_container import (
|
|
1894
|
+
ModelONEXContainer,
|
|
1895
|
+
)
|
|
1896
|
+
|
|
1897
|
+
logger.debug(
|
|
1898
|
+
"Creating container for architecture validation "
|
|
1899
|
+
"(no container provided at init)"
|
|
1900
|
+
)
|
|
1901
|
+
return ModelONEXContainer()
|
|
1902
|
+
|
|
1903
|
+
# =========================================================================
|
|
1904
|
+
# Idempotency Guard Methods (OMN-945)
|
|
1905
|
+
# =========================================================================
|
|
1906
|
+
|
|
1907
|
+
async def _initialize_idempotency_store(self) -> None:
|
|
1908
|
+
"""Initialize idempotency store from configuration.
|
|
1909
|
+
|
|
1910
|
+
Reads idempotency configuration from the runtime config and wires
|
|
1911
|
+
the appropriate store implementation. If not configured or disabled,
|
|
1912
|
+
idempotency checking is skipped.
|
|
1913
|
+
|
|
1914
|
+
Supported store types:
|
|
1915
|
+
- "postgres": PostgreSQL-backed durable store (production)
|
|
1916
|
+
- "memory": In-memory store (testing only)
|
|
1917
|
+
|
|
1918
|
+
Configuration keys:
|
|
1919
|
+
- idempotency.enabled: bool (default: False)
|
|
1920
|
+
- idempotency.store_type: "postgres" | "memory" (default: "postgres")
|
|
1921
|
+
- idempotency.domain_from_operation: bool (default: True)
|
|
1922
|
+
- idempotency.skip_operations: list[str] (default: [])
|
|
1923
|
+
- idempotency_database: dict (PostgreSQL connection config)
|
|
1924
|
+
"""
|
|
1925
|
+
# Check if config exists
|
|
1926
|
+
if self._config is None:
|
|
1927
|
+
logger.debug("No runtime config provided, skipping idempotency setup")
|
|
1928
|
+
return
|
|
1929
|
+
|
|
1930
|
+
# Check if config has idempotency section
|
|
1931
|
+
idempotency_raw = self._config.get("idempotency")
|
|
1932
|
+
if idempotency_raw is None:
|
|
1933
|
+
logger.debug("Idempotency guard not configured, skipping")
|
|
1934
|
+
return
|
|
1935
|
+
|
|
1936
|
+
try:
|
|
1937
|
+
from omnibase_infra.idempotency import ModelIdempotencyGuardConfig
|
|
1938
|
+
|
|
1939
|
+
if isinstance(idempotency_raw, dict):
|
|
1940
|
+
self._idempotency_config = ModelIdempotencyGuardConfig.model_validate(
|
|
1941
|
+
idempotency_raw
|
|
1942
|
+
)
|
|
1943
|
+
elif isinstance(idempotency_raw, ModelIdempotencyGuardConfig):
|
|
1944
|
+
self._idempotency_config = idempotency_raw
|
|
1945
|
+
else:
|
|
1946
|
+
logger.warning(
|
|
1947
|
+
"Invalid idempotency config type",
|
|
1948
|
+
extra={"type": type(idempotency_raw).__name__},
|
|
1949
|
+
)
|
|
1950
|
+
return
|
|
1951
|
+
|
|
1952
|
+
if not self._idempotency_config.enabled:
|
|
1953
|
+
logger.debug("Idempotency guard disabled in config")
|
|
1954
|
+
return
|
|
1955
|
+
|
|
1956
|
+
# Create store based on store_type
|
|
1957
|
+
if self._idempotency_config.store_type == "postgres":
|
|
1958
|
+
from omnibase_infra.idempotency import (
|
|
1959
|
+
ModelPostgresIdempotencyStoreConfig,
|
|
1960
|
+
StoreIdempotencyPostgres,
|
|
1961
|
+
)
|
|
1962
|
+
|
|
1963
|
+
# Get database config from container or config
|
|
1964
|
+
db_config_raw = self._config.get("idempotency_database", {})
|
|
1965
|
+
if isinstance(db_config_raw, dict):
|
|
1966
|
+
db_config = ModelPostgresIdempotencyStoreConfig.model_validate(
|
|
1967
|
+
db_config_raw
|
|
1968
|
+
)
|
|
1969
|
+
elif isinstance(db_config_raw, ModelPostgresIdempotencyStoreConfig):
|
|
1970
|
+
db_config = db_config_raw
|
|
1971
|
+
else:
|
|
1972
|
+
logger.warning(
|
|
1973
|
+
"Invalid idempotency_database config type",
|
|
1974
|
+
extra={"type": type(db_config_raw).__name__},
|
|
1975
|
+
)
|
|
1976
|
+
return
|
|
1977
|
+
|
|
1978
|
+
self._idempotency_store = StoreIdempotencyPostgres(config=db_config)
|
|
1979
|
+
await self._idempotency_store.initialize()
|
|
1980
|
+
|
|
1981
|
+
elif self._idempotency_config.store_type == "memory":
|
|
1982
|
+
from omnibase_infra.idempotency import StoreIdempotencyInmemory
|
|
1983
|
+
|
|
1984
|
+
self._idempotency_store = StoreIdempotencyInmemory()
|
|
1985
|
+
|
|
1986
|
+
else:
|
|
1987
|
+
logger.warning(
|
|
1988
|
+
"Unknown idempotency store type",
|
|
1989
|
+
extra={"store_type": self._idempotency_config.store_type},
|
|
1990
|
+
)
|
|
1991
|
+
return
|
|
1992
|
+
|
|
1993
|
+
logger.info(
|
|
1994
|
+
"Idempotency guard initialized",
|
|
1995
|
+
extra={
|
|
1996
|
+
"store_type": self._idempotency_config.store_type,
|
|
1997
|
+
"domain_from_operation": self._idempotency_config.domain_from_operation,
|
|
1998
|
+
"skip_operations": self._idempotency_config.skip_operations,
|
|
1999
|
+
},
|
|
2000
|
+
)
|
|
2001
|
+
|
|
2002
|
+
except Exception as e:
|
|
2003
|
+
logger.warning(
|
|
2004
|
+
"Failed to initialize idempotency store, proceeding without",
|
|
2005
|
+
extra={"error": str(e)},
|
|
2006
|
+
)
|
|
2007
|
+
self._idempotency_store = None
|
|
2008
|
+
self._idempotency_config = None
|
|
2009
|
+
|
|
2010
|
+
# =========================================================================
|
|
2011
|
+
# WARNING: FAIL-OPEN BEHAVIOR
|
|
2012
|
+
# =========================================================================
|
|
2013
|
+
# This method implements FAIL-OPEN semantics: if the idempotency store
|
|
2014
|
+
# is unavailable or errors, messages are ALLOWED THROUGH for processing.
|
|
2015
|
+
#
|
|
2016
|
+
# This is an intentional design decision prioritizing availability over
|
|
2017
|
+
# exactly-once guarantees. See docstring below for full trade-off analysis.
|
|
2018
|
+
#
|
|
2019
|
+
# IMPORTANT: Downstream handlers MUST be designed for at-least-once delivery
|
|
2020
|
+
# and implement their own idempotency for critical operations.
|
|
2021
|
+
# =========================================================================
|
|
2022
|
+
async def _check_idempotency(
|
|
2023
|
+
self,
|
|
2024
|
+
envelope: dict[str, object],
|
|
2025
|
+
correlation_id: UUID,
|
|
2026
|
+
) -> bool:
|
|
2027
|
+
"""Check if envelope should be processed (idempotency guard).
|
|
2028
|
+
|
|
2029
|
+
Extracts message_id from envelope headers and checks against the
|
|
2030
|
+
idempotency store. If duplicate detected, publishes a duplicate
|
|
2031
|
+
response and returns False.
|
|
2032
|
+
|
|
2033
|
+
Fail-Open Semantics:
|
|
2034
|
+
This method implements **fail-open** error handling: if the
|
|
2035
|
+
idempotency store is unavailable or throws an error, the message
|
|
2036
|
+
is allowed through for processing (with a warning log).
|
|
2037
|
+
|
|
2038
|
+
**Design Rationale**: In distributed event-driven systems, the
|
|
2039
|
+
idempotency store (e.g., Redis/Valkey) is a supporting service,
|
|
2040
|
+
not a critical path dependency. A temporary store outage should
|
|
2041
|
+
not halt message processing entirely, as this would cascade into
|
|
2042
|
+
broader system unavailability.
|
|
2043
|
+
|
|
2044
|
+
**Trade-offs**:
|
|
2045
|
+
- Pro: High availability - processing continues during store outages
|
|
2046
|
+
- Pro: Graceful degradation - system remains functional
|
|
2047
|
+
- Con: May result in duplicate message processing during outages
|
|
2048
|
+
- Con: Downstream handlers must be designed for at-least-once delivery
|
|
2049
|
+
|
|
2050
|
+
**Mitigation**: Handlers consuming messages should implement their
|
|
2051
|
+
own idempotency logic for critical operations (e.g., using database
|
|
2052
|
+
constraints or transaction guards) to ensure correctness even when
|
|
2053
|
+
duplicates slip through.
|
|
2054
|
+
|
|
2055
|
+
Args:
|
|
2056
|
+
envelope: Validated envelope dict.
|
|
2057
|
+
correlation_id: Normalized correlation ID (UUID).
|
|
2058
|
+
|
|
2059
|
+
Returns:
|
|
2060
|
+
True if message should be processed (new message).
|
|
2061
|
+
False if message is duplicate (skip processing).
|
|
2062
|
+
"""
|
|
2063
|
+
# Skip check if idempotency not configured
|
|
2064
|
+
if self._idempotency_store is None or self._idempotency_config is None:
|
|
2065
|
+
return True
|
|
2066
|
+
|
|
2067
|
+
if not self._idempotency_config.enabled:
|
|
2068
|
+
return True
|
|
2069
|
+
|
|
2070
|
+
# Check if operation is in skip list
|
|
2071
|
+
operation = envelope.get("operation")
|
|
2072
|
+
if isinstance(operation, str):
|
|
2073
|
+
if not self._idempotency_config.should_check_idempotency(operation):
|
|
2074
|
+
logger.debug(
|
|
2075
|
+
"Skipping idempotency check for operation",
|
|
2076
|
+
extra={
|
|
2077
|
+
"operation": operation,
|
|
2078
|
+
"correlation_id": str(correlation_id),
|
|
2079
|
+
},
|
|
2080
|
+
)
|
|
2081
|
+
return True
|
|
2082
|
+
|
|
2083
|
+
# Extract message_id from envelope
|
|
2084
|
+
message_id = self._extract_message_id(envelope, correlation_id)
|
|
2085
|
+
|
|
2086
|
+
# Extract domain from operation if configured
|
|
2087
|
+
domain = self._extract_idempotency_domain(envelope)
|
|
2088
|
+
|
|
2089
|
+
# Check and record in store
|
|
2090
|
+
try:
|
|
2091
|
+
is_new = await self._idempotency_store.check_and_record(
|
|
2092
|
+
message_id=message_id,
|
|
2093
|
+
domain=domain,
|
|
2094
|
+
correlation_id=correlation_id,
|
|
2095
|
+
)
|
|
2096
|
+
|
|
2097
|
+
if not is_new:
|
|
2098
|
+
# Duplicate detected - publish duplicate response (NOT an error)
|
|
2099
|
+
logger.info(
|
|
2100
|
+
"Duplicate message detected, skipping processing",
|
|
2101
|
+
extra={
|
|
2102
|
+
"message_id": str(message_id),
|
|
2103
|
+
"domain": domain,
|
|
2104
|
+
"correlation_id": str(correlation_id),
|
|
2105
|
+
},
|
|
2106
|
+
)
|
|
2107
|
+
|
|
2108
|
+
duplicate_response = self._create_duplicate_response(
|
|
2109
|
+
message_id=message_id,
|
|
2110
|
+
correlation_id=correlation_id,
|
|
2111
|
+
)
|
|
2112
|
+
# duplicate_response is already a dict from _create_duplicate_response
|
|
2113
|
+
await self._publish_envelope_safe(
|
|
2114
|
+
duplicate_response, self._output_topic
|
|
2115
|
+
)
|
|
2116
|
+
return False
|
|
2117
|
+
|
|
2118
|
+
return True
|
|
2119
|
+
|
|
2120
|
+
except Exception as e:
|
|
2121
|
+
# FAIL-OPEN: Allow message through on idempotency store errors.
|
|
2122
|
+
# Rationale: Availability over exactly-once. Store outages should not
|
|
2123
|
+
# halt processing. Downstream handlers must tolerate duplicates.
|
|
2124
|
+
# See docstring for full trade-off analysis.
|
|
2125
|
+
logger.warning(
|
|
2126
|
+
"Idempotency check failed, allowing message through (fail-open)",
|
|
2127
|
+
extra={
|
|
2128
|
+
"error": str(e),
|
|
2129
|
+
"error_type": type(e).__name__,
|
|
2130
|
+
"message_id": str(message_id),
|
|
2131
|
+
"domain": domain,
|
|
2132
|
+
"correlation_id": str(correlation_id),
|
|
2133
|
+
},
|
|
2134
|
+
)
|
|
2135
|
+
return True
|
|
2136
|
+
|
|
2137
|
+
def _extract_message_id(
|
|
2138
|
+
self,
|
|
2139
|
+
envelope: dict[str, object],
|
|
2140
|
+
correlation_id: UUID,
|
|
2141
|
+
) -> UUID:
|
|
2142
|
+
"""Extract message_id from envelope, falling back to correlation_id.
|
|
2143
|
+
|
|
2144
|
+
Priority:
|
|
2145
|
+
1. envelope["headers"]["message_id"]
|
|
2146
|
+
2. envelope["message_id"]
|
|
2147
|
+
3. Use correlation_id as message_id (fallback)
|
|
2148
|
+
|
|
2149
|
+
Args:
|
|
2150
|
+
envelope: Envelope dict to extract message_id from.
|
|
2151
|
+
correlation_id: Fallback UUID if message_id not found.
|
|
2152
|
+
|
|
2153
|
+
Returns:
|
|
2154
|
+
UUID representing the message_id.
|
|
2155
|
+
"""
|
|
2156
|
+
# Try headers first
|
|
2157
|
+
headers = envelope.get("headers")
|
|
2158
|
+
if isinstance(headers, dict):
|
|
2159
|
+
header_msg_id = headers.get("message_id")
|
|
2160
|
+
if header_msg_id is not None:
|
|
2161
|
+
if isinstance(header_msg_id, UUID):
|
|
2162
|
+
return header_msg_id
|
|
2163
|
+
if isinstance(header_msg_id, str):
|
|
2164
|
+
try:
|
|
2165
|
+
return UUID(header_msg_id)
|
|
2166
|
+
except ValueError:
|
|
2167
|
+
pass
|
|
2168
|
+
|
|
2169
|
+
# Try top-level message_id
|
|
2170
|
+
top_level_msg_id = envelope.get("message_id")
|
|
2171
|
+
if top_level_msg_id is not None:
|
|
2172
|
+
if isinstance(top_level_msg_id, UUID):
|
|
2173
|
+
return top_level_msg_id
|
|
2174
|
+
if isinstance(top_level_msg_id, str):
|
|
2175
|
+
try:
|
|
2176
|
+
return UUID(top_level_msg_id)
|
|
2177
|
+
except ValueError:
|
|
2178
|
+
pass
|
|
2179
|
+
|
|
2180
|
+
# Fallback: use correlation_id as message_id
|
|
2181
|
+
return correlation_id
|
|
2182
|
+
|
|
2183
|
+
def _extract_idempotency_domain(
|
|
2184
|
+
self,
|
|
2185
|
+
envelope: dict[str, object],
|
|
2186
|
+
) -> str | None:
|
|
2187
|
+
"""Extract domain for idempotency key from envelope.
|
|
2188
|
+
|
|
2189
|
+
If domain_from_operation is enabled in config, extracts domain
|
|
2190
|
+
from the operation prefix (e.g., "db.query" -> "db").
|
|
2191
|
+
|
|
2192
|
+
Args:
|
|
2193
|
+
envelope: Envelope dict to extract domain from.
|
|
2194
|
+
|
|
2195
|
+
Returns:
|
|
2196
|
+
Domain string if found and configured, None otherwise.
|
|
2197
|
+
"""
|
|
2198
|
+
if self._idempotency_config is None:
|
|
2199
|
+
return None
|
|
2200
|
+
|
|
2201
|
+
if not self._idempotency_config.domain_from_operation:
|
|
2202
|
+
return None
|
|
2203
|
+
|
|
2204
|
+
operation = envelope.get("operation")
|
|
2205
|
+
if isinstance(operation, str):
|
|
2206
|
+
return self._idempotency_config.extract_domain(operation)
|
|
2207
|
+
|
|
2208
|
+
return None
|
|
2209
|
+
|
|
2210
|
+
def _create_duplicate_response(
|
|
2211
|
+
self,
|
|
2212
|
+
message_id: UUID,
|
|
2213
|
+
correlation_id: UUID,
|
|
2214
|
+
) -> dict[str, object]:
|
|
2215
|
+
"""Create response for duplicate message detection.
|
|
2216
|
+
|
|
2217
|
+
This is NOT an error response - duplicates are expected under
|
|
2218
|
+
at-least-once delivery. The response indicates successful
|
|
2219
|
+
deduplication.
|
|
2220
|
+
|
|
2221
|
+
Args:
|
|
2222
|
+
message_id: UUID of the duplicate message.
|
|
2223
|
+
correlation_id: Correlation ID for tracing.
|
|
2224
|
+
|
|
2225
|
+
Returns:
|
|
2226
|
+
Dict representation of ModelDuplicateResponse for envelope publishing.
|
|
2227
|
+
"""
|
|
2228
|
+
return ModelDuplicateResponse(
|
|
2229
|
+
message_id=message_id,
|
|
2230
|
+
correlation_id=correlation_id,
|
|
2231
|
+
).model_dump()
|
|
2232
|
+
|
|
2233
|
+
async def _cleanup_idempotency_store(self) -> None:
|
|
2234
|
+
"""Cleanup idempotency store during shutdown.
|
|
2235
|
+
|
|
2236
|
+
Closes the idempotency store connection if initialized.
|
|
2237
|
+
Called during stop() to release resources.
|
|
2238
|
+
"""
|
|
2239
|
+
if self._idempotency_store is None:
|
|
2240
|
+
return
|
|
2241
|
+
|
|
2242
|
+
try:
|
|
2243
|
+
if hasattr(self._idempotency_store, "shutdown"):
|
|
2244
|
+
await self._idempotency_store.shutdown()
|
|
2245
|
+
elif hasattr(self._idempotency_store, "close"):
|
|
2246
|
+
await self._idempotency_store.close()
|
|
2247
|
+
logger.debug("Idempotency store shutdown complete")
|
|
2248
|
+
except Exception as e:
|
|
2249
|
+
logger.warning(
|
|
2250
|
+
"Failed to shutdown idempotency store",
|
|
2251
|
+
extra={"error": str(e)},
|
|
2252
|
+
)
|
|
2253
|
+
finally:
|
|
2254
|
+
self._idempotency_store = None
|
|
2255
|
+
|
|
2256
|
+
|
|
2257
|
+
__all__: list[str] = [
|
|
2258
|
+
"RuntimeHostProcess",
|
|
2259
|
+
"wire_handlers",
|
|
2260
|
+
]
|