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  }