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 }