github.com/m3db/m3@v1.5.0/src/aggregator/client/conn_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 client
    22  
    23  import (
    24  	"errors"
    25  	"fmt"
    26  	"math"
    27  	"net"
    28  	"sync"
    29  	"testing"
    30  	"time"
    31  
    32  	"github.com/m3db/m3/src/x/clock"
    33  
    34  	"github.com/leanovate/gopter"
    35  	"github.com/leanovate/gopter/gen"
    36  	"github.com/leanovate/gopter/prop"
    37  	"github.com/stretchr/testify/require"
    38  )
    39  
    40  const (
    41  	testFakeServerAddr        = "nonexistent"
    42  	testLocalServerAddr       = "127.0.0.1:0"
    43  	testRandomSeeed           = 831992
    44  	testMinSuccessfulTests    = 1000
    45  	testReconnectThreshold    = 1024
    46  	testMaxReconnectThreshold = 8096
    47  )
    48  
    49  var (
    50  	errTestConnect = errors.New("connect error")
    51  	errTestWrite   = errors.New("write error")
    52  )
    53  
    54  func TestConnectionDontReconnectProperties(t *testing.T) {
    55  	props := testConnectionProperties()
    56  	props.Property(
    57  		`When the number of failures is less than or equal to the threshold and the time since last `+
    58  			`connection is less than the maximum duration writes should:
    59  	  - not attempt to reconnect
    60  	  - increment the number of failures`,
    61  		prop.ForAll(
    62  			func(numFailures int32) (bool, error) {
    63  				conn := newConnection(testFakeServerAddr,
    64  					testConnectionOptions().
    65  						SetMaxReconnectDuration(time.Duration(math.MaxInt64)),
    66  				)
    67  				conn.connectWithLockFn = func() error { return errTestConnect }
    68  				conn.numFailures = int(numFailures)
    69  				conn.threshold = testReconnectThreshold
    70  
    71  				if err := conn.Write(nil); err != errNoActiveConnection {
    72  					return false, fmt.Errorf("unexpected error: %v", err)
    73  				}
    74  
    75  				expected := int(numFailures + 1)
    76  				if conn.numFailures != expected {
    77  					return false, fmt.Errorf(
    78  						"expected the number of failures to be: %v, but found: %v", expected, conn.numFailures,
    79  					)
    80  				}
    81  
    82  				return true, nil
    83  			},
    84  			gen.Int32Range(0, testReconnectThreshold),
    85  		))
    86  
    87  	props.TestingRun(t)
    88  }
    89  
    90  func TestConnectionNumFailuresThresholdReconnectProperty(t *testing.T) {
    91  	props := testConnectionProperties()
    92  	props.Property(
    93  		"When number of failures is greater than the threshold, it is multiplied",
    94  		prop.ForAll(
    95  			func(threshold int32) (bool, error) {
    96  				conn := newConnection(testFakeServerAddr, testConnectionOptions())
    97  				conn.connectWithLockFn = func() error { return errTestConnect }
    98  				conn.threshold = int(threshold)
    99  				conn.multiplier = 2
   100  				conn.numFailures = conn.threshold + 1
   101  				conn.maxThreshold = testMaxReconnectThreshold
   102  
   103  				expectedNewThreshold := conn.threshold * conn.multiplier
   104  				if expectedNewThreshold > conn.maxThreshold {
   105  					expectedNewThreshold = conn.maxThreshold
   106  				}
   107  				if err := conn.Write(nil); !errors.Is(err, errTestConnect) {
   108  					return false, fmt.Errorf("unexpected error: %w", err)
   109  				}
   110  
   111  				require.Equal(t, expectedNewThreshold, conn.threshold)
   112  				return true, nil
   113  			},
   114  			gen.Int32Range(1, testMaxReconnectThreshold),
   115  		))
   116  	props.Property(
   117  		"When the number of failures is greater than the threshold writes should attempt to reconnect",
   118  		prop.ForAll(
   119  			func(threshold int32) (bool, error) {
   120  				conn := newConnection(testFakeServerAddr, testConnectionOptions())
   121  				conn.connectWithLockFn = func() error { return errTestConnect }
   122  				conn.threshold = int(threshold)
   123  				conn.numFailures = conn.threshold + 1
   124  				conn.maxThreshold = 2 * conn.numFailures
   125  
   126  				if err := conn.Write(nil); !errors.Is(err, errTestConnect) {
   127  					return false, fmt.Errorf("unexpected error: %w", err)
   128  				}
   129  				return true, nil
   130  			},
   131  			gen.Int32Range(1, testMaxReconnectThreshold),
   132  		))
   133  	props.Property(
   134  		"When the number of failures is greater than the max threshold writes must not attempt to reconnect",
   135  		prop.ForAll(
   136  			func(threshold int32) (bool, error) {
   137  				conn := newConnection(testFakeServerAddr, testConnectionOptions())
   138  				conn.connectWithLockFn = func() error { return errTestConnect }
   139  				// Exhausted max threshold
   140  				conn.threshold = int(threshold)
   141  				conn.maxThreshold = conn.threshold
   142  				conn.maxDuration = math.MaxInt64
   143  				conn.numFailures = conn.maxThreshold + 1
   144  
   145  				if err := conn.Write(nil); !errors.Is(err, errNoActiveConnection) {
   146  					return false, fmt.Errorf("unexpected error: %w", err)
   147  				}
   148  				return true, nil
   149  			},
   150  			gen.Int32Range(1, testMaxReconnectThreshold),
   151  		))
   152  	props.Property(
   153  		`When the number of failures is greater than the max threshold
   154  		 but time since last connection attempt is greater than the maximum duration
   155  		 then writes should attempt to reconnect`,
   156  		prop.ForAll(
   157  			func(delay int64) (bool, error) {
   158  				conn := newConnection(testFakeServerAddr, testConnectionOptions())
   159  				conn.connectWithLockFn = func() error { return errTestConnect }
   160  				// Exhausted max threshold
   161  				conn.threshold = 1
   162  				conn.maxThreshold = conn.threshold
   163  				conn.numFailures = conn.maxThreshold + 1
   164  
   165  				now := time.Now()
   166  				conn.nowFn = func() time.Time { return now }
   167  				conn.lastConnectAttemptNanos = now.UnixNano() - delay
   168  				conn.maxDuration = time.Duration(delay)
   169  
   170  				if err := conn.Write(nil); !errors.Is(err, errTestConnect) {
   171  					return false, fmt.Errorf("unexpected error: %w", err)
   172  				}
   173  				return true, nil
   174  			},
   175  			gen.Int64Range(1, math.MaxInt64),
   176  		))
   177  
   178  	props.TestingRun(t)
   179  }
   180  
   181  func TestConnectionMaxDurationReconnectProperty(t *testing.T) {
   182  	props := testConnectionProperties()
   183  	props.Property(
   184  		"When the time since last connection is greater than the maximum duration writes should attempt to reconnect",
   185  		prop.ForAll(
   186  			func(delay int64) (bool, error) {
   187  				conn := newConnection(testFakeServerAddr, testConnectionOptions())
   188  				conn.connectWithLockFn = func() error { return errTestConnect }
   189  				now := time.Now()
   190  				conn.nowFn = func() time.Time { return now }
   191  				conn.lastConnectAttemptNanos = now.UnixNano() - delay
   192  				conn.maxDuration = time.Duration(delay)
   193  
   194  				if err := conn.Write(nil); err != errTestConnect {
   195  					return false, fmt.Errorf("unexpected error: %v", err)
   196  				}
   197  				return true, nil
   198  			},
   199  			gen.Int64Range(1, math.MaxInt64),
   200  		))
   201  
   202  	props.TestingRun(t)
   203  }
   204  
   205  func TestConnectionReconnectProperties(t *testing.T) {
   206  	props := testConnectionProperties()
   207  	props.Property(
   208  		`When there is no active connection and a write cannot establish one it should:
   209  		- set number of failures to threshold + 2
   210  	  - update the threshold to be min(threshold*multiplier, maxThreshold)`,
   211  		prop.ForAll(
   212  			func(threshold, multiplier int32) (bool, error) {
   213  				conn := newConnection(testFakeServerAddr, testConnectionOptions())
   214  				conn.connectWithLockFn = func() error { return errTestConnect }
   215  				conn.threshold = int(threshold)
   216  				conn.numFailures = conn.threshold + 1
   217  				conn.multiplier = int(multiplier)
   218  				conn.maxThreshold = testMaxReconnectThreshold
   219  
   220  				if err := conn.Write(nil); err != errTestConnect {
   221  					return false, fmt.Errorf("unexpected error: %v", err)
   222  				}
   223  
   224  				if conn.numFailures != int(threshold+2) {
   225  					return false, fmt.Errorf(
   226  						"expected the number of failures to be %d, but found: %v", threshold+2, conn.numFailures,
   227  					)
   228  				}
   229  
   230  				expected := int(threshold * multiplier)
   231  				if expected > testMaxReconnectThreshold {
   232  					expected = testMaxReconnectThreshold
   233  				}
   234  
   235  				if conn.threshold != expected {
   236  					return false, fmt.Errorf(
   237  						"expected the new threshold to be %v, but found: %v", expected, conn.threshold,
   238  					)
   239  				}
   240  
   241  				return true, nil
   242  			},
   243  			gen.Int32Range(1, testMaxReconnectThreshold),
   244  			gen.Int32Range(1, 16),
   245  		))
   246  
   247  	props.TestingRun(t)
   248  }
   249  
   250  func TestConnectionWriteSucceedsOnSecondAttempt(t *testing.T) {
   251  	conn := newConnection(testFakeServerAddr, testConnectionOptions())
   252  	conn.numFailures = 3
   253  	conn.connectWithLockFn = func() error { return nil }
   254  	var count int
   255  	conn.writeWithLockFn = func([]byte) error {
   256  		count++
   257  		if count == 1 {
   258  			return errTestWrite
   259  		}
   260  		return nil
   261  	}
   262  
   263  	require.NoError(t, conn.Write(nil))
   264  	require.Equal(t, 0, conn.numFailures)
   265  	require.Equal(t, 2, conn.threshold)
   266  }
   267  
   268  func TestConnectionWriteFailsOnSecondAttempt(t *testing.T) {
   269  	conn := newConnection(testFakeServerAddr, testConnectionOptions())
   270  	conn.numFailures = 3
   271  	conn.writeWithLockFn = func([]byte) error { return errTestWrite }
   272  	var count int
   273  	conn.connectWithLockFn = func() error {
   274  		count++
   275  		if count == 1 {
   276  			return nil
   277  		}
   278  		return errTestConnect
   279  	}
   280  
   281  	require.Equal(t, errTestConnect, conn.Write(nil))
   282  	require.Equal(t, 1, conn.numFailures)
   283  	require.Equal(t, 2, conn.threshold)
   284  }
   285  
   286  func TestConnectWriteToServer(t *testing.T) {
   287  	data := []byte("foobar")
   288  
   289  	// Start tcp server.
   290  	var wg sync.WaitGroup
   291  	wg.Add(1)
   292  
   293  	l, err := net.Listen(tcpProtocol, testLocalServerAddr)
   294  	require.NoError(t, err)
   295  	serverAddr := l.Addr().String()
   296  
   297  	go func() {
   298  		defer wg.Done()
   299  
   300  		// Ignore the first testing connection.
   301  		conn, err := l.Accept()
   302  		require.NoError(t, err)
   303  		require.NoError(t, conn.Close())
   304  
   305  		// Read from the second connection.
   306  		conn, err = l.Accept()
   307  		require.NoError(t, err)
   308  		buf := make([]byte, 1024)
   309  		n, err := conn.Read(buf)
   310  		require.NoError(t, err)
   311  		require.Equal(t, data, buf[:n])
   312  		conn.Close() // nolint: errcheck
   313  	}()
   314  
   315  	// Wait until the server starts up.
   316  	testConn, err := net.DialTimeout(tcpProtocol, serverAddr, time.Minute)
   317  	require.NoError(t, err)
   318  	require.NoError(t, testConn.Close())
   319  
   320  	// Create a new connection and assert we can write successfully.
   321  	opts := testConnectionOptions().SetInitReconnectThreshold(0)
   322  	conn := newConnection(serverAddr, opts)
   323  	require.NoError(t, conn.Write(data))
   324  	require.Equal(t, 0, conn.numFailures)
   325  	require.NotNil(t, conn.conn)
   326  
   327  	// Stop the server.
   328  	l.Close() // nolint: errcheck
   329  	wg.Wait()
   330  
   331  	// Close the connection
   332  	conn.Close()
   333  	require.Nil(t, conn.conn)
   334  }
   335  
   336  func testConnectionOptions() ConnectionOptions {
   337  	return NewConnectionOptions().
   338  		SetClockOptions(clock.NewOptions()).
   339  		SetConnectionKeepAlive(true).
   340  		SetConnectionTimeout(100 * time.Millisecond).
   341  		SetInitReconnectThreshold(2).
   342  		SetMaxReconnectThreshold(6).
   343  		SetReconnectThresholdMultiplier(2).
   344  		SetWriteTimeout(100 * time.Millisecond)
   345  }
   346  
   347  func testConnectionProperties() *gopter.Properties {
   348  	params := gopter.DefaultTestParameters()
   349  	params.Rng.Seed(testRandomSeeed)
   350  	params.MinSuccessfulTests = testMinSuccessfulTests
   351  	return gopter.NewProperties(params)
   352  }