Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
a1207a5
logpuller: send cdc scan priority
hongyunyan Jun 23, 2026
638d564
logpuller: preserve scan priority for retries
hongyunyan Jun 26, 2026
4e95247
logservice: add changefeed scan task logs
hongyunyan Jun 26, 2026
367bf9c
logpuller: classify initial scan priority by start ts
hongyunyan Jun 27, 2026
d591557
logpuller: protect realtime recovery scans
hongyunyan Jun 29, 2026
efc23de
logpuller: merge latest master for scan priority
hongyunyan Jul 7, 2026
871a7ff
logservice: keep changefeed id in event store merge
hongyunyan Jul 7, 2026
3a26268
logservice: modernize priority scan tests
hongyunyan Jul 7, 2026
51619e1
logpuller: simplify scan priority scheduling
hongyunyan Jul 7, 2026
da6b17a
logpuller: simplify priority scan diagnostics
hongyunyan Jul 7, 2026
ab05078
logpuller: simplify task priority logging
hongyunyan Jul 7, 2026
945e05b
logpuller,grafana: remove cse changefeed id
hongyunyan Jul 8, 2026
0d7ab0b
grafana: fix duplicate scan scheduler panel ids
hongyunyan Jul 8, 2026
90ada3a
logservice: drop redundant changefeed ID plumbing
hongyunyan Jul 8, 2026
343e555
eventstore: drop redundant register changefeed id
hongyunyan Jul 8, 2026
b0435c6
Merge remote-tracking branch 'upstream/master' into codex/cdc-scan-pr…
hongyunyan Jul 14, 2026
73f1c2f
eventstore: restore changefeed ID in registration
hongyunyan Jul 14, 2026
e281c0e
logpuller: simplify scan priority state handling
hongyunyan Jul 15, 2026
3b8c6ae
logpuller: merge latest master
hongyunyan Jul 15, 2026
0d9423a
logpuller: clean up caught-up priority handling
hongyunyan Jul 15, 2026
c7ea317
Update logservice/logpuller/region_state.go
hongyunyan Jul 16, 2026
a8ca4d3
logpuller: merge latest master and update kvproto
hongyunyan Jul 16, 2026
de79c1b
logpuller: centralize scan priority policy
hongyunyan Jul 17, 2026
5303b45
logpuller: clean up scan priority comment
hongyunyan Jul 17, 2026
0bfb025
logpuller: record scan priority with resolved ts
hongyunyan Jul 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ require (
github.com/pierrec/lz4/v4 v4.1.26
github.com/pingcap/errors v0.11.5-0.20260508054701-306e305bcf41
github.com/pingcap/failpoint v0.0.0-20240528011301-b51a646c7c86
github.com/pingcap/kvproto v0.0.0-20260601035955-b2b3bb492278
github.com/pingcap/kvproto v0.0.0-20260716092138-22607720b19a
github.com/pingcap/log v1.1.1-0.20250917021125-19901e015dc9
github.com/pingcap/sysutil v1.0.1-0.20240311050922-ae81ee01f3a5
github.com/pingcap/tidb v1.1.0-beta.0.20260604031706-f9faeaf4828f
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -750,8 +750,8 @@ github.com/pingcap/fn v1.0.0/go.mod h1:u9WZ1ZiOD1RpNhcI42RucFh/lBuzTu6rw88a+oF2Z
github.com/pingcap/goleveldb v0.0.0-20191226122134-f82aafb29989 h1:surzm05a8C9dN8dIUmo4Be2+pMRb6f55i+UIYrluu2E=
github.com/pingcap/goleveldb v0.0.0-20191226122134-f82aafb29989/go.mod h1:O17XtbryoCJhkKGbT62+L2OlrniwqiGLSqrmdHCMzZw=
github.com/pingcap/kvproto v0.0.0-20191211054548-3c6b38ea5107/go.mod h1:WWLmULLO7l8IOcQG+t+ItJ3fEcrL5FxF0Wu+HrMy26w=
github.com/pingcap/kvproto v0.0.0-20260601035955-b2b3bb492278 h1:VV03wSSG2XhOwhF79lvT88Z83lwzMT+aZHXrcCIN9B0=
github.com/pingcap/kvproto v0.0.0-20260601035955-b2b3bb492278/go.mod h1:z6+aAHB7dBkA+LyinEX+48/ImRJ3jag0Hg0c7wkhEvE=
github.com/pingcap/kvproto v0.0.0-20260716092138-22607720b19a h1:byZN6kkQEzeBfkrA7DMasEJciMwpKLNuK2jZiJH1N+o=
github.com/pingcap/kvproto v0.0.0-20260716092138-22607720b19a/go.mod h1:z6+aAHB7dBkA+LyinEX+48/ImRJ3jag0Hg0c7wkhEvE=
github.com/pingcap/log v0.0.0-20191012051959-b742a5d432e9/go.mod h1:4rbK1p9ILyIfb6hU7OG2CiWSqMXnp3JMbiaVJ6mvoY8=
github.com/pingcap/log v0.0.0-20210625125904-98ed8e2eb1c7/go.mod h1:8AanEdAHATuRurdGxZXBz0At+9avep+ub7U1AGYLIMM=
github.com/pingcap/log v1.1.0/go.mod h1:DWQW5jICDR7UJh4HtxXSM20Churx4CQL0fwL/SoOSA4=
Expand Down
33 changes: 31 additions & 2 deletions logservice/logpuller/priority_task.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
package logpuller

import (
"fmt"
"time"

"github.com/pingcap/kvproto/pkg/cdcpb"
"github.com/tikv/client-go/v2/oracle"
)

Expand All @@ -39,7 +39,36 @@ const (
)

func (t TaskType) String() string {
return fmt.Sprintf("%d", t)
switch t {
case TaskHighPrior:
return "high"
case TaskLowPrior:
return "low"
default:
return "unknown"
}
}

func (t TaskType) scanPriority() cdcpb.ScanPriority {
switch t {
case TaskHighPrior:
return cdcpb.ScanPriority_SCAN_PRIORITY_HIGH
case TaskLowPrior:
return cdcpb.ScanPriority_SCAN_PRIORITY_LOW
default:
return cdcpb.ScanPriority_SCAN_PRIORITY_LOW
}
}

func taskTypeFromScanPriority(priority cdcpb.ScanPriority) TaskType {
if priority == cdcpb.ScanPriority_SCAN_PRIORITY_HIGH {
return TaskHighPrior
}
return TaskLowPrior
}

func normalizeScanPriority(priority cdcpb.ScanPriority) cdcpb.ScanPriority {
return taskTypeFromScanPriority(priority).scanPriority()
}

// PriorityTask is the interface for priority-based tasks
Expand Down
10 changes: 10 additions & 0 deletions logservice/logpuller/priority_task_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,23 @@ import (
"testing"
"time"

"github.com/pingcap/kvproto/pkg/cdcpb"
"github.com/pingcap/ticdc/heartbeatpb"
"github.com/pingcap/ticdc/utils/priorityqueue"
"github.com/stretchr/testify/require"
"github.com/tikv/client-go/v2/oracle"
"github.com/tikv/client-go/v2/tikv"
)

func TestTaskTypeScanPriorityMapping(t *testing.T) {
require.Equal(t, cdcpb.ScanPriority_SCAN_PRIORITY_HIGH, TaskHighPrior.scanPriority())
require.Equal(t, cdcpb.ScanPriority_SCAN_PRIORITY_LOW, TaskLowPrior.scanPriority())
require.Equal(t, TaskHighPrior, taskTypeFromScanPriority(cdcpb.ScanPriority_SCAN_PRIORITY_HIGH))
require.Equal(t, TaskLowPrior, taskTypeFromScanPriority(cdcpb.ScanPriority_SCAN_PRIORITY_LOW))
require.Equal(t, TaskLowPrior, taskTypeFromScanPriority(cdcpb.ScanPriority_SCAN_PRIORITY_UNKNOWN))
require.Equal(t, cdcpb.ScanPriority_SCAN_PRIORITY_LOW, normalizeScanPriority(cdcpb.ScanPriority_SCAN_PRIORITY_UNKNOWN))
}

// TestPriorityCalculationLogic tests the priority calculation logic in isolation
func TestPriorityCalculationLogic(t *testing.T) {
currentTime := time.Now()
Expand Down
3 changes: 1 addition & 2 deletions logservice/logpuller/region_event_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -422,8 +422,7 @@ func handleResolvedTs(span *subscribedSpan, state *regionFeedState, resolvedTs u
zap.Uint64("lastResolvedTs", lastResolvedTs),
zap.Float64("decreaseLag(s)", decreaseLag))
}
span.resolvedTs.Store(ts)
span.resolvedTsUpdated.Store(time.Now().Unix())
span.recordResolvedTs(ts)
return ts
}
}
Expand Down
26 changes: 18 additions & 8 deletions logservice/logpuller/region_event_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"github.com/pingcap/ticdc/heartbeatpb"
"github.com/pingcap/ticdc/logservice/logpuller/regionlock"
"github.com/pingcap/ticdc/pkg/common"
"github.com/pingcap/ticdc/pkg/pdutil"
"github.com/pingcap/ticdc/utils/dynstream"
"github.com/stretchr/testify/require"
"github.com/tikv/client-go/v2/tikv"
Expand Down Expand Up @@ -210,6 +211,8 @@ func TestHandleEventEntryEventOutOfOrder(t *testing.T) {
func TestHandleResolvedTs(t *testing.T) {
// initialize
option := dynstream.NewOption()
pdClock := pdutil.NewClock4Test()
pdClock.(*pdutil.Clock4Test).SetTS(10)
ds := dynstream.NewParallelDynamicStream("test", &regionEventHandler{}, option)
ds.Start()

Expand All @@ -225,23 +228,25 @@ func TestHandleResolvedTs(t *testing.T) {
}
state1 := newRegionFeedState(regionInfo{verID: tikv.NewRegionVerID(1, 1, 1)}, uint64(subID1), worker)
state1.start()
var subSpan1 *subscribedSpan
{
span := heartbeatpb.TableSpan{
TableID: 100,
StartKey: common.ToComparableKey([]byte{}), // TODO: remove spanz dependency
EndKey: common.ToComparableKey(common.UpperBoundKey),
}
subSpan := &subscribedSpan{
subSpan1 = &subscribedSpan{
subID: subID1,
span: heartbeatpb.TableSpan{},
rangeLock: regionlock.NewRangeLock(uint64(subID1), span.StartKey, span.EndKey, 1),
consumeKVEvents: consumeKVEvents,
advanceResolvedTs: advanceResolvedTs,
advanceInterval: 0,
priorityPolicy: newScanPriorityPolicy(pdClock, 30*time.Minute),
}
ds.AddPath(subID1, subSpan, dynstream.AreaSettings{})
state1.region.subscribedSpan = subSpan
lockResult := subSpan.rangeLock.LockRange(
ds.AddPath(subID1, subSpan1, dynstream.AreaSettings{})
state1.region.subscribedSpan = subSpan1
lockResult := subSpan1.rangeLock.LockRange(
context.Background(), span.StartKey, span.EndKey, 1, 1)
require.Equal(t, regionlock.LockRangeStatusSuccess, lockResult.Status)
state1.region.lockedRangeState = lockResult.LockedRangeState
Expand All @@ -265,6 +270,7 @@ func TestHandleResolvedTs(t *testing.T) {
consumeKVEvents: consumeKVEvents,
advanceResolvedTs: advanceResolvedTs,
advanceInterval: 0,
priorityPolicy: newScanPriorityPolicy(pdClock, 30*time.Minute),
}
ds.AddPath(subID2, subSpan, dynstream.AreaSettings{})
state2.region.subscribedSpan = subSpan
Expand Down Expand Up @@ -292,6 +298,7 @@ func TestHandleResolvedTs(t *testing.T) {
consumeKVEvents: consumeKVEvents,
advanceResolvedTs: advanceResolvedTs,
advanceInterval: 0,
priorityPolicy: newScanPriorityPolicy(pdClock, 30*time.Minute),
}
ds.AddPath(subID3, subSpan, dynstream.AreaSettings{})
state3.region.subscribedSpan = subSpan
Expand Down Expand Up @@ -343,6 +350,7 @@ func TestHandleResolvedTs(t *testing.T) {
require.Equal(t, uint64(10), state1.getLastResolvedTs())
require.Equal(t, uint64(11), state2.getLastResolvedTs())
require.Equal(t, uint64(8), state3.getLastResolvedTs())
require.True(t, subSpan1.priorityPolicy.everCaughtUp.Load())
}

func TestHandleResolvedTsThrottled(t *testing.T) {
Expand Down Expand Up @@ -372,6 +380,7 @@ func TestHandleResolvedTsThrottled(t *testing.T) {
subID: SubscriptionID(1),
rangeLock: l,
advanceInterval: 100,
priorityPolicy: newScanPriorityPolicy(pdutil.NewClock4Test(), 30*time.Minute),
}
span.lastAdvanceTime.Store(0)
state := newRegionFeedState(
Expand All @@ -397,10 +406,11 @@ func TestSpanInitializedAfterAllRangesInitialized(t *testing.T) {
require.Equal(t, regionlock.LockRangeStatusSuccess, secondLock.Status)

span := &subscribedSpan{
subID: SubscriptionID(1),
startTs: 100,
span: heartbeatpb.TableSpan{StartKey: []byte("a"), EndKey: []byte("z")},
rangeLock: rangeLock,
subID: SubscriptionID(1),
startTs: 100,
span: heartbeatpb.TableSpan{StartKey: []byte("a"), EndKey: []byte("z")},
rangeLock: rangeLock,
priorityPolicy: newScanPriorityPolicy(pdutil.NewClock4Test(), 30*time.Minute),
}
span.resolvedTs.Store(span.startTs)
worker := &regionRequestWorker{requestCache: newRequestCache(2)}
Expand Down
19 changes: 10 additions & 9 deletions logservice/logpuller/region_failure_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ func (r *regionFailureHandler) Run(ctx context.Context) error {

func (r *regionFailureHandler) handleError(ctx context.Context, errInfo regionErrorInfo) error {
err := errors.Cause(errInfo.err)
retryPriority := taskTypeFromScanPriority(errInfo.scanPriority)
//nolint:errorlint // converting large type switch to errors.As is a significant refactor
if _, requestCancelled := err.(*requestCancelledErr); !requestCancelled {
log.Debug("cdc region error",
Expand All @@ -122,27 +123,27 @@ func (r *regionFailureHandler) handleError(ctx context.Context, errInfo regionEr
if notLeader := innerErr.GetNotLeader(); notLeader != nil {
metricFeedNotLeaderCounter.Inc()
r.client.regionCache.UpdateLeader(errInfo.verID, notLeader.GetLeader(), errInfo.rpcCtx.AccessIdx)
r.client.scheduleRegionRequest(ctx, errInfo.regionInfo, TaskHighPrior)
r.client.scheduleRegionRequest(ctx, errInfo.regionInfo, retryPriority)
return nil
}
if innerErr.GetEpochNotMatch() != nil {
metricFeedEpochNotMatchCounter.Inc()
r.client.scheduleRangeRequest(ctx, errInfo.span, errInfo.subscribedSpan, errInfo.filterLoop, TaskHighPrior)
r.client.scheduleRangeRequest(ctx, errInfo.span, errInfo.subscribedSpan, errInfo.filterLoop, retryPriority)
return nil
}
if innerErr.GetRegionNotFound() != nil {
metricFeedRegionNotFoundCounter.Inc()
r.client.scheduleRangeRequest(ctx, errInfo.span, errInfo.subscribedSpan, errInfo.filterLoop, TaskHighPrior)
r.client.scheduleRangeRequest(ctx, errInfo.span, errInfo.subscribedSpan, errInfo.filterLoop, retryPriority)
return nil
}
if innerErr.GetCongested() != nil {
metricKvCongestedCounter.Inc()
r.client.scheduleRegionRequest(ctx, errInfo.regionInfo, TaskLowPrior)
r.client.scheduleRegionRequest(ctx, errInfo.regionInfo, retryPriority)
return nil
}
if innerErr.GetServerIsBusy() != nil {
metricKvIsBusyCounter.Inc()
r.client.scheduleRegionRequest(ctx, errInfo.regionInfo, TaskLowPrior)
r.client.scheduleRegionRequest(ctx, errInfo.regionInfo, retryPriority)
return nil
}
if duplicated := innerErr.GetDuplicateRequest(); duplicated != nil {
Expand All @@ -161,24 +162,24 @@ func (r *regionFailureHandler) handleError(ctx context.Context, errInfo regionEr
zap.Uint64("subscriptionID", uint64(errInfo.subscribedSpan.subID)),
zap.Stringer("error", innerErr))
metricFeedUnknownErrorCounter.Inc()
r.client.scheduleRegionRequest(ctx, errInfo.regionInfo, TaskHighPrior)
r.client.scheduleRegionRequest(ctx, errInfo.regionInfo, retryPriority)
return nil
case *rpcCtxUnavailableErr:
metricFeedRPCCtxUnavailable.Inc()
r.client.scheduleRangeRequest(ctx, errInfo.span, errInfo.subscribedSpan, errInfo.filterLoop, TaskHighPrior)
r.client.scheduleRangeRequest(ctx, errInfo.span, errInfo.subscribedSpan, errInfo.filterLoop, retryPriority)
return nil
case *getStoreErr:
metricGetStoreErr.Inc()
bo := tikv.NewBackoffer(ctx, tikvRequestMaxBackoff)
// cannot get the store the region belongs to, so we need to reload the region.
r.client.regionCache.OnSendFail(bo, errInfo.rpcCtx, true, err)
r.client.scheduleRangeRequest(ctx, errInfo.span, errInfo.subscribedSpan, errInfo.filterLoop, TaskHighPrior)
r.client.scheduleRangeRequest(ctx, errInfo.span, errInfo.subscribedSpan, errInfo.filterLoop, retryPriority)
return nil
case *storeStreamErr:
metricStoreSendRequestErr.Inc()
bo := tikv.NewBackoffer(ctx, tikvRequestMaxBackoff)
r.client.regionCache.OnSendFail(bo, errInfo.rpcCtx, regionScheduleReload, err)
r.client.scheduleRegionRequest(ctx, errInfo.regionInfo, TaskHighPrior)
r.client.scheduleRegionRequest(ctx, errInfo.regionInfo, retryPriority)
return nil
case *requestCancelledErr:
// the corresponding subscription has been unsubscribed, just ignore.
Expand Down
1 change: 1 addition & 0 deletions logservice/logpuller/region_request_worker.go
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,7 @@ func (s *regionRequestWorker) createRegionRequest(region regionInfo) *cdcpb.Chan
EndKey: region.span.EndKey,
ExtraOp: kvrpcpb.ExtraOp_ReadOldValue,
FilterLoop: region.filterLoop,
ScanPriority: normalizeScanPriority(region.scanPriority),
}
}

Expand Down
36 changes: 36 additions & 0 deletions logservice/logpuller/region_request_worker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,42 @@ func prepareRegionForSendTest(region regionInfo) regionInfo {
return region
}

func TestCreateRegionRequestScanPriority(t *testing.T) {
worker := &regionRequestWorker{
client: &subscriptionClient{clusterID: 1},
}

for _, tc := range []struct {
name string
priority cdcpb.ScanPriority
expected cdcpb.ScanPriority
}{
{
name: "high",
priority: cdcpb.ScanPriority_SCAN_PRIORITY_HIGH,
expected: cdcpb.ScanPriority_SCAN_PRIORITY_HIGH,
},
{
name: "low",
priority: cdcpb.ScanPriority_SCAN_PRIORITY_LOW,
expected: cdcpb.ScanPriority_SCAN_PRIORITY_LOW,
},
{
name: "unknown defaults to low",
priority: cdcpb.ScanPriority_SCAN_PRIORITY_UNKNOWN,
expected: cdcpb.ScanPriority_SCAN_PRIORITY_LOW,
},
} {
t.Run(tc.name, func(t *testing.T) {
region := prepareRegionForSendTest(createTestRegionInfo(1, 1))
region.scanPriority = tc.priority

req := worker.createRegionRequest(region)
require.Equal(t, tc.expected, req.GetScanPriority())
})
}
}

func TestRegionStatesOperation(t *testing.T) {
worker := &regionRequestWorker{}
worker.requestedRegions.subscriptions = make(map[SubscriptionID]regionFeedStates)
Expand Down
5 changes: 5 additions & 0 deletions logservice/logpuller/region_state.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package logpuller
import (
"sync"

"github.com/pingcap/kvproto/pkg/cdcpb"
"github.com/pingcap/ticdc/heartbeatpb"
"github.com/pingcap/ticdc/logservice/logpuller/regionlock"
"github.com/tikv/client-go/v2/tikv"
Expand Down Expand Up @@ -46,6 +47,9 @@ type regionInfo struct {
// Whether to filter out the value write by cdc itself.
// It should be `true` in BDR mode
filterLoop bool
// scanPriority is sent to TiKV/CSE so remote incremental scan admission can
// preserve TiCDC's business priority across retries.
scanPriority cdcpb.ScanPriority
}

func (s *regionInfo) isStopped() bool {
Expand All @@ -66,6 +70,7 @@ func newRegionInfo(
rpcCtx: rpcCtx,
subscribedSpan: subscribedSpan,
filterLoop: filterLoop,
scanPriority: TaskLowPrior.scanPriority(),
}
}

Expand Down
Loading
Loading