github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/src/dbnode/client/session_fetch_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 "math" 26 "strconv" 27 "strings" 28 "sync" 29 "testing" 30 "time" 31 32 "github.com/m3db/m3/src/dbnode/encoding" 33 "github.com/m3db/m3/src/dbnode/encoding/m3tsz" 34 "github.com/m3db/m3/src/dbnode/generated/thrift/rpc" 35 "github.com/m3db/m3/src/dbnode/namespace" 36 "github.com/m3db/m3/src/dbnode/topology" 37 "github.com/m3db/m3/src/dbnode/ts" 38 xmetrics "github.com/m3db/m3/src/dbnode/x/metrics" 39 "github.com/m3db/m3/src/x/checked" 40 xerrors "github.com/m3db/m3/src/x/errors" 41 "github.com/m3db/m3/src/x/ident" 42 xretry "github.com/m3db/m3/src/x/retry" 43 xtime "github.com/m3db/m3/src/x/time" 44 45 "github.com/golang/mock/gomock" 46 "github.com/stretchr/testify/assert" 47 "github.com/stretchr/testify/require" 48 "github.com/uber-go/tally" 49 ) 50 51 const ( 52 testNamespaceName = "testNs" 53 ) 54 55 var ( 56 fetchFailureErrStr = "a specific fetch error" 57 ) 58 59 type testOptions struct { 60 nsID ident.ID 61 opts Options 62 encoderPool encoding.EncoderPool 63 setFetchAnn setFetchAnnotation 64 setWriteAnn setWriteAnnotation 65 annEqual assertAnnotationEqual 66 expectedErr error 67 } 68 69 type testFetch struct { 70 id string 71 values []testValue 72 } 73 74 type testFetches []testFetch 75 76 func (f testFetches) IDs() []string { 77 var ids []string 78 for i := range f { 79 ids = append(ids, f[i].id) 80 } 81 return ids 82 } 83 84 func (f testFetches) IDsIter() ident.Iterator { 85 return ident.NewStringIDsSliceIterator(f.IDs()) 86 } 87 88 type testValue struct { 89 value float64 90 t xtime.UnixNano 91 unit xtime.Unit 92 annotation []byte 93 } 94 95 type testValuesByTime []testValue 96 97 type setFetchAnnotation func([]testFetch) []testFetch 98 type setWriteAnnotation func(*writeStub) 99 type assertAnnotationEqual func(*testing.T, []byte, []byte) 100 101 func (v testValuesByTime) Len() int { return len(v) } 102 func (v testValuesByTime) Swap(i, j int) { v[i], v[j] = v[j], v[i] } 103 func (v testValuesByTime) Less(i, j int) bool { 104 return v[i].t.Before(v[j].t) 105 } 106 107 type testFetchResultsAssertion struct { 108 trimToTimeRange int 109 } 110 111 func TestSessionFetchNotOpenError(t *testing.T) { 112 ctrl := gomock.NewController(t) 113 defer ctrl.Finish() 114 115 opts := newSessionTestOptions() 116 s, err := newSession(opts) 117 assert.NoError(t, err) 118 119 now := xtime.Now() 120 _, err = s.Fetch(ident.StringID("namespace"), ident.StringID("foo"), now.Add(-time.Hour), now) 121 assert.Error(t, err) 122 assert.Equal(t, ErrSessionStatusNotOpen, err) 123 } 124 125 func TestSessionFetchIDs(t *testing.T) { 126 opts := newSessionTestOptions() 127 testSessionFetchIDs(t, testOptions{nsID: ident.StringID(testNamespaceName), opts: opts}) 128 } 129 130 func testSessionFetchIDs(t *testing.T, testOpts testOptions) { 131 ctrl := gomock.NewController(t) 132 defer ctrl.Finish() 133 134 nsID := testOpts.nsID 135 opts := testOpts.opts 136 opts = opts.SetFetchBatchSize(2) 137 138 s, err := newSession(opts) 139 require.NoError(t, err) 140 session := s.(*session) 141 142 start := xtime.Now().Truncate(time.Hour) 143 end := start.Add(2 * time.Hour) 144 145 fetches := testFetches([]testFetch{ 146 {"foo", []testValue{ 147 {1.0, start.Add(1 * time.Second), xtime.Second, []byte{1, 2, 3}}, 148 {2.0, start.Add(2 * time.Second), xtime.Second, nil}, 149 {3.0, start.Add(3 * time.Second), xtime.Second, nil}, 150 }}, 151 {"bar", []testValue{ 152 {4.0, start.Add(1 * time.Second), xtime.Second, []byte{4, 5, 6}}, 153 {5.0, start.Add(2 * time.Second), xtime.Second, nil}, 154 {6.0, start.Add(3 * time.Second), xtime.Second, nil}, 155 }}, 156 {"baz", []testValue{ 157 {7.0, start.Add(1 * time.Minute), xtime.Second, []byte{7, 8, 9}}, 158 {8.0, start.Add(2 * time.Minute), xtime.Second, nil}, 159 {9.0, start.Add(3 * time.Minute), xtime.Second, nil}, 160 }}, 161 }) 162 if testOpts.setFetchAnn != nil { 163 fetches = testOpts.setFetchAnn(fetches) 164 } 165 166 expectedFetches := fetches 167 if testOpts.expectedErr != nil { 168 // If an error is expected then don't setup any go mock expectation for the host queues since 169 // they will never be fulfilled and will hang the test. 170 expectedFetches = nil 171 } 172 fetchBatchOps, enqueueWg := prepareTestFetchEnqueues(t, ctrl, session, expectedFetches) 173 174 valueWriteWg := sync.WaitGroup{} 175 valueWriteWg.Add(1) 176 go func() { 177 defer valueWriteWg.Done() 178 // Fulfill fetch ops once enqueued 179 enqueueWg.Wait() 180 fulfillFetchBatchOps(t, testOpts, fetches, *fetchBatchOps, 0) 181 }() 182 183 require.NoError(t, session.Open()) 184 185 results, err := session.FetchIDs(nsID, fetches.IDsIter(), start, end) 186 if testOpts.expectedErr == nil { 187 require.NoError(t, err) 188 // wait for testValues to be written to before reading to assert. 189 valueWriteWg.Wait() 190 assertFetchResults(t, start, end, fetches, results, testOpts.annEqual) 191 } else { 192 require.Equal(t, testOpts.expectedErr, err) 193 } 194 require.NoError(t, session.Close()) 195 } 196 197 func TestSessionFetchIDsWithRetries(t *testing.T) { 198 ctrl := gomock.NewController(t) 199 defer ctrl.Finish() 200 201 opts := newSessionTestOptions(). 202 SetFetchBatchSize(2). 203 SetFetchRetrier( 204 xretry.NewRetrier(xretry.NewOptions().SetMaxRetries(1))) 205 testOpts := testOptions{nsID: ident.StringID(testNamespaceName), opts: opts} 206 207 s, err := newSession(opts) 208 assert.NoError(t, err) 209 session := s.(*session) 210 211 start := xtime.Now().Truncate(time.Hour) 212 end := start.Add(2 * time.Hour) 213 214 fetches := testFetches([]testFetch{ 215 {"foo", []testValue{ 216 {1.0, start.Add(1 * time.Second), xtime.Second, []byte{1, 2, 3}}, 217 {2.0, start.Add(2 * time.Second), xtime.Second, nil}, 218 {3.0, start.Add(3 * time.Second), xtime.Second, nil}, 219 }}, 220 }) 221 222 successBatchOps, enqueueWg := prepareTestFetchEnqueuesWithErrors(t, ctrl, session, fetches) 223 224 go func() { 225 // Fulfill success fetch ops once all are enqueued 226 enqueueWg.Wait() 227 fulfillFetchBatchOps(t, testOpts, fetches, *successBatchOps, 0) 228 }() 229 230 assert.NoError(t, session.Open()) 231 232 results, err := session.FetchIDs(testOpts.nsID, fetches.IDsIter(), start, end) 233 assert.NoError(t, err) 234 assertFetchResults(t, start, end, fetches, results, nil) 235 236 assert.NoError(t, session.Close()) 237 } 238 239 func TestSessionFetchIDsTrimsWindowsInTimeWindow(t *testing.T) { 240 ctrl := gomock.NewController(t) 241 defer ctrl.Finish() 242 243 opts := newSessionTestOptions().SetFetchBatchSize(2) 244 testOpts := testOptions{nsID: ident.StringID(testNamespaceName), opts: opts} 245 246 s, err := newSession(opts) 247 assert.NoError(t, err) 248 session := s.(*session) 249 250 start := xtime.Now().Truncate(time.Hour) 251 end := start.Add(2 * time.Hour) 252 253 fetches := testFetches([]testFetch{ 254 {"foo", []testValue{ 255 {0.0, start.Add(-1 * time.Second), xtime.Second, nil}, 256 {1.0, start.Add(1 * time.Second), xtime.Second, []byte{1, 2, 3}}, 257 {2.0, start.Add(2 * time.Second), xtime.Second, nil}, 258 {3.0, start.Add(3 * time.Second), xtime.Second, nil}, 259 {4.0, end.Add(1 * time.Second), xtime.Second, nil}, 260 }}, 261 }) 262 263 fetchBatchOps, enqueueWg := prepareTestFetchEnqueues(t, ctrl, session, fetches) 264 265 go func() { 266 // Fulfill fetch ops once enqueued 267 enqueueWg.Wait() 268 fulfillFetchBatchOps(t, testOpts, fetches, *fetchBatchOps, 0) 269 }() 270 271 assert.NoError(t, session.Open()) 272 273 result, err := session.Fetch(testOpts.nsID, ident.StringID(fetches[0].id), start, end) 274 assert.NoError(t, err) 275 276 results := encoding.NewSeriesIterators([]encoding.SeriesIterator{result}) 277 assertion := assertFetchResults(t, start, end, fetches, results, nil) 278 assert.Equal(t, 2, assertion.trimToTimeRange) 279 280 assert.NoError(t, session.Close()) 281 } 282 283 func TestSessionFetchIDsBadRequestErrorIsNonRetryable(t *testing.T) { 284 ctrl := gomock.NewController(t) 285 defer ctrl.Finish() 286 287 opts := newSessionTestOptions() 288 s, err := newSession(opts) 289 assert.NoError(t, err) 290 session := s.(*session) 291 292 start := xtime.Now().Truncate(time.Hour) 293 end := start.Add(2 * time.Hour) 294 295 mockHostQueues(ctrl, session, sessionTestReplicas, []testEnqueueFn{ 296 func(idx int, op op) { 297 go func() { 298 op.CompletionFn()(nil, &rpc.Error{ 299 Type: rpc.ErrorType_BAD_REQUEST, 300 Message: "expected bad request error", 301 }) 302 }() 303 }, 304 }) 305 306 assert.NoError(t, session.Open()) 307 308 _, err = session.FetchIDs( 309 ident.StringID(testNamespaceName), 310 ident.NewIDsIterator(ident.StringID("foo"), ident.StringID("bar")), start, end) 311 assert.Error(t, err) 312 assert.True(t, xerrors.IsNonRetryableError(err)) 313 314 assert.NoError(t, session.Close()) 315 } 316 317 func TestSessionFetchReadConsistencyLevelAll(t *testing.T) { 318 ctrl := gomock.NewController(t) 319 defer ctrl.Finish() 320 321 testFetchConsistencyLevel(t, ctrl, topology.ReadConsistencyLevelAll, 0, outcomeSuccess) 322 for i := 1; i <= 3; i++ { 323 testFetchConsistencyLevel(t, ctrl, topology.ReadConsistencyLevelAll, i, outcomeFail) 324 } 325 } 326 327 func TestSessionFetchReadConsistencyLevelUnstrictAll(t *testing.T) { 328 ctrl := gomock.NewController(t) 329 defer ctrl.Finish() 330 331 for i := 0; i <= 2; i++ { 332 testFetchConsistencyLevel(t, ctrl, topology.ReadConsistencyLevelUnstrictAll, i, outcomeSuccess) 333 } 334 testFetchConsistencyLevel(t, ctrl, topology.ReadConsistencyLevelUnstrictAll, 3, outcomeFail) 335 } 336 337 func TestSessionFetchReadConsistencyLevelMajority(t *testing.T) { 338 ctrl := gomock.NewController(t) 339 defer ctrl.Finish() 340 341 for i := 0; i <= 1; i++ { 342 testFetchConsistencyLevel(t, ctrl, topology.ReadConsistencyLevelMajority, i, outcomeSuccess) 343 } 344 for i := 2; i <= 3; i++ { 345 testFetchConsistencyLevel(t, ctrl, topology.ReadConsistencyLevelMajority, i, outcomeFail) 346 } 347 } 348 349 func TestSessionFetchReadConsistencyLevelUnstrictMajority(t *testing.T) { 350 ctrl := gomock.NewController(t) 351 defer ctrl.Finish() 352 353 for i := 0; i <= 2; i++ { 354 testFetchConsistencyLevel(t, ctrl, topology.ReadConsistencyLevelUnstrictMajority, i, outcomeSuccess) 355 } 356 testFetchConsistencyLevel(t, ctrl, topology.ReadConsistencyLevelUnstrictMajority, 3, outcomeFail) 357 } 358 359 func TestSessionFetchReadConsistencyLevelOne(t *testing.T) { 360 ctrl := gomock.NewController(t) 361 defer ctrl.Finish() 362 363 for i := 0; i <= 2; i++ { 364 testFetchConsistencyLevel(t, ctrl, topology.ReadConsistencyLevelOne, i, outcomeSuccess) 365 } 366 testFetchConsistencyLevel(t, ctrl, topology.ReadConsistencyLevelOne, 3, outcomeFail) 367 } 368 369 func testFetchConsistencyLevel( 370 t *testing.T, 371 ctrl *gomock.Controller, 372 level topology.ReadConsistencyLevel, 373 failures int, 374 expected outcome, 375 ) { 376 opts := newSessionTestOptions() 377 opts = opts.SetReadConsistencyLevel(level) 378 379 reporterOpts := xmetrics.NewTestStatsReporterOptions(). 380 SetCaptureEvents(true) 381 reporter := xmetrics.NewTestStatsReporter(reporterOpts) 382 scope, closer := tally.NewRootScope(tally.ScopeOptions{Reporter: reporter}, time.Millisecond) 383 defer closer.Close() 384 385 opts = opts.SetInstrumentOptions(opts.InstrumentOptions(). 386 SetMetricsScope(scope)) 387 388 testOpts := testOptions{nsID: ident.StringID(testNamespaceName), opts: opts} 389 390 s, err := newSession(opts) 391 assert.NoError(t, err) 392 session := s.(*session) 393 394 start := xtime.Now().Truncate(time.Hour) 395 end := start.Add(2 * time.Hour) 396 397 fetches := testFetches([]testFetch{ 398 {"foo", []testValue{ 399 {1.0, start.Add(1 * time.Second), xtime.Second, []byte{1, 2, 3}}, 400 {2.0, start.Add(2 * time.Second), xtime.Second, nil}, 401 {3.0, start.Add(3 * time.Second), xtime.Second, nil}, 402 }}, 403 }) 404 405 fetchBatchOps, enqueueWg := prepareTestFetchEnqueues(t, ctrl, session, fetches) 406 407 go func() { 408 // Fulfill fetch ops once enqueued 409 enqueueWg.Wait() 410 fulfillFetchBatchOps(t, testOpts, fetches, *fetchBatchOps, failures) 411 }() 412 413 assert.NoError(t, session.Open()) 414 415 results, err := session.FetchIDs(ident.StringID(testNamespaceName), 416 fetches.IDsIter(), start, end) 417 if expected == outcomeSuccess { 418 assert.NoError(t, err) 419 assertFetchResults(t, start, end, fetches, results, nil) 420 } else { 421 assert.Error(t, err) 422 assert.True(t, IsInternalServerError(err)) 423 assert.False(t, IsBadRequestError(err)) 424 resultErrStr := fmt.Sprintf("%v", err) 425 assert.True(t, strings.Contains(resultErrStr, 426 fmt.Sprintf("failed to meet consistency level %s with %d/3 success", level.String(), 3-failures))) 427 assert.True(t, strings.Contains(resultErrStr, 428 fetchFailureErrStr)) 429 } 430 431 assert.NoError(t, session.Close()) 432 433 counters := reporter.Counters() 434 for counters["fetch.success"] == 0 && counters["fetch.errors"] == 0 { 435 time.Sleep(time.Millisecond) 436 counters = reporter.Counters() 437 } 438 if expected == outcomeSuccess { 439 assert.Equal(t, 1, int(counters["fetch.success"])) 440 assert.Equal(t, 0, int(counters["fetch.errors"])) 441 } else { 442 assert.Equal(t, 0, int(counters["fetch.success"])) 443 assert.Equal(t, 1, int(counters["fetch.errors"])) 444 } 445 if failures > 0 { 446 for _, event := range reporter.Events() { 447 if event.Name() == "fetch.nodes-responding-error" { 448 nodesFailing, convErr := strconv.Atoi(event.Tags()["nodes"]) 449 require.NoError(t, convErr) 450 assert.True(t, 0 < nodesFailing && nodesFailing <= failures) 451 assert.Equal(t, int64(1), event.Value()) 452 break 453 } 454 } 455 } 456 } 457 458 func prepareTestFetchEnqueuesWithErrors( 459 t *testing.T, 460 ctrl *gomock.Controller, 461 session *session, 462 fetches []testFetch, 463 ) (*[]*fetchBatchOp, *sync.WaitGroup) { 464 failureEnqueueFn := func(idx int, op op) { 465 fetch, ok := op.(*fetchBatchOp) 466 assert.True(t, ok) 467 go func() { 468 fetch.CompletionFn()(nil, fmt.Errorf("random failure")) 469 }() 470 } 471 472 var successBatchOps []*fetchBatchOp 473 successEnqueueFn := func(idx int, op op) { 474 fetch, ok := op.(*fetchBatchOp) 475 assert.True(t, ok) 476 successBatchOps = append(successBatchOps, fetch) 477 } 478 479 var enqueueFns []testEnqueueFn 480 fetchBatchSize := session.opts.FetchBatchSize() 481 for i := 0; i < int(math.Ceil(float64(len(fetches))/float64(fetchBatchSize))); i++ { 482 enqueueFns = append(enqueueFns, failureEnqueueFn) 483 enqueueFns = append(enqueueFns, successEnqueueFn) 484 } 485 486 enqueueWg := mockHostQueues(ctrl, session, sessionTestReplicas, enqueueFns) 487 return &successBatchOps, enqueueWg 488 } 489 490 func prepareTestFetchEnqueues( 491 t *testing.T, 492 ctrl *gomock.Controller, 493 session *session, 494 fetches []testFetch, 495 ) (*[]*fetchBatchOp, *sync.WaitGroup) { 496 var fetchBatchOps []*fetchBatchOp 497 enqueueFn := func(idx int, op op) { 498 fetch, ok := op.(*fetchBatchOp) 499 assert.True(t, ok) 500 fetchBatchOps = append(fetchBatchOps, fetch) 501 } 502 503 var enqueueFns []testEnqueueFn 504 fetchBatchSize := session.opts.FetchBatchSize() 505 for i := 0; i < int(math.Ceil(float64(len(fetches))/float64(fetchBatchSize))); i++ { 506 enqueueFns = append(enqueueFns, enqueueFn) 507 } 508 enqueueWg := mockHostQueues(ctrl, session, sessionTestReplicas, enqueueFns) 509 return &fetchBatchOps, enqueueWg 510 } 511 512 func fulfillFetchBatchOps( 513 t *testing.T, 514 testOpts testOptions, 515 fetches []testFetch, 516 fetchBatchOps []*fetchBatchOp, 517 failures int, 518 ) { 519 failed := make(map[string]int) 520 for _, f := range fetches { 521 failed[f.id] = 0 522 } 523 524 for _, op := range fetchBatchOps { 525 for i, id := range op.request.Ids { 526 calledCompletionFn := false 527 for _, f := range fetches { 528 if f.id != string(id) { 529 continue 530 } 531 532 if failed[f.id] < failures { 533 // Requires failing 534 failed[f.id] = failed[f.id] + 1 535 op.completionFns[i](nil, &rpc.Error{ 536 Type: rpc.ErrorType_INTERNAL_ERROR, 537 Message: fetchFailureErrStr, 538 }) 539 calledCompletionFn = true 540 break 541 } 542 543 var encoder encoding.Encoder 544 if testOpts.encoderPool == nil { 545 encoder = m3tsz.NewEncoder(f.values[0].t, nil, true, nil) 546 } else { 547 encoder = testOpts.encoderPool.Get() 548 nsCtx := namespace.NewContextFor(testOpts.nsID, testOpts.opts.SchemaRegistry()) 549 encoder.Reset(f.values[0].t, 0, nsCtx.Schema) 550 } 551 for _, value := range f.values { 552 dp := ts.Datapoint{ 553 TimestampNanos: value.t, 554 Value: value.value, 555 } 556 encoder.Encode(dp, value.unit, value.annotation) 557 } 558 seg := encoder.Discard() 559 op.completionFns[i]([]*rpc.Segments{{ 560 Merged: &rpc.Segment{Head: bytesIfNotNil(seg.Head), Tail: bytesIfNotNil(seg.Tail)}, 561 }}, nil) 562 calledCompletionFn = true 563 break 564 } 565 assert.True(t, calledCompletionFn) 566 } 567 } 568 } 569 570 func bytesIfNotNil(data checked.Bytes) []byte { 571 if data != nil { 572 return data.Bytes() 573 } 574 return nil 575 } 576 577 func assertFetchResults( 578 t *testing.T, 579 start, end xtime.UnixNano, 580 fetches []testFetch, 581 results encoding.SeriesIterators, 582 annEqual assertAnnotationEqual, 583 ) testFetchResultsAssertion { 584 trimToTimeRange := 0 585 assert.Equal(t, len(fetches), results.Len()) 586 587 for i, series := range results.Iters() { 588 expected := fetches[i] 589 assert.Equal(t, expected.id, series.ID().String()) 590 assert.Equal(t, start, series.Start()) 591 assert.Equal(t, end, series.End()) 592 593 // Trim the expected values to the start/end window 594 var expectedValues []testValue 595 for j := range expected.values { 596 ts := expected.values[j].t 597 if ts.Before(start) { 598 trimToTimeRange++ 599 continue 600 } 601 if ts.Equal(end) || ts.After(end) { 602 trimToTimeRange++ 603 continue 604 } 605 expectedValues = append(expectedValues, expected.values[j]) 606 } 607 608 j := 0 609 for series.Next() { 610 value := expectedValues[j] 611 dp, unit, annotation := series.Current() 612 613 assert.Equal(t, value.t, dp.TimestampNanos) 614 assert.Equal(t, value.value, dp.Value) 615 assert.Equal(t, value.unit, unit) 616 if annEqual != nil { 617 annEqual(t, value.annotation, annotation) 618 } else { 619 assert.Equal(t, value.annotation, []byte(annotation)) 620 } 621 j++ 622 } 623 assert.Equal(t, len(expectedValues), j) 624 assert.NoError(t, series.Err()) 625 } 626 results.Close() 627 628 return testFetchResultsAssertion{trimToTimeRange: trimToTimeRange} 629 }