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

     1  //go:build integration
     2  // +build integration
     3  
     4  package integration
     5  
     6  import (
     7  	"bytes"
     8  	"context"
     9  	"encoding/binary"
    10  	"errors"
    11  	"io"
    12  	"os"
    13  	"runtime/pprof"
    14  	"strconv"
    15  	"strings"
    16  	"sync"
    17  	"sync/atomic"
    18  	"testing"
    19  	"time"
    20  
    21  	"github.com/stretchr/testify/require"
    22  	"google.golang.org/grpc"
    23  
    24  	"github.com/ydb-platform/ydb-go-sdk/v3"
    25  	"github.com/ydb-platform/ydb-go-sdk/v3/config"
    26  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/empty"
    27  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/version"
    28  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/xtest"
    29  	"github.com/ydb-platform/ydb-go-sdk/v3/topic/topicoptions"
    30  	"github.com/ydb-platform/ydb-go-sdk/v3/topic/topicsugar"
    31  	"github.com/ydb-platform/ydb-go-sdk/v3/topic/topictypes"
    32  	"github.com/ydb-platform/ydb-go-sdk/v3/topic/topicwriter"
    33  	"github.com/ydb-platform/ydb-go-sdk/v3/trace"
    34  )
    35  
    36  func TestSendAsyncMessages(t *testing.T) {
    37  	ctx := context.Background()
    38  	db := connect(t)
    39  	topicPath := createTopic(ctx, t, db)
    40  
    41  	content := "hello"
    42  
    43  	writer, err := db.Topic().StartWriter(topicPath)
    44  	require.NoError(t, err)
    45  	require.NotEmpty(t, writer)
    46  	require.NoError(t, writer.Write(ctx, topicwriter.Message{Data: strings.NewReader(content)}))
    47  
    48  	reader, err := db.Topic().StartReader(consumerName, topicoptions.ReadTopic(topicPath))
    49  	require.NoError(t, err)
    50  
    51  	readCtx, cancel := context.WithTimeout(ctx, time.Second)
    52  	defer cancel()
    53  	mess, err := reader.ReadMessage(readCtx)
    54  	require.NoError(t, err)
    55  
    56  	readBytes, err := io.ReadAll(mess)
    57  	require.NoError(t, err)
    58  	require.Equal(t, content, string(readBytes))
    59  }
    60  
    61  func TestSendSyncMessages(t *testing.T) {
    62  	xtest.TestManyTimes(t, func(t testing.TB) {
    63  		ctx := xtest.Context(t)
    64  
    65  		grpcStopper := NewGrpcStopper(errors.New("stop grpc for test"))
    66  
    67  		db := connect(t,
    68  			ydb.With(config.WithGrpcOptions(
    69  				grpc.WithChainUnaryInterceptor(grpcStopper.UnaryClientInterceptor),
    70  				grpc.WithChainStreamInterceptor(grpcStopper.StreamClientInterceptor),
    71  			)),
    72  		)
    73  		topicPath := createTopic(ctx, t, db)
    74  
    75  		writer, err := db.Topic().StartWriter(
    76  			topicPath,
    77  			topicoptions.WithSyncWrite(true),
    78  		)
    79  		require.NoError(t, err)
    80  		msg := topicwriter.Message{Data: strings.NewReader("1")}
    81  		err = writer.Write(ctx, msg)
    82  		require.NoError(t, err)
    83  
    84  		grpcStopper.Stop() // stop any activity through connections
    85  
    86  		// check about connection broken
    87  		msg = topicwriter.Message{CreatedAt: time.Now(), Data: strings.NewReader("nosent")}
    88  		err = writer.Write(ctx, msg)
    89  		require.Error(t, err)
    90  
    91  		db = connect(t)
    92  		writer, err = db.Topic().StartWriter(topicPath,
    93  			topicoptions.WithSyncWrite(true),
    94  		)
    95  		require.NoError(t, err)
    96  		msg = topicwriter.Message{Data: strings.NewReader("2")}
    97  		err = writer.Write(ctx, msg)
    98  		require.NoError(t, err)
    99  
   100  		reader, err := db.Topic().StartReader(consumerName, topicoptions.ReadTopic(topicPath))
   101  		require.NoError(t, err)
   102  		mess, err := reader.ReadMessage(ctx)
   103  		require.NoError(t, err)
   104  		require.NoError(t, topicsugar.ReadMessageDataWithCallback(mess, func(data []byte) error {
   105  			require.Equal(t, "1", string(data))
   106  			return nil
   107  		}))
   108  		mess, err = reader.ReadMessage(ctx)
   109  		require.NoError(t, err)
   110  		require.NoError(t, topicsugar.ReadMessageDataWithCallback(mess, func(data []byte) error {
   111  			require.Equal(t, "2", string(data))
   112  			return nil
   113  		}))
   114  	})
   115  }
   116  
   117  func TestMessageMetadata(t *testing.T) {
   118  	t.Run("NoMetadata", func(t *testing.T) {
   119  		e := newScope(t)
   120  		err := e.TopicWriter().Write(e.Ctx, topicwriter.Message{})
   121  		e.Require.NoError(err)
   122  
   123  		mess, err := e.TopicReader().ReadMessage(e.Ctx)
   124  		e.Require.NoError(err)
   125  		e.Require.Nil(mess.Metadata)
   126  	})
   127  	t.Run("Meta1", func(t *testing.T) {
   128  		if version.Lt(os.Getenv("YDB_VERSION"), "24.0") {
   129  			t.Skip()
   130  		}
   131  		e := newScope(t)
   132  		meta := map[string][]byte{
   133  			"key": []byte("val"),
   134  		}
   135  		err := e.TopicWriter().Write(e.Ctx, topicwriter.Message{
   136  			Metadata: meta,
   137  		})
   138  		e.Require.NoError(err)
   139  
   140  		mess, err := e.TopicReader().ReadMessage(e.Ctx)
   141  		e.Require.NoError(err)
   142  		e.Require.Equal(meta, mess.Metadata)
   143  	})
   144  	t.Run("Meta2", func(t *testing.T) {
   145  		if version.Lt(os.Getenv("YDB_VERSION"), "24.0") {
   146  			t.Skip()
   147  		}
   148  		e := newScope(t)
   149  		meta := map[string][]byte{
   150  			"key1": []byte("val1"),
   151  			"key2": []byte("val2"),
   152  			"key3": []byte("val3"),
   153  		}
   154  		err := e.TopicWriter().Write(e.Ctx, topicwriter.Message{
   155  			Metadata: meta,
   156  		})
   157  		e.Require.NoError(err)
   158  
   159  		mess, err := e.TopicReader().ReadMessage(e.Ctx)
   160  		e.Require.NoError(err)
   161  		e.Require.Equal(meta, mess.Metadata)
   162  	})
   163  }
   164  
   165  func TestManyConcurentReadersWriters(t *testing.T) {
   166  	const partitionCount = 3
   167  	const writersCount = 5
   168  	const readersCount = 10
   169  	const sendMessageCount = 100
   170  	const totalMessageCount = sendMessageCount * writersCount
   171  
   172  	tb := xtest.MakeSyncedTest(t)
   173  	ctx := xtest.Context(tb)
   174  	db := connect(tb, ydb.WithLogger(
   175  		newLogger(t),
   176  		trace.DetailsAll,
   177  	))
   178  
   179  	// create topic
   180  	topicName := tb.Name()
   181  	_ = db.Topic().Drop(ctx, topicName)
   182  	err := db.Topic().Create(
   183  		ctx,
   184  		topicName,
   185  		topicoptions.CreateWithSupportedCodecs(topictypes.CodecRaw),
   186  		topicoptions.CreateWithMinActivePartitions(partitionCount),
   187  	)
   188  	require.NoError(tb, err)
   189  
   190  	// senders
   191  	writer := func(producerID string) {
   192  		pprof.Do(ctx, pprof.Labels("writer", producerID), func(ctx context.Context) {
   193  			w, errWriter := db.Topic().StartWriter(topicName, topicoptions.WithProducerID(producerID))
   194  			require.NoError(tb, errWriter)
   195  
   196  			for i := 0; i < sendMessageCount; i++ {
   197  				buf := &bytes.Buffer{}
   198  				errWriter = binary.Write(buf, binary.BigEndian, int64(i))
   199  				require.NoError(tb, errWriter)
   200  				mess := topicwriter.Message{Data: buf}
   201  				errWriter = w.Write(ctx, mess)
   202  				require.NoError(tb, errWriter)
   203  			}
   204  		})
   205  	}
   206  	for i := 0; i < writersCount; i++ {
   207  		go writer(strconv.Itoa(i))
   208  	}
   209  
   210  	// readers
   211  	type receivedMessT struct {
   212  		ctx     context.Context
   213  		writer  string
   214  		content int64
   215  	}
   216  	readerCtx, readerCancel := context.WithCancel(ctx)
   217  	receivedMessage := make(chan receivedMessT, totalMessageCount)
   218  
   219  	reader := func(consumerID string) {
   220  		pprof.Do(ctx, pprof.Labels("reader", consumerID), func(ctx context.Context) {
   221  			r, errReader := db.Topic().StartReader(
   222  				consumerID,
   223  				topicoptions.ReadTopic(topicName),
   224  				topicoptions.WithCommitTimeLagTrigger(0),
   225  			)
   226  			require.NoError(tb, errReader)
   227  
   228  			for {
   229  				mess, errReader := r.ReadMessage(readerCtx)
   230  				if readerCtx.Err() != nil {
   231  					return
   232  				}
   233  				require.NoError(tb, errReader)
   234  
   235  				var val int64
   236  				errReader = binary.Read(mess, binary.BigEndian, &val)
   237  				require.NoError(tb, errReader)
   238  				receivedMessage <- receivedMessT{
   239  					ctx:     mess.Context(),
   240  					writer:  mess.ProducerID,
   241  					content: val,
   242  				}
   243  				errReader = r.Commit(ctx, mess)
   244  				require.NoError(tb, errReader)
   245  			}
   246  		})
   247  	}
   248  
   249  	err = db.Topic().Alter(ctx, topicName, topicoptions.AlterWithAddConsumers(
   250  		topictypes.Consumer{
   251  			Name: commonConsumerName,
   252  		},
   253  	))
   254  	require.NoError(tb, err)
   255  
   256  	var wg sync.WaitGroup
   257  	wg.Add(readersCount)
   258  	for i := 0; i < readersCount; i++ {
   259  		go func(id int) {
   260  			defer wg.Done()
   261  
   262  			reader(commonConsumerName)
   263  		}(i)
   264  	}
   265  
   266  	received := map[string]int64{}
   267  	for i := 0; i < writersCount; i++ {
   268  		received[strconv.Itoa(i)] = -1
   269  	}
   270  
   271  	cnt := 0
   272  	doubles := 0
   273  	for cnt < totalMessageCount {
   274  		mess := <-receivedMessage
   275  		stored := received[mess.writer]
   276  		if mess.content <= stored {
   277  			// double
   278  			doubles++
   279  			continue
   280  		}
   281  		cnt++
   282  		require.Equal(tb, stored+1, mess.content)
   283  		received[mess.writer] = mess.content
   284  	}
   285  
   286  	// check about no more messages
   287  	select {
   288  	case mess := <-receivedMessage:
   289  		tb.Fatal(mess)
   290  	default:
   291  	}
   292  
   293  	readerCancel()
   294  	wg.Wait()
   295  	tb.Log(doubles)
   296  }
   297  
   298  func TestCommitUnexpectedRange(t *testing.T) {
   299  	sleepTime := time.Second
   300  	ctx := xtest.Context(t)
   301  	db := connect(t)
   302  
   303  	topicName1 := createTopic(ctx, t, db)
   304  	topicName2 := createTopic(ctx, t, db)
   305  	consumer := "test"
   306  	err := addConsumer(ctx, db, topicName1, consumer)
   307  	require.NoError(t, err)
   308  	err = addConsumer(ctx, db, topicName2, consumer)
   309  	require.NoError(t, err)
   310  
   311  	// get range from other reader
   312  	writer, err := db.Topic().StartWriter(topicName1)
   313  	require.NoError(t, err)
   314  
   315  	err = writer.Write(ctx, topicwriter.Message{Data: strings.NewReader("123")})
   316  	require.NoError(t, err)
   317  
   318  	reader1, err := db.Topic().StartReader(consumer, topicoptions.ReadTopic(topicName1))
   319  	require.NoError(t, err)
   320  	mess1, err := reader1.ReadMessage(ctx)
   321  	require.NoError(t, err)
   322  
   323  	connected := make(empty.Chan)
   324  
   325  	tracer := trace.Topic{
   326  		OnReaderInit: func(startInfo trace.TopicReaderInitStartInfo) func(doneInfo trace.TopicReaderInitDoneInfo) {
   327  			return func(doneInfo trace.TopicReaderInitDoneInfo) {
   328  				close(connected)
   329  			}
   330  		},
   331  	}
   332  
   333  	reader, err := db.Topic().StartReader(
   334  		consumer,
   335  		topicoptions.ReadTopic(topicName2),
   336  		topicoptions.WithReaderTrace(tracer),
   337  	)
   338  	require.NoError(t, err)
   339  
   340  	<-connected
   341  
   342  	err = reader.Commit(ctx, mess1)
   343  	require.Error(t, err)
   344  
   345  	readCtx, cancel := context.WithTimeout(ctx, sleepTime)
   346  	defer cancel()
   347  	_, err = reader.ReadMessage(readCtx)
   348  	require.ErrorIs(t, err, context.DeadlineExceeded)
   349  }
   350  
   351  func TestUpdateToken(t *testing.T) {
   352  	ctx := context.Background()
   353  	db := connect(t)
   354  	dbLogging := connectWithGrpcLogging(t)
   355  	topicPath := createTopic(ctx, t, db)
   356  
   357  	tokenInterval := time.Second
   358  	reader, err := dbLogging.Topic().StartReader(
   359  		consumerName,
   360  		topicoptions.ReadTopic(topicPath),
   361  		topicoptions.WithReaderUpdateTokenInterval(tokenInterval),
   362  	)
   363  	require.NoError(t, err)
   364  
   365  	writer, err := db.Topic().StartWriter(
   366  		topicPath,
   367  		topicoptions.WithProducerID("producer-id"),
   368  		topicoptions.WithWriterUpdateTokenInterval(tokenInterval),
   369  	)
   370  	require.NoError(t, err)
   371  
   372  	var wg sync.WaitGroup
   373  
   374  	wg.Add(1)
   375  	stopTopicActivity := atomic.Bool{}
   376  	go func() {
   377  		defer wg.Done()
   378  
   379  		for i := 0; true; i++ {
   380  			if stopTopicActivity.Load() {
   381  				return
   382  			}
   383  
   384  			msgContent := []byte(strconv.Itoa(i))
   385  			err = writer.Write(ctx, topicwriter.Message{Data: bytes.NewReader(msgContent)})
   386  			require.NoError(t, err)
   387  		}
   388  	}()
   389  
   390  	hasMessages := atomic.Bool{}
   391  
   392  	wg.Add(1)
   393  	go func() {
   394  		defer wg.Done()
   395  
   396  		for i := 0; true; i++ {
   397  			if stopTopicActivity.Load() {
   398  				return
   399  			}
   400  
   401  			msg, err := reader.ReadMessage(ctx)
   402  			require.NoError(t, err)
   403  			require.NoError(t, reader.Commit(ctx, msg))
   404  			hasMessages.Store(true)
   405  		}
   406  	}()
   407  
   408  	start := time.Now()
   409  	for i := 0; time.Since(start) < time.Second*10; i++ {
   410  		t.Log(i)
   411  		hasMessages.Store(false)
   412  		xtest.SpinWaitConditionWithTimeout(t, nil, time.Second*10, hasMessages.Load)
   413  		time.Sleep(tokenInterval)
   414  	}
   415  
   416  	stopTopicActivity.Store(true)
   417  
   418  	activityStopped := make(empty.Chan)
   419  	go func() {
   420  		wg.Wait()
   421  		close(activityStopped)
   422  	}()
   423  	xtest.WaitChannelClosed(t, activityStopped)
   424  }
   425  
   426  func TestTopicWriterWithManualPartitionSelect(t *testing.T) {
   427  	ctx := xtest.Context(t)
   428  	db := connect(t)
   429  	topicPath := createTopic(ctx, t, db)
   430  
   431  	writer, err := db.Topic().StartWriter(
   432  		topicPath,
   433  		topicoptions.WithPartitionID(0),
   434  		topicoptions.WithSyncWrite(true),
   435  	)
   436  	require.NoError(t, err)
   437  	err = writer.Write(ctx, topicwriter.Message{Data: strings.NewReader("asd")})
   438  	require.NoError(t, err)
   439  }
   440  
   441  var topicCounter int
   442  
   443  func createTopic(ctx context.Context, t testing.TB, db *ydb.Driver) (topicPath string) {
   444  	topicCounter++
   445  	topicPath = db.Name() + "/" + t.Name() + "--test-topic-" + strconv.Itoa(topicCounter)
   446  	_ = db.Topic().Drop(ctx, topicPath)
   447  	err := db.Topic().Create(
   448  		ctx,
   449  		topicPath,
   450  		topicoptions.CreateWithSupportedCodecs(topictypes.CodecRaw),
   451  		topicoptions.CreateWithConsumer(topictypes.Consumer{Name: consumerName}),
   452  	)
   453  	require.NoError(t, err)
   454  
   455  	return topicPath
   456  }
   457  
   458  func addConsumer(ctx context.Context, db *ydb.Driver, topicName, consumerName string) error {
   459  	consumer := topictypes.Consumer{Name: consumerName}
   460  	return db.Topic().Alter(ctx, topicName, topicoptions.AlterWithAddConsumers(consumer))
   461  }