omnibase_infra 0.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- omnibase_infra/__init__.py +101 -0
- omnibase_infra/cli/__init__.py +1 -0
- omnibase_infra/cli/commands.py +216 -0
- omnibase_infra/clients/__init__.py +0 -0
- omnibase_infra/contracts/handlers/filesystem/handler_contract.yaml +261 -0
- omnibase_infra/contracts/handlers/mcp/handler_contract.yaml +138 -0
- omnibase_infra/decorators/__init__.py +29 -0
- omnibase_infra/decorators/allow_any.py +109 -0
- omnibase_infra/dlq/__init__.py +90 -0
- omnibase_infra/dlq/constants_dlq.py +57 -0
- omnibase_infra/dlq/models/__init__.py +26 -0
- omnibase_infra/dlq/models/enum_replay_status.py +37 -0
- omnibase_infra/dlq/models/model_dlq_replay_record.py +135 -0
- omnibase_infra/dlq/models/model_dlq_tracking_config.py +184 -0
- omnibase_infra/dlq/service_dlq_tracking.py +611 -0
- omnibase_infra/enums/__init__.py +123 -0
- omnibase_infra/enums/enum_any_type_violation.py +104 -0
- omnibase_infra/enums/enum_backend_type.py +27 -0
- omnibase_infra/enums/enum_capture_outcome.py +42 -0
- omnibase_infra/enums/enum_capture_state.py +88 -0
- omnibase_infra/enums/enum_chain_violation_type.py +119 -0
- omnibase_infra/enums/enum_circuit_state.py +51 -0
- omnibase_infra/enums/enum_confirmation_event_type.py +27 -0
- omnibase_infra/enums/enum_contract_type.py +84 -0
- omnibase_infra/enums/enum_dedupe_strategy.py +46 -0
- omnibase_infra/enums/enum_dispatch_status.py +191 -0
- omnibase_infra/enums/enum_environment.py +46 -0
- omnibase_infra/enums/enum_execution_shape_violation.py +103 -0
- omnibase_infra/enums/enum_handler_error_type.py +101 -0
- omnibase_infra/enums/enum_handler_loader_error.py +178 -0
- omnibase_infra/enums/enum_handler_source_type.py +87 -0
- omnibase_infra/enums/enum_handler_type.py +77 -0
- omnibase_infra/enums/enum_handler_type_category.py +61 -0
- omnibase_infra/enums/enum_infra_transport_type.py +73 -0
- omnibase_infra/enums/enum_introspection_reason.py +154 -0
- omnibase_infra/enums/enum_message_category.py +213 -0
- omnibase_infra/enums/enum_node_archetype.py +74 -0
- omnibase_infra/enums/enum_node_output_type.py +185 -0
- omnibase_infra/enums/enum_non_retryable_error_category.py +224 -0
- omnibase_infra/enums/enum_policy_type.py +32 -0
- omnibase_infra/enums/enum_registration_state.py +261 -0
- omnibase_infra/enums/enum_registration_status.py +33 -0
- omnibase_infra/enums/enum_registry_response_status.py +28 -0
- omnibase_infra/enums/enum_response_status.py +26 -0
- omnibase_infra/enums/enum_retry_error_category.py +98 -0
- omnibase_infra/enums/enum_security_rule_id.py +103 -0
- omnibase_infra/enums/enum_selection_strategy.py +91 -0
- omnibase_infra/enums/enum_topic_standard.py +42 -0
- omnibase_infra/enums/enum_validation_severity.py +78 -0
- omnibase_infra/errors/__init__.py +156 -0
- omnibase_infra/errors/error_architecture_violation.py +152 -0
- omnibase_infra/errors/error_chain_propagation.py +188 -0
- omnibase_infra/errors/error_compute_registry.py +92 -0
- omnibase_infra/errors/error_consul.py +132 -0
- omnibase_infra/errors/error_container_wiring.py +243 -0
- omnibase_infra/errors/error_event_bus_registry.py +102 -0
- omnibase_infra/errors/error_infra.py +608 -0
- omnibase_infra/errors/error_message_type_registry.py +101 -0
- omnibase_infra/errors/error_policy_registry.py +112 -0
- omnibase_infra/errors/error_vault.py +123 -0
- omnibase_infra/event_bus/__init__.py +72 -0
- omnibase_infra/event_bus/configs/kafka_event_bus_config.yaml +86 -0
- omnibase_infra/event_bus/event_bus_inmemory.py +743 -0
- omnibase_infra/event_bus/event_bus_kafka.py +1658 -0
- omnibase_infra/event_bus/mixin_kafka_broadcast.py +184 -0
- omnibase_infra/event_bus/mixin_kafka_dlq.py +765 -0
- omnibase_infra/event_bus/models/__init__.py +29 -0
- omnibase_infra/event_bus/models/config/__init__.py +20 -0
- omnibase_infra/event_bus/models/config/model_kafka_event_bus_config.py +725 -0
- omnibase_infra/event_bus/models/model_dlq_event.py +206 -0
- omnibase_infra/event_bus/models/model_dlq_metrics.py +304 -0
- omnibase_infra/event_bus/models/model_event_headers.py +115 -0
- omnibase_infra/event_bus/models/model_event_message.py +60 -0
- omnibase_infra/event_bus/topic_constants.py +376 -0
- omnibase_infra/handlers/__init__.py +75 -0
- omnibase_infra/handlers/filesystem/__init__.py +48 -0
- omnibase_infra/handlers/filesystem/enum_file_system_operation.py +35 -0
- omnibase_infra/handlers/filesystem/model_file_system_request.py +298 -0
- omnibase_infra/handlers/filesystem/model_file_system_result.py +166 -0
- omnibase_infra/handlers/handler_consul.py +787 -0
- omnibase_infra/handlers/handler_db.py +1039 -0
- omnibase_infra/handlers/handler_filesystem.py +1478 -0
- omnibase_infra/handlers/handler_graph.py +1154 -0
- omnibase_infra/handlers/handler_http.py +920 -0
- omnibase_infra/handlers/handler_manifest_persistence.contract.yaml +184 -0
- omnibase_infra/handlers/handler_manifest_persistence.py +1539 -0
- omnibase_infra/handlers/handler_mcp.py +748 -0
- omnibase_infra/handlers/handler_qdrant.py +1076 -0
- omnibase_infra/handlers/handler_vault.py +422 -0
- omnibase_infra/handlers/mcp/__init__.py +19 -0
- omnibase_infra/handlers/mcp/adapter_onex_to_mcp.py +446 -0
- omnibase_infra/handlers/mcp/protocols.py +178 -0
- omnibase_infra/handlers/mcp/transport_streamable_http.py +352 -0
- omnibase_infra/handlers/mixins/__init__.py +42 -0
- omnibase_infra/handlers/mixins/mixin_consul_initialization.py +349 -0
- omnibase_infra/handlers/mixins/mixin_consul_kv.py +337 -0
- omnibase_infra/handlers/mixins/mixin_consul_service.py +277 -0
- omnibase_infra/handlers/mixins/mixin_vault_initialization.py +338 -0
- omnibase_infra/handlers/mixins/mixin_vault_retry.py +412 -0
- omnibase_infra/handlers/mixins/mixin_vault_secrets.py +450 -0
- omnibase_infra/handlers/mixins/mixin_vault_token.py +365 -0
- omnibase_infra/handlers/models/__init__.py +286 -0
- omnibase_infra/handlers/models/consul/__init__.py +81 -0
- omnibase_infra/handlers/models/consul/enum_consul_operation_type.py +57 -0
- omnibase_infra/handlers/models/consul/model_consul_deregister_payload.py +51 -0
- omnibase_infra/handlers/models/consul/model_consul_handler_config.py +153 -0
- omnibase_infra/handlers/models/consul/model_consul_handler_payload.py +89 -0
- omnibase_infra/handlers/models/consul/model_consul_kv_get_found_payload.py +55 -0
- omnibase_infra/handlers/models/consul/model_consul_kv_get_not_found_payload.py +49 -0
- omnibase_infra/handlers/models/consul/model_consul_kv_get_recurse_payload.py +50 -0
- omnibase_infra/handlers/models/consul/model_consul_kv_item.py +33 -0
- omnibase_infra/handlers/models/consul/model_consul_kv_put_payload.py +41 -0
- omnibase_infra/handlers/models/consul/model_consul_register_payload.py +53 -0
- omnibase_infra/handlers/models/consul/model_consul_retry_config.py +66 -0
- omnibase_infra/handlers/models/consul/model_payload_consul.py +66 -0
- omnibase_infra/handlers/models/consul/registry_payload_consul.py +214 -0
- omnibase_infra/handlers/models/graph/__init__.py +35 -0
- omnibase_infra/handlers/models/graph/enum_graph_operation_type.py +20 -0
- omnibase_infra/handlers/models/graph/model_graph_execute_payload.py +38 -0
- omnibase_infra/handlers/models/graph/model_graph_handler_config.py +54 -0
- omnibase_infra/handlers/models/graph/model_graph_handler_payload.py +44 -0
- omnibase_infra/handlers/models/graph/model_graph_query_payload.py +40 -0
- omnibase_infra/handlers/models/graph/model_graph_record.py +22 -0
- omnibase_infra/handlers/models/http/__init__.py +50 -0
- omnibase_infra/handlers/models/http/enum_http_operation_type.py +29 -0
- omnibase_infra/handlers/models/http/model_http_body_content.py +45 -0
- omnibase_infra/handlers/models/http/model_http_get_payload.py +88 -0
- omnibase_infra/handlers/models/http/model_http_handler_payload.py +90 -0
- omnibase_infra/handlers/models/http/model_http_post_payload.py +88 -0
- omnibase_infra/handlers/models/http/model_payload_http.py +66 -0
- omnibase_infra/handlers/models/http/registry_payload_http.py +212 -0
- omnibase_infra/handlers/models/mcp/__init__.py +23 -0
- omnibase_infra/handlers/models/mcp/enum_mcp_operation_type.py +24 -0
- omnibase_infra/handlers/models/mcp/model_mcp_handler_config.py +40 -0
- omnibase_infra/handlers/models/mcp/model_mcp_tool_call.py +32 -0
- omnibase_infra/handlers/models/mcp/model_mcp_tool_result.py +45 -0
- omnibase_infra/handlers/models/model_consul_handler_response.py +96 -0
- omnibase_infra/handlers/models/model_db_describe_response.py +83 -0
- omnibase_infra/handlers/models/model_db_query_payload.py +95 -0
- omnibase_infra/handlers/models/model_db_query_response.py +60 -0
- omnibase_infra/handlers/models/model_filesystem_config.py +98 -0
- omnibase_infra/handlers/models/model_filesystem_delete_payload.py +54 -0
- omnibase_infra/handlers/models/model_filesystem_delete_result.py +77 -0
- omnibase_infra/handlers/models/model_filesystem_directory_entry.py +75 -0
- omnibase_infra/handlers/models/model_filesystem_ensure_directory_payload.py +54 -0
- omnibase_infra/handlers/models/model_filesystem_ensure_directory_result.py +60 -0
- omnibase_infra/handlers/models/model_filesystem_list_directory_payload.py +60 -0
- omnibase_infra/handlers/models/model_filesystem_list_directory_result.py +68 -0
- omnibase_infra/handlers/models/model_filesystem_read_payload.py +62 -0
- omnibase_infra/handlers/models/model_filesystem_read_result.py +61 -0
- omnibase_infra/handlers/models/model_filesystem_write_payload.py +70 -0
- omnibase_infra/handlers/models/model_filesystem_write_result.py +55 -0
- omnibase_infra/handlers/models/model_graph_handler_response.py +98 -0
- omnibase_infra/handlers/models/model_handler_response.py +103 -0
- omnibase_infra/handlers/models/model_http_handler_response.py +101 -0
- omnibase_infra/handlers/models/model_manifest_metadata.py +75 -0
- omnibase_infra/handlers/models/model_manifest_persistence_config.py +62 -0
- omnibase_infra/handlers/models/model_manifest_query_payload.py +90 -0
- omnibase_infra/handlers/models/model_manifest_query_result.py +97 -0
- omnibase_infra/handlers/models/model_manifest_retrieve_payload.py +44 -0
- omnibase_infra/handlers/models/model_manifest_retrieve_result.py +98 -0
- omnibase_infra/handlers/models/model_manifest_store_payload.py +47 -0
- omnibase_infra/handlers/models/model_manifest_store_result.py +67 -0
- omnibase_infra/handlers/models/model_operation_context.py +187 -0
- omnibase_infra/handlers/models/model_qdrant_handler_response.py +98 -0
- omnibase_infra/handlers/models/model_retry_state.py +162 -0
- omnibase_infra/handlers/models/model_vault_handler_response.py +98 -0
- omnibase_infra/handlers/models/qdrant/__init__.py +44 -0
- omnibase_infra/handlers/models/qdrant/enum_qdrant_operation_type.py +26 -0
- omnibase_infra/handlers/models/qdrant/model_qdrant_collection_payload.py +42 -0
- omnibase_infra/handlers/models/qdrant/model_qdrant_delete_payload.py +36 -0
- omnibase_infra/handlers/models/qdrant/model_qdrant_handler_config.py +42 -0
- omnibase_infra/handlers/models/qdrant/model_qdrant_handler_payload.py +54 -0
- omnibase_infra/handlers/models/qdrant/model_qdrant_search_payload.py +42 -0
- omnibase_infra/handlers/models/qdrant/model_qdrant_search_result.py +30 -0
- omnibase_infra/handlers/models/qdrant/model_qdrant_upsert_payload.py +36 -0
- omnibase_infra/handlers/models/vault/__init__.py +69 -0
- omnibase_infra/handlers/models/vault/enum_vault_operation_type.py +35 -0
- omnibase_infra/handlers/models/vault/model_payload_vault.py +66 -0
- omnibase_infra/handlers/models/vault/model_vault_delete_payload.py +57 -0
- omnibase_infra/handlers/models/vault/model_vault_handler_config.py +148 -0
- omnibase_infra/handlers/models/vault/model_vault_handler_payload.py +101 -0
- omnibase_infra/handlers/models/vault/model_vault_list_payload.py +58 -0
- omnibase_infra/handlers/models/vault/model_vault_renew_token_payload.py +67 -0
- omnibase_infra/handlers/models/vault/model_vault_retry_config.py +66 -0
- omnibase_infra/handlers/models/vault/model_vault_secret_payload.py +106 -0
- omnibase_infra/handlers/models/vault/model_vault_write_payload.py +66 -0
- omnibase_infra/handlers/models/vault/registry_payload_vault.py +213 -0
- omnibase_infra/handlers/registration_storage/__init__.py +43 -0
- omnibase_infra/handlers/registration_storage/handler_registration_storage_mock.py +392 -0
- omnibase_infra/handlers/registration_storage/handler_registration_storage_postgres.py +915 -0
- omnibase_infra/handlers/registration_storage/models/__init__.py +23 -0
- omnibase_infra/handlers/registration_storage/models/model_delete_registration_request.py +58 -0
- omnibase_infra/handlers/registration_storage/models/model_update_registration_request.py +73 -0
- omnibase_infra/handlers/registration_storage/protocol_registration_persistence.py +191 -0
- omnibase_infra/handlers/service_discovery/__init__.py +43 -0
- omnibase_infra/handlers/service_discovery/handler_service_discovery_consul.py +747 -0
- omnibase_infra/handlers/service_discovery/handler_service_discovery_mock.py +258 -0
- omnibase_infra/handlers/service_discovery/models/__init__.py +22 -0
- omnibase_infra/handlers/service_discovery/models/model_discovery_result.py +64 -0
- omnibase_infra/handlers/service_discovery/models/model_registration_result.py +138 -0
- omnibase_infra/handlers/service_discovery/models/model_service_info.py +99 -0
- omnibase_infra/handlers/service_discovery/protocol_discovery_operations.py +170 -0
- omnibase_infra/idempotency/__init__.py +94 -0
- omnibase_infra/idempotency/models/__init__.py +43 -0
- omnibase_infra/idempotency/models/model_idempotency_check_result.py +85 -0
- omnibase_infra/idempotency/models/model_idempotency_guard_config.py +130 -0
- omnibase_infra/idempotency/models/model_idempotency_record.py +86 -0
- omnibase_infra/idempotency/models/model_idempotency_store_health_check_result.py +81 -0
- omnibase_infra/idempotency/models/model_idempotency_store_metrics.py +140 -0
- omnibase_infra/idempotency/models/model_postgres_idempotency_store_config.py +299 -0
- omnibase_infra/idempotency/protocol_idempotency_store.py +184 -0
- omnibase_infra/idempotency/store_inmemory.py +265 -0
- omnibase_infra/idempotency/store_postgres.py +923 -0
- omnibase_infra/infrastructure/__init__.py +0 -0
- omnibase_infra/mixins/__init__.py +71 -0
- omnibase_infra/mixins/mixin_async_circuit_breaker.py +655 -0
- omnibase_infra/mixins/mixin_dict_like_accessors.py +146 -0
- omnibase_infra/mixins/mixin_envelope_extraction.py +119 -0
- omnibase_infra/mixins/mixin_node_introspection.py +2465 -0
- omnibase_infra/mixins/mixin_retry_execution.py +386 -0
- omnibase_infra/mixins/protocol_circuit_breaker_aware.py +133 -0
- omnibase_infra/models/__init__.py +136 -0
- omnibase_infra/models/corpus/__init__.py +17 -0
- omnibase_infra/models/corpus/model_capture_config.py +133 -0
- omnibase_infra/models/corpus/model_capture_result.py +86 -0
- omnibase_infra/models/discovery/__init__.py +42 -0
- omnibase_infra/models/discovery/model_dependency_spec.py +319 -0
- omnibase_infra/models/discovery/model_discovered_capabilities.py +50 -0
- omnibase_infra/models/discovery/model_introspection_config.py +311 -0
- omnibase_infra/models/discovery/model_introspection_performance_metrics.py +169 -0
- omnibase_infra/models/discovery/model_introspection_task_config.py +116 -0
- omnibase_infra/models/dispatch/__init__.py +147 -0
- omnibase_infra/models/dispatch/model_dispatch_context.py +439 -0
- omnibase_infra/models/dispatch/model_dispatch_error.py +336 -0
- omnibase_infra/models/dispatch/model_dispatch_log_context.py +400 -0
- omnibase_infra/models/dispatch/model_dispatch_metadata.py +228 -0
- omnibase_infra/models/dispatch/model_dispatch_metrics.py +496 -0
- omnibase_infra/models/dispatch/model_dispatch_outcome.py +317 -0
- omnibase_infra/models/dispatch/model_dispatch_outputs.py +231 -0
- omnibase_infra/models/dispatch/model_dispatch_result.py +436 -0
- omnibase_infra/models/dispatch/model_dispatch_route.py +279 -0
- omnibase_infra/models/dispatch/model_dispatcher_metrics.py +275 -0
- omnibase_infra/models/dispatch/model_dispatcher_registration.py +352 -0
- omnibase_infra/models/dispatch/model_parsed_topic.py +135 -0
- omnibase_infra/models/dispatch/model_topic_parser.py +725 -0
- omnibase_infra/models/dispatch/model_tracing_context.py +285 -0
- omnibase_infra/models/errors/__init__.py +45 -0
- omnibase_infra/models/errors/model_handler_validation_error.py +594 -0
- omnibase_infra/models/errors/model_infra_error_context.py +99 -0
- omnibase_infra/models/errors/model_message_type_registry_error_context.py +71 -0
- omnibase_infra/models/errors/model_timeout_error_context.py +110 -0
- omnibase_infra/models/handlers/__init__.py +37 -0
- omnibase_infra/models/handlers/model_contract_discovery_result.py +80 -0
- omnibase_infra/models/handlers/model_handler_descriptor.py +185 -0
- omnibase_infra/models/handlers/model_handler_identifier.py +215 -0
- omnibase_infra/models/health/__init__.py +9 -0
- omnibase_infra/models/health/model_health_check_result.py +40 -0
- omnibase_infra/models/lifecycle/__init__.py +39 -0
- omnibase_infra/models/logging/__init__.py +51 -0
- omnibase_infra/models/logging/model_log_context.py +756 -0
- omnibase_infra/models/model_retry_error_classification.py +78 -0
- omnibase_infra/models/projection/__init__.py +43 -0
- omnibase_infra/models/projection/model_capability_fields.py +112 -0
- omnibase_infra/models/projection/model_registration_projection.py +434 -0
- omnibase_infra/models/projection/model_registration_snapshot.py +322 -0
- omnibase_infra/models/projection/model_sequence_info.py +182 -0
- omnibase_infra/models/projection/model_snapshot_topic_config.py +590 -0
- omnibase_infra/models/projectors/__init__.py +41 -0
- omnibase_infra/models/projectors/model_projector_column.py +289 -0
- omnibase_infra/models/projectors/model_projector_discovery_result.py +65 -0
- omnibase_infra/models/projectors/model_projector_index.py +270 -0
- omnibase_infra/models/projectors/model_projector_schema.py +415 -0
- omnibase_infra/models/projectors/model_projector_validation_error.py +63 -0
- omnibase_infra/models/projectors/util_sql_identifiers.py +115 -0
- omnibase_infra/models/registration/__init__.py +59 -0
- omnibase_infra/models/registration/commands/__init__.py +15 -0
- omnibase_infra/models/registration/commands/model_node_registration_acked.py +108 -0
- omnibase_infra/models/registration/events/__init__.py +56 -0
- omnibase_infra/models/registration/events/model_node_became_active.py +103 -0
- omnibase_infra/models/registration/events/model_node_liveness_expired.py +103 -0
- omnibase_infra/models/registration/events/model_node_registration_accepted.py +98 -0
- omnibase_infra/models/registration/events/model_node_registration_ack_received.py +98 -0
- omnibase_infra/models/registration/events/model_node_registration_ack_timed_out.py +112 -0
- omnibase_infra/models/registration/events/model_node_registration_initiated.py +107 -0
- omnibase_infra/models/registration/events/model_node_registration_rejected.py +104 -0
- omnibase_infra/models/registration/model_introspection_metrics.py +253 -0
- omnibase_infra/models/registration/model_node_capabilities.py +179 -0
- omnibase_infra/models/registration/model_node_heartbeat_event.py +126 -0
- omnibase_infra/models/registration/model_node_introspection_event.py +175 -0
- omnibase_infra/models/registration/model_node_metadata.py +79 -0
- omnibase_infra/models/registration/model_node_registration.py +162 -0
- omnibase_infra/models/registration/model_node_registration_record.py +162 -0
- omnibase_infra/models/registry/__init__.py +29 -0
- omnibase_infra/models/registry/model_domain_constraint.py +202 -0
- omnibase_infra/models/registry/model_message_type_entry.py +271 -0
- omnibase_infra/models/resilience/__init__.py +9 -0
- omnibase_infra/models/resilience/model_circuit_breaker_config.py +227 -0
- omnibase_infra/models/routing/__init__.py +25 -0
- omnibase_infra/models/routing/model_routing_entry.py +52 -0
- omnibase_infra/models/routing/model_routing_subcontract.py +70 -0
- omnibase_infra/models/runtime/__init__.py +40 -0
- omnibase_infra/models/runtime/model_contract_security_config.py +41 -0
- omnibase_infra/models/runtime/model_discovery_error.py +81 -0
- omnibase_infra/models/runtime/model_discovery_result.py +162 -0
- omnibase_infra/models/runtime/model_discovery_warning.py +74 -0
- omnibase_infra/models/runtime/model_failed_plugin_load.py +63 -0
- omnibase_infra/models/runtime/model_handler_contract.py +280 -0
- omnibase_infra/models/runtime/model_loaded_handler.py +120 -0
- omnibase_infra/models/runtime/model_plugin_load_context.py +93 -0
- omnibase_infra/models/runtime/model_plugin_load_summary.py +124 -0
- omnibase_infra/models/security/__init__.py +50 -0
- omnibase_infra/models/security/classification_levels.py +99 -0
- omnibase_infra/models/security/model_environment_policy.py +145 -0
- omnibase_infra/models/security/model_handler_security_policy.py +107 -0
- omnibase_infra/models/security/model_security_error.py +81 -0
- omnibase_infra/models/security/model_security_validation_result.py +328 -0
- omnibase_infra/models/security/model_security_warning.py +67 -0
- omnibase_infra/models/snapshot/__init__.py +27 -0
- omnibase_infra/models/snapshot/model_field_change.py +65 -0
- omnibase_infra/models/snapshot/model_snapshot.py +270 -0
- omnibase_infra/models/snapshot/model_snapshot_diff.py +203 -0
- omnibase_infra/models/snapshot/model_subject_ref.py +81 -0
- omnibase_infra/models/types/__init__.py +71 -0
- omnibase_infra/models/validation/__init__.py +89 -0
- omnibase_infra/models/validation/model_any_type_validation_result.py +118 -0
- omnibase_infra/models/validation/model_any_type_violation.py +141 -0
- omnibase_infra/models/validation/model_category_match_result.py +345 -0
- omnibase_infra/models/validation/model_chain_violation.py +166 -0
- omnibase_infra/models/validation/model_coverage_metrics.py +316 -0
- omnibase_infra/models/validation/model_execution_shape_rule.py +159 -0
- omnibase_infra/models/validation/model_execution_shape_validation.py +208 -0
- omnibase_infra/models/validation/model_execution_shape_validation_result.py +294 -0
- omnibase_infra/models/validation/model_execution_shape_violation.py +122 -0
- omnibase_infra/models/validation/model_localhandler_validation_result.py +139 -0
- omnibase_infra/models/validation/model_localhandler_violation.py +100 -0
- omnibase_infra/models/validation/model_output_validation_params.py +74 -0
- omnibase_infra/models/validation/model_validate_and_raise_params.py +84 -0
- omnibase_infra/models/validation/model_validation_error_params.py +84 -0
- omnibase_infra/models/validation/model_validation_outcome.py +287 -0
- omnibase_infra/nodes/__init__.py +48 -0
- omnibase_infra/nodes/architecture_validator/__init__.py +79 -0
- omnibase_infra/nodes/architecture_validator/contract.yaml +252 -0
- omnibase_infra/nodes/architecture_validator/contract_architecture_validator.yaml +208 -0
- omnibase_infra/nodes/architecture_validator/mixins/__init__.py +16 -0
- omnibase_infra/nodes/architecture_validator/mixins/mixin_file_path_rule.py +92 -0
- omnibase_infra/nodes/architecture_validator/models/__init__.py +36 -0
- omnibase_infra/nodes/architecture_validator/models/model_architecture_validation_request.py +56 -0
- omnibase_infra/nodes/architecture_validator/models/model_architecture_validation_result.py +311 -0
- omnibase_infra/nodes/architecture_validator/models/model_architecture_violation.py +163 -0
- omnibase_infra/nodes/architecture_validator/models/model_rule_check_result.py +265 -0
- omnibase_infra/nodes/architecture_validator/models/model_validation_request.py +105 -0
- omnibase_infra/nodes/architecture_validator/models/model_validation_result.py +314 -0
- omnibase_infra/nodes/architecture_validator/node.py +262 -0
- omnibase_infra/nodes/architecture_validator/node_architecture_validator.py +383 -0
- omnibase_infra/nodes/architecture_validator/protocols/__init__.py +9 -0
- omnibase_infra/nodes/architecture_validator/protocols/protocol_architecture_rule.py +225 -0
- omnibase_infra/nodes/architecture_validator/registry/__init__.py +28 -0
- omnibase_infra/nodes/architecture_validator/registry/registry_infra_architecture_validator.py +99 -0
- omnibase_infra/nodes/architecture_validator/validators/__init__.py +104 -0
- omnibase_infra/nodes/architecture_validator/validators/validator_no_direct_dispatch.py +422 -0
- omnibase_infra/nodes/architecture_validator/validators/validator_no_handler_publishing.py +481 -0
- omnibase_infra/nodes/architecture_validator/validators/validator_no_orchestrator_fsm.py +491 -0
- omnibase_infra/nodes/effects/README.md +358 -0
- omnibase_infra/nodes/effects/__init__.py +26 -0
- omnibase_infra/nodes/effects/contract.yaml +172 -0
- omnibase_infra/nodes/effects/models/__init__.py +32 -0
- omnibase_infra/nodes/effects/models/model_backend_result.py +190 -0
- omnibase_infra/nodes/effects/models/model_effect_idempotency_config.py +92 -0
- omnibase_infra/nodes/effects/models/model_registry_request.py +132 -0
- omnibase_infra/nodes/effects/models/model_registry_response.py +263 -0
- omnibase_infra/nodes/effects/protocol_consul_client.py +89 -0
- omnibase_infra/nodes/effects/protocol_effect_idempotency_store.py +143 -0
- omnibase_infra/nodes/effects/protocol_postgres_adapter.py +96 -0
- omnibase_infra/nodes/effects/registry_effect.py +525 -0
- omnibase_infra/nodes/effects/store_effect_idempotency_inmemory.py +425 -0
- omnibase_infra/nodes/node_registration_orchestrator/README.md +542 -0
- omnibase_infra/nodes/node_registration_orchestrator/__init__.py +120 -0
- omnibase_infra/nodes/node_registration_orchestrator/contract.yaml +475 -0
- omnibase_infra/nodes/node_registration_orchestrator/dispatchers/__init__.py +53 -0
- omnibase_infra/nodes/node_registration_orchestrator/dispatchers/dispatcher_node_introspected.py +376 -0
- omnibase_infra/nodes/node_registration_orchestrator/dispatchers/dispatcher_node_registration_acked.py +376 -0
- omnibase_infra/nodes/node_registration_orchestrator/dispatchers/dispatcher_runtime_tick.py +373 -0
- omnibase_infra/nodes/node_registration_orchestrator/handlers/__init__.py +62 -0
- omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_node_heartbeat.py +376 -0
- omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_node_introspected.py +609 -0
- omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_node_registration_acked.py +458 -0
- omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_runtime_tick.py +364 -0
- omnibase_infra/nodes/node_registration_orchestrator/introspection_event_router.py +544 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/__init__.py +75 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_consul_intent_payload.py +194 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_consul_registration_intent.py +67 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_intent_execution_result.py +50 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_node_liveness_expired.py +107 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_orchestrator_config.py +67 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_orchestrator_input.py +41 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_orchestrator_output.py +166 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_postgres_intent_payload.py +235 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_postgres_upsert_intent.py +68 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_reducer_execution_result.py +384 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_reducer_state.py +60 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_registration_intent.py +177 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_registry_intent.py +247 -0
- omnibase_infra/nodes/node_registration_orchestrator/node.py +195 -0
- omnibase_infra/nodes/node_registration_orchestrator/plugin.py +909 -0
- omnibase_infra/nodes/node_registration_orchestrator/protocols.py +439 -0
- omnibase_infra/nodes/node_registration_orchestrator/registry/__init__.py +41 -0
- omnibase_infra/nodes/node_registration_orchestrator/registry/registry_infra_node_registration_orchestrator.py +525 -0
- omnibase_infra/nodes/node_registration_orchestrator/timeout_coordinator.py +392 -0
- omnibase_infra/nodes/node_registration_orchestrator/wiring.py +742 -0
- omnibase_infra/nodes/node_registration_reducer/__init__.py +15 -0
- omnibase_infra/nodes/node_registration_reducer/contract.yaml +301 -0
- omnibase_infra/nodes/node_registration_reducer/models/__init__.py +38 -0
- omnibase_infra/nodes/node_registration_reducer/models/model_validation_result.py +113 -0
- omnibase_infra/nodes/node_registration_reducer/node.py +139 -0
- omnibase_infra/nodes/node_registration_reducer/registry/__init__.py +9 -0
- omnibase_infra/nodes/node_registration_reducer/registry/registry_infra_node_registration_reducer.py +79 -0
- omnibase_infra/nodes/node_registration_storage_effect/__init__.py +41 -0
- omnibase_infra/nodes/node_registration_storage_effect/contract.yaml +225 -0
- omnibase_infra/nodes/node_registration_storage_effect/models/__init__.py +44 -0
- omnibase_infra/nodes/node_registration_storage_effect/models/model_delete_result.py +132 -0
- omnibase_infra/nodes/node_registration_storage_effect/models/model_registration_record.py +199 -0
- omnibase_infra/nodes/node_registration_storage_effect/models/model_registration_update.py +155 -0
- omnibase_infra/nodes/node_registration_storage_effect/models/model_storage_health_check_details.py +123 -0
- omnibase_infra/nodes/node_registration_storage_effect/models/model_storage_health_check_result.py +117 -0
- omnibase_infra/nodes/node_registration_storage_effect/models/model_storage_query.py +100 -0
- omnibase_infra/nodes/node_registration_storage_effect/models/model_storage_result.py +136 -0
- omnibase_infra/nodes/node_registration_storage_effect/models/model_upsert_result.py +127 -0
- omnibase_infra/nodes/node_registration_storage_effect/node.py +109 -0
- omnibase_infra/nodes/node_registration_storage_effect/protocols/__init__.py +22 -0
- omnibase_infra/nodes/node_registration_storage_effect/protocols/protocol_registration_persistence.py +333 -0
- omnibase_infra/nodes/node_registration_storage_effect/registry/__init__.py +23 -0
- omnibase_infra/nodes/node_registration_storage_effect/registry/registry_infra_registration_storage.py +194 -0
- omnibase_infra/nodes/node_registry_effect/__init__.py +85 -0
- omnibase_infra/nodes/node_registry_effect/contract.yaml +682 -0
- omnibase_infra/nodes/node_registry_effect/handlers/__init__.py +70 -0
- omnibase_infra/nodes/node_registry_effect/handlers/handler_consul_deregister.py +211 -0
- omnibase_infra/nodes/node_registry_effect/handlers/handler_consul_register.py +212 -0
- omnibase_infra/nodes/node_registry_effect/handlers/handler_partial_retry.py +416 -0
- omnibase_infra/nodes/node_registry_effect/handlers/handler_postgres_deactivate.py +215 -0
- omnibase_infra/nodes/node_registry_effect/handlers/handler_postgres_upsert.py +208 -0
- omnibase_infra/nodes/node_registry_effect/models/__init__.py +43 -0
- omnibase_infra/nodes/node_registry_effect/models/model_partial_retry_request.py +92 -0
- omnibase_infra/nodes/node_registry_effect/node.py +165 -0
- omnibase_infra/nodes/node_registry_effect/registry/__init__.py +27 -0
- omnibase_infra/nodes/node_registry_effect/registry/registry_infra_registry_effect.py +196 -0
- omnibase_infra/nodes/node_service_discovery_effect/__init__.py +111 -0
- omnibase_infra/nodes/node_service_discovery_effect/contract.yaml +246 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/__init__.py +67 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/enum_health_status.py +72 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/enum_service_discovery_operation.py +58 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/model_discovery_query.py +99 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/model_discovery_result.py +98 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/model_health_check_config.py +121 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/model_query_metadata.py +63 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/model_registration_result.py +130 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/model_service_discovery_health_check_details.py +111 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/model_service_discovery_health_check_result.py +119 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/model_service_info.py +106 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/model_service_registration.py +121 -0
- omnibase_infra/nodes/node_service_discovery_effect/node.py +111 -0
- omnibase_infra/nodes/node_service_discovery_effect/protocols/__init__.py +14 -0
- omnibase_infra/nodes/node_service_discovery_effect/protocols/protocol_discovery_operations.py +279 -0
- omnibase_infra/nodes/node_service_discovery_effect/registry/__init__.py +13 -0
- omnibase_infra/nodes/node_service_discovery_effect/registry/registry_infra_service_discovery.py +214 -0
- omnibase_infra/nodes/reducers/__init__.py +30 -0
- omnibase_infra/nodes/reducers/models/__init__.py +32 -0
- omnibase_infra/nodes/reducers/models/model_payload_consul_register.py +76 -0
- omnibase_infra/nodes/reducers/models/model_payload_postgres_upsert_registration.py +60 -0
- omnibase_infra/nodes/reducers/models/model_registration_confirmation.py +166 -0
- omnibase_infra/nodes/reducers/models/model_registration_state.py +433 -0
- omnibase_infra/nodes/reducers/registration_reducer.py +1137 -0
- omnibase_infra/observability/__init__.py +143 -0
- omnibase_infra/observability/constants_metrics.py +91 -0
- omnibase_infra/observability/factory_observability_sink.py +525 -0
- omnibase_infra/observability/handlers/__init__.py +118 -0
- omnibase_infra/observability/handlers/handler_logging_structured.py +967 -0
- omnibase_infra/observability/handlers/handler_metrics_prometheus.py +1120 -0
- omnibase_infra/observability/handlers/model_logging_handler_config.py +71 -0
- omnibase_infra/observability/handlers/model_logging_handler_response.py +77 -0
- omnibase_infra/observability/handlers/model_metrics_handler_config.py +172 -0
- omnibase_infra/observability/handlers/model_metrics_handler_payload.py +135 -0
- omnibase_infra/observability/handlers/model_metrics_handler_response.py +101 -0
- omnibase_infra/observability/hooks/__init__.py +74 -0
- omnibase_infra/observability/hooks/hook_observability.py +1223 -0
- omnibase_infra/observability/models/__init__.py +30 -0
- omnibase_infra/observability/models/enum_required_log_context_key.py +77 -0
- omnibase_infra/observability/models/model_buffered_log_entry.py +117 -0
- omnibase_infra/observability/models/model_logging_sink_config.py +73 -0
- omnibase_infra/observability/models/model_metrics_sink_config.py +156 -0
- omnibase_infra/observability/sinks/__init__.py +69 -0
- omnibase_infra/observability/sinks/sink_logging_structured.py +809 -0
- omnibase_infra/observability/sinks/sink_metrics_prometheus.py +710 -0
- omnibase_infra/plugins/__init__.py +27 -0
- omnibase_infra/plugins/examples/__init__.py +28 -0
- omnibase_infra/plugins/examples/plugin_json_normalizer.py +271 -0
- omnibase_infra/plugins/examples/plugin_json_normalizer_error_handling.py +210 -0
- omnibase_infra/plugins/models/__init__.py +21 -0
- omnibase_infra/plugins/models/model_plugin_context.py +76 -0
- omnibase_infra/plugins/models/model_plugin_input_data.py +58 -0
- omnibase_infra/plugins/models/model_plugin_output_data.py +62 -0
- omnibase_infra/plugins/plugin_compute_base.py +435 -0
- omnibase_infra/projectors/__init__.py +30 -0
- omnibase_infra/projectors/contracts/__init__.py +63 -0
- omnibase_infra/projectors/contracts/registration_projector.yaml +370 -0
- omnibase_infra/projectors/projection_reader_registration.py +1559 -0
- omnibase_infra/projectors/snapshot_publisher_registration.py +1329 -0
- omnibase_infra/protocols/__init__.py +99 -0
- omnibase_infra/protocols/protocol_capability_projection.py +253 -0
- omnibase_infra/protocols/protocol_capability_query.py +251 -0
- omnibase_infra/protocols/protocol_event_bus_like.py +127 -0
- omnibase_infra/protocols/protocol_event_projector.py +96 -0
- omnibase_infra/protocols/protocol_idempotency_store.py +142 -0
- omnibase_infra/protocols/protocol_message_dispatcher.py +247 -0
- omnibase_infra/protocols/protocol_message_type_registry.py +306 -0
- omnibase_infra/protocols/protocol_plugin_compute.py +368 -0
- omnibase_infra/protocols/protocol_projector_schema_validator.py +82 -0
- omnibase_infra/protocols/protocol_registry_metrics.py +215 -0
- omnibase_infra/protocols/protocol_snapshot_publisher.py +396 -0
- omnibase_infra/protocols/protocol_snapshot_store.py +567 -0
- omnibase_infra/runtime/__init__.py +296 -0
- omnibase_infra/runtime/binding_config_resolver.py +2706 -0
- omnibase_infra/runtime/chain_aware_dispatch.py +467 -0
- omnibase_infra/runtime/contract_handler_discovery.py +582 -0
- omnibase_infra/runtime/contract_loaders/__init__.py +42 -0
- omnibase_infra/runtime/contract_loaders/handler_routing_loader.py +464 -0
- omnibase_infra/runtime/dispatch_context_enforcer.py +427 -0
- omnibase_infra/runtime/enums/__init__.py +18 -0
- omnibase_infra/runtime/enums/enum_config_ref_scheme.py +33 -0
- omnibase_infra/runtime/enums/enum_scheduler_status.py +170 -0
- omnibase_infra/runtime/envelope_validator.py +179 -0
- omnibase_infra/runtime/handler_contract_source.py +669 -0
- omnibase_infra/runtime/handler_plugin_loader.py +2029 -0
- omnibase_infra/runtime/handler_registry.py +321 -0
- omnibase_infra/runtime/invocation_security_enforcer.py +427 -0
- omnibase_infra/runtime/kernel.py +40 -0
- omnibase_infra/runtime/mixin_policy_validation.py +522 -0
- omnibase_infra/runtime/mixin_semver_cache.py +378 -0
- omnibase_infra/runtime/mixins/__init__.py +17 -0
- omnibase_infra/runtime/mixins/mixin_projector_sql_operations.py +757 -0
- omnibase_infra/runtime/models/__init__.py +192 -0
- omnibase_infra/runtime/models/model_batch_lifecycle_result.py +217 -0
- omnibase_infra/runtime/models/model_binding_config.py +168 -0
- omnibase_infra/runtime/models/model_binding_config_cache_stats.py +135 -0
- omnibase_infra/runtime/models/model_binding_config_resolver_config.py +329 -0
- omnibase_infra/runtime/models/model_cached_secret.py +138 -0
- omnibase_infra/runtime/models/model_compute_key.py +138 -0
- omnibase_infra/runtime/models/model_compute_registration.py +97 -0
- omnibase_infra/runtime/models/model_config_cache_entry.py +61 -0
- omnibase_infra/runtime/models/model_config_ref.py +331 -0
- omnibase_infra/runtime/models/model_config_ref_parse_result.py +125 -0
- omnibase_infra/runtime/models/model_domain_plugin_config.py +92 -0
- omnibase_infra/runtime/models/model_domain_plugin_result.py +270 -0
- omnibase_infra/runtime/models/model_duplicate_response.py +54 -0
- omnibase_infra/runtime/models/model_enabled_protocols_config.py +61 -0
- omnibase_infra/runtime/models/model_event_bus_config.py +54 -0
- omnibase_infra/runtime/models/model_failed_component.py +55 -0
- omnibase_infra/runtime/models/model_health_check_response.py +168 -0
- omnibase_infra/runtime/models/model_health_check_result.py +228 -0
- omnibase_infra/runtime/models/model_lifecycle_result.py +245 -0
- omnibase_infra/runtime/models/model_logging_config.py +42 -0
- omnibase_infra/runtime/models/model_optional_correlation_id.py +167 -0
- omnibase_infra/runtime/models/model_optional_string.py +94 -0
- omnibase_infra/runtime/models/model_optional_uuid.py +110 -0
- omnibase_infra/runtime/models/model_policy_context.py +100 -0
- omnibase_infra/runtime/models/model_policy_key.py +138 -0
- omnibase_infra/runtime/models/model_policy_registration.py +139 -0
- omnibase_infra/runtime/models/model_policy_result.py +103 -0
- omnibase_infra/runtime/models/model_policy_type_filter.py +157 -0
- omnibase_infra/runtime/models/model_projector_plugin_loader_config.py +47 -0
- omnibase_infra/runtime/models/model_protocol_registration_config.py +65 -0
- omnibase_infra/runtime/models/model_retry_policy.py +105 -0
- omnibase_infra/runtime/models/model_runtime_config.py +150 -0
- omnibase_infra/runtime/models/model_runtime_scheduler_config.py +624 -0
- omnibase_infra/runtime/models/model_runtime_scheduler_metrics.py +233 -0
- omnibase_infra/runtime/models/model_runtime_tick.py +193 -0
- omnibase_infra/runtime/models/model_secret_cache_stats.py +82 -0
- omnibase_infra/runtime/models/model_secret_mapping.py +63 -0
- omnibase_infra/runtime/models/model_secret_resolver_config.py +107 -0
- omnibase_infra/runtime/models/model_secret_resolver_metrics.py +111 -0
- omnibase_infra/runtime/models/model_secret_source_info.py +72 -0
- omnibase_infra/runtime/models/model_secret_source_spec.py +66 -0
- omnibase_infra/runtime/models/model_shutdown_batch_result.py +75 -0
- omnibase_infra/runtime/models/model_shutdown_config.py +94 -0
- omnibase_infra/runtime/projector_plugin_loader.py +1462 -0
- omnibase_infra/runtime/projector_schema_manager.py +565 -0
- omnibase_infra/runtime/projector_shell.py +1102 -0
- omnibase_infra/runtime/protocol_contract_descriptor.py +92 -0
- omnibase_infra/runtime/protocol_contract_source.py +92 -0
- omnibase_infra/runtime/protocol_domain_plugin.py +474 -0
- omnibase_infra/runtime/protocol_handler_discovery.py +221 -0
- omnibase_infra/runtime/protocol_handler_plugin_loader.py +327 -0
- omnibase_infra/runtime/protocol_lifecycle_executor.py +435 -0
- omnibase_infra/runtime/protocol_policy.py +366 -0
- omnibase_infra/runtime/protocols/__init__.py +27 -0
- omnibase_infra/runtime/protocols/protocol_runtime_scheduler.py +468 -0
- omnibase_infra/runtime/registry/__init__.py +93 -0
- omnibase_infra/runtime/registry/mixin_message_type_query.py +326 -0
- omnibase_infra/runtime/registry/mixin_message_type_registration.py +354 -0
- omnibase_infra/runtime/registry/registry_event_bus_binding.py +268 -0
- omnibase_infra/runtime/registry/registry_message_type.py +542 -0
- omnibase_infra/runtime/registry/registry_protocol_binding.py +444 -0
- omnibase_infra/runtime/registry_compute.py +1143 -0
- omnibase_infra/runtime/registry_dispatcher.py +678 -0
- omnibase_infra/runtime/registry_policy.py +1502 -0
- omnibase_infra/runtime/runtime_scheduler.py +1070 -0
- omnibase_infra/runtime/secret_resolver.py +2110 -0
- omnibase_infra/runtime/security_metadata_validator.py +776 -0
- omnibase_infra/runtime/service_kernel.py +1573 -0
- omnibase_infra/runtime/service_message_dispatch_engine.py +1805 -0
- omnibase_infra/runtime/service_runtime_host_process.py +2260 -0
- omnibase_infra/runtime/util_container_wiring.py +1123 -0
- omnibase_infra/runtime/util_validation.py +314 -0
- omnibase_infra/runtime/util_version.py +98 -0
- omnibase_infra/runtime/util_wiring.py +566 -0
- omnibase_infra/schemas/schema_registration_projection.sql +320 -0
- omnibase_infra/services/__init__.py +68 -0
- omnibase_infra/services/corpus_capture.py +678 -0
- omnibase_infra/services/service_capability_query.py +945 -0
- omnibase_infra/services/service_health.py +897 -0
- omnibase_infra/services/service_node_selector.py +530 -0
- omnibase_infra/services/service_timeout_emitter.py +682 -0
- omnibase_infra/services/service_timeout_scanner.py +390 -0
- omnibase_infra/services/snapshot/__init__.py +31 -0
- omnibase_infra/services/snapshot/service_snapshot.py +647 -0
- omnibase_infra/services/snapshot/store_inmemory.py +637 -0
- omnibase_infra/services/snapshot/store_postgres.py +1279 -0
- omnibase_infra/shared/__init__.py +8 -0
- omnibase_infra/testing/__init__.py +10 -0
- omnibase_infra/testing/utils.py +23 -0
- omnibase_infra/types/__init__.py +48 -0
- omnibase_infra/types/type_cache_info.py +49 -0
- omnibase_infra/types/type_dsn.py +173 -0
- omnibase_infra/types/type_infra_aliases.py +60 -0
- omnibase_infra/types/typed_dict/__init__.py +21 -0
- omnibase_infra/types/typed_dict/typed_dict_introspection_cache.py +128 -0
- omnibase_infra/types/typed_dict/typed_dict_performance_metrics_cache.py +140 -0
- omnibase_infra/types/typed_dict_capabilities.py +64 -0
- omnibase_infra/utils/__init__.py +89 -0
- omnibase_infra/utils/correlation.py +208 -0
- omnibase_infra/utils/util_datetime.py +372 -0
- omnibase_infra/utils/util_dsn_validation.py +333 -0
- omnibase_infra/utils/util_env_parsing.py +264 -0
- omnibase_infra/utils/util_error_sanitization.py +457 -0
- omnibase_infra/utils/util_pydantic_validators.py +477 -0
- omnibase_infra/utils/util_semver.py +233 -0
- omnibase_infra/validation/__init__.py +307 -0
- omnibase_infra/validation/enums/__init__.py +11 -0
- omnibase_infra/validation/enums/enum_contract_violation_severity.py +13 -0
- omnibase_infra/validation/infra_validators.py +1486 -0
- omnibase_infra/validation/linter_contract.py +907 -0
- omnibase_infra/validation/mixin_any_type_classification.py +120 -0
- omnibase_infra/validation/mixin_any_type_exemption.py +580 -0
- omnibase_infra/validation/mixin_any_type_reporting.py +106 -0
- omnibase_infra/validation/mixin_execution_shape_violation_checks.py +596 -0
- omnibase_infra/validation/mixin_node_archetype_detection.py +254 -0
- omnibase_infra/validation/models/__init__.py +15 -0
- omnibase_infra/validation/models/model_contract_lint_result.py +101 -0
- omnibase_infra/validation/models/model_contract_violation.py +41 -0
- omnibase_infra/validation/service_validation_aggregator.py +395 -0
- omnibase_infra/validation/validation_exemptions.yaml +1710 -0
- omnibase_infra/validation/validator_any_type.py +715 -0
- omnibase_infra/validation/validator_chain_propagation.py +839 -0
- omnibase_infra/validation/validator_execution_shape.py +465 -0
- omnibase_infra/validation/validator_localhandler.py +261 -0
- omnibase_infra/validation/validator_registration_security.py +410 -0
- omnibase_infra/validation/validator_routing_coverage.py +1020 -0
- omnibase_infra/validation/validator_runtime_shape.py +915 -0
- omnibase_infra/validation/validator_security.py +410 -0
- omnibase_infra/validation/validator_topic_category.py +1152 -0
- omnibase_infra-0.2.1.dist-info/METADATA +197 -0
- omnibase_infra-0.2.1.dist-info/RECORD +675 -0
- omnibase_infra-0.2.1.dist-info/WHEEL +4 -0
- omnibase_infra-0.2.1.dist-info/entry_points.txt +4 -0
- omnibase_infra-0.2.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,1658 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 OmniNode Team
|
|
3
|
+
"""Kafka Event Bus implementation for production message streaming.
|
|
4
|
+
|
|
5
|
+
Implements ProtocolEventBus interface using Apache Kafka (via aiokafka) for
|
|
6
|
+
production-grade message delivery with resilience patterns including circuit
|
|
7
|
+
breaker, retry with exponential backoff, and dead letter queue support.
|
|
8
|
+
|
|
9
|
+
Features:
|
|
10
|
+
- Topic-based message routing with Kafka partitioning
|
|
11
|
+
- Async publish/subscribe with callback handlers
|
|
12
|
+
- Circuit breaker for connection failure protection
|
|
13
|
+
- Retry with exponential backoff on publish failures
|
|
14
|
+
- Dead letter queue (DLQ) for failed message processing
|
|
15
|
+
- Graceful degradation when Kafka is unavailable
|
|
16
|
+
- Support for environment/group-based routing
|
|
17
|
+
- Proper producer/consumer lifecycle management
|
|
18
|
+
|
|
19
|
+
Environment Variables:
|
|
20
|
+
Configuration can be overridden using environment variables. All variables
|
|
21
|
+
are optional and fall back to defaults if not set.
|
|
22
|
+
|
|
23
|
+
Connection Settings:
|
|
24
|
+
KAFKA_BOOTSTRAP_SERVERS: Kafka broker addresses (comma-separated)
|
|
25
|
+
Default: "localhost:9092"
|
|
26
|
+
Example: "kafka1:9092,kafka2:9092,kafka3:9092"
|
|
27
|
+
|
|
28
|
+
KAFKA_ENVIRONMENT: Environment identifier for message routing
|
|
29
|
+
Default: "local"
|
|
30
|
+
Example: "dev", "staging", "prod"
|
|
31
|
+
|
|
32
|
+
KAFKA_GROUP: Consumer group identifier
|
|
33
|
+
Default: "default"
|
|
34
|
+
Example: "my-service-group"
|
|
35
|
+
|
|
36
|
+
Timeout and Retry Settings:
|
|
37
|
+
KAFKA_TIMEOUT_SECONDS: Timeout for Kafka operations (integer seconds)
|
|
38
|
+
Default: 30
|
|
39
|
+
Range: 1-300
|
|
40
|
+
Example: "60"
|
|
41
|
+
|
|
42
|
+
KAFKA_MAX_RETRY_ATTEMPTS: Maximum publish retry attempts
|
|
43
|
+
Default: 3
|
|
44
|
+
Range: 0-10
|
|
45
|
+
Example: "5"
|
|
46
|
+
|
|
47
|
+
NOTE: This is the BUS-LEVEL retry for Kafka connection/publish failures.
|
|
48
|
+
This is distinct from MESSAGE-LEVEL retry tracked in ModelEventHeaders
|
|
49
|
+
(retry_count/max_retries), which is for application-level message
|
|
50
|
+
delivery tracking across services. See "Dual Retry Configuration" below.
|
|
51
|
+
|
|
52
|
+
KAFKA_RETRY_BACKOFF_BASE: Base delay for exponential backoff (float seconds)
|
|
53
|
+
Default: 1.0
|
|
54
|
+
Range: 0.1-60.0
|
|
55
|
+
Example: "2.0"
|
|
56
|
+
|
|
57
|
+
Circuit Breaker Settings:
|
|
58
|
+
KAFKA_CIRCUIT_BREAKER_THRESHOLD: Failures before circuit opens
|
|
59
|
+
Default: 5
|
|
60
|
+
Range: 1-100
|
|
61
|
+
Example: "10"
|
|
62
|
+
|
|
63
|
+
KAFKA_CIRCUIT_BREAKER_RESET_TIMEOUT: Seconds before circuit resets
|
|
64
|
+
Default: 30.0
|
|
65
|
+
Range: 1.0-3600.0
|
|
66
|
+
Example: "60.0"
|
|
67
|
+
|
|
68
|
+
Consumer Settings:
|
|
69
|
+
KAFKA_CONSUMER_SLEEP_INTERVAL: Sleep between poll iterations (float seconds)
|
|
70
|
+
Default: 0.1
|
|
71
|
+
Range: 0.01-10.0
|
|
72
|
+
Example: "0.2"
|
|
73
|
+
|
|
74
|
+
KAFKA_AUTO_OFFSET_RESET: Offset reset policy
|
|
75
|
+
Default: "latest"
|
|
76
|
+
Options: "earliest", "latest"
|
|
77
|
+
|
|
78
|
+
KAFKA_ENABLE_AUTO_COMMIT: Auto-commit consumer offsets
|
|
79
|
+
Default: true
|
|
80
|
+
Options: "true", "1", "yes", "on" (case-insensitive) = True
|
|
81
|
+
All other values = False
|
|
82
|
+
Example: "false"
|
|
83
|
+
|
|
84
|
+
Producer Settings:
|
|
85
|
+
KAFKA_ACKS: Producer acknowledgment policy
|
|
86
|
+
Default: "all"
|
|
87
|
+
Options: "all" (all replicas), "1" (leader only), "0" (no ack)
|
|
88
|
+
|
|
89
|
+
KAFKA_ENABLE_IDEMPOTENCE: Enable idempotent producer
|
|
90
|
+
Default: true
|
|
91
|
+
Options: "true", "1", "yes", "on" (case-insensitive) = True
|
|
92
|
+
All other values = False
|
|
93
|
+
Example: "true"
|
|
94
|
+
|
|
95
|
+
Dead Letter Queue Settings:
|
|
96
|
+
KAFKA_DEAD_LETTER_TOPIC: Topic name for failed messages
|
|
97
|
+
Default: None (DLQ disabled)
|
|
98
|
+
Example: "dlq-events"
|
|
99
|
+
|
|
100
|
+
When configured, messages that fail processing will be published
|
|
101
|
+
to this topic with comprehensive failure metadata including:
|
|
102
|
+
- Original topic and message
|
|
103
|
+
- Failure reason and timestamp
|
|
104
|
+
- Correlation ID for tracking
|
|
105
|
+
- Retry count and error type
|
|
106
|
+
|
|
107
|
+
Dual Retry Configuration:
|
|
108
|
+
ONEX uses TWO distinct retry mechanisms that serve different purposes:
|
|
109
|
+
|
|
110
|
+
1. **Bus-Level Retry** (EventBusKafka internal):
|
|
111
|
+
- Configured via: max_retry_attempts, retry_backoff_base
|
|
112
|
+
- Purpose: Handle transient Kafka connection/publish failures
|
|
113
|
+
- Scope: Single publish operation within the event bus
|
|
114
|
+
- Applies to: Producer.send() failures, timeouts, connection errors
|
|
115
|
+
- Example: If Kafka broker is temporarily unreachable, retry 3 times
|
|
116
|
+
with exponential backoff before failing
|
|
117
|
+
|
|
118
|
+
2. **Message-Level Retry** (ModelEventHeaders):
|
|
119
|
+
- Configured via: retry_count, max_retries in message headers
|
|
120
|
+
- Purpose: Track application-level message delivery attempts
|
|
121
|
+
- Scope: End-to-end message delivery across services
|
|
122
|
+
- Applies to: Business logic failures, handler exceptions
|
|
123
|
+
- Example: If order processing fails, increment retry_count and
|
|
124
|
+
republish; stop after max_retries reached
|
|
125
|
+
|
|
126
|
+
These mechanisms are INDEPENDENT and work together:
|
|
127
|
+
- Bus-level retry handles infrastructure failures (network, broker)
|
|
128
|
+
- Message-level retry handles application failures (handler errors)
|
|
129
|
+
|
|
130
|
+
A single message publish may trigger multiple bus-level retries,
|
|
131
|
+
while still counting as a single message-level delivery attempt.
|
|
132
|
+
|
|
133
|
+
Usage:
|
|
134
|
+
```python
|
|
135
|
+
from omnibase_infra.event_bus.event_bus_kafka import EventBusKafka
|
|
136
|
+
from omnibase_infra.event_bus.models.config import ModelKafkaEventBusConfig
|
|
137
|
+
|
|
138
|
+
# Option 1: Use defaults with environment variable overrides
|
|
139
|
+
bus = EventBusKafka.default()
|
|
140
|
+
await bus.start()
|
|
141
|
+
|
|
142
|
+
# Option 2: Explicit configuration via config model
|
|
143
|
+
config = ModelKafkaEventBusConfig(
|
|
144
|
+
bootstrap_servers="kafka:9092",
|
|
145
|
+
environment="dev",
|
|
146
|
+
)
|
|
147
|
+
bus = EventBusKafka(config=config)
|
|
148
|
+
await bus.start()
|
|
149
|
+
|
|
150
|
+
# Subscribe to a topic
|
|
151
|
+
async def handler(msg):
|
|
152
|
+
print(f"Received: {msg.value}")
|
|
153
|
+
unsubscribe = await bus.subscribe("events", "group1", handler)
|
|
154
|
+
|
|
155
|
+
# Publish a message
|
|
156
|
+
await bus.publish("events", b"key", b"value")
|
|
157
|
+
|
|
158
|
+
# Cleanup
|
|
159
|
+
await unsubscribe()
|
|
160
|
+
await bus.close()
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Protocol Compatibility:
|
|
164
|
+
This class implements ProtocolEventBus from omnibase_core using duck typing
|
|
165
|
+
(no explicit inheritance required per ONEX patterns).
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
from __future__ import annotations
|
|
169
|
+
|
|
170
|
+
import asyncio
|
|
171
|
+
import logging
|
|
172
|
+
import random
|
|
173
|
+
import re
|
|
174
|
+
from collections import defaultdict
|
|
175
|
+
from collections.abc import Awaitable, Callable
|
|
176
|
+
from datetime import UTC, datetime
|
|
177
|
+
from pathlib import Path
|
|
178
|
+
from uuid import UUID, uuid4
|
|
179
|
+
|
|
180
|
+
from aiokafka import AIOKafkaConsumer, AIOKafkaProducer
|
|
181
|
+
from aiokafka.errors import KafkaError
|
|
182
|
+
|
|
183
|
+
from omnibase_infra.enums import EnumInfraTransportType
|
|
184
|
+
from omnibase_infra.errors import (
|
|
185
|
+
InfraConnectionError,
|
|
186
|
+
InfraTimeoutError,
|
|
187
|
+
InfraUnavailableError,
|
|
188
|
+
ModelInfraErrorContext,
|
|
189
|
+
ModelTimeoutErrorContext,
|
|
190
|
+
ProtocolConfigurationError,
|
|
191
|
+
)
|
|
192
|
+
from omnibase_infra.event_bus.mixin_kafka_broadcast import MixinKafkaBroadcast
|
|
193
|
+
from omnibase_infra.event_bus.mixin_kafka_dlq import MixinKafkaDlq
|
|
194
|
+
from omnibase_infra.event_bus.models import (
|
|
195
|
+
ModelEventHeaders,
|
|
196
|
+
ModelEventMessage,
|
|
197
|
+
)
|
|
198
|
+
from omnibase_infra.event_bus.models.config import ModelKafkaEventBusConfig
|
|
199
|
+
from omnibase_infra.mixins import MixinAsyncCircuitBreaker
|
|
200
|
+
|
|
201
|
+
logger = logging.getLogger(__name__)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class EventBusKafka(MixinKafkaBroadcast, MixinKafkaDlq, MixinAsyncCircuitBreaker):
|
|
205
|
+
"""Kafka-backed event bus for production message streaming.
|
|
206
|
+
|
|
207
|
+
Implements ProtocolEventBus interface using Apache Kafka (via aiokafka)
|
|
208
|
+
with resilience patterns including circuit breaker, retry with exponential
|
|
209
|
+
backoff, dead letter queue support, and graceful degradation when Kafka
|
|
210
|
+
is unavailable.
|
|
211
|
+
|
|
212
|
+
Features:
|
|
213
|
+
- Topic-based message routing with Kafka partitioning
|
|
214
|
+
- Multiple subscribers per topic with callback-based delivery
|
|
215
|
+
- Circuit breaker for connection failure protection
|
|
216
|
+
- Retry with exponential backoff on publish failures
|
|
217
|
+
- Dead letter queue (DLQ) for failed message processing
|
|
218
|
+
- Environment and group-based message routing
|
|
219
|
+
- Proper async producer/consumer lifecycle management
|
|
220
|
+
|
|
221
|
+
Attributes:
|
|
222
|
+
environment: Environment identifier (e.g., "local", "dev", "prod")
|
|
223
|
+
group: Consumer group identifier
|
|
224
|
+
adapter: Returns self (for protocol compatibility)
|
|
225
|
+
|
|
226
|
+
Architecture:
|
|
227
|
+
This class uses mixin composition to organize functionality:
|
|
228
|
+
- MixinKafkaBroadcast: Environment/group broadcast messaging, envelope publishing
|
|
229
|
+
- MixinKafkaDlq: Dead letter queue handling and metrics
|
|
230
|
+
- MixinAsyncCircuitBreaker: Circuit breaker resilience pattern
|
|
231
|
+
|
|
232
|
+
The core class provides:
|
|
233
|
+
- Factory methods (3): from_config, from_yaml, default
|
|
234
|
+
- Properties (4): config, adapter, environment, group
|
|
235
|
+
- Lifecycle methods (4): start, initialize, shutdown, close
|
|
236
|
+
- Pub/Sub methods (3): publish, subscribe, start_consuming
|
|
237
|
+
- Health check (1): health_check
|
|
238
|
+
|
|
239
|
+
Example:
|
|
240
|
+
```python
|
|
241
|
+
config = ModelKafkaEventBusConfig(
|
|
242
|
+
bootstrap_servers="kafka:9092",
|
|
243
|
+
environment="dev",
|
|
244
|
+
)
|
|
245
|
+
bus = EventBusKafka(config=config)
|
|
246
|
+
await bus.start()
|
|
247
|
+
|
|
248
|
+
# Subscribe
|
|
249
|
+
async def handler(msg):
|
|
250
|
+
print(f"Received: {msg.value}")
|
|
251
|
+
unsubscribe = await bus.subscribe("events", "group1", handler)
|
|
252
|
+
|
|
253
|
+
# Publish
|
|
254
|
+
await bus.publish("events", b"key", b"value")
|
|
255
|
+
|
|
256
|
+
# Cleanup
|
|
257
|
+
await unsubscribe()
|
|
258
|
+
await bus.close()
|
|
259
|
+
```
|
|
260
|
+
"""
|
|
261
|
+
|
|
262
|
+
def __init__(
|
|
263
|
+
self,
|
|
264
|
+
config: ModelKafkaEventBusConfig | None = None,
|
|
265
|
+
) -> None:
|
|
266
|
+
"""Initialize the Kafka event bus.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
config: Configuration model containing all settings. If not provided,
|
|
270
|
+
defaults are used with environment variable overrides.
|
|
271
|
+
|
|
272
|
+
Raises:
|
|
273
|
+
ProtocolConfigurationError: If circuit_breaker_threshold is not a positive integer
|
|
274
|
+
|
|
275
|
+
Example:
|
|
276
|
+
```python
|
|
277
|
+
# Using config model (recommended)
|
|
278
|
+
config = ModelKafkaEventBusConfig(
|
|
279
|
+
bootstrap_servers="kafka:9092",
|
|
280
|
+
environment="prod",
|
|
281
|
+
)
|
|
282
|
+
bus = EventBusKafka(config=config)
|
|
283
|
+
|
|
284
|
+
# Using factory methods
|
|
285
|
+
bus = EventBusKafka.default()
|
|
286
|
+
bus = EventBusKafka.from_yaml(Path("kafka.yaml"))
|
|
287
|
+
```
|
|
288
|
+
"""
|
|
289
|
+
# Use provided config or create default with environment overrides
|
|
290
|
+
if config is None:
|
|
291
|
+
config = ModelKafkaEventBusConfig.default()
|
|
292
|
+
|
|
293
|
+
# Store config reference
|
|
294
|
+
self._config = config
|
|
295
|
+
|
|
296
|
+
# Apply config values
|
|
297
|
+
self._bootstrap_servers = config.bootstrap_servers
|
|
298
|
+
self._environment = config.environment
|
|
299
|
+
self._group = config.group
|
|
300
|
+
self._timeout_seconds = config.timeout_seconds
|
|
301
|
+
self._max_retry_attempts = config.max_retry_attempts
|
|
302
|
+
self._retry_backoff_base = config.retry_backoff_base
|
|
303
|
+
|
|
304
|
+
# Circuit breaker configuration
|
|
305
|
+
if config.circuit_breaker_threshold < 1:
|
|
306
|
+
context = ModelInfraErrorContext(
|
|
307
|
+
transport_type=EnumInfraTransportType.KAFKA,
|
|
308
|
+
operation="init",
|
|
309
|
+
target_name="kafka_event_bus",
|
|
310
|
+
correlation_id=uuid4(),
|
|
311
|
+
)
|
|
312
|
+
raise ProtocolConfigurationError(
|
|
313
|
+
f"circuit_breaker_threshold must be a positive integer, got {config.circuit_breaker_threshold}",
|
|
314
|
+
context=context,
|
|
315
|
+
parameter="circuit_breaker_threshold",
|
|
316
|
+
value=config.circuit_breaker_threshold,
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
# Initialize circuit breaker mixin
|
|
320
|
+
self._init_circuit_breaker(
|
|
321
|
+
threshold=config.circuit_breaker_threshold,
|
|
322
|
+
reset_timeout=config.circuit_breaker_reset_timeout,
|
|
323
|
+
service_name=f"kafka.{self._environment}",
|
|
324
|
+
transport_type=EnumInfraTransportType.KAFKA,
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
# Kafka producer and consumer
|
|
328
|
+
self._producer: AIOKafkaProducer | None = None
|
|
329
|
+
self._consumers: dict[str, AIOKafkaConsumer] = {}
|
|
330
|
+
|
|
331
|
+
# Subscriber registry: topic -> list of (group_id, subscription_id, callback) tuples
|
|
332
|
+
self._subscribers: dict[
|
|
333
|
+
str, list[tuple[str, str, Callable[[ModelEventMessage], Awaitable[None]]]]
|
|
334
|
+
] = defaultdict(list)
|
|
335
|
+
|
|
336
|
+
# Lock for coroutine safety (protects all shared state)
|
|
337
|
+
self._lock = asyncio.Lock()
|
|
338
|
+
|
|
339
|
+
# State flags
|
|
340
|
+
self._started = False
|
|
341
|
+
self._shutdown = False
|
|
342
|
+
|
|
343
|
+
# Background consumer tasks
|
|
344
|
+
self._consumer_tasks: dict[str, asyncio.Task[None]] = {}
|
|
345
|
+
|
|
346
|
+
# Producer lock for independent producer access (avoids deadlock with main lock)
|
|
347
|
+
self._producer_lock = asyncio.Lock()
|
|
348
|
+
|
|
349
|
+
# Initialize DLQ mixin (metrics tracking, callback hooks)
|
|
350
|
+
self._init_dlq()
|
|
351
|
+
|
|
352
|
+
# =========================================================================
|
|
353
|
+
# Factory Methods
|
|
354
|
+
# =========================================================================
|
|
355
|
+
|
|
356
|
+
@classmethod
|
|
357
|
+
def from_config(cls, config: ModelKafkaEventBusConfig) -> EventBusKafka:
|
|
358
|
+
"""Create EventBusKafka from a configuration model.
|
|
359
|
+
|
|
360
|
+
Args:
|
|
361
|
+
config: Configuration model containing all settings
|
|
362
|
+
|
|
363
|
+
Returns:
|
|
364
|
+
EventBusKafka instance configured with the provided settings
|
|
365
|
+
|
|
366
|
+
Example:
|
|
367
|
+
```python
|
|
368
|
+
config = ModelKafkaEventBusConfig(
|
|
369
|
+
bootstrap_servers="kafka:9092",
|
|
370
|
+
environment="prod",
|
|
371
|
+
timeout_seconds=60,
|
|
372
|
+
)
|
|
373
|
+
bus = EventBusKafka.from_config(config)
|
|
374
|
+
```
|
|
375
|
+
"""
|
|
376
|
+
return cls(config=config)
|
|
377
|
+
|
|
378
|
+
@classmethod
|
|
379
|
+
def from_yaml(cls, path: Path) -> EventBusKafka:
|
|
380
|
+
"""Create EventBusKafka from a YAML configuration file.
|
|
381
|
+
|
|
382
|
+
Loads configuration from a YAML file with environment variable
|
|
383
|
+
overrides applied automatically.
|
|
384
|
+
|
|
385
|
+
Args:
|
|
386
|
+
path: Path to YAML configuration file
|
|
387
|
+
|
|
388
|
+
Returns:
|
|
389
|
+
EventBusKafka instance configured from the YAML file
|
|
390
|
+
|
|
391
|
+
Raises:
|
|
392
|
+
FileNotFoundError: If the YAML file does not exist
|
|
393
|
+
ValueError: If the YAML content is invalid
|
|
394
|
+
|
|
395
|
+
Example:
|
|
396
|
+
```python
|
|
397
|
+
bus = EventBusKafka.from_yaml(Path("/etc/kafka/config.yaml"))
|
|
398
|
+
```
|
|
399
|
+
"""
|
|
400
|
+
config = ModelKafkaEventBusConfig.from_yaml(path)
|
|
401
|
+
return cls(config=config)
|
|
402
|
+
|
|
403
|
+
@classmethod
|
|
404
|
+
def default(cls) -> EventBusKafka:
|
|
405
|
+
"""Create EventBusKafka with default configuration.
|
|
406
|
+
|
|
407
|
+
Creates an instance with default settings and environment variable
|
|
408
|
+
overrides applied automatically. This is the recommended way to
|
|
409
|
+
create a EventBusKafka for most use cases.
|
|
410
|
+
|
|
411
|
+
Returns:
|
|
412
|
+
EventBusKafka instance with default configuration
|
|
413
|
+
|
|
414
|
+
Example:
|
|
415
|
+
```python
|
|
416
|
+
bus = EventBusKafka.default()
|
|
417
|
+
await bus.start()
|
|
418
|
+
```
|
|
419
|
+
"""
|
|
420
|
+
return cls(config=ModelKafkaEventBusConfig.default())
|
|
421
|
+
|
|
422
|
+
# =========================================================================
|
|
423
|
+
# Properties
|
|
424
|
+
# =========================================================================
|
|
425
|
+
|
|
426
|
+
@property
|
|
427
|
+
def config(self) -> ModelKafkaEventBusConfig:
|
|
428
|
+
"""Get the configuration model.
|
|
429
|
+
|
|
430
|
+
Returns:
|
|
431
|
+
Configuration model instance used by this event bus
|
|
432
|
+
"""
|
|
433
|
+
return self._config
|
|
434
|
+
|
|
435
|
+
@property
|
|
436
|
+
def adapter(self) -> EventBusKafka:
|
|
437
|
+
"""Return self for protocol compatibility.
|
|
438
|
+
|
|
439
|
+
Returns:
|
|
440
|
+
Self reference (Kafka bus is its own adapter)
|
|
441
|
+
"""
|
|
442
|
+
return self
|
|
443
|
+
|
|
444
|
+
@property
|
|
445
|
+
def environment(self) -> str:
|
|
446
|
+
"""Get the environment identifier.
|
|
447
|
+
|
|
448
|
+
Returns:
|
|
449
|
+
Environment string (e.g., "local", "dev", "prod")
|
|
450
|
+
"""
|
|
451
|
+
return self._environment
|
|
452
|
+
|
|
453
|
+
@property
|
|
454
|
+
def group(self) -> str:
|
|
455
|
+
"""Get the consumer group identifier.
|
|
456
|
+
|
|
457
|
+
Returns:
|
|
458
|
+
Consumer group string
|
|
459
|
+
"""
|
|
460
|
+
return self._group
|
|
461
|
+
|
|
462
|
+
async def start(self) -> None:
|
|
463
|
+
"""Start the event bus and connect to Kafka.
|
|
464
|
+
|
|
465
|
+
Initializes the Kafka producer with connection retry and circuit
|
|
466
|
+
breaker protection. If connection fails, the bus operates in
|
|
467
|
+
degraded mode where publishes will fail gracefully.
|
|
468
|
+
|
|
469
|
+
Raises:
|
|
470
|
+
InfraConnectionError: If connection fails after all retries and
|
|
471
|
+
circuit breaker is open
|
|
472
|
+
"""
|
|
473
|
+
if self._started:
|
|
474
|
+
logger.debug("EventBusKafka already started")
|
|
475
|
+
return
|
|
476
|
+
|
|
477
|
+
correlation_id = uuid4()
|
|
478
|
+
|
|
479
|
+
async with self._lock:
|
|
480
|
+
if self._started:
|
|
481
|
+
return
|
|
482
|
+
|
|
483
|
+
# Check circuit breaker before attempting connection
|
|
484
|
+
# Note: Circuit breaker requires its own lock to be held
|
|
485
|
+
async with self._circuit_breaker_lock:
|
|
486
|
+
await self._check_circuit_breaker(
|
|
487
|
+
operation="start", correlation_id=correlation_id
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
try:
|
|
491
|
+
# Apply producer configuration from config model
|
|
492
|
+
self._producer = AIOKafkaProducer(
|
|
493
|
+
bootstrap_servers=self._bootstrap_servers,
|
|
494
|
+
acks=self._config.acks,
|
|
495
|
+
enable_idempotence=self._config.enable_idempotence,
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
await asyncio.wait_for(
|
|
499
|
+
self._producer.start(),
|
|
500
|
+
timeout=self._timeout_seconds,
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
self._started = True
|
|
504
|
+
self._shutdown = False
|
|
505
|
+
|
|
506
|
+
# Reset circuit breaker on success
|
|
507
|
+
async with self._circuit_breaker_lock:
|
|
508
|
+
await self._reset_circuit_breaker()
|
|
509
|
+
|
|
510
|
+
logger.info(
|
|
511
|
+
"EventBusKafka started",
|
|
512
|
+
extra={
|
|
513
|
+
"environment": self._environment,
|
|
514
|
+
"group": self._group,
|
|
515
|
+
"bootstrap_servers": self._sanitize_bootstrap_servers(
|
|
516
|
+
self._bootstrap_servers
|
|
517
|
+
),
|
|
518
|
+
},
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
except TimeoutError as e:
|
|
522
|
+
# Clean up producer on failure to prevent resource leak (thread-safe)
|
|
523
|
+
async with self._producer_lock:
|
|
524
|
+
if self._producer is not None:
|
|
525
|
+
try:
|
|
526
|
+
await self._producer.stop()
|
|
527
|
+
except Exception as cleanup_err:
|
|
528
|
+
logger.warning(
|
|
529
|
+
"Cleanup failed for Kafka producer stop: %s",
|
|
530
|
+
cleanup_err,
|
|
531
|
+
exc_info=True,
|
|
532
|
+
)
|
|
533
|
+
self._producer = None
|
|
534
|
+
# Record failure (circuit breaker lock required)
|
|
535
|
+
async with self._circuit_breaker_lock:
|
|
536
|
+
await self._record_circuit_failure(
|
|
537
|
+
operation="start", correlation_id=correlation_id
|
|
538
|
+
)
|
|
539
|
+
# Sanitize servers for safe logging (remove credentials)
|
|
540
|
+
sanitized_servers = self._sanitize_bootstrap_servers(
|
|
541
|
+
self._bootstrap_servers
|
|
542
|
+
)
|
|
543
|
+
timeout_ctx = ModelTimeoutErrorContext(
|
|
544
|
+
transport_type=EnumInfraTransportType.KAFKA,
|
|
545
|
+
operation="start",
|
|
546
|
+
target_name=f"kafka.{self._environment}",
|
|
547
|
+
correlation_id=correlation_id,
|
|
548
|
+
timeout_seconds=self._timeout_seconds,
|
|
549
|
+
)
|
|
550
|
+
logger.warning(
|
|
551
|
+
f"Timeout connecting to Kafka after {self._timeout_seconds}s",
|
|
552
|
+
extra={
|
|
553
|
+
"environment": self._environment,
|
|
554
|
+
"correlation_id": str(correlation_id),
|
|
555
|
+
},
|
|
556
|
+
)
|
|
557
|
+
raise InfraTimeoutError(
|
|
558
|
+
f"Timeout connecting to Kafka after {self._timeout_seconds}s",
|
|
559
|
+
context=timeout_ctx,
|
|
560
|
+
servers=sanitized_servers,
|
|
561
|
+
) from e
|
|
562
|
+
|
|
563
|
+
except Exception as e:
|
|
564
|
+
# Clean up producer on failure to prevent resource leak (thread-safe)
|
|
565
|
+
async with self._producer_lock:
|
|
566
|
+
if self._producer is not None:
|
|
567
|
+
try:
|
|
568
|
+
await self._producer.stop()
|
|
569
|
+
except Exception as cleanup_err:
|
|
570
|
+
logger.warning(
|
|
571
|
+
"Cleanup failed for Kafka producer stop: %s",
|
|
572
|
+
cleanup_err,
|
|
573
|
+
exc_info=True,
|
|
574
|
+
)
|
|
575
|
+
self._producer = None
|
|
576
|
+
# Record failure (circuit breaker lock required)
|
|
577
|
+
async with self._circuit_breaker_lock:
|
|
578
|
+
await self._record_circuit_failure(
|
|
579
|
+
operation="start", correlation_id=correlation_id
|
|
580
|
+
)
|
|
581
|
+
# Sanitize servers for safe logging (remove credentials)
|
|
582
|
+
sanitized_servers = self._sanitize_bootstrap_servers(
|
|
583
|
+
self._bootstrap_servers
|
|
584
|
+
)
|
|
585
|
+
context = ModelInfraErrorContext(
|
|
586
|
+
transport_type=EnumInfraTransportType.KAFKA,
|
|
587
|
+
operation="start",
|
|
588
|
+
target_name=f"kafka.{self._environment}",
|
|
589
|
+
correlation_id=correlation_id,
|
|
590
|
+
)
|
|
591
|
+
logger.warning(
|
|
592
|
+
f"Failed to connect to Kafka: {e}",
|
|
593
|
+
extra={
|
|
594
|
+
"environment": self._environment,
|
|
595
|
+
"error": str(e),
|
|
596
|
+
"correlation_id": str(correlation_id),
|
|
597
|
+
},
|
|
598
|
+
)
|
|
599
|
+
raise InfraConnectionError(
|
|
600
|
+
f"Failed to connect to Kafka: {e}",
|
|
601
|
+
context=context,
|
|
602
|
+
servers=sanitized_servers,
|
|
603
|
+
) from e
|
|
604
|
+
|
|
605
|
+
async def initialize(self, config: dict[str, object]) -> None:
|
|
606
|
+
"""Initialize the event bus with configuration.
|
|
607
|
+
|
|
608
|
+
Protocol method for compatibility with ProtocolEventBus.
|
|
609
|
+
Extracts configuration and delegates to start(). Config updates
|
|
610
|
+
are applied atomically with lock protection to prevent races.
|
|
611
|
+
|
|
612
|
+
Args:
|
|
613
|
+
config: Configuration dictionary with optional keys:
|
|
614
|
+
- environment: Override environment setting
|
|
615
|
+
- group: Override group setting
|
|
616
|
+
- bootstrap_servers: Override bootstrap servers
|
|
617
|
+
- timeout_seconds: Override timeout setting
|
|
618
|
+
"""
|
|
619
|
+
# Apply config updates atomically under lock to prevent races
|
|
620
|
+
async with self._lock:
|
|
621
|
+
if "environment" in config:
|
|
622
|
+
self._environment = str(config["environment"])
|
|
623
|
+
if "group" in config:
|
|
624
|
+
self._group = str(config["group"])
|
|
625
|
+
if "bootstrap_servers" in config:
|
|
626
|
+
self._bootstrap_servers = str(config["bootstrap_servers"])
|
|
627
|
+
if "timeout_seconds" in config:
|
|
628
|
+
self._timeout_seconds = int(str(config["timeout_seconds"]))
|
|
629
|
+
|
|
630
|
+
# Start after config updates are complete
|
|
631
|
+
await self.start()
|
|
632
|
+
|
|
633
|
+
async def shutdown(self) -> None:
|
|
634
|
+
"""Gracefully shutdown the event bus.
|
|
635
|
+
|
|
636
|
+
Protocol method that stops consuming and closes connections.
|
|
637
|
+
"""
|
|
638
|
+
await self.close()
|
|
639
|
+
|
|
640
|
+
async def close(self) -> None:
|
|
641
|
+
"""Close the event bus and release all resources.
|
|
642
|
+
|
|
643
|
+
Stops all background consumer tasks, closes all consumers, and
|
|
644
|
+
stops the producer. Safe to call multiple times. Uses proper
|
|
645
|
+
synchronization to prevent races during shutdown.
|
|
646
|
+
"""
|
|
647
|
+
# First, signal shutdown to all background tasks
|
|
648
|
+
async with self._lock:
|
|
649
|
+
if self._shutdown:
|
|
650
|
+
# Already shutting down or shutdown
|
|
651
|
+
return
|
|
652
|
+
self._shutdown = True
|
|
653
|
+
self._started = False
|
|
654
|
+
|
|
655
|
+
# Cancel all consumer tasks (outside main lock to avoid deadlock)
|
|
656
|
+
tasks_to_cancel = []
|
|
657
|
+
async with self._lock:
|
|
658
|
+
tasks_to_cancel = list(self._consumer_tasks.values())
|
|
659
|
+
|
|
660
|
+
for task in tasks_to_cancel:
|
|
661
|
+
if not task.done():
|
|
662
|
+
task.cancel()
|
|
663
|
+
try:
|
|
664
|
+
await task
|
|
665
|
+
except asyncio.CancelledError:
|
|
666
|
+
pass
|
|
667
|
+
|
|
668
|
+
# Clear task registry
|
|
669
|
+
async with self._lock:
|
|
670
|
+
self._consumer_tasks.clear()
|
|
671
|
+
|
|
672
|
+
# Close all consumers
|
|
673
|
+
consumers_to_close = []
|
|
674
|
+
async with self._lock:
|
|
675
|
+
consumers_to_close = list(self._consumers.values())
|
|
676
|
+
self._consumers.clear()
|
|
677
|
+
|
|
678
|
+
for consumer in consumers_to_close:
|
|
679
|
+
try:
|
|
680
|
+
await consumer.stop()
|
|
681
|
+
except Exception as e:
|
|
682
|
+
logger.warning(f"Error stopping consumer: {e}")
|
|
683
|
+
|
|
684
|
+
# Close producer with proper locking
|
|
685
|
+
async with self._producer_lock:
|
|
686
|
+
if self._producer is not None:
|
|
687
|
+
try:
|
|
688
|
+
await self._producer.stop()
|
|
689
|
+
except Exception as e:
|
|
690
|
+
logger.warning(f"Error stopping producer: {e}")
|
|
691
|
+
self._producer = None
|
|
692
|
+
|
|
693
|
+
# Clear subscribers
|
|
694
|
+
async with self._lock:
|
|
695
|
+
self._subscribers.clear()
|
|
696
|
+
|
|
697
|
+
logger.info(
|
|
698
|
+
"EventBusKafka closed",
|
|
699
|
+
extra={"environment": self._environment, "group": self._group},
|
|
700
|
+
)
|
|
701
|
+
|
|
702
|
+
async def publish(
|
|
703
|
+
self,
|
|
704
|
+
topic: str,
|
|
705
|
+
key: bytes | None,
|
|
706
|
+
value: bytes,
|
|
707
|
+
headers: ModelEventHeaders | None = None,
|
|
708
|
+
) -> None:
|
|
709
|
+
"""Publish message to topic.
|
|
710
|
+
|
|
711
|
+
Publishes a message to the specified Kafka topic with retry and
|
|
712
|
+
circuit breaker protection.
|
|
713
|
+
|
|
714
|
+
Args:
|
|
715
|
+
topic: Target topic name
|
|
716
|
+
key: Optional message key (for partitioning)
|
|
717
|
+
value: Message payload as bytes
|
|
718
|
+
headers: Optional event headers with metadata
|
|
719
|
+
|
|
720
|
+
Raises:
|
|
721
|
+
InfraUnavailableError: If the bus has not been started
|
|
722
|
+
InfraConnectionError: If publish fails after all retries
|
|
723
|
+
"""
|
|
724
|
+
if not self._started:
|
|
725
|
+
context = ModelInfraErrorContext(
|
|
726
|
+
transport_type=EnumInfraTransportType.KAFKA,
|
|
727
|
+
operation="publish",
|
|
728
|
+
target_name=f"kafka.{self._environment}",
|
|
729
|
+
correlation_id=(
|
|
730
|
+
headers.correlation_id if headers is not None else uuid4()
|
|
731
|
+
),
|
|
732
|
+
)
|
|
733
|
+
raise InfraUnavailableError(
|
|
734
|
+
"Event bus not started. Call start() first.",
|
|
735
|
+
context=context,
|
|
736
|
+
topic=topic,
|
|
737
|
+
)
|
|
738
|
+
|
|
739
|
+
# Create headers if not provided
|
|
740
|
+
if headers is None:
|
|
741
|
+
headers = ModelEventHeaders(
|
|
742
|
+
source=f"{self._environment}.{self._group}",
|
|
743
|
+
event_type=topic,
|
|
744
|
+
timestamp=datetime.now(UTC),
|
|
745
|
+
)
|
|
746
|
+
|
|
747
|
+
# Validate topic name
|
|
748
|
+
self._validate_topic_name(topic, headers.correlation_id)
|
|
749
|
+
|
|
750
|
+
# Check circuit breaker - propagate correlation_id from headers (thread-safe)
|
|
751
|
+
async with self._circuit_breaker_lock:
|
|
752
|
+
await self._check_circuit_breaker(
|
|
753
|
+
operation="publish", correlation_id=headers.correlation_id
|
|
754
|
+
)
|
|
755
|
+
|
|
756
|
+
# Convert headers to Kafka format
|
|
757
|
+
kafka_headers = self._model_headers_to_kafka(headers)
|
|
758
|
+
|
|
759
|
+
# Publish with retry
|
|
760
|
+
await self._publish_with_retry(topic, key, value, kafka_headers, headers)
|
|
761
|
+
|
|
762
|
+
async def _publish_with_retry(
|
|
763
|
+
self,
|
|
764
|
+
topic: str,
|
|
765
|
+
key: bytes | None,
|
|
766
|
+
value: bytes,
|
|
767
|
+
kafka_headers: list[tuple[str, bytes]],
|
|
768
|
+
headers: ModelEventHeaders,
|
|
769
|
+
) -> None:
|
|
770
|
+
"""Publish message with exponential backoff retry.
|
|
771
|
+
|
|
772
|
+
Args:
|
|
773
|
+
topic: Target topic name
|
|
774
|
+
key: Optional message key
|
|
775
|
+
value: Message payload
|
|
776
|
+
kafka_headers: Kafka-formatted headers
|
|
777
|
+
headers: Original headers model
|
|
778
|
+
|
|
779
|
+
Raises:
|
|
780
|
+
InfraConnectionError: If publish fails after all retries
|
|
781
|
+
"""
|
|
782
|
+
last_exception: Exception | None = None
|
|
783
|
+
|
|
784
|
+
for attempt in range(self._max_retry_attempts + 1):
|
|
785
|
+
try:
|
|
786
|
+
# Thread-safe producer access - acquire lock to check and use producer
|
|
787
|
+
async with self._producer_lock:
|
|
788
|
+
if self._producer is None:
|
|
789
|
+
raise InfraConnectionError(
|
|
790
|
+
"Kafka producer not initialized",
|
|
791
|
+
context=ModelInfraErrorContext(
|
|
792
|
+
transport_type=EnumInfraTransportType.KAFKA,
|
|
793
|
+
operation="publish",
|
|
794
|
+
target_name=f"kafka.{topic}",
|
|
795
|
+
correlation_id=headers.correlation_id,
|
|
796
|
+
),
|
|
797
|
+
)
|
|
798
|
+
|
|
799
|
+
future = await self._producer.send(
|
|
800
|
+
topic,
|
|
801
|
+
value=value,
|
|
802
|
+
key=key,
|
|
803
|
+
headers=kafka_headers,
|
|
804
|
+
)
|
|
805
|
+
|
|
806
|
+
# Wait for completion outside lock to allow other operations
|
|
807
|
+
record_metadata = await asyncio.wait_for(
|
|
808
|
+
future,
|
|
809
|
+
timeout=self._timeout_seconds,
|
|
810
|
+
)
|
|
811
|
+
|
|
812
|
+
# Success - reset circuit breaker (thread-safe)
|
|
813
|
+
async with self._circuit_breaker_lock:
|
|
814
|
+
await self._reset_circuit_breaker()
|
|
815
|
+
|
|
816
|
+
logger.debug(
|
|
817
|
+
f"Published to topic {topic}",
|
|
818
|
+
extra={
|
|
819
|
+
"partition": record_metadata.partition,
|
|
820
|
+
"offset": record_metadata.offset,
|
|
821
|
+
"correlation_id": str(headers.correlation_id),
|
|
822
|
+
},
|
|
823
|
+
)
|
|
824
|
+
return
|
|
825
|
+
|
|
826
|
+
except TimeoutError as e:
|
|
827
|
+
# Clean up producer on timeout to prevent resource leak (thread-safe)
|
|
828
|
+
async with self._producer_lock:
|
|
829
|
+
if self._producer is not None:
|
|
830
|
+
try:
|
|
831
|
+
await self._producer.stop()
|
|
832
|
+
except Exception as cleanup_err:
|
|
833
|
+
logger.warning(
|
|
834
|
+
"Cleanup failed for Kafka producer stop during publish: %s",
|
|
835
|
+
cleanup_err,
|
|
836
|
+
exc_info=True,
|
|
837
|
+
)
|
|
838
|
+
self._producer = None
|
|
839
|
+
last_exception = e
|
|
840
|
+
async with self._circuit_breaker_lock:
|
|
841
|
+
await self._record_circuit_failure(
|
|
842
|
+
operation="publish", correlation_id=headers.correlation_id
|
|
843
|
+
)
|
|
844
|
+
logger.warning(
|
|
845
|
+
f"Publish timeout (attempt {attempt + 1}/{self._max_retry_attempts + 1})",
|
|
846
|
+
extra={
|
|
847
|
+
"topic": topic,
|
|
848
|
+
"correlation_id": str(headers.correlation_id),
|
|
849
|
+
},
|
|
850
|
+
)
|
|
851
|
+
|
|
852
|
+
except KafkaError as e:
|
|
853
|
+
last_exception = e
|
|
854
|
+
async with self._circuit_breaker_lock:
|
|
855
|
+
await self._record_circuit_failure(
|
|
856
|
+
operation="publish", correlation_id=headers.correlation_id
|
|
857
|
+
)
|
|
858
|
+
logger.warning(
|
|
859
|
+
f"Kafka error on publish (attempt {attempt + 1}/{self._max_retry_attempts + 1}): {e}",
|
|
860
|
+
extra={
|
|
861
|
+
"topic": topic,
|
|
862
|
+
"correlation_id": str(headers.correlation_id),
|
|
863
|
+
},
|
|
864
|
+
)
|
|
865
|
+
|
|
866
|
+
except Exception as e:
|
|
867
|
+
last_exception = e
|
|
868
|
+
async with self._circuit_breaker_lock:
|
|
869
|
+
await self._record_circuit_failure(
|
|
870
|
+
operation="publish", correlation_id=headers.correlation_id
|
|
871
|
+
)
|
|
872
|
+
logger.warning(
|
|
873
|
+
f"Publish error (attempt {attempt + 1}/{self._max_retry_attempts + 1}): {e}",
|
|
874
|
+
extra={
|
|
875
|
+
"topic": topic,
|
|
876
|
+
"correlation_id": str(headers.correlation_id),
|
|
877
|
+
},
|
|
878
|
+
)
|
|
879
|
+
|
|
880
|
+
# Calculate backoff with jitter
|
|
881
|
+
if attempt < self._max_retry_attempts:
|
|
882
|
+
delay = self._retry_backoff_base * (2**attempt)
|
|
883
|
+
jitter = random.uniform(0.5, 1.5)
|
|
884
|
+
delay *= jitter
|
|
885
|
+
await asyncio.sleep(delay)
|
|
886
|
+
|
|
887
|
+
# All retries exhausted - differentiate timeout vs connection errors
|
|
888
|
+
context = ModelInfraErrorContext(
|
|
889
|
+
transport_type=EnumInfraTransportType.KAFKA,
|
|
890
|
+
operation="publish",
|
|
891
|
+
target_name=f"kafka.{topic}",
|
|
892
|
+
correlation_id=headers.correlation_id,
|
|
893
|
+
)
|
|
894
|
+
if isinstance(last_exception, TimeoutError):
|
|
895
|
+
timeout_ctx = ModelTimeoutErrorContext(
|
|
896
|
+
transport_type=EnumInfraTransportType.KAFKA,
|
|
897
|
+
operation="publish",
|
|
898
|
+
target_name=f"kafka.{topic}",
|
|
899
|
+
correlation_id=headers.correlation_id,
|
|
900
|
+
timeout_seconds=self._timeout_seconds,
|
|
901
|
+
)
|
|
902
|
+
raise InfraTimeoutError(
|
|
903
|
+
f"Timeout publishing to topic {topic} after {self._max_retry_attempts + 1} attempts",
|
|
904
|
+
context=timeout_ctx,
|
|
905
|
+
topic=topic,
|
|
906
|
+
retry_count=self._max_retry_attempts + 1,
|
|
907
|
+
) from last_exception
|
|
908
|
+
raise InfraConnectionError(
|
|
909
|
+
f"Failed to publish to topic {topic} after {self._max_retry_attempts + 1} attempts",
|
|
910
|
+
context=context,
|
|
911
|
+
topic=topic,
|
|
912
|
+
retry_count=self._max_retry_attempts + 1,
|
|
913
|
+
) from last_exception
|
|
914
|
+
|
|
915
|
+
async def subscribe(
|
|
916
|
+
self,
|
|
917
|
+
topic: str,
|
|
918
|
+
group_id: str,
|
|
919
|
+
on_message: Callable[[ModelEventMessage], Awaitable[None]],
|
|
920
|
+
) -> Callable[[], Awaitable[None]]:
|
|
921
|
+
"""Subscribe to topic with callback handler.
|
|
922
|
+
|
|
923
|
+
Registers a callback to be invoked for each message received on the topic.
|
|
924
|
+
Returns an unsubscribe function to remove the subscription.
|
|
925
|
+
|
|
926
|
+
Note: Unlike typical Kafka consumer groups, this implementation maintains
|
|
927
|
+
a subscriber registry and fans out messages to all registered callbacks,
|
|
928
|
+
matching the EventBusInmemory interface.
|
|
929
|
+
|
|
930
|
+
Args:
|
|
931
|
+
topic: Topic to subscribe to
|
|
932
|
+
group_id: Consumer group identifier for this subscription
|
|
933
|
+
on_message: Async callback invoked for each message
|
|
934
|
+
|
|
935
|
+
Returns:
|
|
936
|
+
Async unsubscribe function to remove this subscription
|
|
937
|
+
|
|
938
|
+
Example:
|
|
939
|
+
```python
|
|
940
|
+
async def handler(msg):
|
|
941
|
+
print(f"Received: {msg.value}")
|
|
942
|
+
|
|
943
|
+
unsubscribe = await bus.subscribe("events", "group1", handler)
|
|
944
|
+
# ... later ...
|
|
945
|
+
await unsubscribe()
|
|
946
|
+
```
|
|
947
|
+
"""
|
|
948
|
+
subscription_id = str(uuid4())
|
|
949
|
+
correlation_id = uuid4()
|
|
950
|
+
|
|
951
|
+
# Validate topic name
|
|
952
|
+
self._validate_topic_name(topic, correlation_id)
|
|
953
|
+
|
|
954
|
+
async with self._lock:
|
|
955
|
+
# Add to subscriber registry
|
|
956
|
+
self._subscribers[topic].append((group_id, subscription_id, on_message))
|
|
957
|
+
|
|
958
|
+
# Start consumer for this topic if not already running
|
|
959
|
+
if topic not in self._consumers and self._started:
|
|
960
|
+
await self._start_consumer_for_topic(topic, group_id)
|
|
961
|
+
|
|
962
|
+
logger.debug(
|
|
963
|
+
"Subscriber added",
|
|
964
|
+
extra={
|
|
965
|
+
"topic": topic,
|
|
966
|
+
"group_id": group_id,
|
|
967
|
+
"subscription_id": subscription_id,
|
|
968
|
+
},
|
|
969
|
+
)
|
|
970
|
+
|
|
971
|
+
async def unsubscribe() -> None:
|
|
972
|
+
"""Remove this subscription from the topic."""
|
|
973
|
+
async with self._lock:
|
|
974
|
+
try:
|
|
975
|
+
# Find and remove the subscription
|
|
976
|
+
subs = self._subscribers.get(topic, [])
|
|
977
|
+
for i, (_gid, sid, _) in enumerate(subs):
|
|
978
|
+
if sid == subscription_id:
|
|
979
|
+
subs.pop(i)
|
|
980
|
+
break
|
|
981
|
+
|
|
982
|
+
logger.debug(
|
|
983
|
+
"Subscriber removed",
|
|
984
|
+
extra={
|
|
985
|
+
"topic": topic,
|
|
986
|
+
"group_id": group_id,
|
|
987
|
+
"subscription_id": subscription_id,
|
|
988
|
+
},
|
|
989
|
+
)
|
|
990
|
+
|
|
991
|
+
# Stop consumer if no more subscribers for this topic
|
|
992
|
+
if not self._subscribers.get(topic):
|
|
993
|
+
await self._stop_consumer_for_topic(topic)
|
|
994
|
+
|
|
995
|
+
except Exception as e:
|
|
996
|
+
logger.warning(f"Error during unsubscribe: {e}")
|
|
997
|
+
|
|
998
|
+
return unsubscribe
|
|
999
|
+
|
|
1000
|
+
async def _start_consumer_for_topic(self, topic: str, group_id: str) -> None:
|
|
1001
|
+
"""Start a Kafka consumer for a specific topic.
|
|
1002
|
+
|
|
1003
|
+
This method creates and starts a Kafka consumer for the specified topic,
|
|
1004
|
+
then launches a background task to consume messages. All startup failures
|
|
1005
|
+
are logged and propagated to the caller.
|
|
1006
|
+
|
|
1007
|
+
Args:
|
|
1008
|
+
topic: Topic to consume from
|
|
1009
|
+
group_id: Consumer group ID
|
|
1010
|
+
|
|
1011
|
+
Raises:
|
|
1012
|
+
InfraTimeoutError: If consumer startup times out after timeout_seconds
|
|
1013
|
+
InfraConnectionError: If consumer fails to connect to Kafka brokers
|
|
1014
|
+
"""
|
|
1015
|
+
if topic in self._consumers:
|
|
1016
|
+
return
|
|
1017
|
+
|
|
1018
|
+
correlation_id = uuid4()
|
|
1019
|
+
sanitized_servers = self._sanitize_bootstrap_servers(self._bootstrap_servers)
|
|
1020
|
+
|
|
1021
|
+
# Normalize empty string to default group (treats "" same as None)
|
|
1022
|
+
# This ensures consistent behavior when group_id is unset or empty
|
|
1023
|
+
effective_group_id = group_id.strip() if group_id else self._group
|
|
1024
|
+
|
|
1025
|
+
# Apply consumer configuration from config model
|
|
1026
|
+
consumer = AIOKafkaConsumer(
|
|
1027
|
+
topic,
|
|
1028
|
+
bootstrap_servers=self._bootstrap_servers,
|
|
1029
|
+
group_id=f"{self._environment}.{effective_group_id}",
|
|
1030
|
+
auto_offset_reset=self._config.auto_offset_reset,
|
|
1031
|
+
enable_auto_commit=self._config.enable_auto_commit,
|
|
1032
|
+
)
|
|
1033
|
+
|
|
1034
|
+
try:
|
|
1035
|
+
await asyncio.wait_for(
|
|
1036
|
+
consumer.start(),
|
|
1037
|
+
timeout=self._timeout_seconds,
|
|
1038
|
+
)
|
|
1039
|
+
|
|
1040
|
+
self._consumers[topic] = consumer
|
|
1041
|
+
|
|
1042
|
+
# Start background task to consume messages with correlation tracking
|
|
1043
|
+
task = asyncio.create_task(self._consume_loop(topic, correlation_id))
|
|
1044
|
+
self._consumer_tasks[topic] = task
|
|
1045
|
+
|
|
1046
|
+
logger.info(
|
|
1047
|
+
f"Started consumer for topic {topic}",
|
|
1048
|
+
extra={
|
|
1049
|
+
"topic": topic,
|
|
1050
|
+
"group_id": effective_group_id,
|
|
1051
|
+
"correlation_id": str(correlation_id),
|
|
1052
|
+
"servers": sanitized_servers,
|
|
1053
|
+
},
|
|
1054
|
+
)
|
|
1055
|
+
|
|
1056
|
+
except TimeoutError as e:
|
|
1057
|
+
# Clean up consumer on failure to prevent resource leak
|
|
1058
|
+
try:
|
|
1059
|
+
await consumer.stop()
|
|
1060
|
+
except Exception as cleanup_err:
|
|
1061
|
+
logger.warning(
|
|
1062
|
+
"Cleanup failed for Kafka consumer stop (topic=%s): %s",
|
|
1063
|
+
topic,
|
|
1064
|
+
cleanup_err,
|
|
1065
|
+
exc_info=True,
|
|
1066
|
+
)
|
|
1067
|
+
|
|
1068
|
+
# Propagate timeout error to surface startup failures (differentiate from connection errors)
|
|
1069
|
+
timeout_ctx = ModelTimeoutErrorContext(
|
|
1070
|
+
transport_type=EnumInfraTransportType.KAFKA,
|
|
1071
|
+
operation="start_consumer",
|
|
1072
|
+
target_name=f"kafka.{topic}",
|
|
1073
|
+
correlation_id=correlation_id,
|
|
1074
|
+
timeout_seconds=self._timeout_seconds,
|
|
1075
|
+
)
|
|
1076
|
+
logger.exception(
|
|
1077
|
+
f"Timeout starting consumer for topic {topic} after {self._timeout_seconds}s",
|
|
1078
|
+
extra={
|
|
1079
|
+
"topic": topic,
|
|
1080
|
+
"group_id": group_id,
|
|
1081
|
+
"correlation_id": str(correlation_id),
|
|
1082
|
+
"timeout_seconds": self._timeout_seconds,
|
|
1083
|
+
"servers": sanitized_servers,
|
|
1084
|
+
"error_type": "timeout",
|
|
1085
|
+
},
|
|
1086
|
+
)
|
|
1087
|
+
raise InfraTimeoutError(
|
|
1088
|
+
f"Timeout starting consumer for topic {topic} after {self._timeout_seconds}s",
|
|
1089
|
+
context=timeout_ctx,
|
|
1090
|
+
topic=topic,
|
|
1091
|
+
servers=sanitized_servers,
|
|
1092
|
+
) from e
|
|
1093
|
+
|
|
1094
|
+
except Exception as e:
|
|
1095
|
+
# Clean up consumer on failure to prevent resource leak
|
|
1096
|
+
try:
|
|
1097
|
+
await consumer.stop()
|
|
1098
|
+
except Exception as cleanup_err:
|
|
1099
|
+
logger.warning(
|
|
1100
|
+
"Cleanup failed for Kafka consumer stop (topic=%s): %s",
|
|
1101
|
+
topic,
|
|
1102
|
+
cleanup_err,
|
|
1103
|
+
exc_info=True,
|
|
1104
|
+
)
|
|
1105
|
+
|
|
1106
|
+
# Propagate connection error to surface startup failures (differentiate from timeout)
|
|
1107
|
+
context = ModelInfraErrorContext(
|
|
1108
|
+
transport_type=EnumInfraTransportType.KAFKA,
|
|
1109
|
+
operation="start_consumer",
|
|
1110
|
+
target_name=f"kafka.{topic}",
|
|
1111
|
+
correlation_id=correlation_id,
|
|
1112
|
+
)
|
|
1113
|
+
logger.exception(
|
|
1114
|
+
f"Failed to start consumer for topic {topic}: {e}",
|
|
1115
|
+
extra={
|
|
1116
|
+
"topic": topic,
|
|
1117
|
+
"group_id": group_id,
|
|
1118
|
+
"correlation_id": str(correlation_id),
|
|
1119
|
+
"error": str(e),
|
|
1120
|
+
"error_type": type(e).__name__,
|
|
1121
|
+
"servers": sanitized_servers,
|
|
1122
|
+
},
|
|
1123
|
+
)
|
|
1124
|
+
raise InfraConnectionError(
|
|
1125
|
+
f"Failed to start consumer for topic {topic}: {e}",
|
|
1126
|
+
context=context,
|
|
1127
|
+
topic=topic,
|
|
1128
|
+
servers=sanitized_servers,
|
|
1129
|
+
) from e
|
|
1130
|
+
|
|
1131
|
+
async def _stop_consumer_for_topic(self, topic: str) -> None:
|
|
1132
|
+
"""Stop the consumer for a specific topic.
|
|
1133
|
+
|
|
1134
|
+
Args:
|
|
1135
|
+
topic: Topic to stop consuming from
|
|
1136
|
+
"""
|
|
1137
|
+
# Cancel consumer task
|
|
1138
|
+
if topic in self._consumer_tasks:
|
|
1139
|
+
task = self._consumer_tasks.pop(topic)
|
|
1140
|
+
if not task.done():
|
|
1141
|
+
task.cancel()
|
|
1142
|
+
try:
|
|
1143
|
+
await task
|
|
1144
|
+
except asyncio.CancelledError:
|
|
1145
|
+
pass
|
|
1146
|
+
|
|
1147
|
+
# Stop consumer
|
|
1148
|
+
if topic in self._consumers:
|
|
1149
|
+
consumer = self._consumers.pop(topic)
|
|
1150
|
+
try:
|
|
1151
|
+
await consumer.stop()
|
|
1152
|
+
except Exception as e:
|
|
1153
|
+
logger.warning(f"Error stopping consumer for topic {topic}: {e}")
|
|
1154
|
+
|
|
1155
|
+
async def _consume_loop(self, topic: str, correlation_id: UUID) -> None:
|
|
1156
|
+
"""Background loop to consume messages and dispatch to subscribers.
|
|
1157
|
+
|
|
1158
|
+
This method runs in a background task and continuously polls the Kafka consumer
|
|
1159
|
+
for new messages. It handles graceful cancellation, dispatches messages to all
|
|
1160
|
+
registered subscribers, and logs all errors without terminating the loop.
|
|
1161
|
+
|
|
1162
|
+
Args:
|
|
1163
|
+
topic: Topic being consumed
|
|
1164
|
+
correlation_id: Correlation ID for tracking this consumer task
|
|
1165
|
+
"""
|
|
1166
|
+
consumer = self._consumers.get(topic)
|
|
1167
|
+
if consumer is None:
|
|
1168
|
+
logger.warning(
|
|
1169
|
+
f"Consumer not found for topic {topic} in consume loop",
|
|
1170
|
+
extra={
|
|
1171
|
+
"topic": topic,
|
|
1172
|
+
"correlation_id": str(correlation_id),
|
|
1173
|
+
},
|
|
1174
|
+
)
|
|
1175
|
+
return
|
|
1176
|
+
|
|
1177
|
+
logger.debug(
|
|
1178
|
+
f"Consumer loop started for topic {topic}",
|
|
1179
|
+
extra={
|
|
1180
|
+
"topic": topic,
|
|
1181
|
+
"correlation_id": str(correlation_id),
|
|
1182
|
+
},
|
|
1183
|
+
)
|
|
1184
|
+
|
|
1185
|
+
try:
|
|
1186
|
+
async for msg in consumer:
|
|
1187
|
+
if self._shutdown:
|
|
1188
|
+
logger.debug(
|
|
1189
|
+
f"Consumer loop shutdown signal received for topic {topic}",
|
|
1190
|
+
extra={
|
|
1191
|
+
"topic": topic,
|
|
1192
|
+
"correlation_id": str(correlation_id),
|
|
1193
|
+
},
|
|
1194
|
+
)
|
|
1195
|
+
break
|
|
1196
|
+
|
|
1197
|
+
# Convert Kafka message to ModelEventMessage - handle conversion errors
|
|
1198
|
+
try:
|
|
1199
|
+
event_message = self._kafka_msg_to_model(msg, topic)
|
|
1200
|
+
except Exception as e:
|
|
1201
|
+
logger.exception(
|
|
1202
|
+
f"Failed to convert Kafka message to event model for topic {topic}",
|
|
1203
|
+
extra={
|
|
1204
|
+
"topic": topic,
|
|
1205
|
+
"correlation_id": str(correlation_id),
|
|
1206
|
+
"error": str(e),
|
|
1207
|
+
"error_type": type(e).__name__,
|
|
1208
|
+
},
|
|
1209
|
+
)
|
|
1210
|
+
# Deserialization errors are permanent failures - route to DLQ
|
|
1211
|
+
# Create minimal message from raw Kafka data for DLQ context
|
|
1212
|
+
await self._publish_raw_to_dlq(
|
|
1213
|
+
original_topic=topic,
|
|
1214
|
+
raw_msg=msg,
|
|
1215
|
+
error=e,
|
|
1216
|
+
correlation_id=correlation_id,
|
|
1217
|
+
failure_type="deserialization_error",
|
|
1218
|
+
)
|
|
1219
|
+
continue # Skip this message but continue consuming
|
|
1220
|
+
|
|
1221
|
+
# Get subscribers snapshot
|
|
1222
|
+
async with self._lock:
|
|
1223
|
+
subscribers = list(self._subscribers.get(topic, []))
|
|
1224
|
+
|
|
1225
|
+
# Dispatch to all subscribers
|
|
1226
|
+
for group_id, subscription_id, callback in subscribers:
|
|
1227
|
+
try:
|
|
1228
|
+
await callback(event_message)
|
|
1229
|
+
except Exception as e:
|
|
1230
|
+
# Check if message-level retries are exhausted
|
|
1231
|
+
retry_count = event_message.headers.retry_count
|
|
1232
|
+
max_retries = event_message.headers.max_retries
|
|
1233
|
+
retries_exhausted = retry_count >= max_retries
|
|
1234
|
+
|
|
1235
|
+
logger.exception(
|
|
1236
|
+
"Subscriber callback failed",
|
|
1237
|
+
extra={
|
|
1238
|
+
"topic": topic,
|
|
1239
|
+
"group_id": group_id,
|
|
1240
|
+
"subscription_id": subscription_id,
|
|
1241
|
+
"correlation_id": str(correlation_id),
|
|
1242
|
+
"error": str(e),
|
|
1243
|
+
"error_type": type(e).__name__,
|
|
1244
|
+
"retry_count": retry_count,
|
|
1245
|
+
"max_retries": max_retries,
|
|
1246
|
+
"retries_exhausted": retries_exhausted,
|
|
1247
|
+
},
|
|
1248
|
+
)
|
|
1249
|
+
|
|
1250
|
+
# Route to DLQ when retries exhausted (permanent failure)
|
|
1251
|
+
# Per ModelEventHeaders: "When retry_count >= max_retries, message should go to DLQ"
|
|
1252
|
+
if retries_exhausted:
|
|
1253
|
+
await self._publish_to_dlq(
|
|
1254
|
+
original_topic=topic,
|
|
1255
|
+
failed_message=event_message,
|
|
1256
|
+
error=e,
|
|
1257
|
+
correlation_id=correlation_id,
|
|
1258
|
+
)
|
|
1259
|
+
else:
|
|
1260
|
+
# Message still has retries available - log for potential republish
|
|
1261
|
+
# Note: Republishing logic is the responsibility of the caller/handler
|
|
1262
|
+
logger.warning(
|
|
1263
|
+
f"Handler failed but retries available ({retry_count}/{max_retries})",
|
|
1264
|
+
extra={
|
|
1265
|
+
"topic": topic,
|
|
1266
|
+
"correlation_id": str(correlation_id),
|
|
1267
|
+
"retry_count": retry_count,
|
|
1268
|
+
"max_retries": max_retries,
|
|
1269
|
+
},
|
|
1270
|
+
)
|
|
1271
|
+
# Continue dispatching to other subscribers even if one fails
|
|
1272
|
+
|
|
1273
|
+
except asyncio.CancelledError:
|
|
1274
|
+
# Graceful cancellation - this is expected during shutdown
|
|
1275
|
+
logger.info(
|
|
1276
|
+
f"Consumer loop cancelled for topic {topic}",
|
|
1277
|
+
extra={
|
|
1278
|
+
"topic": topic,
|
|
1279
|
+
"correlation_id": str(correlation_id),
|
|
1280
|
+
},
|
|
1281
|
+
)
|
|
1282
|
+
raise # Re-raise to properly handle task cancellation
|
|
1283
|
+
|
|
1284
|
+
except Exception as e:
|
|
1285
|
+
# Unexpected error in consumer loop - log with full context
|
|
1286
|
+
logger.exception(
|
|
1287
|
+
f"Consumer loop error for topic {topic}: {e}",
|
|
1288
|
+
extra={
|
|
1289
|
+
"topic": topic,
|
|
1290
|
+
"correlation_id": str(correlation_id),
|
|
1291
|
+
"error": str(e),
|
|
1292
|
+
"error_type": type(e).__name__,
|
|
1293
|
+
},
|
|
1294
|
+
)
|
|
1295
|
+
# Don't raise - allow task to complete and cleanup to proceed
|
|
1296
|
+
|
|
1297
|
+
finally:
|
|
1298
|
+
logger.info(
|
|
1299
|
+
f"Consumer loop exiting for topic {topic}",
|
|
1300
|
+
extra={
|
|
1301
|
+
"topic": topic,
|
|
1302
|
+
"correlation_id": str(correlation_id),
|
|
1303
|
+
},
|
|
1304
|
+
)
|
|
1305
|
+
|
|
1306
|
+
async def start_consuming(self) -> None:
|
|
1307
|
+
"""Start the consumer loop.
|
|
1308
|
+
|
|
1309
|
+
Protocol method for ProtocolEventBus compatibility.
|
|
1310
|
+
Blocks until shutdown() is called.
|
|
1311
|
+
"""
|
|
1312
|
+
if not self._started:
|
|
1313
|
+
await self.start()
|
|
1314
|
+
|
|
1315
|
+
# Collect topics that need consumers while holding lock briefly
|
|
1316
|
+
topics_to_start: list[tuple[str, str]] = []
|
|
1317
|
+
async with self._lock:
|
|
1318
|
+
for topic in self._subscribers:
|
|
1319
|
+
if topic not in self._consumers:
|
|
1320
|
+
subs = self._subscribers[topic]
|
|
1321
|
+
if subs:
|
|
1322
|
+
group_id = subs[0][0]
|
|
1323
|
+
topics_to_start.append((topic, group_id))
|
|
1324
|
+
|
|
1325
|
+
# Start consumers outside the lock to avoid blocking
|
|
1326
|
+
for topic, group_id in topics_to_start:
|
|
1327
|
+
await self._start_consumer_for_topic(topic, group_id)
|
|
1328
|
+
|
|
1329
|
+
# Block until shutdown
|
|
1330
|
+
while not self._shutdown:
|
|
1331
|
+
await asyncio.sleep(self._config.consumer_sleep_interval)
|
|
1332
|
+
|
|
1333
|
+
async def health_check(self) -> dict[str, object]:
|
|
1334
|
+
"""Check event bus health.
|
|
1335
|
+
|
|
1336
|
+
Protocol method for ProtocolEventBus compatibility.
|
|
1337
|
+
|
|
1338
|
+
Returns:
|
|
1339
|
+
Dictionary with health status information:
|
|
1340
|
+
- healthy: Whether the bus is operational
|
|
1341
|
+
- started: Whether start() has been called
|
|
1342
|
+
- environment: Current environment
|
|
1343
|
+
- group: Current consumer group
|
|
1344
|
+
- bootstrap_servers: Kafka bootstrap servers
|
|
1345
|
+
- circuit_state: Current circuit breaker state
|
|
1346
|
+
- subscriber_count: Total number of active subscriptions
|
|
1347
|
+
- topic_count: Number of topics with subscribers
|
|
1348
|
+
- consumer_count: Number of active consumers
|
|
1349
|
+
"""
|
|
1350
|
+
async with self._lock:
|
|
1351
|
+
subscriber_count = sum(len(subs) for subs in self._subscribers.values())
|
|
1352
|
+
topic_count = len(self._subscribers)
|
|
1353
|
+
consumer_count = len(self._consumers)
|
|
1354
|
+
started = self._started
|
|
1355
|
+
|
|
1356
|
+
# Get circuit breaker state (thread-safe access)
|
|
1357
|
+
async with self._circuit_breaker_lock:
|
|
1358
|
+
circuit_state = "open" if self._circuit_breaker_open else "closed"
|
|
1359
|
+
|
|
1360
|
+
# Check if producer is healthy (thread-safe access)
|
|
1361
|
+
producer_healthy = False
|
|
1362
|
+
async with self._producer_lock:
|
|
1363
|
+
if self._producer is not None:
|
|
1364
|
+
try:
|
|
1365
|
+
# Check if producer client is not closed
|
|
1366
|
+
producer_healthy = not getattr(self._producer, "_closed", True)
|
|
1367
|
+
except Exception:
|
|
1368
|
+
producer_healthy = False
|
|
1369
|
+
|
|
1370
|
+
return {
|
|
1371
|
+
"healthy": started and producer_healthy,
|
|
1372
|
+
"started": started,
|
|
1373
|
+
"environment": self._environment,
|
|
1374
|
+
"group": self._group,
|
|
1375
|
+
"bootstrap_servers": self._sanitize_bootstrap_servers(
|
|
1376
|
+
self._bootstrap_servers
|
|
1377
|
+
),
|
|
1378
|
+
"circuit_state": circuit_state,
|
|
1379
|
+
"subscriber_count": subscriber_count,
|
|
1380
|
+
"topic_count": topic_count,
|
|
1381
|
+
"consumer_count": consumer_count,
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
# =========================================================================
|
|
1385
|
+
# Helper Methods
|
|
1386
|
+
# =========================================================================
|
|
1387
|
+
|
|
1388
|
+
def _sanitize_bootstrap_servers(self, servers: str) -> str:
|
|
1389
|
+
"""Sanitize bootstrap servers string to remove potential credentials.
|
|
1390
|
+
|
|
1391
|
+
Removes any authentication tokens, passwords, or sensitive data from
|
|
1392
|
+
the bootstrap servers string before logging or including in errors.
|
|
1393
|
+
|
|
1394
|
+
Args:
|
|
1395
|
+
servers: Raw bootstrap servers string (may contain credentials)
|
|
1396
|
+
|
|
1397
|
+
Returns:
|
|
1398
|
+
Sanitized servers string safe for logging and error messages
|
|
1399
|
+
|
|
1400
|
+
Example:
|
|
1401
|
+
"user:pass@kafka:9092" -> "kafka:9092"
|
|
1402
|
+
"kafka:9092,kafka2:9092" -> "kafka:9092,kafka2:9092"
|
|
1403
|
+
"""
|
|
1404
|
+
if not servers:
|
|
1405
|
+
return "unknown"
|
|
1406
|
+
|
|
1407
|
+
# Split by comma for multiple servers
|
|
1408
|
+
server_list = [s.strip() for s in servers.split(",")]
|
|
1409
|
+
sanitized = []
|
|
1410
|
+
|
|
1411
|
+
for server in server_list:
|
|
1412
|
+
# Remove any user:pass@ prefix (credentials)
|
|
1413
|
+
if "@" in server:
|
|
1414
|
+
# Keep only the part after @
|
|
1415
|
+
server = server.split("@", 1)[1]
|
|
1416
|
+
sanitized.append(server)
|
|
1417
|
+
|
|
1418
|
+
return ",".join(sanitized)
|
|
1419
|
+
|
|
1420
|
+
def _validate_topic_name(self, topic: str, correlation_id: UUID) -> None:
|
|
1421
|
+
"""Validate Kafka topic name according to Kafka naming rules.
|
|
1422
|
+
|
|
1423
|
+
Kafka topic names must:
|
|
1424
|
+
- Not be empty
|
|
1425
|
+
- Be 255 characters or less
|
|
1426
|
+
- Contain only: a-z, A-Z, 0-9, period (.), underscore (_), hyphen (-)
|
|
1427
|
+
- Not be "." or ".." (reserved)
|
|
1428
|
+
|
|
1429
|
+
Args:
|
|
1430
|
+
topic: Topic name to validate
|
|
1431
|
+
correlation_id: Correlation ID for error context
|
|
1432
|
+
|
|
1433
|
+
Raises:
|
|
1434
|
+
ProtocolConfigurationError: If topic name is invalid
|
|
1435
|
+
|
|
1436
|
+
Reference:
|
|
1437
|
+
https://kafka.apache.org/documentation/#topicconfigs
|
|
1438
|
+
"""
|
|
1439
|
+
context = ModelInfraErrorContext(
|
|
1440
|
+
transport_type=EnumInfraTransportType.KAFKA,
|
|
1441
|
+
operation="validate_topic",
|
|
1442
|
+
target_name=f"kafka.{self._environment}",
|
|
1443
|
+
correlation_id=correlation_id,
|
|
1444
|
+
)
|
|
1445
|
+
|
|
1446
|
+
if not topic:
|
|
1447
|
+
raise ProtocolConfigurationError(
|
|
1448
|
+
"Topic name cannot be empty",
|
|
1449
|
+
context=context,
|
|
1450
|
+
parameter="topic",
|
|
1451
|
+
value=topic,
|
|
1452
|
+
)
|
|
1453
|
+
|
|
1454
|
+
if len(topic) > 255:
|
|
1455
|
+
raise ProtocolConfigurationError(
|
|
1456
|
+
f"Topic name '{topic}' exceeds maximum length of 255 characters",
|
|
1457
|
+
context=context,
|
|
1458
|
+
parameter="topic",
|
|
1459
|
+
value=topic,
|
|
1460
|
+
)
|
|
1461
|
+
|
|
1462
|
+
if topic in (".", ".."):
|
|
1463
|
+
raise ProtocolConfigurationError(
|
|
1464
|
+
f"Topic name '{topic}' is reserved and cannot be used",
|
|
1465
|
+
context=context,
|
|
1466
|
+
parameter="topic",
|
|
1467
|
+
value=topic,
|
|
1468
|
+
)
|
|
1469
|
+
|
|
1470
|
+
# Validate characters (a-z, A-Z, 0-9, '.', '_', '-')
|
|
1471
|
+
if not re.match(r"^[a-zA-Z0-9._-]+$", topic):
|
|
1472
|
+
raise ProtocolConfigurationError(
|
|
1473
|
+
f"Topic name '{topic}' contains invalid characters. "
|
|
1474
|
+
"Only alphanumeric characters, periods (.), underscores (_), "
|
|
1475
|
+
"and hyphens (-) are allowed",
|
|
1476
|
+
context=context,
|
|
1477
|
+
parameter="topic",
|
|
1478
|
+
value=topic,
|
|
1479
|
+
)
|
|
1480
|
+
|
|
1481
|
+
def _model_headers_to_kafka(
|
|
1482
|
+
self, headers: ModelEventHeaders
|
|
1483
|
+
) -> list[tuple[str, bytes]]:
|
|
1484
|
+
"""Convert ModelEventHeaders to Kafka header format.
|
|
1485
|
+
|
|
1486
|
+
Args:
|
|
1487
|
+
headers: Model headers
|
|
1488
|
+
|
|
1489
|
+
Returns:
|
|
1490
|
+
List of (key, value) tuples with bytes values
|
|
1491
|
+
"""
|
|
1492
|
+
kafka_headers: list[tuple[str, bytes]] = [
|
|
1493
|
+
("content_type", headers.content_type.encode("utf-8")),
|
|
1494
|
+
("correlation_id", str(headers.correlation_id).encode("utf-8")),
|
|
1495
|
+
("message_id", str(headers.message_id).encode("utf-8")),
|
|
1496
|
+
("timestamp", headers.timestamp.isoformat().encode("utf-8")),
|
|
1497
|
+
("source", headers.source.encode("utf-8")),
|
|
1498
|
+
("event_type", headers.event_type.encode("utf-8")),
|
|
1499
|
+
("schema_version", headers.schema_version.encode("utf-8")),
|
|
1500
|
+
("priority", headers.priority.encode("utf-8")),
|
|
1501
|
+
("retry_count", str(headers.retry_count).encode("utf-8")),
|
|
1502
|
+
("max_retries", str(headers.max_retries).encode("utf-8")),
|
|
1503
|
+
]
|
|
1504
|
+
|
|
1505
|
+
# Add optional headers if present
|
|
1506
|
+
if headers.destination:
|
|
1507
|
+
kafka_headers.append(("destination", headers.destination.encode("utf-8")))
|
|
1508
|
+
if headers.trace_id:
|
|
1509
|
+
kafka_headers.append(("trace_id", headers.trace_id.encode("utf-8")))
|
|
1510
|
+
if headers.span_id:
|
|
1511
|
+
kafka_headers.append(("span_id", headers.span_id.encode("utf-8")))
|
|
1512
|
+
if headers.parent_span_id:
|
|
1513
|
+
kafka_headers.append(
|
|
1514
|
+
("parent_span_id", headers.parent_span_id.encode("utf-8"))
|
|
1515
|
+
)
|
|
1516
|
+
if headers.operation_name:
|
|
1517
|
+
kafka_headers.append(
|
|
1518
|
+
("operation_name", headers.operation_name.encode("utf-8"))
|
|
1519
|
+
)
|
|
1520
|
+
if headers.routing_key:
|
|
1521
|
+
kafka_headers.append(("routing_key", headers.routing_key.encode("utf-8")))
|
|
1522
|
+
if headers.partition_key:
|
|
1523
|
+
kafka_headers.append(
|
|
1524
|
+
("partition_key", headers.partition_key.encode("utf-8"))
|
|
1525
|
+
)
|
|
1526
|
+
if headers.ttl_seconds is not None:
|
|
1527
|
+
kafka_headers.append(
|
|
1528
|
+
("ttl_seconds", str(headers.ttl_seconds).encode("utf-8"))
|
|
1529
|
+
)
|
|
1530
|
+
|
|
1531
|
+
return kafka_headers
|
|
1532
|
+
|
|
1533
|
+
def _kafka_headers_to_model(
|
|
1534
|
+
self, kafka_headers: list[tuple[str, bytes]] | None
|
|
1535
|
+
) -> ModelEventHeaders:
|
|
1536
|
+
"""Convert Kafka headers to ModelEventHeaders.
|
|
1537
|
+
|
|
1538
|
+
Args:
|
|
1539
|
+
kafka_headers: Kafka header list
|
|
1540
|
+
|
|
1541
|
+
Returns:
|
|
1542
|
+
ModelEventHeaders instance
|
|
1543
|
+
"""
|
|
1544
|
+
if not kafka_headers:
|
|
1545
|
+
return ModelEventHeaders(
|
|
1546
|
+
source="unknown",
|
|
1547
|
+
event_type="unknown",
|
|
1548
|
+
timestamp=datetime.now(UTC),
|
|
1549
|
+
)
|
|
1550
|
+
|
|
1551
|
+
headers_dict: dict[str, str] = {}
|
|
1552
|
+
for key, value in kafka_headers:
|
|
1553
|
+
if value is not None:
|
|
1554
|
+
headers_dict[key] = value.decode("utf-8")
|
|
1555
|
+
|
|
1556
|
+
# Parse correlation_id from string to UUID (with fallback to new UUID)
|
|
1557
|
+
correlation_id_str = headers_dict.get("correlation_id")
|
|
1558
|
+
if correlation_id_str:
|
|
1559
|
+
try:
|
|
1560
|
+
correlation_id = UUID(correlation_id_str)
|
|
1561
|
+
except (ValueError, AttributeError):
|
|
1562
|
+
# Invalid UUID format - generate new one
|
|
1563
|
+
correlation_id = uuid4()
|
|
1564
|
+
else:
|
|
1565
|
+
correlation_id = uuid4()
|
|
1566
|
+
|
|
1567
|
+
# Parse message_id from string to UUID (with fallback to new UUID)
|
|
1568
|
+
message_id_str = headers_dict.get("message_id")
|
|
1569
|
+
if message_id_str:
|
|
1570
|
+
try:
|
|
1571
|
+
message_id = UUID(message_id_str)
|
|
1572
|
+
except (ValueError, AttributeError):
|
|
1573
|
+
# Invalid UUID format - generate new one
|
|
1574
|
+
message_id = uuid4()
|
|
1575
|
+
else:
|
|
1576
|
+
message_id = uuid4()
|
|
1577
|
+
|
|
1578
|
+
# Parse timestamp from ISO format string to datetime (with fallback to now)
|
|
1579
|
+
timestamp_str = headers_dict.get("timestamp")
|
|
1580
|
+
if timestamp_str:
|
|
1581
|
+
timestamp = datetime.fromisoformat(timestamp_str)
|
|
1582
|
+
else:
|
|
1583
|
+
timestamp = datetime.now(UTC)
|
|
1584
|
+
|
|
1585
|
+
# Parse priority with validation (default to "normal" if invalid)
|
|
1586
|
+
priority_str = headers_dict.get("priority", "normal")
|
|
1587
|
+
valid_priorities = ("low", "normal", "high", "critical")
|
|
1588
|
+
priority = priority_str if priority_str in valid_priorities else "normal"
|
|
1589
|
+
|
|
1590
|
+
# Parse integer fields with fallback defaults
|
|
1591
|
+
retry_count_str = headers_dict.get("retry_count")
|
|
1592
|
+
retry_count = int(retry_count_str) if retry_count_str else 0
|
|
1593
|
+
|
|
1594
|
+
max_retries_str = headers_dict.get("max_retries")
|
|
1595
|
+
max_retries = int(max_retries_str) if max_retries_str else 3
|
|
1596
|
+
|
|
1597
|
+
ttl_seconds_str = headers_dict.get("ttl_seconds")
|
|
1598
|
+
ttl_seconds = int(ttl_seconds_str) if ttl_seconds_str else None
|
|
1599
|
+
|
|
1600
|
+
return ModelEventHeaders(
|
|
1601
|
+
content_type=headers_dict.get("content_type", "application/json"),
|
|
1602
|
+
correlation_id=correlation_id,
|
|
1603
|
+
message_id=message_id,
|
|
1604
|
+
timestamp=timestamp,
|
|
1605
|
+
source=headers_dict.get("source", "unknown"),
|
|
1606
|
+
event_type=headers_dict.get("event_type", "unknown"),
|
|
1607
|
+
schema_version=headers_dict.get("schema_version", "1.0.0"),
|
|
1608
|
+
destination=headers_dict.get("destination"),
|
|
1609
|
+
trace_id=headers_dict.get("trace_id"),
|
|
1610
|
+
span_id=headers_dict.get("span_id"),
|
|
1611
|
+
parent_span_id=headers_dict.get("parent_span_id"),
|
|
1612
|
+
operation_name=headers_dict.get("operation_name"),
|
|
1613
|
+
priority=priority,
|
|
1614
|
+
routing_key=headers_dict.get("routing_key"),
|
|
1615
|
+
partition_key=headers_dict.get("partition_key"),
|
|
1616
|
+
retry_count=retry_count,
|
|
1617
|
+
max_retries=max_retries,
|
|
1618
|
+
ttl_seconds=ttl_seconds,
|
|
1619
|
+
)
|
|
1620
|
+
|
|
1621
|
+
def _kafka_msg_to_model(self, msg: object, topic: str) -> ModelEventMessage:
|
|
1622
|
+
"""Convert Kafka ConsumerRecord to ModelEventMessage.
|
|
1623
|
+
|
|
1624
|
+
Args:
|
|
1625
|
+
msg: Kafka ConsumerRecord
|
|
1626
|
+
topic: Topic name
|
|
1627
|
+
|
|
1628
|
+
Returns:
|
|
1629
|
+
ModelEventMessage instance
|
|
1630
|
+
"""
|
|
1631
|
+
# Extract fields from Kafka message
|
|
1632
|
+
key = getattr(msg, "key", None)
|
|
1633
|
+
value = getattr(msg, "value", b"")
|
|
1634
|
+
offset = getattr(msg, "offset", None)
|
|
1635
|
+
partition = getattr(msg, "partition", None)
|
|
1636
|
+
kafka_headers = getattr(msg, "headers", None)
|
|
1637
|
+
|
|
1638
|
+
# Convert key to bytes if it's a string
|
|
1639
|
+
if isinstance(key, str):
|
|
1640
|
+
key = key.encode("utf-8")
|
|
1641
|
+
|
|
1642
|
+
# Ensure value is bytes
|
|
1643
|
+
if isinstance(value, str):
|
|
1644
|
+
value = value.encode("utf-8")
|
|
1645
|
+
|
|
1646
|
+
headers = self._kafka_headers_to_model(kafka_headers)
|
|
1647
|
+
|
|
1648
|
+
return ModelEventMessage(
|
|
1649
|
+
topic=topic,
|
|
1650
|
+
key=key,
|
|
1651
|
+
value=value,
|
|
1652
|
+
headers=headers,
|
|
1653
|
+
offset=str(offset) if offset is not None else None,
|
|
1654
|
+
partition=partition,
|
|
1655
|
+
)
|
|
1656
|
+
|
|
1657
|
+
|
|
1658
|
+
__all__: list[str] = ["EventBusKafka"]
|