github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/src/query/remote/server_test.go (about) 1 // Copyright (c) 2018 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 remote 22 23 import ( 24 "context" 25 "errors" 26 "fmt" 27 "net" 28 "runtime" 29 "sync" 30 "testing" 31 "time" 32 33 "github.com/m3db/m3/src/dbnode/encoding" 34 "github.com/m3db/m3/src/query/block" 35 m3err "github.com/m3db/m3/src/query/errors" 36 rpc "github.com/m3db/m3/src/query/generated/proto/rpcpb" 37 "github.com/m3db/m3/src/query/models" 38 "github.com/m3db/m3/src/query/pools" 39 "github.com/m3db/m3/src/query/storage" 40 "github.com/m3db/m3/src/query/storage/m3" 41 "github.com/m3db/m3/src/query/storage/m3/consolidators" 42 "github.com/m3db/m3/src/query/test" 43 "github.com/m3db/m3/src/x/ident" 44 "github.com/m3db/m3/src/x/instrument" 45 xsync "github.com/m3db/m3/src/x/sync" 46 47 "github.com/golang/mock/gomock" 48 "github.com/stretchr/testify/assert" 49 "github.com/stretchr/testify/require" 50 "google.golang.org/grpc" 51 ) 52 53 var ( 54 testName = "remote_foo" 55 errRead = errors.New("read error") 56 iterPools = pools.BuildIteratorPools(encoding.NewOptions(), 57 pools.BuildIteratorPoolsOptions{}) 58 poolsWrapper = pools.NewPoolsWrapper(iterPools) 59 ) 60 61 type mockStorageOptions struct { 62 err error 63 iters encoding.SeriesIterators 64 fetchCompressedSleep time.Duration 65 cleanup func() error 66 } 67 68 func newMockStorage( 69 t *testing.T, 70 ctrl *gomock.Controller, 71 opts mockStorageOptions, 72 ) *m3.MockStorage { 73 store := m3.NewMockStorage(ctrl) 74 75 store.EXPECT().FetchCompressedResult(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func( 76 ctx context.Context, 77 query *storage.FetchQuery, 78 options *storage.FetchOptions, 79 ) (consolidators.SeriesFetchResult, m3.Cleanup, error) { 80 cleanup := func() error { 81 return nil 82 } 83 if opts.cleanup != nil { 84 cleanup = opts.cleanup 85 } 86 87 if opts.err != nil { 88 return consolidators.SeriesFetchResult{ 89 Metadata: block.NewResultMetadata(), 90 }, cleanup, opts.err 91 } 92 93 if opts.fetchCompressedSleep > 0 { 94 time.Sleep(opts.fetchCompressedSleep) 95 } 96 97 iters := opts.iters 98 if iters == nil { 99 it, err := test.BuildTestSeriesIterator(seriesID) 100 require.NoError(t, err) 101 iters = encoding.NewSeriesIterators([]encoding.SeriesIterator{it}) 102 } 103 104 res, err := consolidators.NewSeriesFetchResult( 105 iters, 106 nil, 107 block.NewResultMetadata(), 108 ) 109 return res, cleanup, err 110 }).AnyTimes() 111 return store 112 } 113 114 func checkRemoteFetch(t *testing.T, r storage.PromResult) { 115 res := r.PromResult 116 seriesList := res.GetTimeseries() 117 require.Equal(t, 1, len(seriesList)) 118 119 for _, series := range seriesList { 120 datapoints := series.GetSamples() 121 values := make([]float64, 0, len(datapoints)) 122 for _, d := range datapoints { 123 values = append(values, d.GetValue()) 124 } 125 126 require.Equal(t, expectedValues(), values) 127 } 128 } 129 130 func startServer(t *testing.T, ctrl *gomock.Controller, 131 store m3.Storage, 132 ) net.Listener { 133 server := NewGRPCServer(store, models.QueryContextOptions{}, 134 poolsWrapper, instrument.NewOptions()) 135 136 listener, err := net.Listen("tcp", "127.0.0.1:0") 137 require.NoError(t, err) 138 139 go func() { 140 server.Serve(listener) 141 }() 142 143 return listener 144 } 145 146 func createCtxReadOpts(t *testing.T) (context.Context, 147 *storage.FetchQuery, *storage.FetchOptions, 148 ) { 149 ctx := context.Background() 150 read, _, _ := createStorageFetchQuery(t) 151 readOpts := storage.NewFetchOptions() 152 readOpts.SeriesLimit = 300 153 return ctx, read, readOpts 154 } 155 156 func checkFetch(ctx context.Context, t *testing.T, client Client, 157 read *storage.FetchQuery, readOpts *storage.FetchOptions, 158 ) { 159 fetch, err := client.FetchProm(ctx, read, readOpts) 160 require.NoError(t, err) 161 checkRemoteFetch(t, fetch) 162 } 163 164 func checkErrorFetch(ctx context.Context, t *testing.T, client Client, 165 read *storage.FetchQuery, readOpts *storage.FetchOptions, 166 ) { 167 _, err := client.FetchProm(ctx, read, readOpts) 168 assert.Equal(t, errRead.Error(), grpc.ErrorDesc(err)) 169 } 170 171 func buildClient(t *testing.T, hosts []string) Client { 172 readWorkerPool, err := xsync.NewPooledWorkerPool(runtime.GOMAXPROCS(0), 173 xsync.NewPooledWorkerPoolOptions()) 174 readWorkerPool.Init() 175 require.NoError(t, err) 176 177 opts := m3.NewOptions(encoding.NewOptions()). 178 SetReadWorkerPool(readWorkerPool). 179 SetTagOptions(models.NewTagOptions()) 180 181 client, err := NewGRPCClient(testName, hosts, poolsWrapper, opts, 182 instrument.NewTestOptions(t)) 183 require.NoError(t, err) 184 return client 185 } 186 187 func TestRpc(t *testing.T) { 188 ctrl := gomock.NewController((*panicReporter)(t)) 189 defer ctrl.Finish() 190 191 ctx, read, readOpts := createCtxReadOpts(t) 192 store := newMockStorage(t, ctrl, mockStorageOptions{}) 193 listener := startServer(t, ctrl, store) 194 client := buildClient(t, []string{listener.Addr().String()}) 195 defer func() { 196 assert.NoError(t, client.Close()) 197 }() 198 199 checkFetch(ctx, t, client, read, readOpts) 200 } 201 202 // panicReporter is a workaround for the fact t.Fatalf calls in background threads 203 // can cause hangs. panicReporter panics instead to fail the test early. 204 type panicReporter testing.T 205 206 func (t *panicReporter) Errorf(format string, args ...interface{}) { 207 (*testing.T)(t).Errorf(format, args...) 208 } 209 210 func (*panicReporter) Fatalf(format string, args ...interface{}) { 211 panic(fmt.Sprintf(format, args...)) 212 } 213 214 func TestRpcHealth(t *testing.T) { 215 ctrl := gomock.NewController(t) 216 defer ctrl.Finish() 217 218 ctx, _, _ := createCtxReadOpts(t) 219 store := newMockStorage(t, ctrl, mockStorageOptions{}) 220 listener := startServer(t, ctrl, store) 221 serverClient := buildClient(t, []string{listener.Addr().String()}) 222 defer func() { 223 assert.NoError(t, serverClient.Close()) 224 }() 225 226 client, ok := serverClient.(*grpcClient) 227 require.True(t, ok) 228 229 resp, err := client.client.Health(ctx, &rpc.HealthRequest{}) 230 require.NoError(t, err) 231 232 uptime, err := time.ParseDuration(resp.UptimeDuration) 233 require.NoError(t, err) 234 assert.True(t, uptime > 0) 235 assert.Equal(t, uptime, time.Duration(resp.UptimeNanoseconds)) 236 } 237 238 func TestRpcMultipleRead(t *testing.T) { 239 ctrl := gomock.NewController(t) 240 defer ctrl.Finish() 241 242 ctx, read, readOpts := createCtxReadOpts(t) 243 store := newMockStorage(t, ctrl, mockStorageOptions{}) 244 listener := startServer(t, ctrl, store) 245 client := buildClient(t, []string{listener.Addr().String()}) 246 defer func() { 247 assert.NoError(t, client.Close()) 248 }() 249 250 fetch, err := client.FetchProm(ctx, read, readOpts) 251 require.NoError(t, err) 252 253 checkRemoteFetch(t, fetch) 254 } 255 256 func TestRpcStopsStreamingWhenFetchKilledOnClient(t *testing.T) { 257 ctrl := gomock.NewController(t) 258 defer ctrl.Finish() 259 260 ctx, read, readOpts := createCtxReadOpts(t) 261 store := newMockStorage(t, ctrl, mockStorageOptions{ 262 fetchCompressedSleep: time.Second, 263 }) 264 265 listener := startServer(t, ctrl, store) 266 client := buildClient(t, []string{listener.Addr().String()}) 267 defer func() { 268 assert.NoError(t, client.Close()) 269 }() 270 271 ctx, cancel := context.WithTimeout(ctx, 10*time.Millisecond) 272 defer cancel() 273 274 _, err := client.FetchProm(ctx, read, readOpts) 275 276 require.Error(t, err) 277 } 278 279 func TestMultipleClientRpc(t *testing.T) { 280 ctrl := gomock.NewController(t) 281 defer ctrl.Finish() 282 283 ctx, read, readOpts := createCtxReadOpts(t) 284 store := newMockStorage(t, ctrl, mockStorageOptions{ 285 fetchCompressedSleep: 10 * time.Millisecond, 286 }) 287 288 listener := startServer(t, ctrl, store) 289 290 var wg sync.WaitGroup 291 clients := make([]Client, 10) 292 for i := range clients { 293 clients[i] = buildClient(t, []string{listener.Addr().String()}) 294 } 295 296 defer func() { 297 for _, client := range clients { 298 assert.NoError(t, client.Close()) 299 } 300 }() 301 302 for _, client := range clients { 303 wg.Add(1) 304 client := client 305 go func() { 306 checkFetch(ctx, t, client, read, readOpts) 307 wg.Done() 308 }() 309 } 310 311 wg.Wait() 312 } 313 314 func TestEmptyAddressListErrors(t *testing.T) { 315 addresses := []string{} 316 opts := m3.NewOptions(encoding.NewOptions()) 317 client, err := NewGRPCClient(testName, addresses, poolsWrapper, opts, 318 instrument.NewTestOptions(t), grpc.WithBlock()) 319 assert.Nil(t, client) 320 assert.Equal(t, m3err.ErrNoClientAddresses, err) 321 } 322 323 func TestErrRpc(t *testing.T) { 324 ctrl := gomock.NewController(t) 325 defer ctrl.Finish() 326 327 ctx, read, readOpts := createCtxReadOpts(t) 328 store := newMockStorage(t, ctrl, mockStorageOptions{ 329 err: errors.New("read error"), 330 }) 331 332 listener := startServer(t, ctrl, store) 333 client := buildClient(t, []string{listener.Addr().String()}) 334 defer func() { 335 assert.NoError(t, client.Close()) 336 }() 337 338 checkErrorFetch(ctx, t, client, read, readOpts) 339 } 340 341 func TestRoundRobinClientRpc(t *testing.T) { 342 ctrl := gomock.NewController(t) 343 defer ctrl.Finish() 344 345 ctx, read, readOpts := createCtxReadOpts(t) 346 store := newMockStorage(t, ctrl, mockStorageOptions{}) 347 errStore := newMockStorage(t, ctrl, mockStorageOptions{ 348 err: errors.New("read error"), 349 }) 350 351 listener1 := startServer(t, ctrl, store) 352 listener2 := startServer(t, ctrl, errStore) 353 354 hosts := []string{listener1.Addr().String(), listener2.Addr().String()} 355 client := buildClient(t, hosts) 356 defer func() { 357 assert.NoError(t, client.Close()) 358 }() 359 360 // Host ordering is not always deterministic; retry several times to ensure 361 // at least one call is made to both hosts. Giving 10 attempts per host should 362 // remove flakiness while guaranteeing round robin behaviour. 363 attempts := 20 364 365 hitHost, hitErrHost := false, false 366 for i := 0; i < attempts; i++ { 367 fetch, err := client.FetchProm(ctx, read, readOpts) 368 if err != nil { 369 assert.Equal(t, errRead.Error(), grpc.ErrorDesc(err)) 370 hitErrHost = true 371 } else { 372 checkRemoteFetch(t, fetch) 373 hitHost = true 374 } 375 if hitHost && hitErrHost { 376 break 377 } 378 } 379 380 assert.True(t, hitHost, "round robin did not fetch from host") 381 assert.True(t, hitErrHost, "round robin did not fetch from error host") 382 } 383 384 func TestBatchedFetch(t *testing.T) { 385 ctrl := gomock.NewController(t) 386 defer ctrl.Finish() 387 388 ctx, read, readOpts := createCtxReadOpts(t) 389 exNames := []string{"baz", "foo"} 390 exValues := []string{"qux", "bar"} 391 sizes := []int{ 392 0, 1, defaultBatch - 1, defaultBatch, 393 defaultBatch + 1, defaultBatch*2 + 1, 394 } 395 396 for _, size := range sizes { 397 var ( 398 msg = fmt.Sprintf("batch size: %d", size) 399 iters = make([]encoding.SeriesIterator, 0, size) 400 cleaned = false 401 ) 402 403 for i := 0; i < size; i++ { 404 id := fmt.Sprintf("%s_%d", seriesID, i) 405 it, err := test.BuildTestSeriesIterator(id) 406 require.NoError(t, err, msg) 407 iters = append(iters, it) 408 } 409 410 store := newMockStorage(t, ctrl, mockStorageOptions{ 411 iters: encoding.NewSeriesIterators(iters), 412 cleanup: func() error { 413 require.False(t, cleaned, msg) 414 cleaned = true 415 return nil 416 }, 417 }) 418 419 listener := startServer(t, ctrl, store) 420 client := buildClient(t, []string{listener.Addr().String()}) 421 defer func() { 422 assert.NoError(t, client.Close()) 423 }() 424 425 fetch, err := client.FetchProm(ctx, read, readOpts) 426 require.NoError(t, err, msg) 427 seriesList := fetch.PromResult.GetTimeseries() 428 require.Equal(t, size, len(seriesList), msg) 429 for _, series := range seriesList { 430 samples := series.GetSamples() 431 values := make([]float64, 0, len(samples)) 432 for _, d := range samples { 433 values = append(values, d.GetValue()) 434 } 435 436 require.Equal(t, expectedValues(), values, msg) 437 require.Equal(t, 2, len(series.GetLabels())) 438 for i, l := range series.GetLabels() { 439 assert.Equal(t, exNames[i], string(l.Name)) 440 assert.Equal(t, exValues[i], string(l.Value)) 441 } 442 } 443 444 require.True(t, cleaned, msg) 445 } 446 } 447 448 func TestBatchedSearch(t *testing.T) { 449 ctrl := gomock.NewController(t) 450 defer ctrl.Finish() 451 452 ctx, q, readOpts := createCtxReadOpts(t) 453 sizes := []int{ 454 0, 1, defaultBatch - 1, defaultBatch, 455 defaultBatch + 1, defaultBatch*2 + 1, 456 } 457 for _, size := range sizes { 458 var ( 459 msg = fmt.Sprintf("batch size: %d", size) 460 tags = make([]consolidators.MultiTagResult, 0, size) 461 names = make([]string, 0, size) 462 cleaned = false 463 ) 464 465 noopCleanup := func() error { 466 require.False(t, cleaned, msg) 467 cleaned = true 468 return nil 469 } 470 471 for i := 0; i < size; i++ { 472 name := fmt.Sprintf("%s_%d", seriesID, i) 473 tag := consolidators.MultiTagResult{ 474 ID: ident.StringID(name), 475 Iter: ident.NewTagsIterator(ident.NewTags( 476 ident.Tag{ 477 Name: ident.StringID(name), 478 Value: ident.StringID(name), 479 }, 480 )), 481 } 482 483 tags = append(tags, tag) 484 names = append(names, name) 485 } 486 487 store := m3.NewMockStorage(ctrl) 488 tagResult := consolidators.TagResult{ 489 Tags: tags, 490 Metadata: block.NewResultMetadata(), 491 } 492 493 store.EXPECT().SearchCompressed(gomock.Any(), gomock.Any(), gomock.Any()). 494 Return(tagResult, noopCleanup, nil) 495 496 listener := startServer(t, ctrl, store) 497 client := buildClient(t, []string{listener.Addr().String()}) 498 defer func() { 499 assert.NoError(t, client.Close()) 500 }() 501 502 result, err := client.SearchSeries(ctx, q, readOpts) 503 require.NoError(t, err, msg) 504 require.Equal(t, size, len(result.Metrics), msg) 505 506 for i, m := range result.Metrics { 507 n := names[i] 508 require.Equal(t, n, string(m.ID), msg) 509 } 510 511 require.True(t, cleaned, msg) 512 } 513 } 514 515 func TestBatchedCompleteTags(t *testing.T) { 516 ctrl := gomock.NewController(t) 517 defer ctrl.Finish() 518 519 ctx, _, readOpts := createCtxReadOpts(t) 520 namesOnly := []bool{true, false} 521 for _, nameOnly := range namesOnly { 522 q := &storage.CompleteTagsQuery{ 523 CompleteNameOnly: nameOnly, 524 } 525 526 sizes := []int{ 527 0, 1, defaultBatch - 1, defaultBatch, 528 defaultBatch + 1, defaultBatch*2 + 1, 529 } 530 for _, size := range sizes { 531 var ( 532 msg = fmt.Sprintf("batch size: %d, name only: %t", size, nameOnly) 533 tags = make([]consolidators.CompletedTag, 0, size) 534 ) 535 536 for i := 0; i < size; i++ { 537 name := fmt.Sprintf("%s_%d", seriesID, i) 538 tag := consolidators.CompletedTag{ 539 Name: []byte(name), 540 } 541 542 if !nameOnly { 543 tag.Values = [][]byte{[]byte("a"), []byte("b")} 544 } 545 546 tags = append(tags, tag) 547 } 548 549 store := m3.NewMockStorage(ctrl) 550 expected := &consolidators.CompleteTagsResult{ 551 CompleteNameOnly: nameOnly, 552 CompletedTags: tags, 553 Metadata: block.ResultMetadata{ 554 Exhaustive: false, 555 LocalOnly: true, 556 Warnings: []block.Warning{{Name: "foo", Message: "bar"}}, 557 }, 558 } 559 560 store.EXPECT().CompleteTagsCompressed(gomock.Any(), gomock.Any(), gomock.Any()). 561 Return(expected, nil) 562 563 listener := startServer(t, ctrl, store) 564 client := buildClient(t, []string{listener.Addr().String()}) 565 defer func() { 566 assert.NoError(t, client.Close()) 567 }() 568 569 result, err := client.CompleteTags(ctx, q, readOpts) 570 require.NoError(t, err, msg) 571 require.Equal(t, size, len(result.CompletedTags), msg) 572 if size == 0 { 573 // NB: 0 result is exhaustive and no warnings should be seen. 574 expected.Metadata = block.NewResultMetadata() 575 } else { 576 // NB: since this is a fanout with remotes, LocalOnly should be false. 577 expected.Metadata.LocalOnly = false 578 } 579 assert.Equal(t, expected, result, msg) 580 } 581 } 582 }