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  }