FK cascade savepoint is missing after pessimistic retry
- 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
Assessment
This issue has not been assessed yet.