fix(#5043): preserve engine exception types across gRPC (Status mapping + class-name trailer)#5184
Conversation
…des + class-name trailer Route commit/begin/createRecord failures through a central GrpcErrorMapper that maps each engine exception to the correct io.grpc.Status code and carries the exception class name (plus duplicate-key index/keys) in a metadata trailer. The client GrpcClientErrorMapper reconstructs the exact type from the trailer, falling back to status-code mapping for older servers. - COR-3: commitTransaction no longer blanket-maps to ABORTED; a commit-time DuplicatedKeyException becomes ALREADY_EXISTS (non-retryable) while ConcurrentModificationException/NeedRetryException stay ABORTED (retryable), so RemoteDatabase.transaction() retry behaves as over HTTP. - COR-2: DuplicatedKeyException -> ALREADY_EXISTS with real index name + keys. - TX-9: beginTransaction passes an already-mapped security StatusRuntimeException through instead of masking it as INTERNAL. - COR-15: executeQuery with an unknown transaction id returns FAILED_PRECONDITION, not INTERNAL.
|
Tick the box to add this pull request to the merge queue (same as
|
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 46 |
🟢 Coverage 56.76% diff coverage · -35.67% coverage variation
Metric Results Coverage variation ✅ -35.67% coverage variation Diff coverage ✅ 56.76% diff coverage Coverage variation details
Coverable lines Covered lines Coverage Common ancestor commit (656373c) 137165 102881 75.01% Head commit (1ccd166) 169099 (+31934) 66516 (-36365) 39.34% (-35.67%) Coverage variation is the difference between the coverage for the head and common ancestor commits of the pull request branch:
<coverage of head commit> - <coverage of common ancestor commit>Diff coverage details
Coverable lines Covered lines Diff coverage Pull request (#5184) 74 42 56.76% Diff coverage is the percentage of lines that are covered by tests out of the coverable lines that the pull request added or modified:
<covered lines added or modified>/<coverable lines added or modified> * 100%
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
|
Warning Gemini encountered an error creating the review. You can try again by commenting |
Review: preserve engine exception types across gRPC (#5184)Nice, focused fix. Mapping engine exceptions to the right io.grpc.Status and carrying the FQN class name in a trailer is the correct approach, mirrors the HTTP protocol, and the trailer-based fallback keeps it backward compatible without a proto change. Test coverage is solid on both sides (server mapping + client reconstruction, including the unwrap and no-trailer fallback cases). The deliberate deferral of COR-1/SEC-6 with rationale is well documented. A few things worth addressing: 1. Fallback for ALREADY_EXISTS without a trailer drops the server message (minor backward-compat regression) - In the legacy/trailer-less path, 2. Duplicate-key trailers use the ASCII marshaller but carry arbitrary user data - DUP_INDEX_KEY / DUP_KEYS_KEY are ASCII_STRING_MARSHALLER, but dup.getKeys() (and to a lesser extent the index name) can contain arbitrary key values: unicode (accented names, non-Latin scripts, emoji in an indexed string field), control characters, etc. gRPC ASCII metadata values are restricted to the printable range (0x20-0x7E); values outside it get lossily coerced or can be rejected by the transport. The status description is safe (gRPC percent-encodes grpc-message), but these custom trailers are not. Consider base64-encoding the keys/index value (or using a -bin binary metadata key) so non-ASCII keys survive the wire intact and cannot break the trailer. 3. Unused import - RemoteGrpcDatabase.java:27 Minor / non-blocking
Overall: correct direction and good tests. Items 1 and 2 are the ones I would fix before merge; 3 is trivial cleanup. |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #5184 +/- ##
=============================================
- Coverage 66.02% 36.99% -29.03%
- Complexity 915 1210 +295
=============================================
Files 1694 1696 +2
Lines 137165 137254 +89
Branches 29328 29354 +26
=============================================
- Hits 90558 50780 -39778
- Misses 34324 77005 +42681
+ Partials 12283 9469 -2814 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
… ALREADY_EXISTS, drop unused import
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request addresses issue #5043 by preserving ArcadeDB engine exception types across gRPC calls. It introduces GrpcErrorMapper on the server side to map exceptions to gRPC statuses and attach the fully-qualified exception class name (and duplicate key details, Base64-encoded) in metadata trailers. On the client side, GrpcClientErrorMapper reconstructs the exact exception type from these trailers, falling back to status-code-only mapping for older servers. This ensures proper exception propagation (such as retrying on NeedRetryException and handling DuplicatedKeyException properly) over gRPC. Unit tests have been added to verify the mapping and reconstruction. No review comments were provided, so there is no feedback to address.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
Review: preserve engine exception types across gRPC (#5184 / closes #5043)Overall this is a clean, well-scoped fix with genuinely good engineering: a single server-side mapper, a symmetric client-side reconstructor, backward compatibility with trailer-less servers, Base64 for non-ASCII index/keys, pass-through of already-mapped statuses, and thoughtful Javadoc. The retryable/non-retryable semantics ( A few points worth considering: 1. Test coverage: no end-to-end round-trip test (moderate)The two mapper sides are each unit-tested in isolation, but nothing exercises the actual wire path: server 2.
|
…ment SecurityException pre-mapping reliance
Review of PR #5184 - preserve engine exception types across gRPCThorough, well-scoped fix. The server/client mapper pair is symmetric, backward-compatible (trailer-less fallback), and the test coverage (server unit, client unit, and a full wire round-trip) is genuinely good. The docs file and the honest "deferred findings" section make the scope easy to reason about. A few observations, mostly minor. Correctness / robustness
Test quality
Security
Nits
Overall: correct, minimal, well-tested, and it delivers the stated fix (retry-on- Automated review by Claude. Verified the exception hierarchy ( |
…als to class names
|
Review: PR #5184 - preserve engine exception types across gRPC Overall this is a clean, well-scoped fix. The central GrpcErrorMapper (server) / GrpcClientErrorMapper (client) pair mirrors what the HTTP protocol already does, the Base64 trailer handling is thoughtful, backward compatibility (trailer-less older servers fall back to status-code mapping) is handled on both sides, and the test coverage is excellent (unit tests for each mapper side in isolation plus a real server-backed round-trip). Nice work. A few observations, none blocking: 1. Unused import (nit) - grpcw/.../GrpcErrorMapper.java:21 2. The "non-retryable duplicate key" framing is slightly imprecise 3. Trailer-less ALREADY_EXISTS fallback produces a doubly-nested message 4. Security: message pass-through can leak internal detail 5. Round-trip test hardcodes port 50051 Positives worth calling out
Note: I reviewed statically and did not run the module build myself; the PR reports both grpcw and grpc-client suites green. |
Review: fix(#5043) preserve engine exception types across gRPCSolid, well-scoped fix. The central-mapper approach mirrors what HTTP already does, the trailer channel is the idiomatic gRPC way to carry error detail (no proto change), backward compatibility is handled cleanly (class-name trailer + status-code fallback), and the test coverage is genuinely good: unit tests for both mapper sides, a class-name-literal sync guard, non-ASCII round-trips, and a real server-backed wire test. Nicely documented in A few observations, none blocking: 1. "Non-retryable" framing for
|
Closes #5043
Summary
The gRPC layer collapsed distinct engine exceptions into a coarse
INTERNAL/ABORTEDstatus (or an in-bandsuccess=falsemessage), so the client could not reconstruct the original exception type. As a resultRemoteDatabase.transaction()'s automatic retry-on-NeedRetryExceptionworked over HTTP but was silently inert over gRPC, and permanent conflicts (a commit-time duplicate key) were mis-typed as retryable.This PR adopts one consistent server-side mapping and preserves the exception type across the wire the way the HTTP protocol already does. A new
GrpcErrorMapper(server) maps each engine exception to the correctio.grpc.Statuscode and attaches the fully-qualified exception class name (plus duplicate-key index/keys) in a metadata trailer. A newGrpcClientErrorMapper(client) reconstructs the exact type from that trailer, falling back to the legacy status-code mapping when the trailer is absent (backward compatible with older servers). No proto change is needed - trailers are the standard gRPC error-detail channel.Findings addressed
commitTransactionno longer blanket-maps every commit failure toABORTED. A commit-timeDuplicatedKeyExceptionbecomesALREADY_EXISTS(non-retryable) while genuineConcurrentModificationException/NeedRetryExceptionstayABORTED(retryable), sotransaction()retry fires over gRPC exactly as over HTTP.DuplicatedKeyExceptionmaps toALREADY_EXISTScarrying the real index name + keys; the client rebuilds a properDuplicatedKeyExceptionwith correctindexName/keysinstead ofDuplicatedKeyException(msg, msg, null).beginTransactionpasses an already-mappedUNAUTHENTICATED/PERMISSION_DENIEDStatusRuntimeException(fromgetDatabase) straight through instead of wrapping it asINTERNAL.executeQuerywith an unknown transaction id returnsFAILED_PRECONDITION, notINTERNAL.Deliberately deferred (follow-ups)
executeCommandreturning in-bandsuccess=false): flipping it toonErrorwould break existing tests that assert this contract (Issue4794GrpcPerDbAuthorizationIT,Issue5040GrpcTransactionHijackIT,Issue4804GrpcCommandErrorMetricsTest); the "never modify existing tests" rule makes this a separate coordinated change.Test plan
GrpcErrorMapperTest(grpcw, unit) - each engine exception maps to the rightStatus.Codeand trailers; already-mapped statuses pass through;ExecutionExceptionis unwrapped. 10/10 pass.Issue5043GrpcErrorTypeReconstructionTest(grpc-client, unit) - client rebuilds the exact type from the trailer: CME stays retryable (NeedRetryException), duplicate-key is reconstructed with correct index/keys and is NOT retryable, unknown class / no trailer falls back to status-code mapping. 9/9 pass.Issue4533ConcurrentModificationTest(commit CME ->ConcurrentModificationException).ArcadeDbGrpcServiceExtendedTesterrored only on a fixed-port50051bind collision from concurrent local runs, not on any assertion).Note: backward compatible - a server without the trailer still maps by status code, and a client without this change still reads the status code.