github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/cdc/processor/sinkmanager/table_sink_wrapper_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 sinkmanager 15 16 import ( 17 "context" 18 "math" 19 "sync" 20 "testing" 21 "time" 22 23 "github.com/pingcap/tiflow/cdc/model" 24 "github.com/pingcap/tiflow/cdc/processor/tablepb" 25 "github.com/pingcap/tiflow/cdc/sink/dmlsink" 26 "github.com/pingcap/tiflow/cdc/sink/tablesink" 27 "github.com/pingcap/tiflow/pkg/pdutil" 28 "github.com/pingcap/tiflow/pkg/sink" 29 "github.com/pingcap/tiflow/pkg/spanz" 30 "github.com/prometheus/client_golang/prometheus" 31 "github.com/stretchr/testify/require" 32 "github.com/tikv/client-go/v2/oracle" 33 ) 34 35 type mockSink struct { 36 mu sync.Mutex 37 events []*dmlsink.CallbackableEvent[*model.RowChangedEvent] 38 writeTimes int 39 } 40 41 func newMockSink() *mockSink { 42 return &mockSink{ 43 events: make([]*dmlsink.CallbackableEvent[*model.RowChangedEvent], 0), 44 } 45 } 46 47 func (m *mockSink) WriteEvents(events ...*dmlsink.CallbackableEvent[*model.RowChangedEvent]) error { 48 m.mu.Lock() 49 defer m.mu.Unlock() 50 m.writeTimes++ 51 m.events = append(m.events, events...) 52 return nil 53 } 54 55 func (m *mockSink) Scheme() string { 56 return sink.BlackHoleScheme 57 } 58 59 func (m *mockSink) GetEvents() []*dmlsink.CallbackableEvent[*model.RowChangedEvent] { 60 m.mu.Lock() 61 defer m.mu.Unlock() 62 return m.events 63 } 64 65 func (m *mockSink) GetWriteTimes() int { 66 m.mu.Lock() 67 defer m.mu.Unlock() 68 return m.writeTimes 69 } 70 71 func (m *mockSink) Close() {} 72 73 func (m *mockSink) Dead() <-chan struct{} { 74 return make(chan struct{}) 75 } 76 77 func (m *mockSink) AckAllEvents() { 78 m.mu.Lock() 79 defer m.mu.Unlock() 80 for _, e := range m.events { 81 e.Callback() 82 } 83 } 84 85 type mockDelayedTableSink struct { 86 tablesink.TableSink 87 88 closeCnt int 89 closeTarget int 90 } 91 92 func (t *mockDelayedTableSink) AsyncClose() bool { 93 t.closeCnt++ 94 if t.closeCnt >= t.closeTarget { 95 t.TableSink.Close() 96 return true 97 } 98 return false 99 } 100 101 //nolint:unparam 102 func createTableSinkWrapper( 103 changefeedID model.ChangeFeedID, span tablepb.Span, 104 ) (*tableSinkWrapper, *mockSink) { 105 tableState := tablepb.TableStatePreparing 106 sink := newMockSink() 107 innerTableSink := tablesink.New[*model.RowChangedEvent]( 108 changefeedID, span, model.Ts(0), 109 sink, &dmlsink.RowChangeEventAppender{}, 110 pdutil.NewClock4Test(), 111 prometheus.NewCounter(prometheus.CounterOpts{}), 112 prometheus.NewHistogram(prometheus.HistogramOpts{})) 113 wrapper := newTableSinkWrapper( 114 changefeedID, 115 span, 116 func() (tablesink.TableSink, uint64) { return innerTableSink, 1 }, 117 tableState, 118 0, 119 100, 120 func(_ context.Context) (model.Ts, error) { return math.MaxUint64, nil }, 121 ) 122 wrapper.tableSink.s, wrapper.tableSink.version = wrapper.tableSinkCreator() 123 return wrapper, sink 124 } 125 126 func TestTableSinkWrapperStop(t *testing.T) { 127 t.Parallel() 128 129 wrapper, _ := createTableSinkWrapper( 130 model.DefaultChangeFeedID("1"), spanz.TableIDToComparableSpan(1)) 131 wrapper.tableSink.s = &mockDelayedTableSink{ 132 TableSink: wrapper.tableSink.s, 133 closeCnt: 0, 134 closeTarget: 10, 135 } 136 require.Equal(t, tablepb.TableStatePreparing, wrapper.getState()) 137 138 closeCnt := 0 139 for { 140 closeCnt++ 141 if wrapper.asyncStop() { 142 break 143 } 144 } 145 require.Equal(t, tablepb.TableStateStopped, wrapper.getState(), "table sink state should be stopped") 146 require.Equal(t, 10, closeCnt, "table sink should be closed 10 times") 147 } 148 149 func TestUpdateReceivedSorterResolvedTs(t *testing.T) { 150 t.Parallel() 151 152 wrapper, _ := createTableSinkWrapper( 153 model.DefaultChangeFeedID("1"), spanz.TableIDToComparableSpan(1)) 154 wrapper.updateReceivedSorterResolvedTs(100) 155 require.Equal(t, uint64(100), wrapper.getReceivedSorterResolvedTs()) 156 require.Equal(t, tablepb.TableStatePrepared, wrapper.getState()) 157 } 158 159 func TestHandleNilRowChangedEvents(t *testing.T) { 160 t.Parallel() 161 162 events := []*model.PolymorphicEvent{nil} 163 changefeedID := model.DefaultChangeFeedID("1") 164 span := spanz.TableIDToComparableSpan(1) 165 result, size := handleRowChangedEvents(changefeedID, span, events...) 166 require.Equal(t, 0, len(result)) 167 require.Equal(t, uint64(0), size) 168 } 169 170 func TestHandleEmptyRowChangedEvents(t *testing.T) { 171 t.Parallel() 172 173 events := []*model.PolymorphicEvent{ 174 { 175 StartTs: 1, 176 CRTs: 2, 177 // the row had no columns 178 Row: &model.RowChangedEvent{ 179 StartTs: 1, 180 CommitTs: 2, 181 }, 182 }, 183 } 184 changefeedID := model.DefaultChangeFeedID("1") 185 span := spanz.TableIDToComparableSpan(1) 186 187 result, size := handleRowChangedEvents(changefeedID, span, events...) 188 require.Equal(t, 0, len(result)) 189 require.Equal(t, uint64(0), size) 190 } 191 192 func TestHandleRowChangedEventNormalEvent(t *testing.T) { 193 t.Parallel() 194 195 // Update non-unique key. 196 columns := []*model.Column{ 197 { 198 Name: "col1", 199 Flag: model.BinaryFlag, 200 Value: "col1-value", 201 }, 202 { 203 Name: "col2", 204 Flag: model.HandleKeyFlag | model.UniqueKeyFlag, 205 Value: "col2-value-updated", 206 }, 207 } 208 preColumns := []*model.Column{ 209 { 210 Name: "col1", 211 Flag: model.BinaryFlag, 212 Value: "col1-value", 213 }, 214 { 215 Name: "col2", 216 Flag: model.HandleKeyFlag | model.UniqueKeyFlag, 217 Value: "col2-value", 218 }, 219 } 220 tableInfo := model.BuildTableInfo("test", "test", columns, nil) 221 events := []*model.PolymorphicEvent{ 222 { 223 CRTs: 1, 224 RawKV: &model.RawKVEntry{OpType: model.OpTypePut}, 225 Row: &model.RowChangedEvent{ 226 CommitTs: 1, 227 TableInfo: tableInfo, 228 Columns: model.Columns2ColumnDatas(columns, tableInfo), 229 PreColumns: model.Columns2ColumnDatas(preColumns, tableInfo), 230 }, 231 }, 232 } 233 changefeedID := model.DefaultChangeFeedID("1") 234 span := spanz.TableIDToComparableSpan(1) 235 result, size := handleRowChangedEvents(changefeedID, span, events...) 236 require.Equal(t, 1, len(result)) 237 require.Equal(t, uint64(testEventSize), size) 238 } 239 240 func TestGetUpperBoundTs(t *testing.T) { 241 t.Parallel() 242 wrapper, _ := createTableSinkWrapper( 243 model.DefaultChangeFeedID("1"), spanz.TableIDToComparableSpan(1)) 244 // Test when there is no resolved ts. 245 wrapper.barrierTs.Store(uint64(10)) 246 wrapper.receivedSorterResolvedTs.Store(uint64(11)) 247 require.Equal(t, uint64(10), wrapper.getUpperBoundTs()) 248 249 wrapper.barrierTs.Store(uint64(12)) 250 require.Equal(t, uint64(11), wrapper.getUpperBoundTs()) 251 } 252 253 func TestNewTableSinkWrapper(t *testing.T) { 254 t.Parallel() 255 wrapper := newTableSinkWrapper( 256 model.DefaultChangeFeedID("1"), 257 spanz.TableIDToComparableSpan(1), 258 nil, 259 tablepb.TableStatePrepared, 260 model.Ts(10), 261 model.Ts(20), 262 func(_ context.Context) (model.Ts, error) { return math.MaxUint64, nil }, 263 ) 264 require.NotNil(t, wrapper) 265 require.Equal(t, uint64(10), wrapper.getUpperBoundTs()) 266 require.Equal(t, uint64(10), wrapper.getReceivedSorterResolvedTs()) 267 checkpointTs := wrapper.getCheckpointTs() 268 require.Equal(t, uint64(10), checkpointTs.ResolvedMark()) 269 } 270 271 func TestTableSinkWrapperSinkVersion(t *testing.T) { 272 t.Parallel() 273 274 innerTableSink := tablesink.New[*model.RowChangedEvent]( 275 model.ChangeFeedID{}, tablepb.Span{}, model.Ts(0), 276 newMockSink(), &dmlsink.RowChangeEventAppender{}, 277 pdutil.NewClock4Test(), 278 prometheus.NewCounter(prometheus.CounterOpts{}), 279 prometheus.NewHistogram(prometheus.HistogramOpts{}), 280 ) 281 version := new(uint64) 282 283 wrapper := newTableSinkWrapper( 284 model.DefaultChangeFeedID("1"), 285 spanz.TableIDToComparableSpan(1), 286 func() (tablesink.TableSink, uint64) { return nil, 0 }, 287 tablepb.TableStatePrepared, 288 model.Ts(10), 289 model.Ts(20), 290 func(_ context.Context) (model.Ts, error) { return math.MaxUint64, nil }, 291 ) 292 293 require.False(t, wrapper.initTableSink()) 294 295 wrapper.tableSinkCreator = func() (tablesink.TableSink, uint64) { 296 *version += 1 297 return innerTableSink, *version 298 } 299 300 require.True(t, wrapper.initTableSink()) 301 require.Equal(t, wrapper.tableSink.version, uint64(1)) 302 303 require.True(t, wrapper.asyncCloseTableSink()) 304 305 wrapper.doTableSinkClear() 306 require.Nil(t, wrapper.tableSink.s) 307 require.Equal(t, wrapper.tableSink.version, uint64(0)) 308 309 require.True(t, wrapper.initTableSink()) 310 require.Equal(t, wrapper.tableSink.version, uint64(2)) 311 312 wrapper.closeTableSink() 313 314 wrapper.doTableSinkClear() 315 require.Nil(t, wrapper.tableSink.s) 316 require.Equal(t, wrapper.tableSink.version, uint64(0)) 317 } 318 319 func TestTableSinkWrapperSinkInner(t *testing.T) { 320 t.Parallel() 321 322 innerTableSink := tablesink.New[*model.RowChangedEvent]( 323 model.ChangeFeedID{}, tablepb.Span{}, model.Ts(0), 324 newMockSink(), &dmlsink.RowChangeEventAppender{}, 325 pdutil.NewClock4Test(), 326 prometheus.NewCounter(prometheus.CounterOpts{}), 327 prometheus.NewHistogram(prometheus.HistogramOpts{}), 328 ) 329 version := new(uint64) 330 331 wrapper := newTableSinkWrapper( 332 model.DefaultChangeFeedID("1"), 333 spanz.TableIDToComparableSpan(1), 334 func() (tablesink.TableSink, uint64) { 335 *version += 1 336 return innerTableSink, *version 337 }, 338 tablepb.TableStatePrepared, 339 oracle.GoTimeToTS(time.Now()), 340 oracle.GoTimeToTS(time.Now().Add(10000*time.Second)), 341 func(_ context.Context) (model.Ts, error) { return math.MaxUint64, nil }, 342 ) 343 344 require.True(t, wrapper.initTableSink()) 345 346 wrapper.closeAndClearTableSink() 347 348 // Shouldn't be stuck because version is 0. 349 require.Equal(t, wrapper.tableSink.version, uint64(0)) 350 isStuck, _ := wrapper.sinkMaybeStuck(100 * time.Millisecond) 351 require.False(t, isStuck) 352 353 // Shouldn't be stuck because tableSink.advanced is just updated. 354 require.True(t, wrapper.initTableSink()) 355 isStuck, _ = wrapper.sinkMaybeStuck(100 * time.Millisecond) 356 require.False(t, isStuck) 357 358 // Shouldn't be stuck because upperbound hasn't been advanced. 359 time.Sleep(200 * time.Millisecond) 360 isStuck, _ = wrapper.sinkMaybeStuck(100 * time.Millisecond) 361 require.False(t, isStuck) 362 363 // Shouldn't be stuck because `getCheckpointTs` will update tableSink.advanced. 364 nowTs := oracle.GoTimeToTS(time.Now()) 365 wrapper.updateReceivedSorterResolvedTs(nowTs) 366 wrapper.barrierTs.Store(nowTs) 367 isStuck, _ = wrapper.sinkMaybeStuck(100 * time.Millisecond) 368 require.False(t, isStuck) 369 370 time.Sleep(200 * time.Millisecond) 371 nowTs = oracle.GoTimeToTS(time.Now()) 372 wrapper.updateReceivedSorterResolvedTs(nowTs) 373 wrapper.barrierTs.Store(nowTs) 374 wrapper.updateResolvedTs(model.NewResolvedTs(nowTs)) 375 isStuck, _ = wrapper.sinkMaybeStuck(100 * time.Millisecond) 376 require.True(t, isStuck) 377 }