omnibase_infra 0.2.5__py3-none-any.whl → 0.2.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- omnibase_infra/constants_topic_patterns.py +26 -0
- omnibase_infra/enums/__init__.py +3 -0
- omnibase_infra/enums/enum_consumer_group_purpose.py +92 -0
- omnibase_infra/enums/enum_handler_source_mode.py +16 -2
- omnibase_infra/errors/__init__.py +4 -0
- omnibase_infra/errors/error_binding_resolution.py +128 -0
- omnibase_infra/event_bus/configs/kafka_event_bus_config.yaml +0 -2
- omnibase_infra/event_bus/event_bus_inmemory.py +64 -10
- omnibase_infra/event_bus/event_bus_kafka.py +105 -47
- omnibase_infra/event_bus/mixin_kafka_broadcast.py +3 -7
- omnibase_infra/event_bus/mixin_kafka_dlq.py +12 -6
- omnibase_infra/event_bus/models/config/model_kafka_event_bus_config.py +0 -81
- omnibase_infra/event_bus/testing/__init__.py +26 -0
- omnibase_infra/event_bus/testing/adapter_protocol_event_publisher_inmemory.py +418 -0
- omnibase_infra/event_bus/testing/model_publisher_metrics.py +64 -0
- omnibase_infra/handlers/handler_consul.py +2 -0
- omnibase_infra/handlers/mixins/__init__.py +5 -0
- omnibase_infra/handlers/mixins/mixin_consul_service.py +274 -10
- omnibase_infra/handlers/mixins/mixin_consul_topic_index.py +585 -0
- omnibase_infra/handlers/models/model_filesystem_config.py +4 -4
- omnibase_infra/migrations/001_create_event_ledger.sql +166 -0
- omnibase_infra/migrations/001_drop_event_ledger.sql +18 -0
- omnibase_infra/mixins/mixin_node_introspection.py +189 -19
- omnibase_infra/models/__init__.py +8 -0
- omnibase_infra/models/bindings/__init__.py +59 -0
- omnibase_infra/models/bindings/constants.py +144 -0
- omnibase_infra/models/bindings/model_binding_resolution_result.py +103 -0
- omnibase_infra/models/bindings/model_operation_binding.py +44 -0
- omnibase_infra/models/bindings/model_operation_bindings_subcontract.py +152 -0
- omnibase_infra/models/bindings/model_parsed_binding.py +52 -0
- omnibase_infra/models/discovery/model_introspection_config.py +25 -17
- omnibase_infra/models/dispatch/__init__.py +8 -0
- omnibase_infra/models/dispatch/model_debug_trace_snapshot.py +114 -0
- omnibase_infra/models/dispatch/model_materialized_dispatch.py +141 -0
- omnibase_infra/models/handlers/model_handler_source_config.py +1 -1
- omnibase_infra/models/model_node_identity.py +126 -0
- omnibase_infra/models/projection/model_snapshot_topic_config.py +3 -2
- omnibase_infra/models/registration/__init__.py +9 -0
- omnibase_infra/models/registration/model_event_bus_topic_entry.py +59 -0
- omnibase_infra/models/registration/model_node_event_bus_config.py +99 -0
- omnibase_infra/models/registration/model_node_introspection_event.py +11 -0
- omnibase_infra/models/runtime/__init__.py +9 -0
- omnibase_infra/models/validation/model_coverage_metrics.py +2 -2
- omnibase_infra/nodes/__init__.py +9 -0
- omnibase_infra/nodes/contract_registry_reducer/__init__.py +29 -0
- omnibase_infra/nodes/contract_registry_reducer/contract.yaml +255 -0
- omnibase_infra/nodes/contract_registry_reducer/models/__init__.py +38 -0
- omnibase_infra/nodes/contract_registry_reducer/models/model_contract_registry_state.py +266 -0
- omnibase_infra/nodes/contract_registry_reducer/models/model_payload_cleanup_topic_references.py +55 -0
- omnibase_infra/nodes/contract_registry_reducer/models/model_payload_deactivate_contract.py +58 -0
- omnibase_infra/nodes/contract_registry_reducer/models/model_payload_mark_stale.py +49 -0
- omnibase_infra/nodes/contract_registry_reducer/models/model_payload_update_heartbeat.py +71 -0
- omnibase_infra/nodes/contract_registry_reducer/models/model_payload_update_topic.py +66 -0
- omnibase_infra/nodes/contract_registry_reducer/models/model_payload_upsert_contract.py +92 -0
- omnibase_infra/nodes/contract_registry_reducer/node.py +121 -0
- omnibase_infra/nodes/contract_registry_reducer/reducer.py +784 -0
- omnibase_infra/nodes/contract_registry_reducer/registry/__init__.py +9 -0
- omnibase_infra/nodes/contract_registry_reducer/registry/registry_infra_contract_registry_reducer.py +101 -0
- omnibase_infra/nodes/handlers/consul/contract.yaml +85 -0
- omnibase_infra/nodes/handlers/db/contract.yaml +72 -0
- omnibase_infra/nodes/handlers/graph/contract.yaml +127 -0
- omnibase_infra/nodes/handlers/http/contract.yaml +74 -0
- omnibase_infra/nodes/handlers/intent/contract.yaml +66 -0
- omnibase_infra/nodes/handlers/mcp/contract.yaml +69 -0
- omnibase_infra/nodes/handlers/vault/contract.yaml +91 -0
- omnibase_infra/nodes/node_ledger_projection_compute/__init__.py +50 -0
- omnibase_infra/nodes/node_ledger_projection_compute/contract.yaml +104 -0
- omnibase_infra/nodes/node_ledger_projection_compute/node.py +284 -0
- omnibase_infra/nodes/node_ledger_projection_compute/registry/__init__.py +29 -0
- omnibase_infra/nodes/node_ledger_projection_compute/registry/registry_infra_ledger_projection.py +118 -0
- omnibase_infra/nodes/node_ledger_write_effect/__init__.py +82 -0
- omnibase_infra/nodes/node_ledger_write_effect/contract.yaml +200 -0
- omnibase_infra/nodes/node_ledger_write_effect/handlers/__init__.py +22 -0
- omnibase_infra/nodes/node_ledger_write_effect/handlers/handler_ledger_append.py +372 -0
- omnibase_infra/nodes/node_ledger_write_effect/handlers/handler_ledger_query.py +597 -0
- omnibase_infra/nodes/node_ledger_write_effect/models/__init__.py +31 -0
- omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_append_result.py +54 -0
- omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_entry.py +92 -0
- omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_query.py +53 -0
- omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_query_result.py +41 -0
- omnibase_infra/nodes/node_ledger_write_effect/node.py +89 -0
- omnibase_infra/nodes/node_ledger_write_effect/protocols/__init__.py +13 -0
- omnibase_infra/nodes/node_ledger_write_effect/protocols/protocol_ledger_persistence.py +127 -0
- omnibase_infra/nodes/node_ledger_write_effect/registry/__init__.py +9 -0
- omnibase_infra/nodes/node_ledger_write_effect/registry/registry_infra_ledger_write.py +121 -0
- omnibase_infra/nodes/node_registration_orchestrator/registry/registry_infra_node_registration_orchestrator.py +7 -5
- omnibase_infra/nodes/reducers/models/__init__.py +7 -2
- omnibase_infra/nodes/reducers/models/model_payload_consul_register.py +11 -0
- omnibase_infra/nodes/reducers/models/model_payload_ledger_append.py +133 -0
- omnibase_infra/nodes/reducers/registration_reducer.py +1 -0
- omnibase_infra/protocols/__init__.py +3 -0
- omnibase_infra/protocols/protocol_dispatch_engine.py +152 -0
- omnibase_infra/runtime/__init__.py +60 -0
- omnibase_infra/runtime/binding_resolver.py +753 -0
- omnibase_infra/runtime/constants_security.py +70 -0
- omnibase_infra/runtime/contract_loaders/__init__.py +9 -0
- omnibase_infra/runtime/contract_loaders/operation_bindings_loader.py +789 -0
- omnibase_infra/runtime/emit_daemon/__init__.py +97 -0
- omnibase_infra/runtime/emit_daemon/cli.py +844 -0
- omnibase_infra/runtime/emit_daemon/client.py +811 -0
- omnibase_infra/runtime/emit_daemon/config.py +535 -0
- omnibase_infra/runtime/emit_daemon/daemon.py +812 -0
- omnibase_infra/runtime/emit_daemon/event_registry.py +477 -0
- omnibase_infra/runtime/emit_daemon/model_daemon_request.py +139 -0
- omnibase_infra/runtime/emit_daemon/model_daemon_response.py +191 -0
- omnibase_infra/runtime/emit_daemon/queue.py +618 -0
- omnibase_infra/runtime/event_bus_subcontract_wiring.py +466 -0
- omnibase_infra/runtime/handler_source_resolver.py +43 -2
- omnibase_infra/runtime/kafka_contract_source.py +984 -0
- omnibase_infra/runtime/models/__init__.py +13 -0
- omnibase_infra/runtime/models/model_contract_load_result.py +224 -0
- omnibase_infra/runtime/models/model_runtime_contract_config.py +268 -0
- omnibase_infra/runtime/models/model_runtime_scheduler_config.py +4 -3
- omnibase_infra/runtime/models/model_security_config.py +109 -0
- omnibase_infra/runtime/publisher_topic_scoped.py +294 -0
- omnibase_infra/runtime/runtime_contract_config_loader.py +406 -0
- omnibase_infra/runtime/service_kernel.py +76 -6
- omnibase_infra/runtime/service_message_dispatch_engine.py +558 -15
- omnibase_infra/runtime/service_runtime_host_process.py +770 -20
- omnibase_infra/runtime/transition_notification_publisher.py +3 -2
- omnibase_infra/runtime/util_wiring.py +206 -62
- omnibase_infra/services/mcp/service_mcp_tool_sync.py +27 -9
- omnibase_infra/services/session/config_consumer.py +25 -8
- omnibase_infra/services/session/config_store.py +2 -2
- omnibase_infra/services/session/consumer.py +1 -1
- omnibase_infra/topics/__init__.py +45 -0
- omnibase_infra/topics/platform_topic_suffixes.py +140 -0
- omnibase_infra/topics/util_topic_composition.py +95 -0
- omnibase_infra/types/typed_dict/__init__.py +9 -1
- omnibase_infra/types/typed_dict/typed_dict_envelope_build_params.py +115 -0
- omnibase_infra/utils/__init__.py +9 -0
- omnibase_infra/utils/util_consumer_group.py +232 -0
- omnibase_infra/validation/infra_validators.py +18 -1
- omnibase_infra/validation/validation_exemptions.yaml +192 -0
- {omnibase_infra-0.2.5.dist-info → omnibase_infra-0.2.7.dist-info}/METADATA +3 -3
- {omnibase_infra-0.2.5.dist-info → omnibase_infra-0.2.7.dist-info}/RECORD +139 -52
- {omnibase_infra-0.2.5.dist-info → omnibase_infra-0.2.7.dist-info}/entry_points.txt +1 -0
- {omnibase_infra-0.2.5.dist-info → omnibase_infra-0.2.7.dist-info}/WHEEL +0 -0
- {omnibase_infra-0.2.5.dist-info → omnibase_infra-0.2.7.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 OmniNode Team
|
|
3
|
+
"""Event bus subcontract wiring for contract-driven Kafka subscriptions.
|
|
4
|
+
|
|
5
|
+
This module provides the bridge between contract-declared topics (from the
|
|
6
|
+
`event_bus` subcontract) and actual Kafka subscriptions. The runtime owns
|
|
7
|
+
all Kafka plumbing - nodes/handlers never create consumers or producers directly.
|
|
8
|
+
|
|
9
|
+
Architecture:
|
|
10
|
+
The EventBusSubcontractWiring class is responsible for:
|
|
11
|
+
1. Reading `subscribe_topics` from ModelEventBusSubcontract
|
|
12
|
+
2. Resolving topic suffixes to full topic names with environment prefix
|
|
13
|
+
3. Creating Kafka subscriptions with appropriate consumer groups
|
|
14
|
+
4. Bridging received messages to the MessageDispatchEngine
|
|
15
|
+
5. Managing subscription lifecycle (creation and cleanup)
|
|
16
|
+
|
|
17
|
+
This follows the ARCH-002 principle: "Runtime owns all Kafka plumbing."
|
|
18
|
+
Nodes and handlers declare their topic requirements in contracts, but
|
|
19
|
+
never directly interact with Kafka consumers or producers.
|
|
20
|
+
|
|
21
|
+
Topic Resolution:
|
|
22
|
+
Topic suffixes from contracts follow the ONEX naming convention:
|
|
23
|
+
onex.{kind}.{producer}.{event-name}.v{n}
|
|
24
|
+
|
|
25
|
+
The wiring resolves these to full topics by prepending the environment:
|
|
26
|
+
{environment}.onex.{kind}.{producer}.{event-name}.v{n}
|
|
27
|
+
|
|
28
|
+
Example:
|
|
29
|
+
- Contract declares: "onex.evt.omniintelligence.intent-classified.v1"
|
|
30
|
+
- Resolved (dev): "dev.onex.evt.omniintelligence.intent-classified.v1"
|
|
31
|
+
- Resolved (prod): "prod.onex.evt.omniintelligence.intent-classified.v1"
|
|
32
|
+
|
|
33
|
+
Related:
|
|
34
|
+
- OMN-1621: Runtime consumes event_bus subcontract for contract-driven wiring
|
|
35
|
+
- ModelEventBusSubcontract: Contract model defining subscribe/publish topics
|
|
36
|
+
- MessageDispatchEngine: Dispatch engine that processes received messages
|
|
37
|
+
- EventBusKafka: Kafka event bus implementation
|
|
38
|
+
|
|
39
|
+
.. versionadded:: 0.2.5
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
from __future__ import annotations
|
|
43
|
+
|
|
44
|
+
import json
|
|
45
|
+
import logging
|
|
46
|
+
from collections.abc import Awaitable, Callable
|
|
47
|
+
from pathlib import Path
|
|
48
|
+
from typing import TYPE_CHECKING
|
|
49
|
+
|
|
50
|
+
import yaml
|
|
51
|
+
from pydantic import ValidationError
|
|
52
|
+
|
|
53
|
+
from omnibase_core.models.contracts.subcontracts import ModelEventBusSubcontract
|
|
54
|
+
from omnibase_core.models.events.model_event_envelope import ModelEventEnvelope
|
|
55
|
+
from omnibase_core.protocols.event_bus.protocol_event_bus_subscriber import (
|
|
56
|
+
ProtocolEventBusSubscriber,
|
|
57
|
+
)
|
|
58
|
+
from omnibase_core.protocols.event_bus.protocol_event_message import (
|
|
59
|
+
ProtocolEventMessage,
|
|
60
|
+
)
|
|
61
|
+
from omnibase_infra.enums import EnumInfraTransportType
|
|
62
|
+
from omnibase_infra.errors import ModelInfraErrorContext, RuntimeHostError
|
|
63
|
+
from omnibase_infra.protocols import ProtocolDispatchEngine
|
|
64
|
+
|
|
65
|
+
if TYPE_CHECKING:
|
|
66
|
+
from omnibase_infra.event_bus.event_bus_inmemory import EventBusInmemory
|
|
67
|
+
from omnibase_infra.event_bus.event_bus_kafka import EventBusKafka
|
|
68
|
+
from omnibase_infra.runtime.service_message_dispatch_engine import (
|
|
69
|
+
MessageDispatchEngine,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class EventBusSubcontractWiring:
|
|
74
|
+
"""Wires event_bus subcontracts to Kafka subscriptions and publishers.
|
|
75
|
+
|
|
76
|
+
This class bridges contract-declared topics to actual Kafka subscriptions,
|
|
77
|
+
ensuring that nodes/handlers never directly interact with Kafka infrastructure.
|
|
78
|
+
The runtime owns all Kafka plumbing per ARCH-002.
|
|
79
|
+
|
|
80
|
+
Responsibilities:
|
|
81
|
+
- Parse subscribe_topics from ModelEventBusSubcontract
|
|
82
|
+
- Resolve topic suffixes to full topic names with environment prefix
|
|
83
|
+
- Create Kafka subscriptions with appropriate consumer groups
|
|
84
|
+
- Deserialize incoming messages to ModelEventEnvelope
|
|
85
|
+
- Dispatch envelopes to MessageDispatchEngine
|
|
86
|
+
- Manage subscription lifecycle (cleanup on shutdown)
|
|
87
|
+
|
|
88
|
+
Thread Safety:
|
|
89
|
+
This class is designed for single-threaded async use. All subscription
|
|
90
|
+
operations should be performed from a single async context. The underlying
|
|
91
|
+
event bus implementations (EventBusKafka, EventBusInmemory) handle their
|
|
92
|
+
own thread safety for message delivery.
|
|
93
|
+
|
|
94
|
+
Example:
|
|
95
|
+
```python
|
|
96
|
+
from omnibase_infra.runtime import EventBusSubcontractWiring
|
|
97
|
+
from omnibase_core.models.contracts.subcontracts import ModelEventBusSubcontract
|
|
98
|
+
|
|
99
|
+
# Create wiring with event bus and dispatch engine
|
|
100
|
+
wiring = EventBusSubcontractWiring(
|
|
101
|
+
event_bus=event_bus,
|
|
102
|
+
dispatch_engine=dispatch_engine,
|
|
103
|
+
environment="dev",
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Wire subscriptions from subcontract
|
|
107
|
+
subcontract = ModelEventBusSubcontract(
|
|
108
|
+
version=ModelSemVer(major=1, minor=0, patch=0),
|
|
109
|
+
subscribe_topics=["onex.evt.omniintelligence.intent-classified.v1"],
|
|
110
|
+
)
|
|
111
|
+
await wiring.wire_subscriptions(subcontract, node_name="my-handler")
|
|
112
|
+
|
|
113
|
+
# Cleanup on shutdown
|
|
114
|
+
await wiring.cleanup()
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Attributes:
|
|
118
|
+
_event_bus: The event bus implementation (Kafka or in-memory)
|
|
119
|
+
_dispatch_engine: Engine to dispatch received messages to handlers
|
|
120
|
+
_environment: Environment prefix for topics (e.g., 'dev', 'prod')
|
|
121
|
+
_unsubscribe_callables: List of callables to unsubscribe from topics
|
|
122
|
+
_logger: Logger for debug and error messages
|
|
123
|
+
|
|
124
|
+
.. versionadded:: 0.2.5
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
def __init__(
|
|
128
|
+
self,
|
|
129
|
+
event_bus: ProtocolEventBusSubscriber,
|
|
130
|
+
dispatch_engine: ProtocolDispatchEngine,
|
|
131
|
+
environment: str,
|
|
132
|
+
) -> None:
|
|
133
|
+
"""Initialize event bus wiring.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
event_bus: The event bus implementation (EventBusKafka or EventBusInmemory).
|
|
137
|
+
Must implement subscribe(topic, group_id, on_message) -> unsubscribe callable.
|
|
138
|
+
Duck typed per ONEX patterns.
|
|
139
|
+
dispatch_engine: Engine to dispatch received messages to handlers.
|
|
140
|
+
Must implement ProtocolDispatchEngine interface.
|
|
141
|
+
Must be frozen (registrations complete) before wiring subscriptions.
|
|
142
|
+
environment: Environment prefix for topics (e.g., 'dev', 'prod').
|
|
143
|
+
Used to resolve topic suffixes to full topic names.
|
|
144
|
+
|
|
145
|
+
Note:
|
|
146
|
+
The dispatch_engine should be frozen before wiring subscriptions.
|
|
147
|
+
Attempting to dispatch to an unfrozen engine will raise an error.
|
|
148
|
+
|
|
149
|
+
Raises:
|
|
150
|
+
ValueError: If environment is empty or whitespace-only.
|
|
151
|
+
"""
|
|
152
|
+
if not environment or not environment.strip():
|
|
153
|
+
raise ValueError("environment must be a non-empty string")
|
|
154
|
+
|
|
155
|
+
self._event_bus = event_bus
|
|
156
|
+
self._dispatch_engine = dispatch_engine
|
|
157
|
+
self._environment = environment
|
|
158
|
+
self._unsubscribe_callables: list[Callable[[], Awaitable[None]]] = []
|
|
159
|
+
self._logger = logging.getLogger(__name__)
|
|
160
|
+
|
|
161
|
+
def resolve_topic(self, topic_suffix: str) -> str:
|
|
162
|
+
"""Resolve topic suffix to full topic name with environment prefix.
|
|
163
|
+
|
|
164
|
+
Topic suffixes from contracts follow the ONEX naming convention:
|
|
165
|
+
onex.{kind}.{producer}.{event-name}.v{n}
|
|
166
|
+
|
|
167
|
+
This method prepends the environment to create the full topic name:
|
|
168
|
+
{environment}.onex.{kind}.{producer}.{event-name}.v{n}
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
topic_suffix: ONEX format topic suffix
|
|
172
|
+
(e.g., 'onex.evt.omniintelligence.intent-classified.v1')
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
Full topic name with environment prefix
|
|
176
|
+
(e.g., 'dev.onex.evt.omniintelligence.intent-classified.v1')
|
|
177
|
+
|
|
178
|
+
Example:
|
|
179
|
+
>>> wiring = EventBusSubcontractWiring(bus, engine, "dev")
|
|
180
|
+
>>> wiring.resolve_topic("onex.evt.user.created.v1")
|
|
181
|
+
'dev.onex.evt.user.created.v1'
|
|
182
|
+
"""
|
|
183
|
+
return f"{self._environment}.{topic_suffix}"
|
|
184
|
+
|
|
185
|
+
async def wire_subscriptions(
|
|
186
|
+
self,
|
|
187
|
+
subcontract: ModelEventBusSubcontract,
|
|
188
|
+
node_name: str,
|
|
189
|
+
) -> None:
|
|
190
|
+
"""Wire Kafka subscriptions from subcontract.subscribe_topics.
|
|
191
|
+
|
|
192
|
+
Creates Kafka subscriptions for each topic declared in the subcontract's
|
|
193
|
+
subscribe_topics list. Each subscription uses a consumer group ID based
|
|
194
|
+
on the environment and node name for proper load balancing.
|
|
195
|
+
|
|
196
|
+
Consumer Group Naming:
|
|
197
|
+
Consumer groups are named as: {environment}.{node_name}
|
|
198
|
+
Example: "dev.registration-handler"
|
|
199
|
+
|
|
200
|
+
This ensures:
|
|
201
|
+
- Each node instance in an environment shares the same consumer group
|
|
202
|
+
- Multiple instances of the same node load-balance message processing
|
|
203
|
+
- Different environments are completely isolated
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
subcontract: The event_bus subcontract from a handler's contract.
|
|
207
|
+
Contains subscribe_topics list with topic suffixes.
|
|
208
|
+
node_name: Name of the node/handler for consumer group identification.
|
|
209
|
+
Should be unique per handler type (e.g., "registration-handler").
|
|
210
|
+
|
|
211
|
+
Raises:
|
|
212
|
+
InfraConnectionError: If Kafka connection fails during subscription.
|
|
213
|
+
InfraTimeoutError: If subscription times out.
|
|
214
|
+
|
|
215
|
+
Example:
|
|
216
|
+
>>> subcontract = ModelEventBusSubcontract(
|
|
217
|
+
... version=ModelSemVer(major=1, minor=0, patch=0),
|
|
218
|
+
... subscribe_topics=["onex.evt.node.introspected.v1"],
|
|
219
|
+
... )
|
|
220
|
+
>>> await wiring.wire_subscriptions(subcontract, "registration-handler")
|
|
221
|
+
"""
|
|
222
|
+
if not subcontract.subscribe_topics:
|
|
223
|
+
self._logger.debug(
|
|
224
|
+
"No subscribe_topics in subcontract for node '%s'",
|
|
225
|
+
node_name,
|
|
226
|
+
)
|
|
227
|
+
return
|
|
228
|
+
|
|
229
|
+
for topic_suffix in subcontract.subscribe_topics:
|
|
230
|
+
full_topic = self.resolve_topic(topic_suffix)
|
|
231
|
+
group_id = f"{self._environment}.{node_name}"
|
|
232
|
+
|
|
233
|
+
# Create dispatch callback for this topic
|
|
234
|
+
callback = self._create_dispatch_callback(full_topic)
|
|
235
|
+
|
|
236
|
+
# Subscribe and store unsubscribe callable
|
|
237
|
+
unsubscribe = await self._event_bus.subscribe(
|
|
238
|
+
topic=full_topic,
|
|
239
|
+
group_id=group_id,
|
|
240
|
+
on_message=callback,
|
|
241
|
+
)
|
|
242
|
+
self._unsubscribe_callables.append(unsubscribe)
|
|
243
|
+
|
|
244
|
+
self._logger.info(
|
|
245
|
+
"Wired subscription: topic=%s, group_id=%s, node=%s",
|
|
246
|
+
full_topic,
|
|
247
|
+
group_id,
|
|
248
|
+
node_name,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
def _create_dispatch_callback(
|
|
252
|
+
self,
|
|
253
|
+
topic: str,
|
|
254
|
+
) -> Callable[[ProtocolEventMessage], Awaitable[None]]:
|
|
255
|
+
"""Create callback that bridges Kafka consumer to dispatch engine.
|
|
256
|
+
|
|
257
|
+
Creates an async callback function that:
|
|
258
|
+
1. Receives ProtocolEventMessage from the Kafka consumer
|
|
259
|
+
2. Deserializes the message value to ModelEventEnvelope
|
|
260
|
+
3. Dispatches the envelope to the MessageDispatchEngine
|
|
261
|
+
|
|
262
|
+
Error Handling:
|
|
263
|
+
- Deserialization errors are logged and the message is skipped
|
|
264
|
+
- Dispatch errors are propagated (handled by the event bus DLQ logic)
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
topic: The full topic name for routing context in logs.
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
Async callback function compatible with event bus subscribe().
|
|
271
|
+
"""
|
|
272
|
+
|
|
273
|
+
async def callback(message: ProtocolEventMessage) -> None:
|
|
274
|
+
"""Process incoming Kafka message and dispatch to engine."""
|
|
275
|
+
try:
|
|
276
|
+
envelope = self._deserialize_to_envelope(message)
|
|
277
|
+
# Dispatch via ProtocolDispatchEngine interface
|
|
278
|
+
await self._dispatch_engine.dispatch(topic, envelope)
|
|
279
|
+
except json.JSONDecodeError as e:
|
|
280
|
+
self._logger.exception(
|
|
281
|
+
"Failed to deserialize message from topic '%s': %s",
|
|
282
|
+
topic,
|
|
283
|
+
e,
|
|
284
|
+
)
|
|
285
|
+
# Wrap in OnexError per CLAUDE.md: "OnexError Only"
|
|
286
|
+
raise RuntimeHostError(
|
|
287
|
+
f"Failed to deserialize message from topic '{topic}'",
|
|
288
|
+
context=ModelInfraErrorContext.with_correlation(
|
|
289
|
+
transport_type=EnumInfraTransportType.KAFKA,
|
|
290
|
+
operation="event_bus_deserialize",
|
|
291
|
+
),
|
|
292
|
+
) from e
|
|
293
|
+
except Exception as e:
|
|
294
|
+
self._logger.exception(
|
|
295
|
+
"Failed to dispatch message from topic '%s': %s",
|
|
296
|
+
topic,
|
|
297
|
+
e,
|
|
298
|
+
)
|
|
299
|
+
# Wrap in OnexError per CLAUDE.md: "OnexError Only"
|
|
300
|
+
raise RuntimeHostError(
|
|
301
|
+
f"Failed to dispatch message from topic '{topic}'",
|
|
302
|
+
context=ModelInfraErrorContext.with_correlation(
|
|
303
|
+
transport_type=EnumInfraTransportType.KAFKA,
|
|
304
|
+
operation="event_bus_dispatch",
|
|
305
|
+
),
|
|
306
|
+
) from e
|
|
307
|
+
|
|
308
|
+
return callback
|
|
309
|
+
|
|
310
|
+
def _deserialize_to_envelope(
|
|
311
|
+
self,
|
|
312
|
+
message: ProtocolEventMessage,
|
|
313
|
+
) -> ModelEventEnvelope[object]:
|
|
314
|
+
"""Deserialize Kafka message to event envelope.
|
|
315
|
+
|
|
316
|
+
Converts the raw bytes in ProtocolEventMessage.value to a ModelEventEnvelope
|
|
317
|
+
that can be processed by the dispatch engine.
|
|
318
|
+
|
|
319
|
+
Deserialization Strategy:
|
|
320
|
+
1. Decode message.value from UTF-8 bytes to string
|
|
321
|
+
2. Parse JSON string to dict
|
|
322
|
+
3. Validate and construct ModelEventEnvelope
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
message: ProtocolEventMessage from Kafka consumer containing raw bytes.
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
Deserialized ModelEventEnvelope for dispatch.
|
|
329
|
+
|
|
330
|
+
Raises:
|
|
331
|
+
json.JSONDecodeError: If message value is not valid JSON.
|
|
332
|
+
ValidationError: If JSON does not match ModelEventEnvelope schema.
|
|
333
|
+
"""
|
|
334
|
+
# Decode bytes to string
|
|
335
|
+
json_str = message.value.decode("utf-8")
|
|
336
|
+
|
|
337
|
+
# Parse JSON to dict
|
|
338
|
+
data = json.loads(json_str)
|
|
339
|
+
|
|
340
|
+
# Validate and construct envelope
|
|
341
|
+
return ModelEventEnvelope[object].model_validate(data)
|
|
342
|
+
|
|
343
|
+
async def cleanup(self) -> None:
|
|
344
|
+
"""Unsubscribe from all topics.
|
|
345
|
+
|
|
346
|
+
Should be called during runtime shutdown to properly clean up
|
|
347
|
+
Kafka consumer subscriptions. This ensures:
|
|
348
|
+
- Consumer group offsets are committed
|
|
349
|
+
- Connections are properly closed
|
|
350
|
+
- Resources are released
|
|
351
|
+
|
|
352
|
+
This method is safe to call multiple times - subsequent calls
|
|
353
|
+
are no-ops after the first successful cleanup.
|
|
354
|
+
|
|
355
|
+
Example:
|
|
356
|
+
>>> # During shutdown
|
|
357
|
+
>>> await wiring.cleanup()
|
|
358
|
+
"""
|
|
359
|
+
cleanup_count = len(self._unsubscribe_callables)
|
|
360
|
+
|
|
361
|
+
for unsubscribe in self._unsubscribe_callables:
|
|
362
|
+
try:
|
|
363
|
+
await unsubscribe()
|
|
364
|
+
except Exception as e:
|
|
365
|
+
self._logger.warning(
|
|
366
|
+
"Error during unsubscribe: %s",
|
|
367
|
+
e,
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
self._unsubscribe_callables.clear()
|
|
371
|
+
|
|
372
|
+
if cleanup_count > 0:
|
|
373
|
+
self._logger.info(
|
|
374
|
+
"Cleaned up %d event bus subscription(s)",
|
|
375
|
+
cleanup_count,
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def load_event_bus_subcontract(
|
|
380
|
+
contract_path: Path,
|
|
381
|
+
logger: logging.Logger | None = None,
|
|
382
|
+
) -> ModelEventBusSubcontract | None:
|
|
383
|
+
"""Load event_bus subcontract from contract YAML file.
|
|
384
|
+
|
|
385
|
+
Reads a contract YAML file and extracts the event_bus section,
|
|
386
|
+
returning a validated ModelEventBusSubcontract if present.
|
|
387
|
+
|
|
388
|
+
File Format:
|
|
389
|
+
The contract YAML should have an `event_bus` section:
|
|
390
|
+
|
|
391
|
+
```yaml
|
|
392
|
+
event_bus:
|
|
393
|
+
version:
|
|
394
|
+
major: 1
|
|
395
|
+
minor: 0
|
|
396
|
+
patch: 0
|
|
397
|
+
subscribe_topics:
|
|
398
|
+
- onex.evt.node.introspected.v1
|
|
399
|
+
- onex.evt.node.registered.v1
|
|
400
|
+
publish_topics:
|
|
401
|
+
- onex.cmd.node.register.v1
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
Args:
|
|
405
|
+
contract_path: Path to the contract YAML file.
|
|
406
|
+
logger: Optional logger for warnings. If not provided, uses module logger.
|
|
407
|
+
|
|
408
|
+
Returns:
|
|
409
|
+
ModelEventBusSubcontract if event_bus section exists and is valid,
|
|
410
|
+
None otherwise.
|
|
411
|
+
|
|
412
|
+
Example:
|
|
413
|
+
>>> subcontract = load_event_bus_subcontract(Path("contract.yaml"))
|
|
414
|
+
>>> if subcontract:
|
|
415
|
+
... print(f"Subscribe topics: {subcontract.subscribe_topics}")
|
|
416
|
+
"""
|
|
417
|
+
_logger = logger or logging.getLogger(__name__)
|
|
418
|
+
|
|
419
|
+
if not contract_path.exists():
|
|
420
|
+
_logger.warning(
|
|
421
|
+
"Contract file not found: %s",
|
|
422
|
+
contract_path,
|
|
423
|
+
)
|
|
424
|
+
return None
|
|
425
|
+
|
|
426
|
+
try:
|
|
427
|
+
with contract_path.open() as f:
|
|
428
|
+
contract_data = yaml.safe_load(f)
|
|
429
|
+
|
|
430
|
+
if contract_data is None:
|
|
431
|
+
_logger.warning(
|
|
432
|
+
"Empty contract file: %s",
|
|
433
|
+
contract_path,
|
|
434
|
+
)
|
|
435
|
+
return None
|
|
436
|
+
|
|
437
|
+
event_bus_data = contract_data.get("event_bus")
|
|
438
|
+
if not event_bus_data:
|
|
439
|
+
_logger.debug(
|
|
440
|
+
"No event_bus section in contract: %s",
|
|
441
|
+
contract_path,
|
|
442
|
+
)
|
|
443
|
+
return None
|
|
444
|
+
|
|
445
|
+
return ModelEventBusSubcontract.model_validate(event_bus_data)
|
|
446
|
+
|
|
447
|
+
except yaml.YAMLError as e:
|
|
448
|
+
_logger.warning(
|
|
449
|
+
"Failed to parse YAML in contract %s: %s",
|
|
450
|
+
contract_path,
|
|
451
|
+
e,
|
|
452
|
+
)
|
|
453
|
+
return None
|
|
454
|
+
except ValidationError as e:
|
|
455
|
+
_logger.warning(
|
|
456
|
+
"Invalid event_bus subcontract in %s: %s",
|
|
457
|
+
contract_path,
|
|
458
|
+
e,
|
|
459
|
+
)
|
|
460
|
+
return None
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
__all__: list[str] = [
|
|
464
|
+
"EventBusSubcontractWiring",
|
|
465
|
+
"load_event_bus_subcontract",
|
|
466
|
+
]
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"""Handler Source Resolver for Multi-Source Handler Discovery.
|
|
4
4
|
|
|
5
5
|
This module provides the HandlerSourceResolver class, which resolves handlers
|
|
6
|
-
from multiple sources (bootstrap, contract) based on the configured mode.
|
|
6
|
+
from multiple sources (bootstrap, contract, Kafka events) based on the configured mode.
|
|
7
7
|
|
|
8
8
|
Part of OMN-1095: Handler Source Mode Hybrid Resolution.
|
|
9
9
|
|
|
@@ -11,6 +11,7 @@ Resolution Modes:
|
|
|
11
11
|
- BOOTSTRAP: Only use hardcoded bootstrap handlers.
|
|
12
12
|
- CONTRACT: Only use YAML contract-discovered handlers.
|
|
13
13
|
- HYBRID: Per-handler resolution with configurable precedence.
|
|
14
|
+
- KAFKA_EVENTS: Use Kafka-based contract source for cache-based discovery.
|
|
14
15
|
|
|
15
16
|
In HYBRID mode, the resolver performs per-handler identity resolution:
|
|
16
17
|
1. Discovers handlers from both bootstrap and contract sources
|
|
@@ -20,10 +21,16 @@ In HYBRID mode, the resolver performs per-handler identity resolution:
|
|
|
20
21
|
- True: Bootstrap handlers override contract handlers
|
|
21
22
|
4. Non-conflicting handlers are included from both sources
|
|
22
23
|
|
|
24
|
+
In KAFKA_EVENTS mode, the resolver delegates to a KafkaContractSource instance
|
|
25
|
+
that returns cached descriptors from contract registration events. This is a
|
|
26
|
+
beta cache-only implementation where discovered contracts take effect on the
|
|
27
|
+
next runtime restart.
|
|
28
|
+
|
|
23
29
|
See Also:
|
|
24
30
|
- EnumHandlerSourceMode: Defines the resolution modes
|
|
25
31
|
- HandlerBootstrapSource: Provides bootstrap handlers
|
|
26
32
|
- HandlerContractSource: Provides contract-discovered handlers
|
|
33
|
+
- KafkaContractSource: Provides Kafka cache-based handler discovery
|
|
27
34
|
- ProtocolContractSource: Protocol for handler sources
|
|
28
35
|
|
|
29
36
|
.. versionadded:: 0.7.0
|
|
@@ -60,7 +67,7 @@ class HandlerSourceResolver:
|
|
|
60
67
|
"""Resolver for multi-source handler discovery with configurable modes.
|
|
61
68
|
|
|
62
69
|
This class resolves handlers from bootstrap and contract sources based on
|
|
63
|
-
the configured mode. It supports
|
|
70
|
+
the configured mode. It supports four resolution strategies:
|
|
64
71
|
|
|
65
72
|
- BOOTSTRAP: Use only bootstrap handlers, ignore contracts.
|
|
66
73
|
- CONTRACT: Use only contract handlers, ignore bootstrap.
|
|
@@ -70,6 +77,9 @@ class HandlerSourceResolver:
|
|
|
70
77
|
- allow_bootstrap_override=True: Bootstrap handlers take precedence
|
|
71
78
|
over contract handlers with the same handler_id.
|
|
72
79
|
In both cases, handlers without conflicts are included from both sources.
|
|
80
|
+
- KAFKA_EVENTS: Use Kafka-based contract source for cache-based discovery.
|
|
81
|
+
Delegates to a KafkaContractSource that returns cached descriptors from
|
|
82
|
+
contract registration events. This is a beta cache-only implementation.
|
|
73
83
|
|
|
74
84
|
Attributes:
|
|
75
85
|
mode: The configured resolution mode.
|
|
@@ -142,6 +152,7 @@ class HandlerSourceResolver:
|
|
|
142
152
|
- BOOTSTRAP: Only queries bootstrap source
|
|
143
153
|
- CONTRACT: Only queries contract source
|
|
144
154
|
- HYBRID: Queries both sources and merges with contract precedence
|
|
155
|
+
- KAFKA_EVENTS: Queries Kafka-based contract cache
|
|
145
156
|
|
|
146
157
|
Returns:
|
|
147
158
|
ModelContractDiscoveryResult: Container with discovered descriptors
|
|
@@ -151,6 +162,8 @@ class HandlerSourceResolver:
|
|
|
151
162
|
return await self._resolve_bootstrap()
|
|
152
163
|
elif self._mode == EnumHandlerSourceMode.CONTRACT:
|
|
153
164
|
return await self._resolve_contract()
|
|
165
|
+
elif self._mode == EnumHandlerSourceMode.KAFKA_EVENTS:
|
|
166
|
+
return await self._resolve_kafka_events()
|
|
154
167
|
else:
|
|
155
168
|
# HYBRID mode
|
|
156
169
|
return await self._resolve_hybrid()
|
|
@@ -193,6 +206,34 @@ class HandlerSourceResolver:
|
|
|
193
206
|
|
|
194
207
|
return result
|
|
195
208
|
|
|
209
|
+
async def _resolve_kafka_events(self) -> ModelContractDiscoveryResult:
|
|
210
|
+
"""Resolve handlers using Kafka-based contract source.
|
|
211
|
+
|
|
212
|
+
For KAFKA_EVENTS mode, the contract_source is expected to be a
|
|
213
|
+
KafkaContractSource instance that returns cached descriptors from
|
|
214
|
+
contract registration events.
|
|
215
|
+
|
|
216
|
+
Note:
|
|
217
|
+
This is a beta cache-only implementation. Discovered contracts
|
|
218
|
+
take effect on the next runtime restart.
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
ModelContractDiscoveryResult: Discovery result from the Kafka
|
|
222
|
+
contract cache.
|
|
223
|
+
"""
|
|
224
|
+
result = await self._contract_source.discover_handlers()
|
|
225
|
+
|
|
226
|
+
logger.info(
|
|
227
|
+
"Handler resolution completed (KAFKA_EVENTS mode)",
|
|
228
|
+
extra={
|
|
229
|
+
"mode": self._mode.value,
|
|
230
|
+
"kafka_handler_count": len(result.descriptors),
|
|
231
|
+
"resolved_handler_count": len(result.descriptors),
|
|
232
|
+
},
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
return result
|
|
236
|
+
|
|
196
237
|
async def _resolve_hybrid(self) -> ModelContractDiscoveryResult:
|
|
197
238
|
"""Resolve handlers using both sources with configurable precedence.
|
|
198
239
|
|