omnibase_infra 0.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- omnibase_infra/__init__.py +101 -0
- omnibase_infra/cli/__init__.py +1 -0
- omnibase_infra/cli/commands.py +216 -0
- omnibase_infra/clients/__init__.py +0 -0
- omnibase_infra/contracts/handlers/filesystem/handler_contract.yaml +261 -0
- omnibase_infra/contracts/handlers/mcp/handler_contract.yaml +138 -0
- omnibase_infra/decorators/__init__.py +29 -0
- omnibase_infra/decorators/allow_any.py +109 -0
- omnibase_infra/dlq/__init__.py +90 -0
- omnibase_infra/dlq/constants_dlq.py +57 -0
- omnibase_infra/dlq/models/__init__.py +26 -0
- omnibase_infra/dlq/models/enum_replay_status.py +37 -0
- omnibase_infra/dlq/models/model_dlq_replay_record.py +135 -0
- omnibase_infra/dlq/models/model_dlq_tracking_config.py +184 -0
- omnibase_infra/dlq/service_dlq_tracking.py +611 -0
- omnibase_infra/enums/__init__.py +123 -0
- omnibase_infra/enums/enum_any_type_violation.py +104 -0
- omnibase_infra/enums/enum_backend_type.py +27 -0
- omnibase_infra/enums/enum_capture_outcome.py +42 -0
- omnibase_infra/enums/enum_capture_state.py +88 -0
- omnibase_infra/enums/enum_chain_violation_type.py +119 -0
- omnibase_infra/enums/enum_circuit_state.py +51 -0
- omnibase_infra/enums/enum_confirmation_event_type.py +27 -0
- omnibase_infra/enums/enum_contract_type.py +84 -0
- omnibase_infra/enums/enum_dedupe_strategy.py +46 -0
- omnibase_infra/enums/enum_dispatch_status.py +191 -0
- omnibase_infra/enums/enum_environment.py +46 -0
- omnibase_infra/enums/enum_execution_shape_violation.py +103 -0
- omnibase_infra/enums/enum_handler_error_type.py +101 -0
- omnibase_infra/enums/enum_handler_loader_error.py +178 -0
- omnibase_infra/enums/enum_handler_source_type.py +87 -0
- omnibase_infra/enums/enum_handler_type.py +77 -0
- omnibase_infra/enums/enum_handler_type_category.py +61 -0
- omnibase_infra/enums/enum_infra_transport_type.py +73 -0
- omnibase_infra/enums/enum_introspection_reason.py +154 -0
- omnibase_infra/enums/enum_message_category.py +213 -0
- omnibase_infra/enums/enum_node_archetype.py +74 -0
- omnibase_infra/enums/enum_node_output_type.py +185 -0
- omnibase_infra/enums/enum_non_retryable_error_category.py +224 -0
- omnibase_infra/enums/enum_policy_type.py +32 -0
- omnibase_infra/enums/enum_registration_state.py +261 -0
- omnibase_infra/enums/enum_registration_status.py +33 -0
- omnibase_infra/enums/enum_registry_response_status.py +28 -0
- omnibase_infra/enums/enum_response_status.py +26 -0
- omnibase_infra/enums/enum_retry_error_category.py +98 -0
- omnibase_infra/enums/enum_security_rule_id.py +103 -0
- omnibase_infra/enums/enum_selection_strategy.py +91 -0
- omnibase_infra/enums/enum_topic_standard.py +42 -0
- omnibase_infra/enums/enum_validation_severity.py +78 -0
- omnibase_infra/errors/__init__.py +156 -0
- omnibase_infra/errors/error_architecture_violation.py +152 -0
- omnibase_infra/errors/error_chain_propagation.py +188 -0
- omnibase_infra/errors/error_compute_registry.py +92 -0
- omnibase_infra/errors/error_consul.py +132 -0
- omnibase_infra/errors/error_container_wiring.py +243 -0
- omnibase_infra/errors/error_event_bus_registry.py +102 -0
- omnibase_infra/errors/error_infra.py +608 -0
- omnibase_infra/errors/error_message_type_registry.py +101 -0
- omnibase_infra/errors/error_policy_registry.py +112 -0
- omnibase_infra/errors/error_vault.py +123 -0
- omnibase_infra/event_bus/__init__.py +72 -0
- omnibase_infra/event_bus/configs/kafka_event_bus_config.yaml +86 -0
- omnibase_infra/event_bus/event_bus_inmemory.py +743 -0
- omnibase_infra/event_bus/event_bus_kafka.py +1658 -0
- omnibase_infra/event_bus/mixin_kafka_broadcast.py +184 -0
- omnibase_infra/event_bus/mixin_kafka_dlq.py +765 -0
- omnibase_infra/event_bus/models/__init__.py +29 -0
- omnibase_infra/event_bus/models/config/__init__.py +20 -0
- omnibase_infra/event_bus/models/config/model_kafka_event_bus_config.py +725 -0
- omnibase_infra/event_bus/models/model_dlq_event.py +206 -0
- omnibase_infra/event_bus/models/model_dlq_metrics.py +304 -0
- omnibase_infra/event_bus/models/model_event_headers.py +115 -0
- omnibase_infra/event_bus/models/model_event_message.py +60 -0
- omnibase_infra/event_bus/topic_constants.py +376 -0
- omnibase_infra/handlers/__init__.py +75 -0
- omnibase_infra/handlers/filesystem/__init__.py +48 -0
- omnibase_infra/handlers/filesystem/enum_file_system_operation.py +35 -0
- omnibase_infra/handlers/filesystem/model_file_system_request.py +298 -0
- omnibase_infra/handlers/filesystem/model_file_system_result.py +166 -0
- omnibase_infra/handlers/handler_consul.py +787 -0
- omnibase_infra/handlers/handler_db.py +1039 -0
- omnibase_infra/handlers/handler_filesystem.py +1478 -0
- omnibase_infra/handlers/handler_graph.py +1154 -0
- omnibase_infra/handlers/handler_http.py +920 -0
- omnibase_infra/handlers/handler_manifest_persistence.contract.yaml +184 -0
- omnibase_infra/handlers/handler_manifest_persistence.py +1539 -0
- omnibase_infra/handlers/handler_mcp.py +748 -0
- omnibase_infra/handlers/handler_qdrant.py +1076 -0
- omnibase_infra/handlers/handler_vault.py +422 -0
- omnibase_infra/handlers/mcp/__init__.py +19 -0
- omnibase_infra/handlers/mcp/adapter_onex_to_mcp.py +446 -0
- omnibase_infra/handlers/mcp/protocols.py +178 -0
- omnibase_infra/handlers/mcp/transport_streamable_http.py +352 -0
- omnibase_infra/handlers/mixins/__init__.py +42 -0
- omnibase_infra/handlers/mixins/mixin_consul_initialization.py +349 -0
- omnibase_infra/handlers/mixins/mixin_consul_kv.py +337 -0
- omnibase_infra/handlers/mixins/mixin_consul_service.py +277 -0
- omnibase_infra/handlers/mixins/mixin_vault_initialization.py +338 -0
- omnibase_infra/handlers/mixins/mixin_vault_retry.py +412 -0
- omnibase_infra/handlers/mixins/mixin_vault_secrets.py +450 -0
- omnibase_infra/handlers/mixins/mixin_vault_token.py +365 -0
- omnibase_infra/handlers/models/__init__.py +286 -0
- omnibase_infra/handlers/models/consul/__init__.py +81 -0
- omnibase_infra/handlers/models/consul/enum_consul_operation_type.py +57 -0
- omnibase_infra/handlers/models/consul/model_consul_deregister_payload.py +51 -0
- omnibase_infra/handlers/models/consul/model_consul_handler_config.py +153 -0
- omnibase_infra/handlers/models/consul/model_consul_handler_payload.py +89 -0
- omnibase_infra/handlers/models/consul/model_consul_kv_get_found_payload.py +55 -0
- omnibase_infra/handlers/models/consul/model_consul_kv_get_not_found_payload.py +49 -0
- omnibase_infra/handlers/models/consul/model_consul_kv_get_recurse_payload.py +50 -0
- omnibase_infra/handlers/models/consul/model_consul_kv_item.py +33 -0
- omnibase_infra/handlers/models/consul/model_consul_kv_put_payload.py +41 -0
- omnibase_infra/handlers/models/consul/model_consul_register_payload.py +53 -0
- omnibase_infra/handlers/models/consul/model_consul_retry_config.py +66 -0
- omnibase_infra/handlers/models/consul/model_payload_consul.py +66 -0
- omnibase_infra/handlers/models/consul/registry_payload_consul.py +214 -0
- omnibase_infra/handlers/models/graph/__init__.py +35 -0
- omnibase_infra/handlers/models/graph/enum_graph_operation_type.py +20 -0
- omnibase_infra/handlers/models/graph/model_graph_execute_payload.py +38 -0
- omnibase_infra/handlers/models/graph/model_graph_handler_config.py +54 -0
- omnibase_infra/handlers/models/graph/model_graph_handler_payload.py +44 -0
- omnibase_infra/handlers/models/graph/model_graph_query_payload.py +40 -0
- omnibase_infra/handlers/models/graph/model_graph_record.py +22 -0
- omnibase_infra/handlers/models/http/__init__.py +50 -0
- omnibase_infra/handlers/models/http/enum_http_operation_type.py +29 -0
- omnibase_infra/handlers/models/http/model_http_body_content.py +45 -0
- omnibase_infra/handlers/models/http/model_http_get_payload.py +88 -0
- omnibase_infra/handlers/models/http/model_http_handler_payload.py +90 -0
- omnibase_infra/handlers/models/http/model_http_post_payload.py +88 -0
- omnibase_infra/handlers/models/http/model_payload_http.py +66 -0
- omnibase_infra/handlers/models/http/registry_payload_http.py +212 -0
- omnibase_infra/handlers/models/mcp/__init__.py +23 -0
- omnibase_infra/handlers/models/mcp/enum_mcp_operation_type.py +24 -0
- omnibase_infra/handlers/models/mcp/model_mcp_handler_config.py +40 -0
- omnibase_infra/handlers/models/mcp/model_mcp_tool_call.py +32 -0
- omnibase_infra/handlers/models/mcp/model_mcp_tool_result.py +45 -0
- omnibase_infra/handlers/models/model_consul_handler_response.py +96 -0
- omnibase_infra/handlers/models/model_db_describe_response.py +83 -0
- omnibase_infra/handlers/models/model_db_query_payload.py +95 -0
- omnibase_infra/handlers/models/model_db_query_response.py +60 -0
- omnibase_infra/handlers/models/model_filesystem_config.py +98 -0
- omnibase_infra/handlers/models/model_filesystem_delete_payload.py +54 -0
- omnibase_infra/handlers/models/model_filesystem_delete_result.py +77 -0
- omnibase_infra/handlers/models/model_filesystem_directory_entry.py +75 -0
- omnibase_infra/handlers/models/model_filesystem_ensure_directory_payload.py +54 -0
- omnibase_infra/handlers/models/model_filesystem_ensure_directory_result.py +60 -0
- omnibase_infra/handlers/models/model_filesystem_list_directory_payload.py +60 -0
- omnibase_infra/handlers/models/model_filesystem_list_directory_result.py +68 -0
- omnibase_infra/handlers/models/model_filesystem_read_payload.py +62 -0
- omnibase_infra/handlers/models/model_filesystem_read_result.py +61 -0
- omnibase_infra/handlers/models/model_filesystem_write_payload.py +70 -0
- omnibase_infra/handlers/models/model_filesystem_write_result.py +55 -0
- omnibase_infra/handlers/models/model_graph_handler_response.py +98 -0
- omnibase_infra/handlers/models/model_handler_response.py +103 -0
- omnibase_infra/handlers/models/model_http_handler_response.py +101 -0
- omnibase_infra/handlers/models/model_manifest_metadata.py +75 -0
- omnibase_infra/handlers/models/model_manifest_persistence_config.py +62 -0
- omnibase_infra/handlers/models/model_manifest_query_payload.py +90 -0
- omnibase_infra/handlers/models/model_manifest_query_result.py +97 -0
- omnibase_infra/handlers/models/model_manifest_retrieve_payload.py +44 -0
- omnibase_infra/handlers/models/model_manifest_retrieve_result.py +98 -0
- omnibase_infra/handlers/models/model_manifest_store_payload.py +47 -0
- omnibase_infra/handlers/models/model_manifest_store_result.py +67 -0
- omnibase_infra/handlers/models/model_operation_context.py +187 -0
- omnibase_infra/handlers/models/model_qdrant_handler_response.py +98 -0
- omnibase_infra/handlers/models/model_retry_state.py +162 -0
- omnibase_infra/handlers/models/model_vault_handler_response.py +98 -0
- omnibase_infra/handlers/models/qdrant/__init__.py +44 -0
- omnibase_infra/handlers/models/qdrant/enum_qdrant_operation_type.py +26 -0
- omnibase_infra/handlers/models/qdrant/model_qdrant_collection_payload.py +42 -0
- omnibase_infra/handlers/models/qdrant/model_qdrant_delete_payload.py +36 -0
- omnibase_infra/handlers/models/qdrant/model_qdrant_handler_config.py +42 -0
- omnibase_infra/handlers/models/qdrant/model_qdrant_handler_payload.py +54 -0
- omnibase_infra/handlers/models/qdrant/model_qdrant_search_payload.py +42 -0
- omnibase_infra/handlers/models/qdrant/model_qdrant_search_result.py +30 -0
- omnibase_infra/handlers/models/qdrant/model_qdrant_upsert_payload.py +36 -0
- omnibase_infra/handlers/models/vault/__init__.py +69 -0
- omnibase_infra/handlers/models/vault/enum_vault_operation_type.py +35 -0
- omnibase_infra/handlers/models/vault/model_payload_vault.py +66 -0
- omnibase_infra/handlers/models/vault/model_vault_delete_payload.py +57 -0
- omnibase_infra/handlers/models/vault/model_vault_handler_config.py +148 -0
- omnibase_infra/handlers/models/vault/model_vault_handler_payload.py +101 -0
- omnibase_infra/handlers/models/vault/model_vault_list_payload.py +58 -0
- omnibase_infra/handlers/models/vault/model_vault_renew_token_payload.py +67 -0
- omnibase_infra/handlers/models/vault/model_vault_retry_config.py +66 -0
- omnibase_infra/handlers/models/vault/model_vault_secret_payload.py +106 -0
- omnibase_infra/handlers/models/vault/model_vault_write_payload.py +66 -0
- omnibase_infra/handlers/models/vault/registry_payload_vault.py +213 -0
- omnibase_infra/handlers/registration_storage/__init__.py +43 -0
- omnibase_infra/handlers/registration_storage/handler_registration_storage_mock.py +392 -0
- omnibase_infra/handlers/registration_storage/handler_registration_storage_postgres.py +915 -0
- omnibase_infra/handlers/registration_storage/models/__init__.py +23 -0
- omnibase_infra/handlers/registration_storage/models/model_delete_registration_request.py +58 -0
- omnibase_infra/handlers/registration_storage/models/model_update_registration_request.py +73 -0
- omnibase_infra/handlers/registration_storage/protocol_registration_persistence.py +191 -0
- omnibase_infra/handlers/service_discovery/__init__.py +43 -0
- omnibase_infra/handlers/service_discovery/handler_service_discovery_consul.py +747 -0
- omnibase_infra/handlers/service_discovery/handler_service_discovery_mock.py +258 -0
- omnibase_infra/handlers/service_discovery/models/__init__.py +22 -0
- omnibase_infra/handlers/service_discovery/models/model_discovery_result.py +64 -0
- omnibase_infra/handlers/service_discovery/models/model_registration_result.py +138 -0
- omnibase_infra/handlers/service_discovery/models/model_service_info.py +99 -0
- omnibase_infra/handlers/service_discovery/protocol_discovery_operations.py +170 -0
- omnibase_infra/idempotency/__init__.py +94 -0
- omnibase_infra/idempotency/models/__init__.py +43 -0
- omnibase_infra/idempotency/models/model_idempotency_check_result.py +85 -0
- omnibase_infra/idempotency/models/model_idempotency_guard_config.py +130 -0
- omnibase_infra/idempotency/models/model_idempotency_record.py +86 -0
- omnibase_infra/idempotency/models/model_idempotency_store_health_check_result.py +81 -0
- omnibase_infra/idempotency/models/model_idempotency_store_metrics.py +140 -0
- omnibase_infra/idempotency/models/model_postgres_idempotency_store_config.py +299 -0
- omnibase_infra/idempotency/protocol_idempotency_store.py +184 -0
- omnibase_infra/idempotency/store_inmemory.py +265 -0
- omnibase_infra/idempotency/store_postgres.py +923 -0
- omnibase_infra/infrastructure/__init__.py +0 -0
- omnibase_infra/mixins/__init__.py +71 -0
- omnibase_infra/mixins/mixin_async_circuit_breaker.py +655 -0
- omnibase_infra/mixins/mixin_dict_like_accessors.py +146 -0
- omnibase_infra/mixins/mixin_envelope_extraction.py +119 -0
- omnibase_infra/mixins/mixin_node_introspection.py +2465 -0
- omnibase_infra/mixins/mixin_retry_execution.py +386 -0
- omnibase_infra/mixins/protocol_circuit_breaker_aware.py +133 -0
- omnibase_infra/models/__init__.py +136 -0
- omnibase_infra/models/corpus/__init__.py +17 -0
- omnibase_infra/models/corpus/model_capture_config.py +133 -0
- omnibase_infra/models/corpus/model_capture_result.py +86 -0
- omnibase_infra/models/discovery/__init__.py +42 -0
- omnibase_infra/models/discovery/model_dependency_spec.py +319 -0
- omnibase_infra/models/discovery/model_discovered_capabilities.py +50 -0
- omnibase_infra/models/discovery/model_introspection_config.py +311 -0
- omnibase_infra/models/discovery/model_introspection_performance_metrics.py +169 -0
- omnibase_infra/models/discovery/model_introspection_task_config.py +116 -0
- omnibase_infra/models/dispatch/__init__.py +147 -0
- omnibase_infra/models/dispatch/model_dispatch_context.py +439 -0
- omnibase_infra/models/dispatch/model_dispatch_error.py +336 -0
- omnibase_infra/models/dispatch/model_dispatch_log_context.py +400 -0
- omnibase_infra/models/dispatch/model_dispatch_metadata.py +228 -0
- omnibase_infra/models/dispatch/model_dispatch_metrics.py +496 -0
- omnibase_infra/models/dispatch/model_dispatch_outcome.py +317 -0
- omnibase_infra/models/dispatch/model_dispatch_outputs.py +231 -0
- omnibase_infra/models/dispatch/model_dispatch_result.py +436 -0
- omnibase_infra/models/dispatch/model_dispatch_route.py +279 -0
- omnibase_infra/models/dispatch/model_dispatcher_metrics.py +275 -0
- omnibase_infra/models/dispatch/model_dispatcher_registration.py +352 -0
- omnibase_infra/models/dispatch/model_parsed_topic.py +135 -0
- omnibase_infra/models/dispatch/model_topic_parser.py +725 -0
- omnibase_infra/models/dispatch/model_tracing_context.py +285 -0
- omnibase_infra/models/errors/__init__.py +45 -0
- omnibase_infra/models/errors/model_handler_validation_error.py +594 -0
- omnibase_infra/models/errors/model_infra_error_context.py +99 -0
- omnibase_infra/models/errors/model_message_type_registry_error_context.py +71 -0
- omnibase_infra/models/errors/model_timeout_error_context.py +110 -0
- omnibase_infra/models/handlers/__init__.py +37 -0
- omnibase_infra/models/handlers/model_contract_discovery_result.py +80 -0
- omnibase_infra/models/handlers/model_handler_descriptor.py +185 -0
- omnibase_infra/models/handlers/model_handler_identifier.py +215 -0
- omnibase_infra/models/health/__init__.py +9 -0
- omnibase_infra/models/health/model_health_check_result.py +40 -0
- omnibase_infra/models/lifecycle/__init__.py +39 -0
- omnibase_infra/models/logging/__init__.py +51 -0
- omnibase_infra/models/logging/model_log_context.py +756 -0
- omnibase_infra/models/model_retry_error_classification.py +78 -0
- omnibase_infra/models/projection/__init__.py +43 -0
- omnibase_infra/models/projection/model_capability_fields.py +112 -0
- omnibase_infra/models/projection/model_registration_projection.py +434 -0
- omnibase_infra/models/projection/model_registration_snapshot.py +322 -0
- omnibase_infra/models/projection/model_sequence_info.py +182 -0
- omnibase_infra/models/projection/model_snapshot_topic_config.py +590 -0
- omnibase_infra/models/projectors/__init__.py +41 -0
- omnibase_infra/models/projectors/model_projector_column.py +289 -0
- omnibase_infra/models/projectors/model_projector_discovery_result.py +65 -0
- omnibase_infra/models/projectors/model_projector_index.py +270 -0
- omnibase_infra/models/projectors/model_projector_schema.py +415 -0
- omnibase_infra/models/projectors/model_projector_validation_error.py +63 -0
- omnibase_infra/models/projectors/util_sql_identifiers.py +115 -0
- omnibase_infra/models/registration/__init__.py +59 -0
- omnibase_infra/models/registration/commands/__init__.py +15 -0
- omnibase_infra/models/registration/commands/model_node_registration_acked.py +108 -0
- omnibase_infra/models/registration/events/__init__.py +56 -0
- omnibase_infra/models/registration/events/model_node_became_active.py +103 -0
- omnibase_infra/models/registration/events/model_node_liveness_expired.py +103 -0
- omnibase_infra/models/registration/events/model_node_registration_accepted.py +98 -0
- omnibase_infra/models/registration/events/model_node_registration_ack_received.py +98 -0
- omnibase_infra/models/registration/events/model_node_registration_ack_timed_out.py +112 -0
- omnibase_infra/models/registration/events/model_node_registration_initiated.py +107 -0
- omnibase_infra/models/registration/events/model_node_registration_rejected.py +104 -0
- omnibase_infra/models/registration/model_introspection_metrics.py +253 -0
- omnibase_infra/models/registration/model_node_capabilities.py +179 -0
- omnibase_infra/models/registration/model_node_heartbeat_event.py +126 -0
- omnibase_infra/models/registration/model_node_introspection_event.py +175 -0
- omnibase_infra/models/registration/model_node_metadata.py +79 -0
- omnibase_infra/models/registration/model_node_registration.py +162 -0
- omnibase_infra/models/registration/model_node_registration_record.py +162 -0
- omnibase_infra/models/registry/__init__.py +29 -0
- omnibase_infra/models/registry/model_domain_constraint.py +202 -0
- omnibase_infra/models/registry/model_message_type_entry.py +271 -0
- omnibase_infra/models/resilience/__init__.py +9 -0
- omnibase_infra/models/resilience/model_circuit_breaker_config.py +227 -0
- omnibase_infra/models/routing/__init__.py +25 -0
- omnibase_infra/models/routing/model_routing_entry.py +52 -0
- omnibase_infra/models/routing/model_routing_subcontract.py +70 -0
- omnibase_infra/models/runtime/__init__.py +40 -0
- omnibase_infra/models/runtime/model_contract_security_config.py +41 -0
- omnibase_infra/models/runtime/model_discovery_error.py +81 -0
- omnibase_infra/models/runtime/model_discovery_result.py +162 -0
- omnibase_infra/models/runtime/model_discovery_warning.py +74 -0
- omnibase_infra/models/runtime/model_failed_plugin_load.py +63 -0
- omnibase_infra/models/runtime/model_handler_contract.py +280 -0
- omnibase_infra/models/runtime/model_loaded_handler.py +120 -0
- omnibase_infra/models/runtime/model_plugin_load_context.py +93 -0
- omnibase_infra/models/runtime/model_plugin_load_summary.py +124 -0
- omnibase_infra/models/security/__init__.py +50 -0
- omnibase_infra/models/security/classification_levels.py +99 -0
- omnibase_infra/models/security/model_environment_policy.py +145 -0
- omnibase_infra/models/security/model_handler_security_policy.py +107 -0
- omnibase_infra/models/security/model_security_error.py +81 -0
- omnibase_infra/models/security/model_security_validation_result.py +328 -0
- omnibase_infra/models/security/model_security_warning.py +67 -0
- omnibase_infra/models/snapshot/__init__.py +27 -0
- omnibase_infra/models/snapshot/model_field_change.py +65 -0
- omnibase_infra/models/snapshot/model_snapshot.py +270 -0
- omnibase_infra/models/snapshot/model_snapshot_diff.py +203 -0
- omnibase_infra/models/snapshot/model_subject_ref.py +81 -0
- omnibase_infra/models/types/__init__.py +71 -0
- omnibase_infra/models/validation/__init__.py +89 -0
- omnibase_infra/models/validation/model_any_type_validation_result.py +118 -0
- omnibase_infra/models/validation/model_any_type_violation.py +141 -0
- omnibase_infra/models/validation/model_category_match_result.py +345 -0
- omnibase_infra/models/validation/model_chain_violation.py +166 -0
- omnibase_infra/models/validation/model_coverage_metrics.py +316 -0
- omnibase_infra/models/validation/model_execution_shape_rule.py +159 -0
- omnibase_infra/models/validation/model_execution_shape_validation.py +208 -0
- omnibase_infra/models/validation/model_execution_shape_validation_result.py +294 -0
- omnibase_infra/models/validation/model_execution_shape_violation.py +122 -0
- omnibase_infra/models/validation/model_localhandler_validation_result.py +139 -0
- omnibase_infra/models/validation/model_localhandler_violation.py +100 -0
- omnibase_infra/models/validation/model_output_validation_params.py +74 -0
- omnibase_infra/models/validation/model_validate_and_raise_params.py +84 -0
- omnibase_infra/models/validation/model_validation_error_params.py +84 -0
- omnibase_infra/models/validation/model_validation_outcome.py +287 -0
- omnibase_infra/nodes/__init__.py +48 -0
- omnibase_infra/nodes/architecture_validator/__init__.py +79 -0
- omnibase_infra/nodes/architecture_validator/contract.yaml +252 -0
- omnibase_infra/nodes/architecture_validator/contract_architecture_validator.yaml +208 -0
- omnibase_infra/nodes/architecture_validator/mixins/__init__.py +16 -0
- omnibase_infra/nodes/architecture_validator/mixins/mixin_file_path_rule.py +92 -0
- omnibase_infra/nodes/architecture_validator/models/__init__.py +36 -0
- omnibase_infra/nodes/architecture_validator/models/model_architecture_validation_request.py +56 -0
- omnibase_infra/nodes/architecture_validator/models/model_architecture_validation_result.py +311 -0
- omnibase_infra/nodes/architecture_validator/models/model_architecture_violation.py +163 -0
- omnibase_infra/nodes/architecture_validator/models/model_rule_check_result.py +265 -0
- omnibase_infra/nodes/architecture_validator/models/model_validation_request.py +105 -0
- omnibase_infra/nodes/architecture_validator/models/model_validation_result.py +314 -0
- omnibase_infra/nodes/architecture_validator/node.py +262 -0
- omnibase_infra/nodes/architecture_validator/node_architecture_validator.py +383 -0
- omnibase_infra/nodes/architecture_validator/protocols/__init__.py +9 -0
- omnibase_infra/nodes/architecture_validator/protocols/protocol_architecture_rule.py +225 -0
- omnibase_infra/nodes/architecture_validator/registry/__init__.py +28 -0
- omnibase_infra/nodes/architecture_validator/registry/registry_infra_architecture_validator.py +99 -0
- omnibase_infra/nodes/architecture_validator/validators/__init__.py +104 -0
- omnibase_infra/nodes/architecture_validator/validators/validator_no_direct_dispatch.py +422 -0
- omnibase_infra/nodes/architecture_validator/validators/validator_no_handler_publishing.py +481 -0
- omnibase_infra/nodes/architecture_validator/validators/validator_no_orchestrator_fsm.py +491 -0
- omnibase_infra/nodes/effects/README.md +358 -0
- omnibase_infra/nodes/effects/__init__.py +26 -0
- omnibase_infra/nodes/effects/contract.yaml +172 -0
- omnibase_infra/nodes/effects/models/__init__.py +32 -0
- omnibase_infra/nodes/effects/models/model_backend_result.py +190 -0
- omnibase_infra/nodes/effects/models/model_effect_idempotency_config.py +92 -0
- omnibase_infra/nodes/effects/models/model_registry_request.py +132 -0
- omnibase_infra/nodes/effects/models/model_registry_response.py +263 -0
- omnibase_infra/nodes/effects/protocol_consul_client.py +89 -0
- omnibase_infra/nodes/effects/protocol_effect_idempotency_store.py +143 -0
- omnibase_infra/nodes/effects/protocol_postgres_adapter.py +96 -0
- omnibase_infra/nodes/effects/registry_effect.py +525 -0
- omnibase_infra/nodes/effects/store_effect_idempotency_inmemory.py +425 -0
- omnibase_infra/nodes/node_registration_orchestrator/README.md +542 -0
- omnibase_infra/nodes/node_registration_orchestrator/__init__.py +120 -0
- omnibase_infra/nodes/node_registration_orchestrator/contract.yaml +475 -0
- omnibase_infra/nodes/node_registration_orchestrator/dispatchers/__init__.py +53 -0
- omnibase_infra/nodes/node_registration_orchestrator/dispatchers/dispatcher_node_introspected.py +376 -0
- omnibase_infra/nodes/node_registration_orchestrator/dispatchers/dispatcher_node_registration_acked.py +376 -0
- omnibase_infra/nodes/node_registration_orchestrator/dispatchers/dispatcher_runtime_tick.py +373 -0
- omnibase_infra/nodes/node_registration_orchestrator/handlers/__init__.py +62 -0
- omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_node_heartbeat.py +376 -0
- omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_node_introspected.py +609 -0
- omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_node_registration_acked.py +458 -0
- omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_runtime_tick.py +364 -0
- omnibase_infra/nodes/node_registration_orchestrator/introspection_event_router.py +544 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/__init__.py +75 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_consul_intent_payload.py +194 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_consul_registration_intent.py +67 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_intent_execution_result.py +50 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_node_liveness_expired.py +107 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_orchestrator_config.py +67 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_orchestrator_input.py +41 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_orchestrator_output.py +166 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_postgres_intent_payload.py +235 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_postgres_upsert_intent.py +68 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_reducer_execution_result.py +384 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_reducer_state.py +60 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_registration_intent.py +177 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_registry_intent.py +247 -0
- omnibase_infra/nodes/node_registration_orchestrator/node.py +195 -0
- omnibase_infra/nodes/node_registration_orchestrator/plugin.py +909 -0
- omnibase_infra/nodes/node_registration_orchestrator/protocols.py +439 -0
- omnibase_infra/nodes/node_registration_orchestrator/registry/__init__.py +41 -0
- omnibase_infra/nodes/node_registration_orchestrator/registry/registry_infra_node_registration_orchestrator.py +525 -0
- omnibase_infra/nodes/node_registration_orchestrator/timeout_coordinator.py +392 -0
- omnibase_infra/nodes/node_registration_orchestrator/wiring.py +742 -0
- omnibase_infra/nodes/node_registration_reducer/__init__.py +15 -0
- omnibase_infra/nodes/node_registration_reducer/contract.yaml +301 -0
- omnibase_infra/nodes/node_registration_reducer/models/__init__.py +38 -0
- omnibase_infra/nodes/node_registration_reducer/models/model_validation_result.py +113 -0
- omnibase_infra/nodes/node_registration_reducer/node.py +139 -0
- omnibase_infra/nodes/node_registration_reducer/registry/__init__.py +9 -0
- omnibase_infra/nodes/node_registration_reducer/registry/registry_infra_node_registration_reducer.py +79 -0
- omnibase_infra/nodes/node_registration_storage_effect/__init__.py +41 -0
- omnibase_infra/nodes/node_registration_storage_effect/contract.yaml +225 -0
- omnibase_infra/nodes/node_registration_storage_effect/models/__init__.py +44 -0
- omnibase_infra/nodes/node_registration_storage_effect/models/model_delete_result.py +132 -0
- omnibase_infra/nodes/node_registration_storage_effect/models/model_registration_record.py +199 -0
- omnibase_infra/nodes/node_registration_storage_effect/models/model_registration_update.py +155 -0
- omnibase_infra/nodes/node_registration_storage_effect/models/model_storage_health_check_details.py +123 -0
- omnibase_infra/nodes/node_registration_storage_effect/models/model_storage_health_check_result.py +117 -0
- omnibase_infra/nodes/node_registration_storage_effect/models/model_storage_query.py +100 -0
- omnibase_infra/nodes/node_registration_storage_effect/models/model_storage_result.py +136 -0
- omnibase_infra/nodes/node_registration_storage_effect/models/model_upsert_result.py +127 -0
- omnibase_infra/nodes/node_registration_storage_effect/node.py +109 -0
- omnibase_infra/nodes/node_registration_storage_effect/protocols/__init__.py +22 -0
- omnibase_infra/nodes/node_registration_storage_effect/protocols/protocol_registration_persistence.py +333 -0
- omnibase_infra/nodes/node_registration_storage_effect/registry/__init__.py +23 -0
- omnibase_infra/nodes/node_registration_storage_effect/registry/registry_infra_registration_storage.py +194 -0
- omnibase_infra/nodes/node_registry_effect/__init__.py +85 -0
- omnibase_infra/nodes/node_registry_effect/contract.yaml +682 -0
- omnibase_infra/nodes/node_registry_effect/handlers/__init__.py +70 -0
- omnibase_infra/nodes/node_registry_effect/handlers/handler_consul_deregister.py +211 -0
- omnibase_infra/nodes/node_registry_effect/handlers/handler_consul_register.py +212 -0
- omnibase_infra/nodes/node_registry_effect/handlers/handler_partial_retry.py +416 -0
- omnibase_infra/nodes/node_registry_effect/handlers/handler_postgres_deactivate.py +215 -0
- omnibase_infra/nodes/node_registry_effect/handlers/handler_postgres_upsert.py +208 -0
- omnibase_infra/nodes/node_registry_effect/models/__init__.py +43 -0
- omnibase_infra/nodes/node_registry_effect/models/model_partial_retry_request.py +92 -0
- omnibase_infra/nodes/node_registry_effect/node.py +165 -0
- omnibase_infra/nodes/node_registry_effect/registry/__init__.py +27 -0
- omnibase_infra/nodes/node_registry_effect/registry/registry_infra_registry_effect.py +196 -0
- omnibase_infra/nodes/node_service_discovery_effect/__init__.py +111 -0
- omnibase_infra/nodes/node_service_discovery_effect/contract.yaml +246 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/__init__.py +67 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/enum_health_status.py +72 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/enum_service_discovery_operation.py +58 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/model_discovery_query.py +99 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/model_discovery_result.py +98 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/model_health_check_config.py +121 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/model_query_metadata.py +63 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/model_registration_result.py +130 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/model_service_discovery_health_check_details.py +111 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/model_service_discovery_health_check_result.py +119 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/model_service_info.py +106 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/model_service_registration.py +121 -0
- omnibase_infra/nodes/node_service_discovery_effect/node.py +111 -0
- omnibase_infra/nodes/node_service_discovery_effect/protocols/__init__.py +14 -0
- omnibase_infra/nodes/node_service_discovery_effect/protocols/protocol_discovery_operations.py +279 -0
- omnibase_infra/nodes/node_service_discovery_effect/registry/__init__.py +13 -0
- omnibase_infra/nodes/node_service_discovery_effect/registry/registry_infra_service_discovery.py +214 -0
- omnibase_infra/nodes/reducers/__init__.py +30 -0
- omnibase_infra/nodes/reducers/models/__init__.py +32 -0
- omnibase_infra/nodes/reducers/models/model_payload_consul_register.py +76 -0
- omnibase_infra/nodes/reducers/models/model_payload_postgres_upsert_registration.py +60 -0
- omnibase_infra/nodes/reducers/models/model_registration_confirmation.py +166 -0
- omnibase_infra/nodes/reducers/models/model_registration_state.py +433 -0
- omnibase_infra/nodes/reducers/registration_reducer.py +1137 -0
- omnibase_infra/observability/__init__.py +143 -0
- omnibase_infra/observability/constants_metrics.py +91 -0
- omnibase_infra/observability/factory_observability_sink.py +525 -0
- omnibase_infra/observability/handlers/__init__.py +118 -0
- omnibase_infra/observability/handlers/handler_logging_structured.py +967 -0
- omnibase_infra/observability/handlers/handler_metrics_prometheus.py +1120 -0
- omnibase_infra/observability/handlers/model_logging_handler_config.py +71 -0
- omnibase_infra/observability/handlers/model_logging_handler_response.py +77 -0
- omnibase_infra/observability/handlers/model_metrics_handler_config.py +172 -0
- omnibase_infra/observability/handlers/model_metrics_handler_payload.py +135 -0
- omnibase_infra/observability/handlers/model_metrics_handler_response.py +101 -0
- omnibase_infra/observability/hooks/__init__.py +74 -0
- omnibase_infra/observability/hooks/hook_observability.py +1223 -0
- omnibase_infra/observability/models/__init__.py +30 -0
- omnibase_infra/observability/models/enum_required_log_context_key.py +77 -0
- omnibase_infra/observability/models/model_buffered_log_entry.py +117 -0
- omnibase_infra/observability/models/model_logging_sink_config.py +73 -0
- omnibase_infra/observability/models/model_metrics_sink_config.py +156 -0
- omnibase_infra/observability/sinks/__init__.py +69 -0
- omnibase_infra/observability/sinks/sink_logging_structured.py +809 -0
- omnibase_infra/observability/sinks/sink_metrics_prometheus.py +710 -0
- omnibase_infra/plugins/__init__.py +27 -0
- omnibase_infra/plugins/examples/__init__.py +28 -0
- omnibase_infra/plugins/examples/plugin_json_normalizer.py +271 -0
- omnibase_infra/plugins/examples/plugin_json_normalizer_error_handling.py +210 -0
- omnibase_infra/plugins/models/__init__.py +21 -0
- omnibase_infra/plugins/models/model_plugin_context.py +76 -0
- omnibase_infra/plugins/models/model_plugin_input_data.py +58 -0
- omnibase_infra/plugins/models/model_plugin_output_data.py +62 -0
- omnibase_infra/plugins/plugin_compute_base.py +435 -0
- omnibase_infra/projectors/__init__.py +30 -0
- omnibase_infra/projectors/contracts/__init__.py +63 -0
- omnibase_infra/projectors/contracts/registration_projector.yaml +370 -0
- omnibase_infra/projectors/projection_reader_registration.py +1559 -0
- omnibase_infra/projectors/snapshot_publisher_registration.py +1329 -0
- omnibase_infra/protocols/__init__.py +99 -0
- omnibase_infra/protocols/protocol_capability_projection.py +253 -0
- omnibase_infra/protocols/protocol_capability_query.py +251 -0
- omnibase_infra/protocols/protocol_event_bus_like.py +127 -0
- omnibase_infra/protocols/protocol_event_projector.py +96 -0
- omnibase_infra/protocols/protocol_idempotency_store.py +142 -0
- omnibase_infra/protocols/protocol_message_dispatcher.py +247 -0
- omnibase_infra/protocols/protocol_message_type_registry.py +306 -0
- omnibase_infra/protocols/protocol_plugin_compute.py +368 -0
- omnibase_infra/protocols/protocol_projector_schema_validator.py +82 -0
- omnibase_infra/protocols/protocol_registry_metrics.py +215 -0
- omnibase_infra/protocols/protocol_snapshot_publisher.py +396 -0
- omnibase_infra/protocols/protocol_snapshot_store.py +567 -0
- omnibase_infra/runtime/__init__.py +296 -0
- omnibase_infra/runtime/binding_config_resolver.py +2706 -0
- omnibase_infra/runtime/chain_aware_dispatch.py +467 -0
- omnibase_infra/runtime/contract_handler_discovery.py +582 -0
- omnibase_infra/runtime/contract_loaders/__init__.py +42 -0
- omnibase_infra/runtime/contract_loaders/handler_routing_loader.py +464 -0
- omnibase_infra/runtime/dispatch_context_enforcer.py +427 -0
- omnibase_infra/runtime/enums/__init__.py +18 -0
- omnibase_infra/runtime/enums/enum_config_ref_scheme.py +33 -0
- omnibase_infra/runtime/enums/enum_scheduler_status.py +170 -0
- omnibase_infra/runtime/envelope_validator.py +179 -0
- omnibase_infra/runtime/handler_contract_source.py +669 -0
- omnibase_infra/runtime/handler_plugin_loader.py +2029 -0
- omnibase_infra/runtime/handler_registry.py +321 -0
- omnibase_infra/runtime/invocation_security_enforcer.py +427 -0
- omnibase_infra/runtime/kernel.py +40 -0
- omnibase_infra/runtime/mixin_policy_validation.py +522 -0
- omnibase_infra/runtime/mixin_semver_cache.py +378 -0
- omnibase_infra/runtime/mixins/__init__.py +17 -0
- omnibase_infra/runtime/mixins/mixin_projector_sql_operations.py +757 -0
- omnibase_infra/runtime/models/__init__.py +192 -0
- omnibase_infra/runtime/models/model_batch_lifecycle_result.py +217 -0
- omnibase_infra/runtime/models/model_binding_config.py +168 -0
- omnibase_infra/runtime/models/model_binding_config_cache_stats.py +135 -0
- omnibase_infra/runtime/models/model_binding_config_resolver_config.py +329 -0
- omnibase_infra/runtime/models/model_cached_secret.py +138 -0
- omnibase_infra/runtime/models/model_compute_key.py +138 -0
- omnibase_infra/runtime/models/model_compute_registration.py +97 -0
- omnibase_infra/runtime/models/model_config_cache_entry.py +61 -0
- omnibase_infra/runtime/models/model_config_ref.py +331 -0
- omnibase_infra/runtime/models/model_config_ref_parse_result.py +125 -0
- omnibase_infra/runtime/models/model_domain_plugin_config.py +92 -0
- omnibase_infra/runtime/models/model_domain_plugin_result.py +270 -0
- omnibase_infra/runtime/models/model_duplicate_response.py +54 -0
- omnibase_infra/runtime/models/model_enabled_protocols_config.py +61 -0
- omnibase_infra/runtime/models/model_event_bus_config.py +54 -0
- omnibase_infra/runtime/models/model_failed_component.py +55 -0
- omnibase_infra/runtime/models/model_health_check_response.py +168 -0
- omnibase_infra/runtime/models/model_health_check_result.py +228 -0
- omnibase_infra/runtime/models/model_lifecycle_result.py +245 -0
- omnibase_infra/runtime/models/model_logging_config.py +42 -0
- omnibase_infra/runtime/models/model_optional_correlation_id.py +167 -0
- omnibase_infra/runtime/models/model_optional_string.py +94 -0
- omnibase_infra/runtime/models/model_optional_uuid.py +110 -0
- omnibase_infra/runtime/models/model_policy_context.py +100 -0
- omnibase_infra/runtime/models/model_policy_key.py +138 -0
- omnibase_infra/runtime/models/model_policy_registration.py +139 -0
- omnibase_infra/runtime/models/model_policy_result.py +103 -0
- omnibase_infra/runtime/models/model_policy_type_filter.py +157 -0
- omnibase_infra/runtime/models/model_projector_plugin_loader_config.py +47 -0
- omnibase_infra/runtime/models/model_protocol_registration_config.py +65 -0
- omnibase_infra/runtime/models/model_retry_policy.py +105 -0
- omnibase_infra/runtime/models/model_runtime_config.py +150 -0
- omnibase_infra/runtime/models/model_runtime_scheduler_config.py +624 -0
- omnibase_infra/runtime/models/model_runtime_scheduler_metrics.py +233 -0
- omnibase_infra/runtime/models/model_runtime_tick.py +193 -0
- omnibase_infra/runtime/models/model_secret_cache_stats.py +82 -0
- omnibase_infra/runtime/models/model_secret_mapping.py +63 -0
- omnibase_infra/runtime/models/model_secret_resolver_config.py +107 -0
- omnibase_infra/runtime/models/model_secret_resolver_metrics.py +111 -0
- omnibase_infra/runtime/models/model_secret_source_info.py +72 -0
- omnibase_infra/runtime/models/model_secret_source_spec.py +66 -0
- omnibase_infra/runtime/models/model_shutdown_batch_result.py +75 -0
- omnibase_infra/runtime/models/model_shutdown_config.py +94 -0
- omnibase_infra/runtime/projector_plugin_loader.py +1462 -0
- omnibase_infra/runtime/projector_schema_manager.py +565 -0
- omnibase_infra/runtime/projector_shell.py +1102 -0
- omnibase_infra/runtime/protocol_contract_descriptor.py +92 -0
- omnibase_infra/runtime/protocol_contract_source.py +92 -0
- omnibase_infra/runtime/protocol_domain_plugin.py +474 -0
- omnibase_infra/runtime/protocol_handler_discovery.py +221 -0
- omnibase_infra/runtime/protocol_handler_plugin_loader.py +327 -0
- omnibase_infra/runtime/protocol_lifecycle_executor.py +435 -0
- omnibase_infra/runtime/protocol_policy.py +366 -0
- omnibase_infra/runtime/protocols/__init__.py +27 -0
- omnibase_infra/runtime/protocols/protocol_runtime_scheduler.py +468 -0
- omnibase_infra/runtime/registry/__init__.py +93 -0
- omnibase_infra/runtime/registry/mixin_message_type_query.py +326 -0
- omnibase_infra/runtime/registry/mixin_message_type_registration.py +354 -0
- omnibase_infra/runtime/registry/registry_event_bus_binding.py +268 -0
- omnibase_infra/runtime/registry/registry_message_type.py +542 -0
- omnibase_infra/runtime/registry/registry_protocol_binding.py +444 -0
- omnibase_infra/runtime/registry_compute.py +1143 -0
- omnibase_infra/runtime/registry_dispatcher.py +678 -0
- omnibase_infra/runtime/registry_policy.py +1502 -0
- omnibase_infra/runtime/runtime_scheduler.py +1070 -0
- omnibase_infra/runtime/secret_resolver.py +2110 -0
- omnibase_infra/runtime/security_metadata_validator.py +776 -0
- omnibase_infra/runtime/service_kernel.py +1573 -0
- omnibase_infra/runtime/service_message_dispatch_engine.py +1805 -0
- omnibase_infra/runtime/service_runtime_host_process.py +2260 -0
- omnibase_infra/runtime/util_container_wiring.py +1123 -0
- omnibase_infra/runtime/util_validation.py +314 -0
- omnibase_infra/runtime/util_version.py +98 -0
- omnibase_infra/runtime/util_wiring.py +566 -0
- omnibase_infra/schemas/schema_registration_projection.sql +320 -0
- omnibase_infra/services/__init__.py +68 -0
- omnibase_infra/services/corpus_capture.py +678 -0
- omnibase_infra/services/service_capability_query.py +945 -0
- omnibase_infra/services/service_health.py +897 -0
- omnibase_infra/services/service_node_selector.py +530 -0
- omnibase_infra/services/service_timeout_emitter.py +682 -0
- omnibase_infra/services/service_timeout_scanner.py +390 -0
- omnibase_infra/services/snapshot/__init__.py +31 -0
- omnibase_infra/services/snapshot/service_snapshot.py +647 -0
- omnibase_infra/services/snapshot/store_inmemory.py +637 -0
- omnibase_infra/services/snapshot/store_postgres.py +1279 -0
- omnibase_infra/shared/__init__.py +8 -0
- omnibase_infra/testing/__init__.py +10 -0
- omnibase_infra/testing/utils.py +23 -0
- omnibase_infra/types/__init__.py +48 -0
- omnibase_infra/types/type_cache_info.py +49 -0
- omnibase_infra/types/type_dsn.py +173 -0
- omnibase_infra/types/type_infra_aliases.py +60 -0
- omnibase_infra/types/typed_dict/__init__.py +21 -0
- omnibase_infra/types/typed_dict/typed_dict_introspection_cache.py +128 -0
- omnibase_infra/types/typed_dict/typed_dict_performance_metrics_cache.py +140 -0
- omnibase_infra/types/typed_dict_capabilities.py +64 -0
- omnibase_infra/utils/__init__.py +89 -0
- omnibase_infra/utils/correlation.py +208 -0
- omnibase_infra/utils/util_datetime.py +372 -0
- omnibase_infra/utils/util_dsn_validation.py +333 -0
- omnibase_infra/utils/util_env_parsing.py +264 -0
- omnibase_infra/utils/util_error_sanitization.py +457 -0
- omnibase_infra/utils/util_pydantic_validators.py +477 -0
- omnibase_infra/utils/util_semver.py +233 -0
- omnibase_infra/validation/__init__.py +307 -0
- omnibase_infra/validation/enums/__init__.py +11 -0
- omnibase_infra/validation/enums/enum_contract_violation_severity.py +13 -0
- omnibase_infra/validation/infra_validators.py +1486 -0
- omnibase_infra/validation/linter_contract.py +907 -0
- omnibase_infra/validation/mixin_any_type_classification.py +120 -0
- omnibase_infra/validation/mixin_any_type_exemption.py +580 -0
- omnibase_infra/validation/mixin_any_type_reporting.py +106 -0
- omnibase_infra/validation/mixin_execution_shape_violation_checks.py +596 -0
- omnibase_infra/validation/mixin_node_archetype_detection.py +254 -0
- omnibase_infra/validation/models/__init__.py +15 -0
- omnibase_infra/validation/models/model_contract_lint_result.py +101 -0
- omnibase_infra/validation/models/model_contract_violation.py +41 -0
- omnibase_infra/validation/service_validation_aggregator.py +395 -0
- omnibase_infra/validation/validation_exemptions.yaml +1710 -0
- omnibase_infra/validation/validator_any_type.py +715 -0
- omnibase_infra/validation/validator_chain_propagation.py +839 -0
- omnibase_infra/validation/validator_execution_shape.py +465 -0
- omnibase_infra/validation/validator_localhandler.py +261 -0
- omnibase_infra/validation/validator_registration_security.py +410 -0
- omnibase_infra/validation/validator_routing_coverage.py +1020 -0
- omnibase_infra/validation/validator_runtime_shape.py +915 -0
- omnibase_infra/validation/validator_security.py +410 -0
- omnibase_infra/validation/validator_topic_category.py +1152 -0
- omnibase_infra-0.2.1.dist-info/METADATA +197 -0
- omnibase_infra-0.2.1.dist-info/RECORD +675 -0
- omnibase_infra-0.2.1.dist-info/WHEEL +4 -0
- omnibase_infra-0.2.1.dist-info/entry_points.txt +4 -0
- omnibase_infra-0.2.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,1805 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 OmniNode Team
|
|
3
|
+
# ruff: noqa: TRY400
|
|
4
|
+
# TRY400 disabled: logger.error is intentional to avoid leaking sensitive data in stack traces
|
|
5
|
+
"""
|
|
6
|
+
Message Dispatch Engine.
|
|
7
|
+
|
|
8
|
+
Runtime dispatch engine for routing messages based on topic category and
|
|
9
|
+
message type. Routes incoming messages to registered dispatchers and collects
|
|
10
|
+
dispatcher outputs for publishing.
|
|
11
|
+
|
|
12
|
+
Design Principles:
|
|
13
|
+
- **Pure Routing**: Routes messages to dispatchers, no workflow inference
|
|
14
|
+
- **Deterministic**: Same input always produces same dispatcher selection
|
|
15
|
+
- **Fan-out Support**: Multiple dispatchers can process the same message type
|
|
16
|
+
- **Freeze-After-Init**: Thread-safe after registration phase completes
|
|
17
|
+
- **Observable**: Structured logging and comprehensive metrics
|
|
18
|
+
|
|
19
|
+
Architecture:
|
|
20
|
+
The dispatch engine provides:
|
|
21
|
+
- Route registration for topic pattern matching
|
|
22
|
+
- Dispatcher registration by category and message type
|
|
23
|
+
- Message dispatch with category validation
|
|
24
|
+
- Metrics collection for observability
|
|
25
|
+
- Structured logging for debugging and monitoring
|
|
26
|
+
|
|
27
|
+
It does NOT:
|
|
28
|
+
- Infer workflow semantics from message content
|
|
29
|
+
- Manage dispatcher lifecycle (dispatchers are external)
|
|
30
|
+
- Perform message transformation or enrichment
|
|
31
|
+
- Make decisions about message ordering or priority
|
|
32
|
+
|
|
33
|
+
Data Flow:
|
|
34
|
+
```
|
|
35
|
+
+------------------------------------------------------------------+
|
|
36
|
+
| Message Dispatch Engine |
|
|
37
|
+
+------------------------------------------------------------------+
|
|
38
|
+
| |
|
|
39
|
+
| 1. Parse Topic 2. Validate 3. Match Dispatchers |
|
|
40
|
+
| | | | |
|
|
41
|
+
| | topic string | category match | |
|
|
42
|
+
| |-------------------|----------------------| |
|
|
43
|
+
| | | | |
|
|
44
|
+
| | EnumMessageCategory | dispatchers[]|
|
|
45
|
+
| |<------------------| |------------>|
|
|
46
|
+
| | | | |
|
|
47
|
+
| 4. Execute Dispatchers 5. Collect Outputs 6. Return Result |
|
|
48
|
+
| | | | |
|
|
49
|
+
| | dispatcher outputs| aggregate | |
|
|
50
|
+
| |-------------------|----------------------| |
|
|
51
|
+
| | | | |
|
|
52
|
+
| | | ModelDispatchResult | |
|
|
53
|
+
| |<------------------|<---------------------| |
|
|
54
|
+
| |
|
|
55
|
+
+------------------------------------------------------------------+
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Thread Safety:
|
|
59
|
+
MessageDispatchEngine follows the "freeze after init" pattern:
|
|
60
|
+
|
|
61
|
+
1. **Registration Phase** (single-threaded): Register routes and dispatchers
|
|
62
|
+
2. **Freeze**: Call freeze() to prevent further modifications
|
|
63
|
+
3. **Dispatch Phase** (multi-threaded safe): Route messages to dispatchers
|
|
64
|
+
|
|
65
|
+
After freeze(), the engine becomes read-only and can be safely shared
|
|
66
|
+
across threads for concurrent dispatch operations.
|
|
67
|
+
|
|
68
|
+
**Metrics Thread Safety (TOCTOU Prevention)**:
|
|
69
|
+
A core design goal of the metrics system is preventing TOCTOU (time-of-check-
|
|
70
|
+
to-time-of-use) race conditions. Without proper synchronization, concurrent
|
|
71
|
+
dispatch operations could:
|
|
72
|
+
|
|
73
|
+
1. Read current metrics state (check)
|
|
74
|
+
2. Compute new values based on that state
|
|
75
|
+
3. Write updated values (use)
|
|
76
|
+
|
|
77
|
+
If another thread modifies the state between steps 1 and 3, the final write
|
|
78
|
+
would clobber that concurrent update, causing lost increments or corrupted
|
|
79
|
+
aggregations.
|
|
80
|
+
|
|
81
|
+
**Solution**: All read-modify-write operations on ``_structured_metrics`` are
|
|
82
|
+
performed atomically within a single ``_metrics_lock`` acquisition. This
|
|
83
|
+
ensures that the sequence (read → compute → write) completes without
|
|
84
|
+
interleaving from other threads.
|
|
85
|
+
|
|
86
|
+
**Why holding the lock during computation is acceptable**:
|
|
87
|
+
The computations within the lock (``record_execution()``, ``model_copy()``)
|
|
88
|
+
are:
|
|
89
|
+
|
|
90
|
+
- **Pure**: No I/O, no external calls, no blocking operations
|
|
91
|
+
- **Fast**: Simple arithmetic and Pydantic model copying (~microseconds)
|
|
92
|
+
- **Bounded**: Fixed computational complexity regardless of data size
|
|
93
|
+
|
|
94
|
+
The lock is NEVER held during I/O operations (dispatcher execution), ensuring
|
|
95
|
+
that slow dispatchers do not block metrics updates in other threads.
|
|
96
|
+
|
|
97
|
+
For production monitoring, use ``get_structured_metrics()`` which returns
|
|
98
|
+
a consistent snapshot.
|
|
99
|
+
|
|
100
|
+
Related:
|
|
101
|
+
- OMN-934: Message dispatch engine implementation
|
|
102
|
+
- EnvelopeRouter: Transport-agnostic orchestrator (reference for freeze pattern)
|
|
103
|
+
|
|
104
|
+
Category Support:
|
|
105
|
+
The engine supports three ONEX message categories for routing:
|
|
106
|
+
- EVENT: Domain events (e.g., UserCreatedEvent)
|
|
107
|
+
- COMMAND: Action requests (e.g., CreateUserCommand)
|
|
108
|
+
- INTENT: User intentions (e.g., ProvisionUserIntent)
|
|
109
|
+
|
|
110
|
+
Topic naming constraints:
|
|
111
|
+
- EVENT topics: Must contain ".events" segment
|
|
112
|
+
- COMMAND topics: Must contain ".commands" segment
|
|
113
|
+
- INTENT topics: Must contain ".intents" segment
|
|
114
|
+
|
|
115
|
+
Note on PROJECTION:
|
|
116
|
+
PROJECTION is NOT a message category for routing. Projections are
|
|
117
|
+
node output types (EnumNodeOutputType.PROJECTION) produced by REDUCER
|
|
118
|
+
nodes as local state outputs. Projections are:
|
|
119
|
+
- NOT routed via Kafka topics
|
|
120
|
+
- NOT part of EnumMessageCategory
|
|
121
|
+
- Applied locally by the runtime to a projection sink
|
|
122
|
+
|
|
123
|
+
See EnumNodeOutputType for projection semantics and CLAUDE.md
|
|
124
|
+
"Enum Usage" section for the distinction between message categories
|
|
125
|
+
and node output types.
|
|
126
|
+
|
|
127
|
+
.. versionadded:: 0.4.0
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
from __future__ import annotations
|
|
131
|
+
|
|
132
|
+
__all__ = ["MessageDispatchEngine"]
|
|
133
|
+
|
|
134
|
+
import asyncio
|
|
135
|
+
import inspect
|
|
136
|
+
import logging
|
|
137
|
+
import threading
|
|
138
|
+
import time
|
|
139
|
+
from collections.abc import Awaitable, Callable
|
|
140
|
+
from datetime import UTC, datetime
|
|
141
|
+
from typing import TYPE_CHECKING, TypedDict, Unpack, cast, overload
|
|
142
|
+
from uuid import UUID, uuid4
|
|
143
|
+
|
|
144
|
+
from pydantic import ValidationError
|
|
145
|
+
|
|
146
|
+
from omnibase_core.enums import EnumCoreErrorCode
|
|
147
|
+
from omnibase_core.models.errors import ModelOnexError
|
|
148
|
+
from omnibase_core.models.events.model_event_envelope import ModelEventEnvelope
|
|
149
|
+
from omnibase_core.types import PrimitiveValue
|
|
150
|
+
from omnibase_infra.enums import (
|
|
151
|
+
EnumDispatchStatus,
|
|
152
|
+
EnumInfraTransportType,
|
|
153
|
+
EnumMessageCategory,
|
|
154
|
+
)
|
|
155
|
+
from omnibase_infra.errors import ModelInfraErrorContext, ProtocolConfigurationError
|
|
156
|
+
from omnibase_infra.models.dispatch.model_dispatch_context import ModelDispatchContext
|
|
157
|
+
from omnibase_infra.models.dispatch.model_dispatch_log_context import (
|
|
158
|
+
ModelDispatchLogContext,
|
|
159
|
+
)
|
|
160
|
+
from omnibase_infra.models.dispatch.model_dispatch_metrics import ModelDispatchMetrics
|
|
161
|
+
from omnibase_infra.models.dispatch.model_dispatch_outcome import ModelDispatchOutcome
|
|
162
|
+
from omnibase_infra.models.dispatch.model_dispatch_outputs import ModelDispatchOutputs
|
|
163
|
+
from omnibase_infra.models.dispatch.model_dispatch_result import ModelDispatchResult
|
|
164
|
+
from omnibase_infra.models.dispatch.model_dispatch_route import ModelDispatchRoute
|
|
165
|
+
from omnibase_infra.models.dispatch.model_dispatcher_metrics import (
|
|
166
|
+
ModelDispatcherMetrics,
|
|
167
|
+
)
|
|
168
|
+
from omnibase_infra.runtime.dispatch_context_enforcer import DispatchContextEnforcer
|
|
169
|
+
from omnibase_infra.utils import sanitize_error_message
|
|
170
|
+
|
|
171
|
+
if TYPE_CHECKING:
|
|
172
|
+
from omnibase_core.enums.enum_node_kind import EnumNodeKind
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class ModelLogContextKwargs(TypedDict, total=False):
|
|
176
|
+
"""TypedDict for _build_log_context kwargs to ensure type safety.
|
|
177
|
+
|
|
178
|
+
All fields are optional (total=False) since callers pass only the
|
|
179
|
+
relevant subset. ModelDispatchLogContext validators handle None-to-sentinel
|
|
180
|
+
conversion.
|
|
181
|
+
|
|
182
|
+
.. versionadded:: 0.6.3
|
|
183
|
+
Created as part of Union Reduction Phase 2 (OMN-1002) to eliminate
|
|
184
|
+
type: ignore comment in _build_log_context.
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
topic: str | None
|
|
188
|
+
category: EnumMessageCategory | None
|
|
189
|
+
message_type: str | None
|
|
190
|
+
dispatcher_id: str | None
|
|
191
|
+
dispatcher_count: int | None
|
|
192
|
+
duration_ms: float | None
|
|
193
|
+
correlation_id: UUID | None
|
|
194
|
+
trace_id: UUID | None
|
|
195
|
+
error_code: EnumCoreErrorCode | None
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
# Type alias for dispatcher output topics
|
|
199
|
+
#
|
|
200
|
+
# Dispatchers can return:
|
|
201
|
+
# - str: A single output topic
|
|
202
|
+
# - list[str]: Multiple output topics
|
|
203
|
+
# - None: No output topics to publish
|
|
204
|
+
# - ModelDispatchResult: Protocol-based dispatchers return this for structured output
|
|
205
|
+
DispatcherOutput = str | list[str] | None | ModelDispatchResult
|
|
206
|
+
|
|
207
|
+
# Module-level logger for fallback when no custom logger is provided
|
|
208
|
+
_module_logger = logging.getLogger(__name__)
|
|
209
|
+
|
|
210
|
+
# Minimum number of parameters for a dispatcher to be considered context-aware.
|
|
211
|
+
# Context-aware dispatchers have signature: (envelope, context, ...)
|
|
212
|
+
# Non-context-aware dispatchers have signature: (envelope)
|
|
213
|
+
# We use >= MIN_PARAMS_FOR_CONTEXT (not ==) to support dispatchers with additional
|
|
214
|
+
# optional parameters (e.g., for testing, logging, or future extensibility).
|
|
215
|
+
MIN_PARAMS_FOR_CONTEXT = 2
|
|
216
|
+
|
|
217
|
+
# Type alias for dispatcher functions
|
|
218
|
+
#
|
|
219
|
+
# Design Note (PR #61 Review):
|
|
220
|
+
# ModelEventEnvelope[object] is used instead of Any to satisfy ONEX "no Any types" rule.
|
|
221
|
+
#
|
|
222
|
+
# Rationale:
|
|
223
|
+
# - Input: ModelEventEnvelope[object] is intentionally generic because dispatchers
|
|
224
|
+
# must accept envelopes with any payload type. The dispatch engine routes based
|
|
225
|
+
# on topic/category/message_type, not payload shape. Using a TypeVar would require
|
|
226
|
+
# dispatchers to be generic, adding complexity without benefit since the engine
|
|
227
|
+
# already performs type-based routing.
|
|
228
|
+
# - Output: DispatcherOutput | Awaitable[DispatcherOutput] defines the valid return
|
|
229
|
+
# types: str (single topic), list[str] (multiple topics), or None (no output).
|
|
230
|
+
# Dispatchers can be sync or async.
|
|
231
|
+
#
|
|
232
|
+
# Using `object` instead of `Any` provides:
|
|
233
|
+
# - Explicit "any object" semantics that are more informative to type checkers
|
|
234
|
+
# - Compliance with ONEX coding guidelines
|
|
235
|
+
# - Same runtime behavior as Any but with clearer intent
|
|
236
|
+
#
|
|
237
|
+
# See also: ProtocolMessageDispatcher in dispatcher_registry.py for protocol-based
|
|
238
|
+
# dispatchers that return ModelDispatchResult.
|
|
239
|
+
DispatcherFunc = Callable[
|
|
240
|
+
[ModelEventEnvelope[object]], DispatcherOutput | Awaitable[DispatcherOutput]
|
|
241
|
+
]
|
|
242
|
+
|
|
243
|
+
# Context-aware dispatcher type (for dispatchers registered with node_kind)
|
|
244
|
+
# These dispatchers receive a ModelDispatchContext with time injection based on node_kind:
|
|
245
|
+
# - REDUCER/COMPUTE: now=None (deterministic)
|
|
246
|
+
# - ORCHESTRATOR/EFFECT/RUNTIME_HOST: now=datetime.now(UTC)
|
|
247
|
+
#
|
|
248
|
+
# This type is used when register_dispatcher() is called with node_kind parameter.
|
|
249
|
+
# The dispatch engine inspects the callable's signature to determine if it accepts context.
|
|
250
|
+
ContextAwareDispatcherFunc = Callable[
|
|
251
|
+
[ModelEventEnvelope[object], ModelDispatchContext],
|
|
252
|
+
DispatcherOutput | Awaitable[DispatcherOutput],
|
|
253
|
+
]
|
|
254
|
+
|
|
255
|
+
# Sync-only dispatcher type for use with run_in_executor
|
|
256
|
+
# Used internally after runtime type narrowing via inspect.iscoroutinefunction
|
|
257
|
+
_SyncDispatcherFunc = Callable[[ModelEventEnvelope[object]], DispatcherOutput]
|
|
258
|
+
|
|
259
|
+
# Sync-only context-aware dispatcher type for use with run_in_executor
|
|
260
|
+
_SyncContextAwareDispatcherFunc = Callable[
|
|
261
|
+
[ModelEventEnvelope[object], ModelDispatchContext], DispatcherOutput
|
|
262
|
+
]
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
class DispatchEntryInternal:
|
|
266
|
+
"""
|
|
267
|
+
Internal storage for dispatcher registration metadata.
|
|
268
|
+
|
|
269
|
+
This class is an implementation detail and not part of the public API.
|
|
270
|
+
It stores the dispatcher callable and associated metadata for the
|
|
271
|
+
MessageDispatchEngine's internal routing.
|
|
272
|
+
|
|
273
|
+
Attributes:
|
|
274
|
+
dispatcher_id: Unique identifier for this dispatcher.
|
|
275
|
+
dispatcher: The callable that processes messages.
|
|
276
|
+
category: Message category this dispatcher handles.
|
|
277
|
+
message_types: Specific message types to handle (None = all types).
|
|
278
|
+
node_kind: Optional ONEX node kind for time injection context.
|
|
279
|
+
When set, the dispatcher receives a ModelDispatchContext with
|
|
280
|
+
appropriate time injection based on ONEX rules:
|
|
281
|
+
- REDUCER/COMPUTE: now=None (deterministic)
|
|
282
|
+
- ORCHESTRATOR/EFFECT/RUNTIME_HOST: now=datetime.now(UTC)
|
|
283
|
+
accepts_context: Cached result of signature inspection indicating
|
|
284
|
+
whether the dispatcher accepts a context parameter (2+ params).
|
|
285
|
+
Computed once at registration time for performance.
|
|
286
|
+
"""
|
|
287
|
+
|
|
288
|
+
__slots__ = (
|
|
289
|
+
"accepts_context",
|
|
290
|
+
"category",
|
|
291
|
+
"dispatcher",
|
|
292
|
+
"dispatcher_id",
|
|
293
|
+
"message_types",
|
|
294
|
+
"node_kind",
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
def __init__(
|
|
298
|
+
self,
|
|
299
|
+
dispatcher_id: str,
|
|
300
|
+
dispatcher: DispatcherFunc | ContextAwareDispatcherFunc,
|
|
301
|
+
category: EnumMessageCategory,
|
|
302
|
+
message_types: set[str] | None,
|
|
303
|
+
node_kind: EnumNodeKind | None = None,
|
|
304
|
+
accepts_context: bool = False,
|
|
305
|
+
) -> None:
|
|
306
|
+
self.dispatcher_id = dispatcher_id
|
|
307
|
+
self.dispatcher = dispatcher
|
|
308
|
+
self.category = category
|
|
309
|
+
self.message_types = message_types # None means "all types"
|
|
310
|
+
self.node_kind = node_kind # None means no context injection
|
|
311
|
+
self.accepts_context = accepts_context # Cached: dispatcher has 2+ params
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
class MessageDispatchEngine:
|
|
315
|
+
"""
|
|
316
|
+
Runtime dispatch engine for message routing.
|
|
317
|
+
|
|
318
|
+
Routes messages based on topic category and message type to registered
|
|
319
|
+
dispatchers. Supports fan-out (multiple dispatchers per message type) and
|
|
320
|
+
collects dispatcher outputs for publishing.
|
|
321
|
+
|
|
322
|
+
Key Characteristics:
|
|
323
|
+
- **Pure Routing**: No workflow inference or semantic understanding
|
|
324
|
+
- **Deterministic**: Same input always produces same dispatcher selection
|
|
325
|
+
- **Fan-out**: Multiple dispatchers can process the same message type
|
|
326
|
+
- **Observable**: Structured logging and comprehensive metrics
|
|
327
|
+
|
|
328
|
+
Registration Semantics:
|
|
329
|
+
- **Routes**: Keyed by route_id, duplicates raise error
|
|
330
|
+
- **Dispatchers**: Keyed by dispatcher_id, duplicates raise error
|
|
331
|
+
- Both must complete before freeze() is called
|
|
332
|
+
|
|
333
|
+
Thread Safety:
|
|
334
|
+
Follows the freeze-after-init pattern. All registrations must complete
|
|
335
|
+
before calling freeze(). After freeze(), dispatch operations are
|
|
336
|
+
thread-safe for concurrent access.
|
|
337
|
+
|
|
338
|
+
**TOCTOU Prevention** (core design goal):
|
|
339
|
+
Structured metrics use ``_metrics_lock`` to ensure atomic read-modify-write
|
|
340
|
+
operations. Without this, concurrent dispatches could lose updates:
|
|
341
|
+
|
|
342
|
+
- Thread A reads metrics, computes increment
|
|
343
|
+
- Thread B reads (stale) metrics, computes increment
|
|
344
|
+
- Thread A writes → Thread B writes → Thread A's update is lost
|
|
345
|
+
|
|
346
|
+
By holding the lock during the entire read→compute→write sequence, we
|
|
347
|
+
guarantee no interleaving occurs. The computations within the lock are
|
|
348
|
+
pure and fast (~microseconds), so lock contention is minimal.
|
|
349
|
+
|
|
350
|
+
- Structured metrics: Use ``_metrics_lock`` for atomic updates
|
|
351
|
+
- Use ``get_structured_metrics()`` for production monitoring
|
|
352
|
+
|
|
353
|
+
**METRICS CAVEAT**: While metrics updates are protected by a lock,
|
|
354
|
+
get_structured_metrics() provides point-in-time snapshots. Under high
|
|
355
|
+
concurrent load, metrics may be approximate between snapshot reads.
|
|
356
|
+
For production monitoring, consider exporting metrics to a dedicated
|
|
357
|
+
metrics backend (Prometheus, StatsD, etc.) for accurate aggregation
|
|
358
|
+
across time windows.
|
|
359
|
+
|
|
360
|
+
Logging Levels:
|
|
361
|
+
- **INFO**: Dispatch start/complete with topic, category, dispatcher count
|
|
362
|
+
- **DEBUG**: Dispatcher execution details, routing decisions
|
|
363
|
+
- **WARNING**: No dispatchers found, category mismatches
|
|
364
|
+
- **ERROR**: Dispatcher exceptions, validation failures
|
|
365
|
+
|
|
366
|
+
Example:
|
|
367
|
+
>>> from omnibase_infra.runtime import MessageDispatchEngine
|
|
368
|
+
>>> from omnibase_infra.models.dispatch import ModelDispatchRoute
|
|
369
|
+
>>> from omnibase_infra.enums import EnumMessageCategory
|
|
370
|
+
>>>
|
|
371
|
+
>>> # Create engine with optional custom logger
|
|
372
|
+
>>> engine = MessageDispatchEngine(logger=my_logger)
|
|
373
|
+
>>> engine.register_dispatcher(
|
|
374
|
+
... dispatcher_id="user-dispatcher",
|
|
375
|
+
... dispatcher=process_user_event,
|
|
376
|
+
... category=EnumMessageCategory.EVENT,
|
|
377
|
+
... message_types={"UserCreated", "UserUpdated"},
|
|
378
|
+
... )
|
|
379
|
+
>>> engine.register_route(ModelDispatchRoute(
|
|
380
|
+
... route_id="user-route",
|
|
381
|
+
... topic_pattern="*.user.events.*",
|
|
382
|
+
... message_category=EnumMessageCategory.EVENT,
|
|
383
|
+
... dispatcher_id="user-dispatcher",
|
|
384
|
+
... ))
|
|
385
|
+
>>> engine.freeze()
|
|
386
|
+
>>>
|
|
387
|
+
>>> # Dispatch (thread-safe after freeze)
|
|
388
|
+
>>> result = await engine.dispatch("dev.user.events.v1", envelope)
|
|
389
|
+
|
|
390
|
+
Attributes:
|
|
391
|
+
_routes: Registry of routes by route_id
|
|
392
|
+
_dispatchers: Registry of dispatchers by dispatcher_id
|
|
393
|
+
_dispatchers_by_category: Index of dispatchers by category for fast lookup
|
|
394
|
+
_frozen: If True, registration methods raise ModelOnexError
|
|
395
|
+
_registration_lock: Lock protecting registration methods
|
|
396
|
+
_metrics_lock: Lock protecting structured metrics updates
|
|
397
|
+
_structured_metrics: Pydantic-based metrics model for observability
|
|
398
|
+
_logger: Optional custom logger for structured logging
|
|
399
|
+
|
|
400
|
+
See Also:
|
|
401
|
+
- :class:`~omnibase_infra.models.dispatch.ModelDispatchRoute`: Route model
|
|
402
|
+
- :class:`~omnibase_infra.models.dispatch.ModelDispatchResult`: Result model
|
|
403
|
+
- :class:`~omnibase_infra.models.dispatch.ModelDispatchMetrics`: Metrics model
|
|
404
|
+
- :class:`~omnibase_core.runtime.EnvelopeRouter`: Reference implementation
|
|
405
|
+
|
|
406
|
+
.. versionadded:: 0.4.0
|
|
407
|
+
"""
|
|
408
|
+
|
|
409
|
+
def __init__(
|
|
410
|
+
self,
|
|
411
|
+
logger: logging.Logger | None = None,
|
|
412
|
+
) -> None:
|
|
413
|
+
"""
|
|
414
|
+
Initialize MessageDispatchEngine with empty registries.
|
|
415
|
+
|
|
416
|
+
Creates empty route and dispatcher registries and initializes metrics.
|
|
417
|
+
Call freeze() after registration to enable thread-safe dispatch.
|
|
418
|
+
|
|
419
|
+
Args:
|
|
420
|
+
logger: Optional custom logger for structured logging.
|
|
421
|
+
If not provided, uses module-level logger.
|
|
422
|
+
"""
|
|
423
|
+
# Optional custom logger
|
|
424
|
+
self._logger: logging.Logger = logger if logger is not None else _module_logger
|
|
425
|
+
|
|
426
|
+
# Route storage: route_id -> ModelDispatchRoute
|
|
427
|
+
self._routes: dict[str, ModelDispatchRoute] = {}
|
|
428
|
+
|
|
429
|
+
# Dispatcher storage: dispatcher_id -> DispatchEntryInternal
|
|
430
|
+
self._dispatchers: dict[str, DispatchEntryInternal] = {}
|
|
431
|
+
|
|
432
|
+
# Index for fast dispatcher lookup by category
|
|
433
|
+
# category -> list of dispatcher_ids
|
|
434
|
+
# NOTE: Only routable message categories are indexed here.
|
|
435
|
+
# PROJECTION is NOT included because projections are reducer outputs,
|
|
436
|
+
# not routable messages. See CLAUDE.md "Enum Usage" section.
|
|
437
|
+
self._dispatchers_by_category: dict[EnumMessageCategory, list[str]] = {
|
|
438
|
+
EnumMessageCategory.EVENT: [],
|
|
439
|
+
EnumMessageCategory.COMMAND: [],
|
|
440
|
+
EnumMessageCategory.INTENT: [],
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
# Freeze state
|
|
444
|
+
self._frozen: bool = False
|
|
445
|
+
self._registration_lock: threading.Lock = threading.Lock()
|
|
446
|
+
|
|
447
|
+
# Metrics lock for TOCTOU-safe structured metrics updates
|
|
448
|
+
# This lock protects the entire read-modify-write sequence on _structured_metrics:
|
|
449
|
+
# 1. Read current metrics state
|
|
450
|
+
# 2. Compute new values (record_execution, model_copy)
|
|
451
|
+
# 3. Write updated metrics back
|
|
452
|
+
# Holding the lock during computation prevents lost updates from concurrent dispatches.
|
|
453
|
+
# The computations are pure and fast (~microseconds), minimizing lock contention.
|
|
454
|
+
self._metrics_lock: threading.Lock = threading.Lock()
|
|
455
|
+
|
|
456
|
+
# Structured metrics (Pydantic model)
|
|
457
|
+
self._structured_metrics: ModelDispatchMetrics = ModelDispatchMetrics()
|
|
458
|
+
|
|
459
|
+
# Context enforcer for creating dispatch contexts based on node_kind.
|
|
460
|
+
# Delegates time injection rule enforcement to a single source of truth.
|
|
461
|
+
self._context_enforcer: DispatchContextEnforcer = DispatchContextEnforcer()
|
|
462
|
+
|
|
463
|
+
def register_route(self, route: ModelDispatchRoute) -> None:
|
|
464
|
+
"""
|
|
465
|
+
Register a routing rule.
|
|
466
|
+
|
|
467
|
+
Routes define how messages are matched to dispatchers based on topic
|
|
468
|
+
pattern, message category, and optionally message type.
|
|
469
|
+
|
|
470
|
+
Args:
|
|
471
|
+
route: The routing rule to register. Must have unique route_id.
|
|
472
|
+
|
|
473
|
+
Raises:
|
|
474
|
+
ModelOnexError: If engine is frozen (INVALID_STATE)
|
|
475
|
+
ModelOnexError: If route is None (INVALID_PARAMETER)
|
|
476
|
+
ModelOnexError: If route with same route_id exists (DUPLICATE_REGISTRATION)
|
|
477
|
+
ModelOnexError: If route.dispatcher_id references non-existent dispatcher
|
|
478
|
+
(ITEM_NOT_REGISTERED) - only checked after freeze
|
|
479
|
+
|
|
480
|
+
Example:
|
|
481
|
+
>>> engine.register_route(ModelDispatchRoute(
|
|
482
|
+
... route_id="order-events",
|
|
483
|
+
... topic_pattern="*.order.events.*",
|
|
484
|
+
... message_category=EnumMessageCategory.EVENT,
|
|
485
|
+
... dispatcher_id="order-dispatcher",
|
|
486
|
+
... ))
|
|
487
|
+
|
|
488
|
+
Note:
|
|
489
|
+
Route-to-dispatcher consistency is NOT validated during registration
|
|
490
|
+
to allow flexible registration order. Validation occurs at freeze()
|
|
491
|
+
time or during dispatch.
|
|
492
|
+
"""
|
|
493
|
+
if route is None:
|
|
494
|
+
raise ModelOnexError(
|
|
495
|
+
message="Cannot register None route. ModelDispatchRoute is required.",
|
|
496
|
+
error_code=EnumCoreErrorCode.INVALID_PARAMETER,
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
with self._registration_lock:
|
|
500
|
+
if self._frozen:
|
|
501
|
+
raise ModelOnexError(
|
|
502
|
+
message="Cannot register route: MessageDispatchEngine is frozen. "
|
|
503
|
+
"Registration is not allowed after freeze() has been called.",
|
|
504
|
+
error_code=EnumCoreErrorCode.INVALID_STATE,
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
if route.route_id in self._routes:
|
|
508
|
+
raise ModelOnexError(
|
|
509
|
+
message=f"Route with ID '{route.route_id}' is already registered. "
|
|
510
|
+
"Cannot register duplicate route ID.",
|
|
511
|
+
error_code=EnumCoreErrorCode.DUPLICATE_REGISTRATION,
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
self._routes[route.route_id] = route
|
|
515
|
+
self._logger.debug(
|
|
516
|
+
"Registered route '%s' for pattern '%s' (category=%s, dispatcher=%s)",
|
|
517
|
+
route.route_id,
|
|
518
|
+
route.topic_pattern,
|
|
519
|
+
route.message_category,
|
|
520
|
+
route.dispatcher_id,
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
# --- @overload stubs for static type safety ---
|
|
524
|
+
#
|
|
525
|
+
# NOTE: These are TYPE STUBS only - they provide no runtime behavior.
|
|
526
|
+
# The actual implementation is in the non-overloaded register_dispatcher() below.
|
|
527
|
+
#
|
|
528
|
+
# Purpose: Enable type checkers (mypy, pyright) to validate that:
|
|
529
|
+
# - When node_kind=None (or omitted): dispatcher must be DispatcherFunc
|
|
530
|
+
# - When node_kind=EnumNodeKind: dispatcher must be ContextAwareDispatcherFunc
|
|
531
|
+
#
|
|
532
|
+
# This pattern enforces compile-time type safety for the relationship between
|
|
533
|
+
# node_kind presence and expected dispatcher signature.
|
|
534
|
+
#
|
|
535
|
+
# See ADR_DISPATCHER_TYPE_SAFETY.md Option 4 for design rationale.
|
|
536
|
+
|
|
537
|
+
@overload
|
|
538
|
+
def register_dispatcher(
|
|
539
|
+
self,
|
|
540
|
+
dispatcher_id: str,
|
|
541
|
+
dispatcher: DispatcherFunc,
|
|
542
|
+
category: EnumMessageCategory,
|
|
543
|
+
message_types: set[str] | None = None,
|
|
544
|
+
node_kind: None = None,
|
|
545
|
+
) -> None: ... # Stub: no node_kind -> DispatcherFunc (no context)
|
|
546
|
+
|
|
547
|
+
@overload
|
|
548
|
+
def register_dispatcher(
|
|
549
|
+
self,
|
|
550
|
+
dispatcher_id: str,
|
|
551
|
+
dispatcher: ContextAwareDispatcherFunc,
|
|
552
|
+
category: EnumMessageCategory,
|
|
553
|
+
message_types: set[str] | None = None,
|
|
554
|
+
*,
|
|
555
|
+
node_kind: EnumNodeKind,
|
|
556
|
+
) -> None: ... # Stub: with node_kind -> ContextAwareDispatcherFunc (gets context)
|
|
557
|
+
|
|
558
|
+
def register_dispatcher(
|
|
559
|
+
self,
|
|
560
|
+
dispatcher_id: str,
|
|
561
|
+
dispatcher: DispatcherFunc | ContextAwareDispatcherFunc,
|
|
562
|
+
category: EnumMessageCategory,
|
|
563
|
+
message_types: set[str] | None = None,
|
|
564
|
+
node_kind: EnumNodeKind | None = None,
|
|
565
|
+
) -> None:
|
|
566
|
+
"""
|
|
567
|
+
Register a message dispatcher.
|
|
568
|
+
|
|
569
|
+
Dispatchers process messages that match their category and (optionally)
|
|
570
|
+
message type. Multiple dispatchers can register for the same category
|
|
571
|
+
and message type (fan-out pattern).
|
|
572
|
+
|
|
573
|
+
Args:
|
|
574
|
+
dispatcher_id: Unique identifier for this dispatcher
|
|
575
|
+
dispatcher: Callable that processes messages. Can be sync or async.
|
|
576
|
+
Signature: (envelope: ModelEventEnvelope[object]) -> DispatcherOutput
|
|
577
|
+
Or with context:
|
|
578
|
+
(envelope: ModelEventEnvelope[object], context: ModelDispatchContext) -> DispatcherOutput
|
|
579
|
+
category: Message category this dispatcher processes
|
|
580
|
+
message_types: Optional set of specific message types to handle.
|
|
581
|
+
When None, handles all message types in the category.
|
|
582
|
+
node_kind: Optional ONEX node kind for time injection context.
|
|
583
|
+
When provided, the dispatcher receives a ModelDispatchContext
|
|
584
|
+
with appropriate time injection based on ONEX rules:
|
|
585
|
+
- REDUCER/COMPUTE: now=None (deterministic execution)
|
|
586
|
+
- ORCHESTRATOR/EFFECT/RUNTIME_HOST: now=datetime.now(UTC)
|
|
587
|
+
When None, dispatcher is called without context.
|
|
588
|
+
|
|
589
|
+
Raises:
|
|
590
|
+
ModelOnexError: If engine is frozen (INVALID_STATE)
|
|
591
|
+
ModelOnexError: If dispatcher_id is empty (INVALID_PARAMETER)
|
|
592
|
+
ModelOnexError: If dispatcher is not callable (INVALID_PARAMETER)
|
|
593
|
+
ModelOnexError: If dispatcher with same ID exists (DUPLICATE_REGISTRATION)
|
|
594
|
+
|
|
595
|
+
Example:
|
|
596
|
+
>>> async def process_user_event(envelope):
|
|
597
|
+
... user_data = envelope.payload
|
|
598
|
+
... # Process the event
|
|
599
|
+
... return {"processed": True}
|
|
600
|
+
>>>
|
|
601
|
+
>>> engine.register_dispatcher(
|
|
602
|
+
... dispatcher_id="user-event-dispatcher",
|
|
603
|
+
... dispatcher=process_user_event,
|
|
604
|
+
... category=EnumMessageCategory.EVENT,
|
|
605
|
+
... message_types={"UserCreated", "UserUpdated"},
|
|
606
|
+
... )
|
|
607
|
+
>>>
|
|
608
|
+
>>> # With time injection context for orchestrator
|
|
609
|
+
>>> async def process_with_context(envelope, context):
|
|
610
|
+
... current_time = context.now # Injected time
|
|
611
|
+
... return "processed"
|
|
612
|
+
>>>
|
|
613
|
+
>>> engine.register_dispatcher(
|
|
614
|
+
... dispatcher_id="orchestrator-dispatcher",
|
|
615
|
+
... dispatcher=process_with_context,
|
|
616
|
+
... category=EnumMessageCategory.COMMAND,
|
|
617
|
+
... node_kind=EnumNodeKind.ORCHESTRATOR,
|
|
618
|
+
... )
|
|
619
|
+
|
|
620
|
+
Note:
|
|
621
|
+
Dispatchers are NOT automatically linked to routes. You must register
|
|
622
|
+
routes separately that reference the dispatcher_id.
|
|
623
|
+
|
|
624
|
+
.. versionchanged:: 0.5.0
|
|
625
|
+
Added ``node_kind`` parameter for time injection context support.
|
|
626
|
+
"""
|
|
627
|
+
# Validate inputs before acquiring lock
|
|
628
|
+
if not dispatcher_id or not dispatcher_id.strip():
|
|
629
|
+
raise ModelOnexError(
|
|
630
|
+
message="Dispatcher ID cannot be empty or whitespace.",
|
|
631
|
+
error_code=EnumCoreErrorCode.INVALID_PARAMETER,
|
|
632
|
+
)
|
|
633
|
+
|
|
634
|
+
if dispatcher is None or not callable(dispatcher):
|
|
635
|
+
raise ModelOnexError(
|
|
636
|
+
message=f"Dispatcher for '{dispatcher_id}' must be callable. "
|
|
637
|
+
f"Got {type(dispatcher).__name__}.",
|
|
638
|
+
error_code=EnumCoreErrorCode.INVALID_PARAMETER,
|
|
639
|
+
)
|
|
640
|
+
|
|
641
|
+
if not isinstance(category, EnumMessageCategory):
|
|
642
|
+
raise ModelOnexError(
|
|
643
|
+
message=f"Category must be EnumMessageCategory, got {type(category).__name__}.",
|
|
644
|
+
error_code=EnumCoreErrorCode.INVALID_PARAMETER,
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
# Runtime validation for node_kind to catch dynamic dispatch issues
|
|
648
|
+
# where type checkers can't help (e.g., dynamically constructed arguments)
|
|
649
|
+
if node_kind is not None:
|
|
650
|
+
# Import here to avoid circular import at module level
|
|
651
|
+
# EnumNodeKind is only in TYPE_CHECKING block at top of file
|
|
652
|
+
from omnibase_core.enums.enum_node_kind import EnumNodeKind
|
|
653
|
+
|
|
654
|
+
if not isinstance(node_kind, EnumNodeKind):
|
|
655
|
+
context = ModelInfraErrorContext.with_correlation(
|
|
656
|
+
transport_type=EnumInfraTransportType.RUNTIME,
|
|
657
|
+
operation="register_dispatcher",
|
|
658
|
+
)
|
|
659
|
+
raise ProtocolConfigurationError(
|
|
660
|
+
f"node_kind must be EnumNodeKind or None, got {type(node_kind).__name__}",
|
|
661
|
+
context=context,
|
|
662
|
+
)
|
|
663
|
+
|
|
664
|
+
with self._registration_lock:
|
|
665
|
+
if self._frozen:
|
|
666
|
+
raise ModelOnexError(
|
|
667
|
+
message="Cannot register dispatcher: MessageDispatchEngine is frozen. "
|
|
668
|
+
"Registration is not allowed after freeze() has been called.",
|
|
669
|
+
error_code=EnumCoreErrorCode.INVALID_STATE,
|
|
670
|
+
)
|
|
671
|
+
|
|
672
|
+
if dispatcher_id in self._dispatchers:
|
|
673
|
+
raise ModelOnexError(
|
|
674
|
+
message=f"Dispatcher with ID '{dispatcher_id}' is already registered. "
|
|
675
|
+
"Cannot register duplicate dispatcher ID.",
|
|
676
|
+
error_code=EnumCoreErrorCode.DUPLICATE_REGISTRATION,
|
|
677
|
+
)
|
|
678
|
+
|
|
679
|
+
# Compute accepts_context once at registration time (cached)
|
|
680
|
+
# This avoids expensive inspect.signature() calls on every dispatch
|
|
681
|
+
accepts_context = self._dispatcher_accepts_context(dispatcher)
|
|
682
|
+
|
|
683
|
+
# Store dispatcher entry
|
|
684
|
+
entry = DispatchEntryInternal(
|
|
685
|
+
dispatcher_id=dispatcher_id,
|
|
686
|
+
dispatcher=dispatcher,
|
|
687
|
+
category=category,
|
|
688
|
+
message_types=message_types,
|
|
689
|
+
node_kind=node_kind,
|
|
690
|
+
accepts_context=accepts_context,
|
|
691
|
+
)
|
|
692
|
+
self._dispatchers[dispatcher_id] = entry
|
|
693
|
+
|
|
694
|
+
# Update category index
|
|
695
|
+
self._dispatchers_by_category[category].append(dispatcher_id)
|
|
696
|
+
|
|
697
|
+
self._logger.debug(
|
|
698
|
+
"Registered dispatcher '%s' for category %s (message_types=%s, node_kind=%s)",
|
|
699
|
+
dispatcher_id,
|
|
700
|
+
category,
|
|
701
|
+
message_types if message_types else "all",
|
|
702
|
+
node_kind.value if node_kind else "none",
|
|
703
|
+
)
|
|
704
|
+
|
|
705
|
+
def freeze(self) -> None:
|
|
706
|
+
"""
|
|
707
|
+
Freeze the engine to prevent further registration.
|
|
708
|
+
|
|
709
|
+
Once frozen, any calls to register_route() or register_dispatcher()
|
|
710
|
+
will raise ModelOnexError with INVALID_STATE. This enforces the
|
|
711
|
+
read-only-after-init pattern for thread safety.
|
|
712
|
+
|
|
713
|
+
The freeze operation validates route-to-dispatcher consistency:
|
|
714
|
+
all routes must reference existing dispatchers.
|
|
715
|
+
|
|
716
|
+
Raises:
|
|
717
|
+
ModelOnexError: If any route references a non-existent dispatcher
|
|
718
|
+
(ITEM_NOT_REGISTERED)
|
|
719
|
+
|
|
720
|
+
Example:
|
|
721
|
+
>>> engine = MessageDispatchEngine()
|
|
722
|
+
>>> engine.register_dispatcher("d1", dispatcher, EnumMessageCategory.EVENT)
|
|
723
|
+
>>> engine.register_route(route)
|
|
724
|
+
>>> engine.freeze() # Validates and freezes
|
|
725
|
+
>>> assert engine.is_frozen
|
|
726
|
+
|
|
727
|
+
Note:
|
|
728
|
+
This is a one-way operation. There is no unfreeze() method
|
|
729
|
+
by design, as unfreezing would defeat thread-safety guarantees.
|
|
730
|
+
|
|
731
|
+
.. versionadded:: 0.4.0
|
|
732
|
+
"""
|
|
733
|
+
with self._registration_lock:
|
|
734
|
+
if self._frozen:
|
|
735
|
+
# Idempotent - already frozen
|
|
736
|
+
return
|
|
737
|
+
|
|
738
|
+
# Validate all routes reference existing dispatchers
|
|
739
|
+
for route in self._routes.values():
|
|
740
|
+
if route.dispatcher_id not in self._dispatchers:
|
|
741
|
+
raise ModelOnexError(
|
|
742
|
+
message=f"Route '{route.route_id}' references dispatcher "
|
|
743
|
+
f"'{route.dispatcher_id}' which is not registered. "
|
|
744
|
+
"Register the dispatcher before freezing.",
|
|
745
|
+
error_code=EnumCoreErrorCode.ITEM_NOT_REGISTERED,
|
|
746
|
+
)
|
|
747
|
+
|
|
748
|
+
self._frozen = True
|
|
749
|
+
self._logger.info(
|
|
750
|
+
"MessageDispatchEngine frozen with %d routes and %d dispatchers",
|
|
751
|
+
len(self._routes),
|
|
752
|
+
len(self._dispatchers),
|
|
753
|
+
)
|
|
754
|
+
|
|
755
|
+
@property
|
|
756
|
+
def is_frozen(self) -> bool:
|
|
757
|
+
"""
|
|
758
|
+
Check if the engine is frozen.
|
|
759
|
+
|
|
760
|
+
Returns:
|
|
761
|
+
True if frozen and registration is disabled, False otherwise
|
|
762
|
+
|
|
763
|
+
.. versionadded:: 0.4.0
|
|
764
|
+
"""
|
|
765
|
+
return self._frozen
|
|
766
|
+
|
|
767
|
+
def _build_log_context(
|
|
768
|
+
self, **kwargs: Unpack[ModelLogContextKwargs]
|
|
769
|
+
) -> dict[str, PrimitiveValue]:
|
|
770
|
+
"""
|
|
771
|
+
Build structured log context dictionary.
|
|
772
|
+
|
|
773
|
+
.. versionchanged:: 0.6.0
|
|
774
|
+
Now delegates to ModelDispatchLogContext.to_dict() for type-safe
|
|
775
|
+
context construction.
|
|
776
|
+
|
|
777
|
+
.. versionchanged:: 0.6.2
|
|
778
|
+
Refactored to use ``**kwargs`` forwarding to eliminate 9 union
|
|
779
|
+
parameters from method signature (OMN-1002 Union Reduction Phase 2).
|
|
780
|
+
ModelDispatchLogContext validators handle None-to-sentinel conversion.
|
|
781
|
+
|
|
782
|
+
.. versionchanged:: 0.6.3
|
|
783
|
+
Updated to use ``Unpack[ModelLogContextKwargs]`` TypedDict for type-safe
|
|
784
|
+
kwargs (OMN-1002). Eliminates need for ``type: ignore`` comment.
|
|
785
|
+
|
|
786
|
+
Design Note (Union Reduction - OMN-1002):
|
|
787
|
+
This private method uses typed ``**kwargs`` via ``ModelLogContextKwargs``
|
|
788
|
+
TypedDict to forward parameters to ModelDispatchLogContext. The
|
|
789
|
+
TypedDict provides compile-time type checking while the model's
|
|
790
|
+
field validators handle None-to-sentinel conversion at runtime.
|
|
791
|
+
|
|
792
|
+
Args:
|
|
793
|
+
**kwargs: Keyword arguments forwarded to ModelDispatchLogContext.
|
|
794
|
+
Typed via ``ModelLogContextKwargs`` TypedDict with supported keys:
|
|
795
|
+
topic, category, message_type, dispatcher_id, dispatcher_count,
|
|
796
|
+
duration_ms, correlation_id, trace_id, error_code.
|
|
797
|
+
None values are automatically converted to sentinel values by
|
|
798
|
+
the model's field validators.
|
|
799
|
+
|
|
800
|
+
Returns:
|
|
801
|
+
Dictionary with non-sentinel values for structured logging.
|
|
802
|
+
UUID values are converted to strings at serialization time.
|
|
803
|
+
"""
|
|
804
|
+
# Forward all kwargs to ModelDispatchLogContext which handles
|
|
805
|
+
# None-to-sentinel conversion via field validators.
|
|
806
|
+
# Use model_validate() to properly invoke "before" validators that
|
|
807
|
+
# accept None via object type annotation.
|
|
808
|
+
ctx = ModelDispatchLogContext.model_validate(kwargs)
|
|
809
|
+
return ctx.to_dict()
|
|
810
|
+
|
|
811
|
+
async def dispatch(
|
|
812
|
+
self,
|
|
813
|
+
topic: str,
|
|
814
|
+
envelope: ModelEventEnvelope[object],
|
|
815
|
+
) -> ModelDispatchResult:
|
|
816
|
+
"""
|
|
817
|
+
Dispatch a message to matching dispatchers.
|
|
818
|
+
|
|
819
|
+
Routes the message based on topic category and message type, executes
|
|
820
|
+
all matching dispatchers, and collects their outputs.
|
|
821
|
+
|
|
822
|
+
Dispatch Process:
|
|
823
|
+
1. Parse topic to extract message category
|
|
824
|
+
2. Validate envelope category matches topic category
|
|
825
|
+
3. Get message type from envelope payload
|
|
826
|
+
4. Find all matching dispatchers (by category + message type)
|
|
827
|
+
5. Execute dispatchers (fan-out)
|
|
828
|
+
6. Collect outputs and return result
|
|
829
|
+
|
|
830
|
+
Args:
|
|
831
|
+
topic: The topic the message was received on (e.g., "dev.user.events.v1")
|
|
832
|
+
envelope: The message envelope to dispatch
|
|
833
|
+
|
|
834
|
+
Returns:
|
|
835
|
+
ModelDispatchResult with dispatch status, metrics, and dispatcher outputs
|
|
836
|
+
|
|
837
|
+
Raises:
|
|
838
|
+
ModelOnexError: If engine is not frozen (INVALID_STATE)
|
|
839
|
+
ModelOnexError: If topic is empty (INVALID_PARAMETER)
|
|
840
|
+
ModelOnexError: If envelope is None (INVALID_PARAMETER)
|
|
841
|
+
|
|
842
|
+
Example:
|
|
843
|
+
>>> result = await engine.dispatch(
|
|
844
|
+
... topic="dev.user.events.v1",
|
|
845
|
+
... envelope=ModelEventEnvelope(payload=UserCreatedEvent(...)),
|
|
846
|
+
... )
|
|
847
|
+
>>> if result.is_successful():
|
|
848
|
+
... print(f"Dispatched to {result.output_count} dispatchers")
|
|
849
|
+
|
|
850
|
+
Note:
|
|
851
|
+
Dispatcher exceptions are caught and reported in the result.
|
|
852
|
+
The dispatch continues to other dispatchers even if one fails.
|
|
853
|
+
|
|
854
|
+
.. versionadded:: 0.4.0
|
|
855
|
+
"""
|
|
856
|
+
# Enforce freeze contract
|
|
857
|
+
if not self._frozen:
|
|
858
|
+
raise ModelOnexError(
|
|
859
|
+
message="dispatch() called before freeze(). "
|
|
860
|
+
"Registration MUST complete and freeze() MUST be called before dispatch. "
|
|
861
|
+
"This is required for thread safety.",
|
|
862
|
+
error_code=EnumCoreErrorCode.INVALID_STATE,
|
|
863
|
+
)
|
|
864
|
+
|
|
865
|
+
# Validate inputs
|
|
866
|
+
if not topic or not topic.strip():
|
|
867
|
+
raise ModelOnexError(
|
|
868
|
+
message="Topic cannot be empty or whitespace.",
|
|
869
|
+
error_code=EnumCoreErrorCode.INVALID_PARAMETER,
|
|
870
|
+
)
|
|
871
|
+
|
|
872
|
+
if envelope is None:
|
|
873
|
+
raise ModelOnexError(
|
|
874
|
+
message="Cannot dispatch None envelope. ModelEventEnvelope is required.",
|
|
875
|
+
error_code=EnumCoreErrorCode.INVALID_PARAMETER,
|
|
876
|
+
)
|
|
877
|
+
|
|
878
|
+
# Start timing
|
|
879
|
+
start_time = time.perf_counter()
|
|
880
|
+
dispatch_id = uuid4()
|
|
881
|
+
started_at = datetime.now(UTC)
|
|
882
|
+
|
|
883
|
+
# Extract correlation/trace IDs for logging (kept as UUID, converted to string at serialization)
|
|
884
|
+
# Per ONEX guidelines: auto-generate correlation_id if not provided (uuid4())
|
|
885
|
+
correlation_id = envelope.correlation_id or uuid4()
|
|
886
|
+
trace_id = envelope.trace_id
|
|
887
|
+
|
|
888
|
+
# Step 1: Parse topic to get category
|
|
889
|
+
topic_category = EnumMessageCategory.from_topic(topic)
|
|
890
|
+
if topic_category is None:
|
|
891
|
+
# Capture duration and completed_at together for consistency
|
|
892
|
+
duration_ms = (time.perf_counter() - start_time) * 1000
|
|
893
|
+
completed_at = datetime.now(UTC)
|
|
894
|
+
|
|
895
|
+
# Update metrics (protected by lock for thread safety)
|
|
896
|
+
with self._metrics_lock:
|
|
897
|
+
self._structured_metrics = self._structured_metrics.record_dispatch(
|
|
898
|
+
duration_ms=duration_ms,
|
|
899
|
+
success=False,
|
|
900
|
+
category=None,
|
|
901
|
+
no_dispatcher=False,
|
|
902
|
+
category_mismatch=False,
|
|
903
|
+
topic=topic,
|
|
904
|
+
)
|
|
905
|
+
|
|
906
|
+
# Log error
|
|
907
|
+
self._logger.error(
|
|
908
|
+
"Dispatch failed: invalid topic category",
|
|
909
|
+
extra=self._build_log_context(
|
|
910
|
+
topic=topic,
|
|
911
|
+
duration_ms=duration_ms,
|
|
912
|
+
correlation_id=correlation_id,
|
|
913
|
+
trace_id=trace_id,
|
|
914
|
+
error_code=EnumCoreErrorCode.VALIDATION_ERROR,
|
|
915
|
+
),
|
|
916
|
+
)
|
|
917
|
+
|
|
918
|
+
return ModelDispatchResult(
|
|
919
|
+
dispatch_id=dispatch_id,
|
|
920
|
+
status=EnumDispatchStatus.INVALID_MESSAGE,
|
|
921
|
+
topic=topic,
|
|
922
|
+
started_at=started_at,
|
|
923
|
+
completed_at=completed_at,
|
|
924
|
+
duration_ms=duration_ms,
|
|
925
|
+
error_message=f"Cannot infer message category from topic '{topic}'. "
|
|
926
|
+
"Topic must contain .events, .commands, .intents, or .projections segment.",
|
|
927
|
+
error_code=EnumCoreErrorCode.VALIDATION_ERROR,
|
|
928
|
+
correlation_id=correlation_id,
|
|
929
|
+
output_events=[],
|
|
930
|
+
)
|
|
931
|
+
|
|
932
|
+
# Log dispatch start at INFO level
|
|
933
|
+
self._logger.info(
|
|
934
|
+
"Dispatch started",
|
|
935
|
+
extra=self._build_log_context(
|
|
936
|
+
topic=topic,
|
|
937
|
+
category=topic_category,
|
|
938
|
+
correlation_id=correlation_id,
|
|
939
|
+
trace_id=trace_id,
|
|
940
|
+
),
|
|
941
|
+
)
|
|
942
|
+
|
|
943
|
+
# Step 2: Validate envelope category matches topic category
|
|
944
|
+
# NOTE: ModelEventEnvelope.infer_category() is not yet implemented in omnibase_core.
|
|
945
|
+
# Until it is, we trust the topic category as the source of truth for routing.
|
|
946
|
+
# This is safe because the topic defines the message category, and handlers
|
|
947
|
+
# are registered for specific categories - any mismatch would be a caller error.
|
|
948
|
+
# TODO(OMN-934): Re-enable envelope category validation when infer_category() is available
|
|
949
|
+
#
|
|
950
|
+
# The code below is disabled until infer_category() is available:
|
|
951
|
+
# envelope_category = envelope.infer_category()
|
|
952
|
+
# if envelope_category != topic_category:
|
|
953
|
+
# ... (category mismatch handling with structured metrics)
|
|
954
|
+
|
|
955
|
+
# Step 3: Get message type from payload
|
|
956
|
+
message_type = type(envelope.payload).__name__
|
|
957
|
+
|
|
958
|
+
# Step 4: Find matching dispatchers
|
|
959
|
+
matching_dispatchers = self._find_matching_dispatchers(
|
|
960
|
+
topic=topic,
|
|
961
|
+
category=topic_category,
|
|
962
|
+
message_type=message_type,
|
|
963
|
+
)
|
|
964
|
+
|
|
965
|
+
# Log routing decision at DEBUG level
|
|
966
|
+
self._logger.debug(
|
|
967
|
+
"Routing decision: %d dispatchers matched for message_type '%s'",
|
|
968
|
+
len(matching_dispatchers),
|
|
969
|
+
message_type,
|
|
970
|
+
extra=self._build_log_context(
|
|
971
|
+
topic=topic,
|
|
972
|
+
category=topic_category,
|
|
973
|
+
message_type=message_type,
|
|
974
|
+
dispatcher_count=len(matching_dispatchers),
|
|
975
|
+
correlation_id=correlation_id,
|
|
976
|
+
trace_id=trace_id,
|
|
977
|
+
),
|
|
978
|
+
)
|
|
979
|
+
|
|
980
|
+
if not matching_dispatchers:
|
|
981
|
+
# Capture duration and completed_at together for consistency
|
|
982
|
+
duration_ms = (time.perf_counter() - start_time) * 1000
|
|
983
|
+
completed_at = datetime.now(UTC)
|
|
984
|
+
|
|
985
|
+
# Update metrics (protected by lock for thread safety)
|
|
986
|
+
with self._metrics_lock:
|
|
987
|
+
self._structured_metrics = self._structured_metrics.record_dispatch(
|
|
988
|
+
duration_ms=duration_ms,
|
|
989
|
+
success=False,
|
|
990
|
+
category=topic_category,
|
|
991
|
+
no_dispatcher=True,
|
|
992
|
+
topic=topic,
|
|
993
|
+
)
|
|
994
|
+
|
|
995
|
+
# Log warning
|
|
996
|
+
self._logger.warning(
|
|
997
|
+
"No dispatcher found for category '%s' and message type '%s'",
|
|
998
|
+
topic_category,
|
|
999
|
+
message_type,
|
|
1000
|
+
extra=self._build_log_context(
|
|
1001
|
+
topic=topic,
|
|
1002
|
+
category=topic_category,
|
|
1003
|
+
message_type=message_type,
|
|
1004
|
+
dispatcher_count=0,
|
|
1005
|
+
duration_ms=duration_ms,
|
|
1006
|
+
correlation_id=correlation_id,
|
|
1007
|
+
trace_id=trace_id,
|
|
1008
|
+
error_code=EnumCoreErrorCode.ITEM_NOT_REGISTERED,
|
|
1009
|
+
),
|
|
1010
|
+
)
|
|
1011
|
+
|
|
1012
|
+
return ModelDispatchResult(
|
|
1013
|
+
dispatch_id=dispatch_id,
|
|
1014
|
+
status=EnumDispatchStatus.NO_DISPATCHER,
|
|
1015
|
+
topic=topic,
|
|
1016
|
+
message_category=topic_category,
|
|
1017
|
+
message_type=message_type,
|
|
1018
|
+
started_at=started_at,
|
|
1019
|
+
completed_at=completed_at,
|
|
1020
|
+
duration_ms=duration_ms,
|
|
1021
|
+
error_message=f"No dispatcher registered for category '{topic_category}' "
|
|
1022
|
+
f"and message type '{message_type}' matching topic '{topic}'.",
|
|
1023
|
+
error_code=EnumCoreErrorCode.ITEM_NOT_REGISTERED,
|
|
1024
|
+
correlation_id=correlation_id,
|
|
1025
|
+
output_events=[],
|
|
1026
|
+
)
|
|
1027
|
+
|
|
1028
|
+
# Step 5: Execute dispatchers and collect outputs
|
|
1029
|
+
outputs: list[str] = []
|
|
1030
|
+
dispatcher_errors: list[str] = []
|
|
1031
|
+
executed_dispatcher_ids: list[str] = []
|
|
1032
|
+
|
|
1033
|
+
for dispatcher_entry in matching_dispatchers:
|
|
1034
|
+
dispatcher_start_time = time.perf_counter()
|
|
1035
|
+
|
|
1036
|
+
# Log dispatcher execution at DEBUG level
|
|
1037
|
+
self._logger.debug(
|
|
1038
|
+
"Executing dispatcher '%s'",
|
|
1039
|
+
dispatcher_entry.dispatcher_id,
|
|
1040
|
+
extra=self._build_log_context(
|
|
1041
|
+
topic=topic,
|
|
1042
|
+
category=topic_category,
|
|
1043
|
+
message_type=message_type,
|
|
1044
|
+
dispatcher_id=dispatcher_entry.dispatcher_id,
|
|
1045
|
+
correlation_id=correlation_id,
|
|
1046
|
+
trace_id=trace_id,
|
|
1047
|
+
),
|
|
1048
|
+
)
|
|
1049
|
+
|
|
1050
|
+
try:
|
|
1051
|
+
result = await self._execute_dispatcher(dispatcher_entry, envelope)
|
|
1052
|
+
dispatcher_duration_ms = (
|
|
1053
|
+
time.perf_counter() - dispatcher_start_time
|
|
1054
|
+
) * 1000
|
|
1055
|
+
executed_dispatcher_ids.append(dispatcher_entry.dispatcher_id)
|
|
1056
|
+
|
|
1057
|
+
# TOCTOU Prevention: Update per-dispatcher metrics atomically
|
|
1058
|
+
# ---------------------------------------------------------
|
|
1059
|
+
# The entire read-modify-write sequence below MUST execute within
|
|
1060
|
+
# a single lock acquisition to prevent race conditions:
|
|
1061
|
+
# 1. Read: Get existing dispatcher metrics (or create default)
|
|
1062
|
+
# 2. Modify: Call record_execution() to compute new values
|
|
1063
|
+
# 3. Write: Update _structured_metrics with new dispatcher entry
|
|
1064
|
+
#
|
|
1065
|
+
# These operations are pure (no I/O) and fast (~microseconds),
|
|
1066
|
+
# so holding the lock during computation is acceptable.
|
|
1067
|
+
with self._metrics_lock:
|
|
1068
|
+
existing_dispatcher_metrics = (
|
|
1069
|
+
self._structured_metrics.dispatcher_metrics.get(
|
|
1070
|
+
dispatcher_entry.dispatcher_id
|
|
1071
|
+
)
|
|
1072
|
+
)
|
|
1073
|
+
if existing_dispatcher_metrics is None:
|
|
1074
|
+
existing_dispatcher_metrics = ModelDispatcherMetrics(
|
|
1075
|
+
dispatcher_id=dispatcher_entry.dispatcher_id
|
|
1076
|
+
)
|
|
1077
|
+
new_dispatcher_metrics = (
|
|
1078
|
+
existing_dispatcher_metrics.record_execution(
|
|
1079
|
+
duration_ms=dispatcher_duration_ms,
|
|
1080
|
+
success=True,
|
|
1081
|
+
topic=topic,
|
|
1082
|
+
)
|
|
1083
|
+
)
|
|
1084
|
+
new_dispatcher_metrics_dict = {
|
|
1085
|
+
**self._structured_metrics.dispatcher_metrics,
|
|
1086
|
+
dispatcher_entry.dispatcher_id: new_dispatcher_metrics,
|
|
1087
|
+
}
|
|
1088
|
+
self._structured_metrics = self._structured_metrics.model_copy(
|
|
1089
|
+
update={
|
|
1090
|
+
"dispatcher_execution_count": (
|
|
1091
|
+
self._structured_metrics.dispatcher_execution_count + 1
|
|
1092
|
+
),
|
|
1093
|
+
"dispatcher_metrics": new_dispatcher_metrics_dict,
|
|
1094
|
+
}
|
|
1095
|
+
)
|
|
1096
|
+
|
|
1097
|
+
# Log dispatcher completion at DEBUG level
|
|
1098
|
+
self._logger.debug(
|
|
1099
|
+
"Dispatcher '%s' completed successfully in %.2f ms",
|
|
1100
|
+
dispatcher_entry.dispatcher_id,
|
|
1101
|
+
dispatcher_duration_ms,
|
|
1102
|
+
extra=self._build_log_context(
|
|
1103
|
+
topic=topic,
|
|
1104
|
+
category=topic_category,
|
|
1105
|
+
message_type=message_type,
|
|
1106
|
+
dispatcher_id=dispatcher_entry.dispatcher_id,
|
|
1107
|
+
duration_ms=dispatcher_duration_ms,
|
|
1108
|
+
correlation_id=correlation_id,
|
|
1109
|
+
trace_id=trace_id,
|
|
1110
|
+
),
|
|
1111
|
+
)
|
|
1112
|
+
|
|
1113
|
+
# Normalize dispatcher output using ModelDispatchOutcome to avoid
|
|
1114
|
+
# manual isinstance checks on the 3-way union (str | list[str] | None).
|
|
1115
|
+
# This centralizes the union handling in the model's from_legacy_output().
|
|
1116
|
+
outcome = ModelDispatchOutcome.from_legacy_output(result)
|
|
1117
|
+
outputs.extend(outcome.topics)
|
|
1118
|
+
except (SystemExit, KeyboardInterrupt, GeneratorExit):
|
|
1119
|
+
# Never catch cancellation/exit signals
|
|
1120
|
+
raise
|
|
1121
|
+
except asyncio.CancelledError:
|
|
1122
|
+
# Never suppress async cancellation
|
|
1123
|
+
raise
|
|
1124
|
+
except Exception as e:
|
|
1125
|
+
dispatcher_duration_ms = (
|
|
1126
|
+
time.perf_counter() - dispatcher_start_time
|
|
1127
|
+
) * 1000
|
|
1128
|
+
# Sanitize exception message to prevent credential leakage
|
|
1129
|
+
# (e.g., connection strings with passwords, API keys in URLs)
|
|
1130
|
+
sanitized_error = sanitize_error_message(e)
|
|
1131
|
+
error_msg = (
|
|
1132
|
+
f"Dispatcher '{dispatcher_entry.dispatcher_id}' "
|
|
1133
|
+
f"failed: {sanitized_error}"
|
|
1134
|
+
)
|
|
1135
|
+
dispatcher_errors.append(error_msg)
|
|
1136
|
+
|
|
1137
|
+
# TOCTOU Prevention: Update per-dispatcher error metrics atomically
|
|
1138
|
+
# ----------------------------------------------------------------
|
|
1139
|
+
# The entire read-modify-write sequence below MUST execute within
|
|
1140
|
+
# a single lock acquisition to prevent race conditions:
|
|
1141
|
+
# 1. Read: Get existing dispatcher metrics (or create default)
|
|
1142
|
+
# 2. Modify: Call record_execution() to compute new error values
|
|
1143
|
+
# 3. Write: Update _structured_metrics with new dispatcher entry
|
|
1144
|
+
#
|
|
1145
|
+
# These operations are pure (no I/O) and fast (~microseconds),
|
|
1146
|
+
# so holding the lock during computation is acceptable.
|
|
1147
|
+
with self._metrics_lock:
|
|
1148
|
+
existing_dispatcher_metrics = (
|
|
1149
|
+
self._structured_metrics.dispatcher_metrics.get(
|
|
1150
|
+
dispatcher_entry.dispatcher_id
|
|
1151
|
+
)
|
|
1152
|
+
)
|
|
1153
|
+
if existing_dispatcher_metrics is None:
|
|
1154
|
+
existing_dispatcher_metrics = ModelDispatcherMetrics(
|
|
1155
|
+
dispatcher_id=dispatcher_entry.dispatcher_id
|
|
1156
|
+
)
|
|
1157
|
+
new_dispatcher_metrics = (
|
|
1158
|
+
existing_dispatcher_metrics.record_execution(
|
|
1159
|
+
duration_ms=dispatcher_duration_ms,
|
|
1160
|
+
success=False,
|
|
1161
|
+
topic=topic,
|
|
1162
|
+
# Use sanitized error message for metrics as well
|
|
1163
|
+
error_message=sanitized_error,
|
|
1164
|
+
)
|
|
1165
|
+
)
|
|
1166
|
+
new_dispatcher_metrics_dict = {
|
|
1167
|
+
**self._structured_metrics.dispatcher_metrics,
|
|
1168
|
+
dispatcher_entry.dispatcher_id: new_dispatcher_metrics,
|
|
1169
|
+
}
|
|
1170
|
+
self._structured_metrics = self._structured_metrics.model_copy(
|
|
1171
|
+
update={
|
|
1172
|
+
"dispatcher_execution_count": (
|
|
1173
|
+
self._structured_metrics.dispatcher_execution_count + 1
|
|
1174
|
+
),
|
|
1175
|
+
"dispatcher_error_count": (
|
|
1176
|
+
self._structured_metrics.dispatcher_error_count + 1
|
|
1177
|
+
),
|
|
1178
|
+
"dispatcher_metrics": new_dispatcher_metrics_dict,
|
|
1179
|
+
}
|
|
1180
|
+
)
|
|
1181
|
+
|
|
1182
|
+
# Log error with sanitized message
|
|
1183
|
+
# Note: Using logger.error() with sanitized message instead of
|
|
1184
|
+
# logger.exception() to avoid leaking sensitive data in stack traces.
|
|
1185
|
+
# The sanitized_error variable already contains safe error details.
|
|
1186
|
+
# TRY400: Intentionally using error() instead of exception() for security
|
|
1187
|
+
self._logger.error(
|
|
1188
|
+
"Dispatcher '%s' failed: %s",
|
|
1189
|
+
dispatcher_entry.dispatcher_id,
|
|
1190
|
+
sanitized_error,
|
|
1191
|
+
extra=self._build_log_context(
|
|
1192
|
+
topic=topic,
|
|
1193
|
+
category=topic_category,
|
|
1194
|
+
message_type=message_type,
|
|
1195
|
+
dispatcher_id=dispatcher_entry.dispatcher_id,
|
|
1196
|
+
duration_ms=dispatcher_duration_ms,
|
|
1197
|
+
correlation_id=correlation_id,
|
|
1198
|
+
trace_id=trace_id,
|
|
1199
|
+
error_code=EnumCoreErrorCode.HANDLER_EXECUTION_ERROR,
|
|
1200
|
+
),
|
|
1201
|
+
)
|
|
1202
|
+
|
|
1203
|
+
# Step 6: Build result
|
|
1204
|
+
# Capture duration and completed_at together for consistency
|
|
1205
|
+
duration_ms = (time.perf_counter() - start_time) * 1000
|
|
1206
|
+
completed_at = datetime.now(UTC)
|
|
1207
|
+
|
|
1208
|
+
# Determine final status
|
|
1209
|
+
if dispatcher_errors:
|
|
1210
|
+
# Either partial or total failure
|
|
1211
|
+
status = EnumDispatchStatus.HANDLER_ERROR
|
|
1212
|
+
else:
|
|
1213
|
+
status = EnumDispatchStatus.SUCCESS
|
|
1214
|
+
|
|
1215
|
+
# Update all metrics atomically (protected by lock)
|
|
1216
|
+
with self._metrics_lock:
|
|
1217
|
+
# NOTE: dispatcher_id and handler_error are NOT passed here because
|
|
1218
|
+
# per-dispatcher metrics (including dispatcher_execution_count and
|
|
1219
|
+
# dispatcher_error_count) are already updated in the dispatcher loop
|
|
1220
|
+
# above. Passing them here would cause double-counting.
|
|
1221
|
+
self._structured_metrics = self._structured_metrics.record_dispatch(
|
|
1222
|
+
duration_ms=duration_ms,
|
|
1223
|
+
success=status == EnumDispatchStatus.SUCCESS,
|
|
1224
|
+
category=topic_category,
|
|
1225
|
+
dispatcher_id=None, # Already tracked in dispatcher loop
|
|
1226
|
+
handler_error=False, # Already tracked in dispatcher loop
|
|
1227
|
+
routes_matched=len(matching_dispatchers),
|
|
1228
|
+
topic=topic,
|
|
1229
|
+
error_message=dispatcher_errors[0] if dispatcher_errors else None,
|
|
1230
|
+
)
|
|
1231
|
+
|
|
1232
|
+
# Find route ID that matched (first matching route for logging)
|
|
1233
|
+
# Use empty string sentinel internally to avoid str | None union
|
|
1234
|
+
matched_route_id: str = ""
|
|
1235
|
+
for route in self._routes.values():
|
|
1236
|
+
if route.matches(topic, topic_category, message_type):
|
|
1237
|
+
matched_route_id = route.route_id
|
|
1238
|
+
break
|
|
1239
|
+
|
|
1240
|
+
# Log dispatch completion at INFO level
|
|
1241
|
+
# Use empty string sentinel to avoid str | None union in local scope
|
|
1242
|
+
dispatcher_ids_str: str = (
|
|
1243
|
+
", ".join(executed_dispatcher_ids) if executed_dispatcher_ids else ""
|
|
1244
|
+
)
|
|
1245
|
+
if status == EnumDispatchStatus.SUCCESS:
|
|
1246
|
+
self._logger.info(
|
|
1247
|
+
"Dispatch completed successfully",
|
|
1248
|
+
extra=self._build_log_context(
|
|
1249
|
+
topic=topic,
|
|
1250
|
+
category=topic_category,
|
|
1251
|
+
message_type=message_type,
|
|
1252
|
+
dispatcher_id=dispatcher_ids_str,
|
|
1253
|
+
dispatcher_count=len(executed_dispatcher_ids),
|
|
1254
|
+
duration_ms=duration_ms,
|
|
1255
|
+
correlation_id=correlation_id,
|
|
1256
|
+
trace_id=trace_id,
|
|
1257
|
+
),
|
|
1258
|
+
)
|
|
1259
|
+
else:
|
|
1260
|
+
self._logger.error(
|
|
1261
|
+
"Dispatch completed with errors",
|
|
1262
|
+
extra=self._build_log_context(
|
|
1263
|
+
topic=topic,
|
|
1264
|
+
category=topic_category,
|
|
1265
|
+
message_type=message_type,
|
|
1266
|
+
dispatcher_id=dispatcher_ids_str,
|
|
1267
|
+
dispatcher_count=len(matching_dispatchers),
|
|
1268
|
+
duration_ms=duration_ms,
|
|
1269
|
+
correlation_id=correlation_id,
|
|
1270
|
+
trace_id=trace_id,
|
|
1271
|
+
error_code=EnumCoreErrorCode.HANDLER_EXECUTION_ERROR,
|
|
1272
|
+
),
|
|
1273
|
+
)
|
|
1274
|
+
|
|
1275
|
+
# Convert list of output topics to ModelDispatchOutputs
|
|
1276
|
+
# Handle Pydantic validation errors (e.g., invalid topic format)
|
|
1277
|
+
dispatch_outputs: ModelDispatchOutputs | None = None
|
|
1278
|
+
if outputs:
|
|
1279
|
+
try:
|
|
1280
|
+
dispatch_outputs = ModelDispatchOutputs(topics=outputs)
|
|
1281
|
+
except (ValueError, ValidationError) as validation_error:
|
|
1282
|
+
# Log validation failure with context (no secrets in topic names)
|
|
1283
|
+
# Note: Using sanitize_error_message for consistency, though topic
|
|
1284
|
+
# validation errors typically don't contain sensitive data
|
|
1285
|
+
sanitized_validation_error = sanitize_error_message(validation_error)
|
|
1286
|
+
# TRY400: Intentionally using error() instead of exception() for security
|
|
1287
|
+
# - exception() would log stack trace which may expose internal paths
|
|
1288
|
+
# - sanitized_validation_error already contains safe error details
|
|
1289
|
+
self._logger.error(
|
|
1290
|
+
"Failed to validate dispatch outputs (%d topics): %s",
|
|
1291
|
+
len(outputs),
|
|
1292
|
+
sanitized_validation_error,
|
|
1293
|
+
extra=self._build_log_context(
|
|
1294
|
+
topic=topic,
|
|
1295
|
+
category=topic_category,
|
|
1296
|
+
message_type=message_type,
|
|
1297
|
+
correlation_id=correlation_id,
|
|
1298
|
+
trace_id=trace_id,
|
|
1299
|
+
error_code=EnumCoreErrorCode.VALIDATION_ERROR,
|
|
1300
|
+
),
|
|
1301
|
+
)
|
|
1302
|
+
# Add validation error to dispatcher_errors for result
|
|
1303
|
+
validation_error_msg = (
|
|
1304
|
+
f"Output validation failed: {sanitized_validation_error}"
|
|
1305
|
+
)
|
|
1306
|
+
dispatcher_errors.append(validation_error_msg)
|
|
1307
|
+
# Update status to reflect validation error
|
|
1308
|
+
status = EnumDispatchStatus.HANDLER_ERROR
|
|
1309
|
+
|
|
1310
|
+
# Construct final dispatch result with ValidationError protection
|
|
1311
|
+
# This ensures any Pydantic validation failure in ModelDispatchResult
|
|
1312
|
+
# is handled gracefully rather than propagating as an unhandled exception
|
|
1313
|
+
try:
|
|
1314
|
+
return ModelDispatchResult(
|
|
1315
|
+
dispatch_id=dispatch_id,
|
|
1316
|
+
status=status,
|
|
1317
|
+
route_id=matched_route_id,
|
|
1318
|
+
dispatcher_id=dispatcher_ids_str,
|
|
1319
|
+
topic=topic,
|
|
1320
|
+
message_category=topic_category,
|
|
1321
|
+
message_type=message_type,
|
|
1322
|
+
duration_ms=duration_ms,
|
|
1323
|
+
started_at=started_at,
|
|
1324
|
+
completed_at=completed_at,
|
|
1325
|
+
outputs=dispatch_outputs,
|
|
1326
|
+
output_count=len(outputs),
|
|
1327
|
+
error_message="; ".join(dispatcher_errors)
|
|
1328
|
+
if dispatcher_errors
|
|
1329
|
+
else None,
|
|
1330
|
+
error_code=EnumCoreErrorCode.HANDLER_EXECUTION_ERROR
|
|
1331
|
+
if dispatcher_errors
|
|
1332
|
+
else None,
|
|
1333
|
+
correlation_id=correlation_id,
|
|
1334
|
+
trace_id=trace_id,
|
|
1335
|
+
span_id=envelope.span_id,
|
|
1336
|
+
)
|
|
1337
|
+
except ValidationError as result_validation_error:
|
|
1338
|
+
# Pydantic validation failed during result construction
|
|
1339
|
+
# This is a critical internal error - log and return a minimal error result
|
|
1340
|
+
sanitized_result_error = sanitize_error_message(result_validation_error)
|
|
1341
|
+
# TRY400: Intentionally using error() instead of exception() for security
|
|
1342
|
+
self._logger.error(
|
|
1343
|
+
"Failed to construct ModelDispatchResult: %s",
|
|
1344
|
+
sanitized_result_error,
|
|
1345
|
+
extra=self._build_log_context(
|
|
1346
|
+
topic=topic,
|
|
1347
|
+
category=topic_category,
|
|
1348
|
+
message_type=message_type,
|
|
1349
|
+
correlation_id=correlation_id,
|
|
1350
|
+
trace_id=trace_id,
|
|
1351
|
+
error_code=EnumCoreErrorCode.INTERNAL_ERROR,
|
|
1352
|
+
),
|
|
1353
|
+
)
|
|
1354
|
+
# Return a minimal fallback result that should always succeed
|
|
1355
|
+
return ModelDispatchResult(
|
|
1356
|
+
dispatch_id=dispatch_id,
|
|
1357
|
+
status=EnumDispatchStatus.INTERNAL_ERROR,
|
|
1358
|
+
topic=topic,
|
|
1359
|
+
started_at=started_at,
|
|
1360
|
+
completed_at=datetime.now(UTC),
|
|
1361
|
+
duration_ms=duration_ms,
|
|
1362
|
+
error_message=f"Internal error constructing dispatch result: {sanitized_result_error}",
|
|
1363
|
+
error_code=EnumCoreErrorCode.INTERNAL_ERROR,
|
|
1364
|
+
correlation_id=correlation_id,
|
|
1365
|
+
output_events=[],
|
|
1366
|
+
)
|
|
1367
|
+
|
|
1368
|
+
def _find_matching_dispatchers(
|
|
1369
|
+
self,
|
|
1370
|
+
topic: str,
|
|
1371
|
+
category: EnumMessageCategory,
|
|
1372
|
+
message_type: str,
|
|
1373
|
+
) -> list[DispatchEntryInternal]:
|
|
1374
|
+
"""
|
|
1375
|
+
Find all dispatchers that match the given criteria.
|
|
1376
|
+
|
|
1377
|
+
Matching is done in two phases:
|
|
1378
|
+
1. Find routes that match topic pattern and category
|
|
1379
|
+
2. Find dispatchers for those routes that accept the message type
|
|
1380
|
+
|
|
1381
|
+
Args:
|
|
1382
|
+
topic: The topic to match
|
|
1383
|
+
category: The message category
|
|
1384
|
+
message_type: The specific message type
|
|
1385
|
+
|
|
1386
|
+
Returns:
|
|
1387
|
+
List of matching dispatcher entries (may be empty)
|
|
1388
|
+
"""
|
|
1389
|
+
matching_dispatchers: list[DispatchEntryInternal] = []
|
|
1390
|
+
seen_dispatcher_ids: set[str] = set()
|
|
1391
|
+
|
|
1392
|
+
# Find all routes that match this topic and category
|
|
1393
|
+
for route in self._routes.values():
|
|
1394
|
+
if not route.enabled:
|
|
1395
|
+
continue
|
|
1396
|
+
if not route.matches_topic(topic):
|
|
1397
|
+
continue
|
|
1398
|
+
if route.message_category != category:
|
|
1399
|
+
continue
|
|
1400
|
+
# Route-level message type filter (if specified)
|
|
1401
|
+
if route.message_type is not None and route.message_type != message_type:
|
|
1402
|
+
continue
|
|
1403
|
+
|
|
1404
|
+
# Get the dispatcher for this route
|
|
1405
|
+
dispatcher_id = route.dispatcher_id
|
|
1406
|
+
if dispatcher_id in seen_dispatcher_ids:
|
|
1407
|
+
# Avoid duplicate dispatcher execution
|
|
1408
|
+
continue
|
|
1409
|
+
|
|
1410
|
+
entry = self._dispatchers.get(dispatcher_id)
|
|
1411
|
+
if entry is None:
|
|
1412
|
+
# Dispatcher not found (should have been caught at freeze)
|
|
1413
|
+
self._logger.warning(
|
|
1414
|
+
"Route '%s' references missing dispatcher '%s'",
|
|
1415
|
+
route.route_id,
|
|
1416
|
+
dispatcher_id,
|
|
1417
|
+
)
|
|
1418
|
+
continue
|
|
1419
|
+
|
|
1420
|
+
# Check dispatcher-level message type filter
|
|
1421
|
+
if (
|
|
1422
|
+
entry.message_types is not None
|
|
1423
|
+
and message_type not in entry.message_types
|
|
1424
|
+
):
|
|
1425
|
+
continue
|
|
1426
|
+
|
|
1427
|
+
matching_dispatchers.append(entry)
|
|
1428
|
+
seen_dispatcher_ids.add(dispatcher_id)
|
|
1429
|
+
|
|
1430
|
+
return matching_dispatchers
|
|
1431
|
+
|
|
1432
|
+
async def _execute_dispatcher(
|
|
1433
|
+
self,
|
|
1434
|
+
entry: DispatchEntryInternal,
|
|
1435
|
+
envelope: ModelEventEnvelope[object],
|
|
1436
|
+
) -> DispatcherOutput:
|
|
1437
|
+
"""
|
|
1438
|
+
Execute a dispatcher (sync or async).
|
|
1439
|
+
|
|
1440
|
+
Sync dispatchers are executed via ``loop.run_in_executor()`` using the
|
|
1441
|
+
default ``ThreadPoolExecutor``. This allows sync code to run without
|
|
1442
|
+
blocking the event loop, but has important implications:
|
|
1443
|
+
|
|
1444
|
+
Thread Pool Considerations:
|
|
1445
|
+
- The default executor uses a limited thread pool (typically
|
|
1446
|
+
``min(32, os.cpu_count() + 4)`` threads in Python 3.8+)
|
|
1447
|
+
- Each sync dispatcher execution consumes one thread until completion
|
|
1448
|
+
- Blocking dispatchers can exhaust the thread pool, causing:
|
|
1449
|
+
- Starvation of other sync dispatchers waiting for threads
|
|
1450
|
+
- Delayed scheduling of new async tasks
|
|
1451
|
+
- Potential deadlocks under high concurrent load
|
|
1452
|
+
- Increased latency for all executor-based operations
|
|
1453
|
+
|
|
1454
|
+
Best Practices:
|
|
1455
|
+
- Sync dispatchers SHOULD complete quickly (< 100ms recommended)
|
|
1456
|
+
- For blocking I/O (network, database, file), use async dispatchers
|
|
1457
|
+
- For CPU-bound work, consider using a dedicated ProcessPoolExecutor
|
|
1458
|
+
- Monitor ``dispatcher_execution_count`` metrics for bottlenecks
|
|
1459
|
+
|
|
1460
|
+
Args:
|
|
1461
|
+
entry: The dispatcher entry containing the callable
|
|
1462
|
+
envelope: The message envelope to process
|
|
1463
|
+
|
|
1464
|
+
Returns:
|
|
1465
|
+
DispatcherOutput: str (single topic), list[str] (multiple topics),
|
|
1466
|
+
or None (no output topics)
|
|
1467
|
+
|
|
1468
|
+
Raises:
|
|
1469
|
+
Any exception raised by the dispatcher
|
|
1470
|
+
|
|
1471
|
+
Warning:
|
|
1472
|
+
Sync dispatchers that block for extended periods (> 100ms) can
|
|
1473
|
+
severely degrade dispatch engine throughput. Prefer async dispatchers
|
|
1474
|
+
for any operation involving I/O or external service calls.
|
|
1475
|
+
|
|
1476
|
+
.. versionchanged:: 0.5.0
|
|
1477
|
+
Added support for context-aware dispatchers via ``node_kind``.
|
|
1478
|
+
"""
|
|
1479
|
+
dispatcher = entry.dispatcher
|
|
1480
|
+
|
|
1481
|
+
# Create context ONLY if both conditions are met:
|
|
1482
|
+
# 1. node_kind is set (time injection rules apply)
|
|
1483
|
+
# 2. dispatcher accepts context (will actually use it)
|
|
1484
|
+
# This avoids unnecessary object creation on the dispatch hot path when
|
|
1485
|
+
# a dispatcher has node_kind set but doesn't accept a context parameter.
|
|
1486
|
+
context: ModelDispatchContext | None = None
|
|
1487
|
+
if entry.node_kind is not None and entry.accepts_context:
|
|
1488
|
+
context = self._create_context_for_entry(entry, envelope)
|
|
1489
|
+
|
|
1490
|
+
# Check if dispatcher is async
|
|
1491
|
+
# Note: context is only non-None when entry.accepts_context is True,
|
|
1492
|
+
# so checking `context is not None` is sufficient to determine whether
|
|
1493
|
+
# to pass context to the dispatcher.
|
|
1494
|
+
if inspect.iscoroutinefunction(dispatcher):
|
|
1495
|
+
if context is not None:
|
|
1496
|
+
# NOTE: Dispatcher signature varies - context param may be optional.
|
|
1497
|
+
# Return type depends on dispatcher implementation (dict or model).
|
|
1498
|
+
return await dispatcher(envelope, context) # type: ignore[call-arg,no-any-return] # NOTE: dispatcher signature varies
|
|
1499
|
+
# NOTE: Return type depends on dispatcher implementation (dict or model).
|
|
1500
|
+
return await dispatcher(envelope) # type: ignore[no-any-return] # NOTE: dispatcher return type varies
|
|
1501
|
+
else:
|
|
1502
|
+
# Sync dispatcher execution via ThreadPoolExecutor
|
|
1503
|
+
# -----------------------------------------------
|
|
1504
|
+
# WARNING: Sync dispatchers MUST be non-blocking (< 100ms execution).
|
|
1505
|
+
# Blocking dispatchers can exhaust the thread pool, causing:
|
|
1506
|
+
# - Starvation of other sync dispatchers
|
|
1507
|
+
# - Delayed async dispatcher scheduling
|
|
1508
|
+
# - Potential deadlocks under high load
|
|
1509
|
+
#
|
|
1510
|
+
# For blocking I/O operations, use async dispatchers instead.
|
|
1511
|
+
loop = asyncio.get_running_loop()
|
|
1512
|
+
|
|
1513
|
+
if context is not None:
|
|
1514
|
+
# Context-aware sync dispatcher
|
|
1515
|
+
sync_ctx_dispatcher = cast(_SyncContextAwareDispatcherFunc, dispatcher)
|
|
1516
|
+
return await loop.run_in_executor(
|
|
1517
|
+
None,
|
|
1518
|
+
sync_ctx_dispatcher,
|
|
1519
|
+
# NOTE: run_in_executor expects positional args as *args,
|
|
1520
|
+
# type checker cannot verify generic envelope type matches dispatcher.
|
|
1521
|
+
envelope, # type: ignore[arg-type] # NOTE: generic envelope type erasure
|
|
1522
|
+
context,
|
|
1523
|
+
)
|
|
1524
|
+
else:
|
|
1525
|
+
# Cast to sync-only type - safe because iscoroutinefunction check above
|
|
1526
|
+
# guarantees this branch only executes for non-async callables
|
|
1527
|
+
sync_dispatcher = cast(_SyncDispatcherFunc, dispatcher)
|
|
1528
|
+
return await loop.run_in_executor(
|
|
1529
|
+
None,
|
|
1530
|
+
sync_dispatcher,
|
|
1531
|
+
# NOTE: run_in_executor expects positional args as *args,
|
|
1532
|
+
# type checker cannot verify generic envelope type matches dispatcher.
|
|
1533
|
+
envelope, # type: ignore[arg-type] # NOTE: generic envelope type erasure
|
|
1534
|
+
)
|
|
1535
|
+
|
|
1536
|
+
def _create_context_for_entry(
|
|
1537
|
+
self,
|
|
1538
|
+
entry: DispatchEntryInternal,
|
|
1539
|
+
envelope: ModelEventEnvelope[object],
|
|
1540
|
+
) -> ModelDispatchContext:
|
|
1541
|
+
"""
|
|
1542
|
+
Create dispatch context based on entry's node_kind.
|
|
1543
|
+
|
|
1544
|
+
Delegates to DispatchContextEnforcer.create_context_for_node_kind() to
|
|
1545
|
+
ensure a single source of truth for time injection rules. This method
|
|
1546
|
+
is a thin wrapper that validates node_kind is not None before delegation.
|
|
1547
|
+
|
|
1548
|
+
Creates a ModelDispatchContext with appropriate time injection based on
|
|
1549
|
+
the ONEX node kind:
|
|
1550
|
+
- REDUCER: now=None (deterministic state aggregation)
|
|
1551
|
+
- COMPUTE: now=None (pure transformation)
|
|
1552
|
+
- ORCHESTRATOR: now=datetime.now(UTC) (coordination)
|
|
1553
|
+
- EFFECT: now=datetime.now(UTC) (I/O operations)
|
|
1554
|
+
- RUNTIME_HOST: now=datetime.now(UTC) (infrastructure)
|
|
1555
|
+
|
|
1556
|
+
Args:
|
|
1557
|
+
entry: The dispatcher entry containing node_kind.
|
|
1558
|
+
envelope: The event envelope containing correlation metadata.
|
|
1559
|
+
|
|
1560
|
+
Returns:
|
|
1561
|
+
ModelDispatchContext configured appropriately for the node kind.
|
|
1562
|
+
|
|
1563
|
+
Raises:
|
|
1564
|
+
ModelOnexError: If node_kind is None or unrecognized.
|
|
1565
|
+
|
|
1566
|
+
Note:
|
|
1567
|
+
This is an internal method. Callers should ensure entry.node_kind
|
|
1568
|
+
is not None before calling.
|
|
1569
|
+
|
|
1570
|
+
Time Semantics:
|
|
1571
|
+
The ``now`` field is captured at context creation time (dispatch time),
|
|
1572
|
+
NOT at handler execution time. For ORCHESTRATOR, EFFECT, and RUNTIME_HOST
|
|
1573
|
+
nodes, this means:
|
|
1574
|
+
|
|
1575
|
+
- ``now`` represents when MessageDispatchEngine created the context
|
|
1576
|
+
- Handler execution may occur microseconds to milliseconds later
|
|
1577
|
+
- For most use cases, this drift is negligible
|
|
1578
|
+
- If sub-millisecond precision is required, handlers should capture
|
|
1579
|
+
their own time at the start of execution
|
|
1580
|
+
|
|
1581
|
+
.. versionadded:: 0.5.0
|
|
1582
|
+
.. versionchanged:: 0.5.1
|
|
1583
|
+
Now delegates to DispatchContextEnforcer.create_context_for_node_kind()
|
|
1584
|
+
to eliminate code duplication.
|
|
1585
|
+
"""
|
|
1586
|
+
node_kind = entry.node_kind
|
|
1587
|
+
if node_kind is None:
|
|
1588
|
+
raise ModelOnexError(
|
|
1589
|
+
message=f"Cannot create context for dispatcher '{entry.dispatcher_id}': "
|
|
1590
|
+
"node_kind is None. This is an internal error.",
|
|
1591
|
+
error_code=EnumCoreErrorCode.INTERNAL_ERROR,
|
|
1592
|
+
)
|
|
1593
|
+
|
|
1594
|
+
# Delegate to the shared context enforcer for time injection rules.
|
|
1595
|
+
# This eliminates duplication between MessageDispatchEngine and any
|
|
1596
|
+
# other components that need to create contexts based on node_kind.
|
|
1597
|
+
return self._context_enforcer.create_context_for_node_kind(
|
|
1598
|
+
node_kind=node_kind,
|
|
1599
|
+
envelope=envelope,
|
|
1600
|
+
dispatcher_id=entry.dispatcher_id,
|
|
1601
|
+
)
|
|
1602
|
+
|
|
1603
|
+
def _dispatcher_accepts_context(
|
|
1604
|
+
self,
|
|
1605
|
+
dispatcher: DispatcherFunc | ContextAwareDispatcherFunc,
|
|
1606
|
+
) -> bool:
|
|
1607
|
+
"""
|
|
1608
|
+
Check if a dispatcher callable accepts a context parameter.
|
|
1609
|
+
|
|
1610
|
+
Uses inspect.signature to determine if the dispatcher has a second
|
|
1611
|
+
parameter for ModelDispatchContext. This enables backwards-compatible
|
|
1612
|
+
context injection - dispatchers without a context parameter will be
|
|
1613
|
+
called with just the envelope.
|
|
1614
|
+
|
|
1615
|
+
This method is called once at registration time and the result is
|
|
1616
|
+
cached in DispatchEntryInternal.accepts_context for performance.
|
|
1617
|
+
No signature inspection occurs during dispatch execution.
|
|
1618
|
+
|
|
1619
|
+
Type Safety Warnings:
|
|
1620
|
+
When a dispatcher has 2+ parameters but the second parameter doesn't
|
|
1621
|
+
follow conventional naming (containing 'context' or 'ctx'), a warning
|
|
1622
|
+
is logged to help developers identify potential signature mismatches.
|
|
1623
|
+
This is non-blocking - the method still returns True for backwards
|
|
1624
|
+
compatibility with existing dispatchers.
|
|
1625
|
+
|
|
1626
|
+
Args:
|
|
1627
|
+
dispatcher: The dispatcher callable to inspect.
|
|
1628
|
+
|
|
1629
|
+
Returns:
|
|
1630
|
+
True if dispatcher accepts a context parameter, False otherwise.
|
|
1631
|
+
|
|
1632
|
+
.. versionadded:: 0.5.0
|
|
1633
|
+
.. versionchanged:: 0.5.1
|
|
1634
|
+
Added warning logging for unconventional parameter naming.
|
|
1635
|
+
"""
|
|
1636
|
+
try:
|
|
1637
|
+
sig = inspect.signature(dispatcher)
|
|
1638
|
+
params = list(sig.parameters.values())
|
|
1639
|
+
# Dispatcher with context has 2+ parameters: (envelope, context, ...)
|
|
1640
|
+
# Dispatcher without context has 1 parameter: (envelope)
|
|
1641
|
+
#
|
|
1642
|
+
# Design Decision: We use >= MIN_PARAMS_FOR_CONTEXT (not ==) intentionally
|
|
1643
|
+
# to support:
|
|
1644
|
+
# - Future extensibility (e.g., envelope, context, **kwargs)
|
|
1645
|
+
# - Dispatchers with additional optional parameters for testing/logging
|
|
1646
|
+
# - Protocol compliance without strict arity enforcement
|
|
1647
|
+
#
|
|
1648
|
+
# Strict == MIN_PARAMS_FOR_CONTEXT would reject valid dispatchers that
|
|
1649
|
+
# happen to have extra optional parameters, which is unnecessarily restrictive.
|
|
1650
|
+
if len(params) < MIN_PARAMS_FOR_CONTEXT:
|
|
1651
|
+
return False
|
|
1652
|
+
|
|
1653
|
+
# Type safety enhancement: Warn if second parameter doesn't follow
|
|
1654
|
+
# context naming convention. This helps developers identify potential
|
|
1655
|
+
# signature mismatches where a 2+ parameter dispatcher might not
|
|
1656
|
+
# actually expect a ModelDispatchContext.
|
|
1657
|
+
#
|
|
1658
|
+
# This is NON-BLOCKING - we still return True.
|
|
1659
|
+
# The warning is informational to help improve code quality.
|
|
1660
|
+
second_param = params[1]
|
|
1661
|
+
second_name = second_param.name.lower()
|
|
1662
|
+
if "context" not in second_name and "ctx" not in second_name:
|
|
1663
|
+
dispatcher_name = getattr(dispatcher, "__name__", str(dispatcher))
|
|
1664
|
+
self._logger.warning(
|
|
1665
|
+
"Dispatcher '%s' has 2+ parameters but second parameter '%s' "
|
|
1666
|
+
"doesn't follow context naming convention. "
|
|
1667
|
+
"Expected parameter name containing 'context' or 'ctx'. "
|
|
1668
|
+
"If this dispatcher expects a ModelDispatchContext, consider "
|
|
1669
|
+
"renaming the parameter for clarity.",
|
|
1670
|
+
dispatcher_name,
|
|
1671
|
+
second_param.name,
|
|
1672
|
+
)
|
|
1673
|
+
|
|
1674
|
+
return True
|
|
1675
|
+
except (ValueError, TypeError) as e:
|
|
1676
|
+
# If we can't inspect the signature, assume no context and log warning
|
|
1677
|
+
self._logger.warning(
|
|
1678
|
+
"Failed to inspect dispatcher signature: %s. "
|
|
1679
|
+
"Assuming no context parameter. Uninspectable dispatchers "
|
|
1680
|
+
"(C extensions, certain decorators) will receive envelope only.",
|
|
1681
|
+
e,
|
|
1682
|
+
)
|
|
1683
|
+
return False
|
|
1684
|
+
|
|
1685
|
+
def get_structured_metrics(self) -> ModelDispatchMetrics:
|
|
1686
|
+
"""
|
|
1687
|
+
Get structured dispatch metrics using Pydantic model.
|
|
1688
|
+
|
|
1689
|
+
Returns a comprehensive metrics model including:
|
|
1690
|
+
- Dispatch counts and success/error rates
|
|
1691
|
+
- Latency statistics (average, min, max)
|
|
1692
|
+
- Latency histogram for distribution analysis
|
|
1693
|
+
- Per-dispatcher metrics breakdown
|
|
1694
|
+
- Per-category metrics breakdown
|
|
1695
|
+
|
|
1696
|
+
Thread Safety:
|
|
1697
|
+
This method acquires ``_metrics_lock`` to return a consistent snapshot.
|
|
1698
|
+
The same lock protects all metrics updates, ensuring TOCTOU-safe
|
|
1699
|
+
read-modify-write operations during dispatch. The returned Pydantic
|
|
1700
|
+
model is immutable and safe to use after the lock is released.
|
|
1701
|
+
|
|
1702
|
+
Returns:
|
|
1703
|
+
ModelDispatchMetrics with all observability data
|
|
1704
|
+
|
|
1705
|
+
Example:
|
|
1706
|
+
>>> metrics = engine.get_structured_metrics()
|
|
1707
|
+
>>> print(f"Success rate: {metrics.success_rate:.1%}")
|
|
1708
|
+
>>> print(f"Avg latency: {metrics.avg_latency_ms:.2f} ms")
|
|
1709
|
+
>>> for dispatcher_id, dispatcher_metrics in metrics.dispatcher_metrics.items():
|
|
1710
|
+
... print(f"Dispatcher {dispatcher_id}: {dispatcher_metrics.execution_count} executions")
|
|
1711
|
+
|
|
1712
|
+
.. versionadded:: 0.4.0
|
|
1713
|
+
"""
|
|
1714
|
+
# Return under lock to ensure consistent snapshot
|
|
1715
|
+
with self._metrics_lock:
|
|
1716
|
+
return self._structured_metrics
|
|
1717
|
+
|
|
1718
|
+
def reset_metrics(self) -> None:
|
|
1719
|
+
"""
|
|
1720
|
+
Reset all metrics to initial state.
|
|
1721
|
+
|
|
1722
|
+
Useful for testing or when starting a new monitoring period.
|
|
1723
|
+
|
|
1724
|
+
Thread Safety:
|
|
1725
|
+
This method acquires ``_metrics_lock`` to ensure atomic reset
|
|
1726
|
+
of all metrics. Safe to call during concurrent dispatch operations,
|
|
1727
|
+
though the reset will briefly block in-flight metric updates.
|
|
1728
|
+
|
|
1729
|
+
Example:
|
|
1730
|
+
>>> engine.reset_metrics()
|
|
1731
|
+
>>> assert engine.get_structured_metrics().total_dispatches == 0
|
|
1732
|
+
|
|
1733
|
+
.. versionadded:: 0.4.0
|
|
1734
|
+
"""
|
|
1735
|
+
with self._metrics_lock:
|
|
1736
|
+
self._structured_metrics = ModelDispatchMetrics()
|
|
1737
|
+
self._logger.debug("Metrics reset to initial state")
|
|
1738
|
+
|
|
1739
|
+
def get_dispatcher_metrics(
|
|
1740
|
+
self, dispatcher_id: str
|
|
1741
|
+
) -> ModelDispatcherMetrics | None:
|
|
1742
|
+
"""
|
|
1743
|
+
Get metrics for a specific dispatcher.
|
|
1744
|
+
|
|
1745
|
+
Thread Safety:
|
|
1746
|
+
This method acquires ``_metrics_lock`` to return a consistent snapshot.
|
|
1747
|
+
The returned Pydantic model is immutable and safe to use after the
|
|
1748
|
+
lock is released.
|
|
1749
|
+
|
|
1750
|
+
Args:
|
|
1751
|
+
dispatcher_id: The dispatcher's unique identifier.
|
|
1752
|
+
|
|
1753
|
+
Returns:
|
|
1754
|
+
ModelDispatcherMetrics for the dispatcher, or None if no metrics recorded.
|
|
1755
|
+
|
|
1756
|
+
Example:
|
|
1757
|
+
>>> metrics = engine.get_dispatcher_metrics("user-event-dispatcher")
|
|
1758
|
+
>>> if metrics:
|
|
1759
|
+
... print(f"Executions: {metrics.execution_count}")
|
|
1760
|
+
... print(f"Error rate: {metrics.error_rate:.1%}")
|
|
1761
|
+
|
|
1762
|
+
.. versionadded:: 0.4.0
|
|
1763
|
+
"""
|
|
1764
|
+
with self._metrics_lock:
|
|
1765
|
+
return self._structured_metrics.dispatcher_metrics.get(dispatcher_id)
|
|
1766
|
+
|
|
1767
|
+
@property
|
|
1768
|
+
def route_count(self) -> int:
|
|
1769
|
+
"""Get the number of registered routes."""
|
|
1770
|
+
return len(self._routes)
|
|
1771
|
+
|
|
1772
|
+
@property
|
|
1773
|
+
def dispatcher_count(self) -> int:
|
|
1774
|
+
"""Get the number of registered dispatchers."""
|
|
1775
|
+
return len(self._dispatchers)
|
|
1776
|
+
|
|
1777
|
+
def __str__(self) -> str:
|
|
1778
|
+
"""Human-readable string representation."""
|
|
1779
|
+
return (
|
|
1780
|
+
f"MessageDispatchEngine[routes={len(self._routes)}, "
|
|
1781
|
+
f"dispatchers={len(self._dispatchers)}, frozen={self._frozen}]"
|
|
1782
|
+
)
|
|
1783
|
+
|
|
1784
|
+
def __repr__(self) -> str:
|
|
1785
|
+
"""Detailed representation for debugging."""
|
|
1786
|
+
route_ids = list(self._routes.keys())[:10]
|
|
1787
|
+
dispatcher_ids = list(self._dispatchers.keys())[:10]
|
|
1788
|
+
|
|
1789
|
+
route_repr = (
|
|
1790
|
+
repr(route_ids)
|
|
1791
|
+
if len(self._routes) <= 10
|
|
1792
|
+
else f"<{len(self._routes)} routes>"
|
|
1793
|
+
)
|
|
1794
|
+
dispatcher_repr = (
|
|
1795
|
+
repr(dispatcher_ids)
|
|
1796
|
+
if len(self._dispatchers) <= 10
|
|
1797
|
+
else f"<{len(self._dispatchers)} dispatchers>"
|
|
1798
|
+
)
|
|
1799
|
+
|
|
1800
|
+
return (
|
|
1801
|
+
f"MessageDispatchEngine("
|
|
1802
|
+
f"routes={route_repr}, "
|
|
1803
|
+
f"dispatchers={dispatcher_repr}, "
|
|
1804
|
+
f"frozen={self._frozen})"
|
|
1805
|
+
)
|