pingcap / pingcap/tidb

[gc] FLASHBACK DATABASE can report success after an in-flight GC deletes the recovered table ranges

Open
#70,108 0 comments 0 reactions 0 assignees View on GitHub
component/GC found-by-ai may-affects-7.5 may-affects-8.1 may-affects-8.5 severity/critical type/bug
Dominant language
Go
Stars
40.5k
Forks
6.2k
PR merge metrics
PR metrics pending

Description

## Bug Report

### 1. Minimal reproduce step (Required)

**Environment**

TiDB master 05b396fb66; behavior introduced by #14403 and still present on upstream master

**Steps**

On one TiDB, one PD, and one real TiKV with MDL enabled, create a database containing one 64-row table with a unique index and drop it. Start a due GC prepare and pause it after reading tidb_gc_enable=ON. Start FLASHBACK DATABASE and pause after its old safe-point check, before delete-range removal. Resume prepare and run the full production GC job, including the 100-second synchronization wait. GC loads and deletes five ranges. Resume flashback. It reports public/synced success, but a fresh session reads zero rows. Scheduler callbacks only select the overlap and target; they inject no error.

### 2. What did you expect to see? (Required)

GC prepare must serialize with the OFF write. Recovery must either remove its delete-range tasks before GC can load them, or observe the newer safe point and fail before publishing the database.

### 3. What did you see instead? (Required)

GC deletes five eligible ranges and moves their tasks to done. FLASHBACK DATABASE still reports success and publishes the table, but all 64 rows and the unique-index entries are gone. ADMIN CHECK TABLE passes because both keyspaces are consistently empty.

### 4. What is your TiDB version? (Required)

TiDB master 05b396fb66; behavior introduced by #14403 and still present on upstream master

Likely root cause and fix direction

**Likely root cause**

prepare starts a transaction on session A, but checkPrepare reads and writes mysql.tidb through fresh sessions B..N. SELECT FOR UPDATE therefore locks and commits outside A. A 2020 refactor that removed GCWorker.session split the original transaction boundary. Even routing all operations through A is insufficient with plain BEGIN because the internal transaction is optimistic and does not wait on the row lock.

**Fix direction**

Run prepare in BEGIN PESSIMISTIC and use the outer session for the enable SELECT FOR UPDATE. Keep that row lock until safe-point preparation commits. The matched GREEN makes FLASHBACK wait, then return error 8055 before publication.

Contributor guide

Open the contributing guide

Research direction

Start by locating checkPrepare and the GC prepare transaction handling, then trace how mysql.tidb is read and locked during BEGIN PESSIMISTIC. Reproduce the interleaving with FLASHBACK DATABASE and production GC described in the report. Done means GC preparation serializes with recovery, and recovery observes the newer safe point and fails before publication.

Written by the indexing model from the issue text.

Assessment

Tech stack
go, sql
Domain
databases, distributed-systems
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.