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

  1. Retry the transaction with a fresh StartTs — this is the standard recovery for ErrTsTooOld
  2. Shorten transaction lifetimes so they complete before snapshot GC moves the watermark
  3. Check for lagging Dgraph Alpha replicas (watermark lag) and address replication/network issues
  4. 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

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


AI-assisted analysis of dgraph-io/dgraph@759e242be6 (2026-09-01). Data as JSON: /api/errors/10d6c1e6e8a2089f. Report an issue: GitHub.