omnibase_infra 0.3.1__py3-none-any.whl → 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. omnibase_infra/__init__.py +1 -1
  2. omnibase_infra/enums/__init__.py +3 -0
  3. omnibase_infra/enums/enum_consumer_group_purpose.py +9 -0
  4. omnibase_infra/enums/enum_postgres_error_code.py +188 -0
  5. omnibase_infra/errors/__init__.py +4 -0
  6. omnibase_infra/errors/error_infra.py +60 -0
  7. omnibase_infra/handlers/__init__.py +3 -0
  8. omnibase_infra/handlers/handler_slack_webhook.py +426 -0
  9. omnibase_infra/handlers/models/__init__.py +14 -0
  10. omnibase_infra/handlers/models/enum_alert_severity.py +36 -0
  11. omnibase_infra/handlers/models/model_slack_alert.py +24 -0
  12. omnibase_infra/handlers/models/model_slack_alert_payload.py +77 -0
  13. omnibase_infra/handlers/models/model_slack_alert_result.py +73 -0
  14. omnibase_infra/handlers/registration_storage/handler_registration_storage_postgres.py +29 -20
  15. omnibase_infra/mixins/__init__.py +14 -0
  16. omnibase_infra/mixins/mixin_node_introspection.py +42 -20
  17. omnibase_infra/mixins/mixin_postgres_error_response.py +314 -0
  18. omnibase_infra/mixins/mixin_postgres_op_executor.py +298 -0
  19. omnibase_infra/models/__init__.py +3 -0
  20. omnibase_infra/models/discovery/model_dependency_spec.py +1 -0
  21. omnibase_infra/models/discovery/model_discovered_capabilities.py +1 -1
  22. omnibase_infra/models/discovery/model_introspection_config.py +28 -1
  23. omnibase_infra/models/discovery/model_introspection_performance_metrics.py +1 -0
  24. omnibase_infra/models/discovery/model_introspection_task_config.py +1 -0
  25. omnibase_infra/{nodes/effects/models → models}/model_backend_result.py +22 -6
  26. omnibase_infra/models/projection/__init__.py +11 -0
  27. omnibase_infra/models/projection/model_contract_projection.py +170 -0
  28. omnibase_infra/models/projection/model_topic_projection.py +148 -0
  29. omnibase_infra/models/runtime/__init__.py +4 -0
  30. omnibase_infra/models/runtime/model_resolved_dependencies.py +116 -0
  31. omnibase_infra/nodes/contract_registry_reducer/__init__.py +5 -0
  32. omnibase_infra/nodes/contract_registry_reducer/contract.yaml +6 -5
  33. omnibase_infra/nodes/contract_registry_reducer/contract_registration_event_router.py +689 -0
  34. omnibase_infra/nodes/contract_registry_reducer/reducer.py +9 -26
  35. omnibase_infra/nodes/effects/__init__.py +1 -1
  36. omnibase_infra/nodes/effects/models/__init__.py +6 -4
  37. omnibase_infra/nodes/effects/models/model_registry_response.py +1 -1
  38. omnibase_infra/nodes/effects/protocol_consul_client.py +1 -1
  39. omnibase_infra/nodes/effects/protocol_postgres_adapter.py +1 -1
  40. omnibase_infra/nodes/effects/registry_effect.py +1 -1
  41. omnibase_infra/nodes/node_contract_persistence_effect/__init__.py +101 -0
  42. omnibase_infra/nodes/node_contract_persistence_effect/contract.yaml +490 -0
  43. omnibase_infra/nodes/node_contract_persistence_effect/handlers/__init__.py +74 -0
  44. omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_cleanup_topics.py +217 -0
  45. omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_contract_upsert.py +242 -0
  46. omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_deactivate.py +194 -0
  47. omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_heartbeat.py +243 -0
  48. omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_mark_stale.py +208 -0
  49. omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_topic_update.py +298 -0
  50. omnibase_infra/nodes/node_contract_persistence_effect/models/__init__.py +15 -0
  51. omnibase_infra/nodes/node_contract_persistence_effect/models/model_persistence_result.py +52 -0
  52. omnibase_infra/nodes/node_contract_persistence_effect/node.py +131 -0
  53. omnibase_infra/nodes/node_contract_persistence_effect/registry/__init__.py +27 -0
  54. omnibase_infra/nodes/node_contract_persistence_effect/registry/registry_infra_contract_persistence_effect.py +251 -0
  55. omnibase_infra/nodes/node_registration_orchestrator/models/model_postgres_intent_payload.py +8 -12
  56. omnibase_infra/nodes/node_registry_effect/models/__init__.py +2 -2
  57. omnibase_infra/nodes/node_slack_alerter_effect/__init__.py +33 -0
  58. omnibase_infra/nodes/node_slack_alerter_effect/contract.yaml +291 -0
  59. omnibase_infra/nodes/node_slack_alerter_effect/node.py +106 -0
  60. omnibase_infra/projectors/__init__.py +6 -0
  61. omnibase_infra/projectors/projection_reader_contract.py +1301 -0
  62. omnibase_infra/runtime/__init__.py +12 -0
  63. omnibase_infra/runtime/baseline_subscriptions.py +13 -6
  64. omnibase_infra/runtime/contract_dependency_resolver.py +455 -0
  65. omnibase_infra/runtime/contract_registration_event_router.py +500 -0
  66. omnibase_infra/runtime/db/__init__.py +4 -0
  67. omnibase_infra/runtime/db/models/__init__.py +15 -10
  68. omnibase_infra/runtime/db/models/model_db_operation.py +40 -0
  69. omnibase_infra/runtime/db/models/model_db_param.py +24 -0
  70. omnibase_infra/runtime/db/models/model_db_repository_contract.py +40 -0
  71. omnibase_infra/runtime/db/models/model_db_return.py +26 -0
  72. omnibase_infra/runtime/db/models/model_db_safety_policy.py +32 -0
  73. omnibase_infra/runtime/emit_daemon/event_registry.py +34 -22
  74. omnibase_infra/runtime/event_bus_subcontract_wiring.py +63 -23
  75. omnibase_infra/runtime/intent_execution_router.py +430 -0
  76. omnibase_infra/runtime/models/__init__.py +6 -0
  77. omnibase_infra/runtime/models/model_contract_registry_config.py +41 -0
  78. omnibase_infra/runtime/models/model_intent_execution_summary.py +79 -0
  79. omnibase_infra/runtime/models/model_runtime_config.py +8 -0
  80. omnibase_infra/runtime/protocols/__init__.py +16 -0
  81. omnibase_infra/runtime/protocols/protocol_intent_executor.py +107 -0
  82. omnibase_infra/runtime/publisher_topic_scoped.py +16 -11
  83. omnibase_infra/runtime/registry_policy.py +29 -15
  84. omnibase_infra/runtime/request_response_wiring.py +793 -0
  85. omnibase_infra/runtime/service_kernel.py +295 -8
  86. omnibase_infra/runtime/service_runtime_host_process.py +149 -5
  87. omnibase_infra/runtime/util_version.py +5 -1
  88. omnibase_infra/schemas/schema_latency_baseline.sql +135 -0
  89. omnibase_infra/services/contract_publisher/config.py +4 -4
  90. omnibase_infra/services/contract_publisher/service.py +8 -5
  91. omnibase_infra/services/observability/injection_effectiveness/__init__.py +67 -0
  92. omnibase_infra/services/observability/injection_effectiveness/config.py +295 -0
  93. omnibase_infra/services/observability/injection_effectiveness/consumer.py +1461 -0
  94. omnibase_infra/services/observability/injection_effectiveness/models/__init__.py +32 -0
  95. omnibase_infra/services/observability/injection_effectiveness/models/model_agent_match.py +79 -0
  96. omnibase_infra/services/observability/injection_effectiveness/models/model_context_utilization.py +118 -0
  97. omnibase_infra/services/observability/injection_effectiveness/models/model_latency_breakdown.py +107 -0
  98. omnibase_infra/services/observability/injection_effectiveness/models/model_pattern_utilization.py +46 -0
  99. omnibase_infra/services/observability/injection_effectiveness/writer_postgres.py +596 -0
  100. omnibase_infra/services/registry_api/models/__init__.py +25 -0
  101. omnibase_infra/services/registry_api/models/model_contract_ref.py +44 -0
  102. omnibase_infra/services/registry_api/models/model_contract_view.py +81 -0
  103. omnibase_infra/services/registry_api/models/model_response_contracts.py +50 -0
  104. omnibase_infra/services/registry_api/models/model_response_topics.py +50 -0
  105. omnibase_infra/services/registry_api/models/model_topic_summary.py +57 -0
  106. omnibase_infra/services/registry_api/models/model_topic_view.py +63 -0
  107. omnibase_infra/services/registry_api/routes.py +205 -6
  108. omnibase_infra/services/registry_api/service.py +528 -1
  109. omnibase_infra/utils/__init__.py +7 -0
  110. omnibase_infra/utils/util_db_error_context.py +292 -0
  111. omnibase_infra/validation/infra_validators.py +3 -1
  112. omnibase_infra/validation/validation_exemptions.yaml +65 -0
  113. {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.4.0.dist-info}/METADATA +3 -3
  114. {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.4.0.dist-info}/RECORD +117 -58
  115. {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.4.0.dist-info}/WHEEL +0 -0
  116. {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.4.0.dist-info}/entry_points.txt +0 -0
  117. {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,243 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2025 OmniNode Team
3
+ """Handler for updating contract heartbeat timestamp.
4
+
5
+ This handler encapsulates PostgreSQL-specific heartbeat update logic for the
6
+ NodeContractPersistenceEffect node, following the declarative node pattern where
7
+ handlers are extracted for testability and separation of concerns.
8
+
9
+ Architecture:
10
+ HandlerPostgresHeartbeat is responsible for:
11
+ - Executing heartbeat timestamp updates against PostgreSQL
12
+ - Tracking whether the target row was found
13
+ - Returning structured ModelBackendResult
14
+
15
+ Timing, error classification, and sanitization are delegated to
16
+ MixinPostgresOpExecutor to eliminate boilerplate drift across handlers.
17
+
18
+ Operation:
19
+ Updates the last_seen_at timestamp for an active contract identified
20
+ by contract_id. Only active contracts (is_active = TRUE) are updated.
21
+ If the contract is not found or is inactive, the operation succeeds
22
+ but row_found is logged as false.
23
+
24
+ SQL:
25
+ UPDATE contracts
26
+ SET last_seen_at = $1
27
+ WHERE contract_id = $2 AND is_active = TRUE
28
+
29
+ Coroutine Safety:
30
+ This handler is stateless and coroutine-safe for concurrent calls
31
+ with different payload instances. Thread-safety depends on the
32
+ underlying asyncpg.Pool implementation.
33
+
34
+ Related:
35
+ - NodeContractPersistenceEffect: Parent effect node that coordinates handlers
36
+ - ModelPayloadUpdateHeartbeat: Payload model defining heartbeat parameters
37
+ - ModelBackendResult: Structured result model for backend operations
38
+ - MixinPostgresOpExecutor: Shared execution core for timing/error handling
39
+ - OMN-1845: Implementation ticket
40
+ - OMN-1857: Executor extraction ticket
41
+ """
42
+
43
+ from __future__ import annotations
44
+
45
+ import logging
46
+ from typing import TYPE_CHECKING
47
+ from uuid import UUID
48
+
49
+ from omnibase_infra.enums import (
50
+ EnumHandlerType,
51
+ EnumHandlerTypeCategory,
52
+ EnumPostgresErrorCode,
53
+ )
54
+ from omnibase_infra.mixins.mixin_postgres_op_executor import MixinPostgresOpExecutor
55
+ from omnibase_infra.models.model_backend_result import ModelBackendResult
56
+
57
+ if TYPE_CHECKING:
58
+ import asyncpg
59
+
60
+ from omnibase_infra.nodes.contract_registry_reducer.models.model_payload_update_heartbeat import (
61
+ ModelPayloadUpdateHeartbeat,
62
+ )
63
+
64
+ _logger = logging.getLogger(__name__)
65
+
66
+ # SQL for updating heartbeat timestamp
67
+ _UPDATE_HEARTBEAT_SQL = """
68
+ UPDATE contracts
69
+ SET last_seen_at = $1
70
+ WHERE contract_id = $2 AND is_active = TRUE
71
+ """
72
+
73
+
74
+ class HandlerPostgresHeartbeat(MixinPostgresOpExecutor):
75
+ """Handler for updating contract heartbeat timestamp.
76
+
77
+ Encapsulates PostgreSQL-specific heartbeat update logic extracted from
78
+ NodeContractPersistenceEffect for declarative node compliance. The handler
79
+ provides a clean interface for executing timestamp updates.
80
+
81
+ Timing, error classification, and sanitization are handled by the
82
+ MixinPostgresOpExecutor base class, reducing boilerplate and ensuring
83
+ consistent error handling across all PostgreSQL handlers.
84
+
85
+ The heartbeat operation updates the last_seen_at timestamp for an active
86
+ contract, supporting contract lifecycle management by tracking node
87
+ liveness.
88
+
89
+ Attributes:
90
+ _pool: asyncpg connection pool for database operations.
91
+
92
+ Example:
93
+ >>> from unittest.mock import AsyncMock, MagicMock
94
+ >>> conn = MagicMock()
95
+ >>> conn.execute = AsyncMock(return_value="UPDATE 1")
96
+ >>> pool = MagicMock()
97
+ >>> pool.acquire = MagicMock(return_value=AsyncContextManager(conn))
98
+ >>> handler = HandlerPostgresHeartbeat(pool)
99
+ >>> payload = MagicMock(
100
+ ... contract_id="my-node:1.0.0",
101
+ ... last_seen_at=datetime.now(),
102
+ ... )
103
+ >>> result = await handler.handle(payload, uuid4())
104
+ >>> result.success
105
+ True
106
+
107
+ See Also:
108
+ - NodeContractPersistenceEffect: Parent node that uses this handler
109
+ - ModelPayloadUpdateHeartbeat: Payload model for heartbeat parameters
110
+ - MixinPostgresOpExecutor: Shared execution mechanics
111
+ """
112
+
113
+ def __init__(self, pool: asyncpg.Pool) -> None:
114
+ """Initialize handler with asyncpg connection pool.
115
+
116
+ Args:
117
+ pool: asyncpg connection pool for executing heartbeat
118
+ update operations against the contracts table.
119
+ """
120
+ self._pool = pool
121
+
122
+ @property
123
+ def handler_type(self) -> EnumHandlerType:
124
+ """Architectural role of this handler."""
125
+ return EnumHandlerType.INFRA_HANDLER
126
+
127
+ @property
128
+ def handler_category(self) -> EnumHandlerTypeCategory:
129
+ """Behavioral classification of this handler."""
130
+ return EnumHandlerTypeCategory.EFFECT
131
+
132
+ async def handle(
133
+ self,
134
+ payload: ModelPayloadUpdateHeartbeat,
135
+ correlation_id: UUID,
136
+ ) -> ModelBackendResult:
137
+ """Execute heartbeat timestamp update for a contract.
138
+
139
+ Updates the last_seen_at timestamp for the contract identified
140
+ by contract_id, if the contract exists and is active.
141
+
142
+ Args:
143
+ payload: Heartbeat parameters containing:
144
+ - contract_id: Derived natural key (node_name:major.minor.patch)
145
+ - last_seen_at: New heartbeat timestamp
146
+ - node_name: Contract node name (for logging)
147
+ - source_node_id: Optional source node ID (for logging)
148
+ - uptime_seconds: Optional node uptime (for logging)
149
+ - sequence_number: Optional heartbeat sequence (for logging)
150
+ correlation_id: Request correlation ID for distributed tracing.
151
+
152
+ Returns:
153
+ ModelBackendResult with:
154
+ - success: True if update completed (even if row not found)
155
+ - error: Sanitized error message if failed
156
+ - error_code: Error code for programmatic handling
157
+ - duration_ms: Operation duration in milliseconds
158
+ - backend_id: Set to "postgres"
159
+ - correlation_id: Passed through for tracing
160
+
161
+ Note:
162
+ The row_found status is logged for observability but not
163
+ returned in the result model (ModelBackendResult does not support
164
+ metadata). The operation is considered successful even if no row
165
+ was found (the contract may have been deregistered), which allows
166
+ heartbeat processing to continue without errors.
167
+ """
168
+ return await self._execute_postgres_op(
169
+ op_error_code=EnumPostgresErrorCode.HEARTBEAT_ERROR,
170
+ correlation_id=correlation_id,
171
+ log_context={
172
+ "contract_id": payload.contract_id,
173
+ "node_name": payload.node_name,
174
+ "source_node_id": payload.source_node_id,
175
+ "uptime_seconds": payload.uptime_seconds,
176
+ "sequence_number": payload.sequence_number,
177
+ },
178
+ fn=lambda: self._execute_heartbeat(payload, correlation_id),
179
+ )
180
+
181
+ async def _execute_heartbeat(
182
+ self,
183
+ payload: ModelPayloadUpdateHeartbeat,
184
+ correlation_id: UUID,
185
+ ) -> None:
186
+ """Execute the heartbeat UPDATE query.
187
+
188
+ This method contains the handler-specific business logic:
189
+ - Execute the UPDATE query
190
+ - Parse the affected row count
191
+ - Log success/warning based on row_found
192
+
193
+ Args:
194
+ payload: Heartbeat payload with contract_id and timestamp.
195
+ correlation_id: Correlation ID for logging.
196
+
197
+ Raises:
198
+ Any exception from asyncpg (handled by MixinPostgresOpExecutor).
199
+ """
200
+ async with self._pool.acquire() as conn:
201
+ # Execute update - returns status string like "UPDATE 1" or "UPDATE 0"
202
+ status = await conn.execute(
203
+ _UPDATE_HEARTBEAT_SQL,
204
+ payload.last_seen_at,
205
+ payload.contract_id,
206
+ )
207
+
208
+ # Parse affected row count from status (format: "UPDATE N")
209
+ row_found = False
210
+ if status and status.startswith("UPDATE "):
211
+ try:
212
+ affected_rows = int(status.split()[1])
213
+ row_found = affected_rows > 0
214
+ except (IndexError, ValueError):
215
+ pass # Fallback to False if parsing fails
216
+
217
+ # Log for observability
218
+ _logger.info(
219
+ "Heartbeat update completed",
220
+ extra={
221
+ "correlation_id": str(correlation_id),
222
+ "contract_id": payload.contract_id,
223
+ "node_name": payload.node_name,
224
+ "row_found": row_found,
225
+ "source_node_id": payload.source_node_id,
226
+ "uptime_seconds": payload.uptime_seconds,
227
+ "sequence_number": payload.sequence_number,
228
+ },
229
+ )
230
+
231
+ # Log warning if row not found (contract may be deregistered)
232
+ if not row_found:
233
+ _logger.warning(
234
+ "Heartbeat update found no matching active contract",
235
+ extra={
236
+ "correlation_id": str(correlation_id),
237
+ "contract_id": payload.contract_id,
238
+ "node_name": payload.node_name,
239
+ },
240
+ )
241
+
242
+
243
+ __all__: list[str] = ["HandlerPostgresHeartbeat"]
@@ -0,0 +1,208 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2025 OmniNode Team
3
+ """Handler for batch marking stale contracts as inactive.
4
+
5
+ This handler encapsulates PostgreSQL-specific staleness marking logic for the
6
+ NodeContractPersistenceEffect node, following the declarative node pattern where
7
+ handlers are extracted for testability and separation of concerns.
8
+
9
+ Architecture:
10
+ HandlerPostgresMarkStale is responsible for:
11
+ - Executing batch update operations against PostgreSQL
12
+ - Returning structured ModelBackendResult with affected row count
13
+
14
+ Timing, error classification, and sanitization are delegated to
15
+ MixinPostgresOpExecutor to eliminate boilerplate drift across handlers.
16
+
17
+ Operation:
18
+ Marks all active contracts with last_seen_at before the stale_cutoff
19
+ timestamp as inactive. This is a batch operation that may affect
20
+ multiple rows in a single execution.
21
+
22
+ SQL:
23
+ UPDATE contracts
24
+ SET is_active = FALSE, deregistered_at = $1
25
+ WHERE is_active = TRUE AND last_seen_at < $2
26
+
27
+ Coroutine Safety:
28
+ This handler is stateless and coroutine-safe for concurrent calls
29
+ with different payload instances. Thread-safety depends on the
30
+ underlying asyncpg.Pool implementation.
31
+
32
+ Related:
33
+ - NodeContractPersistenceEffect: Parent effect node that coordinates handlers
34
+ - ModelPayloadMarkStale: Payload model defining staleness parameters
35
+ - ModelBackendResult: Structured result model for backend operations
36
+ - MixinPostgresOpExecutor: Shared execution core for timing/error handling
37
+ - OMN-1845: Implementation ticket
38
+ - OMN-1857: Executor extraction ticket
39
+ """
40
+
41
+ from __future__ import annotations
42
+
43
+ import logging
44
+ from typing import TYPE_CHECKING
45
+ from uuid import UUID
46
+
47
+ from omnibase_infra.enums import (
48
+ EnumHandlerType,
49
+ EnumHandlerTypeCategory,
50
+ EnumPostgresErrorCode,
51
+ )
52
+ from omnibase_infra.mixins.mixin_postgres_op_executor import MixinPostgresOpExecutor
53
+ from omnibase_infra.models.model_backend_result import ModelBackendResult
54
+
55
+ if TYPE_CHECKING:
56
+ import asyncpg
57
+
58
+ from omnibase_infra.nodes.contract_registry_reducer.models.model_payload_mark_stale import (
59
+ ModelPayloadMarkStale,
60
+ )
61
+
62
+ _logger = logging.getLogger(__name__)
63
+
64
+ # SQL for batch marking stale contracts
65
+ _MARK_STALE_SQL = """
66
+ UPDATE contracts
67
+ SET is_active = FALSE, deregistered_at = $1
68
+ WHERE is_active = TRUE AND last_seen_at < $2
69
+ """
70
+
71
+
72
+ class HandlerPostgresMarkStale(MixinPostgresOpExecutor):
73
+ """Handler for batch marking stale contracts as inactive.
74
+
75
+ Encapsulates PostgreSQL-specific batch staleness marking logic extracted
76
+ from NodeContractPersistenceEffect for declarative node compliance.
77
+
78
+ Timing, error classification, and sanitization are handled by the
79
+ MixinPostgresOpExecutor base class, reducing boilerplate and ensuring
80
+ consistent error handling across all PostgreSQL handlers.
81
+
82
+ The staleness operation marks contracts as inactive if their last_seen_at
83
+ timestamp is older than the specified stale_cutoff, supporting contract
84
+ lifecycle management through automatic deregistration of stale nodes.
85
+
86
+ Attributes:
87
+ _pool: asyncpg connection pool for database operations.
88
+
89
+ Example:
90
+ >>> from unittest.mock import AsyncMock, MagicMock
91
+ >>> conn = MagicMock()
92
+ >>> conn.execute = AsyncMock(return_value="UPDATE 5")
93
+ >>> pool = MagicMock()
94
+ >>> pool.acquire = MagicMock(return_value=AsyncContextManager(conn))
95
+ >>> handler = HandlerPostgresMarkStale(pool)
96
+ >>> payload = MagicMock(stale_cutoff=datetime.now(), checked_at=datetime.now())
97
+ >>> result = await handler.handle(payload, uuid4())
98
+ >>> result.success
99
+ True
100
+
101
+ See Also:
102
+ - NodeContractPersistenceEffect: Parent node that uses this handler
103
+ - ModelPayloadMarkStale: Payload model for staleness parameters
104
+ - MixinPostgresOpExecutor: Shared execution mechanics
105
+ """
106
+
107
+ def __init__(self, pool: asyncpg.Pool) -> None:
108
+ """Initialize handler with asyncpg connection pool.
109
+
110
+ Args:
111
+ pool: asyncpg connection pool for executing batch update
112
+ operations against the contracts table.
113
+ """
114
+ self._pool = pool
115
+
116
+ @property
117
+ def handler_type(self) -> EnumHandlerType:
118
+ """Architectural role of this handler."""
119
+ return EnumHandlerType.INFRA_HANDLER
120
+
121
+ @property
122
+ def handler_category(self) -> EnumHandlerTypeCategory:
123
+ """Behavioral classification of this handler."""
124
+ return EnumHandlerTypeCategory.EFFECT
125
+
126
+ async def handle(
127
+ self,
128
+ payload: ModelPayloadMarkStale,
129
+ correlation_id: UUID,
130
+ ) -> ModelBackendResult:
131
+ """Execute batch staleness update on contracts.
132
+
133
+ Marks all active contracts with last_seen_at before stale_cutoff
134
+ as inactive. This is a batch operation that may affect multiple
135
+ rows in a single execution.
136
+
137
+ Args:
138
+ payload: Staleness parameters containing:
139
+ - stale_cutoff: Contracts older than this are marked stale
140
+ - checked_at: Timestamp used for deregistered_at value
141
+ correlation_id: Request correlation ID for distributed tracing.
142
+
143
+ Returns:
144
+ ModelBackendResult with:
145
+ - success: True if update completed successfully
146
+ - error: Sanitized error message if failed
147
+ - error_code: Error code for programmatic handling
148
+ - duration_ms: Operation duration in milliseconds
149
+ - backend_id: Set to "postgres"
150
+ - correlation_id: Passed through for tracing
151
+
152
+ Note:
153
+ The number of affected rows is logged for observability but not
154
+ returned in the result model (ModelBackendResult does not support
155
+ metadata). Callers requiring the count should query the database
156
+ separately or rely on log aggregation.
157
+ """
158
+ return await self._execute_postgres_op(
159
+ op_error_code=EnumPostgresErrorCode.MARK_STALE_ERROR,
160
+ correlation_id=correlation_id,
161
+ log_context={
162
+ "stale_cutoff": payload.stale_cutoff.isoformat(),
163
+ },
164
+ fn=lambda: self._execute_mark_stale(payload, correlation_id),
165
+ )
166
+
167
+ async def _execute_mark_stale(
168
+ self,
169
+ payload: ModelPayloadMarkStale,
170
+ correlation_id: UUID,
171
+ ) -> None:
172
+ """Execute the batch staleness UPDATE query.
173
+
174
+ Args:
175
+ payload: Mark stale payload with timestamps.
176
+ correlation_id: Correlation ID for logging.
177
+
178
+ Raises:
179
+ Any exception from asyncpg (handled by MixinPostgresOpExecutor).
180
+ """
181
+ async with self._pool.acquire() as conn:
182
+ # Execute batch update - returns status string like "UPDATE 5"
183
+ status = await conn.execute(
184
+ _MARK_STALE_SQL,
185
+ payload.checked_at,
186
+ payload.stale_cutoff,
187
+ )
188
+
189
+ # Parse affected row count from status (format: "UPDATE N")
190
+ affected_rows = 0
191
+ if status and status.startswith("UPDATE "):
192
+ try:
193
+ affected_rows = int(status.split()[1])
194
+ except (IndexError, ValueError):
195
+ pass # Fallback to 0 if parsing fails
196
+
197
+ # Log for observability
198
+ _logger.info(
199
+ "Mark stale operation completed",
200
+ extra={
201
+ "correlation_id": str(correlation_id),
202
+ "affected_rows": affected_rows,
203
+ "stale_cutoff": payload.stale_cutoff.isoformat(),
204
+ },
205
+ )
206
+
207
+
208
+ __all__: list[str] = ["HandlerPostgresMarkStale"]