Skip to content

Release 6.5.6 hotfix 20260409 - #67725

Open
HunDunDM wants to merge 14 commits into
pingcap:release-6.5-20231229-v6.5.6from
HunDunDM:hundundm/260409-empty-region-656
Open

Release 6.5.6 hotfix 20260409#67725
HunDunDM wants to merge 14 commits into
pingcap:release-6.5-20231229-v6.5.6from
HunDunDM:hundundm/260409-empty-region-656

Conversation

@HunDunDM

@HunDunDM HunDunDM commented Apr 13, 2026

Copy link
Copy Markdown
Contributor

Only for CI

What problem does this PR solve?

Issue Number: close #xxx

Problem Summary:

What changed and how does it work?

Check List

Tests

  • Unit test
  • Integration test
  • Manual test (add detailed scripts or steps below)
  • No need to test
    • I checked and no code files have been changed.

Side effects

  • Performance regression: Consumes more CPU
  • Performance regression: Consumes more Memory
  • Breaking backward compatibility

Documentation

  • Affects user behaviors
  • Contains syntax changes
  • Contains variable changes
  • Contains experimental features
  • Changes MySQL compatibility

Release note

Please refer to Release Notes Language Style Guide to write a quality release note.

None

Summary by CodeRabbit

  • New Features

    • Force-merge reporting to PD for GC of dropped/truncated tables, schemas, and partitions (batched, retried)
    • Background empty-region merge scanner with persistent checkpoint and a new POST /merge-empty-regions/reset HTTP endpoint
    • New global system variable tidb_enable_drop_table_force_merge to toggle the feature
    • GC now enqueues force-merge ranges during delete-range processing
  • Bug Fixes

    • Fixed _tidb_rowid behavior around table partition EXCHANGE operations

@HunDunDM
HunDunDM requested a review from a team as a code owner April 13, 2026 07:47
@ti-chi-bot ti-chi-bot Bot added do-not-merge/invalid-title release-note-none Denotes a PR that doesn't merit a release note. size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files. labels Apr 13, 2026
@ti-chi-bot

ti-chi-bot Bot commented Apr 13, 2026

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign glorv for approval. For more information see the Code Review Process.
Please ensure that each of them provides their approval before proceeding.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@tiprow

tiprow Bot commented Apr 13, 2026

Copy link
Copy Markdown

Hi @HunDunDM. Thanks for your PR.

PRs from untrusted users cannot be marked as trusted with /ok-to-test in this repo meaning untrusted PR authors can never trigger tests themselves. Collaborators can still trigger tests on the PR using /test all.

I understand the commands that are listed here.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository.

@coderabbitai

coderabbitai Bot commented Apr 13, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds PD force-merge range computation/dispatch for GC, a domain background scanner with checkpointing and reset API, delete-range integration, running-jobs tracking changes with an owner-retire hook, a sysvar feature flag, and many accompanying tests and build updates.

Changes

Cohort / File(s) Summary
Build files
ddl/BUILD.bazel, domain/BUILD.bazel, domain/infosync/BUILD.bazel, sessionctx/variable/BUILD.bazel
Added new source/test files to build targets for force-merge features and tests.
DDL force-merge & delete-range
ddl/pkdb_force_merge.go, ddl/pkdb_force_merge_test.go, ddl/delete_range.go
New logic to compute force-merge key ranges (physical + optional logical ranges for partition/global-index), integration into delete-range flow, and unit tests.
Running-jobs & owner retire hook
ddl/ddl_running_jobs.go, ddl/ddl_running_jobs_test.go, ddl/ddl.go, owner/manager.go, owner/mock.go
Separate processing vs unfinished job sets, adjusted add/remove semantics, add Manager.SetRetireOwnerHook and call to reinit runningJobs on retire.
Domain orchestration & checkpointing
domain/pkdb_force_merge.go, domain/pkdb_force_merge_test.go, domain/domain.go, meta/meta.go, meta/meta_test.go
Domain merge-empty-regions loop, persisted minTableID meta Get/Set/CAS, checkpoint reset method and tests.
Infosync PD dispatch & HTTP client
domain/infosync/pkdb_force_merge.go, domain/infosync/pkdb_force_merge_test.go, domain/infosync/info.go
Add ForceMergeKeyRange type and AddForceMergeRanges batching POSTs to PD with per-request timeout, body-factory retry support, batching/sleep semantics; tests for batching, retries, and timeouts.
GC worker integration
store/gcworker/gc_worker.go
Call GetForceMergeRangesForGCDeleteRange and infosync.AddForceMergeRanges after delete-range completion; per-run table cache added; errors logged non-fatally.
Server HTTP API & tests
server/http_handler.go, server/http_handler_test.go, server/http_status.go
Added POST /merge-empty-regions/reset handler wired to Domain.ResetMergeEmptyRegionsMinTableID with tests and route registration.
Feature flag / sysvar
sessionctx/variable/tidb_vars.go, sessionctx/variable/sysvar.go, sessionctx/variable/pkdb_force_merge_test.go
Introduced tidb_enable_drop_table_force_merge sysvar, exported atomic EnableDropTableForceMerge, and tests validating behavior.
Partition / allocator / executor updates & tests
infoschema/builder.go, ddl/db_partition_test.go, executor/update.go
Treat EXCHANGE PARTITION like AlterSequence for allocator reuse, add TestExchangeTiDBRowID, and adjust update executor change-tracking for partitioned tables.
Meta & HTTP helpers
meta/meta.go, meta/meta_test.go, server/http_handler.go, server/http_handler_test.go
Persisted merge-empty-regions minTableID meta key, accessors, CAS-store, and HTTP reset handler + tests.
Minor fixes
sessionctx/variable/session.go, util/printer/printer.go
Small lock refactor and formatting tweak to go:linkname.

Sequence Diagram(s)

sequenceDiagram
    participant DeleteRange as DeleteRangeEmulator
    participant DDL as DDL
    participant InfoSync as InfoSync
    participant PD as PD

    DeleteRange->>DDL: notify task complete (JobID, StartKey, EndKey)
    DDL->>DDL: load history Job by JobID
    DDL->>DDL: GetForceMergeRangesForGCDeleteRange(job, dr, cache)
    DDL-->>DeleteRange: []ForceMergeKeyRange (physical [+ logical if needed])
    DeleteRange->>InfoSync: AddForceMergeRanges(ctx, ranges)
    loop per-batch (<=2048)
      InfoSync->>PD: POST /regions/force-merge (hex-encoded batch body)
      PD-->>InfoSync: response / timeout / error
      InfoSync->>InfoSync: sleep between batches (respect ctx)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

ok-to-test

Suggested reviewers

  • YangKeao
  • wjhuang2016
  • yudongusa
  • YuJuncen

Poem

🐰 Hop-hop, I scan the keys,

Empty ranges drift like peas,
PD munches gaps with gentle thrum,
Checkpoints hop back — the work is done,
Tests applaud the tidy crumbs.

🚥 Pre-merge checks | ❌ 3

❌ Failed checks (2 warnings, 1 inconclusive)

Check name Status Explanation Resolution
Description check ⚠️ Warning PR description is incomplete; it only contains 'Only for CI' plus the unmodified template with all sections empty (no issue link, no problem summary, no explanation of changes, no test selections, and release note set to 'None'). Complete the PR description by filling in the required sections: link a relevant issue, summarize the problem and solution, detail what code changes were made, mark the appropriate test checkboxes, and provide a meaningful release note.
Docstring Coverage ⚠️ Warning Docstring coverage is 10.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive Title is vague and does not clearly describe the main changes; it uses a generic release/version format without specifying what problem is solved or what functionality is added. Clarify the title to indicate the primary changes (e.g., 'ddl: Implement force-merge range generation for GC delete ranges' or similar) to help reviewers understand the core purpose.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.11.4)

Error: can't load config: unsupported version of the configuration: "" See https://golangci-lint.run/docs/product/migration-guide for migration instructions
The command is terminated due to an error: can't load config: unsupported version of the configuration: "" See https://golangci-lint.run/docs/product/migration-guide for migration instructions


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
owner/mock.go (1)

64-66: ⚠️ Potential issue | 🟠 Major

SetRetireOwnerHook is a no-op because RetireOwner never calls retireHook.

The new hook is wired but never executed, so retire-time side effects won’t run in mockManager.

💡 Suggested fix
 func (m *mockManager) RetireOwner() {
+	if m.retireHook != nil {
+		m.retireHook()
+	}
 	atomic.StoreInt32(&m.owner, 0)
 }

Also applies to: 105-108

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@owner/mock.go` around lines 64 - 66, The mockManager's SetRetireOwnerHook is
never exercised because RetireOwner doesn't call the hook; update func (m
*mockManager) RetireOwner() to invoke m.retireHook() (guarded by a nil check)
after atomic.StoreInt32(&m.owner, 0); also apply the same change to the other
retire path referenced in the file (the second method that sets m.owner to 0
around the other retire implementation) so both retire flows call m.retireHook()
when present.
ddl/ddl_running_jobs.go (1)

68-132: ⚠️ Potential issue | 🔴 Critical

unfinishedSchema now blocks the same unfinished job from running again.

remove() keeps the schema entry for any non-terminal job, but checkRunnable() blindly rejects any job whose schema/table is present there. After the first processing round, the job leaves processingIDs yet still matches its own unfinishedSchema entry, so it can no longer be dispatched. With the current set-only structure, there's no way to distinguish “this job” from “another conflicting job.”

This needs per-schema job IDs/refcounts, or an explicit self-ignore path, before the new unfinished-state split is safe.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ddl/ddl_running_jobs.go` around lines 68 - 132, The unfinishedSchema
currently blocks a job from being rescheduled because it only records
schema/table presence; change it to track per-schema job IDs (e.g.
unfinishedSchema: map[string]map[string]map[JobID]struct{} or add a per-schema
refcount/map) and update runningJobs.remove and wherever unfinishedSchema is
written to add/remove the job.ID instead of just toggling presence; then modify
runningJobs.checkRunnable to ignore unfinishedSchema entries that only contain
the same job.ID (i.e., allow self-ignore) and only treat a schema/table as
conflicting when it contains any job ID != job.ID. Ensure
updateInternalRunningJobIDs and processingIDs logic remains consistent with the
new structure.
store/gcworker/gc_worker.go (1)

723-739: ⚠️ Potential issue | 🟠 Major

Don't complete the delete-range task before the force-merge report is durable.

CompleteDeleteRange runs before doGCForceMergeRanges, but a force-merge failure is only logged. That means a transient AddForceMergeRanges error permanently drops the report, because this task will no longer be loaded from gc_delete_range on the next GC round. The opposite edge also happens today: when CompleteDeleteRange fails, you still report force-merge ranges and can emit duplicates on retry.

Please make the report path retryable with the task state instead of best-effort after completion.

Also applies to: 766-783

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@store/gcworker/gc_worker.go` around lines 723 - 739, The code currently calls
util.CompleteDeleteRange before doGCForceMergeRanges, which discards a transient
AddForceMergeRanges failure; swap the order and make the force-merge report
durable/required before completing the delete-range task: call
w.doGCForceMergeRanges(ctx, se, r, gcForceMergeTableCache) first and if it
returns an error, return that error (do not call util.CompleteDeleteRange) so
the task remains retriable; only after doGCForceMergeRanges succeeds should you
call util.CompleteDeleteRange and handle its error separately; ensure
AddForceMergeRanges/reporting path is idempotent or persisted so retries won’t
double-report (use r.JobID/r.ElementID as idempotency keys) and apply the same
change for the other affected block (the 766-783 region).
ddl/delete_range.go (1)

251-257: ⚠️ Potential issue | 🟠 Major

Don't delete the retry source before sending the force-merge report.

Line 251 completes the gc_delete_range task before Lines 255-257 attempt the PD report. If that report fails transiently, or ctx is canceled during shutdown, the error is only logged and there is no task left to retry from, so the force-merge request can be dropped permanently. Please either make reporting happen before completion, or persist/retry it independently of the delete-range row.

Also applies to: 274-287

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ddl/delete_range.go` around lines 251 - 257, The code currently calls
util.CompleteDeleteRange(sctx, r) before attempting
dr.reportForceMergeRanges(...), which can drop the retry source if reporting
fails; change the order so that dr.reportForceMergeRanges(ctx, sctx, r,
gcForceMergeTableCache) is attempted (and retried/persisted on transient
failure) before calling util.CompleteDeleteRange(sctx, r), or alternatively make
reportForceMergeRanges persist its own retry record independent of the
gc_delete_range row so failures do not remove the ability to retry; update the
blocks around util.CompleteDeleteRange and dr.reportForceMergeRanges (and
similarly the other occurrence that mirrors lines 274-287) to ensure reporting
is durable or executed prior to completing the delete-range row.
🧹 Nitpick comments (1)
owner/manager.go (1)

62-63: Fix the retire-hook contract comment.

The new interface comment says the hook runs after retiring the owner, but RetireOwner() actually invokes it before clearing m.elec. That ordering matters here, so the public comment should match the real contract.

As per coding guidelines, "Comments SHOULD explain non-obvious intent, constraints, invariants, concurrency guarantees, SQL/compatibility contracts, or important performance trade-offs, and SHOULD NOT restate what the code already makes clear."

Also applies to: 195-199

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@owner/manager.go` around lines 62 - 63, Update the comment for
SetRetireOwnerHook to reflect the actual contract: the hook is invoked by
RetireOwner() before the manager clears m.elec (i.e., prior to removing owner
state), not after; change both occurrences (the interface comment near
SetRetireOwnerHook and the similar comment at the other location) to state that
the hook runs before clearing owner/election state so the documented ordering
matches the behavior in RetireOwner and preserves the intended
concurrency/invariant reasoning.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ddl/ddl.go`:
- Around line 757-759: The retire-owner hook currently swaps the shared pointer
by calling d.ownerManager.SetRetireOwnerHook(func() { d.runningJobs =
newRunningJobs() }), which is unsafe because concurrent goroutines like
delivery2worker will keep referencing the old instance; instead, add a
Clear/Reset method on the runningJobs type (or reuse an existing lock-protected
method) that zeroes its internals under its own mutex, then change the hook to
call d.runningJobs.Clear() (or Reset()) rather than replacing the pointer so the
runningJobs pointer remains stable while its contents are cleared safely under
lock.

In `@domain/infosync/info.go`:
- Around line 392-399: doRequest currently captures and reuses the same
io.Reader across retries, which causes later attempts to read from a partially
consumed reader; fix by reading the full body into a byte slice if body != nil
and call doRequestWithBodyBytes(ctx, apiName, addrs, route, method, bodyBytes)
so each retry gets a fresh reader (or otherwise construct a new bytes.Reader per
attempt); update the doRequest function to convert the incoming body to bytes
and delegate to doRequestWithBodyBytes instead of creating a closure that
returns the same io.Reader, referencing doRequest, doRequestWithBodyFactory and
doRequestWithBodyBytes to locate the change.

In `@domain/pkdb_force_merge.go`:
- Around line 104-121: The helpers loadOrInitMergeEmptyRegionsMinTableID and the
similar loadMergeEmptyRegionsMaxTableID replace the caller context with
context.Background(), blocking cancellation; change their signatures to accept a
context.Context parameter and use that ctx when calling
kv.WithInternalSourceType (i.e., replace context.Background() with the passed
ctx inside kv.RunInNewTxn), and update the caller doMergeEmptyRegions to pass
its ctx through so the checkpoint KV operations become cancellation-aware.
Ensure all call sites are updated to provide the context and adjust any error
handling signatures accordingly.

In `@executor/update.go`:
- Around line 85-93: The current logic sets skipMultipleChangesOnSameRow=false
for all non-clustered PartitionedTable instances, which globally disables dedup
and causes repeated updates; instead, implement a partition-aware dedup keyed by
(physical partition ID + handle). Change the branch that checks
tbl.(table.PartitionedTable) / tbl.Meta().HasClusteredIndex() to stop flipping a
global flag and instead ensure the update executor (where e.changed is cleared
for repeated handles) consults a new per-table map (e.g.,
seenHandlesByPartition) keyed by the partition identity plus the row handle;
mark entries when a physical (partitionID, handle) pair is updated so subsequent
matches for the same physical row skip setting e.changed=false only if the same
physical key was seen, leaving cross-partition duplicates unaffected. Ensure the
map is created/cleared per statement and referenced where e.changed is evaluated
for repeated handles.

In `@sessionctx/variable/pkdb_force_merge_test.go`:
- Around line 24-58: The unit test only covers the in-process hook; add a SQL
integration test named e.g. TestSetTiDBEnableDropTableForceMerge_SQL that uses
the SQL test harness (TestKit) to SET the variable and read it back via SQL to
cover the user-visible path: execute "SET GLOBAL
tidb_enable_drop_table_force_merge=ON" (and OFF), then verify with "SELECT
@@global.tidb_enable_drop_table_force_merge" and "SELECT
@@tidb_enable_drop_table_force_merge" (and session-level checks if appropriate),
and ensure cleanup/restore of the original value; reference the existing
TestSetTiDBEnableDropTableForceMerge, the sysvar TiDBEnableDropTableForceMerge
and the boolean flag EnableDropTableForceMerge when asserting effects.

In `@util/printer/printer.go`:
- Line 171: The compiler directive on the line containing the pseudo-symbol
buildVersion is malformed: remove the space between the comment slashes and the
directive so the link directive uses the exact "//go:linkname" prefix and links
buildVersion to runtime.buildVersion; update the line in printer.go where "//
go:linkname buildVersion runtime.buildVersion" appears to use the correct
"//go:linkname" form so the compiler recognizes the directive.

---

Outside diff comments:
In `@ddl/ddl_running_jobs.go`:
- Around line 68-132: The unfinishedSchema currently blocks a job from being
rescheduled because it only records schema/table presence; change it to track
per-schema job IDs (e.g. unfinishedSchema:
map[string]map[string]map[JobID]struct{} or add a per-schema refcount/map) and
update runningJobs.remove and wherever unfinishedSchema is written to add/remove
the job.ID instead of just toggling presence; then modify
runningJobs.checkRunnable to ignore unfinishedSchema entries that only contain
the same job.ID (i.e., allow self-ignore) and only treat a schema/table as
conflicting when it contains any job ID != job.ID. Ensure
updateInternalRunningJobIDs and processingIDs logic remains consistent with the
new structure.

In `@ddl/delete_range.go`:
- Around line 251-257: The code currently calls util.CompleteDeleteRange(sctx,
r) before attempting dr.reportForceMergeRanges(...), which can drop the retry
source if reporting fails; change the order so that
dr.reportForceMergeRanges(ctx, sctx, r, gcForceMergeTableCache) is attempted
(and retried/persisted on transient failure) before calling
util.CompleteDeleteRange(sctx, r), or alternatively make reportForceMergeRanges
persist its own retry record independent of the gc_delete_range row so failures
do not remove the ability to retry; update the blocks around
util.CompleteDeleteRange and dr.reportForceMergeRanges (and similarly the other
occurrence that mirrors lines 274-287) to ensure reporting is durable or
executed prior to completing the delete-range row.

In `@owner/mock.go`:
- Around line 64-66: The mockManager's SetRetireOwnerHook is never exercised
because RetireOwner doesn't call the hook; update func (m *mockManager)
RetireOwner() to invoke m.retireHook() (guarded by a nil check) after
atomic.StoreInt32(&m.owner, 0); also apply the same change to the other retire
path referenced in the file (the second method that sets m.owner to 0 around the
other retire implementation) so both retire flows call m.retireHook() when
present.

In `@store/gcworker/gc_worker.go`:
- Around line 723-739: The code currently calls util.CompleteDeleteRange before
doGCForceMergeRanges, which discards a transient AddForceMergeRanges failure;
swap the order and make the force-merge report durable/required before
completing the delete-range task: call w.doGCForceMergeRanges(ctx, se, r,
gcForceMergeTableCache) first and if it returns an error, return that error (do
not call util.CompleteDeleteRange) so the task remains retriable; only after
doGCForceMergeRanges succeeds should you call util.CompleteDeleteRange and
handle its error separately; ensure AddForceMergeRanges/reporting path is
idempotent or persisted so retries won’t double-report (use r.JobID/r.ElementID
as idempotency keys) and apply the same change for the other affected block (the
766-783 region).

---

Nitpick comments:
In `@owner/manager.go`:
- Around line 62-63: Update the comment for SetRetireOwnerHook to reflect the
actual contract: the hook is invoked by RetireOwner() before the manager clears
m.elec (i.e., prior to removing owner state), not after; change both occurrences
(the interface comment near SetRetireOwnerHook and the similar comment at the
other location) to state that the hook runs before clearing owner/election state
so the documented ordering matches the behavior in RetireOwner and preserves the
intended concurrency/invariant reasoning.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 4b5e1d72-0e91-4bc1-b0fc-a6dd715ef6cc

📥 Commits

Reviewing files that changed from the base of the PR and between a9fb0f0 and acfbbd0.

📒 Files selected for processing (32)
  • ddl/BUILD.bazel
  • ddl/db_partition_test.go
  • ddl/ddl.go
  • ddl/ddl_running_jobs.go
  • ddl/ddl_running_jobs_test.go
  • ddl/delete_range.go
  • ddl/pkdb_force_merge.go
  • ddl/pkdb_force_merge_test.go
  • domain/BUILD.bazel
  • domain/domain.go
  • domain/infosync/BUILD.bazel
  • domain/infosync/info.go
  • domain/infosync/pkdb_force_merge.go
  • domain/infosync/pkdb_force_merge_test.go
  • domain/pkdb_force_merge.go
  • domain/pkdb_force_merge_test.go
  • executor/update.go
  • infoschema/builder.go
  • meta/meta.go
  • meta/meta_test.go
  • owner/manager.go
  • owner/mock.go
  • server/http_handler.go
  • server/http_handler_test.go
  • server/http_status.go
  • sessionctx/variable/BUILD.bazel
  • sessionctx/variable/pkdb_force_merge_test.go
  • sessionctx/variable/session.go
  • sessionctx/variable/sysvar.go
  • sessionctx/variable/tidb_vars.go
  • store/gcworker/gc_worker.go
  • util/printer/printer.go

Comment thread ddl/ddl.go
Comment on lines +757 to +759
d.ownerManager.SetRetireOwnerHook(func() {
d.runningJobs = newRunningJobs()
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify all pointer-write/read sites for runningJobs and hook wiring.
rg -nP 'SetRetireOwnerHook|d\.runningJobs\s*=' -C2
rg -nP '\bd\.runningJobs\.' -C2

Repository: pingcap/tidb

Length of output: 2976


🏁 Script executed:

# Find the runningJobs struct definition and its locking mechanism
fd -e go | xargs grep -l "type.*runningJobs" | head -5

Repository: pingcap/tidb

Length of output: 79


🏁 Script executed:

# Get the full definition of runningJobs struct
rg -A 20 "type\s+runningJobs\s+struct" ddl/

Repository: pingcap/tidb

Length of output: 1255


🏁 Script executed:

# Examine the RetireOwner implementation to understand hook invocation semantics
rg -B 5 -A 15 "func.*RetireOwner\(" owner/manager.go

Repository: pingcap/tidb

Length of output: 610


🏁 Script executed:

# Check if d.runningJobs is assigned to local variables or captured in goroutines
rg -n "runningJobs\s*:=|=.*runningJobs|runningJobs\s*\)" ddl/ owner/

Repository: pingcap/tidb

Length of output: 505


🏁 Script executed:

# Examine the add/remove method implementations to confirm they use the lock
rg -B 2 -A 10 "func \(j \*runningJobs\) (add|remove)" ddl/

Repository: pingcap/tidb

Length of output: 1325


🏁 Script executed:

# Check if there are any direct field accesses on d.runningJobs or runningJobs (e.g., d.runningJobs.processingIDs)
rg "\.processingIDs|\.unfinishedIDs|\.unfinishedSchema" ddl/

Repository: pingcap/tidb

Length of output: 1298


Avoid swapping d.runningJobs pointer inside retire-owner hook.

Lines 757–759 replace a shared pointer unsafely. Async goroutines spawned before the swap (e.g., delivery2worker at job_table.go:210–215) will reference the old instance, causing jobs to be added to one instance and removed from another.

Safer approach: keep pointer stable, clear internals under lock
 	d.ownerManager.SetRetireOwnerHook(func() {
-		d.runningJobs = newRunningJobs()
+		d.runningJobs.Lock()
+		defer d.runningJobs.Unlock()
+		d.runningJobs.processingIDs = make(map[int64]struct{})
+		d.runningJobs.processingIDsStr = ""
+		d.runningJobs.unfinishedIDs = make(map[int64]struct{})
+		d.runningJobs.unfinishedSchema = make(map[string]map[string]struct{})
 	})
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
d.ownerManager.SetRetireOwnerHook(func() {
d.runningJobs = newRunningJobs()
})
d.ownerManager.SetRetireOwnerHook(func() {
d.runningJobs.Lock()
defer d.runningJobs.Unlock()
d.runningJobs.processingIDs = make(map[int64]struct{})
d.runningJobs.processingIDsStr = ""
d.runningJobs.unfinishedIDs = make(map[int64]struct{})
d.runningJobs.unfinishedSchema = make(map[string]map[string]struct{})
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ddl/ddl.go` around lines 757 - 759, The retire-owner hook currently swaps the
shared pointer by calling d.ownerManager.SetRetireOwnerHook(func() {
d.runningJobs = newRunningJobs() }), which is unsafe because concurrent
goroutines like delivery2worker will keep referencing the old instance; instead,
add a Clear/Reset method on the runningJobs type (or reuse an existing
lock-protected method) that zeroes its internals under its own mutex, then
change the hook to call d.runningJobs.Clear() (or Reset()) rather than replacing
the pointer so the runningJobs pointer remains stable while its contents are
cleared safely under lock.

Comment thread domain/infosync/info.go
Comment on lines 392 to +399
func doRequest(ctx context.Context, apiName string, addrs []string, route, method string, body io.Reader) ([]byte, error) {
var bodyFactory func() io.Reader
if body != nil {
bodyFactory = func() io.Reader {
return body
}
}
return doRequestWithBodyFactory(ctx, apiName, addrs, route, method, bodyFactory)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== All doRequest call sites in domain/infosync ==="
rg -n --type=go '\bdoRequest\s*\(' domain/infosync -C2

echo
echo "=== Likely non-nil body call sites (manual check required) ==="
# Shows call sites; verify the 6th argument is not nil and whether call should move to doRequestWithBodyBytes.
rg -n --type=go '\bdoRequest\s*\(' domain/infosync -C4 | rg -n -v 'nil'

Repository: pingcap/tidb

Length of output: 19972


🏁 Script executed:

cat -n domain/infosync/info.go | sed -n '380,450p'

Repository: pingcap/tidb

Length of output: 2991


doRequest reuses a consumed request body across address retries.

The closure captures the io.Reader and returns the same instance for each address. After the first attempt, the reader is partially consumed; subsequent retries use it from the wrong position. While all current call sites work around this by passing fresh bytes.NewBuffer() or bytes.NewReader() objects, the abstraction is fragile and violates io.Reader semantics.

Use doRequestWithBodyBytes instead, which correctly creates a new reader per attempt via the factory function.

💡 Suggested fix
 func doRequest(ctx context.Context, apiName string, addrs []string, route, method string, body io.Reader) ([]byte, error) {
-	var bodyFactory func() io.Reader
-	if body != nil {
-		bodyFactory = func() io.Reader {
-			return body
-		}
-	}
-	return doRequestWithBodyFactory(ctx, apiName, addrs, route, method, bodyFactory)
+	if body == nil {
+		return doRequestWithBodyFactory(ctx, apiName, addrs, route, method, nil)
+	}
+	bodyBytes, err := io.ReadAll(body)
+	if err != nil {
+		return nil, err
+	}
+	return doRequestWithBodyBytes(ctx, apiName, addrs, route, method, bodyBytes)
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func doRequest(ctx context.Context, apiName string, addrs []string, route, method string, body io.Reader) ([]byte, error) {
var bodyFactory func() io.Reader
if body != nil {
bodyFactory = func() io.Reader {
return body
}
}
return doRequestWithBodyFactory(ctx, apiName, addrs, route, method, bodyFactory)
func doRequest(ctx context.Context, apiName string, addrs []string, route, method string, body io.Reader) ([]byte, error) {
if body == nil {
return doRequestWithBodyFactory(ctx, apiName, addrs, route, method, nil)
}
bodyBytes, err := io.ReadAll(body)
if err != nil {
return nil, err
}
return doRequestWithBodyBytes(ctx, apiName, addrs, route, method, bodyBytes)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@domain/infosync/info.go` around lines 392 - 399, doRequest currently captures
and reuses the same io.Reader across retries, which causes later attempts to
read from a partially consumed reader; fix by reading the full body into a byte
slice if body != nil and call doRequestWithBodyBytes(ctx, apiName, addrs, route,
method, bodyBytes) so each retry gets a fresh reader (or otherwise construct a
new bytes.Reader per attempt); update the doRequest function to convert the
incoming body to bytes and delegate to doRequestWithBodyBytes instead of
creating a closure that returns the same io.Reader, referencing doRequest,
doRequestWithBodyFactory and doRequestWithBodyBytes to locate the change.

Comment on lines +104 to +121
func (do *Domain) loadOrInitMergeEmptyRegionsMinTableID() (int64, error) {
minTableID := mergeEmptyRegionsInitMinTableID
err := kv.RunInNewTxn(kv.WithInternalSourceType(context.Background(), kv.InternalTxnDDL), do.store, true, func(ctx context.Context, txn kv.Transaction) error {
t := meta.NewMeta(txn)
tableID, ok, err := t.GetMergeEmptyRegionsMinTableID()
if err != nil {
return errors.Trace(err)
}
if !ok || tableID < mergeEmptyRegionsInitMinTableID {
tableID = mergeEmptyRegionsInitMinTableID
if err := t.SetMergeEmptyRegionsMinTableID(tableID); err != nil {
return errors.Trace(err)
}
}
minTableID = tableID
return nil
})
return minTableID, errors.Trace(err)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Thread the loop context into the checkpoint transactions.

doMergeEmptyRegions() is cancellation-aware, but these helpers replace the caller context with context.Background(). If meta KV is slow during shutdown, the loop can't abort the load/store/CAS phase and this goroutine can block domain teardown until those calls return.

💡 Suggested direction
-func (do *Domain) loadOrInitMergeEmptyRegionsMinTableID() (int64, error) {
+func (do *Domain) loadOrInitMergeEmptyRegionsMinTableID(ctx context.Context) (int64, error) {
 	minTableID := mergeEmptyRegionsInitMinTableID
-	err := kv.RunInNewTxn(kv.WithInternalSourceType(context.Background(), kv.InternalTxnDDL), do.store, true, func(ctx context.Context, txn kv.Transaction) error {
+	err := kv.RunInNewTxn(kv.WithInternalSourceType(ctx, kv.InternalTxnDDL), do.store, true, func(ctx context.Context, txn kv.Transaction) error {
 		...
 	})
 	return minTableID, errors.Trace(err)
 }

-func (do *Domain) storeMergeEmptyRegionsMinTableID(tableID int64) error {
+func (do *Domain) storeMergeEmptyRegionsMinTableID(ctx context.Context, tableID int64) error {
 	...
-	return errors.Trace(kv.RunInNewTxn(kv.WithInternalSourceType(context.Background(), kv.InternalTxnDDL), do.store, true, func(ctx context.Context, txn kv.Transaction) error {
+	return errors.Trace(kv.RunInNewTxn(kv.WithInternalSourceType(ctx, kv.InternalTxnDDL), do.store, true, func(ctx context.Context, txn kv.Transaction) error {
 		return errors.Trace(meta.NewMeta(txn).SetMergeEmptyRegionsMinTableID(tableID))
 	}))
 }

-func (do *Domain) storeMergeEmptyRegionsMinTableIDIfUnchanged(expectedTableID, nextTableID int64) (bool, error) {
+func (do *Domain) storeMergeEmptyRegionsMinTableIDIfUnchanged(ctx context.Context, expectedTableID, nextTableID int64) (bool, error) {
 	...
-	err := kv.RunInNewTxn(kv.WithInternalSourceType(context.Background(), kv.InternalTxnDDL), do.store, true, func(ctx context.Context, txn kv.Transaction) error {
+	err := kv.RunInNewTxn(kv.WithInternalSourceType(ctx, kv.InternalTxnDDL), do.store, true, func(ctx context.Context, txn kv.Transaction) error {
 		...
 	})
 	return updated, errors.Trace(err)
 }

Also applies to: 124-157

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@domain/pkdb_force_merge.go` around lines 104 - 121, The helpers
loadOrInitMergeEmptyRegionsMinTableID and the similar
loadMergeEmptyRegionsMaxTableID replace the caller context with
context.Background(), blocking cancellation; change their signatures to accept a
context.Context parameter and use that ctx when calling
kv.WithInternalSourceType (i.e., replace context.Background() with the passed
ctx inside kv.RunInNewTxn), and update the caller doMergeEmptyRegions to pass
its ctx through so the checkpoint KV operations become cancellation-aware.
Ensure all call sites are updated to provide the context and adjust any error
handling signatures accordingly.

Comment thread executor/update.go
Comment on lines +85 to +93
// Partitioned tables can have duplicate _tidb_rowid between different partitions
// due to EXCHANGE PARTITION, if so, do not optimize skipping rows with multiple changes
skipMultipleChangesOnSameRow := true
tbl := e.tblID2table[content.TblID]
if _, ok := tbl.(table.PartitionedTable); ok {
if !tbl.Meta().HasClusteredIndex() {
skipMultipleChangesOnSameRow = false
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid globally disabling duplicate-row suppression for non-clustered partitioned tables

Line 119 forces e.changed to false for every repeated handle when skipMultipleChangesOnSameRow=false. That can re-run updates for the same physical row under join fan-out, violating the “update once per matched row” contract and risking inflated changed-row accounting. Please fix this via a partition-aware dedup key (physical partition identity + handle), instead of disabling dedup for the whole table class.

Based on learnings: "TiDB executor behavior for multi-table UPDATE ... Each matched row is updated once..." and "VirtualAssignmentsOffset semantics ... Executor applies ... only after the 'changed' check."

Also applies to: 119-120

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@executor/update.go` around lines 85 - 93, The current logic sets
skipMultipleChangesOnSameRow=false for all non-clustered PartitionedTable
instances, which globally disables dedup and causes repeated updates; instead,
implement a partition-aware dedup keyed by (physical partition ID + handle).
Change the branch that checks tbl.(table.PartitionedTable) /
tbl.Meta().HasClusteredIndex() to stop flipping a global flag and instead ensure
the update executor (where e.changed is cleared for repeated handles) consults a
new per-table map (e.g., seenHandlesByPartition) keyed by the partition identity
plus the row handle; mark entries when a physical (partitionID, handle) pair is
updated so subsequent matches for the same physical row skip setting
e.changed=false only if the same physical key was seen, leaving cross-partition
duplicates unaffected. Ensure the map is created/cleared per statement and
referenced where e.changed is evaluated for repeated handles.

Comment on lines +24 to +58
func TestSetTiDBEnableDropTableForceMerge(t *testing.T) {
vars := NewSessionVars(nil)
mock := NewMockGlobalAccessor4Tests()
mock.SessionVars = vars
vars.GlobalVarsAccessor = mock

oldValue := EnableDropTableForceMerge.Load()
EnableDropTableForceMerge.Store(DefTiDBEnableDropTableForceMerge)
defer EnableDropTableForceMerge.Store(oldValue)

sysVar := GetSysVar(TiDBEnableDropTableForceMerge)
require.Equal(t, Off, sysVar.Value)

require.NoError(t, mock.SetGlobalSysVar(context.Background(), TiDBEnableDropTableForceMerge, On))
require.True(t, EnableDropTableForceMerge.Load())

val, err := mock.GetGlobalSysVar(TiDBEnableDropTableForceMerge)
require.NoError(t, err)
require.Equal(t, On, val)

val, err = sysVar.GetGlobalFromHook(context.Background(), vars)
require.NoError(t, err)
require.Equal(t, On, val)

require.NoError(t, mock.SetGlobalSysVar(context.Background(), TiDBEnableDropTableForceMerge, Off))
require.False(t, EnableDropTableForceMerge.Load())

val, err = mock.GetGlobalSysVar(TiDBEnableDropTableForceMerge)
require.NoError(t, err)
require.Equal(t, Off, val)

val, err = sysVar.GetGlobalFromHook(context.Background(), vars)
require.NoError(t, err)
require.Equal(t, Off, val)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add a SQL-level regression for the new sysvar.

This only verifies the in-process hook path. Please add a SQL integration test that sets tidb_enable_drop_table_force_merge and reads it back through SQL as well, so the user-visible behavior is covered end to end.

Based on learnings, "For session variables or protocol behavior changes, perform targeted package tests plus SQL integration tests for user-visible behavior."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sessionctx/variable/pkdb_force_merge_test.go` around lines 24 - 58, The unit
test only covers the in-process hook; add a SQL integration test named e.g.
TestSetTiDBEnableDropTableForceMerge_SQL that uses the SQL test harness
(TestKit) to SET the variable and read it back via SQL to cover the user-visible
path: execute "SET GLOBAL tidb_enable_drop_table_force_merge=ON" (and OFF), then
verify with "SELECT @@global.tidb_enable_drop_table_force_merge" and "SELECT
@@tidb_enable_drop_table_force_merge" (and session-level checks if appropriate),
and ensure cleanup/restore of the original value; reference the existing
TestSetTiDBEnableDropTableForceMerge, the sysvar TiDBEnableDropTableForceMerge
and the boolean flag EnableDropTableForceMerge when asserting effects.

Comment thread util/printer/printer.go
}

//go:linkname buildVersion runtime.buildVersion
// go:linkname buildVersion runtime.buildVersion

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP --type go '^\s*//\s+go:linkname\b|^\s*//go:linkname\b' \
  util/printer/printer.go \
  util/localpool/localpool.go \
  util/fastrand/random.go \
  br/pkg/lightning/manual/manual.go

Repository: pingcap/tidb

Length of output: 410


Fix malformed compiler directive on Line 171: remove space between // and go:linkname.

The go:linkname directive requires the exact //go: prefix without spaces. The current // go:linkname will not be recognized by the compiler and breaks the link to runtime.buildVersion. All other files in the codebase use the correct //go:linkname format.

Proposed fix
-// go:linkname buildVersion runtime.buildVersion
+//go:linkname buildVersion runtime.buildVersion
 var buildVersion string
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// go:linkname buildVersion runtime.buildVersion
//go:linkname buildVersion runtime.buildVersion
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@util/printer/printer.go` at line 171, The compiler directive on the line
containing the pseudo-symbol buildVersion is malformed: remove the space between
the comment slashes and the directive so the link directive uses the exact
"//go:linkname" prefix and links buildVersion to runtime.buildVersion; update
the line in printer.go where "// go:linkname buildVersion runtime.buildVersion"
appears to use the correct "//go:linkname" form so the compiler recognizes the
directive.

Signed-off-by: HunDunDM <hundundm@gmail.com>
@HunDunDM

Copy link
Copy Markdown
Contributor Author

/test unit-test

@tiprow

tiprow Bot commented Apr 13, 2026

Copy link
Copy Markdown

@HunDunDM: PRs from untrusted users cannot be marked as trusted with /ok-to-test in this repo meaning untrusted PR authors can never trigger tests themselves. Collaborators can still trigger tests on the PR using /test.

Details

In response to this:

/test unit-test

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository.

Signed-off-by: HunDunDM <hundundm@gmail.com>
@HunDunDM
HunDunDM force-pushed the hundundm/260409-empty-region-656 branch from 1ef0449 to 34e2d44 Compare April 13, 2026 10:24
@coderabbitai

coderabbitai Bot commented Apr 13, 2026

Copy link
Copy Markdown

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
{}

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
domain/infosync/pkdb_force_merge.go (2)

31-35: Clarify exported key-range boundary semantics.

StartKey/EndKey are exported, but the interval contract is implicit. A short field-level comment (inclusive/exclusive) will reduce caller ambiguity.

Proposed change
 type ForceMergeKeyRange struct {
-	StartKey []byte
-	EndKey   []byte
+	// StartKey is the inclusive lower bound of the force-merge range.
+	StartKey []byte
+	// EndKey is the exclusive upper bound of the force-merge range.
+	EndKey   []byte
 }
As per coding guidelines: "Comments SHOULD explain non-obvious intent, constraints, invariants..."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@domain/infosync/pkdb_force_merge.go` around lines 31 - 35, The exported
struct ForceMergeKeyRange has ambiguous boundary semantics for StartKey and
EndKey; add concise field-level comments to StartKey and EndKey describing
whether each bound is inclusive or exclusive (e.g., StartKey is inclusive,
EndKey is exclusive), and document any sentinel meanings (nil/empty =>
unbounded) to remove caller ambiguity; update the struct's field comments next
to StartKey and EndKey accordingly.

156-157: Use a log field name that matches the value semantics.

Line 156 logs batchIndex, but the value is 1-based (batchNumber). Rename to batchNumber (or log zero-based index) to avoid confusion in log consumers.

Proposed change
-		zap.Int("batchIndex", batchNumber),
+		zap.Int("batchNumber", batchNumber),
As per coding guidelines: "Code SHOULD be self-documenting through clear naming and structure."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@domain/infosync/pkdb_force_merge.go` around lines 156 - 157, The log field
name is misleading: zap.Int("batchIndex", batchNumber) uses a 1-based
batchNumber but names it batchIndex; update the zap.Int call to use a matching
field name (e.g., zap.Int("batchNumber", batchNumber)) where this appears in
domain/infosync/pkdb_force_merge.go (look for the zap.Int invocation that
references batchNumber) so the log key matches the value semantics.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@domain/infosync/pkdb_force_merge.go`:
- Around line 31-35: The exported struct ForceMergeKeyRange has ambiguous
boundary semantics for StartKey and EndKey; add concise field-level comments to
StartKey and EndKey describing whether each bound is inclusive or exclusive
(e.g., StartKey is inclusive, EndKey is exclusive), and document any sentinel
meanings (nil/empty => unbounded) to remove caller ambiguity; update the
struct's field comments next to StartKey and EndKey accordingly.
- Around line 156-157: The log field name is misleading: zap.Int("batchIndex",
batchNumber) uses a 1-based batchNumber but names it batchIndex; update the
zap.Int call to use a matching field name (e.g., zap.Int("batchNumber",
batchNumber)) where this appears in domain/infosync/pkdb_force_merge.go (look
for the zap.Int invocation that references batchNumber) so the log key matches
the value semantics.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 2106509c-b0f6-4752-8b02-e43fcc7c7168

📥 Commits

Reviewing files that changed from the base of the PR and between 1ef0449 and 34e2d44.

📒 Files selected for processing (3)
  • domain/infosync/BUILD.bazel
  • domain/infosync/pkdb_force_merge.go
  • domain/infosync/pkdb_force_merge_test.go
✅ Files skipped from review due to trivial changes (1)
  • domain/infosync/pkdb_force_merge_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • domain/infosync/BUILD.bazel

@HunDunDM

Copy link
Copy Markdown
Contributor Author

/test mysql-test

@tiprow

tiprow Bot commented Apr 13, 2026

Copy link
Copy Markdown

@HunDunDM: PRs from untrusted users cannot be marked as trusted with /ok-to-test in this repo meaning untrusted PR authors can never trigger tests themselves. Collaborators can still trigger tests on the PR using /test.

Details

In response to this:

/test mysql-test

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository.

@codecov

codecov Bot commented Apr 13, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 62.76978% with 207 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (release-6.5-20231229-v6.5.6@a9fb0f0). Learn more about missing BASE report.

Additional details and impacted files
@@                       Coverage Diff                        @@
##             release-6.5-20231229-v6.5.6     #67725   +/-   ##
================================================================
  Coverage                               ?   73.6291%           
================================================================
  Files                                  ?       1091           
  Lines                                  ?     350239           
  Branches                               ?          0           
================================================================
  Hits                                   ?     257878           
  Misses                                 ?      75778           
  Partials                               ?      16583           
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Signed-off-by: HunDunDM <hundundm@gmail.com>
@ti-chi-bot

ti-chi-bot Bot commented Apr 14, 2026

Copy link
Copy Markdown

[FORMAT CHECKER NOTIFICATION]

Notice: To remove the do-not-merge/invalid-title label, please follow title format, for example pkg [, pkg2, pkg3]: what is changed or *: what is changed.

📖 For more info, you can check the "Contribute Code" section in the development guide.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
domain/pkdb_force_merge.go (1)

112-166: ⚠️ Potential issue | 🟠 Major

Thread the caller ctx into the checkpoint transactions.

doMergeEmptyRegions is cancellation-aware, but all three checkpoint helpers still hardcode context.Background(). A slow meta KV read/write can therefore outlive domain shutdown, and ResetMergeEmptyRegionsMinTableID() cannot honor request cancellation either. Please pass the caller context through these helpers and use it in kv.WithInternalSourceType(...) instead of context.Background().

💡 Suggested direction
-func (do *Domain) doMergeEmptyRegions(ctx context.Context) error {
-	minTableID, err := do.loadOrInitMergeEmptyRegionsMinTableID()
+func (do *Domain) doMergeEmptyRegions(ctx context.Context) error {
+	minTableID, err := do.loadOrInitMergeEmptyRegionsMinTableID(ctx)
 	if err != nil {
 		return errors.Trace(err)
 	}
 	...
-	updated, err := do.storeMergeEmptyRegionsMinTableIDIfUnchanged(minTableID, maxTableID)
+	updated, err := do.storeMergeEmptyRegionsMinTableIDIfUnchanged(ctx, minTableID, maxTableID)
 	...
 }

-func (do *Domain) loadOrInitMergeEmptyRegionsMinTableID() (int64, error) {
+func (do *Domain) loadOrInitMergeEmptyRegionsMinTableID(ctx context.Context) (int64, error) {
 	minTableID := mergeEmptyRegionsInitMinTableID
-	err := kv.RunInNewTxn(kv.WithInternalSourceType(context.Background(), kv.InternalTxnDDL), do.store, true, func(ctx context.Context, txn kv.Transaction) error {
+	err := kv.RunInNewTxn(kv.WithInternalSourceType(ctx, kv.InternalTxnDDL), do.store, true, func(ctx context.Context, txn kv.Transaction) error {
 		...
 	})
 	return minTableID, errors.Trace(err)
 }

-func (do *Domain) storeMergeEmptyRegionsMinTableID(tableID int64) error {
+func (do *Domain) storeMergeEmptyRegionsMinTableID(ctx context.Context, tableID int64) error {
 	...
-	return errors.Trace(kv.RunInNewTxn(kv.WithInternalSourceType(context.Background(), kv.InternalTxnDDL), do.store, true, func(ctx context.Context, txn kv.Transaction) error {
+	return errors.Trace(kv.RunInNewTxn(kv.WithInternalSourceType(ctx, kv.InternalTxnDDL), do.store, true, func(ctx context.Context, txn kv.Transaction) error {
 		return errors.Trace(meta.NewMeta(txn).SetMergeEmptyRegionsMinTableID(tableID))
 	}))
 }

-func (do *Domain) storeMergeEmptyRegionsMinTableIDIfUnchanged(expectedTableID, nextTableID int64) (bool, error) {
+func (do *Domain) storeMergeEmptyRegionsMinTableIDIfUnchanged(ctx context.Context, expectedTableID, nextTableID int64) (bool, error) {
 	...
-	err := kv.RunInNewTxn(kv.WithInternalSourceType(context.Background(), kv.InternalTxnDDL), do.store, true, func(ctx context.Context, txn kv.Transaction) error {
+	err := kv.RunInNewTxn(kv.WithInternalSourceType(ctx, kv.InternalTxnDDL), do.store, true, func(ctx context.Context, txn kv.Transaction) error {
 		...
 	})
 	return updated, errors.Trace(err)
 }

Also applies to: 171-173

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@domain/pkdb_force_merge.go` around lines 112 - 166, The helper functions
loadOrInitMergeEmptyRegionsMinTableID, storeMergeEmptyRegionsMinTableID, and
storeMergeEmptyRegionsMinTableIDIfUnchanged must accept and use the caller's ctx
instead of always using context.Background(); add a context.Context parameter to
each function signature, replace kv.WithInternalSourceType(context.Background(),
...) with kv.WithInternalSourceType(ctx, ...), and propagate that ctx from
callers (e.g., doMergeEmptyRegions and ResetMergeEmptyRegionsMinTableID) so
checkpoint transactions respect cancellation/timeouts. Ensure all call sites are
updated to pass the caller ctx and keep existing behavior (clamping tableID to
mergeEmptyRegionsInitMinTableID) unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ddl/delete_range.go`:
- Around line 275-292: The code currently lets
GetForceMergeRangesForGCDeleteRange mutate gcForceMergeTableCache before calling
infosync.AddForceMergeRanges, causing a logical-table entry to be cached even if
AddForceMergeRanges fails; change the flow in delRange.reportForceMergeRanges so
that gcForceMergeTableCache is only updated after a successful
infosync.AddForceMergeRanges call (or, alternatively, make
GetForceMergeRangesForGCDeleteRange return the ranges without mutating the cache
and then update gcForceMergeTableCache inside reportForceMergeRanges only when
AddForceMergeRanges returns nil); reference the functions
delRange.reportForceMergeRanges, GetForceMergeRangesForGCDeleteRange, the
gcForceMergeTableCache map, and infosync.AddForceMergeRanges when making this
change.

---

Duplicate comments:
In `@domain/pkdb_force_merge.go`:
- Around line 112-166: The helper functions
loadOrInitMergeEmptyRegionsMinTableID, storeMergeEmptyRegionsMinTableID, and
storeMergeEmptyRegionsMinTableIDIfUnchanged must accept and use the caller's ctx
instead of always using context.Background(); add a context.Context parameter to
each function signature, replace kv.WithInternalSourceType(context.Background(),
...) with kv.WithInternalSourceType(ctx, ...), and propagate that ctx from
callers (e.g., doMergeEmptyRegions and ResetMergeEmptyRegionsMinTableID) so
checkpoint transactions respect cancellation/timeouts. Ensure all call sites are
updated to pass the caller ctx and keep existing behavior (clamping tableID to
mergeEmptyRegionsInitMinTableID) unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: f4c97b6b-141c-42f9-acfd-f5c66dde84e6

📥 Commits

Reviewing files that changed from the base of the PR and between 34e2d44 and 73cf274.

📒 Files selected for processing (2)
  • ddl/delete_range.go
  • domain/pkdb_force_merge.go

Comment thread ddl/delete_range.go
Comment on lines +275 to +292
func (dr *delRange) reportForceMergeRanges(ctx context.Context, sctx sessionctx.Context, r util.DelRangeTask, gcForceMergeTableCache map[int64]struct{}) error {
if !variable.EnableDropTableForceMerge.Load() {
return nil
}

historyJob, err := GetHistoryJobByID(sctx, r.JobID)
if err != nil {
return err
}
if historyJob == nil {
return errors.Errorf("ddl job %d not found", r.JobID)
}

forceMergeRanges := GetForceMergeRangesForGCDeleteRange(historyJob, r, gcForceMergeTableCache)
if len(forceMergeRanges) == 0 {
return nil
}
return infosync.AddForceMergeRanges(ctx, forceMergeRanges)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't mark the logical-table range as sent before PD accepts it.

GetForceMergeRangesForGCDeleteRange(...) updates gcForceMergeTableCache inside ddl/pkdb_force_merge.go:100-120 before infosync.AddForceMergeRanges(...) runs. If the first partition task for a dropped/truncated partitioned table hits a transient PD error here, the table ID stays cached and the remaining partition tasks in the same doDelRangeWork pass will skip the logical-table/global-index range entirely. That turns one failed dispatch into a permanent missed force-merge report for the logical table.

Only commit the dedupe-cache entry after a successful send, or roll it back on error.

💡 Suggested direction
 func (dr *delRange) reportForceMergeRanges(ctx context.Context, sctx sessionctx.Context, r util.DelRangeTask, gcForceMergeTableCache map[int64]struct{}) error {
 	if !variable.EnableDropTableForceMerge.Load() {
 		return nil
 	}

 	historyJob, err := GetHistoryJobByID(sctx, r.JobID)
 	if err != nil {
 		return err
 	}
 	if historyJob == nil {
 		return errors.Errorf("ddl job %d not found", r.JobID)
 	}

+	cacheSize := len(gcForceMergeTableCache)
 	forceMergeRanges := GetForceMergeRangesForGCDeleteRange(historyJob, r, gcForceMergeTableCache)
 	if len(forceMergeRanges) == 0 {
 		return nil
 	}
-	return infosync.AddForceMergeRanges(ctx, forceMergeRanges)
+	if err := infosync.AddForceMergeRanges(ctx, forceMergeRanges); err != nil {
+		if len(gcForceMergeTableCache) != cacheSize {
+			delete(gcForceMergeTableCache, historyJob.TableID)
+		}
+		return err
+	}
+	return nil
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ddl/delete_range.go` around lines 275 - 292, The code currently lets
GetForceMergeRangesForGCDeleteRange mutate gcForceMergeTableCache before calling
infosync.AddForceMergeRanges, causing a logical-table entry to be cached even if
AddForceMergeRanges fails; change the flow in delRange.reportForceMergeRanges so
that gcForceMergeTableCache is only updated after a successful
infosync.AddForceMergeRanges call (or, alternatively, make
GetForceMergeRangesForGCDeleteRange return the ranges without mutating the cache
and then update gcForceMergeTableCache inside reportForceMergeRanges only when
AddForceMergeRanges returns nil); reference the functions
delRange.reportForceMergeRanges, GetForceMergeRangesForGCDeleteRange, the
gcForceMergeTableCache map, and infosync.AddForceMergeRanges when making this
change.

@HunDunDM

Copy link
Copy Markdown
Contributor Author

/test mysql-test

@tiprow

tiprow Bot commented Apr 14, 2026

Copy link
Copy Markdown

@HunDunDM: PRs from untrusted users cannot be marked as trusted with /ok-to-test in this repo meaning untrusted PR authors can never trigger tests themselves. Collaborators can still trigger tests on the PR using /test.

Details

In response to this:

/test mysql-test

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

do-not-merge/invalid-title release-note-none Denotes a PR that doesn't merit a release note. size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants