Files
ziti/controller/handler_ctrl/dial_state_test.go

183 lines
5.3 KiB
Go

/*
Copyright NetFoundry Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package handler_ctrl
import (
"testing"
"time"
"github.com/openziti/ziti/v2/controller/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var testAddresses = []string{"tls:127.0.0.1:6262"}
func testConfig() *config.CtrlDialerConfig {
return &config.CtrlDialerConfig{
MinRetryInterval: time.Second,
MaxRetryInterval: 5 * time.Minute,
RetryBackoffFactor: 1.5,
FastFailureWindow: 5 * time.Second,
}
}
func TestDialState_DialFailed_BackoffProgression(t *testing.T) {
cfg := testConfig()
state := &routerDialState{
routerId: "test-router",
addresses: testAddresses,
}
// first failure should set retryDelay to at least MinRetryInterval
state.dialFailed(cfg)
assert.Equal(t, statusNeedsDial, state.status)
assert.GreaterOrEqual(t, state.retryDelay, cfg.MinRetryInterval)
assert.LessOrEqual(t, state.retryDelay, cfg.MaxRetryInterval)
assert.Equal(t, uint32(1), state.dialAttempts)
prevDelay := state.retryDelay
// subsequent failures should increase the delay
for i := 0; i < 5; i++ {
state.dialFailed(cfg)
assert.GreaterOrEqual(t, state.retryDelay, prevDelay,
"delay should not decrease on iteration %d", i)
prevDelay = state.retryDelay
}
}
func TestDialState_DialFailed_BackoffClamping(t *testing.T) {
cfg := testConfig()
state := &routerDialState{
routerId: "test-router",
addresses: testAddresses,
retryDelay: 4 * time.Minute, // close to max
}
// after many failures, should never exceed MaxRetryInterval
for i := 0; i < 20; i++ {
state.dialFailed(cfg)
assert.LessOrEqual(t, state.retryDelay, cfg.MaxRetryInterval,
"delay should not exceed max on iteration %d", i)
}
}
func TestDialState_DialFailed_MinRetryFloor(t *testing.T) {
cfg := testConfig()
state := &routerDialState{
routerId: "test-router",
addresses: testAddresses,
retryDelay: 0, // starting from zero
}
state.dialFailed(cfg)
assert.GreaterOrEqual(t, state.retryDelay, cfg.MinRetryInterval)
}
func TestDialState_DialSucceeded(t *testing.T) {
state := &routerDialState{
routerId: "test-router",
addresses: testAddresses,
status: statusDialing,
retryDelay: 30 * time.Second,
dialAttempts: 5,
}
state.dialSucceeded()
assert.Equal(t, statusConnected, state.status)
assert.False(t, state.connectedAt.IsZero())
// retryDelay is intentionally preserved for fast failure detection
assert.Equal(t, 30*time.Second, state.retryDelay)
}
func TestDialState_ConnectionLost_NormalDisconnect(t *testing.T) {
cfg := testConfig()
state := &routerDialState{
routerId: "test-router",
addresses: testAddresses,
status: statusConnected,
connectedAt: time.Now().Add(-time.Minute), // connected a minute ago
retryDelay: 30 * time.Second, // had some previous backoff
}
state.connectionLost(cfg)
assert.Equal(t, statusNeedsDial, state.status)
assert.Equal(t, time.Duration(0), state.retryDelay, "normal disconnect should reset backoff")
// nextDial should be approximately now
require.WithinDuration(t, time.Now(), state.nextDial, time.Second)
}
func TestDialState_ConnectionLost_FastFailure(t *testing.T) {
cfg := testConfig()
state := &routerDialState{
routerId: "test-router",
addresses: testAddresses,
status: statusConnected,
connectedAt: time.Now().Add(-time.Second), // connected just 1 second ago
retryDelay: 2 * time.Second, // some existing backoff
}
state.connectionLost(cfg)
assert.Equal(t, statusNeedsDial, state.status)
// fast failure should apply backoff, not reset
assert.Greater(t, state.retryDelay, time.Duration(0),
"fast failure should apply backoff")
assert.Greater(t, state.nextDial, time.Now(),
"fast failure should schedule future redial")
}
func TestDialState_ConnectionLost_NotConnected(t *testing.T) {
cfg := testConfig()
state := &routerDialState{
routerId: "test-router",
addresses: testAddresses,
status: statusNeedsDial,
retryDelay: 30 * time.Second,
}
state.connectionLost(cfg)
// when not in connected state, should reset and retry immediately
assert.Equal(t, statusNeedsDial, state.status)
assert.Equal(t, time.Duration(0), state.retryDelay)
require.WithinDuration(t, time.Now(), state.nextDial, time.Second)
}
func TestDialState_FastFailure_BackoffAccumulation(t *testing.T) {
cfg := testConfig()
state := &routerDialState{
routerId: "test-router",
addresses: testAddresses,
}
// simulate repeated fast failures: connect then disconnect quickly
for i := 0; i < 5; i++ {
state.dialSucceeded()
state.connectedAt = time.Now().Add(-100 * time.Millisecond) // simulate very short connection
state.connectionLost(cfg)
}
// backoff should have accumulated
assert.Greater(t, state.retryDelay, cfg.MinRetryInterval,
"repeated fast failures should accumulate backoff")
}