@@ -17,9 +17,40 @@ import (
1717 "context"
1818 "testing"
1919
20+ "github.com/pingcap/errors"
21+ "github.com/pingcap/kvproto/pkg/cdcpb"
22+ "github.com/pingcap/kvproto/pkg/metapb"
23+ "github.com/pingcap/ticdc/logservice/logpuller/regionlock"
2024 "github.com/stretchr/testify/require"
25+ "github.com/tikv/client-go/v2/tikv"
26+ "google.golang.org/grpc"
27+ "google.golang.org/grpc/metadata"
2128)
2229
30+ type mockEventFeedV2Client struct {
31+ sendErr error
32+ }
33+
34+ func (m * mockEventFeedV2Client ) Send (* cdcpb.ChangeDataRequest ) error { return m .sendErr }
35+ func (m * mockEventFeedV2Client ) Recv () (* cdcpb.ChangeDataEvent , error ) { return nil , nil }
36+ func (m * mockEventFeedV2Client ) Header () (metadata.MD , error ) { return metadata.MD {}, nil }
37+ func (m * mockEventFeedV2Client ) Trailer () metadata.MD { return metadata.MD {} }
38+ func (m * mockEventFeedV2Client ) CloseSend () error { return nil }
39+ func (m * mockEventFeedV2Client ) Context () context.Context { return context .Background () }
40+ func (m * mockEventFeedV2Client ) SendMsg (any ) error { return nil }
41+ func (m * mockEventFeedV2Client ) RecvMsg (any ) error { return nil }
42+
43+ func prepareRegionForSendTest (region regionInfo ) regionInfo {
44+ region .rpcCtx = & tikv.RPCContext {
45+ Meta : & metapb.Region {
46+ RegionEpoch : & metapb.RegionEpoch {Version : 1 , ConfVer : 1 },
47+ },
48+ }
49+ region .lockedRangeState = & regionlock.LockedRangeState {}
50+ region .lockedRangeState .ResolvedTs .Store (100 )
51+ return region
52+ }
53+
2354func TestRegionStatesOperation (t * testing.T ) {
2455 worker := & regionRequestWorker {}
2556 worker .requestedRegions .subscriptions = make (map [SubscriptionID ]regionFeedStates )
@@ -64,3 +95,71 @@ func TestClearPendingRegionsReleaseSlotForPreFetchedRegion(t *testing.T) {
6495 require .Nil (t , worker .preFetchForConnecting )
6596 require .Equal (t , 0 , worker .requestCache .getPendingCount ())
6697}
98+
99+ func TestClearPendingRegionsDoesNotReturnStoppedSentRegion (t * testing.T ) {
100+ worker := & regionRequestWorker {
101+ requestCache : newRequestCache (10 ),
102+ }
103+ worker .requestedRegions .subscriptions = make (map [SubscriptionID ]regionFeedStates )
104+
105+ ctx := context .Background ()
106+ region := createTestRegionInfo (1 , 1 )
107+
108+ ok , err := worker .requestCache .add (ctx , region , false )
109+ require .NoError (t , err )
110+ require .True (t , ok )
111+
112+ req , err := worker .requestCache .pop (ctx )
113+ require .NoError (t , err )
114+
115+ state := newRegionFeedState (req .regionInfo , uint64 (req .regionInfo .subscribedSpan .subID ), worker )
116+ state .start ()
117+ worker .addRegionState (req .regionInfo .subscribedSpan .subID , req .regionInfo .verID .GetID (), state )
118+
119+ // Simulate the race we are fixing in processRegionSendTask:
120+ // once a request is visible in sentRequests, a fast region error may mark the
121+ // region stopped before worker cleanup runs. In that case, markStopped should
122+ // remove the sent request immediately, so clearPendingRegions must not return
123+ // the stale region again during worker shutdown.
124+ worker .requestCache .markSent (req )
125+ state .markStopped (errors .New ("send request to store error" ))
126+ worker .takeRegionState (req .regionInfo .subscribedSpan .subID , req .regionInfo .verID .GetID ())
127+
128+ require .Equal (t , 0 , worker .requestCache .getPendingCount ())
129+ require .Empty (t , worker .clearPendingRegions ())
130+ }
131+
132+ func TestProcessRegionSendTaskSendFailureCleansSentRequest (t * testing.T ) {
133+ worker := & regionRequestWorker {
134+ requestCache : newRequestCache (10 ),
135+ store : & requestedStore {storeAddr : "store-1" },
136+ client : & subscriptionClient {},
137+ }
138+ worker .requestedRegions .subscriptions = make (map [SubscriptionID ]regionFeedStates )
139+
140+ ctx := context .Background ()
141+ region := prepareRegionForSendTest (createTestRegionInfo (1 , 1 ))
142+
143+ ok , err := worker .requestCache .add (ctx , region , false )
144+ require .NoError (t , err )
145+ require .True (t , ok )
146+ require .Equal (t , 1 , worker .requestCache .getPendingCount ())
147+
148+ req , err := worker .requestCache .pop (ctx )
149+ require .NoError (t , err )
150+ worker .preFetchForConnecting = new (regionInfo )
151+ * worker .preFetchForConnecting = req .regionInfo
152+
153+ sendErr := errors .New ("send failed" )
154+ conn := & ConnAndClient {
155+ Client : & mockEventFeedV2Client {sendErr : sendErr },
156+ Conn : & grpc.ClientConn {},
157+ }
158+
159+ err = worker .processRegionSendTask (ctx , conn )
160+ require .ErrorIs (t , err , sendErr )
161+ require .Equal (t , 0 , worker .requestCache .getPendingCount ())
162+ require .Empty (t , worker .requestCache .sentRequests .regionReqs )
163+ state := worker .getRegionState (req .regionInfo .subscribedSpan .subID , req .regionInfo .verID .GetID ())
164+ require .True (t , state == nil || state .isStale (), "region state should be removed or marked stale after send failure" )
165+ }
0 commit comments