Skip to content

Commit 782a6df

Browse files
sciascidneilalexander
authored andcommitted
[FIXED] Avoid stalling read loop on leafnode ErrMinimumVersionRequired
Remove the accept side wait from leaf minimum version rejection handling and apply the 5s backoff on the soliciting side instead. That is similar to the same cluster name reconnect delay path. This keeps the read loop from waiting during rejection and makes the reconnect behavior explicit on new peers. Note that the previous accept side 5s delay was not effective: the inbound leaf connection would be closed earlier by the existing ping timer, the hold period was not enforced. Signed-off-by: Daniele Sciascia <daniele@nats.io>
1 parent 74a5f6f commit 782a6df

4 files changed

Lines changed: 33 additions & 32 deletions

File tree

server/client.go

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1465,11 +1465,8 @@ func (c *client) readLoop(pre []byte) {
14651465
if err := c.parse(bufs[i]); err != nil {
14661466
if err == ErrMinimumVersionRequired {
14671467
// Special case here, currently only for leaf node connections.
1468-
// When process the CONNECT protocol, if the minimum version
1469-
// required was not met, an error was printed and sent back to
1470-
// the remote, and connection was closed after a certain delay
1471-
// (to avoid "rapid" reconnection from the remote).
1472-
// We don't need to do any of the things below, simply return.
1468+
// processLeafConnect() already sent the rejection and closed
1469+
// the connection, so there is nothing else to do here.
14731470
return
14741471
}
14751472
if dur := time.Since(c.in.start); dur >= readLoopReportThreshold {

server/errors.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,9 @@ var (
207207

208208
// ErrMinimumVersionRequired is returned when a connection is not at the minimum version required.
209209
ErrMinimumVersionRequired = errors.New("minimum version required")
210+
// ErrLeafNodeMinVersionRejected is the leafnode protocol error prefix used
211+
// when rejecting a remote due to leafnodes.min_version.
212+
ErrLeafNodeMinVersionRejected = errors.New("connection rejected since minimum version required is")
210213

211214
// ErrInvalidMappingDestination is used for all subject mapping destination errors
212215
ErrInvalidMappingDestination = errors.New("invalid mapping destination")

server/leafnode.go

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,9 @@ const (
6363
// LEAF connection as opposed to a CLIENT.
6464
leafNodeWSPath = "/leafnode"
6565

66-
// This is the time the server will wait, when receiving a CONNECT,
67-
// before closing the connection if the required minimum version is not met.
68-
leafNodeWaitBeforeClose = 5 * time.Second
66+
// When a soliciting leafnode is rejected because it does not meet the
67+
// configured minimum version, delay the next reconnect attempt by this long.
68+
leafNodeMinVersionReconnectDelay = 5 * time.Second
6969
)
7070

7171
type leaf struct {
@@ -1885,17 +1885,11 @@ func (c *client) processLeafNodeConnect(s *Server, arg []byte, lang string) erro
18851885
if mv := s.getOpts().LeafNode.MinVersion; mv != _EMPTY_ {
18861886
major, minor, update, _ := versionComponents(mv)
18871887
if !versionAtLeast(proto.Version, major, minor, update) {
1888-
// We are going to send back an INFO because otherwise recent
1889-
// versions of the remote server would simply break the connection
1890-
// after 2 seconds if not receiving it. Instead, we want the
1891-
// other side to just "stall" until we finish waiting for the holding
1892-
// period and close the connection below.
1888+
// Send back an INFO so recent remote servers process the rejection
1889+
// cleanly, then close immediately. The soliciting side applies the
1890+
// reconnect delay when it processes the error.
18931891
s.sendPermsAndAccountInfo(c)
1894-
c.sendErrAndErr(fmt.Sprintf("connection rejected since minimum version required is %q", mv))
1895-
select {
1896-
case <-c.srv.quitCh:
1897-
case <-time.After(leafNodeWaitBeforeClose):
1898-
}
1892+
c.sendErrAndErr(fmt.Sprintf("%s %q", ErrLeafNodeMinVersionRejected, mv))
18991893
c.closeConnection(MinimumVersionRequired)
19001894
return ErrMinimumVersionRequired
19011895
}
@@ -2984,6 +2978,11 @@ func (c *client) leafProcessErr(errStr string) {
29842978
c.Errorf("Leafnode connection dropped with same cluster name error. Delaying attempt to reconnect for %v", delay)
29852979
return
29862980
}
2981+
if strings.Contains(errStr, ErrLeafNodeMinVersionRejected.Error()) {
2982+
_, delay := c.setLeafConnectDelayIfSoliciting(leafNodeMinVersionReconnectDelay)
2983+
c.Errorf("Leafnode connection dropped due to minimum version requirement. Delaying attempt to reconnect for %v", delay)
2984+
return
2985+
}
29872986

29882987
// We will look for Loop detected error coming from the other side.
29892988
// If we solicit, set the connect delay.

server/leafnode_test.go

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6041,15 +6041,15 @@ leafnodes:{
60416041

60426042
type checkLeafMinVersionLogger struct {
60436043
DummyLogger
6044-
errCh chan string
6045-
connCh chan string
6044+
errCh chan time.Time
6045+
connCh chan time.Time
60466046
}
60476047

60486048
func (l *checkLeafMinVersionLogger) Errorf(format string, args ...any) {
60496049
msg := fmt.Sprintf(format, args...)
60506050
if strings.Contains(msg, "minimum version") {
60516051
select {
6052-
case l.errCh <- msg:
6052+
case l.errCh <- time.Now():
60536053
default:
60546054
}
60556055
}
@@ -6059,7 +6059,7 @@ func (l *checkLeafMinVersionLogger) Noticef(format string, args ...any) {
60596059
msg := fmt.Sprintf(format, args...)
60606060
if strings.Contains(msg, "Leafnode connection created") {
60616061
select {
6062-
case l.connCh <- msg:
6062+
case l.connCh <- time.Now():
60636063
default:
60646064
}
60656065
}
@@ -6134,7 +6134,7 @@ func TestLeafNodeMinVersion(t *testing.T) {
61346134
s, o = RunServerWithConfig(conf)
61356135
defer s.Shutdown()
61366136

6137-
l := &checkLeafMinVersionLogger{errCh: make(chan string, 1), connCh: make(chan string, 1)}
6137+
l := &checkLeafMinVersionLogger{errCh: make(chan time.Time, 1), connCh: make(chan time.Time, 1)}
61386138
s.SetLogger(l, false, false)
61396139

61406140
rconf = createConfFile(t, []byte(fmt.Sprintf(`
@@ -6156,21 +6156,23 @@ func TestLeafNodeMinVersion(t *testing.T) {
61566156
t.Fatal("Remote did not try to connect")
61576157
}
61586158

6159+
var rejectAt time.Time
61596160
select {
6160-
case <-l.errCh:
6161+
case rejectAt = <-l.errCh:
61616162
case <-time.After(time.Second):
61626163
t.Fatal("Did not get the minimum version required error")
61636164
}
61646165

6165-
// Since we have a very small reconnect interval, if the connection was
6166-
// closed "right away", then we should have had a reconnect attempt with
6167-
// another failure. This should not be the case because the server will
6168-
// wait 5s before closing the connection.
6166+
// Since we have a very small reconnect interval, the next attempt should be
6167+
// delayed by the dedicated minimum version reconnect delay on the soliciting
6168+
// side, not by the normal reconnect interval.
61696169
select {
6170-
case <-l.connCh:
6171-
t.Fatal("Should not have tried to reconnect")
6172-
case <-time.After(250 * time.Millisecond):
6173-
// OK
6170+
case secondAttemptAt := <-l.connCh:
6171+
if elapsed := secondAttemptAt.Sub(rejectAt); elapsed < leafNodeMinVersionReconnectDelay {
6172+
t.Fatalf("Expected reconnect attempt after at least %v, got %v", leafNodeMinVersionReconnectDelay, elapsed)
6173+
}
6174+
case <-time.After(7 * time.Second):
6175+
t.Fatal("Did not get the reconnect attempt")
61746176
}
61756177
}
61766178

0 commit comments

Comments
 (0)