github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/src/dbnode/client/session_write_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  	"errors"
    25  	"fmt"
    26  	"strconv"
    27  	"strings"
    28  	"sync"
    29  	"testing"
    30  	"time"
    31  
    32  	"github.com/m3db/m3/src/dbnode/generated/thrift/rpc"
    33  	"github.com/m3db/m3/src/dbnode/topology"
    34  	xmetrics "github.com/m3db/m3/src/dbnode/x/metrics"
    35  	xerrors "github.com/m3db/m3/src/x/errors"
    36  	"github.com/m3db/m3/src/x/ident"
    37  	"github.com/m3db/m3/src/x/instrument"
    38  	xretry "github.com/m3db/m3/src/x/retry"
    39  	xtest "github.com/m3db/m3/src/x/test"
    40  	xtime "github.com/m3db/m3/src/x/time"
    41  
    42  	"github.com/golang/mock/gomock"
    43  	"github.com/stretchr/testify/assert"
    44  	"github.com/stretchr/testify/require"
    45  	"github.com/uber-go/tally"
    46  )
    47  
    48  func TestSessionWriteNotOpenError(t *testing.T) {
    49  	ctrl := gomock.NewController(t)
    50  	defer ctrl.Finish()
    51  
    52  	s := newDefaultTestSession(t)
    53  
    54  	err := s.Write(ident.StringID("namespace"), ident.StringID("foo"), xtime.Now(),
    55  		1.337, xtime.Second, nil)
    56  	assert.Equal(t, ErrSessionStatusNotOpen, err)
    57  }
    58  
    59  func TestSessionWrite(t *testing.T) {
    60  	testSessionWrite(t, testOptions{
    61  		opts: newSessionTestOptions(),
    62  	})
    63  }
    64  
    65  func testSessionWrite(t *testing.T, testOpts testOptions) {
    66  	ctrl := gomock.NewController(t)
    67  	defer ctrl.Finish()
    68  
    69  	session := newTestSession(t, testOpts.opts).(*session)
    70  
    71  	w := newWriteStub()
    72  	if testOpts.setWriteAnn != nil {
    73  		testOpts.setWriteAnn(&w)
    74  	}
    75  
    76  	var completionFn completionFn
    77  	enqueueWg := mockHostQueues(ctrl, session, sessionTestReplicas, []testEnqueueFn{func(idx int, op op) {
    78  		completionFn = op.CompletionFn()
    79  		write, ok := op.(*writeOperation)
    80  		assert.True(t, ok)
    81  		assert.Equal(t, w.id.String(), string(write.request.ID))
    82  		assert.Equal(t, w.value, write.request.Datapoint.Value)
    83  		assert.Equal(t, w.t.Seconds(), write.request.Datapoint.Timestamp)
    84  		assert.Equal(t, rpc.TimeType_UNIX_SECONDS, write.request.Datapoint.TimestampTimeType)
    85  		assert.NotNil(t, write.completionFn)
    86  		if testOpts.annEqual != nil {
    87  			testOpts.annEqual(t, w.annotation, write.request.Datapoint.Annotation)
    88  		}
    89  	}})
    90  
    91  	assert.NoError(t, session.Open())
    92  
    93  	// Ensure consecutive opens cause errors
    94  	consecutiveOpenErr := session.Open()
    95  	assert.Error(t, consecutiveOpenErr)
    96  	assert.Equal(t, errSessionStatusNotInitial, consecutiveOpenErr)
    97  
    98  	// Begin write
    99  	var resultErr error
   100  	var writeWg sync.WaitGroup
   101  	writeWg.Add(1)
   102  	go func() {
   103  		resultErr = session.Write(w.ns, w.id, w.t, w.value, w.unit, w.annotation)
   104  		writeWg.Done()
   105  	}()
   106  
   107  	// Callback
   108  	enqueueWg.Wait()
   109  	for i := 0; i < session.state.topoMap.Replicas(); i++ {
   110  		completionFn(session.state.topoMap.Hosts()[0], nil)
   111  	}
   112  
   113  	// Wait for write to complete
   114  	writeWg.Wait()
   115  	assert.Nil(t, resultErr)
   116  
   117  	assert.NoError(t, session.Close())
   118  }
   119  
   120  func TestSessionWriteDoesNotCloneNoFinalize(t *testing.T) {
   121  	ctrl := gomock.NewController(t)
   122  	defer ctrl.Finish()
   123  
   124  	session := newDefaultTestSession(t).(*session)
   125  	w := newWriteStub()
   126  	var completionFn completionFn
   127  	enqueueWg := mockHostQueues(ctrl, session, sessionTestReplicas, []testEnqueueFn{func(idx int, op op) {
   128  		completionFn = op.CompletionFn()
   129  		write, ok := op.(*writeOperation)
   130  		require.True(t, ok)
   131  		require.True(t,
   132  			xtest.ByteSlicesBackedBySameData(
   133  				w.ns.Bytes(),
   134  				write.namespace.Bytes()))
   135  		require.True(t,
   136  			xtest.ByteSlicesBackedBySameData(
   137  				w.id.Bytes(),
   138  				write.request.ID))
   139  	}})
   140  
   141  	require.NoError(t, session.Open())
   142  
   143  	// Begin write
   144  	var resultErr error
   145  	var writeWg sync.WaitGroup
   146  	writeWg.Add(1)
   147  	go func() {
   148  		resultErr = session.Write(w.ns, w.id, w.t, w.value, w.unit, w.annotation)
   149  		writeWg.Done()
   150  	}()
   151  
   152  	// Callback
   153  	enqueueWg.Wait()
   154  	for i := 0; i < session.state.topoMap.Replicas(); i++ {
   155  		completionFn(session.state.topoMap.Hosts()[0], nil)
   156  	}
   157  
   158  	writeWg.Wait()
   159  	require.NoError(t, resultErr)
   160  	require.NoError(t, session.Close())
   161  }
   162  
   163  func TestSessionWriteBadUnitErr(t *testing.T) {
   164  	ctrl := gomock.NewController(t)
   165  	defer ctrl.Finish()
   166  
   167  	session := newDefaultTestSession(t).(*session)
   168  
   169  	w := struct {
   170  		ns         ident.ID
   171  		id         ident.ID
   172  		value      float64
   173  		t          xtime.UnixNano
   174  		unit       xtime.Unit
   175  		annotation []byte
   176  	}{
   177  		ns:         ident.StringID("testNs"),
   178  		id:         ident.StringID("foo"),
   179  		value:      1.0,
   180  		t:          xtime.Now(),
   181  		unit:       xtime.Unit(byte(255)),
   182  		annotation: nil,
   183  	}
   184  
   185  	mockHostQueues(ctrl, session, sessionTestReplicas, nil)
   186  
   187  	assert.NoError(t, session.Open())
   188  
   189  	assert.Error(t, session.Write(w.ns, w.id, w.t, w.value, w.unit, w.annotation))
   190  
   191  	assert.NoError(t, session.Close())
   192  }
   193  
   194  func TestSessionWriteBadRequestErrorIsNonRetryable(t *testing.T) {
   195  	ctrl := gomock.NewController(t)
   196  	defer ctrl.Finish()
   197  
   198  	scope := tally.NewTestScope("", nil)
   199  	opts := newSessionTestOptions().
   200  		SetInstrumentOptions(instrument.NewOptions().SetMetricsScope(scope))
   201  	session := newTestSession(t, opts).(*session)
   202  
   203  	w := struct {
   204  		ns         ident.ID
   205  		id         ident.ID
   206  		value      float64
   207  		t          xtime.UnixNano
   208  		unit       xtime.Unit
   209  		annotation []byte
   210  	}{
   211  		ns:         ident.StringID("testNs"),
   212  		id:         ident.StringID("foo"),
   213  		value:      1.0,
   214  		t:          xtime.Now(),
   215  		unit:       xtime.Second,
   216  		annotation: nil,
   217  	}
   218  
   219  	var hosts []topology.Host
   220  
   221  	mockHostQueues(ctrl, session, sessionTestReplicas, []testEnqueueFn{
   222  		func(idx int, op op) {
   223  			go func() {
   224  				op.CompletionFn()(hosts[idx], &rpc.Error{
   225  					Type:    rpc.ErrorType_BAD_REQUEST,
   226  					Message: "expected bad request error",
   227  				})
   228  			}()
   229  		},
   230  	})
   231  
   232  	assert.NoError(t, session.Open())
   233  
   234  	session.state.RLock()
   235  	hosts = session.state.topoMap.Hosts()
   236  	session.state.RUnlock()
   237  
   238  	err := session.Write(w.ns, w.id, w.t, w.value, w.unit, w.annotation)
   239  	assert.Error(t, err)
   240  	assert.True(t, xerrors.IsNonRetryableError(err))
   241  
   242  	// Assert counting bad request errors by number of nodes
   243  	counters := scope.Snapshot().Counters()
   244  	nodesBadRequestErrors, ok := counters["write.nodes-responding-error+error_type=bad_request_error,nodes=3"]
   245  	require.True(t, ok)
   246  	assert.Equal(t, int64(1), nodesBadRequestErrors.Value())
   247  
   248  	assert.NoError(t, session.Close())
   249  }
   250  
   251  func TestSessionWriteRetry(t *testing.T) {
   252  	ctrl := gomock.NewController(t)
   253  	defer ctrl.Finish()
   254  
   255  	scope := tally.NewTestScope("", nil)
   256  	opts := newSessionTestOptions().
   257  		SetInstrumentOptions(instrument.NewOptions().SetMetricsScope(scope))
   258  	session := newRetryEnabledTestSession(t, opts).(*session)
   259  
   260  	w := struct {
   261  		ns         ident.ID
   262  		id         ident.ID
   263  		value      float64
   264  		t          xtime.UnixNano
   265  		unit       xtime.Unit
   266  		annotation []byte
   267  	}{
   268  		ns:         ident.StringID("testNs"),
   269  		id:         ident.StringID("foo"),
   270  		value:      1.0,
   271  		t:          xtime.Now(),
   272  		unit:       xtime.Second,
   273  		annotation: nil,
   274  	}
   275  
   276  	var hosts []topology.Host
   277  	var completionFn completionFn
   278  	enqueueWg := mockHostQueues(ctrl, session, sessionTestReplicas, []testEnqueueFn{
   279  		func(idx int, op op) {
   280  			go func() {
   281  				op.CompletionFn()(hosts[idx], &rpc.Error{
   282  					Type:    rpc.ErrorType_INTERNAL_ERROR,
   283  					Message: "random internal issue",
   284  				})
   285  			}()
   286  		},
   287  		func(idx int, op op) {
   288  			write, ok := op.(*writeOperation)
   289  			assert.True(t, ok)
   290  			assert.Equal(t, w.id.String(), string(write.request.ID))
   291  			assert.Equal(t, w.value, write.request.Datapoint.Value)
   292  			assert.Equal(t, w.t.Seconds(), write.request.Datapoint.Timestamp)
   293  			assert.Equal(t, rpc.TimeType_UNIX_SECONDS, write.request.Datapoint.TimestampTimeType)
   294  			assert.NotNil(t, write.completionFn)
   295  			completionFn = write.completionFn
   296  		},
   297  	})
   298  
   299  	assert.NoError(t, session.Open())
   300  
   301  	session.state.RLock()
   302  	hosts = session.state.topoMap.Hosts()
   303  	session.state.RUnlock()
   304  
   305  	// Begin write
   306  	var resultErr error
   307  	var writeWg sync.WaitGroup
   308  	writeWg.Add(1)
   309  	go func() {
   310  		resultErr = session.Write(w.ns, w.id, w.t, w.value, w.unit, w.annotation)
   311  		writeWg.Done()
   312  	}()
   313  
   314  	// Callback
   315  	enqueueWg.Wait()
   316  	for i := 0; i < session.state.topoMap.Replicas(); i++ {
   317  		completionFn(session.state.topoMap.Hosts()[0], nil)
   318  	}
   319  
   320  	// Wait for write to complete
   321  	writeWg.Wait()
   322  	assert.Nil(t, resultErr)
   323  
   324  	// Assert counting bad request errors by number of nodes
   325  	counters := scope.Snapshot().Counters()
   326  	nodesBadRequestErrors, ok := counters["write.nodes-responding-error+error_type=server_error,nodes=3"]
   327  	require.True(t, ok)
   328  	assert.Equal(t, int64(1), nodesBadRequestErrors.Value())
   329  
   330  	assert.NoError(t, session.Close())
   331  }
   332  
   333  func TestSessionWriteConsistencyLevelAll(t *testing.T) {
   334  	ctrl := gomock.NewController(t)
   335  	defer ctrl.Finish()
   336  
   337  	level := topology.ConsistencyLevelAll
   338  	testWriteConsistencyLevel(t, ctrl, level, 3, 0, outcomeSuccess)
   339  	for i := 1; i <= 3; i++ {
   340  		testWriteConsistencyLevel(t, ctrl, level, 3-i, i, outcomeFail)
   341  	}
   342  }
   343  
   344  func TestSessionWriteConsistencyLevelMajority(t *testing.T) {
   345  	ctrl := gomock.NewController(t)
   346  	defer ctrl.Finish()
   347  
   348  	level := topology.ConsistencyLevelMajority
   349  	for i := 0; i <= 1; i++ {
   350  		testWriteConsistencyLevel(t, ctrl, level, 3-i, i, outcomeSuccess)
   351  		testWriteConsistencyLevel(t, ctrl, level, 3-i, 0, outcomeSuccess)
   352  	}
   353  	for i := 2; i <= 3; i++ {
   354  		testWriteConsistencyLevel(t, ctrl, level, 3-i, i, outcomeFail)
   355  	}
   356  }
   357  
   358  func TestSessionWriteConsistencyLevelOne(t *testing.T) {
   359  	ctrl := gomock.NewController(t)
   360  	defer ctrl.Finish()
   361  
   362  	level := topology.ConsistencyLevelOne
   363  	for i := 0; i <= 2; i++ {
   364  		testWriteConsistencyLevel(t, ctrl, level, 3-i, i, outcomeSuccess)
   365  		testWriteConsistencyLevel(t, ctrl, level, 3-i, 0, outcomeSuccess)
   366  	}
   367  	testWriteConsistencyLevel(t, ctrl, level, 0, 3, outcomeFail)
   368  }
   369  
   370  func testWriteConsistencyLevel(
   371  	t *testing.T,
   372  	ctrl *gomock.Controller,
   373  	level topology.ConsistencyLevel,
   374  	success, failures int,
   375  	expected outcome,
   376  ) {
   377  	opts := newSessionTestOptions()
   378  	opts = opts.SetWriteConsistencyLevel(level)
   379  
   380  	reporterOpts := xmetrics.NewTestStatsReporterOptions().
   381  		SetCaptureEvents(true)
   382  	reporter := xmetrics.NewTestStatsReporter(reporterOpts)
   383  	scope, closer := tally.NewRootScope(tally.ScopeOptions{Reporter: reporter}, time.Millisecond)
   384  
   385  	defer closer.Close()
   386  
   387  	opts = opts.SetInstrumentOptions(opts.InstrumentOptions().
   388  		SetMetricsScope(scope))
   389  
   390  	session := newTestSession(t, opts).(*session)
   391  
   392  	w := struct {
   393  		ns         ident.ID
   394  		id         ident.ID
   395  		value      float64
   396  		t          xtime.UnixNano
   397  		unit       xtime.Unit
   398  		annotation []byte
   399  	}{
   400  		ns:         ident.StringID("testNs"),
   401  		id:         ident.StringID("foo"),
   402  		value:      1.0,
   403  		t:          xtime.Now(),
   404  		unit:       xtime.Second,
   405  		annotation: nil,
   406  	}
   407  
   408  	var completionFn completionFn
   409  	enqueueWg := mockHostQueues(ctrl, session, sessionTestReplicas, []testEnqueueFn{func(idx int, op op) {
   410  		completionFn = op.CompletionFn()
   411  	}})
   412  
   413  	assert.NoError(t, session.Open())
   414  
   415  	// Begin write
   416  	var resultErr error
   417  	var writeWg sync.WaitGroup
   418  	writeWg.Add(1)
   419  	go func() {
   420  		resultErr = session.Write(w.ns, w.id, w.t, w.value, w.unit, w.annotation)
   421  		writeWg.Done()
   422  	}()
   423  
   424  	// Callback
   425  	enqueueWg.Wait()
   426  	host := session.state.topoMap.Hosts()[0] // any host
   427  	writeErr := "a specific write error"
   428  	for i := 0; i < success; i++ {
   429  		completionFn(host, nil)
   430  	}
   431  	for i := 0; i < failures; i++ {
   432  		completionFn(host, fmt.Errorf(writeErr))
   433  	}
   434  
   435  	// Wait for write to complete or timeout
   436  	doneCh := make(chan struct{})
   437  	go func() {
   438  		writeWg.Wait()
   439  		close(doneCh)
   440  	}()
   441  
   442  	// NB(bl): Check whether we're correctly signaling in
   443  	// write_state.completionFn. If not, the write won't complete.
   444  	select {
   445  	case <-time.After(time.Second):
   446  		require.NoError(t, errors.New("session write failed to signal"))
   447  	case <-doneCh:
   448  		// continue
   449  	}
   450  
   451  	switch expected {
   452  	case outcomeSuccess:
   453  		assert.NoError(t, resultErr)
   454  	case outcomeFail:
   455  		assert.Error(t, resultErr)
   456  
   457  		resultErrStr := fmt.Sprintf("%v", resultErr)
   458  		assert.True(t, strings.Contains(resultErrStr,
   459  			fmt.Sprintf("failed to meet consistency level %s", level.String())))
   460  		assert.True(t, strings.Contains(resultErrStr,
   461  			writeErr))
   462  	}
   463  
   464  	assert.NoError(t, session.Close())
   465  
   466  	counters := reporter.Counters()
   467  	for counters["write.success"] == 0 && counters["write.errors"] == 0 {
   468  		time.Sleep(time.Millisecond)
   469  		counters = reporter.Counters()
   470  	}
   471  	if expected == outcomeSuccess {
   472  		assert.Equal(t, 1, int(counters["write.success"]))
   473  		assert.Equal(t, 0, int(counters["write.errors"]))
   474  	} else {
   475  		assert.Equal(t, 0, int(counters["write.success"]))
   476  		assert.Equal(t, 1, int(counters["write.errors"]))
   477  	}
   478  	if failures > 0 {
   479  		for _, event := range reporter.Events() {
   480  			if event.Name() == "write.nodes-responding-error" {
   481  				nodesFailing, convErr := strconv.Atoi(event.Tags()["nodes"])
   482  				require.NoError(t, convErr)
   483  				assert.True(t, 0 < nodesFailing && nodesFailing <= failures)
   484  				assert.Equal(t, int64(1), event.Value())
   485  				break
   486  			}
   487  		}
   488  	}
   489  }
   490  
   491  type writeStub struct {
   492  	ns         ident.ID
   493  	id         ident.ID
   494  	value      float64
   495  	t          xtime.UnixNano
   496  	unit       xtime.Unit
   497  	annotation []byte
   498  }
   499  
   500  func newTestSession(t *testing.T, opts Options) clientSession {
   501  	s, err := newSession(opts)
   502  	assert.NoError(t, err)
   503  	return s
   504  }
   505  
   506  func newDefaultTestSession(t *testing.T) clientSession {
   507  	return newTestSession(t, newSessionTestOptions())
   508  }
   509  
   510  func newRetryEnabledTestSession(t *testing.T, opts Options) clientSession {
   511  	opts = opts.
   512  		SetWriteRetrier(
   513  			xretry.NewRetrier(xretry.NewOptions().SetMaxRetries(1)))
   514  	return newTestSession(t, opts)
   515  }
   516  
   517  func newWriteStub() writeStub {
   518  	return writeStub{
   519  		ns:         ident.StringID("testNs"),
   520  		id:         ident.StringID("foo"),
   521  		value:      1.0,
   522  		t:          xtime.Now(),
   523  		unit:       xtime.Second,
   524  		annotation: nil}
   525  }