Repository navigation
feat(handlers): implement circuit breaker pattern for DbHandler [OMN-780] - #159
Conversation
…780] Add MixinAsyncCircuitBreaker to HandlerDb for connection resilience. The circuit breaker protects against transient infrastructure failures (connection errors, timeouts) while allowing application-level errors (syntax errors, missing tables/columns) to pass through without tripping the circuit. Changes: - Add MixinAsyncCircuitBreaker to class inheritance - Initialize circuit breaker after pool creation (threshold=5, reset=30s) - Wrap _execute_query() and _execute_statement() with CB pattern - Update describe() to include circuit breaker state - Update shutdown() to reset circuit breaker - Add circuit_breaker field to ModelDbDescribeResponse
📝 WalkthroughWalkthroughIntegrates a circuit breaker into HandlerDb, adds PostgreSQL error classification for transient vs permanent failures, wires breaker lifecycle into initialization/shutdown and query execution, and exposes breaker state in the DB describe response. Tests for classification and breaker behavior were added. Changes
Sequence DiagramsequenceDiagram
participant Client as Client
participant Handler as HandlerDb
participant CB as CircuitBreaker
participant Pool as ConnectionPool
participant PG as AsyncPG
Client->>Handler: execute_query(...)
Handler->>CB: check_allow_request()
alt Circuit Open
CB-->>Handler: raise CircuitOpen
Handler-->>Client: CircuitOpen error
else Circuit Closed/Half-Open
CB-->>Handler: allow
Handler->>Pool: acquire_connection()
Pool-->>Handler: connection
Handler->>PG: execute SQL
alt Success
PG-->>Handler: result
Handler->>CB: record_success / reset
Handler-->>Client: result
else Error (transient)
PG-->>Handler: error (sqlstate ∈ transient set)
Handler->>CB: record_failure()
Handler-->>Client: error
else Error (permanent)
PG-->>Handler: error (sqlstate ∉ transient set)
Handler-->>Client: error
end
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
Comment |
Code Review - PR #159: Circuit Breaker Pattern for HandlerDbSummaryThis PR implements the circuit breaker pattern for ✅ Strengths1. Correct Circuit Breaker Integration
2. Smart Error ClassificationThe distinction between infrastructure vs application errors is well-designed:
This is exactly correct - the circuit breaker should only open for transient infrastructure failures, not permanent application bugs. 3. Proper Lock UsageAll circuit breaker operations correctly use
4. Clean Shutdown HandlingProperly resets circuit breaker state in 5. Good Observability
|
| Category | Rating | Notes |
|---|---|---|
| Correctness | Missing explicit error handling in _execute_statement |
|
| Code Quality | ✅ 9/10 | Clean, follows patterns, minor inconsistencies |
| Test Coverage | No circuit breaker-specific tests | |
| Security | ✅ 10/10 | No issues |
| Performance | ✅ 9/10 | Minimal overhead, configurable timeout needed |
| Documentation | ✅ 8/10 | Good inline docs, could add ADR |
✅ Approval Recommendation
Approve with minor changes requested.
Required Changes:
- ✅ Refactor
_execute_statementerror handling to use explicit catch blocks (consistency with_execute_query)
Recommended Changes (not blocking):
- Add circuit breaker integration tests
- Make reset timeout configurable
- Add ADR for database circuit breaker pattern
🚀 Next Steps
After this PR merges:
- Add circuit breaker tests in follow-up PR
- Monitor circuit breaker metrics in production
- Consider making threshold/timeout configurable if needed
- Update handbook with database resilience patterns
Great work overall! The circuit breaker pattern is exactly what's needed for database resilience. The error classification logic is particularly well thought out. Just needs consistency improvements in error handling.
…ment [OMN-780] Address PR review feedback: - Replace catch-all PostgresError handling with explicit catch blocks - Match _execute_query pattern for consistency and maintainability - Infrastructure errors (QueryCanceledError, PostgresConnectionError) trip circuit - Application errors (PostgresSyntaxError, UndefinedTable/Column) do not trip circuit
PR Review: Circuit Breaker Implementation for HandlerDbOverviewThis PR implements circuit breaker resilience for ✅ Strengths1. Correct Circuit Breaker Pattern Usage
2. Intelligent Error ClassificationThe error handling design correctly distinguishes infrastructure vs application errors:
This prevents circuit breaker from masking application bugs - excellent design decision. 3. Proper State Management
4. Clean Integration
🔍 Issues & ConcernsCRITICAL: Missing Test CoverageThe PR states "All 65 existing unit tests pass" but does not add new tests for circuit breaker behavior. This is a significant gap. Required test coverage:
Recommendation: Add a dedicated test class MEDIUM: Lock Acquisition OrderingLocation: Lines 540-546, 627-633 The current pattern acquires the lock separately for check and record: # Check circuit (lock acquired)
if self._circuit_breaker_initialized:
async with self._circuit_breaker_lock:
await self._check_circuit_breaker(...)
# Execute query
async with self._pool.acquire() as conn:
rows = await conn.fetch(sql, *parameters)
# Reset circuit (lock acquired separately)
if self._circuit_breaker_initialized:
async with self._circuit_breaker_lock:
await self._reset_circuit_breaker()Issue: The lock is released between operations, allowing race conditions if multiple coroutines execute queries concurrently. A failure in one coroutine during the gap between "check" and "record" could result in inconsistent state. However: This pattern is consistent with other uses in the codebase (e.g., Recommendation: Consider documenting this behavior in MEDIUM: Shutdown Race ConditionLocation: Lines 356-359 async def shutdown(self) -> None:
"""Close database connection pool and release resources."""
# Reset circuit breaker state
if self._circuit_breaker_initialized:
async with self._circuit_breaker_lock:
await self._reset_circuit_breaker()
self._circuit_breaker_initialized = False
if self._pool is not None:
await self._pool.close()
self._pool = None
self._initialized = FalseIssue: If a query is in-flight during shutdown, the following sequence could occur:
Impact: Low - likely only affects graceful shutdown scenarios. Recommendation: Set LOW: Generic PostgresError HandlingLocation: Lines 595-599 (in except asyncpg.PostgresError as e:
# Generic PostgreSQL error - do NOT trip circuit (application bug likely)
raise RuntimeHostError(
f"Database error: {type(e).__name__}", context=ctx
) from eIssue: The catch-all
These should arguably trip the circuit breaker. Recommendation: Add explicit handlers for infrastructure-related LOW: Configuration Not ExposedLocation: Lines 294-300 self._init_circuit_breaker(
threshold=5,
reset_timeout=30.0,
service_name="db_handler",
transport_type=EnumInfraTransportType.DATABASE,
)Issue: Circuit breaker configuration is hardcoded. Threshold=5 and timeout=30s may not be appropriate for all deployments. Recommendation:
LOW: Removed Error MappingLocation: Line 685 The PR removes the # OLD (line 573):
except asyncpg.PostgresError as e:
raise self._map_postgres_error(e, ctx) from e
# NEW (line 685):
except asyncpg.PostgresError as e:
raise RuntimeHostError(f"Database error: {type(e).__name__}", context=ctx) from eIssue: The Impact: Loss of error specificity - harder to diagnose issues. Recommendation: Restore 📋 Documentation & Style✅ Good:
|
| Rule | Status | Notes |
|---|---|---|
Use MixinAsyncCircuitBreaker |
✅ Pass | Correctly extends mixin |
| Lock usage pattern | ✅ Pass | Properly uses async with self._circuit_breaker_lock |
| Error hierarchy | Should use infrastructure-specific errors for connection issues | |
Zero Any types |
✅ Pass | No Any types introduced |
| Strong typing | ✅ Pass | All parameters properly typed |
| Test coverage | ❌ Fail | No new tests for circuit breaker behavior |
🚦 Verdict
Recommendation: APPROVE WITH CHANGES REQUESTED
Blockers (must fix before merge):
- Add circuit breaker test coverage - at minimum, test that circuit opens on connection errors and does NOT open on application errors
Strongly Recommended (high value, low effort):
- Make circuit breaker config (threshold, timeout) configurable
- Add explicit handlers for infrastructure-related
PostgresErrorsubclasses - Restore
_map_postgres_error()logic for specific error types
Nice-to-Have (can be follow-up PRs):
- Document shutdown race condition mitigation strategy
- Add ADR for error classification design
- Benchmark lock contention under high load
🎉 Overall Assessment
This is a solid implementation that correctly applies the circuit breaker pattern following ONEX conventions. The error classification design is particularly well thought out. The main gap is test coverage - please add tests before merging.
Great work on improving database resilience! 🚀
Reviewed by: Claude Sonnet 4.5 (ONEX Infrastructure Review Agent)
Review Date: 2026-01-16
…ions [OMN-780] Add specific exception handlers for ForeignKeyViolationError and NotNullViolationError with descriptive error messages. This improves error classification for debugging and fixes CI test assertions that expect "foreign key" and "not null" phrases in error messages.
PR Review: Circuit Breaker Pattern for HandlerDbSummaryThis PR implements the circuit breaker pattern for ✅ Strengths1. Correct Error ClassificationThe implementation properly distinguishes between infrastructure failures (which trip the circuit) and application errors (which don't):
This design prevents the circuit from opening due to code bugs, which is exactly the right behavior. 2. Consistent Pattern ApplicationBoth
3. Proper Initialization & Cleanup
4. Observability EnhancementThe
|
…t breaker [OMN-780] Add SQLSTATE-based error classification to distinguish transient errors (connection failures, timeouts, resource exhaustion) from permanent errors (constraint violations, syntax errors). Only transient errors now trip the circuit breaker, preventing unnecessary service degradation from application bugs like FK or NOT NULL violations.
PR Review: Circuit Breaker Implementation for HandlerDb [OMN-780]✅ Overall AssessmentThis is a well-implemented, production-ready circuit breaker integration that follows ONEX patterns correctly. The intelligent SQLSTATE-based error classification is particularly impressive and demonstrates deep understanding of PostgreSQL failure modes. 🎯 Strengths1. Intelligent Error ClassificationThe
Reference: 2. Correct Circuit Breaker Pattern
3. Comprehensive Test CoverageThe test suite is thorough with 445 new test lines:
Reference: 4. Security & Best Practices
🔍 Code Quality ObservationsExcellent Design Decisions
|
| Requirement | Status | Evidence |
|---|---|---|
No Any types |
✅ Pass | No violations in diff |
| Container injection | ✅ N/A | Handler doesn't use container (MVP pattern) |
| Circuit breaker mixin | ✅ Pass | Correctly extends MixinAsyncCircuitBreaker |
| Error hierarchy | ✅ Pass | Uses InfraConnectionError, InfraTimeoutError, RuntimeHostError |
| Error context | ✅ Pass | All errors include ModelInfraErrorContext |
| Lock safety | ✅ Pass | All circuit methods use async with self._circuit_breaker_lock |
| Declarative nodes | ✅ N/A | This is a handler, not a node |
🎯 Final Recommendations
Must Fix Before Merge
None - this PR is ready to merge as-is.
Nice-to-Have Improvements
- Add
UniqueViolationErrorexplicit handler for consistency - Document circuit breaker threshold/timeout rationale with inline comment
- Consider adding integration test for circuit state transitions (CLOSED → OPEN → HALF_OPEN)
Questions for Author
- Was the 30s reset timeout intentionally more aggressive than Vault's 60s?
- Have you considered the connection pool exhaustion scenario (e.g., all 5 connections stuck in query timeout)?
✅ Approval
Recommendation: APPROVE ✅
This PR demonstrates:
- Deep understanding of PostgreSQL failure modes
- Correct application of ONEX circuit breaker patterns
- Excellent test coverage
- Production-ready error handling
The SQLSTATE-based classification is a significant improvement over naive exception type checking. The minor suggestions above are optional refinements, not blockers.
Great work! 🚀
📚 References
- CLAUDE.md Circuit Breaker Pattern: Lines 533-548
MixinAsyncCircuitBreaker:src/omnibase_infra/mixins/mixin_async_circuit_breaker.py- Dispatcher Resilience:
docs/patterns/dispatcher_resilience.md - PostgreSQL SQLSTATE Codes: https://www.postgresql.org/docs/current/errcodes-appendix.html
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@src/omnibase_infra/handlers/handler_db.py`:
- Around line 138-159: The code currently logs full asyncpg error strings
(exposing message/detail/internal_query) in the DB exception handlers; update
those handlers to stop logging str(error) and instead log only non-sensitive
metadata: the exception class name, the error.code or error.sqlstate (if
present), and whether that sqlstate's class is in
_TRANSIENT_SQLSTATE_CLASSES/_PERMANENT_SQLSTATE_CLASSES; remove any inclusion of
error.message, error.detail, error.context, error.internal_query or parameter
values and replace with a short generic message (e.g., "Postgres error
encountered: code=<code>, class=<class>, transient=<bool>") in the
functions/methods that currently call processLogger/errorLogger with str(error)
(the DB exception handlers referenced alongside _TRANSIENT_SQLSTATE_CLASSES and
_PERMANENT_SQLSTATE_CLASSES).
🧹 Nitpick comments (3)
src/omnibase_infra/handlers/handler_db.py (2)
210-218: Consider DI container injection inHandlerDb.__init__.If this handler is treated as a service, align the constructor with the
ModelONEXContainerDI pattern so dependencies are explicit and consistent.As per coding guidelines, services should accept
ModelONEXContainervia__init__.
964-978: UseJsonTypefor the circuit breaker state.
circuit_breakeris a JSON-compatible payload; aligning the local type withJsonTypekeeps it consistent with repo typing standards.♻️ Suggested change
-from omnibase_core.models.dispatch import ModelHandlerOutput +from omnibase_core.models.dispatch import ModelHandlerOutput +from omnibase_core.types import JsonType @@ - cb_state: dict[str, object] | None = None + cb_state: JsonType | None = NoneAs per coding guidelines, JSON-compatible values should use
JsonType.src/omnibase_infra/handlers/models/model_db_describe_response.py (1)
28-29: Typecircuit_breakerasJsonType.This field represents JSON state; using the shared alias avoids overly generic dicts and keeps consistency.
♻️ Suggested change
-from pydantic import BaseModel, ConfigDict, Field +from pydantic import BaseModel, ConfigDict, Field +from omnibase_core.types import JsonType @@ - circuit_breaker: dict[str, object] | None = Field( + circuit_breaker: JsonType | None = Field( default=None, description="Circuit breaker state information (state, failures, threshold, etc.)", )As per coding guidelines, JSON-compatible values should use
JsonType.Also applies to: 75-78
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Lite
📒 Files selected for processing (3)
src/omnibase_infra/handlers/handler_db.pysrc/omnibase_infra/handlers/models/model_db_describe_response.pytests/unit/handlers/test_handler_db.py
🧰 Additional context used
📓 Path-based instructions (3)
**/*.py
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.py: Never useAnytype - useobjectfor generic payloads in function parameters and return types
UseX | None(PEP 604) instead ofOptional[X]for nullable types
All services must useModelONEXContainerfor dependency injection via__init__(self, container: ModelONEXContainer)
Use@allow_anydecorator with documented reason as exemption mechanism forAnytype violations
UseJsonTypefromomnibase_core.typesas the canonical type alias for JSON-compatible values
UseModelEventEnvelope[object]for generic dispatcher interfaces andobjectfor generic payloads
Infrastructure error handling must useOnexErrorbase class and never expose passwords, API keys, PII, or connection strings with credentials in error messages
UseInfraConnectionError,InfraTimeoutError,InfraAuthenticationError,InfraUnavailableErrorfor transport failures with properModelInfraErrorContext
Correlation IDs must be propagated from incoming requests, auto-generated withuuid4()if missing, and included in all error contexts
External service adapters must implementMixinAsyncCircuitBreakerwith appropriate threshold and reset_timeout configuration
Protocol resolution must use duck typing via protocols, never useisinstancechecks
Files:
src/omnibase_infra/handlers/models/model_db_describe_response.pytests/unit/handlers/test_handler_db.pysrc/omnibase_infra/handlers/handler_db.py
**/model_*.py
📄 CodeRabbit inference engine (CLAUDE.md)
**/model_*.py: File naming convention: Models must usemodel_<name>.pywith class nameModel<Name>
Each model file must contain exactly oneModel*class
Pydantic workaround forAnytype must include# NOTE:comment documenting the reason when technically required
UseSerializeAsAnytype wrapper for Pydantic fields containing complex nested models to preserve subclass fields during serialization
Files:
src/omnibase_infra/handlers/models/model_db_describe_response.py
**/handler_*.py
📄 CodeRabbit inference engine (CLAUDE.md)
Handlers must NOT have direct event bus access - only orchestrators may have bus parameters and publish events
Files:
src/omnibase_infra/handlers/handler_db.py
🧠 Learnings (6)
📓 Common learnings
Learnt from: CR
Repo: OmniNode-ai/omninode_bridge PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-30T21:55:10.298Z
Learning: Applies to src/metadata_stamping/database/**/*.py : Database layer MUST use connection pooling (10-50 connections), prepared statements, and circuit breaker pattern for resilience. Monitor pool exhaustion at >90% utilization.
📚 Learning: 2025-11-30T21:55:10.298Z
Learnt from: CR
Repo: OmniNode-ai/omninode_bridge PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-30T21:55:10.298Z
Learning: Applies to src/metadata_stamping/database/**/*.py : Database layer MUST use connection pooling (10-50 connections), prepared statements, and circuit breaker pattern for resilience. Monitor pool exhaustion at >90% utilization.
Applied to files:
src/omnibase_infra/handlers/handler_db.py
📚 Learning: 2025-11-30T21:55:10.298Z
Learnt from: CR
Repo: OmniNode-ai/omninode_bridge PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-30T21:55:10.298Z
Learning: Applies to src/omninode_bridge/nodes/**/*.py : Import mixins from omnibase_core.mixins.* and use Mixin* naming pattern (e.g., MixinHealthCheck, MixinMetrics, MixinEventBus) - never use local custom mixins unless experimental and documented
Applied to files:
src/omnibase_infra/handlers/handler_db.py
📚 Learning: 2025-11-24T16:32:55.606Z
Learnt from: CR
Repo: OmniNode-ai/omninode_bridge PR: 0
File: .cursor/rules/node_standards.mdc:0-0
Timestamp: 2025-11-24T16:32:55.606Z
Learning: Node communication must use event-driven patterns through `ModelEventEnvelope` from `omnibase_core.models.events.model_event_envelope`
Applied to files:
src/omnibase_infra/handlers/handler_db.py
📚 Learning: 2025-12-06T22:21:32.649Z
Learnt from: CR
Repo: OmniNode-ai/omniagent PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-06T22:21:32.649Z
Learning: Applies to nodes/**/*.py : Use `omnibase_infra` handlers for OmniIntelligence queries via HttpRestAdapter envelope pattern
Applied to files:
src/omnibase_infra/handlers/handler_db.py
📚 Learning: 2025-11-30T21:55:10.298Z
Learnt from: CR
Repo: OmniNode-ai/omninode_bridge PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-30T21:55:10.298Z
Learning: Applies to src/metadata_stamping/database/**/*.py : All input validation MUST prevent SQL injection using prepared statements and parameterized queries. Use asyncpg for PostgreSQL operations.
Applied to files:
src/omnibase_infra/handlers/handler_db.py
🧬 Code graph analysis (3)
src/omnibase_infra/handlers/models/model_db_describe_response.py (2)
src/omnibase_infra/event_bus/kafka_event_bus.py (1)
default(415-431)src/omnibase_infra/event_bus/models/config/model_kafka_event_bus_config.py (1)
default(567-592)
tests/unit/handlers/test_handler_db.py (2)
src/omnibase_infra/handlers/handler_db.py (2)
_is_transient_error(537-635)shutdown(377-389)src/omnibase_infra/errors/error_infra.py (2)
InfraConnectionError(232-339)RuntimeHostError(82-151)
src/omnibase_infra/handlers/handler_db.py (1)
src/omnibase_infra/mixins/mixin_async_circuit_breaker.py (5)
_init_circuit_breaker(187-269)_reset_circuit_breaker(506-581)_check_circuit_breaker(315-412)_record_circuit_failure(414-504)_get_circuit_breaker_state(583-652)
🔇 Additional comments (4)
src/omnibase_infra/handlers/handler_db.py (3)
97-97: Circuit breaker mixin + docs look good.Clear inheritance and docstring updates make the CB behavior easy to discover.
Also applies to: 162-208
316-324: CB lifecycle wiring looks solid.Initializing after pool creation and resetting during shutdown keeps breaker state aligned with handler lifecycle.
Also applies to: 379-384
663-744: Check → execute → reset/record flow is consistent.The pre-check and post-success reset plus transient failure recording are applied uniformly to both query and execute paths.
Also applies to: 772-852
tests/unit/handlers/test_handler_db.py (1)
1468-1907: Nice coverage for transient/permanent classification and CB behavior.The new suites exercise SQLSTATE class handling and circuit-breaker effects, and the exports stay in sync.
Also applies to: 1911-1925
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
Resolve merge conflict in handler_db.py by combining: - Circuit breaker failure recording from feature branch - ModelTimeoutErrorContext usage from main branch
…A_MAX_UNIONS [OMN-1181] Merge main branch changes into PR #158: - Resolve 9 merge conflicts across source and test files - Update INFRA_MAX_UNIONS: 81 → 83 (+2 for EnumPolicyType | str validators) - Fix exemption pattern for error_message_type_registry.py (was registry_message_type.py) - Accept main's file renames (event_bus_*, handler_*, registry_*, validator_*, etc.) - Accept main's test refactoring for renamed modules - Add missing imports to registry_policy.py for semver cache methods Key changes from main: - OMN-1305: isinstance union exclusion from threshold - OMN-1358: Type alias replacements - File naming standardization (service_*, handler_*, validator_* prefixes) - MessageTypeRegistryError added to errors module
Security: - Remove error_message from log context to prevent sensitive data exposure (SQL fragments, data values could contain PII) Bug fixes: - Add explicit UniqueViolationError handlers in _execute_query and _execute_statement methods (was falling through to generic handler) Type improvements: - Use JsonType for circuit_breaker field in ModelDbDescribeResponse instead of dict[str, object] per ONEX typing conventions
Pull Request Review: Circuit Breaker Pattern for HandlerDbSummaryThis PR implements circuit breaker resilience for the database handler following established ONEX patterns. The implementation correctly uses ✅ Strengths1. Excellent Error Classification DesignThe intelligent error classification based on PostgreSQL SQLSTATE codes is well-designed:
The 2. Comprehensive Test CoverageThe test suite is exemplary with 445 new lines covering:
3. Proper Mixin IntegrationThe circuit breaker initialization follows ONEX patterns correctly:
4. Documentation QualityClear inline documentation:
🔍 Issues & Concerns1. Critical: Potential Race Condition in Circuit Reset
|
| Aspect | Rating | Notes |
|---|---|---|
| Adherence to ONEX Patterns | ✅ Excellent | Follows CLAUDE.md patterns precisely |
| Test Coverage | ✅ Excellent | 445 lines of comprehensive tests |
| Type Safety | ✅ Excellent | No Any types, proper type annotations |
| Error Handling | ✅ Good | Intelligent classification, minor duplication |
| Documentation | ✅ Excellent | Clear inline comments and docstrings |
| Concurrency Safety | Potential race condition in circuit reset | |
| Performance | ✅ Good | Minimal overhead, SQLSTATE lookup is O(1) |
🔐 Security Review
Strengths:
- DSN sanitization maintained (never logged/exposed)
- No SQL injection risks introduced
- Proper error context without credential leakage
Concerns:
- Circuit breaker state exposure (see Issue feat: Complete infrastructure containers operational with Docker secrets #4 above)
- Consider rate limiting on
describe()if externally accessible
🚀 Performance Considerations
Strengths:
- SQLSTATE classification uses frozenset lookups (O(1))
- Circuit breaker check is fast (async lock + state check)
- No blocking operations in error handling path
Potential Impact:
- Extra lock acquisition per query (~microseconds overhead)
- Negligible compared to database I/O time
📋 Recommended Actions
Before Merge:
- [REQUIRED] Fix race condition in circuit breaker reset (Issue feat: PostgreSQL Adapter with Comprehensive Tests and Structured Logging #1)
- [REQUIRED] Clarify/document Class 40 (Transaction Rollback) classification rationale (Issue Add Claude Code GitHub Workflow #2)
- [RECOMMENDED] Document security posture of
describe()endpoint (Issue feat: Complete infrastructure containers operational with Docker secrets #4)
Optional Refactoring (Post-Merge):
- Extract duplicate error handling to shared method (Issue feat: RedPanda Event Bus Integration with Fail-Fast Infrastructure #3)
Verdict
Status: ✅ Approve with Minor Changes
This is a well-crafted implementation that follows ONEX patterns correctly. The intelligent error classification is particularly strong. The race condition in circuit reset should be addressed before merge, but the fix is straightforward and low-risk.
Estimated Risk: Low (once race condition fixed)
Confidence Level: High
References
- CLAUDE.md Circuit Breaker Pattern ✅
- CLAUDE.md Error Recovery Patterns ✅
docs/patterns/circuit_breaker_implementation.md✅docs/patterns/mixin_dependencies.md✅
Great work on this implementation! 🎉
- Fix race condition in circuit breaker reset by moving it inside connection context (prevents stale failure counts between coroutines) - Expand Class 40 (Transaction Rollback) documentation with design rationale explaining why deadlocks are classified as permanent - Add security documentation for circuit breaker state exposure in describe() method with recommendations for external API exposure
Pull Request Review - Circuit Breaker Pattern for DbHandlerOverall AssessmentThis PR implements a well-designed circuit breaker pattern for HandlerDb that follows ONEX conventions and demonstrates thoughtful error classification. The implementation is production-ready with comprehensive test coverage. Strengths1. Excellent Error Classification Design
2. Comprehensive Documentation
3. Race Condition Fix
4. Extensive Test Coverage
5. Security Best Practices
6. Follows ONEX Conventions
Minor Observations1. Circuit Breaker Threshold Configuration
2. Error Message Repetition
3. Unknown SQLSTATE Classes
4. Circuit Breaker State Exposure
Performance ConsiderationsLock Contention
Circuit Reset Pattern
Code QualityType Safety: No Any types, proper JsonType usage RecommendationsMust Address Before Merge: None - ready to merge Consider for Future PRs:
Security Review
Compliance with CLAUDE.md
Final VerdictAPPROVE - Merge Recommended Excellent implementation demonstrating:
Production-ready for MVP workloads. Minor optimization opportunities exist but are not blockers. Great work on the deadlock classification rationale! Checklist
Reviewed following ONEX conventions per CLAUDE.md |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@src/omnibase_infra/handlers/handler_db.py`:
- Around line 858-872: Replace the constraint-violation error messages that
interpolate sensitive database values from the asyncpg exceptions in the except
handlers (asyncpg.ForeignKeyViolationError, asyncpg.NotNullViolationError,
asyncpg.UniqueViolationError) by removing e.message from the RuntimeHostError
message strings in handler_db.py; instead use a generic, non-sensitive message
like "Foreign key constraint violation", "Not null constraint violation",
"Unique constraint violation" (preserving context=ctx and raising from e) so the
handlers (RuntimeHostError) still indicate the error type without exposing
sensitive data.
- Around line 738-752: The exception handlers for
asyncpg.ForeignKeyViolationError, asyncpg.NotNullViolationError, and
asyncpg.UniqueViolationError currently include e.message which may leak
sensitive values; update each RuntimeHostError raised in these blocks (the
handlers referencing RuntimeHostError in handler_db.py) to use only the safe
error prefix (e.g., the corresponding entry from _POSTGRES_ERROR_PREFIXES or a
fixed descriptive string like "Foreign key constraint violation") instead of
including e.message, matching how the generic PostgresError handler is
sanitized.
- Around line 139-173: The _PERMANENT_SQLSTATE_CLASSES set is missing the "40"
transaction-rollback class referenced in the design notes; update the
_PERMANENT_SQLSTATE_CLASSES frozenset (symbol name: _PERMANENT_SQLSTATE_CLASSES)
to include "40" so it becomes {"22","23","28","40","42"}, ensuring Class 40
errors are treated as permanent (and hit the DEBUG path rather than the
unknown-class WARNING).
🧹 Nitpick comments (1)
src/omnibase_infra/handlers/handler_db.py (1)
770-888: Consider extracting shared error handling logic.
_execute_queryand_execute_statementhave nearly identical circuit breaker integration and exception handling (~80 lines duplicated). Consider extracting a shared helper or using a decorator pattern to reduce maintenance burden.This is optional for this PR but would improve maintainability for future changes.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Lite
📒 Files selected for processing (2)
src/omnibase_infra/handlers/handler_db.pysrc/omnibase_infra/handlers/models/model_db_describe_response.py
🚧 Files skipped from review as they are similar to previous changes (1)
- src/omnibase_infra/handlers/models/model_db_describe_response.py
🧰 Additional context used
📓 Path-based instructions (2)
**/*.py
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.py: ALL coding tasks MUST use sub-agents (agent-commit, agent-testing, agent-contract-validator, agent-onex-coordinator, agent-workflow-coordinator) - NO direct coding allowed
NEVER userun_in_background: truefor Task tool - use single message with multiple Task tool calls for parallel execution
ALL changes are breaking changes with NO backwards compatibility - remove old patterns immediately, do not leave deprecated code, and do not provide migration guides
NEVER create versioned directories likev1_0_0/orv2/- version throughcontract.yamlfields only
NEVER useAnytype in function parameters, return types, type aliases, or Pydantic Field() annotations without# NOTE:comment and documented justification
Useobjectinstead ofAnyfor generic payloads in function parameters and return types
Pydantic models: one model per file, use PEP 604 unions (X | NonenotOptional[X]), all data structures must be proper Pydantic models
Use nullable type syntaxX | None(PEP 604) instead ofOptional[X]for all type annotations
UseModelEventEnvelope[object]for generic event handling in dispatchers and handlers - do not useAny
Import and useJsonTypefromomnibase_core.types(oromnibase_infra.models.typesre-export) for generic JSON-compatible values
File naming: adapter_.py, dispatcher_.py, error/ directory, model_.py, node.py, plugin_.py, service_.py, etc. per naming table
Class naming: Adapter, Dispatcher, Error, Model, Node, Plugin, Service, etc. per naming table
Standalone registries should be namedregistry_<purpose>.pywith class nameRegistry<Purpose>
Result models may override__bool__()to enable idiomatic conditional checks - always document in docstring Warning section
ALL services MUST use ModelONEXContainer for dependency injection - pass container to init and call super().init(container)
Import node archetypes from omnib...
Files:
src/omnibase_infra/handlers/handler_db.py
**/*handler*.py
📄 CodeRabbit inference engine (CLAUDE.md)
**/*handler*.py: Handlers MUST NOT have direct event bus access - no _bus, _event_bus, or _publisher attributes, no publish() methods, protocol compliance with ProtocolHandler
Handlers using node introspection (MixinNodeIntrospection) must prefix internal/sensitive methods with _ and use generic parameter names
Files:
src/omnibase_infra/handlers/handler_db.py
🧠 Learnings (7)
📓 Common learnings
Learnt from: CR
Repo: OmniNode-ai/omninode_bridge PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-30T21:55:10.298Z
Learning: Applies to src/metadata_stamping/database/**/*.py : Database layer MUST use connection pooling (10-50 connections), prepared statements, and circuit breaker pattern for resilience. Monitor pool exhaustion at >90% utilization.
📚 Learning: 2025-11-30T21:55:10.298Z
Learnt from: CR
Repo: OmniNode-ai/omninode_bridge PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-30T21:55:10.298Z
Learning: Applies to src/metadata_stamping/database/**/*.py : Database layer MUST use connection pooling (10-50 connections), prepared statements, and circuit breaker pattern for resilience. Monitor pool exhaustion at >90% utilization.
Applied to files:
src/omnibase_infra/handlers/handler_db.py
📚 Learning: 2026-01-16T17:34:10.902Z
Learnt from: CR
Repo: OmniNode-ai/omnibase_infra PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-16T17:34:10.902Z
Learning: Applies to **/*adapter*.py : External service adapters MUST implement MixinAsyncCircuitBreaker for resilience - use _init_circuit_breaker() with threshold, reset_timeout, service_name, transport_type
Applied to files:
src/omnibase_infra/handlers/handler_db.py
📚 Learning: 2025-11-30T21:55:10.298Z
Learnt from: CR
Repo: OmniNode-ai/omninode_bridge PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-30T21:55:10.298Z
Learning: Applies to src/metadata_stamping/database/**/*.py : All input validation MUST prevent SQL injection using prepared statements and parameterized queries. Use asyncpg for PostgreSQL operations.
Applied to files:
src/omnibase_infra/handlers/handler_db.py
📚 Learning: 2025-11-30T21:55:10.298Z
Learnt from: CR
Repo: OmniNode-ai/omninode_bridge PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-30T21:55:10.298Z
Learning: Applies to src/omninode_bridge/nodes/**/*.py : Import mixins from omnibase_core.mixins.* and use Mixin* naming pattern (e.g., MixinHealthCheck, MixinMetrics, MixinEventBus) - never use local custom mixins unless experimental and documented
Applied to files:
src/omnibase_infra/handlers/handler_db.py
📚 Learning: 2025-11-24T16:32:55.606Z
Learnt from: CR
Repo: OmniNode-ai/omninode_bridge PR: 0
File: .cursor/rules/node_standards.mdc:0-0
Timestamp: 2025-11-24T16:32:55.606Z
Learning: Node communication must use event-driven patterns through `ModelEventEnvelope` from `omnibase_core.models.events.model_event_envelope`
Applied to files:
src/omnibase_infra/handlers/handler_db.py
📚 Learning: 2025-12-06T22:21:32.649Z
Learnt from: CR
Repo: OmniNode-ai/omniagent PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-06T22:21:32.649Z
Learning: Applies to nodes/**/*.py : Use `omnibase_infra` handlers for OmniIntelligence queries via HttpRestAdapter envelope pattern
Applied to files:
src/omnibase_infra/handlers/handler_db.py
🧬 Code graph analysis (1)
src/omnibase_infra/handlers/handler_db.py (1)
src/omnibase_infra/mixins/mixin_async_circuit_breaker.py (5)
MixinAsyncCircuitBreaker(115-652)_reset_circuit_breaker(506-581)_check_circuit_breaker(315-412)_record_circuit_failure(414-504)_get_circuit_breaker_state(583-652)
🔇 Additional comments (8)
src/omnibase_infra/handlers/handler_db.py (8)
98-98: LGTM!Import follows the
Mixin*naming pattern and sources infrastructure mixins from the appropriate package.
176-222: LGTM!Class inheritance correctly integrates both mixins, and the circuit breaker documentation clearly describes the state machine and error classification policy. Based on learnings, database handlers should use circuit breaker pattern for resilience.
224-231: LGTM!The
_circuit_breaker_initializedflag correctly tracks lazy initialization state, ensuring circuit breaker methods aren't called before_init_circuit_breaker().
329-337: LGTM!Circuit breaker initialization follows the mixin contract with appropriate parameters. Initializing after pool creation ensures the circuit breaker only activates when the handler is fully functional. Based on learnings, this follows the required pattern for external service adapters.
551-647: LGTM!The transient error classification logic is well-designed:
- SQLSTATE-based classification with type-based fallback
- Conservative default to permanent prevents over-tripping
- Logging only includes safe metadata (error_type, sqlstate) without sensitive error messages
This correctly addresses the security concern from past reviews about not exposing sensitive error details.
675-698: LGTM!The circuit breaker integration follows the correct pattern:
- Pre-check before acquiring connection prevents wasting pool resources
- Reset inside connection context (per PR notes) avoids race conditions between coroutines
- Lock is correctly acquired for both check and reset operations
The brief window between check and execute is acceptable—circuit breakers are heuristic protections, not strict guarantees.
1002-1035: LGTM!The security consideration documentation is thorough and provides actionable guidance for external API exposure. The circuit breaker state retrieval correctly checks initialization status before accessing mixin methods.
675-681: No issue found. All circuit breaker mixin methods (_check_circuit_breaker,_record_circuit_failure,_reset_circuit_breaker) are properly declared asasync definMixinAsyncCircuitBreaker. The await expressions in handler_db.py are correct.Likely an incorrect or invalid review comment.
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
| # PostgreSQL SQLSTATE class codes for error classification | ||
| # See: https://www.postgresql.org/docs/current/errcodes-appendix.html | ||
| # | ||
| # TRANSIENT errors (should trip circuit breaker): | ||
| # - Class 08: Connection Exception (database unreachable, connection lost) | ||
| # - Class 53: Insufficient Resources (out of memory, disk full, too many connections) | ||
| # - Class 57: Operator Intervention (admin shutdown, crash recovery, cannot connect now) | ||
| # - Class 58: System Error (I/O error, undefined file, duplicate file) | ||
| # | ||
| # PERMANENT errors (should NOT trip circuit breaker): | ||
| # - Class 23: Integrity Constraint Violation (FK, NOT NULL, unique, check) | ||
| # - Class 42: Syntax Error or Access Rule Violation (bad SQL, undefined table/column) | ||
| # - Class 28: Invalid Authorization Specification (bad credentials) | ||
| # - Class 22: Data Exception (division by zero, string data truncation) | ||
| # - Class 40: Transaction Rollback (serialization failure, deadlock detected) | ||
| # | ||
| # DESIGN DECISION: Classified as PERMANENT despite deadlocks being retry-able. | ||
| # | ||
| # Rationale: | ||
| # 1. Deadlocks indicate transaction contention, not infrastructure failure | ||
| # 2. The database is healthy - it correctly detected and resolved the deadlock | ||
| # 3. Retrying at application level (with backoff) typically succeeds | ||
| # 4. Tripping the circuit would block ALL queries, not just the conflicting ones | ||
| # 5. High deadlock rates indicate application design issues (lock ordering, | ||
| # transaction scope) that should be fixed in code, not masked by circuit breaker | ||
| # | ||
| # Note: If sustained deadlock storms occur, this is a symptom of application | ||
| # issues or schema contention that monitoring/alerting should surface, but | ||
| # the circuit breaker is not the right mitigation tool. | ||
| # | ||
| # The key insight: transient errors indicate the DATABASE INFRASTRUCTURE is unhealthy, | ||
| # while permanent errors indicate the QUERY/APPLICATION is invalid. The circuit breaker | ||
| # protects against infrastructure failures, not application bugs. | ||
| _TRANSIENT_SQLSTATE_CLASSES: frozenset[str] = frozenset({"08", "53", "57", "58"}) | ||
| _PERMANENT_SQLSTATE_CLASSES: frozenset[str] = frozenset({"22", "23", "28", "42"}) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n src/omnibase_infra/handlers/handler_db.py | sed -n '139,200p'Repository: OmniNode-ai/omnibase_infra
Length of output: 4148
🏁 Script executed:
# Check the _is_transient_error method to understand unknown class handling
rg -n "_is_transient_error" src/omnibase_infra/handlers/handler_db.py -A 15Repository: OmniNode-ai/omnibase_infra
Length of output: 3748
🏁 Script executed:
# Verify the full structure of SQLSTATE constants and their usage
rg -n "_TRANSIENT_SQLSTATE_CLASSES|_PERMANENT_SQLSTATE_CLASSES" src/omnibase_infra/handlers/handler_db.py -B 2 -A 2Repository: OmniNode-ai/omnibase_infra
Length of output: 891
🏁 Script executed:
cat -n src/omnibase_infra/handlers/handler_db.py | sed -n '587,640p'Repository: OmniNode-ai/omnibase_infra
Length of output: 2835
Add Class 40 to _PERMANENT_SQLSTATE_CLASSES.
Class 40 (Transaction Rollback / deadlocks) is documented as PERMANENT in the design decision (lines 153-167) but missing from _PERMANENT_SQLSTATE_CLASSES (line 173). This causes Class 40 errors to fall through to the "unknown class" path at line 637, generating unnecessary WARNING logs instead of the intended DEBUG log. Add "40" to the set:
_PERMANENT_SQLSTATE_CLASSES: frozenset[str] = frozenset({"22", "23", "28", "40", "42"})🤖 Prompt for AI Agents
In `@src/omnibase_infra/handlers/handler_db.py` around lines 139 - 173, The
_PERMANENT_SQLSTATE_CLASSES set is missing the "40" transaction-rollback class
referenced in the design notes; update the _PERMANENT_SQLSTATE_CLASSES frozenset
(symbol name: _PERMANENT_SQLSTATE_CLASSES) to include "40" so it becomes
{"22","23","28","40","42"}, ensuring Class 40 errors are treated as permanent
(and hit the DEBUG path rather than the unknown-class WARNING).
| except asyncpg.ForeignKeyViolationError as e: | ||
| # Application error - do NOT trip circuit | ||
| raise RuntimeHostError( | ||
| f"Foreign key constraint violation: {e.message}", context=ctx | ||
| ) from e | ||
| except asyncpg.NotNullViolationError as e: | ||
| # Application error - do NOT trip circuit | ||
| raise RuntimeHostError( | ||
| f"Not null constraint violation: {e.message}", context=ctx | ||
| ) from e | ||
| except asyncpg.UniqueViolationError as e: | ||
| # Application error - do NOT trip circuit | ||
| raise RuntimeHostError( | ||
| f"Unique constraint violation: {e.message}", context=ctx | ||
| ) from e |
There was a problem hiding this comment.
Constraint violation messages may expose sensitive data.
The e.message for constraint violations (FK, NOT NULL, unique) can include the conflicting value, which may contain sensitive user data. For example, a unique violation on an email column would expose the email address.
Consider using the error prefix from _POSTGRES_ERROR_PREFIXES without the message, similar to the generic PostgresError handler at line 767:
🛡️ Suggested change
except asyncpg.ForeignKeyViolationError as e:
# Application error - do NOT trip circuit
raise RuntimeHostError(
- f"Foreign key constraint violation: {e.message}", context=ctx
+ "Foreign key constraint violation", context=ctx
) from e
except asyncpg.NotNullViolationError as e:
# Application error - do NOT trip circuit
raise RuntimeHostError(
- f"Not null constraint violation: {e.message}", context=ctx
+ "Not null constraint violation", context=ctx
) from e
except asyncpg.UniqueViolationError as e:
# Application error - do NOT trip circuit
raise RuntimeHostError(
- f"Unique constraint violation: {e.message}", context=ctx
+ "Unique constraint violation", context=ctx
) from e📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| except asyncpg.ForeignKeyViolationError as e: | |
| # Application error - do NOT trip circuit | |
| raise RuntimeHostError( | |
| f"Foreign key constraint violation: {e.message}", context=ctx | |
| ) from e | |
| except asyncpg.NotNullViolationError as e: | |
| # Application error - do NOT trip circuit | |
| raise RuntimeHostError( | |
| f"Not null constraint violation: {e.message}", context=ctx | |
| ) from e | |
| except asyncpg.UniqueViolationError as e: | |
| # Application error - do NOT trip circuit | |
| raise RuntimeHostError( | |
| f"Unique constraint violation: {e.message}", context=ctx | |
| ) from e | |
| except asyncpg.ForeignKeyViolationError as e: | |
| # Application error - do NOT trip circuit | |
| raise RuntimeHostError( | |
| "Foreign key constraint violation", context=ctx | |
| ) from e | |
| except asyncpg.NotNullViolationError as e: | |
| # Application error - do NOT trip circuit | |
| raise RuntimeHostError( | |
| "Not null constraint violation", context=ctx | |
| ) from e | |
| except asyncpg.UniqueViolationError as e: | |
| # Application error - do NOT trip circuit | |
| raise RuntimeHostError( | |
| "Unique constraint violation", context=ctx | |
| ) from e |
🤖 Prompt for AI Agents
In `@src/omnibase_infra/handlers/handler_db.py` around lines 738 - 752, The
exception handlers for asyncpg.ForeignKeyViolationError,
asyncpg.NotNullViolationError, and asyncpg.UniqueViolationError currently
include e.message which may leak sensitive values; update each RuntimeHostError
raised in these blocks (the handlers referencing RuntimeHostError in
handler_db.py) to use only the safe error prefix (e.g., the corresponding entry
from _POSTGRES_ERROR_PREFIXES or a fixed descriptive string like "Foreign key
constraint violation") instead of including e.message, matching how the generic
PostgresError handler is sanitized.
| except asyncpg.ForeignKeyViolationError as e: | ||
| # Application error - do NOT trip circuit | ||
| raise RuntimeHostError( | ||
| f"Foreign key constraint violation: {e.message}", context=ctx | ||
| ) from e | ||
| except asyncpg.NotNullViolationError as e: | ||
| # Application error - do NOT trip circuit | ||
| raise RuntimeHostError( | ||
| f"Not null constraint violation: {e.message}", context=ctx | ||
| ) from e | ||
| except asyncpg.UniqueViolationError as e: | ||
| # Application error - do NOT trip circuit | ||
| raise RuntimeHostError( | ||
| f"Unique constraint violation: {e.message}", context=ctx | ||
| ) from e |
There was a problem hiding this comment.
Same sensitive data concern in constraint violation messages.
Same issue as in _execute_query — constraint violation messages at lines 861, 866, 871 may expose sensitive values. Apply the same fix to remove e.message from these error strings.
🤖 Prompt for AI Agents
In `@src/omnibase_infra/handlers/handler_db.py` around lines 858 - 872, Replace
the constraint-violation error messages that interpolate sensitive database
values from the asyncpg exceptions in the except handlers
(asyncpg.ForeignKeyViolationError, asyncpg.NotNullViolationError,
asyncpg.UniqueViolationError) by removing e.message from the RuntimeHostError
message strings in handler_db.py; instead use a generic, non-sensitive message
like "Foreign key constraint violation", "Not null constraint violation",
"Unique constraint violation" (preserving context=ctx and raising from e) so the
handlers (RuntimeHostError) still indicate the error type without exposing
sensitive data.
Summary
HandlerDbto provide connection resilienceMixinAsyncCircuitBreakerfollowing established codebase patternsChanges
MixinAsyncCircuitBreaker_execute_query()_execute_statement()describe()shutdown()ModelDbDescribeResponsecircuit_breakerfieldError Handling Design
PostgresConnectionErrorQueryCanceledErrorPostgresSyntaxErrorUndefinedTableErrorUndefinedColumnErrorTest plan
Linear
Closes OMN-780
Summary by CodeRabbit
New Features
Tests
✏️ Tip: You can customize this high-level summary in your review settings.