github.com/ydb-platform/ydb-go-sdk/v3@v3.89.2/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(sourceTest *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 t := xtest.MakeSyncedTest(sourceTest) 173 ctx := xtest.Context(t) 174 db := connect(t, ydb.WithLogger( 175 newLogger(t), 176 trace.DetailsAll, 177 )) 178 179 // create topic 180 topicName := t.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(t, 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(t, 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(t, errWriter) 200 mess := topicwriter.Message{Data: buf} 201 errWriter = w.Write(ctx, mess) 202 require.NoError(t, 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(t, errReader) 227 228 for { 229 mess, errReader := r.ReadMessage(readerCtx) 230 if readerCtx.Err() != nil { 231 return 232 } 233 require.NoError(t, errReader) 234 235 var val int64 236 errReader = binary.Read(mess, binary.BigEndian, &val) 237 require.NoError(t, 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(t, 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(t, 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(t, 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 t.Fatal(mess) 290 default: 291 } 292 293 readerCancel() 294 wg.Wait() 295 t.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 scope := newScope(t) 353 ctx := scope.Ctx 354 db := scope.Driver() 355 dbLogging := scope.DriverWithGRPCLogging() 356 topicPath := scope.TopicPath() 357 358 tokenInterval := time.Second 359 reader, err := dbLogging.Topic().StartReader( 360 scope.TopicConsumerName(), 361 topicoptions.ReadTopic(topicPath), 362 topicoptions.WithReaderUpdateTokenInterval(tokenInterval), 363 ) 364 scope.Require.NoError(err) 365 366 writer, err := db.Topic().StartWriter( 367 topicPath, 368 topicoptions.WithWriterProducerID("producer-id"), 369 topicoptions.WithWriterUpdateTokenInterval(tokenInterval), 370 ) 371 scope.Require.NoError(err) 372 373 var wg sync.WaitGroup 374 375 wg.Add(1) 376 stopTopicActivity := atomic.Bool{} 377 go func() { 378 defer wg.Done() 379 380 for i := 0; true; i++ { 381 if stopTopicActivity.Load() { 382 return 383 } 384 385 msgContent := []byte(strconv.Itoa(i)) 386 err = writer.Write(ctx, topicwriter.Message{Data: bytes.NewReader(msgContent)}) 387 scope.Require.NoError(err) 388 } 389 }() 390 391 hasMessages := atomic.Bool{} 392 393 wg.Add(1) 394 go func() { 395 defer wg.Done() 396 397 for i := 0; true; i++ { 398 if stopTopicActivity.Load() { 399 return 400 } 401 402 msg, err := reader.ReadMessage(ctx) 403 scope.Require.NoError(err) 404 scope.Require.NoError(reader.Commit(ctx, msg)) 405 hasMessages.Store(true) 406 } 407 }() 408 409 start := time.Now() 410 for i := 0; time.Since(start) < time.Second*10; i++ { 411 t.Log(i) 412 hasMessages.Store(false) 413 xtest.SpinWaitConditionWithTimeout(t, nil, time.Second*10, hasMessages.Load) 414 time.Sleep(tokenInterval) 415 } 416 417 stopTopicActivity.Store(true) 418 419 activityStopped := make(empty.Chan) 420 go func() { 421 wg.Wait() 422 close(activityStopped) 423 }() 424 xtest.WaitChannelClosed(t, activityStopped) 425 } 426 427 func TestTopicWriterWithManualPartitionSelect(t *testing.T) { 428 ctx := xtest.Context(t) 429 db := connect(t) 430 topicPath := createTopic(ctx, t, db) 431 432 writer, err := db.Topic().StartWriter( 433 topicPath, 434 topicoptions.WithPartitionID(0), 435 topicoptions.WithSyncWrite(true), 436 ) 437 require.NoError(t, err) 438 err = writer.Write(ctx, topicwriter.Message{Data: strings.NewReader("asd")}) 439 require.NoError(t, err) 440 } 441 442 func TestWriterFlushMessagesBeforeClose(t *testing.T) { 443 s := newScope(t) 444 ctx := s.Ctx 445 writer, err := s.Driver().Topic().StartWriter(s.TopicPath(), topicoptions.WithWriterWaitServerAck(false)) 446 require.NoError(t, err) 447 448 count := 1000 449 for i := 0; i < count; i++ { 450 require.NoError(t, writer.Write(ctx, topicwriter.Message{Data: strings.NewReader(strconv.Itoa(i))})) 451 } 452 require.NoError(t, writer.Close(ctx)) 453 454 for i := 0; i < count; i++ { 455 readCtx, cancel := context.WithTimeout(ctx, time.Second) 456 mess, err := s.TopicReader().ReadMessage(readCtx) 457 cancel() 458 require.NoError(t, err) 459 460 messBody, err := io.ReadAll(mess) 461 require.NoError(t, err) 462 messBodyString := string(messBody) 463 require.Equal(t, strconv.Itoa(i), messBodyString) 464 cancel() 465 } 466 } 467 468 var topicCounter int 469 470 func createTopic(ctx context.Context, t testing.TB, db *ydb.Driver) (topicPath string) { 471 topicCounter++ 472 topicPath = db.Name() + "/" + t.Name() + "--test-topic-" + strconv.Itoa(topicCounter) 473 _ = db.Topic().Drop(ctx, topicPath) 474 err := db.Topic().Create( 475 ctx, 476 topicPath, 477 topicoptions.CreateWithSupportedCodecs(topictypes.CodecRaw), 478 topicoptions.CreateWithConsumer(topictypes.Consumer{Name: consumerName}), 479 ) 480 require.NoError(t, err) 481 482 return topicPath 483 } 484 485 func addConsumer(ctx context.Context, db *ydb.Driver, topicName, consumerName string) error { 486 consumer := topictypes.Consumer{Name: consumerName} 487 return db.Topic().Alter(ctx, topicName, topicoptions.AlterWithAddConsumers(consumer)) 488 }