diff --git a/docusaurus-docs/docs/clients/index.md b/docusaurus-docs/docs/clients/index.md index d024e93d..590bbf39 100644 --- a/docusaurus-docs/docs/clients/index.md +++ b/docusaurus-docs/docs/clients/index.md @@ -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 `": "` prefix on the gRPC `ABORTED` status message +(and on the HTTP error message), where `` 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 diff --git a/docusaurus-docs/docs/clients/java.md b/docusaurus-docs/docs/clients/java.md index 730d8db4..ab737315 100644 --- a/docusaurus-docs/docs/clients/java.md +++ b/docusaurus-docs/docs/clients/java.md @@ -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: diff --git a/docusaurus-docs/docs/clients/python.md b/docusaurus-docs/docs/clients/python.md index 44b5fee9..e27320ef 100644 --- a/docusaurus-docs/docs/clients/python.md +++ b/docusaurus-docs/docs/clients/python.md @@ -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: diff --git a/docusaurus-docs/docs/clients/raw-http.md b/docusaurus-docs/docs/clients/raw-http.md index ecd4f392..06b85078 100644 --- a/docusaurus-docs/docs/clients/raw-http.md +++ b/docusaurus-docs/docs/clients/raw-http.md @@ -399,7 +399,7 @@ 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" } ] } @@ -407,6 +407,47 @@ If another client were to perform another transaction concurrently affecting the 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 `": "` category describing why the commit +aborted, so a client can react appropriately. The `` 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 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: