Skip to content

feat(txn): surface transaction abort reason on AbortedError - #288

Open
matthewmcneely wants to merge 3 commits into
mainfrom
matthewmcneely/abort-reason
Open

matthewmcneely wants to merge 3 commits into
mainfrom
matthewmcneely/abort-reason

Conversation

@matthewmcneely

@matthewmcneely matthewmcneely commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

Description

This PR surfaces the server's transaction abort reason in dgo, matching what dgraph-io/dgraph4j#290 and dgraph-io/pydgraph#307 do for the Java and Python clients.

dgraph-io/dgraph#9747 has Zero report why it aborted a transaction as a "<code>: <detail>" prefix on the gRPC Aborted status (conflict, predicate-move, or stale-startts). Today Txn flattens every Aborted status to the bare dgo.ErrAborted sentinel, so callers can't tell a write-write conflict (retry now) from a predicate move (back off first).

Changes

  • New AbortReason type with AbortReasonConflict, AbortReasonPredicateMove, AbortReasonStaleStartTs, and AbortReasonUnknown.
  • New *AbortedError{Reason, Message}, returned by Txn.Commit and by the mutation paths of Txn.Mutate/Txn.Do in place of ErrAborted. Error() returns the server's message verbatim, same as getMessage() in dgraph4j and str(e) in pydgraph.
  • AbortedError implements GRPCStatus(), so status.Code(err) reports codes.Aborted instead of codes.Unknown.
  • ParseAbortReason(msg) is exported for callers using the raw api.DgraphClient. Parsing follows the other two clients: text before the first colon, trimmed and case-insensitive. Anything unrecognized maps to AbortReasonUnknown, which covers every message from a server without #9747 as well as causes the server deliberately leaves uncategorized.
  • README: the commit example now uses errors.Is (it also had a stray y.ErrAborted typo), plus a short errors.As example.
var ae *dgo.AbortedError
if errors.As(err, &ae) && ae.Reason == dgo.AbortReasonPredicateMove {
  // back off before retrying
}

Compatibility

AbortedError unwraps to ErrAborted, so errors.Is(err, dgo.ErrAborted) keeps working, and every abort message from the server still contains "Transaction has been aborted. Please retry", so substring checks on err.Error() keep matching too. A direct err == dgo.ErrAborted comparison no longer matches. The Java and Python clients could hang the reason off their existing exception type, but Go can't attach data to a sentinel value, so this is the trade. It should get a call-out in the release notes.

The dgraph repo had a handful of == dgo.ErrAborted comparisons in the live loader and systests. Those are switched to errors.Is in #9747, so bumping dgraph to this dgo is safe.

Testing

  • abort_test.go: unit tests for prefix parsing (including colons inside the detail, unknown categories, and pre-#9747 messages) and for errors.Is/errors.As through a wrapped error.
  • TestTxnErrAborted now actually checks the error. The old require.Error(t, err, dgo.ErrAborted, ...) only checked for non-nil, since the sentinel landed in msgAndArgs. It asserts ErrorIs, ErrorAs, and a reason of conflict or unknown, so it passes against dgraph main before and after #9747 merges.
  • Ran the full suite against a local cluster built from the #9747 branch; the conflict test reports AbortReasonConflict with the server's full conflict message.

Checklist

Dgraph now reports why it aborted a transaction as a "<code>: <detail>"
prefix on the gRPC Aborted status (dgraph-io/dgraph#9747). Txn flattened
every Aborted status to the bare ErrAborted sentinel, so that reason never
reached the caller.

Txn.Commit and the mutation paths of Txn.Do/Mutate now return an
*AbortedError carrying the parsed Reason and the server's message. It
unwraps to ErrAborted, so errors.Is(err, dgo.ErrAborted) keeps working;
a direct == comparison against ErrAborted no longer matches.

Parsing matches dgraph4j#290 and pydgraph#307: the text before the first
colon, trimmed and case-insensitive, mapped onto conflict, predicate-move
or stale-startts, with anything else (including every message from an
older server) reported as AbortReasonUnknown.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: f783b184-2db5-4c2f-b3a6-8af9994276c0

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

status.Code(err) on an *AbortedError returned codes.Unknown, so code that
classifies errors by gRPC status (the dgraph live loader, for one) didn't
see an abort. GRPCStatus reports the Aborted status with the server's
message.

Test fixtures now use the server's final framing, where every abort
message carries "Transaction has been aborted. Please retry" after the
category.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
matthewmcneely added a commit to rahst12/dgraph that referenced this pull request Oct 22, 2026
dgraph-io/dgo#288 makes Txn return an *AbortedError that wraps
ErrAborted, carrying the server's abort reason. A direct == comparison
against the sentinel stops matching once dgraph moves to that dgo, so the
live loader would print aborts as "Error while mutating" and the
mutations systests would treat an abort as a failure.

errors.Is matches both the current sentinel and the wrapped error, so
this is safe ahead of the dgo bump. The edgraph comparisons are left as
is: they check worker.CommitOverNetwork's return, not a dgo client error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
matthewmcneely added a commit to rahst12/dgraph that referenced this pull request Oct 22, 2026
- Abort details from checkPreds name the predicate as the user wrote it.
  Preds entries carry the namespaced attribute, so clients were about to
  receive "0-name" in a wire message that has never been published
  before. displayPred strips the namespace with a guarded strings.Cut,
  since Preds is client-supplied and x.ParseAttr would panic on an entry
  without a separator.
- CommitOverNetwork emits its "Commit status" span event for
  server-decided aborts too, now with the reason, so the aborts most
  worth tracing don't go dark.
- The pre-aborted detail lists a concurrent client Discard as a fourth
  cause and stops reading as exhaustive. proposeTxn's race comment no
  longer claims a predicate move aborts pending transactions: a move
  blocks commits in checkPreds, and the move path waits with WaitForTs
  rather than calling TryAbort.
- Comments that described dgraph4j and pydgraph behavior in the present
  tense now name the open client PRs, including dgraph-io/dgo#288.
- TestStaleStartTsAbortReason polls the commit itself until it returns
  codes.Aborted. A Zero leader existing doesn't mean the Alpha has a
  connection to it yet, and in that window the commit fails with
  codes.Unknown.
- Copyright headers on the new test files use 2017-2026.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
matthewmcneely added a commit to rahst12/dgraph that referenced this pull request Oct 22, 2026
Abort messages can now name a predicate, so a predicate-move abort on a
predicate called e.g. x509cert would hit the x509 case in handleError and
make the live loader exit. The abort check now comes first and also
matches dgo.ErrAborted via errors.Is, since dgo hands the loader that
rather than the raw status.

TestTxnDiscardBeforeCommit asserted NotEqual(err, dgo.ErrAborted), which
passes vacuously once dgo returns a wrapping error; it now requires no
error. Test comments no longer state that dgo always discards the abort
reason, since dgraph-io/dgo#288 keeps it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
staticcheck ST1023 flagged the explicit error type on a fmt.Errorf result.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant