github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/src/dbnode/client/host_queue_write_batch_test.go (about) 1 // Copyright (c) 2016 Uber Technologies, Inc. 2 // 3 // Permission is hereby granted, free of charge, to any person obtaining a copy 4 // of this software and associated documentation files (the "Software"), to deal 5 // in the Software without restriction, including without limitation the rights 6 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 // copies of the Software, and to permit persons to whom the Software is 8 // furnished to do so, subject to the following conditions: 9 // 10 // The above copyright notice and this permission notice shall be included in 11 // all copies or substantial portions of the Software. 12 // 13 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 // THE SOFTWARE. 20 21 package client 22 23 import ( 24 "fmt" 25 "sync" 26 "testing" 27 "time" 28 29 "github.com/m3db/m3/src/dbnode/generated/thrift/rpc" 30 "github.com/m3db/m3/src/x/ident" 31 32 "github.com/golang/mock/gomock" 33 "github.com/stretchr/testify/assert" 34 "github.com/uber/tchannel-go/thrift" 35 ) 36 37 func TestHostQueueWriteErrorBeforeOpen(t *testing.T) { 38 opts := newHostQueueTestOptions() 39 queue := newTestHostQueue(opts) 40 assert.Error(t, queue.Enqueue(&writeOperation{})) 41 } 42 43 func TestHostQueueWriteErrorAfterClose(t *testing.T) { 44 opts := newHostQueueTestOptions() 45 queue := newTestHostQueue(opts) 46 queue.Open() 47 queue.Close() 48 assert.Error(t, queue.Enqueue(&writeOperation{})) 49 } 50 51 func TestHostQueueWriteBatches(t *testing.T) { 52 for _, opts := range []Options{ 53 newHostQueueTestOptions().SetUseV2BatchAPIs(false), 54 newHostQueueTestOptions().SetUseV2BatchAPIs(true), 55 } { 56 t.Run(fmt.Sprintf("useV2: %v", opts.UseV2BatchAPIs()), func(t *testing.T) { 57 ctrl := gomock.NewController(t) 58 defer ctrl.Finish() 59 60 mockConnPool := NewMockconnectionPool(ctrl) 61 opts := newHostQueueTestOptions() 62 queue := newTestHostQueue(opts) 63 queue.connPool = mockConnPool 64 65 // Open 66 mockConnPool.EXPECT().Open() 67 queue.Open() 68 assert.Equal(t, statusOpen, queue.status) 69 70 // Prepare callback for writes 71 var ( 72 results []hostQueueResult 73 wg sync.WaitGroup 74 ) 75 callback := func(r interface{}, err error) { 76 results = append(results, hostQueueResult{r, err}) 77 wg.Done() 78 } 79 80 // Prepare writes 81 writes := []*writeOperation{ 82 testWriteOp("testNs", "foo", 1.0, 1000, rpc.TimeType_UNIX_SECONDS, callback), 83 testWriteOp("testNs", "bar", 2.0, 2000, rpc.TimeType_UNIX_SECONDS, callback), 84 testWriteOp("testNs", "baz", 3.0, 3000, rpc.TimeType_UNIX_SECONDS, callback), 85 testWriteOp("testNs", "qux", 4.0, 4000, rpc.TimeType_UNIX_SECONDS, callback), 86 } 87 wg.Add(len(writes)) 88 89 for i, write := range writes[:3] { 90 assert.NoError(t, queue.Enqueue(write)) 91 assert.Equal(t, i+1, queue.Len()) 92 93 // Sleep some so that we can ensure flushing is not happening until queue is full 94 time.Sleep(20 * time.Millisecond) 95 } 96 97 // Prepare mocks for flush 98 mockClient := rpc.NewMockTChanNode(ctrl) 99 if opts.UseV2BatchAPIs() { 100 writeBatch := func(ctx thrift.Context, req *rpc.WriteBatchRawV2Request) { 101 for i, write := range writes { 102 assert.Equal(t, req.Elements[i].NameSpace, 0) 103 assert.Equal(t, req.Elements[i].ID, write.request.ID) 104 assert.Equal(t, req.Elements[i].Datapoint, write.request.Datapoint) 105 } 106 } 107 mockClient.EXPECT().WriteBatchRawV2(gomock.Any(), gomock.Any()).Do(writeBatch).Return(nil) 108 } else { 109 writeBatch := func(ctx thrift.Context, req *rpc.WriteBatchRawRequest) { 110 for i, write := range writes { 111 assert.Equal(t, req.Elements[i].ID, write.request.ID) 112 assert.Equal(t, req.Elements[i].Datapoint, write.request.Datapoint) 113 } 114 } 115 mockClient.EXPECT().WriteBatchRaw(gomock.Any(), gomock.Any()).Do(writeBatch).Return(nil) 116 } 117 118 mockConnPool.EXPECT().NextClient().Return(mockClient, &noopPooledChannel{}, nil) 119 120 // Final write will flush 121 assert.NoError(t, queue.Enqueue(writes[3])) 122 assert.Equal(t, 0, queue.Len()) 123 124 // Wait for all writes 125 wg.Wait() 126 127 // Assert writes successful 128 assert.Equal(t, len(writes), len(results)) 129 for _, result := range results { 130 assert.Nil(t, result.err) 131 } 132 133 // Close 134 var closeWg sync.WaitGroup 135 closeWg.Add(1) 136 mockConnPool.EXPECT().Close().Do(func() { 137 closeWg.Done() 138 }) 139 queue.Close() 140 closeWg.Wait() 141 }) 142 } 143 } 144 145 func TestHostQueueWriteBatchesDifferentNamespaces(t *testing.T) { 146 for _, opts := range []Options{ 147 newHostQueueTestOptions().SetUseV2BatchAPIs(false), 148 newHostQueueTestOptions().SetUseV2BatchAPIs(true), 149 } { 150 t.Run(fmt.Sprintf("useV2: %v", opts.UseV2BatchAPIs()), func(t *testing.T) { 151 ctrl := gomock.NewController(t) 152 defer ctrl.Finish() 153 154 mockConnPool := NewMockconnectionPool(ctrl) 155 156 queue := newTestHostQueue(opts) 157 queue.connPool = mockConnPool 158 159 // Open 160 mockConnPool.EXPECT().Open() 161 queue.Open() 162 assert.Equal(t, statusOpen, queue.status) 163 164 // Prepare callback for writes 165 var ( 166 results []hostQueueResult 167 resultsLock sync.Mutex 168 wg sync.WaitGroup 169 ) 170 callback := func(r interface{}, err error) { 171 resultsLock.Lock() 172 results = append(results, hostQueueResult{r, err}) 173 resultsLock.Unlock() 174 wg.Done() 175 } 176 177 // Prepare writes 178 writes := []*writeOperation{ 179 testWriteOp("testNs1", "foo", 1.0, 1000, rpc.TimeType_UNIX_SECONDS, callback), 180 testWriteOp("testNs1", "bar", 2.0, 2000, rpc.TimeType_UNIX_SECONDS, callback), 181 testWriteOp("testNs1", "baz", 3.0, 3000, rpc.TimeType_UNIX_SECONDS, callback), 182 testWriteOp("testNs2", "qux", 4.0, 4000, rpc.TimeType_UNIX_SECONDS, callback), 183 } 184 wg.Add(len(writes)) 185 186 // Prepare mocks for flush 187 mockClient := rpc.NewMockTChanNode(ctrl) 188 189 if opts.UseV2BatchAPIs() { 190 writeBatch := func(ctx thrift.Context, req *rpc.WriteBatchRawV2Request) { 191 assert.Equal(t, 2, len(req.NameSpaces)) 192 assert.Equal(t, len(writes), len(req.Elements)) 193 for i, write := range writes { 194 if i < 3 { 195 assert.Equal(t, req.Elements[i].NameSpace, int64(0)) 196 } else { 197 assert.Equal(t, req.Elements[i].NameSpace, int64(1)) 198 } 199 assert.Equal(t, req.Elements[i].ID, write.request.ID) 200 assert.Equal(t, req.Elements[i].Datapoint, write.request.Datapoint) 201 } 202 } 203 204 // Assert the writes will be handled in two batches 205 mockClient.EXPECT().WriteBatchRawV2(gomock.Any(), gomock.Any()).Do(writeBatch).Return(nil).Times(1) 206 mockConnPool.EXPECT().NextClient().Return(mockClient, &noopPooledChannel{}, nil).Times(1) 207 } else { 208 writeBatch := func(ctx thrift.Context, req *rpc.WriteBatchRawRequest) { 209 var writesForNamespace []*writeOperation 210 if string(req.NameSpace) == "testNs1" { 211 writesForNamespace = writes[:3] 212 } else { 213 writesForNamespace = writes[3:] 214 } 215 assert.Equal(t, len(writesForNamespace), len(req.Elements)) 216 for i, write := range writesForNamespace { 217 assert.Equal(t, req.Elements[i].ID, write.request.ID) 218 assert.Equal(t, req.Elements[i].Datapoint, write.request.Datapoint) 219 } 220 } 221 222 // Assert the writes will be handled in two batches 223 mockClient.EXPECT().WriteBatchRaw(gomock.Any(), gomock.Any()).Do(writeBatch).Return(nil).Times(2) 224 mockConnPool.EXPECT().NextClient().Return(mockClient, &noopPooledChannel{}, nil).Times(2) 225 } 226 227 for _, write := range writes { 228 assert.NoError(t, queue.Enqueue(write)) 229 } 230 231 // Wait for all writes 232 wg.Wait() 233 234 // Assert writes successful 235 assert.Equal(t, len(writes), len(results)) 236 for _, result := range results { 237 assert.Nil(t, result.err) 238 } 239 240 // Close 241 var closeWg sync.WaitGroup 242 closeWg.Add(1) 243 mockConnPool.EXPECT().Close().Do(func() { 244 closeWg.Done() 245 }) 246 queue.Close() 247 closeWg.Wait() 248 }) 249 } 250 } 251 252 func TestHostQueueWriteBatchesNoClientAvailable(t *testing.T) { 253 ctrl := gomock.NewController(t) 254 defer ctrl.Finish() 255 256 mockConnPool := NewMockconnectionPool(ctrl) 257 258 opts := newHostQueueTestOptions() 259 opts = opts.SetHostQueueOpsFlushInterval(time.Millisecond) 260 queue := newTestHostQueue(opts) 261 queue.connPool = mockConnPool 262 263 // Open 264 mockConnPool.EXPECT().Open() 265 queue.Open() 266 assert.Equal(t, statusOpen, queue.status) 267 268 // Prepare mocks for flush 269 nextClientErr := fmt.Errorf("an error") 270 mockConnPool.EXPECT().NextClient().Return(nil, nil, nextClientErr) 271 272 // Write 273 var wg sync.WaitGroup 274 wg.Add(1) 275 callback := func(r interface{}, err error) { 276 assert.Error(t, err) 277 assert.Equal(t, nextClientErr, err) 278 wg.Done() 279 } 280 assert.NoError(t, queue.Enqueue(testWriteOp("testNs", "foo", 1.0, 1000, rpc.TimeType_UNIX_SECONDS, callback))) 281 282 // Wait for background flush 283 wg.Wait() 284 285 // Close 286 var closeWg sync.WaitGroup 287 closeWg.Add(1) 288 mockConnPool.EXPECT().Close().Do(func() { 289 closeWg.Done() 290 }) 291 queue.Close() 292 closeWg.Wait() 293 } 294 295 func TestHostQueueWriteBatchesPartialBatchErrs(t *testing.T) { 296 for _, opts := range []Options{ 297 newHostQueueTestOptions().SetUseV2BatchAPIs(false), 298 newHostQueueTestOptions().SetUseV2BatchAPIs(true), 299 } { 300 t.Run(fmt.Sprintf("useV2: %v", opts.UseV2BatchAPIs()), func(t *testing.T) { 301 ctrl := gomock.NewController(t) 302 defer ctrl.Finish() 303 304 mockConnPool := NewMockconnectionPool(ctrl) 305 306 opts = opts.SetHostQueueOpsFlushSize(2) 307 queue := newTestHostQueue(opts) 308 queue.connPool = mockConnPool 309 310 // Open 311 mockConnPool.EXPECT().Open() 312 queue.Open() 313 assert.Equal(t, statusOpen, queue.status) 314 315 // Prepare writes 316 var wg sync.WaitGroup 317 writeErr := "a write error" 318 writes := []*writeOperation{ 319 testWriteOp("testNs", "foo", 1.0, 1000, rpc.TimeType_UNIX_SECONDS, func(r interface{}, err error) { 320 assert.Error(t, err) 321 rpcErr, ok := err.(*rpc.Error) 322 assert.True(t, ok) 323 assert.Equal(t, rpc.ErrorType_INTERNAL_ERROR, rpcErr.Type) 324 assert.Equal(t, writeErr, rpcErr.Message) 325 wg.Done() 326 }), 327 testWriteOp("testNs", "bar", 2.0, 2000, rpc.TimeType_UNIX_SECONDS, func(r interface{}, err error) { 328 assert.NoError(t, err) 329 wg.Done() 330 }), 331 } 332 wg.Add(len(writes)) 333 334 // Prepare mocks for flush 335 mockClient := rpc.NewMockTChanNode(ctrl) 336 batchErrs := &rpc.WriteBatchRawErrors{Errors: []*rpc.WriteBatchRawError{ 337 {Index: 0, Err: &rpc.Error{ 338 Type: rpc.ErrorType_INTERNAL_ERROR, 339 Message: writeErr, 340 }}, 341 }} 342 if opts.UseV2BatchAPIs() { 343 writeBatch := func(ctx thrift.Context, req *rpc.WriteBatchRawV2Request) { 344 for i, write := range writes { 345 assert.Equal(t, req.Elements[i].NameSpace, int64(0)) 346 assert.Equal(t, req.Elements[i].ID, write.request.ID) 347 assert.Equal(t, req.Elements[i].Datapoint, write.request.Datapoint) 348 } 349 } 350 mockClient.EXPECT().WriteBatchRawV2(gomock.Any(), gomock.Any()).Do(writeBatch).Return(batchErrs) 351 } else { 352 writeBatch := func(ctx thrift.Context, req *rpc.WriteBatchRawRequest) { 353 for i, write := range writes { 354 assert.Equal(t, req.Elements[i].ID, write.request.ID) 355 assert.Equal(t, req.Elements[i].Datapoint, write.request.Datapoint) 356 } 357 } 358 mockClient.EXPECT().WriteBatchRaw(gomock.Any(), gomock.Any()).Do(writeBatch).Return(batchErrs) 359 } 360 mockConnPool.EXPECT().NextClient().Return(mockClient, &noopPooledChannel{}, nil) 361 362 // Perform writes 363 for _, write := range writes { 364 assert.NoError(t, queue.Enqueue(write)) 365 } 366 367 // Wait for flush 368 wg.Wait() 369 370 // Close 371 var closeWg sync.WaitGroup 372 closeWg.Add(1) 373 mockConnPool.EXPECT().Close().Do(func() { 374 closeWg.Done() 375 }) 376 queue.Close() 377 closeWg.Wait() 378 }) 379 } 380 } 381 382 func TestHostQueueWriteBatchesEntireBatchErr(t *testing.T) { 383 ctrl := gomock.NewController(t) 384 defer ctrl.Finish() 385 386 mockConnPool := NewMockconnectionPool(ctrl) 387 388 opts := newHostQueueTestOptions() 389 opts = opts.SetHostQueueOpsFlushSize(2) 390 queue := newTestHostQueue(opts) 391 queue.connPool = mockConnPool 392 393 // Open 394 mockConnPool.EXPECT().Open() 395 queue.Open() 396 assert.Equal(t, statusOpen, queue.status) 397 398 // Prepare writes 399 var wg sync.WaitGroup 400 writeErr := fmt.Errorf("an error") 401 callback := func(r interface{}, err error) { 402 assert.Error(t, err) 403 assert.Equal(t, writeErr, err) 404 wg.Done() 405 } 406 writes := []*writeOperation{ 407 testWriteOp("testNs", "foo", 1.0, 1000, rpc.TimeType_UNIX_SECONDS, callback), 408 testWriteOp("testNs", "bar", 2.0, 2000, rpc.TimeType_UNIX_SECONDS, callback), 409 } 410 wg.Add(len(writes)) 411 412 // Prepare mocks for flush 413 mockClient := rpc.NewMockTChanNode(ctrl) 414 writeBatch := func(ctx thrift.Context, req *rpc.WriteBatchRawRequest) { 415 for i, write := range writes { 416 assert.Equal(t, req.Elements[i].ID, write.request.ID) 417 assert.Equal(t, req.Elements[i].Datapoint, write.request.Datapoint) 418 } 419 } 420 mockClient.EXPECT().WriteBatchRaw(gomock.Any(), gomock.Any()).Do(writeBatch).Return(writeErr) 421 mockConnPool.EXPECT().NextClient().Return(mockClient, &noopPooledChannel{}, nil) 422 423 // Perform writes 424 for _, write := range writes { 425 assert.NoError(t, queue.Enqueue(write)) 426 } 427 428 // Wait for flush 429 wg.Wait() 430 431 // Close 432 var closeWg sync.WaitGroup 433 closeWg.Add(1) 434 mockConnPool.EXPECT().Close().Do(func() { 435 closeWg.Done() 436 }) 437 queue.Close() 438 closeWg.Wait() 439 } 440 441 func TestHostQueueDrainOnClose(t *testing.T) { 442 ctrl := gomock.NewController(t) 443 defer ctrl.Finish() 444 445 mockConnPool := NewMockconnectionPool(ctrl) 446 447 opts := newHostQueueTestOptions() 448 queue := newTestHostQueue(opts) 449 queue.connPool = mockConnPool 450 451 // Open 452 mockConnPool.EXPECT().Open() 453 queue.Open() 454 assert.Equal(t, statusOpen, queue.status) 455 456 // Prepare callback for writes 457 var ( 458 results []hostQueueResult 459 wg sync.WaitGroup 460 ) 461 callback := func(r interface{}, err error) { 462 results = append(results, hostQueueResult{r, err}) 463 wg.Done() 464 } 465 466 // Prepare writes 467 writes := []*writeOperation{ 468 testWriteOp("testNs", "foo", 1.0, 1000, rpc.TimeType_UNIX_SECONDS, callback), 469 testWriteOp("testNs", "bar", 2.0, 2000, rpc.TimeType_UNIX_SECONDS, callback), 470 testWriteOp("testNs", "baz", 3.0, 3000, rpc.TimeType_UNIX_SECONDS, callback), 471 } 472 473 for i, write := range writes { 474 wg.Add(1) 475 assert.NoError(t, queue.Enqueue(write)) 476 assert.Equal(t, i+1, queue.Len()) 477 478 // Sleep some so that we can ensure flushing is not happening until queue is full 479 time.Sleep(20 * time.Millisecond) 480 } 481 482 mockClient := rpc.NewMockTChanNode(ctrl) 483 writeBatch := func(ctx thrift.Context, req *rpc.WriteBatchRawRequest) { 484 for i, write := range writes { 485 assert.Equal(t, req.Elements[i].ID, write.request.ID) 486 assert.Equal(t, req.Elements[i].Datapoint, write.request.Datapoint) 487 } 488 } 489 mockClient.EXPECT().WriteBatchRaw(gomock.Any(), gomock.Any()).Do(writeBatch).Return(nil) 490 491 mockConnPool.EXPECT().NextClient().Return(mockClient, &noopPooledChannel{}, nil) 492 493 mockConnPool.EXPECT().Close().AnyTimes() 494 495 // Close the queue should cause all writes to be flushed 496 queue.Close() 497 498 closeCh := make(chan struct{}) 499 500 go func() { 501 // Wait for all writes 502 wg.Wait() 503 504 close(closeCh) 505 }() 506 507 select { 508 case <-closeCh: 509 case <-time.After(time.Minute): 510 assert.Fail(t, "Not flushing writes") 511 } 512 513 // Assert writes successful 514 assert.Equal(t, len(writes), len(results)) 515 for _, result := range results { 516 assert.Nil(t, result.err) 517 } 518 } 519 520 func testWriteOp( 521 namespace string, 522 id string, 523 value float64, 524 timestamp int64, 525 timeType rpc.TimeType, 526 completionFn completionFn, 527 ) *writeOperation { 528 w := &writeOperation{} 529 w.reset() 530 w.namespace = ident.StringID(namespace) 531 w.request.ID = []byte(id) 532 w.request.Datapoint = &rpc.Datapoint{ 533 Value: value, 534 Timestamp: timestamp, 535 TimestampTimeType: timeType, 536 } 537 w.requestV2.ID = w.request.ID 538 w.requestV2.Datapoint = w.request.Datapoint 539 w.completionFn = completionFn 540 return w 541 }