@@ -18,6 +18,8 @@ import (
1818 "go.opentelemetry.io/otel"
1919 attribute "go.opentelemetry.io/otel/attribute"
2020 trace "go.opentelemetry.io/otel/trace"
21+ "google.golang.org/grpc/codes"
22+ "google.golang.org/grpc/status"
2123
2224 "github.com/dgraph-io/badger/v4/y"
2325 "github.com/dgraph-io/dgo/v250/protos/api"
@@ -338,21 +340,56 @@ func (s *Server) proposeTxn(ctx context.Context, src *api.TxnContext) error {
338340 return nil
339341}
340342
343+ // Abort-reason codes. When Zero decides to abort a transaction it surfaces the category to the
344+ // client as the prefix of a codes.Aborted gRPC status message, formatted as "<code>: <detail>".
345+ // api.TxnContext (external dgo module) has no field for the reason, so it rides on the error;
346+ // the status code stays codes.Aborted, so existing abort handling is unaffected. The dgraph4j
347+ // client parses these prefixes into TxnConflictException.AbortReason — keep the two in sync.
348+ const (
349+ abortReasonConflict = "conflict"
350+ abortReasonStaleStartTs = "stale-startts"
351+ abortReasonPredicateMove = "predicate-move"
352+ )
353+
354+ // abortReason builds the wire string the client parses: "<code>: <detail>".
355+ func abortReason (code , detail string ) string {
356+ return code + ": " + detail
357+ }
358+
341359func (s * Server ) commit (ctx context.Context , src * api.TxnContext ) error {
342360 span := trace .SpanFromContext (ctx )
343361 span .SetAttributes (attribute .Int64 ("startTs" , int64 (src .StartTs )))
344362 if src .Aborted {
363+ // Client-initiated discard (txn.Discard); not a server-decided abort, so no reason.
345364 return s .proposeTxn (ctx , src )
346365 }
347366
367+ // abortWithReason marks the txn aborted, proposes it (advancing the watermark exactly as the
368+ // previous code did), and then surfaces the categorized reason to the caller as a gRPC
369+ // Aborted status. proposeTxn still runs identically — only the post-propose return changes.
370+ abortWithReason := func (reason string ) error {
371+ span .SetAttributes (attribute .Bool ("abort" , true ))
372+ src .Aborted = true
373+ if err := s .proposeTxn (ctx , src ); err != nil {
374+ return err
375+ }
376+ return status .Error (codes .Aborted , reason )
377+ }
378+
348379 // Use the start timestamp to check if we have a conflict, before we need to assign a commit ts.
349380 s .orc .RLock ()
350381 conflict := s .orc .hasConflict (src )
382+ // A txn whose startTs predates this leader's lease is aborted by hasConflict, but it's a
383+ // leader change rather than a write-write conflict — report it distinctly.
384+ stale := src .StartTs < s .orc .startTxnTs
351385 s .orc .RUnlock ()
352386 if conflict {
353- span .SetAttributes (attribute .Bool ("abort" , true ))
354- src .Aborted = true
355- return s .proposeTxn (ctx , src )
387+ if stale {
388+ return abortWithReason (abortReason (abortReasonStaleStartTs ,
389+ "Transaction has been aborted due to a leader change. Please retry" ))
390+ }
391+ return abortWithReason (abortReason (abortReasonConflict ,
392+ "Transaction has been aborted. Please retry" ))
356393 }
357394
358395 checkPreds := func () error {
@@ -385,9 +422,9 @@ func (s *Server) commit(ctx context.Context, src *api.TxnContext) error {
385422 return nil
386423 }
387424 if err := checkPreds (); err != nil {
388- span . SetAttributes ( attribute . Bool ( "abort" , true ))
389- src . Aborted = true
390- return s . proposeTxn ( ctx , src )
425+ // checkPreds builds rich messages, e.g. "Commits on predicate %s are blocked due to
426+ // predicate move" — forward them instead of swallowing the reason.
427+ return abortWithReason ( abortReason ( abortReasonPredicateMove , err . Error ()) )
391428 }
392429
393430 num := pb.Num {Val : 1 , Type : pb .Num_TXN_TS }
@@ -402,16 +439,27 @@ func (s *Server) commit(ctx context.Context, src *api.TxnContext) error {
402439 span .SetAttributes (attribute .Int64 ("nodeId" , int64 (s .Node .Id )))
403440 span .AddEvent (fmt .Sprintf ("TXN Context: %+v" , src ))
404441
442+ aborted := false
405443 if err := s .orc .commit (src ); err != nil {
406444 span .SetAttributes (attribute .Bool ("abort" , true ))
407445 src .Aborted = true
446+ aborted = true
408447 }
409448 if err := ctx .Err (); err != nil {
410449 span .SetAttributes (attribute .Bool ("abort" , true ))
411450 src .Aborted = true
451+ aborted = true
412452 }
413453 // Propose txn should be used to set watermark as done.
414- return s .proposeTxn (ctx , src )
454+ if err := s .proposeTxn (ctx , src ); err != nil {
455+ return err
456+ }
457+ if aborted {
458+ // A late write-write conflict detected at commit time (keyCommit), or a cancelled ctx.
459+ return status .Error (codes .Aborted , abortReason (abortReasonConflict ,
460+ "Transaction has been aborted. Please retry" ))
461+ }
462+ return nil
415463}
416464
417465// CommitOrAbort either commits a transaction or aborts it.
0 commit comments