dgraph-io/dgraph · error
Adding reverse mutation helper count
Error message
Adding reverse mutation helper count
What it means
addReverseMutationHelper reads the current count posting to maintain the count index for reverse edges. When countBefore < 0, the posting list could not be read at the transaction's start timestamp — typically because the snapshot was garbage-collected and the read is older than the watermark (ErrTsTooOld). The error is wrapped as 'Adding reverse mutation helper count' to add context about the reverse/count-index mutation path.
Source
Thrown at posting/index.go:251
edge.Op = pb.DirectedEdge_OVR
}
return true
} else {
return edge.Op != pb.DirectedEdge_DEL
}
}
func (txn *Txn) addReverseMutationHelper(ctx context.Context, plist *List,
hasCountIndex bool, edge *pb.DirectedEdge) (countParams, error) {
countBefore, countAfter := 0, 0
found := false
plist.Lock()
defer plist.Unlock()
if hasCountIndex {
countBefore, found, _ = plist.getPostingAndLengthNoSort(txn.StartTs, 0, edge.ValueId)
if countBefore < 0 {
return emptyCountParams, errors.Wrapf(ErrTsTooOld, "Adding reverse mutation helper count")
}
}
if !(hasCountIndex && !shouldAddCountEdge(found, edge)) {
if err := plist.addMutationInternal(ctx, txn, edge); err != nil {
return emptyCountParams, err
}
}
if hasCountIndex {
pk, _ := x.Parse(plist.key)
shouldCountOneUid := !schema.State().IsList(edge.Attr) && !pk.IsReverse()
countAfter = countAfterMutation(countBefore, found, edge.Op, shouldCountOneUid)
return countParams{
attr: edge.Attr,
countBefore: countBefore,
countAfter: countAfter,
entity: edge.Entity,View on GitHub (pinned to 759e242be6)
Solutions
- Retry the transaction with a fresh StartTs — this is the standard recovery for ErrTsTooOld
- Shorten transaction lifetimes so they complete before snapshot GC moves the watermark
- Check for lagging Dgraph Alpha replicas (watermark lag) and address replication/network issues
- Reduce GC pressure (snapshot/memory settings) if transactions routinely become too old
Example fix
// before: long txn reused across many operations until it errored
txn := dgo.NewTxn(); ...hours later... txn.Mutate(...) // ErrTsTooOld
// after: fresh txn per logical unit, retry on ErrTsTooOld
for {
txn := d.NewTxn()
_, err := txn.Mutate(ctx, mu)
if errors.Is(err, geom.ErrTsTooOld) { txn.Discard(ctx); continue }
break
} Defensive patterns
Strategy: retry
Validate before calling
// Ensure txn is fresh: readTs should be recent, not from a long-lived txn
if time.Since(txnStartedAt) > maxTxnLifetime {
return fmt.Errorf("transaction too old; discard and restart before mutating")
} Try / catch
for attempt := 0; attempt < 3; attempt++ {
txn := d.NewTxn()
_, err := txn.Mutate(ctx, mu)
if err != nil && strings.Contains(err.Error(), "Adding reverse mutation helper count") || xerr.IsErrTsTooOld(err) {
txn.Discard(ctx)
continue // retry with fresh StartTs
}
return err
} Prevention
- Keep transactions short-lived; never hold a txn open across long computations
- Implement retry-on-ErrTsTooOld at the transaction layer
- Monitor Alpha watermarks and replica lag; restart or drain lagging Alphas
- Tune snapshot GC settings if workloads routinely produce ts-too-old reads
When it happens
Trigger: A transaction with an old StartTs performs a reverse/count-indexed mutation and plist.getPostingAndLengthNoSort returns a negative count because the posting list data at that timestamp was GC'd (readTs below the service watermark).
Common situations: Long-running or stalled transactions that started before a snapshot GC cycle; lagging followers serving stale reads; heavy write load causing the write watermark to advance past the transaction's read point.
Related errors
- while leasing txn timestamp. Id: %+v
- Transaction is too old
- count length < 4 for key: %q, parsed key: %+v
AI-assisted analysis of dgraph-io/dgraph@759e242be6 (2026-09-01).
Data as JSON: /api/errors/10d6c1e6e8a2089f.
Report an issue: GitHub.