omnibase_infra 0.3.1__py3-none-any.whl → 0.3.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/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/handlers/registration_storage/handler_registration_storage_postgres.py +29 -20
- omnibase_infra/mixins/__init__.py +14 -0
- 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/{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/nodes/contract_registry_reducer/__init__.py +5 -0
- omnibase_infra/nodes/contract_registry_reducer/contract_registration_event_router.py +689 -0
- 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 +114 -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 +220 -0
- omnibase_infra/nodes/node_registry_effect/models/__init__.py +2 -2
- omnibase_infra/projectors/__init__.py +6 -0
- omnibase_infra/projectors/projection_reader_contract.py +1301 -0
- omnibase_infra/runtime/__init__.py +5 -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/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/request_response_wiring.py +785 -0
- omnibase_infra/runtime/service_kernel.py +295 -8
- 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/validation/infra_validators.py +3 -1
- omnibase_infra/validation/validation_exemptions.yaml +54 -0
- {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.3.2.dist-info}/METADATA +3 -3
- {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.3.2.dist-info}/RECORD +72 -34
- {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.3.2.dist-info}/WHEEL +0 -0
- {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.3.2.dist-info}/entry_points.txt +0 -0
- {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.3.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 OmniNode Team
|
|
3
|
+
"""PostgreSQL Error Response Mixin.
|
|
4
|
+
|
|
5
|
+
Provides standardized PostgreSQL exception handling for persistence operations
|
|
6
|
+
in NodeContractPersistenceEffect. Extracts the common ~60-line exception
|
|
7
|
+
handling pattern into a reusable mixin.
|
|
8
|
+
|
|
9
|
+
Architecture:
|
|
10
|
+
MixinPostgresErrorResponse is designed to be mixed into PostgreSQL
|
|
11
|
+
persistence classes to provide consistent error handling, sanitization,
|
|
12
|
+
logging, and ModelBackendResult construction.
|
|
13
|
+
|
|
14
|
+
The mixin handles:
|
|
15
|
+
- TimeoutError/InfraTimeoutError -> TIMEOUT_ERROR code
|
|
16
|
+
- InfraAuthenticationError -> AUTH_ERROR code
|
|
17
|
+
- InfraConnectionError -> CONNECTION_ERROR code
|
|
18
|
+
- RepositoryExecutionError -> operation-specific error code
|
|
19
|
+
- Generic Exception -> UNKNOWN_ERROR code
|
|
20
|
+
|
|
21
|
+
Error Sanitization:
|
|
22
|
+
All error messages are sanitized using utility functions to prevent
|
|
23
|
+
exposure of sensitive information (credentials, connection strings)
|
|
24
|
+
in logs and responses.
|
|
25
|
+
|
|
26
|
+
Logging:
|
|
27
|
+
- Timeout/Connection errors: logger.warning (retriable)
|
|
28
|
+
- Auth errors: logger.exception (non-retriable, needs attention)
|
|
29
|
+
- Repository errors: logger.warning (may be retriable)
|
|
30
|
+
- Unknown errors: logger.exception (needs investigation)
|
|
31
|
+
|
|
32
|
+
Usage:
|
|
33
|
+
>>> class MyPersistence(MixinPostgresErrorResponse):
|
|
34
|
+
... async def handle(self, payload, correlation_id):
|
|
35
|
+
... start_time = time.perf_counter()
|
|
36
|
+
... try:
|
|
37
|
+
... # ... database operation ...
|
|
38
|
+
... except Exception as e:
|
|
39
|
+
... ctx = PostgresErrorContext(
|
|
40
|
+
... exception=e,
|
|
41
|
+
... operation="my_operation",
|
|
42
|
+
... correlation_id=correlation_id,
|
|
43
|
+
... start_time=start_time,
|
|
44
|
+
... log_context={"my_field": "value"},
|
|
45
|
+
... operation_error_code=EnumPostgresErrorCode.UPSERT_ERROR,
|
|
46
|
+
... )
|
|
47
|
+
... return self._build_error_response(ctx)
|
|
48
|
+
|
|
49
|
+
Related:
|
|
50
|
+
- NodeContractPersistenceEffect: Parent effect node
|
|
51
|
+
- EnumPostgresErrorCode: Error code enumeration
|
|
52
|
+
- ModelBackendResult: Structured result model
|
|
53
|
+
- OMN-1845: Implementation ticket
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
from __future__ import annotations
|
|
57
|
+
|
|
58
|
+
import logging
|
|
59
|
+
import time
|
|
60
|
+
from dataclasses import dataclass, field
|
|
61
|
+
from typing import TYPE_CHECKING
|
|
62
|
+
|
|
63
|
+
from omnibase_infra.enums import EnumPostgresErrorCode
|
|
64
|
+
from omnibase_infra.errors import (
|
|
65
|
+
InfraAuthenticationError,
|
|
66
|
+
InfraConnectionError,
|
|
67
|
+
InfraTimeoutError,
|
|
68
|
+
RepositoryExecutionError,
|
|
69
|
+
)
|
|
70
|
+
from omnibase_infra.utils import sanitize_backend_error, sanitize_error_message
|
|
71
|
+
|
|
72
|
+
if TYPE_CHECKING:
|
|
73
|
+
from uuid import UUID
|
|
74
|
+
|
|
75
|
+
from omnibase_infra.models.model_backend_result import (
|
|
76
|
+
ModelBackendResult,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
logger = logging.getLogger(__name__)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass
|
|
83
|
+
class PostgresErrorContext:
|
|
84
|
+
"""Context for PostgreSQL error handling.
|
|
85
|
+
|
|
86
|
+
Encapsulates all parameters needed for error handling to reduce
|
|
87
|
+
function parameter count and improve readability.
|
|
88
|
+
|
|
89
|
+
Attributes:
|
|
90
|
+
exception: The exception that was raised during the operation.
|
|
91
|
+
operation: Name of the operation for logging.
|
|
92
|
+
correlation_id: Request correlation ID for distributed tracing.
|
|
93
|
+
start_time: Result of time.perf_counter() captured before operation.
|
|
94
|
+
log_context: Additional context fields for log messages.
|
|
95
|
+
operation_error_code: Error code for RepositoryExecutionError.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
exception: Exception
|
|
99
|
+
operation: str
|
|
100
|
+
correlation_id: UUID
|
|
101
|
+
start_time: float
|
|
102
|
+
log_context: dict[str, object] = field(default_factory=dict)
|
|
103
|
+
operation_error_code: EnumPostgresErrorCode | None = None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class MixinPostgresErrorResponse:
|
|
107
|
+
"""Mixin providing standardized PostgreSQL exception handling.
|
|
108
|
+
|
|
109
|
+
Consolidates the common exception handling pattern used across all
|
|
110
|
+
PostgreSQL handlers in NodeContractPersistenceEffect. This ensures
|
|
111
|
+
consistent error classification, sanitization, logging, and result
|
|
112
|
+
construction.
|
|
113
|
+
|
|
114
|
+
The mixin is designed to be used with any class that needs to handle
|
|
115
|
+
PostgreSQL operation errors and return ModelBackendResult.
|
|
116
|
+
|
|
117
|
+
Error Handling Matrix:
|
|
118
|
+
| Exception Type | Error Code | Log Level | Retriable |
|
|
119
|
+
|--------------------------|------------------|------------|-----------|
|
|
120
|
+
| TimeoutError | TIMEOUT_ERROR | warning | Yes |
|
|
121
|
+
| InfraTimeoutError | TIMEOUT_ERROR | warning | Yes |
|
|
122
|
+
| InfraAuthenticationError | AUTH_ERROR | exception | No |
|
|
123
|
+
| InfraConnectionError | CONNECTION_ERROR | warning | Yes |
|
|
124
|
+
| RepositoryExecutionError | (configurable) | warning | Maybe |
|
|
125
|
+
| Exception (catch-all) | UNKNOWN_ERROR | exception | No |
|
|
126
|
+
|
|
127
|
+
Example:
|
|
128
|
+
>>> class HandlerPostgresExample(MixinPostgresErrorResponse):
|
|
129
|
+
... async def handle(self, payload, correlation_id):
|
|
130
|
+
... start_time = time.perf_counter()
|
|
131
|
+
... try:
|
|
132
|
+
... async with self._pool.acquire() as conn:
|
|
133
|
+
... await conn.execute("SELECT 1")
|
|
134
|
+
... duration_ms = (time.perf_counter() - start_time) * 1000
|
|
135
|
+
... return ModelBackendResult(
|
|
136
|
+
... success=True,
|
|
137
|
+
... duration_ms=duration_ms,
|
|
138
|
+
... backend_id="postgres",
|
|
139
|
+
... correlation_id=correlation_id,
|
|
140
|
+
... )
|
|
141
|
+
... except Exception as e:
|
|
142
|
+
... ctx = PostgresErrorContext(
|
|
143
|
+
... exception=e,
|
|
144
|
+
... operation="example_operation",
|
|
145
|
+
... correlation_id=correlation_id,
|
|
146
|
+
... start_time=start_time,
|
|
147
|
+
... )
|
|
148
|
+
... return self._build_error_response(ctx)
|
|
149
|
+
|
|
150
|
+
See Also:
|
|
151
|
+
- HandlerPostgresContractUpsert: Example handler using this mixin
|
|
152
|
+
- HandlerPostgresTopicUpdate: Example handler using this mixin
|
|
153
|
+
- EnumPostgresErrorCode: Error code classification
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
def _build_error_response(
|
|
157
|
+
self,
|
|
158
|
+
ctx: PostgresErrorContext,
|
|
159
|
+
) -> ModelBackendResult:
|
|
160
|
+
"""Build ModelBackendResult for PostgreSQL operation exceptions.
|
|
161
|
+
|
|
162
|
+
Processes an exception raised during a PostgreSQL operation and
|
|
163
|
+
returns a properly constructed ModelBackendResult with:
|
|
164
|
+
- Appropriate error code based on exception type
|
|
165
|
+
- Sanitized error message (no credentials/PII)
|
|
166
|
+
- Operation duration in milliseconds
|
|
167
|
+
- Correlation ID for distributed tracing
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
ctx: PostgresErrorContext containing all error handling parameters.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
ModelBackendResult with:
|
|
174
|
+
- success: Always False (this is error handling)
|
|
175
|
+
- error: Sanitized error message safe for logs/responses
|
|
176
|
+
- error_code: EnumPostgresErrorCode based on exception type
|
|
177
|
+
- duration_ms: Operation duration in milliseconds
|
|
178
|
+
- backend_id: Always "postgres"
|
|
179
|
+
- correlation_id: Passed through for tracing
|
|
180
|
+
|
|
181
|
+
Note:
|
|
182
|
+
This method never raises exceptions. All error paths return
|
|
183
|
+
a properly constructed ModelBackendResult.
|
|
184
|
+
"""
|
|
185
|
+
# Extract context fields for readability
|
|
186
|
+
exception = ctx.exception
|
|
187
|
+
operation = ctx.operation
|
|
188
|
+
correlation_id = ctx.correlation_id
|
|
189
|
+
start_time = ctx.start_time
|
|
190
|
+
log_context = ctx.log_context
|
|
191
|
+
operation_error_code = ctx.operation_error_code
|
|
192
|
+
# Local import to avoid circular import at module load time
|
|
193
|
+
# (mixins/__init__.py loads before nodes/__init__.py in some paths)
|
|
194
|
+
from omnibase_infra.models.model_backend_result import (
|
|
195
|
+
ModelBackendResult as BackendResult,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
duration_ms = (time.perf_counter() - start_time) * 1000
|
|
199
|
+
|
|
200
|
+
# Build base log context
|
|
201
|
+
base_context: dict[str, object] = {
|
|
202
|
+
"correlation_id": str(correlation_id),
|
|
203
|
+
"duration_ms": duration_ms,
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
# Merge caller-provided context
|
|
207
|
+
if log_context:
|
|
208
|
+
base_context.update(log_context)
|
|
209
|
+
|
|
210
|
+
# Handle timeout errors - retriable infrastructure failures
|
|
211
|
+
if isinstance(exception, (TimeoutError, InfraTimeoutError)):
|
|
212
|
+
sanitized_error = sanitize_error_message(exception)
|
|
213
|
+
base_context["error"] = sanitized_error
|
|
214
|
+
|
|
215
|
+
logger.warning(
|
|
216
|
+
f"{operation} timed out",
|
|
217
|
+
extra=base_context,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
return BackendResult(
|
|
221
|
+
success=False,
|
|
222
|
+
error=sanitized_error,
|
|
223
|
+
error_code=EnumPostgresErrorCode.TIMEOUT_ERROR,
|
|
224
|
+
duration_ms=duration_ms,
|
|
225
|
+
backend_id="postgres",
|
|
226
|
+
correlation_id=correlation_id,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
# Handle authentication errors - non-retriable configuration failures
|
|
230
|
+
if isinstance(exception, InfraAuthenticationError):
|
|
231
|
+
sanitized_error = sanitize_error_message(exception)
|
|
232
|
+
base_context["error"] = sanitized_error
|
|
233
|
+
|
|
234
|
+
logger.exception(
|
|
235
|
+
f"{operation} authentication failed",
|
|
236
|
+
extra=base_context,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
return BackendResult(
|
|
240
|
+
success=False,
|
|
241
|
+
error=sanitized_error,
|
|
242
|
+
error_code=EnumPostgresErrorCode.AUTH_ERROR,
|
|
243
|
+
duration_ms=duration_ms,
|
|
244
|
+
backend_id="postgres",
|
|
245
|
+
correlation_id=correlation_id,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
# Handle connection errors - retriable infrastructure failures
|
|
249
|
+
if isinstance(exception, InfraConnectionError):
|
|
250
|
+
sanitized_error = sanitize_error_message(exception)
|
|
251
|
+
base_context["error"] = sanitized_error
|
|
252
|
+
|
|
253
|
+
logger.warning(
|
|
254
|
+
f"{operation} connection failed",
|
|
255
|
+
extra=base_context,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
return BackendResult(
|
|
259
|
+
success=False,
|
|
260
|
+
error=sanitized_error,
|
|
261
|
+
error_code=EnumPostgresErrorCode.CONNECTION_ERROR,
|
|
262
|
+
duration_ms=duration_ms,
|
|
263
|
+
backend_id="postgres",
|
|
264
|
+
correlation_id=correlation_id,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
# Handle repository execution errors - operation-specific failures
|
|
268
|
+
if isinstance(exception, RepositoryExecutionError):
|
|
269
|
+
sanitized_error = sanitize_error_message(exception)
|
|
270
|
+
base_context["error"] = sanitized_error
|
|
271
|
+
|
|
272
|
+
logger.warning(
|
|
273
|
+
f"{operation} execution failed",
|
|
274
|
+
extra=base_context,
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
# Use operation-specific error code if provided, else fall back to UNKNOWN
|
|
278
|
+
error_code = operation_error_code or EnumPostgresErrorCode.UNKNOWN_ERROR
|
|
279
|
+
|
|
280
|
+
return BackendResult(
|
|
281
|
+
success=False,
|
|
282
|
+
error=sanitized_error,
|
|
283
|
+
error_code=error_code,
|
|
284
|
+
duration_ms=duration_ms,
|
|
285
|
+
backend_id="postgres",
|
|
286
|
+
correlation_id=correlation_id,
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
# Generic catch-all for unexpected exceptions
|
|
290
|
+
# This catch-all is required because database adapters may raise
|
|
291
|
+
# unexpected exceptions beyond typed infrastructure errors (e.g.,
|
|
292
|
+
# driver errors, encoding errors, connection pool errors, asyncpg-
|
|
293
|
+
# specific exceptions). All errors must be sanitized to prevent
|
|
294
|
+
# credential exposure.
|
|
295
|
+
sanitized_error = sanitize_backend_error("postgres", exception)
|
|
296
|
+
base_context["error"] = sanitized_error
|
|
297
|
+
base_context["error_type"] = type(exception).__name__
|
|
298
|
+
|
|
299
|
+
logger.exception(
|
|
300
|
+
f"{operation} failed with unexpected error",
|
|
301
|
+
extra=base_context,
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
return BackendResult(
|
|
305
|
+
success=False,
|
|
306
|
+
error=sanitized_error,
|
|
307
|
+
error_code=EnumPostgresErrorCode.UNKNOWN_ERROR,
|
|
308
|
+
duration_ms=duration_ms,
|
|
309
|
+
backend_id="postgres",
|
|
310
|
+
correlation_id=correlation_id,
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
__all__: list[str] = ["MixinPostgresErrorResponse", "PostgresErrorContext"]
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 OmniNode Team
|
|
3
|
+
"""Shared execution core for PostgreSQL operation handlers.
|
|
4
|
+
|
|
5
|
+
This mixin centralizes the mechanical aspects of PostgreSQL handler execution:
|
|
6
|
+
- Timing via time.perf_counter()
|
|
7
|
+
- Error classification and sanitization
|
|
8
|
+
- ModelBackendResult construction
|
|
9
|
+
- Structured logging with correlation IDs
|
|
10
|
+
|
|
11
|
+
By extracting this boilerplate into a reusable mixin, handlers are reduced from
|
|
12
|
+
~200 lines to ~30 lines, eliminating drift risk where error handling patterns
|
|
13
|
+
could diverge across handlers.
|
|
14
|
+
|
|
15
|
+
Architecture:
|
|
16
|
+
Handlers inherit from MixinPostgresOpExecutor and call _execute_postgres_op()
|
|
17
|
+
with their operation-specific logic wrapped in a callable. The mixin handles
|
|
18
|
+
all timing, error classification, sanitization, and result construction.
|
|
19
|
+
|
|
20
|
+
Error Classification:
|
|
21
|
+
- TimeoutError, InfraTimeoutError → POSTGRES_TIMEOUT_ERROR (retriable)
|
|
22
|
+
- InfraAuthenticationError → POSTGRES_AUTH_ERROR (non-retriable)
|
|
23
|
+
- InfraConnectionError → POSTGRES_CONNECTION_ERROR (retriable)
|
|
24
|
+
- RepositoryExecutionError → op_error_code (handler-specified)
|
|
25
|
+
- Exception → POSTGRES_UNKNOWN_ERROR (non-retriable)
|
|
26
|
+
|
|
27
|
+
Usage:
|
|
28
|
+
```python
|
|
29
|
+
class HandlerPostgresHeartbeat(MixinPostgresOpExecutor):
|
|
30
|
+
async def handle(self, payload, correlation_id) -> ModelBackendResult:
|
|
31
|
+
return await self._execute_postgres_op(
|
|
32
|
+
op_error_code=EnumPostgresErrorCode.HEARTBEAT_ERROR,
|
|
33
|
+
correlation_id=correlation_id,
|
|
34
|
+
log_context={"contract_id": payload.contract_id},
|
|
35
|
+
fn=lambda: self._do_heartbeat(payload),
|
|
36
|
+
)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Related:
|
|
40
|
+
- EnumPostgresErrorCode: Error code enumeration with retriability metadata
|
|
41
|
+
- MixinAsyncCircuitBreaker: Circuit breaker mixin (not integrated here)
|
|
42
|
+
- OMN-1857: Extraction ticket for this mixin
|
|
43
|
+
|
|
44
|
+
Note on Circuit Breaker:
|
|
45
|
+
Per OMN-1857 design decision 1A, the executor should manage circuit breaker
|
|
46
|
+
internally. However, this initial implementation focuses on the core execution
|
|
47
|
+
mechanics. Circuit breaker integration will be added as a follow-up once the
|
|
48
|
+
basic pattern is validated.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
from __future__ import annotations
|
|
52
|
+
|
|
53
|
+
import logging
|
|
54
|
+
import time
|
|
55
|
+
from collections.abc import Awaitable, Callable
|
|
56
|
+
from typing import TYPE_CHECKING, TypeVar
|
|
57
|
+
|
|
58
|
+
from omnibase_infra.enums import EnumPostgresErrorCode
|
|
59
|
+
from omnibase_infra.errors import (
|
|
60
|
+
InfraAuthenticationError,
|
|
61
|
+
InfraConnectionError,
|
|
62
|
+
InfraTimeoutError,
|
|
63
|
+
RepositoryExecutionError,
|
|
64
|
+
)
|
|
65
|
+
from omnibase_infra.models.model_backend_result import ModelBackendResult
|
|
66
|
+
from omnibase_infra.utils import sanitize_backend_error, sanitize_error_message
|
|
67
|
+
|
|
68
|
+
if TYPE_CHECKING:
|
|
69
|
+
from uuid import UUID
|
|
70
|
+
|
|
71
|
+
logger = logging.getLogger(__name__)
|
|
72
|
+
|
|
73
|
+
T = TypeVar("T")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class MixinPostgresOpExecutor:
|
|
77
|
+
"""Shared execution core for PostgreSQL operation handlers.
|
|
78
|
+
|
|
79
|
+
Centralizes timing, error handling, sanitization, and result construction
|
|
80
|
+
for PostgreSQL operations. Handlers inherit this mixin and delegate to
|
|
81
|
+
_execute_postgres_op() for consistent mechanical behavior.
|
|
82
|
+
|
|
83
|
+
This mixin does NOT manage circuit breaker state - that responsibility
|
|
84
|
+
remains with the handler or a separate MixinAsyncCircuitBreaker composition.
|
|
85
|
+
|
|
86
|
+
Example:
|
|
87
|
+
```python
|
|
88
|
+
class HandlerPostgresUpsert(MixinPostgresOpExecutor):
|
|
89
|
+
def __init__(self, pool: asyncpg.Pool) -> None:
|
|
90
|
+
self._pool = pool
|
|
91
|
+
|
|
92
|
+
async def handle(
|
|
93
|
+
self, payload: ModelPayloadUpsertContract, correlation_id: UUID
|
|
94
|
+
) -> ModelBackendResult:
|
|
95
|
+
return await self._execute_postgres_op(
|
|
96
|
+
op_error_code=EnumPostgresErrorCode.UPSERT_ERROR,
|
|
97
|
+
correlation_id=correlation_id,
|
|
98
|
+
log_context={
|
|
99
|
+
"contract_id": payload.contract_id,
|
|
100
|
+
"node_name": payload.node_name,
|
|
101
|
+
},
|
|
102
|
+
fn=lambda: self._execute_upsert(payload),
|
|
103
|
+
)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
See Also:
|
|
107
|
+
- EnumPostgresErrorCode: Error codes with is_retriable property
|
|
108
|
+
- sanitize_error_message: Error sanitization utility
|
|
109
|
+
- ModelBackendResult: Structured result model
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
async def _execute_postgres_op(
|
|
113
|
+
self,
|
|
114
|
+
*,
|
|
115
|
+
op_error_code: EnumPostgresErrorCode,
|
|
116
|
+
correlation_id: UUID,
|
|
117
|
+
log_context: dict[str, object],
|
|
118
|
+
fn: Callable[[], Awaitable[T]],
|
|
119
|
+
) -> ModelBackendResult:
|
|
120
|
+
"""Execute a PostgreSQL operation with timing, error handling, and sanitization.
|
|
121
|
+
|
|
122
|
+
This method wraps the actual database operation (fn) with:
|
|
123
|
+
1. Timing measurement via time.perf_counter()
|
|
124
|
+
2. Exception classification into appropriate error codes
|
|
125
|
+
3. Error message sanitization to prevent credential exposure
|
|
126
|
+
4. Structured logging with correlation ID and context
|
|
127
|
+
5. ModelBackendResult construction
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
op_error_code: Operation-specific error code for non-infrastructure
|
|
131
|
+
failures (e.g., UPSERT_ERROR, HEARTBEAT_ERROR). Used when the
|
|
132
|
+
operation fails due to business logic or query issues rather
|
|
133
|
+
than connection/auth problems.
|
|
134
|
+
correlation_id: Request correlation ID for distributed tracing.
|
|
135
|
+
log_context: Additional fields for structured logging (e.g.,
|
|
136
|
+
contract_id, node_name). Included in all log messages.
|
|
137
|
+
fn: Async callable that performs the actual database operation.
|
|
138
|
+
Should return any value on success. The return value is not
|
|
139
|
+
used - only success/failure matters.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
ModelBackendResult with:
|
|
143
|
+
- success: True if fn() completed without exception
|
|
144
|
+
- error: Sanitized error message (empty string on success)
|
|
145
|
+
- error_code: Appropriate EnumPostgresErrorCode
|
|
146
|
+
- duration_ms: Operation duration in milliseconds
|
|
147
|
+
- backend_id: "postgres"
|
|
148
|
+
- correlation_id: Passed through for tracing
|
|
149
|
+
|
|
150
|
+
Error Classification:
|
|
151
|
+
| Exception Type | Error Code | Retriable |
|
|
152
|
+
|-----------------------------|-------------------------|-----------|
|
|
153
|
+
| TimeoutError | TIMEOUT_ERROR | Yes |
|
|
154
|
+
| InfraTimeoutError | TIMEOUT_ERROR | Yes |
|
|
155
|
+
| InfraAuthenticationError | AUTH_ERROR | No |
|
|
156
|
+
| InfraConnectionError | CONNECTION_ERROR | Yes |
|
|
157
|
+
| RepositoryExecutionError | op_error_code | No |
|
|
158
|
+
| Exception | UNKNOWN_ERROR | No |
|
|
159
|
+
|
|
160
|
+
Note:
|
|
161
|
+
This method never raises exceptions. All errors are captured,
|
|
162
|
+
sanitized, logged, and returned in the result model.
|
|
163
|
+
"""
|
|
164
|
+
start_time = time.perf_counter()
|
|
165
|
+
log_extra = {
|
|
166
|
+
"correlation_id": str(correlation_id),
|
|
167
|
+
**log_context,
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
# Execute the operation
|
|
172
|
+
await fn()
|
|
173
|
+
|
|
174
|
+
duration_ms = (time.perf_counter() - start_time) * 1000
|
|
175
|
+
|
|
176
|
+
logger.debug(
|
|
177
|
+
"PostgreSQL operation completed successfully",
|
|
178
|
+
extra={**log_extra, "duration_ms": duration_ms},
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
return ModelBackendResult(
|
|
182
|
+
success=True,
|
|
183
|
+
duration_ms=duration_ms,
|
|
184
|
+
backend_id="postgres",
|
|
185
|
+
correlation_id=correlation_id,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
except (TimeoutError, InfraTimeoutError) as e:
|
|
189
|
+
# Timeout - retriable error
|
|
190
|
+
duration_ms = (time.perf_counter() - start_time) * 1000
|
|
191
|
+
sanitized_error = sanitize_error_message(e)
|
|
192
|
+
logger.warning(
|
|
193
|
+
"PostgreSQL operation timed out",
|
|
194
|
+
extra={
|
|
195
|
+
**log_extra,
|
|
196
|
+
"duration_ms": duration_ms,
|
|
197
|
+
"error": sanitized_error,
|
|
198
|
+
},
|
|
199
|
+
)
|
|
200
|
+
return ModelBackendResult(
|
|
201
|
+
success=False,
|
|
202
|
+
error=sanitized_error,
|
|
203
|
+
error_code=EnumPostgresErrorCode.TIMEOUT_ERROR,
|
|
204
|
+
duration_ms=duration_ms,
|
|
205
|
+
backend_id="postgres",
|
|
206
|
+
correlation_id=correlation_id,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
except InfraAuthenticationError as e:
|
|
210
|
+
# Authentication failure - non-retriable
|
|
211
|
+
duration_ms = (time.perf_counter() - start_time) * 1000
|
|
212
|
+
sanitized_error = sanitize_error_message(e)
|
|
213
|
+
logger.exception(
|
|
214
|
+
"PostgreSQL authentication failed",
|
|
215
|
+
extra={
|
|
216
|
+
**log_extra,
|
|
217
|
+
"duration_ms": duration_ms,
|
|
218
|
+
"error": sanitized_error,
|
|
219
|
+
},
|
|
220
|
+
)
|
|
221
|
+
return ModelBackendResult(
|
|
222
|
+
success=False,
|
|
223
|
+
error=sanitized_error,
|
|
224
|
+
error_code=EnumPostgresErrorCode.AUTH_ERROR,
|
|
225
|
+
duration_ms=duration_ms,
|
|
226
|
+
backend_id="postgres",
|
|
227
|
+
correlation_id=correlation_id,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
except InfraConnectionError as e:
|
|
231
|
+
# Connection failure - retriable
|
|
232
|
+
duration_ms = (time.perf_counter() - start_time) * 1000
|
|
233
|
+
sanitized_error = sanitize_error_message(e)
|
|
234
|
+
logger.warning(
|
|
235
|
+
"PostgreSQL connection failed",
|
|
236
|
+
extra={
|
|
237
|
+
**log_extra,
|
|
238
|
+
"duration_ms": duration_ms,
|
|
239
|
+
"error": sanitized_error,
|
|
240
|
+
},
|
|
241
|
+
)
|
|
242
|
+
return ModelBackendResult(
|
|
243
|
+
success=False,
|
|
244
|
+
error=sanitized_error,
|
|
245
|
+
error_code=EnumPostgresErrorCode.CONNECTION_ERROR,
|
|
246
|
+
duration_ms=duration_ms,
|
|
247
|
+
backend_id="postgres",
|
|
248
|
+
correlation_id=correlation_id,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
except RepositoryExecutionError as e:
|
|
252
|
+
# Query/operation failure - use handler-provided error code
|
|
253
|
+
duration_ms = (time.perf_counter() - start_time) * 1000
|
|
254
|
+
sanitized_error = sanitize_error_message(e)
|
|
255
|
+
logger.warning(
|
|
256
|
+
"PostgreSQL operation failed",
|
|
257
|
+
extra={
|
|
258
|
+
**log_extra,
|
|
259
|
+
"duration_ms": duration_ms,
|
|
260
|
+
"error": sanitized_error,
|
|
261
|
+
"error_code": op_error_code.value,
|
|
262
|
+
},
|
|
263
|
+
)
|
|
264
|
+
return ModelBackendResult(
|
|
265
|
+
success=False,
|
|
266
|
+
error=sanitized_error,
|
|
267
|
+
error_code=op_error_code,
|
|
268
|
+
duration_ms=duration_ms,
|
|
269
|
+
backend_id="postgres",
|
|
270
|
+
correlation_id=correlation_id,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
except (
|
|
274
|
+
Exception
|
|
275
|
+
) as e: # ONEX: catch-all for driver errors, encoding errors, pool errors
|
|
276
|
+
# Unknown error - non-retriable, requires investigation
|
|
277
|
+
duration_ms = (time.perf_counter() - start_time) * 1000
|
|
278
|
+
sanitized_error = sanitize_backend_error("postgres", e)
|
|
279
|
+
logger.exception(
|
|
280
|
+
"PostgreSQL operation failed with unexpected error",
|
|
281
|
+
extra={
|
|
282
|
+
**log_extra,
|
|
283
|
+
"duration_ms": duration_ms,
|
|
284
|
+
"error_type": type(e).__name__,
|
|
285
|
+
"error": sanitized_error,
|
|
286
|
+
},
|
|
287
|
+
)
|
|
288
|
+
return ModelBackendResult(
|
|
289
|
+
success=False,
|
|
290
|
+
error=sanitized_error,
|
|
291
|
+
error_code=EnumPostgresErrorCode.UNKNOWN_ERROR,
|
|
292
|
+
duration_ms=duration_ms,
|
|
293
|
+
backend_id="postgres",
|
|
294
|
+
correlation_id=correlation_id,
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
__all__ = ["MixinPostgresOpExecutor"]
|
|
@@ -30,6 +30,7 @@ from omnibase_infra.models.event_bus import (
|
|
|
30
30
|
from omnibase_infra.models.handlers import ModelHandlerIdentifier
|
|
31
31
|
from omnibase_infra.models.health import ModelHealthCheckResult
|
|
32
32
|
from omnibase_infra.models.logging import ModelLogContext
|
|
33
|
+
from omnibase_infra.models.model_backend_result import ModelBackendResult
|
|
33
34
|
from omnibase_infra.models.model_node_identity import ModelNodeIdentity
|
|
34
35
|
from omnibase_infra.models.model_retry_error_classification import (
|
|
35
36
|
ModelRetryErrorClassification,
|
|
@@ -93,6 +94,8 @@ __all__: list[str] = [
|
|
|
93
94
|
"ModelConsumerRetryConfig",
|
|
94
95
|
"ModelIdempotencyConfig",
|
|
95
96
|
"ModelOffsetPolicyConfig",
|
|
97
|
+
# Backend result models
|
|
98
|
+
"ModelBackendResult",
|
|
96
99
|
# Resilience models
|
|
97
100
|
"ModelCircuitBreakerConfig",
|
|
98
101
|
# Validation models
|
|
@@ -61,7 +61,9 @@ class ModelBackendResult(BaseModel):
|
|
|
61
61
|
Attributes:
|
|
62
62
|
success: Whether the backend operation completed successfully.
|
|
63
63
|
error: Sanitized error message if success is False.
|
|
64
|
-
error_code:
|
|
64
|
+
error_code: Error code for programmatic handling. PostgreSQL handlers
|
|
65
|
+
use EnumPostgresErrorCode enum values which serialize to strings
|
|
66
|
+
(e.g., "POSTGRES_CONNECTION_ERROR"). Other backends use string codes.
|
|
65
67
|
duration_ms: Time taken for the operation in milliseconds.
|
|
66
68
|
backend_id: Optional identifier for the backend instance.
|
|
67
69
|
|
|
@@ -96,18 +98,30 @@ class ModelBackendResult(BaseModel):
|
|
|
96
98
|
>>> result.success
|
|
97
99
|
True
|
|
98
100
|
|
|
99
|
-
Example (failure case):
|
|
101
|
+
Example (failure case with PostgreSQL enum):
|
|
102
|
+
>>> from omnibase_infra.enums import EnumPostgresErrorCode
|
|
100
103
|
>>> result = ModelBackendResult(
|
|
101
104
|
... success=False,
|
|
102
105
|
... error="Connection refused to database host",
|
|
103
|
-
... error_code=
|
|
106
|
+
... error_code=EnumPostgresErrorCode.CONNECTION_ERROR,
|
|
104
107
|
... duration_ms=5000.0,
|
|
105
108
|
... backend_id="postgres",
|
|
106
109
|
... )
|
|
107
110
|
>>> result.success
|
|
108
111
|
False
|
|
109
|
-
>>> result.
|
|
110
|
-
'
|
|
112
|
+
>>> result.error_code # Enum serializes to string
|
|
113
|
+
'POSTGRES_CONNECTION_ERROR'
|
|
114
|
+
|
|
115
|
+
Example (failure case with Consul string code):
|
|
116
|
+
>>> result = ModelBackendResult(
|
|
117
|
+
... success=False,
|
|
118
|
+
... error="Service registration failed",
|
|
119
|
+
... error_code="CONSUL_CONNECTION_ERROR",
|
|
120
|
+
... duration_ms=1500.0,
|
|
121
|
+
... backend_id="consul",
|
|
122
|
+
... )
|
|
123
|
+
>>> result.error_code
|
|
124
|
+
'CONSUL_CONNECTION_ERROR'
|
|
111
125
|
"""
|
|
112
126
|
|
|
113
127
|
model_config = ConfigDict(frozen=True, extra="forbid", from_attributes=True)
|
|
@@ -122,7 +136,9 @@ class ModelBackendResult(BaseModel):
|
|
|
122
136
|
)
|
|
123
137
|
error_code: str | None = Field(
|
|
124
138
|
default=None,
|
|
125
|
-
description="Error code for programmatic handling
|
|
139
|
+
description="Error code for programmatic handling. PostgreSQL handlers use "
|
|
140
|
+
"EnumPostgresErrorCode enum values (which serialize to strings like "
|
|
141
|
+
"'POSTGRES_CONNECTION_ERROR'). Other backends use string codes directly.",
|
|
126
142
|
)
|
|
127
143
|
duration_ms: float = Field(
|
|
128
144
|
default=0.0,
|