Skip to content

Commit 73b69f0

Browse files
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 d92c615 commit 73b69f0

File tree

5 files changed

+221
-4
lines changed

5 files changed

+221
-4
lines changed

gbn/config.go

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

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

gbn/timeout_manager.go

Lines changed: 68 additions & 2 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
)
@@ -143,6 +144,32 @@ type TimeoutManager struct {
143144
// latestSentSYNTime field.
144145
latestSentSYNTimeMu sync.Mutex
145146

147+
// resendBooster is used to boost the resend timeout when we timeout
148+
// when sending a data packet before receiving a response. The resend
149+
// timeout will remain boosted until it is updated dynamically, as the
150+
// timeout set during the dynamic update most accurately reflects the
151+
// current response time.
152+
resendBooster *TimeoutBooster
153+
154+
// resendBoostPercent is the percentage value the resend timeout will be
155+
// boosted by, any time the Boost function is called for the
156+
// resendBooster.
157+
resendBoostPercent float32
158+
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
164+
// will abort and restart the handshake if the expected response is
165+
// not received from the peer.
166+
handshakeBooster *TimeoutBooster
167+
168+
// handshakeBoostPercent is the percentage value the handshake timeout
169+
// will be by boosted, any time the Boost function is called for the
170+
// handshakeBooster.
171+
handshakeBoostPercent float32
172+
146173
// handshakeTimeout is the time after which the server or client
147174
// will abort and restart the handshake if the expected response is
148175
// not received from the peer.
@@ -206,6 +233,8 @@ func NewTimeOutManager(logger btclog.Logger,
206233
log: logger,
207234
resendTimeout: defaultResendTimeout,
208235
handshakeTimeout: defaultHandshakeTimeout,
236+
resendBoostPercent: defaultBoostPercent,
237+
handshakeBoostPercent: defaultBoostPercent,
209238
useStaticTimeout: false,
210239
resendMultiplier: defaultResendMultiplier,
211240
finSendTimeout: defaultFinSendTimeout,
@@ -219,6 +248,23 @@ func NewTimeOutManager(logger btclog.Logger,
219248
opt(m)
220249
}
221250

251+
// When we are resending packets, it's likely that we'll resend a range
252+
// of packets. As we don't want every packet in that range to boost the
253+
// resend timeout, we'll initialize the resend booster with a ticker,
254+
// which will ensure that only the first resent packet in the range will
255+
// boost the resend timeout.
256+
m.resendBooster = NewTimeoutBooster(
257+
m.resendTimeout,
258+
m.resendBoostPercent,
259+
true,
260+
)
261+
262+
m.handshakeBooster = NewTimeoutBooster(
263+
m.handshakeTimeout,
264+
m.handshakeBoostPercent,
265+
false,
266+
)
267+
222268
return m
223269
}
224270

@@ -256,6 +302,11 @@ func (m *TimeoutManager) Sent(msg Message, resent bool) {
256302
// if the response is for the resent SYN or the original SYN.
257303
m.latestSentSYNTime = time.Time{}
258304

305+
// We'll also temporarily boost the handshake timeout while
306+
// we're resending the SYN message. This might occur multiple
307+
// times until we receive the corresponding response.
308+
m.handshakeBooster.Boost()
309+
259310
case *PacketData:
260311
m.sentTimesMu.Lock()
261312
defer m.sentTimesMu.Unlock()
@@ -267,6 +318,8 @@ func (m *TimeoutManager) Sent(msg Message, resent bool) {
267318
// corresponding response.
268319
delete(m.sentTimes, msg.Seq)
269320

321+
m.resendBooster.Boost()
322+
270323
return
271324
}
272325

@@ -361,22 +414,35 @@ func (m *TimeoutManager) updateResendTimeoutUnsafe(responseTime time.Duration) {
361414
m.log.Tracef("Updating resendTimeout to %v", multipliedTimeout)
362415

363416
m.resendTimeout = multipliedTimeout
417+
418+
// Also update and reset the resend booster, as the new dynamic
419+
// resend timeout most accurately reflects the current response
420+
// time.
421+
m.resendBooster.Reset(multipliedTimeout)
364422
}
365423

366424
// GetResendTimeout returns the current resend timeout.
367425
func (m *TimeoutManager) GetResendTimeout() time.Duration {
368426
m.mu.RLock()
369427
defer m.mu.RUnlock()
370428

371-
return m.resendTimeout
429+
resendTimeout := m.resendBooster.GetCurrentTimeout()
430+
431+
m.log.Debugf("Returning resendTimeout %v", resendTimeout)
432+
433+
return resendTimeout
372434
}
373435

374436
// GetHandshakeTimeout returns the handshake timeout.
375437
func (m *TimeoutManager) GetHandshakeTimeout() time.Duration {
376438
m.mu.RLock()
377439
defer m.mu.RUnlock()
378440

379-
return m.handshakeTimeout
441+
handshake := m.handshakeBooster.GetCurrentTimeout()
442+
443+
m.log.Debugf("Returning handshakeTimeout %v", handshake)
444+
445+
return handshake
380446
}
381447

382448
// GetFinSendTimeout returns the fin send timeout.

gbn/timeout_manager_test.go

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

131131
// Then we'll test that the resend timeout isn't dynamically set if
132-
// when simulating a that the SYN message has been resent.
132+
// when simulating a that the SYN message has been resent, but that the
133+
// handshake timeout is boosted.
134+
tm.handshakeBooster.boostPercent = 0.2
135+
originalHandshakeTimeout := tm.GetHandshakeTimeout()
136+
133137
sendAndReceive(t, tm, synMsg, synMsg, true)
134138

135139
unchangedResendTimeout := tm.GetResendTimeout()
136140
require.Equal(t, newTimeout, unchangedResendTimeout)
141+
142+
newHandshakeTimeout := tm.GetHandshakeTimeout()
143+
require.Equal(
144+
t,
145+
time.Duration(float32(originalHandshakeTimeout)*1.2),
146+
newHandshakeTimeout,
147+
)
137148
}
138149

139150
// TestDataPackageDynamicTimeout ensures that the resend timeout is dynamically
@@ -187,7 +198,9 @@ func TestDataPackageDynamicTimeout(t *testing.T) {
187198
require.NotEqual(t, resendTimeout, newResendTimeout)
188199

189200
// Finally let's test that the resend timeout isn't dynamically set when
190-
// simulating that the data packet has been resent.
201+
// simulating that the data packet has been resent. The resend timeout
202+
// shouldn't be boosted either, as the resend timeout is only boosted
203+
// if we resend a packet after the duration of the previous resend time.
191204
tm.timeoutUpdateFrequency = 1
192205
tm.resendMultiplier = 100
193206

@@ -197,6 +210,124 @@ func TestDataPackageDynamicTimeout(t *testing.T) {
197210
require.Equal(t, newResendTimeout, unchangedResendTimeout)
198211
}
199212

213+
// TestResendBooster tests that the resend timeout booster works as expected,
214+
// and that timeout manager's resendTimeout get's boosted when we need to resend
215+
// a packet again due to not receiving a response within the resend timeout.
216+
func TestResendBooster(t *testing.T) {
217+
t.Parallel()
218+
219+
tm := NewTimeOutManager(nil)
220+
setResendTimeout := time.Millisecond * 1000
221+
tm.resendTimeout = setResendTimeout
222+
223+
initialResendTimeout := tm.GetResendTimeout()
224+
msg := &PacketData{Seq: 20}
225+
response := &PacketACK{Seq: 20}
226+
227+
// As the resend timeout won't be dynamically set when we are resending
228+
// packets, we'll first test that the resend timeout didn't get
229+
// dynamically updated by a resent data packet. This will however
230+
// boost the resend timeout, so let's initially set the boost percent
231+
// to 0 so we can test that the resend timeout wasn't set.
232+
tm.timeoutUpdateFrequency = 1
233+
tm.resendMultiplier = 1
234+
235+
tm.resendBooster.boostPercent = 0
236+
237+
sendAndReceiveWithDuration(
238+
t, tm, time.Millisecond, msg, response, true,
239+
)
240+
241+
unchangedResendTimeout := tm.GetResendTimeout()
242+
require.Equal(t, initialResendTimeout, unchangedResendTimeout)
243+
244+
// Now let's change the boost percent to a non-zero value and test that
245+
// the resend timeout was boosted as expected.
246+
tm.resendBooster.boostPercent = 0.1
247+
248+
changedResendTimeout := tm.GetResendTimeout()
249+
250+
require.Equal(
251+
t,
252+
time.Duration(float32(initialResendTimeout)*1.1),
253+
changedResendTimeout,
254+
)
255+
256+
// Now let's resend another packet again, which shouldn't boost the
257+
// resend timeout again, as the duration of the previous resend timeout
258+
// hasn't passed.
259+
sendAndReceiveWithDuration(
260+
t, tm, time.Millisecond, msg, response, true,
261+
)
262+
263+
unchangedResendTimeout = tm.GetResendTimeout()
264+
265+
require.Equal(
266+
t,
267+
time.Duration(float32(initialResendTimeout)*1.1),
268+
unchangedResendTimeout,
269+
)
270+
271+
// Now let's wait for the duration of the previous resend timeout and
272+
// then resend another packet. This should boost the resend timeout
273+
// once more, as the duration of the previous resend timeout has passed.
274+
err := wait.Invariant(func() bool {
275+
currentResendTimeout := tm.GetResendTimeout()
276+
277+
return unchangedResendTimeout == currentResendTimeout
278+
}, setResendTimeout)
279+
require.NoError(t, err)
280+
281+
sendAndReceiveWithDuration(
282+
t, tm, time.Millisecond, msg, response, true,
283+
)
284+
285+
changedResendTimeout = tm.GetResendTimeout()
286+
287+
require.Equal(
288+
t,
289+
time.Duration(float32(initialResendTimeout)*1.2),
290+
changedResendTimeout,
291+
)
292+
293+
// Now let's verify that in case the resend timeout is dynamically set,
294+
// the boost of the resend timeout is reset. Note that we're not
295+
// simulating a resend here, as that will dynamically set the resend
296+
// timeout as the timeout update frequency is set to 1.
297+
sendAndReceiveWithDuration(
298+
t, tm, time.Second, msg, response, false,
299+
)
300+
301+
newResendTimeout := tm.GetResendTimeout()
302+
303+
require.NotEqual(t, changedResendTimeout, newResendTimeout)
304+
require.Equal(t, 0, tm.resendBooster.boostCount)
305+
306+
// Finally let's check that the resend timeout isn't boosted if we
307+
// simulate a resend before the duration of the newly set resend
308+
// timeout hasn't passed.
309+
sendAndReceiveWithDuration(
310+
t, tm, time.Millisecond, msg, response, true,
311+
)
312+
313+
require.Equal(t, 0, tm.resendBooster.boostCount)
314+
315+
// But if we wait for the duration of the newly set resend timeout and
316+
// then simulate a resend, then the resend timeout should be boosted.
317+
err = wait.Invariant(func() bool {
318+
currentResendTimeout := tm.GetResendTimeout()
319+
320+
return newResendTimeout == currentResendTimeout
321+
}, newResendTimeout)
322+
require.NoError(t, err)
323+
324+
sendAndReceiveWithDuration(
325+
t, tm, time.Millisecond, msg, response, true,
326+
)
327+
328+
require.Equal(t, 1, tm.resendBooster.boostCount)
329+
}
330+
200331
// TestStaticTimeout ensures that the resend timeout isn't dynamically set if a
201332
// static timeout has been set.
202333
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)