Skip to content

Commit 504aff7

Browse files
committed
gbn: add boosting of resend & handshake timeouts
When we need to resend a data packet, or when we need to resend the SYN message during the handshake, due to the other side not responding within given timeout, we will boost the timeout by 50% for each time we need to resend without receiving any response. This ensures that if the original timeouts are set to a too short duration given the current network latency, we will eventually boost the timeouts to a long enough duration that allows the other side to be able to respond within the timeout.
1 parent f325697 commit 504aff7

File tree

5 files changed

+219
-8
lines changed

5 files changed

+219
-8
lines changed

gbn/config.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,18 @@ func WithKeepalivePing(ping, pong time.Duration) TimeoutOptions {
6565
}
6666
}
6767

68+
// WithBoostPercent is used to set the boost percent that the timeout manager
69+
// will use to boost the resend timeout & handshake timeout every time a resend
70+
// is required due to not receiving a response within the current timeout.
71+
func WithBoostPercent(boostPercent float32) TimeoutOptions {
72+
return func(manager *TimeoutManager) {
73+
if boostPercent > 0 {
74+
manager.handshakeBooster.boostPercent = boostPercent
75+
manager.resendBooster.boostPercent = boostPercent
76+
}
77+
}
78+
}
79+
6880
// config holds the configuration values for an instance of GoBackNConn.
6981
type config struct {
7082
// n is the window size. The sender can send a maximum of n packets

gbn/timeout_manager.go

Lines changed: 66 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const (
1515
defaultFinSendTimeout = 1000 * time.Millisecond
1616
defaultResendMultiplier = 5
1717
defaultTimeoutUpdateFrequency = 100
18+
defaultBoostPercent = 0.5
1819
DefaultSendTimeout = math.MaxInt64
1920
DefaultRecvTimeout = math.MaxInt64
2021
)
@@ -155,10 +156,21 @@ type TimeoutManager struct {
155156
// latestSentSYNTime field.
156157
latestSentSYNTimeMu sync.Mutex
157158

158-
// handshakeTimeout is the time after which the server or client
159+
// handshakeBooster is used to boost the handshake timeout if we timeout
160+
// when sending the SYN message before receiving the corresponding
161+
// response. The handshake timeout will remain boosted throughout the
162+
// lifespan of the connection if it's boosted.
163+
// The handshake timeout is the time after which the server or client
159164
// will abort and restart the handshake if the expected response is
160165
// not received from the peer.
161-
handshakeTimeout time.Duration
166+
handshakeBooster *TimeoutBooster
167+
168+
// resendBooster is used to boost the resend timeout when we timeout
169+
// when sending a data packet before receiving a response. The resend
170+
// timeout will remain boosted until it is updated dynamically, as the
171+
// timeout set during the dynamic update most accurately reflects the
172+
// current response time.
173+
resendBooster *TimeoutBooster
162174

163175
// finSendTimeout is the timeout after which the created context for
164176
// sending a FIN message will be time out.
@@ -208,17 +220,35 @@ func NewTimeOutManager(logger btclog.Logger,
208220
logger = log
209221
}
210222

223+
handshakeBooster := NewTimeoutBooster(
224+
defaultHandshakeTimeout,
225+
defaultBoostPercent,
226+
false,
227+
)
228+
229+
// When we are resending packets, we are likely to resend multiple
230+
// packets at in a range. As we don't like every packet in that range
231+
// to boost the resend timeout, we'll initialize the resend booster
232+
// with a ticker, which will ensure that only the first resent packet in
233+
// the range will boost the resend timeout.
234+
responseBooster := NewTimeoutBooster(
235+
defaultResendTimeout,
236+
defaultBoostPercent,
237+
true,
238+
)
239+
211240
m := &TimeoutManager{
212241
log: logger,
213242
resendTimeout: defaultResendTimeout,
214-
handshakeTimeout: defaultHandshakeTimeout,
215243
useStaticTimeout: false,
216244
resendMultiplier: defaultResendMultiplier,
217245
finSendTimeout: defaultFinSendTimeout,
218246
recvTimeout: DefaultRecvTimeout,
219247
sendTimeout: DefaultSendTimeout,
220248
sentTimes: make(map[uint8]time.Time),
221249
timeoutUpdateFrequency: defaultTimeoutUpdateFrequency,
250+
handshakeBooster: handshakeBooster,
251+
resendBooster: responseBooster,
222252
}
223253

224254
for _, opt := range timeoutOpts {
@@ -256,6 +286,12 @@ func (m *TimeoutManager) Sent(msg Message, resent bool) {
256286
// the response is for the resent SYN or the original
257287
// SYN.
258288
m.latestSentSYNTime = time.Time{}
289+
290+
// We'll also temporarily boost the handshake timeout
291+
// while we're resending the SYN message.
292+
// This might occur multiple times until we receive
293+
// the corresponding response.
294+
m.handshakeBooster.Boost()
259295
} else {
260296
m.latestSentSYNTime = time.Now()
261297
}
@@ -270,6 +306,8 @@ func (m *TimeoutManager) Sent(msg Message, resent bool) {
270306
// update the resend timeout when we receive the
271307
// corresponding response.
272308
delete(m.sentTimes, msg.Seq)
309+
310+
m.resendBooster.Boost()
273311
} else {
274312
m.sentTimes[msg.Seq] = time.Now()
275313
}
@@ -358,22 +396,42 @@ func (m *TimeoutManager) updateResendTimeout(responseTime time.Duration) {
358396
defer m.timeoutManagerMu.Unlock()
359397

360398
m.resendTimeout = multipliedTimeout
399+
400+
// Also update and reset the resend booster, as the new dynamic
401+
// resend timeout most accurately reflects the current response
402+
// time.
403+
m.resendBooster.SetOriginalTimeout(multipliedTimeout)
404+
m.resendBooster.Reset()
405+
406+
// As we may have received a data packet that executes this function
407+
// while we are also concurrently resending the queue, we also restart
408+
// the frequency timeout, to ensure that the messages we're resending
409+
// won't boost the resend timeout.
410+
m.resendBooster.RestartFrequencyTimeout()
361411
}
362412

363413
// GetResendTimeout returns the current resend timeout.
364414
func (m *TimeoutManager) GetResendTimeout() time.Duration {
365415
m.timeoutManagerMu.RLock()
366416
defer m.timeoutManagerMu.RUnlock()
367417

368-
return m.resendTimeout
418+
resendTimeout := m.resendBooster.GetCurrentTimeout()
419+
420+
m.log.Debugf("Returning resendTimeout %v", resendTimeout)
421+
422+
return resendTimeout
369423
}
370424

371425
// GetHandshakeTimeout returns the handshake timeout.
372426
func (m *TimeoutManager) GetHandshakeTimeout() time.Duration {
373427
m.timeoutManagerMu.RLock()
374428
defer m.timeoutManagerMu.RUnlock()
375429

376-
return m.handshakeTimeout
430+
handshake := m.handshakeBooster.GetCurrentTimeout()
431+
432+
m.log.Debugf("Returning handshakeTimeout %v", handshake)
433+
434+
return handshake
377435
}
378436

379437
// GetFinSendTimeout returns the fin send timeout.
@@ -435,6 +493,8 @@ func (m *TimeoutManager) SetStaticResendTimeout(resendTimeout time.Duration) {
435493
defer m.timeoutManagerMu.Unlock()
436494

437495
m.resendTimeout = resendTimeout
496+
m.resendBooster.SetOriginalTimeout(resendTimeout)
497+
438498
m.useStaticTimeout = true
439499
}
440500

@@ -443,7 +503,7 @@ func (m *TimeoutManager) SetHandshakeTimeout(handshakeTimeout time.Duration) {
443503
m.timeoutManagerMu.Lock()
444504
defer m.timeoutManagerMu.Unlock()
445505

446-
m.handshakeTimeout = handshakeTimeout
506+
m.handshakeBooster.SetOriginalTimeout(handshakeTimeout)
447507
}
448508

449509
// SetSendTimeout sets the send timeout.

gbn/timeout_manager_test.go

Lines changed: 133 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,22 @@ func TestSYNDynamicTimeout(t *testing.T) {
6161
require.Equal(t, minimumResendTimeout, newTimeout)
6262

6363
// Then we'll test that the resend timeout isn't dynamically set if
64-
// when simulating a that the SYN message has been resent.
64+
// when simulating a that the SYN message has been resent, but that the
65+
// handshake timeout is boosted.
66+
tm.handshakeBooster.boostPercent = 0.2
67+
originalHandshakeTimeout := tm.GetHandshakeTimeout()
68+
6569
sendAndReceive(t, tm, synMsg, synMsg, true)
6670

6771
unchangedResendTimeout := tm.GetResendTimeout()
6872
require.Equal(t, newTimeout, unchangedResendTimeout)
73+
74+
newHandshakeTimeout := tm.GetHandshakeTimeout()
75+
require.Equal(
76+
t,
77+
time.Duration(float32(originalHandshakeTimeout)*1.2),
78+
newHandshakeTimeout,
79+
)
6980
}
7081

7182
// TestDataPackageDynamicTimeout ensures that the resend timeout is dynamically
@@ -121,7 +132,9 @@ func TestDataPackageDynamicTimeout(t *testing.T) {
121132
require.NotEqual(t, resendTimeout, newResendTimeout)
122133

123134
// Finally let's test that the resend timeout isn't dynamically set when
124-
// simulating that the data packet has been resent.
135+
// simulating that the data packet has been resent. The resend timeout
136+
// shouldn't be boosted either, as the resend timeout is only boosted
137+
// if we resend a packet after the duration of the previous resend time.
125138
tm.timeoutUpdateFrequency = 1
126139
tm.resendMultiplier = 100
127140

@@ -131,6 +144,124 @@ func TestDataPackageDynamicTimeout(t *testing.T) {
131144
require.Equal(t, newResendTimeout, unchangedResendTimeout)
132145
}
133146

147+
// TestResendBooster tests that the resend timeout booster works as expected,
148+
// and that timeout manager's resendTimeout get's boosted when we need to resend
149+
// a packet again due to not receiving a response within the resend timeout.
150+
func TestResendBooster(t *testing.T) {
151+
t.Parallel()
152+
153+
tm := NewTimeOutManager(nil)
154+
setResendTimeout := time.Millisecond * 1000
155+
tm.resendTimeout = setResendTimeout
156+
157+
initialResendTimeout := tm.GetResendTimeout()
158+
msg := &PacketData{Seq: 20}
159+
response := &PacketACK{Seq: 20}
160+
161+
// As the resend timeout won't be dynamically set when we are resending
162+
// packets, we'll first test that the resend timeout didn't get
163+
// dynamically updated by a resent data packet. This will however
164+
// boost the resend timeout, so let's initially set the boost percent
165+
// to 0 so we can test that the resend timeout wasn't set.
166+
tm.timeoutUpdateFrequency = 1
167+
tm.resendMultiplier = 1
168+
169+
tm.resendBooster.boostPercent = 0
170+
171+
sendAndReceiveWithDuration(
172+
t, tm, time.Millisecond, msg, response, true,
173+
)
174+
175+
unchangedResendTimeout := tm.GetResendTimeout()
176+
require.Equal(t, initialResendTimeout, unchangedResendTimeout)
177+
178+
// Now let's change the boost percent to a non-zero value and test that
179+
// the resend timeout was boosted as expected.
180+
tm.resendBooster.boostPercent = 0.1
181+
182+
changedResendTimeout := tm.GetResendTimeout()
183+
184+
require.Equal(
185+
t,
186+
time.Duration(float32(initialResendTimeout)*1.1),
187+
changedResendTimeout,
188+
)
189+
190+
// Now let's resend another packet again, which shouldn't boost the
191+
// resend timeout again, as the duration of the previous resend timeout
192+
// hasn't passed.
193+
sendAndReceiveWithDuration(
194+
t, tm, time.Millisecond, msg, response, true,
195+
)
196+
197+
unchangedResendTimeout = tm.GetResendTimeout()
198+
199+
require.Equal(
200+
t,
201+
time.Duration(float32(initialResendTimeout)*1.1),
202+
unchangedResendTimeout,
203+
)
204+
205+
// Now let's wait for the duration of the previous resend timeout and
206+
// then resend another packet. This should boost the resend timeout
207+
// once more, as the duration of the previous resend timeout has passed.
208+
err := wait.Invariant(func() bool {
209+
currentResendTimeout := tm.GetResendTimeout()
210+
211+
return unchangedResendTimeout == currentResendTimeout
212+
}, setResendTimeout)
213+
require.NoError(t, err)
214+
215+
sendAndReceiveWithDuration(
216+
t, tm, time.Millisecond, msg, response, true,
217+
)
218+
219+
changedResendTimeout = tm.GetResendTimeout()
220+
221+
require.Equal(
222+
t,
223+
time.Duration(float32(initialResendTimeout)*1.2),
224+
changedResendTimeout,
225+
)
226+
227+
// Now let's verify that in case the resend timeout is dynamically set,
228+
// the boost of the resend timeout is reset. Note that we're not
229+
// simulating a resend here, as that will dynamically set the resend
230+
// timeout as the timeout update frequency is set to 1.
231+
sendAndReceiveWithDuration(
232+
t, tm, time.Second, msg, response, false,
233+
)
234+
235+
newResendTimeout := tm.GetResendTimeout()
236+
237+
require.NotEqual(t, changedResendTimeout, newResendTimeout)
238+
require.Equal(t, 0, tm.resendBooster.boostCount)
239+
240+
// Finally let's check that the resend timeout isn't boosted if we
241+
// simulate a resend before the duration of the newly set resend
242+
// timeout hasn't passed.
243+
sendAndReceiveWithDuration(
244+
t, tm, time.Millisecond, msg, response, true,
245+
)
246+
247+
require.Equal(t, 0, tm.resendBooster.boostCount)
248+
249+
// But if we wait for the duration of the newly set resend timeout and
250+
// then simulate a resend, then the resend timeout should be boosted.
251+
err = wait.Invariant(func() bool {
252+
currentResendTimeout := tm.GetResendTimeout()
253+
254+
return newResendTimeout == currentResendTimeout
255+
}, newResendTimeout)
256+
require.NoError(t, err)
257+
258+
sendAndReceiveWithDuration(
259+
t, tm, time.Millisecond, msg, response, true,
260+
)
261+
262+
require.Equal(t, 1, tm.resendBooster.boostCount)
263+
}
264+
134265
// TestStaticTimeout ensures that the resend timeout isn't dynamically set if a
135266
// static timeout has been set.
136267
func TestStaticTimeout(t *testing.T) {

mailbox/client_conn.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,12 @@ const (
8282
// gbnPongTimout is the time after sending the pong message that we will
8383
// timeout if we do not receive any message from our peer.
8484
gbnPongTimeout = 3 * time.Second
85+
86+
// gbnBoostPercent is the percentage value that the resend and handshake
87+
// timeout will be boosted any time we need to resend a packet due to
88+
// the corresponding response not being received within the previous
89+
// timeout.
90+
gbnBoostPercent = 0.5
8591
)
8692

8793
// ClientStatus is a description of the connection status of the client.
@@ -183,6 +189,7 @@ func NewClientConn(ctx context.Context, sid [64]byte, serverHost string,
183189
gbn.WithKeepalivePing(
184190
gbnClientPingTimeout, gbnPongTimeout,
185191
),
192+
gbn.WithBoostPercent(gbnBoostPercent),
186193
),
187194
gbn.WithOnFIN(func() {
188195
// We force the connection to set a new status after

mailbox/server_conn.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ func NewServerConn(ctx context.Context, serverHost string,
8989
gbn.WithKeepalivePing(
9090
gbnServerPingTimeout, gbnPongTimeout,
9191
),
92+
gbn.WithBoostPercent(gbnBoostPercent),
9293
),
9394
},
9495
status: ServerStatusNotConnected,

0 commit comments

Comments
 (0)