omnibase_infra 0.2.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- omnibase_infra/__init__.py +101 -0
- omnibase_infra/adapters/adapter_onex_tool_execution.py +451 -0
- omnibase_infra/capabilities/__init__.py +15 -0
- omnibase_infra/capabilities/capability_inference_rules.py +211 -0
- omnibase_infra/capabilities/contract_capability_extractor.py +221 -0
- omnibase_infra/capabilities/intent_type_extractor.py +160 -0
- omnibase_infra/cli/__init__.py +1 -0
- omnibase_infra/cli/commands.py +216 -0
- omnibase_infra/clients/__init__.py +0 -0
- omnibase_infra/configs/widget_mapping.yaml +176 -0
- omnibase_infra/constants_topic_patterns.py +26 -0
- omnibase_infra/contracts/handlers/filesystem/handler_contract.yaml +264 -0
- omnibase_infra/contracts/handlers/mcp/handler_contract.yaml +141 -0
- omnibase_infra/decorators/__init__.py +29 -0
- omnibase_infra/decorators/allow_any.py +109 -0
- omnibase_infra/dlq/__init__.py +90 -0
- omnibase_infra/dlq/constants_dlq.py +57 -0
- omnibase_infra/dlq/models/__init__.py +26 -0
- omnibase_infra/dlq/models/enum_replay_status.py +37 -0
- omnibase_infra/dlq/models/model_dlq_replay_record.py +135 -0
- omnibase_infra/dlq/models/model_dlq_tracking_config.py +184 -0
- omnibase_infra/dlq/service_dlq_tracking.py +611 -0
- omnibase_infra/enums/__init__.py +132 -0
- omnibase_infra/enums/enum_any_type_violation.py +104 -0
- omnibase_infra/enums/enum_backend_type.py +27 -0
- omnibase_infra/enums/enum_capture_outcome.py +42 -0
- omnibase_infra/enums/enum_capture_state.py +88 -0
- omnibase_infra/enums/enum_chain_violation_type.py +119 -0
- omnibase_infra/enums/enum_circuit_state.py +51 -0
- omnibase_infra/enums/enum_confirmation_event_type.py +27 -0
- omnibase_infra/enums/enum_consumer_group_purpose.py +92 -0
- omnibase_infra/enums/enum_contract_type.py +84 -0
- omnibase_infra/enums/enum_dedupe_strategy.py +46 -0
- omnibase_infra/enums/enum_dispatch_status.py +191 -0
- omnibase_infra/enums/enum_environment.py +46 -0
- omnibase_infra/enums/enum_execution_shape_violation.py +103 -0
- omnibase_infra/enums/enum_handler_error_type.py +111 -0
- omnibase_infra/enums/enum_handler_loader_error.py +178 -0
- omnibase_infra/enums/enum_handler_source_mode.py +86 -0
- omnibase_infra/enums/enum_handler_source_type.py +87 -0
- omnibase_infra/enums/enum_handler_type.py +77 -0
- omnibase_infra/enums/enum_handler_type_category.py +61 -0
- omnibase_infra/enums/enum_infra_transport_type.py +73 -0
- omnibase_infra/enums/enum_introspection_reason.py +154 -0
- omnibase_infra/enums/enum_kafka_acks.py +99 -0
- omnibase_infra/enums/enum_message_category.py +213 -0
- omnibase_infra/enums/enum_node_archetype.py +74 -0
- omnibase_infra/enums/enum_node_output_type.py +185 -0
- omnibase_infra/enums/enum_non_retryable_error_category.py +224 -0
- omnibase_infra/enums/enum_policy_type.py +32 -0
- omnibase_infra/enums/enum_registration_state.py +261 -0
- omnibase_infra/enums/enum_registration_status.py +33 -0
- omnibase_infra/enums/enum_registry_response_status.py +28 -0
- omnibase_infra/enums/enum_response_status.py +26 -0
- omnibase_infra/enums/enum_retry_error_category.py +98 -0
- omnibase_infra/enums/enum_security_rule_id.py +103 -0
- omnibase_infra/enums/enum_selection_strategy.py +91 -0
- omnibase_infra/enums/enum_topic_standard.py +42 -0
- omnibase_infra/enums/enum_validation_severity.py +78 -0
- omnibase_infra/errors/__init__.py +160 -0
- omnibase_infra/errors/error_architecture_violation.py +152 -0
- omnibase_infra/errors/error_binding_resolution.py +128 -0
- omnibase_infra/errors/error_chain_propagation.py +188 -0
- omnibase_infra/errors/error_compute_registry.py +95 -0
- omnibase_infra/errors/error_consul.py +132 -0
- omnibase_infra/errors/error_container_wiring.py +243 -0
- omnibase_infra/errors/error_event_bus_registry.py +105 -0
- omnibase_infra/errors/error_infra.py +610 -0
- omnibase_infra/errors/error_message_type_registry.py +101 -0
- omnibase_infra/errors/error_policy_registry.py +115 -0
- omnibase_infra/errors/error_vault.py +123 -0
- omnibase_infra/event_bus/__init__.py +72 -0
- omnibase_infra/event_bus/configs/kafka_event_bus_config.yaml +84 -0
- omnibase_infra/event_bus/event_bus_inmemory.py +797 -0
- omnibase_infra/event_bus/event_bus_kafka.py +1716 -0
- omnibase_infra/event_bus/mixin_kafka_broadcast.py +180 -0
- omnibase_infra/event_bus/mixin_kafka_dlq.py +771 -0
- omnibase_infra/event_bus/models/__init__.py +29 -0
- omnibase_infra/event_bus/models/config/__init__.py +20 -0
- omnibase_infra/event_bus/models/config/model_kafka_event_bus_config.py +693 -0
- omnibase_infra/event_bus/models/model_dlq_event.py +206 -0
- omnibase_infra/event_bus/models/model_dlq_metrics.py +304 -0
- omnibase_infra/event_bus/models/model_event_headers.py +115 -0
- omnibase_infra/event_bus/models/model_event_message.py +60 -0
- omnibase_infra/event_bus/testing/__init__.py +26 -0
- omnibase_infra/event_bus/testing/adapter_protocol_event_publisher_inmemory.py +418 -0
- omnibase_infra/event_bus/testing/model_publisher_metrics.py +64 -0
- omnibase_infra/event_bus/topic_constants.py +376 -0
- omnibase_infra/handlers/__init__.py +82 -0
- omnibase_infra/handlers/filesystem/__init__.py +48 -0
- omnibase_infra/handlers/filesystem/enum_file_system_operation.py +35 -0
- omnibase_infra/handlers/filesystem/model_file_system_request.py +298 -0
- omnibase_infra/handlers/filesystem/model_file_system_result.py +166 -0
- omnibase_infra/handlers/handler_consul.py +795 -0
- omnibase_infra/handlers/handler_db.py +1046 -0
- omnibase_infra/handlers/handler_filesystem.py +1478 -0
- omnibase_infra/handlers/handler_graph.py +2015 -0
- omnibase_infra/handlers/handler_http.py +926 -0
- omnibase_infra/handlers/handler_intent.py +387 -0
- omnibase_infra/handlers/handler_manifest_persistence.contract.yaml +184 -0
- omnibase_infra/handlers/handler_manifest_persistence.py +1539 -0
- omnibase_infra/handlers/handler_mcp.py +1430 -0
- omnibase_infra/handlers/handler_qdrant.py +1076 -0
- omnibase_infra/handlers/handler_vault.py +428 -0
- omnibase_infra/handlers/mcp/__init__.py +19 -0
- omnibase_infra/handlers/mcp/adapter_onex_to_mcp.py +446 -0
- omnibase_infra/handlers/mcp/protocols.py +178 -0
- omnibase_infra/handlers/mcp/transport_streamable_http.py +352 -0
- omnibase_infra/handlers/mixins/__init__.py +47 -0
- omnibase_infra/handlers/mixins/mixin_consul_initialization.py +349 -0
- omnibase_infra/handlers/mixins/mixin_consul_kv.py +338 -0
- omnibase_infra/handlers/mixins/mixin_consul_service.py +542 -0
- omnibase_infra/handlers/mixins/mixin_consul_topic_index.py +585 -0
- omnibase_infra/handlers/mixins/mixin_vault_initialization.py +338 -0
- omnibase_infra/handlers/mixins/mixin_vault_retry.py +412 -0
- omnibase_infra/handlers/mixins/mixin_vault_secrets.py +450 -0
- omnibase_infra/handlers/mixins/mixin_vault_token.py +365 -0
- omnibase_infra/handlers/models/__init__.py +286 -0
- omnibase_infra/handlers/models/consul/__init__.py +81 -0
- omnibase_infra/handlers/models/consul/enum_consul_operation_type.py +57 -0
- omnibase_infra/handlers/models/consul/model_consul_deregister_payload.py +51 -0
- omnibase_infra/handlers/models/consul/model_consul_handler_config.py +153 -0
- omnibase_infra/handlers/models/consul/model_consul_handler_payload.py +89 -0
- omnibase_infra/handlers/models/consul/model_consul_kv_get_found_payload.py +55 -0
- omnibase_infra/handlers/models/consul/model_consul_kv_get_not_found_payload.py +49 -0
- omnibase_infra/handlers/models/consul/model_consul_kv_get_recurse_payload.py +50 -0
- omnibase_infra/handlers/models/consul/model_consul_kv_item.py +33 -0
- omnibase_infra/handlers/models/consul/model_consul_kv_put_payload.py +41 -0
- omnibase_infra/handlers/models/consul/model_consul_register_payload.py +53 -0
- omnibase_infra/handlers/models/consul/model_consul_retry_config.py +66 -0
- omnibase_infra/handlers/models/consul/model_payload_consul.py +66 -0
- omnibase_infra/handlers/models/consul/registry_payload_consul.py +214 -0
- omnibase_infra/handlers/models/graph/__init__.py +35 -0
- omnibase_infra/handlers/models/graph/enum_graph_operation_type.py +20 -0
- omnibase_infra/handlers/models/graph/model_graph_execute_payload.py +38 -0
- omnibase_infra/handlers/models/graph/model_graph_handler_config.py +54 -0
- omnibase_infra/handlers/models/graph/model_graph_handler_payload.py +44 -0
- omnibase_infra/handlers/models/graph/model_graph_query_payload.py +40 -0
- omnibase_infra/handlers/models/graph/model_graph_record.py +22 -0
- omnibase_infra/handlers/models/http/__init__.py +50 -0
- omnibase_infra/handlers/models/http/enum_http_operation_type.py +29 -0
- omnibase_infra/handlers/models/http/model_http_body_content.py +45 -0
- omnibase_infra/handlers/models/http/model_http_get_payload.py +88 -0
- omnibase_infra/handlers/models/http/model_http_handler_payload.py +90 -0
- omnibase_infra/handlers/models/http/model_http_post_payload.py +88 -0
- omnibase_infra/handlers/models/http/model_payload_http.py +66 -0
- omnibase_infra/handlers/models/http/registry_payload_http.py +212 -0
- omnibase_infra/handlers/models/mcp/__init__.py +23 -0
- omnibase_infra/handlers/models/mcp/enum_mcp_operation_type.py +24 -0
- omnibase_infra/handlers/models/mcp/model_mcp_handler_config.py +40 -0
- omnibase_infra/handlers/models/mcp/model_mcp_tool_call.py +32 -0
- omnibase_infra/handlers/models/mcp/model_mcp_tool_result.py +45 -0
- omnibase_infra/handlers/models/model_consul_handler_response.py +96 -0
- omnibase_infra/handlers/models/model_db_describe_response.py +83 -0
- omnibase_infra/handlers/models/model_db_query_payload.py +95 -0
- omnibase_infra/handlers/models/model_db_query_response.py +60 -0
- omnibase_infra/handlers/models/model_filesystem_config.py +98 -0
- omnibase_infra/handlers/models/model_filesystem_delete_payload.py +54 -0
- omnibase_infra/handlers/models/model_filesystem_delete_result.py +77 -0
- omnibase_infra/handlers/models/model_filesystem_directory_entry.py +75 -0
- omnibase_infra/handlers/models/model_filesystem_ensure_directory_payload.py +54 -0
- omnibase_infra/handlers/models/model_filesystem_ensure_directory_result.py +60 -0
- omnibase_infra/handlers/models/model_filesystem_list_directory_payload.py +60 -0
- omnibase_infra/handlers/models/model_filesystem_list_directory_result.py +68 -0
- omnibase_infra/handlers/models/model_filesystem_read_payload.py +62 -0
- omnibase_infra/handlers/models/model_filesystem_read_result.py +61 -0
- omnibase_infra/handlers/models/model_filesystem_write_payload.py +70 -0
- omnibase_infra/handlers/models/model_filesystem_write_result.py +55 -0
- omnibase_infra/handlers/models/model_graph_handler_response.py +98 -0
- omnibase_infra/handlers/models/model_handler_response.py +103 -0
- omnibase_infra/handlers/models/model_http_handler_response.py +101 -0
- omnibase_infra/handlers/models/model_manifest_metadata.py +75 -0
- omnibase_infra/handlers/models/model_manifest_persistence_config.py +62 -0
- omnibase_infra/handlers/models/model_manifest_query_payload.py +90 -0
- omnibase_infra/handlers/models/model_manifest_query_result.py +97 -0
- omnibase_infra/handlers/models/model_manifest_retrieve_payload.py +44 -0
- omnibase_infra/handlers/models/model_manifest_retrieve_result.py +98 -0
- omnibase_infra/handlers/models/model_manifest_store_payload.py +47 -0
- omnibase_infra/handlers/models/model_manifest_store_result.py +67 -0
- omnibase_infra/handlers/models/model_operation_context.py +187 -0
- omnibase_infra/handlers/models/model_qdrant_handler_response.py +98 -0
- omnibase_infra/handlers/models/model_retry_state.py +162 -0
- omnibase_infra/handlers/models/model_vault_handler_response.py +98 -0
- omnibase_infra/handlers/models/qdrant/__init__.py +44 -0
- omnibase_infra/handlers/models/qdrant/enum_qdrant_operation_type.py +26 -0
- omnibase_infra/handlers/models/qdrant/model_qdrant_collection_payload.py +42 -0
- omnibase_infra/handlers/models/qdrant/model_qdrant_delete_payload.py +36 -0
- omnibase_infra/handlers/models/qdrant/model_qdrant_handler_config.py +42 -0
- omnibase_infra/handlers/models/qdrant/model_qdrant_handler_payload.py +54 -0
- omnibase_infra/handlers/models/qdrant/model_qdrant_search_payload.py +42 -0
- omnibase_infra/handlers/models/qdrant/model_qdrant_search_result.py +30 -0
- omnibase_infra/handlers/models/qdrant/model_qdrant_upsert_payload.py +36 -0
- omnibase_infra/handlers/models/vault/__init__.py +69 -0
- omnibase_infra/handlers/models/vault/enum_vault_operation_type.py +35 -0
- omnibase_infra/handlers/models/vault/model_payload_vault.py +66 -0
- omnibase_infra/handlers/models/vault/model_vault_delete_payload.py +57 -0
- omnibase_infra/handlers/models/vault/model_vault_handler_config.py +148 -0
- omnibase_infra/handlers/models/vault/model_vault_handler_payload.py +101 -0
- omnibase_infra/handlers/models/vault/model_vault_list_payload.py +58 -0
- omnibase_infra/handlers/models/vault/model_vault_renew_token_payload.py +67 -0
- omnibase_infra/handlers/models/vault/model_vault_retry_config.py +66 -0
- omnibase_infra/handlers/models/vault/model_vault_secret_payload.py +106 -0
- omnibase_infra/handlers/models/vault/model_vault_write_payload.py +66 -0
- omnibase_infra/handlers/models/vault/registry_payload_vault.py +213 -0
- omnibase_infra/handlers/registration_storage/__init__.py +43 -0
- omnibase_infra/handlers/registration_storage/handler_registration_storage_mock.py +392 -0
- omnibase_infra/handlers/registration_storage/handler_registration_storage_postgres.py +922 -0
- omnibase_infra/handlers/registration_storage/models/__init__.py +23 -0
- omnibase_infra/handlers/registration_storage/models/model_delete_registration_request.py +58 -0
- omnibase_infra/handlers/registration_storage/models/model_update_registration_request.py +73 -0
- omnibase_infra/handlers/registration_storage/protocol_registration_persistence.py +191 -0
- omnibase_infra/handlers/service_discovery/__init__.py +43 -0
- omnibase_infra/handlers/service_discovery/handler_service_discovery_consul.py +1051 -0
- omnibase_infra/handlers/service_discovery/handler_service_discovery_mock.py +258 -0
- omnibase_infra/handlers/service_discovery/models/__init__.py +22 -0
- omnibase_infra/handlers/service_discovery/models/model_discovery_result.py +64 -0
- omnibase_infra/handlers/service_discovery/models/model_registration_result.py +138 -0
- omnibase_infra/handlers/service_discovery/models/model_service_info.py +109 -0
- omnibase_infra/handlers/service_discovery/protocol_discovery_operations.py +170 -0
- omnibase_infra/idempotency/__init__.py +94 -0
- omnibase_infra/idempotency/models/__init__.py +43 -0
- omnibase_infra/idempotency/models/model_idempotency_check_result.py +85 -0
- omnibase_infra/idempotency/models/model_idempotency_guard_config.py +130 -0
- omnibase_infra/idempotency/models/model_idempotency_record.py +86 -0
- omnibase_infra/idempotency/models/model_idempotency_store_health_check_result.py +81 -0
- omnibase_infra/idempotency/models/model_idempotency_store_metrics.py +140 -0
- omnibase_infra/idempotency/models/model_postgres_idempotency_store_config.py +299 -0
- omnibase_infra/idempotency/protocol_idempotency_store.py +184 -0
- omnibase_infra/idempotency/store_inmemory.py +265 -0
- omnibase_infra/idempotency/store_postgres.py +923 -0
- omnibase_infra/infrastructure/__init__.py +0 -0
- omnibase_infra/migrations/001_create_event_ledger.sql +166 -0
- omnibase_infra/migrations/001_drop_event_ledger.sql +18 -0
- omnibase_infra/mixins/__init__.py +71 -0
- omnibase_infra/mixins/mixin_async_circuit_breaker.py +656 -0
- omnibase_infra/mixins/mixin_dict_like_accessors.py +146 -0
- omnibase_infra/mixins/mixin_envelope_extraction.py +119 -0
- omnibase_infra/mixins/mixin_node_introspection.py +2670 -0
- omnibase_infra/mixins/mixin_retry_execution.py +386 -0
- omnibase_infra/mixins/protocol_circuit_breaker_aware.py +133 -0
- omnibase_infra/models/__init__.py +144 -0
- omnibase_infra/models/bindings/__init__.py +59 -0
- omnibase_infra/models/bindings/constants.py +144 -0
- omnibase_infra/models/bindings/model_binding_resolution_result.py +103 -0
- omnibase_infra/models/bindings/model_operation_binding.py +44 -0
- omnibase_infra/models/bindings/model_operation_bindings_subcontract.py +152 -0
- omnibase_infra/models/bindings/model_parsed_binding.py +52 -0
- omnibase_infra/models/corpus/__init__.py +17 -0
- omnibase_infra/models/corpus/model_capture_config.py +133 -0
- omnibase_infra/models/corpus/model_capture_result.py +86 -0
- omnibase_infra/models/discovery/__init__.py +42 -0
- omnibase_infra/models/discovery/model_dependency_spec.py +319 -0
- omnibase_infra/models/discovery/model_discovered_capabilities.py +50 -0
- omnibase_infra/models/discovery/model_introspection_config.py +330 -0
- omnibase_infra/models/discovery/model_introspection_performance_metrics.py +169 -0
- omnibase_infra/models/discovery/model_introspection_task_config.py +116 -0
- omnibase_infra/models/dispatch/__init__.py +155 -0
- omnibase_infra/models/dispatch/model_debug_trace_snapshot.py +114 -0
- omnibase_infra/models/dispatch/model_dispatch_context.py +439 -0
- omnibase_infra/models/dispatch/model_dispatch_error.py +336 -0
- omnibase_infra/models/dispatch/model_dispatch_log_context.py +400 -0
- omnibase_infra/models/dispatch/model_dispatch_metadata.py +228 -0
- omnibase_infra/models/dispatch/model_dispatch_metrics.py +496 -0
- omnibase_infra/models/dispatch/model_dispatch_outcome.py +317 -0
- omnibase_infra/models/dispatch/model_dispatch_outputs.py +231 -0
- omnibase_infra/models/dispatch/model_dispatch_result.py +436 -0
- omnibase_infra/models/dispatch/model_dispatch_route.py +279 -0
- omnibase_infra/models/dispatch/model_dispatcher_metrics.py +275 -0
- omnibase_infra/models/dispatch/model_dispatcher_registration.py +352 -0
- omnibase_infra/models/dispatch/model_materialized_dispatch.py +141 -0
- omnibase_infra/models/dispatch/model_parsed_topic.py +135 -0
- omnibase_infra/models/dispatch/model_topic_parser.py +725 -0
- omnibase_infra/models/dispatch/model_tracing_context.py +285 -0
- omnibase_infra/models/errors/__init__.py +45 -0
- omnibase_infra/models/errors/model_handler_validation_error.py +594 -0
- omnibase_infra/models/errors/model_infra_error_context.py +99 -0
- omnibase_infra/models/errors/model_message_type_registry_error_context.py +71 -0
- omnibase_infra/models/errors/model_timeout_error_context.py +110 -0
- omnibase_infra/models/handlers/__init__.py +80 -0
- omnibase_infra/models/handlers/model_bootstrap_handler_descriptor.py +162 -0
- omnibase_infra/models/handlers/model_contract_discovery_result.py +82 -0
- omnibase_infra/models/handlers/model_handler_descriptor.py +200 -0
- omnibase_infra/models/handlers/model_handler_identifier.py +215 -0
- omnibase_infra/models/handlers/model_handler_source_config.py +220 -0
- omnibase_infra/models/health/__init__.py +9 -0
- omnibase_infra/models/health/model_health_check_result.py +40 -0
- omnibase_infra/models/lifecycle/__init__.py +39 -0
- omnibase_infra/models/logging/__init__.py +51 -0
- omnibase_infra/models/logging/model_log_context.py +756 -0
- omnibase_infra/models/mcp/__init__.py +15 -0
- omnibase_infra/models/mcp/model_mcp_contract_config.py +80 -0
- omnibase_infra/models/mcp/model_mcp_server_config.py +67 -0
- omnibase_infra/models/mcp/model_mcp_tool_definition.py +73 -0
- omnibase_infra/models/mcp/model_mcp_tool_parameter.py +35 -0
- omnibase_infra/models/model_node_identity.py +126 -0
- omnibase_infra/models/model_retry_error_classification.py +78 -0
- omnibase_infra/models/projection/__init__.py +43 -0
- omnibase_infra/models/projection/model_capability_fields.py +112 -0
- omnibase_infra/models/projection/model_registration_projection.py +434 -0
- omnibase_infra/models/projection/model_registration_snapshot.py +322 -0
- omnibase_infra/models/projection/model_sequence_info.py +182 -0
- omnibase_infra/models/projection/model_snapshot_topic_config.py +591 -0
- omnibase_infra/models/projectors/__init__.py +41 -0
- omnibase_infra/models/projectors/model_projector_column.py +289 -0
- omnibase_infra/models/projectors/model_projector_discovery_result.py +65 -0
- omnibase_infra/models/projectors/model_projector_index.py +270 -0
- omnibase_infra/models/projectors/model_projector_schema.py +415 -0
- omnibase_infra/models/projectors/model_projector_validation_error.py +63 -0
- omnibase_infra/models/projectors/util_sql_identifiers.py +115 -0
- omnibase_infra/models/registration/__init__.py +68 -0
- omnibase_infra/models/registration/commands/__init__.py +15 -0
- omnibase_infra/models/registration/commands/model_node_registration_acked.py +108 -0
- omnibase_infra/models/registration/events/__init__.py +56 -0
- omnibase_infra/models/registration/events/model_node_became_active.py +103 -0
- omnibase_infra/models/registration/events/model_node_liveness_expired.py +103 -0
- omnibase_infra/models/registration/events/model_node_registration_accepted.py +98 -0
- omnibase_infra/models/registration/events/model_node_registration_ack_received.py +98 -0
- omnibase_infra/models/registration/events/model_node_registration_ack_timed_out.py +112 -0
- omnibase_infra/models/registration/events/model_node_registration_initiated.py +107 -0
- omnibase_infra/models/registration/events/model_node_registration_rejected.py +104 -0
- omnibase_infra/models/registration/model_event_bus_topic_entry.py +59 -0
- omnibase_infra/models/registration/model_introspection_metrics.py +253 -0
- omnibase_infra/models/registration/model_node_capabilities.py +190 -0
- omnibase_infra/models/registration/model_node_event_bus_config.py +99 -0
- omnibase_infra/models/registration/model_node_heartbeat_event.py +126 -0
- omnibase_infra/models/registration/model_node_introspection_event.py +195 -0
- omnibase_infra/models/registration/model_node_metadata.py +79 -0
- omnibase_infra/models/registration/model_node_registration.py +162 -0
- omnibase_infra/models/registration/model_node_registration_record.py +162 -0
- omnibase_infra/models/registry/__init__.py +29 -0
- omnibase_infra/models/registry/model_domain_constraint.py +202 -0
- omnibase_infra/models/registry/model_message_type_entry.py +271 -0
- omnibase_infra/models/resilience/__init__.py +9 -0
- omnibase_infra/models/resilience/model_circuit_breaker_config.py +227 -0
- omnibase_infra/models/routing/__init__.py +25 -0
- omnibase_infra/models/routing/model_routing_entry.py +52 -0
- omnibase_infra/models/routing/model_routing_subcontract.py +70 -0
- omnibase_infra/models/runtime/__init__.py +49 -0
- omnibase_infra/models/runtime/model_contract_security_config.py +41 -0
- omnibase_infra/models/runtime/model_discovery_error.py +81 -0
- omnibase_infra/models/runtime/model_discovery_result.py +162 -0
- omnibase_infra/models/runtime/model_discovery_warning.py +74 -0
- omnibase_infra/models/runtime/model_failed_plugin_load.py +63 -0
- omnibase_infra/models/runtime/model_handler_contract.py +296 -0
- omnibase_infra/models/runtime/model_loaded_handler.py +129 -0
- omnibase_infra/models/runtime/model_plugin_load_context.py +93 -0
- omnibase_infra/models/runtime/model_plugin_load_summary.py +124 -0
- omnibase_infra/models/security/__init__.py +50 -0
- omnibase_infra/models/security/classification_levels.py +99 -0
- omnibase_infra/models/security/model_environment_policy.py +145 -0
- omnibase_infra/models/security/model_handler_security_policy.py +107 -0
- omnibase_infra/models/security/model_security_error.py +81 -0
- omnibase_infra/models/security/model_security_validation_result.py +328 -0
- omnibase_infra/models/security/model_security_warning.py +67 -0
- omnibase_infra/models/snapshot/__init__.py +27 -0
- omnibase_infra/models/snapshot/model_field_change.py +65 -0
- omnibase_infra/models/snapshot/model_snapshot.py +270 -0
- omnibase_infra/models/snapshot/model_snapshot_diff.py +203 -0
- omnibase_infra/models/snapshot/model_subject_ref.py +81 -0
- omnibase_infra/models/types/__init__.py +71 -0
- omnibase_infra/models/validation/__init__.py +89 -0
- omnibase_infra/models/validation/model_any_type_validation_result.py +118 -0
- omnibase_infra/models/validation/model_any_type_violation.py +141 -0
- omnibase_infra/models/validation/model_category_match_result.py +345 -0
- omnibase_infra/models/validation/model_chain_violation.py +166 -0
- omnibase_infra/models/validation/model_coverage_metrics.py +316 -0
- omnibase_infra/models/validation/model_execution_shape_rule.py +159 -0
- omnibase_infra/models/validation/model_execution_shape_validation.py +208 -0
- omnibase_infra/models/validation/model_execution_shape_validation_result.py +294 -0
- omnibase_infra/models/validation/model_execution_shape_violation.py +122 -0
- omnibase_infra/models/validation/model_localhandler_validation_result.py +139 -0
- omnibase_infra/models/validation/model_localhandler_violation.py +100 -0
- omnibase_infra/models/validation/model_output_validation_params.py +74 -0
- omnibase_infra/models/validation/model_validate_and_raise_params.py +84 -0
- omnibase_infra/models/validation/model_validation_error_params.py +84 -0
- omnibase_infra/models/validation/model_validation_outcome.py +287 -0
- omnibase_infra/nodes/__init__.py +57 -0
- omnibase_infra/nodes/architecture_validator/__init__.py +79 -0
- omnibase_infra/nodes/architecture_validator/contract.yaml +252 -0
- omnibase_infra/nodes/architecture_validator/contract_architecture_validator.yaml +203 -0
- omnibase_infra/nodes/architecture_validator/mixins/__init__.py +16 -0
- omnibase_infra/nodes/architecture_validator/mixins/mixin_file_path_rule.py +92 -0
- omnibase_infra/nodes/architecture_validator/models/__init__.py +36 -0
- omnibase_infra/nodes/architecture_validator/models/model_architecture_validation_request.py +56 -0
- omnibase_infra/nodes/architecture_validator/models/model_architecture_validation_result.py +311 -0
- omnibase_infra/nodes/architecture_validator/models/model_architecture_violation.py +163 -0
- omnibase_infra/nodes/architecture_validator/models/model_rule_check_result.py +265 -0
- omnibase_infra/nodes/architecture_validator/models/model_validation_request.py +105 -0
- omnibase_infra/nodes/architecture_validator/models/model_validation_result.py +314 -0
- omnibase_infra/nodes/architecture_validator/node.py +262 -0
- omnibase_infra/nodes/architecture_validator/node_architecture_validator.py +383 -0
- omnibase_infra/nodes/architecture_validator/protocols/__init__.py +9 -0
- omnibase_infra/nodes/architecture_validator/protocols/protocol_architecture_rule.py +225 -0
- omnibase_infra/nodes/architecture_validator/registry/__init__.py +28 -0
- omnibase_infra/nodes/architecture_validator/registry/registry_infra_architecture_validator.py +106 -0
- omnibase_infra/nodes/architecture_validator/validators/__init__.py +104 -0
- omnibase_infra/nodes/architecture_validator/validators/validator_no_direct_dispatch.py +422 -0
- omnibase_infra/nodes/architecture_validator/validators/validator_no_handler_publishing.py +481 -0
- omnibase_infra/nodes/architecture_validator/validators/validator_no_orchestrator_fsm.py +491 -0
- omnibase_infra/nodes/contract_registry_reducer/__init__.py +29 -0
- omnibase_infra/nodes/contract_registry_reducer/contract.yaml +255 -0
- omnibase_infra/nodes/contract_registry_reducer/models/__init__.py +38 -0
- omnibase_infra/nodes/contract_registry_reducer/models/model_contract_registry_state.py +266 -0
- omnibase_infra/nodes/contract_registry_reducer/models/model_payload_cleanup_topic_references.py +55 -0
- omnibase_infra/nodes/contract_registry_reducer/models/model_payload_deactivate_contract.py +58 -0
- omnibase_infra/nodes/contract_registry_reducer/models/model_payload_mark_stale.py +49 -0
- omnibase_infra/nodes/contract_registry_reducer/models/model_payload_update_heartbeat.py +71 -0
- omnibase_infra/nodes/contract_registry_reducer/models/model_payload_update_topic.py +66 -0
- omnibase_infra/nodes/contract_registry_reducer/models/model_payload_upsert_contract.py +92 -0
- omnibase_infra/nodes/contract_registry_reducer/node.py +121 -0
- omnibase_infra/nodes/contract_registry_reducer/reducer.py +784 -0
- omnibase_infra/nodes/contract_registry_reducer/registry/__init__.py +9 -0
- omnibase_infra/nodes/contract_registry_reducer/registry/registry_infra_contract_registry_reducer.py +101 -0
- omnibase_infra/nodes/effects/README.md +358 -0
- omnibase_infra/nodes/effects/__init__.py +26 -0
- omnibase_infra/nodes/effects/contract.yaml +167 -0
- omnibase_infra/nodes/effects/models/__init__.py +32 -0
- omnibase_infra/nodes/effects/models/model_backend_result.py +190 -0
- omnibase_infra/nodes/effects/models/model_effect_idempotency_config.py +92 -0
- omnibase_infra/nodes/effects/models/model_registry_request.py +132 -0
- omnibase_infra/nodes/effects/models/model_registry_response.py +263 -0
- omnibase_infra/nodes/effects/protocol_consul_client.py +89 -0
- omnibase_infra/nodes/effects/protocol_effect_idempotency_store.py +143 -0
- omnibase_infra/nodes/effects/protocol_postgres_adapter.py +96 -0
- omnibase_infra/nodes/effects/registry_effect.py +525 -0
- omnibase_infra/nodes/effects/store_effect_idempotency_inmemory.py +425 -0
- omnibase_infra/nodes/handlers/consul/contract.yaml +85 -0
- omnibase_infra/nodes/handlers/db/contract.yaml +72 -0
- omnibase_infra/nodes/handlers/graph/contract.yaml +127 -0
- omnibase_infra/nodes/handlers/http/contract.yaml +74 -0
- omnibase_infra/nodes/handlers/intent/contract.yaml +66 -0
- omnibase_infra/nodes/handlers/mcp/contract.yaml +69 -0
- omnibase_infra/nodes/handlers/vault/contract.yaml +91 -0
- omnibase_infra/nodes/node_intent_storage_effect/__init__.py +50 -0
- omnibase_infra/nodes/node_intent_storage_effect/contract.yaml +194 -0
- omnibase_infra/nodes/node_intent_storage_effect/models/__init__.py +24 -0
- omnibase_infra/nodes/node_intent_storage_effect/models/model_intent_storage_input.py +141 -0
- omnibase_infra/nodes/node_intent_storage_effect/models/model_intent_storage_output.py +130 -0
- omnibase_infra/nodes/node_intent_storage_effect/node.py +94 -0
- omnibase_infra/nodes/node_intent_storage_effect/registry/__init__.py +35 -0
- omnibase_infra/nodes/node_intent_storage_effect/registry/registry_infra_intent_storage.py +294 -0
- omnibase_infra/nodes/node_ledger_projection_compute/__init__.py +50 -0
- omnibase_infra/nodes/node_ledger_projection_compute/contract.yaml +104 -0
- omnibase_infra/nodes/node_ledger_projection_compute/node.py +284 -0
- omnibase_infra/nodes/node_ledger_projection_compute/registry/__init__.py +29 -0
- omnibase_infra/nodes/node_ledger_projection_compute/registry/registry_infra_ledger_projection.py +118 -0
- omnibase_infra/nodes/node_ledger_write_effect/__init__.py +82 -0
- omnibase_infra/nodes/node_ledger_write_effect/contract.yaml +200 -0
- omnibase_infra/nodes/node_ledger_write_effect/handlers/__init__.py +22 -0
- omnibase_infra/nodes/node_ledger_write_effect/handlers/handler_ledger_append.py +372 -0
- omnibase_infra/nodes/node_ledger_write_effect/handlers/handler_ledger_query.py +597 -0
- omnibase_infra/nodes/node_ledger_write_effect/models/__init__.py +31 -0
- omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_append_result.py +54 -0
- omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_entry.py +92 -0
- omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_query.py +53 -0
- omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_query_result.py +41 -0
- omnibase_infra/nodes/node_ledger_write_effect/node.py +89 -0
- omnibase_infra/nodes/node_ledger_write_effect/protocols/__init__.py +13 -0
- omnibase_infra/nodes/node_ledger_write_effect/protocols/protocol_ledger_persistence.py +127 -0
- omnibase_infra/nodes/node_ledger_write_effect/registry/__init__.py +9 -0
- omnibase_infra/nodes/node_ledger_write_effect/registry/registry_infra_ledger_write.py +121 -0
- omnibase_infra/nodes/node_registration_orchestrator/README.md +542 -0
- omnibase_infra/nodes/node_registration_orchestrator/__init__.py +120 -0
- omnibase_infra/nodes/node_registration_orchestrator/contract.yaml +482 -0
- omnibase_infra/nodes/node_registration_orchestrator/dispatchers/__init__.py +53 -0
- omnibase_infra/nodes/node_registration_orchestrator/dispatchers/dispatcher_node_introspected.py +376 -0
- omnibase_infra/nodes/node_registration_orchestrator/dispatchers/dispatcher_node_registration_acked.py +376 -0
- omnibase_infra/nodes/node_registration_orchestrator/dispatchers/dispatcher_runtime_tick.py +373 -0
- omnibase_infra/nodes/node_registration_orchestrator/handlers/__init__.py +62 -0
- omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_node_heartbeat.py +376 -0
- omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_node_introspected.py +694 -0
- omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_node_registration_acked.py +458 -0
- omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_runtime_tick.py +364 -0
- omnibase_infra/nodes/node_registration_orchestrator/introspection_event_router.py +544 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/__init__.py +75 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_consul_intent_payload.py +194 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_consul_registration_intent.py +67 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_intent_execution_result.py +50 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_node_liveness_expired.py +107 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_orchestrator_config.py +67 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_orchestrator_input.py +41 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_orchestrator_output.py +166 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_postgres_intent_payload.py +235 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_postgres_upsert_intent.py +68 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_reducer_execution_result.py +384 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_reducer_state.py +60 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_registration_intent.py +177 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_registry_intent.py +247 -0
- omnibase_infra/nodes/node_registration_orchestrator/node.py +195 -0
- omnibase_infra/nodes/node_registration_orchestrator/plugin.py +909 -0
- omnibase_infra/nodes/node_registration_orchestrator/protocols.py +439 -0
- omnibase_infra/nodes/node_registration_orchestrator/registry/__init__.py +41 -0
- omnibase_infra/nodes/node_registration_orchestrator/registry/registry_infra_node_registration_orchestrator.py +528 -0
- omnibase_infra/nodes/node_registration_orchestrator/timeout_coordinator.py +393 -0
- omnibase_infra/nodes/node_registration_orchestrator/wiring.py +743 -0
- omnibase_infra/nodes/node_registration_reducer/__init__.py +15 -0
- omnibase_infra/nodes/node_registration_reducer/contract.yaml +301 -0
- omnibase_infra/nodes/node_registration_reducer/models/__init__.py +38 -0
- omnibase_infra/nodes/node_registration_reducer/models/model_validation_result.py +113 -0
- omnibase_infra/nodes/node_registration_reducer/node.py +139 -0
- omnibase_infra/nodes/node_registration_reducer/registry/__init__.py +9 -0
- omnibase_infra/nodes/node_registration_reducer/registry/registry_infra_node_registration_reducer.py +79 -0
- omnibase_infra/nodes/node_registration_storage_effect/__init__.py +41 -0
- omnibase_infra/nodes/node_registration_storage_effect/contract.yaml +220 -0
- omnibase_infra/nodes/node_registration_storage_effect/models/__init__.py +44 -0
- omnibase_infra/nodes/node_registration_storage_effect/models/model_delete_result.py +132 -0
- omnibase_infra/nodes/node_registration_storage_effect/models/model_registration_record.py +199 -0
- omnibase_infra/nodes/node_registration_storage_effect/models/model_registration_update.py +155 -0
- omnibase_infra/nodes/node_registration_storage_effect/models/model_storage_health_check_details.py +123 -0
- omnibase_infra/nodes/node_registration_storage_effect/models/model_storage_health_check_result.py +117 -0
- omnibase_infra/nodes/node_registration_storage_effect/models/model_storage_query.py +100 -0
- omnibase_infra/nodes/node_registration_storage_effect/models/model_storage_result.py +136 -0
- omnibase_infra/nodes/node_registration_storage_effect/models/model_upsert_result.py +127 -0
- omnibase_infra/nodes/node_registration_storage_effect/node.py +112 -0
- omnibase_infra/nodes/node_registration_storage_effect/protocols/__init__.py +22 -0
- omnibase_infra/nodes/node_registration_storage_effect/protocols/protocol_registration_persistence.py +333 -0
- omnibase_infra/nodes/node_registration_storage_effect/registry/__init__.py +23 -0
- omnibase_infra/nodes/node_registration_storage_effect/registry/registry_infra_registration_storage.py +215 -0
- omnibase_infra/nodes/node_registry_effect/__init__.py +85 -0
- omnibase_infra/nodes/node_registry_effect/contract.yaml +677 -0
- omnibase_infra/nodes/node_registry_effect/handlers/__init__.py +70 -0
- omnibase_infra/nodes/node_registry_effect/handlers/handler_consul_deregister.py +211 -0
- omnibase_infra/nodes/node_registry_effect/handlers/handler_consul_register.py +212 -0
- omnibase_infra/nodes/node_registry_effect/handlers/handler_partial_retry.py +417 -0
- omnibase_infra/nodes/node_registry_effect/handlers/handler_postgres_deactivate.py +215 -0
- omnibase_infra/nodes/node_registry_effect/handlers/handler_postgres_upsert.py +208 -0
- omnibase_infra/nodes/node_registry_effect/models/__init__.py +43 -0
- omnibase_infra/nodes/node_registry_effect/models/model_partial_retry_request.py +92 -0
- omnibase_infra/nodes/node_registry_effect/node.py +165 -0
- omnibase_infra/nodes/node_registry_effect/registry/__init__.py +27 -0
- omnibase_infra/nodes/node_registry_effect/registry/registry_infra_registry_effect.py +196 -0
- omnibase_infra/nodes/node_service_discovery_effect/__init__.py +111 -0
- omnibase_infra/nodes/node_service_discovery_effect/contract.yaml +246 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/__init__.py +67 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/enum_health_status.py +72 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/enum_service_discovery_operation.py +58 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/model_discovery_query.py +99 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/model_discovery_result.py +98 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/model_health_check_config.py +121 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/model_query_metadata.py +63 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/model_registration_result.py +130 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/model_service_discovery_health_check_details.py +111 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/model_service_discovery_health_check_result.py +119 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/model_service_info.py +106 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/model_service_registration.py +121 -0
- omnibase_infra/nodes/node_service_discovery_effect/node.py +111 -0
- omnibase_infra/nodes/node_service_discovery_effect/protocols/__init__.py +14 -0
- omnibase_infra/nodes/node_service_discovery_effect/protocols/protocol_discovery_operations.py +279 -0
- omnibase_infra/nodes/node_service_discovery_effect/registry/__init__.py +13 -0
- omnibase_infra/nodes/node_service_discovery_effect/registry/registry_infra_service_discovery.py +222 -0
- omnibase_infra/nodes/reducers/__init__.py +30 -0
- omnibase_infra/nodes/reducers/models/__init__.py +37 -0
- omnibase_infra/nodes/reducers/models/model_payload_consul_register.py +87 -0
- omnibase_infra/nodes/reducers/models/model_payload_ledger_append.py +133 -0
- omnibase_infra/nodes/reducers/models/model_payload_postgres_upsert_registration.py +60 -0
- omnibase_infra/nodes/reducers/models/model_registration_confirmation.py +166 -0
- omnibase_infra/nodes/reducers/models/model_registration_state.py +433 -0
- omnibase_infra/nodes/reducers/registration_reducer.py +1138 -0
- omnibase_infra/observability/__init__.py +143 -0
- omnibase_infra/observability/constants_metrics.py +91 -0
- omnibase_infra/observability/factory_observability_sink.py +525 -0
- omnibase_infra/observability/handlers/__init__.py +118 -0
- omnibase_infra/observability/handlers/handler_logging_structured.py +967 -0
- omnibase_infra/observability/handlers/handler_metrics_prometheus.py +1120 -0
- omnibase_infra/observability/handlers/model_logging_handler_config.py +71 -0
- omnibase_infra/observability/handlers/model_logging_handler_response.py +77 -0
- omnibase_infra/observability/handlers/model_metrics_handler_config.py +172 -0
- omnibase_infra/observability/handlers/model_metrics_handler_payload.py +135 -0
- omnibase_infra/observability/handlers/model_metrics_handler_response.py +101 -0
- omnibase_infra/observability/hooks/__init__.py +74 -0
- omnibase_infra/observability/hooks/hook_observability.py +1223 -0
- omnibase_infra/observability/models/__init__.py +30 -0
- omnibase_infra/observability/models/enum_required_log_context_key.py +77 -0
- omnibase_infra/observability/models/model_buffered_log_entry.py +117 -0
- omnibase_infra/observability/models/model_logging_sink_config.py +73 -0
- omnibase_infra/observability/models/model_metrics_sink_config.py +156 -0
- omnibase_infra/observability/sinks/__init__.py +69 -0
- omnibase_infra/observability/sinks/sink_logging_structured.py +809 -0
- omnibase_infra/observability/sinks/sink_metrics_prometheus.py +710 -0
- omnibase_infra/plugins/__init__.py +27 -0
- omnibase_infra/plugins/examples/__init__.py +28 -0
- omnibase_infra/plugins/examples/plugin_json_normalizer.py +271 -0
- omnibase_infra/plugins/examples/plugin_json_normalizer_error_handling.py +210 -0
- omnibase_infra/plugins/models/__init__.py +21 -0
- omnibase_infra/plugins/models/model_plugin_context.py +76 -0
- omnibase_infra/plugins/models/model_plugin_input_data.py +58 -0
- omnibase_infra/plugins/models/model_plugin_output_data.py +62 -0
- omnibase_infra/plugins/plugin_compute_base.py +449 -0
- omnibase_infra/projectors/__init__.py +30 -0
- omnibase_infra/projectors/contracts/__init__.py +63 -0
- omnibase_infra/projectors/contracts/registration_projector.yaml +370 -0
- omnibase_infra/projectors/projection_reader_registration.py +1559 -0
- omnibase_infra/projectors/snapshot_publisher_registration.py +1329 -0
- omnibase_infra/protocols/__init__.py +104 -0
- omnibase_infra/protocols/protocol_capability_projection.py +253 -0
- omnibase_infra/protocols/protocol_capability_query.py +251 -0
- omnibase_infra/protocols/protocol_container_aware.py +200 -0
- omnibase_infra/protocols/protocol_dispatch_engine.py +152 -0
- omnibase_infra/protocols/protocol_event_bus_like.py +127 -0
- omnibase_infra/protocols/protocol_event_projector.py +96 -0
- omnibase_infra/protocols/protocol_idempotency_store.py +142 -0
- omnibase_infra/protocols/protocol_message_dispatcher.py +247 -0
- omnibase_infra/protocols/protocol_message_type_registry.py +306 -0
- omnibase_infra/protocols/protocol_plugin_compute.py +368 -0
- omnibase_infra/protocols/protocol_projector_schema_validator.py +82 -0
- omnibase_infra/protocols/protocol_registry_metrics.py +215 -0
- omnibase_infra/protocols/protocol_snapshot_publisher.py +396 -0
- omnibase_infra/protocols/protocol_snapshot_store.py +567 -0
- omnibase_infra/runtime/__init__.py +445 -0
- omnibase_infra/runtime/binding_config_resolver.py +2771 -0
- omnibase_infra/runtime/binding_resolver.py +753 -0
- omnibase_infra/runtime/chain_aware_dispatch.py +467 -0
- omnibase_infra/runtime/constants_notification.py +75 -0
- omnibase_infra/runtime/constants_security.py +70 -0
- omnibase_infra/runtime/contract_handler_discovery.py +587 -0
- omnibase_infra/runtime/contract_loaders/__init__.py +51 -0
- omnibase_infra/runtime/contract_loaders/handler_routing_loader.py +464 -0
- omnibase_infra/runtime/contract_loaders/operation_bindings_loader.py +789 -0
- omnibase_infra/runtime/dispatch_context_enforcer.py +427 -0
- omnibase_infra/runtime/emit_daemon/__init__.py +97 -0
- omnibase_infra/runtime/emit_daemon/cli.py +844 -0
- omnibase_infra/runtime/emit_daemon/client.py +811 -0
- omnibase_infra/runtime/emit_daemon/config.py +535 -0
- omnibase_infra/runtime/emit_daemon/daemon.py +812 -0
- omnibase_infra/runtime/emit_daemon/event_registry.py +477 -0
- omnibase_infra/runtime/emit_daemon/model_daemon_request.py +139 -0
- omnibase_infra/runtime/emit_daemon/model_daemon_response.py +191 -0
- omnibase_infra/runtime/emit_daemon/queue.py +618 -0
- omnibase_infra/runtime/enums/__init__.py +18 -0
- omnibase_infra/runtime/enums/enum_config_ref_scheme.py +33 -0
- omnibase_infra/runtime/enums/enum_scheduler_status.py +170 -0
- omnibase_infra/runtime/envelope_validator.py +179 -0
- omnibase_infra/runtime/event_bus_subcontract_wiring.py +466 -0
- omnibase_infra/runtime/handler_bootstrap_source.py +507 -0
- omnibase_infra/runtime/handler_contract_config_loader.py +603 -0
- omnibase_infra/runtime/handler_contract_source.py +750 -0
- omnibase_infra/runtime/handler_identity.py +81 -0
- omnibase_infra/runtime/handler_plugin_loader.py +2046 -0
- omnibase_infra/runtime/handler_registry.py +329 -0
- omnibase_infra/runtime/handler_source_resolver.py +367 -0
- omnibase_infra/runtime/invocation_security_enforcer.py +427 -0
- omnibase_infra/runtime/kafka_contract_source.py +984 -0
- omnibase_infra/runtime/kernel.py +40 -0
- omnibase_infra/runtime/mixin_policy_validation.py +522 -0
- omnibase_infra/runtime/mixin_semver_cache.py +402 -0
- omnibase_infra/runtime/mixins/__init__.py +24 -0
- omnibase_infra/runtime/mixins/mixin_projector_notification_publishing.py +566 -0
- omnibase_infra/runtime/mixins/mixin_projector_sql_operations.py +778 -0
- omnibase_infra/runtime/models/__init__.py +229 -0
- omnibase_infra/runtime/models/model_batch_lifecycle_result.py +217 -0
- omnibase_infra/runtime/models/model_binding_config.py +168 -0
- omnibase_infra/runtime/models/model_binding_config_cache_stats.py +135 -0
- omnibase_infra/runtime/models/model_binding_config_resolver_config.py +329 -0
- omnibase_infra/runtime/models/model_cached_secret.py +138 -0
- omnibase_infra/runtime/models/model_compute_key.py +138 -0
- omnibase_infra/runtime/models/model_compute_registration.py +97 -0
- omnibase_infra/runtime/models/model_config_cache_entry.py +61 -0
- omnibase_infra/runtime/models/model_config_ref.py +331 -0
- omnibase_infra/runtime/models/model_config_ref_parse_result.py +125 -0
- omnibase_infra/runtime/models/model_contract_load_result.py +224 -0
- omnibase_infra/runtime/models/model_domain_plugin_config.py +92 -0
- omnibase_infra/runtime/models/model_domain_plugin_result.py +270 -0
- omnibase_infra/runtime/models/model_duplicate_response.py +54 -0
- omnibase_infra/runtime/models/model_enabled_protocols_config.py +61 -0
- omnibase_infra/runtime/models/model_event_bus_config.py +54 -0
- omnibase_infra/runtime/models/model_failed_component.py +55 -0
- omnibase_infra/runtime/models/model_health_check_response.py +168 -0
- omnibase_infra/runtime/models/model_health_check_result.py +229 -0
- omnibase_infra/runtime/models/model_lifecycle_result.py +245 -0
- omnibase_infra/runtime/models/model_logging_config.py +42 -0
- omnibase_infra/runtime/models/model_optional_correlation_id.py +167 -0
- omnibase_infra/runtime/models/model_optional_string.py +94 -0
- omnibase_infra/runtime/models/model_optional_uuid.py +110 -0
- omnibase_infra/runtime/models/model_policy_context.py +100 -0
- omnibase_infra/runtime/models/model_policy_key.py +138 -0
- omnibase_infra/runtime/models/model_policy_registration.py +139 -0
- omnibase_infra/runtime/models/model_policy_result.py +103 -0
- omnibase_infra/runtime/models/model_policy_type_filter.py +157 -0
- omnibase_infra/runtime/models/model_projector_notification_config.py +171 -0
- omnibase_infra/runtime/models/model_projector_plugin_loader_config.py +47 -0
- omnibase_infra/runtime/models/model_protocol_registration_config.py +65 -0
- omnibase_infra/runtime/models/model_retry_policy.py +105 -0
- omnibase_infra/runtime/models/model_runtime_config.py +150 -0
- omnibase_infra/runtime/models/model_runtime_contract_config.py +268 -0
- omnibase_infra/runtime/models/model_runtime_scheduler_config.py +625 -0
- omnibase_infra/runtime/models/model_runtime_scheduler_metrics.py +233 -0
- omnibase_infra/runtime/models/model_runtime_tick.py +193 -0
- omnibase_infra/runtime/models/model_secret_cache_stats.py +82 -0
- omnibase_infra/runtime/models/model_secret_mapping.py +63 -0
- omnibase_infra/runtime/models/model_secret_resolver_config.py +107 -0
- omnibase_infra/runtime/models/model_secret_resolver_metrics.py +111 -0
- omnibase_infra/runtime/models/model_secret_source_info.py +72 -0
- omnibase_infra/runtime/models/model_secret_source_spec.py +66 -0
- omnibase_infra/runtime/models/model_security_config.py +109 -0
- omnibase_infra/runtime/models/model_shutdown_batch_result.py +75 -0
- omnibase_infra/runtime/models/model_shutdown_config.py +94 -0
- omnibase_infra/runtime/models/model_transition_notification_outbox_config.py +112 -0
- omnibase_infra/runtime/models/model_transition_notification_outbox_metrics.py +140 -0
- omnibase_infra/runtime/models/model_transition_notification_publisher_metrics.py +357 -0
- omnibase_infra/runtime/projector_plugin_loader.py +1462 -0
- omnibase_infra/runtime/projector_schema_manager.py +565 -0
- omnibase_infra/runtime/projector_shell.py +1330 -0
- omnibase_infra/runtime/protocol_contract_descriptor.py +92 -0
- omnibase_infra/runtime/protocol_contract_source.py +92 -0
- omnibase_infra/runtime/protocol_domain_plugin.py +474 -0
- omnibase_infra/runtime/protocol_handler_discovery.py +221 -0
- omnibase_infra/runtime/protocol_handler_plugin_loader.py +327 -0
- omnibase_infra/runtime/protocol_lifecycle_executor.py +435 -0
- omnibase_infra/runtime/protocol_policy.py +366 -0
- omnibase_infra/runtime/protocols/__init__.py +37 -0
- omnibase_infra/runtime/protocols/protocol_runtime_scheduler.py +468 -0
- omnibase_infra/runtime/publisher_topic_scoped.py +294 -0
- omnibase_infra/runtime/registry/__init__.py +93 -0
- omnibase_infra/runtime/registry/mixin_message_type_query.py +326 -0
- omnibase_infra/runtime/registry/mixin_message_type_registration.py +354 -0
- omnibase_infra/runtime/registry/registry_event_bus_binding.py +268 -0
- omnibase_infra/runtime/registry/registry_message_type.py +542 -0
- omnibase_infra/runtime/registry/registry_protocol_binding.py +445 -0
- omnibase_infra/runtime/registry_compute.py +1143 -0
- omnibase_infra/runtime/registry_contract_source.py +693 -0
- omnibase_infra/runtime/registry_dispatcher.py +678 -0
- omnibase_infra/runtime/registry_policy.py +1185 -0
- omnibase_infra/runtime/runtime_contract_config_loader.py +406 -0
- omnibase_infra/runtime/runtime_scheduler.py +1070 -0
- omnibase_infra/runtime/secret_resolver.py +2112 -0
- omnibase_infra/runtime/security_metadata_validator.py +776 -0
- omnibase_infra/runtime/service_kernel.py +1651 -0
- omnibase_infra/runtime/service_message_dispatch_engine.py +2350 -0
- omnibase_infra/runtime/service_runtime_host_process.py +3493 -0
- omnibase_infra/runtime/transition_notification_outbox.py +1190 -0
- omnibase_infra/runtime/transition_notification_publisher.py +765 -0
- omnibase_infra/runtime/util_container_wiring.py +1124 -0
- omnibase_infra/runtime/util_validation.py +314 -0
- omnibase_infra/runtime/util_version.py +98 -0
- omnibase_infra/runtime/util_wiring.py +723 -0
- omnibase_infra/schemas/schema_registration_projection.sql +320 -0
- omnibase_infra/schemas/schema_transition_notification_outbox.sql +245 -0
- omnibase_infra/services/__init__.py +89 -0
- omnibase_infra/services/corpus_capture.py +684 -0
- omnibase_infra/services/mcp/__init__.py +31 -0
- omnibase_infra/services/mcp/mcp_server_lifecycle.py +449 -0
- omnibase_infra/services/mcp/service_mcp_tool_discovery.py +411 -0
- omnibase_infra/services/mcp/service_mcp_tool_registry.py +329 -0
- omnibase_infra/services/mcp/service_mcp_tool_sync.py +565 -0
- omnibase_infra/services/registry_api/__init__.py +40 -0
- omnibase_infra/services/registry_api/main.py +261 -0
- omnibase_infra/services/registry_api/models/__init__.py +66 -0
- omnibase_infra/services/registry_api/models/model_capability_widget_mapping.py +38 -0
- omnibase_infra/services/registry_api/models/model_pagination_info.py +48 -0
- omnibase_infra/services/registry_api/models/model_registry_discovery_response.py +73 -0
- omnibase_infra/services/registry_api/models/model_registry_health_response.py +49 -0
- omnibase_infra/services/registry_api/models/model_registry_instance_view.py +88 -0
- omnibase_infra/services/registry_api/models/model_registry_node_view.py +88 -0
- omnibase_infra/services/registry_api/models/model_registry_summary.py +60 -0
- omnibase_infra/services/registry_api/models/model_response_list_instances.py +43 -0
- omnibase_infra/services/registry_api/models/model_response_list_nodes.py +51 -0
- omnibase_infra/services/registry_api/models/model_warning.py +49 -0
- omnibase_infra/services/registry_api/models/model_widget_defaults.py +28 -0
- omnibase_infra/services/registry_api/models/model_widget_mapping.py +51 -0
- omnibase_infra/services/registry_api/routes.py +371 -0
- omnibase_infra/services/registry_api/service.py +837 -0
- omnibase_infra/services/service_capability_query.py +945 -0
- omnibase_infra/services/service_health.py +898 -0
- omnibase_infra/services/service_node_selector.py +530 -0
- omnibase_infra/services/service_timeout_emitter.py +699 -0
- omnibase_infra/services/service_timeout_scanner.py +394 -0
- omnibase_infra/services/session/__init__.py +56 -0
- omnibase_infra/services/session/config_consumer.py +137 -0
- omnibase_infra/services/session/config_store.py +139 -0
- omnibase_infra/services/session/consumer.py +1007 -0
- omnibase_infra/services/session/protocol_session_aggregator.py +117 -0
- omnibase_infra/services/session/store.py +997 -0
- omnibase_infra/services/snapshot/__init__.py +31 -0
- omnibase_infra/services/snapshot/service_snapshot.py +647 -0
- omnibase_infra/services/snapshot/store_inmemory.py +637 -0
- omnibase_infra/services/snapshot/store_postgres.py +1279 -0
- omnibase_infra/shared/__init__.py +8 -0
- omnibase_infra/testing/__init__.py +10 -0
- omnibase_infra/testing/utils.py +23 -0
- omnibase_infra/topics/__init__.py +45 -0
- omnibase_infra/topics/platform_topic_suffixes.py +140 -0
- omnibase_infra/topics/util_topic_composition.py +95 -0
- omnibase_infra/types/__init__.py +48 -0
- omnibase_infra/types/type_cache_info.py +49 -0
- omnibase_infra/types/type_dsn.py +173 -0
- omnibase_infra/types/type_infra_aliases.py +60 -0
- omnibase_infra/types/typed_dict/__init__.py +29 -0
- omnibase_infra/types/typed_dict/typed_dict_envelope_build_params.py +115 -0
- omnibase_infra/types/typed_dict/typed_dict_introspection_cache.py +128 -0
- omnibase_infra/types/typed_dict/typed_dict_performance_metrics_cache.py +140 -0
- omnibase_infra/types/typed_dict_capabilities.py +64 -0
- omnibase_infra/utils/__init__.py +117 -0
- omnibase_infra/utils/correlation.py +208 -0
- omnibase_infra/utils/util_atomic_file.py +261 -0
- omnibase_infra/utils/util_consumer_group.py +232 -0
- omnibase_infra/utils/util_datetime.py +372 -0
- omnibase_infra/utils/util_db_transaction.py +239 -0
- omnibase_infra/utils/util_dsn_validation.py +333 -0
- omnibase_infra/utils/util_env_parsing.py +264 -0
- omnibase_infra/utils/util_error_sanitization.py +457 -0
- omnibase_infra/utils/util_pydantic_validators.py +477 -0
- omnibase_infra/utils/util_retry_optimistic.py +281 -0
- omnibase_infra/utils/util_semver.py +233 -0
- omnibase_infra/validation/__init__.py +307 -0
- omnibase_infra/validation/contracts/security.validation.yaml +114 -0
- omnibase_infra/validation/enums/__init__.py +11 -0
- omnibase_infra/validation/enums/enum_contract_violation_severity.py +13 -0
- omnibase_infra/validation/infra_validators.py +1514 -0
- omnibase_infra/validation/linter_contract.py +907 -0
- omnibase_infra/validation/mixin_any_type_classification.py +120 -0
- omnibase_infra/validation/mixin_any_type_exemption.py +580 -0
- omnibase_infra/validation/mixin_any_type_reporting.py +106 -0
- omnibase_infra/validation/mixin_execution_shape_violation_checks.py +596 -0
- omnibase_infra/validation/mixin_node_archetype_detection.py +254 -0
- omnibase_infra/validation/models/__init__.py +15 -0
- omnibase_infra/validation/models/model_contract_lint_result.py +101 -0
- omnibase_infra/validation/models/model_contract_violation.py +41 -0
- omnibase_infra/validation/service_validation_aggregator.py +395 -0
- omnibase_infra/validation/validation_exemptions.yaml +2033 -0
- omnibase_infra/validation/validator_any_type.py +715 -0
- omnibase_infra/validation/validator_chain_propagation.py +839 -0
- omnibase_infra/validation/validator_execution_shape.py +465 -0
- omnibase_infra/validation/validator_localhandler.py +261 -0
- omnibase_infra/validation/validator_registration_security.py +410 -0
- omnibase_infra/validation/validator_routing_coverage.py +1020 -0
- omnibase_infra/validation/validator_runtime_shape.py +915 -0
- omnibase_infra/validation/validator_security.py +513 -0
- omnibase_infra/validation/validator_topic_category.py +1152 -0
- omnibase_infra-0.2.6.dist-info/METADATA +197 -0
- omnibase_infra-0.2.6.dist-info/RECORD +833 -0
- omnibase_infra-0.2.6.dist-info/WHEEL +4 -0
- omnibase_infra-0.2.6.dist-info/entry_points.txt +5 -0
- omnibase_infra-0.2.6.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,1329 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 OmniNode Team
|
|
3
|
+
"""Snapshot Publisher for Registration Projections.
|
|
4
|
+
|
|
5
|
+
Publishes compacted snapshots to Kafka for read optimization. Snapshots are
|
|
6
|
+
derived from projections and NEVER replace the event log. The event log
|
|
7
|
+
remains the absolute source of truth.
|
|
8
|
+
|
|
9
|
+
Architecture Overview:
|
|
10
|
+
This service implements F2 (Snapshot Publishing) of the ONEX registration
|
|
11
|
+
projection pipeline:
|
|
12
|
+
|
|
13
|
+
1. Projectors (F1) persist projections to PostgreSQL via ProjectorRegistration
|
|
14
|
+
2. Snapshot Publisher (F2) reads projections and publishes compacted snapshots
|
|
15
|
+
3. Consumers read snapshots for fast O(1) state queries
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
Events -> Projector -> PostgreSQL -> Snapshot Publisher -> Kafka (compacted)
|
|
19
|
+
|
|
|
20
|
+
v
|
|
21
|
+
Orchestrators/Readers
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Design Principles:
|
|
25
|
+
- **Read Optimization Only**: Snapshots are for fast reads, not data integrity
|
|
26
|
+
- **Kafka Compaction**: Only latest snapshot per entity_id retained
|
|
27
|
+
- **Tombstone Support**: Null values delete snapshots during compaction
|
|
28
|
+
- **Version Tracking**: Monotonic versions for conflict resolution
|
|
29
|
+
- **Circuit Breaker**: Resilience against Kafka failures
|
|
30
|
+
- **Lazy Consumer**: Consumer for reads is created on-demand
|
|
31
|
+
|
|
32
|
+
Concurrency Safety:
|
|
33
|
+
This implementation is coroutine-safe for concurrent async publishing.
|
|
34
|
+
Uses asyncio locks for circuit breaker state management and
|
|
35
|
+
version tracker synchronization. Note: This is coroutine-safe, not
|
|
36
|
+
thread-safe. For multi-threaded access, additional synchronization
|
|
37
|
+
would be required.
|
|
38
|
+
|
|
39
|
+
Error Handling:
|
|
40
|
+
All methods raise ONEX error types:
|
|
41
|
+
- InfraConnectionError: Kafka unavailable or connection failed
|
|
42
|
+
- InfraTimeoutError: Publish operation timed out
|
|
43
|
+
- InfraUnavailableError: Circuit breaker open
|
|
44
|
+
|
|
45
|
+
Example Usage:
|
|
46
|
+
```python
|
|
47
|
+
from aiokafka import AIOKafkaProducer
|
|
48
|
+
from omnibase_infra.projectors import SnapshotPublisherRegistration
|
|
49
|
+
from omnibase_infra.models.projection import ModelSnapshotTopicConfig
|
|
50
|
+
|
|
51
|
+
# Create producer and config
|
|
52
|
+
producer = AIOKafkaProducer(bootstrap_servers="localhost:9092")
|
|
53
|
+
config = ModelSnapshotTopicConfig.default()
|
|
54
|
+
|
|
55
|
+
# Initialize publisher
|
|
56
|
+
publisher = SnapshotPublisherRegistration(producer, config)
|
|
57
|
+
await publisher.start()
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
# Publish snapshot from projection
|
|
61
|
+
snapshot = await publisher.publish_from_projection(projection)
|
|
62
|
+
print(f"Published snapshot version {snapshot.snapshot_version}")
|
|
63
|
+
|
|
64
|
+
# Or publish pre-built snapshot
|
|
65
|
+
await publisher.publish_snapshot(snapshot)
|
|
66
|
+
|
|
67
|
+
# Batch publish
|
|
68
|
+
count = await publisher.publish_batch(snapshots)
|
|
69
|
+
print(f"Published {count} snapshots")
|
|
70
|
+
|
|
71
|
+
# Read snapshot (uses lazy consumer and in-memory cache)
|
|
72
|
+
snapshot = await publisher.get_latest_snapshot("entity-123", "registration")
|
|
73
|
+
if snapshot:
|
|
74
|
+
print(f"Entity state: {snapshot.current_state}")
|
|
75
|
+
|
|
76
|
+
# Delete snapshot (tombstone)
|
|
77
|
+
await publisher.delete_snapshot("entity-123", "registration")
|
|
78
|
+
finally:
|
|
79
|
+
await publisher.stop()
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Performance Considerations:
|
|
83
|
+
- Use publish_batch for bulk operations (e.g., periodic snapshot jobs)
|
|
84
|
+
- Consider publish_from_projection for single updates (handles versioning)
|
|
85
|
+
- Tombstones are cheap - use delete_snapshot for permanent removals
|
|
86
|
+
- Monitor circuit breaker state for Kafka health
|
|
87
|
+
- First read triggers cache loading (may take a few seconds for large topics)
|
|
88
|
+
- Subsequent reads are O(1) from in-memory cache
|
|
89
|
+
|
|
90
|
+
Related Tickets:
|
|
91
|
+
- OMN-947 (F2): Snapshot Publishing
|
|
92
|
+
- OMN-944 (F1): Implement Registration Projection Schema
|
|
93
|
+
- OMN-940 (F0): Define Projector Execution Model
|
|
94
|
+
- OMN-1059: Implement snapshot read functionality
|
|
95
|
+
|
|
96
|
+
See Also:
|
|
97
|
+
- ProtocolSnapshotPublisher: Protocol definition for snapshot publishers
|
|
98
|
+
- ModelRegistrationSnapshot: Snapshot model definition
|
|
99
|
+
- ModelSnapshotTopicConfig: Topic configuration for compacted topics
|
|
100
|
+
- ProjectorRegistration: Projection persistence (source for snapshots)
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
from __future__ import annotations
|
|
104
|
+
|
|
105
|
+
import asyncio
|
|
106
|
+
import logging
|
|
107
|
+
from datetime import UTC, datetime
|
|
108
|
+
from typing import TYPE_CHECKING
|
|
109
|
+
from uuid import UUID, uuid4
|
|
110
|
+
|
|
111
|
+
from omnibase_infra.enums import EnumInfraTransportType
|
|
112
|
+
from omnibase_infra.errors import (
|
|
113
|
+
InfraConnectionError,
|
|
114
|
+
InfraTimeoutError,
|
|
115
|
+
InfraUnavailableError,
|
|
116
|
+
ModelInfraErrorContext,
|
|
117
|
+
ModelTimeoutErrorContext,
|
|
118
|
+
)
|
|
119
|
+
from omnibase_infra.mixins import MixinAsyncCircuitBreaker
|
|
120
|
+
from omnibase_infra.models.projection import (
|
|
121
|
+
ModelRegistrationProjection,
|
|
122
|
+
ModelRegistrationSnapshot,
|
|
123
|
+
ModelSnapshotTopicConfig,
|
|
124
|
+
)
|
|
125
|
+
from omnibase_infra.models.resilience import ModelCircuitBreakerConfig
|
|
126
|
+
|
|
127
|
+
if TYPE_CHECKING:
|
|
128
|
+
from aiokafka import AIOKafkaConsumer, AIOKafkaProducer
|
|
129
|
+
|
|
130
|
+
logger = logging.getLogger(__name__)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class SnapshotPublisherRegistration(MixinAsyncCircuitBreaker):
|
|
134
|
+
"""Publishes registration snapshots to a compacted Kafka topic.
|
|
135
|
+
|
|
136
|
+
This service reads registration projections and publishes them as
|
|
137
|
+
optimized snapshots to a Kafka compacted topic. Kafka compaction
|
|
138
|
+
ensures only the latest snapshot per entity is retained, enabling
|
|
139
|
+
fast state reconstruction without replaying events.
|
|
140
|
+
|
|
141
|
+
The publisher implements ProtocolSnapshotPublisher for structural
|
|
142
|
+
typing compatibility, allowing it to be used wherever the protocol
|
|
143
|
+
is expected.
|
|
144
|
+
|
|
145
|
+
Compaction Semantics:
|
|
146
|
+
- Key: "{domain}:{entity_id}" (e.g., "registration:uuid-here")
|
|
147
|
+
- Value: JSON-serialized ModelRegistrationSnapshot
|
|
148
|
+
- Tombstone: null value deletes the key during compaction
|
|
149
|
+
- After compaction: only latest snapshot per key survives
|
|
150
|
+
|
|
151
|
+
Circuit Breaker:
|
|
152
|
+
Uses MixinAsyncCircuitBreaker for resilience:
|
|
153
|
+
- Opens after 5 consecutive failures
|
|
154
|
+
- Resets after 60 seconds
|
|
155
|
+
- Raises InfraUnavailableError when open
|
|
156
|
+
|
|
157
|
+
Version Tracking:
|
|
158
|
+
The publisher maintains a version tracker per entity to ensure
|
|
159
|
+
monotonically increasing snapshot versions. This enables conflict
|
|
160
|
+
resolution and ordering guarantees during compaction.
|
|
161
|
+
|
|
162
|
+
Version Tracker Semantics:
|
|
163
|
+
- Versions start at 1 for each new entity
|
|
164
|
+
- Versions increment monotonically per entity within publisher lifetime
|
|
165
|
+
- Version tracker resets when publisher is recreated (new instance)
|
|
166
|
+
- delete_snapshot clears the version tracker entry for that entity
|
|
167
|
+
- For persistent version tracking across restarts, inject a shared
|
|
168
|
+
snapshot_version_tracker dict in __init__
|
|
169
|
+
- Coroutine-safe: Uses asyncio.Lock for concurrent access
|
|
170
|
+
|
|
171
|
+
NOTE: Snapshots are for READ OPTIMIZATION only. The immutable event
|
|
172
|
+
log remains the authoritative source of truth. Snapshots can be
|
|
173
|
+
regenerated from the event log at any time.
|
|
174
|
+
|
|
175
|
+
Attributes:
|
|
176
|
+
_producer: Kafka producer for publishing snapshots
|
|
177
|
+
_config: Snapshot topic configuration
|
|
178
|
+
_version_tracker: Dict tracking versions per entity
|
|
179
|
+
_started: Whether the publisher has been started
|
|
180
|
+
|
|
181
|
+
Example:
|
|
182
|
+
>>> config = ModelSnapshotTopicConfig.default()
|
|
183
|
+
>>> publisher = SnapshotPublisherRegistration(producer, config)
|
|
184
|
+
>>> await publisher.start()
|
|
185
|
+
>>>
|
|
186
|
+
>>> # Publish snapshot from projection
|
|
187
|
+
>>> snapshot = await publisher.publish_from_projection(projection)
|
|
188
|
+
>>>
|
|
189
|
+
>>> # Or publish existing snapshot
|
|
190
|
+
>>> await publisher.publish_snapshot(snapshot)
|
|
191
|
+
>>>
|
|
192
|
+
>>> await publisher.stop()
|
|
193
|
+
"""
|
|
194
|
+
|
|
195
|
+
def __init__(
|
|
196
|
+
self,
|
|
197
|
+
producer: AIOKafkaProducer,
|
|
198
|
+
config: ModelSnapshotTopicConfig,
|
|
199
|
+
*,
|
|
200
|
+
snapshot_version_tracker: dict[str, int] | None = None,
|
|
201
|
+
bootstrap_servers: str | None = None,
|
|
202
|
+
consumer_timeout_ms: int = 5000,
|
|
203
|
+
) -> None:
|
|
204
|
+
"""Initialize snapshot publisher.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
producer: AIOKafka producer for publishing snapshots. The producer
|
|
208
|
+
should be configured for the target Kafka cluster but NOT
|
|
209
|
+
started - the publisher will manage its lifecycle.
|
|
210
|
+
config: Snapshot topic configuration defining the target topic
|
|
211
|
+
and compaction settings.
|
|
212
|
+
snapshot_version_tracker: Optional dict to track versions per entity.
|
|
213
|
+
If not provided, a new dict is created internally. Useful for
|
|
214
|
+
sharing version state across multiple publishers or for testing.
|
|
215
|
+
bootstrap_servers: Kafka bootstrap servers for the consumer (for reads).
|
|
216
|
+
Required if you intend to use get_latest_snapshot(). If not provided,
|
|
217
|
+
reads will attempt to extract from the producer configuration.
|
|
218
|
+
consumer_timeout_ms: Timeout in milliseconds for consumer poll operations.
|
|
219
|
+
Default is 5000ms (5 seconds). Used when loading the snapshot cache.
|
|
220
|
+
|
|
221
|
+
Example:
|
|
222
|
+
>>> producer = AIOKafkaProducer(
|
|
223
|
+
... bootstrap_servers="localhost:9092",
|
|
224
|
+
... value_serializer=lambda v: v, # Publisher handles serialization
|
|
225
|
+
... )
|
|
226
|
+
>>> config = ModelSnapshotTopicConfig.default()
|
|
227
|
+
>>> publisher = SnapshotPublisherRegistration(
|
|
228
|
+
... producer,
|
|
229
|
+
... config,
|
|
230
|
+
... bootstrap_servers="localhost:9092",
|
|
231
|
+
... )
|
|
232
|
+
"""
|
|
233
|
+
self._producer = producer
|
|
234
|
+
self._config = config
|
|
235
|
+
self._version_tracker = snapshot_version_tracker or {}
|
|
236
|
+
self._version_tracker_lock = asyncio.Lock()
|
|
237
|
+
self._started = False
|
|
238
|
+
|
|
239
|
+
# Consumer configuration for read operations
|
|
240
|
+
self._bootstrap_servers = bootstrap_servers
|
|
241
|
+
self._consumer_timeout_ms = consumer_timeout_ms
|
|
242
|
+
self._consumer: AIOKafkaConsumer | None = None
|
|
243
|
+
self._consumer_started = False
|
|
244
|
+
|
|
245
|
+
# In-memory cache for O(1) snapshot lookups
|
|
246
|
+
# Key: "{domain}:{entity_id}", Value: ModelRegistrationSnapshot
|
|
247
|
+
#
|
|
248
|
+
# Cache Size Expectations:
|
|
249
|
+
# - Typical deployment: 100-1000 registered nodes
|
|
250
|
+
# - Large deployment: 5000-10000 nodes
|
|
251
|
+
# - Maximum practical: ~50000 nodes (memory ~100MB with full snapshots)
|
|
252
|
+
# - Each snapshot is approximately 2KB serialized
|
|
253
|
+
#
|
|
254
|
+
# Memory Footprint Estimation:
|
|
255
|
+
# - 1000 nodes * 2KB = ~2MB
|
|
256
|
+
# - 10000 nodes * 2KB = ~20MB
|
|
257
|
+
# - 50000 nodes * 2KB = ~100MB
|
|
258
|
+
self._snapshot_cache: dict[str, ModelRegistrationSnapshot] = {}
|
|
259
|
+
self._cache_lock = asyncio.Lock()
|
|
260
|
+
self._cache_loaded = False
|
|
261
|
+
self._cache_warming_in_progress = False
|
|
262
|
+
|
|
263
|
+
# Initialize circuit breaker with Kafka-appropriate settings
|
|
264
|
+
cb_config = ModelCircuitBreakerConfig.from_env(
|
|
265
|
+
service_name=f"snapshot-publisher.{config.topic}",
|
|
266
|
+
transport_type=EnumInfraTransportType.KAFKA,
|
|
267
|
+
)
|
|
268
|
+
self._init_circuit_breaker_from_config(cb_config)
|
|
269
|
+
|
|
270
|
+
@property
|
|
271
|
+
def topic(self) -> str:
|
|
272
|
+
"""Get the configured topic."""
|
|
273
|
+
return self._config.topic
|
|
274
|
+
|
|
275
|
+
@property
|
|
276
|
+
def is_started(self) -> bool:
|
|
277
|
+
"""Check if the publisher has been started."""
|
|
278
|
+
return self._started
|
|
279
|
+
|
|
280
|
+
async def start(self, *, warm_cache: bool = False) -> None:
|
|
281
|
+
"""Start the snapshot publisher.
|
|
282
|
+
|
|
283
|
+
Starts the underlying Kafka producer. Must be called before
|
|
284
|
+
publishing any snapshots.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
warm_cache: If True, pre-load the snapshot cache from Kafka
|
|
288
|
+
during startup. This is useful for read-heavy workloads
|
|
289
|
+
where you want the first read to be fast. The warming
|
|
290
|
+
is performed asynchronously and does not block start().
|
|
291
|
+
Default is False for backward compatibility.
|
|
292
|
+
|
|
293
|
+
Raises:
|
|
294
|
+
InfraConnectionError: If Kafka connection fails
|
|
295
|
+
|
|
296
|
+
Example:
|
|
297
|
+
>>> publisher = SnapshotPublisherRegistration(producer, config)
|
|
298
|
+
>>> await publisher.start()
|
|
299
|
+
>>> # Now ready to publish
|
|
300
|
+
>>>
|
|
301
|
+
>>> # With cache warming for read-heavy workloads
|
|
302
|
+
>>> await publisher.start(warm_cache=True)
|
|
303
|
+
"""
|
|
304
|
+
if self._started:
|
|
305
|
+
logger.debug("Snapshot publisher already started")
|
|
306
|
+
return
|
|
307
|
+
|
|
308
|
+
correlation_id = uuid4()
|
|
309
|
+
ctx = ModelInfraErrorContext(
|
|
310
|
+
transport_type=EnumInfraTransportType.KAFKA,
|
|
311
|
+
operation="start",
|
|
312
|
+
target_name=self._config.topic,
|
|
313
|
+
correlation_id=correlation_id,
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
try:
|
|
317
|
+
await self._producer.start()
|
|
318
|
+
self._started = True
|
|
319
|
+
logger.info(
|
|
320
|
+
"Snapshot publisher started for topic %s",
|
|
321
|
+
self._config.topic,
|
|
322
|
+
extra={"correlation_id": str(correlation_id)},
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
# Optionally warm the cache in the background
|
|
326
|
+
if warm_cache and self._bootstrap_servers:
|
|
327
|
+
await self._warm_cache_async(correlation_id)
|
|
328
|
+
|
|
329
|
+
except Exception as e:
|
|
330
|
+
raise InfraConnectionError(
|
|
331
|
+
f"Failed to start Kafka producer for topic {self._config.topic}",
|
|
332
|
+
context=ctx,
|
|
333
|
+
) from e
|
|
334
|
+
|
|
335
|
+
async def _warm_cache_async(self, correlation_id: UUID) -> None:
|
|
336
|
+
"""Warm the snapshot cache asynchronously.
|
|
337
|
+
|
|
338
|
+
Pre-loads all snapshots from the Kafka topic into the in-memory
|
|
339
|
+
cache. This is called during start() when warm_cache=True.
|
|
340
|
+
|
|
341
|
+
Cache warming is performed inline (not in background task) to ensure
|
|
342
|
+
the cache is populated before start() returns. This provides
|
|
343
|
+
predictable behavior for read-heavy workloads.
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
correlation_id: Correlation ID for tracing
|
|
347
|
+
|
|
348
|
+
Note:
|
|
349
|
+
Errors during cache warming are logged but do not fail startup.
|
|
350
|
+
The cache will be loaded lazily on the first read if warming fails.
|
|
351
|
+
"""
|
|
352
|
+
if self._cache_warming_in_progress:
|
|
353
|
+
logger.debug("Cache warming already in progress, skipping")
|
|
354
|
+
return
|
|
355
|
+
|
|
356
|
+
self._cache_warming_in_progress = True
|
|
357
|
+
|
|
358
|
+
try:
|
|
359
|
+
logger.info(
|
|
360
|
+
"Warming snapshot cache for topic %s",
|
|
361
|
+
self._config.topic,
|
|
362
|
+
extra={"correlation_id": str(correlation_id)},
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
await self._load_cache_from_topic(correlation_id)
|
|
366
|
+
|
|
367
|
+
async with self._cache_lock:
|
|
368
|
+
cache_size = len(self._snapshot_cache)
|
|
369
|
+
|
|
370
|
+
logger.info(
|
|
371
|
+
"Cache warming completed: %d snapshots loaded for topic %s",
|
|
372
|
+
cache_size,
|
|
373
|
+
self._config.topic,
|
|
374
|
+
extra={
|
|
375
|
+
"correlation_id": str(correlation_id),
|
|
376
|
+
"cache_size": cache_size,
|
|
377
|
+
"topic": self._config.topic,
|
|
378
|
+
},
|
|
379
|
+
)
|
|
380
|
+
except Exception as e:
|
|
381
|
+
# Log but don't fail startup - cache can be loaded lazily
|
|
382
|
+
logger.warning(
|
|
383
|
+
"Cache warming failed for topic %s: %s. "
|
|
384
|
+
"Cache will be loaded lazily on first read.",
|
|
385
|
+
self._config.topic,
|
|
386
|
+
str(e),
|
|
387
|
+
extra={
|
|
388
|
+
"correlation_id": str(correlation_id),
|
|
389
|
+
"error_type": type(e).__name__,
|
|
390
|
+
},
|
|
391
|
+
)
|
|
392
|
+
finally:
|
|
393
|
+
self._cache_warming_in_progress = False
|
|
394
|
+
|
|
395
|
+
async def stop(self) -> None:
|
|
396
|
+
"""Stop the snapshot publisher.
|
|
397
|
+
|
|
398
|
+
Stops the underlying Kafka producer, consumer (if started), and
|
|
399
|
+
cleans up resources. Safe to call multiple times.
|
|
400
|
+
|
|
401
|
+
Example:
|
|
402
|
+
>>> await publisher.stop()
|
|
403
|
+
>>> # Publisher is now stopped
|
|
404
|
+
"""
|
|
405
|
+
# Stop consumer if it was started
|
|
406
|
+
if self._consumer_started and self._consumer is not None:
|
|
407
|
+
try:
|
|
408
|
+
await self._consumer.stop()
|
|
409
|
+
self._consumer_started = False
|
|
410
|
+
self._consumer = None
|
|
411
|
+
logger.debug(
|
|
412
|
+
"Snapshot consumer stopped for topic %s", self._config.topic
|
|
413
|
+
)
|
|
414
|
+
except Exception as e:
|
|
415
|
+
# Log but don't raise - stop should be best-effort
|
|
416
|
+
logger.warning(
|
|
417
|
+
"Error stopping Kafka consumer: %s",
|
|
418
|
+
str(e),
|
|
419
|
+
extra={"topic": self._config.topic},
|
|
420
|
+
)
|
|
421
|
+
self._consumer_started = False
|
|
422
|
+
self._consumer = None
|
|
423
|
+
|
|
424
|
+
if not self._started:
|
|
425
|
+
logger.debug("Snapshot publisher already stopped")
|
|
426
|
+
return
|
|
427
|
+
|
|
428
|
+
try:
|
|
429
|
+
await self._producer.stop()
|
|
430
|
+
self._started = False
|
|
431
|
+
logger.info("Snapshot publisher stopped for topic %s", self._config.topic)
|
|
432
|
+
except Exception as e:
|
|
433
|
+
# Log but don't raise - stop should be best-effort
|
|
434
|
+
logger.warning(
|
|
435
|
+
"Error stopping Kafka producer: %s",
|
|
436
|
+
str(e),
|
|
437
|
+
extra={"topic": self._config.topic},
|
|
438
|
+
)
|
|
439
|
+
self._started = False
|
|
440
|
+
|
|
441
|
+
# Clear the cache on stop
|
|
442
|
+
async with self._cache_lock:
|
|
443
|
+
self._snapshot_cache.clear()
|
|
444
|
+
self._cache_loaded = False
|
|
445
|
+
|
|
446
|
+
async def _get_next_version(self, entity_id: str, domain: str) -> int:
|
|
447
|
+
"""Get the next snapshot version for an entity.
|
|
448
|
+
|
|
449
|
+
Increments and returns the version counter for the given entity.
|
|
450
|
+
Versions are monotonically increasing within the lifetime of
|
|
451
|
+
this publisher instance.
|
|
452
|
+
|
|
453
|
+
Concurrency Safety:
|
|
454
|
+
Uses _version_tracker_lock (asyncio.Lock) to ensure atomic
|
|
455
|
+
read-modify-write operations in concurrent coroutine contexts.
|
|
456
|
+
|
|
457
|
+
Args:
|
|
458
|
+
entity_id: The entity identifier
|
|
459
|
+
domain: The domain namespace
|
|
460
|
+
|
|
461
|
+
Returns:
|
|
462
|
+
Next version number (starting from 1)
|
|
463
|
+
"""
|
|
464
|
+
key = f"{domain}:{entity_id}"
|
|
465
|
+
async with self._version_tracker_lock:
|
|
466
|
+
current = self._version_tracker.get(key, 0)
|
|
467
|
+
next_version = current + 1
|
|
468
|
+
self._version_tracker[key] = next_version
|
|
469
|
+
return next_version
|
|
470
|
+
|
|
471
|
+
async def _cleanup_consumer(self) -> None:
|
|
472
|
+
"""Clean up Kafka consumer after cache load operations.
|
|
473
|
+
|
|
474
|
+
Stops the consumer, resets the started flag, and clears the reference.
|
|
475
|
+
This method is idempotent and safe to call even if no consumer exists.
|
|
476
|
+
Used for cleanup after both successful and failed cache load operations.
|
|
477
|
+
"""
|
|
478
|
+
if self._consumer_started:
|
|
479
|
+
try:
|
|
480
|
+
if self._consumer is not None:
|
|
481
|
+
await self._consumer.stop()
|
|
482
|
+
except Exception:
|
|
483
|
+
pass
|
|
484
|
+
self._consumer_started = False
|
|
485
|
+
self._consumer = None
|
|
486
|
+
|
|
487
|
+
async def publish_snapshot(
|
|
488
|
+
self,
|
|
489
|
+
snapshot: ModelRegistrationProjection,
|
|
490
|
+
) -> None:
|
|
491
|
+
"""Publish a single snapshot to the snapshot topic.
|
|
492
|
+
|
|
493
|
+
Publishes the projection as a snapshot to the compacted Kafka topic.
|
|
494
|
+
The key is derived from (entity_id, domain) for proper compaction.
|
|
495
|
+
|
|
496
|
+
NOTE: This is a READ OPTIMIZATION. The event log remains source of truth.
|
|
497
|
+
|
|
498
|
+
This method implements ProtocolSnapshotPublisher.publish_snapshot using
|
|
499
|
+
ModelRegistrationProjection as the input type. For publishing pre-built
|
|
500
|
+
ModelRegistrationSnapshot objects, use _publish_snapshot_model.
|
|
501
|
+
|
|
502
|
+
Args:
|
|
503
|
+
snapshot: The projection to publish as a snapshot. Must contain
|
|
504
|
+
valid entity_id and domain for key construction.
|
|
505
|
+
|
|
506
|
+
Raises:
|
|
507
|
+
InfraConnectionError: If Kafka connection fails
|
|
508
|
+
InfraTimeoutError: If publish times out
|
|
509
|
+
InfraUnavailableError: If circuit breaker is open
|
|
510
|
+
|
|
511
|
+
Example:
|
|
512
|
+
>>> projection = await reader.get_entity_state(entity_id)
|
|
513
|
+
>>> await publisher.publish_snapshot(projection)
|
|
514
|
+
"""
|
|
515
|
+
# Delegate to publish_from_projection for versioning and publishing
|
|
516
|
+
await self.publish_from_projection(
|
|
517
|
+
projection=snapshot,
|
|
518
|
+
node_name=None,
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
async def _publish_snapshot_model(
|
|
522
|
+
self,
|
|
523
|
+
snapshot: ModelRegistrationSnapshot,
|
|
524
|
+
) -> None:
|
|
525
|
+
"""Publish a pre-built snapshot model to Kafka.
|
|
526
|
+
|
|
527
|
+
Internal method for publishing ModelRegistrationSnapshot objects.
|
|
528
|
+
Use publish_snapshot for protocol compliance or publish_from_projection
|
|
529
|
+
for automatic version tracking.
|
|
530
|
+
|
|
531
|
+
Args:
|
|
532
|
+
snapshot: The snapshot model to publish
|
|
533
|
+
|
|
534
|
+
Raises:
|
|
535
|
+
InfraConnectionError: If Kafka connection fails
|
|
536
|
+
InfraTimeoutError: If publish times out
|
|
537
|
+
InfraUnavailableError: If circuit breaker is open
|
|
538
|
+
"""
|
|
539
|
+
correlation_id = uuid4()
|
|
540
|
+
|
|
541
|
+
# Check circuit breaker before operation
|
|
542
|
+
async with self._circuit_breaker_lock:
|
|
543
|
+
await self._check_circuit_breaker("publish_snapshot", correlation_id)
|
|
544
|
+
|
|
545
|
+
ctx = ModelInfraErrorContext(
|
|
546
|
+
transport_type=EnumInfraTransportType.KAFKA,
|
|
547
|
+
operation="publish_snapshot",
|
|
548
|
+
target_name=self._config.topic,
|
|
549
|
+
correlation_id=correlation_id,
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
try:
|
|
553
|
+
# Build key and value for Kafka
|
|
554
|
+
key = snapshot.to_kafka_key().encode("utf-8")
|
|
555
|
+
value = snapshot.model_dump_json().encode("utf-8")
|
|
556
|
+
|
|
557
|
+
# Send and wait for acknowledgment
|
|
558
|
+
await self._producer.send_and_wait(
|
|
559
|
+
self._config.topic,
|
|
560
|
+
key=key,
|
|
561
|
+
value=value,
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
# Record success
|
|
565
|
+
async with self._circuit_breaker_lock:
|
|
566
|
+
await self._reset_circuit_breaker()
|
|
567
|
+
|
|
568
|
+
# Update cache if loaded (for read-after-write consistency)
|
|
569
|
+
if self._cache_loaded:
|
|
570
|
+
cache_key = snapshot.to_kafka_key()
|
|
571
|
+
async with self._cache_lock:
|
|
572
|
+
self._snapshot_cache[cache_key] = snapshot
|
|
573
|
+
|
|
574
|
+
logger.debug(
|
|
575
|
+
"Published snapshot for %s version %d",
|
|
576
|
+
snapshot.to_kafka_key(),
|
|
577
|
+
snapshot.snapshot_version,
|
|
578
|
+
extra={"correlation_id": str(correlation_id)},
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
except TimeoutError as e:
|
|
582
|
+
async with self._circuit_breaker_lock:
|
|
583
|
+
await self._record_circuit_failure("publish_snapshot", correlation_id)
|
|
584
|
+
raise InfraTimeoutError(
|
|
585
|
+
f"Timeout publishing snapshot: {snapshot.to_kafka_key()}",
|
|
586
|
+
context=ModelTimeoutErrorContext(
|
|
587
|
+
transport_type=ctx.transport_type,
|
|
588
|
+
operation=ctx.operation,
|
|
589
|
+
target_name=ctx.target_name,
|
|
590
|
+
correlation_id=ctx.correlation_id,
|
|
591
|
+
# timeout_seconds omitted - value not available in this context (defaults to None)
|
|
592
|
+
),
|
|
593
|
+
) from e
|
|
594
|
+
|
|
595
|
+
except Exception as e:
|
|
596
|
+
async with self._circuit_breaker_lock:
|
|
597
|
+
await self._record_circuit_failure("publish_snapshot", correlation_id)
|
|
598
|
+
raise InfraConnectionError(
|
|
599
|
+
f"Failed to publish snapshot: {snapshot.to_kafka_key()}",
|
|
600
|
+
context=ctx,
|
|
601
|
+
) from e
|
|
602
|
+
|
|
603
|
+
async def publish_batch(
|
|
604
|
+
self,
|
|
605
|
+
snapshots: list[ModelRegistrationProjection],
|
|
606
|
+
*,
|
|
607
|
+
parallel: bool = True,
|
|
608
|
+
) -> int:
|
|
609
|
+
"""Publish multiple snapshots in a batch operation.
|
|
610
|
+
|
|
611
|
+
Publishes each projection as a snapshot, continuing on individual
|
|
612
|
+
failures. This is the recommended method for bulk snapshot jobs.
|
|
613
|
+
|
|
614
|
+
NOTE: This is a READ OPTIMIZATION. The event log remains source of truth.
|
|
615
|
+
|
|
616
|
+
Args:
|
|
617
|
+
snapshots: List of projections to publish as snapshots
|
|
618
|
+
parallel: If True (default), publish concurrently using asyncio.gather.
|
|
619
|
+
Set to False for sequential publishing (useful for debugging
|
|
620
|
+
or rate-limited scenarios).
|
|
621
|
+
|
|
622
|
+
Returns:
|
|
623
|
+
Count of successfully published snapshots.
|
|
624
|
+
May be less than len(snapshots) if some fail.
|
|
625
|
+
|
|
626
|
+
Raises:
|
|
627
|
+
InfraConnectionError: Only if connection fails before any publishing
|
|
628
|
+
|
|
629
|
+
Example:
|
|
630
|
+
>>> projections = await reader.get_all()
|
|
631
|
+
>>> count = await publisher.publish_batch(projections)
|
|
632
|
+
>>> print(f"Published {count}/{len(projections)} snapshots")
|
|
633
|
+
>>>
|
|
634
|
+
>>> # Sequential publishing for debugging
|
|
635
|
+
>>> count = await publisher.publish_batch(projections, parallel=False)
|
|
636
|
+
"""
|
|
637
|
+
if not snapshots:
|
|
638
|
+
return 0
|
|
639
|
+
|
|
640
|
+
if parallel:
|
|
641
|
+
# Parallel publishing using asyncio.gather with return_exceptions=True
|
|
642
|
+
results = await asyncio.gather(
|
|
643
|
+
*[self.publish_snapshot(projection) for projection in snapshots],
|
|
644
|
+
return_exceptions=True,
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
success_count = 0
|
|
648
|
+
for i, result in enumerate(results):
|
|
649
|
+
if isinstance(result, Exception):
|
|
650
|
+
projection = snapshots[i]
|
|
651
|
+
logger.warning(
|
|
652
|
+
"Failed to publish snapshot %s:%s: %s",
|
|
653
|
+
projection.domain,
|
|
654
|
+
str(projection.entity_id),
|
|
655
|
+
str(result),
|
|
656
|
+
extra={
|
|
657
|
+
"entity_id": str(projection.entity_id),
|
|
658
|
+
"domain": projection.domain,
|
|
659
|
+
},
|
|
660
|
+
)
|
|
661
|
+
else:
|
|
662
|
+
success_count += 1
|
|
663
|
+
else:
|
|
664
|
+
# Sequential publishing (original behavior)
|
|
665
|
+
success_count = 0
|
|
666
|
+
for projection in snapshots:
|
|
667
|
+
try:
|
|
668
|
+
await self.publish_snapshot(projection)
|
|
669
|
+
success_count += 1
|
|
670
|
+
except (
|
|
671
|
+
InfraConnectionError,
|
|
672
|
+
InfraTimeoutError,
|
|
673
|
+
InfraUnavailableError,
|
|
674
|
+
) as e:
|
|
675
|
+
logger.warning(
|
|
676
|
+
"Failed to publish snapshot %s:%s: %s",
|
|
677
|
+
projection.domain,
|
|
678
|
+
str(projection.entity_id),
|
|
679
|
+
str(e),
|
|
680
|
+
extra={
|
|
681
|
+
"entity_id": str(projection.entity_id),
|
|
682
|
+
"domain": projection.domain,
|
|
683
|
+
},
|
|
684
|
+
)
|
|
685
|
+
# Continue with remaining snapshots (best-effort)
|
|
686
|
+
|
|
687
|
+
logger.info(
|
|
688
|
+
"Batch publish completed: %d/%d snapshots published (parallel=%s)",
|
|
689
|
+
success_count,
|
|
690
|
+
len(snapshots),
|
|
691
|
+
parallel,
|
|
692
|
+
extra={"topic": self._config.topic},
|
|
693
|
+
)
|
|
694
|
+
return success_count
|
|
695
|
+
|
|
696
|
+
async def get_latest_snapshot(
|
|
697
|
+
self,
|
|
698
|
+
entity_id: str,
|
|
699
|
+
domain: str,
|
|
700
|
+
) -> ModelRegistrationSnapshot | None:
|
|
701
|
+
"""Retrieve the latest snapshot for an entity.
|
|
702
|
+
|
|
703
|
+
Reads the snapshot from an in-memory cache that is built from the
|
|
704
|
+
compacted Kafka topic. The cache is loaded lazily on first read.
|
|
705
|
+
|
|
706
|
+
IMPORTANT: Snapshot may be slightly stale. For guaranteed freshness,
|
|
707
|
+
combine with event log events since snapshot.updated_at. Call
|
|
708
|
+
refresh_cache() to reload the cache from Kafka.
|
|
709
|
+
|
|
710
|
+
Args:
|
|
711
|
+
entity_id: The entity identifier (UUID as string)
|
|
712
|
+
domain: The domain namespace (e.g., "registration")
|
|
713
|
+
|
|
714
|
+
Returns:
|
|
715
|
+
The latest snapshot if found, None otherwise.
|
|
716
|
+
|
|
717
|
+
Raises:
|
|
718
|
+
InfraConnectionError: If Kafka connection fails during cache load
|
|
719
|
+
InfraTimeoutError: If cache loading times out
|
|
720
|
+
InfraUnavailableError: If circuit breaker is open
|
|
721
|
+
|
|
722
|
+
Example:
|
|
723
|
+
>>> snapshot = await publisher.get_latest_snapshot("uuid", "registration")
|
|
724
|
+
>>> if snapshot is not None:
|
|
725
|
+
... print(f"Entity state: {snapshot.current_state}")
|
|
726
|
+
... else:
|
|
727
|
+
... print("Entity not found")
|
|
728
|
+
"""
|
|
729
|
+
correlation_id = uuid4()
|
|
730
|
+
|
|
731
|
+
# Load cache if not already loaded
|
|
732
|
+
# Circuit breaker check is now inside _load_cache_from_topic()
|
|
733
|
+
if not self._cache_loaded:
|
|
734
|
+
await self._load_cache_from_topic(correlation_id)
|
|
735
|
+
|
|
736
|
+
# Lookup in cache (O(1))
|
|
737
|
+
key = f"{domain}:{entity_id}"
|
|
738
|
+
async with self._cache_lock:
|
|
739
|
+
snapshot = self._snapshot_cache.get(key)
|
|
740
|
+
|
|
741
|
+
if snapshot is None:
|
|
742
|
+
logger.debug(
|
|
743
|
+
"Snapshot not found in cache for %s:%s",
|
|
744
|
+
domain,
|
|
745
|
+
entity_id,
|
|
746
|
+
extra={
|
|
747
|
+
"entity_id": entity_id,
|
|
748
|
+
"domain": domain,
|
|
749
|
+
"topic": self._config.topic,
|
|
750
|
+
"correlation_id": str(correlation_id),
|
|
751
|
+
},
|
|
752
|
+
)
|
|
753
|
+
else:
|
|
754
|
+
logger.debug(
|
|
755
|
+
"Snapshot retrieved from cache for %s:%s version %d",
|
|
756
|
+
domain,
|
|
757
|
+
entity_id,
|
|
758
|
+
snapshot.snapshot_version,
|
|
759
|
+
extra={
|
|
760
|
+
"entity_id": entity_id,
|
|
761
|
+
"domain": domain,
|
|
762
|
+
"snapshot_version": snapshot.snapshot_version,
|
|
763
|
+
"correlation_id": str(correlation_id),
|
|
764
|
+
},
|
|
765
|
+
)
|
|
766
|
+
|
|
767
|
+
return snapshot
|
|
768
|
+
|
|
769
|
+
async def _load_cache_from_topic(self, correlation_id: UUID) -> None:
|
|
770
|
+
"""Load the snapshot cache from the compacted Kafka topic.
|
|
771
|
+
|
|
772
|
+
Reads all snapshots from the topic and populates the in-memory cache.
|
|
773
|
+
Uses getmany() with timeout to avoid blocking indefinitely.
|
|
774
|
+
|
|
775
|
+
This method is called lazily on the first read operation. It includes
|
|
776
|
+
circuit breaker protection to ensure consistent protection regardless
|
|
777
|
+
of the call site.
|
|
778
|
+
|
|
779
|
+
Performance Notes:
|
|
780
|
+
- Uses model_validate_json() for ~30% faster JSON parsing vs
|
|
781
|
+
json.loads() + model_validate()
|
|
782
|
+
- Logs progress every 1000 messages for observability during
|
|
783
|
+
large topic scans (5000+ messages)
|
|
784
|
+
|
|
785
|
+
Args:
|
|
786
|
+
correlation_id: Correlation ID for tracing
|
|
787
|
+
|
|
788
|
+
Raises:
|
|
789
|
+
InfraConnectionError: If Kafka connection fails
|
|
790
|
+
InfraTimeoutError: If consumer startup times out
|
|
791
|
+
InfraUnavailableError: If circuit breaker is open
|
|
792
|
+
"""
|
|
793
|
+
# Progress logging interval (log every N messages)
|
|
794
|
+
progress_log_interval = 1000
|
|
795
|
+
|
|
796
|
+
# Check circuit breaker before operation - moved inside this method
|
|
797
|
+
# to ensure consistent protection regardless of call site
|
|
798
|
+
async with self._circuit_breaker_lock:
|
|
799
|
+
await self._check_circuit_breaker("load_cache", correlation_id)
|
|
800
|
+
|
|
801
|
+
async with self._cache_lock:
|
|
802
|
+
# Double-check after acquiring lock
|
|
803
|
+
if self._cache_loaded:
|
|
804
|
+
return
|
|
805
|
+
|
|
806
|
+
ctx = ModelInfraErrorContext(
|
|
807
|
+
transport_type=EnumInfraTransportType.KAFKA,
|
|
808
|
+
operation="load_cache",
|
|
809
|
+
target_name=self._config.topic,
|
|
810
|
+
correlation_id=correlation_id,
|
|
811
|
+
)
|
|
812
|
+
|
|
813
|
+
# Get bootstrap servers - must be explicitly configured
|
|
814
|
+
# We don't try to extract from producer because:
|
|
815
|
+
# 1. The producer may use internal/private attributes that vary by version
|
|
816
|
+
# 2. Mock producers don't have real bootstrap_servers
|
|
817
|
+
# 3. It's cleaner to require explicit configuration for reads
|
|
818
|
+
bootstrap_servers = self._bootstrap_servers
|
|
819
|
+
|
|
820
|
+
# Validate bootstrap_servers is non-empty string with proper format
|
|
821
|
+
if not bootstrap_servers or not bootstrap_servers.strip():
|
|
822
|
+
raise InfraConnectionError(
|
|
823
|
+
"bootstrap_servers not configured or empty. Provide bootstrap_servers "
|
|
824
|
+
"in constructor to enable snapshot reads "
|
|
825
|
+
"(e.g., 'localhost:9092' or 'kafka1:9092,kafka2:9092').",
|
|
826
|
+
context=ctx,
|
|
827
|
+
)
|
|
828
|
+
|
|
829
|
+
# Validate host:port format for each server
|
|
830
|
+
stripped_servers = bootstrap_servers.strip()
|
|
831
|
+
for server in stripped_servers.split(","):
|
|
832
|
+
server = server.strip()
|
|
833
|
+
if not server:
|
|
834
|
+
raise InfraConnectionError(
|
|
835
|
+
f"bootstrap_servers contains empty entries: '{bootstrap_servers}'. "
|
|
836
|
+
"Each entry must be in 'host:port' format.",
|
|
837
|
+
context=ctx,
|
|
838
|
+
)
|
|
839
|
+
if ":" not in server:
|
|
840
|
+
raise InfraConnectionError(
|
|
841
|
+
f"Invalid bootstrap server format '{server}'. "
|
|
842
|
+
"Expected 'host:port' (e.g., 'localhost:9092').",
|
|
843
|
+
context=ctx,
|
|
844
|
+
)
|
|
845
|
+
host, port_str = server.rsplit(":", 1)
|
|
846
|
+
if not host:
|
|
847
|
+
raise InfraConnectionError(
|
|
848
|
+
f"Invalid bootstrap server format '{server}'. "
|
|
849
|
+
"Host cannot be empty.",
|
|
850
|
+
context=ctx,
|
|
851
|
+
)
|
|
852
|
+
try:
|
|
853
|
+
port = int(port_str)
|
|
854
|
+
if port < 1 or port > 65535:
|
|
855
|
+
raise InfraConnectionError(
|
|
856
|
+
f"Invalid port {port} in '{server}'. "
|
|
857
|
+
"Port must be between 1 and 65535.",
|
|
858
|
+
context=ctx,
|
|
859
|
+
)
|
|
860
|
+
except ValueError:
|
|
861
|
+
raise InfraConnectionError(
|
|
862
|
+
f"Invalid port '{port_str}' in '{server}'. "
|
|
863
|
+
"Port must be a valid integer.",
|
|
864
|
+
context=ctx,
|
|
865
|
+
) from None
|
|
866
|
+
|
|
867
|
+
# Use the stripped and validated version
|
|
868
|
+
bootstrap_servers = stripped_servers
|
|
869
|
+
|
|
870
|
+
# Import consumer here to avoid circular imports
|
|
871
|
+
from aiokafka import AIOKafkaConsumer
|
|
872
|
+
from pydantic import ValidationError
|
|
873
|
+
|
|
874
|
+
# Create consumer with unique group ID for this publisher instance
|
|
875
|
+
# Using a unique group ensures we get our own offset tracking
|
|
876
|
+
consumer_group = f"snapshot-reader-{self._config.topic}-{uuid4()!s}"
|
|
877
|
+
consumer = AIOKafkaConsumer(
|
|
878
|
+
self._config.topic,
|
|
879
|
+
bootstrap_servers=bootstrap_servers,
|
|
880
|
+
group_id=consumer_group,
|
|
881
|
+
auto_offset_reset="earliest",
|
|
882
|
+
enable_auto_commit=False,
|
|
883
|
+
)
|
|
884
|
+
|
|
885
|
+
try:
|
|
886
|
+
await consumer.start()
|
|
887
|
+
self._consumer = consumer
|
|
888
|
+
self._consumer_started = True
|
|
889
|
+
|
|
890
|
+
# Seek to beginning to read all snapshots
|
|
891
|
+
await consumer.seek_to_beginning()
|
|
892
|
+
|
|
893
|
+
# Read all messages from the topic until no more messages
|
|
894
|
+
messages_read = 0
|
|
895
|
+
tombstones_applied = 0
|
|
896
|
+
parse_errors = 0
|
|
897
|
+
last_progress_log = 0
|
|
898
|
+
|
|
899
|
+
while True:
|
|
900
|
+
# Poll with timeout - returns empty dict when no more messages
|
|
901
|
+
messages = await consumer.getmany(
|
|
902
|
+
timeout_ms=self._consumer_timeout_ms
|
|
903
|
+
)
|
|
904
|
+
if not messages:
|
|
905
|
+
break # No more messages within timeout
|
|
906
|
+
|
|
907
|
+
for _tp, msgs in messages.items():
|
|
908
|
+
for message in msgs:
|
|
909
|
+
key = message.key.decode("utf-8") if message.key else None
|
|
910
|
+
|
|
911
|
+
if key is None:
|
|
912
|
+
# Skip messages without keys
|
|
913
|
+
continue
|
|
914
|
+
|
|
915
|
+
if message.value is None:
|
|
916
|
+
# Tombstone - remove from cache
|
|
917
|
+
self._snapshot_cache.pop(key, None)
|
|
918
|
+
tombstones_applied += 1
|
|
919
|
+
else:
|
|
920
|
+
# Parse snapshot using model_validate_json for
|
|
921
|
+
# ~30% faster parsing (Pydantic v2 optimization)
|
|
922
|
+
try:
|
|
923
|
+
snapshot = (
|
|
924
|
+
ModelRegistrationSnapshot.model_validate_json(
|
|
925
|
+
message.value
|
|
926
|
+
)
|
|
927
|
+
)
|
|
928
|
+
self._snapshot_cache[key] = snapshot
|
|
929
|
+
messages_read += 1
|
|
930
|
+
except (ValidationError, ValueError) as e:
|
|
931
|
+
parse_errors += 1
|
|
932
|
+
logger.warning(
|
|
933
|
+
"Failed to parse snapshot for key %s: %s",
|
|
934
|
+
key,
|
|
935
|
+
str(e),
|
|
936
|
+
extra={
|
|
937
|
+
"key": key,
|
|
938
|
+
"correlation_id": str(correlation_id),
|
|
939
|
+
},
|
|
940
|
+
)
|
|
941
|
+
|
|
942
|
+
# Log progress for large topic scans
|
|
943
|
+
total_processed = messages_read + tombstones_applied
|
|
944
|
+
if (
|
|
945
|
+
total_processed - last_progress_log
|
|
946
|
+
>= progress_log_interval
|
|
947
|
+
):
|
|
948
|
+
logger.info(
|
|
949
|
+
"Cache loading progress: %d messages processed "
|
|
950
|
+
"(%d snapshots, %d tombstones, %d errors)",
|
|
951
|
+
total_processed,
|
|
952
|
+
messages_read,
|
|
953
|
+
tombstones_applied,
|
|
954
|
+
parse_errors,
|
|
955
|
+
extra={
|
|
956
|
+
"topic": self._config.topic,
|
|
957
|
+
"messages_processed": total_processed,
|
|
958
|
+
"snapshots": messages_read,
|
|
959
|
+
"tombstones": tombstones_applied,
|
|
960
|
+
"parse_errors": parse_errors,
|
|
961
|
+
"correlation_id": str(correlation_id),
|
|
962
|
+
},
|
|
963
|
+
)
|
|
964
|
+
last_progress_log = total_processed
|
|
965
|
+
|
|
966
|
+
self._cache_loaded = True
|
|
967
|
+
|
|
968
|
+
# Reset circuit breaker on success
|
|
969
|
+
async with self._circuit_breaker_lock:
|
|
970
|
+
await self._reset_circuit_breaker()
|
|
971
|
+
|
|
972
|
+
# Calculate cache memory estimate (approx 2KB per snapshot)
|
|
973
|
+
cache_size = len(self._snapshot_cache)
|
|
974
|
+
estimated_memory_kb = cache_size * 2
|
|
975
|
+
|
|
976
|
+
logger.info(
|
|
977
|
+
"Snapshot cache loaded: %d snapshots, %d tombstones applied, "
|
|
978
|
+
"%d parse errors, cache size: %d entries (~%dKB)",
|
|
979
|
+
messages_read,
|
|
980
|
+
tombstones_applied,
|
|
981
|
+
parse_errors,
|
|
982
|
+
cache_size,
|
|
983
|
+
estimated_memory_kb,
|
|
984
|
+
extra={
|
|
985
|
+
"topic": self._config.topic,
|
|
986
|
+
"snapshots_loaded": messages_read,
|
|
987
|
+
"tombstones_applied": tombstones_applied,
|
|
988
|
+
"parse_errors": parse_errors,
|
|
989
|
+
"cache_size": cache_size,
|
|
990
|
+
"estimated_memory_kb": estimated_memory_kb,
|
|
991
|
+
"correlation_id": str(correlation_id),
|
|
992
|
+
},
|
|
993
|
+
)
|
|
994
|
+
|
|
995
|
+
# Stop consumer after successful cache load - consumer is only
|
|
996
|
+
# needed during the cache loading phase, not for ongoing reads
|
|
997
|
+
await self._cleanup_consumer()
|
|
998
|
+
|
|
999
|
+
except TimeoutError as e:
|
|
1000
|
+
async with self._circuit_breaker_lock:
|
|
1001
|
+
await self._record_circuit_failure("load_cache", correlation_id)
|
|
1002
|
+
await self._cleanup_consumer()
|
|
1003
|
+
raise InfraTimeoutError(
|
|
1004
|
+
f"Timeout loading snapshot cache from topic {self._config.topic}",
|
|
1005
|
+
context=ModelTimeoutErrorContext(
|
|
1006
|
+
transport_type=ctx.transport_type,
|
|
1007
|
+
operation=ctx.operation,
|
|
1008
|
+
target_name=ctx.target_name,
|
|
1009
|
+
correlation_id=ctx.correlation_id,
|
|
1010
|
+
timeout_seconds=float(self._consumer_timeout_ms) / 1000.0,
|
|
1011
|
+
),
|
|
1012
|
+
) from e
|
|
1013
|
+
|
|
1014
|
+
except Exception as e:
|
|
1015
|
+
async with self._circuit_breaker_lock:
|
|
1016
|
+
await self._record_circuit_failure("load_cache", correlation_id)
|
|
1017
|
+
await self._cleanup_consumer()
|
|
1018
|
+
raise InfraConnectionError(
|
|
1019
|
+
f"Failed to load snapshot cache from topic {self._config.topic}: {e}",
|
|
1020
|
+
context=ctx,
|
|
1021
|
+
) from e
|
|
1022
|
+
|
|
1023
|
+
async def refresh_cache(self) -> int:
|
|
1024
|
+
"""Refresh the snapshot cache by reloading from the Kafka topic.
|
|
1025
|
+
|
|
1026
|
+
Reloads all snapshots from the compacted topic. Use this to ensure
|
|
1027
|
+
the cache reflects the latest published state.
|
|
1028
|
+
|
|
1029
|
+
Error Recovery:
|
|
1030
|
+
If cache loading fails, the existing cache is preserved to avoid
|
|
1031
|
+
leaving the system in a broken state. This follows the principle
|
|
1032
|
+
of graceful degradation - stale data is better than no data.
|
|
1033
|
+
|
|
1034
|
+
Returns:
|
|
1035
|
+
Number of snapshots loaded into the cache.
|
|
1036
|
+
|
|
1037
|
+
Raises:
|
|
1038
|
+
InfraConnectionError: If Kafka connection fails
|
|
1039
|
+
InfraTimeoutError: If cache loading times out
|
|
1040
|
+
InfraUnavailableError: If circuit breaker is open
|
|
1041
|
+
|
|
1042
|
+
Example:
|
|
1043
|
+
>>> count = await publisher.refresh_cache()
|
|
1044
|
+
>>> print(f"Loaded {count} snapshots")
|
|
1045
|
+
"""
|
|
1046
|
+
correlation_id = uuid4()
|
|
1047
|
+
|
|
1048
|
+
# Check circuit breaker before operation
|
|
1049
|
+
async with self._circuit_breaker_lock:
|
|
1050
|
+
await self._check_circuit_breaker("refresh_cache", correlation_id)
|
|
1051
|
+
|
|
1052
|
+
# Stop existing consumer if running
|
|
1053
|
+
if self._consumer_started and self._consumer is not None:
|
|
1054
|
+
try:
|
|
1055
|
+
await self._consumer.stop()
|
|
1056
|
+
except Exception:
|
|
1057
|
+
pass
|
|
1058
|
+
self._consumer_started = False
|
|
1059
|
+
self._consumer = None
|
|
1060
|
+
|
|
1061
|
+
# Preserve existing cache state before attempting reload
|
|
1062
|
+
# This allows rollback on failure (graceful degradation)
|
|
1063
|
+
async with self._cache_lock:
|
|
1064
|
+
old_cache = self._snapshot_cache.copy()
|
|
1065
|
+
old_cache_loaded = self._cache_loaded
|
|
1066
|
+
self._snapshot_cache.clear()
|
|
1067
|
+
self._cache_loaded = False
|
|
1068
|
+
|
|
1069
|
+
try:
|
|
1070
|
+
await self._load_cache_from_topic(correlation_id)
|
|
1071
|
+
|
|
1072
|
+
async with self._cache_lock:
|
|
1073
|
+
count = len(self._snapshot_cache)
|
|
1074
|
+
|
|
1075
|
+
logger.info(
|
|
1076
|
+
"Snapshot cache refreshed with %d snapshots",
|
|
1077
|
+
count,
|
|
1078
|
+
extra={
|
|
1079
|
+
"topic": self._config.topic,
|
|
1080
|
+
"snapshot_count": count,
|
|
1081
|
+
"correlation_id": str(correlation_id),
|
|
1082
|
+
},
|
|
1083
|
+
)
|
|
1084
|
+
|
|
1085
|
+
return count
|
|
1086
|
+
|
|
1087
|
+
except Exception as e:
|
|
1088
|
+
# Restore previous cache on failure (graceful degradation)
|
|
1089
|
+
async with self._cache_lock:
|
|
1090
|
+
self._snapshot_cache = old_cache
|
|
1091
|
+
self._cache_loaded = old_cache_loaded
|
|
1092
|
+
|
|
1093
|
+
logger.warning(
|
|
1094
|
+
"Cache refresh failed, preserving existing cache with %d snapshots",
|
|
1095
|
+
len(old_cache),
|
|
1096
|
+
extra={
|
|
1097
|
+
"topic": self._config.topic,
|
|
1098
|
+
"preserved_count": len(old_cache),
|
|
1099
|
+
"error_type": type(e).__name__,
|
|
1100
|
+
"correlation_id": str(correlation_id),
|
|
1101
|
+
},
|
|
1102
|
+
)
|
|
1103
|
+
raise
|
|
1104
|
+
|
|
1105
|
+
@property
|
|
1106
|
+
def cache_size(self) -> int:
|
|
1107
|
+
"""Get the number of snapshots in the cache.
|
|
1108
|
+
|
|
1109
|
+
Returns:
|
|
1110
|
+
Number of snapshots currently in the cache.
|
|
1111
|
+
|
|
1112
|
+
Note:
|
|
1113
|
+
This is a synchronous property that does not trigger cache loading.
|
|
1114
|
+
Call get_latest_snapshot() or refresh_cache() to load the cache first.
|
|
1115
|
+
"""
|
|
1116
|
+
return len(self._snapshot_cache)
|
|
1117
|
+
|
|
1118
|
+
@property
|
|
1119
|
+
def is_cache_loaded(self) -> bool:
|
|
1120
|
+
"""Check if the cache has been loaded.
|
|
1121
|
+
|
|
1122
|
+
Returns:
|
|
1123
|
+
True if the cache has been loaded from Kafka, False otherwise.
|
|
1124
|
+
"""
|
|
1125
|
+
return self._cache_loaded
|
|
1126
|
+
|
|
1127
|
+
async def delete_snapshot(
|
|
1128
|
+
self,
|
|
1129
|
+
entity_id: str,
|
|
1130
|
+
domain: str,
|
|
1131
|
+
) -> bool:
|
|
1132
|
+
"""Publish a tombstone to remove a snapshot.
|
|
1133
|
+
|
|
1134
|
+
In Kafka compaction, a message with null value acts as a tombstone,
|
|
1135
|
+
causing the key to be removed during compaction. This effectively
|
|
1136
|
+
deletes the snapshot for the given entity.
|
|
1137
|
+
|
|
1138
|
+
NOTE: This does NOT delete events from the event log. The event log
|
|
1139
|
+
is immutable and retains full history. Tombstones only affect the
|
|
1140
|
+
snapshot read path.
|
|
1141
|
+
|
|
1142
|
+
Use Cases:
|
|
1143
|
+
- Node deregistration (permanent removal)
|
|
1144
|
+
- Entity lifecycle completion
|
|
1145
|
+
- Data retention cleanup
|
|
1146
|
+
|
|
1147
|
+
Args:
|
|
1148
|
+
entity_id: The entity identifier (UUID as string)
|
|
1149
|
+
domain: The domain namespace (e.g., "registration")
|
|
1150
|
+
|
|
1151
|
+
Returns:
|
|
1152
|
+
True if tombstone was published successfully.
|
|
1153
|
+
False if publish failed (caller should retry or handle).
|
|
1154
|
+
|
|
1155
|
+
Raises:
|
|
1156
|
+
InfraUnavailableError: If circuit breaker is open (fail-fast).
|
|
1157
|
+
|
|
1158
|
+
Example:
|
|
1159
|
+
>>> # Handle node deregistration
|
|
1160
|
+
>>> deleted = await publisher.delete_snapshot(str(node_id), "registration")
|
|
1161
|
+
>>> if not deleted:
|
|
1162
|
+
... logger.warning(f"Failed to delete snapshot for {node_id}")
|
|
1163
|
+
"""
|
|
1164
|
+
correlation_id = uuid4()
|
|
1165
|
+
|
|
1166
|
+
# Check circuit breaker before operation - let InfraUnavailableError propagate
|
|
1167
|
+
# per ONEX fail-fast principles (callers need to know service is unavailable)
|
|
1168
|
+
async with self._circuit_breaker_lock:
|
|
1169
|
+
await self._check_circuit_breaker("delete_snapshot", correlation_id)
|
|
1170
|
+
|
|
1171
|
+
try:
|
|
1172
|
+
# Build key for tombstone
|
|
1173
|
+
key = f"{domain}:{entity_id}".encode()
|
|
1174
|
+
|
|
1175
|
+
# Publish tombstone (null value)
|
|
1176
|
+
await self._producer.send_and_wait(
|
|
1177
|
+
self._config.topic,
|
|
1178
|
+
key=key,
|
|
1179
|
+
value=None, # Tombstone - null value triggers deletion on compaction
|
|
1180
|
+
)
|
|
1181
|
+
|
|
1182
|
+
# Record success
|
|
1183
|
+
async with self._circuit_breaker_lock:
|
|
1184
|
+
await self._reset_circuit_breaker()
|
|
1185
|
+
|
|
1186
|
+
# Clear version tracker for this entity (thread-safe)
|
|
1187
|
+
tracker_key = f"{domain}:{entity_id}"
|
|
1188
|
+
async with self._version_tracker_lock:
|
|
1189
|
+
self._version_tracker.pop(tracker_key, None)
|
|
1190
|
+
|
|
1191
|
+
# Also remove from cache if loaded (for consistency)
|
|
1192
|
+
if self._cache_loaded:
|
|
1193
|
+
async with self._cache_lock:
|
|
1194
|
+
self._snapshot_cache.pop(tracker_key, None)
|
|
1195
|
+
|
|
1196
|
+
logger.info(
|
|
1197
|
+
"Published tombstone for %s:%s",
|
|
1198
|
+
domain,
|
|
1199
|
+
entity_id,
|
|
1200
|
+
extra={"correlation_id": str(correlation_id)},
|
|
1201
|
+
)
|
|
1202
|
+
return True
|
|
1203
|
+
|
|
1204
|
+
except Exception:
|
|
1205
|
+
async with self._circuit_breaker_lock:
|
|
1206
|
+
await self._record_circuit_failure("delete_snapshot", correlation_id)
|
|
1207
|
+
|
|
1208
|
+
logger.exception(
|
|
1209
|
+
"Failed to publish tombstone for %s:%s",
|
|
1210
|
+
domain,
|
|
1211
|
+
entity_id,
|
|
1212
|
+
extra={"correlation_id": str(correlation_id)},
|
|
1213
|
+
)
|
|
1214
|
+
return False
|
|
1215
|
+
|
|
1216
|
+
async def publish_from_projection(
|
|
1217
|
+
self,
|
|
1218
|
+
projection: ModelRegistrationProjection,
|
|
1219
|
+
*,
|
|
1220
|
+
node_name: str | None = None,
|
|
1221
|
+
) -> ModelRegistrationSnapshot:
|
|
1222
|
+
"""Create and publish a snapshot from a projection.
|
|
1223
|
+
|
|
1224
|
+
Convenience method that handles version tracking automatically.
|
|
1225
|
+
Converts the projection to a snapshot model, assigns the next
|
|
1226
|
+
version number, and publishes to Kafka.
|
|
1227
|
+
|
|
1228
|
+
This is the recommended method for publishing snapshots when you
|
|
1229
|
+
have a projection and want automatic version management.
|
|
1230
|
+
|
|
1231
|
+
Args:
|
|
1232
|
+
projection: The projection to convert and publish
|
|
1233
|
+
node_name: Optional node name to include in snapshot.
|
|
1234
|
+
Not stored in projection, must be provided externally
|
|
1235
|
+
(e.g., from introspection data).
|
|
1236
|
+
|
|
1237
|
+
Returns:
|
|
1238
|
+
The published snapshot model with assigned version
|
|
1239
|
+
|
|
1240
|
+
Raises:
|
|
1241
|
+
InfraConnectionError: If Kafka connection fails
|
|
1242
|
+
InfraTimeoutError: If publish times out
|
|
1243
|
+
InfraUnavailableError: If circuit breaker is open
|
|
1244
|
+
|
|
1245
|
+
Example:
|
|
1246
|
+
>>> # Automatic versioning
|
|
1247
|
+
>>> snapshot1 = await publisher.publish_from_projection(proj)
|
|
1248
|
+
>>> print(snapshot1.snapshot_version) # 1
|
|
1249
|
+
>>>
|
|
1250
|
+
>>> # Next snapshot for same entity increments version
|
|
1251
|
+
>>> snapshot2 = await publisher.publish_from_projection(proj)
|
|
1252
|
+
>>> print(snapshot2.snapshot_version) # 2
|
|
1253
|
+
>>>
|
|
1254
|
+
>>> # Include node name for service discovery
|
|
1255
|
+
>>> snapshot = await publisher.publish_from_projection(
|
|
1256
|
+
... projection,
|
|
1257
|
+
... node_name="PostgresAdapter",
|
|
1258
|
+
... )
|
|
1259
|
+
"""
|
|
1260
|
+
entity_id_str = str(projection.entity_id)
|
|
1261
|
+
version = await self._get_next_version(entity_id_str, projection.domain)
|
|
1262
|
+
|
|
1263
|
+
# Create snapshot from projection
|
|
1264
|
+
snapshot = ModelRegistrationSnapshot.from_projection(
|
|
1265
|
+
projection=projection,
|
|
1266
|
+
snapshot_version=version,
|
|
1267
|
+
snapshot_created_at=datetime.now(UTC),
|
|
1268
|
+
node_name=node_name,
|
|
1269
|
+
)
|
|
1270
|
+
|
|
1271
|
+
# Publish the snapshot model
|
|
1272
|
+
await self._publish_snapshot_model(snapshot)
|
|
1273
|
+
|
|
1274
|
+
return snapshot
|
|
1275
|
+
|
|
1276
|
+
async def publish_snapshot_batch(
|
|
1277
|
+
self,
|
|
1278
|
+
snapshots: list[ModelRegistrationSnapshot],
|
|
1279
|
+
) -> int:
|
|
1280
|
+
"""Publish multiple pre-built snapshots in a batch.
|
|
1281
|
+
|
|
1282
|
+
Similar to publish_batch but for pre-built ModelRegistrationSnapshot
|
|
1283
|
+
objects instead of projections. Use this when you have already
|
|
1284
|
+
constructed snapshot models (e.g., from a different source).
|
|
1285
|
+
|
|
1286
|
+
Args:
|
|
1287
|
+
snapshots: List of snapshot models to publish
|
|
1288
|
+
|
|
1289
|
+
Returns:
|
|
1290
|
+
Count of successfully published snapshots
|
|
1291
|
+
|
|
1292
|
+
Example:
|
|
1293
|
+
>>> snapshots = [
|
|
1294
|
+
... ModelRegistrationSnapshot.from_projection(p, version=1, ...)
|
|
1295
|
+
... for p in projections
|
|
1296
|
+
... ]
|
|
1297
|
+
>>> count = await publisher.publish_snapshot_batch(snapshots)
|
|
1298
|
+
"""
|
|
1299
|
+
if not snapshots:
|
|
1300
|
+
return 0
|
|
1301
|
+
|
|
1302
|
+
success_count = 0
|
|
1303
|
+
for snapshot in snapshots:
|
|
1304
|
+
try:
|
|
1305
|
+
await self._publish_snapshot_model(snapshot)
|
|
1306
|
+
success_count += 1
|
|
1307
|
+
except (
|
|
1308
|
+
InfraConnectionError,
|
|
1309
|
+
InfraTimeoutError,
|
|
1310
|
+
InfraUnavailableError,
|
|
1311
|
+
) as e:
|
|
1312
|
+
logger.warning(
|
|
1313
|
+
"Failed to publish snapshot %s version %d: %s",
|
|
1314
|
+
snapshot.to_kafka_key(),
|
|
1315
|
+
snapshot.snapshot_version,
|
|
1316
|
+
str(e),
|
|
1317
|
+
)
|
|
1318
|
+
# Continue with remaining snapshots (best-effort)
|
|
1319
|
+
|
|
1320
|
+
logger.info(
|
|
1321
|
+
"Batch publish completed: %d/%d snapshots published",
|
|
1322
|
+
success_count,
|
|
1323
|
+
len(snapshots),
|
|
1324
|
+
extra={"topic": self._config.topic},
|
|
1325
|
+
)
|
|
1326
|
+
return success_count
|
|
1327
|
+
|
|
1328
|
+
|
|
1329
|
+
__all__: list[str] = ["SnapshotPublisherRegistration"]
|