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 }