pingcap / pingcap/tidb

FK cascade savepoint is missing after pessimistic retry

Open
#70,197 1 comment 0 reactions 1 assignee Claimed by @YangKeao View on GitHub
affects-6.5 affects-7.1 affects-7.5 affects-8.1 affects-8.5 severity/major sig/sql-infra type/bug
Dominant language
Go
Stars
40.5k
Forks
6.2k
PR merge metrics
PR metrics pending

Description

## Problem

A foreign-key cascade statement can fail with:

```text

2026-07-29 11:17:11 | [2026/07/29 03:17:11.627 +00:00] [INFO] [conn.go:1202] ["command dispatched failed"] [keyspaceName=10393794543881917049] [conn=3152045258] [session_alias=] [connInfo="id:3152045258, addr:10.0.4.117:54156 status:11, collation:utf8mb4_general_ci, user:tsx_user"] [command=Execute] [status="inTxn:1, autocommit:1"] [sql="update `zzz` . `xxxx` set `id` = ? , `level` = ? , `title` = ? , `message` = ? , `content` = ? , `rewarditempackageid` = ? , `createdat` = ? , `expiresat` = ? , `gifttype` = ? where ( `zzz` . `xxxx` . `id` in ( ? ) and ( `tsx` . `gift` . `id` = ? and ? = ? ) )"] [txn_mode=PESSIMISTIC] [timestamp=468004380714467334] [err="handle foreign key trigger error failed, err: foreign key cascade savepoint 'fk_sp_468004380714467334' not found, transaction is rollback, should never happen, original_err: [kv:9007]Write conflict, txnStartTS=468004380714467334, conflictStartTS=468004380740681739, conflictCommitTS=468004380753789232, key=????, reason=PessimisticRetry [try again later]\...]
-- | --

```

after a retryable pessimistic write conflict.

## Root cause

TiDB creates an internal savepoint for statements with FK cascades:

```go
func (a *ExecStmt) prepareFKCascadeContext(e exec.Executor) {
// ...
savepointName := "fk_sp_" + strconv.FormatUint(txn.StartTS(), 10)
sessVar.TxnCtx.AddSavepoint(savepointName, memDBCheckpoint)
sessVar.StmtCtx.ForeignKeyTriggerCtx.SavepointName = savepointName
}
```

If an FK trigger fails, TiDB rolls back to and releases that savepoint:

```go
savepointRecord := TxnCtx.RollbackToSavepoint(
sc.ForeignKeyTriggerCtx.SavepointName,
)
txn.RollbackMemDBToCheckpoint(savepointRecord.MemDBCheckpoint)
TxnCtx.ReleaseSavepoint(sc.ForeignKeyTriggerCtx.SavepointName)
```

However, after a retryable pessimistic write conflict, `handlePessimisticLockError` rebuilds and reopens the executor without recreating the FK savepoint:

```go
e, err := a.buildExecutor()

a.Ctx.StmtRollback(ctx, true)
a.Ctx.GetSessionVars().StmtCtx.ResetForRetry()

a.openExecutor(ctx, e)
```

The retry path does not call:

```go
a.prepareFKCascadeContext(e)
```

`StatementContext.ResetForRetry()` also does not clear `ForeignKeyTriggerCtx.SavepointName`. Therefore, a later FK trigger error can attempt to roll back to a stale, already released `fk_sp_`, causing the unexpected `savepoint not found` error and rolling back the whole transaction.

## Expected behavior

Each pessimistic retry should have a valid FK rollback context, or stale FK savepoint state should be cleared before retrying. A retryable write conflict must not result in `foreign key cascade savepoint ... not found`.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.