omnibase_infra 0.2.1__py3-none-any.whl → 0.2.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. omnibase_infra/__init__.py +1 -1
  2. omnibase_infra/adapters/adapter_onex_tool_execution.py +446 -0
  3. omnibase_infra/cli/commands.py +1 -1
  4. omnibase_infra/configs/widget_mapping.yaml +176 -0
  5. omnibase_infra/contracts/handlers/filesystem/handler_contract.yaml +4 -1
  6. omnibase_infra/contracts/handlers/mcp/handler_contract.yaml +4 -1
  7. omnibase_infra/errors/error_compute_registry.py +4 -1
  8. omnibase_infra/errors/error_event_bus_registry.py +4 -1
  9. omnibase_infra/errors/error_infra.py +3 -1
  10. omnibase_infra/errors/error_policy_registry.py +4 -1
  11. omnibase_infra/handlers/handler_db.py +2 -1
  12. omnibase_infra/handlers/handler_graph.py +10 -5
  13. omnibase_infra/handlers/handler_mcp.py +736 -63
  14. omnibase_infra/handlers/mixins/mixin_consul_kv.py +4 -3
  15. omnibase_infra/handlers/mixins/mixin_consul_service.py +2 -1
  16. omnibase_infra/handlers/service_discovery/handler_service_discovery_consul.py +301 -4
  17. omnibase_infra/handlers/service_discovery/models/model_service_info.py +10 -0
  18. omnibase_infra/mixins/mixin_async_circuit_breaker.py +3 -2
  19. omnibase_infra/mixins/mixin_node_introspection.py +24 -7
  20. omnibase_infra/mixins/mixin_retry_execution.py +1 -1
  21. omnibase_infra/models/handlers/__init__.py +10 -0
  22. omnibase_infra/models/handlers/model_bootstrap_handler_descriptor.py +162 -0
  23. omnibase_infra/models/handlers/model_handler_descriptor.py +15 -0
  24. omnibase_infra/models/mcp/__init__.py +15 -0
  25. omnibase_infra/models/mcp/model_mcp_contract_config.py +80 -0
  26. omnibase_infra/models/mcp/model_mcp_server_config.py +67 -0
  27. omnibase_infra/models/mcp/model_mcp_tool_definition.py +73 -0
  28. omnibase_infra/models/mcp/model_mcp_tool_parameter.py +35 -0
  29. omnibase_infra/models/registration/model_node_capabilities.py +11 -0
  30. omnibase_infra/nodes/architecture_validator/contract_architecture_validator.yaml +0 -5
  31. omnibase_infra/nodes/architecture_validator/registry/registry_infra_architecture_validator.py +17 -10
  32. omnibase_infra/nodes/effects/contract.yaml +0 -5
  33. omnibase_infra/nodes/node_registration_orchestrator/contract.yaml +7 -0
  34. omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_node_introspected.py +86 -1
  35. omnibase_infra/nodes/node_registration_orchestrator/introspection_event_router.py +3 -3
  36. omnibase_infra/nodes/node_registration_orchestrator/registry/registry_infra_node_registration_orchestrator.py +9 -8
  37. omnibase_infra/nodes/node_registration_orchestrator/wiring.py +14 -13
  38. omnibase_infra/nodes/node_registration_storage_effect/contract.yaml +0 -5
  39. omnibase_infra/nodes/node_registration_storage_effect/registry/registry_infra_registration_storage.py +46 -25
  40. omnibase_infra/nodes/node_registry_effect/contract.yaml +0 -5
  41. omnibase_infra/nodes/node_registry_effect/handlers/handler_partial_retry.py +2 -1
  42. omnibase_infra/nodes/node_service_discovery_effect/registry/registry_infra_service_discovery.py +24 -19
  43. omnibase_infra/plugins/examples/plugin_json_normalizer.py +2 -2
  44. omnibase_infra/plugins/examples/plugin_json_normalizer_error_handling.py +2 -2
  45. omnibase_infra/plugins/plugin_compute_base.py +16 -2
  46. omnibase_infra/protocols/protocol_event_projector.py +1 -1
  47. omnibase_infra/runtime/__init__.py +51 -1
  48. omnibase_infra/runtime/binding_config_resolver.py +102 -37
  49. omnibase_infra/runtime/constants_notification.py +75 -0
  50. omnibase_infra/runtime/contract_handler_discovery.py +6 -1
  51. omnibase_infra/runtime/handler_bootstrap_source.py +514 -0
  52. omnibase_infra/runtime/handler_contract_config_loader.py +603 -0
  53. omnibase_infra/runtime/handler_contract_source.py +289 -167
  54. omnibase_infra/runtime/handler_plugin_loader.py +4 -2
  55. omnibase_infra/runtime/mixin_semver_cache.py +25 -1
  56. omnibase_infra/runtime/mixins/__init__.py +7 -0
  57. omnibase_infra/runtime/mixins/mixin_projector_notification_publishing.py +566 -0
  58. omnibase_infra/runtime/mixins/mixin_projector_sql_operations.py +31 -10
  59. omnibase_infra/runtime/models/__init__.py +24 -0
  60. omnibase_infra/runtime/models/model_health_check_result.py +2 -1
  61. omnibase_infra/runtime/models/model_projector_notification_config.py +171 -0
  62. omnibase_infra/runtime/models/model_transition_notification_outbox_config.py +112 -0
  63. omnibase_infra/runtime/models/model_transition_notification_outbox_metrics.py +140 -0
  64. omnibase_infra/runtime/models/model_transition_notification_publisher_metrics.py +357 -0
  65. omnibase_infra/runtime/projector_plugin_loader.py +1 -1
  66. omnibase_infra/runtime/projector_shell.py +229 -1
  67. omnibase_infra/runtime/protocols/__init__.py +10 -0
  68. omnibase_infra/runtime/registry/registry_protocol_binding.py +3 -2
  69. omnibase_infra/runtime/registry_policy.py +9 -326
  70. omnibase_infra/runtime/secret_resolver.py +4 -2
  71. omnibase_infra/runtime/service_kernel.py +10 -2
  72. omnibase_infra/runtime/service_message_dispatch_engine.py +4 -2
  73. omnibase_infra/runtime/service_runtime_host_process.py +225 -15
  74. omnibase_infra/runtime/transition_notification_outbox.py +1190 -0
  75. omnibase_infra/runtime/transition_notification_publisher.py +764 -0
  76. omnibase_infra/runtime/util_container_wiring.py +6 -5
  77. omnibase_infra/runtime/util_wiring.py +5 -1
  78. omnibase_infra/schemas/schema_transition_notification_outbox.sql +245 -0
  79. omnibase_infra/services/mcp/__init__.py +31 -0
  80. omnibase_infra/services/mcp/mcp_server_lifecycle.py +443 -0
  81. omnibase_infra/services/mcp/service_mcp_tool_discovery.py +411 -0
  82. omnibase_infra/services/mcp/service_mcp_tool_registry.py +329 -0
  83. omnibase_infra/services/mcp/service_mcp_tool_sync.py +547 -0
  84. omnibase_infra/services/registry_api/__init__.py +40 -0
  85. omnibase_infra/services/registry_api/main.py +243 -0
  86. omnibase_infra/services/registry_api/models/__init__.py +66 -0
  87. omnibase_infra/services/registry_api/models/model_capability_widget_mapping.py +38 -0
  88. omnibase_infra/services/registry_api/models/model_pagination_info.py +48 -0
  89. omnibase_infra/services/registry_api/models/model_registry_discovery_response.py +73 -0
  90. omnibase_infra/services/registry_api/models/model_registry_health_response.py +49 -0
  91. omnibase_infra/services/registry_api/models/model_registry_instance_view.py +88 -0
  92. omnibase_infra/services/registry_api/models/model_registry_node_view.py +88 -0
  93. omnibase_infra/services/registry_api/models/model_registry_summary.py +60 -0
  94. omnibase_infra/services/registry_api/models/model_response_list_instances.py +43 -0
  95. omnibase_infra/services/registry_api/models/model_response_list_nodes.py +51 -0
  96. omnibase_infra/services/registry_api/models/model_warning.py +49 -0
  97. omnibase_infra/services/registry_api/models/model_widget_defaults.py +28 -0
  98. omnibase_infra/services/registry_api/models/model_widget_mapping.py +51 -0
  99. omnibase_infra/services/registry_api/routes.py +371 -0
  100. omnibase_infra/services/registry_api/service.py +846 -0
  101. omnibase_infra/services/service_capability_query.py +4 -4
  102. omnibase_infra/services/service_health.py +3 -2
  103. omnibase_infra/services/service_timeout_emitter.py +13 -2
  104. omnibase_infra/utils/util_dsn_validation.py +1 -1
  105. omnibase_infra/validation/__init__.py +3 -19
  106. omnibase_infra/validation/contracts/security.validation.yaml +114 -0
  107. omnibase_infra/validation/infra_validators.py +35 -24
  108. omnibase_infra/validation/validation_exemptions.yaml +113 -9
  109. omnibase_infra/validation/validator_chain_propagation.py +2 -2
  110. omnibase_infra/validation/validator_runtime_shape.py +1 -1
  111. omnibase_infra/validation/validator_security.py +473 -370
  112. {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.2.dist-info}/METADATA +2 -2
  113. {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.2.dist-info}/RECORD +116 -74
  114. {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.2.dist-info}/WHEEL +0 -0
  115. {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.2.dist-info}/entry_points.txt +0 -0
  116. {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.2.dist-info}/licenses/LICENSE +0 -0
@@ -47,12 +47,19 @@ Exports:
47
47
  ModelSecretResolverMetrics: Resolution metrics for observability
48
48
  ModelSecretSourceInfo: Non-sensitive source information for introspection
49
49
  ModelRetryPolicy: Retry policy configuration for handler operations
50
+ ModelTransitionNotificationPublisherMetrics: Metrics for transition notification publisher
51
+ ModelTransitionNotificationOutboxMetrics: Metrics for transition notification outbox
52
+ ModelTransitionNotificationOutboxConfig: Configuration for transition notification outbox
53
+ ModelStateTransitionNotification: State transition notification (re-export from omnibase_core)
54
+ ModelProjectorNotificationConfig: Configuration for projector notification publishing
50
55
  ModelBindingConfig: Configuration for binding a handler to the runtime
51
56
  ModelBindingConfigCacheStats: Cache statistics for BindingConfigResolver
52
57
  ModelBindingConfigResolverConfig: Configuration for BindingConfigResolver
53
58
  ModelConfigCacheEntry: Internal cache entry for BindingConfigResolver
54
59
  """
55
60
 
61
+ # Re-export from omnibase_core for convenience
62
+ from omnibase_core.models.notifications import ModelStateTransitionNotification
56
63
  from omnibase_infra.runtime.enums.enum_config_ref_scheme import EnumConfigRefScheme
57
64
  from omnibase_infra.runtime.models.model_batch_lifecycle_result import (
58
65
  ModelBatchLifecycleResult,
@@ -110,6 +117,9 @@ from omnibase_infra.runtime.models.model_policy_registration import (
110
117
  )
111
118
  from omnibase_infra.runtime.models.model_policy_result import ModelPolicyResult
112
119
  from omnibase_infra.runtime.models.model_policy_type_filter import ModelPolicyTypeFilter
120
+ from omnibase_infra.runtime.models.model_projector_notification_config import (
121
+ ModelProjectorNotificationConfig,
122
+ )
113
123
  from omnibase_infra.runtime.models.model_projector_plugin_loader_config import (
114
124
  ModelProjectorPluginLoaderConfig,
115
125
  )
@@ -142,6 +152,15 @@ from omnibase_infra.runtime.models.model_shutdown_batch_result import (
142
152
  ModelShutdownBatchResult,
143
153
  )
144
154
  from omnibase_infra.runtime.models.model_shutdown_config import ModelShutdownConfig
155
+ from omnibase_infra.runtime.models.model_transition_notification_outbox_config import (
156
+ ModelTransitionNotificationOutboxConfig,
157
+ )
158
+ from omnibase_infra.runtime.models.model_transition_notification_outbox_metrics import (
159
+ ModelTransitionNotificationOutboxMetrics,
160
+ )
161
+ from omnibase_infra.runtime.models.model_transition_notification_publisher_metrics import (
162
+ ModelTransitionNotificationPublisherMetrics,
163
+ )
145
164
 
146
165
  __all__: list[str] = [
147
166
  "EnumConfigRefScheme",
@@ -173,6 +192,7 @@ __all__: list[str] = [
173
192
  "ModelPolicyRegistration",
174
193
  "ModelPolicyResult",
175
194
  "ModelPolicyTypeFilter",
195
+ "ModelProjectorNotificationConfig",
176
196
  "ModelProjectorPluginLoaderConfig",
177
197
  "ModelProtocolRegistrationConfig",
178
198
  "ModelRetryPolicy",
@@ -188,5 +208,9 @@ __all__: list[str] = [
188
208
  "ModelSecretSourceSpec",
189
209
  "ModelShutdownBatchResult",
190
210
  "ModelShutdownConfig",
211
+ "ModelStateTransitionNotification",
212
+ "ModelTransitionNotificationOutboxConfig",
213
+ "ModelTransitionNotificationOutboxMetrics",
214
+ "ModelTransitionNotificationPublisherMetrics",
191
215
  "SecretSourceType",
192
216
  ]
@@ -209,10 +209,11 @@ class ModelHealthCheckResult(BaseModel):
209
209
  details=health_response,
210
210
  )
211
211
  # Non-dict response - treat as details, assume healthy
212
+ # Convert to string representation for JsonType compatibility
212
213
  return cls(
213
214
  handler_type=handler_type,
214
215
  healthy=True,
215
- details={"raw_response": health_response},
216
+ details={"raw_response": str(health_response)},
216
217
  )
217
218
 
218
219
  def __str__(self) -> str:
@@ -0,0 +1,171 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2025 OmniNode Team
3
+ """Projector Notification Configuration Model.
4
+
5
+ Defines configuration for state transition notification publishing from projectors.
6
+ When configured, the projector will publish notifications to the event bus after
7
+ successful state transitions are committed.
8
+
9
+ This enables the Observer pattern for orchestrator coordination without tight
10
+ coupling between reducers and workflow coordinators.
11
+
12
+ Architecture Overview:
13
+ 1. ProjectorShell processes events and persists state changes
14
+ 2. Before commit, the previous state is fetched (if state tracking enabled)
15
+ 3. After successful commit, a notification is published with from_state/to_state
16
+ 4. Orchestrators subscribe to notifications and coordinate downstream workflows
17
+
18
+ Configuration Fields:
19
+ - expected_topic: Expected event bus topic for validation/documentation (required).
20
+ NOTE: The actual publishing topic is determined by TransitionNotificationPublisher.
21
+ This field accepts "topic" as an alias for backwards compatibility.
22
+ - state_column: Column name containing the FSM state (required)
23
+ - aggregate_id_column: Column name containing the aggregate ID (required)
24
+ - version_column: Column name containing the projection version (optional)
25
+ - enabled: Whether notifications are enabled (default: True)
26
+
27
+ Example Usage:
28
+ >>> from omnibase_infra.runtime.models import ModelProjectorNotificationConfig
29
+ >>>
30
+ >>> # Using the preferred field name
31
+ >>> config = ModelProjectorNotificationConfig(
32
+ ... expected_topic="onex.fsm.state.transitions.v1",
33
+ ... state_column="current_state",
34
+ ... aggregate_id_column="entity_id",
35
+ ... version_column="version",
36
+ ... enabled=True,
37
+ ... )
38
+ >>>
39
+ >>> # Using the backwards-compatible alias
40
+ >>> config = ModelProjectorNotificationConfig(
41
+ ... topic="onex.fsm.state.transitions.v1", # alias for expected_topic
42
+ ... state_column="current_state",
43
+ ... aggregate_id_column="entity_id",
44
+ ... )
45
+
46
+ Related Tickets:
47
+ - OMN-1139: Implement TransitionNotificationPublisher integration with ProjectorShell
48
+
49
+ Thread Safety:
50
+ This model is immutable (frozen=True) after creation, making it thread-safe
51
+ for concurrent read access.
52
+
53
+ .. versionadded:: 0.8.0
54
+ """
55
+
56
+ from __future__ import annotations
57
+
58
+ from pydantic import BaseModel, ConfigDict, Field
59
+
60
+
61
+ class ModelProjectorNotificationConfig(BaseModel):
62
+ """Configuration for state transition notification publishing.
63
+
64
+ When attached to a ProjectorShell via the notification_config parameter,
65
+ enables automatic publishing of state transition notifications after
66
+ successful projection commits.
67
+
68
+ Attributes:
69
+ expected_topic: Expected event bus topic for state transition notifications.
70
+ This field is for documentation and validation purposes only - the actual
71
+ publishing topic is determined by TransitionNotificationPublisher's
72
+ configuration. ProjectorShell will log a warning if this value differs
73
+ from the publisher's configured topic. Example topics:
74
+ - "onex.fsm.state.transitions.v1"
75
+ - "registration.state.transitions.v1"
76
+ Accepts "topic" as an alias for backwards compatibility.
77
+ state_column: Name of the column that contains the FSM state value.
78
+ This column must exist in the projection schema and contain string
79
+ values representing the current state.
80
+ aggregate_id_column: Name of the column that contains the aggregate ID.
81
+ This column must exist in the projection schema and typically contains
82
+ a UUID identifying the aggregate instance.
83
+ version_column: Optional name of the column that contains the projection
84
+ version. If specified, the version value will be included in
85
+ notifications for ordering and idempotency detection.
86
+ enabled: Whether notification publishing is enabled. Defaults to True.
87
+ Set to False to disable notifications without removing configuration.
88
+
89
+ Example:
90
+ >>> config = ModelProjectorNotificationConfig(
91
+ ... expected_topic="onex.fsm.state.transitions.v1",
92
+ ... state_column="current_state",
93
+ ... aggregate_id_column="entity_id",
94
+ ... version_column="version",
95
+ ... )
96
+ >>> config.expected_topic
97
+ 'onex.fsm.state.transitions.v1'
98
+ >>> config.state_column
99
+ 'current_state'
100
+ >>> config.enabled
101
+ True
102
+
103
+ Note:
104
+ The column names specified must match columns defined in the projector's
105
+ contract schema. The ProjectorShell will validate these column names
106
+ against the schema at initialization time.
107
+
108
+ Important: The expected_topic field does NOT control where notifications
109
+ are published. The actual topic is determined by TransitionNotificationPublisher.
110
+ This field exists so ProjectorShell can warn when there's a mismatch between
111
+ the expected and actual topics, helping catch configuration errors early.
112
+
113
+ See Also:
114
+ - ProjectorShell: Uses this config for notification integration
115
+ - TransitionNotificationPublisher: Publishes the notifications
116
+ - ModelStateTransitionNotification: The notification payload model
117
+ """
118
+
119
+ model_config = ConfigDict(
120
+ frozen=True,
121
+ extra="forbid",
122
+ from_attributes=True,
123
+ populate_by_name=True, # Allow both "expected_topic" and "topic" (alias)
124
+ )
125
+
126
+ expected_topic: str = Field(
127
+ ...,
128
+ alias="topic", # Backwards compatibility
129
+ min_length=1,
130
+ max_length=256,
131
+ pattern=r"^[a-zA-Z][a-zA-Z0-9]*([._-][a-zA-Z0-9]+)*$",
132
+ description=(
133
+ "Expected event bus topic for state transition notifications. "
134
+ "Must start with a letter (a-zA-Z), end with an alphanumeric character, "
135
+ "and contain only alphanumeric characters with single dots, underscores, "
136
+ "or hyphens as separators. Consecutive separators (e.g., '..', '--', '__', '.-') "
137
+ "and trailing separators are not allowed. "
138
+ "NOTE: This field is for documentation and validation purposes only. "
139
+ "The actual publishing topic is determined by TransitionNotificationPublisher. "
140
+ "ProjectorShell will warn if this value differs from the publisher's topic."
141
+ ),
142
+ )
143
+
144
+ state_column: str = Field(
145
+ ...,
146
+ min_length=1,
147
+ max_length=128,
148
+ description="Column name containing the FSM state value",
149
+ )
150
+
151
+ aggregate_id_column: str = Field(
152
+ ...,
153
+ min_length=1,
154
+ max_length=128,
155
+ description="Column name containing the aggregate ID",
156
+ )
157
+
158
+ version_column: str | None = Field(
159
+ default=None,
160
+ min_length=1,
161
+ max_length=128,
162
+ description="Optional column name containing the projection version",
163
+ )
164
+
165
+ enabled: bool = Field(
166
+ default=True,
167
+ description="Whether notification publishing is enabled",
168
+ )
169
+
170
+
171
+ __all__: list[str] = ["ModelProjectorNotificationConfig"]
@@ -0,0 +1,112 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2025 OmniNode Team
3
+ """Configuration model for TransitionNotificationOutbox.
4
+
5
+ This module provides a Pydantic model for configuring the TransitionNotificationOutbox,
6
+ which implements the outbox pattern for guaranteed notification delivery of state
7
+ transition events.
8
+
9
+ Related:
10
+ - TransitionNotificationOutbox: The outbox implementation
11
+ - ModelTransitionNotificationOutboxMetrics: Metrics model for observability
12
+ - docs/patterns/retry_backoff_compensation_strategy.md: Outbox pattern docs
13
+
14
+ .. versionadded:: 0.8.0
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from pydantic import BaseModel, ConfigDict, Field
20
+
21
+
22
+ class ModelTransitionNotificationOutboxConfig(BaseModel):
23
+ """Configuration for TransitionNotificationOutbox.
24
+
25
+ This model encapsulates all configuration options for the outbox pattern
26
+ implementation that ensures at-least-once delivery semantics for state
27
+ transition notifications.
28
+
29
+ The outbox stores notifications in the same database transaction as projections,
30
+ then processes them asynchronously via a background processor.
31
+
32
+ Attributes:
33
+ outbox_table: PostgreSQL table name for the outbox.
34
+ batch_size: Maximum notifications to process per batch.
35
+ poll_interval_seconds: Seconds between polls when idle.
36
+ query_timeout_seconds: Timeout for database queries.
37
+ strict_transaction_mode: If True, raises error when store() called
38
+ outside a transaction context.
39
+ shutdown_timeout_seconds: Timeout for graceful shutdown.
40
+ max_retries: Maximum retry attempts before moving to DLQ. None disables DLQ.
41
+ dlq_topic: Topic name for DLQ (for metrics/logging purposes).
42
+
43
+ Example:
44
+ >>> config = ModelTransitionNotificationOutboxConfig(
45
+ ... outbox_table="state_transition_outbox",
46
+ ... batch_size=50,
47
+ ... poll_interval_seconds=0.5,
48
+ ... max_retries=3,
49
+ ... dlq_topic="notifications-dlq",
50
+ ... )
51
+ >>> outbox = TransitionNotificationOutbox(
52
+ ... pool=pool,
53
+ ... publisher=publisher,
54
+ ... **config.model_dump(), # Unpack as kwargs
55
+ ... )
56
+
57
+ Related:
58
+ - TransitionNotificationOutbox: Uses this configuration
59
+ - ModelTransitionNotificationOutboxMetrics: Runtime metrics
60
+ """
61
+
62
+ model_config = ConfigDict(frozen=True)
63
+
64
+ outbox_table: str = Field(
65
+ default="transition_notification_outbox",
66
+ min_length=1,
67
+ max_length=63, # PostgreSQL identifier limit
68
+ description="PostgreSQL table name for the outbox",
69
+ )
70
+ batch_size: int = Field(
71
+ default=100,
72
+ ge=1,
73
+ le=1000,
74
+ description="Maximum notifications to process per batch",
75
+ )
76
+ poll_interval_seconds: float = Field(
77
+ default=1.0,
78
+ ge=0.1,
79
+ le=60.0,
80
+ description="Seconds between background processor polls when idle",
81
+ )
82
+ query_timeout_seconds: float = Field(
83
+ default=30.0,
84
+ ge=1.0,
85
+ le=300.0,
86
+ description="Timeout in seconds for database queries",
87
+ )
88
+ strict_transaction_mode: bool = Field(
89
+ default=True,
90
+ description="If True, raises error when store() called outside transaction",
91
+ )
92
+ shutdown_timeout_seconds: float = Field(
93
+ default=10.0,
94
+ ge=1.0,
95
+ le=300.0,
96
+ description="Timeout in seconds for graceful shutdown during stop()",
97
+ )
98
+ max_retries: int | None = Field(
99
+ default=None,
100
+ ge=1,
101
+ le=100,
102
+ description="Maximum retry attempts before moving to DLQ. None disables DLQ",
103
+ )
104
+ dlq_topic: str | None = Field(
105
+ default=None,
106
+ description="Topic name for DLQ (for metrics/logging purposes)",
107
+ )
108
+
109
+
110
+ __all__: list[str] = [
111
+ "ModelTransitionNotificationOutboxConfig",
112
+ ]
@@ -0,0 +1,140 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2025 OmniNode Team
3
+ """Transition Notification Outbox Metrics Model.
4
+
5
+ This module provides a strongly-typed Pydantic model for outbox metrics,
6
+ replacing the untyped dict return from get_metrics().
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import ClassVar
12
+
13
+ from pydantic import BaseModel, ConfigDict, Field
14
+
15
+
16
+ class ModelTransitionNotificationOutboxMetrics(BaseModel):
17
+ """Metrics for transition notification outbox operation.
18
+
19
+ This model provides type-safe access to outbox metrics for observability
20
+ and monitoring purposes.
21
+
22
+ Attributes:
23
+ table_name: The outbox table name.
24
+ is_running: Whether the background processor is currently running.
25
+ notifications_stored: Total count of notifications stored in outbox.
26
+ notifications_processed: Total count of notifications successfully processed.
27
+ notifications_failed: Total count of notifications that failed processing.
28
+ notifications_sent_to_dlq: Total count of notifications sent to DLQ after
29
+ exceeding max retry attempts.
30
+ dlq_publish_failures: Count of failed DLQ publish attempts.
31
+ batch_size: Configured batch size for processing.
32
+ poll_interval_seconds: Configured poll interval in seconds.
33
+ max_retries: Configured maximum retry attempts before DLQ (None if DLQ disabled).
34
+ dlq_topic: Configured DLQ topic name (None if DLQ disabled).
35
+
36
+ Class Variables:
37
+ DEFAULT_DLQ_ALERT_THRESHOLD: Recommended threshold for alerting on DLQ
38
+ unavailability. Non-zero dlq_publish_failures indicate the DLQ is
39
+ having issues; reaching this threshold suggests operator intervention
40
+ is needed.
41
+
42
+ Observability Helpers:
43
+ dlq_needs_attention(): Returns True when DLQ publish failures have reached
44
+ the alert threshold, indicating operator intervention may be needed.
45
+ pending_dlq_ratio(): Returns the ratio of notifications stuck in DLQ retry
46
+ state, helping operators understand what fraction of failures are
47
+ DLQ-related versus normal processing failures.
48
+ """
49
+
50
+ DEFAULT_DLQ_ALERT_THRESHOLD: ClassVar[int] = 3
51
+ """Recommended threshold for alerting on DLQ unavailability.
52
+
53
+ Non-zero dlq_publish_failures indicate the DLQ is having issues. When this
54
+ threshold is reached, it suggests the DLQ has been unavailable for multiple
55
+ consecutive attempts and operator intervention is needed to investigate:
56
+ - Network connectivity to the DLQ broker
57
+ - DLQ topic existence and permissions
58
+ - Broker availability and health
59
+ """
60
+
61
+ model_config = ConfigDict(
62
+ frozen=True,
63
+ extra="forbid",
64
+ )
65
+
66
+ table_name: str = Field(..., description="The outbox table name")
67
+ is_running: bool = Field(default=False, description="Whether processor is running")
68
+ notifications_stored: int = Field(
69
+ default=0, ge=0, description="Total notifications stored"
70
+ )
71
+ notifications_processed: int = Field(
72
+ default=0, ge=0, description="Total notifications successfully processed"
73
+ )
74
+ notifications_failed: int = Field(
75
+ default=0, ge=0, description="Total notifications that failed processing"
76
+ )
77
+ notifications_sent_to_dlq: int = Field(
78
+ default=0, ge=0, description="Total notifications sent to DLQ"
79
+ )
80
+ dlq_publish_failures: int = Field(
81
+ default=0,
82
+ ge=0,
83
+ description=(
84
+ "Count of failed DLQ publish attempts. Non-zero values indicate DLQ "
85
+ "availability issues. Monitor this metric to detect when the DLQ is "
86
+ "unavailable, which can cause infinite retry loops for notifications "
87
+ "that have exceeded max_retries."
88
+ ),
89
+ )
90
+ batch_size: int = Field(default=100, ge=1, description="Configured batch size")
91
+ poll_interval_seconds: float = Field(
92
+ default=1.0, gt=0, description="Configured poll interval"
93
+ )
94
+ max_retries: int | None = Field(
95
+ default=None, ge=1, description="Max retries before DLQ (None if DLQ disabled)"
96
+ )
97
+ dlq_topic: str | None = Field(
98
+ default=None, description="DLQ topic name (None if DLQ disabled)"
99
+ )
100
+
101
+ def dlq_needs_attention(self) -> bool:
102
+ """Check if DLQ publish failures have reached the alert threshold.
103
+
104
+ This method indicates when the DLQ is experiencing availability issues
105
+ that may require operator intervention. When True, it suggests:
106
+
107
+ - The DLQ has been unavailable for multiple consecutive attempts
108
+ - Notifications that exceeded max_retries are stuck in retry loops
109
+ - Operator should investigate DLQ broker connectivity and health
110
+
111
+ Returns:
112
+ False if DLQ is disabled (max_retries is None).
113
+ True if dlq_publish_failures >= DEFAULT_DLQ_ALERT_THRESHOLD.
114
+ False otherwise (DLQ is enabled but failures below threshold).
115
+ """
116
+ if self.max_retries is None:
117
+ return False
118
+ return self.dlq_publish_failures >= self.DEFAULT_DLQ_ALERT_THRESHOLD
119
+
120
+ def pending_dlq_ratio(self) -> float:
121
+ """Calculate the ratio of notifications stuck in DLQ retry state.
122
+
123
+ This metric helps operators understand what fraction of failed
124
+ notifications are DLQ-related versus normal processing failures.
125
+ A high ratio indicates DLQ availability is the bottleneck.
126
+
127
+ Returns:
128
+ 0.0 if DLQ is disabled (max_retries is None).
129
+ 0.0 if no notifications have failed (notifications_failed == 0).
130
+ Ratio of dlq_publish_failures to notifications_failed otherwise.
131
+ Formula: dlq_publish_failures / max(1, notifications_failed)
132
+ """
133
+ if self.max_retries is None:
134
+ return 0.0
135
+ if self.notifications_failed == 0:
136
+ return 0.0
137
+ return self.dlq_publish_failures / max(1, self.notifications_failed)
138
+
139
+
140
+ __all__: list[str] = ["ModelTransitionNotificationOutboxMetrics"]