{"record":{"id":"10d6c1e6e8a2089f","repo":"dgraph-io/dgraph","slug":"adding-reverse-mutation-helper-count","errorCode":null,"errorMessage":"Adding reverse mutation helper count","messagePattern":"Adding reverse mutation helper count","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"posting/index.go","lineNumber":251,"sourceCode":"\t\t\tedge.Op = pb.DirectedEdge_OVR\n\t\t}\n\t\treturn true\n\t} else {\n\t\treturn edge.Op != pb.DirectedEdge_DEL\n\t}\n}\n\nfunc (txn *Txn) addReverseMutationHelper(ctx context.Context, plist *List,\n\thasCountIndex bool, edge *pb.DirectedEdge) (countParams, error) {\n\tcountBefore, countAfter := 0, 0\n\tfound := false\n\n\tplist.Lock()\n\tdefer plist.Unlock()\n\tif hasCountIndex {\n\t\tcountBefore, found, _ = plist.getPostingAndLengthNoSort(txn.StartTs, 0, edge.ValueId)\n\t\tif countBefore < 0 {\n\t\t\treturn emptyCountParams, errors.Wrapf(ErrTsTooOld, \"Adding reverse mutation helper count\")\n\t\t}\n\t}\n\n\tif !(hasCountIndex && !shouldAddCountEdge(found, edge)) {\n\t\tif err := plist.addMutationInternal(ctx, txn, edge); err != nil {\n\t\t\treturn emptyCountParams, err\n\t\t}\n\t}\n\n\tif hasCountIndex {\n\t\tpk, _ := x.Parse(plist.key)\n\t\tshouldCountOneUid := !schema.State().IsList(edge.Attr) && !pk.IsReverse()\n\t\tcountAfter = countAfterMutation(countBefore, found, edge.Op, shouldCountOneUid)\n\t\treturn countParams{\n\t\t\tattr:        edge.Attr,\n\t\t\tcountBefore: countBefore,\n\t\t\tcountAfter:  countAfter,\n\t\t\tentity:      edge.Entity,","sourceCodeStart":233,"sourceCodeEnd":269,"githubUrl":"https://github.com/dgraph-io/dgraph/blob/759e242be62c91f8d084da06ad0c8d21256d9c07/posting/index.go#L233-L269","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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"],"exampleFix":"// before: long txn reused across many operations until it errored\ntxn := dgo.NewTxn(); ...hours later... txn.Mutate(...) // ErrTsTooOld\n// after: fresh txn per logical unit, retry on ErrTsTooOld\nfor {\n    txn := d.NewTxn()\n    _, err := txn.Mutate(ctx, mu)\n    if errors.Is(err, geom.ErrTsTooOld) { txn.Discard(ctx); continue }\n    break\n}","handlingStrategy":"retry","validationCode":"// Ensure txn is fresh: readTs should be recent, not from a long-lived txn\nif time.Since(txnStartedAt) > maxTxnLifetime {\n    return fmt.Errorf(\"transaction too old; discard and restart before mutating\")\n}","typeGuard":null,"tryCatchPattern":"for attempt := 0; attempt < 3; attempt++ {\n    txn := d.NewTxn()\n    _, err := txn.Mutate(ctx, mu)\n    if err != nil && strings.Contains(err.Error(), \"Adding reverse mutation helper count\") || xerr.IsErrTsTooOld(err) {\n        txn.Discard(ctx)\n        continue // retry with fresh StartTs\n    }\n    return err\n}","preventionTips":["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"],"tags":["reverse-index","count-index","timestamp","too-old","transaction"],"backgroundTag":"ts-too-old-snapshot-gc","analyzedSha":"759e242be62c91f8d084da06ad0c8d21256d9c07","analyzedAt":"2026-09-01T14:42:12.034Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}