Skip to content

Commit 7e60c46

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 fc41efe commit 7e60c46

File tree

5 files changed

+227
-4
lines changed

5 files changed

+227
-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: 74 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
)
@@ -147,6 +148,32 @@ type TimeoutManager struct {
147148
// latestSentSYNTime field.
148149
latestSentSYNTimeMu sync.Mutex
149150

151+
// resendBooster is used to boost the resend timeout when we timeout
152+
// when sending a data packet before receiving a response. The resend
153+
// timeout will remain boosted until it is updated dynamically, as the
154+
// timeout set during the dynamic update most accurately reflects the
155+
// current response time.
156+
resendBooster *TimeoutBooster
157+
158+
// resendBoostPercent is the percentage value the resend timeout will be
159+
// boosted by, any time the Boost function is called for the
160+
// resendBooster.
161+
resendBoostPercent float32
162+
163+
// handshakeBooster is used to boost the handshake timeout if we timeout
164+
// when sending the SYN message before receiving the corresponding
165+
// response. The handshake timeout will remain boosted throughout the
166+
// lifespan of the connection if it's boosted.
167+
// The handshake timeout is the time after which the server or client
168+
// will abort and restart the handshake if the expected response is
169+
// not received from the peer.
170+
handshakeBooster *TimeoutBooster
171+
172+
// handshakeBoostPercent is the percentage value the handshake timeout
173+
// will be by boosted, any time the Boost function is called for the
174+
// handshakeBooster.
175+
handshakeBoostPercent float32
176+
150177
// handshakeTimeout is the time after which the server or client
151178
// will abort and restart the handshake if the expected response is
152179
// not received from the peer.
@@ -207,6 +234,8 @@ func NewTimeOutManager(logger btclog.Logger,
207234
log: logger,
208235
resendTimeout: defaultResendTimeout,
209236
handshakeTimeout: defaultHandshakeTimeout,
237+
resendBoostPercent: defaultBoostPercent,
238+
handshakeBoostPercent: defaultBoostPercent,
210239
useStaticTimeout: false,
211240
resendMultiplier: defaultResendMultiplier,
212241
finSendTimeout: defaultFinSendTimeout,
@@ -220,6 +249,23 @@ func NewTimeOutManager(logger btclog.Logger,
220249
opt(m)
221250
}
222251

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

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

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

320+
m.resendBooster.Boost()
321+
269322
return
270323
}
271324

@@ -356,22 +409,41 @@ func (m *TimeoutManager) updateResendTimeout(responseTime time.Duration) {
356409
m.log.Tracef("Updating resendTimeout to %v", multipliedTimeout)
357410

358411
m.resendTimeout = multipliedTimeout
412+
413+
// Also update and reset the resend booster, as the new dynamic
414+
// resend timeout most accurately reflects the current response
415+
// time.
416+
m.resendBooster.Reset(multipliedTimeout)
417+
418+
// As we may have received a data packet that executes this function
419+
// while we are also concurrently resending the queue, we also restart
420+
// the frequency timeout, to ensure that the messages we're resending
421+
// won't boost the resend timeout.
422+
m.resendBooster.RestartFrequencyTimeout()
359423
}
360424

361425
// GetResendTimeout returns the current resend timeout.
362426
func (m *TimeoutManager) GetResendTimeout() time.Duration {
363427
m.mu.RLock()
364428
defer m.mu.RUnlock()
365429

366-
return m.resendTimeout
430+
resendTimeout := m.resendBooster.GetCurrentTimeout()
431+
432+
m.log.Debugf("Returning resendTimeout %v", resendTimeout)
433+
434+
return resendTimeout
367435
}
368436

369437
// GetHandshakeTimeout returns the handshake timeout.
370438
func (m *TimeoutManager) GetHandshakeTimeout() time.Duration {
371439
m.mu.RLock()
372440
defer m.mu.RUnlock()
373441

374-
return m.handshakeTimeout
442+
handshake := m.handshakeBooster.GetCurrentTimeout()
443+
444+
m.log.Debugf("Returning handshakeTimeout %v", handshake)
445+
446+
return handshake
375447
}
376448

377449
// 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
@@ -64,11 +64,22 @@ func TestSYNDynamicTimeout(t *testing.T) {
6464
require.Equal(t, minimumResendTimeout, newTimeout)
6565

6666
// Then we'll test that the resend timeout isn't dynamically set if
67-
// when simulating a that the SYN message has been resent.
67+
// when simulating a that the SYN message has been resent, but that the
68+
// handshake timeout is boosted.
69+
tm.handshakeBooster.boostPercent = 0.2
70+
originalHandshakeTimeout := tm.GetHandshakeTimeout()
71+
6872
sendAndReceive(t, tm, synMsg, synMsg, true)
6973

7074
unchangedResendTimeout := tm.GetResendTimeout()
7175
require.Equal(t, newTimeout, unchangedResendTimeout)
76+
77+
newHandshakeTimeout := tm.GetHandshakeTimeout()
78+
require.Equal(
79+
t,
80+
time.Duration(float32(originalHandshakeTimeout)*1.2),
81+
newHandshakeTimeout,
82+
)
7283
}
7384

7485
// TestDataPackageDynamicTimeout ensures that the resend timeout is dynamically
@@ -122,7 +133,9 @@ func TestDataPackageDynamicTimeout(t *testing.T) {
122133
require.NotEqual(t, resendTimeout, newResendTimeout)
123134

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

@@ -132,6 +145,124 @@ func TestDataPackageDynamicTimeout(t *testing.T) {
132145
require.Equal(t, newResendTimeout, unchangedResendTimeout)
133146
}
134147

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