Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions docusaurus-docs/docs/clients/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,72 @@ transactions conflict when both transactions:

When a transaction is aborted, all its changes are discarded. Transactions can be manually aborted.

#### Abort reasons

A write-write conflict is not the only reason a commit can abort. When Dgraph
aborts a commit it reports a *category* describing why, so clients can decide
how to react (for example, retry immediately versus back off). The category is
carried as a `"<code>: <detail>"` prefix on the gRPC `ABORTED` status message
(and on the HTTP error message), where `<code>` is one of:

- `conflict` — a write-write conflict with another concurrent transaction, as
described above. Retrying the transaction typically succeeds.
- `stale-startts` — the transaction's start timestamp is older than the oldest
timestamp the server can still validate against. That happens after a Zero
leader change, and also when Zero trims its conflict map at a snapshot, which
is not a leader change. Retrying with a fresh transaction succeeds.
- `predicate-move` — a predicate the transaction wrote is being moved between
groups so commits on it are blocked, or it finished moving while the
transaction was open. Retrying once the move completes succeeds.

:::note[Not every abort has a category]
Some aborts carry **no** prefix, and that is deliberate. Where the server cannot
map a cause onto one of the categories above, it sends the explanation without a
code rather than borrowing one that would imply the wrong remedy. Clients report
those as `UNKNOWN`.

That covers aborts from **older servers**, which do not categorize at all, and
also causes a **current** server declines to categorize — for example a
transaction already aborted out of band by a schema change or the
idle-transaction reaper, a cancelled or timed-out request, or a predicate no
group currently serves. `UNKNOWN` therefore does not mean your server is out of
date. The description still explains what happened; only the machine-readable
category is absent.
:::

:::tip[Detect aborts by status code, not message text]
Treat the gRPC `ABORTED` status code (or the HTTP error response) as the signal
that a commit aborted, and the message as detail only. Message text differs per
category and is not a stable contract — it may be reworded or extended between
releases. The official clients all branch on the status code, and an
unrecognized or absent prefix degrades to `UNKNOWN`, so a newer server can add
categories without breaking an older client.
:::

#### What a `conflict` can and cannot tell you

A conflict does not name a predicate or UID, and cannot: conflict keys are
one-way fingerprints by the time the server compares them, so the specific key
is unrecoverable.

This matters when tuning a schema, because the culprit is not necessarily the
data you wrote directly. Writing one triple can write several keys, and any of
them can collide:

- the **data** key you set;
- an **index** key, if the predicate is indexed;
- a **count** key, if the predicate has `@count`.

`@upsert` changes the rule rather than adding a key: the UID is excluded from
the comparison, so **any** two transactions writing the same value conflict, not
just two writing the same node. That is how uniqueness is enforced, and it is a
common source of surprising conflicts in upsert-heavy ingest.

Reverse edges (`@reverse`) never cause conflicts, and predicates marked
`@noconflict` are excluded from conflict detection entirely.

The official [Java](/clients/java) and [Python](/clients/python) clients parse
this category into a typed enum; see those pages for the API.


### In this section
48 changes: 48 additions & 0 deletions docusaurus-docs/docs/clients/java.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,54 @@ client.loginIntoNamespace("groot", "password", 123);

Once logged in, the client can perform all operations allowed for that user in the specified namespace.

## Handling aborted transactions

When a commit aborts, the client throws a `TxnConflictException`. In addition to
the full server message, the exception exposes the [abort reason](/clients#abort-reasons)
as a typed `TxnConflictException.AbortReason`, so you can branch on *why* the
commit aborted:

```java
import io.dgraph.TxnConflictException;
import io.dgraph.TxnConflictException.AbortReason;

Transaction txn = client.newTransaction();
try {
// ... mutations ...
txn.commit();
} catch (TxnConflictException e) {
switch (e.getReason()) {
case CONFLICT:
case STALE_STARTTS:
// Safe to retry with a fresh transaction.
break;
case PREDICATE_MOVE:
// A predicate is being moved between groups; retry after a short backoff.
break;
case UNKNOWN:
// No category reported. Fall back to the message, which still explains
// what happened. Reached against an older server, and also when a
// current server declines to categorize a cause.
break;
}
System.err.println("commit aborted: " + e.getMessage());
} finally {
txn.discard();
}
```

`getReason()` returns `AbortReason.UNKNOWN` whenever the server reports no
category, so the code above works unchanged against older Dgraph servers — and
also against current ones, which deliberately leave some causes uncategorized
rather than implying the wrong remedy (see
[abort reasons](/clients#abort-reasons)). Always keep an `UNKNOWN` branch;
`getMessage()` still carries the explanation. An unrecognized category also
degrades to `UNKNOWN`, so a newer server can add categories without breaking
this code.

`isRetryable()` remains available for code that only needs the retry/no-retry
distinction.

## Documentation

For complete API documentation, examples, and advanced usage:
Expand Down
38 changes: 38 additions & 0 deletions docusaurus-docs/docs/clients/python.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,44 @@ client.login_into_namespace("groot", "password", "123")

Once logged in, the client can perform all operations allowed for that user in the specified namespace.

## Handling aborted transactions

When a commit aborts, the client raises `pydgraph.AbortedError`. In addition to
the full server message, the error exposes the [abort reason](/clients#abort-reasons)
as a typed `pydgraph.AbortReason`, so you can branch on *why* the commit aborted:

```python
import pydgraph

txn = client.txn()
try:
txn.mutate(set_obj={"name": "Alice"})
txn.commit()
except pydgraph.AbortedError as e:
if e.reason in (pydgraph.AbortReason.CONFLICT, pydgraph.AbortReason.STALE_STARTTS):
# Safe to retry with a fresh transaction.
...
elif e.reason == pydgraph.AbortReason.PREDICATE_MOVE:
# A predicate is being moved between groups; retry after a short backoff.
...
else: # pydgraph.AbortReason.UNKNOWN
# No category reported. Fall back to the message, which still explains
# what happened. Reached against an older server, and also when a
# current server declines to categorize a cause.
...
print("commit aborted:", e)
finally:
txn.discard()
```

`AbortedError.reason` is `pydgraph.AbortReason.UNKNOWN` whenever the server
reports no category, so the code above works unchanged against older Dgraph
servers — and also against current ones, which deliberately leave some causes
uncategorized rather than implying the wrong remedy (see
[abort reasons](/clients#abort-reasons)). Always keep the `UNKNOWN` branch; the
message still carries the explanation. An unrecognized category also degrades to
`UNKNOWN`, so a newer server can add categories without breaking this code.

## Documentation

For complete API documentation, examples, and advanced usage:
Expand Down
43 changes: 42 additions & 1 deletion docusaurus-docs/docs/clients/raw-http.md
Original file line number Diff line number Diff line change
Expand Up @@ -399,14 +399,55 @@ If another client were to perform another transaction concurrently affecting the
"errors": [
{
"code": "Error",
"message": "Transaction has been aborted. Please retry."
"message": "rpc error: code = Aborted desc = conflict: Transaction has been aborted. Please retry. Another transaction committed to one of the same keys. The conflicting key cannot be identified: it may be the data key written directly, or an index or count key derived from it. On an @upsert predicate the uid is excluded, so any two transactions writing the same value conflict"
}
]
}
```

In this case, it should be up to the user of the client to decide if they wish to retry the transaction.

The `message` embeds a `"<code>: <detail>"` category describing why the commit
aborted, so a client can react appropriately. The `<code>` is one of:

| Code | Meaning |
| ---------------- | ----------------------------------------------------------------------------------------- |
| `conflict` | Write-write conflict with another concurrent transaction. Retrying typically succeeds. |
| `stale-startts` | The transaction's start timestamp is older than the oldest timestamp the server can still validate against — after a Zero leader change, or when Zero trims its conflict map at a snapshot. Retry with a fresh transaction. |
| `predicate-move` | A predicate the transaction wrote is being moved between groups so commits on it are blocked, or it finished moving mid-transaction. Retry after the move completes. |

For example, a blocked predicate move reports
`predicate-move: Commits on predicate <name> are blocked due to predicate move`.

:::caution[Match the code, do not match the whole message]
The category appears *within* the `message`, not at the start of it: the HTTP
layer renders the underlying gRPC error, so the string also carries an
`rpc error: code = Aborted desc = ` preamble ahead of the category. Search for
the category within the message rather than anchoring to the beginning.

Better still, treat the presence of an error response as the signal that the
commit aborted, and use the category only to decide how to react. Message text
differs per category and is not a stable contract — it may be reworded or
extended between releases, and it is considerably longer than the examples here.
:::

:::note[Not every abort has a category]
Some aborts carry no category, deliberately: where the server cannot map a cause
onto one of the codes above, it sends the explanation alone rather than
borrowing a code that would imply the wrong remedy. Treat a missing or
unrecognized category as *unknown* rather than failing to parse.

This covers aborts from **older servers**, which do not categorize at all, and
causes a **current** server declines to categorize — for example a transaction
already aborted out of band by a schema change or the idle-transaction reaper, a
cancelled or timed-out request, or a predicate no group currently serves. An
uncategorized abort does not mean your server is out of date.
:::

See [abort reasons](/clients#abort-reasons) for what a `conflict` can and cannot
tell you — in particular, why an index, count or `@upsert` key derived from the
data you wrote may be the one that actually collided.

### Abort the Transaction

To abort a transaction, use the same `/commit` endpoint with the `abort=true` parameter while specifying the `startTs` value for the transaction:
Expand Down