omnibase_infra 0.3.1__py3-none-any.whl → 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- omnibase_infra/__init__.py +1 -1
- omnibase_infra/enums/__init__.py +3 -0
- omnibase_infra/enums/enum_consumer_group_purpose.py +9 -0
- omnibase_infra/enums/enum_postgres_error_code.py +188 -0
- omnibase_infra/errors/__init__.py +4 -0
- omnibase_infra/errors/error_infra.py +60 -0
- omnibase_infra/handlers/__init__.py +3 -0
- omnibase_infra/handlers/handler_slack_webhook.py +426 -0
- omnibase_infra/handlers/models/__init__.py +14 -0
- omnibase_infra/handlers/models/enum_alert_severity.py +36 -0
- omnibase_infra/handlers/models/model_slack_alert.py +24 -0
- omnibase_infra/handlers/models/model_slack_alert_payload.py +77 -0
- omnibase_infra/handlers/models/model_slack_alert_result.py +73 -0
- omnibase_infra/handlers/registration_storage/handler_registration_storage_postgres.py +29 -20
- omnibase_infra/mixins/__init__.py +14 -0
- omnibase_infra/mixins/mixin_node_introspection.py +42 -20
- omnibase_infra/mixins/mixin_postgres_error_response.py +314 -0
- omnibase_infra/mixins/mixin_postgres_op_executor.py +298 -0
- omnibase_infra/models/__init__.py +3 -0
- omnibase_infra/models/discovery/model_dependency_spec.py +1 -0
- omnibase_infra/models/discovery/model_discovered_capabilities.py +1 -1
- omnibase_infra/models/discovery/model_introspection_config.py +28 -1
- omnibase_infra/models/discovery/model_introspection_performance_metrics.py +1 -0
- omnibase_infra/models/discovery/model_introspection_task_config.py +1 -0
- omnibase_infra/{nodes/effects/models → models}/model_backend_result.py +22 -6
- omnibase_infra/models/projection/__init__.py +11 -0
- omnibase_infra/models/projection/model_contract_projection.py +170 -0
- omnibase_infra/models/projection/model_topic_projection.py +148 -0
- omnibase_infra/models/runtime/__init__.py +4 -0
- omnibase_infra/models/runtime/model_resolved_dependencies.py +116 -0
- omnibase_infra/nodes/contract_registry_reducer/__init__.py +5 -0
- omnibase_infra/nodes/contract_registry_reducer/contract.yaml +6 -5
- omnibase_infra/nodes/contract_registry_reducer/contract_registration_event_router.py +689 -0
- omnibase_infra/nodes/contract_registry_reducer/reducer.py +9 -26
- omnibase_infra/nodes/effects/__init__.py +1 -1
- omnibase_infra/nodes/effects/models/__init__.py +6 -4
- omnibase_infra/nodes/effects/models/model_registry_response.py +1 -1
- omnibase_infra/nodes/effects/protocol_consul_client.py +1 -1
- omnibase_infra/nodes/effects/protocol_postgres_adapter.py +1 -1
- omnibase_infra/nodes/effects/registry_effect.py +1 -1
- omnibase_infra/nodes/node_contract_persistence_effect/__init__.py +101 -0
- omnibase_infra/nodes/node_contract_persistence_effect/contract.yaml +490 -0
- omnibase_infra/nodes/node_contract_persistence_effect/handlers/__init__.py +74 -0
- omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_cleanup_topics.py +217 -0
- omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_contract_upsert.py +242 -0
- omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_deactivate.py +194 -0
- omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_heartbeat.py +243 -0
- omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_mark_stale.py +208 -0
- omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_topic_update.py +298 -0
- omnibase_infra/nodes/node_contract_persistence_effect/models/__init__.py +15 -0
- omnibase_infra/nodes/node_contract_persistence_effect/models/model_persistence_result.py +52 -0
- omnibase_infra/nodes/node_contract_persistence_effect/node.py +131 -0
- omnibase_infra/nodes/node_contract_persistence_effect/registry/__init__.py +27 -0
- omnibase_infra/nodes/node_contract_persistence_effect/registry/registry_infra_contract_persistence_effect.py +251 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_postgres_intent_payload.py +8 -12
- omnibase_infra/nodes/node_registry_effect/models/__init__.py +2 -2
- omnibase_infra/nodes/node_slack_alerter_effect/__init__.py +33 -0
- omnibase_infra/nodes/node_slack_alerter_effect/contract.yaml +291 -0
- omnibase_infra/nodes/node_slack_alerter_effect/node.py +106 -0
- omnibase_infra/projectors/__init__.py +6 -0
- omnibase_infra/projectors/projection_reader_contract.py +1301 -0
- omnibase_infra/runtime/__init__.py +12 -0
- omnibase_infra/runtime/baseline_subscriptions.py +13 -6
- omnibase_infra/runtime/contract_dependency_resolver.py +455 -0
- omnibase_infra/runtime/contract_registration_event_router.py +500 -0
- omnibase_infra/runtime/db/__init__.py +4 -0
- omnibase_infra/runtime/db/models/__init__.py +15 -10
- omnibase_infra/runtime/db/models/model_db_operation.py +40 -0
- omnibase_infra/runtime/db/models/model_db_param.py +24 -0
- omnibase_infra/runtime/db/models/model_db_repository_contract.py +40 -0
- omnibase_infra/runtime/db/models/model_db_return.py +26 -0
- omnibase_infra/runtime/db/models/model_db_safety_policy.py +32 -0
- omnibase_infra/runtime/emit_daemon/event_registry.py +34 -22
- omnibase_infra/runtime/event_bus_subcontract_wiring.py +63 -23
- omnibase_infra/runtime/intent_execution_router.py +430 -0
- omnibase_infra/runtime/models/__init__.py +6 -0
- omnibase_infra/runtime/models/model_contract_registry_config.py +41 -0
- omnibase_infra/runtime/models/model_intent_execution_summary.py +79 -0
- omnibase_infra/runtime/models/model_runtime_config.py +8 -0
- omnibase_infra/runtime/protocols/__init__.py +16 -0
- omnibase_infra/runtime/protocols/protocol_intent_executor.py +107 -0
- omnibase_infra/runtime/publisher_topic_scoped.py +16 -11
- omnibase_infra/runtime/registry_policy.py +29 -15
- omnibase_infra/runtime/request_response_wiring.py +793 -0
- omnibase_infra/runtime/service_kernel.py +295 -8
- omnibase_infra/runtime/service_runtime_host_process.py +149 -5
- omnibase_infra/runtime/util_version.py +5 -1
- omnibase_infra/schemas/schema_latency_baseline.sql +135 -0
- omnibase_infra/services/contract_publisher/config.py +4 -4
- omnibase_infra/services/contract_publisher/service.py +8 -5
- omnibase_infra/services/observability/injection_effectiveness/__init__.py +67 -0
- omnibase_infra/services/observability/injection_effectiveness/config.py +295 -0
- omnibase_infra/services/observability/injection_effectiveness/consumer.py +1461 -0
- omnibase_infra/services/observability/injection_effectiveness/models/__init__.py +32 -0
- omnibase_infra/services/observability/injection_effectiveness/models/model_agent_match.py +79 -0
- omnibase_infra/services/observability/injection_effectiveness/models/model_context_utilization.py +118 -0
- omnibase_infra/services/observability/injection_effectiveness/models/model_latency_breakdown.py +107 -0
- omnibase_infra/services/observability/injection_effectiveness/models/model_pattern_utilization.py +46 -0
- omnibase_infra/services/observability/injection_effectiveness/writer_postgres.py +596 -0
- omnibase_infra/services/registry_api/models/__init__.py +25 -0
- omnibase_infra/services/registry_api/models/model_contract_ref.py +44 -0
- omnibase_infra/services/registry_api/models/model_contract_view.py +81 -0
- omnibase_infra/services/registry_api/models/model_response_contracts.py +50 -0
- omnibase_infra/services/registry_api/models/model_response_topics.py +50 -0
- omnibase_infra/services/registry_api/models/model_topic_summary.py +57 -0
- omnibase_infra/services/registry_api/models/model_topic_view.py +63 -0
- omnibase_infra/services/registry_api/routes.py +205 -6
- omnibase_infra/services/registry_api/service.py +528 -1
- omnibase_infra/utils/__init__.py +7 -0
- omnibase_infra/utils/util_db_error_context.py +292 -0
- omnibase_infra/validation/infra_validators.py +3 -1
- omnibase_infra/validation/validation_exemptions.yaml +65 -0
- {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.4.0.dist-info}/METADATA +3 -3
- {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.4.0.dist-info}/RECORD +117 -58
- {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.4.0.dist-info}/WHEEL +0 -0
- {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.4.0.dist-info}/entry_points.txt +0 -0
- {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.4.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -55,6 +55,7 @@ import signal
|
|
|
55
55
|
import sys
|
|
56
56
|
import time
|
|
57
57
|
from collections.abc import Awaitable, Callable
|
|
58
|
+
from functools import partial
|
|
58
59
|
from importlib.metadata import version as get_package_version
|
|
59
60
|
from pathlib import Path
|
|
60
61
|
from typing import cast
|
|
@@ -75,7 +76,15 @@ from omnibase_infra.errors import (
|
|
|
75
76
|
from omnibase_infra.event_bus.event_bus_inmemory import EventBusInmemory
|
|
76
77
|
from omnibase_infra.event_bus.event_bus_kafka import EventBusKafka
|
|
77
78
|
from omnibase_infra.event_bus.models.config import ModelKafkaEventBusConfig
|
|
79
|
+
from omnibase_infra.event_bus.models.model_event_message import ModelEventMessage
|
|
78
80
|
from omnibase_infra.models import ModelNodeIdentity
|
|
81
|
+
from omnibase_infra.nodes.contract_registry_reducer.contract_registration_event_router import (
|
|
82
|
+
ContractRegistrationEventRouter,
|
|
83
|
+
ProtocolIntentEffect,
|
|
84
|
+
)
|
|
85
|
+
from omnibase_infra.nodes.contract_registry_reducer.reducer import (
|
|
86
|
+
ContractRegistryReducer,
|
|
87
|
+
)
|
|
79
88
|
from omnibase_infra.nodes.node_registration_orchestrator.dispatchers import (
|
|
80
89
|
DispatcherNodeIntrospected,
|
|
81
90
|
)
|
|
@@ -423,6 +432,11 @@ async def bootstrap() -> int:
|
|
|
423
432
|
health_server: ServiceHealth | None = None
|
|
424
433
|
postgres_pool: asyncpg.Pool | None = None
|
|
425
434
|
introspection_unsubscribe: Callable[[], Awaitable[None]] | None = None
|
|
435
|
+
# Contract registry unsubscribe functions and router
|
|
436
|
+
contract_router: ContractRegistrationEventRouter | None = None
|
|
437
|
+
contract_unsub_registered: Callable[[], Awaitable[None]] | None = None
|
|
438
|
+
contract_unsub_deregistered: Callable[[], Awaitable[None]] | None = None
|
|
439
|
+
contract_unsub_heartbeat: Callable[[], Awaitable[None]] | None = None
|
|
426
440
|
correlation_id = generate_correlation_id()
|
|
427
441
|
bootstrap_start_time = time.time()
|
|
428
442
|
|
|
@@ -458,6 +472,7 @@ async def bootstrap() -> int:
|
|
|
458
472
|
|
|
459
473
|
# 3. Create event bus
|
|
460
474
|
# Dispatch based on configuration or environment variable:
|
|
475
|
+
# - ONEX_EVENT_BUS_TYPE env var overrides config.event_bus.type
|
|
461
476
|
# - If KAFKA_BOOTSTRAP_SERVERS env var is set, use EventBusKafka
|
|
462
477
|
# - If config.event_bus.type == "kafka", use EventBusKafka
|
|
463
478
|
# - Otherwise, use EventBusInmemory for local development/testing
|
|
@@ -465,12 +480,54 @@ async def bootstrap() -> int:
|
|
|
465
480
|
environment = os.getenv("ONEX_ENVIRONMENT") or config.event_bus.environment
|
|
466
481
|
kafka_bootstrap_servers = os.getenv("KAFKA_BOOTSTRAP_SERVERS")
|
|
467
482
|
|
|
468
|
-
#
|
|
469
|
-
#
|
|
470
|
-
#
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
483
|
+
# Check for ONEX_EVENT_BUS_TYPE environment variable override
|
|
484
|
+
# This allows CI/testing environments to force inmemory event bus
|
|
485
|
+
# even when the config file defaults to kafka.
|
|
486
|
+
event_bus_type_override = os.getenv("ONEX_EVENT_BUS_TYPE", "").lower()
|
|
487
|
+
if event_bus_type_override:
|
|
488
|
+
logger.debug(
|
|
489
|
+
"Event bus type override from ONEX_EVENT_BUS_TYPE=%s (correlation_id=%s)",
|
|
490
|
+
event_bus_type_override,
|
|
491
|
+
correlation_id,
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
# Determine effective event bus type with override precedence:
|
|
495
|
+
# 1. ONEX_EVENT_BUS_TYPE env var (highest priority)
|
|
496
|
+
# 2. KAFKA_BOOTSTRAP_SERVERS env var (if set, implies kafka)
|
|
497
|
+
# 3. config.event_bus.type (from runtime_config.yaml)
|
|
498
|
+
if event_bus_type_override == "inmemory":
|
|
499
|
+
# Explicit inmemory override - use inmemory regardless of other config
|
|
500
|
+
use_kafka = False
|
|
501
|
+
logger.info(
|
|
502
|
+
"Using inmemory event bus (ONEX_EVENT_BUS_TYPE override) (correlation_id=%s)",
|
|
503
|
+
correlation_id,
|
|
504
|
+
)
|
|
505
|
+
elif event_bus_type_override == "kafka":
|
|
506
|
+
# Explicit kafka override - validate that bootstrap_servers is available
|
|
507
|
+
use_kafka = True
|
|
508
|
+
elif event_bus_type_override and event_bus_type_override not in (
|
|
509
|
+
"inmemory",
|
|
510
|
+
"kafka",
|
|
511
|
+
):
|
|
512
|
+
# Invalid override value - warn and fall back to config
|
|
513
|
+
logger.warning(
|
|
514
|
+
"Invalid ONEX_EVENT_BUS_TYPE value '%s', expected 'inmemory' or 'kafka'. "
|
|
515
|
+
"Falling back to config.event_bus.type='%s' (correlation_id=%s)",
|
|
516
|
+
event_bus_type_override,
|
|
517
|
+
config.event_bus.type,
|
|
518
|
+
correlation_id,
|
|
519
|
+
)
|
|
520
|
+
use_kafka = (
|
|
521
|
+
bool(kafka_bootstrap_servers) or config.event_bus.type == "kafka"
|
|
522
|
+
)
|
|
523
|
+
else:
|
|
524
|
+
# No override - use original logic
|
|
525
|
+
# Explicit bool evaluation (not truthy string) for kafka usage.
|
|
526
|
+
# KAFKA_BOOTSTRAP_SERVERS env var takes precedence over config.event_bus.type.
|
|
527
|
+
# This prevents implicit "kafka but localhost" fallback scenarios.
|
|
528
|
+
use_kafka = (
|
|
529
|
+
bool(kafka_bootstrap_servers) or config.event_bus.type == "kafka"
|
|
530
|
+
)
|
|
474
531
|
|
|
475
532
|
# Validate bootstrap_servers is provided when kafka is requested via config
|
|
476
533
|
# This prevents confusing implicit localhost:9092 fallback
|
|
@@ -931,6 +988,81 @@ async def bootstrap() -> int:
|
|
|
931
988
|
},
|
|
932
989
|
)
|
|
933
990
|
|
|
991
|
+
# 4.9. Wire ContractRegistrationEventRouter if contract_registry.enabled
|
|
992
|
+
# This router subscribes to contract lifecycle events (registration,
|
|
993
|
+
# deregistration, heartbeat) and routes them to the ContractRegistryReducer.
|
|
994
|
+
# The router also runs an internal tick timer for staleness computation.
|
|
995
|
+
if config.contract_registry.enabled and postgres_pool is not None:
|
|
996
|
+
# Import postgres handlers for contract persistence
|
|
997
|
+
# Deferred import to avoid loading heavy dependencies when not needed
|
|
998
|
+
from omnibase_infra.nodes.node_contract_persistence_effect.handlers import (
|
|
999
|
+
HandlerPostgresCleanupTopics,
|
|
1000
|
+
HandlerPostgresContractUpsert,
|
|
1001
|
+
HandlerPostgresDeactivate,
|
|
1002
|
+
HandlerPostgresHeartbeat,
|
|
1003
|
+
HandlerPostgresMarkStale,
|
|
1004
|
+
HandlerPostgresTopicUpdate,
|
|
1005
|
+
)
|
|
1006
|
+
|
|
1007
|
+
# Create effect handlers keyed by intent_type
|
|
1008
|
+
# These handlers execute PostgreSQL operations for intents from the reducer
|
|
1009
|
+
# Note: Handlers implement ProtocolIntentEffect duck-typing style with
|
|
1010
|
+
# more specific payload types. Cast tells mypy they satisfy the protocol.
|
|
1011
|
+
contract_effect_handlers: dict[str, ProtocolIntentEffect] = {
|
|
1012
|
+
"postgres.upsert_contract": cast(
|
|
1013
|
+
"ProtocolIntentEffect",
|
|
1014
|
+
HandlerPostgresContractUpsert(postgres_pool),
|
|
1015
|
+
),
|
|
1016
|
+
"postgres.update_topic": cast(
|
|
1017
|
+
"ProtocolIntentEffect",
|
|
1018
|
+
HandlerPostgresTopicUpdate(postgres_pool),
|
|
1019
|
+
),
|
|
1020
|
+
"postgres.mark_stale": cast(
|
|
1021
|
+
"ProtocolIntentEffect",
|
|
1022
|
+
HandlerPostgresMarkStale(postgres_pool),
|
|
1023
|
+
),
|
|
1024
|
+
"postgres.update_heartbeat": cast(
|
|
1025
|
+
"ProtocolIntentEffect",
|
|
1026
|
+
HandlerPostgresHeartbeat(postgres_pool),
|
|
1027
|
+
),
|
|
1028
|
+
"postgres.deactivate_contract": cast(
|
|
1029
|
+
"ProtocolIntentEffect",
|
|
1030
|
+
HandlerPostgresDeactivate(postgres_pool),
|
|
1031
|
+
),
|
|
1032
|
+
"postgres.cleanup_topic_references": cast(
|
|
1033
|
+
"ProtocolIntentEffect",
|
|
1034
|
+
HandlerPostgresCleanupTopics(postgres_pool),
|
|
1035
|
+
),
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
# Create reducer and router
|
|
1039
|
+
contract_reducer = ContractRegistryReducer()
|
|
1040
|
+
contract_router = ContractRegistrationEventRouter(
|
|
1041
|
+
container=container,
|
|
1042
|
+
reducer=contract_reducer,
|
|
1043
|
+
effect_handlers=contract_effect_handlers,
|
|
1044
|
+
event_bus=event_bus,
|
|
1045
|
+
tick_interval_seconds=config.contract_registry.tick_interval_seconds,
|
|
1046
|
+
)
|
|
1047
|
+
|
|
1048
|
+
logger.info(
|
|
1049
|
+
"ContractRegistrationEventRouter created (correlation_id=%s)",
|
|
1050
|
+
correlation_id,
|
|
1051
|
+
extra={
|
|
1052
|
+
"tick_interval_seconds": config.contract_registry.tick_interval_seconds,
|
|
1053
|
+
"handler_count": len(contract_effect_handlers),
|
|
1054
|
+
},
|
|
1055
|
+
)
|
|
1056
|
+
else:
|
|
1057
|
+
logger.debug(
|
|
1058
|
+
"Contract registry disabled or no postgres_pool (correlation_id=%s)",
|
|
1059
|
+
correlation_id,
|
|
1060
|
+
extra={
|
|
1061
|
+
"contract_registry_enabled": config.contract_registry.enabled,
|
|
1062
|
+
"postgres_pool_available": postgres_pool is not None,
|
|
1063
|
+
},
|
|
1064
|
+
)
|
|
1065
|
+
|
|
934
1066
|
except Exception as pool_error:
|
|
935
1067
|
# Log warning but continue without registration support
|
|
936
1068
|
# Use sanitize_error_message to prevent credential leakage in logs
|
|
@@ -1298,6 +1430,84 @@ async def bootstrap() -> int:
|
|
|
1298
1430
|
},
|
|
1299
1431
|
)
|
|
1300
1432
|
|
|
1433
|
+
# 9.6. Start contract registry event consumer if router is available
|
|
1434
|
+
# This consumer subscribes to 3 Kafka topics for contract lifecycle events
|
|
1435
|
+
# and routes them to the ContractRegistryReducer for projection.
|
|
1436
|
+
if contract_router is not None and has_subscribe:
|
|
1437
|
+
# Create typed node identity for contract registry subscriptions
|
|
1438
|
+
contract_node_identity = ModelNodeIdentity(
|
|
1439
|
+
env=environment,
|
|
1440
|
+
service=config.name or "onex-kernel",
|
|
1441
|
+
node_name="contract-registry",
|
|
1442
|
+
version=config.contract_version or "v1",
|
|
1443
|
+
)
|
|
1444
|
+
|
|
1445
|
+
# Subscribe to 3 contract lifecycle topics with same identity
|
|
1446
|
+
contract_subscribe_start_time = time.time()
|
|
1447
|
+
|
|
1448
|
+
# Derive environment-aware topic names (avoid hardcoded "dev." prefix)
|
|
1449
|
+
contract_registered_topic = (
|
|
1450
|
+
f"{environment}.onex.evt.platform.contract-registered.v1"
|
|
1451
|
+
)
|
|
1452
|
+
contract_deregistered_topic = (
|
|
1453
|
+
f"{environment}.onex.evt.platform.contract-deregistered.v1"
|
|
1454
|
+
)
|
|
1455
|
+
node_heartbeat_topic = f"{environment}.onex.evt.platform.node-heartbeat.v1"
|
|
1456
|
+
|
|
1457
|
+
logger.info(
|
|
1458
|
+
"Subscribing to contract registry events on event bus (correlation_id=%s)",
|
|
1459
|
+
correlation_id,
|
|
1460
|
+
extra={
|
|
1461
|
+
"topics": [
|
|
1462
|
+
contract_registered_topic,
|
|
1463
|
+
contract_deregistered_topic,
|
|
1464
|
+
node_heartbeat_topic,
|
|
1465
|
+
],
|
|
1466
|
+
"node_identity": {
|
|
1467
|
+
"env": contract_node_identity.env,
|
|
1468
|
+
"service": contract_node_identity.service,
|
|
1469
|
+
"node_name": contract_node_identity.node_name,
|
|
1470
|
+
"version": contract_node_identity.version,
|
|
1471
|
+
},
|
|
1472
|
+
"purpose": EnumConsumerGroupPurpose.CONTRACT_REGISTRY.value,
|
|
1473
|
+
},
|
|
1474
|
+
)
|
|
1475
|
+
|
|
1476
|
+
contract_unsub_registered = await event_bus.subscribe(
|
|
1477
|
+
topic=contract_registered_topic,
|
|
1478
|
+
node_identity=contract_node_identity,
|
|
1479
|
+
on_message=contract_router.handle_message,
|
|
1480
|
+
purpose=EnumConsumerGroupPurpose.CONTRACT_REGISTRY,
|
|
1481
|
+
)
|
|
1482
|
+
contract_unsub_deregistered = await event_bus.subscribe(
|
|
1483
|
+
topic=contract_deregistered_topic,
|
|
1484
|
+
node_identity=contract_node_identity,
|
|
1485
|
+
on_message=contract_router.handle_message,
|
|
1486
|
+
purpose=EnumConsumerGroupPurpose.CONTRACT_REGISTRY,
|
|
1487
|
+
)
|
|
1488
|
+
contract_unsub_heartbeat = await event_bus.subscribe(
|
|
1489
|
+
topic=node_heartbeat_topic,
|
|
1490
|
+
node_identity=contract_node_identity,
|
|
1491
|
+
on_message=contract_router.handle_message,
|
|
1492
|
+
purpose=EnumConsumerGroupPurpose.CONTRACT_REGISTRY,
|
|
1493
|
+
)
|
|
1494
|
+
|
|
1495
|
+
# Start the router's tick timer
|
|
1496
|
+
await contract_router.start()
|
|
1497
|
+
|
|
1498
|
+
contract_subscribe_duration = time.time() - contract_subscribe_start_time
|
|
1499
|
+
logger.info(
|
|
1500
|
+
"Contract registry event consumers started successfully in %.3fs (correlation_id=%s)",
|
|
1501
|
+
contract_subscribe_duration,
|
|
1502
|
+
correlation_id,
|
|
1503
|
+
extra={
|
|
1504
|
+
"topics_count": 3,
|
|
1505
|
+
"tick_interval_seconds": contract_router.tick_interval_seconds,
|
|
1506
|
+
"subscribe_duration_seconds": contract_subscribe_duration,
|
|
1507
|
+
"event_bus_type": event_bus_type,
|
|
1508
|
+
},
|
|
1509
|
+
)
|
|
1510
|
+
|
|
1301
1511
|
# Calculate total bootstrap time
|
|
1302
1512
|
bootstrap_duration = time.time() - bootstrap_start_time
|
|
1303
1513
|
|
|
@@ -1309,14 +1519,24 @@ async def bootstrap() -> int:
|
|
|
1309
1519
|
registration_status = "enabled (PostgreSQL only)"
|
|
1310
1520
|
else:
|
|
1311
1521
|
registration_status = "disabled"
|
|
1522
|
+
|
|
1523
|
+
# Contract registry status for banner
|
|
1524
|
+
if contract_router is not None:
|
|
1525
|
+
contract_registry_status = (
|
|
1526
|
+
f"enabled (tick: {config.contract_registry.tick_interval_seconds}s)"
|
|
1527
|
+
)
|
|
1528
|
+
else:
|
|
1529
|
+
contract_registry_status = "disabled"
|
|
1530
|
+
|
|
1312
1531
|
banner_lines = [
|
|
1313
1532
|
"=" * 60,
|
|
1314
1533
|
f"ONEX Runtime Kernel v{KERNEL_VERSION}",
|
|
1315
1534
|
f"Environment: {environment}",
|
|
1316
1535
|
f"Contracts: {contracts_dir}",
|
|
1317
1536
|
f"Event Bus: {event_bus_type} (group: {config.consumer_group})",
|
|
1318
|
-
f"Topics: {config.input_topic}
|
|
1537
|
+
f"Topics: {config.input_topic} -> {config.output_topic}",
|
|
1319
1538
|
f"Registration: {registration_status}",
|
|
1539
|
+
f"Contract Registry: {contract_registry_status}",
|
|
1320
1540
|
f"Health endpoint: http://0.0.0.0:{http_port}/health",
|
|
1321
1541
|
f"Bootstrap time: {bootstrap_duration:.3f}s",
|
|
1322
1542
|
f"Correlation ID: {correlation_id}",
|
|
@@ -1367,6 +1587,47 @@ async def bootstrap() -> int:
|
|
|
1367
1587
|
)
|
|
1368
1588
|
introspection_unsubscribe = None
|
|
1369
1589
|
|
|
1590
|
+
# Stop contract registry router and consumers
|
|
1591
|
+
if contract_router is not None:
|
|
1592
|
+
try:
|
|
1593
|
+
await contract_router.stop()
|
|
1594
|
+
logger.debug(
|
|
1595
|
+
"Contract registry router stopped (correlation_id=%s)",
|
|
1596
|
+
correlation_id,
|
|
1597
|
+
)
|
|
1598
|
+
except Exception as router_stop_error:
|
|
1599
|
+
logger.warning(
|
|
1600
|
+
"Failed to stop contract registry router: %s (correlation_id=%s)",
|
|
1601
|
+
sanitize_error_message(router_stop_error),
|
|
1602
|
+
correlation_id,
|
|
1603
|
+
)
|
|
1604
|
+
contract_router = None
|
|
1605
|
+
|
|
1606
|
+
# Unsubscribe from contract registry topics
|
|
1607
|
+
for unsub_name, unsub_func in [
|
|
1608
|
+
("contract-registered", contract_unsub_registered),
|
|
1609
|
+
("contract-deregistered", contract_unsub_deregistered),
|
|
1610
|
+
("node-heartbeat", contract_unsub_heartbeat),
|
|
1611
|
+
]:
|
|
1612
|
+
if unsub_func is not None:
|
|
1613
|
+
try:
|
|
1614
|
+
await unsub_func()
|
|
1615
|
+
logger.debug(
|
|
1616
|
+
"Contract registry consumer %s stopped (correlation_id=%s)",
|
|
1617
|
+
unsub_name,
|
|
1618
|
+
correlation_id,
|
|
1619
|
+
)
|
|
1620
|
+
except Exception as unsub_error:
|
|
1621
|
+
logger.warning(
|
|
1622
|
+
"Failed to stop contract registry consumer %s: %s (correlation_id=%s)",
|
|
1623
|
+
unsub_name,
|
|
1624
|
+
sanitize_error_message(unsub_error),
|
|
1625
|
+
correlation_id,
|
|
1626
|
+
)
|
|
1627
|
+
contract_unsub_registered = None
|
|
1628
|
+
contract_unsub_deregistered = None
|
|
1629
|
+
contract_unsub_heartbeat = None
|
|
1630
|
+
|
|
1370
1631
|
# Stop health server (fast, non-blocking)
|
|
1371
1632
|
if health_server is not None:
|
|
1372
1633
|
try:
|
|
@@ -1492,7 +1753,7 @@ async def bootstrap() -> int:
|
|
|
1492
1753
|
|
|
1493
1754
|
finally:
|
|
1494
1755
|
# Guard cleanup - stop all resources if not already stopped
|
|
1495
|
-
# Order: introspection consumer -> health server -> runtime -> pool
|
|
1756
|
+
# Order: introspection consumer -> contract registry -> health server -> runtime -> pool
|
|
1496
1757
|
|
|
1497
1758
|
if introspection_unsubscribe is not None:
|
|
1498
1759
|
try:
|
|
@@ -1504,6 +1765,32 @@ async def bootstrap() -> int:
|
|
|
1504
1765
|
correlation_id,
|
|
1505
1766
|
)
|
|
1506
1767
|
|
|
1768
|
+
# Cleanup contract registry router and consumers
|
|
1769
|
+
if contract_router is not None:
|
|
1770
|
+
try:
|
|
1771
|
+
await contract_router.stop()
|
|
1772
|
+
except Exception as cleanup_error:
|
|
1773
|
+
logger.warning(
|
|
1774
|
+
"Failed to stop contract registry router during cleanup: %s (correlation_id=%s)",
|
|
1775
|
+
sanitize_error_message(cleanup_error),
|
|
1776
|
+
correlation_id,
|
|
1777
|
+
)
|
|
1778
|
+
|
|
1779
|
+
for unsub_func in [
|
|
1780
|
+
contract_unsub_registered,
|
|
1781
|
+
contract_unsub_deregistered,
|
|
1782
|
+
contract_unsub_heartbeat,
|
|
1783
|
+
]:
|
|
1784
|
+
if unsub_func is not None:
|
|
1785
|
+
try:
|
|
1786
|
+
await unsub_func()
|
|
1787
|
+
except Exception as cleanup_error:
|
|
1788
|
+
logger.warning(
|
|
1789
|
+
"Failed to stop contract registry consumer during cleanup: %s (correlation_id=%s)",
|
|
1790
|
+
sanitize_error_message(cleanup_error),
|
|
1791
|
+
correlation_id,
|
|
1792
|
+
)
|
|
1793
|
+
|
|
1507
1794
|
if health_server is not None:
|
|
1508
1795
|
try:
|
|
1509
1796
|
await health_server.stop()
|
|
@@ -73,6 +73,12 @@ from omnibase_infra.errors import (
|
|
|
73
73
|
from omnibase_infra.event_bus.event_bus_inmemory import EventBusInmemory
|
|
74
74
|
from omnibase_infra.event_bus.event_bus_kafka import EventBusKafka
|
|
75
75
|
from omnibase_infra.models import ModelNodeIdentity
|
|
76
|
+
from omnibase_infra.models.runtime.model_resolved_dependencies import (
|
|
77
|
+
ModelResolvedDependencies,
|
|
78
|
+
)
|
|
79
|
+
from omnibase_infra.runtime.contract_dependency_resolver import (
|
|
80
|
+
ContractDependencyResolver,
|
|
81
|
+
)
|
|
76
82
|
from omnibase_infra.runtime.envelope_validator import (
|
|
77
83
|
normalize_correlation_id,
|
|
78
84
|
validate_envelope,
|
|
@@ -764,6 +770,10 @@ class RuntimeHostProcess:
|
|
|
764
770
|
# Enables contract config to be passed to handlers via initialize()
|
|
765
771
|
self._handler_descriptors: dict[str, ModelHandlerDescriptor] = {}
|
|
766
772
|
|
|
773
|
+
# Contract dependency resolver for protocol auto-injection (OMN-1903)
|
|
774
|
+
# Lazy-created when first needed during handler population
|
|
775
|
+
self._dependency_resolver: ContractDependencyResolver | None = None
|
|
776
|
+
|
|
767
777
|
# Pending message tracking for graceful shutdown (OMN-756)
|
|
768
778
|
# Tracks count of in-flight messages currently being processed
|
|
769
779
|
self._pending_message_count: int = 0
|
|
@@ -1798,11 +1808,44 @@ class RuntimeHostProcess:
|
|
|
1798
1808
|
handler_type
|
|
1799
1809
|
)
|
|
1800
1810
|
|
|
1801
|
-
#
|
|
1811
|
+
# Get descriptor early for dependency resolution (OMN-1903)
|
|
1812
|
+
descriptor = self._handler_descriptors.get(handler_type)
|
|
1813
|
+
|
|
1814
|
+
# R1/R3: Resolve dependencies if contract has them (OMN-1903)
|
|
1815
|
+
# Returns None if descriptor has no contract_path or no dependencies
|
|
1816
|
+
resolved_dependencies: ModelResolvedDependencies | None = None
|
|
1817
|
+
if descriptor:
|
|
1818
|
+
# This may raise ProtocolDependencyResolutionError (R2: fail-fast)
|
|
1819
|
+
resolved_dependencies = await self._resolve_handler_dependencies(
|
|
1820
|
+
descriptor
|
|
1821
|
+
)
|
|
1822
|
+
|
|
1823
|
+
# Instantiate the handler with container (and dependencies if supported)
|
|
1802
1824
|
# ProtocolContainerAware defines __init__(container: ModelONEXContainer)
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1825
|
+
# Handlers that support OMN-1732 can accept optional dependencies parameter
|
|
1826
|
+
handler_instance: ProtocolContainerAware
|
|
1827
|
+
if resolved_dependencies and self._accepts_dependencies_param(
|
|
1828
|
+
handler_cls
|
|
1829
|
+
):
|
|
1830
|
+
# New-style handler with dependency injection
|
|
1831
|
+
# Type ignore: handler_cls is typed as ProtocolContainerAware which doesn't
|
|
1832
|
+
# have dependencies param, but runtime introspection confirmed it exists
|
|
1833
|
+
handler_instance = handler_cls( # type: ignore[call-arg]
|
|
1834
|
+
container=container,
|
|
1835
|
+
dependencies=resolved_dependencies,
|
|
1836
|
+
)
|
|
1837
|
+
logger.debug(
|
|
1838
|
+
"Instantiated handler with resolved dependencies",
|
|
1839
|
+
extra={
|
|
1840
|
+
"handler_type": handler_type,
|
|
1841
|
+
"resolved_protocols": list(
|
|
1842
|
+
resolved_dependencies.protocols.keys()
|
|
1843
|
+
),
|
|
1844
|
+
},
|
|
1845
|
+
)
|
|
1846
|
+
else:
|
|
1847
|
+
# Legacy handler without dependency parameter
|
|
1848
|
+
handler_instance = handler_cls(container=container)
|
|
1806
1849
|
|
|
1807
1850
|
# Call initialize() if the handler has this method
|
|
1808
1851
|
# Handlers may require async initialization with config
|
|
@@ -1814,7 +1857,6 @@ class RuntimeHostProcess:
|
|
|
1814
1857
|
config_source = "runtime_only"
|
|
1815
1858
|
|
|
1816
1859
|
# Layer 1: Contract config as baseline (if descriptor exists with config)
|
|
1817
|
-
descriptor = self._handler_descriptors.get(handler_type)
|
|
1818
1860
|
if descriptor and descriptor.contract_config:
|
|
1819
1861
|
effective_config.update(descriptor.contract_config)
|
|
1820
1862
|
config_source = "contract_only"
|
|
@@ -1890,6 +1932,106 @@ class RuntimeHostProcess:
|
|
|
1890
1932
|
},
|
|
1891
1933
|
)
|
|
1892
1934
|
|
|
1935
|
+
async def _resolve_handler_dependencies(
|
|
1936
|
+
self,
|
|
1937
|
+
descriptor: ModelHandlerDescriptor,
|
|
1938
|
+
) -> ModelResolvedDependencies | None:
|
|
1939
|
+
"""Resolve protocol dependencies for a handler from its contract.
|
|
1940
|
+
|
|
1941
|
+
Part of OMN-1903: Runtime dependency injection integration.
|
|
1942
|
+
|
|
1943
|
+
If the handler's contract declares protocol dependencies, this method
|
|
1944
|
+
resolves them from the container's service_registry. Returns None if:
|
|
1945
|
+
- No contract_path in descriptor (opt-in behavior, R3)
|
|
1946
|
+
- Contract has no dependencies section
|
|
1947
|
+
|
|
1948
|
+
Args:
|
|
1949
|
+
descriptor: Handler descriptor containing contract_path.
|
|
1950
|
+
|
|
1951
|
+
Returns:
|
|
1952
|
+
ModelResolvedDependencies with resolved protocols, or None if no
|
|
1953
|
+
dependencies to resolve.
|
|
1954
|
+
|
|
1955
|
+
Raises:
|
|
1956
|
+
ProtocolDependencyResolutionError: If any required protocol cannot
|
|
1957
|
+
be resolved (fail-fast behavior, R2).
|
|
1958
|
+
ProtocolConfigurationError: If contract file cannot be loaded.
|
|
1959
|
+
"""
|
|
1960
|
+
# R3: Opt-in behavior - skip if no contract_path
|
|
1961
|
+
if not descriptor.contract_path:
|
|
1962
|
+
logger.debug(
|
|
1963
|
+
"Handler has no contract_path, skipping dependency resolution",
|
|
1964
|
+
extra={"handler_id": descriptor.handler_id},
|
|
1965
|
+
)
|
|
1966
|
+
return None
|
|
1967
|
+
|
|
1968
|
+
# Lazy-create resolver on first use
|
|
1969
|
+
if self._dependency_resolver is None:
|
|
1970
|
+
container = self._get_or_create_container()
|
|
1971
|
+
self._dependency_resolver = ContractDependencyResolver(container)
|
|
1972
|
+
|
|
1973
|
+
# R1: Call resolver with contract path
|
|
1974
|
+
contract_path = Path(descriptor.contract_path)
|
|
1975
|
+
logger.debug(
|
|
1976
|
+
"Resolving dependencies for handler",
|
|
1977
|
+
extra={
|
|
1978
|
+
"handler_id": descriptor.handler_id,
|
|
1979
|
+
"contract_path": str(contract_path),
|
|
1980
|
+
},
|
|
1981
|
+
)
|
|
1982
|
+
|
|
1983
|
+
# R2: Fail-fast on missing protocols (allow_missing=False)
|
|
1984
|
+
resolved = await self._dependency_resolver.resolve_from_path(
|
|
1985
|
+
contract_path,
|
|
1986
|
+
allow_missing=False,
|
|
1987
|
+
)
|
|
1988
|
+
|
|
1989
|
+
if resolved:
|
|
1990
|
+
logger.debug(
|
|
1991
|
+
"Resolved dependencies for handler",
|
|
1992
|
+
extra={
|
|
1993
|
+
"handler_id": descriptor.handler_id,
|
|
1994
|
+
"resolved_protocols": list(resolved.protocols.keys()),
|
|
1995
|
+
},
|
|
1996
|
+
)
|
|
1997
|
+
else:
|
|
1998
|
+
logger.debug(
|
|
1999
|
+
"No protocol dependencies in contract",
|
|
2000
|
+
extra={
|
|
2001
|
+
"handler_id": descriptor.handler_id,
|
|
2002
|
+
"contract_path": str(contract_path),
|
|
2003
|
+
},
|
|
2004
|
+
)
|
|
2005
|
+
|
|
2006
|
+
return resolved if resolved else None
|
|
2007
|
+
|
|
2008
|
+
def _accepts_dependencies_param(self, handler_cls: type) -> bool:
|
|
2009
|
+
"""Check if a handler class accepts 'dependencies' in its constructor.
|
|
2010
|
+
|
|
2011
|
+
Part of OMN-1903: Runtime dependency injection integration.
|
|
2012
|
+
|
|
2013
|
+
Uses introspection to check if the handler's __init__ accepts a
|
|
2014
|
+
'dependencies' keyword argument. This enables gradual migration:
|
|
2015
|
+
- Legacy handlers: __init__(container) - no dependencies param
|
|
2016
|
+
- New handlers: __init__(container, dependencies=...) - receives deps
|
|
2017
|
+
|
|
2018
|
+
Args:
|
|
2019
|
+
handler_cls: The handler class to check.
|
|
2020
|
+
|
|
2021
|
+
Returns:
|
|
2022
|
+
True if the handler accepts 'dependencies' parameter, False otherwise.
|
|
2023
|
+
"""
|
|
2024
|
+
import inspect
|
|
2025
|
+
|
|
2026
|
+
try:
|
|
2027
|
+
# Use inspect.signature on the class itself, not __init__
|
|
2028
|
+
# This avoids the "unsound instance access" mypy warning
|
|
2029
|
+
sig = inspect.signature(handler_cls)
|
|
2030
|
+
return "dependencies" in sig.parameters
|
|
2031
|
+
except (ValueError, TypeError):
|
|
2032
|
+
# Cannot inspect signature (e.g., builtin class)
|
|
2033
|
+
return False
|
|
2034
|
+
|
|
1893
2035
|
async def _load_contract_configs(self, correlation_id: UUID) -> None:
|
|
1894
2036
|
"""Load contract configurations from all discovered contracts.
|
|
1895
2037
|
|
|
@@ -2919,6 +3061,8 @@ class RuntimeHostProcess:
|
|
|
2919
3061
|
dispatch_engine=self._dispatch_engine,
|
|
2920
3062
|
environment=environment,
|
|
2921
3063
|
node_name="runtime-host",
|
|
3064
|
+
service=self._node_identity.service,
|
|
3065
|
+
version=self._node_identity.version,
|
|
2922
3066
|
)
|
|
2923
3067
|
|
|
2924
3068
|
# Wire subscriptions for each handler with a contract
|
|
@@ -14,6 +14,7 @@ ModelPolicyKey, and ModelPolicyRegistration.
|
|
|
14
14
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
|
+
from omnibase_core.models.errors import ModelOnexError
|
|
17
18
|
from omnibase_core.models.primitives import ModelSemVer
|
|
18
19
|
|
|
19
20
|
|
|
@@ -81,9 +82,12 @@ def normalize_version(version: str) -> str:
|
|
|
81
82
|
expanded_version = ".".join(version_nums)
|
|
82
83
|
|
|
83
84
|
# Parse with ModelSemVer for validation
|
|
85
|
+
# Note: ModelSemVer.parse() raises ModelOnexError for invalid versions,
|
|
86
|
+
# but we also catch ValueError for defensive programming against
|
|
87
|
+
# potential upstream changes.
|
|
84
88
|
try:
|
|
85
89
|
semver = ModelSemVer.parse(expanded_version)
|
|
86
|
-
except
|
|
90
|
+
except (ModelOnexError, ValueError) as e:
|
|
87
91
|
raise ValueError(f"Invalid version format: {e}") from e
|
|
88
92
|
|
|
89
93
|
result: str = semver.to_string()
|