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
|
@@ -46,20 +46,25 @@ import asyncio
|
|
|
46
46
|
import importlib
|
|
47
47
|
import json
|
|
48
48
|
import logging
|
|
49
|
+
import os
|
|
49
50
|
from collections.abc import Awaitable, Callable
|
|
50
51
|
from pathlib import Path
|
|
51
|
-
from typing import TYPE_CHECKING
|
|
52
|
+
from typing import TYPE_CHECKING, cast
|
|
52
53
|
from uuid import UUID, uuid4
|
|
53
54
|
|
|
54
55
|
from pydantic import BaseModel
|
|
55
56
|
|
|
56
57
|
from omnibase_infra.enums import (
|
|
58
|
+
EnumConsumerGroupPurpose,
|
|
57
59
|
EnumHandlerSourceMode,
|
|
58
60
|
EnumHandlerTypeCategory,
|
|
59
61
|
EnumInfraTransportType,
|
|
60
62
|
)
|
|
61
63
|
from omnibase_infra.errors import (
|
|
62
64
|
EnvelopeValidationError,
|
|
65
|
+
InfraConsulError,
|
|
66
|
+
InfraTimeoutError,
|
|
67
|
+
InfraUnavailableError,
|
|
63
68
|
ModelInfraErrorContext,
|
|
64
69
|
ProtocolConfigurationError,
|
|
65
70
|
RuntimeHostError,
|
|
@@ -67,14 +72,22 @@ from omnibase_infra.errors import (
|
|
|
67
72
|
)
|
|
68
73
|
from omnibase_infra.event_bus.event_bus_inmemory import EventBusInmemory
|
|
69
74
|
from omnibase_infra.event_bus.event_bus_kafka import EventBusKafka
|
|
75
|
+
from omnibase_infra.models import ModelNodeIdentity
|
|
70
76
|
from omnibase_infra.runtime.envelope_validator import (
|
|
71
77
|
normalize_correlation_id,
|
|
72
78
|
validate_envelope,
|
|
73
79
|
)
|
|
74
80
|
from omnibase_infra.runtime.handler_registry import RegistryProtocolBinding
|
|
75
|
-
from omnibase_infra.runtime.models import
|
|
81
|
+
from omnibase_infra.runtime.models import (
|
|
82
|
+
ModelDuplicateResponse,
|
|
83
|
+
ModelRuntimeContractConfig,
|
|
84
|
+
)
|
|
76
85
|
from omnibase_infra.runtime.protocol_lifecycle_executor import ProtocolLifecycleExecutor
|
|
86
|
+
from omnibase_infra.runtime.runtime_contract_config_loader import (
|
|
87
|
+
RuntimeContractConfigLoader,
|
|
88
|
+
)
|
|
77
89
|
from omnibase_infra.runtime.util_wiring import wire_default_handlers
|
|
90
|
+
from omnibase_infra.utils.util_consumer_group import compute_consumer_group_id
|
|
78
91
|
from omnibase_infra.utils.util_env_parsing import parse_env_float
|
|
79
92
|
|
|
80
93
|
if TYPE_CHECKING:
|
|
@@ -90,8 +103,14 @@ if TYPE_CHECKING:
|
|
|
90
103
|
from omnibase_infra.runtime.contract_handler_discovery import (
|
|
91
104
|
ContractHandlerDiscovery,
|
|
92
105
|
)
|
|
106
|
+
from omnibase_infra.runtime.service_message_dispatch_engine import (
|
|
107
|
+
MessageDispatchEngine,
|
|
108
|
+
)
|
|
93
109
|
|
|
94
110
|
# Imports for PluginLoaderContractSource adapter class
|
|
111
|
+
from omnibase_core.protocols.event_bus.protocol_event_bus_subscriber import (
|
|
112
|
+
ProtocolEventBusSubscriber,
|
|
113
|
+
)
|
|
95
114
|
from omnibase_infra.models.errors import ModelHandlerValidationError
|
|
96
115
|
from omnibase_infra.models.handlers import (
|
|
97
116
|
LiteralHandlerKind,
|
|
@@ -99,11 +118,20 @@ from omnibase_infra.models.handlers import (
|
|
|
99
118
|
ModelHandlerDescriptor,
|
|
100
119
|
)
|
|
101
120
|
from omnibase_infra.models.types import JsonDict
|
|
121
|
+
from omnibase_infra.runtime.event_bus_subcontract_wiring import (
|
|
122
|
+
EventBusSubcontractWiring,
|
|
123
|
+
load_event_bus_subcontract,
|
|
124
|
+
)
|
|
102
125
|
from omnibase_infra.runtime.handler_identity import (
|
|
103
126
|
HANDLER_IDENTITY_PREFIX,
|
|
104
127
|
handler_identity,
|
|
105
128
|
)
|
|
106
129
|
from omnibase_infra.runtime.handler_plugin_loader import HandlerPluginLoader
|
|
130
|
+
from omnibase_infra.runtime.kafka_contract_source import (
|
|
131
|
+
TOPIC_SUFFIX_CONTRACT_DEREGISTERED,
|
|
132
|
+
TOPIC_SUFFIX_CONTRACT_REGISTERED,
|
|
133
|
+
KafkaContractSource,
|
|
134
|
+
)
|
|
107
135
|
from omnibase_infra.runtime.protocol_contract_source import ProtocolContractSource
|
|
108
136
|
|
|
109
137
|
# Expose wire_default_handlers as wire_handlers for test patching compatibility
|
|
@@ -159,6 +187,51 @@ DEFAULT_DRAIN_TIMEOUT_SECONDS: float = parse_env_float(
|
|
|
159
187
|
)
|
|
160
188
|
|
|
161
189
|
|
|
190
|
+
def _parse_contract_event_payload(
|
|
191
|
+
msg: ModelEventMessage,
|
|
192
|
+
) -> tuple[dict[str, object], UUID] | None:
|
|
193
|
+
"""Parse contract event message payload and extract correlation ID.
|
|
194
|
+
|
|
195
|
+
This helper extracts common JSON parsing and correlation ID extraction logic
|
|
196
|
+
used by contract registration and deregistration handlers.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
msg: The event message to parse.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
A tuple of (payload_dict, correlation_id) if message has a value,
|
|
203
|
+
None if message value is empty.
|
|
204
|
+
|
|
205
|
+
Raises:
|
|
206
|
+
json.JSONDecodeError: If the message value is not valid JSON.
|
|
207
|
+
UnicodeDecodeError: If the message value cannot be decoded as UTF-8.
|
|
208
|
+
|
|
209
|
+
Note:
|
|
210
|
+
This function is intentionally a module-level utility rather than a
|
|
211
|
+
class method because it performs pure data transformation without
|
|
212
|
+
requiring any class state.
|
|
213
|
+
|
|
214
|
+
.. versionadded:: 0.8.0
|
|
215
|
+
Created for OMN-1654 to reduce duplication in contract event handlers.
|
|
216
|
+
"""
|
|
217
|
+
if not msg.value:
|
|
218
|
+
return None
|
|
219
|
+
|
|
220
|
+
payload: dict[str, object] = json.loads(msg.value.decode("utf-8"))
|
|
221
|
+
|
|
222
|
+
# Extract correlation ID from headers if available, or generate new
|
|
223
|
+
correlation_id: UUID
|
|
224
|
+
if msg.headers and msg.headers.correlation_id:
|
|
225
|
+
try:
|
|
226
|
+
correlation_id = UUID(str(msg.headers.correlation_id))
|
|
227
|
+
except (ValueError, TypeError):
|
|
228
|
+
correlation_id = uuid4()
|
|
229
|
+
else:
|
|
230
|
+
correlation_id = uuid4()
|
|
231
|
+
|
|
232
|
+
return (payload, correlation_id)
|
|
233
|
+
|
|
234
|
+
|
|
162
235
|
class PluginLoaderContractSource(ProtocolContractSource):
|
|
163
236
|
"""Adapter that uses HandlerPluginLoader for contract discovery.
|
|
164
237
|
|
|
@@ -526,6 +599,9 @@ class RuntimeHostProcess:
|
|
|
526
599
|
# Handler discovery service (lazy-created if contract_paths provided)
|
|
527
600
|
self._handler_discovery: ContractHandlerDiscovery | None = None
|
|
528
601
|
|
|
602
|
+
# Kafka contract source (created if KAFKA_EVENTS mode, wired separately)
|
|
603
|
+
self._kafka_contract_source: KafkaContractSource | None = None
|
|
604
|
+
|
|
529
605
|
# Create or use provided event bus
|
|
530
606
|
self._event_bus: EventBusInmemory | EventBusKafka = (
|
|
531
607
|
event_bus or EventBusInmemory()
|
|
@@ -537,16 +613,40 @@ class RuntimeHostProcess:
|
|
|
537
613
|
# Topic configuration (config overrides constructor args)
|
|
538
614
|
self._input_topic: str = str(config.get("input_topic", input_topic))
|
|
539
615
|
self._output_topic: str = str(config.get("output_topic", output_topic))
|
|
540
|
-
|
|
541
|
-
#
|
|
542
|
-
#
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
616
|
+
|
|
617
|
+
# Node identity configuration (required for consumer group derivation)
|
|
618
|
+
# Extract components from config - fail-fast if required fields are missing
|
|
619
|
+
_env = config.get("env")
|
|
620
|
+
env: str = str(_env).strip() if _env else "local"
|
|
621
|
+
|
|
622
|
+
_service_name = config.get("service_name")
|
|
623
|
+
if not _service_name or not str(_service_name).strip():
|
|
624
|
+
raise ValueError(
|
|
625
|
+
"RuntimeHostProcess requires 'service_name' in config. "
|
|
626
|
+
"This is the service name from your node's contract (e.g., 'omniintelligence'). "
|
|
627
|
+
"Cannot infer service_name - please provide it explicitly."
|
|
628
|
+
)
|
|
629
|
+
service_name: str = str(_service_name).strip()
|
|
630
|
+
|
|
631
|
+
_node_name = config.get("node_name")
|
|
632
|
+
if not _node_name or not str(_node_name).strip():
|
|
633
|
+
raise ValueError(
|
|
634
|
+
"RuntimeHostProcess requires 'node_name' in config. "
|
|
635
|
+
"This is the node name from your contract (e.g., 'claude_hook_event_effect'). "
|
|
636
|
+
"Cannot infer node_name - please provide it explicitly."
|
|
637
|
+
)
|
|
638
|
+
node_name: str = str(_node_name).strip()
|
|
639
|
+
|
|
640
|
+
_version = config.get("version")
|
|
641
|
+
version: str = (
|
|
642
|
+
str(_version).strip() if _version and str(_version).strip() else "v1"
|
|
643
|
+
)
|
|
644
|
+
|
|
645
|
+
self._node_identity: ModelNodeIdentity = ModelNodeIdentity(
|
|
646
|
+
env=env,
|
|
647
|
+
service=service_name,
|
|
648
|
+
node_name=node_name,
|
|
649
|
+
version=version,
|
|
550
650
|
)
|
|
551
651
|
|
|
552
652
|
# Health check configuration (from lifecycle subcontract pattern)
|
|
@@ -678,12 +778,32 @@ class RuntimeHostProcess:
|
|
|
678
778
|
self._idempotency_store: ProtocolIdempotencyStore | None = None
|
|
679
779
|
self._idempotency_config: ModelIdempotencyGuardConfig | None = None
|
|
680
780
|
|
|
781
|
+
# Event bus subcontract wiring for contract-driven subscriptions (OMN-1621)
|
|
782
|
+
# Bridges contract-declared topics to Kafka subscriptions.
|
|
783
|
+
# None until wired during start() when dispatch_engine is available.
|
|
784
|
+
self._event_bus_wiring: EventBusSubcontractWiring | None = None
|
|
785
|
+
|
|
786
|
+
# Message dispatch engine for routing received messages (OMN-1621)
|
|
787
|
+
# Used by event_bus_wiring to dispatch messages to handlers.
|
|
788
|
+
# None = not configured, wiring will be skipped
|
|
789
|
+
self._dispatch_engine: MessageDispatchEngine | None = None
|
|
790
|
+
|
|
791
|
+
# Baseline subscriptions for platform-reserved topics (OMN-1654)
|
|
792
|
+
# Stores unsubscribe callbacks for contract registration/deregistration topics.
|
|
793
|
+
# Wired when KAFKA_EVENTS mode is active with a KafkaContractSource.
|
|
794
|
+
self._baseline_subscriptions: list[Callable[[], Awaitable[None]]] = []
|
|
795
|
+
|
|
796
|
+
# Contract configuration loaded at startup (OMN-1519)
|
|
797
|
+
# Contains consolidated handler_routing and operation_bindings from all contracts.
|
|
798
|
+
# None until loaded during start() via _load_contract_configs()
|
|
799
|
+
self._contract_config: ModelRuntimeContractConfig | None = None
|
|
800
|
+
|
|
681
801
|
logger.debug(
|
|
682
802
|
"RuntimeHostProcess initialized",
|
|
683
803
|
extra={
|
|
684
804
|
"input_topic": self._input_topic,
|
|
685
805
|
"output_topic": self._output_topic,
|
|
686
|
-
"group_id": self.
|
|
806
|
+
"group_id": self.group_id,
|
|
687
807
|
"health_check_timeout_seconds": self._health_check_timeout_seconds,
|
|
688
808
|
"drain_timeout_seconds": self._drain_timeout_seconds,
|
|
689
809
|
"has_container": self._container is not None,
|
|
@@ -702,6 +822,31 @@ class RuntimeHostProcess:
|
|
|
702
822
|
"""
|
|
703
823
|
return self._container
|
|
704
824
|
|
|
825
|
+
@property
|
|
826
|
+
def contract_config(self) -> ModelRuntimeContractConfig | None:
|
|
827
|
+
"""Return the loaded contract configuration.
|
|
828
|
+
|
|
829
|
+
Contains consolidated handler_routing and operation_bindings from all
|
|
830
|
+
contracts discovered during startup. Returns None if contracts have
|
|
831
|
+
not been loaded yet (before start() is called).
|
|
832
|
+
|
|
833
|
+
The contract config provides access to:
|
|
834
|
+
- handler_routing_configs: All loaded handler routing configurations
|
|
835
|
+
- operation_bindings_configs: All loaded operation bindings
|
|
836
|
+
- success_rate: Ratio of successfully loaded contracts
|
|
837
|
+
- error_messages: Any errors encountered during loading
|
|
838
|
+
|
|
839
|
+
Returns:
|
|
840
|
+
ModelRuntimeContractConfig if loaded, None if not yet loaded.
|
|
841
|
+
|
|
842
|
+
Example:
|
|
843
|
+
>>> process = RuntimeHostProcess(...)
|
|
844
|
+
>>> await process.start()
|
|
845
|
+
>>> if process.contract_config:
|
|
846
|
+
... print(f"Loaded {process.contract_config.total_contracts_loaded} contracts")
|
|
847
|
+
"""
|
|
848
|
+
return self._contract_config
|
|
849
|
+
|
|
705
850
|
@property
|
|
706
851
|
def event_bus(self) -> EventBusInmemory | EventBusKafka:
|
|
707
852
|
"""Return the owned event bus instance.
|
|
@@ -742,10 +887,28 @@ class RuntimeHostProcess:
|
|
|
742
887
|
def group_id(self) -> str:
|
|
743
888
|
"""Return the consumer group identifier.
|
|
744
889
|
|
|
890
|
+
Computes the consumer group ID from the node identity using the canonical
|
|
891
|
+
format: ``{env}.{service}.{node_name}.{purpose}.{version}``
|
|
892
|
+
|
|
893
|
+
Returns:
|
|
894
|
+
The computed consumer group ID for this process.
|
|
895
|
+
"""
|
|
896
|
+
return compute_consumer_group_id(
|
|
897
|
+
self._node_identity, EnumConsumerGroupPurpose.CONSUME
|
|
898
|
+
)
|
|
899
|
+
|
|
900
|
+
@property
|
|
901
|
+
def node_identity(self) -> ModelNodeIdentity:
|
|
902
|
+
"""Return the node identity used for consumer group derivation.
|
|
903
|
+
|
|
904
|
+
The node identity contains the environment, service name, node name,
|
|
905
|
+
and version that uniquely identify this runtime host process within
|
|
906
|
+
the ONEX infrastructure.
|
|
907
|
+
|
|
745
908
|
Returns:
|
|
746
|
-
The
|
|
909
|
+
The immutable node identity for this process.
|
|
747
910
|
"""
|
|
748
|
-
return self.
|
|
911
|
+
return self._node_identity
|
|
749
912
|
|
|
750
913
|
@property
|
|
751
914
|
def is_draining(self) -> bool:
|
|
@@ -877,7 +1040,7 @@ class RuntimeHostProcess:
|
|
|
877
1040
|
extra={
|
|
878
1041
|
"input_topic": self._input_topic,
|
|
879
1042
|
"output_topic": self._output_topic,
|
|
880
|
-
"group_id": self.
|
|
1043
|
+
"group_id": self.group_id,
|
|
881
1044
|
"has_contract_paths": len(self._contract_paths) > 0,
|
|
882
1045
|
},
|
|
883
1046
|
)
|
|
@@ -980,14 +1143,32 @@ class RuntimeHostProcess:
|
|
|
980
1143
|
registry_protocol_count=registry_protocol_count,
|
|
981
1144
|
)
|
|
982
1145
|
|
|
1146
|
+
# Step 4.15: Load contract configurations (OMN-1519)
|
|
1147
|
+
# Loads handler_routing and operation_bindings from all discovered contracts.
|
|
1148
|
+
# Uses the same contract_paths configured for handler discovery.
|
|
1149
|
+
# The loaded config is accessible via self.contract_config property.
|
|
1150
|
+
startup_correlation_id = uuid4()
|
|
1151
|
+
await self._load_contract_configs(correlation_id=startup_correlation_id)
|
|
1152
|
+
|
|
1153
|
+
# Step 4.2: Wire event bus subscriptions from contracts (OMN-1621)
|
|
1154
|
+
# This bridges contract-declared topics to Kafka subscriptions.
|
|
1155
|
+
# Requires dispatch_engine to be available for message routing.
|
|
1156
|
+
await self._wire_event_bus_subscriptions()
|
|
1157
|
+
|
|
1158
|
+
# Step 4.3: Wire baseline subscriptions for contract discovery (OMN-1654)
|
|
1159
|
+
# When KAFKA_EVENTS mode is active, subscribe to platform-reserved
|
|
1160
|
+
# contract topics to receive registration/deregistration events.
|
|
1161
|
+
await self._wire_baseline_subscriptions()
|
|
1162
|
+
|
|
983
1163
|
# Step 4.5: Initialize idempotency store if configured (OMN-945)
|
|
984
1164
|
await self._initialize_idempotency_store()
|
|
985
1165
|
|
|
986
1166
|
# Step 5: Subscribe to input topic
|
|
987
1167
|
self._subscription = await self._event_bus.subscribe(
|
|
988
1168
|
topic=self._input_topic,
|
|
989
|
-
|
|
1169
|
+
node_identity=self._node_identity,
|
|
990
1170
|
on_message=self._on_message,
|
|
1171
|
+
purpose=EnumConsumerGroupPurpose.CONSUME,
|
|
991
1172
|
)
|
|
992
1173
|
|
|
993
1174
|
self._is_running = True
|
|
@@ -997,7 +1178,7 @@ class RuntimeHostProcess:
|
|
|
997
1178
|
extra={
|
|
998
1179
|
"input_topic": self._input_topic,
|
|
999
1180
|
"output_topic": self._output_topic,
|
|
1000
|
-
"group_id": self.
|
|
1181
|
+
"group_id": self.group_id,
|
|
1001
1182
|
"registered_handlers": list(self._handlers.keys()),
|
|
1002
1183
|
},
|
|
1003
1184
|
)
|
|
@@ -1148,6 +1329,26 @@ class RuntimeHostProcess:
|
|
|
1148
1329
|
# Step 2.5: Cleanup idempotency store if initialized (OMN-945)
|
|
1149
1330
|
await self._cleanup_idempotency_store()
|
|
1150
1331
|
|
|
1332
|
+
# Step 2.6: Cleanup event bus subcontract wiring (OMN-1621)
|
|
1333
|
+
if self._event_bus_wiring:
|
|
1334
|
+
await self._event_bus_wiring.cleanup()
|
|
1335
|
+
|
|
1336
|
+
# Step 2.7: Cleanup baseline subscriptions for contract discovery (OMN-1654)
|
|
1337
|
+
if self._baseline_subscriptions:
|
|
1338
|
+
for unsubscribe in self._baseline_subscriptions:
|
|
1339
|
+
try:
|
|
1340
|
+
await unsubscribe()
|
|
1341
|
+
except Exception as e:
|
|
1342
|
+
logger.warning(
|
|
1343
|
+
"Failed to unsubscribe baseline subscription",
|
|
1344
|
+
extra={"error": str(e)},
|
|
1345
|
+
)
|
|
1346
|
+
self._baseline_subscriptions.clear()
|
|
1347
|
+
logger.debug("Baseline contract subscriptions cleaned up")
|
|
1348
|
+
|
|
1349
|
+
# Step 2.8: Nullify KafkaContractSource reference for proper cleanup (OMN-1654)
|
|
1350
|
+
self._kafka_contract_source = None
|
|
1351
|
+
|
|
1151
1352
|
# Step 3: Close event bus
|
|
1152
1353
|
await self._event_bus.close()
|
|
1153
1354
|
|
|
@@ -1295,12 +1496,34 @@ class RuntimeHostProcess:
|
|
|
1295
1496
|
# Create bootstrap source
|
|
1296
1497
|
bootstrap_source = HandlerBootstrapSource()
|
|
1297
1498
|
|
|
1499
|
+
# Check for KAFKA_EVENTS mode first
|
|
1500
|
+
if source_config.effective_mode == EnumHandlerSourceMode.KAFKA_EVENTS:
|
|
1501
|
+
# Create Kafka-based contract source (cache-only beta)
|
|
1502
|
+
# Note: Kafka subscriptions are wired separately in _wire_baseline_subscriptions()
|
|
1503
|
+
environment = self._get_environment_from_config()
|
|
1504
|
+
kafka_source = KafkaContractSource(
|
|
1505
|
+
environment=environment,
|
|
1506
|
+
graceful_mode=True,
|
|
1507
|
+
)
|
|
1508
|
+
contract_source: ProtocolContractSource = kafka_source
|
|
1509
|
+
|
|
1510
|
+
# Store reference for subscription wiring
|
|
1511
|
+
self._kafka_contract_source = kafka_source
|
|
1512
|
+
|
|
1513
|
+
logger.info(
|
|
1514
|
+
"Using KafkaContractSource for contract discovery",
|
|
1515
|
+
extra={
|
|
1516
|
+
"environment": environment,
|
|
1517
|
+
"mode": "KAFKA_EVENTS",
|
|
1518
|
+
"correlation_id": str(kafka_source.correlation_id),
|
|
1519
|
+
},
|
|
1520
|
+
)
|
|
1298
1521
|
# Contract source needs paths - use configured paths or default
|
|
1299
1522
|
# If no contract_paths provided, reuse bootstrap_source as placeholder
|
|
1300
|
-
|
|
1523
|
+
elif self._contract_paths:
|
|
1301
1524
|
# Use PluginLoaderContractSource which uses the simpler contract schema
|
|
1302
1525
|
# compatible with test contracts (handler_name, handler_class, handler_type)
|
|
1303
|
-
contract_source
|
|
1526
|
+
contract_source = PluginLoaderContractSource(
|
|
1304
1527
|
contract_paths=self._contract_paths,
|
|
1305
1528
|
)
|
|
1306
1529
|
else:
|
|
@@ -1667,6 +1890,77 @@ class RuntimeHostProcess:
|
|
|
1667
1890
|
},
|
|
1668
1891
|
)
|
|
1669
1892
|
|
|
1893
|
+
async def _load_contract_configs(self, correlation_id: UUID) -> None:
|
|
1894
|
+
"""Load contract configurations from all discovered contracts.
|
|
1895
|
+
|
|
1896
|
+
Uses RuntimeContractConfigLoader to scan for contract.yaml files and
|
|
1897
|
+
load handler_routing and operation_bindings subcontracts into a
|
|
1898
|
+
consolidated configuration.
|
|
1899
|
+
|
|
1900
|
+
This method is called during start() after handler discovery but before
|
|
1901
|
+
event bus subscriptions are wired. The loaded config is stored in
|
|
1902
|
+
self._contract_config and accessible via the contract_config property.
|
|
1903
|
+
|
|
1904
|
+
Error Handling:
|
|
1905
|
+
Individual contract load failures are logged but do not stop the
|
|
1906
|
+
overall loading process. This enables graceful degradation where
|
|
1907
|
+
some contracts can be loaded even if others fail. Errors are
|
|
1908
|
+
collected in the ModelRuntimeContractConfig for introspection.
|
|
1909
|
+
|
|
1910
|
+
Args:
|
|
1911
|
+
correlation_id: Correlation ID for tracing this load operation.
|
|
1912
|
+
|
|
1913
|
+
Part of OMN-1519: Runtime contract config loader integration.
|
|
1914
|
+
"""
|
|
1915
|
+
# Skip if no contract paths configured
|
|
1916
|
+
if not self._contract_paths:
|
|
1917
|
+
logger.debug(
|
|
1918
|
+
"No contract paths configured, skipping contract config loading",
|
|
1919
|
+
extra={"correlation_id": str(correlation_id)},
|
|
1920
|
+
)
|
|
1921
|
+
return
|
|
1922
|
+
|
|
1923
|
+
# Create loader - no namespace restrictions by default
|
|
1924
|
+
# (namespace allowlisting can be added via constructor parameter if needed)
|
|
1925
|
+
loader = RuntimeContractConfigLoader()
|
|
1926
|
+
|
|
1927
|
+
# Load all contracts from configured paths
|
|
1928
|
+
self._contract_config = loader.load_all_contracts(
|
|
1929
|
+
search_paths=self._contract_paths,
|
|
1930
|
+
correlation_id=correlation_id,
|
|
1931
|
+
)
|
|
1932
|
+
|
|
1933
|
+
# Log summary at INFO level
|
|
1934
|
+
if self._contract_config.total_errors > 0:
|
|
1935
|
+
logger.warning(
|
|
1936
|
+
"Contract config loading completed with errors",
|
|
1937
|
+
extra={
|
|
1938
|
+
"total_contracts_found": self._contract_config.total_contracts_found,
|
|
1939
|
+
"total_contracts_loaded": self._contract_config.total_contracts_loaded,
|
|
1940
|
+
"total_errors": self._contract_config.total_errors,
|
|
1941
|
+
"success_rate": f"{self._contract_config.success_rate:.1%}",
|
|
1942
|
+
"correlation_id": str(correlation_id),
|
|
1943
|
+
"error_paths": [
|
|
1944
|
+
str(p) for p in self._contract_config.error_messages
|
|
1945
|
+
],
|
|
1946
|
+
},
|
|
1947
|
+
)
|
|
1948
|
+
else:
|
|
1949
|
+
logger.info(
|
|
1950
|
+
"Contract config loading completed successfully",
|
|
1951
|
+
extra={
|
|
1952
|
+
"total_contracts_found": self._contract_config.total_contracts_found,
|
|
1953
|
+
"total_contracts_loaded": self._contract_config.total_contracts_loaded,
|
|
1954
|
+
"handler_routing_count": len(
|
|
1955
|
+
self._contract_config.handler_routing_configs
|
|
1956
|
+
),
|
|
1957
|
+
"operation_bindings_count": len(
|
|
1958
|
+
self._contract_config.operation_bindings_configs
|
|
1959
|
+
),
|
|
1960
|
+
"correlation_id": str(correlation_id),
|
|
1961
|
+
},
|
|
1962
|
+
)
|
|
1963
|
+
|
|
1670
1964
|
async def _get_handler_registry(self) -> RegistryProtocolBinding:
|
|
1671
1965
|
"""Get handler registry (pre-resolved, container, or singleton).
|
|
1672
1966
|
|
|
@@ -2213,6 +2507,171 @@ class RuntimeHostProcess:
|
|
|
2213
2507
|
"""
|
|
2214
2508
|
return self._handlers.get(handler_type)
|
|
2215
2509
|
|
|
2510
|
+
async def get_subscribers_for_topic(self, topic: str) -> list[UUID]:
|
|
2511
|
+
"""Query Consul for node IDs that subscribe to a topic.
|
|
2512
|
+
|
|
2513
|
+
This method provides dynamic topic-to-subscriber lookup via Consul KV store.
|
|
2514
|
+
Topics are stored at `onex/topics/{topic}/subscribers` and contain a JSON
|
|
2515
|
+
array of node UUID strings.
|
|
2516
|
+
|
|
2517
|
+
Args:
|
|
2518
|
+
topic: Environment-qualified topic string
|
|
2519
|
+
(e.g., "dev.onex.evt.intent-classified.v1")
|
|
2520
|
+
|
|
2521
|
+
Returns:
|
|
2522
|
+
List of node UUIDs that subscribe to this topic.
|
|
2523
|
+
Empty list if no subscribers registered or Consul unavailable.
|
|
2524
|
+
|
|
2525
|
+
Note:
|
|
2526
|
+
Returns node IDs, not handler names. Node ID is the stable registry key.
|
|
2527
|
+
Handler selection is a separate concern that can change independently.
|
|
2528
|
+
|
|
2529
|
+
Example:
|
|
2530
|
+
>>> runtime = RuntimeHostProcess()
|
|
2531
|
+
>>> await runtime.start()
|
|
2532
|
+
>>> subscribers = await runtime.get_subscribers_for_topic(
|
|
2533
|
+
... "dev.onex.evt.intent-classified.v1"
|
|
2534
|
+
... )
|
|
2535
|
+
>>> print(subscribers) # [UUID('abc123...'), UUID('def456...')]
|
|
2536
|
+
|
|
2537
|
+
Related:
|
|
2538
|
+
- OMN-1613: Add event bus topic storage to registry for dynamic topic discovery
|
|
2539
|
+
- MixinConsulTopicIndex: Consul mixin that manages topic index storage
|
|
2540
|
+
"""
|
|
2541
|
+
consul_handler = self.get_handler("consul")
|
|
2542
|
+
if consul_handler is None:
|
|
2543
|
+
logger.debug(
|
|
2544
|
+
"Consul handler not available for topic subscriber lookup",
|
|
2545
|
+
extra={"topic": topic},
|
|
2546
|
+
)
|
|
2547
|
+
return []
|
|
2548
|
+
|
|
2549
|
+
try:
|
|
2550
|
+
correlation_id = uuid4()
|
|
2551
|
+
envelope: dict[str, object] = {
|
|
2552
|
+
"operation": "consul.kv_get",
|
|
2553
|
+
"payload": {"key": f"onex/topics/{topic}/subscribers"},
|
|
2554
|
+
"correlation_id": str(correlation_id),
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2557
|
+
# Execute the Consul KV get operation
|
|
2558
|
+
# NOTE: MVP adapters use legacy execute(envelope: dict) signature.
|
|
2559
|
+
result = await consul_handler.execute(envelope) # type: ignore[call-arg]
|
|
2560
|
+
|
|
2561
|
+
# Navigate to the value in the response structure:
|
|
2562
|
+
# ModelHandlerOutput -> result (ModelConsulHandlerResponse)
|
|
2563
|
+
# -> payload (ModelConsulHandlerPayload) -> data (ConsulPayload)
|
|
2564
|
+
if result is None:
|
|
2565
|
+
return []
|
|
2566
|
+
|
|
2567
|
+
# Check if result has the expected structure
|
|
2568
|
+
if not hasattr(result, "result") or result.result is None:
|
|
2569
|
+
return []
|
|
2570
|
+
|
|
2571
|
+
response = result.result
|
|
2572
|
+
if not hasattr(response, "payload") or response.payload is None:
|
|
2573
|
+
return []
|
|
2574
|
+
|
|
2575
|
+
payload_data = response.payload.data
|
|
2576
|
+
if payload_data is None:
|
|
2577
|
+
return []
|
|
2578
|
+
|
|
2579
|
+
# Check for "not found" response - key doesn't exist
|
|
2580
|
+
if hasattr(payload_data, "found") and payload_data.found is False:
|
|
2581
|
+
return []
|
|
2582
|
+
|
|
2583
|
+
# Get the value field from the payload
|
|
2584
|
+
value = getattr(payload_data, "value", None)
|
|
2585
|
+
if not value:
|
|
2586
|
+
return []
|
|
2587
|
+
|
|
2588
|
+
# Parse JSON array of node ID strings
|
|
2589
|
+
node_ids_raw = json.loads(value)
|
|
2590
|
+
if not isinstance(node_ids_raw, list):
|
|
2591
|
+
logger.warning(
|
|
2592
|
+
"Topic subscriber value is not a list",
|
|
2593
|
+
extra={
|
|
2594
|
+
"topic": topic,
|
|
2595
|
+
"correlation_id": str(correlation_id),
|
|
2596
|
+
"value_type": type(node_ids_raw).__name__,
|
|
2597
|
+
},
|
|
2598
|
+
)
|
|
2599
|
+
return []
|
|
2600
|
+
|
|
2601
|
+
# Convert string UUIDs to UUID objects (skip invalid entries)
|
|
2602
|
+
subscribers: list[UUID] = []
|
|
2603
|
+
invalid_ids: list[str] = []
|
|
2604
|
+
for nid in node_ids_raw:
|
|
2605
|
+
if not isinstance(nid, str):
|
|
2606
|
+
continue
|
|
2607
|
+
try:
|
|
2608
|
+
subscribers.append(UUID(nid))
|
|
2609
|
+
except ValueError:
|
|
2610
|
+
invalid_ids.append(nid)
|
|
2611
|
+
|
|
2612
|
+
if invalid_ids:
|
|
2613
|
+
logger.warning(
|
|
2614
|
+
"Invalid UUIDs in topic subscriber list",
|
|
2615
|
+
extra={
|
|
2616
|
+
"topic": topic,
|
|
2617
|
+
"correlation_id": str(correlation_id),
|
|
2618
|
+
"invalid_count": len(invalid_ids),
|
|
2619
|
+
},
|
|
2620
|
+
)
|
|
2621
|
+
return subscribers
|
|
2622
|
+
|
|
2623
|
+
except json.JSONDecodeError as e:
|
|
2624
|
+
logger.warning(
|
|
2625
|
+
"Failed to parse topic subscriber JSON",
|
|
2626
|
+
extra={
|
|
2627
|
+
"topic": topic,
|
|
2628
|
+
"error": str(e),
|
|
2629
|
+
},
|
|
2630
|
+
)
|
|
2631
|
+
return []
|
|
2632
|
+
except InfraConsulError as e:
|
|
2633
|
+
logger.warning(
|
|
2634
|
+
"Consul error querying topic subscribers",
|
|
2635
|
+
extra={
|
|
2636
|
+
"topic": topic,
|
|
2637
|
+
"error": str(e),
|
|
2638
|
+
"error_type": "InfraConsulError",
|
|
2639
|
+
"consul_key": getattr(e, "consul_key", None),
|
|
2640
|
+
},
|
|
2641
|
+
)
|
|
2642
|
+
return []
|
|
2643
|
+
except InfraTimeoutError as e:
|
|
2644
|
+
logger.warning(
|
|
2645
|
+
"Timeout querying topic subscribers",
|
|
2646
|
+
extra={
|
|
2647
|
+
"topic": topic,
|
|
2648
|
+
"error": str(e),
|
|
2649
|
+
"error_type": "InfraTimeoutError",
|
|
2650
|
+
},
|
|
2651
|
+
)
|
|
2652
|
+
return []
|
|
2653
|
+
except InfraUnavailableError as e:
|
|
2654
|
+
logger.warning(
|
|
2655
|
+
"Service unavailable for topic subscriber query",
|
|
2656
|
+
extra={
|
|
2657
|
+
"topic": topic,
|
|
2658
|
+
"error": str(e),
|
|
2659
|
+
"error_type": "InfraUnavailableError",
|
|
2660
|
+
},
|
|
2661
|
+
)
|
|
2662
|
+
return []
|
|
2663
|
+
except Exception as e:
|
|
2664
|
+
# Graceful degradation - Consul unavailable is not fatal
|
|
2665
|
+
logger.warning(
|
|
2666
|
+
"Failed to query topic subscribers from Consul",
|
|
2667
|
+
extra={
|
|
2668
|
+
"topic": topic,
|
|
2669
|
+
"error": str(e),
|
|
2670
|
+
"error_type": type(e).__name__,
|
|
2671
|
+
},
|
|
2672
|
+
)
|
|
2673
|
+
return []
|
|
2674
|
+
|
|
2216
2675
|
# =========================================================================
|
|
2217
2676
|
# Architecture Validation Methods (OMN-1138)
|
|
2218
2677
|
# =========================================================================
|
|
@@ -2383,6 +2842,297 @@ class RuntimeHostProcess:
|
|
|
2383
2842
|
self._container = ModelONEXContainer()
|
|
2384
2843
|
return self._container
|
|
2385
2844
|
|
|
2845
|
+
def _get_environment_from_config(self) -> str:
|
|
2846
|
+
"""Extract environment setting from config with consistent fallback.
|
|
2847
|
+
|
|
2848
|
+
Handles both dict-based config and object-based config (e.g., Pydantic models)
|
|
2849
|
+
with a unified access pattern.
|
|
2850
|
+
|
|
2851
|
+
Resolution order:
|
|
2852
|
+
1. config["event_bus"]["environment"] (if config is dict-like)
|
|
2853
|
+
2. config.event_bus.environment (if config is object-like)
|
|
2854
|
+
3. ONEX_ENVIRONMENT environment variable
|
|
2855
|
+
4. "dev" (hardcoded default)
|
|
2856
|
+
|
|
2857
|
+
Returns:
|
|
2858
|
+
Environment string (e.g., "dev", "staging", "prod").
|
|
2859
|
+
"""
|
|
2860
|
+
default_env = os.getenv("ONEX_ENVIRONMENT", "dev")
|
|
2861
|
+
config = self._config or {}
|
|
2862
|
+
|
|
2863
|
+
event_bus_config = config.get("event_bus", {})
|
|
2864
|
+
if isinstance(event_bus_config, dict):
|
|
2865
|
+
return str(event_bus_config.get("environment", default_env))
|
|
2866
|
+
|
|
2867
|
+
# Object-based config (e.g., ModelEventBusConfig)
|
|
2868
|
+
return str(getattr(event_bus_config, "environment", default_env))
|
|
2869
|
+
|
|
2870
|
+
# =========================================================================
|
|
2871
|
+
# Event Bus Subcontract Wiring Methods (OMN-1621)
|
|
2872
|
+
# =========================================================================
|
|
2873
|
+
|
|
2874
|
+
async def _wire_event_bus_subscriptions(self) -> None:
|
|
2875
|
+
"""Wire Kafka subscriptions from handler contract event_bus sections.
|
|
2876
|
+
|
|
2877
|
+
This method bridges contract-declared topics to actual Kafka subscriptions
|
|
2878
|
+
using the EventBusSubcontractWiring class. It reads the event_bus subcontract
|
|
2879
|
+
from each handler's contract YAML and creates subscriptions for declared
|
|
2880
|
+
subscribe_topics.
|
|
2881
|
+
|
|
2882
|
+
Preconditions:
|
|
2883
|
+
- self._event_bus must be available and started
|
|
2884
|
+
- self._dispatch_engine must be set (otherwise wiring is skipped)
|
|
2885
|
+
- self._handler_descriptors must be populated
|
|
2886
|
+
|
|
2887
|
+
The wiring creates subscriptions that route messages to the dispatch engine,
|
|
2888
|
+
which then routes to appropriate handlers based on topic/category matching.
|
|
2889
|
+
|
|
2890
|
+
Per ARCH-002: "Runtime owns all Kafka plumbing" - nodes and handlers declare
|
|
2891
|
+
their topic requirements in contracts but never directly interact with Kafka.
|
|
2892
|
+
|
|
2893
|
+
Note:
|
|
2894
|
+
If dispatch_engine is not configured, this method logs a debug message
|
|
2895
|
+
and returns without creating any subscriptions. This allows the runtime
|
|
2896
|
+
to operate in legacy mode without contract-driven subscriptions.
|
|
2897
|
+
|
|
2898
|
+
.. versionadded:: 0.2.5
|
|
2899
|
+
Part of OMN-1621 contract-driven event bus wiring.
|
|
2900
|
+
"""
|
|
2901
|
+
# Guard: require both event_bus and dispatch_engine
|
|
2902
|
+
if not self._event_bus:
|
|
2903
|
+
logger.debug("Event bus not available, skipping subcontract wiring")
|
|
2904
|
+
return
|
|
2905
|
+
|
|
2906
|
+
if not self._dispatch_engine:
|
|
2907
|
+
logger.debug(
|
|
2908
|
+
"Dispatch engine not configured, skipping event bus subcontract wiring"
|
|
2909
|
+
)
|
|
2910
|
+
return
|
|
2911
|
+
|
|
2912
|
+
if not self._handler_descriptors:
|
|
2913
|
+
logger.debug(
|
|
2914
|
+
"No handler descriptors available, skipping subcontract wiring"
|
|
2915
|
+
)
|
|
2916
|
+
return
|
|
2917
|
+
|
|
2918
|
+
environment = self._get_environment_from_config()
|
|
2919
|
+
|
|
2920
|
+
# Create wiring instance
|
|
2921
|
+
# Cast to protocol type - both EventBusKafka and EventBusInmemory implement
|
|
2922
|
+
# the ProtocolEventBusSubscriber interface (subscribe method)
|
|
2923
|
+
self._event_bus_wiring = EventBusSubcontractWiring(
|
|
2924
|
+
event_bus=cast("ProtocolEventBusSubscriber", self._event_bus),
|
|
2925
|
+
dispatch_engine=self._dispatch_engine,
|
|
2926
|
+
environment=environment,
|
|
2927
|
+
)
|
|
2928
|
+
|
|
2929
|
+
# Wire subscriptions for each handler with a contract
|
|
2930
|
+
wired_count = 0
|
|
2931
|
+
for handler_type, descriptor in self._handler_descriptors.items():
|
|
2932
|
+
contract_path_str = descriptor.contract_path
|
|
2933
|
+
if not contract_path_str:
|
|
2934
|
+
continue
|
|
2935
|
+
|
|
2936
|
+
contract_path = Path(contract_path_str)
|
|
2937
|
+
|
|
2938
|
+
# Load event_bus subcontract from contract YAML
|
|
2939
|
+
subcontract = load_event_bus_subcontract(contract_path, logger)
|
|
2940
|
+
if subcontract and subcontract.subscribe_topics:
|
|
2941
|
+
await self._event_bus_wiring.wire_subscriptions(
|
|
2942
|
+
subcontract=subcontract,
|
|
2943
|
+
node_name=descriptor.name or handler_type,
|
|
2944
|
+
)
|
|
2945
|
+
wired_count += 1
|
|
2946
|
+
logger.info(
|
|
2947
|
+
"Wired subscription(s) for handler '%s': topics=%s",
|
|
2948
|
+
descriptor.name or handler_type,
|
|
2949
|
+
subcontract.subscribe_topics,
|
|
2950
|
+
)
|
|
2951
|
+
|
|
2952
|
+
if wired_count > 0:
|
|
2953
|
+
logger.info(
|
|
2954
|
+
"Event bus subcontract wiring complete",
|
|
2955
|
+
extra={
|
|
2956
|
+
"wired_handler_count": wired_count,
|
|
2957
|
+
"total_handler_count": len(self._handler_descriptors),
|
|
2958
|
+
"environment": environment,
|
|
2959
|
+
},
|
|
2960
|
+
)
|
|
2961
|
+
else:
|
|
2962
|
+
logger.debug(
|
|
2963
|
+
"No handlers with event_bus subscriptions found",
|
|
2964
|
+
extra={"handler_count": len(self._handler_descriptors)},
|
|
2965
|
+
)
|
|
2966
|
+
|
|
2967
|
+
async def _wire_baseline_subscriptions(self) -> None:
|
|
2968
|
+
"""Wire platform-baseline topic subscriptions for contract discovery.
|
|
2969
|
+
|
|
2970
|
+
These subscriptions are wired at runtime startup to receive contract
|
|
2971
|
+
registration and deregistration events from Kafka. This enables
|
|
2972
|
+
dynamic contract discovery without polling.
|
|
2973
|
+
|
|
2974
|
+
The subscriptions route events to KafkaContractSource callbacks:
|
|
2975
|
+
- on_contract_registered(): Parses contract YAML and caches descriptor
|
|
2976
|
+
- on_contract_deregistered(): Removes descriptor from cache
|
|
2977
|
+
|
|
2978
|
+
Preconditions:
|
|
2979
|
+
- KAFKA_EVENTS mode must be active (self._kafka_contract_source set)
|
|
2980
|
+
- Event bus must be available and started
|
|
2981
|
+
|
|
2982
|
+
Topic Format:
|
|
2983
|
+
- Registration: {env}.{TOPIC_SUFFIX_CONTRACT_REGISTERED}
|
|
2984
|
+
- Deregistration: {env}.{TOPIC_SUFFIX_CONTRACT_DEREGISTERED}
|
|
2985
|
+
|
|
2986
|
+
Note:
|
|
2987
|
+
Unsubscribe callbacks are stored in self._baseline_subscriptions
|
|
2988
|
+
for cleanup during stop().
|
|
2989
|
+
|
|
2990
|
+
Part of OMN-1654: KafkaContractSource cache discovery.
|
|
2991
|
+
|
|
2992
|
+
.. versionadded:: 0.8.0
|
|
2993
|
+
Created for event-driven contract discovery.
|
|
2994
|
+
"""
|
|
2995
|
+
# Guard: only wire if KafkaContractSource is active
|
|
2996
|
+
if self._kafka_contract_source is None:
|
|
2997
|
+
logger.debug(
|
|
2998
|
+
"KafkaContractSource not active, skipping baseline subscriptions"
|
|
2999
|
+
)
|
|
3000
|
+
return
|
|
3001
|
+
|
|
3002
|
+
# Guard: event bus must be available
|
|
3003
|
+
if self._event_bus is None:
|
|
3004
|
+
logger.warning(
|
|
3005
|
+
"Event bus not available, cannot wire baseline contract subscriptions",
|
|
3006
|
+
extra={"mode": "KAFKA_EVENTS"},
|
|
3007
|
+
)
|
|
3008
|
+
return
|
|
3009
|
+
|
|
3010
|
+
source = self._kafka_contract_source
|
|
3011
|
+
environment = source.environment
|
|
3012
|
+
|
|
3013
|
+
# Compose topic names using platform-reserved suffixes
|
|
3014
|
+
registration_topic = f"{environment}.{TOPIC_SUFFIX_CONTRACT_REGISTERED}"
|
|
3015
|
+
deregistration_topic = f"{environment}.{TOPIC_SUFFIX_CONTRACT_DEREGISTERED}"
|
|
3016
|
+
|
|
3017
|
+
# Import ModelEventMessage type for handler signature
|
|
3018
|
+
from omnibase_infra.event_bus.models.model_event_message import (
|
|
3019
|
+
ModelEventMessage,
|
|
3020
|
+
)
|
|
3021
|
+
|
|
3022
|
+
async def handle_registration(msg: ModelEventMessage) -> None:
|
|
3023
|
+
"""Handle contract registration event from Kafka."""
|
|
3024
|
+
try:
|
|
3025
|
+
parsed = _parse_contract_event_payload(msg)
|
|
3026
|
+
if parsed is None:
|
|
3027
|
+
return
|
|
3028
|
+
|
|
3029
|
+
payload, correlation_id = parsed
|
|
3030
|
+
|
|
3031
|
+
source.on_contract_registered(
|
|
3032
|
+
node_name=str(payload.get("node_name", "")),
|
|
3033
|
+
contract_yaml=str(payload.get("contract_yaml", "")),
|
|
3034
|
+
correlation_id=correlation_id,
|
|
3035
|
+
)
|
|
3036
|
+
|
|
3037
|
+
logger.debug(
|
|
3038
|
+
"Processed contract registration event",
|
|
3039
|
+
extra={
|
|
3040
|
+
"node_name": payload.get("node_name"),
|
|
3041
|
+
"topic": registration_topic,
|
|
3042
|
+
"correlation_id": str(correlation_id),
|
|
3043
|
+
},
|
|
3044
|
+
)
|
|
3045
|
+
|
|
3046
|
+
except Exception as e:
|
|
3047
|
+
logger.warning(
|
|
3048
|
+
"Failed to process contract registration event",
|
|
3049
|
+
extra={
|
|
3050
|
+
"error": str(e),
|
|
3051
|
+
"error_type": type(e).__name__,
|
|
3052
|
+
"topic": registration_topic,
|
|
3053
|
+
},
|
|
3054
|
+
)
|
|
3055
|
+
|
|
3056
|
+
async def handle_deregistration(msg: ModelEventMessage) -> None:
|
|
3057
|
+
"""Handle contract deregistration event from Kafka."""
|
|
3058
|
+
try:
|
|
3059
|
+
parsed = _parse_contract_event_payload(msg)
|
|
3060
|
+
if parsed is None:
|
|
3061
|
+
return
|
|
3062
|
+
|
|
3063
|
+
payload, correlation_id = parsed
|
|
3064
|
+
|
|
3065
|
+
source.on_contract_deregistered(
|
|
3066
|
+
node_name=str(payload.get("node_name", "")),
|
|
3067
|
+
correlation_id=correlation_id,
|
|
3068
|
+
)
|
|
3069
|
+
|
|
3070
|
+
logger.debug(
|
|
3071
|
+
"Processed contract deregistration event",
|
|
3072
|
+
extra={
|
|
3073
|
+
"node_name": payload.get("node_name"),
|
|
3074
|
+
"topic": deregistration_topic,
|
|
3075
|
+
"correlation_id": str(correlation_id),
|
|
3076
|
+
},
|
|
3077
|
+
)
|
|
3078
|
+
|
|
3079
|
+
except Exception as e:
|
|
3080
|
+
logger.warning(
|
|
3081
|
+
"Failed to process contract deregistration event",
|
|
3082
|
+
extra={
|
|
3083
|
+
"error": str(e),
|
|
3084
|
+
"error_type": type(e).__name__,
|
|
3085
|
+
"topic": deregistration_topic,
|
|
3086
|
+
},
|
|
3087
|
+
)
|
|
3088
|
+
|
|
3089
|
+
# Subscribe to topics
|
|
3090
|
+
try:
|
|
3091
|
+
# Create node identity for baseline subscriptions
|
|
3092
|
+
baseline_identity = ModelNodeIdentity(
|
|
3093
|
+
env=environment,
|
|
3094
|
+
service=self._node_identity.service,
|
|
3095
|
+
node_name=f"{self._node_identity.node_name}-contract-discovery",
|
|
3096
|
+
version=self._node_identity.version,
|
|
3097
|
+
)
|
|
3098
|
+
|
|
3099
|
+
# Subscribe to registration topic
|
|
3100
|
+
reg_unsub = await self._event_bus.subscribe(
|
|
3101
|
+
topic=registration_topic,
|
|
3102
|
+
node_identity=baseline_identity,
|
|
3103
|
+
on_message=handle_registration,
|
|
3104
|
+
purpose=EnumConsumerGroupPurpose.CONSUME,
|
|
3105
|
+
)
|
|
3106
|
+
self._baseline_subscriptions.append(reg_unsub)
|
|
3107
|
+
|
|
3108
|
+
# Subscribe to deregistration topic
|
|
3109
|
+
dereg_unsub = await self._event_bus.subscribe(
|
|
3110
|
+
topic=deregistration_topic,
|
|
3111
|
+
node_identity=baseline_identity,
|
|
3112
|
+
on_message=handle_deregistration,
|
|
3113
|
+
purpose=EnumConsumerGroupPurpose.CONSUME,
|
|
3114
|
+
)
|
|
3115
|
+
self._baseline_subscriptions.append(dereg_unsub)
|
|
3116
|
+
|
|
3117
|
+
logger.info(
|
|
3118
|
+
"Wired baseline contract subscriptions",
|
|
3119
|
+
extra={
|
|
3120
|
+
"registration_topic": registration_topic,
|
|
3121
|
+
"deregistration_topic": deregistration_topic,
|
|
3122
|
+
"environment": environment,
|
|
3123
|
+
"subscription_count": len(self._baseline_subscriptions),
|
|
3124
|
+
},
|
|
3125
|
+
)
|
|
3126
|
+
|
|
3127
|
+
except Exception:
|
|
3128
|
+
logger.exception(
|
|
3129
|
+
"Failed to wire baseline subscriptions",
|
|
3130
|
+
extra={
|
|
3131
|
+
"registration_topic": registration_topic,
|
|
3132
|
+
"deregistration_topic": deregistration_topic,
|
|
3133
|
+
},
|
|
3134
|
+
)
|
|
3135
|
+
|
|
2386
3136
|
# =========================================================================
|
|
2387
3137
|
# Idempotency Guard Methods (OMN-945)
|
|
2388
3138
|
# =========================================================================
|