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  }