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,2465 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 OmniNode Team
|
|
3
|
+
# ruff: noqa: G201
|
|
4
|
+
# G201 disabled: Logging extra dict is intentional for structured logging with correlation IDs
|
|
5
|
+
"""Node introspection mixin providing automatic capability discovery.
|
|
6
|
+
|
|
7
|
+
This module provides a reusable mixin for ONEX nodes to implement automatic
|
|
8
|
+
capability discovery, endpoint reporting, and periodic heartbeat broadcasting.
|
|
9
|
+
It uses reflection to discover node capabilities and integrates with the event
|
|
10
|
+
bus for distributed service discovery.
|
|
11
|
+
|
|
12
|
+
Features:
|
|
13
|
+
- Automatic capability discovery via reflection
|
|
14
|
+
- Endpoint URL discovery (health, api, metrics)
|
|
15
|
+
- FSM state reporting if applicable
|
|
16
|
+
- Cached introspection data with configurable TTL
|
|
17
|
+
- Background heartbeat task for periodic health broadcasts
|
|
18
|
+
- Registry listener for REQUEST_INTROSPECTION events
|
|
19
|
+
- Graceful degradation when event bus is unavailable
|
|
20
|
+
|
|
21
|
+
Note:
|
|
22
|
+
- active_operations_count in heartbeats is tracked via ``track_operation()``
|
|
23
|
+
context manager. Nodes should wrap their operations with this context
|
|
24
|
+
manager to accurately report concurrent operation counts.
|
|
25
|
+
|
|
26
|
+
- **track_operation() Usage Guidelines**:
|
|
27
|
+
|
|
28
|
+
Within MixinNodeIntrospection itself, only ``publish_introspection()`` uses
|
|
29
|
+
``track_operation()``. This is intentional for the following reasons:
|
|
30
|
+
|
|
31
|
+
1. **_publish_heartbeat()**: Explicitly excluded because it's an internal
|
|
32
|
+
background task. Tracking it would cause self-referential counting
|
|
33
|
+
(heartbeat counting itself as active) and would report infrastructure
|
|
34
|
+
overhead rather than business load.
|
|
35
|
+
|
|
36
|
+
2. **get_introspection_data()**: Called by ``publish_introspection()``, which
|
|
37
|
+
already wraps the entire operation. Adding tracking here would cause
|
|
38
|
+
double-counting. Additionally, this is metadata gathering, not a business
|
|
39
|
+
operation that represents node load.
|
|
40
|
+
|
|
41
|
+
3. **start/stop_introspection_tasks()**: One-time lifecycle operations that
|
|
42
|
+
complete quickly. They spawn/cancel background tasks but don't represent
|
|
43
|
+
ongoing load. The counter would increment and immediately decrement.
|
|
44
|
+
|
|
45
|
+
4. **get_capabilities(), get_endpoints(), get_current_state()**: Internal
|
|
46
|
+
metadata operations that are part of introspection data gathering, not
|
|
47
|
+
independent business operations.
|
|
48
|
+
|
|
49
|
+
**For consuming nodes**: Use ``track_operation()`` in your business methods
|
|
50
|
+
(e.g., ``execute_query()``, ``process_request()``, ``handle_event()``) to
|
|
51
|
+
accurately report concurrent operation counts in heartbeats. See the
|
|
52
|
+
``track_operation()`` docstring for usage examples.
|
|
53
|
+
|
|
54
|
+
Security Considerations:
|
|
55
|
+
This mixin uses Python reflection (via the ``inspect`` module) to automatically
|
|
56
|
+
discover node capabilities. While this enables powerful service discovery, it
|
|
57
|
+
has security implications that developers must understand.
|
|
58
|
+
|
|
59
|
+
**Threat Model**:
|
|
60
|
+
|
|
61
|
+
Introspection data could be valuable to an attacker for:
|
|
62
|
+
|
|
63
|
+
- **Reconnaissance**: Learning what operations a node supports to identify
|
|
64
|
+
attack vectors (e.g., discovering ``decrypt_*``, ``admin_*`` methods).
|
|
65
|
+
- **Architecture mapping**: Understanding system topology through protocol
|
|
66
|
+
and mixin discovery (e.g., which nodes implement ``ProtocolDatabaseAdapter``).
|
|
67
|
+
- **Version fingerprinting**: Identifying outdated versions with known
|
|
68
|
+
vulnerabilities via the ``version`` field.
|
|
69
|
+
- **State inference**: Deducing system state or health from FSM state values.
|
|
70
|
+
|
|
71
|
+
**What Gets Exposed via Introspection**:
|
|
72
|
+
|
|
73
|
+
- **Public method names**: Method names that may reveal operations
|
|
74
|
+
(e.g., ``execute_query``, ``process_payment``).
|
|
75
|
+
- **Method signatures**: Full signatures including parameter names and type
|
|
76
|
+
annotations. Parameter names like ``api_key``, ``user_password``, or
|
|
77
|
+
``decrypt_key`` reveal sensitive parameter purposes.
|
|
78
|
+
- **Protocol implementations**: Class names from inheritance hierarchy that
|
|
79
|
+
start with ``Protocol`` or ``Mixin`` (e.g., ``ProtocolDatabaseAdapter``,
|
|
80
|
+
``MixinAsyncCircuitBreaker``).
|
|
81
|
+
- **FSM state information**: Current state value if FSM attributes exist
|
|
82
|
+
(e.g., ``connected``, ``authenticated``, ``processing``).
|
|
83
|
+
- **Endpoint URLs**: Health, API, and metrics endpoint paths.
|
|
84
|
+
- **Node metadata**: Node ID (UUID), type via ``EnumNodeKind`` (EFFECT/COMPUTE/REDUCER/ORCHESTRATOR), and version.
|
|
85
|
+
|
|
86
|
+
**What is NOT Exposed**:
|
|
87
|
+
|
|
88
|
+
- Private methods (prefixed with ``_``) - completely excluded from discovery.
|
|
89
|
+
- Method implementations or source code - only signatures, not logic.
|
|
90
|
+
- Internal state variables - only FSM state if present.
|
|
91
|
+
- Configuration values - secrets, connection strings, etc. are not exposed.
|
|
92
|
+
- Environment variables or runtime parameters.
|
|
93
|
+
- Request/response payloads or historical data.
|
|
94
|
+
|
|
95
|
+
**Built-in Protections**:
|
|
96
|
+
|
|
97
|
+
The mixin includes filtering mechanisms to limit exposure:
|
|
98
|
+
|
|
99
|
+
- **Private method exclusion**: Methods prefixed with ``_`` are excluded from
|
|
100
|
+
capability discovery.
|
|
101
|
+
- **Utility method filtering**: Common utility prefixes (``get_*``, ``set_*``,
|
|
102
|
+
``initialize*``, ``start_*``, ``stop_*``) are filtered out by default.
|
|
103
|
+
- **Operation keyword matching**: Only methods containing operation keywords
|
|
104
|
+
(``execute``, ``handle``, ``process``, ``run``, ``invoke``, ``call``) are
|
|
105
|
+
reported as capabilities in the operations list.
|
|
106
|
+
- **Configurable exclusions**: The ``exclude_prefixes`` parameter in
|
|
107
|
+
``initialize_introspection()`` allows additional filtering.
|
|
108
|
+
- **Caching with TTL**: Introspection data is cached to reduce reflection
|
|
109
|
+
frequency, with configurable TTL for freshness.
|
|
110
|
+
|
|
111
|
+
**Best Practices for Node Developers**:
|
|
112
|
+
|
|
113
|
+
- Prefix internal/sensitive methods with ``_`` to exclude them from introspection.
|
|
114
|
+
- Avoid exposing sensitive business logic in public method names (e.g., use
|
|
115
|
+
``process_request`` instead of ``decrypt_and_forward_to_payment_gateway``).
|
|
116
|
+
- Use generic parameter names for public methods (e.g., ``data`` instead of
|
|
117
|
+
``user_credentials``, ``payload`` instead of ``encrypted_secret``).
|
|
118
|
+
- Review exposed capabilities before deploying to production environments.
|
|
119
|
+
- Consider network segmentation for introspection event topics in multi-tenant
|
|
120
|
+
environments.
|
|
121
|
+
- Use the ``exclude_prefixes`` parameter to filter additional method patterns
|
|
122
|
+
if needed.
|
|
123
|
+
|
|
124
|
+
**Network Security Considerations**:
|
|
125
|
+
|
|
126
|
+
- Introspection data is published to Kafka topics (``node.introspection``,
|
|
127
|
+
``node.heartbeat``, ``node.request_introspection``).
|
|
128
|
+
- In multi-tenant environments, ensure proper topic ACLs are configured.
|
|
129
|
+
- Consider whether introspection topics should be accessible outside the cluster.
|
|
130
|
+
- Monitor introspection topic consumers for unauthorized access.
|
|
131
|
+
- The registry listener responds to ANY request on the request topic without
|
|
132
|
+
authentication - secure the topic with Kafka ACLs.
|
|
133
|
+
|
|
134
|
+
**Production Deployment Checklist**:
|
|
135
|
+
|
|
136
|
+
1. Review ``get_capabilities()`` output for each node before deployment.
|
|
137
|
+
2. Verify no sensitive method names or parameter names are exposed.
|
|
138
|
+
3. Configure Kafka topic ACLs to restrict introspection topic access.
|
|
139
|
+
4. Consider disabling ``enable_registry_listener`` if not needed.
|
|
140
|
+
5. Monitor introspection topic consumer groups for unexpected consumers.
|
|
141
|
+
6. Use network segmentation to isolate introspection traffic if required.
|
|
142
|
+
|
|
143
|
+
For more details, see the "Node Introspection Security Considerations" section
|
|
144
|
+
in ``CLAUDE.md``.
|
|
145
|
+
|
|
146
|
+
Usage:
|
|
147
|
+
```python
|
|
148
|
+
from omnibase_core.enums import EnumNodeKind
|
|
149
|
+
from omnibase_infra.mixins import MixinNodeIntrospection
|
|
150
|
+
from omnibase_infra.models.discovery import ModelIntrospectionConfig
|
|
151
|
+
|
|
152
|
+
class MyNode(MixinNodeIntrospection):
|
|
153
|
+
def __init__(self, node_config, event_bus=None):
|
|
154
|
+
config = ModelIntrospectionConfig(
|
|
155
|
+
node_id=node_config.node_id,
|
|
156
|
+
node_type=EnumNodeKind.EFFECT,
|
|
157
|
+
event_bus=event_bus,
|
|
158
|
+
)
|
|
159
|
+
self.initialize_introspection(config)
|
|
160
|
+
|
|
161
|
+
async def startup(self):
|
|
162
|
+
# Publish initial introspection on startup
|
|
163
|
+
await self.publish_introspection(reason="startup")
|
|
164
|
+
|
|
165
|
+
# Start background tasks
|
|
166
|
+
await self.start_introspection_tasks(
|
|
167
|
+
enable_heartbeat=True,
|
|
168
|
+
heartbeat_interval_seconds=30.0,
|
|
169
|
+
enable_registry_listener=True,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
async def shutdown(self):
|
|
173
|
+
# Publish shutdown introspection
|
|
174
|
+
await self.publish_introspection(reason="shutdown")
|
|
175
|
+
|
|
176
|
+
# Stop background tasks
|
|
177
|
+
await self.stop_introspection_tasks()
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Integration Requirements:
|
|
181
|
+
Classes using this mixin must:
|
|
182
|
+
1. Call `initialize_introspection(config)` during initialization with a
|
|
183
|
+
ModelIntrospectionConfig instance
|
|
184
|
+
2. Optionally call `start_introspection_tasks()` for background operations
|
|
185
|
+
3. Call `stop_introspection_tasks()` during shutdown
|
|
186
|
+
4. Ensure event_bus has `publish_envelope()` method if provided
|
|
187
|
+
|
|
188
|
+
See Also:
|
|
189
|
+
- ModelIntrospectionConfig for configuration options
|
|
190
|
+
- MixinAsyncCircuitBreaker for circuit breaker pattern
|
|
191
|
+
- ModelNodeIntrospectionEvent for event model
|
|
192
|
+
- ModelNodeHeartbeatEvent for heartbeat model
|
|
193
|
+
- CLAUDE.md "Node Introspection Security Considerations" section
|
|
194
|
+
"""
|
|
195
|
+
|
|
196
|
+
from __future__ import annotations
|
|
197
|
+
|
|
198
|
+
import asyncio
|
|
199
|
+
import inspect
|
|
200
|
+
import json
|
|
201
|
+
import logging
|
|
202
|
+
import time
|
|
203
|
+
from collections.abc import AsyncIterator, Awaitable, Callable
|
|
204
|
+
from contextlib import asynccontextmanager
|
|
205
|
+
from datetime import UTC, datetime
|
|
206
|
+
from typing import TYPE_CHECKING, ClassVar, TypedDict, cast
|
|
207
|
+
from uuid import UUID, uuid4
|
|
208
|
+
|
|
209
|
+
from omnibase_core.enums import EnumNodeKind
|
|
210
|
+
from omnibase_core.models.primitives.model_semver import ModelSemVer
|
|
211
|
+
from omnibase_infra.enums import EnumInfraTransportType, EnumIntrospectionReason
|
|
212
|
+
from omnibase_infra.errors import ModelInfraErrorContext, ProtocolConfigurationError
|
|
213
|
+
from omnibase_infra.models.discovery import (
|
|
214
|
+
ModelDiscoveredCapabilities,
|
|
215
|
+
ModelIntrospectionConfig,
|
|
216
|
+
ModelIntrospectionTaskConfig,
|
|
217
|
+
)
|
|
218
|
+
from omnibase_infra.models.discovery.model_introspection_performance_metrics import (
|
|
219
|
+
ModelIntrospectionPerformanceMetrics,
|
|
220
|
+
)
|
|
221
|
+
from omnibase_infra.models.registration import (
|
|
222
|
+
ModelNodeCapabilities,
|
|
223
|
+
ModelNodeHeartbeatEvent,
|
|
224
|
+
)
|
|
225
|
+
from omnibase_infra.models.registration.model_node_introspection_event import (
|
|
226
|
+
ModelNodeIntrospectionEvent,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
if TYPE_CHECKING:
|
|
230
|
+
from omnibase_core.protocols.event_bus.protocol_event_bus import ProtocolEventBus
|
|
231
|
+
from omnibase_infra.event_bus.models import ModelEventMessage
|
|
232
|
+
|
|
233
|
+
logger = logging.getLogger(__name__)
|
|
234
|
+
|
|
235
|
+
# Event topic constants
|
|
236
|
+
INTROSPECTION_TOPIC = "node.introspection"
|
|
237
|
+
HEARTBEAT_TOPIC = "node.heartbeat"
|
|
238
|
+
REQUEST_INTROSPECTION_TOPIC = "node.request_introspection"
|
|
239
|
+
|
|
240
|
+
# Performance threshold constants (in milliseconds)
|
|
241
|
+
PERF_THRESHOLD_GET_CAPABILITIES_MS = 50.0
|
|
242
|
+
PERF_THRESHOLD_DISCOVER_CAPABILITIES_MS = 30.0
|
|
243
|
+
PERF_THRESHOLD_GET_INTROSPECTION_DATA_MS = 50.0
|
|
244
|
+
PERF_THRESHOLD_CACHE_HIT_MS = 1.0
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class PerformanceMetricsCacheDict(TypedDict, total=False):
|
|
248
|
+
"""TypedDict for JSON-serialized ModelIntrospectionPerformanceMetrics.
|
|
249
|
+
|
|
250
|
+
This type matches the output of ModelIntrospectionPerformanceMetrics.model_dump(mode="json"),
|
|
251
|
+
enabling proper type checking for cached performance metrics.
|
|
252
|
+
|
|
253
|
+
Attributes:
|
|
254
|
+
get_capabilities_ms: Time taken by get_capabilities() in milliseconds.
|
|
255
|
+
discover_capabilities_ms: Time taken by _discover_capabilities() in ms.
|
|
256
|
+
get_endpoints_ms: Time taken by get_endpoints() in milliseconds.
|
|
257
|
+
get_current_state_ms: Time taken by get_current_state() in milliseconds.
|
|
258
|
+
total_introspection_ms: Total time for get_introspection_data() in ms.
|
|
259
|
+
cache_hit: Whether the result was served from cache.
|
|
260
|
+
method_count: Number of methods discovered during reflection.
|
|
261
|
+
threshold_exceeded: Whether any operation exceeded performance thresholds.
|
|
262
|
+
slow_operations: List of operation names that exceeded their thresholds.
|
|
263
|
+
captured_at: UTC timestamp when metrics were captured (ISO string).
|
|
264
|
+
"""
|
|
265
|
+
|
|
266
|
+
get_capabilities_ms: float
|
|
267
|
+
discover_capabilities_ms: float
|
|
268
|
+
get_endpoints_ms: float
|
|
269
|
+
get_current_state_ms: float
|
|
270
|
+
total_introspection_ms: float
|
|
271
|
+
cache_hit: bool
|
|
272
|
+
method_count: int
|
|
273
|
+
threshold_exceeded: bool
|
|
274
|
+
slow_operations: list[str]
|
|
275
|
+
captured_at: str # datetime serializes to ISO string in JSON mode
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
class DiscoveredCapabilitiesCacheDict(TypedDict, total=False):
|
|
279
|
+
"""TypedDict for JSON-serialized ModelDiscoveredCapabilities.
|
|
280
|
+
|
|
281
|
+
Attributes:
|
|
282
|
+
operations: List of method names matching operation keywords.
|
|
283
|
+
has_fsm: Whether the node has FSM state management.
|
|
284
|
+
method_signatures: Mapping of method names to signature strings.
|
|
285
|
+
attributes: Additional discovered attributes.
|
|
286
|
+
"""
|
|
287
|
+
|
|
288
|
+
operations: list[str]
|
|
289
|
+
has_fsm: bool
|
|
290
|
+
method_signatures: dict[str, str]
|
|
291
|
+
attributes: dict[str, object]
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
class IntrospectionCacheDict(TypedDict):
|
|
295
|
+
"""TypedDict representing the JSON-serialized ModelNodeIntrospectionEvent.
|
|
296
|
+
|
|
297
|
+
This type matches the output of ModelNodeIntrospectionEvent.model_dump(mode="json"),
|
|
298
|
+
enabling proper type checking for cache operations without requiring type: ignore comments.
|
|
299
|
+
|
|
300
|
+
Note:
|
|
301
|
+
The capabilities are split into declared_capabilities (from contract) and
|
|
302
|
+
discovered_capabilities (from reflection). This reflects the fundamental
|
|
303
|
+
difference between what a node declares and what introspection discovers.
|
|
304
|
+
"""
|
|
305
|
+
|
|
306
|
+
node_id: str
|
|
307
|
+
node_type: str
|
|
308
|
+
node_version: dict[str, int] # ModelSemVer serializes to {major, minor, patch}
|
|
309
|
+
declared_capabilities: dict[str, object] # ModelNodeCapabilities (flexible schema)
|
|
310
|
+
discovered_capabilities: DiscoveredCapabilitiesCacheDict
|
|
311
|
+
endpoints: dict[str, str]
|
|
312
|
+
current_state: str | None
|
|
313
|
+
reason: str # EnumIntrospectionReason serializes to string
|
|
314
|
+
correlation_id: str # UUID serializes to string in JSON mode (required field)
|
|
315
|
+
timestamp: str # datetime serializes to ISO string in JSON mode
|
|
316
|
+
# Optional fields
|
|
317
|
+
node_role: str | None
|
|
318
|
+
metadata: dict[str, object] # ModelNodeMetadata serializes to dict
|
|
319
|
+
network_id: str | None
|
|
320
|
+
deployment_id: str | None
|
|
321
|
+
epoch: int | None
|
|
322
|
+
# Performance metrics from introspection operation (may be None)
|
|
323
|
+
performance_metrics: PerformanceMetricsCacheDict | None
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
class MixinNodeIntrospection:
|
|
327
|
+
"""Mixin providing node introspection capabilities.
|
|
328
|
+
|
|
329
|
+
Provides automatic capability discovery using reflection, endpoint
|
|
330
|
+
reporting, and periodic heartbeat broadcasting for ONEX nodes.
|
|
331
|
+
|
|
332
|
+
State Variables:
|
|
333
|
+
_introspection_cache: Cached introspection data
|
|
334
|
+
_introspection_cache_ttl: Cache time-to-live in seconds
|
|
335
|
+
_introspection_cached_at: Timestamp when cache was populated
|
|
336
|
+
|
|
337
|
+
Background Task Variables:
|
|
338
|
+
_heartbeat_task: Background heartbeat task
|
|
339
|
+
_registry_listener_task: Background registry listener task
|
|
340
|
+
_introspection_stop_event: Event to signal task shutdown
|
|
341
|
+
|
|
342
|
+
Configuration Variables:
|
|
343
|
+
_introspection_node_id: Node identifier
|
|
344
|
+
_introspection_node_type: Node type classification
|
|
345
|
+
_introspection_event_bus: Optional event bus for publishing
|
|
346
|
+
_introspection_version: Node version string
|
|
347
|
+
_introspection_start_time: Node startup timestamp
|
|
348
|
+
|
|
349
|
+
Security Considerations:
|
|
350
|
+
This mixin uses Python reflection (via the ``inspect`` module) to
|
|
351
|
+
automatically discover node capabilities. While this enables powerful
|
|
352
|
+
service discovery, it has security implications:
|
|
353
|
+
|
|
354
|
+
**Threat Model**:
|
|
355
|
+
|
|
356
|
+
- **Reconnaissance**: Method names may reveal attack vectors
|
|
357
|
+
- **Architecture mapping**: Protocol discovery exposes topology
|
|
358
|
+
- **Version fingerprinting**: Version field enables vulnerability scanning
|
|
359
|
+
- **State inference**: FSM state reveals system status
|
|
360
|
+
|
|
361
|
+
**Exposed Information**:
|
|
362
|
+
|
|
363
|
+
- Public method names (potential operations a node can perform)
|
|
364
|
+
- Method signatures (parameter names and type annotations)
|
|
365
|
+
- Protocol and mixin implementations (discovered capabilities)
|
|
366
|
+
- FSM state information (if state attributes are present)
|
|
367
|
+
- Endpoint URLs (health, API, metrics paths)
|
|
368
|
+
- Node metadata (name, version, type)
|
|
369
|
+
|
|
370
|
+
**What is NOT Exposed**:
|
|
371
|
+
|
|
372
|
+
- Private methods (``_`` prefix) - excluded from discovery
|
|
373
|
+
- Method implementations or source code
|
|
374
|
+
- Configuration values, secrets, or connection strings
|
|
375
|
+
- Environment variables or runtime parameters
|
|
376
|
+
- Request/response payloads or historical data
|
|
377
|
+
|
|
378
|
+
**Built-in Protections**:
|
|
379
|
+
|
|
380
|
+
- Private methods (prefixed with ``_``) are excluded by default
|
|
381
|
+
- Utility method prefixes (``get_*``, ``set_*``, etc.) are filtered
|
|
382
|
+
- Only methods containing operation keywords are reported as operations
|
|
383
|
+
- Configure ``exclude_prefixes`` in ``initialize_introspection()`` for
|
|
384
|
+
additional filtering
|
|
385
|
+
- Caching with TTL reduces reflection frequency
|
|
386
|
+
|
|
387
|
+
**Recommendations for Production**:
|
|
388
|
+
|
|
389
|
+
- Prefix internal/sensitive methods with ``_`` to exclude them
|
|
390
|
+
- Use generic operation names that don't reveal implementation details
|
|
391
|
+
- Use generic parameter names (``data`` instead of ``user_credentials``)
|
|
392
|
+
- Review ``get_capabilities()`` output before production deployment
|
|
393
|
+
- In multi-tenant environments, configure Kafka topic ACLs for
|
|
394
|
+
introspection events (``node.introspection``, ``node.heartbeat``,
|
|
395
|
+
``node.request_introspection``)
|
|
396
|
+
- Monitor introspection topic consumers for unauthorized access
|
|
397
|
+
- Consider network segmentation for introspection event topics
|
|
398
|
+
- Consider disabling ``enable_registry_listener`` if not needed
|
|
399
|
+
|
|
400
|
+
See Also:
|
|
401
|
+
- Module docstring for detailed security documentation and threat model
|
|
402
|
+
- CLAUDE.md "Node Introspection Security Considerations" section
|
|
403
|
+
- ``get_capabilities()`` for filtering logic details
|
|
404
|
+
|
|
405
|
+
Example:
|
|
406
|
+
```python
|
|
407
|
+
from uuid import UUID
|
|
408
|
+
from omnibase_core.enums import EnumNodeKind
|
|
409
|
+
from omnibase_infra.models.discovery import ModelIntrospectionConfig
|
|
410
|
+
|
|
411
|
+
class PostgresAdapter(MixinNodeIntrospection):
|
|
412
|
+
def __init__(self, node_id: UUID, adapter_config):
|
|
413
|
+
config = ModelIntrospectionConfig(
|
|
414
|
+
node_id=node_id,
|
|
415
|
+
node_type=EnumNodeKind.EFFECT,
|
|
416
|
+
event_bus=adapter_config.event_bus,
|
|
417
|
+
)
|
|
418
|
+
self.initialize_introspection(config)
|
|
419
|
+
|
|
420
|
+
async def execute(self, query: str) -> list[dict]:
|
|
421
|
+
# Node operation - WILL be exposed via introspection
|
|
422
|
+
...
|
|
423
|
+
|
|
424
|
+
def _internal_helper(self, data: dict) -> dict:
|
|
425
|
+
# Private method - will NOT be exposed
|
|
426
|
+
...
|
|
427
|
+
```
|
|
428
|
+
"""
|
|
429
|
+
|
|
430
|
+
# Class-level cache for method signatures (populated once per class)
|
|
431
|
+
# Maps class -> {method_name: signature_string}
|
|
432
|
+
# This avoids expensive reflection on each introspection call since
|
|
433
|
+
# method signatures don't change after class definition.
|
|
434
|
+
# NOTE: ClassVar is intentionally shared across all instances - this is correct
|
|
435
|
+
# behavior for a per-class cache of immutable method signatures.
|
|
436
|
+
_class_method_cache: ClassVar[dict[type, dict[str, str]]] = {}
|
|
437
|
+
|
|
438
|
+
# Type annotations for instance attributes (no default values to avoid shared state)
|
|
439
|
+
# All of these are initialized in initialize_introspection()
|
|
440
|
+
#
|
|
441
|
+
# Caching attributes
|
|
442
|
+
_introspection_cache: IntrospectionCacheDict | None
|
|
443
|
+
_introspection_cache_ttl: float
|
|
444
|
+
_introspection_cached_at: float | None
|
|
445
|
+
|
|
446
|
+
# Background task attributes
|
|
447
|
+
_heartbeat_task: asyncio.Task[None] | None
|
|
448
|
+
_registry_listener_task: asyncio.Task[None] | None
|
|
449
|
+
_introspection_stop_event: asyncio.Event | None
|
|
450
|
+
_registry_unsubscribe: Callable[[], None] | Callable[[], Awaitable[None]] | None
|
|
451
|
+
|
|
452
|
+
# Configuration attributes
|
|
453
|
+
_introspection_node_id: UUID | None
|
|
454
|
+
_introspection_node_type: EnumNodeKind | None
|
|
455
|
+
_introspection_event_bus: ProtocolEventBus | None
|
|
456
|
+
_introspection_version: str
|
|
457
|
+
_introspection_start_time: float | None
|
|
458
|
+
|
|
459
|
+
# Capability discovery configuration
|
|
460
|
+
_introspection_operation_keywords: frozenset[str]
|
|
461
|
+
_introspection_exclude_prefixes: frozenset[str]
|
|
462
|
+
|
|
463
|
+
# Registry listener callback error tracking (instance-level)
|
|
464
|
+
# Used for rate-limiting error logging to prevent log spam during
|
|
465
|
+
# sustained failures. These are initialized in initialize_introspection().
|
|
466
|
+
_registry_callback_consecutive_failures: int
|
|
467
|
+
_registry_callback_last_failure_time: float
|
|
468
|
+
_registry_callback_failure_log_threshold: int
|
|
469
|
+
|
|
470
|
+
# Performance metrics tracking (instance-level)
|
|
471
|
+
# Stores the most recent performance metrics from introspection operations
|
|
472
|
+
_introspection_last_metrics: ModelIntrospectionPerformanceMetrics | None
|
|
473
|
+
|
|
474
|
+
# Active operations tracking (instance-level)
|
|
475
|
+
# Thread-safe counter for tracking concurrent operations
|
|
476
|
+
# Used by heartbeat to report active_operations_count
|
|
477
|
+
_active_operations: int
|
|
478
|
+
_operations_lock: asyncio.Lock
|
|
479
|
+
|
|
480
|
+
# Default operation keywords for capability discovery
|
|
481
|
+
DEFAULT_OPERATION_KEYWORDS: ClassVar[frozenset[str]] = frozenset(
|
|
482
|
+
{
|
|
483
|
+
"execute",
|
|
484
|
+
"handle",
|
|
485
|
+
"process",
|
|
486
|
+
"run",
|
|
487
|
+
"invoke",
|
|
488
|
+
"call",
|
|
489
|
+
}
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
# Default prefixes to exclude from capability discovery
|
|
493
|
+
DEFAULT_EXCLUDE_PREFIXES: ClassVar[frozenset[str]] = frozenset(
|
|
494
|
+
{
|
|
495
|
+
"_",
|
|
496
|
+
"get_",
|
|
497
|
+
"set_",
|
|
498
|
+
"initialize",
|
|
499
|
+
"start_",
|
|
500
|
+
"stop_",
|
|
501
|
+
}
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
# Node-type-specific operation keyword suggestions
|
|
505
|
+
# Uses EnumNodeKind as keys to ensure type safety when accessing with node_type.
|
|
506
|
+
# Example: keywords = NODE_TYPE_OPERATION_KEYWORDS.get(node_type, set())
|
|
507
|
+
NODE_TYPE_OPERATION_KEYWORDS: ClassVar[dict[EnumNodeKind, set[str]]] = {
|
|
508
|
+
EnumNodeKind.EFFECT: {
|
|
509
|
+
"execute",
|
|
510
|
+
"handle",
|
|
511
|
+
"process",
|
|
512
|
+
"run",
|
|
513
|
+
"invoke",
|
|
514
|
+
"call",
|
|
515
|
+
"fetch",
|
|
516
|
+
"send",
|
|
517
|
+
"query",
|
|
518
|
+
"connect",
|
|
519
|
+
},
|
|
520
|
+
EnumNodeKind.COMPUTE: {
|
|
521
|
+
"execute",
|
|
522
|
+
"handle",
|
|
523
|
+
"process",
|
|
524
|
+
"run",
|
|
525
|
+
"compute",
|
|
526
|
+
"transform",
|
|
527
|
+
"calculate",
|
|
528
|
+
"convert",
|
|
529
|
+
"parse",
|
|
530
|
+
},
|
|
531
|
+
EnumNodeKind.REDUCER: {
|
|
532
|
+
"execute",
|
|
533
|
+
"handle",
|
|
534
|
+
"process",
|
|
535
|
+
"run",
|
|
536
|
+
"aggregate",
|
|
537
|
+
"reduce",
|
|
538
|
+
"merge",
|
|
539
|
+
"combine",
|
|
540
|
+
"accumulate",
|
|
541
|
+
},
|
|
542
|
+
EnumNodeKind.ORCHESTRATOR: {
|
|
543
|
+
"execute",
|
|
544
|
+
"handle",
|
|
545
|
+
"process",
|
|
546
|
+
"run",
|
|
547
|
+
"orchestrate",
|
|
548
|
+
"coordinate",
|
|
549
|
+
"schedule",
|
|
550
|
+
"dispatch",
|
|
551
|
+
},
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
def initialize_introspection(
|
|
555
|
+
self,
|
|
556
|
+
config: ModelIntrospectionConfig,
|
|
557
|
+
) -> None:
|
|
558
|
+
"""Initialize introspection from a configuration model.
|
|
559
|
+
|
|
560
|
+
This method accepts a typed configuration model for all introspection
|
|
561
|
+
settings. Must be called during class initialization before any
|
|
562
|
+
introspection operations are performed.
|
|
563
|
+
|
|
564
|
+
Args:
|
|
565
|
+
config: Configuration model containing all introspection settings.
|
|
566
|
+
See ModelIntrospectionConfig for available options.
|
|
567
|
+
|
|
568
|
+
Raises:
|
|
569
|
+
ValueError: If config.node_id is not a valid UUID or config.node_type
|
|
570
|
+
is not a valid EnumNodeKind member.
|
|
571
|
+
TypeError: If node_type is neither EnumNodeKind nor str.
|
|
572
|
+
|
|
573
|
+
Example:
|
|
574
|
+
```python
|
|
575
|
+
from omnibase_core.enums import EnumNodeKind
|
|
576
|
+
from omnibase_infra.models.discovery import ModelIntrospectionConfig
|
|
577
|
+
|
|
578
|
+
class MyNode(MixinNodeIntrospection):
|
|
579
|
+
def __init__(self, node_config):
|
|
580
|
+
config = ModelIntrospectionConfig(
|
|
581
|
+
node_id=node_config.node_id,
|
|
582
|
+
node_type=EnumNodeKind.EFFECT,
|
|
583
|
+
event_bus=node_config.event_bus,
|
|
584
|
+
version="1.2.0",
|
|
585
|
+
)
|
|
586
|
+
self.initialize_introspection(config)
|
|
587
|
+
|
|
588
|
+
# With custom operation keywords
|
|
589
|
+
class MyEffectNode(MixinNodeIntrospection):
|
|
590
|
+
def __init__(self, node_config):
|
|
591
|
+
config = ModelIntrospectionConfig(
|
|
592
|
+
node_id=node_config.node_id,
|
|
593
|
+
node_type=EnumNodeKind.EFFECT,
|
|
594
|
+
event_bus=node_config.event_bus,
|
|
595
|
+
operation_keywords=frozenset({"fetch", "upload", "download"}),
|
|
596
|
+
)
|
|
597
|
+
self.initialize_introspection(config)
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
See Also:
|
|
601
|
+
ModelIntrospectionConfig: Configuration model with all available options.
|
|
602
|
+
"""
|
|
603
|
+
# Note: Pydantic validates node_id is a valid UUID and node_type is EnumNodeKind
|
|
604
|
+
|
|
605
|
+
# Configuration - extract from config model
|
|
606
|
+
self._introspection_node_id = config.node_id
|
|
607
|
+
|
|
608
|
+
# Defensive type handling for node_type: accept both EnumNodeKind and string.
|
|
609
|
+
# While ModelIntrospectionConfig's validator ensures EnumNodeKind, this defensive
|
|
610
|
+
# check handles edge cases like mocked configs or direct attribute access patterns.
|
|
611
|
+
if isinstance(config.node_type, EnumNodeKind):
|
|
612
|
+
self._introspection_node_type = config.node_type
|
|
613
|
+
elif isinstance(config.node_type, str):
|
|
614
|
+
# Coerce string to EnumNodeKind (handles both "effect" and "EFFECT")
|
|
615
|
+
self._introspection_node_type = EnumNodeKind(config.node_type.lower())
|
|
616
|
+
else:
|
|
617
|
+
# Should never happen with proper ModelIntrospectionConfig, but handle gracefully
|
|
618
|
+
context = ModelInfraErrorContext.with_correlation(
|
|
619
|
+
transport_type=EnumInfraTransportType.RUNTIME,
|
|
620
|
+
operation="initialize_introspection",
|
|
621
|
+
target_name=str(config.node_id),
|
|
622
|
+
)
|
|
623
|
+
raise ProtocolConfigurationError(
|
|
624
|
+
f"node_type must be EnumNodeKind or str, got {type(config.node_type).__name__}",
|
|
625
|
+
context=context,
|
|
626
|
+
parameter="node_type",
|
|
627
|
+
actual_type=type(config.node_type).__name__,
|
|
628
|
+
)
|
|
629
|
+
self._introspection_event_bus = config.event_bus
|
|
630
|
+
self._introspection_version = config.version
|
|
631
|
+
self._introspection_cache_ttl = config.cache_ttl
|
|
632
|
+
|
|
633
|
+
# Capability discovery configuration - frozensets are immutable, no copy needed
|
|
634
|
+
self._introspection_operation_keywords = (
|
|
635
|
+
config.operation_keywords
|
|
636
|
+
if config.operation_keywords is not None
|
|
637
|
+
else self.DEFAULT_OPERATION_KEYWORDS
|
|
638
|
+
)
|
|
639
|
+
self._introspection_exclude_prefixes = (
|
|
640
|
+
config.exclude_prefixes
|
|
641
|
+
if config.exclude_prefixes is not None
|
|
642
|
+
else self.DEFAULT_EXCLUDE_PREFIXES
|
|
643
|
+
)
|
|
644
|
+
|
|
645
|
+
# Topic configuration - extract from config model
|
|
646
|
+
self._introspection_topic = config.introspection_topic
|
|
647
|
+
self._heartbeat_topic = config.heartbeat_topic
|
|
648
|
+
self._request_introspection_topic = config.request_introspection_topic
|
|
649
|
+
|
|
650
|
+
# State
|
|
651
|
+
self._introspection_cache = None
|
|
652
|
+
self._introspection_cached_at = None
|
|
653
|
+
self._introspection_start_time = time.time()
|
|
654
|
+
|
|
655
|
+
# Background tasks
|
|
656
|
+
self._heartbeat_task = None
|
|
657
|
+
self._registry_listener_task = None
|
|
658
|
+
self._introspection_stop_event = asyncio.Event()
|
|
659
|
+
self._registry_unsubscribe = None
|
|
660
|
+
|
|
661
|
+
# Registry listener callback error tracking
|
|
662
|
+
# Used for rate-limiting error logging to prevent log spam
|
|
663
|
+
self._registry_callback_consecutive_failures = 0
|
|
664
|
+
self._registry_callback_last_failure_time = 0.0
|
|
665
|
+
# Only log every Nth consecutive failure to prevent log spam
|
|
666
|
+
self._registry_callback_failure_log_threshold = 5
|
|
667
|
+
|
|
668
|
+
# Performance metrics tracking
|
|
669
|
+
self._introspection_last_metrics = None
|
|
670
|
+
|
|
671
|
+
# Active operations tracking
|
|
672
|
+
# Thread-safe counter for tracking concurrent operations
|
|
673
|
+
self._active_operations = 0
|
|
674
|
+
self._operations_lock = asyncio.Lock()
|
|
675
|
+
|
|
676
|
+
if config.event_bus is None:
|
|
677
|
+
logger.warning(
|
|
678
|
+
f"Introspection initialized without event bus for {config.node_id}",
|
|
679
|
+
extra={
|
|
680
|
+
"node_id": config.node_id,
|
|
681
|
+
"node_type": config.node_type.value
|
|
682
|
+
if hasattr(config.node_type, "value")
|
|
683
|
+
else str(config.node_type),
|
|
684
|
+
},
|
|
685
|
+
)
|
|
686
|
+
|
|
687
|
+
logger.debug(
|
|
688
|
+
f"Introspection initialized for {config.node_id}",
|
|
689
|
+
extra={
|
|
690
|
+
"node_id": config.node_id,
|
|
691
|
+
"node_type": config.node_type.value
|
|
692
|
+
if hasattr(config.node_type, "value")
|
|
693
|
+
else str(config.node_type),
|
|
694
|
+
"version": config.version,
|
|
695
|
+
"cache_ttl": config.cache_ttl,
|
|
696
|
+
"has_event_bus": config.event_bus is not None,
|
|
697
|
+
"operation_keywords_count": len(self._introspection_operation_keywords),
|
|
698
|
+
"exclude_prefixes_count": len(self._introspection_exclude_prefixes),
|
|
699
|
+
"introspection_topic": self._introspection_topic,
|
|
700
|
+
"heartbeat_topic": self._heartbeat_topic,
|
|
701
|
+
"request_introspection_topic": self._request_introspection_topic,
|
|
702
|
+
},
|
|
703
|
+
)
|
|
704
|
+
|
|
705
|
+
def _ensure_initialized(self) -> None:
|
|
706
|
+
"""Ensure introspection has been initialized.
|
|
707
|
+
|
|
708
|
+
This method validates that `initialize_introspection()` was called
|
|
709
|
+
before using introspection methods. It should be called at the start
|
|
710
|
+
of public entry point methods.
|
|
711
|
+
|
|
712
|
+
Raises:
|
|
713
|
+
ProtocolConfigurationError: If initialize_introspection() was not called.
|
|
714
|
+
|
|
715
|
+
Example:
|
|
716
|
+
```python
|
|
717
|
+
async def get_introspection_data(self) -> ModelNodeIntrospectionEvent:
|
|
718
|
+
self._ensure_initialized()
|
|
719
|
+
# ... rest of method
|
|
720
|
+
```
|
|
721
|
+
"""
|
|
722
|
+
# Use getattr with sentinel to avoid AttributeError if initialize_introspection()
|
|
723
|
+
# was never called. This ensures we always raise structured error, not AttributeError.
|
|
724
|
+
_not_set = object()
|
|
725
|
+
node_id = getattr(self, "_introspection_node_id", _not_set)
|
|
726
|
+
if node_id is _not_set or node_id is None:
|
|
727
|
+
ctx = ModelInfraErrorContext(
|
|
728
|
+
transport_type=EnumInfraTransportType.KAFKA,
|
|
729
|
+
operation="_ensure_initialized",
|
|
730
|
+
target_name="node_introspection_mixin",
|
|
731
|
+
)
|
|
732
|
+
raise ProtocolConfigurationError(
|
|
733
|
+
"MixinNodeIntrospection not initialized. "
|
|
734
|
+
"Call initialize_introspection() before using introspection methods.",
|
|
735
|
+
context=ctx,
|
|
736
|
+
)
|
|
737
|
+
|
|
738
|
+
def _get_class_method_signatures(self) -> dict[str, str]:
|
|
739
|
+
"""Get method signatures from class-level cache.
|
|
740
|
+
|
|
741
|
+
This method returns cached method signatures for the current class,
|
|
742
|
+
populating the cache on first access. The cache is shared across all
|
|
743
|
+
instances of the same class, avoiding expensive reflection operations
|
|
744
|
+
on each introspection call.
|
|
745
|
+
|
|
746
|
+
Security Note:
|
|
747
|
+
This method uses Python's ``inspect`` module to extract method
|
|
748
|
+
signatures, which exposes detailed type information:
|
|
749
|
+
|
|
750
|
+
- Parameter names may reveal business logic (e.g., ``user_id``,
|
|
751
|
+
``payment_token``, ``decrypt_key``)
|
|
752
|
+
- Type annotations expose internal data structures
|
|
753
|
+
- Return types reveal output formats
|
|
754
|
+
|
|
755
|
+
**Filtering Applied**:
|
|
756
|
+
|
|
757
|
+
- Only public methods (not starting with ``_``) are included
|
|
758
|
+
- Methods without inspectable signatures get ``(...)`` placeholder
|
|
759
|
+
|
|
760
|
+
**Mitigation**:
|
|
761
|
+
|
|
762
|
+
- Use generic parameter names for public methods
|
|
763
|
+
- Prefix sensitive helper methods with ``_``
|
|
764
|
+
|
|
765
|
+
Returns:
|
|
766
|
+
Dictionary mapping public method names to signature strings.
|
|
767
|
+
|
|
768
|
+
Note:
|
|
769
|
+
The cache is populated lazily on first access and persists for
|
|
770
|
+
the lifetime of the class. Use `_invalidate_class_method_cache()`
|
|
771
|
+
if methods are added dynamically at runtime.
|
|
772
|
+
|
|
773
|
+
Example:
|
|
774
|
+
```python
|
|
775
|
+
# First call populates cache
|
|
776
|
+
signatures = self._get_class_method_signatures()
|
|
777
|
+
# {"execute": "(query: str) -> list[dict]", ...}
|
|
778
|
+
|
|
779
|
+
# Subsequent calls return cached data
|
|
780
|
+
signatures = self._get_class_method_signatures()
|
|
781
|
+
```
|
|
782
|
+
"""
|
|
783
|
+
cls = type(self)
|
|
784
|
+
if cls not in MixinNodeIntrospection._class_method_cache:
|
|
785
|
+
# Populate cache for this class
|
|
786
|
+
signatures: dict[str, str] = {}
|
|
787
|
+
for name in dir(self):
|
|
788
|
+
if name.startswith("_"):
|
|
789
|
+
continue
|
|
790
|
+
attr = getattr(self, name, None)
|
|
791
|
+
if callable(attr) and inspect.ismethod(attr):
|
|
792
|
+
try:
|
|
793
|
+
sig = inspect.signature(attr)
|
|
794
|
+
signatures[name] = str(sig)
|
|
795
|
+
except (ValueError, TypeError):
|
|
796
|
+
# Some methods don't have inspectable signatures
|
|
797
|
+
signatures[name] = "(...)"
|
|
798
|
+
MixinNodeIntrospection._class_method_cache[cls] = signatures
|
|
799
|
+
return MixinNodeIntrospection._class_method_cache[cls]
|
|
800
|
+
|
|
801
|
+
@classmethod
|
|
802
|
+
def _invalidate_class_method_cache(cls, target_class: type | None = None) -> None:
|
|
803
|
+
"""Invalidate the class-level method signature cache.
|
|
804
|
+
|
|
805
|
+
Call this method when methods are dynamically added or removed from
|
|
806
|
+
a class at runtime. For most use cases, this is not necessary as
|
|
807
|
+
class methods are defined at class creation time.
|
|
808
|
+
|
|
809
|
+
Args:
|
|
810
|
+
target_class: Specific class to invalidate cache for.
|
|
811
|
+
If None, clears cache for all classes.
|
|
812
|
+
|
|
813
|
+
Example:
|
|
814
|
+
```python
|
|
815
|
+
# Invalidate cache for a specific class
|
|
816
|
+
MixinNodeIntrospection._invalidate_class_method_cache(MyNodeClass)
|
|
817
|
+
|
|
818
|
+
# Invalidate cache for all classes
|
|
819
|
+
MixinNodeIntrospection._invalidate_class_method_cache()
|
|
820
|
+
```
|
|
821
|
+
|
|
822
|
+
Note:
|
|
823
|
+
This is typically only needed in testing scenarios or when
|
|
824
|
+
using dynamic method registration patterns.
|
|
825
|
+
"""
|
|
826
|
+
if target_class is not None:
|
|
827
|
+
cls._class_method_cache.pop(target_class, None)
|
|
828
|
+
else:
|
|
829
|
+
cls._class_method_cache.clear()
|
|
830
|
+
|
|
831
|
+
def _should_skip_method(self, method_name: str) -> bool:
|
|
832
|
+
"""Check if method should be excluded from capability discovery.
|
|
833
|
+
|
|
834
|
+
Uses the configured exclude_prefixes set for efficient prefix matching.
|
|
835
|
+
|
|
836
|
+
Order-Dependent Matching:
|
|
837
|
+
This method uses ``any()`` with a generator expression, which
|
|
838
|
+
short-circuits on the first matching prefix. This means:
|
|
839
|
+
|
|
840
|
+
- **Performance**: Prefixes earlier in the set that match common
|
|
841
|
+
patterns will provide faster filtering. However, since frozenset
|
|
842
|
+
has no guaranteed iteration order, this is not controllable.
|
|
843
|
+
- **Correctness**: The result is deterministic regardless of order.
|
|
844
|
+
A method is skipped if ANY prefix matches, so iteration order
|
|
845
|
+
does not affect the outcome.
|
|
846
|
+
|
|
847
|
+
The default exclude prefixes are: ``_``, ``get_``, ``set_``,
|
|
848
|
+
``initialize``, ``start_``, ``stop_``.
|
|
849
|
+
|
|
850
|
+
Args:
|
|
851
|
+
method_name: Name of the method to check
|
|
852
|
+
|
|
853
|
+
Returns:
|
|
854
|
+
True if method should be skipped, False otherwise
|
|
855
|
+
"""
|
|
856
|
+
return any(
|
|
857
|
+
method_name.startswith(prefix)
|
|
858
|
+
for prefix in self._introspection_exclude_prefixes
|
|
859
|
+
)
|
|
860
|
+
|
|
861
|
+
def _is_operation_method(self, method_name: str) -> bool:
|
|
862
|
+
"""Check if method name indicates an operation.
|
|
863
|
+
|
|
864
|
+
Uses the configured operation_keywords set to identify methods
|
|
865
|
+
that represent node operations.
|
|
866
|
+
|
|
867
|
+
Order-Dependent Matching:
|
|
868
|
+
This method uses ``any()`` with a generator expression, which
|
|
869
|
+
short-circuits on the first matching keyword. This means:
|
|
870
|
+
|
|
871
|
+
- **Performance**: Keywords earlier in the set that appear more
|
|
872
|
+
frequently in method names will provide faster matching. However,
|
|
873
|
+
since frozenset has no guaranteed iteration order, this is not
|
|
874
|
+
directly controllable.
|
|
875
|
+
- **Correctness**: The result is deterministic regardless of order.
|
|
876
|
+
A method is classified as an operation if ANY keyword is found
|
|
877
|
+
in its lowercase name, so iteration order does not affect the
|
|
878
|
+
classification outcome.
|
|
879
|
+
|
|
880
|
+
The default operation keywords are: ``execute``, ``handle``,
|
|
881
|
+
``process``, ``run``, ``invoke``, ``call``.
|
|
882
|
+
|
|
883
|
+
Node-type-specific keywords are available via
|
|
884
|
+
``NODE_TYPE_OPERATION_KEYWORDS`` for specialized filtering.
|
|
885
|
+
|
|
886
|
+
Args:
|
|
887
|
+
method_name: Name of the method to check
|
|
888
|
+
|
|
889
|
+
Returns:
|
|
890
|
+
True if method appears to be an operation, False otherwise
|
|
891
|
+
"""
|
|
892
|
+
name_lower = method_name.lower()
|
|
893
|
+
return any(
|
|
894
|
+
keyword in name_lower for keyword in self._introspection_operation_keywords
|
|
895
|
+
)
|
|
896
|
+
|
|
897
|
+
def _has_fsm_state(self) -> bool:
|
|
898
|
+
"""Check if this class has FSM state management.
|
|
899
|
+
|
|
900
|
+
Looks for common FSM state attribute patterns.
|
|
901
|
+
|
|
902
|
+
Returns:
|
|
903
|
+
True if FSM state attributes are found, False otherwise
|
|
904
|
+
"""
|
|
905
|
+
fsm_indicators = {"_state", "current_state", "_current_state", "state"}
|
|
906
|
+
return any(hasattr(self, indicator) for indicator in fsm_indicators)
|
|
907
|
+
|
|
908
|
+
def _extract_state_value(self, state: object) -> str:
|
|
909
|
+
"""Extract string value from a state object.
|
|
910
|
+
|
|
911
|
+
Handles both enum states (with .value attribute) and plain values.
|
|
912
|
+
|
|
913
|
+
Args:
|
|
914
|
+
state: The state object to extract value from
|
|
915
|
+
|
|
916
|
+
Returns:
|
|
917
|
+
String representation of the state value
|
|
918
|
+
"""
|
|
919
|
+
if hasattr(state, "value"):
|
|
920
|
+
return str(state.value)
|
|
921
|
+
return str(state)
|
|
922
|
+
|
|
923
|
+
def _get_state_from_attribute(self, attr_name: str) -> str | None:
|
|
924
|
+
"""Try to get state value from a named attribute.
|
|
925
|
+
|
|
926
|
+
Args:
|
|
927
|
+
attr_name: Name of the attribute to check
|
|
928
|
+
|
|
929
|
+
Returns:
|
|
930
|
+
State value as string if found and not None, None otherwise
|
|
931
|
+
"""
|
|
932
|
+
if not hasattr(self, attr_name):
|
|
933
|
+
return None
|
|
934
|
+
state = getattr(self, attr_name)
|
|
935
|
+
if state is None:
|
|
936
|
+
return None
|
|
937
|
+
return self._extract_state_value(state)
|
|
938
|
+
|
|
939
|
+
async def _get_state_from_method(self) -> str | None:
|
|
940
|
+
"""Try to get state value from get_state method.
|
|
941
|
+
|
|
942
|
+
Handles both sync and async get_state methods.
|
|
943
|
+
|
|
944
|
+
Returns:
|
|
945
|
+
State value as string if method exists and returns non-None, None otherwise
|
|
946
|
+
"""
|
|
947
|
+
if not hasattr(self, "get_state"):
|
|
948
|
+
return None
|
|
949
|
+
|
|
950
|
+
method = self.get_state
|
|
951
|
+
if not callable(method):
|
|
952
|
+
return None
|
|
953
|
+
|
|
954
|
+
try:
|
|
955
|
+
result = method()
|
|
956
|
+
if asyncio.iscoroutine(result):
|
|
957
|
+
result = await result
|
|
958
|
+
if result is None:
|
|
959
|
+
return None
|
|
960
|
+
return self._extract_state_value(result)
|
|
961
|
+
except Exception as e:
|
|
962
|
+
logger.debug(
|
|
963
|
+
f"Failed to get state from get_state method: {e}",
|
|
964
|
+
extra={"error": str(e)},
|
|
965
|
+
)
|
|
966
|
+
return None
|
|
967
|
+
|
|
968
|
+
async def get_capabilities(self) -> ModelDiscoveredCapabilities:
|
|
969
|
+
"""Extract node capabilities via reflection.
|
|
970
|
+
|
|
971
|
+
Uses the inspect module to discover:
|
|
972
|
+
- Public methods (potential operations)
|
|
973
|
+
- FSM state attributes
|
|
974
|
+
|
|
975
|
+
Method signatures are cached at the class level for performance
|
|
976
|
+
optimization, as they don't change after class definition.
|
|
977
|
+
|
|
978
|
+
Security Note:
|
|
979
|
+
This method exposes information about the node's public interface.
|
|
980
|
+
The returned data includes method names, parameter signatures, and
|
|
981
|
+
type annotations which may reveal implementation details.
|
|
982
|
+
|
|
983
|
+
**What Gets Exposed**:
|
|
984
|
+
|
|
985
|
+
- Method names matching operation keywords (execute, handle, etc.)
|
|
986
|
+
- Full method signatures including parameter names and types
|
|
987
|
+
- Whether FSM state management is present
|
|
988
|
+
|
|
989
|
+
**Filtering Applied**:
|
|
990
|
+
|
|
991
|
+
- Private methods (``_`` prefix) are excluded
|
|
992
|
+
- Utility methods (``get_*``, ``set_*``, ``initialize*``, etc.) are
|
|
993
|
+
filtered based on ``exclude_prefixes`` configuration
|
|
994
|
+
- Only methods containing configured ``operation_keywords`` are
|
|
995
|
+
listed in the ``operations`` field
|
|
996
|
+
|
|
997
|
+
**Best Practices**:
|
|
998
|
+
|
|
999
|
+
- Review this output before production deployment
|
|
1000
|
+
- Use generic operation names (e.g., ``process_request`` instead of
|
|
1001
|
+
``decrypt_and_forward_to_payment_gateway``)
|
|
1002
|
+
- Prefix sensitive internal methods with ``_``
|
|
1003
|
+
- Configure additional ``exclude_prefixes`` if needed
|
|
1004
|
+
|
|
1005
|
+
Returns:
|
|
1006
|
+
ModelDiscoveredCapabilities containing:
|
|
1007
|
+
- operations: Tuple of public method names that may be operations
|
|
1008
|
+
- has_fsm: Boolean indicating if node has FSM state management
|
|
1009
|
+
- method_signatures: Dict of method names to signature strings
|
|
1010
|
+
|
|
1011
|
+
Raises:
|
|
1012
|
+
ProtocolConfigurationError: If initialize_introspection() was not called.
|
|
1013
|
+
|
|
1014
|
+
Example:
|
|
1015
|
+
```python
|
|
1016
|
+
capabilities = await node.get_capabilities()
|
|
1017
|
+
# ModelDiscoveredCapabilities(
|
|
1018
|
+
# operations=("execute", "query", "batch_execute"),
|
|
1019
|
+
# has_fsm=True,
|
|
1020
|
+
# method_signatures={
|
|
1021
|
+
# "execute": "(query: str) -> list[dict]",
|
|
1022
|
+
# ...
|
|
1023
|
+
# }
|
|
1024
|
+
# )
|
|
1025
|
+
|
|
1026
|
+
# Review exposed capabilities before production
|
|
1027
|
+
for op in capabilities.operations:
|
|
1028
|
+
print(f"Exposed operation: {op}")
|
|
1029
|
+
```
|
|
1030
|
+
"""
|
|
1031
|
+
self._ensure_initialized()
|
|
1032
|
+
start_time = time.perf_counter()
|
|
1033
|
+
|
|
1034
|
+
# Get cached method signatures (class-level, computed once per class)
|
|
1035
|
+
# Track discovery time separately for performance analysis
|
|
1036
|
+
discover_start = time.perf_counter()
|
|
1037
|
+
cached_signatures = self._get_class_method_signatures()
|
|
1038
|
+
discover_elapsed_ms = (time.perf_counter() - discover_start) * 1000
|
|
1039
|
+
|
|
1040
|
+
# Filter signatures and identify operations
|
|
1041
|
+
operations: list[str] = []
|
|
1042
|
+
method_signatures: dict[str, str] = {}
|
|
1043
|
+
|
|
1044
|
+
for name, sig in cached_signatures.items():
|
|
1045
|
+
# Skip utility methods based on configured prefixes
|
|
1046
|
+
if self._should_skip_method(name):
|
|
1047
|
+
continue
|
|
1048
|
+
|
|
1049
|
+
# Add method signature to filtered results
|
|
1050
|
+
method_signatures[name] = sig
|
|
1051
|
+
|
|
1052
|
+
# Add methods that look like operations
|
|
1053
|
+
if self._is_operation_method(name):
|
|
1054
|
+
operations.append(name)
|
|
1055
|
+
|
|
1056
|
+
# Build capabilities model
|
|
1057
|
+
capabilities = ModelDiscoveredCapabilities(
|
|
1058
|
+
operations=tuple(operations),
|
|
1059
|
+
has_fsm=self._has_fsm_state(),
|
|
1060
|
+
method_signatures=method_signatures,
|
|
1061
|
+
)
|
|
1062
|
+
|
|
1063
|
+
# Performance instrumentation
|
|
1064
|
+
elapsed_ms = (time.perf_counter() - start_time) * 1000
|
|
1065
|
+
if elapsed_ms > PERF_THRESHOLD_GET_CAPABILITIES_MS:
|
|
1066
|
+
logger.warning(
|
|
1067
|
+
"Capability discovery exceeded 50ms target",
|
|
1068
|
+
extra={
|
|
1069
|
+
"node_id": self._introspection_node_id,
|
|
1070
|
+
"elapsed_ms": round(elapsed_ms, 2),
|
|
1071
|
+
"discover_elapsed_ms": round(discover_elapsed_ms, 2),
|
|
1072
|
+
"method_count": len(cached_signatures),
|
|
1073
|
+
"operation_count": len(operations),
|
|
1074
|
+
"threshold_ms": PERF_THRESHOLD_GET_CAPABILITIES_MS,
|
|
1075
|
+
},
|
|
1076
|
+
)
|
|
1077
|
+
|
|
1078
|
+
return capabilities
|
|
1079
|
+
|
|
1080
|
+
async def get_endpoints(self) -> dict[str, str]:
|
|
1081
|
+
"""Discover endpoint URLs for this node.
|
|
1082
|
+
|
|
1083
|
+
Looks for common endpoint attributes and methods to build
|
|
1084
|
+
a dictionary of available endpoints.
|
|
1085
|
+
|
|
1086
|
+
Returns:
|
|
1087
|
+
Dictionary mapping endpoint names to URLs.
|
|
1088
|
+
Common keys: health, api, metrics, readiness, liveness
|
|
1089
|
+
|
|
1090
|
+
Raises:
|
|
1091
|
+
ProtocolConfigurationError: If initialize_introspection() was not called.
|
|
1092
|
+
|
|
1093
|
+
Example:
|
|
1094
|
+
```python
|
|
1095
|
+
endpoints = await node.get_endpoints()
|
|
1096
|
+
# {
|
|
1097
|
+
# "health": "http://localhost:8080/health",
|
|
1098
|
+
# "metrics": "http://localhost:8080/metrics",
|
|
1099
|
+
# }
|
|
1100
|
+
```
|
|
1101
|
+
"""
|
|
1102
|
+
self._ensure_initialized()
|
|
1103
|
+
endpoints: dict[str, str] = {}
|
|
1104
|
+
|
|
1105
|
+
# Check for endpoint attributes
|
|
1106
|
+
endpoint_attrs = [
|
|
1107
|
+
("health_url", "health"),
|
|
1108
|
+
("health_endpoint", "health"),
|
|
1109
|
+
("api_url", "api"),
|
|
1110
|
+
("api_endpoint", "api"),
|
|
1111
|
+
("metrics_url", "metrics"),
|
|
1112
|
+
("metrics_endpoint", "metrics"),
|
|
1113
|
+
("readiness_url", "readiness"),
|
|
1114
|
+
("readiness_endpoint", "readiness"),
|
|
1115
|
+
("liveness_url", "liveness"),
|
|
1116
|
+
("liveness_endpoint", "liveness"),
|
|
1117
|
+
]
|
|
1118
|
+
|
|
1119
|
+
for attr_name, endpoint_name in endpoint_attrs:
|
|
1120
|
+
if hasattr(self, attr_name):
|
|
1121
|
+
value = getattr(self, attr_name)
|
|
1122
|
+
if value and isinstance(value, str):
|
|
1123
|
+
endpoints[endpoint_name] = value
|
|
1124
|
+
|
|
1125
|
+
# Check for endpoint methods
|
|
1126
|
+
endpoint_methods = [
|
|
1127
|
+
("get_health_url", "health"),
|
|
1128
|
+
("get_api_url", "api"),
|
|
1129
|
+
("get_metrics_url", "metrics"),
|
|
1130
|
+
]
|
|
1131
|
+
|
|
1132
|
+
for method_name, endpoint_name in endpoint_methods:
|
|
1133
|
+
if hasattr(self, method_name) and endpoint_name not in endpoints:
|
|
1134
|
+
method = getattr(self, method_name)
|
|
1135
|
+
if callable(method):
|
|
1136
|
+
try:
|
|
1137
|
+
# Handle both sync and async methods
|
|
1138
|
+
result = method()
|
|
1139
|
+
if asyncio.iscoroutine(result):
|
|
1140
|
+
result = await result
|
|
1141
|
+
if result and isinstance(result, str):
|
|
1142
|
+
endpoints[endpoint_name] = result
|
|
1143
|
+
except Exception as e:
|
|
1144
|
+
logger.debug(
|
|
1145
|
+
f"Failed to get endpoint from {method_name}: {e}",
|
|
1146
|
+
extra={"method": method_name, "error": str(e)},
|
|
1147
|
+
)
|
|
1148
|
+
|
|
1149
|
+
return endpoints
|
|
1150
|
+
|
|
1151
|
+
async def get_current_state(self) -> str | None:
|
|
1152
|
+
"""Get the current FSM state if applicable.
|
|
1153
|
+
|
|
1154
|
+
Checks common FSM state attribute patterns and returns
|
|
1155
|
+
the current state value if found.
|
|
1156
|
+
|
|
1157
|
+
Returns:
|
|
1158
|
+
Current state string if FSM state is found, None otherwise.
|
|
1159
|
+
|
|
1160
|
+
Raises:
|
|
1161
|
+
ProtocolConfigurationError: If initialize_introspection() was not called.
|
|
1162
|
+
|
|
1163
|
+
Example:
|
|
1164
|
+
```python
|
|
1165
|
+
state = await node.get_current_state()
|
|
1166
|
+
# "connected" or None
|
|
1167
|
+
```
|
|
1168
|
+
"""
|
|
1169
|
+
self._ensure_initialized()
|
|
1170
|
+
|
|
1171
|
+
# Check for state attributes in order of preference
|
|
1172
|
+
state_attrs = ["_state", "current_state", "_current_state", "state"]
|
|
1173
|
+
for attr_name in state_attrs:
|
|
1174
|
+
state_value = self._get_state_from_attribute(attr_name)
|
|
1175
|
+
if state_value is not None:
|
|
1176
|
+
return state_value
|
|
1177
|
+
|
|
1178
|
+
# Fall back to get_state method
|
|
1179
|
+
return await self._get_state_from_method()
|
|
1180
|
+
|
|
1181
|
+
async def get_introspection_data(self) -> ModelNodeIntrospectionEvent:
|
|
1182
|
+
"""Get introspection data with caching support.
|
|
1183
|
+
|
|
1184
|
+
Returns cached data if available and not expired, otherwise
|
|
1185
|
+
builds fresh introspection data and caches it.
|
|
1186
|
+
|
|
1187
|
+
Performance metrics are captured for each call and stored in
|
|
1188
|
+
``_introspection_last_metrics``. Use ``get_performance_metrics()``
|
|
1189
|
+
to retrieve the most recent metrics.
|
|
1190
|
+
|
|
1191
|
+
Returns:
|
|
1192
|
+
ModelNodeIntrospectionEvent containing full introspection data.
|
|
1193
|
+
|
|
1194
|
+
Raises:
|
|
1195
|
+
ProtocolConfigurationError: If initialize_introspection() was not called.
|
|
1196
|
+
|
|
1197
|
+
Example:
|
|
1198
|
+
```python
|
|
1199
|
+
data = await node.get_introspection_data()
|
|
1200
|
+
print(f"Node {data.node_id} has capabilities: {data.discovered_capabilities}")
|
|
1201
|
+
```
|
|
1202
|
+
"""
|
|
1203
|
+
self._ensure_initialized()
|
|
1204
|
+
total_start = time.perf_counter()
|
|
1205
|
+
current_time = time.time()
|
|
1206
|
+
|
|
1207
|
+
# Collect metrics values in local variables (model is frozen)
|
|
1208
|
+
get_capabilities_ms = 0.0
|
|
1209
|
+
get_endpoints_ms = 0.0
|
|
1210
|
+
get_current_state_ms = 0.0
|
|
1211
|
+
method_count = 0
|
|
1212
|
+
slow_operations: list[str] = []
|
|
1213
|
+
|
|
1214
|
+
# Check cache validity
|
|
1215
|
+
if (
|
|
1216
|
+
self._introspection_cache is not None
|
|
1217
|
+
and self._introspection_cached_at is not None
|
|
1218
|
+
and current_time - self._introspection_cached_at
|
|
1219
|
+
< self._introspection_cache_ttl
|
|
1220
|
+
):
|
|
1221
|
+
# Return cached data (timestamp reflects when cache was populated, not current time)
|
|
1222
|
+
cached_event = ModelNodeIntrospectionEvent(**self._introspection_cache)
|
|
1223
|
+
|
|
1224
|
+
# Record cache hit metrics
|
|
1225
|
+
elapsed_ms = (time.perf_counter() - total_start) * 1000
|
|
1226
|
+
threshold_exceeded = elapsed_ms > PERF_THRESHOLD_CACHE_HIT_MS
|
|
1227
|
+
if threshold_exceeded:
|
|
1228
|
+
slow_operations.append("cache_hit")
|
|
1229
|
+
|
|
1230
|
+
# Create frozen metrics object with final values
|
|
1231
|
+
metrics = ModelIntrospectionPerformanceMetrics(
|
|
1232
|
+
total_introspection_ms=elapsed_ms,
|
|
1233
|
+
cache_hit=True,
|
|
1234
|
+
threshold_exceeded=threshold_exceeded,
|
|
1235
|
+
slow_operations=slow_operations,
|
|
1236
|
+
)
|
|
1237
|
+
self._introspection_last_metrics = metrics
|
|
1238
|
+
return cached_event
|
|
1239
|
+
|
|
1240
|
+
# Build fresh introspection data with timing for each component
|
|
1241
|
+
# First, measure the class method signature discovery time separately.
|
|
1242
|
+
# This is cached at the class level, so subsequent calls are instant.
|
|
1243
|
+
discover_start = time.perf_counter()
|
|
1244
|
+
self._get_class_method_signatures() # Force cache population if not already done
|
|
1245
|
+
discover_capabilities_ms = (time.perf_counter() - discover_start) * 1000
|
|
1246
|
+
|
|
1247
|
+
cap_start = time.perf_counter()
|
|
1248
|
+
discovered_capabilities = await self.get_capabilities()
|
|
1249
|
+
get_capabilities_ms = (time.perf_counter() - cap_start) * 1000
|
|
1250
|
+
|
|
1251
|
+
# Extract method count from capabilities (now a Pydantic model)
|
|
1252
|
+
method_count = len(discovered_capabilities.method_signatures)
|
|
1253
|
+
|
|
1254
|
+
endpoints_start = time.perf_counter()
|
|
1255
|
+
endpoints = await self.get_endpoints()
|
|
1256
|
+
get_endpoints_ms = (time.perf_counter() - endpoints_start) * 1000
|
|
1257
|
+
|
|
1258
|
+
state_start = time.perf_counter()
|
|
1259
|
+
current_state = await self.get_current_state()
|
|
1260
|
+
get_current_state_ms = (time.perf_counter() - state_start) * 1000
|
|
1261
|
+
|
|
1262
|
+
# Get node_id and node_type with fallback logging
|
|
1263
|
+
# The nil UUID fallback indicates a potential initialization issue
|
|
1264
|
+
node_id_uuid = self._introspection_node_id
|
|
1265
|
+
if node_id_uuid is None:
|
|
1266
|
+
logger.warning(
|
|
1267
|
+
"Node ID not initialized, using nil UUID - "
|
|
1268
|
+
"ensure initialize_introspection() was called correctly",
|
|
1269
|
+
extra={"operation": "get_introspection_data"},
|
|
1270
|
+
)
|
|
1271
|
+
# Use nil UUID (all zeros) as sentinel for uninitialized node
|
|
1272
|
+
node_id_uuid = UUID("00000000-0000-0000-0000-000000000000")
|
|
1273
|
+
|
|
1274
|
+
node_type = self._introspection_node_type
|
|
1275
|
+
if node_type is None:
|
|
1276
|
+
# Design Note: EnumNodeKind.EFFECT is the intended sentinel/default value
|
|
1277
|
+
# when node_type is uninitialized. EFFECT is chosen because:
|
|
1278
|
+
# 1. It's the most common node type in the ONEX ecosystem
|
|
1279
|
+
# 2. Effect nodes have the broadest capability expectations
|
|
1280
|
+
# 3. Fallback to EFFECT is safer than ORCHESTRATOR (avoids privilege escalation)
|
|
1281
|
+
logger.warning(
|
|
1282
|
+
"Node type not initialized, using EFFECT as fallback - "
|
|
1283
|
+
"ensure initialize_introspection() was called correctly",
|
|
1284
|
+
extra={
|
|
1285
|
+
"node_id": str(node_id_uuid),
|
|
1286
|
+
"operation": "get_introspection_data",
|
|
1287
|
+
},
|
|
1288
|
+
)
|
|
1289
|
+
node_type = EnumNodeKind.EFFECT
|
|
1290
|
+
|
|
1291
|
+
# Extract operations count from discovered capabilities
|
|
1292
|
+
operations_count = len(discovered_capabilities.operations)
|
|
1293
|
+
|
|
1294
|
+
# Finalize metrics calculations
|
|
1295
|
+
total_introspection_ms = (time.perf_counter() - total_start) * 1000
|
|
1296
|
+
threshold_exceeded = False
|
|
1297
|
+
|
|
1298
|
+
# Check thresholds and identify slow operations
|
|
1299
|
+
if get_capabilities_ms > PERF_THRESHOLD_GET_CAPABILITIES_MS:
|
|
1300
|
+
threshold_exceeded = True
|
|
1301
|
+
slow_operations.append("get_capabilities")
|
|
1302
|
+
|
|
1303
|
+
if total_introspection_ms > PERF_THRESHOLD_GET_INTROSPECTION_DATA_MS:
|
|
1304
|
+
threshold_exceeded = True
|
|
1305
|
+
if "total_introspection" not in slow_operations:
|
|
1306
|
+
slow_operations.append("total_introspection")
|
|
1307
|
+
|
|
1308
|
+
# Create frozen metrics object with final values
|
|
1309
|
+
metrics = ModelIntrospectionPerformanceMetrics(
|
|
1310
|
+
get_capabilities_ms=get_capabilities_ms,
|
|
1311
|
+
discover_capabilities_ms=discover_capabilities_ms,
|
|
1312
|
+
get_endpoints_ms=get_endpoints_ms,
|
|
1313
|
+
get_current_state_ms=get_current_state_ms,
|
|
1314
|
+
total_introspection_ms=total_introspection_ms,
|
|
1315
|
+
cache_hit=False,
|
|
1316
|
+
method_count=method_count,
|
|
1317
|
+
threshold_exceeded=threshold_exceeded,
|
|
1318
|
+
slow_operations=slow_operations,
|
|
1319
|
+
)
|
|
1320
|
+
|
|
1321
|
+
# Store metrics for later retrieval
|
|
1322
|
+
self._introspection_last_metrics = metrics
|
|
1323
|
+
|
|
1324
|
+
# Parse version string into ModelSemVer
|
|
1325
|
+
try:
|
|
1326
|
+
version_parts = self._introspection_version.split(".")
|
|
1327
|
+
node_version = ModelSemVer(
|
|
1328
|
+
major=int(version_parts[0]) if len(version_parts) > 0 else 1,
|
|
1329
|
+
minor=int(version_parts[1]) if len(version_parts) > 1 else 0,
|
|
1330
|
+
patch=int(version_parts[2].split("-")[0])
|
|
1331
|
+
if len(version_parts) > 2
|
|
1332
|
+
else 0,
|
|
1333
|
+
)
|
|
1334
|
+
except (ValueError, IndexError):
|
|
1335
|
+
# Fallback to 1.0.0 if version parsing fails
|
|
1336
|
+
node_version = ModelSemVer(major=1, minor=0, patch=0)
|
|
1337
|
+
|
|
1338
|
+
# Create event with performance metrics (metrics is already Pydantic model)
|
|
1339
|
+
event = ModelNodeIntrospectionEvent(
|
|
1340
|
+
node_id=node_id_uuid,
|
|
1341
|
+
node_type=node_type,
|
|
1342
|
+
node_version=node_version,
|
|
1343
|
+
declared_capabilities=ModelNodeCapabilities(),
|
|
1344
|
+
discovered_capabilities=discovered_capabilities,
|
|
1345
|
+
endpoints=endpoints,
|
|
1346
|
+
current_state=current_state,
|
|
1347
|
+
reason=EnumIntrospectionReason.HEARTBEAT, # cache_refresh maps to heartbeat
|
|
1348
|
+
correlation_id=uuid4(),
|
|
1349
|
+
timestamp=datetime.now(UTC),
|
|
1350
|
+
performance_metrics=metrics,
|
|
1351
|
+
)
|
|
1352
|
+
|
|
1353
|
+
# Update cache - cast the model_dump output to our typed dict since we know
|
|
1354
|
+
# the structure matches (model_dump returns dict[str, Any] by default)
|
|
1355
|
+
self._introspection_cache = cast(
|
|
1356
|
+
IntrospectionCacheDict, event.model_dump(mode="json")
|
|
1357
|
+
)
|
|
1358
|
+
self._introspection_cached_at = current_time
|
|
1359
|
+
|
|
1360
|
+
# Log if any threshold was exceeded
|
|
1361
|
+
if metrics.threshold_exceeded:
|
|
1362
|
+
logger.warning(
|
|
1363
|
+
"Introspection exceeded performance threshold",
|
|
1364
|
+
extra={
|
|
1365
|
+
"node_id": self._introspection_node_id,
|
|
1366
|
+
"total_ms": round(metrics.total_introspection_ms, 2),
|
|
1367
|
+
"get_capabilities_ms": round(metrics.get_capabilities_ms, 2),
|
|
1368
|
+
"get_endpoints_ms": round(metrics.get_endpoints_ms, 2),
|
|
1369
|
+
"get_current_state_ms": round(metrics.get_current_state_ms, 2),
|
|
1370
|
+
"method_count": metrics.method_count,
|
|
1371
|
+
"slow_operations": metrics.slow_operations,
|
|
1372
|
+
"threshold_ms": PERF_THRESHOLD_GET_INTROSPECTION_DATA_MS,
|
|
1373
|
+
},
|
|
1374
|
+
)
|
|
1375
|
+
|
|
1376
|
+
logger.debug(
|
|
1377
|
+
f"Introspection data refreshed for {self._introspection_node_id}",
|
|
1378
|
+
extra={
|
|
1379
|
+
"node_id": self._introspection_node_id,
|
|
1380
|
+
"capabilities_count": operations_count,
|
|
1381
|
+
"endpoints_count": len(endpoints),
|
|
1382
|
+
"total_ms": round(metrics.total_introspection_ms, 2),
|
|
1383
|
+
},
|
|
1384
|
+
)
|
|
1385
|
+
|
|
1386
|
+
return event
|
|
1387
|
+
|
|
1388
|
+
async def publish_introspection(
|
|
1389
|
+
self,
|
|
1390
|
+
reason: str | EnumIntrospectionReason = EnumIntrospectionReason.STARTUP,
|
|
1391
|
+
correlation_id: UUID | None = None,
|
|
1392
|
+
) -> bool:
|
|
1393
|
+
"""Publish introspection event to the event bus.
|
|
1394
|
+
|
|
1395
|
+
Gracefully degrades if event bus is unavailable - logs warning
|
|
1396
|
+
and returns False instead of raising an exception.
|
|
1397
|
+
|
|
1398
|
+
This method uses ``track_operation()`` to track active operations
|
|
1399
|
+
for heartbeat reporting, demonstrating the recommended pattern
|
|
1400
|
+
for integrating operation tracking into node operations.
|
|
1401
|
+
|
|
1402
|
+
Args:
|
|
1403
|
+
reason: Reason for the introspection event. Can be an
|
|
1404
|
+
EnumIntrospectionReason or a string matching enum values
|
|
1405
|
+
(startup, shutdown, request, heartbeat, health_change,
|
|
1406
|
+
capability_change). Invalid strings default to HEARTBEAT.
|
|
1407
|
+
correlation_id: Optional correlation ID for tracing
|
|
1408
|
+
|
|
1409
|
+
Returns:
|
|
1410
|
+
True if published successfully, False otherwise
|
|
1411
|
+
|
|
1412
|
+
Raises:
|
|
1413
|
+
ProtocolConfigurationError: If initialize_introspection() was not called.
|
|
1414
|
+
|
|
1415
|
+
Example:
|
|
1416
|
+
```python
|
|
1417
|
+
# On startup (using enum - preferred)
|
|
1418
|
+
success = await node.publish_introspection(
|
|
1419
|
+
reason=EnumIntrospectionReason.STARTUP
|
|
1420
|
+
)
|
|
1421
|
+
|
|
1422
|
+
# On shutdown (using string - backwards compatible)
|
|
1423
|
+
success = await node.publish_introspection(reason="shutdown")
|
|
1424
|
+
```
|
|
1425
|
+
"""
|
|
1426
|
+
self._ensure_initialized()
|
|
1427
|
+
|
|
1428
|
+
# Convert reason to enum - check Enum first since EnumIntrospectionReason
|
|
1429
|
+
# inherits from str, so isinstance(..., str) would match both types.
|
|
1430
|
+
# Normalize string inputs with strip().lower() for robust matching.
|
|
1431
|
+
reason_enum: EnumIntrospectionReason
|
|
1432
|
+
if isinstance(reason, EnumIntrospectionReason):
|
|
1433
|
+
reason_enum = reason
|
|
1434
|
+
elif isinstance(reason, str):
|
|
1435
|
+
try:
|
|
1436
|
+
# Normalize: strip whitespace, lowercase for case-insensitive match
|
|
1437
|
+
reason_enum = EnumIntrospectionReason(reason.strip().lower())
|
|
1438
|
+
except ValueError:
|
|
1439
|
+
logger.warning(
|
|
1440
|
+
f"Unknown introspection reason '{reason}', defaulting to HEARTBEAT",
|
|
1441
|
+
extra={
|
|
1442
|
+
"node_id": self._introspection_node_id,
|
|
1443
|
+
"provided_reason": reason,
|
|
1444
|
+
},
|
|
1445
|
+
)
|
|
1446
|
+
reason_enum = EnumIntrospectionReason.HEARTBEAT
|
|
1447
|
+
else:
|
|
1448
|
+
context = ModelInfraErrorContext.with_correlation(
|
|
1449
|
+
transport_type=EnumInfraTransportType.RUNTIME,
|
|
1450
|
+
operation="publish_introspection",
|
|
1451
|
+
target_name=str(self._introspection_node_id),
|
|
1452
|
+
)
|
|
1453
|
+
raise ProtocolConfigurationError(
|
|
1454
|
+
f"reason must be str or EnumIntrospectionReason, got {type(reason).__name__}",
|
|
1455
|
+
context=context,
|
|
1456
|
+
parameter="reason",
|
|
1457
|
+
actual_type=type(reason).__name__,
|
|
1458
|
+
)
|
|
1459
|
+
|
|
1460
|
+
if self._introspection_event_bus is None:
|
|
1461
|
+
logger.warning(
|
|
1462
|
+
f"Cannot publish introspection - no event bus configured for {self._introspection_node_id}",
|
|
1463
|
+
extra={
|
|
1464
|
+
"node_id": self._introspection_node_id,
|
|
1465
|
+
"reason": reason_enum.value,
|
|
1466
|
+
},
|
|
1467
|
+
)
|
|
1468
|
+
return False
|
|
1469
|
+
|
|
1470
|
+
# Track this operation for heartbeat reporting
|
|
1471
|
+
async with self.track_operation("publish_introspection"):
|
|
1472
|
+
try:
|
|
1473
|
+
# Get introspection data
|
|
1474
|
+
event = await self.get_introspection_data()
|
|
1475
|
+
|
|
1476
|
+
# Create publish event with updated reason and correlation_id
|
|
1477
|
+
# Use model_copy for clean field updates (Pydantic v2)
|
|
1478
|
+
final_correlation_id = correlation_id or uuid4()
|
|
1479
|
+
publish_event = event.model_copy(
|
|
1480
|
+
update={
|
|
1481
|
+
"reason": reason_enum,
|
|
1482
|
+
"correlation_id": final_correlation_id,
|
|
1483
|
+
}
|
|
1484
|
+
)
|
|
1485
|
+
|
|
1486
|
+
# Publish to event bus using configured topic
|
|
1487
|
+
# Type narrowing: we've already checked _introspection_event_bus is not None above
|
|
1488
|
+
event_bus = self._introspection_event_bus
|
|
1489
|
+
assert event_bus is not None # Redundant but helps mypy
|
|
1490
|
+
topic = self._introspection_topic
|
|
1491
|
+
if hasattr(event_bus, "publish_envelope"):
|
|
1492
|
+
await event_bus.publish_envelope(
|
|
1493
|
+
envelope=publish_event,
|
|
1494
|
+
topic=topic,
|
|
1495
|
+
)
|
|
1496
|
+
else:
|
|
1497
|
+
# Fallback to publish method with raw bytes
|
|
1498
|
+
event_data = publish_event.model_dump(mode="json")
|
|
1499
|
+
value = json.dumps(event_data).encode("utf-8")
|
|
1500
|
+
await event_bus.publish(
|
|
1501
|
+
topic=topic,
|
|
1502
|
+
key=str(self._introspection_node_id).encode("utf-8")
|
|
1503
|
+
if self._introspection_node_id is not None
|
|
1504
|
+
else None,
|
|
1505
|
+
value=value,
|
|
1506
|
+
)
|
|
1507
|
+
|
|
1508
|
+
logger.info(
|
|
1509
|
+
f"Published introspection event for {self._introspection_node_id}",
|
|
1510
|
+
extra={
|
|
1511
|
+
"node_id": self._introspection_node_id,
|
|
1512
|
+
"reason": reason_enum.value,
|
|
1513
|
+
"correlation_id": str(final_correlation_id),
|
|
1514
|
+
},
|
|
1515
|
+
)
|
|
1516
|
+
return True
|
|
1517
|
+
|
|
1518
|
+
except Exception as e:
|
|
1519
|
+
# Use error() with exc_info=True instead of exception() to include
|
|
1520
|
+
# structured error_type and error_message fields for log aggregation
|
|
1521
|
+
logger.error(
|
|
1522
|
+
f"Failed to publish introspection for {self._introspection_node_id}",
|
|
1523
|
+
extra={
|
|
1524
|
+
"node_id": self._introspection_node_id,
|
|
1525
|
+
"reason": reason_enum.value,
|
|
1526
|
+
"error_type": type(e).__name__,
|
|
1527
|
+
"error_message": str(e),
|
|
1528
|
+
},
|
|
1529
|
+
exc_info=True,
|
|
1530
|
+
)
|
|
1531
|
+
return False
|
|
1532
|
+
|
|
1533
|
+
async def _publish_heartbeat(self) -> bool:
|
|
1534
|
+
"""Publish heartbeat event to the event bus.
|
|
1535
|
+
|
|
1536
|
+
Internal method for heartbeat broadcasting. Calculates uptime
|
|
1537
|
+
and publishes heartbeat event.
|
|
1538
|
+
|
|
1539
|
+
Note:
|
|
1540
|
+
This method intentionally does NOT use ``track_operation()``
|
|
1541
|
+
because:
|
|
1542
|
+
1. It's an internal background task, not a business operation
|
|
1543
|
+
2. Tracking it would cause self-referential counting (the
|
|
1544
|
+
heartbeat would count itself as an active operation)
|
|
1545
|
+
3. The purpose of operation tracking is to report business
|
|
1546
|
+
load, not infrastructure overhead
|
|
1547
|
+
|
|
1548
|
+
For business operations, use ``track_operation()`` as
|
|
1549
|
+
demonstrated in ``publish_introspection()``.
|
|
1550
|
+
|
|
1551
|
+
Returns:
|
|
1552
|
+
True if published successfully, False otherwise
|
|
1553
|
+
"""
|
|
1554
|
+
if self._introspection_event_bus is None:
|
|
1555
|
+
return False
|
|
1556
|
+
|
|
1557
|
+
try:
|
|
1558
|
+
# Calculate uptime
|
|
1559
|
+
uptime_seconds = 0.0
|
|
1560
|
+
if self._introspection_start_time is not None:
|
|
1561
|
+
uptime_seconds = time.time() - self._introspection_start_time
|
|
1562
|
+
|
|
1563
|
+
# Get node_id and node_type with fallback logging
|
|
1564
|
+
# The nil UUID fallback indicates a potential initialization issue
|
|
1565
|
+
node_id = self._introspection_node_id
|
|
1566
|
+
if node_id is None:
|
|
1567
|
+
logger.warning(
|
|
1568
|
+
"Node ID not initialized, using nil UUID in heartbeat - "
|
|
1569
|
+
"ensure initialize_introspection() was called correctly",
|
|
1570
|
+
extra={"operation": "_publish_heartbeat"},
|
|
1571
|
+
)
|
|
1572
|
+
# Use nil UUID (all zeros) as sentinel for uninitialized node
|
|
1573
|
+
node_id = UUID("00000000-0000-0000-0000-000000000000")
|
|
1574
|
+
|
|
1575
|
+
node_type = self._introspection_node_type
|
|
1576
|
+
if node_type is None:
|
|
1577
|
+
# Design Note: EnumNodeKind.EFFECT is the intended sentinel/default value.
|
|
1578
|
+
# See get_introspection_data() for detailed rationale.
|
|
1579
|
+
logger.warning(
|
|
1580
|
+
"Node type not initialized, using EFFECT in heartbeat - "
|
|
1581
|
+
"ensure initialize_introspection() was called correctly",
|
|
1582
|
+
extra={"node_id": str(node_id), "operation": "_publish_heartbeat"},
|
|
1583
|
+
)
|
|
1584
|
+
node_type = EnumNodeKind.EFFECT
|
|
1585
|
+
|
|
1586
|
+
# Get current active operations count (coroutine-safe)
|
|
1587
|
+
async with self._operations_lock:
|
|
1588
|
+
active_ops_count = self._active_operations
|
|
1589
|
+
|
|
1590
|
+
# Create heartbeat event
|
|
1591
|
+
now = datetime.now(UTC)
|
|
1592
|
+
heartbeat = ModelNodeHeartbeatEvent(
|
|
1593
|
+
node_id=node_id,
|
|
1594
|
+
node_type=node_type,
|
|
1595
|
+
uptime_seconds=uptime_seconds,
|
|
1596
|
+
active_operations_count=active_ops_count,
|
|
1597
|
+
correlation_id=uuid4(),
|
|
1598
|
+
timestamp=now, # Required: time injection pattern
|
|
1599
|
+
)
|
|
1600
|
+
|
|
1601
|
+
# Publish to event bus using configured topic
|
|
1602
|
+
# Type narrowing: we've already checked _introspection_event_bus is not None above
|
|
1603
|
+
event_bus = self._introspection_event_bus
|
|
1604
|
+
assert event_bus is not None # Redundant but helps mypy
|
|
1605
|
+
topic = self._heartbeat_topic
|
|
1606
|
+
if hasattr(event_bus, "publish_envelope"):
|
|
1607
|
+
await event_bus.publish_envelope(
|
|
1608
|
+
envelope=heartbeat,
|
|
1609
|
+
topic=topic,
|
|
1610
|
+
)
|
|
1611
|
+
else:
|
|
1612
|
+
value = json.dumps(heartbeat.model_dump(mode="json")).encode("utf-8")
|
|
1613
|
+
await event_bus.publish(
|
|
1614
|
+
topic=topic,
|
|
1615
|
+
key=str(self._introspection_node_id).encode("utf-8")
|
|
1616
|
+
if self._introspection_node_id is not None
|
|
1617
|
+
else None,
|
|
1618
|
+
value=value,
|
|
1619
|
+
)
|
|
1620
|
+
|
|
1621
|
+
logger.debug(
|
|
1622
|
+
f"Published heartbeat for {self._introspection_node_id}",
|
|
1623
|
+
extra={
|
|
1624
|
+
"node_id": self._introspection_node_id,
|
|
1625
|
+
"uptime_seconds": uptime_seconds,
|
|
1626
|
+
"active_operations": active_ops_count,
|
|
1627
|
+
"topic": topic,
|
|
1628
|
+
},
|
|
1629
|
+
)
|
|
1630
|
+
return True
|
|
1631
|
+
|
|
1632
|
+
except Exception as e:
|
|
1633
|
+
# Use error() with exc_info=True instead of exception() to include
|
|
1634
|
+
# structured error_type and error_message fields for log aggregation
|
|
1635
|
+
logger.error(
|
|
1636
|
+
f"Failed to publish heartbeat for {self._introspection_node_id}",
|
|
1637
|
+
extra={
|
|
1638
|
+
"node_id": self._introspection_node_id,
|
|
1639
|
+
"error_type": type(e).__name__,
|
|
1640
|
+
"error_message": str(e),
|
|
1641
|
+
},
|
|
1642
|
+
exc_info=True,
|
|
1643
|
+
)
|
|
1644
|
+
return False
|
|
1645
|
+
|
|
1646
|
+
async def _heartbeat_loop(self, interval: float) -> None:
|
|
1647
|
+
"""Background loop for periodic heartbeat publishing.
|
|
1648
|
+
|
|
1649
|
+
Runs until stop event is set, publishing heartbeats at the
|
|
1650
|
+
specified interval.
|
|
1651
|
+
|
|
1652
|
+
Args:
|
|
1653
|
+
interval: Time between heartbeats in seconds
|
|
1654
|
+
"""
|
|
1655
|
+
# Ensure stop event is initialized
|
|
1656
|
+
if self._introspection_stop_event is None:
|
|
1657
|
+
self._introspection_stop_event = asyncio.Event()
|
|
1658
|
+
|
|
1659
|
+
logger.info(
|
|
1660
|
+
f"Starting heartbeat loop for {self._introspection_node_id}",
|
|
1661
|
+
extra={
|
|
1662
|
+
"node_id": self._introspection_node_id,
|
|
1663
|
+
"interval_seconds": interval,
|
|
1664
|
+
},
|
|
1665
|
+
)
|
|
1666
|
+
|
|
1667
|
+
while not self._introspection_stop_event.is_set():
|
|
1668
|
+
try:
|
|
1669
|
+
await self._publish_heartbeat()
|
|
1670
|
+
except asyncio.CancelledError:
|
|
1671
|
+
logger.debug(
|
|
1672
|
+
f"Heartbeat loop cancelled for {self._introspection_node_id}",
|
|
1673
|
+
extra={"node_id": self._introspection_node_id},
|
|
1674
|
+
)
|
|
1675
|
+
break
|
|
1676
|
+
except Exception as e:
|
|
1677
|
+
# Use error() with exc_info=True instead of exception() to include
|
|
1678
|
+
# structured error_type and error_message fields for log aggregation
|
|
1679
|
+
logger.error(
|
|
1680
|
+
f"Error in heartbeat loop for {self._introspection_node_id}",
|
|
1681
|
+
extra={
|
|
1682
|
+
"node_id": self._introspection_node_id,
|
|
1683
|
+
"error_type": type(e).__name__,
|
|
1684
|
+
"error_message": str(e),
|
|
1685
|
+
},
|
|
1686
|
+
exc_info=True,
|
|
1687
|
+
)
|
|
1688
|
+
|
|
1689
|
+
# Wait for next interval or stop event
|
|
1690
|
+
try:
|
|
1691
|
+
await asyncio.wait_for(
|
|
1692
|
+
self._introspection_stop_event.wait(),
|
|
1693
|
+
timeout=interval,
|
|
1694
|
+
)
|
|
1695
|
+
# Stop event was set
|
|
1696
|
+
break
|
|
1697
|
+
except TimeoutError:
|
|
1698
|
+
# Normal timeout, continue loop
|
|
1699
|
+
pass
|
|
1700
|
+
|
|
1701
|
+
logger.info(
|
|
1702
|
+
f"Heartbeat loop stopped for {self._introspection_node_id}",
|
|
1703
|
+
extra={"node_id": self._introspection_node_id},
|
|
1704
|
+
)
|
|
1705
|
+
|
|
1706
|
+
def _parse_correlation_id(self, raw_value: str | None) -> UUID | None:
|
|
1707
|
+
"""Parse correlation ID from request data with graceful fallback.
|
|
1708
|
+
|
|
1709
|
+
Args:
|
|
1710
|
+
raw_value: Raw correlation_id value from request JSON
|
|
1711
|
+
|
|
1712
|
+
Returns:
|
|
1713
|
+
Parsed UUID or None if parsing fails or value is empty
|
|
1714
|
+
"""
|
|
1715
|
+
if not raw_value:
|
|
1716
|
+
return None
|
|
1717
|
+
|
|
1718
|
+
try:
|
|
1719
|
+
# UUID() raises ValueError for malformed strings,
|
|
1720
|
+
# TypeError for non-string inputs (e.g., int, list).
|
|
1721
|
+
# Convert to string first for safer handling of unexpected types.
|
|
1722
|
+
return UUID(str(raw_value))
|
|
1723
|
+
except (ValueError, TypeError) as e:
|
|
1724
|
+
# Log warning with structured fields for monitoring.
|
|
1725
|
+
# Truncate received value preview to avoid log bloat
|
|
1726
|
+
# from potentially malicious oversized input.
|
|
1727
|
+
logger.warning(
|
|
1728
|
+
"Invalid correlation_id format in introspection "
|
|
1729
|
+
"request, generating new correlation_id",
|
|
1730
|
+
extra={
|
|
1731
|
+
"node_id": self._introspection_node_id,
|
|
1732
|
+
"error_type": type(e).__name__,
|
|
1733
|
+
"error_message": str(e),
|
|
1734
|
+
"received_value_type": type(raw_value).__name__,
|
|
1735
|
+
"received_value_preview": str(raw_value)[:50],
|
|
1736
|
+
},
|
|
1737
|
+
)
|
|
1738
|
+
return None
|
|
1739
|
+
|
|
1740
|
+
@staticmethod
|
|
1741
|
+
def _should_log_failure(consecutive_failures: int, threshold: int) -> bool:
|
|
1742
|
+
"""Determine if failure should be logged based on rate limiting.
|
|
1743
|
+
|
|
1744
|
+
Logs first failure and every Nth consecutive failure to prevent log spam.
|
|
1745
|
+
|
|
1746
|
+
Args:
|
|
1747
|
+
consecutive_failures: Current consecutive failure count
|
|
1748
|
+
threshold: Log every Nth failure
|
|
1749
|
+
|
|
1750
|
+
Returns:
|
|
1751
|
+
True if this failure should be logged at error level
|
|
1752
|
+
"""
|
|
1753
|
+
return consecutive_failures == 1 or consecutive_failures % threshold == 0
|
|
1754
|
+
|
|
1755
|
+
async def _cleanup_registry_subscription(self) -> None:
|
|
1756
|
+
"""Clean up the current registry subscription."""
|
|
1757
|
+
if self._registry_unsubscribe is not None:
|
|
1758
|
+
try:
|
|
1759
|
+
result = self._registry_unsubscribe()
|
|
1760
|
+
if asyncio.iscoroutine(result):
|
|
1761
|
+
await result
|
|
1762
|
+
except Exception as cleanup_error:
|
|
1763
|
+
logger.debug(
|
|
1764
|
+
"Error unsubscribing registry listener for "
|
|
1765
|
+
f"{self._introspection_node_id}",
|
|
1766
|
+
extra={
|
|
1767
|
+
"node_id": self._introspection_node_id,
|
|
1768
|
+
"error_type": type(cleanup_error).__name__,
|
|
1769
|
+
"error_message": str(cleanup_error),
|
|
1770
|
+
},
|
|
1771
|
+
)
|
|
1772
|
+
self._registry_unsubscribe = None
|
|
1773
|
+
|
|
1774
|
+
async def _handle_introspection_request(self, message: ModelEventMessage) -> None:
|
|
1775
|
+
"""Handle incoming introspection request.
|
|
1776
|
+
|
|
1777
|
+
Includes error recovery with rate-limited logging to prevent
|
|
1778
|
+
log spam during sustained failures. Continues processing on
|
|
1779
|
+
non-fatal errors to maintain graceful degradation.
|
|
1780
|
+
|
|
1781
|
+
Args:
|
|
1782
|
+
message: The incoming event message
|
|
1783
|
+
"""
|
|
1784
|
+
try:
|
|
1785
|
+
await self._process_introspection_request(message)
|
|
1786
|
+
# Reset failure counter on success
|
|
1787
|
+
self._registry_callback_consecutive_failures = 0
|
|
1788
|
+
except Exception as e:
|
|
1789
|
+
self._handle_request_error(e)
|
|
1790
|
+
|
|
1791
|
+
async def _process_introspection_request(self, message: ModelEventMessage) -> None:
|
|
1792
|
+
"""Process the introspection request message.
|
|
1793
|
+
|
|
1794
|
+
Args:
|
|
1795
|
+
message: The incoming event message
|
|
1796
|
+
|
|
1797
|
+
Raises:
|
|
1798
|
+
Exception: If processing fails (will be caught by caller)
|
|
1799
|
+
"""
|
|
1800
|
+
# Early exit if message has no parseable value
|
|
1801
|
+
if not hasattr(message, "value") or not message.value:
|
|
1802
|
+
await self.publish_introspection(
|
|
1803
|
+
reason="request",
|
|
1804
|
+
correlation_id=uuid4(),
|
|
1805
|
+
)
|
|
1806
|
+
return
|
|
1807
|
+
|
|
1808
|
+
# Parse request data
|
|
1809
|
+
request_data = json.loads(message.value.decode("utf-8"))
|
|
1810
|
+
|
|
1811
|
+
# Check if request targets a specific node (early exit if not us)
|
|
1812
|
+
# Note: Compare as strings since target_node_id from JSON is a string
|
|
1813
|
+
# while _introspection_node_id is a UUID object
|
|
1814
|
+
target_node_id = request_data.get("target_node_id")
|
|
1815
|
+
if target_node_id and str(target_node_id) != str(self._introspection_node_id):
|
|
1816
|
+
return
|
|
1817
|
+
|
|
1818
|
+
# Parse correlation ID with graceful fallback
|
|
1819
|
+
correlation_id = self._parse_correlation_id(request_data.get("correlation_id"))
|
|
1820
|
+
|
|
1821
|
+
# Respond with introspection data
|
|
1822
|
+
await self.publish_introspection(
|
|
1823
|
+
reason="request",
|
|
1824
|
+
correlation_id=correlation_id,
|
|
1825
|
+
)
|
|
1826
|
+
|
|
1827
|
+
def _handle_request_error(self, error: Exception) -> None:
|
|
1828
|
+
"""Handle error during introspection request processing.
|
|
1829
|
+
|
|
1830
|
+
Tracks consecutive failures and rate-limits error logging.
|
|
1831
|
+
|
|
1832
|
+
Args:
|
|
1833
|
+
error: The exception that occurred
|
|
1834
|
+
"""
|
|
1835
|
+
# Track consecutive failures for rate-limited logging
|
|
1836
|
+
self._registry_callback_consecutive_failures += 1
|
|
1837
|
+
self._registry_callback_last_failure_time = time.time()
|
|
1838
|
+
|
|
1839
|
+
# Rate-limit error logging to prevent log spam during sustained failures
|
|
1840
|
+
if self._should_log_failure(
|
|
1841
|
+
self._registry_callback_consecutive_failures,
|
|
1842
|
+
self._registry_callback_failure_log_threshold,
|
|
1843
|
+
):
|
|
1844
|
+
logger.error(
|
|
1845
|
+
f"Error handling introspection request for {self._introspection_node_id}",
|
|
1846
|
+
extra={
|
|
1847
|
+
"node_id": self._introspection_node_id,
|
|
1848
|
+
"error_type": type(error).__name__,
|
|
1849
|
+
"error_message": str(error),
|
|
1850
|
+
"consecutive_failures": self._registry_callback_consecutive_failures,
|
|
1851
|
+
"log_rate_limited": self._registry_callback_consecutive_failures
|
|
1852
|
+
> 1,
|
|
1853
|
+
},
|
|
1854
|
+
exc_info=True,
|
|
1855
|
+
)
|
|
1856
|
+
else:
|
|
1857
|
+
# Log at debug level for rate-limited failures
|
|
1858
|
+
logger.debug(
|
|
1859
|
+
f"Suppressed error log for introspection request "
|
|
1860
|
+
f"(failure {self._registry_callback_consecutive_failures})",
|
|
1861
|
+
extra={
|
|
1862
|
+
"node_id": self._introspection_node_id,
|
|
1863
|
+
"error_type": type(error).__name__,
|
|
1864
|
+
"consecutive_failures": self._registry_callback_consecutive_failures,
|
|
1865
|
+
},
|
|
1866
|
+
)
|
|
1867
|
+
|
|
1868
|
+
async def _attempt_subscription(self) -> bool:
|
|
1869
|
+
"""Attempt to subscribe to the request introspection topic.
|
|
1870
|
+
|
|
1871
|
+
Returns:
|
|
1872
|
+
True if subscribed successfully and should wait for stop signal,
|
|
1873
|
+
False if subscription not supported or failed
|
|
1874
|
+
|
|
1875
|
+
Note:
|
|
1876
|
+
This method should only be called when event bus is verified to exist.
|
|
1877
|
+
The caller (_registry_listener_loop) checks for None before calling.
|
|
1878
|
+
"""
|
|
1879
|
+
event_bus = self._introspection_event_bus
|
|
1880
|
+
if event_bus is None or not hasattr(event_bus, "subscribe"):
|
|
1881
|
+
logger.warning(
|
|
1882
|
+
"Event bus does not support subscribe for "
|
|
1883
|
+
f"{self._introspection_node_id}",
|
|
1884
|
+
extra={"node_id": self._introspection_node_id},
|
|
1885
|
+
)
|
|
1886
|
+
return False
|
|
1887
|
+
|
|
1888
|
+
request_topic = self._request_introspection_topic
|
|
1889
|
+
unsubscribe = await event_bus.subscribe(
|
|
1890
|
+
topic=request_topic,
|
|
1891
|
+
group_id=f"introspection-{self._introspection_node_id}",
|
|
1892
|
+
on_message=self._handle_introspection_request,
|
|
1893
|
+
)
|
|
1894
|
+
self._registry_unsubscribe = unsubscribe
|
|
1895
|
+
|
|
1896
|
+
logger.info(
|
|
1897
|
+
f"Registry listener subscribed for {self._introspection_node_id}",
|
|
1898
|
+
extra={
|
|
1899
|
+
"node_id": self._introspection_node_id,
|
|
1900
|
+
"topic": request_topic,
|
|
1901
|
+
},
|
|
1902
|
+
)
|
|
1903
|
+
return True
|
|
1904
|
+
|
|
1905
|
+
async def _wait_for_backoff_or_stop(self, backoff_seconds: float) -> bool:
|
|
1906
|
+
"""Wait for backoff period or stop signal.
|
|
1907
|
+
|
|
1908
|
+
Args:
|
|
1909
|
+
backoff_seconds: Time to wait in seconds
|
|
1910
|
+
|
|
1911
|
+
Returns:
|
|
1912
|
+
True if stop signal received, False if timeout (should retry)
|
|
1913
|
+
|
|
1914
|
+
Note:
|
|
1915
|
+
This method should only be called when stop_event is verified to exist.
|
|
1916
|
+
The caller (_registry_listener_loop) initializes the event before calling.
|
|
1917
|
+
"""
|
|
1918
|
+
stop_event = self._introspection_stop_event
|
|
1919
|
+
if stop_event is None:
|
|
1920
|
+
# Should not happen if called correctly, but handle gracefully
|
|
1921
|
+
return False
|
|
1922
|
+
|
|
1923
|
+
try:
|
|
1924
|
+
await asyncio.wait_for(
|
|
1925
|
+
stop_event.wait(),
|
|
1926
|
+
timeout=backoff_seconds,
|
|
1927
|
+
)
|
|
1928
|
+
# Stop signal received during backoff
|
|
1929
|
+
return True
|
|
1930
|
+
except TimeoutError:
|
|
1931
|
+
# Normal timeout, continue to retry
|
|
1932
|
+
return False
|
|
1933
|
+
|
|
1934
|
+
async def _registry_listener_loop(
|
|
1935
|
+
self,
|
|
1936
|
+
max_retries: int = 3,
|
|
1937
|
+
base_backoff_seconds: float = 1.0,
|
|
1938
|
+
) -> None:
|
|
1939
|
+
"""Background loop listening for REQUEST_INTROSPECTION events.
|
|
1940
|
+
|
|
1941
|
+
Subscribes to the request_introspection topic and responds
|
|
1942
|
+
with introspection data when requests are received. Includes
|
|
1943
|
+
retry logic with exponential backoff for subscription failures.
|
|
1944
|
+
|
|
1945
|
+
Security Note:
|
|
1946
|
+
This method subscribes to the ``node.request_introspection`` Kafka
|
|
1947
|
+
topic and responds with full introspection data to any request.
|
|
1948
|
+
This creates a network-accessible endpoint for capability discovery.
|
|
1949
|
+
|
|
1950
|
+
**Network Exposure**:
|
|
1951
|
+
|
|
1952
|
+
- Any consumer on the Kafka cluster can request introspection data
|
|
1953
|
+
- Responses are published to ``node.introspection`` topic
|
|
1954
|
+
- No authentication is performed on incoming requests
|
|
1955
|
+
|
|
1956
|
+
**Multi-tenant Considerations**:
|
|
1957
|
+
|
|
1958
|
+
- Configure Kafka topic ACLs to restrict access to introspection
|
|
1959
|
+
topics in multi-tenant environments
|
|
1960
|
+
- Consider whether introspection topics should be accessible
|
|
1961
|
+
outside the cluster boundary
|
|
1962
|
+
- Monitor topic consumers for unauthorized access patterns
|
|
1963
|
+
- Use separate Kafka clusters for different security domains
|
|
1964
|
+
|
|
1965
|
+
**Request Validation**:
|
|
1966
|
+
|
|
1967
|
+
- The ``target_node_id`` field allows filtering requests to
|
|
1968
|
+
specific nodes - only matching requests are processed
|
|
1969
|
+
- Malformed requests are handled gracefully without crashing
|
|
1970
|
+
- Correlation IDs are validated but invalid IDs don't block
|
|
1971
|
+
processing
|
|
1972
|
+
|
|
1973
|
+
Args:
|
|
1974
|
+
max_retries: Maximum subscription retry attempts (default: 3)
|
|
1975
|
+
base_backoff_seconds: Base backoff time for exponential retry
|
|
1976
|
+
"""
|
|
1977
|
+
if self._introspection_event_bus is None:
|
|
1978
|
+
logger.warning(
|
|
1979
|
+
f"Cannot start registry listener - no event bus for {self._introspection_node_id}",
|
|
1980
|
+
extra={"node_id": self._introspection_node_id},
|
|
1981
|
+
)
|
|
1982
|
+
return
|
|
1983
|
+
|
|
1984
|
+
# Ensure stop event is initialized
|
|
1985
|
+
if self._introspection_stop_event is None:
|
|
1986
|
+
self._introspection_stop_event = asyncio.Event()
|
|
1987
|
+
|
|
1988
|
+
logger.info(
|
|
1989
|
+
f"Starting registry listener for {self._introspection_node_id}",
|
|
1990
|
+
extra={"node_id": self._introspection_node_id},
|
|
1991
|
+
)
|
|
1992
|
+
|
|
1993
|
+
# Retry loop with exponential backoff for subscription failures
|
|
1994
|
+
retry_count = 0
|
|
1995
|
+
while not self._introspection_stop_event.is_set():
|
|
1996
|
+
try:
|
|
1997
|
+
if await self._attempt_subscription():
|
|
1998
|
+
# Wait for stop signal
|
|
1999
|
+
await self._introspection_stop_event.wait()
|
|
2000
|
+
# Exit loop after subscription ends or not supported
|
|
2001
|
+
break
|
|
2002
|
+
|
|
2003
|
+
except asyncio.CancelledError:
|
|
2004
|
+
logger.debug(
|
|
2005
|
+
f"Registry listener cancelled for {self._introspection_node_id}",
|
|
2006
|
+
extra={"node_id": self._introspection_node_id},
|
|
2007
|
+
)
|
|
2008
|
+
break
|
|
2009
|
+
except Exception as e:
|
|
2010
|
+
retry_count += 1
|
|
2011
|
+
if not await self._handle_subscription_error(
|
|
2012
|
+
e, retry_count, max_retries, base_backoff_seconds
|
|
2013
|
+
):
|
|
2014
|
+
break
|
|
2015
|
+
|
|
2016
|
+
# Final cleanup
|
|
2017
|
+
await self._cleanup_registry_subscription()
|
|
2018
|
+
|
|
2019
|
+
logger.info(
|
|
2020
|
+
f"Registry listener stopped for {self._introspection_node_id}",
|
|
2021
|
+
extra={"node_id": self._introspection_node_id},
|
|
2022
|
+
)
|
|
2023
|
+
|
|
2024
|
+
async def _handle_subscription_error(
|
|
2025
|
+
self,
|
|
2026
|
+
error: Exception,
|
|
2027
|
+
retry_count: int,
|
|
2028
|
+
max_retries: int,
|
|
2029
|
+
base_backoff_seconds: float,
|
|
2030
|
+
) -> bool:
|
|
2031
|
+
"""Handle subscription error with retry logic.
|
|
2032
|
+
|
|
2033
|
+
Args:
|
|
2034
|
+
error: The exception that occurred
|
|
2035
|
+
retry_count: Current retry attempt number
|
|
2036
|
+
max_retries: Maximum retry attempts
|
|
2037
|
+
base_backoff_seconds: Base backoff time for exponential retry
|
|
2038
|
+
|
|
2039
|
+
Returns:
|
|
2040
|
+
True if should continue retrying, False if should stop
|
|
2041
|
+
"""
|
|
2042
|
+
logger.error(
|
|
2043
|
+
f"Error in registry listener for {self._introspection_node_id}",
|
|
2044
|
+
extra={
|
|
2045
|
+
"node_id": self._introspection_node_id,
|
|
2046
|
+
"error_type": type(error).__name__,
|
|
2047
|
+
"error_message": str(error),
|
|
2048
|
+
"retry_count": retry_count,
|
|
2049
|
+
"max_retries": max_retries,
|
|
2050
|
+
},
|
|
2051
|
+
exc_info=True,
|
|
2052
|
+
)
|
|
2053
|
+
|
|
2054
|
+
# Clean up any partial subscription before retry
|
|
2055
|
+
await self._cleanup_registry_subscription()
|
|
2056
|
+
|
|
2057
|
+
# Check if we should retry
|
|
2058
|
+
if retry_count >= max_retries:
|
|
2059
|
+
logger.error(
|
|
2060
|
+
"Registry listener exhausted retries",
|
|
2061
|
+
extra={
|
|
2062
|
+
"node_id": self._introspection_node_id,
|
|
2063
|
+
"retry_count": retry_count,
|
|
2064
|
+
"max_retries": max_retries,
|
|
2065
|
+
"error_type": type(error).__name__,
|
|
2066
|
+
"error_message": str(error),
|
|
2067
|
+
},
|
|
2068
|
+
exc_info=True,
|
|
2069
|
+
)
|
|
2070
|
+
return False
|
|
2071
|
+
|
|
2072
|
+
# Exponential backoff before retry
|
|
2073
|
+
backoff = base_backoff_seconds * (2 ** (retry_count - 1))
|
|
2074
|
+
logger.info(
|
|
2075
|
+
f"Registry listener retrying in {backoff}s for "
|
|
2076
|
+
f"{self._introspection_node_id}",
|
|
2077
|
+
extra={
|
|
2078
|
+
"node_id": self._introspection_node_id,
|
|
2079
|
+
"backoff_seconds": backoff,
|
|
2080
|
+
"retry_count": retry_count,
|
|
2081
|
+
},
|
|
2082
|
+
)
|
|
2083
|
+
|
|
2084
|
+
# Wait for backoff period or stop signal
|
|
2085
|
+
if await self._wait_for_backoff_or_stop(backoff):
|
|
2086
|
+
return False # Stop signal received
|
|
2087
|
+
|
|
2088
|
+
return True # Continue retrying
|
|
2089
|
+
|
|
2090
|
+
async def start_introspection_tasks(
|
|
2091
|
+
self,
|
|
2092
|
+
enable_heartbeat: bool = True,
|
|
2093
|
+
heartbeat_interval_seconds: float = 30.0,
|
|
2094
|
+
enable_registry_listener: bool = True,
|
|
2095
|
+
) -> None:
|
|
2096
|
+
"""Start background introspection tasks.
|
|
2097
|
+
|
|
2098
|
+
Starts the heartbeat loop and/or registry listener as background
|
|
2099
|
+
tasks. Safe to call multiple times - won't start duplicate tasks.
|
|
2100
|
+
|
|
2101
|
+
Args:
|
|
2102
|
+
enable_heartbeat: Whether to start the heartbeat loop
|
|
2103
|
+
heartbeat_interval_seconds: Interval between heartbeats in seconds
|
|
2104
|
+
enable_registry_listener: Whether to start the registry listener
|
|
2105
|
+
|
|
2106
|
+
Raises:
|
|
2107
|
+
ProtocolConfigurationError: If initialize_introspection() was not called.
|
|
2108
|
+
|
|
2109
|
+
Example:
|
|
2110
|
+
```python
|
|
2111
|
+
await node.start_introspection_tasks(
|
|
2112
|
+
enable_heartbeat=True,
|
|
2113
|
+
heartbeat_interval_seconds=30.0,
|
|
2114
|
+
enable_registry_listener=True,
|
|
2115
|
+
)
|
|
2116
|
+
```
|
|
2117
|
+
"""
|
|
2118
|
+
self._ensure_initialized()
|
|
2119
|
+
# Reset stop event if previously set
|
|
2120
|
+
if self._introspection_stop_event is None:
|
|
2121
|
+
self._introspection_stop_event = asyncio.Event()
|
|
2122
|
+
elif self._introspection_stop_event.is_set():
|
|
2123
|
+
self._introspection_stop_event.clear()
|
|
2124
|
+
|
|
2125
|
+
# Start heartbeat task if enabled and not running
|
|
2126
|
+
if enable_heartbeat and self._heartbeat_task is None:
|
|
2127
|
+
self._heartbeat_task = asyncio.create_task(
|
|
2128
|
+
self._heartbeat_loop(heartbeat_interval_seconds),
|
|
2129
|
+
name=f"heartbeat-{self._introspection_node_id}",
|
|
2130
|
+
)
|
|
2131
|
+
logger.debug(
|
|
2132
|
+
f"Started heartbeat task for {self._introspection_node_id}",
|
|
2133
|
+
extra={
|
|
2134
|
+
"node_id": self._introspection_node_id,
|
|
2135
|
+
"interval": heartbeat_interval_seconds,
|
|
2136
|
+
},
|
|
2137
|
+
)
|
|
2138
|
+
|
|
2139
|
+
# Start registry listener if enabled and not running
|
|
2140
|
+
if enable_registry_listener and self._registry_listener_task is None:
|
|
2141
|
+
self._registry_listener_task = asyncio.create_task(
|
|
2142
|
+
self._registry_listener_loop(),
|
|
2143
|
+
name=f"registry-listener-{self._introspection_node_id}",
|
|
2144
|
+
)
|
|
2145
|
+
logger.debug(
|
|
2146
|
+
f"Started registry listener task for {self._introspection_node_id}",
|
|
2147
|
+
extra={"node_id": self._introspection_node_id},
|
|
2148
|
+
)
|
|
2149
|
+
|
|
2150
|
+
async def start_introspection_tasks_from_config(
|
|
2151
|
+
self,
|
|
2152
|
+
config: ModelIntrospectionTaskConfig,
|
|
2153
|
+
) -> None:
|
|
2154
|
+
"""Start background introspection tasks from a configuration model.
|
|
2155
|
+
|
|
2156
|
+
This method provides an alternative to ``start_introspection_tasks()``
|
|
2157
|
+
using a configuration model instead of individual parameters. This
|
|
2158
|
+
reduces union types in calling code and follows ONEX patterns.
|
|
2159
|
+
|
|
2160
|
+
Args:
|
|
2161
|
+
config: Configuration model containing task settings.
|
|
2162
|
+
See ModelIntrospectionTaskConfig for available options.
|
|
2163
|
+
|
|
2164
|
+
Raises:
|
|
2165
|
+
ProtocolConfigurationError: If initialize_introspection() was not called.
|
|
2166
|
+
|
|
2167
|
+
Example:
|
|
2168
|
+
```python
|
|
2169
|
+
from omnibase_infra.models.discovery import ModelIntrospectionTaskConfig
|
|
2170
|
+
|
|
2171
|
+
class MyNode(MixinNodeIntrospection):
|
|
2172
|
+
async def startup(self):
|
|
2173
|
+
config = ModelIntrospectionTaskConfig(
|
|
2174
|
+
enable_heartbeat=True,
|
|
2175
|
+
heartbeat_interval_seconds=15.0,
|
|
2176
|
+
enable_registry_listener=True,
|
|
2177
|
+
)
|
|
2178
|
+
await self.start_introspection_tasks_from_config(config)
|
|
2179
|
+
|
|
2180
|
+
# Using defaults
|
|
2181
|
+
class SimpleNode(MixinNodeIntrospection):
|
|
2182
|
+
async def startup(self):
|
|
2183
|
+
config = ModelIntrospectionTaskConfig()
|
|
2184
|
+
await self.start_introspection_tasks_from_config(config)
|
|
2185
|
+
```
|
|
2186
|
+
|
|
2187
|
+
See Also:
|
|
2188
|
+
start_introspection_tasks: Original method with parameters.
|
|
2189
|
+
ModelIntrospectionTaskConfig: Configuration model with all options.
|
|
2190
|
+
"""
|
|
2191
|
+
await self.start_introspection_tasks(
|
|
2192
|
+
enable_heartbeat=config.enable_heartbeat,
|
|
2193
|
+
heartbeat_interval_seconds=config.heartbeat_interval_seconds,
|
|
2194
|
+
enable_registry_listener=config.enable_registry_listener,
|
|
2195
|
+
)
|
|
2196
|
+
|
|
2197
|
+
async def stop_introspection_tasks(self) -> None:
|
|
2198
|
+
"""Stop all background introspection tasks.
|
|
2199
|
+
|
|
2200
|
+
Signals tasks to stop and waits for clean shutdown.
|
|
2201
|
+
Safe to call multiple times.
|
|
2202
|
+
|
|
2203
|
+
Example:
|
|
2204
|
+
```python
|
|
2205
|
+
await node.stop_introspection_tasks()
|
|
2206
|
+
```
|
|
2207
|
+
"""
|
|
2208
|
+
logger.info(
|
|
2209
|
+
f"Stopping introspection tasks for {self._introspection_node_id}",
|
|
2210
|
+
extra={"node_id": self._introspection_node_id},
|
|
2211
|
+
)
|
|
2212
|
+
|
|
2213
|
+
# Signal tasks to stop
|
|
2214
|
+
if self._introspection_stop_event is not None:
|
|
2215
|
+
self._introspection_stop_event.set()
|
|
2216
|
+
|
|
2217
|
+
# Cancel and wait for heartbeat task
|
|
2218
|
+
if self._heartbeat_task is not None:
|
|
2219
|
+
self._heartbeat_task.cancel()
|
|
2220
|
+
try:
|
|
2221
|
+
await self._heartbeat_task
|
|
2222
|
+
except asyncio.CancelledError:
|
|
2223
|
+
pass
|
|
2224
|
+
self._heartbeat_task = None
|
|
2225
|
+
|
|
2226
|
+
# Cancel and wait for registry listener task
|
|
2227
|
+
if self._registry_listener_task is not None:
|
|
2228
|
+
self._registry_listener_task.cancel()
|
|
2229
|
+
try:
|
|
2230
|
+
await self._registry_listener_task
|
|
2231
|
+
except asyncio.CancelledError:
|
|
2232
|
+
pass
|
|
2233
|
+
self._registry_listener_task = None
|
|
2234
|
+
|
|
2235
|
+
logger.info(
|
|
2236
|
+
f"Introspection tasks stopped for {self._introspection_node_id}",
|
|
2237
|
+
extra={"node_id": self._introspection_node_id},
|
|
2238
|
+
)
|
|
2239
|
+
|
|
2240
|
+
def invalidate_introspection_cache(self) -> None:
|
|
2241
|
+
"""Invalidate the introspection cache.
|
|
2242
|
+
|
|
2243
|
+
Call this when node capabilities change to ensure fresh
|
|
2244
|
+
data is reported on next introspection request.
|
|
2245
|
+
|
|
2246
|
+
Example:
|
|
2247
|
+
```python
|
|
2248
|
+
node.register_new_handler(handler)
|
|
2249
|
+
node.invalidate_introspection_cache()
|
|
2250
|
+
```
|
|
2251
|
+
"""
|
|
2252
|
+
self._introspection_cache = None
|
|
2253
|
+
self._introspection_cached_at = None
|
|
2254
|
+
logger.debug(
|
|
2255
|
+
f"Introspection cache invalidated for {self._introspection_node_id}",
|
|
2256
|
+
extra={"node_id": self._introspection_node_id},
|
|
2257
|
+
)
|
|
2258
|
+
|
|
2259
|
+
def get_performance_metrics(self) -> ModelIntrospectionPerformanceMetrics | None:
|
|
2260
|
+
"""Get the most recent performance metrics from introspection operations.
|
|
2261
|
+
|
|
2262
|
+
Returns the performance metrics captured during the last call to
|
|
2263
|
+
``get_introspection_data()``. Use this to monitor introspection
|
|
2264
|
+
performance and detect when operations exceed the <50ms threshold.
|
|
2265
|
+
|
|
2266
|
+
Returns:
|
|
2267
|
+
ModelIntrospectionPerformanceMetrics if introspection has been called,
|
|
2268
|
+
None if no introspection has been performed yet.
|
|
2269
|
+
|
|
2270
|
+
Example:
|
|
2271
|
+
```python
|
|
2272
|
+
# After calling introspection
|
|
2273
|
+
await node.get_introspection_data()
|
|
2274
|
+
|
|
2275
|
+
# Check performance metrics
|
|
2276
|
+
metrics = node.get_performance_metrics()
|
|
2277
|
+
if metrics and metrics.threshold_exceeded:
|
|
2278
|
+
logger.warning(
|
|
2279
|
+
"Slow introspection detected",
|
|
2280
|
+
extra={
|
|
2281
|
+
"slow_operations": metrics.slow_operations,
|
|
2282
|
+
"total_ms": metrics.total_introspection_ms,
|
|
2283
|
+
}
|
|
2284
|
+
)
|
|
2285
|
+
|
|
2286
|
+
# Access individual timings
|
|
2287
|
+
if metrics:
|
|
2288
|
+
print(f"Total time: {metrics.total_introspection_ms:.2f}ms")
|
|
2289
|
+
print(f"Cache hit: {metrics.cache_hit}")
|
|
2290
|
+
print(f"Methods discovered: {metrics.method_count}")
|
|
2291
|
+
```
|
|
2292
|
+
"""
|
|
2293
|
+
return self._introspection_last_metrics
|
|
2294
|
+
|
|
2295
|
+
@asynccontextmanager
|
|
2296
|
+
async def track_operation(
|
|
2297
|
+
self,
|
|
2298
|
+
operation_name: str | None = None,
|
|
2299
|
+
) -> AsyncIterator[None]:
|
|
2300
|
+
"""Context manager for tracking active operations.
|
|
2301
|
+
|
|
2302
|
+
Provides coroutine-safe tracking of concurrent operations for
|
|
2303
|
+
heartbeat reporting. Increments the active operations counter
|
|
2304
|
+
on entry and decrements it on exit (whether successful or not).
|
|
2305
|
+
|
|
2306
|
+
Concurrency Safety:
|
|
2307
|
+
Uses asyncio.Lock for coroutine-safe counter updates.
|
|
2308
|
+
The lock is held only during counter updates, not during
|
|
2309
|
+
the operation itself. Logging occurs AFTER lock release
|
|
2310
|
+
to prevent blocking during I/O.
|
|
2311
|
+
|
|
2312
|
+
Error Handling:
|
|
2313
|
+
Counter updates are protected with try/except to ensure
|
|
2314
|
+
operation tracking failures don't affect the main operation.
|
|
2315
|
+
The counter will never go negative due to atomic operations.
|
|
2316
|
+
|
|
2317
|
+
Args:
|
|
2318
|
+
operation_name: Optional name for logging/debugging.
|
|
2319
|
+
Not used for counter logic but useful for diagnostics.
|
|
2320
|
+
|
|
2321
|
+
Yields:
|
|
2322
|
+
None. The context manager is used purely for side effects.
|
|
2323
|
+
|
|
2324
|
+
Example:
|
|
2325
|
+
```python
|
|
2326
|
+
class MyNode(MixinNodeIntrospection):
|
|
2327
|
+
async def execute_query(self, query: str) -> Result:
|
|
2328
|
+
async with self.track_operation("execute_query"):
|
|
2329
|
+
# This operation is now tracked in heartbeats
|
|
2330
|
+
return await self._database.execute(query)
|
|
2331
|
+
|
|
2332
|
+
async def process_batch(self, items: list[Item]) -> None:
|
|
2333
|
+
# Track multiple concurrent operations
|
|
2334
|
+
async with asyncio.TaskGroup() as tg:
|
|
2335
|
+
for item in items:
|
|
2336
|
+
tg.create_task(self._process_with_tracking(item))
|
|
2337
|
+
|
|
2338
|
+
async def _process_with_tracking(self, item: Item) -> None:
|
|
2339
|
+
async with self.track_operation("process_item"):
|
|
2340
|
+
await self._process_single(item)
|
|
2341
|
+
```
|
|
2342
|
+
|
|
2343
|
+
Note:
|
|
2344
|
+
The counter is read by ``_publish_heartbeat()`` to report
|
|
2345
|
+
the current number of active operations. This provides
|
|
2346
|
+
visibility into node load for monitoring and scaling.
|
|
2347
|
+
"""
|
|
2348
|
+
# Increment counter on entry - capture count inside lock, log outside
|
|
2349
|
+
count_after_increment = 0
|
|
2350
|
+
increment_succeeded = False
|
|
2351
|
+
try:
|
|
2352
|
+
async with self._operations_lock:
|
|
2353
|
+
self._active_operations += 1
|
|
2354
|
+
count_after_increment = self._active_operations
|
|
2355
|
+
increment_succeeded = True
|
|
2356
|
+
except Exception as e:
|
|
2357
|
+
# Log but don't fail the operation
|
|
2358
|
+
logger.warning(
|
|
2359
|
+
f"Failed to increment operation counter: {e}",
|
|
2360
|
+
extra={
|
|
2361
|
+
"node_id": self._introspection_node_id,
|
|
2362
|
+
"operation": operation_name,
|
|
2363
|
+
"error_type": type(e).__name__,
|
|
2364
|
+
},
|
|
2365
|
+
)
|
|
2366
|
+
|
|
2367
|
+
# Log AFTER releasing lock to prevent blocking during I/O
|
|
2368
|
+
if increment_succeeded and operation_name:
|
|
2369
|
+
logger.debug(
|
|
2370
|
+
f"Operation started: {operation_name}",
|
|
2371
|
+
extra={
|
|
2372
|
+
"node_id": self._introspection_node_id,
|
|
2373
|
+
"operation": operation_name,
|
|
2374
|
+
"active_operations": count_after_increment,
|
|
2375
|
+
},
|
|
2376
|
+
)
|
|
2377
|
+
|
|
2378
|
+
try:
|
|
2379
|
+
yield
|
|
2380
|
+
finally:
|
|
2381
|
+
# Decrement counter on exit - capture state inside lock, log outside
|
|
2382
|
+
count_after_decrement = 0
|
|
2383
|
+
decrement_succeeded = False
|
|
2384
|
+
counter_was_zero = False
|
|
2385
|
+
try:
|
|
2386
|
+
async with self._operations_lock:
|
|
2387
|
+
# Prevent negative counter (defensive check)
|
|
2388
|
+
if self._active_operations > 0:
|
|
2389
|
+
self._active_operations -= 1
|
|
2390
|
+
else:
|
|
2391
|
+
counter_was_zero = True
|
|
2392
|
+
count_after_decrement = self._active_operations
|
|
2393
|
+
decrement_succeeded = True
|
|
2394
|
+
except Exception as e:
|
|
2395
|
+
# Log but don't fail the operation
|
|
2396
|
+
logger.warning(
|
|
2397
|
+
f"Failed to decrement operation counter: {e}",
|
|
2398
|
+
extra={
|
|
2399
|
+
"node_id": self._introspection_node_id,
|
|
2400
|
+
"operation": operation_name,
|
|
2401
|
+
"error_type": type(e).__name__,
|
|
2402
|
+
},
|
|
2403
|
+
)
|
|
2404
|
+
|
|
2405
|
+
# Log AFTER releasing lock to prevent blocking during I/O
|
|
2406
|
+
if decrement_succeeded:
|
|
2407
|
+
if counter_was_zero:
|
|
2408
|
+
# This should never happen, but log if it does
|
|
2409
|
+
logger.warning(
|
|
2410
|
+
"Active operations counter already at zero during decrement",
|
|
2411
|
+
extra={
|
|
2412
|
+
"node_id": self._introspection_node_id,
|
|
2413
|
+
"operation": operation_name,
|
|
2414
|
+
},
|
|
2415
|
+
)
|
|
2416
|
+
elif operation_name:
|
|
2417
|
+
logger.debug(
|
|
2418
|
+
f"Operation completed: {operation_name}",
|
|
2419
|
+
extra={
|
|
2420
|
+
"node_id": self._introspection_node_id,
|
|
2421
|
+
"operation": operation_name,
|
|
2422
|
+
"active_operations": count_after_decrement,
|
|
2423
|
+
},
|
|
2424
|
+
)
|
|
2425
|
+
|
|
2426
|
+
async def get_active_operations_count(self) -> int:
|
|
2427
|
+
"""Get the current count of active operations.
|
|
2428
|
+
|
|
2429
|
+
Returns the number of operations currently being tracked via
|
|
2430
|
+
``track_operation()``. This is the same value reported in
|
|
2431
|
+
heartbeat events.
|
|
2432
|
+
|
|
2433
|
+
Concurrency Safety:
|
|
2434
|
+
Uses asyncio.Lock for coroutine-safe counter access.
|
|
2435
|
+
The returned value is a snapshot; concurrent operations
|
|
2436
|
+
may change the count immediately after reading.
|
|
2437
|
+
|
|
2438
|
+
Returns:
|
|
2439
|
+
Current number of active operations (>= 0).
|
|
2440
|
+
|
|
2441
|
+
Example:
|
|
2442
|
+
```python
|
|
2443
|
+
count = await node.get_active_operations_count()
|
|
2444
|
+
if count > threshold:
|
|
2445
|
+
logger.warning(f"High operation load: {count} active")
|
|
2446
|
+
```
|
|
2447
|
+
"""
|
|
2448
|
+
async with self._operations_lock:
|
|
2449
|
+
return self._active_operations
|
|
2450
|
+
|
|
2451
|
+
|
|
2452
|
+
__all__ = [
|
|
2453
|
+
"HEARTBEAT_TOPIC",
|
|
2454
|
+
"INTROSPECTION_TOPIC",
|
|
2455
|
+
"PERF_THRESHOLD_CACHE_HIT_MS",
|
|
2456
|
+
"PERF_THRESHOLD_DISCOVER_CAPABILITIES_MS",
|
|
2457
|
+
"PERF_THRESHOLD_GET_CAPABILITIES_MS",
|
|
2458
|
+
"PERF_THRESHOLD_GET_INTROSPECTION_DATA_MS",
|
|
2459
|
+
"REQUEST_INTROSPECTION_TOPIC",
|
|
2460
|
+
"DiscoveredCapabilitiesCacheDict", # TypedDict for cached discovered capabilities
|
|
2461
|
+
"IntrospectionCacheDict",
|
|
2462
|
+
"MixinNodeIntrospection",
|
|
2463
|
+
"ModelIntrospectionPerformanceMetrics",
|
|
2464
|
+
"PerformanceMetricsCacheDict", # TypedDict for cached performance metrics
|
|
2465
|
+
]
|