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,923 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 OmniNode Team
|
|
3
|
+
# ruff: noqa: S608
|
|
4
|
+
# S608 disabled: All SQL f-strings use table_name which is validated via:
|
|
5
|
+
# 1. Pydantic regex pattern ^[a-zA-Z_][a-zA-Z0-9_]*$ in ModelPostgresIdempotencyStoreConfig
|
|
6
|
+
# 2. Runtime validation in _validate_table_name() (defense-in-depth)
|
|
7
|
+
# This defense-in-depth approach ensures only valid PostgreSQL identifiers are used,
|
|
8
|
+
# preventing SQL injection even if config validation is somehow bypassed.
|
|
9
|
+
"""PostgreSQL-based Idempotency Store Implementation.
|
|
10
|
+
|
|
11
|
+
This module provides a PostgreSQL-based implementation of the
|
|
12
|
+
ProtocolIdempotencyStore protocol for tracking processed messages
|
|
13
|
+
and preventing duplicate processing in distributed systems.
|
|
14
|
+
|
|
15
|
+
The store uses atomic INSERT ... ON CONFLICT DO NOTHING for thread-safe
|
|
16
|
+
idempotency checking and asyncpg for async database operations.
|
|
17
|
+
|
|
18
|
+
Table Schema:
|
|
19
|
+
CREATE TABLE IF NOT EXISTS idempotency_records (
|
|
20
|
+
id UUID PRIMARY KEY,
|
|
21
|
+
domain VARCHAR(255),
|
|
22
|
+
message_id UUID NOT NULL,
|
|
23
|
+
correlation_id UUID,
|
|
24
|
+
processed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
|
25
|
+
UNIQUE (domain, message_id)
|
|
26
|
+
);
|
|
27
|
+
CREATE INDEX IF NOT EXISTS idx_idempotency_processed_at ON idempotency_records(processed_at);
|
|
28
|
+
CREATE INDEX IF NOT EXISTS idx_idempotency_domain ON idempotency_records(domain);
|
|
29
|
+
CREATE INDEX IF NOT EXISTS idx_idempotency_correlation_id ON idempotency_records(correlation_id)
|
|
30
|
+
WHERE correlation_id IS NOT NULL;
|
|
31
|
+
|
|
32
|
+
Clock Skew Considerations:
|
|
33
|
+
In distributed systems, nodes may have slightly different system clocks.
|
|
34
|
+
This can cause issues with TTL-based cleanup:
|
|
35
|
+
|
|
36
|
+
Problem Scenario:
|
|
37
|
+
- Node A processes message at 10:00:01 (its clock)
|
|
38
|
+
- Node B's clock is 1 second behind (shows 10:00:00)
|
|
39
|
+
- Cleanup runs on Node B using now() - it might delete records
|
|
40
|
+
that Node A considers still valid
|
|
41
|
+
|
|
42
|
+
Solution:
|
|
43
|
+
The store applies a clock_skew_tolerance_seconds buffer (default: 60s)
|
|
44
|
+
to all TTL calculations during cleanup. The effective TTL becomes:
|
|
45
|
+
effective_ttl = ttl_seconds + clock_skew_tolerance_seconds
|
|
46
|
+
|
|
47
|
+
This ensures records are only cleaned up after ALL nodes in the
|
|
48
|
+
distributed system would consider them expired.
|
|
49
|
+
|
|
50
|
+
Production Recommendations:
|
|
51
|
+
1. Use NTP (Network Time Protocol) on all nodes to minimize clock drift
|
|
52
|
+
2. Set clock_skew_tolerance_seconds to at least the maximum expected
|
|
53
|
+
clock drift between nodes (default 60s is conservative for NTP-synced systems)
|
|
54
|
+
3. Monitor NTP synchronization status across your infrastructure
|
|
55
|
+
4. Consider higher tolerance values (e.g., 300s) for multi-datacenter deployments
|
|
56
|
+
|
|
57
|
+
Security Note:
|
|
58
|
+
- DSN contains credentials - never log the raw value
|
|
59
|
+
- Use parameterized queries to prevent SQL injection
|
|
60
|
+
- Connection pool handles credential management
|
|
61
|
+
- Table names are validated at both config and runtime level (defense-in-depth)
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
from __future__ import annotations
|
|
65
|
+
|
|
66
|
+
import asyncio
|
|
67
|
+
import logging
|
|
68
|
+
import re
|
|
69
|
+
from datetime import UTC, datetime
|
|
70
|
+
from uuid import UUID, uuid4
|
|
71
|
+
|
|
72
|
+
import asyncpg
|
|
73
|
+
|
|
74
|
+
from omnibase_infra.enums import EnumInfraTransportType
|
|
75
|
+
from omnibase_infra.errors import (
|
|
76
|
+
InfraConnectionError,
|
|
77
|
+
InfraTimeoutError,
|
|
78
|
+
ModelInfraErrorContext,
|
|
79
|
+
ModelTimeoutErrorContext,
|
|
80
|
+
ProtocolConfigurationError,
|
|
81
|
+
RuntimeHostError,
|
|
82
|
+
)
|
|
83
|
+
from omnibase_infra.idempotency.models import (
|
|
84
|
+
ModelIdempotencyStoreHealthCheckResult,
|
|
85
|
+
ModelIdempotencyStoreMetrics,
|
|
86
|
+
ModelPostgresIdempotencyStoreConfig,
|
|
87
|
+
)
|
|
88
|
+
from omnibase_infra.idempotency.protocol_idempotency_store import (
|
|
89
|
+
ProtocolIdempotencyStore,
|
|
90
|
+
)
|
|
91
|
+
from omnibase_infra.utils.util_datetime import is_timezone_aware
|
|
92
|
+
|
|
93
|
+
logger = logging.getLogger(__name__)
|
|
94
|
+
|
|
95
|
+
# Regex pattern for valid PostgreSQL table names (defense-in-depth validation)
|
|
96
|
+
# Must start with letter or underscore, followed by letters, digits, or underscores
|
|
97
|
+
_TABLE_NAME_PATTERN = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_]*$")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class StoreIdempotencyPostgres(ProtocolIdempotencyStore):
|
|
101
|
+
"""PostgreSQL-based idempotency store using asyncpg connection pool.
|
|
102
|
+
|
|
103
|
+
This implementation provides exactly-once semantics by using PostgreSQL's
|
|
104
|
+
INSERT ... ON CONFLICT DO NOTHING pattern for atomic check-and-record
|
|
105
|
+
operations.
|
|
106
|
+
|
|
107
|
+
Features:
|
|
108
|
+
- Atomic check_and_record using INSERT ON CONFLICT
|
|
109
|
+
- Connection pooling via asyncpg
|
|
110
|
+
- TTL-based cleanup for expired records
|
|
111
|
+
- Composite key (domain, message_id) for domain-isolated deduplication
|
|
112
|
+
- Full correlation ID support for distributed tracing
|
|
113
|
+
- Defense-in-depth table name validation for SQL injection prevention
|
|
114
|
+
|
|
115
|
+
Security:
|
|
116
|
+
Table names are validated at two levels:
|
|
117
|
+
1. Pydantic config model: regex pattern on table_name field
|
|
118
|
+
2. Runtime validation: _validate_table_name() in __init__
|
|
119
|
+
|
|
120
|
+
This defense-in-depth approach ensures SQL injection prevention even
|
|
121
|
+
if the config validation is somehow bypassed (e.g., through direct
|
|
122
|
+
attribute assignment or deserialization from untrusted sources).
|
|
123
|
+
|
|
124
|
+
Concurrency Safety:
|
|
125
|
+
This store is coroutine-safe for asyncio concurrent access. The
|
|
126
|
+
underlying asyncpg pool handles connection management and concurrent
|
|
127
|
+
coroutine access safely. All metrics updates are protected by
|
|
128
|
+
``_metrics_lock`` (asyncio.Lock) to ensure atomic read-modify-write
|
|
129
|
+
operations for observability counters.
|
|
130
|
+
|
|
131
|
+
Note: This is not thread-safe. For multi-threaded access, additional
|
|
132
|
+
synchronization would be required (e.g., threading.Lock or
|
|
133
|
+
thread-safe connection pooling).
|
|
134
|
+
|
|
135
|
+
Example:
|
|
136
|
+
>>> from uuid import uuid4
|
|
137
|
+
>>> config = ModelPostgresIdempotencyStoreConfig(
|
|
138
|
+
... dsn="postgresql://user:pass@localhost:5432/mydb",
|
|
139
|
+
... table_name="idempotency_records",
|
|
140
|
+
... )
|
|
141
|
+
>>> store = StoreIdempotencyPostgres(config)
|
|
142
|
+
>>> await store.initialize()
|
|
143
|
+
>>> try:
|
|
144
|
+
... is_new = await store.check_and_record(
|
|
145
|
+
... message_id=uuid4(),
|
|
146
|
+
... domain="registration",
|
|
147
|
+
... )
|
|
148
|
+
... if is_new:
|
|
149
|
+
... print("Processing message...")
|
|
150
|
+
... finally:
|
|
151
|
+
... await store.shutdown()
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
def __init__(self, config: ModelPostgresIdempotencyStoreConfig) -> None:
|
|
155
|
+
"""Initialize the PostgreSQL idempotency store.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
config: Configuration model containing DSN, pool settings, and TTL options.
|
|
159
|
+
|
|
160
|
+
Raises:
|
|
161
|
+
ProtocolConfigurationError: If table_name contains invalid characters
|
|
162
|
+
(defense-in-depth validation for SQL injection prevention).
|
|
163
|
+
"""
|
|
164
|
+
self._config = config
|
|
165
|
+
self._pool: asyncpg.Pool | None = None
|
|
166
|
+
self._initialized: bool = False
|
|
167
|
+
self._metrics = ModelIdempotencyStoreMetrics()
|
|
168
|
+
self._metrics_lock = asyncio.Lock()
|
|
169
|
+
|
|
170
|
+
# Defense-in-depth: Validate table name at runtime even though
|
|
171
|
+
# Pydantic config already validates it. This protects against:
|
|
172
|
+
# - Direct attribute assignment bypassing Pydantic validation
|
|
173
|
+
# - Deserialization from untrusted sources
|
|
174
|
+
# - Future code changes that might bypass config validation
|
|
175
|
+
self._validate_table_name(config.table_name)
|
|
176
|
+
|
|
177
|
+
@property
|
|
178
|
+
def is_initialized(self) -> bool:
|
|
179
|
+
"""Return True if the store has been initialized."""
|
|
180
|
+
return self._initialized
|
|
181
|
+
|
|
182
|
+
async def get_metrics(self) -> ModelIdempotencyStoreMetrics:
|
|
183
|
+
"""Get current store metrics for observability.
|
|
184
|
+
|
|
185
|
+
Returns a copy of the current metrics to prevent external mutation.
|
|
186
|
+
Metrics include:
|
|
187
|
+
- total_checks: Total check_and_record calls
|
|
188
|
+
- duplicate_count: Number of duplicates detected
|
|
189
|
+
- error_count: Number of failed checks
|
|
190
|
+
- duplicate_rate: Ratio of duplicates to total checks
|
|
191
|
+
- error_rate: Ratio of errors to total checks
|
|
192
|
+
- total_cleanup_deleted: Total records cleaned up
|
|
193
|
+
- last_cleanup_deleted: Records deleted in last cleanup
|
|
194
|
+
- last_cleanup_at: Timestamp of last cleanup
|
|
195
|
+
|
|
196
|
+
Concurrency Safety:
|
|
197
|
+
This method acquires ``_metrics_lock`` (asyncio.Lock) to return
|
|
198
|
+
a consistent snapshot. Safe for concurrent coroutine access.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
Copy of current metrics.
|
|
202
|
+
"""
|
|
203
|
+
async with self._metrics_lock:
|
|
204
|
+
return self._metrics.model_copy()
|
|
205
|
+
|
|
206
|
+
def _validate_table_name(self, table_name: str) -> None:
|
|
207
|
+
"""Validate table name for SQL injection prevention (defense-in-depth).
|
|
208
|
+
|
|
209
|
+
This method provides runtime validation of the table name pattern,
|
|
210
|
+
complementing the Pydantic field validation in the config model.
|
|
211
|
+
Together they form a defense-in-depth approach to prevent SQL injection.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
table_name: The table name to validate.
|
|
215
|
+
|
|
216
|
+
Raises:
|
|
217
|
+
ProtocolConfigurationError: If table_name doesn't match the
|
|
218
|
+
expected pattern ^[a-zA-Z_][a-zA-Z0-9_]*$
|
|
219
|
+
"""
|
|
220
|
+
if not _TABLE_NAME_PATTERN.match(table_name):
|
|
221
|
+
context = ModelInfraErrorContext.with_correlation(
|
|
222
|
+
transport_type=EnumInfraTransportType.DATABASE,
|
|
223
|
+
operation="validate_table_name",
|
|
224
|
+
target_name="postgres_idempotency_store",
|
|
225
|
+
)
|
|
226
|
+
raise ProtocolConfigurationError(
|
|
227
|
+
f"Invalid table name: {table_name}. "
|
|
228
|
+
"Must match pattern ^[a-zA-Z_][a-zA-Z0-9_]*$ "
|
|
229
|
+
"(letters, digits, underscores only, must start with letter or underscore)",
|
|
230
|
+
context=context,
|
|
231
|
+
parameter="table_name",
|
|
232
|
+
value=table_name,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
async def initialize(self) -> None:
|
|
236
|
+
"""Initialize the connection pool and ensure table exists.
|
|
237
|
+
|
|
238
|
+
Creates the asyncpg connection pool and verifies (or creates)
|
|
239
|
+
the idempotency_records table with proper schema.
|
|
240
|
+
|
|
241
|
+
Raises:
|
|
242
|
+
InfraConnectionError: If database connection fails.
|
|
243
|
+
RuntimeHostError: If pool creation or table setup fails.
|
|
244
|
+
"""
|
|
245
|
+
if self._initialized:
|
|
246
|
+
return
|
|
247
|
+
|
|
248
|
+
correlation_id = uuid4()
|
|
249
|
+
context = ModelInfraErrorContext(
|
|
250
|
+
transport_type=EnumInfraTransportType.DATABASE,
|
|
251
|
+
operation="initialize",
|
|
252
|
+
target_name="postgres_idempotency_store",
|
|
253
|
+
correlation_id=correlation_id,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
try:
|
|
257
|
+
self._pool = await asyncpg.create_pool(
|
|
258
|
+
dsn=self._config.dsn,
|
|
259
|
+
min_size=self._config.pool_min_size,
|
|
260
|
+
max_size=self._config.pool_max_size,
|
|
261
|
+
command_timeout=self._config.command_timeout,
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
# Ensure table exists with proper schema
|
|
265
|
+
await self._ensure_table_exists()
|
|
266
|
+
|
|
267
|
+
self._initialized = True
|
|
268
|
+
logger.info(
|
|
269
|
+
"StoreIdempotencyPostgres initialized",
|
|
270
|
+
extra={
|
|
271
|
+
"table_name": self._config.table_name,
|
|
272
|
+
"pool_min_size": self._config.pool_min_size,
|
|
273
|
+
"pool_max_size": self._config.pool_max_size,
|
|
274
|
+
},
|
|
275
|
+
)
|
|
276
|
+
except asyncpg.InvalidPasswordError as e:
|
|
277
|
+
# Clean up pool if it was created before table setup failed
|
|
278
|
+
if self._pool is not None:
|
|
279
|
+
await self._pool.close()
|
|
280
|
+
self._pool = None
|
|
281
|
+
raise InfraConnectionError(
|
|
282
|
+
"Database authentication failed - check credentials",
|
|
283
|
+
context=context,
|
|
284
|
+
) from e
|
|
285
|
+
except asyncpg.InvalidCatalogNameError as e:
|
|
286
|
+
# Clean up pool if it was created before table setup failed
|
|
287
|
+
if self._pool is not None:
|
|
288
|
+
await self._pool.close()
|
|
289
|
+
self._pool = None
|
|
290
|
+
raise InfraConnectionError(
|
|
291
|
+
"Database not found - check database name",
|
|
292
|
+
context=context,
|
|
293
|
+
) from e
|
|
294
|
+
except OSError as e:
|
|
295
|
+
# Clean up pool if it was created before table setup failed
|
|
296
|
+
if self._pool is not None:
|
|
297
|
+
await self._pool.close()
|
|
298
|
+
self._pool = None
|
|
299
|
+
raise InfraConnectionError(
|
|
300
|
+
"Failed to connect to database - check host and port",
|
|
301
|
+
context=context,
|
|
302
|
+
) from e
|
|
303
|
+
except Exception as e:
|
|
304
|
+
# Clean up pool if it was created before table setup failed
|
|
305
|
+
if self._pool is not None:
|
|
306
|
+
await self._pool.close()
|
|
307
|
+
self._pool = None
|
|
308
|
+
raise RuntimeHostError(
|
|
309
|
+
f"Failed to initialize idempotency store: {type(e).__name__}",
|
|
310
|
+
context=context,
|
|
311
|
+
) from e
|
|
312
|
+
|
|
313
|
+
async def _ensure_table_exists(self) -> None:
|
|
314
|
+
"""Create the idempotency table if it doesn't exist.
|
|
315
|
+
|
|
316
|
+
Creates the table with:
|
|
317
|
+
- UUID primary key
|
|
318
|
+
- Composite unique constraint on (domain, message_id)
|
|
319
|
+
- Index on processed_at for efficient TTL cleanup
|
|
320
|
+
- Index on domain for efficient is_processed queries
|
|
321
|
+
- Partial index on correlation_id for distributed tracing queries
|
|
322
|
+
"""
|
|
323
|
+
if self._pool is None:
|
|
324
|
+
raise RuntimeHostError(
|
|
325
|
+
"Pool not initialized - call initialize() first",
|
|
326
|
+
context=ModelInfraErrorContext.with_correlation(
|
|
327
|
+
transport_type=EnumInfraTransportType.DATABASE,
|
|
328
|
+
operation="_ensure_table_exists",
|
|
329
|
+
target_name="postgres_idempotency_store",
|
|
330
|
+
),
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
# Note: Table name is validated in config (alphanumeric + underscore only)
|
|
334
|
+
# so safe to use in SQL. We still use parameterized queries for data values.
|
|
335
|
+
create_table_sql = f"""
|
|
336
|
+
CREATE TABLE IF NOT EXISTS {self._config.table_name} (
|
|
337
|
+
id UUID PRIMARY KEY,
|
|
338
|
+
domain VARCHAR(255),
|
|
339
|
+
message_id UUID NOT NULL,
|
|
340
|
+
correlation_id UUID,
|
|
341
|
+
processed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
|
342
|
+
UNIQUE (domain, message_id)
|
|
343
|
+
)
|
|
344
|
+
"""
|
|
345
|
+
|
|
346
|
+
create_processed_at_index_sql = f"""
|
|
347
|
+
CREATE INDEX IF NOT EXISTS idx_{self._config.table_name}_processed_at
|
|
348
|
+
ON {self._config.table_name}(processed_at)
|
|
349
|
+
"""
|
|
350
|
+
|
|
351
|
+
# Index on domain for efficient is_processed queries filtering by domain
|
|
352
|
+
create_domain_index_sql = f"""
|
|
353
|
+
CREATE INDEX IF NOT EXISTS idx_{self._config.table_name}_domain
|
|
354
|
+
ON {self._config.table_name}(domain)
|
|
355
|
+
"""
|
|
356
|
+
|
|
357
|
+
# Partial index on correlation_id for distributed tracing queries
|
|
358
|
+
# Uses partial index (WHERE NOT NULL) to save space since correlation_id is optional
|
|
359
|
+
create_correlation_index_sql = f"""
|
|
360
|
+
CREATE INDEX IF NOT EXISTS idx_{self._config.table_name}_correlation_id
|
|
361
|
+
ON {self._config.table_name}(correlation_id)
|
|
362
|
+
WHERE correlation_id IS NOT NULL
|
|
363
|
+
"""
|
|
364
|
+
|
|
365
|
+
async with self._pool.acquire() as conn:
|
|
366
|
+
await conn.execute(create_table_sql)
|
|
367
|
+
await conn.execute(create_processed_at_index_sql)
|
|
368
|
+
await conn.execute(create_domain_index_sql)
|
|
369
|
+
await conn.execute(create_correlation_index_sql)
|
|
370
|
+
|
|
371
|
+
async def shutdown(self) -> None:
|
|
372
|
+
"""Close the connection pool and release resources."""
|
|
373
|
+
if self._pool is not None:
|
|
374
|
+
await self._pool.close()
|
|
375
|
+
self._pool = None
|
|
376
|
+
self._initialized = False
|
|
377
|
+
logger.info("StoreIdempotencyPostgres shutdown complete")
|
|
378
|
+
|
|
379
|
+
async def check_and_record(
|
|
380
|
+
self,
|
|
381
|
+
message_id: UUID,
|
|
382
|
+
domain: str | None = None,
|
|
383
|
+
correlation_id: UUID | None = None,
|
|
384
|
+
) -> bool:
|
|
385
|
+
"""Atomically check if message was processed and record if not.
|
|
386
|
+
|
|
387
|
+
Uses INSERT ... ON CONFLICT DO NOTHING for atomic operation:
|
|
388
|
+
- If insert succeeds, message is new -> return True
|
|
389
|
+
- If insert conflicts, message is duplicate -> return False
|
|
390
|
+
|
|
391
|
+
Args:
|
|
392
|
+
message_id: Unique identifier for the message.
|
|
393
|
+
domain: Optional domain namespace for isolated deduplication.
|
|
394
|
+
correlation_id: Optional correlation ID for tracing.
|
|
395
|
+
|
|
396
|
+
Returns:
|
|
397
|
+
True if message is new (should be processed).
|
|
398
|
+
False if message is duplicate (should be skipped).
|
|
399
|
+
|
|
400
|
+
Raises:
|
|
401
|
+
InfraConnectionError: If database connection fails.
|
|
402
|
+
InfraTimeoutError: If operation times out.
|
|
403
|
+
RuntimeHostError: If store is not initialized.
|
|
404
|
+
"""
|
|
405
|
+
op_correlation_id = correlation_id or uuid4()
|
|
406
|
+
context = ModelInfraErrorContext(
|
|
407
|
+
transport_type=EnumInfraTransportType.DATABASE,
|
|
408
|
+
operation="check_and_record",
|
|
409
|
+
target_name="postgres_idempotency_store",
|
|
410
|
+
correlation_id=op_correlation_id,
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
if not self._initialized or self._pool is None:
|
|
414
|
+
raise RuntimeHostError(
|
|
415
|
+
"Store not initialized - call initialize() first",
|
|
416
|
+
context=context,
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
record_id = uuid4()
|
|
420
|
+
processed_at = datetime.now(UTC)
|
|
421
|
+
|
|
422
|
+
# INSERT ... ON CONFLICT DO NOTHING returns affected row count
|
|
423
|
+
# 1 = insert succeeded (new message), 0 = conflict (duplicate)
|
|
424
|
+
# table_name is validated via regex in ModelPostgresIdempotencyStoreConfig
|
|
425
|
+
insert_sql = f""" # noqa: S608
|
|
426
|
+
INSERT INTO {self._config.table_name}
|
|
427
|
+
(id, domain, message_id, correlation_id, processed_at)
|
|
428
|
+
VALUES ($1, $2, $3, $4, $5)
|
|
429
|
+
ON CONFLICT (domain, message_id) DO NOTHING
|
|
430
|
+
"""
|
|
431
|
+
|
|
432
|
+
is_new: bool = False # Initialize before try block for finally access
|
|
433
|
+
metrics_updated: bool = False # Track if exception handler updated metrics
|
|
434
|
+
try:
|
|
435
|
+
async with self._pool.acquire() as conn:
|
|
436
|
+
result = await conn.execute(
|
|
437
|
+
insert_sql,
|
|
438
|
+
record_id,
|
|
439
|
+
domain,
|
|
440
|
+
message_id,
|
|
441
|
+
correlation_id,
|
|
442
|
+
processed_at,
|
|
443
|
+
)
|
|
444
|
+
# asyncpg returns "INSERT 0 1" for success, "INSERT 0 0" for conflict
|
|
445
|
+
is_new = str(result).endswith(" 1")
|
|
446
|
+
|
|
447
|
+
if is_new:
|
|
448
|
+
logger.debug(
|
|
449
|
+
"Recorded new message",
|
|
450
|
+
extra={
|
|
451
|
+
"message_id": str(message_id),
|
|
452
|
+
"domain": domain,
|
|
453
|
+
"correlation_id": str(correlation_id)
|
|
454
|
+
if correlation_id
|
|
455
|
+
else None,
|
|
456
|
+
},
|
|
457
|
+
)
|
|
458
|
+
else:
|
|
459
|
+
logger.debug(
|
|
460
|
+
"Duplicate message detected",
|
|
461
|
+
extra={
|
|
462
|
+
"message_id": str(message_id),
|
|
463
|
+
"domain": domain,
|
|
464
|
+
},
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
return is_new
|
|
468
|
+
|
|
469
|
+
except asyncpg.QueryCanceledError as e:
|
|
470
|
+
async with self._metrics_lock:
|
|
471
|
+
self._metrics.total_checks += 1
|
|
472
|
+
self._metrics.error_count += 1
|
|
473
|
+
metrics_updated = True
|
|
474
|
+
raise InfraTimeoutError(
|
|
475
|
+
f"Check and record timed out after {self._config.command_timeout}s",
|
|
476
|
+
context=ModelTimeoutErrorContext(
|
|
477
|
+
transport_type=context.transport_type,
|
|
478
|
+
operation=context.operation,
|
|
479
|
+
target_name=context.target_name,
|
|
480
|
+
correlation_id=context.correlation_id,
|
|
481
|
+
timeout_seconds=self._config.command_timeout,
|
|
482
|
+
),
|
|
483
|
+
) from e
|
|
484
|
+
except asyncpg.PostgresConnectionError as e:
|
|
485
|
+
async with self._metrics_lock:
|
|
486
|
+
self._metrics.total_checks += 1
|
|
487
|
+
self._metrics.error_count += 1
|
|
488
|
+
metrics_updated = True
|
|
489
|
+
raise InfraConnectionError(
|
|
490
|
+
"Database connection lost during check_and_record",
|
|
491
|
+
context=context,
|
|
492
|
+
) from e
|
|
493
|
+
except asyncpg.PostgresError as e:
|
|
494
|
+
async with self._metrics_lock:
|
|
495
|
+
self._metrics.total_checks += 1
|
|
496
|
+
self._metrics.error_count += 1
|
|
497
|
+
metrics_updated = True
|
|
498
|
+
raise RuntimeHostError(
|
|
499
|
+
f"Database error during check_and_record: {type(e).__name__}",
|
|
500
|
+
context=context,
|
|
501
|
+
) from e
|
|
502
|
+
finally:
|
|
503
|
+
# Always update metrics for success path, even if logging fails.
|
|
504
|
+
# Exception handlers above set metrics_updated=True before re-raising,
|
|
505
|
+
# so we only update here for the success path (no exception caught).
|
|
506
|
+
if not metrics_updated:
|
|
507
|
+
async with self._metrics_lock:
|
|
508
|
+
self._metrics.total_checks += 1
|
|
509
|
+
if not is_new:
|
|
510
|
+
self._metrics.duplicate_count += 1
|
|
511
|
+
|
|
512
|
+
async def is_processed(
|
|
513
|
+
self,
|
|
514
|
+
message_id: UUID,
|
|
515
|
+
domain: str | None = None,
|
|
516
|
+
) -> bool:
|
|
517
|
+
"""Check if a message was already processed (read-only).
|
|
518
|
+
|
|
519
|
+
This is a read-only query that does not modify the store.
|
|
520
|
+
|
|
521
|
+
Args:
|
|
522
|
+
message_id: Unique identifier for the message.
|
|
523
|
+
domain: Optional domain namespace.
|
|
524
|
+
|
|
525
|
+
Returns:
|
|
526
|
+
True if the message has been processed.
|
|
527
|
+
False if the message has not been processed or has expired.
|
|
528
|
+
|
|
529
|
+
Raises:
|
|
530
|
+
InfraConnectionError: If database connection fails.
|
|
531
|
+
InfraTimeoutError: If query times out.
|
|
532
|
+
RuntimeHostError: If store is not initialized.
|
|
533
|
+
"""
|
|
534
|
+
context = ModelInfraErrorContext.with_correlation(
|
|
535
|
+
transport_type=EnumInfraTransportType.DATABASE,
|
|
536
|
+
operation="is_processed",
|
|
537
|
+
target_name="postgres_idempotency_store",
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
if not self._initialized or self._pool is None:
|
|
541
|
+
raise RuntimeHostError(
|
|
542
|
+
"Store not initialized - call initialize() first",
|
|
543
|
+
context=context,
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
# table_name is validated via regex in ModelPostgresIdempotencyStoreConfig
|
|
547
|
+
query_sql = f""" # noqa: S608
|
|
548
|
+
SELECT 1 FROM {self._config.table_name}
|
|
549
|
+
WHERE domain IS NOT DISTINCT FROM $1 AND message_id = $2
|
|
550
|
+
LIMIT 1
|
|
551
|
+
"""
|
|
552
|
+
|
|
553
|
+
try:
|
|
554
|
+
async with self._pool.acquire() as conn:
|
|
555
|
+
row = await conn.fetchrow(query_sql, domain, message_id)
|
|
556
|
+
return row is not None
|
|
557
|
+
|
|
558
|
+
except asyncpg.QueryCanceledError as e:
|
|
559
|
+
raise InfraTimeoutError(
|
|
560
|
+
f"Processed check query timed out after {self._config.command_timeout}s",
|
|
561
|
+
context=ModelTimeoutErrorContext(
|
|
562
|
+
transport_type=context.transport_type,
|
|
563
|
+
operation=context.operation,
|
|
564
|
+
target_name=context.target_name,
|
|
565
|
+
correlation_id=context.correlation_id,
|
|
566
|
+
timeout_seconds=self._config.command_timeout,
|
|
567
|
+
),
|
|
568
|
+
) from e
|
|
569
|
+
except asyncpg.PostgresConnectionError as e:
|
|
570
|
+
raise InfraConnectionError(
|
|
571
|
+
"Database connection lost during processed check query",
|
|
572
|
+
context=context,
|
|
573
|
+
) from e
|
|
574
|
+
except asyncpg.PostgresError as e:
|
|
575
|
+
raise RuntimeHostError(
|
|
576
|
+
f"Database error during is_processed: {type(e).__name__}",
|
|
577
|
+
context=context,
|
|
578
|
+
) from e
|
|
579
|
+
|
|
580
|
+
async def mark_processed(
|
|
581
|
+
self,
|
|
582
|
+
message_id: UUID,
|
|
583
|
+
domain: str | None = None,
|
|
584
|
+
correlation_id: UUID | None = None,
|
|
585
|
+
processed_at: datetime | None = None,
|
|
586
|
+
) -> None:
|
|
587
|
+
"""Mark a message as processed (upsert).
|
|
588
|
+
|
|
589
|
+
Records a message as processed. If the record already exists,
|
|
590
|
+
updates the processed_at timestamp.
|
|
591
|
+
|
|
592
|
+
Args:
|
|
593
|
+
message_id: Unique identifier for the message.
|
|
594
|
+
domain: Optional domain namespace for isolated deduplication.
|
|
595
|
+
correlation_id: Optional correlation ID for tracing.
|
|
596
|
+
processed_at: Optional timestamp. If None, uses datetime.now(timezone.utc).
|
|
597
|
+
Must be timezone-aware.
|
|
598
|
+
|
|
599
|
+
Raises:
|
|
600
|
+
InfraConnectionError: If database connection fails.
|
|
601
|
+
InfraTimeoutError: If operation times out.
|
|
602
|
+
RuntimeHostError: If store is not initialized or if processed_at
|
|
603
|
+
is a naive (timezone-unaware) datetime.
|
|
604
|
+
"""
|
|
605
|
+
op_correlation_id = correlation_id or uuid4()
|
|
606
|
+
context = ModelInfraErrorContext(
|
|
607
|
+
transport_type=EnumInfraTransportType.DATABASE,
|
|
608
|
+
operation="mark_processed",
|
|
609
|
+
target_name="postgres_idempotency_store",
|
|
610
|
+
correlation_id=op_correlation_id,
|
|
611
|
+
)
|
|
612
|
+
|
|
613
|
+
if not self._initialized or self._pool is None:
|
|
614
|
+
raise RuntimeHostError(
|
|
615
|
+
"Store not initialized - call initialize() first",
|
|
616
|
+
context=context,
|
|
617
|
+
)
|
|
618
|
+
|
|
619
|
+
# Validate timezone awareness - fail fast on naive datetime
|
|
620
|
+
# Note: This guards against external callers passing naive datetimes.
|
|
621
|
+
# Our internal datetime.now(UTC) is always timezone-aware.
|
|
622
|
+
if processed_at is not None and not is_timezone_aware(processed_at):
|
|
623
|
+
raise RuntimeHostError(
|
|
624
|
+
"processed_at must be timezone-aware (got naive datetime)",
|
|
625
|
+
context=context,
|
|
626
|
+
)
|
|
627
|
+
|
|
628
|
+
effective_processed_at = processed_at or datetime.now(UTC)
|
|
629
|
+
record_id = uuid4()
|
|
630
|
+
|
|
631
|
+
# Use ON CONFLICT ... DO UPDATE to ensure idempotent upsert
|
|
632
|
+
# table_name is validated via regex in ModelPostgresIdempotencyStoreConfig
|
|
633
|
+
upsert_sql = f""" # noqa: S608
|
|
634
|
+
INSERT INTO {self._config.table_name}
|
|
635
|
+
(id, domain, message_id, correlation_id, processed_at)
|
|
636
|
+
VALUES ($1, $2, $3, $4, $5)
|
|
637
|
+
ON CONFLICT (domain, message_id) DO UPDATE
|
|
638
|
+
SET processed_at = EXCLUDED.processed_at,
|
|
639
|
+
correlation_id = COALESCE(EXCLUDED.correlation_id, {self._config.table_name}.correlation_id)
|
|
640
|
+
"""
|
|
641
|
+
|
|
642
|
+
try:
|
|
643
|
+
async with self._pool.acquire() as conn:
|
|
644
|
+
await conn.execute(
|
|
645
|
+
upsert_sql,
|
|
646
|
+
record_id,
|
|
647
|
+
domain,
|
|
648
|
+
message_id,
|
|
649
|
+
correlation_id,
|
|
650
|
+
effective_processed_at,
|
|
651
|
+
)
|
|
652
|
+
logger.debug(
|
|
653
|
+
"Marked message as processed",
|
|
654
|
+
extra={
|
|
655
|
+
"message_id": str(message_id),
|
|
656
|
+
"domain": domain,
|
|
657
|
+
"correlation_id": str(correlation_id)
|
|
658
|
+
if correlation_id
|
|
659
|
+
else None,
|
|
660
|
+
},
|
|
661
|
+
)
|
|
662
|
+
|
|
663
|
+
except asyncpg.QueryCanceledError as e:
|
|
664
|
+
raise InfraTimeoutError(
|
|
665
|
+
f"Mark processed timed out after {self._config.command_timeout}s",
|
|
666
|
+
context=ModelTimeoutErrorContext(
|
|
667
|
+
transport_type=context.transport_type,
|
|
668
|
+
operation=context.operation,
|
|
669
|
+
target_name=context.target_name,
|
|
670
|
+
correlation_id=context.correlation_id,
|
|
671
|
+
timeout_seconds=self._config.command_timeout,
|
|
672
|
+
),
|
|
673
|
+
) from e
|
|
674
|
+
except asyncpg.PostgresConnectionError as e:
|
|
675
|
+
raise InfraConnectionError(
|
|
676
|
+
"Database connection lost during mark processed",
|
|
677
|
+
context=context,
|
|
678
|
+
) from e
|
|
679
|
+
except asyncpg.PostgresError as e:
|
|
680
|
+
raise RuntimeHostError(
|
|
681
|
+
f"Database error during mark_processed: {type(e).__name__}",
|
|
682
|
+
context=context,
|
|
683
|
+
) from e
|
|
684
|
+
|
|
685
|
+
async def cleanup_expired(
|
|
686
|
+
self,
|
|
687
|
+
ttl_seconds: int,
|
|
688
|
+
batch_size: int | None = None,
|
|
689
|
+
max_iterations: int | None = None,
|
|
690
|
+
) -> int:
|
|
691
|
+
"""Remove entries older than TTL using batched deletion with clock skew tolerance.
|
|
692
|
+
|
|
693
|
+
Cleans up old idempotency records based on processed_at timestamp.
|
|
694
|
+
Uses batched deletion to reduce lock contention on high-volume tables.
|
|
695
|
+
|
|
696
|
+
Batched Deletion Benefits:
|
|
697
|
+
- Reduces lock contention by breaking large deletes into smaller
|
|
698
|
+
transactions
|
|
699
|
+
- Prevents long-running transactions that can block other operations
|
|
700
|
+
- Allows other database operations to interleave between batches
|
|
701
|
+
- Limits transaction log growth by committing in smaller chunks
|
|
702
|
+
|
|
703
|
+
Clock Skew Handling:
|
|
704
|
+
In distributed systems, nodes may have slightly different system clocks.
|
|
705
|
+
To prevent premature deletion of records that some nodes may still
|
|
706
|
+
consider valid, we add a clock_skew_tolerance_seconds buffer to the TTL.
|
|
707
|
+
|
|
708
|
+
Example scenario without tolerance:
|
|
709
|
+
- Node A processes message at 10:00:01 (its clock)
|
|
710
|
+
- Node B checks for duplicate at 10:00:00 (its clock is 1 second behind)
|
|
711
|
+
- If cleanup runs on Node B using now(), it might delete records
|
|
712
|
+
that Node A just created (from Node B's perspective, they're from
|
|
713
|
+
the "future")
|
|
714
|
+
|
|
715
|
+
With tolerance, effective_ttl = ttl_seconds + clock_skew_tolerance_seconds,
|
|
716
|
+
ensuring records are only cleaned up after ALL nodes would consider them
|
|
717
|
+
expired.
|
|
718
|
+
|
|
719
|
+
Args:
|
|
720
|
+
ttl_seconds: Time-to-live in seconds. Records older than
|
|
721
|
+
(ttl_seconds + clock_skew_tolerance_seconds) are removed.
|
|
722
|
+
batch_size: Number of records to delete per batch. Defaults to
|
|
723
|
+
config.cleanup_batch_size (10000). Use larger values for faster
|
|
724
|
+
cleanup at the cost of longer locks, smaller values for better
|
|
725
|
+
concurrency.
|
|
726
|
+
max_iterations: Maximum number of batch iterations. Defaults to
|
|
727
|
+
config.cleanup_max_iterations (100). Prevents runaway cleanup
|
|
728
|
+
loops. Total max records = batch_size * max_iterations.
|
|
729
|
+
|
|
730
|
+
Returns:
|
|
731
|
+
Total number of entries removed across all batches.
|
|
732
|
+
|
|
733
|
+
Raises:
|
|
734
|
+
InfraConnectionError: If database connection fails.
|
|
735
|
+
InfraTimeoutError: If cleanup times out.
|
|
736
|
+
RuntimeHostError: If store is not initialized.
|
|
737
|
+
|
|
738
|
+
Example:
|
|
739
|
+
>>> # Standard cleanup using config defaults
|
|
740
|
+
>>> removed = await store.cleanup_expired(ttl_seconds=86400)
|
|
741
|
+
>>> print(f"Removed {removed} expired records")
|
|
742
|
+
|
|
743
|
+
>>> # High-concurrency system: smaller batches
|
|
744
|
+
>>> removed = await store.cleanup_expired(
|
|
745
|
+
... ttl_seconds=86400,
|
|
746
|
+
... batch_size=1000,
|
|
747
|
+
... )
|
|
748
|
+
|
|
749
|
+
>>> # Bulk cleanup with large batches
|
|
750
|
+
>>> removed = await store.cleanup_expired(
|
|
751
|
+
... ttl_seconds=86400,
|
|
752
|
+
... batch_size=50000,
|
|
753
|
+
... max_iterations=10,
|
|
754
|
+
... )
|
|
755
|
+
"""
|
|
756
|
+
context = ModelInfraErrorContext.with_correlation(
|
|
757
|
+
transport_type=EnumInfraTransportType.DATABASE,
|
|
758
|
+
operation="cleanup_expired",
|
|
759
|
+
target_name="postgres_idempotency_store",
|
|
760
|
+
)
|
|
761
|
+
|
|
762
|
+
if not self._initialized or self._pool is None:
|
|
763
|
+
raise RuntimeHostError(
|
|
764
|
+
"Store not initialized - call initialize() first",
|
|
765
|
+
context=context,
|
|
766
|
+
)
|
|
767
|
+
|
|
768
|
+
# Use config defaults if not specified
|
|
769
|
+
effective_batch_size = batch_size or self._config.cleanup_batch_size
|
|
770
|
+
effective_max_iterations = max_iterations or self._config.cleanup_max_iterations
|
|
771
|
+
|
|
772
|
+
# Apply clock skew tolerance to prevent premature deletion
|
|
773
|
+
# effective_ttl = ttl_seconds + clock_skew_tolerance
|
|
774
|
+
effective_ttl = ttl_seconds + self._config.clock_skew_tolerance_seconds
|
|
775
|
+
|
|
776
|
+
# Batched delete using subquery with LIMIT
|
|
777
|
+
# This pattern:
|
|
778
|
+
# 1. Selects up to batch_size expired record IDs
|
|
779
|
+
# 2. Deletes only those specific records
|
|
780
|
+
# 3. Repeats until no more records are found or max_iterations reached
|
|
781
|
+
#
|
|
782
|
+
# table_name is validated via regex in ModelPostgresIdempotencyStoreConfig
|
|
783
|
+
delete_batch_sql = f""" # noqa: S608
|
|
784
|
+
DELETE FROM {self._config.table_name}
|
|
785
|
+
WHERE id IN (
|
|
786
|
+
SELECT id FROM {self._config.table_name}
|
|
787
|
+
WHERE processed_at < now() - interval '1 second' * $1
|
|
788
|
+
LIMIT $2
|
|
789
|
+
)
|
|
790
|
+
"""
|
|
791
|
+
|
|
792
|
+
total_removed = 0
|
|
793
|
+
iteration = 0
|
|
794
|
+
|
|
795
|
+
try:
|
|
796
|
+
while iteration < effective_max_iterations:
|
|
797
|
+
iteration += 1
|
|
798
|
+
|
|
799
|
+
async with self._pool.acquire() as conn:
|
|
800
|
+
result = await conn.execute(
|
|
801
|
+
delete_batch_sql, effective_ttl, effective_batch_size
|
|
802
|
+
)
|
|
803
|
+
# Parse "DELETE N" to get count
|
|
804
|
+
batch_removed = int(result.split()[-1]) if result else 0
|
|
805
|
+
|
|
806
|
+
total_removed += batch_removed
|
|
807
|
+
|
|
808
|
+
logger.debug(
|
|
809
|
+
"Cleanup batch completed",
|
|
810
|
+
extra={
|
|
811
|
+
"batch_removed": batch_removed,
|
|
812
|
+
"total_removed": total_removed,
|
|
813
|
+
"iteration": iteration,
|
|
814
|
+
"batch_size": effective_batch_size,
|
|
815
|
+
},
|
|
816
|
+
)
|
|
817
|
+
|
|
818
|
+
# Log progress every 10 batches for visibility during large cleanups
|
|
819
|
+
if iteration % 10 == 0:
|
|
820
|
+
logger.info(
|
|
821
|
+
"Cleanup progress",
|
|
822
|
+
extra={
|
|
823
|
+
"total_removed": total_removed,
|
|
824
|
+
"iteration": iteration,
|
|
825
|
+
"batch_size": effective_batch_size,
|
|
826
|
+
},
|
|
827
|
+
)
|
|
828
|
+
|
|
829
|
+
# If we deleted fewer than batch_size, we're done
|
|
830
|
+
if batch_removed < effective_batch_size:
|
|
831
|
+
break
|
|
832
|
+
|
|
833
|
+
# Update cleanup metrics (protected by lock for thread safety)
|
|
834
|
+
async with self._metrics_lock:
|
|
835
|
+
self._metrics.total_cleanup_deleted += total_removed
|
|
836
|
+
self._metrics.last_cleanup_deleted = total_removed
|
|
837
|
+
self._metrics.last_cleanup_at = datetime.now(UTC)
|
|
838
|
+
|
|
839
|
+
logger.info(
|
|
840
|
+
"Cleaned up expired idempotency records",
|
|
841
|
+
extra={
|
|
842
|
+
"total_removed": total_removed,
|
|
843
|
+
"ttl_seconds": ttl_seconds,
|
|
844
|
+
"clock_skew_tolerance_seconds": self._config.clock_skew_tolerance_seconds,
|
|
845
|
+
"effective_ttl_seconds": effective_ttl,
|
|
846
|
+
"table_name": self._config.table_name,
|
|
847
|
+
"iterations": iteration,
|
|
848
|
+
"batch_size": effective_batch_size,
|
|
849
|
+
},
|
|
850
|
+
)
|
|
851
|
+
|
|
852
|
+
return total_removed
|
|
853
|
+
|
|
854
|
+
except asyncpg.QueryCanceledError as e:
|
|
855
|
+
raise InfraTimeoutError(
|
|
856
|
+
f"Cleanup timed out after {self._config.command_timeout}s",
|
|
857
|
+
context=ModelTimeoutErrorContext(
|
|
858
|
+
transport_type=context.transport_type,
|
|
859
|
+
operation=context.operation,
|
|
860
|
+
target_name=context.target_name,
|
|
861
|
+
correlation_id=context.correlation_id,
|
|
862
|
+
timeout_seconds=self._config.command_timeout,
|
|
863
|
+
),
|
|
864
|
+
) from e
|
|
865
|
+
except asyncpg.PostgresConnectionError as e:
|
|
866
|
+
raise InfraConnectionError(
|
|
867
|
+
"Database connection lost during cleanup",
|
|
868
|
+
context=context,
|
|
869
|
+
) from e
|
|
870
|
+
except asyncpg.PostgresError as e:
|
|
871
|
+
raise RuntimeHostError(
|
|
872
|
+
f"Database error during cleanup: {type(e).__name__}",
|
|
873
|
+
context=context,
|
|
874
|
+
) from e
|
|
875
|
+
|
|
876
|
+
async def health_check(self) -> ModelIdempotencyStoreHealthCheckResult:
|
|
877
|
+
"""Check if the store is healthy and can accept operations.
|
|
878
|
+
|
|
879
|
+
Performs read verification and table existence check to ensure
|
|
880
|
+
the database is operational without writing data.
|
|
881
|
+
|
|
882
|
+
Returns:
|
|
883
|
+
ModelIdempotencyStoreHealthCheckResult with health status and diagnostics:
|
|
884
|
+
- healthy: bool - True if store is healthy
|
|
885
|
+
- reason: str - "ok", "not_initialized", "table_not_found", or "check_failed"
|
|
886
|
+
- error_type: str | None - Exception type if check failed
|
|
887
|
+
"""
|
|
888
|
+
if not self._initialized or self._pool is None:
|
|
889
|
+
return ModelIdempotencyStoreHealthCheckResult(
|
|
890
|
+
healthy=False, reason="not_initialized"
|
|
891
|
+
)
|
|
892
|
+
|
|
893
|
+
try:
|
|
894
|
+
async with self._pool.acquire() as conn:
|
|
895
|
+
# Step 1: Verify read access
|
|
896
|
+
await conn.fetchval("SELECT 1")
|
|
897
|
+
|
|
898
|
+
# Step 2: Verify table exists and is accessible
|
|
899
|
+
# This confirms schema setup without writes
|
|
900
|
+
check_table_sql = """
|
|
901
|
+
SELECT 1 FROM information_schema.tables
|
|
902
|
+
WHERE table_name = $1
|
|
903
|
+
LIMIT 1
|
|
904
|
+
"""
|
|
905
|
+
table_exists = await conn.fetchval(
|
|
906
|
+
check_table_sql, self._config.table_name
|
|
907
|
+
)
|
|
908
|
+
|
|
909
|
+
if table_exists is None:
|
|
910
|
+
return ModelIdempotencyStoreHealthCheckResult(
|
|
911
|
+
healthy=False, reason="table_not_found"
|
|
912
|
+
)
|
|
913
|
+
return ModelIdempotencyStoreHealthCheckResult(healthy=True, reason="ok")
|
|
914
|
+
|
|
915
|
+
except Exception as e:
|
|
916
|
+
return ModelIdempotencyStoreHealthCheckResult(
|
|
917
|
+
healthy=False,
|
|
918
|
+
reason="check_failed",
|
|
919
|
+
error_type=type(e).__name__,
|
|
920
|
+
)
|
|
921
|
+
|
|
922
|
+
|
|
923
|
+
__all__: list[str] = ["StoreIdempotencyPostgres"]
|