github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/cdc/capture/capture_test.go (about) 1 // Copyright 2022 PingCAP, Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // See the License for the specific language governing permissions and 12 // limitations under the License. 13 14 package capture 15 16 import ( 17 "context" 18 "sync" 19 "testing" 20 "time" 21 22 "github.com/golang/mock/gomock" 23 "github.com/pingcap/tiflow/cdc/model" 24 mock_owner "github.com/pingcap/tiflow/cdc/owner/mock" 25 mock_processor "github.com/pingcap/tiflow/cdc/processor/mock" 26 "github.com/pingcap/tiflow/cdc/vars" 27 "github.com/pingcap/tiflow/pkg/config" 28 "github.com/pingcap/tiflow/pkg/etcd" 29 mock_etcd "github.com/pingcap/tiflow/pkg/etcd/mock" 30 "github.com/prometheus/client_golang/prometheus" 31 "github.com/stretchr/testify/require" 32 "go.etcd.io/etcd/client/pkg/v3/logutil" 33 clientv3 "go.etcd.io/etcd/client/v3" 34 "go.uber.org/zap" 35 "go.uber.org/zap/zapcore" 36 ) 37 38 func TestReset(t *testing.T) { 39 ctx, cancel := context.WithCancel(context.Background()) 40 41 // init etcd mocker 42 clientURL, etcdServer, err := etcd.SetupEmbedEtcd(t.TempDir()) 43 require.Nil(t, err) 44 logConfig := logutil.DefaultZapLoggerConfig 45 logConfig.Level = zap.NewAtomicLevelAt(zapcore.DebugLevel) 46 etcdCli, err := clientv3.New(clientv3.Config{ 47 Endpoints: []string{clientURL.String()}, 48 Context: ctx, 49 LogConfig: &logConfig, 50 DialTimeout: 3 * time.Second, 51 }) 52 require.NoError(t, err) 53 54 client, err := etcd.NewCDCEtcdClient(ctx, etcdCli, etcd.DefaultCDCClusterID) 55 require.Nil(t, err) 56 // Close the client before the test function exits to prevent possible 57 // ctx leaks. 58 // Ref: https://github.com/grpc/grpc-go/blob/master/stream.go#L229 59 defer client.Close() 60 61 cp := NewCapture4Test(nil) 62 cp.EtcdClient = client 63 64 // simulate network isolation scenarios 65 etcdServer.Close() 66 wg := sync.WaitGroup{} 67 wg.Add(1) 68 go func() { 69 _, err = cp.reset(ctx) 70 require.Regexp(t, ".*context canceled.*", err) 71 wg.Done() 72 }() 73 time.Sleep(100 * time.Millisecond) 74 info, err := cp.Info() 75 require.Nil(t, err) 76 require.NotNil(t, info) 77 cancel() 78 wg.Wait() 79 } 80 81 type mockEtcdClient struct { 82 etcd.CDCEtcdClient 83 clientv3.Lease 84 called chan struct{} 85 } 86 87 func (m *mockEtcdClient) GetEtcdClient() *etcd.Client { 88 cli := &clientv3.Client{Lease: m} 89 return etcd.Wrap(cli, map[string]prometheus.Counter{}) 90 } 91 92 func (m *mockEtcdClient) Grant(_ context.Context, _ int64) (*clientv3.LeaseGrantResponse, error) { 93 select { 94 case m.called <- struct{}{}: 95 default: 96 } 97 return nil, context.DeadlineExceeded 98 } 99 100 func TestRetryInternalContextDeadlineExceeded(t *testing.T) { 101 ctx, cancel := context.WithCancel(context.Background()) 102 103 called := make(chan struct{}, 2) 104 cp := NewCapture4Test(nil) 105 // In the current implementation, the first RPC is grant. 106 // the mock client always retry DeadlineExceeded for the RPC. 107 cp.EtcdClient = &mockEtcdClient{called: called} 108 109 errCh := make(chan error, 1) 110 go func() { 111 errCh <- cp.Run(ctx) 112 }() 113 time.Sleep(100 * time.Millisecond) 114 // Waiting for Grant to be called. 115 <-called 116 time.Sleep(100 * time.Millisecond) 117 // Make sure it retrys 118 <-called 119 120 // Do not retry context canceled. 121 cancel() 122 select { 123 case err := <-errCh: 124 require.NoError(t, err) 125 case <-time.After(5 * time.Second): 126 require.Fail(t, "timeout") 127 } 128 } 129 130 func TestInfo(t *testing.T) { 131 cp := NewCapture4Test(nil) 132 cp.info = nil 133 require.NotPanics(t, func() { cp.Info() }) 134 } 135 136 func TestDrainCaptureBySignal(t *testing.T) { 137 t.Parallel() 138 139 ctrl := gomock.NewController(t) 140 mm := mock_processor.NewMockManager(ctrl) 141 me := mock_etcd.NewMockCDCEtcdClient(ctrl) 142 cp := &captureImpl{ 143 info: &model.CaptureInfo{ 144 ID: "capture-for-test", 145 AdvertiseAddr: "127.0.0.1", Version: "test", 146 }, 147 processorManager: mm, 148 config: config.GetDefaultServerConfig(), 149 EtcdClient: me, 150 } 151 require.Equal(t, model.LivenessCaptureAlive, cp.Liveness()) 152 153 done := cp.Drain() 154 select { 155 case <-done: 156 require.Equal(t, model.LivenessCaptureStopping, cp.Liveness()) 157 case <-time.After(time.Second): 158 require.Fail(t, "timeout") 159 } 160 } 161 162 func TestDrainWaitsOwnerResign(t *testing.T) { 163 t.Parallel() 164 165 ctrl := gomock.NewController(t) 166 mo := mock_owner.NewMockOwner(ctrl) 167 mm := mock_processor.NewMockManager(ctrl) 168 me := mock_etcd.NewMockCDCEtcdClient(ctrl) 169 cp := &captureImpl{ 170 EtcdClient: me, 171 info: &model.CaptureInfo{ 172 ID: "capture-for-test", 173 AdvertiseAddr: "127.0.0.1", Version: "test", 174 }, 175 processorManager: mm, 176 owner: mo, 177 config: config.GetDefaultServerConfig(), 178 } 179 require.Equal(t, model.LivenessCaptureAlive, cp.Liveness()) 180 181 mo.EXPECT().AsyncStop().Do(func() {}).AnyTimes() 182 183 done := cp.Drain() 184 select { 185 case <-time.After(3 * time.Second): 186 require.Fail(t, "timeout") 187 case <-done: 188 require.Equal(t, model.LivenessCaptureStopping, cp.Liveness()) 189 } 190 } 191 192 type mockElection struct { 193 campaignRequestCh chan struct{} 194 campaignGrantCh chan struct{} 195 196 campaignFlag, resignFlag bool 197 } 198 199 func (e *mockElection) campaign(ctx context.Context, key string) error { 200 e.campaignRequestCh <- struct{}{} 201 <-e.campaignGrantCh 202 e.campaignFlag = true 203 return nil 204 } 205 206 func (e *mockElection) resign(ctx context.Context) error { 207 e.resignFlag = true 208 return nil 209 } 210 211 func TestCampaignLiveness(t *testing.T) { 212 t.Parallel() 213 214 me := &mockElection{ 215 campaignRequestCh: make(chan struct{}, 1), 216 campaignGrantCh: make(chan struct{}, 1), 217 } 218 cp := &captureImpl{ 219 config: config.GetDefaultServerConfig(), 220 info: &model.CaptureInfo{ID: "test"}, 221 election: me, 222 } 223 globalVars := vars.NewGlobalVars4Test() 224 ctx := context.Background() 225 cp.liveness.Store(model.LivenessCaptureStopping) 226 err := cp.campaignOwner(ctx, globalVars) 227 require.Nil(t, err) 228 require.False(t, me.campaignFlag) 229 230 // Force set alive. 231 cp.liveness = model.LivenessCaptureAlive 232 wg := sync.WaitGroup{} 233 wg.Add(1) 234 go func() { 235 defer wg.Done() 236 // Grant campaign 237 g := <-me.campaignRequestCh 238 // Set liveness to stopping 239 cp.liveness.Store(model.LivenessCaptureStopping) 240 me.campaignGrantCh <- g 241 }() 242 err = cp.campaignOwner(ctx, globalVars) 243 require.Nil(t, err) 244 require.True(t, me.campaignFlag) 245 require.True(t, me.resignFlag) 246 247 wg.Wait() 248 }