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