github.com/ydb-platform/ydb-go-sdk/v3@v3.57.0/tests/integration/topic_stress_test.go (about)

     1  //go:build integration
     2  // +build integration
     3  
     4  package integration
     5  
     6  import (
     7  	"context"
     8  	"errors"
     9  	"math/rand"
    10  	"runtime"
    11  	"strconv"
    12  	"strings"
    13  	"sync"
    14  	"sync/atomic"
    15  	"testing"
    16  	"time"
    17  
    18  	"github.com/stretchr/testify/require"
    19  	"golang.org/x/sync/errgroup"
    20  
    21  	"github.com/ydb-platform/ydb-go-sdk/v3"
    22  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/xerrors"
    23  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/xtest"
    24  	"github.com/ydb-platform/ydb-go-sdk/v3/topic/topicoptions"
    25  	"github.com/ydb-platform/ydb-go-sdk/v3/topic/topicreader"
    26  	"github.com/ydb-platform/ydb-go-sdk/v3/topic/topictypes"
    27  	"github.com/ydb-platform/ydb-go-sdk/v3/topic/topicwriter"
    28  )
    29  
    30  func TestReadersWritersStress(t *testing.T) {
    31  	ctx := xtest.Context(t)
    32  	db := connect(t)
    33  
    34  	topicPrefix := db.Name() + "/stress-topic-"
    35  	consumerName := commonConsumerName
    36  
    37  	writeTime := time.Second * 10
    38  	topicCount := runtime.GOMAXPROCS(0)
    39  	if topicCount > 10 {
    40  		topicCount = 10
    41  	}
    42  	t.Log("topic count: ", topicCount)
    43  
    44  	topicPartitions := 3
    45  	writersPerTopic := topicPartitions * 2
    46  	readersPerTopic := 2
    47  
    48  	var topics []string
    49  	for i := 0; i < topicCount; i++ {
    50  		topicPath := topicPrefix + strconv.Itoa(i)
    51  		_ = db.Topic().Drop(ctx, topicPath)
    52  		err := db.Topic().Create(ctx, topicPath,
    53  			topicoptions.CreateWithMinActivePartitions(int64(topicPartitions)),
    54  			topicoptions.CreateWithConsumer(topictypes.Consumer{Name: consumerName}),
    55  		)
    56  		require.NoError(t, err)
    57  		topics = append(topics, topicPath)
    58  	}
    59  
    60  	errGrp, grpCtx := errgroup.WithContext(ctx)
    61  	for _, topicOuter := range topics {
    62  		topicInner := topicOuter
    63  		errGrp.Go(func() error {
    64  			return stressTestInATopic(
    65  				grpCtx,
    66  				t,
    67  				db,
    68  				writeTime,
    69  				topicInner,
    70  				consumerName,
    71  				writersPerTopic,
    72  				readersPerTopic,
    73  			)
    74  		})
    75  	}
    76  	require.NoError(t, errGrp.Wait())
    77  }
    78  
    79  func stressTestInATopic(
    80  	ctx context.Context,
    81  	t testing.TB,
    82  	db *ydb.Driver,
    83  	testTime time.Duration,
    84  	topicPath string,
    85  	consumerName string,
    86  	topicWriters, topicReaders int,
    87  ) error {
    88  	maxMessagesInBatch := 5
    89  	var mStatus sync.Mutex
    90  	writeStatusWriterSeqno := map[string]int64{}
    91  	readStatusWriterMaxSeqNo := map[string]int64{}
    92  
    93  	var stopWrite atomic.Bool
    94  
    95  	writeToTopic := func(ctx context.Context, producerID string, wg *sync.WaitGroup) (resErr error) {
    96  		var writer *topicwriter.Writer
    97  
    98  		defer func() {
    99  			closeErr := writer.Close(context.Background())
   100  
   101  			if resErr == nil && closeErr != nil {
   102  				resErr = closeErr
   103  			}
   104  
   105  			wg.Done()
   106  		}()
   107  
   108  		writer, err := db.Topic().StartWriter(topicPath,
   109  			topicoptions.WithProducerID(producerID),
   110  			topicoptions.WithSyncWrite(true),
   111  			topicoptions.WithWriterSetAutoSeqNo(false),
   112  		)
   113  		if err != nil {
   114  			return xerrors.WithStackTrace(err)
   115  		}
   116  
   117  		seqNo := int64(0)
   118  		for !stopWrite.Load() {
   119  			messageCount := rand.Intn(maxMessagesInBatch) + 1 //nolint:gosec
   120  			var messages []topicwriter.Message
   121  			for i := 0; i < messageCount; i++ {
   122  				seqNo++
   123  				message := topicwriter.Message{
   124  					SeqNo: seqNo,
   125  					Data:  strings.NewReader(strconv.FormatInt(seqNo, 10) + "-content"),
   126  				}
   127  				messages = append(messages, message)
   128  			}
   129  			err = writer.Write(ctx, messages...)
   130  			if err != nil {
   131  				return err
   132  			}
   133  			mStatus.Lock()
   134  			writeStatusWriterSeqno[producerID] = seqNo
   135  			mStatus.Unlock()
   136  		}
   137  		return nil
   138  	}
   139  
   140  	readFromTopic := func(ctx context.Context, wg *sync.WaitGroup) (resErr error) {
   141  		var reader *topicreader.Reader
   142  		defer func() {
   143  			closeErr := reader.Close(context.Background())
   144  
   145  			if resErr == nil && closeErr != nil {
   146  				resErr = closeErr
   147  			}
   148  
   149  			if ctx.Err() != nil && errors.Is(resErr, context.Canceled) {
   150  				resErr = nil
   151  			}
   152  
   153  			wg.Done()
   154  		}()
   155  
   156  		reader, err := db.Topic().StartReader(consumerName, topicoptions.ReadTopic(topicPath))
   157  		if err != nil {
   158  			return err
   159  		}
   160  
   161  		for {
   162  			mess, err := reader.ReadMessage(ctx)
   163  			if err != nil {
   164  				return err
   165  			}
   166  
   167  			// store max readed seqno for every producer id
   168  			mStatus.Lock()
   169  			oldSeq := readStatusWriterMaxSeqNo[mess.ProducerID]
   170  			if mess.SeqNo > oldSeq {
   171  				readStatusWriterMaxSeqNo[mess.ProducerID] = mess.SeqNo
   172  			}
   173  			mStatus.Unlock()
   174  
   175  			err = reader.Commit(ctx, mess)
   176  			if err != nil {
   177  				return err
   178  			}
   179  		}
   180  	}
   181  
   182  	var writersWG sync.WaitGroup
   183  	writersErrors := make(chan error, topicWriters)
   184  	for i := 0; i < topicWriters; i++ {
   185  		producerID := "producer-" + strconv.Itoa(i)
   186  		writersWG.Add(1)
   187  		go func() {
   188  			writersErrors <- writeToTopic(ctx, producerID, &writersWG)
   189  		}()
   190  	}
   191  
   192  	var readersWG sync.WaitGroup
   193  	readersError := make(chan error, topicReaders)
   194  	readCtx, stopReader := context.WithCancel(ctx)
   195  	defer stopReader()
   196  
   197  	for i := 0; i < topicReaders; i++ {
   198  		readersWG.Add(1)
   199  		go func() {
   200  			readersError <- readFromTopic(readCtx, &readersWG)
   201  		}()
   202  	}
   203  
   204  	time.Sleep(testTime)
   205  	stopWrite.Store(true)
   206  
   207  	xtest.WaitGroup(t, &writersWG)
   208  
   209  	for i := 0; i < topicWriters; i++ {
   210  		err := <-writersErrors
   211  		if err != nil {
   212  			return err
   213  		}
   214  	}
   215  
   216  	xtest.SpinWaitProgressWithTimeout(t, time.Minute, func() (progressValue interface{}, finished bool) {
   217  		time.Sleep(time.Millisecond)
   218  		needReadMessages := int64(0)
   219  		mStatus.Lock()
   220  		for producerID, writtenSeqNo := range writeStatusWriterSeqno {
   221  			readedSeqNo := readStatusWriterMaxSeqNo[producerID]
   222  			needReadMessages += writtenSeqNo - readedSeqNo
   223  		}
   224  		mStatus.Unlock()
   225  		return needReadMessages, needReadMessages == 0
   226  	})
   227  
   228  	stopReader()
   229  	xtest.WaitGroup(t, &readersWG)
   230  
   231  	for i := 0; i < topicReaders; i++ {
   232  		err := <-readersError
   233  		if err != nil {
   234  			return err
   235  		}
   236  	}
   237  
   238  	//nolint:gocritic
   239  	//// check about all messages are committed
   240  	// https://github.com/ydb-platform/ydb-go-sdk/issues/531
   241  	//reader, err := db.Topic().StartReader(consumerName, topicoptions.ReadTopic(topicPath))
   242  	//if err != nil {
   243  	//	return err
   244  	//}
   245  	//readWithTimeout, readCancel := context.WithTimeout(ctx, time.Second/10)
   246  	//defer readCancel()
   247  	//_, err = reader.ReadMessage(readWithTimeout)
   248  	//t.Log("err: ", err)
   249  	//require.ErrorIs(t, err, context.DeadlineExceeded)
   250  
   251  	return nil
   252  }