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.
- omnibase_infra/__init__.py +1 -1
- omnibase_infra/adapters/adapter_onex_tool_execution.py +446 -0
- omnibase_infra/cli/commands.py +1 -1
- omnibase_infra/configs/widget_mapping.yaml +176 -0
- omnibase_infra/contracts/handlers/filesystem/handler_contract.yaml +4 -1
- omnibase_infra/contracts/handlers/mcp/handler_contract.yaml +4 -1
- omnibase_infra/errors/error_compute_registry.py +4 -1
- omnibase_infra/errors/error_event_bus_registry.py +4 -1
- omnibase_infra/errors/error_infra.py +3 -1
- omnibase_infra/errors/error_policy_registry.py +4 -1
- omnibase_infra/handlers/handler_db.py +2 -1
- omnibase_infra/handlers/handler_graph.py +10 -5
- omnibase_infra/handlers/handler_mcp.py +736 -63
- omnibase_infra/handlers/mixins/mixin_consul_kv.py +4 -3
- omnibase_infra/handlers/mixins/mixin_consul_service.py +2 -1
- omnibase_infra/handlers/service_discovery/handler_service_discovery_consul.py +301 -4
- omnibase_infra/handlers/service_discovery/models/model_service_info.py +10 -0
- omnibase_infra/mixins/mixin_async_circuit_breaker.py +3 -2
- omnibase_infra/mixins/mixin_node_introspection.py +24 -7
- omnibase_infra/mixins/mixin_retry_execution.py +1 -1
- omnibase_infra/models/handlers/__init__.py +10 -0
- omnibase_infra/models/handlers/model_bootstrap_handler_descriptor.py +162 -0
- omnibase_infra/models/handlers/model_handler_descriptor.py +15 -0
- omnibase_infra/models/mcp/__init__.py +15 -0
- omnibase_infra/models/mcp/model_mcp_contract_config.py +80 -0
- omnibase_infra/models/mcp/model_mcp_server_config.py +67 -0
- omnibase_infra/models/mcp/model_mcp_tool_definition.py +73 -0
- omnibase_infra/models/mcp/model_mcp_tool_parameter.py +35 -0
- omnibase_infra/models/registration/model_node_capabilities.py +11 -0
- omnibase_infra/nodes/architecture_validator/contract_architecture_validator.yaml +0 -5
- omnibase_infra/nodes/architecture_validator/registry/registry_infra_architecture_validator.py +17 -10
- omnibase_infra/nodes/effects/contract.yaml +0 -5
- omnibase_infra/nodes/node_registration_orchestrator/contract.yaml +7 -0
- omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_node_introspected.py +86 -1
- omnibase_infra/nodes/node_registration_orchestrator/introspection_event_router.py +3 -3
- omnibase_infra/nodes/node_registration_orchestrator/registry/registry_infra_node_registration_orchestrator.py +9 -8
- omnibase_infra/nodes/node_registration_orchestrator/wiring.py +14 -13
- omnibase_infra/nodes/node_registration_storage_effect/contract.yaml +0 -5
- omnibase_infra/nodes/node_registration_storage_effect/registry/registry_infra_registration_storage.py +46 -25
- omnibase_infra/nodes/node_registry_effect/contract.yaml +0 -5
- omnibase_infra/nodes/node_registry_effect/handlers/handler_partial_retry.py +2 -1
- omnibase_infra/nodes/node_service_discovery_effect/registry/registry_infra_service_discovery.py +24 -19
- omnibase_infra/plugins/examples/plugin_json_normalizer.py +2 -2
- omnibase_infra/plugins/examples/plugin_json_normalizer_error_handling.py +2 -2
- omnibase_infra/plugins/plugin_compute_base.py +16 -2
- omnibase_infra/protocols/protocol_event_projector.py +1 -1
- omnibase_infra/runtime/__init__.py +51 -1
- omnibase_infra/runtime/binding_config_resolver.py +102 -37
- omnibase_infra/runtime/constants_notification.py +75 -0
- omnibase_infra/runtime/contract_handler_discovery.py +6 -1
- omnibase_infra/runtime/handler_bootstrap_source.py +514 -0
- omnibase_infra/runtime/handler_contract_config_loader.py +603 -0
- omnibase_infra/runtime/handler_contract_source.py +289 -167
- omnibase_infra/runtime/handler_plugin_loader.py +4 -2
- omnibase_infra/runtime/mixin_semver_cache.py +25 -1
- omnibase_infra/runtime/mixins/__init__.py +7 -0
- omnibase_infra/runtime/mixins/mixin_projector_notification_publishing.py +566 -0
- omnibase_infra/runtime/mixins/mixin_projector_sql_operations.py +31 -10
- omnibase_infra/runtime/models/__init__.py +24 -0
- omnibase_infra/runtime/models/model_health_check_result.py +2 -1
- omnibase_infra/runtime/models/model_projector_notification_config.py +171 -0
- omnibase_infra/runtime/models/model_transition_notification_outbox_config.py +112 -0
- omnibase_infra/runtime/models/model_transition_notification_outbox_metrics.py +140 -0
- omnibase_infra/runtime/models/model_transition_notification_publisher_metrics.py +357 -0
- omnibase_infra/runtime/projector_plugin_loader.py +1 -1
- omnibase_infra/runtime/projector_shell.py +229 -1
- omnibase_infra/runtime/protocols/__init__.py +10 -0
- omnibase_infra/runtime/registry/registry_protocol_binding.py +3 -2
- omnibase_infra/runtime/registry_policy.py +9 -326
- omnibase_infra/runtime/secret_resolver.py +4 -2
- omnibase_infra/runtime/service_kernel.py +10 -2
- omnibase_infra/runtime/service_message_dispatch_engine.py +4 -2
- omnibase_infra/runtime/service_runtime_host_process.py +225 -15
- omnibase_infra/runtime/transition_notification_outbox.py +1190 -0
- omnibase_infra/runtime/transition_notification_publisher.py +764 -0
- omnibase_infra/runtime/util_container_wiring.py +6 -5
- omnibase_infra/runtime/util_wiring.py +5 -1
- omnibase_infra/schemas/schema_transition_notification_outbox.sql +245 -0
- omnibase_infra/services/mcp/__init__.py +31 -0
- omnibase_infra/services/mcp/mcp_server_lifecycle.py +443 -0
- omnibase_infra/services/mcp/service_mcp_tool_discovery.py +411 -0
- omnibase_infra/services/mcp/service_mcp_tool_registry.py +329 -0
- omnibase_infra/services/mcp/service_mcp_tool_sync.py +547 -0
- omnibase_infra/services/registry_api/__init__.py +40 -0
- omnibase_infra/services/registry_api/main.py +243 -0
- omnibase_infra/services/registry_api/models/__init__.py +66 -0
- omnibase_infra/services/registry_api/models/model_capability_widget_mapping.py +38 -0
- omnibase_infra/services/registry_api/models/model_pagination_info.py +48 -0
- omnibase_infra/services/registry_api/models/model_registry_discovery_response.py +73 -0
- omnibase_infra/services/registry_api/models/model_registry_health_response.py +49 -0
- omnibase_infra/services/registry_api/models/model_registry_instance_view.py +88 -0
- omnibase_infra/services/registry_api/models/model_registry_node_view.py +88 -0
- omnibase_infra/services/registry_api/models/model_registry_summary.py +60 -0
- omnibase_infra/services/registry_api/models/model_response_list_instances.py +43 -0
- omnibase_infra/services/registry_api/models/model_response_list_nodes.py +51 -0
- omnibase_infra/services/registry_api/models/model_warning.py +49 -0
- omnibase_infra/services/registry_api/models/model_widget_defaults.py +28 -0
- omnibase_infra/services/registry_api/models/model_widget_mapping.py +51 -0
- omnibase_infra/services/registry_api/routes.py +371 -0
- omnibase_infra/services/registry_api/service.py +846 -0
- omnibase_infra/services/service_capability_query.py +4 -4
- omnibase_infra/services/service_health.py +3 -2
- omnibase_infra/services/service_timeout_emitter.py +13 -2
- omnibase_infra/utils/util_dsn_validation.py +1 -1
- omnibase_infra/validation/__init__.py +3 -19
- omnibase_infra/validation/contracts/security.validation.yaml +114 -0
- omnibase_infra/validation/infra_validators.py +35 -24
- omnibase_infra/validation/validation_exemptions.yaml +113 -9
- omnibase_infra/validation/validator_chain_propagation.py +2 -2
- omnibase_infra/validation/validator_runtime_shape.py +1 -1
- omnibase_infra/validation/validator_security.py +473 -370
- {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.2.dist-info}/METADATA +2 -2
- {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.2.dist-info}/RECORD +116 -74
- {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.2.dist-info}/WHEEL +0 -0
- {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.2.dist-info}/entry_points.txt +0 -0
- {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"]
|