omnibase_infra 0.2.6__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/adapters/adapter_onex_tool_execution.py +451 -0
- omnibase_infra/capabilities/__init__.py +15 -0
- omnibase_infra/capabilities/capability_inference_rules.py +211 -0
- omnibase_infra/capabilities/contract_capability_extractor.py +221 -0
- omnibase_infra/capabilities/intent_type_extractor.py +160 -0
- omnibase_infra/cli/__init__.py +1 -0
- omnibase_infra/cli/commands.py +216 -0
- omnibase_infra/clients/__init__.py +0 -0
- omnibase_infra/configs/widget_mapping.yaml +176 -0
- omnibase_infra/constants_topic_patterns.py +26 -0
- omnibase_infra/contracts/handlers/filesystem/handler_contract.yaml +264 -0
- omnibase_infra/contracts/handlers/mcp/handler_contract.yaml +141 -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 +132 -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_consumer_group_purpose.py +92 -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 +111 -0
- omnibase_infra/enums/enum_handler_loader_error.py +178 -0
- omnibase_infra/enums/enum_handler_source_mode.py +86 -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_kafka_acks.py +99 -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 +160 -0
- omnibase_infra/errors/error_architecture_violation.py +152 -0
- omnibase_infra/errors/error_binding_resolution.py +128 -0
- omnibase_infra/errors/error_chain_propagation.py +188 -0
- omnibase_infra/errors/error_compute_registry.py +95 -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 +105 -0
- omnibase_infra/errors/error_infra.py +610 -0
- omnibase_infra/errors/error_message_type_registry.py +101 -0
- omnibase_infra/errors/error_policy_registry.py +115 -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 +84 -0
- omnibase_infra/event_bus/event_bus_inmemory.py +797 -0
- omnibase_infra/event_bus/event_bus_kafka.py +1716 -0
- omnibase_infra/event_bus/mixin_kafka_broadcast.py +180 -0
- omnibase_infra/event_bus/mixin_kafka_dlq.py +771 -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 +693 -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/testing/__init__.py +26 -0
- omnibase_infra/event_bus/testing/adapter_protocol_event_publisher_inmemory.py +418 -0
- omnibase_infra/event_bus/testing/model_publisher_metrics.py +64 -0
- omnibase_infra/event_bus/topic_constants.py +376 -0
- omnibase_infra/handlers/__init__.py +82 -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 +795 -0
- omnibase_infra/handlers/handler_db.py +1046 -0
- omnibase_infra/handlers/handler_filesystem.py +1478 -0
- omnibase_infra/handlers/handler_graph.py +2015 -0
- omnibase_infra/handlers/handler_http.py +926 -0
- omnibase_infra/handlers/handler_intent.py +387 -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 +1430 -0
- omnibase_infra/handlers/handler_qdrant.py +1076 -0
- omnibase_infra/handlers/handler_vault.py +428 -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 +47 -0
- omnibase_infra/handlers/mixins/mixin_consul_initialization.py +349 -0
- omnibase_infra/handlers/mixins/mixin_consul_kv.py +338 -0
- omnibase_infra/handlers/mixins/mixin_consul_service.py +542 -0
- omnibase_infra/handlers/mixins/mixin_consul_topic_index.py +585 -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 +922 -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 +1051 -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 +109 -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/migrations/001_create_event_ledger.sql +166 -0
- omnibase_infra/migrations/001_drop_event_ledger.sql +18 -0
- omnibase_infra/mixins/__init__.py +71 -0
- omnibase_infra/mixins/mixin_async_circuit_breaker.py +656 -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 +2670 -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 +144 -0
- omnibase_infra/models/bindings/__init__.py +59 -0
- omnibase_infra/models/bindings/constants.py +144 -0
- omnibase_infra/models/bindings/model_binding_resolution_result.py +103 -0
- omnibase_infra/models/bindings/model_operation_binding.py +44 -0
- omnibase_infra/models/bindings/model_operation_bindings_subcontract.py +152 -0
- omnibase_infra/models/bindings/model_parsed_binding.py +52 -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 +330 -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 +155 -0
- omnibase_infra/models/dispatch/model_debug_trace_snapshot.py +114 -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_materialized_dispatch.py +141 -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 +80 -0
- omnibase_infra/models/handlers/model_bootstrap_handler_descriptor.py +162 -0
- omnibase_infra/models/handlers/model_contract_discovery_result.py +82 -0
- omnibase_infra/models/handlers/model_handler_descriptor.py +200 -0
- omnibase_infra/models/handlers/model_handler_identifier.py +215 -0
- omnibase_infra/models/handlers/model_handler_source_config.py +220 -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/mcp/__init__.py +15 -0
- omnibase_infra/models/mcp/model_mcp_contract_config.py +80 -0
- omnibase_infra/models/mcp/model_mcp_server_config.py +67 -0
- omnibase_infra/models/mcp/model_mcp_tool_definition.py +73 -0
- omnibase_infra/models/mcp/model_mcp_tool_parameter.py +35 -0
- omnibase_infra/models/model_node_identity.py +126 -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 +591 -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 +68 -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_event_bus_topic_entry.py +59 -0
- omnibase_infra/models/registration/model_introspection_metrics.py +253 -0
- omnibase_infra/models/registration/model_node_capabilities.py +190 -0
- omnibase_infra/models/registration/model_node_event_bus_config.py +99 -0
- omnibase_infra/models/registration/model_node_heartbeat_event.py +126 -0
- omnibase_infra/models/registration/model_node_introspection_event.py +195 -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 +49 -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 +296 -0
- omnibase_infra/models/runtime/model_loaded_handler.py +129 -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 +57 -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 +203 -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 +106 -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/contract_registry_reducer/__init__.py +29 -0
- omnibase_infra/nodes/contract_registry_reducer/contract.yaml +255 -0
- omnibase_infra/nodes/contract_registry_reducer/models/__init__.py +38 -0
- omnibase_infra/nodes/contract_registry_reducer/models/model_contract_registry_state.py +266 -0
- omnibase_infra/nodes/contract_registry_reducer/models/model_payload_cleanup_topic_references.py +55 -0
- omnibase_infra/nodes/contract_registry_reducer/models/model_payload_deactivate_contract.py +58 -0
- omnibase_infra/nodes/contract_registry_reducer/models/model_payload_mark_stale.py +49 -0
- omnibase_infra/nodes/contract_registry_reducer/models/model_payload_update_heartbeat.py +71 -0
- omnibase_infra/nodes/contract_registry_reducer/models/model_payload_update_topic.py +66 -0
- omnibase_infra/nodes/contract_registry_reducer/models/model_payload_upsert_contract.py +92 -0
- omnibase_infra/nodes/contract_registry_reducer/node.py +121 -0
- omnibase_infra/nodes/contract_registry_reducer/reducer.py +784 -0
- omnibase_infra/nodes/contract_registry_reducer/registry/__init__.py +9 -0
- omnibase_infra/nodes/contract_registry_reducer/registry/registry_infra_contract_registry_reducer.py +101 -0
- omnibase_infra/nodes/effects/README.md +358 -0
- omnibase_infra/nodes/effects/__init__.py +26 -0
- omnibase_infra/nodes/effects/contract.yaml +167 -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/handlers/consul/contract.yaml +85 -0
- omnibase_infra/nodes/handlers/db/contract.yaml +72 -0
- omnibase_infra/nodes/handlers/graph/contract.yaml +127 -0
- omnibase_infra/nodes/handlers/http/contract.yaml +74 -0
- omnibase_infra/nodes/handlers/intent/contract.yaml +66 -0
- omnibase_infra/nodes/handlers/mcp/contract.yaml +69 -0
- omnibase_infra/nodes/handlers/vault/contract.yaml +91 -0
- omnibase_infra/nodes/node_intent_storage_effect/__init__.py +50 -0
- omnibase_infra/nodes/node_intent_storage_effect/contract.yaml +194 -0
- omnibase_infra/nodes/node_intent_storage_effect/models/__init__.py +24 -0
- omnibase_infra/nodes/node_intent_storage_effect/models/model_intent_storage_input.py +141 -0
- omnibase_infra/nodes/node_intent_storage_effect/models/model_intent_storage_output.py +130 -0
- omnibase_infra/nodes/node_intent_storage_effect/node.py +94 -0
- omnibase_infra/nodes/node_intent_storage_effect/registry/__init__.py +35 -0
- omnibase_infra/nodes/node_intent_storage_effect/registry/registry_infra_intent_storage.py +294 -0
- omnibase_infra/nodes/node_ledger_projection_compute/__init__.py +50 -0
- omnibase_infra/nodes/node_ledger_projection_compute/contract.yaml +104 -0
- omnibase_infra/nodes/node_ledger_projection_compute/node.py +284 -0
- omnibase_infra/nodes/node_ledger_projection_compute/registry/__init__.py +29 -0
- omnibase_infra/nodes/node_ledger_projection_compute/registry/registry_infra_ledger_projection.py +118 -0
- omnibase_infra/nodes/node_ledger_write_effect/__init__.py +82 -0
- omnibase_infra/nodes/node_ledger_write_effect/contract.yaml +200 -0
- omnibase_infra/nodes/node_ledger_write_effect/handlers/__init__.py +22 -0
- omnibase_infra/nodes/node_ledger_write_effect/handlers/handler_ledger_append.py +372 -0
- omnibase_infra/nodes/node_ledger_write_effect/handlers/handler_ledger_query.py +597 -0
- omnibase_infra/nodes/node_ledger_write_effect/models/__init__.py +31 -0
- omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_append_result.py +54 -0
- omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_entry.py +92 -0
- omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_query.py +53 -0
- omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_query_result.py +41 -0
- omnibase_infra/nodes/node_ledger_write_effect/node.py +89 -0
- omnibase_infra/nodes/node_ledger_write_effect/protocols/__init__.py +13 -0
- omnibase_infra/nodes/node_ledger_write_effect/protocols/protocol_ledger_persistence.py +127 -0
- omnibase_infra/nodes/node_ledger_write_effect/registry/__init__.py +9 -0
- omnibase_infra/nodes/node_ledger_write_effect/registry/registry_infra_ledger_write.py +121 -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 +482 -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 +694 -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 +528 -0
- omnibase_infra/nodes/node_registration_orchestrator/timeout_coordinator.py +393 -0
- omnibase_infra/nodes/node_registration_orchestrator/wiring.py +743 -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 +220 -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 +112 -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 +215 -0
- omnibase_infra/nodes/node_registry_effect/__init__.py +85 -0
- omnibase_infra/nodes/node_registry_effect/contract.yaml +677 -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 +417 -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 +222 -0
- omnibase_infra/nodes/reducers/__init__.py +30 -0
- omnibase_infra/nodes/reducers/models/__init__.py +37 -0
- omnibase_infra/nodes/reducers/models/model_payload_consul_register.py +87 -0
- omnibase_infra/nodes/reducers/models/model_payload_ledger_append.py +133 -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 +1138 -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 +449 -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 +104 -0
- omnibase_infra/protocols/protocol_capability_projection.py +253 -0
- omnibase_infra/protocols/protocol_capability_query.py +251 -0
- omnibase_infra/protocols/protocol_container_aware.py +200 -0
- omnibase_infra/protocols/protocol_dispatch_engine.py +152 -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 +445 -0
- omnibase_infra/runtime/binding_config_resolver.py +2771 -0
- omnibase_infra/runtime/binding_resolver.py +753 -0
- omnibase_infra/runtime/chain_aware_dispatch.py +467 -0
- omnibase_infra/runtime/constants_notification.py +75 -0
- omnibase_infra/runtime/constants_security.py +70 -0
- omnibase_infra/runtime/contract_handler_discovery.py +587 -0
- omnibase_infra/runtime/contract_loaders/__init__.py +51 -0
- omnibase_infra/runtime/contract_loaders/handler_routing_loader.py +464 -0
- omnibase_infra/runtime/contract_loaders/operation_bindings_loader.py +789 -0
- omnibase_infra/runtime/dispatch_context_enforcer.py +427 -0
- omnibase_infra/runtime/emit_daemon/__init__.py +97 -0
- omnibase_infra/runtime/emit_daemon/cli.py +844 -0
- omnibase_infra/runtime/emit_daemon/client.py +811 -0
- omnibase_infra/runtime/emit_daemon/config.py +535 -0
- omnibase_infra/runtime/emit_daemon/daemon.py +812 -0
- omnibase_infra/runtime/emit_daemon/event_registry.py +477 -0
- omnibase_infra/runtime/emit_daemon/model_daemon_request.py +139 -0
- omnibase_infra/runtime/emit_daemon/model_daemon_response.py +191 -0
- omnibase_infra/runtime/emit_daemon/queue.py +618 -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/event_bus_subcontract_wiring.py +466 -0
- omnibase_infra/runtime/handler_bootstrap_source.py +507 -0
- omnibase_infra/runtime/handler_contract_config_loader.py +603 -0
- omnibase_infra/runtime/handler_contract_source.py +750 -0
- omnibase_infra/runtime/handler_identity.py +81 -0
- omnibase_infra/runtime/handler_plugin_loader.py +2046 -0
- omnibase_infra/runtime/handler_registry.py +329 -0
- omnibase_infra/runtime/handler_source_resolver.py +367 -0
- omnibase_infra/runtime/invocation_security_enforcer.py +427 -0
- omnibase_infra/runtime/kafka_contract_source.py +984 -0
- omnibase_infra/runtime/kernel.py +40 -0
- omnibase_infra/runtime/mixin_policy_validation.py +522 -0
- omnibase_infra/runtime/mixin_semver_cache.py +402 -0
- omnibase_infra/runtime/mixins/__init__.py +24 -0
- omnibase_infra/runtime/mixins/mixin_projector_notification_publishing.py +566 -0
- omnibase_infra/runtime/mixins/mixin_projector_sql_operations.py +778 -0
- omnibase_infra/runtime/models/__init__.py +229 -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_contract_load_result.py +224 -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 +229 -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_notification_config.py +171 -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_contract_config.py +268 -0
- omnibase_infra/runtime/models/model_runtime_scheduler_config.py +625 -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_security_config.py +109 -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/models/model_transition_notification_outbox_config.py +112 -0
- omnibase_infra/runtime/models/model_transition_notification_outbox_metrics.py +140 -0
- omnibase_infra/runtime/models/model_transition_notification_publisher_metrics.py +357 -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 +1330 -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 +37 -0
- omnibase_infra/runtime/protocols/protocol_runtime_scheduler.py +468 -0
- omnibase_infra/runtime/publisher_topic_scoped.py +294 -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 +445 -0
- omnibase_infra/runtime/registry_compute.py +1143 -0
- omnibase_infra/runtime/registry_contract_source.py +693 -0
- omnibase_infra/runtime/registry_dispatcher.py +678 -0
- omnibase_infra/runtime/registry_policy.py +1185 -0
- omnibase_infra/runtime/runtime_contract_config_loader.py +406 -0
- omnibase_infra/runtime/runtime_scheduler.py +1070 -0
- omnibase_infra/runtime/secret_resolver.py +2112 -0
- omnibase_infra/runtime/security_metadata_validator.py +776 -0
- omnibase_infra/runtime/service_kernel.py +1651 -0
- omnibase_infra/runtime/service_message_dispatch_engine.py +2350 -0
- omnibase_infra/runtime/service_runtime_host_process.py +3493 -0
- omnibase_infra/runtime/transition_notification_outbox.py +1190 -0
- omnibase_infra/runtime/transition_notification_publisher.py +765 -0
- omnibase_infra/runtime/util_container_wiring.py +1124 -0
- omnibase_infra/runtime/util_validation.py +314 -0
- omnibase_infra/runtime/util_version.py +98 -0
- omnibase_infra/runtime/util_wiring.py +723 -0
- omnibase_infra/schemas/schema_registration_projection.sql +320 -0
- omnibase_infra/schemas/schema_transition_notification_outbox.sql +245 -0
- omnibase_infra/services/__init__.py +89 -0
- omnibase_infra/services/corpus_capture.py +684 -0
- omnibase_infra/services/mcp/__init__.py +31 -0
- omnibase_infra/services/mcp/mcp_server_lifecycle.py +449 -0
- omnibase_infra/services/mcp/service_mcp_tool_discovery.py +411 -0
- omnibase_infra/services/mcp/service_mcp_tool_registry.py +329 -0
- omnibase_infra/services/mcp/service_mcp_tool_sync.py +565 -0
- omnibase_infra/services/registry_api/__init__.py +40 -0
- omnibase_infra/services/registry_api/main.py +261 -0
- omnibase_infra/services/registry_api/models/__init__.py +66 -0
- omnibase_infra/services/registry_api/models/model_capability_widget_mapping.py +38 -0
- omnibase_infra/services/registry_api/models/model_pagination_info.py +48 -0
- omnibase_infra/services/registry_api/models/model_registry_discovery_response.py +73 -0
- omnibase_infra/services/registry_api/models/model_registry_health_response.py +49 -0
- omnibase_infra/services/registry_api/models/model_registry_instance_view.py +88 -0
- omnibase_infra/services/registry_api/models/model_registry_node_view.py +88 -0
- omnibase_infra/services/registry_api/models/model_registry_summary.py +60 -0
- omnibase_infra/services/registry_api/models/model_response_list_instances.py +43 -0
- omnibase_infra/services/registry_api/models/model_response_list_nodes.py +51 -0
- omnibase_infra/services/registry_api/models/model_warning.py +49 -0
- omnibase_infra/services/registry_api/models/model_widget_defaults.py +28 -0
- omnibase_infra/services/registry_api/models/model_widget_mapping.py +51 -0
- omnibase_infra/services/registry_api/routes.py +371 -0
- omnibase_infra/services/registry_api/service.py +837 -0
- omnibase_infra/services/service_capability_query.py +945 -0
- omnibase_infra/services/service_health.py +898 -0
- omnibase_infra/services/service_node_selector.py +530 -0
- omnibase_infra/services/service_timeout_emitter.py +699 -0
- omnibase_infra/services/service_timeout_scanner.py +394 -0
- omnibase_infra/services/session/__init__.py +56 -0
- omnibase_infra/services/session/config_consumer.py +137 -0
- omnibase_infra/services/session/config_store.py +139 -0
- omnibase_infra/services/session/consumer.py +1007 -0
- omnibase_infra/services/session/protocol_session_aggregator.py +117 -0
- omnibase_infra/services/session/store.py +997 -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/topics/__init__.py +45 -0
- omnibase_infra/topics/platform_topic_suffixes.py +140 -0
- omnibase_infra/topics/util_topic_composition.py +95 -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 +29 -0
- omnibase_infra/types/typed_dict/typed_dict_envelope_build_params.py +115 -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 +117 -0
- omnibase_infra/utils/correlation.py +208 -0
- omnibase_infra/utils/util_atomic_file.py +261 -0
- omnibase_infra/utils/util_consumer_group.py +232 -0
- omnibase_infra/utils/util_datetime.py +372 -0
- omnibase_infra/utils/util_db_transaction.py +239 -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_retry_optimistic.py +281 -0
- omnibase_infra/utils/util_semver.py +233 -0
- omnibase_infra/validation/__init__.py +307 -0
- omnibase_infra/validation/contracts/security.validation.yaml +114 -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 +1514 -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 +2033 -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 +513 -0
- omnibase_infra/validation/validator_topic_category.py +1152 -0
- omnibase_infra-0.2.6.dist-info/METADATA +197 -0
- omnibase_infra-0.2.6.dist-info/RECORD +833 -0
- omnibase_infra-0.2.6.dist-info/WHEEL +4 -0
- omnibase_infra-0.2.6.dist-info/entry_points.txt +5 -0
- omnibase_infra-0.2.6.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,3493 @@
|
|
|
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 importlib
|
|
47
|
+
import json
|
|
48
|
+
import logging
|
|
49
|
+
import os
|
|
50
|
+
from collections.abc import Awaitable, Callable
|
|
51
|
+
from pathlib import Path
|
|
52
|
+
from typing import TYPE_CHECKING, cast
|
|
53
|
+
from uuid import UUID, uuid4
|
|
54
|
+
|
|
55
|
+
from pydantic import BaseModel
|
|
56
|
+
|
|
57
|
+
from omnibase_infra.enums import (
|
|
58
|
+
EnumConsumerGroupPurpose,
|
|
59
|
+
EnumHandlerSourceMode,
|
|
60
|
+
EnumHandlerTypeCategory,
|
|
61
|
+
EnumInfraTransportType,
|
|
62
|
+
)
|
|
63
|
+
from omnibase_infra.errors import (
|
|
64
|
+
EnvelopeValidationError,
|
|
65
|
+
InfraConsulError,
|
|
66
|
+
InfraTimeoutError,
|
|
67
|
+
InfraUnavailableError,
|
|
68
|
+
ModelInfraErrorContext,
|
|
69
|
+
ProtocolConfigurationError,
|
|
70
|
+
RuntimeHostError,
|
|
71
|
+
UnknownHandlerTypeError,
|
|
72
|
+
)
|
|
73
|
+
from omnibase_infra.event_bus.event_bus_inmemory import EventBusInmemory
|
|
74
|
+
from omnibase_infra.event_bus.event_bus_kafka import EventBusKafka
|
|
75
|
+
from omnibase_infra.models import ModelNodeIdentity
|
|
76
|
+
from omnibase_infra.runtime.envelope_validator import (
|
|
77
|
+
normalize_correlation_id,
|
|
78
|
+
validate_envelope,
|
|
79
|
+
)
|
|
80
|
+
from omnibase_infra.runtime.handler_registry import RegistryProtocolBinding
|
|
81
|
+
from omnibase_infra.runtime.models import (
|
|
82
|
+
ModelDuplicateResponse,
|
|
83
|
+
ModelRuntimeContractConfig,
|
|
84
|
+
)
|
|
85
|
+
from omnibase_infra.runtime.protocol_lifecycle_executor import ProtocolLifecycleExecutor
|
|
86
|
+
from omnibase_infra.runtime.runtime_contract_config_loader import (
|
|
87
|
+
RuntimeContractConfigLoader,
|
|
88
|
+
)
|
|
89
|
+
from omnibase_infra.runtime.util_wiring import wire_default_handlers
|
|
90
|
+
from omnibase_infra.utils.util_consumer_group import compute_consumer_group_id
|
|
91
|
+
from omnibase_infra.utils.util_env_parsing import parse_env_float
|
|
92
|
+
|
|
93
|
+
if TYPE_CHECKING:
|
|
94
|
+
from omnibase_core.container import ModelONEXContainer
|
|
95
|
+
from omnibase_infra.event_bus.models import ModelEventMessage
|
|
96
|
+
from omnibase_infra.idempotency import ModelIdempotencyGuardConfig
|
|
97
|
+
from omnibase_infra.idempotency.protocol_idempotency_store import (
|
|
98
|
+
ProtocolIdempotencyStore,
|
|
99
|
+
)
|
|
100
|
+
from omnibase_infra.models.handlers import ModelHandlerSourceConfig
|
|
101
|
+
from omnibase_infra.nodes.architecture_validator import ProtocolArchitectureRule
|
|
102
|
+
from omnibase_infra.protocols import ProtocolContainerAware
|
|
103
|
+
from omnibase_infra.runtime.contract_handler_discovery import (
|
|
104
|
+
ContractHandlerDiscovery,
|
|
105
|
+
)
|
|
106
|
+
from omnibase_infra.runtime.service_message_dispatch_engine import (
|
|
107
|
+
MessageDispatchEngine,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Imports for PluginLoaderContractSource adapter class
|
|
111
|
+
from omnibase_core.protocols.event_bus.protocol_event_bus_subscriber import (
|
|
112
|
+
ProtocolEventBusSubscriber,
|
|
113
|
+
)
|
|
114
|
+
from omnibase_infra.models.errors import ModelHandlerValidationError
|
|
115
|
+
from omnibase_infra.models.handlers import (
|
|
116
|
+
LiteralHandlerKind,
|
|
117
|
+
ModelContractDiscoveryResult,
|
|
118
|
+
ModelHandlerDescriptor,
|
|
119
|
+
)
|
|
120
|
+
from omnibase_infra.models.types import JsonDict
|
|
121
|
+
from omnibase_infra.runtime.event_bus_subcontract_wiring import (
|
|
122
|
+
EventBusSubcontractWiring,
|
|
123
|
+
load_event_bus_subcontract,
|
|
124
|
+
)
|
|
125
|
+
from omnibase_infra.runtime.handler_identity import (
|
|
126
|
+
HANDLER_IDENTITY_PREFIX,
|
|
127
|
+
handler_identity,
|
|
128
|
+
)
|
|
129
|
+
from omnibase_infra.runtime.handler_plugin_loader import HandlerPluginLoader
|
|
130
|
+
from omnibase_infra.runtime.kafka_contract_source import (
|
|
131
|
+
TOPIC_SUFFIX_CONTRACT_DEREGISTERED,
|
|
132
|
+
TOPIC_SUFFIX_CONTRACT_REGISTERED,
|
|
133
|
+
KafkaContractSource,
|
|
134
|
+
)
|
|
135
|
+
from omnibase_infra.runtime.protocol_contract_source import ProtocolContractSource
|
|
136
|
+
|
|
137
|
+
# Expose wire_default_handlers as wire_handlers for test patching compatibility
|
|
138
|
+
# Tests patch "omnibase_infra.runtime.service_runtime_host_process.wire_handlers"
|
|
139
|
+
wire_handlers = wire_default_handlers
|
|
140
|
+
|
|
141
|
+
logger = logging.getLogger(__name__)
|
|
142
|
+
|
|
143
|
+
# Mapping from EnumHandlerTypeCategory to LiteralHandlerKind for descriptor creation.
|
|
144
|
+
# COMPUTE and EFFECT map directly to their string values.
|
|
145
|
+
# NONDETERMINISTIC_COMPUTE maps to "compute" because it is architecturally pure
|
|
146
|
+
# (no I/O) even though it may produce different results between runs.
|
|
147
|
+
# "effect" is used as the fallback for any unknown types as the safer option
|
|
148
|
+
# (effect handlers have stricter policy envelopes for I/O operations).
|
|
149
|
+
_HANDLER_TYPE_TO_KIND: dict[EnumHandlerTypeCategory, LiteralHandlerKind] = {
|
|
150
|
+
EnumHandlerTypeCategory.COMPUTE: "compute",
|
|
151
|
+
EnumHandlerTypeCategory.EFFECT: "effect",
|
|
152
|
+
EnumHandlerTypeCategory.NONDETERMINISTIC_COMPUTE: "compute",
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
# Default handler kind for unknown handler types. "effect" is the safe default
|
|
156
|
+
# because effect handlers have stricter policy envelopes for I/O operations.
|
|
157
|
+
_DEFAULT_HANDLER_KIND: LiteralHandlerKind = "effect"
|
|
158
|
+
|
|
159
|
+
# Default configuration values
|
|
160
|
+
DEFAULT_INPUT_TOPIC = "requests"
|
|
161
|
+
DEFAULT_OUTPUT_TOPIC = "responses"
|
|
162
|
+
DEFAULT_GROUP_ID = "runtime-host"
|
|
163
|
+
|
|
164
|
+
# Health check timeout bounds (per ModelLifecycleSubcontract)
|
|
165
|
+
MIN_HEALTH_CHECK_TIMEOUT = 1.0
|
|
166
|
+
MAX_HEALTH_CHECK_TIMEOUT = 60.0
|
|
167
|
+
DEFAULT_HEALTH_CHECK_TIMEOUT: float = parse_env_float(
|
|
168
|
+
"ONEX_HEALTH_CHECK_TIMEOUT",
|
|
169
|
+
5.0,
|
|
170
|
+
min_value=MIN_HEALTH_CHECK_TIMEOUT,
|
|
171
|
+
max_value=MAX_HEALTH_CHECK_TIMEOUT,
|
|
172
|
+
transport_type=EnumInfraTransportType.RUNTIME,
|
|
173
|
+
service_name="runtime_host_process",
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# Drain timeout bounds for graceful shutdown (OMN-756)
|
|
177
|
+
# Controls how long to wait for in-flight messages to complete before shutdown
|
|
178
|
+
MIN_DRAIN_TIMEOUT_SECONDS = 1.0
|
|
179
|
+
MAX_DRAIN_TIMEOUT_SECONDS = 300.0
|
|
180
|
+
DEFAULT_DRAIN_TIMEOUT_SECONDS: float = parse_env_float(
|
|
181
|
+
"ONEX_DRAIN_TIMEOUT",
|
|
182
|
+
30.0,
|
|
183
|
+
min_value=MIN_DRAIN_TIMEOUT_SECONDS,
|
|
184
|
+
max_value=MAX_DRAIN_TIMEOUT_SECONDS,
|
|
185
|
+
transport_type=EnumInfraTransportType.RUNTIME,
|
|
186
|
+
service_name="runtime_host_process",
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _parse_contract_event_payload(
|
|
191
|
+
msg: ModelEventMessage,
|
|
192
|
+
) -> tuple[dict[str, object], UUID] | None:
|
|
193
|
+
"""Parse contract event message payload and extract correlation ID.
|
|
194
|
+
|
|
195
|
+
This helper extracts common JSON parsing and correlation ID extraction logic
|
|
196
|
+
used by contract registration and deregistration handlers.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
msg: The event message to parse.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
A tuple of (payload_dict, correlation_id) if message has a value,
|
|
203
|
+
None if message value is empty.
|
|
204
|
+
|
|
205
|
+
Raises:
|
|
206
|
+
json.JSONDecodeError: If the message value is not valid JSON.
|
|
207
|
+
UnicodeDecodeError: If the message value cannot be decoded as UTF-8.
|
|
208
|
+
|
|
209
|
+
Note:
|
|
210
|
+
This function is intentionally a module-level utility rather than a
|
|
211
|
+
class method because it performs pure data transformation without
|
|
212
|
+
requiring any class state.
|
|
213
|
+
|
|
214
|
+
.. versionadded:: 0.8.0
|
|
215
|
+
Created for OMN-1654 to reduce duplication in contract event handlers.
|
|
216
|
+
"""
|
|
217
|
+
if not msg.value:
|
|
218
|
+
return None
|
|
219
|
+
|
|
220
|
+
payload: dict[str, object] = json.loads(msg.value.decode("utf-8"))
|
|
221
|
+
|
|
222
|
+
# Extract correlation ID from headers if available, or generate new
|
|
223
|
+
correlation_id: UUID
|
|
224
|
+
if msg.headers and msg.headers.correlation_id:
|
|
225
|
+
try:
|
|
226
|
+
correlation_id = UUID(str(msg.headers.correlation_id))
|
|
227
|
+
except (ValueError, TypeError):
|
|
228
|
+
correlation_id = uuid4()
|
|
229
|
+
else:
|
|
230
|
+
correlation_id = uuid4()
|
|
231
|
+
|
|
232
|
+
return (payload, correlation_id)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
class PluginLoaderContractSource(ProtocolContractSource):
|
|
236
|
+
"""Adapter that uses HandlerPluginLoader for contract discovery.
|
|
237
|
+
|
|
238
|
+
This adapter implements ProtocolContractSource using HandlerPluginLoader,
|
|
239
|
+
which uses the simpler contract schema (handler_name, handler_class,
|
|
240
|
+
handler_type, capability_tags) rather than the full ONEX contract schema.
|
|
241
|
+
|
|
242
|
+
This class wraps the HandlerPluginLoader to conform to the ProtocolContractSource
|
|
243
|
+
interface expected by HandlerSourceResolver, enabling plugin-based handler
|
|
244
|
+
discovery within the unified handler source resolution framework.
|
|
245
|
+
|
|
246
|
+
Attributes:
|
|
247
|
+
_contract_paths: List of filesystem paths to scan for handler contracts.
|
|
248
|
+
_plugin_loader: The underlying HandlerPluginLoader instance.
|
|
249
|
+
|
|
250
|
+
Example:
|
|
251
|
+
```python
|
|
252
|
+
from pathlib import Path
|
|
253
|
+
source = PluginLoaderContractSource(
|
|
254
|
+
contract_paths=[Path("/etc/onex/handlers")]
|
|
255
|
+
)
|
|
256
|
+
result = await source.discover_handlers()
|
|
257
|
+
for descriptor in result.descriptors:
|
|
258
|
+
print(f"Found handler: {descriptor.name}")
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
.. versionadded:: 0.7.0
|
|
262
|
+
Extracted from _resolve_handler_descriptors() method for better
|
|
263
|
+
testability and code organization.
|
|
264
|
+
"""
|
|
265
|
+
|
|
266
|
+
def __init__(
|
|
267
|
+
self,
|
|
268
|
+
contract_paths: list[Path],
|
|
269
|
+
allowed_namespaces: tuple[str, ...] | None = None,
|
|
270
|
+
) -> None:
|
|
271
|
+
"""Initialize the contract source with paths to scan.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
contract_paths: List of filesystem paths containing handler contracts.
|
|
275
|
+
allowed_namespaces: Optional tuple of allowed module namespaces for
|
|
276
|
+
handler class imports. If None, all namespaces are allowed.
|
|
277
|
+
"""
|
|
278
|
+
self._contract_paths = contract_paths
|
|
279
|
+
self._allowed_namespaces = allowed_namespaces
|
|
280
|
+
self._plugin_loader = HandlerPluginLoader(
|
|
281
|
+
allowed_namespaces=list(allowed_namespaces) if allowed_namespaces else None
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
@property
|
|
285
|
+
def source_type(self) -> str:
|
|
286
|
+
"""Return the source type identifier.
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
str: Always "CONTRACT" for this filesystem-based source.
|
|
290
|
+
"""
|
|
291
|
+
return "CONTRACT"
|
|
292
|
+
|
|
293
|
+
async def discover_handlers(self) -> ModelContractDiscoveryResult:
|
|
294
|
+
"""Discover handlers using HandlerPluginLoader.
|
|
295
|
+
|
|
296
|
+
Scans all configured contract paths and loads handler contracts using
|
|
297
|
+
the HandlerPluginLoader. Each discovered handler is converted to a
|
|
298
|
+
ModelHandlerDescriptor for use by the handler resolution framework.
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
ModelContractDiscoveryResult: Container with discovered descriptors
|
|
302
|
+
and any validation errors encountered during discovery.
|
|
303
|
+
|
|
304
|
+
Note:
|
|
305
|
+
This method uses graceful degradation - if a single contract path
|
|
306
|
+
fails to load, discovery continues with remaining paths and the
|
|
307
|
+
error is logged but not raised.
|
|
308
|
+
"""
|
|
309
|
+
# NOTE: ModelContractDiscoveryResult.model_rebuild() is called at module-level
|
|
310
|
+
# in handler_source_resolver.py and handler_contract_source.py to resolve
|
|
311
|
+
# forward references. No need to call it here - see those modules for rationale.
|
|
312
|
+
|
|
313
|
+
descriptors: list[ModelHandlerDescriptor] = []
|
|
314
|
+
validation_errors: list[ModelHandlerValidationError] = []
|
|
315
|
+
|
|
316
|
+
for path in self._contract_paths:
|
|
317
|
+
path_obj = Path(path) if isinstance(path, str) else path
|
|
318
|
+
if not path_obj.exists():
|
|
319
|
+
logger.warning(
|
|
320
|
+
"Contract path does not exist, skipping: %s",
|
|
321
|
+
path_obj,
|
|
322
|
+
)
|
|
323
|
+
continue
|
|
324
|
+
|
|
325
|
+
try:
|
|
326
|
+
# Use plugin loader to discover handlers with simpler schema
|
|
327
|
+
loaded_handlers = self._plugin_loader.load_from_directory(
|
|
328
|
+
directory=path_obj,
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
# Convert ModelLoadedHandler to ModelHandlerDescriptor
|
|
332
|
+
for loaded in loaded_handlers:
|
|
333
|
+
# Map EnumHandlerTypeCategory to LiteralHandlerKind.
|
|
334
|
+
# handler_type is required on ModelLoadedHandler, so this always
|
|
335
|
+
# provides a valid value. The mapping handles COMPUTE, EFFECT,
|
|
336
|
+
# and NONDETERMINISTIC_COMPUTE. Falls back to "effect" for any
|
|
337
|
+
# unknown types as the safer option (stricter policy envelope).
|
|
338
|
+
handler_kind = _HANDLER_TYPE_TO_KIND.get(
|
|
339
|
+
loaded.handler_type, _DEFAULT_HANDLER_KIND
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
descriptor = ModelHandlerDescriptor(
|
|
343
|
+
# NOTE: Uses handler_identity() for consistent ID generation.
|
|
344
|
+
# In HYBRID mode, HandlerSourceResolver compares handler_id values to
|
|
345
|
+
# determine which handler wins when both sources provide the same handler.
|
|
346
|
+
# Contract handlers need matching IDs to override their bootstrap equivalents.
|
|
347
|
+
#
|
|
348
|
+
# The "proto." prefix is a **protocol identity namespace**, NOT a source
|
|
349
|
+
# indicator. Both bootstrap and contract sources use this prefix via the
|
|
350
|
+
# shared handler_identity() helper. This enables per-handler identity
|
|
351
|
+
# matching regardless of which source discovered the handler.
|
|
352
|
+
#
|
|
353
|
+
# See: HandlerSourceResolver._resolve_hybrid() for resolution logic.
|
|
354
|
+
# See: handler_identity.py for the shared helper function.
|
|
355
|
+
handler_id=handler_identity(loaded.protocol_type),
|
|
356
|
+
name=loaded.handler_name,
|
|
357
|
+
version=loaded.handler_version,
|
|
358
|
+
handler_kind=handler_kind,
|
|
359
|
+
input_model="omnibase_infra.models.types.JsonDict",
|
|
360
|
+
output_model="omnibase_core.models.dispatch.ModelHandlerOutput",
|
|
361
|
+
description=f"Handler: {loaded.handler_name}",
|
|
362
|
+
handler_class=loaded.handler_class,
|
|
363
|
+
contract_path=str(loaded.contract_path),
|
|
364
|
+
)
|
|
365
|
+
descriptors.append(descriptor)
|
|
366
|
+
|
|
367
|
+
except Exception as e:
|
|
368
|
+
logger.warning(
|
|
369
|
+
"Failed to load handlers from path %s: %s",
|
|
370
|
+
path_obj,
|
|
371
|
+
e,
|
|
372
|
+
)
|
|
373
|
+
# Continue with other paths (graceful degradation)
|
|
374
|
+
|
|
375
|
+
return ModelContractDiscoveryResult(
|
|
376
|
+
descriptors=descriptors,
|
|
377
|
+
validation_errors=validation_errors,
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
class RuntimeHostProcess:
|
|
382
|
+
"""Runtime host process that owns event bus and coordinates handlers.
|
|
383
|
+
|
|
384
|
+
The RuntimeHostProcess is the central coordinator for ONEX infrastructure
|
|
385
|
+
runtime. It owns an event bus instance (EventBusInmemory or EventBusKafka),
|
|
386
|
+
registers handlers via the wiring module, and routes incoming envelopes to
|
|
387
|
+
appropriate handlers.
|
|
388
|
+
|
|
389
|
+
Container Integration:
|
|
390
|
+
RuntimeHostProcess now accepts a ModelONEXContainer parameter for
|
|
391
|
+
dependency injection. The container provides access to:
|
|
392
|
+
- RegistryProtocolBinding: Handler registry for protocol routing
|
|
393
|
+
|
|
394
|
+
This follows ONEX container-based DI patterns for better testability
|
|
395
|
+
and lifecycle management. The legacy singleton pattern is deprecated
|
|
396
|
+
in favor of container resolution.
|
|
397
|
+
|
|
398
|
+
Attributes:
|
|
399
|
+
event_bus: The owned event bus instance (EventBusInmemory or EventBusKafka)
|
|
400
|
+
is_running: Whether the process is currently running
|
|
401
|
+
input_topic: Topic to subscribe to for incoming envelopes
|
|
402
|
+
output_topic: Topic to publish responses to
|
|
403
|
+
group_id: Consumer group identifier
|
|
404
|
+
|
|
405
|
+
Example:
|
|
406
|
+
```python
|
|
407
|
+
from omnibase_core.container import ModelONEXContainer
|
|
408
|
+
from omnibase_infra.runtime.util_container_wiring import wire_infrastructure_services
|
|
409
|
+
|
|
410
|
+
# Container-based initialization (preferred)
|
|
411
|
+
container = ModelONEXContainer()
|
|
412
|
+
wire_infrastructure_services(container)
|
|
413
|
+
process = RuntimeHostProcess(container=container)
|
|
414
|
+
await process.start()
|
|
415
|
+
health = await process.health_check()
|
|
416
|
+
await process.stop()
|
|
417
|
+
|
|
418
|
+
# Direct initialization (without container)
|
|
419
|
+
process = RuntimeHostProcess() # Uses singleton registries
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
Graceful Shutdown:
|
|
423
|
+
The stop() method implements graceful shutdown with a configurable drain
|
|
424
|
+
period. After unsubscribing from topics, it waits for in-flight messages
|
|
425
|
+
to complete before shutting down handlers and closing the event bus.
|
|
426
|
+
See stop() docstring for configuration details.
|
|
427
|
+
"""
|
|
428
|
+
|
|
429
|
+
def __init__(
|
|
430
|
+
self,
|
|
431
|
+
container: ModelONEXContainer | None = None,
|
|
432
|
+
event_bus: EventBusInmemory | EventBusKafka | None = None,
|
|
433
|
+
input_topic: str = DEFAULT_INPUT_TOPIC,
|
|
434
|
+
output_topic: str = DEFAULT_OUTPUT_TOPIC,
|
|
435
|
+
config: dict[str, object] | None = None,
|
|
436
|
+
handler_registry: RegistryProtocolBinding | None = None,
|
|
437
|
+
architecture_rules: tuple[ProtocolArchitectureRule, ...] | None = None,
|
|
438
|
+
contract_paths: list[str] | None = None,
|
|
439
|
+
) -> None:
|
|
440
|
+
"""Initialize the runtime host process.
|
|
441
|
+
|
|
442
|
+
Args:
|
|
443
|
+
container: Optional ONEX dependency injection container. When provided,
|
|
444
|
+
the runtime host can resolve dependencies from the container if they
|
|
445
|
+
are not explicitly provided. This follows the ONEX container-based
|
|
446
|
+
DI pattern for better testability and explicit dependency management.
|
|
447
|
+
|
|
448
|
+
Container Resolution (during async start()):
|
|
449
|
+
- If handler_registry is None and container is provided, resolves
|
|
450
|
+
RegistryProtocolBinding from container.service_registry
|
|
451
|
+
- Event bus must be provided explicitly or defaults to EventBusInmemory
|
|
452
|
+
(required immediately during __init__)
|
|
453
|
+
|
|
454
|
+
Usage:
|
|
455
|
+
```python
|
|
456
|
+
from omnibase_core.container import ModelONEXContainer
|
|
457
|
+
from omnibase_infra.runtime.util_container_wiring import wire_infrastructure_services
|
|
458
|
+
|
|
459
|
+
container = ModelONEXContainer()
|
|
460
|
+
await wire_infrastructure_services(container)
|
|
461
|
+
process = RuntimeHostProcess(container=container)
|
|
462
|
+
await process.start()
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
event_bus: Optional event bus instance (EventBusInmemory or EventBusKafka).
|
|
466
|
+
If None, creates EventBusInmemory.
|
|
467
|
+
input_topic: Topic to subscribe to for incoming envelopes.
|
|
468
|
+
output_topic: Topic to publish responses to.
|
|
469
|
+
config: Optional configuration dict that can override topics and group_id.
|
|
470
|
+
Supported keys:
|
|
471
|
+
- input_topic: Override input topic
|
|
472
|
+
- output_topic: Override output topic
|
|
473
|
+
- group_id: Override consumer group identifier
|
|
474
|
+
- health_check_timeout_seconds: Timeout for individual handler
|
|
475
|
+
health checks (default: 5.0 seconds, valid range: 1-60 per
|
|
476
|
+
ModelLifecycleSubcontract). Values outside this range are
|
|
477
|
+
clamped to the nearest bound with a warning logged.
|
|
478
|
+
Invalid string values fall back to the default with a warning.
|
|
479
|
+
- drain_timeout_seconds: Maximum time to wait for in-flight
|
|
480
|
+
messages to complete during graceful shutdown (default: 30.0
|
|
481
|
+
seconds, valid range: 1-300). Values outside this range are
|
|
482
|
+
clamped to the nearest bound with a warning logged.
|
|
483
|
+
handler_registry: Optional RegistryProtocolBinding instance for handler lookup.
|
|
484
|
+
Type: RegistryProtocolBinding | None
|
|
485
|
+
|
|
486
|
+
Purpose:
|
|
487
|
+
Provides the registry that maps handler_type strings (e.g., "http", "db")
|
|
488
|
+
to their corresponding ProtocolContainerAware classes. The registry is queried
|
|
489
|
+
during start() to instantiate and initialize all registered handlers.
|
|
490
|
+
|
|
491
|
+
Resolution Order:
|
|
492
|
+
1. If handler_registry is provided, uses this pre-resolved registry
|
|
493
|
+
2. If container is provided, resolves from container.service_registry
|
|
494
|
+
3. If None, falls back to singleton via get_handler_registry()
|
|
495
|
+
|
|
496
|
+
Container Integration:
|
|
497
|
+
When using container-based DI (recommended), resolve the registry from
|
|
498
|
+
the container and pass it to RuntimeHostProcess:
|
|
499
|
+
|
|
500
|
+
```python
|
|
501
|
+
async def create_runtime() -> RuntimeHostProcess:
|
|
502
|
+
container = ModelONEXContainer()
|
|
503
|
+
await wire_infrastructure_services(container)
|
|
504
|
+
registry = await container.service_registry.resolve_service(
|
|
505
|
+
RegistryProtocolBinding
|
|
506
|
+
)
|
|
507
|
+
return RuntimeHostProcess(handler_registry=registry)
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
This follows ONEX container-based DI patterns for better testability
|
|
511
|
+
and explicit dependency management.
|
|
512
|
+
|
|
513
|
+
container: Optional ONEX container for dependency injection. Required for
|
|
514
|
+
architecture validation. If None and architecture validation is requested,
|
|
515
|
+
a minimal container will be created.
|
|
516
|
+
|
|
517
|
+
architecture_rules: Optional tuple of architecture rules to validate at startup.
|
|
518
|
+
Type: tuple[ProtocolArchitectureRule, ...] | None
|
|
519
|
+
|
|
520
|
+
Purpose:
|
|
521
|
+
Architecture rules are validated BEFORE the runtime starts. Violations
|
|
522
|
+
with ERROR severity will prevent startup. Violations with WARNING
|
|
523
|
+
severity are logged but don't block startup.
|
|
524
|
+
|
|
525
|
+
Rules implementing ProtocolArchitectureRule can be:
|
|
526
|
+
- Custom rules specific to your application
|
|
527
|
+
- Standard rules from OMN-1099 validators
|
|
528
|
+
|
|
529
|
+
Example:
|
|
530
|
+
```python
|
|
531
|
+
from my_rules import NoHandlerPublishingRule, NoAnyTypesRule
|
|
532
|
+
|
|
533
|
+
process = RuntimeHostProcess(
|
|
534
|
+
container=container,
|
|
535
|
+
architecture_rules=(
|
|
536
|
+
NoHandlerPublishingRule(),
|
|
537
|
+
NoAnyTypesRule(),
|
|
538
|
+
),
|
|
539
|
+
)
|
|
540
|
+
await process.start() # Validates architecture first
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
contract_paths: Optional list of paths to scan for handler contracts.
|
|
544
|
+
Type: list[str] | None
|
|
545
|
+
|
|
546
|
+
Purpose:
|
|
547
|
+
Enables contract-based handler discovery. When provided, the runtime
|
|
548
|
+
will auto-discover and register handlers from these paths during
|
|
549
|
+
start() instead of using wire_default_handlers().
|
|
550
|
+
|
|
551
|
+
Paths can be:
|
|
552
|
+
- Directories: Recursively scanned for handler contracts
|
|
553
|
+
- Files: Directly loaded as contract files
|
|
554
|
+
|
|
555
|
+
Behavior:
|
|
556
|
+
- If contract_paths is provided: Uses ContractHandlerDiscovery
|
|
557
|
+
to auto-discover and register handlers from the specified paths.
|
|
558
|
+
- If contract_paths is None or empty: Falls back to the existing
|
|
559
|
+
wire_default_handlers() behavior.
|
|
560
|
+
|
|
561
|
+
Error Handling:
|
|
562
|
+
Discovery errors are logged but do not block startup. This enables
|
|
563
|
+
graceful degradation where some handlers can be registered even
|
|
564
|
+
if others fail to load.
|
|
565
|
+
|
|
566
|
+
Example:
|
|
567
|
+
```python
|
|
568
|
+
# Contract-based handler discovery
|
|
569
|
+
process = RuntimeHostProcess(
|
|
570
|
+
contract_paths=["src/nodes/handlers", "plugins/"]
|
|
571
|
+
)
|
|
572
|
+
await process.start()
|
|
573
|
+
|
|
574
|
+
# Or with explicit file paths
|
|
575
|
+
process = RuntimeHostProcess(
|
|
576
|
+
contract_paths=[
|
|
577
|
+
"handlers/auth/handler_contract.yaml",
|
|
578
|
+
"handlers/db/handler_contract.yaml",
|
|
579
|
+
]
|
|
580
|
+
)
|
|
581
|
+
```
|
|
582
|
+
"""
|
|
583
|
+
# Store container reference for dependency resolution
|
|
584
|
+
self._container: ModelONEXContainer | None = container
|
|
585
|
+
# Handler registry (container-based DI or singleton fallback)
|
|
586
|
+
self._handler_registry: RegistryProtocolBinding | None = handler_registry
|
|
587
|
+
|
|
588
|
+
# Architecture rules for startup validation
|
|
589
|
+
self._architecture_rules: tuple[ProtocolArchitectureRule, ...] = (
|
|
590
|
+
architecture_rules or ()
|
|
591
|
+
)
|
|
592
|
+
|
|
593
|
+
# Contract paths for handler discovery (OMN-1133)
|
|
594
|
+
# Convert strings to Path objects for consistent filesystem operations
|
|
595
|
+
self._contract_paths: list[Path] = (
|
|
596
|
+
[Path(p) for p in contract_paths] if contract_paths else []
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
# Handler discovery service (lazy-created if contract_paths provided)
|
|
600
|
+
self._handler_discovery: ContractHandlerDiscovery | None = None
|
|
601
|
+
|
|
602
|
+
# Kafka contract source (created if KAFKA_EVENTS mode, wired separately)
|
|
603
|
+
self._kafka_contract_source: KafkaContractSource | None = None
|
|
604
|
+
|
|
605
|
+
# Create or use provided event bus
|
|
606
|
+
self._event_bus: EventBusInmemory | EventBusKafka = (
|
|
607
|
+
event_bus or EventBusInmemory()
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
# Extract configuration with defaults
|
|
611
|
+
config = config or {}
|
|
612
|
+
|
|
613
|
+
# Topic configuration (config overrides constructor args)
|
|
614
|
+
self._input_topic: str = str(config.get("input_topic", input_topic))
|
|
615
|
+
self._output_topic: str = str(config.get("output_topic", output_topic))
|
|
616
|
+
|
|
617
|
+
# Node identity configuration (required for consumer group derivation)
|
|
618
|
+
# Extract components from config - fail-fast if required fields are missing
|
|
619
|
+
_env = config.get("env")
|
|
620
|
+
env: str = str(_env).strip() if _env else "local"
|
|
621
|
+
|
|
622
|
+
_service_name = config.get("service_name")
|
|
623
|
+
if not _service_name or not str(_service_name).strip():
|
|
624
|
+
raise ValueError(
|
|
625
|
+
"RuntimeHostProcess requires 'service_name' in config. "
|
|
626
|
+
"This is the service name from your node's contract (e.g., 'omniintelligence'). "
|
|
627
|
+
"Cannot infer service_name - please provide it explicitly."
|
|
628
|
+
)
|
|
629
|
+
service_name: str = str(_service_name).strip()
|
|
630
|
+
|
|
631
|
+
_node_name = config.get("node_name")
|
|
632
|
+
if not _node_name or not str(_node_name).strip():
|
|
633
|
+
raise ValueError(
|
|
634
|
+
"RuntimeHostProcess requires 'node_name' in config. "
|
|
635
|
+
"This is the node name from your contract (e.g., 'claude_hook_event_effect'). "
|
|
636
|
+
"Cannot infer node_name - please provide it explicitly."
|
|
637
|
+
)
|
|
638
|
+
node_name: str = str(_node_name).strip()
|
|
639
|
+
|
|
640
|
+
_version = config.get("version")
|
|
641
|
+
version: str = (
|
|
642
|
+
str(_version).strip() if _version and str(_version).strip() else "v1"
|
|
643
|
+
)
|
|
644
|
+
|
|
645
|
+
self._node_identity: ModelNodeIdentity = ModelNodeIdentity(
|
|
646
|
+
env=env,
|
|
647
|
+
service=service_name,
|
|
648
|
+
node_name=node_name,
|
|
649
|
+
version=version,
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
# Health check configuration (from lifecycle subcontract pattern)
|
|
653
|
+
# Default: 5.0 seconds, valid range: 1-60 seconds per ModelLifecycleSubcontract
|
|
654
|
+
# Values outside bounds are clamped with a warning
|
|
655
|
+
_timeout_raw = config.get("health_check_timeout_seconds")
|
|
656
|
+
timeout_value: float = DEFAULT_HEALTH_CHECK_TIMEOUT
|
|
657
|
+
if isinstance(_timeout_raw, int | float):
|
|
658
|
+
timeout_value = float(_timeout_raw)
|
|
659
|
+
elif isinstance(_timeout_raw, str):
|
|
660
|
+
try:
|
|
661
|
+
timeout_value = float(_timeout_raw)
|
|
662
|
+
except ValueError:
|
|
663
|
+
logger.warning(
|
|
664
|
+
"Invalid health_check_timeout_seconds string value, using default",
|
|
665
|
+
extra={
|
|
666
|
+
"invalid_value": _timeout_raw,
|
|
667
|
+
"default_value": DEFAULT_HEALTH_CHECK_TIMEOUT,
|
|
668
|
+
},
|
|
669
|
+
)
|
|
670
|
+
timeout_value = DEFAULT_HEALTH_CHECK_TIMEOUT
|
|
671
|
+
|
|
672
|
+
# Validate bounds and clamp if necessary
|
|
673
|
+
if (
|
|
674
|
+
timeout_value < MIN_HEALTH_CHECK_TIMEOUT
|
|
675
|
+
or timeout_value > MAX_HEALTH_CHECK_TIMEOUT
|
|
676
|
+
):
|
|
677
|
+
logger.warning(
|
|
678
|
+
"health_check_timeout_seconds out of valid range, clamping",
|
|
679
|
+
extra={
|
|
680
|
+
"original_value": timeout_value,
|
|
681
|
+
"min_value": MIN_HEALTH_CHECK_TIMEOUT,
|
|
682
|
+
"max_value": MAX_HEALTH_CHECK_TIMEOUT,
|
|
683
|
+
"clamped_value": max(
|
|
684
|
+
MIN_HEALTH_CHECK_TIMEOUT,
|
|
685
|
+
min(timeout_value, MAX_HEALTH_CHECK_TIMEOUT),
|
|
686
|
+
),
|
|
687
|
+
},
|
|
688
|
+
)
|
|
689
|
+
timeout_value = max(
|
|
690
|
+
MIN_HEALTH_CHECK_TIMEOUT,
|
|
691
|
+
min(timeout_value, MAX_HEALTH_CHECK_TIMEOUT),
|
|
692
|
+
)
|
|
693
|
+
|
|
694
|
+
self._health_check_timeout_seconds: float = timeout_value
|
|
695
|
+
|
|
696
|
+
# Drain timeout configuration for graceful shutdown (OMN-756)
|
|
697
|
+
# Default: 30.0 seconds, valid range: 1-300 seconds
|
|
698
|
+
# Values outside bounds are clamped with a warning
|
|
699
|
+
_drain_timeout_raw = config.get("drain_timeout_seconds")
|
|
700
|
+
drain_timeout_value: float = DEFAULT_DRAIN_TIMEOUT_SECONDS
|
|
701
|
+
if isinstance(_drain_timeout_raw, int | float):
|
|
702
|
+
drain_timeout_value = float(_drain_timeout_raw)
|
|
703
|
+
elif isinstance(_drain_timeout_raw, str):
|
|
704
|
+
try:
|
|
705
|
+
drain_timeout_value = float(_drain_timeout_raw)
|
|
706
|
+
except ValueError:
|
|
707
|
+
logger.warning(
|
|
708
|
+
"Invalid drain_timeout_seconds string value, using default",
|
|
709
|
+
extra={
|
|
710
|
+
"invalid_value": _drain_timeout_raw,
|
|
711
|
+
"default_value": DEFAULT_DRAIN_TIMEOUT_SECONDS,
|
|
712
|
+
},
|
|
713
|
+
)
|
|
714
|
+
drain_timeout_value = DEFAULT_DRAIN_TIMEOUT_SECONDS
|
|
715
|
+
|
|
716
|
+
# Validate drain timeout bounds and clamp if necessary
|
|
717
|
+
if (
|
|
718
|
+
drain_timeout_value < MIN_DRAIN_TIMEOUT_SECONDS
|
|
719
|
+
or drain_timeout_value > MAX_DRAIN_TIMEOUT_SECONDS
|
|
720
|
+
):
|
|
721
|
+
logger.warning(
|
|
722
|
+
"drain_timeout_seconds out of valid range, clamping",
|
|
723
|
+
extra={
|
|
724
|
+
"original_value": drain_timeout_value,
|
|
725
|
+
"min_value": MIN_DRAIN_TIMEOUT_SECONDS,
|
|
726
|
+
"max_value": MAX_DRAIN_TIMEOUT_SECONDS,
|
|
727
|
+
"clamped_value": max(
|
|
728
|
+
MIN_DRAIN_TIMEOUT_SECONDS,
|
|
729
|
+
min(drain_timeout_value, MAX_DRAIN_TIMEOUT_SECONDS),
|
|
730
|
+
),
|
|
731
|
+
},
|
|
732
|
+
)
|
|
733
|
+
drain_timeout_value = max(
|
|
734
|
+
MIN_DRAIN_TIMEOUT_SECONDS,
|
|
735
|
+
min(drain_timeout_value, MAX_DRAIN_TIMEOUT_SECONDS),
|
|
736
|
+
)
|
|
737
|
+
|
|
738
|
+
self._drain_timeout_seconds: float = drain_timeout_value
|
|
739
|
+
|
|
740
|
+
# Handler executor for lifecycle operations (shutdown, health check)
|
|
741
|
+
self._lifecycle_executor = ProtocolLifecycleExecutor(
|
|
742
|
+
health_check_timeout_seconds=self._health_check_timeout_seconds
|
|
743
|
+
)
|
|
744
|
+
|
|
745
|
+
# Store full config for handler initialization
|
|
746
|
+
self._config: dict[str, object] | None = config
|
|
747
|
+
|
|
748
|
+
# Runtime state
|
|
749
|
+
self._is_running: bool = False
|
|
750
|
+
|
|
751
|
+
# Subscription handle (callable to unsubscribe)
|
|
752
|
+
self._subscription: Callable[[], Awaitable[None]] | None = None
|
|
753
|
+
|
|
754
|
+
# Handler registry (handler_type -> handler instance)
|
|
755
|
+
# This will be populated from the singleton registry during start()
|
|
756
|
+
self._handlers: dict[str, ProtocolContainerAware] = {}
|
|
757
|
+
|
|
758
|
+
# Track failed handler instantiations (handler_type -> error message)
|
|
759
|
+
# Used by health_check() to report degraded state
|
|
760
|
+
self._failed_handlers: dict[str, str] = {}
|
|
761
|
+
|
|
762
|
+
# Handler descriptors (handler_type -> descriptor with contract_config)
|
|
763
|
+
# Stored during registration for use during handler initialization
|
|
764
|
+
# Enables contract config to be passed to handlers via initialize()
|
|
765
|
+
self._handler_descriptors: dict[str, ModelHandlerDescriptor] = {}
|
|
766
|
+
|
|
767
|
+
# Pending message tracking for graceful shutdown (OMN-756)
|
|
768
|
+
# Tracks count of in-flight messages currently being processed
|
|
769
|
+
self._pending_message_count: int = 0
|
|
770
|
+
self._pending_lock: asyncio.Lock = asyncio.Lock()
|
|
771
|
+
|
|
772
|
+
# Drain state tracking for graceful shutdown (OMN-756)
|
|
773
|
+
# True when stop() has been called and we're waiting for messages to drain
|
|
774
|
+
self._is_draining: bool = False
|
|
775
|
+
|
|
776
|
+
# Idempotency guard for duplicate message detection (OMN-945)
|
|
777
|
+
# None = disabled, otherwise points to configured store
|
|
778
|
+
self._idempotency_store: ProtocolIdempotencyStore | None = None
|
|
779
|
+
self._idempotency_config: ModelIdempotencyGuardConfig | None = None
|
|
780
|
+
|
|
781
|
+
# Event bus subcontract wiring for contract-driven subscriptions (OMN-1621)
|
|
782
|
+
# Bridges contract-declared topics to Kafka subscriptions.
|
|
783
|
+
# None until wired during start() when dispatch_engine is available.
|
|
784
|
+
self._event_bus_wiring: EventBusSubcontractWiring | None = None
|
|
785
|
+
|
|
786
|
+
# Message dispatch engine for routing received messages (OMN-1621)
|
|
787
|
+
# Used by event_bus_wiring to dispatch messages to handlers.
|
|
788
|
+
# None = not configured, wiring will be skipped
|
|
789
|
+
self._dispatch_engine: MessageDispatchEngine | None = None
|
|
790
|
+
|
|
791
|
+
# Baseline subscriptions for platform-reserved topics (OMN-1654)
|
|
792
|
+
# Stores unsubscribe callbacks for contract registration/deregistration topics.
|
|
793
|
+
# Wired when KAFKA_EVENTS mode is active with a KafkaContractSource.
|
|
794
|
+
self._baseline_subscriptions: list[Callable[[], Awaitable[None]]] = []
|
|
795
|
+
|
|
796
|
+
# Contract configuration loaded at startup (OMN-1519)
|
|
797
|
+
# Contains consolidated handler_routing and operation_bindings from all contracts.
|
|
798
|
+
# None until loaded during start() via _load_contract_configs()
|
|
799
|
+
self._contract_config: ModelRuntimeContractConfig | None = None
|
|
800
|
+
|
|
801
|
+
logger.debug(
|
|
802
|
+
"RuntimeHostProcess initialized",
|
|
803
|
+
extra={
|
|
804
|
+
"input_topic": self._input_topic,
|
|
805
|
+
"output_topic": self._output_topic,
|
|
806
|
+
"group_id": self.group_id,
|
|
807
|
+
"health_check_timeout_seconds": self._health_check_timeout_seconds,
|
|
808
|
+
"drain_timeout_seconds": self._drain_timeout_seconds,
|
|
809
|
+
"has_container": self._container is not None,
|
|
810
|
+
"has_handler_registry": self._handler_registry is not None,
|
|
811
|
+
"has_contract_paths": len(self._contract_paths) > 0,
|
|
812
|
+
"contract_path_count": len(self._contract_paths),
|
|
813
|
+
},
|
|
814
|
+
)
|
|
815
|
+
|
|
816
|
+
@property
|
|
817
|
+
def container(self) -> ModelONEXContainer | None:
|
|
818
|
+
"""Return the optional ONEX dependency injection container.
|
|
819
|
+
|
|
820
|
+
Returns:
|
|
821
|
+
The container if provided during initialization, None otherwise.
|
|
822
|
+
"""
|
|
823
|
+
return self._container
|
|
824
|
+
|
|
825
|
+
@property
|
|
826
|
+
def contract_config(self) -> ModelRuntimeContractConfig | None:
|
|
827
|
+
"""Return the loaded contract configuration.
|
|
828
|
+
|
|
829
|
+
Contains consolidated handler_routing and operation_bindings from all
|
|
830
|
+
contracts discovered during startup. Returns None if contracts have
|
|
831
|
+
not been loaded yet (before start() is called).
|
|
832
|
+
|
|
833
|
+
The contract config provides access to:
|
|
834
|
+
- handler_routing_configs: All loaded handler routing configurations
|
|
835
|
+
- operation_bindings_configs: All loaded operation bindings
|
|
836
|
+
- success_rate: Ratio of successfully loaded contracts
|
|
837
|
+
- error_messages: Any errors encountered during loading
|
|
838
|
+
|
|
839
|
+
Returns:
|
|
840
|
+
ModelRuntimeContractConfig if loaded, None if not yet loaded.
|
|
841
|
+
|
|
842
|
+
Example:
|
|
843
|
+
>>> process = RuntimeHostProcess(...)
|
|
844
|
+
>>> await process.start()
|
|
845
|
+
>>> if process.contract_config:
|
|
846
|
+
... print(f"Loaded {process.contract_config.total_contracts_loaded} contracts")
|
|
847
|
+
"""
|
|
848
|
+
return self._contract_config
|
|
849
|
+
|
|
850
|
+
@property
|
|
851
|
+
def event_bus(self) -> EventBusInmemory | EventBusKafka:
|
|
852
|
+
"""Return the owned event bus instance.
|
|
853
|
+
|
|
854
|
+
Returns:
|
|
855
|
+
The event bus instance managed by this process.
|
|
856
|
+
"""
|
|
857
|
+
return self._event_bus
|
|
858
|
+
|
|
859
|
+
@property
|
|
860
|
+
def is_running(self) -> bool:
|
|
861
|
+
"""Return True if runtime is started.
|
|
862
|
+
|
|
863
|
+
Returns:
|
|
864
|
+
Boolean indicating whether the process is running.
|
|
865
|
+
"""
|
|
866
|
+
return self._is_running
|
|
867
|
+
|
|
868
|
+
@property
|
|
869
|
+
def input_topic(self) -> str:
|
|
870
|
+
"""Return the input topic for envelope subscription.
|
|
871
|
+
|
|
872
|
+
Returns:
|
|
873
|
+
The topic name to subscribe to for incoming envelopes.
|
|
874
|
+
"""
|
|
875
|
+
return self._input_topic
|
|
876
|
+
|
|
877
|
+
@property
|
|
878
|
+
def output_topic(self) -> str:
|
|
879
|
+
"""Return the output topic for response publishing.
|
|
880
|
+
|
|
881
|
+
Returns:
|
|
882
|
+
The topic name to publish responses to.
|
|
883
|
+
"""
|
|
884
|
+
return self._output_topic
|
|
885
|
+
|
|
886
|
+
@property
|
|
887
|
+
def group_id(self) -> str:
|
|
888
|
+
"""Return the consumer group identifier.
|
|
889
|
+
|
|
890
|
+
Computes the consumer group ID from the node identity using the canonical
|
|
891
|
+
format: ``{env}.{service}.{node_name}.{purpose}.{version}``
|
|
892
|
+
|
|
893
|
+
Returns:
|
|
894
|
+
The computed consumer group ID for this process.
|
|
895
|
+
"""
|
|
896
|
+
return compute_consumer_group_id(
|
|
897
|
+
self._node_identity, EnumConsumerGroupPurpose.CONSUME
|
|
898
|
+
)
|
|
899
|
+
|
|
900
|
+
@property
|
|
901
|
+
def node_identity(self) -> ModelNodeIdentity:
|
|
902
|
+
"""Return the node identity used for consumer group derivation.
|
|
903
|
+
|
|
904
|
+
The node identity contains the environment, service name, node name,
|
|
905
|
+
and version that uniquely identify this runtime host process within
|
|
906
|
+
the ONEX infrastructure.
|
|
907
|
+
|
|
908
|
+
Returns:
|
|
909
|
+
The immutable node identity for this process.
|
|
910
|
+
"""
|
|
911
|
+
return self._node_identity
|
|
912
|
+
|
|
913
|
+
@property
|
|
914
|
+
def is_draining(self) -> bool:
|
|
915
|
+
"""Return True if the process is draining pending messages during shutdown.
|
|
916
|
+
|
|
917
|
+
This property indicates whether the runtime host is in the graceful shutdown
|
|
918
|
+
drain period - the phase where stop() has been called, new messages are no
|
|
919
|
+
longer being accepted, and the process is waiting for in-flight messages to
|
|
920
|
+
complete before shutting down handlers and the event bus.
|
|
921
|
+
|
|
922
|
+
Drain State Transitions:
|
|
923
|
+
- False: Normal operation (accepting and processing messages)
|
|
924
|
+
- True: Drain period active (stop() called, waiting for pending messages)
|
|
925
|
+
- False: After drain completes and shutdown finishes
|
|
926
|
+
|
|
927
|
+
Use Cases:
|
|
928
|
+
- Health check reporting (indicate service is shutting down)
|
|
929
|
+
- Load balancer integration (remove from rotation during drain)
|
|
930
|
+
- Monitoring dashboards (show lifecycle state)
|
|
931
|
+
- Debugging shutdown behavior
|
|
932
|
+
|
|
933
|
+
Returns:
|
|
934
|
+
True if currently in drain period during graceful shutdown, False otherwise.
|
|
935
|
+
"""
|
|
936
|
+
return self._is_draining
|
|
937
|
+
|
|
938
|
+
@property
|
|
939
|
+
def pending_message_count(self) -> int:
|
|
940
|
+
"""Return the current count of in-flight messages being processed.
|
|
941
|
+
|
|
942
|
+
This property provides visibility into how many messages are currently
|
|
943
|
+
being processed by the runtime host. Used for graceful shutdown to
|
|
944
|
+
determine when it's safe to complete the shutdown process.
|
|
945
|
+
|
|
946
|
+
Atomicity Guarantees:
|
|
947
|
+
This property returns the raw counter value WITHOUT acquiring the
|
|
948
|
+
async lock (_pending_lock). This is safe because:
|
|
949
|
+
|
|
950
|
+
1. Single int read is atomic under CPython's GIL - reading a single
|
|
951
|
+
integer value cannot be interrupted mid-operation
|
|
952
|
+
2. The value is only used for observability/monitoring purposes
|
|
953
|
+
where exact precision is not required
|
|
954
|
+
3. The slight possibility of reading a stale value during concurrent
|
|
955
|
+
increment/decrement is acceptable for monitoring use cases
|
|
956
|
+
|
|
957
|
+
Thread Safety Considerations:
|
|
958
|
+
While the read itself is atomic, the value may be approximate if
|
|
959
|
+
read occurs during concurrent message processing:
|
|
960
|
+
- Another coroutine may be in the middle of incrementing/decrementing
|
|
961
|
+
- The value represents a point-in-time snapshot, not a synchronized view
|
|
962
|
+
- For observability, this approximation is acceptable and avoids
|
|
963
|
+
lock contention that would impact performance
|
|
964
|
+
|
|
965
|
+
Use Cases (appropriate for this property):
|
|
966
|
+
- Logging current message count for debugging
|
|
967
|
+
- Metrics/observability dashboards
|
|
968
|
+
- Approximate health status reporting
|
|
969
|
+
- Monitoring drain progress during shutdown
|
|
970
|
+
|
|
971
|
+
When to use shutdown_ready() instead:
|
|
972
|
+
For shutdown decisions requiring precise count, use the async
|
|
973
|
+
shutdown_ready() method which acquires the lock to ensure no
|
|
974
|
+
race condition with in-flight message processing. The stop()
|
|
975
|
+
method uses shutdown_ready() internally for this reason.
|
|
976
|
+
|
|
977
|
+
Returns:
|
|
978
|
+
Current count of messages being processed. May be approximate
|
|
979
|
+
if reads occur during concurrent increment/decrement operations.
|
|
980
|
+
"""
|
|
981
|
+
return self._pending_message_count
|
|
982
|
+
|
|
983
|
+
async def shutdown_ready(self) -> bool:
|
|
984
|
+
"""Check if process is ready for shutdown (no pending messages).
|
|
985
|
+
|
|
986
|
+
This method acquires the pending message lock to ensure an accurate
|
|
987
|
+
count of in-flight messages. Use this method during graceful shutdown
|
|
988
|
+
to determine when all pending messages have been processed.
|
|
989
|
+
|
|
990
|
+
Returns:
|
|
991
|
+
True if no messages are currently being processed, False otherwise.
|
|
992
|
+
"""
|
|
993
|
+
async with self._pending_lock:
|
|
994
|
+
return self._pending_message_count == 0
|
|
995
|
+
|
|
996
|
+
async def start(self) -> None:
|
|
997
|
+
"""Start the runtime host.
|
|
998
|
+
|
|
999
|
+
Performs the following steps:
|
|
1000
|
+
1. Validate architecture compliance (if rules configured) - OMN-1138
|
|
1001
|
+
2. Start event bus (if not already started)
|
|
1002
|
+
3. Discover/wire handlers:
|
|
1003
|
+
- If contract_paths provided: Auto-discover handlers from contracts (OMN-1133)
|
|
1004
|
+
- Otherwise: Wire default handlers via wiring module
|
|
1005
|
+
4. Populate self._handlers from singleton registry (instantiate and initialize)
|
|
1006
|
+
5. Subscribe to input topic
|
|
1007
|
+
|
|
1008
|
+
Architecture Validation (OMN-1138):
|
|
1009
|
+
If architecture_rules were provided at init, validation runs FIRST
|
|
1010
|
+
before any other startup logic. This ensures:
|
|
1011
|
+
- Violations are caught before resources are allocated
|
|
1012
|
+
- Fast feedback for CI/CD pipelines
|
|
1013
|
+
- Clean startup/failure without partial state
|
|
1014
|
+
|
|
1015
|
+
ERROR severity violations block startup by raising
|
|
1016
|
+
ArchitectureViolationError. WARNING/INFO violations are logged
|
|
1017
|
+
but don't block startup.
|
|
1018
|
+
|
|
1019
|
+
Contract-Based Handler Discovery (OMN-1133):
|
|
1020
|
+
If contract_paths were provided at init, the runtime will auto-discover
|
|
1021
|
+
handlers from these paths instead of using wire_default_handlers().
|
|
1022
|
+
|
|
1023
|
+
Discovery errors are logged but do not block startup, enabling
|
|
1024
|
+
graceful degradation where some handlers can be registered even
|
|
1025
|
+
if others fail to load.
|
|
1026
|
+
|
|
1027
|
+
This method is idempotent - calling start() on an already started
|
|
1028
|
+
process is safe and has no effect.
|
|
1029
|
+
|
|
1030
|
+
Raises:
|
|
1031
|
+
ArchitectureViolationError: If architecture validation fails with
|
|
1032
|
+
blocking violations (ERROR severity).
|
|
1033
|
+
"""
|
|
1034
|
+
if self._is_running:
|
|
1035
|
+
logger.debug("RuntimeHostProcess already started, skipping")
|
|
1036
|
+
return
|
|
1037
|
+
|
|
1038
|
+
logger.info(
|
|
1039
|
+
"Starting RuntimeHostProcess",
|
|
1040
|
+
extra={
|
|
1041
|
+
"input_topic": self._input_topic,
|
|
1042
|
+
"output_topic": self._output_topic,
|
|
1043
|
+
"group_id": self.group_id,
|
|
1044
|
+
"has_contract_paths": len(self._contract_paths) > 0,
|
|
1045
|
+
},
|
|
1046
|
+
)
|
|
1047
|
+
|
|
1048
|
+
# Step 1: Validate architecture compliance FIRST (OMN-1138)
|
|
1049
|
+
# This runs before event bus starts or handlers are wired to ensure
|
|
1050
|
+
# clean failure without partial state if validation fails
|
|
1051
|
+
await self._validate_architecture()
|
|
1052
|
+
|
|
1053
|
+
# Step 2: Start event bus
|
|
1054
|
+
await self._event_bus.start()
|
|
1055
|
+
|
|
1056
|
+
# Step 3: Discover/wire handlers (OMN-1133)
|
|
1057
|
+
# If contract_paths provided, use ContractHandlerDiscovery to auto-discover
|
|
1058
|
+
# handlers from contract files. Otherwise, fall back to wire_default_handlers().
|
|
1059
|
+
await self._discover_or_wire_handlers()
|
|
1060
|
+
|
|
1061
|
+
# Step 4: Populate self._handlers from singleton registry
|
|
1062
|
+
# The wiring/discovery step registers handler classes, so we need to:
|
|
1063
|
+
# - Get each registered handler class from the singleton registry
|
|
1064
|
+
# - Instantiate the handler class
|
|
1065
|
+
# - Call initialize() on each handler instance with config
|
|
1066
|
+
# - Store the handler instance in self._handlers for routing
|
|
1067
|
+
await self._populate_handlers_from_registry()
|
|
1068
|
+
|
|
1069
|
+
# Step 4.1: FAIL-FAST validation - runtime MUST have at least one handler
|
|
1070
|
+
# A runtime with no handlers cannot process any events and is misconfigured.
|
|
1071
|
+
# This catches configuration issues early rather than silently starting a
|
|
1072
|
+
# runtime that cannot do anything useful.
|
|
1073
|
+
if not self._handlers:
|
|
1074
|
+
correlation_id = uuid4()
|
|
1075
|
+
context = ModelInfraErrorContext(
|
|
1076
|
+
transport_type=EnumInfraTransportType.RUNTIME,
|
|
1077
|
+
operation="validate_handlers",
|
|
1078
|
+
target_name="runtime_host_process",
|
|
1079
|
+
correlation_id=correlation_id,
|
|
1080
|
+
)
|
|
1081
|
+
|
|
1082
|
+
# Build informative error message with context about what was attempted
|
|
1083
|
+
contract_paths_info = (
|
|
1084
|
+
f" * contract_paths provided: {[str(p) for p in self._contract_paths]}\n"
|
|
1085
|
+
if self._contract_paths
|
|
1086
|
+
else " * contract_paths: NOT PROVIDED (using ONEX_CONTRACTS_DIR env var)\n"
|
|
1087
|
+
)
|
|
1088
|
+
|
|
1089
|
+
# Get registry count for additional context
|
|
1090
|
+
handler_registry = await self._get_handler_registry()
|
|
1091
|
+
registry_protocol_count = len(handler_registry.list_protocols())
|
|
1092
|
+
|
|
1093
|
+
# Build additional diagnostic info
|
|
1094
|
+
failed_handlers_detail = ""
|
|
1095
|
+
if self._failed_handlers:
|
|
1096
|
+
failed_handlers_detail = "FAILED HANDLERS (check these first):\n"
|
|
1097
|
+
for handler_type, error_msg in self._failed_handlers.items():
|
|
1098
|
+
failed_handlers_detail += f" * {handler_type}: {error_msg}\n"
|
|
1099
|
+
failed_handlers_detail += "\n"
|
|
1100
|
+
|
|
1101
|
+
raise ProtocolConfigurationError(
|
|
1102
|
+
"No handlers registered. The runtime cannot start without at least one handler.\n\n"
|
|
1103
|
+
"CURRENT CONFIGURATION:\n"
|
|
1104
|
+
f"{contract_paths_info}"
|
|
1105
|
+
f" * Registry protocol count: {registry_protocol_count}\n"
|
|
1106
|
+
f" * Failed handlers: {len(self._failed_handlers)}\n"
|
|
1107
|
+
f" * Correlation ID: {correlation_id}\n\n"
|
|
1108
|
+
f"{failed_handlers_detail}"
|
|
1109
|
+
"TROUBLESHOOTING STEPS:\n"
|
|
1110
|
+
" 1. Verify ONEX_CONTRACTS_DIR points to a valid contracts directory:\n"
|
|
1111
|
+
" - Run: echo $ONEX_CONTRACTS_DIR && ls -la $ONEX_CONTRACTS_DIR\n"
|
|
1112
|
+
" - Expected: Directory containing handler_contract.yaml or contract.yaml files\n\n"
|
|
1113
|
+
" 2. Check for handler contract files:\n"
|
|
1114
|
+
" - Run: find $ONEX_CONTRACTS_DIR -name 'handler_contract.yaml' -o -name 'contract.yaml'\n"
|
|
1115
|
+
" - If empty: No contracts found - create handler contracts or set correct path\n\n"
|
|
1116
|
+
" 3. Verify handler contracts have required fields:\n"
|
|
1117
|
+
" - Required: handler_name, handler_class, handler_type\n"
|
|
1118
|
+
" - Example:\n"
|
|
1119
|
+
" handler_name: my_handler\n"
|
|
1120
|
+
" handler_class: mymodule.handlers.MyHandler\n"
|
|
1121
|
+
" handler_type: http\n\n"
|
|
1122
|
+
" 4. Verify handler modules are importable:\n"
|
|
1123
|
+
" - Run: python -c 'from mymodule.handlers import MyHandler; print(MyHandler)'\n"
|
|
1124
|
+
" - Check PYTHONPATH includes your handler module paths\n\n"
|
|
1125
|
+
" 5. Check application logs for loader errors:\n"
|
|
1126
|
+
" - Look for: MODULE_NOT_FOUND (HANDLER_LOADER_010)\n"
|
|
1127
|
+
" - Look for: CLASS_NOT_FOUND (HANDLER_LOADER_011)\n"
|
|
1128
|
+
" - Look for: IMPORT_ERROR (HANDLER_LOADER_012)\n"
|
|
1129
|
+
" - Look for: AMBIGUOUS_CONTRACT (HANDLER_LOADER_040)\n\n"
|
|
1130
|
+
" 6. If using wire_handlers() manually:\n"
|
|
1131
|
+
" - Ensure wire_handlers() is called before RuntimeHostProcess.start()\n"
|
|
1132
|
+
" - Check that handlers implement ProtocolContainerAware interface\n\n"
|
|
1133
|
+
" 7. Docker/container environment:\n"
|
|
1134
|
+
" - Verify volume mounts include handler contract directories\n"
|
|
1135
|
+
" - Check ONEX_CONTRACTS_DIR is set in docker-compose.yml/Dockerfile\n"
|
|
1136
|
+
" - Run: docker exec <container> ls $ONEX_CONTRACTS_DIR\n\n"
|
|
1137
|
+
"For verbose handler discovery logging, set LOG_LEVEL=DEBUG.",
|
|
1138
|
+
context=context,
|
|
1139
|
+
registered_handler_count=0,
|
|
1140
|
+
failed_handler_count=len(self._failed_handlers),
|
|
1141
|
+
failed_handlers=list(self._failed_handlers.keys()),
|
|
1142
|
+
contract_paths=[str(p) for p in self._contract_paths],
|
|
1143
|
+
registry_protocol_count=registry_protocol_count,
|
|
1144
|
+
)
|
|
1145
|
+
|
|
1146
|
+
# Step 4.15: Load contract configurations (OMN-1519)
|
|
1147
|
+
# Loads handler_routing and operation_bindings from all discovered contracts.
|
|
1148
|
+
# Uses the same contract_paths configured for handler discovery.
|
|
1149
|
+
# The loaded config is accessible via self.contract_config property.
|
|
1150
|
+
startup_correlation_id = uuid4()
|
|
1151
|
+
await self._load_contract_configs(correlation_id=startup_correlation_id)
|
|
1152
|
+
|
|
1153
|
+
# Step 4.2: Wire event bus subscriptions from contracts (OMN-1621)
|
|
1154
|
+
# This bridges contract-declared topics to Kafka subscriptions.
|
|
1155
|
+
# Requires dispatch_engine to be available for message routing.
|
|
1156
|
+
await self._wire_event_bus_subscriptions()
|
|
1157
|
+
|
|
1158
|
+
# Step 4.3: Wire baseline subscriptions for contract discovery (OMN-1654)
|
|
1159
|
+
# When KAFKA_EVENTS mode is active, subscribe to platform-reserved
|
|
1160
|
+
# contract topics to receive registration/deregistration events.
|
|
1161
|
+
await self._wire_baseline_subscriptions()
|
|
1162
|
+
|
|
1163
|
+
# Step 4.5: Initialize idempotency store if configured (OMN-945)
|
|
1164
|
+
await self._initialize_idempotency_store()
|
|
1165
|
+
|
|
1166
|
+
# Step 5: Subscribe to input topic
|
|
1167
|
+
self._subscription = await self._event_bus.subscribe(
|
|
1168
|
+
topic=self._input_topic,
|
|
1169
|
+
node_identity=self._node_identity,
|
|
1170
|
+
on_message=self._on_message,
|
|
1171
|
+
purpose=EnumConsumerGroupPurpose.CONSUME,
|
|
1172
|
+
)
|
|
1173
|
+
|
|
1174
|
+
self._is_running = True
|
|
1175
|
+
|
|
1176
|
+
logger.info(
|
|
1177
|
+
"RuntimeHostProcess started successfully",
|
|
1178
|
+
extra={
|
|
1179
|
+
"input_topic": self._input_topic,
|
|
1180
|
+
"output_topic": self._output_topic,
|
|
1181
|
+
"group_id": self.group_id,
|
|
1182
|
+
"registered_handlers": list(self._handlers.keys()),
|
|
1183
|
+
},
|
|
1184
|
+
)
|
|
1185
|
+
|
|
1186
|
+
async def stop(self) -> None:
|
|
1187
|
+
"""Stop the runtime host with graceful drain period.
|
|
1188
|
+
|
|
1189
|
+
Performs the following steps:
|
|
1190
|
+
1. Unsubscribe from topics (stop receiving new messages)
|
|
1191
|
+
2. Wait for in-flight messages to drain (up to drain_timeout_seconds)
|
|
1192
|
+
3. Shutdown all registered handlers by priority (release resources)
|
|
1193
|
+
4. Close event bus
|
|
1194
|
+
|
|
1195
|
+
This method is idempotent - calling stop() on an already stopped
|
|
1196
|
+
process is safe and has no effect.
|
|
1197
|
+
|
|
1198
|
+
Drain Period:
|
|
1199
|
+
After unsubscribing from topics, the process waits for in-flight
|
|
1200
|
+
messages to complete processing. The drain period is controlled by
|
|
1201
|
+
the drain_timeout_seconds configuration parameter (default: 30.0
|
|
1202
|
+
seconds, valid range: 1-300).
|
|
1203
|
+
|
|
1204
|
+
During the drain period:
|
|
1205
|
+
- No new messages are received (unsubscribed from topics)
|
|
1206
|
+
- Messages currently being processed are allowed to complete
|
|
1207
|
+
- shutdown_ready() is polled every 100ms to check completion
|
|
1208
|
+
- If timeout is exceeded, shutdown proceeds with a warning
|
|
1209
|
+
|
|
1210
|
+
Handler Shutdown Order:
|
|
1211
|
+
Handlers are shutdown in priority order, with higher priority handlers
|
|
1212
|
+
shutting down first. Within the same priority level, handlers are
|
|
1213
|
+
shutdown in parallel for performance.
|
|
1214
|
+
|
|
1215
|
+
Priority is determined by the handler's shutdown_priority() method:
|
|
1216
|
+
- Higher values = shutdown first
|
|
1217
|
+
- Handlers without shutdown_priority() get default priority of 0
|
|
1218
|
+
|
|
1219
|
+
Recommended Priority Scheme:
|
|
1220
|
+
- 100: Consumers (stop receiving before stopping producers)
|
|
1221
|
+
- 80: Active connections (close before closing pools)
|
|
1222
|
+
- 50: Producers (stop producing before closing pools)
|
|
1223
|
+
- 40: Connection pools (close last)
|
|
1224
|
+
- 0: Default for handlers without explicit priority
|
|
1225
|
+
|
|
1226
|
+
This ensures dependency-based ordering:
|
|
1227
|
+
- Consumers shutdown before producers
|
|
1228
|
+
- Connections shutdown before connection pools
|
|
1229
|
+
- Downstream resources shutdown before upstream resources
|
|
1230
|
+
"""
|
|
1231
|
+
if not self._is_running:
|
|
1232
|
+
logger.debug("RuntimeHostProcess already stopped, skipping")
|
|
1233
|
+
return
|
|
1234
|
+
|
|
1235
|
+
logger.info("Stopping RuntimeHostProcess")
|
|
1236
|
+
|
|
1237
|
+
# Step 1: Unsubscribe from topics (stop receiving new messages)
|
|
1238
|
+
if self._subscription is not None:
|
|
1239
|
+
await self._subscription()
|
|
1240
|
+
self._subscription = None
|
|
1241
|
+
|
|
1242
|
+
# Step 1.5: Wait for in-flight messages to drain (OMN-756)
|
|
1243
|
+
# This allows messages currently being processed to complete
|
|
1244
|
+
loop = asyncio.get_running_loop()
|
|
1245
|
+
drain_start = loop.time()
|
|
1246
|
+
drain_deadline = drain_start + self._drain_timeout_seconds
|
|
1247
|
+
last_progress_log = drain_start
|
|
1248
|
+
|
|
1249
|
+
# Mark drain state for health check visibility (OMN-756)
|
|
1250
|
+
self._is_draining = True
|
|
1251
|
+
|
|
1252
|
+
# Log drain start for observability
|
|
1253
|
+
logger.info(
|
|
1254
|
+
"Starting drain period",
|
|
1255
|
+
extra={
|
|
1256
|
+
"pending_messages": self._pending_message_count,
|
|
1257
|
+
"drain_timeout_seconds": self._drain_timeout_seconds,
|
|
1258
|
+
},
|
|
1259
|
+
)
|
|
1260
|
+
|
|
1261
|
+
while not await self.shutdown_ready():
|
|
1262
|
+
remaining = drain_deadline - loop.time()
|
|
1263
|
+
if remaining <= 0:
|
|
1264
|
+
logger.warning(
|
|
1265
|
+
"Drain timeout exceeded, forcing shutdown",
|
|
1266
|
+
extra={
|
|
1267
|
+
"pending_messages": self._pending_message_count,
|
|
1268
|
+
"drain_timeout_seconds": self._drain_timeout_seconds,
|
|
1269
|
+
"metric.drain_timeout_exceeded": True,
|
|
1270
|
+
"metric.pending_at_timeout": self._pending_message_count,
|
|
1271
|
+
},
|
|
1272
|
+
)
|
|
1273
|
+
break
|
|
1274
|
+
|
|
1275
|
+
# Wait a short interval before checking again
|
|
1276
|
+
await asyncio.sleep(min(0.1, remaining))
|
|
1277
|
+
|
|
1278
|
+
# Log progress every 5 seconds during long drains for observability
|
|
1279
|
+
elapsed = loop.time() - drain_start
|
|
1280
|
+
if elapsed - (last_progress_log - drain_start) >= 5.0:
|
|
1281
|
+
logger.info(
|
|
1282
|
+
"Drain in progress",
|
|
1283
|
+
extra={
|
|
1284
|
+
"pending_messages": self._pending_message_count,
|
|
1285
|
+
"elapsed_seconds": round(elapsed, 2),
|
|
1286
|
+
"remaining_seconds": round(remaining, 2),
|
|
1287
|
+
},
|
|
1288
|
+
)
|
|
1289
|
+
last_progress_log = loop.time()
|
|
1290
|
+
|
|
1291
|
+
# Clear drain state after drain period completes
|
|
1292
|
+
self._is_draining = False
|
|
1293
|
+
|
|
1294
|
+
logger.info(
|
|
1295
|
+
"Drain period completed",
|
|
1296
|
+
extra={
|
|
1297
|
+
"drain_duration_seconds": loop.time() - drain_start,
|
|
1298
|
+
"pending_messages": self._pending_message_count,
|
|
1299
|
+
"metric.drain_duration": loop.time() - drain_start,
|
|
1300
|
+
"metric.forced_shutdown": self._pending_message_count > 0,
|
|
1301
|
+
},
|
|
1302
|
+
)
|
|
1303
|
+
|
|
1304
|
+
# Step 2: Shutdown all handlers by priority (release resources like DB/Kafka connections)
|
|
1305
|
+
# Delegates to ProtocolLifecycleExecutor which handles:
|
|
1306
|
+
# - Grouping handlers by priority (higher priority first)
|
|
1307
|
+
# - Parallel shutdown within priority groups for performance
|
|
1308
|
+
if self._handlers:
|
|
1309
|
+
shutdown_result = (
|
|
1310
|
+
await self._lifecycle_executor.shutdown_handlers_by_priority(
|
|
1311
|
+
self._handlers
|
|
1312
|
+
)
|
|
1313
|
+
)
|
|
1314
|
+
|
|
1315
|
+
# Log summary (ProtocolLifecycleExecutor already logs detailed info)
|
|
1316
|
+
logger.info(
|
|
1317
|
+
"Handler shutdown completed",
|
|
1318
|
+
extra={
|
|
1319
|
+
"succeeded_handlers": shutdown_result.succeeded_handlers,
|
|
1320
|
+
"failed_handlers": [
|
|
1321
|
+
f.handler_type for f in shutdown_result.failed_handlers
|
|
1322
|
+
],
|
|
1323
|
+
"total_handlers": shutdown_result.total_count,
|
|
1324
|
+
"success_count": shutdown_result.success_count,
|
|
1325
|
+
"failure_count": shutdown_result.failure_count,
|
|
1326
|
+
},
|
|
1327
|
+
)
|
|
1328
|
+
|
|
1329
|
+
# Step 2.5: Cleanup idempotency store if initialized (OMN-945)
|
|
1330
|
+
await self._cleanup_idempotency_store()
|
|
1331
|
+
|
|
1332
|
+
# Step 2.6: Cleanup event bus subcontract wiring (OMN-1621)
|
|
1333
|
+
if self._event_bus_wiring:
|
|
1334
|
+
await self._event_bus_wiring.cleanup()
|
|
1335
|
+
|
|
1336
|
+
# Step 2.7: Cleanup baseline subscriptions for contract discovery (OMN-1654)
|
|
1337
|
+
if self._baseline_subscriptions:
|
|
1338
|
+
for unsubscribe in self._baseline_subscriptions:
|
|
1339
|
+
try:
|
|
1340
|
+
await unsubscribe()
|
|
1341
|
+
except Exception as e:
|
|
1342
|
+
logger.warning(
|
|
1343
|
+
"Failed to unsubscribe baseline subscription",
|
|
1344
|
+
extra={"error": str(e)},
|
|
1345
|
+
)
|
|
1346
|
+
self._baseline_subscriptions.clear()
|
|
1347
|
+
logger.debug("Baseline contract subscriptions cleaned up")
|
|
1348
|
+
|
|
1349
|
+
# Step 2.8: Nullify KafkaContractSource reference for proper cleanup (OMN-1654)
|
|
1350
|
+
self._kafka_contract_source = None
|
|
1351
|
+
|
|
1352
|
+
# Step 3: Close event bus
|
|
1353
|
+
await self._event_bus.close()
|
|
1354
|
+
|
|
1355
|
+
self._is_running = False
|
|
1356
|
+
|
|
1357
|
+
logger.info("RuntimeHostProcess stopped successfully")
|
|
1358
|
+
|
|
1359
|
+
def _load_handler_source_config(self) -> ModelHandlerSourceConfig:
|
|
1360
|
+
"""Load handler source configuration from runtime config.
|
|
1361
|
+
|
|
1362
|
+
Loads the handler source mode configuration that controls how handlers
|
|
1363
|
+
are discovered (BOOTSTRAP, CONTRACT, or HYBRID mode).
|
|
1364
|
+
|
|
1365
|
+
Config Keys:
|
|
1366
|
+
handler_source_mode: "bootstrap" | "contract" | "hybrid" (default: "hybrid")
|
|
1367
|
+
bootstrap_expires_at: ISO-8601 datetime string (optional, UTC required)
|
|
1368
|
+
|
|
1369
|
+
Returns:
|
|
1370
|
+
ModelHandlerSourceConfig with validated settings.
|
|
1371
|
+
|
|
1372
|
+
Note:
|
|
1373
|
+
If no configuration is provided, defaults to HYBRID mode with no
|
|
1374
|
+
bootstrap expiry (bootstrap handlers always available as fallback).
|
|
1375
|
+
|
|
1376
|
+
.. versionadded:: 0.7.0
|
|
1377
|
+
Part of OMN-1095 handler source mode integration.
|
|
1378
|
+
"""
|
|
1379
|
+
# Deferred imports: avoid circular dependencies at module load time
|
|
1380
|
+
# and reduce import overhead when this method is not called.
|
|
1381
|
+
from datetime import datetime
|
|
1382
|
+
|
|
1383
|
+
from pydantic import ValidationError
|
|
1384
|
+
|
|
1385
|
+
from omnibase_infra.models.handlers import ModelHandlerSourceConfig
|
|
1386
|
+
|
|
1387
|
+
config = self._config or {}
|
|
1388
|
+
handler_source_config = config.get("handler_source", {})
|
|
1389
|
+
|
|
1390
|
+
if isinstance(handler_source_config, dict):
|
|
1391
|
+
mode_str = handler_source_config.get(
|
|
1392
|
+
"mode", EnumHandlerSourceMode.HYBRID.value
|
|
1393
|
+
)
|
|
1394
|
+
expires_at_str = handler_source_config.get("bootstrap_expires_at")
|
|
1395
|
+
allow_override_raw = handler_source_config.get(
|
|
1396
|
+
"allow_bootstrap_override", False
|
|
1397
|
+
)
|
|
1398
|
+
|
|
1399
|
+
# Parse mode
|
|
1400
|
+
try:
|
|
1401
|
+
mode = EnumHandlerSourceMode(mode_str)
|
|
1402
|
+
except ValueError:
|
|
1403
|
+
logger.warning(
|
|
1404
|
+
"Invalid handler_source_mode, defaulting to HYBRID",
|
|
1405
|
+
extra={"invalid_value": mode_str},
|
|
1406
|
+
)
|
|
1407
|
+
mode = EnumHandlerSourceMode.HYBRID
|
|
1408
|
+
|
|
1409
|
+
# Parse expiry datetime
|
|
1410
|
+
expires_at = None
|
|
1411
|
+
if expires_at_str:
|
|
1412
|
+
try:
|
|
1413
|
+
expires_at = datetime.fromisoformat(str(expires_at_str))
|
|
1414
|
+
except ValueError:
|
|
1415
|
+
logger.warning(
|
|
1416
|
+
"Invalid bootstrap_expires_at format, ignoring",
|
|
1417
|
+
extra={"invalid_value": expires_at_str},
|
|
1418
|
+
)
|
|
1419
|
+
|
|
1420
|
+
# Construct config with validation - catch naive datetime errors
|
|
1421
|
+
# Note: allow_bootstrap_override coercion handled by Pydantic field validator
|
|
1422
|
+
try:
|
|
1423
|
+
return ModelHandlerSourceConfig(
|
|
1424
|
+
handler_source_mode=mode,
|
|
1425
|
+
bootstrap_expires_at=expires_at,
|
|
1426
|
+
allow_bootstrap_override=allow_override_raw,
|
|
1427
|
+
)
|
|
1428
|
+
except ValidationError as e:
|
|
1429
|
+
# Check if error is due to naive datetime (no timezone info)
|
|
1430
|
+
error_messages = [err.get("msg", "") for err in e.errors()]
|
|
1431
|
+
if any("timezone-aware" in msg for msg in error_messages):
|
|
1432
|
+
logger.warning(
|
|
1433
|
+
"bootstrap_expires_at must be timezone-aware (UTC recommended). "
|
|
1434
|
+
"Naive datetime provided - falling back to no expiry. "
|
|
1435
|
+
"Use ISO format with timezone: '2026-02-01T00:00:00+00:00' "
|
|
1436
|
+
"or '2026-02-01T00:00:00Z'",
|
|
1437
|
+
extra={
|
|
1438
|
+
"invalid_value": expires_at_str,
|
|
1439
|
+
"parsed_datetime": str(expires_at) if expires_at else None,
|
|
1440
|
+
},
|
|
1441
|
+
)
|
|
1442
|
+
# Fall back to config without expiry
|
|
1443
|
+
return ModelHandlerSourceConfig(
|
|
1444
|
+
handler_source_mode=mode,
|
|
1445
|
+
bootstrap_expires_at=None,
|
|
1446
|
+
allow_bootstrap_override=allow_override_raw,
|
|
1447
|
+
)
|
|
1448
|
+
# Re-raise other validation errors
|
|
1449
|
+
raise
|
|
1450
|
+
|
|
1451
|
+
# Default: HYBRID mode with no expiry
|
|
1452
|
+
return ModelHandlerSourceConfig(
|
|
1453
|
+
handler_source_mode=EnumHandlerSourceMode.HYBRID
|
|
1454
|
+
)
|
|
1455
|
+
|
|
1456
|
+
async def _resolve_handler_descriptors(self) -> list[ModelHandlerDescriptor]:
|
|
1457
|
+
"""Resolve handler descriptors using the configured source mode.
|
|
1458
|
+
|
|
1459
|
+
Uses HandlerSourceResolver to discover handlers based on the configured
|
|
1460
|
+
mode (BOOTSTRAP, CONTRACT, or HYBRID). This replaces the previous
|
|
1461
|
+
sequential discovery logic with a unified, mode-driven approach.
|
|
1462
|
+
|
|
1463
|
+
Resolution Modes:
|
|
1464
|
+
- BOOTSTRAP: Only hardcoded bootstrap handlers
|
|
1465
|
+
- CONTRACT: Only filesystem contract-discovered handlers
|
|
1466
|
+
- HYBRID: Contract handlers win per-identity, bootstrap as fallback
|
|
1467
|
+
|
|
1468
|
+
Returns:
|
|
1469
|
+
List of resolved handler descriptors.
|
|
1470
|
+
|
|
1471
|
+
Raises:
|
|
1472
|
+
RuntimeHostError: If validation errors occur and fail-fast is enabled.
|
|
1473
|
+
|
|
1474
|
+
.. versionadded:: 0.7.0
|
|
1475
|
+
Part of OMN-1095 handler source mode integration.
|
|
1476
|
+
"""
|
|
1477
|
+
from omnibase_infra.runtime.handler_bootstrap_source import (
|
|
1478
|
+
HandlerBootstrapSource,
|
|
1479
|
+
)
|
|
1480
|
+
from omnibase_infra.runtime.handler_source_resolver import HandlerSourceResolver
|
|
1481
|
+
|
|
1482
|
+
source_config = self._load_handler_source_config()
|
|
1483
|
+
|
|
1484
|
+
logger.info(
|
|
1485
|
+
"Resolving handlers with source mode",
|
|
1486
|
+
extra={
|
|
1487
|
+
"mode": source_config.handler_source_mode.value,
|
|
1488
|
+
"effective_mode": source_config.effective_mode.value,
|
|
1489
|
+
"bootstrap_expires_at": str(source_config.bootstrap_expires_at)
|
|
1490
|
+
if source_config.bootstrap_expires_at
|
|
1491
|
+
else None,
|
|
1492
|
+
"is_bootstrap_expired": source_config.is_bootstrap_expired,
|
|
1493
|
+
},
|
|
1494
|
+
)
|
|
1495
|
+
|
|
1496
|
+
# Create bootstrap source
|
|
1497
|
+
bootstrap_source = HandlerBootstrapSource()
|
|
1498
|
+
|
|
1499
|
+
# Check for KAFKA_EVENTS mode first
|
|
1500
|
+
if source_config.effective_mode == EnumHandlerSourceMode.KAFKA_EVENTS:
|
|
1501
|
+
# Create Kafka-based contract source (cache-only beta)
|
|
1502
|
+
# Note: Kafka subscriptions are wired separately in _wire_baseline_subscriptions()
|
|
1503
|
+
environment = self._get_environment_from_config()
|
|
1504
|
+
kafka_source = KafkaContractSource(
|
|
1505
|
+
environment=environment,
|
|
1506
|
+
graceful_mode=True,
|
|
1507
|
+
)
|
|
1508
|
+
contract_source: ProtocolContractSource = kafka_source
|
|
1509
|
+
|
|
1510
|
+
# Store reference for subscription wiring
|
|
1511
|
+
self._kafka_contract_source = kafka_source
|
|
1512
|
+
|
|
1513
|
+
logger.info(
|
|
1514
|
+
"Using KafkaContractSource for contract discovery",
|
|
1515
|
+
extra={
|
|
1516
|
+
"environment": environment,
|
|
1517
|
+
"mode": "KAFKA_EVENTS",
|
|
1518
|
+
"correlation_id": str(kafka_source.correlation_id),
|
|
1519
|
+
},
|
|
1520
|
+
)
|
|
1521
|
+
# Contract source needs paths - use configured paths or default
|
|
1522
|
+
# If no contract_paths provided, reuse bootstrap_source as placeholder
|
|
1523
|
+
elif self._contract_paths:
|
|
1524
|
+
# Use PluginLoaderContractSource which uses the simpler contract schema
|
|
1525
|
+
# compatible with test contracts (handler_name, handler_class, handler_type)
|
|
1526
|
+
contract_source = PluginLoaderContractSource(
|
|
1527
|
+
contract_paths=self._contract_paths,
|
|
1528
|
+
)
|
|
1529
|
+
else:
|
|
1530
|
+
# No contract paths provided
|
|
1531
|
+
if source_config.effective_mode == EnumHandlerSourceMode.CONTRACT:
|
|
1532
|
+
# CONTRACT mode REQUIRES contract_paths - fail fast
|
|
1533
|
+
raise ProtocolConfigurationError(
|
|
1534
|
+
"CONTRACT mode requires contract_paths to be provided. "
|
|
1535
|
+
"Either provide contract_paths or use HYBRID/BOOTSTRAP mode.",
|
|
1536
|
+
context=ModelInfraErrorContext.with_correlation(
|
|
1537
|
+
transport_type=EnumInfraTransportType.RUNTIME,
|
|
1538
|
+
operation="resolve_handler_descriptors",
|
|
1539
|
+
),
|
|
1540
|
+
)
|
|
1541
|
+
# BOOTSTRAP or HYBRID mode without contract_paths - use bootstrap as fallback
|
|
1542
|
+
#
|
|
1543
|
+
# HYBRID MODE NOTE: When HYBRID mode is configured but no contract_paths
|
|
1544
|
+
# are provided, we reuse bootstrap_source for both the bootstrap_source
|
|
1545
|
+
# and contract_source parameters of HandlerSourceResolver. This means
|
|
1546
|
+
# discover_handlers() will be called twice on the same instance:
|
|
1547
|
+
# 1. Once as the "contract source" (returns bootstrap handlers)
|
|
1548
|
+
# 2. Once as the "bootstrap source" (returns same bootstrap handlers)
|
|
1549
|
+
#
|
|
1550
|
+
# This is intentional: HYBRID semantics require consulting both sources,
|
|
1551
|
+
# and with no contracts available, bootstrap provides all handlers.
|
|
1552
|
+
# The HandlerSourceResolver's HYBRID merge logic (contract wins per-identity,
|
|
1553
|
+
# bootstrap as fallback) produces the correct result since both sources
|
|
1554
|
+
# return identical handlers. The outcome is functionally equivalent to
|
|
1555
|
+
# BOOTSTRAP mode but maintains HYBRID logging/metrics for observability.
|
|
1556
|
+
#
|
|
1557
|
+
# DO NOT "optimize" this to skip the second call - it would break
|
|
1558
|
+
# metrics expectations (contract_handler_count would not be logged)
|
|
1559
|
+
# and change HYBRID mode semantics. See test_bootstrap_source_integration.py
|
|
1560
|
+
# test_bootstrap_source_called_during_start() for the verification test.
|
|
1561
|
+
logger.debug(
|
|
1562
|
+
"HYBRID mode: No contract_paths provided, using bootstrap source "
|
|
1563
|
+
"as fallback for contract source",
|
|
1564
|
+
extra={
|
|
1565
|
+
"mode": source_config.effective_mode.value,
|
|
1566
|
+
"behavior": "bootstrap_source_reused",
|
|
1567
|
+
},
|
|
1568
|
+
)
|
|
1569
|
+
contract_source = bootstrap_source
|
|
1570
|
+
|
|
1571
|
+
# Create resolver with the effective mode (handles expiry enforcement)
|
|
1572
|
+
resolver = HandlerSourceResolver(
|
|
1573
|
+
bootstrap_source=bootstrap_source,
|
|
1574
|
+
contract_source=contract_source,
|
|
1575
|
+
mode=source_config.effective_mode,
|
|
1576
|
+
allow_bootstrap_override=source_config.allow_bootstrap_override,
|
|
1577
|
+
)
|
|
1578
|
+
|
|
1579
|
+
# Resolve handlers
|
|
1580
|
+
result = await resolver.resolve_handlers()
|
|
1581
|
+
|
|
1582
|
+
# Log resolution results
|
|
1583
|
+
logger.info(
|
|
1584
|
+
"Handler resolution completed",
|
|
1585
|
+
extra={
|
|
1586
|
+
"descriptor_count": len(result.descriptors),
|
|
1587
|
+
"validation_error_count": len(result.validation_errors),
|
|
1588
|
+
"mode": source_config.effective_mode.value,
|
|
1589
|
+
},
|
|
1590
|
+
)
|
|
1591
|
+
|
|
1592
|
+
# Log validation errors but continue with valid descriptors (graceful degradation)
|
|
1593
|
+
# This allows the runtime to start with bootstrap handlers even if some contracts fail
|
|
1594
|
+
if result.validation_errors:
|
|
1595
|
+
error_summary = "; ".join(
|
|
1596
|
+
f"{e.handler_identity.handler_id or 'unknown'}: {e.message}"
|
|
1597
|
+
for e in result.validation_errors[:5] # Show first 5
|
|
1598
|
+
)
|
|
1599
|
+
if len(result.validation_errors) > 5:
|
|
1600
|
+
error_summary += f" ... and {len(result.validation_errors) - 5} more"
|
|
1601
|
+
|
|
1602
|
+
logger.warning(
|
|
1603
|
+
"Handler resolution completed with validation errors (continuing with valid handlers)",
|
|
1604
|
+
extra={
|
|
1605
|
+
"error_count": len(result.validation_errors),
|
|
1606
|
+
"valid_descriptor_count": len(result.descriptors),
|
|
1607
|
+
"error_summary": error_summary,
|
|
1608
|
+
},
|
|
1609
|
+
)
|
|
1610
|
+
|
|
1611
|
+
return list(result.descriptors)
|
|
1612
|
+
|
|
1613
|
+
async def _discover_or_wire_handlers(self) -> None:
|
|
1614
|
+
"""Discover and register handlers for the runtime.
|
|
1615
|
+
|
|
1616
|
+
This method implements the handler discovery/wiring step (Step 3) of the
|
|
1617
|
+
start() sequence. It uses HandlerSourceResolver to discover handlers
|
|
1618
|
+
based on the configured source mode.
|
|
1619
|
+
|
|
1620
|
+
Handler Source Modes (OMN-1095):
|
|
1621
|
+
- BOOTSTRAP: Only hardcoded bootstrap handlers (fast, no filesystem I/O)
|
|
1622
|
+
- CONTRACT: Only filesystem contract-discovered handlers
|
|
1623
|
+
- HYBRID: Contract handlers win per-identity, bootstrap as fallback
|
|
1624
|
+
|
|
1625
|
+
The mode is configured via runtime config:
|
|
1626
|
+
handler_source:
|
|
1627
|
+
mode: "hybrid" # bootstrap|contract|hybrid
|
|
1628
|
+
bootstrap_expires_at: "2026-02-01T00:00:00Z" # Optional, UTC
|
|
1629
|
+
|
|
1630
|
+
The discovery/wiring step registers handler CLASSES with the handler registry.
|
|
1631
|
+
The subsequent _populate_handlers_from_registry() step instantiates and
|
|
1632
|
+
initializes these handler classes.
|
|
1633
|
+
|
|
1634
|
+
.. versionchanged:: 0.7.0
|
|
1635
|
+
Replaced sequential bootstrap+contract discovery with unified
|
|
1636
|
+
HandlerSourceResolver-based resolution (OMN-1095).
|
|
1637
|
+
"""
|
|
1638
|
+
# Resolve handlers using configured source mode
|
|
1639
|
+
descriptors = await self._resolve_handler_descriptors()
|
|
1640
|
+
|
|
1641
|
+
# Get handler registry for registration
|
|
1642
|
+
handler_registry = await self._get_handler_registry()
|
|
1643
|
+
|
|
1644
|
+
registered_count = 0
|
|
1645
|
+
error_count = 0
|
|
1646
|
+
|
|
1647
|
+
for descriptor in descriptors:
|
|
1648
|
+
try:
|
|
1649
|
+
# Extract protocol type from handler_id
|
|
1650
|
+
# Handler IDs use "proto." prefix for identity matching (e.g., "proto.consul" -> "consul")
|
|
1651
|
+
# Contract handlers also use this prefix for HYBRID mode resolution
|
|
1652
|
+
# removeprefix() is a no-op if prefix doesn't exist, so handlers without prefix keep their name as-is
|
|
1653
|
+
protocol_type = descriptor.handler_id.removeprefix(
|
|
1654
|
+
f"{HANDLER_IDENTITY_PREFIX}."
|
|
1655
|
+
)
|
|
1656
|
+
|
|
1657
|
+
# Import the handler class from fully qualified path
|
|
1658
|
+
handler_class_path = descriptor.handler_class
|
|
1659
|
+
if handler_class_path is None:
|
|
1660
|
+
logger.warning(
|
|
1661
|
+
"Handler descriptor missing handler_class, skipping",
|
|
1662
|
+
extra={
|
|
1663
|
+
"handler_id": descriptor.handler_id,
|
|
1664
|
+
"handler_name": descriptor.name,
|
|
1665
|
+
},
|
|
1666
|
+
)
|
|
1667
|
+
error_count += 1
|
|
1668
|
+
continue
|
|
1669
|
+
|
|
1670
|
+
# Import class using rsplit pattern
|
|
1671
|
+
if "." not in handler_class_path:
|
|
1672
|
+
logger.error(
|
|
1673
|
+
"Invalid handler class path (must be fully qualified): %s",
|
|
1674
|
+
handler_class_path,
|
|
1675
|
+
extra={"handler_id": descriptor.handler_id},
|
|
1676
|
+
)
|
|
1677
|
+
error_count += 1
|
|
1678
|
+
continue
|
|
1679
|
+
|
|
1680
|
+
module_path, class_name = handler_class_path.rsplit(".", 1)
|
|
1681
|
+
module = importlib.import_module(module_path)
|
|
1682
|
+
handler_cls = getattr(module, class_name)
|
|
1683
|
+
|
|
1684
|
+
# Verify handler_cls is actually a class before registration
|
|
1685
|
+
if not isinstance(handler_cls, type):
|
|
1686
|
+
logger.error(
|
|
1687
|
+
"Handler class path does not resolve to a class type",
|
|
1688
|
+
extra={
|
|
1689
|
+
"handler_id": descriptor.handler_id,
|
|
1690
|
+
"handler_class_path": handler_class_path,
|
|
1691
|
+
"resolved_type": type(handler_cls).__name__,
|
|
1692
|
+
},
|
|
1693
|
+
)
|
|
1694
|
+
error_count += 1
|
|
1695
|
+
continue
|
|
1696
|
+
|
|
1697
|
+
# Register with handler registry
|
|
1698
|
+
handler_registry.register(protocol_type, handler_cls)
|
|
1699
|
+
|
|
1700
|
+
# Store descriptor for later use during initialization
|
|
1701
|
+
self._handler_descriptors[protocol_type] = descriptor
|
|
1702
|
+
|
|
1703
|
+
registered_count += 1
|
|
1704
|
+
logger.debug(
|
|
1705
|
+
"Registered handler from descriptor",
|
|
1706
|
+
extra={
|
|
1707
|
+
"handler_id": descriptor.handler_id,
|
|
1708
|
+
"protocol_type": protocol_type,
|
|
1709
|
+
"handler_class": handler_class_path,
|
|
1710
|
+
},
|
|
1711
|
+
)
|
|
1712
|
+
|
|
1713
|
+
except (ImportError, AttributeError):
|
|
1714
|
+
logger.exception(
|
|
1715
|
+
"Failed to import handler",
|
|
1716
|
+
extra={
|
|
1717
|
+
"handler_id": descriptor.handler_id,
|
|
1718
|
+
"handler_class": descriptor.handler_class,
|
|
1719
|
+
},
|
|
1720
|
+
)
|
|
1721
|
+
error_count += 1
|
|
1722
|
+
except Exception:
|
|
1723
|
+
logger.exception(
|
|
1724
|
+
"Unexpected error registering handler",
|
|
1725
|
+
extra={
|
|
1726
|
+
"handler_id": descriptor.handler_id,
|
|
1727
|
+
"handler_class": descriptor.handler_class,
|
|
1728
|
+
},
|
|
1729
|
+
)
|
|
1730
|
+
error_count += 1
|
|
1731
|
+
|
|
1732
|
+
logger.info(
|
|
1733
|
+
"Handler discovery completed",
|
|
1734
|
+
extra={
|
|
1735
|
+
"registered_count": registered_count,
|
|
1736
|
+
"error_count": error_count,
|
|
1737
|
+
"total_descriptors": len(descriptors),
|
|
1738
|
+
},
|
|
1739
|
+
)
|
|
1740
|
+
|
|
1741
|
+
async def _populate_handlers_from_registry(self) -> None:
|
|
1742
|
+
"""Populate self._handlers from handler registry (container or singleton).
|
|
1743
|
+
|
|
1744
|
+
This method bridges the gap between the wiring module (which registers
|
|
1745
|
+
handler CLASSES to the registry) and the RuntimeHostProcess
|
|
1746
|
+
(which needs handler INSTANCES in self._handlers for routing).
|
|
1747
|
+
|
|
1748
|
+
Registry Resolution:
|
|
1749
|
+
- If handler_registry provided: Uses pre-resolved registry
|
|
1750
|
+
- If no handler_registry: Falls back to singleton get_handler_registry()
|
|
1751
|
+
|
|
1752
|
+
For each registered handler type in the registry:
|
|
1753
|
+
1. Skip if handler type is already registered (e.g., by tests)
|
|
1754
|
+
2. Get the handler class from the registry
|
|
1755
|
+
3. Instantiate the handler class
|
|
1756
|
+
4. Call initialize() on the handler instance with self._config
|
|
1757
|
+
5. Store the handler instance in self._handlers
|
|
1758
|
+
|
|
1759
|
+
This ensures that after start() is called, self._handlers contains
|
|
1760
|
+
fully initialized handler instances ready for envelope routing.
|
|
1761
|
+
|
|
1762
|
+
Note: Handlers already in self._handlers (e.g., injected by tests via
|
|
1763
|
+
register_handler() or patch.object()) are preserved and not overwritten.
|
|
1764
|
+
"""
|
|
1765
|
+
# Get handler registry (pre-resolved, container, or singleton)
|
|
1766
|
+
handler_registry = await self._get_handler_registry()
|
|
1767
|
+
registered_types = handler_registry.list_protocols()
|
|
1768
|
+
|
|
1769
|
+
logger.debug(
|
|
1770
|
+
"Populating handlers from registry",
|
|
1771
|
+
extra={
|
|
1772
|
+
"registered_types": registered_types,
|
|
1773
|
+
"existing_handlers": list(self._handlers.keys()),
|
|
1774
|
+
},
|
|
1775
|
+
)
|
|
1776
|
+
|
|
1777
|
+
# Get or create container once for all handlers to share
|
|
1778
|
+
# This ensures all handlers have access to the same DI container
|
|
1779
|
+
container = self._get_or_create_container()
|
|
1780
|
+
|
|
1781
|
+
for handler_type in registered_types:
|
|
1782
|
+
# Skip if handler is already registered (e.g., by tests or explicit registration)
|
|
1783
|
+
if handler_type in self._handlers:
|
|
1784
|
+
logger.debug(
|
|
1785
|
+
"Handler already registered, skipping",
|
|
1786
|
+
extra={
|
|
1787
|
+
"handler_type": handler_type,
|
|
1788
|
+
"existing_handler_class": type(
|
|
1789
|
+
self._handlers[handler_type]
|
|
1790
|
+
).__name__,
|
|
1791
|
+
},
|
|
1792
|
+
)
|
|
1793
|
+
continue
|
|
1794
|
+
|
|
1795
|
+
try:
|
|
1796
|
+
# Get handler class from singleton registry
|
|
1797
|
+
handler_cls: type[ProtocolContainerAware] = handler_registry.get(
|
|
1798
|
+
handler_type
|
|
1799
|
+
)
|
|
1800
|
+
|
|
1801
|
+
# Instantiate the handler with container for dependency injection
|
|
1802
|
+
# ProtocolContainerAware defines __init__(container: ModelONEXContainer)
|
|
1803
|
+
handler_instance: ProtocolContainerAware = handler_cls(
|
|
1804
|
+
container=container
|
|
1805
|
+
)
|
|
1806
|
+
|
|
1807
|
+
# Call initialize() if the handler has this method
|
|
1808
|
+
# Handlers may require async initialization with config
|
|
1809
|
+
if hasattr(handler_instance, "initialize"):
|
|
1810
|
+
# Build effective config: contract config as base, runtime overrides on top
|
|
1811
|
+
# This enables contracts to provide handler-specific defaults while
|
|
1812
|
+
# allowing runtime/deploy-time customization without touching contracts
|
|
1813
|
+
effective_config: dict[str, object] = {}
|
|
1814
|
+
config_source = "runtime_only"
|
|
1815
|
+
|
|
1816
|
+
# Layer 1: Contract config as baseline (if descriptor exists with config)
|
|
1817
|
+
descriptor = self._handler_descriptors.get(handler_type)
|
|
1818
|
+
if descriptor and descriptor.contract_config:
|
|
1819
|
+
effective_config.update(descriptor.contract_config)
|
|
1820
|
+
config_source = "contract_only"
|
|
1821
|
+
|
|
1822
|
+
# Layer 2: Runtime config overrides
|
|
1823
|
+
# Runtime config takes precedence, enabling deploy-time customization
|
|
1824
|
+
if self._config:
|
|
1825
|
+
effective_config.update(self._config)
|
|
1826
|
+
if descriptor and descriptor.contract_config:
|
|
1827
|
+
config_source = "contract+runtime_override"
|
|
1828
|
+
|
|
1829
|
+
# Pass empty dict if no config, not None
|
|
1830
|
+
# Handlers expect dict interface (e.g., config.get("key"))
|
|
1831
|
+
await handler_instance.initialize(effective_config)
|
|
1832
|
+
|
|
1833
|
+
logger.debug(
|
|
1834
|
+
"Handler initialized with effective config",
|
|
1835
|
+
extra={
|
|
1836
|
+
"handler_type": handler_type,
|
|
1837
|
+
"config_source": config_source,
|
|
1838
|
+
"effective_config_keys": list(effective_config.keys()),
|
|
1839
|
+
"has_contract_config": bool(
|
|
1840
|
+
descriptor and descriptor.contract_config
|
|
1841
|
+
),
|
|
1842
|
+
"has_runtime_config": bool(self._config),
|
|
1843
|
+
},
|
|
1844
|
+
)
|
|
1845
|
+
|
|
1846
|
+
# Store the handler instance for routing
|
|
1847
|
+
self._handlers[handler_type] = handler_instance
|
|
1848
|
+
|
|
1849
|
+
logger.debug(
|
|
1850
|
+
"Handler instantiated and initialized",
|
|
1851
|
+
extra={
|
|
1852
|
+
"handler_type": handler_type,
|
|
1853
|
+
"handler_class": handler_cls.__name__,
|
|
1854
|
+
},
|
|
1855
|
+
)
|
|
1856
|
+
|
|
1857
|
+
except Exception as e:
|
|
1858
|
+
# Track the failure for health_check() reporting
|
|
1859
|
+
self._failed_handlers[handler_type] = str(e)
|
|
1860
|
+
|
|
1861
|
+
# Log error but continue with other handlers
|
|
1862
|
+
# This allows partial handler availability
|
|
1863
|
+
correlation_id = uuid4()
|
|
1864
|
+
context = ModelInfraErrorContext(
|
|
1865
|
+
transport_type=EnumInfraTransportType.RUNTIME,
|
|
1866
|
+
operation="populate_handlers",
|
|
1867
|
+
target_name=handler_type,
|
|
1868
|
+
correlation_id=correlation_id,
|
|
1869
|
+
)
|
|
1870
|
+
infra_error = RuntimeHostError(
|
|
1871
|
+
f"Failed to instantiate handler for type {handler_type}: {e}",
|
|
1872
|
+
context=context,
|
|
1873
|
+
)
|
|
1874
|
+
infra_error.__cause__ = e
|
|
1875
|
+
|
|
1876
|
+
logger.warning(
|
|
1877
|
+
"Failed to instantiate handler, skipping",
|
|
1878
|
+
extra={
|
|
1879
|
+
"handler_type": handler_type,
|
|
1880
|
+
"error": str(e),
|
|
1881
|
+
"correlation_id": str(correlation_id),
|
|
1882
|
+
},
|
|
1883
|
+
)
|
|
1884
|
+
|
|
1885
|
+
logger.info(
|
|
1886
|
+
"Handlers populated from registry",
|
|
1887
|
+
extra={
|
|
1888
|
+
"populated_handlers": list(self._handlers.keys()),
|
|
1889
|
+
"total_count": len(self._handlers),
|
|
1890
|
+
},
|
|
1891
|
+
)
|
|
1892
|
+
|
|
1893
|
+
async def _load_contract_configs(self, correlation_id: UUID) -> None:
|
|
1894
|
+
"""Load contract configurations from all discovered contracts.
|
|
1895
|
+
|
|
1896
|
+
Uses RuntimeContractConfigLoader to scan for contract.yaml files and
|
|
1897
|
+
load handler_routing and operation_bindings subcontracts into a
|
|
1898
|
+
consolidated configuration.
|
|
1899
|
+
|
|
1900
|
+
This method is called during start() after handler discovery but before
|
|
1901
|
+
event bus subscriptions are wired. The loaded config is stored in
|
|
1902
|
+
self._contract_config and accessible via the contract_config property.
|
|
1903
|
+
|
|
1904
|
+
Error Handling:
|
|
1905
|
+
Individual contract load failures are logged but do not stop the
|
|
1906
|
+
overall loading process. This enables graceful degradation where
|
|
1907
|
+
some contracts can be loaded even if others fail. Errors are
|
|
1908
|
+
collected in the ModelRuntimeContractConfig for introspection.
|
|
1909
|
+
|
|
1910
|
+
Args:
|
|
1911
|
+
correlation_id: Correlation ID for tracing this load operation.
|
|
1912
|
+
|
|
1913
|
+
Part of OMN-1519: Runtime contract config loader integration.
|
|
1914
|
+
"""
|
|
1915
|
+
# Skip if no contract paths configured
|
|
1916
|
+
if not self._contract_paths:
|
|
1917
|
+
logger.debug(
|
|
1918
|
+
"No contract paths configured, skipping contract config loading",
|
|
1919
|
+
extra={"correlation_id": str(correlation_id)},
|
|
1920
|
+
)
|
|
1921
|
+
return
|
|
1922
|
+
|
|
1923
|
+
# Create loader - no namespace restrictions by default
|
|
1924
|
+
# (namespace allowlisting can be added via constructor parameter if needed)
|
|
1925
|
+
loader = RuntimeContractConfigLoader()
|
|
1926
|
+
|
|
1927
|
+
# Load all contracts from configured paths
|
|
1928
|
+
self._contract_config = loader.load_all_contracts(
|
|
1929
|
+
search_paths=self._contract_paths,
|
|
1930
|
+
correlation_id=correlation_id,
|
|
1931
|
+
)
|
|
1932
|
+
|
|
1933
|
+
# Log summary at INFO level
|
|
1934
|
+
if self._contract_config.total_errors > 0:
|
|
1935
|
+
logger.warning(
|
|
1936
|
+
"Contract config loading completed with errors",
|
|
1937
|
+
extra={
|
|
1938
|
+
"total_contracts_found": self._contract_config.total_contracts_found,
|
|
1939
|
+
"total_contracts_loaded": self._contract_config.total_contracts_loaded,
|
|
1940
|
+
"total_errors": self._contract_config.total_errors,
|
|
1941
|
+
"success_rate": f"{self._contract_config.success_rate:.1%}",
|
|
1942
|
+
"correlation_id": str(correlation_id),
|
|
1943
|
+
"error_paths": [
|
|
1944
|
+
str(p) for p in self._contract_config.error_messages
|
|
1945
|
+
],
|
|
1946
|
+
},
|
|
1947
|
+
)
|
|
1948
|
+
else:
|
|
1949
|
+
logger.info(
|
|
1950
|
+
"Contract config loading completed successfully",
|
|
1951
|
+
extra={
|
|
1952
|
+
"total_contracts_found": self._contract_config.total_contracts_found,
|
|
1953
|
+
"total_contracts_loaded": self._contract_config.total_contracts_loaded,
|
|
1954
|
+
"handler_routing_count": len(
|
|
1955
|
+
self._contract_config.handler_routing_configs
|
|
1956
|
+
),
|
|
1957
|
+
"operation_bindings_count": len(
|
|
1958
|
+
self._contract_config.operation_bindings_configs
|
|
1959
|
+
),
|
|
1960
|
+
"correlation_id": str(correlation_id),
|
|
1961
|
+
},
|
|
1962
|
+
)
|
|
1963
|
+
|
|
1964
|
+
async def _get_handler_registry(self) -> RegistryProtocolBinding:
|
|
1965
|
+
"""Get handler registry (pre-resolved, container, or singleton).
|
|
1966
|
+
|
|
1967
|
+
Resolution order:
|
|
1968
|
+
1. If handler_registry was provided to __init__, uses it (cached)
|
|
1969
|
+
2. If container was provided and has RegistryProtocolBinding, resolves from container
|
|
1970
|
+
3. Falls back to singleton via get_handler_registry()
|
|
1971
|
+
|
|
1972
|
+
Caching Behavior:
|
|
1973
|
+
The resolved registry is cached after the first successful resolution.
|
|
1974
|
+
Subsequent calls return the cached instance without re-resolving from
|
|
1975
|
+
the container or re-fetching the singleton. This ensures consistent
|
|
1976
|
+
registry usage throughout the runtime's lifecycle and avoids redundant
|
|
1977
|
+
resolution operations.
|
|
1978
|
+
|
|
1979
|
+
Returns:
|
|
1980
|
+
RegistryProtocolBinding instance.
|
|
1981
|
+
"""
|
|
1982
|
+
if self._handler_registry is not None:
|
|
1983
|
+
# Use pre-resolved registry from constructor
|
|
1984
|
+
return self._handler_registry
|
|
1985
|
+
|
|
1986
|
+
# Try to resolve from container if provided
|
|
1987
|
+
if self._container is not None and self._container.service_registry is not None:
|
|
1988
|
+
try:
|
|
1989
|
+
resolved_registry: RegistryProtocolBinding = (
|
|
1990
|
+
await self._container.service_registry.resolve_service(
|
|
1991
|
+
RegistryProtocolBinding
|
|
1992
|
+
)
|
|
1993
|
+
)
|
|
1994
|
+
# Cache the resolved registry for subsequent calls
|
|
1995
|
+
self._handler_registry = resolved_registry
|
|
1996
|
+
logger.debug(
|
|
1997
|
+
"Handler registry resolved from container",
|
|
1998
|
+
extra={"registry_type": type(resolved_registry).__name__},
|
|
1999
|
+
)
|
|
2000
|
+
return resolved_registry
|
|
2001
|
+
except (
|
|
2002
|
+
RuntimeError,
|
|
2003
|
+
ValueError,
|
|
2004
|
+
KeyError,
|
|
2005
|
+
AttributeError,
|
|
2006
|
+
LookupError,
|
|
2007
|
+
) as e:
|
|
2008
|
+
# Container resolution failed, fall through to singleton
|
|
2009
|
+
logger.debug(
|
|
2010
|
+
"Container registry resolution failed, falling back to singleton",
|
|
2011
|
+
extra={"error": str(e)},
|
|
2012
|
+
)
|
|
2013
|
+
|
|
2014
|
+
# Graceful degradation: fall back to singleton pattern when container unavailable
|
|
2015
|
+
from omnibase_infra.runtime.handler_registry import get_handler_registry
|
|
2016
|
+
|
|
2017
|
+
singleton_registry = get_handler_registry()
|
|
2018
|
+
# Cache for consistency with container resolution path
|
|
2019
|
+
self._handler_registry = singleton_registry
|
|
2020
|
+
logger.debug(
|
|
2021
|
+
"Handler registry resolved from singleton",
|
|
2022
|
+
extra={"registry_type": type(singleton_registry).__name__},
|
|
2023
|
+
)
|
|
2024
|
+
return singleton_registry
|
|
2025
|
+
|
|
2026
|
+
async def _on_message(self, message: ModelEventMessage) -> None:
|
|
2027
|
+
"""Handle incoming message from event bus subscription.
|
|
2028
|
+
|
|
2029
|
+
This is the callback invoked by the event bus when a message arrives
|
|
2030
|
+
on the input topic. It deserializes the envelope and routes it.
|
|
2031
|
+
|
|
2032
|
+
The method tracks pending messages for graceful shutdown support (OMN-756).
|
|
2033
|
+
The pending message count is incremented at the start of processing and
|
|
2034
|
+
decremented when processing completes (success or failure).
|
|
2035
|
+
|
|
2036
|
+
Args:
|
|
2037
|
+
message: The event message containing the envelope payload.
|
|
2038
|
+
"""
|
|
2039
|
+
# Increment pending message count (OMN-756: graceful shutdown tracking)
|
|
2040
|
+
async with self._pending_lock:
|
|
2041
|
+
self._pending_message_count += 1
|
|
2042
|
+
|
|
2043
|
+
try:
|
|
2044
|
+
# Deserialize envelope from message value
|
|
2045
|
+
envelope = json.loads(message.value.decode("utf-8"))
|
|
2046
|
+
await self._handle_envelope(envelope)
|
|
2047
|
+
except json.JSONDecodeError as e:
|
|
2048
|
+
# Create infrastructure error context for tracing
|
|
2049
|
+
correlation_id = uuid4()
|
|
2050
|
+
context = ModelInfraErrorContext(
|
|
2051
|
+
transport_type=EnumInfraTransportType.RUNTIME,
|
|
2052
|
+
operation="decode_envelope",
|
|
2053
|
+
target_name=message.topic,
|
|
2054
|
+
correlation_id=correlation_id,
|
|
2055
|
+
)
|
|
2056
|
+
# Chain the error with infrastructure context
|
|
2057
|
+
infra_error = RuntimeHostError(
|
|
2058
|
+
f"Failed to decode JSON envelope from message: {e}",
|
|
2059
|
+
context=context,
|
|
2060
|
+
)
|
|
2061
|
+
infra_error.__cause__ = e # Proper error chaining
|
|
2062
|
+
|
|
2063
|
+
logger.exception(
|
|
2064
|
+
"Failed to decode envelope from message",
|
|
2065
|
+
extra={
|
|
2066
|
+
"error": str(e),
|
|
2067
|
+
"topic": message.topic,
|
|
2068
|
+
"offset": message.offset,
|
|
2069
|
+
"correlation_id": str(correlation_id),
|
|
2070
|
+
},
|
|
2071
|
+
)
|
|
2072
|
+
# Publish error response for malformed messages
|
|
2073
|
+
error_response = self._create_error_response(
|
|
2074
|
+
error=f"Invalid JSON in message: {e}",
|
|
2075
|
+
correlation_id=correlation_id,
|
|
2076
|
+
)
|
|
2077
|
+
await self._publish_envelope_safe(error_response, self._output_topic)
|
|
2078
|
+
finally:
|
|
2079
|
+
# Decrement pending message count (OMN-756: graceful shutdown tracking)
|
|
2080
|
+
async with self._pending_lock:
|
|
2081
|
+
self._pending_message_count -= 1
|
|
2082
|
+
|
|
2083
|
+
async def _handle_envelope(self, envelope: dict[str, object]) -> None:
|
|
2084
|
+
"""Route envelope to appropriate handler.
|
|
2085
|
+
|
|
2086
|
+
Validates envelope before dispatch and routes it to the appropriate
|
|
2087
|
+
registered handler. Publishes the response to the output topic.
|
|
2088
|
+
|
|
2089
|
+
Validation (performed before dispatch):
|
|
2090
|
+
1. Operation presence and type validation
|
|
2091
|
+
2. Handler prefix validation against registry
|
|
2092
|
+
3. Payload requirement validation for specific operations
|
|
2093
|
+
4. Correlation ID normalization to UUID
|
|
2094
|
+
|
|
2095
|
+
Args:
|
|
2096
|
+
envelope: Dict with 'operation', 'payload', optional 'correlation_id',
|
|
2097
|
+
and 'handler_type'.
|
|
2098
|
+
"""
|
|
2099
|
+
# Pre-validation: Get correlation_id for error responses if validation fails
|
|
2100
|
+
# This handles the case where validation itself throws before normalizing
|
|
2101
|
+
pre_validation_correlation_id = normalize_correlation_id(
|
|
2102
|
+
envelope.get("correlation_id")
|
|
2103
|
+
)
|
|
2104
|
+
|
|
2105
|
+
# Step 1: Validate envelope BEFORE dispatch
|
|
2106
|
+
# This validates operation, prefix, payload requirements, and normalizes correlation_id
|
|
2107
|
+
try:
|
|
2108
|
+
validate_envelope(envelope, await self._get_handler_registry())
|
|
2109
|
+
except EnvelopeValidationError as e:
|
|
2110
|
+
# Validation failed - missing operation or payload
|
|
2111
|
+
error_response = self._create_error_response(
|
|
2112
|
+
error=str(e),
|
|
2113
|
+
correlation_id=pre_validation_correlation_id,
|
|
2114
|
+
)
|
|
2115
|
+
await self._publish_envelope_safe(error_response, self._output_topic)
|
|
2116
|
+
logger.warning(
|
|
2117
|
+
"Envelope validation failed",
|
|
2118
|
+
extra={
|
|
2119
|
+
"error": str(e),
|
|
2120
|
+
"correlation_id": str(pre_validation_correlation_id),
|
|
2121
|
+
"error_type": "EnvelopeValidationError",
|
|
2122
|
+
},
|
|
2123
|
+
)
|
|
2124
|
+
return
|
|
2125
|
+
except UnknownHandlerTypeError as e:
|
|
2126
|
+
# Unknown handler prefix - hard failure
|
|
2127
|
+
error_response = self._create_error_response(
|
|
2128
|
+
error=str(e),
|
|
2129
|
+
correlation_id=pre_validation_correlation_id,
|
|
2130
|
+
)
|
|
2131
|
+
await self._publish_envelope_safe(error_response, self._output_topic)
|
|
2132
|
+
logger.warning(
|
|
2133
|
+
"Unknown handler type in envelope",
|
|
2134
|
+
extra={
|
|
2135
|
+
"error": str(e),
|
|
2136
|
+
"correlation_id": str(pre_validation_correlation_id),
|
|
2137
|
+
"error_type": "UnknownHandlerTypeError",
|
|
2138
|
+
},
|
|
2139
|
+
)
|
|
2140
|
+
return
|
|
2141
|
+
|
|
2142
|
+
# After validation, correlation_id is guaranteed to be a UUID
|
|
2143
|
+
correlation_id = envelope.get("correlation_id")
|
|
2144
|
+
if not isinstance(correlation_id, UUID):
|
|
2145
|
+
correlation_id = pre_validation_correlation_id
|
|
2146
|
+
|
|
2147
|
+
# Step 2: Check idempotency before handler dispatch (OMN-945)
|
|
2148
|
+
# This prevents duplicate processing under at-least-once delivery
|
|
2149
|
+
if not await self._check_idempotency(envelope, correlation_id):
|
|
2150
|
+
# Duplicate detected - response already published, return early
|
|
2151
|
+
return
|
|
2152
|
+
|
|
2153
|
+
# Extract operation (validated to exist and be a string)
|
|
2154
|
+
operation = str(envelope.get("operation"))
|
|
2155
|
+
|
|
2156
|
+
# Determine handler_type from envelope
|
|
2157
|
+
# If handler_type not explicit, extract from operation (e.g., "http.get" -> "http")
|
|
2158
|
+
handler_type = envelope.get("handler_type")
|
|
2159
|
+
if handler_type is None:
|
|
2160
|
+
handler_type = operation.split(".")[0]
|
|
2161
|
+
|
|
2162
|
+
# Get handler from registry
|
|
2163
|
+
handler = self._handlers.get(str(handler_type))
|
|
2164
|
+
|
|
2165
|
+
if handler is None:
|
|
2166
|
+
# Handler not instantiated (different from unknown prefix - validation already passed)
|
|
2167
|
+
# This can happen if handler registration failed during start()
|
|
2168
|
+
context = ModelInfraErrorContext(
|
|
2169
|
+
transport_type=EnumInfraTransportType.RUNTIME,
|
|
2170
|
+
operation=str(operation),
|
|
2171
|
+
target_name=str(handler_type),
|
|
2172
|
+
correlation_id=correlation_id,
|
|
2173
|
+
)
|
|
2174
|
+
|
|
2175
|
+
# Create structured error for logging and tracking
|
|
2176
|
+
routing_error = RuntimeHostError(
|
|
2177
|
+
f"Handler type {handler_type!r} is registered but not instantiated",
|
|
2178
|
+
context=context,
|
|
2179
|
+
)
|
|
2180
|
+
|
|
2181
|
+
# Publish error response for envelope-based error handling
|
|
2182
|
+
error_response = self._create_error_response(
|
|
2183
|
+
error=str(routing_error),
|
|
2184
|
+
correlation_id=correlation_id,
|
|
2185
|
+
)
|
|
2186
|
+
await self._publish_envelope_safe(error_response, self._output_topic)
|
|
2187
|
+
|
|
2188
|
+
# Log with structured error
|
|
2189
|
+
logger.warning(
|
|
2190
|
+
"Handler registered but not instantiated",
|
|
2191
|
+
extra={
|
|
2192
|
+
"handler_type": handler_type,
|
|
2193
|
+
"correlation_id": str(correlation_id),
|
|
2194
|
+
"operation": operation,
|
|
2195
|
+
"registered_handlers": list(self._handlers.keys()),
|
|
2196
|
+
"error": str(routing_error),
|
|
2197
|
+
},
|
|
2198
|
+
)
|
|
2199
|
+
return
|
|
2200
|
+
|
|
2201
|
+
# Execute handler
|
|
2202
|
+
try:
|
|
2203
|
+
# Handler expected to have async execute(envelope) method
|
|
2204
|
+
# NOTE: MVP adapters use legacy execute(envelope: dict) signature.
|
|
2205
|
+
# TODO(OMN-40): Migrate handlers to new protocol signature execute(request, operation_config)
|
|
2206
|
+
response = await handler.execute(envelope) # type: ignore[call-arg] # NOTE: legacy signature
|
|
2207
|
+
|
|
2208
|
+
# Ensure response has correlation_id
|
|
2209
|
+
# Make a copy to avoid mutating handler's internal state
|
|
2210
|
+
if isinstance(response, dict):
|
|
2211
|
+
response = dict(response)
|
|
2212
|
+
if "correlation_id" not in response:
|
|
2213
|
+
response["correlation_id"] = correlation_id
|
|
2214
|
+
|
|
2215
|
+
await self._publish_envelope_safe(response, self._output_topic)
|
|
2216
|
+
|
|
2217
|
+
logger.debug(
|
|
2218
|
+
"Handler executed successfully",
|
|
2219
|
+
extra={
|
|
2220
|
+
"handler_type": handler_type,
|
|
2221
|
+
"correlation_id": str(correlation_id),
|
|
2222
|
+
"operation": operation,
|
|
2223
|
+
},
|
|
2224
|
+
)
|
|
2225
|
+
|
|
2226
|
+
except Exception as e:
|
|
2227
|
+
# Create infrastructure error context for handler execution failure
|
|
2228
|
+
context = ModelInfraErrorContext(
|
|
2229
|
+
transport_type=EnumInfraTransportType.RUNTIME,
|
|
2230
|
+
operation="handler_execution",
|
|
2231
|
+
target_name=str(handler_type),
|
|
2232
|
+
correlation_id=correlation_id,
|
|
2233
|
+
)
|
|
2234
|
+
# Chain the error with infrastructure context
|
|
2235
|
+
infra_error = RuntimeHostError(
|
|
2236
|
+
f"Handler execution failed for {handler_type}: {e}",
|
|
2237
|
+
context=context,
|
|
2238
|
+
)
|
|
2239
|
+
infra_error.__cause__ = e # Proper error chaining
|
|
2240
|
+
|
|
2241
|
+
# Handler execution failed - produce failure envelope
|
|
2242
|
+
error_response = self._create_error_response(
|
|
2243
|
+
error=str(e),
|
|
2244
|
+
correlation_id=correlation_id,
|
|
2245
|
+
)
|
|
2246
|
+
await self._publish_envelope_safe(error_response, self._output_topic)
|
|
2247
|
+
|
|
2248
|
+
logger.exception(
|
|
2249
|
+
"Handler execution failed",
|
|
2250
|
+
extra={
|
|
2251
|
+
"handler_type": handler_type,
|
|
2252
|
+
"correlation_id": str(correlation_id),
|
|
2253
|
+
"operation": operation,
|
|
2254
|
+
"error": str(e),
|
|
2255
|
+
"infra_error": str(infra_error),
|
|
2256
|
+
},
|
|
2257
|
+
)
|
|
2258
|
+
|
|
2259
|
+
def _create_error_response(
|
|
2260
|
+
self,
|
|
2261
|
+
error: str,
|
|
2262
|
+
correlation_id: UUID | None,
|
|
2263
|
+
) -> dict[str, object]:
|
|
2264
|
+
"""Create a standardized error response envelope.
|
|
2265
|
+
|
|
2266
|
+
Args:
|
|
2267
|
+
error: Error message to include.
|
|
2268
|
+
correlation_id: Correlation ID to preserve for tracking.
|
|
2269
|
+
|
|
2270
|
+
Returns:
|
|
2271
|
+
Error response dict with success=False and error details.
|
|
2272
|
+
"""
|
|
2273
|
+
# Use correlation_id or generate a new one, keeping as UUID for internal use
|
|
2274
|
+
final_correlation_id = correlation_id or uuid4()
|
|
2275
|
+
return {
|
|
2276
|
+
"success": False,
|
|
2277
|
+
"status": "error",
|
|
2278
|
+
"error": error,
|
|
2279
|
+
"correlation_id": final_correlation_id,
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
def _serialize_envelope(
|
|
2283
|
+
self, envelope: dict[str, object] | BaseModel
|
|
2284
|
+
) -> dict[str, object]:
|
|
2285
|
+
"""Recursively convert UUID objects to strings for JSON serialization.
|
|
2286
|
+
|
|
2287
|
+
Handles both dict envelopes and Pydantic models (e.g., ModelDuplicateResponse).
|
|
2288
|
+
|
|
2289
|
+
Args:
|
|
2290
|
+
envelope: Envelope dict or Pydantic model that may contain UUID objects.
|
|
2291
|
+
|
|
2292
|
+
Returns:
|
|
2293
|
+
New dict with all UUIDs converted to strings.
|
|
2294
|
+
"""
|
|
2295
|
+
# Convert Pydantic models to dict first, ensuring type safety
|
|
2296
|
+
envelope_dict: JsonDict = (
|
|
2297
|
+
envelope.model_dump() if isinstance(envelope, BaseModel) else envelope
|
|
2298
|
+
)
|
|
2299
|
+
|
|
2300
|
+
def convert_value(value: object) -> object:
|
|
2301
|
+
if isinstance(value, UUID):
|
|
2302
|
+
return str(value)
|
|
2303
|
+
elif isinstance(value, dict):
|
|
2304
|
+
return {k: convert_value(v) for k, v in value.items()}
|
|
2305
|
+
elif isinstance(value, list):
|
|
2306
|
+
return [convert_value(item) for item in value]
|
|
2307
|
+
return value
|
|
2308
|
+
|
|
2309
|
+
return {k: convert_value(v) for k, v in envelope_dict.items()}
|
|
2310
|
+
|
|
2311
|
+
async def _publish_envelope_safe(
|
|
2312
|
+
self, envelope: dict[str, object] | BaseModel, topic: str
|
|
2313
|
+
) -> None:
|
|
2314
|
+
"""Publish envelope with UUID serialization support.
|
|
2315
|
+
|
|
2316
|
+
Converts any UUID objects to strings before publishing to ensure
|
|
2317
|
+
JSON serialization works correctly.
|
|
2318
|
+
|
|
2319
|
+
Args:
|
|
2320
|
+
envelope: Envelope dict or Pydantic model (may contain UUID objects).
|
|
2321
|
+
topic: Target topic to publish to.
|
|
2322
|
+
"""
|
|
2323
|
+
# Always serialize UUIDs upfront - single code path
|
|
2324
|
+
json_safe_envelope = self._serialize_envelope(envelope)
|
|
2325
|
+
await self._event_bus.publish_envelope(json_safe_envelope, topic)
|
|
2326
|
+
|
|
2327
|
+
async def health_check(self) -> dict[str, object]:
|
|
2328
|
+
"""Return health check status.
|
|
2329
|
+
|
|
2330
|
+
Returns:
|
|
2331
|
+
Dictionary with health status information:
|
|
2332
|
+
- healthy: Overall health status (True only if running,
|
|
2333
|
+
event bus healthy, no handlers failed to instantiate,
|
|
2334
|
+
all registered handlers are healthy, AND at least one
|
|
2335
|
+
handler is registered - a runtime without handlers is useless)
|
|
2336
|
+
- degraded: True when process is running but some handlers
|
|
2337
|
+
failed to instantiate. Indicates partial functionality -
|
|
2338
|
+
the system is operational but not at full capacity.
|
|
2339
|
+
- is_running: Whether the process is running
|
|
2340
|
+
- is_draining: Whether the process is in graceful shutdown drain
|
|
2341
|
+
period, waiting for in-flight messages to complete (OMN-756).
|
|
2342
|
+
Load balancers can use this to remove the service from rotation
|
|
2343
|
+
before the container becomes unhealthy.
|
|
2344
|
+
- pending_message_count: Number of messages currently being
|
|
2345
|
+
processed. Useful for monitoring drain progress and determining
|
|
2346
|
+
when the service is ready for shutdown.
|
|
2347
|
+
- event_bus: Event bus health status (if running)
|
|
2348
|
+
- event_bus_healthy: Boolean indicating event bus health
|
|
2349
|
+
- failed_handlers: Dict of handler_type -> error message for
|
|
2350
|
+
handlers that failed to instantiate during start()
|
|
2351
|
+
- registered_handlers: List of successfully registered handler types
|
|
2352
|
+
- handlers: Dict of handler_type -> health status for each
|
|
2353
|
+
registered handler
|
|
2354
|
+
- no_handlers_registered: True if no handlers are registered.
|
|
2355
|
+
This indicates a critical configuration issue - the runtime
|
|
2356
|
+
cannot process any events without handlers (OMN-1317).
|
|
2357
|
+
|
|
2358
|
+
Health State Matrix:
|
|
2359
|
+
- healthy=True, degraded=False: Fully operational
|
|
2360
|
+
- healthy=False, degraded=True: Running with reduced functionality
|
|
2361
|
+
- healthy=False, degraded=False: Not running, event bus unhealthy,
|
|
2362
|
+
or no handlers registered (critical configuration issue)
|
|
2363
|
+
- healthy=False, no_handlers_registered=True: Configuration error,
|
|
2364
|
+
runtime cannot process events
|
|
2365
|
+
|
|
2366
|
+
Drain State:
|
|
2367
|
+
When is_draining=True, the service is shutting down gracefully:
|
|
2368
|
+
- New messages are no longer being accepted
|
|
2369
|
+
- In-flight messages are being allowed to complete
|
|
2370
|
+
- Health status may still show healthy during drain
|
|
2371
|
+
- Load balancers should remove the service from rotation
|
|
2372
|
+
|
|
2373
|
+
Note:
|
|
2374
|
+
Handler health checks are performed concurrently using asyncio.gather()
|
|
2375
|
+
with individual timeouts (configurable via health_check_timeout_seconds
|
|
2376
|
+
config, default: 5.0 seconds) to prevent slow handlers from blocking.
|
|
2377
|
+
"""
|
|
2378
|
+
# Get event bus health if available
|
|
2379
|
+
event_bus_health: dict[str, object] = {}
|
|
2380
|
+
event_bus_healthy = False
|
|
2381
|
+
|
|
2382
|
+
try:
|
|
2383
|
+
event_bus_health = await self._event_bus.health_check()
|
|
2384
|
+
# Explicit type guard (not assert) for production safety
|
|
2385
|
+
# health_check() returns dict per contract
|
|
2386
|
+
if not isinstance(event_bus_health, dict):
|
|
2387
|
+
context = ModelInfraErrorContext(
|
|
2388
|
+
transport_type=EnumInfraTransportType.RUNTIME,
|
|
2389
|
+
operation="health_check",
|
|
2390
|
+
)
|
|
2391
|
+
raise ProtocolConfigurationError(
|
|
2392
|
+
f"health_check() must return dict, got {type(event_bus_health).__name__}",
|
|
2393
|
+
context=context,
|
|
2394
|
+
)
|
|
2395
|
+
event_bus_healthy = bool(event_bus_health.get("healthy", False))
|
|
2396
|
+
except Exception as e:
|
|
2397
|
+
# Create infrastructure error context for health check failure
|
|
2398
|
+
correlation_id = uuid4()
|
|
2399
|
+
context = ModelInfraErrorContext(
|
|
2400
|
+
transport_type=EnumInfraTransportType.RUNTIME,
|
|
2401
|
+
operation="health_check",
|
|
2402
|
+
target_name="event_bus",
|
|
2403
|
+
correlation_id=correlation_id,
|
|
2404
|
+
)
|
|
2405
|
+
# Chain the error with infrastructure context
|
|
2406
|
+
infra_error = RuntimeHostError(
|
|
2407
|
+
f"Event bus health check failed: {e}",
|
|
2408
|
+
context=context,
|
|
2409
|
+
)
|
|
2410
|
+
infra_error.__cause__ = e # Proper error chaining
|
|
2411
|
+
|
|
2412
|
+
logger.warning(
|
|
2413
|
+
"Event bus health check failed",
|
|
2414
|
+
extra={
|
|
2415
|
+
"error": str(e),
|
|
2416
|
+
"correlation_id": str(correlation_id),
|
|
2417
|
+
"infra_error": str(infra_error),
|
|
2418
|
+
},
|
|
2419
|
+
exc_info=True,
|
|
2420
|
+
)
|
|
2421
|
+
event_bus_health = {"error": str(e), "correlation_id": str(correlation_id)}
|
|
2422
|
+
event_bus_healthy = False
|
|
2423
|
+
|
|
2424
|
+
# Check handler health for all registered handlers concurrently
|
|
2425
|
+
# Delegates to ProtocolLifecycleExecutor with configured timeout to prevent blocking
|
|
2426
|
+
handler_health_results: dict[str, object] = {}
|
|
2427
|
+
handlers_all_healthy = True
|
|
2428
|
+
|
|
2429
|
+
if self._handlers:
|
|
2430
|
+
# Run all handler health checks concurrently using asyncio.gather()
|
|
2431
|
+
health_check_tasks = [
|
|
2432
|
+
self._lifecycle_executor.check_handler_health(handler_type, handler)
|
|
2433
|
+
for handler_type, handler in self._handlers.items()
|
|
2434
|
+
]
|
|
2435
|
+
results = await asyncio.gather(*health_check_tasks)
|
|
2436
|
+
|
|
2437
|
+
# Process results and build the results dict
|
|
2438
|
+
for health_result in results:
|
|
2439
|
+
handler_health_results[health_result.handler_type] = (
|
|
2440
|
+
health_result.details
|
|
2441
|
+
)
|
|
2442
|
+
if not health_result.healthy:
|
|
2443
|
+
handlers_all_healthy = False
|
|
2444
|
+
|
|
2445
|
+
# Check for failed handlers - any failures indicate degraded state
|
|
2446
|
+
has_failed_handlers = len(self._failed_handlers) > 0
|
|
2447
|
+
|
|
2448
|
+
# Check for no handlers registered - critical configuration issue
|
|
2449
|
+
# A runtime with no handlers cannot process any events and should be unhealthy
|
|
2450
|
+
no_handlers_registered = len(self._handlers) == 0
|
|
2451
|
+
|
|
2452
|
+
# Degraded state: process is running but some handlers failed to instantiate
|
|
2453
|
+
# This means the system is operational but with reduced functionality
|
|
2454
|
+
degraded = self._is_running and has_failed_handlers
|
|
2455
|
+
|
|
2456
|
+
# Overall health is True only if running, event bus is healthy,
|
|
2457
|
+
# no handlers failed to instantiate, all registered handlers are healthy,
|
|
2458
|
+
# AND at least one handler is registered (runtime without handlers is useless)
|
|
2459
|
+
healthy = (
|
|
2460
|
+
self._is_running
|
|
2461
|
+
and event_bus_healthy
|
|
2462
|
+
and not has_failed_handlers
|
|
2463
|
+
and handlers_all_healthy
|
|
2464
|
+
and not no_handlers_registered
|
|
2465
|
+
)
|
|
2466
|
+
|
|
2467
|
+
return {
|
|
2468
|
+
"healthy": healthy,
|
|
2469
|
+
"degraded": degraded,
|
|
2470
|
+
"is_running": self._is_running,
|
|
2471
|
+
"is_draining": self._is_draining,
|
|
2472
|
+
"pending_message_count": self._pending_message_count,
|
|
2473
|
+
"event_bus": event_bus_health,
|
|
2474
|
+
"event_bus_healthy": event_bus_healthy,
|
|
2475
|
+
"failed_handlers": self._failed_handlers,
|
|
2476
|
+
"registered_handlers": list(self._handlers.keys()),
|
|
2477
|
+
"handlers": handler_health_results,
|
|
2478
|
+
"no_handlers_registered": no_handlers_registered,
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2481
|
+
def register_handler(
|
|
2482
|
+
self, handler_type: str, handler: ProtocolContainerAware
|
|
2483
|
+
) -> None:
|
|
2484
|
+
"""Register a handler for a specific type.
|
|
2485
|
+
|
|
2486
|
+
Args:
|
|
2487
|
+
handler_type: Protocol type identifier (e.g., "http", "db").
|
|
2488
|
+
handler: Handler instance implementing the ProtocolContainerAware protocol.
|
|
2489
|
+
"""
|
|
2490
|
+
self._handlers[handler_type] = handler
|
|
2491
|
+
logger.debug(
|
|
2492
|
+
"Handler registered",
|
|
2493
|
+
extra={
|
|
2494
|
+
"handler_type": handler_type,
|
|
2495
|
+
"handler_class": type(handler).__name__,
|
|
2496
|
+
},
|
|
2497
|
+
)
|
|
2498
|
+
|
|
2499
|
+
def get_handler(self, handler_type: str) -> ProtocolContainerAware | None:
|
|
2500
|
+
"""Get handler for type, returns None if not registered.
|
|
2501
|
+
|
|
2502
|
+
Args:
|
|
2503
|
+
handler_type: Protocol type identifier.
|
|
2504
|
+
|
|
2505
|
+
Returns:
|
|
2506
|
+
Handler instance if registered, None otherwise.
|
|
2507
|
+
"""
|
|
2508
|
+
return self._handlers.get(handler_type)
|
|
2509
|
+
|
|
2510
|
+
async def get_subscribers_for_topic(self, topic: str) -> list[UUID]:
|
|
2511
|
+
"""Query Consul for node IDs that subscribe to a topic.
|
|
2512
|
+
|
|
2513
|
+
This method provides dynamic topic-to-subscriber lookup via Consul KV store.
|
|
2514
|
+
Topics are stored at `onex/topics/{topic}/subscribers` and contain a JSON
|
|
2515
|
+
array of node UUID strings.
|
|
2516
|
+
|
|
2517
|
+
Args:
|
|
2518
|
+
topic: Environment-qualified topic string
|
|
2519
|
+
(e.g., "dev.onex.evt.intent-classified.v1")
|
|
2520
|
+
|
|
2521
|
+
Returns:
|
|
2522
|
+
List of node UUIDs that subscribe to this topic.
|
|
2523
|
+
Empty list if no subscribers registered or Consul unavailable.
|
|
2524
|
+
|
|
2525
|
+
Note:
|
|
2526
|
+
Returns node IDs, not handler names. Node ID is the stable registry key.
|
|
2527
|
+
Handler selection is a separate concern that can change independently.
|
|
2528
|
+
|
|
2529
|
+
Example:
|
|
2530
|
+
>>> runtime = RuntimeHostProcess()
|
|
2531
|
+
>>> await runtime.start()
|
|
2532
|
+
>>> subscribers = await runtime.get_subscribers_for_topic(
|
|
2533
|
+
... "dev.onex.evt.intent-classified.v1"
|
|
2534
|
+
... )
|
|
2535
|
+
>>> print(subscribers) # [UUID('abc123...'), UUID('def456...')]
|
|
2536
|
+
|
|
2537
|
+
Related:
|
|
2538
|
+
- OMN-1613: Add event bus topic storage to registry for dynamic topic discovery
|
|
2539
|
+
- MixinConsulTopicIndex: Consul mixin that manages topic index storage
|
|
2540
|
+
"""
|
|
2541
|
+
consul_handler = self.get_handler("consul")
|
|
2542
|
+
if consul_handler is None:
|
|
2543
|
+
logger.debug(
|
|
2544
|
+
"Consul handler not available for topic subscriber lookup",
|
|
2545
|
+
extra={"topic": topic},
|
|
2546
|
+
)
|
|
2547
|
+
return []
|
|
2548
|
+
|
|
2549
|
+
try:
|
|
2550
|
+
correlation_id = uuid4()
|
|
2551
|
+
envelope: dict[str, object] = {
|
|
2552
|
+
"operation": "consul.kv_get",
|
|
2553
|
+
"payload": {"key": f"onex/topics/{topic}/subscribers"},
|
|
2554
|
+
"correlation_id": str(correlation_id),
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2557
|
+
# Execute the Consul KV get operation
|
|
2558
|
+
# NOTE: MVP adapters use legacy execute(envelope: dict) signature.
|
|
2559
|
+
result = await consul_handler.execute(envelope) # type: ignore[call-arg]
|
|
2560
|
+
|
|
2561
|
+
# Navigate to the value in the response structure:
|
|
2562
|
+
# ModelHandlerOutput -> result (ModelConsulHandlerResponse)
|
|
2563
|
+
# -> payload (ModelConsulHandlerPayload) -> data (ConsulPayload)
|
|
2564
|
+
if result is None:
|
|
2565
|
+
return []
|
|
2566
|
+
|
|
2567
|
+
# Check if result has the expected structure
|
|
2568
|
+
if not hasattr(result, "result") or result.result is None:
|
|
2569
|
+
return []
|
|
2570
|
+
|
|
2571
|
+
response = result.result
|
|
2572
|
+
if not hasattr(response, "payload") or response.payload is None:
|
|
2573
|
+
return []
|
|
2574
|
+
|
|
2575
|
+
payload_data = response.payload.data
|
|
2576
|
+
if payload_data is None:
|
|
2577
|
+
return []
|
|
2578
|
+
|
|
2579
|
+
# Check for "not found" response - key doesn't exist
|
|
2580
|
+
if hasattr(payload_data, "found") and payload_data.found is False:
|
|
2581
|
+
return []
|
|
2582
|
+
|
|
2583
|
+
# Get the value field from the payload
|
|
2584
|
+
value = getattr(payload_data, "value", None)
|
|
2585
|
+
if not value:
|
|
2586
|
+
return []
|
|
2587
|
+
|
|
2588
|
+
# Parse JSON array of node ID strings
|
|
2589
|
+
node_ids_raw = json.loads(value)
|
|
2590
|
+
if not isinstance(node_ids_raw, list):
|
|
2591
|
+
logger.warning(
|
|
2592
|
+
"Topic subscriber value is not a list",
|
|
2593
|
+
extra={
|
|
2594
|
+
"topic": topic,
|
|
2595
|
+
"correlation_id": str(correlation_id),
|
|
2596
|
+
"value_type": type(node_ids_raw).__name__,
|
|
2597
|
+
},
|
|
2598
|
+
)
|
|
2599
|
+
return []
|
|
2600
|
+
|
|
2601
|
+
# Convert string UUIDs to UUID objects (skip invalid entries)
|
|
2602
|
+
subscribers: list[UUID] = []
|
|
2603
|
+
invalid_ids: list[str] = []
|
|
2604
|
+
for nid in node_ids_raw:
|
|
2605
|
+
if not isinstance(nid, str):
|
|
2606
|
+
continue
|
|
2607
|
+
try:
|
|
2608
|
+
subscribers.append(UUID(nid))
|
|
2609
|
+
except ValueError:
|
|
2610
|
+
invalid_ids.append(nid)
|
|
2611
|
+
|
|
2612
|
+
if invalid_ids:
|
|
2613
|
+
logger.warning(
|
|
2614
|
+
"Invalid UUIDs in topic subscriber list",
|
|
2615
|
+
extra={
|
|
2616
|
+
"topic": topic,
|
|
2617
|
+
"correlation_id": str(correlation_id),
|
|
2618
|
+
"invalid_count": len(invalid_ids),
|
|
2619
|
+
},
|
|
2620
|
+
)
|
|
2621
|
+
return subscribers
|
|
2622
|
+
|
|
2623
|
+
except json.JSONDecodeError as e:
|
|
2624
|
+
logger.warning(
|
|
2625
|
+
"Failed to parse topic subscriber JSON",
|
|
2626
|
+
extra={
|
|
2627
|
+
"topic": topic,
|
|
2628
|
+
"error": str(e),
|
|
2629
|
+
},
|
|
2630
|
+
)
|
|
2631
|
+
return []
|
|
2632
|
+
except InfraConsulError as e:
|
|
2633
|
+
logger.warning(
|
|
2634
|
+
"Consul error querying topic subscribers",
|
|
2635
|
+
extra={
|
|
2636
|
+
"topic": topic,
|
|
2637
|
+
"error": str(e),
|
|
2638
|
+
"error_type": "InfraConsulError",
|
|
2639
|
+
"consul_key": getattr(e, "consul_key", None),
|
|
2640
|
+
},
|
|
2641
|
+
)
|
|
2642
|
+
return []
|
|
2643
|
+
except InfraTimeoutError as e:
|
|
2644
|
+
logger.warning(
|
|
2645
|
+
"Timeout querying topic subscribers",
|
|
2646
|
+
extra={
|
|
2647
|
+
"topic": topic,
|
|
2648
|
+
"error": str(e),
|
|
2649
|
+
"error_type": "InfraTimeoutError",
|
|
2650
|
+
},
|
|
2651
|
+
)
|
|
2652
|
+
return []
|
|
2653
|
+
except InfraUnavailableError as e:
|
|
2654
|
+
logger.warning(
|
|
2655
|
+
"Service unavailable for topic subscriber query",
|
|
2656
|
+
extra={
|
|
2657
|
+
"topic": topic,
|
|
2658
|
+
"error": str(e),
|
|
2659
|
+
"error_type": "InfraUnavailableError",
|
|
2660
|
+
},
|
|
2661
|
+
)
|
|
2662
|
+
return []
|
|
2663
|
+
except Exception as e:
|
|
2664
|
+
# Graceful degradation - Consul unavailable is not fatal
|
|
2665
|
+
logger.warning(
|
|
2666
|
+
"Failed to query topic subscribers from Consul",
|
|
2667
|
+
extra={
|
|
2668
|
+
"topic": topic,
|
|
2669
|
+
"error": str(e),
|
|
2670
|
+
"error_type": type(e).__name__,
|
|
2671
|
+
},
|
|
2672
|
+
)
|
|
2673
|
+
return []
|
|
2674
|
+
|
|
2675
|
+
# =========================================================================
|
|
2676
|
+
# Architecture Validation Methods (OMN-1138)
|
|
2677
|
+
# =========================================================================
|
|
2678
|
+
|
|
2679
|
+
async def _validate_architecture(self) -> None:
|
|
2680
|
+
"""Validate architecture compliance before starting runtime.
|
|
2681
|
+
|
|
2682
|
+
This method is called at the beginning of start() to validate nodes
|
|
2683
|
+
and handlers against registered architecture rules. If any violations
|
|
2684
|
+
with ERROR severity are detected, startup is blocked.
|
|
2685
|
+
|
|
2686
|
+
Validation occurs BEFORE:
|
|
2687
|
+
- Event bus starts
|
|
2688
|
+
- Handlers are wired
|
|
2689
|
+
- Subscription begins
|
|
2690
|
+
|
|
2691
|
+
Validation Behavior:
|
|
2692
|
+
- ERROR severity violations: Block startup, raise ArchitectureViolationError
|
|
2693
|
+
- WARNING severity violations: Log warning, continue startup
|
|
2694
|
+
- INFO severity violations: Log info, continue startup
|
|
2695
|
+
|
|
2696
|
+
Raises:
|
|
2697
|
+
ArchitectureViolationError: If blocking violations (ERROR severity)
|
|
2698
|
+
are detected. Contains all blocking violations for inspection.
|
|
2699
|
+
|
|
2700
|
+
Example:
|
|
2701
|
+
>>> # Validation is automatic in start()
|
|
2702
|
+
>>> try:
|
|
2703
|
+
... await runtime.start()
|
|
2704
|
+
... except ArchitectureViolationError as e:
|
|
2705
|
+
... print(f"Startup blocked: {len(e.violations)} violations")
|
|
2706
|
+
... for v in e.violations:
|
|
2707
|
+
... print(v.format_for_logging())
|
|
2708
|
+
|
|
2709
|
+
Note:
|
|
2710
|
+
Validation only runs if architecture_rules were provided at init.
|
|
2711
|
+
If no rules are configured, this method returns immediately.
|
|
2712
|
+
|
|
2713
|
+
Related:
|
|
2714
|
+
- OMN-1138: Architecture Validator for omnibase_infra
|
|
2715
|
+
- OMN-1099: Validators implementing ProtocolArchitectureRule
|
|
2716
|
+
"""
|
|
2717
|
+
# Skip validation if no rules configured
|
|
2718
|
+
if not self._architecture_rules:
|
|
2719
|
+
logger.debug("No architecture rules configured, skipping validation")
|
|
2720
|
+
return
|
|
2721
|
+
|
|
2722
|
+
logger.info(
|
|
2723
|
+
"Validating architecture compliance",
|
|
2724
|
+
extra={
|
|
2725
|
+
"rule_count": len(self._architecture_rules),
|
|
2726
|
+
"rule_ids": tuple(r.rule_id for r in self._architecture_rules),
|
|
2727
|
+
},
|
|
2728
|
+
)
|
|
2729
|
+
|
|
2730
|
+
# Import architecture validator components
|
|
2731
|
+
from omnibase_infra.errors import ArchitectureViolationError
|
|
2732
|
+
from omnibase_infra.nodes.architecture_validator import (
|
|
2733
|
+
ModelArchitectureValidationRequest,
|
|
2734
|
+
NodeArchitectureValidatorCompute,
|
|
2735
|
+
)
|
|
2736
|
+
|
|
2737
|
+
# Create or get container
|
|
2738
|
+
container = self._get_or_create_container()
|
|
2739
|
+
|
|
2740
|
+
# Instantiate validator with rules
|
|
2741
|
+
validator = NodeArchitectureValidatorCompute(
|
|
2742
|
+
container=container,
|
|
2743
|
+
rules=self._architecture_rules,
|
|
2744
|
+
)
|
|
2745
|
+
|
|
2746
|
+
# Build validation request
|
|
2747
|
+
# Note: At this point, handlers haven't been instantiated yet (that happens
|
|
2748
|
+
# after validation in _populate_handlers_from_registry). We validate the
|
|
2749
|
+
# handler CLASSES from the registry, not handler instances.
|
|
2750
|
+
handler_registry = await self._get_handler_registry()
|
|
2751
|
+
handler_classes: list[type[ProtocolContainerAware]] = []
|
|
2752
|
+
for handler_type in handler_registry.list_protocols():
|
|
2753
|
+
try:
|
|
2754
|
+
handler_cls = handler_registry.get(handler_type)
|
|
2755
|
+
handler_classes.append(handler_cls)
|
|
2756
|
+
except Exception as e:
|
|
2757
|
+
# If a handler class can't be retrieved, skip it for validation
|
|
2758
|
+
# (it will fail later during instantiation anyway)
|
|
2759
|
+
logger.debug(
|
|
2760
|
+
"Skipping handler class for architecture validation",
|
|
2761
|
+
extra={
|
|
2762
|
+
"handler_type": handler_type,
|
|
2763
|
+
"error": str(e),
|
|
2764
|
+
},
|
|
2765
|
+
)
|
|
2766
|
+
|
|
2767
|
+
request = ModelArchitectureValidationRequest(
|
|
2768
|
+
nodes=(), # Nodes not yet available at this point
|
|
2769
|
+
handlers=tuple(handler_classes),
|
|
2770
|
+
)
|
|
2771
|
+
|
|
2772
|
+
# Execute validation
|
|
2773
|
+
result = validator.compute(request)
|
|
2774
|
+
|
|
2775
|
+
# Separate blocking and non-blocking violations
|
|
2776
|
+
blocking_violations = tuple(v for v in result.violations if v.blocks_startup())
|
|
2777
|
+
warning_violations = tuple(
|
|
2778
|
+
v for v in result.violations if not v.blocks_startup()
|
|
2779
|
+
)
|
|
2780
|
+
|
|
2781
|
+
# Log warnings but don't block
|
|
2782
|
+
for violation in warning_violations:
|
|
2783
|
+
# Note: We can't use to_structured_dict() directly because 'message'
|
|
2784
|
+
# is a reserved key in Python logging's extra parameter.
|
|
2785
|
+
# We use format_for_logging() instead for the log message.
|
|
2786
|
+
logger.warning(
|
|
2787
|
+
"Architecture warning: %s",
|
|
2788
|
+
violation.format_for_logging(),
|
|
2789
|
+
extra={
|
|
2790
|
+
"rule_id": violation.rule_id,
|
|
2791
|
+
"severity": violation.severity.value,
|
|
2792
|
+
"target_type": violation.target_type,
|
|
2793
|
+
"target_name": violation.target_name,
|
|
2794
|
+
},
|
|
2795
|
+
)
|
|
2796
|
+
|
|
2797
|
+
# Block startup on ERROR violations
|
|
2798
|
+
if blocking_violations:
|
|
2799
|
+
logger.error(
|
|
2800
|
+
"Architecture validation failed",
|
|
2801
|
+
extra={
|
|
2802
|
+
"blocking_violation_count": len(blocking_violations),
|
|
2803
|
+
"warning_violation_count": len(warning_violations),
|
|
2804
|
+
"blocking_rule_ids": tuple(v.rule_id for v in blocking_violations),
|
|
2805
|
+
},
|
|
2806
|
+
)
|
|
2807
|
+
raise ArchitectureViolationError(
|
|
2808
|
+
message=f"Architecture validation failed with {len(blocking_violations)} blocking violations",
|
|
2809
|
+
violations=blocking_violations,
|
|
2810
|
+
)
|
|
2811
|
+
|
|
2812
|
+
logger.info(
|
|
2813
|
+
"Architecture validation passed",
|
|
2814
|
+
extra={
|
|
2815
|
+
"rules_checked": result.rules_checked,
|
|
2816
|
+
"handlers_checked": result.handlers_checked,
|
|
2817
|
+
"warning_count": len(warning_violations),
|
|
2818
|
+
},
|
|
2819
|
+
)
|
|
2820
|
+
|
|
2821
|
+
def _get_or_create_container(self) -> ModelONEXContainer:
|
|
2822
|
+
"""Get the injected container or create and cache a new one.
|
|
2823
|
+
|
|
2824
|
+
Returns:
|
|
2825
|
+
ModelONEXContainer instance for dependency injection.
|
|
2826
|
+
|
|
2827
|
+
Note:
|
|
2828
|
+
If no container was provided at init, a new container is created
|
|
2829
|
+
and cached in self._container. This ensures all handlers share
|
|
2830
|
+
the same container instance. The container provides basic
|
|
2831
|
+
infrastructure for node execution but may not have all services wired.
|
|
2832
|
+
"""
|
|
2833
|
+
if self._container is not None:
|
|
2834
|
+
return self._container
|
|
2835
|
+
|
|
2836
|
+
# Create container and cache it for reuse
|
|
2837
|
+
from omnibase_core.models.container.model_onex_container import (
|
|
2838
|
+
ModelONEXContainer,
|
|
2839
|
+
)
|
|
2840
|
+
|
|
2841
|
+
logger.debug("Creating and caching container (no container provided at init)")
|
|
2842
|
+
self._container = ModelONEXContainer()
|
|
2843
|
+
return self._container
|
|
2844
|
+
|
|
2845
|
+
def _get_environment_from_config(self) -> str:
|
|
2846
|
+
"""Extract environment setting from config with consistent fallback.
|
|
2847
|
+
|
|
2848
|
+
Handles both dict-based config and object-based config (e.g., Pydantic models)
|
|
2849
|
+
with a unified access pattern.
|
|
2850
|
+
|
|
2851
|
+
Resolution order:
|
|
2852
|
+
1. config["event_bus"]["environment"] (if config is dict-like)
|
|
2853
|
+
2. config.event_bus.environment (if config is object-like)
|
|
2854
|
+
3. ONEX_ENVIRONMENT environment variable
|
|
2855
|
+
4. "dev" (hardcoded default)
|
|
2856
|
+
|
|
2857
|
+
Returns:
|
|
2858
|
+
Environment string (e.g., "dev", "staging", "prod").
|
|
2859
|
+
"""
|
|
2860
|
+
default_env = os.getenv("ONEX_ENVIRONMENT", "dev")
|
|
2861
|
+
config = self._config or {}
|
|
2862
|
+
|
|
2863
|
+
event_bus_config = config.get("event_bus", {})
|
|
2864
|
+
if isinstance(event_bus_config, dict):
|
|
2865
|
+
return str(event_bus_config.get("environment", default_env))
|
|
2866
|
+
|
|
2867
|
+
# Object-based config (e.g., ModelEventBusConfig)
|
|
2868
|
+
return str(getattr(event_bus_config, "environment", default_env))
|
|
2869
|
+
|
|
2870
|
+
# =========================================================================
|
|
2871
|
+
# Event Bus Subcontract Wiring Methods (OMN-1621)
|
|
2872
|
+
# =========================================================================
|
|
2873
|
+
|
|
2874
|
+
async def _wire_event_bus_subscriptions(self) -> None:
|
|
2875
|
+
"""Wire Kafka subscriptions from handler contract event_bus sections.
|
|
2876
|
+
|
|
2877
|
+
This method bridges contract-declared topics to actual Kafka subscriptions
|
|
2878
|
+
using the EventBusSubcontractWiring class. It reads the event_bus subcontract
|
|
2879
|
+
from each handler's contract YAML and creates subscriptions for declared
|
|
2880
|
+
subscribe_topics.
|
|
2881
|
+
|
|
2882
|
+
Preconditions:
|
|
2883
|
+
- self._event_bus must be available and started
|
|
2884
|
+
- self._dispatch_engine must be set (otherwise wiring is skipped)
|
|
2885
|
+
- self._handler_descriptors must be populated
|
|
2886
|
+
|
|
2887
|
+
The wiring creates subscriptions that route messages to the dispatch engine,
|
|
2888
|
+
which then routes to appropriate handlers based on topic/category matching.
|
|
2889
|
+
|
|
2890
|
+
Per ARCH-002: "Runtime owns all Kafka plumbing" - nodes and handlers declare
|
|
2891
|
+
their topic requirements in contracts but never directly interact with Kafka.
|
|
2892
|
+
|
|
2893
|
+
Note:
|
|
2894
|
+
If dispatch_engine is not configured, this method logs a debug message
|
|
2895
|
+
and returns without creating any subscriptions. This allows the runtime
|
|
2896
|
+
to operate in legacy mode without contract-driven subscriptions.
|
|
2897
|
+
|
|
2898
|
+
.. versionadded:: 0.2.5
|
|
2899
|
+
Part of OMN-1621 contract-driven event bus wiring.
|
|
2900
|
+
"""
|
|
2901
|
+
# Guard: require both event_bus and dispatch_engine
|
|
2902
|
+
if not self._event_bus:
|
|
2903
|
+
logger.debug("Event bus not available, skipping subcontract wiring")
|
|
2904
|
+
return
|
|
2905
|
+
|
|
2906
|
+
if not self._dispatch_engine:
|
|
2907
|
+
logger.debug(
|
|
2908
|
+
"Dispatch engine not configured, skipping event bus subcontract wiring"
|
|
2909
|
+
)
|
|
2910
|
+
return
|
|
2911
|
+
|
|
2912
|
+
if not self._handler_descriptors:
|
|
2913
|
+
logger.debug(
|
|
2914
|
+
"No handler descriptors available, skipping subcontract wiring"
|
|
2915
|
+
)
|
|
2916
|
+
return
|
|
2917
|
+
|
|
2918
|
+
environment = self._get_environment_from_config()
|
|
2919
|
+
|
|
2920
|
+
# Create wiring instance
|
|
2921
|
+
# Cast to protocol type - both EventBusKafka and EventBusInmemory implement
|
|
2922
|
+
# the ProtocolEventBusSubscriber interface (subscribe method)
|
|
2923
|
+
self._event_bus_wiring = EventBusSubcontractWiring(
|
|
2924
|
+
event_bus=cast("ProtocolEventBusSubscriber", self._event_bus),
|
|
2925
|
+
dispatch_engine=self._dispatch_engine,
|
|
2926
|
+
environment=environment,
|
|
2927
|
+
)
|
|
2928
|
+
|
|
2929
|
+
# Wire subscriptions for each handler with a contract
|
|
2930
|
+
wired_count = 0
|
|
2931
|
+
for handler_type, descriptor in self._handler_descriptors.items():
|
|
2932
|
+
contract_path_str = descriptor.contract_path
|
|
2933
|
+
if not contract_path_str:
|
|
2934
|
+
continue
|
|
2935
|
+
|
|
2936
|
+
contract_path = Path(contract_path_str)
|
|
2937
|
+
|
|
2938
|
+
# Load event_bus subcontract from contract YAML
|
|
2939
|
+
subcontract = load_event_bus_subcontract(contract_path, logger)
|
|
2940
|
+
if subcontract and subcontract.subscribe_topics:
|
|
2941
|
+
await self._event_bus_wiring.wire_subscriptions(
|
|
2942
|
+
subcontract=subcontract,
|
|
2943
|
+
node_name=descriptor.name or handler_type,
|
|
2944
|
+
)
|
|
2945
|
+
wired_count += 1
|
|
2946
|
+
logger.info(
|
|
2947
|
+
"Wired subscription(s) for handler '%s': topics=%s",
|
|
2948
|
+
descriptor.name or handler_type,
|
|
2949
|
+
subcontract.subscribe_topics,
|
|
2950
|
+
)
|
|
2951
|
+
|
|
2952
|
+
if wired_count > 0:
|
|
2953
|
+
logger.info(
|
|
2954
|
+
"Event bus subcontract wiring complete",
|
|
2955
|
+
extra={
|
|
2956
|
+
"wired_handler_count": wired_count,
|
|
2957
|
+
"total_handler_count": len(self._handler_descriptors),
|
|
2958
|
+
"environment": environment,
|
|
2959
|
+
},
|
|
2960
|
+
)
|
|
2961
|
+
else:
|
|
2962
|
+
logger.debug(
|
|
2963
|
+
"No handlers with event_bus subscriptions found",
|
|
2964
|
+
extra={"handler_count": len(self._handler_descriptors)},
|
|
2965
|
+
)
|
|
2966
|
+
|
|
2967
|
+
async def _wire_baseline_subscriptions(self) -> None:
|
|
2968
|
+
"""Wire platform-baseline topic subscriptions for contract discovery.
|
|
2969
|
+
|
|
2970
|
+
These subscriptions are wired at runtime startup to receive contract
|
|
2971
|
+
registration and deregistration events from Kafka. This enables
|
|
2972
|
+
dynamic contract discovery without polling.
|
|
2973
|
+
|
|
2974
|
+
The subscriptions route events to KafkaContractSource callbacks:
|
|
2975
|
+
- on_contract_registered(): Parses contract YAML and caches descriptor
|
|
2976
|
+
- on_contract_deregistered(): Removes descriptor from cache
|
|
2977
|
+
|
|
2978
|
+
Preconditions:
|
|
2979
|
+
- KAFKA_EVENTS mode must be active (self._kafka_contract_source set)
|
|
2980
|
+
- Event bus must be available and started
|
|
2981
|
+
|
|
2982
|
+
Topic Format:
|
|
2983
|
+
- Registration: {env}.{TOPIC_SUFFIX_CONTRACT_REGISTERED}
|
|
2984
|
+
- Deregistration: {env}.{TOPIC_SUFFIX_CONTRACT_DEREGISTERED}
|
|
2985
|
+
|
|
2986
|
+
Note:
|
|
2987
|
+
Unsubscribe callbacks are stored in self._baseline_subscriptions
|
|
2988
|
+
for cleanup during stop().
|
|
2989
|
+
|
|
2990
|
+
Part of OMN-1654: KafkaContractSource cache discovery.
|
|
2991
|
+
|
|
2992
|
+
.. versionadded:: 0.8.0
|
|
2993
|
+
Created for event-driven contract discovery.
|
|
2994
|
+
"""
|
|
2995
|
+
# Guard: only wire if KafkaContractSource is active
|
|
2996
|
+
if self._kafka_contract_source is None:
|
|
2997
|
+
logger.debug(
|
|
2998
|
+
"KafkaContractSource not active, skipping baseline subscriptions"
|
|
2999
|
+
)
|
|
3000
|
+
return
|
|
3001
|
+
|
|
3002
|
+
# Guard: event bus must be available
|
|
3003
|
+
if self._event_bus is None:
|
|
3004
|
+
logger.warning(
|
|
3005
|
+
"Event bus not available, cannot wire baseline contract subscriptions",
|
|
3006
|
+
extra={"mode": "KAFKA_EVENTS"},
|
|
3007
|
+
)
|
|
3008
|
+
return
|
|
3009
|
+
|
|
3010
|
+
source = self._kafka_contract_source
|
|
3011
|
+
environment = source.environment
|
|
3012
|
+
|
|
3013
|
+
# Compose topic names using platform-reserved suffixes
|
|
3014
|
+
registration_topic = f"{environment}.{TOPIC_SUFFIX_CONTRACT_REGISTERED}"
|
|
3015
|
+
deregistration_topic = f"{environment}.{TOPIC_SUFFIX_CONTRACT_DEREGISTERED}"
|
|
3016
|
+
|
|
3017
|
+
# Import ModelEventMessage type for handler signature
|
|
3018
|
+
from omnibase_infra.event_bus.models.model_event_message import (
|
|
3019
|
+
ModelEventMessage,
|
|
3020
|
+
)
|
|
3021
|
+
|
|
3022
|
+
async def handle_registration(msg: ModelEventMessage) -> None:
|
|
3023
|
+
"""Handle contract registration event from Kafka."""
|
|
3024
|
+
try:
|
|
3025
|
+
parsed = _parse_contract_event_payload(msg)
|
|
3026
|
+
if parsed is None:
|
|
3027
|
+
return
|
|
3028
|
+
|
|
3029
|
+
payload, correlation_id = parsed
|
|
3030
|
+
|
|
3031
|
+
source.on_contract_registered(
|
|
3032
|
+
node_name=str(payload.get("node_name", "")),
|
|
3033
|
+
contract_yaml=str(payload.get("contract_yaml", "")),
|
|
3034
|
+
correlation_id=correlation_id,
|
|
3035
|
+
)
|
|
3036
|
+
|
|
3037
|
+
logger.debug(
|
|
3038
|
+
"Processed contract registration event",
|
|
3039
|
+
extra={
|
|
3040
|
+
"node_name": payload.get("node_name"),
|
|
3041
|
+
"topic": registration_topic,
|
|
3042
|
+
"correlation_id": str(correlation_id),
|
|
3043
|
+
},
|
|
3044
|
+
)
|
|
3045
|
+
|
|
3046
|
+
except Exception as e:
|
|
3047
|
+
logger.warning(
|
|
3048
|
+
"Failed to process contract registration event",
|
|
3049
|
+
extra={
|
|
3050
|
+
"error": str(e),
|
|
3051
|
+
"error_type": type(e).__name__,
|
|
3052
|
+
"topic": registration_topic,
|
|
3053
|
+
},
|
|
3054
|
+
)
|
|
3055
|
+
|
|
3056
|
+
async def handle_deregistration(msg: ModelEventMessage) -> None:
|
|
3057
|
+
"""Handle contract deregistration event from Kafka."""
|
|
3058
|
+
try:
|
|
3059
|
+
parsed = _parse_contract_event_payload(msg)
|
|
3060
|
+
if parsed is None:
|
|
3061
|
+
return
|
|
3062
|
+
|
|
3063
|
+
payload, correlation_id = parsed
|
|
3064
|
+
|
|
3065
|
+
source.on_contract_deregistered(
|
|
3066
|
+
node_name=str(payload.get("node_name", "")),
|
|
3067
|
+
correlation_id=correlation_id,
|
|
3068
|
+
)
|
|
3069
|
+
|
|
3070
|
+
logger.debug(
|
|
3071
|
+
"Processed contract deregistration event",
|
|
3072
|
+
extra={
|
|
3073
|
+
"node_name": payload.get("node_name"),
|
|
3074
|
+
"topic": deregistration_topic,
|
|
3075
|
+
"correlation_id": str(correlation_id),
|
|
3076
|
+
},
|
|
3077
|
+
)
|
|
3078
|
+
|
|
3079
|
+
except Exception as e:
|
|
3080
|
+
logger.warning(
|
|
3081
|
+
"Failed to process contract deregistration event",
|
|
3082
|
+
extra={
|
|
3083
|
+
"error": str(e),
|
|
3084
|
+
"error_type": type(e).__name__,
|
|
3085
|
+
"topic": deregistration_topic,
|
|
3086
|
+
},
|
|
3087
|
+
)
|
|
3088
|
+
|
|
3089
|
+
# Subscribe to topics
|
|
3090
|
+
try:
|
|
3091
|
+
# Create node identity for baseline subscriptions
|
|
3092
|
+
baseline_identity = ModelNodeIdentity(
|
|
3093
|
+
env=environment,
|
|
3094
|
+
service=self._node_identity.service,
|
|
3095
|
+
node_name=f"{self._node_identity.node_name}-contract-discovery",
|
|
3096
|
+
version=self._node_identity.version,
|
|
3097
|
+
)
|
|
3098
|
+
|
|
3099
|
+
# Subscribe to registration topic
|
|
3100
|
+
reg_unsub = await self._event_bus.subscribe(
|
|
3101
|
+
topic=registration_topic,
|
|
3102
|
+
node_identity=baseline_identity,
|
|
3103
|
+
on_message=handle_registration,
|
|
3104
|
+
purpose=EnumConsumerGroupPurpose.CONSUME,
|
|
3105
|
+
)
|
|
3106
|
+
self._baseline_subscriptions.append(reg_unsub)
|
|
3107
|
+
|
|
3108
|
+
# Subscribe to deregistration topic
|
|
3109
|
+
dereg_unsub = await self._event_bus.subscribe(
|
|
3110
|
+
topic=deregistration_topic,
|
|
3111
|
+
node_identity=baseline_identity,
|
|
3112
|
+
on_message=handle_deregistration,
|
|
3113
|
+
purpose=EnumConsumerGroupPurpose.CONSUME,
|
|
3114
|
+
)
|
|
3115
|
+
self._baseline_subscriptions.append(dereg_unsub)
|
|
3116
|
+
|
|
3117
|
+
logger.info(
|
|
3118
|
+
"Wired baseline contract subscriptions",
|
|
3119
|
+
extra={
|
|
3120
|
+
"registration_topic": registration_topic,
|
|
3121
|
+
"deregistration_topic": deregistration_topic,
|
|
3122
|
+
"environment": environment,
|
|
3123
|
+
"subscription_count": len(self._baseline_subscriptions),
|
|
3124
|
+
},
|
|
3125
|
+
)
|
|
3126
|
+
|
|
3127
|
+
except Exception:
|
|
3128
|
+
logger.exception(
|
|
3129
|
+
"Failed to wire baseline subscriptions",
|
|
3130
|
+
extra={
|
|
3131
|
+
"registration_topic": registration_topic,
|
|
3132
|
+
"deregistration_topic": deregistration_topic,
|
|
3133
|
+
},
|
|
3134
|
+
)
|
|
3135
|
+
|
|
3136
|
+
# =========================================================================
|
|
3137
|
+
# Idempotency Guard Methods (OMN-945)
|
|
3138
|
+
# =========================================================================
|
|
3139
|
+
|
|
3140
|
+
async def _initialize_idempotency_store(self) -> None:
|
|
3141
|
+
"""Initialize idempotency store from configuration.
|
|
3142
|
+
|
|
3143
|
+
Reads idempotency configuration from the runtime config and wires
|
|
3144
|
+
the appropriate store implementation. If not configured or disabled,
|
|
3145
|
+
idempotency checking is skipped.
|
|
3146
|
+
|
|
3147
|
+
Supported store types:
|
|
3148
|
+
- "postgres": PostgreSQL-backed durable store (production)
|
|
3149
|
+
- "memory": In-memory store (testing only)
|
|
3150
|
+
|
|
3151
|
+
Configuration keys:
|
|
3152
|
+
- idempotency.enabled: bool (default: False)
|
|
3153
|
+
- idempotency.store_type: "postgres" | "memory" (default: "postgres")
|
|
3154
|
+
- idempotency.domain_from_operation: bool (default: True)
|
|
3155
|
+
- idempotency.skip_operations: list[str] (default: [])
|
|
3156
|
+
- idempotency_database: dict (PostgreSQL connection config)
|
|
3157
|
+
"""
|
|
3158
|
+
# Check if config exists
|
|
3159
|
+
if self._config is None:
|
|
3160
|
+
logger.debug("No runtime config provided, skipping idempotency setup")
|
|
3161
|
+
return
|
|
3162
|
+
|
|
3163
|
+
# Check if config has idempotency section
|
|
3164
|
+
idempotency_raw = self._config.get("idempotency")
|
|
3165
|
+
if idempotency_raw is None:
|
|
3166
|
+
logger.debug("Idempotency guard not configured, skipping")
|
|
3167
|
+
return
|
|
3168
|
+
|
|
3169
|
+
try:
|
|
3170
|
+
from omnibase_infra.idempotency import ModelIdempotencyGuardConfig
|
|
3171
|
+
|
|
3172
|
+
if isinstance(idempotency_raw, dict):
|
|
3173
|
+
self._idempotency_config = ModelIdempotencyGuardConfig.model_validate(
|
|
3174
|
+
idempotency_raw
|
|
3175
|
+
)
|
|
3176
|
+
elif isinstance(idempotency_raw, ModelIdempotencyGuardConfig):
|
|
3177
|
+
self._idempotency_config = idempotency_raw
|
|
3178
|
+
else:
|
|
3179
|
+
logger.warning(
|
|
3180
|
+
"Invalid idempotency config type",
|
|
3181
|
+
extra={"type": type(idempotency_raw).__name__},
|
|
3182
|
+
)
|
|
3183
|
+
return
|
|
3184
|
+
|
|
3185
|
+
if not self._idempotency_config.enabled:
|
|
3186
|
+
logger.debug("Idempotency guard disabled in config")
|
|
3187
|
+
return
|
|
3188
|
+
|
|
3189
|
+
# Create store based on store_type
|
|
3190
|
+
if self._idempotency_config.store_type == "postgres":
|
|
3191
|
+
from omnibase_infra.idempotency import (
|
|
3192
|
+
ModelPostgresIdempotencyStoreConfig,
|
|
3193
|
+
StoreIdempotencyPostgres,
|
|
3194
|
+
)
|
|
3195
|
+
|
|
3196
|
+
# Get database config from container or config
|
|
3197
|
+
db_config_raw = self._config.get("idempotency_database", {})
|
|
3198
|
+
if isinstance(db_config_raw, dict):
|
|
3199
|
+
db_config = ModelPostgresIdempotencyStoreConfig.model_validate(
|
|
3200
|
+
db_config_raw
|
|
3201
|
+
)
|
|
3202
|
+
elif isinstance(db_config_raw, ModelPostgresIdempotencyStoreConfig):
|
|
3203
|
+
db_config = db_config_raw
|
|
3204
|
+
else:
|
|
3205
|
+
logger.warning(
|
|
3206
|
+
"Invalid idempotency_database config type",
|
|
3207
|
+
extra={"type": type(db_config_raw).__name__},
|
|
3208
|
+
)
|
|
3209
|
+
return
|
|
3210
|
+
|
|
3211
|
+
self._idempotency_store = StoreIdempotencyPostgres(config=db_config)
|
|
3212
|
+
await self._idempotency_store.initialize()
|
|
3213
|
+
|
|
3214
|
+
elif self._idempotency_config.store_type == "memory":
|
|
3215
|
+
from omnibase_infra.idempotency import StoreIdempotencyInmemory
|
|
3216
|
+
|
|
3217
|
+
self._idempotency_store = StoreIdempotencyInmemory()
|
|
3218
|
+
|
|
3219
|
+
else:
|
|
3220
|
+
logger.warning(
|
|
3221
|
+
"Unknown idempotency store type",
|
|
3222
|
+
extra={"store_type": self._idempotency_config.store_type},
|
|
3223
|
+
)
|
|
3224
|
+
return
|
|
3225
|
+
|
|
3226
|
+
logger.info(
|
|
3227
|
+
"Idempotency guard initialized",
|
|
3228
|
+
extra={
|
|
3229
|
+
"store_type": self._idempotency_config.store_type,
|
|
3230
|
+
"domain_from_operation": self._idempotency_config.domain_from_operation,
|
|
3231
|
+
"skip_operations": self._idempotency_config.skip_operations,
|
|
3232
|
+
},
|
|
3233
|
+
)
|
|
3234
|
+
|
|
3235
|
+
except Exception as e:
|
|
3236
|
+
logger.warning(
|
|
3237
|
+
"Failed to initialize idempotency store, proceeding without",
|
|
3238
|
+
extra={"error": str(e)},
|
|
3239
|
+
)
|
|
3240
|
+
self._idempotency_store = None
|
|
3241
|
+
self._idempotency_config = None
|
|
3242
|
+
|
|
3243
|
+
# =========================================================================
|
|
3244
|
+
# WARNING: FAIL-OPEN BEHAVIOR
|
|
3245
|
+
# =========================================================================
|
|
3246
|
+
# This method implements FAIL-OPEN semantics: if the idempotency store
|
|
3247
|
+
# is unavailable or errors, messages are ALLOWED THROUGH for processing.
|
|
3248
|
+
#
|
|
3249
|
+
# This is an intentional design decision prioritizing availability over
|
|
3250
|
+
# exactly-once guarantees. See docstring below for full trade-off analysis.
|
|
3251
|
+
#
|
|
3252
|
+
# IMPORTANT: Downstream handlers MUST be designed for at-least-once delivery
|
|
3253
|
+
# and implement their own idempotency for critical operations.
|
|
3254
|
+
# =========================================================================
|
|
3255
|
+
async def _check_idempotency(
|
|
3256
|
+
self,
|
|
3257
|
+
envelope: dict[str, object],
|
|
3258
|
+
correlation_id: UUID,
|
|
3259
|
+
) -> bool:
|
|
3260
|
+
"""Check if envelope should be processed (idempotency guard).
|
|
3261
|
+
|
|
3262
|
+
Extracts message_id from envelope headers and checks against the
|
|
3263
|
+
idempotency store. If duplicate detected, publishes a duplicate
|
|
3264
|
+
response and returns False.
|
|
3265
|
+
|
|
3266
|
+
Fail-Open Semantics:
|
|
3267
|
+
This method implements **fail-open** error handling: if the
|
|
3268
|
+
idempotency store is unavailable or throws an error, the message
|
|
3269
|
+
is allowed through for processing (with a warning log).
|
|
3270
|
+
|
|
3271
|
+
**Design Rationale**: In distributed event-driven systems, the
|
|
3272
|
+
idempotency store (e.g., Redis/Valkey) is a supporting service,
|
|
3273
|
+
not a critical path dependency. A temporary store outage should
|
|
3274
|
+
not halt message processing entirely, as this would cascade into
|
|
3275
|
+
broader system unavailability.
|
|
3276
|
+
|
|
3277
|
+
**Trade-offs**:
|
|
3278
|
+
- Pro: High availability - processing continues during store outages
|
|
3279
|
+
- Pro: Graceful degradation - system remains functional
|
|
3280
|
+
- Con: May result in duplicate message processing during outages
|
|
3281
|
+
- Con: Downstream handlers must be designed for at-least-once delivery
|
|
3282
|
+
|
|
3283
|
+
**Mitigation**: Handlers consuming messages should implement their
|
|
3284
|
+
own idempotency logic for critical operations (e.g., using database
|
|
3285
|
+
constraints or transaction guards) to ensure correctness even when
|
|
3286
|
+
duplicates slip through.
|
|
3287
|
+
|
|
3288
|
+
Args:
|
|
3289
|
+
envelope: Validated envelope dict.
|
|
3290
|
+
correlation_id: Normalized correlation ID (UUID).
|
|
3291
|
+
|
|
3292
|
+
Returns:
|
|
3293
|
+
True if message should be processed (new message).
|
|
3294
|
+
False if message is duplicate (skip processing).
|
|
3295
|
+
"""
|
|
3296
|
+
# Skip check if idempotency not configured
|
|
3297
|
+
if self._idempotency_store is None or self._idempotency_config is None:
|
|
3298
|
+
return True
|
|
3299
|
+
|
|
3300
|
+
if not self._idempotency_config.enabled:
|
|
3301
|
+
return True
|
|
3302
|
+
|
|
3303
|
+
# Check if operation is in skip list
|
|
3304
|
+
operation = envelope.get("operation")
|
|
3305
|
+
if isinstance(operation, str):
|
|
3306
|
+
if not self._idempotency_config.should_check_idempotency(operation):
|
|
3307
|
+
logger.debug(
|
|
3308
|
+
"Skipping idempotency check for operation",
|
|
3309
|
+
extra={
|
|
3310
|
+
"operation": operation,
|
|
3311
|
+
"correlation_id": str(correlation_id),
|
|
3312
|
+
},
|
|
3313
|
+
)
|
|
3314
|
+
return True
|
|
3315
|
+
|
|
3316
|
+
# Extract message_id from envelope
|
|
3317
|
+
message_id = self._extract_message_id(envelope, correlation_id)
|
|
3318
|
+
|
|
3319
|
+
# Extract domain from operation if configured
|
|
3320
|
+
domain = self._extract_idempotency_domain(envelope)
|
|
3321
|
+
|
|
3322
|
+
# Check and record in store
|
|
3323
|
+
try:
|
|
3324
|
+
is_new = await self._idempotency_store.check_and_record(
|
|
3325
|
+
message_id=message_id,
|
|
3326
|
+
domain=domain,
|
|
3327
|
+
correlation_id=correlation_id,
|
|
3328
|
+
)
|
|
3329
|
+
|
|
3330
|
+
if not is_new:
|
|
3331
|
+
# Duplicate detected - publish duplicate response (NOT an error)
|
|
3332
|
+
logger.info(
|
|
3333
|
+
"Duplicate message detected, skipping processing",
|
|
3334
|
+
extra={
|
|
3335
|
+
"message_id": str(message_id),
|
|
3336
|
+
"domain": domain,
|
|
3337
|
+
"correlation_id": str(correlation_id),
|
|
3338
|
+
},
|
|
3339
|
+
)
|
|
3340
|
+
|
|
3341
|
+
duplicate_response = self._create_duplicate_response(
|
|
3342
|
+
message_id=message_id,
|
|
3343
|
+
correlation_id=correlation_id,
|
|
3344
|
+
)
|
|
3345
|
+
# duplicate_response is already a dict from _create_duplicate_response
|
|
3346
|
+
await self._publish_envelope_safe(
|
|
3347
|
+
duplicate_response, self._output_topic
|
|
3348
|
+
)
|
|
3349
|
+
return False
|
|
3350
|
+
|
|
3351
|
+
return True
|
|
3352
|
+
|
|
3353
|
+
except Exception as e:
|
|
3354
|
+
# FAIL-OPEN: Allow message through on idempotency store errors.
|
|
3355
|
+
# Rationale: Availability over exactly-once. Store outages should not
|
|
3356
|
+
# halt processing. Downstream handlers must tolerate duplicates.
|
|
3357
|
+
# See docstring for full trade-off analysis.
|
|
3358
|
+
logger.warning(
|
|
3359
|
+
"Idempotency check failed, allowing message through (fail-open)",
|
|
3360
|
+
extra={
|
|
3361
|
+
"error": str(e),
|
|
3362
|
+
"error_type": type(e).__name__,
|
|
3363
|
+
"message_id": str(message_id),
|
|
3364
|
+
"domain": domain,
|
|
3365
|
+
"correlation_id": str(correlation_id),
|
|
3366
|
+
},
|
|
3367
|
+
)
|
|
3368
|
+
return True
|
|
3369
|
+
|
|
3370
|
+
def _extract_message_id(
|
|
3371
|
+
self,
|
|
3372
|
+
envelope: dict[str, object],
|
|
3373
|
+
correlation_id: UUID,
|
|
3374
|
+
) -> UUID:
|
|
3375
|
+
"""Extract message_id from envelope, falling back to correlation_id.
|
|
3376
|
+
|
|
3377
|
+
Priority:
|
|
3378
|
+
1. envelope["headers"]["message_id"]
|
|
3379
|
+
2. envelope["message_id"]
|
|
3380
|
+
3. Use correlation_id as message_id (fallback)
|
|
3381
|
+
|
|
3382
|
+
Args:
|
|
3383
|
+
envelope: Envelope dict to extract message_id from.
|
|
3384
|
+
correlation_id: Fallback UUID if message_id not found.
|
|
3385
|
+
|
|
3386
|
+
Returns:
|
|
3387
|
+
UUID representing the message_id.
|
|
3388
|
+
"""
|
|
3389
|
+
# Try headers first
|
|
3390
|
+
headers = envelope.get("headers")
|
|
3391
|
+
if isinstance(headers, dict):
|
|
3392
|
+
header_msg_id = headers.get("message_id")
|
|
3393
|
+
if header_msg_id is not None:
|
|
3394
|
+
if isinstance(header_msg_id, UUID):
|
|
3395
|
+
return header_msg_id
|
|
3396
|
+
if isinstance(header_msg_id, str):
|
|
3397
|
+
try:
|
|
3398
|
+
return UUID(header_msg_id)
|
|
3399
|
+
except ValueError:
|
|
3400
|
+
pass
|
|
3401
|
+
|
|
3402
|
+
# Try top-level message_id
|
|
3403
|
+
top_level_msg_id = envelope.get("message_id")
|
|
3404
|
+
if top_level_msg_id is not None:
|
|
3405
|
+
if isinstance(top_level_msg_id, UUID):
|
|
3406
|
+
return top_level_msg_id
|
|
3407
|
+
if isinstance(top_level_msg_id, str):
|
|
3408
|
+
try:
|
|
3409
|
+
return UUID(top_level_msg_id)
|
|
3410
|
+
except ValueError:
|
|
3411
|
+
pass
|
|
3412
|
+
|
|
3413
|
+
# Fallback: use correlation_id as message_id
|
|
3414
|
+
return correlation_id
|
|
3415
|
+
|
|
3416
|
+
def _extract_idempotency_domain(
|
|
3417
|
+
self,
|
|
3418
|
+
envelope: dict[str, object],
|
|
3419
|
+
) -> str | None:
|
|
3420
|
+
"""Extract domain for idempotency key from envelope.
|
|
3421
|
+
|
|
3422
|
+
If domain_from_operation is enabled in config, extracts domain
|
|
3423
|
+
from the operation prefix (e.g., "db.query" -> "db").
|
|
3424
|
+
|
|
3425
|
+
Args:
|
|
3426
|
+
envelope: Envelope dict to extract domain from.
|
|
3427
|
+
|
|
3428
|
+
Returns:
|
|
3429
|
+
Domain string if found and configured, None otherwise.
|
|
3430
|
+
"""
|
|
3431
|
+
if self._idempotency_config is None:
|
|
3432
|
+
return None
|
|
3433
|
+
|
|
3434
|
+
if not self._idempotency_config.domain_from_operation:
|
|
3435
|
+
return None
|
|
3436
|
+
|
|
3437
|
+
operation = envelope.get("operation")
|
|
3438
|
+
if isinstance(operation, str):
|
|
3439
|
+
return self._idempotency_config.extract_domain(operation)
|
|
3440
|
+
|
|
3441
|
+
return None
|
|
3442
|
+
|
|
3443
|
+
def _create_duplicate_response(
|
|
3444
|
+
self,
|
|
3445
|
+
message_id: UUID,
|
|
3446
|
+
correlation_id: UUID,
|
|
3447
|
+
) -> dict[str, object]:
|
|
3448
|
+
"""Create response for duplicate message detection.
|
|
3449
|
+
|
|
3450
|
+
This is NOT an error response - duplicates are expected under
|
|
3451
|
+
at-least-once delivery. The response indicates successful
|
|
3452
|
+
deduplication.
|
|
3453
|
+
|
|
3454
|
+
Args:
|
|
3455
|
+
message_id: UUID of the duplicate message.
|
|
3456
|
+
correlation_id: Correlation ID for tracing.
|
|
3457
|
+
|
|
3458
|
+
Returns:
|
|
3459
|
+
Dict representation of ModelDuplicateResponse for envelope publishing.
|
|
3460
|
+
"""
|
|
3461
|
+
return ModelDuplicateResponse(
|
|
3462
|
+
message_id=message_id,
|
|
3463
|
+
correlation_id=correlation_id,
|
|
3464
|
+
).model_dump()
|
|
3465
|
+
|
|
3466
|
+
async def _cleanup_idempotency_store(self) -> None:
|
|
3467
|
+
"""Cleanup idempotency store during shutdown.
|
|
3468
|
+
|
|
3469
|
+
Closes the idempotency store connection if initialized.
|
|
3470
|
+
Called during stop() to release resources.
|
|
3471
|
+
"""
|
|
3472
|
+
if self._idempotency_store is None:
|
|
3473
|
+
return
|
|
3474
|
+
|
|
3475
|
+
try:
|
|
3476
|
+
if hasattr(self._idempotency_store, "shutdown"):
|
|
3477
|
+
await self._idempotency_store.shutdown()
|
|
3478
|
+
elif hasattr(self._idempotency_store, "close"):
|
|
3479
|
+
await self._idempotency_store.close()
|
|
3480
|
+
logger.debug("Idempotency store shutdown complete")
|
|
3481
|
+
except Exception as e:
|
|
3482
|
+
logger.warning(
|
|
3483
|
+
"Failed to shutdown idempotency store",
|
|
3484
|
+
extra={"error": str(e)},
|
|
3485
|
+
)
|
|
3486
|
+
finally:
|
|
3487
|
+
self._idempotency_store = None
|
|
3488
|
+
|
|
3489
|
+
|
|
3490
|
+
__all__: list[str] = [
|
|
3491
|
+
"RuntimeHostProcess",
|
|
3492
|
+
"wire_handlers",
|
|
3493
|
+
]
|