Skip to content

Commit 3da5e84

Browse files
authored
fix: keepalive causes a reconnect loop when connection is lost (#1779)
* fix: keepalive causes a reconnect loop when connection is lost Fixes #1778 * fix: add test
1 parent 44a2f2f commit 3da5e84

File tree

4 files changed

+106
-20
lines changed

4 files changed

+106
-20
lines changed

example.ts

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,58 @@
11
import mqtt from '.'
22

3-
const client = mqtt.connect('mqtt://test.mosquitto.org')
3+
const client = mqtt.connect('mqtt://test.mosquitto.org', {
4+
keepalive: 10,
5+
reconnectPeriod: 15000,
6+
})
47

58
const testTopic = 'presence'
69

7-
client.subscribe(testTopic, (err) => {
8-
if (!err) {
9-
console.log('subscribed to', testTopic)
10-
client.publish(testTopic, 'Hello mqtt', (err2) => {
10+
function publish() {
11+
client.publish(
12+
testTopic,
13+
`Hello mqtt ${new Date().toISOString()}`,
14+
(err2) => {
1115
if (!err2) {
1216
console.log('message published')
1317
} else {
1418
console.error(err2)
1519
}
16-
})
20+
},
21+
)
22+
}
23+
24+
client.subscribe(testTopic, (err) => {
25+
if (!err) {
26+
console.log('subscribed to', testTopic)
1727
} else {
1828
console.error(err)
1929
}
2030
})
2131

2232
client.on('message', (topic, message) => {
2333
console.log('received message "%s" from topic "%s"', message, topic)
24-
client.end()
34+
setTimeout(() => {
35+
publish()
36+
}, 2000)
37+
})
38+
39+
client.on('error', (err) => {
40+
console.error(err)
41+
})
42+
43+
client.on('connect', () => {
44+
console.log('connected')
45+
publish()
46+
})
47+
48+
client.on('disconnect', () => {
49+
console.log('disconnected')
50+
})
51+
52+
client.on('offline', () => {
53+
console.log('offline')
54+
})
55+
56+
client.on('reconnect', () => {
57+
console.log('reconnect')
2558
})

src/lib/PingTimer.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,7 @@ export default class PingTimer {
2121
constructor(keepalive: number, checkPing: () => void) {
2222
this.keepalive = keepalive * 1000
2323
this.checkPing = checkPing
24-
this.setup()
25-
}
26-
27-
private setup() {
28-
this.timer = this._setTimeout(() => {
29-
this.checkPing()
30-
this.reschedule()
31-
}, this.keepalive)
24+
this.reschedule()
3225
}
3326

3427
clear() {
@@ -40,6 +33,13 @@ export default class PingTimer {
4033

4134
reschedule() {
4235
this.clear()
43-
this.setup()
36+
this.timer = this._setTimeout(() => {
37+
this.checkPing()
38+
// prevent possible race condition where the timer is destroyed on _cleauUp
39+
// and recreated here
40+
if (this.timer) {
41+
this.reschedule()
42+
}
43+
}, this.keepalive)
4444
}
4545
}

src/lib/client.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -639,7 +639,7 @@ export default class MqttClient extends TypedEventEmitter<MqttClientEventCallbac
639639
clearTimeout(this.connackTimer)
640640

641641
this.log('close :: clearing ping timer')
642-
if (this.pingTimer !== null) {
642+
if (this.pingTimer) {
643643
this.pingTimer.clear()
644644
this.pingTimer = null
645645
}
@@ -1752,15 +1752,15 @@ export default class MqttClient extends TypedEventEmitter<MqttClientEventCallbac
17521752
})
17531753
}
17541754

1755-
if (!this.disconnecting) {
1755+
if (!this.disconnecting && !this.reconnecting) {
17561756
this.log(
1757-
'_cleanUp :: client not disconnecting. Clearing and resetting reconnect.',
1757+
'_cleanUp :: client not disconnecting/reconnecting. Clearing and resetting reconnect.',
17581758
)
17591759
this._clearReconnect()
17601760
this._setupReconnect()
17611761
}
17621762

1763-
if (this.pingTimer !== null) {
1763+
if (this.pingTimer) {
17641764
this.log('_cleanUp :: clearing pingTimer')
17651765
this.pingTimer.clear()
17661766
this.pingTimer = null

test/pingTimer.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { afterEach, beforeEach, describe, it } from 'node:test'
2+
import PingTimer from '../src/lib/PingTimer'
3+
import { assert } from 'chai'
4+
import { useFakeTimers, spy } from 'sinon'
5+
6+
describe('PingTimer', () => {
7+
let clock: sinon.SinonFakeTimers
8+
beforeEach(() => {
9+
clock = useFakeTimers()
10+
})
11+
12+
afterEach(() => {
13+
clock.restore()
14+
})
15+
16+
it('should schedule and clear', () => {
17+
const keepalive = 10 // seconds
18+
const cb = spy()
19+
const pingTimer = new PingTimer(keepalive, cb)
20+
21+
assert.ok(pingTimer['timer'], 'timer should be created automatically')
22+
23+
clock.tick(keepalive * 1000 + 1)
24+
assert.equal(
25+
cb.callCount,
26+
1,
27+
'should trigger the callback after keepalive seconds',
28+
)
29+
clock.tick(keepalive * 1000 + 1)
30+
assert.equal(cb.callCount, 2, 'should reschedule automatically')
31+
pingTimer.clear()
32+
assert.ok(!pingTimer['timer'], 'timer should not exists after clear()')
33+
})
34+
35+
it('should not re-schedule if timer has been cleared in check ping', () => {
36+
const keepalive = 10 // seconds
37+
const cb = spy()
38+
const pingTimer = new PingTimer(keepalive, () => {
39+
pingTimer.clear()
40+
cb()
41+
})
42+
43+
clock.tick(keepalive * 1000 + 1)
44+
assert.equal(
45+
cb.callCount,
46+
1,
47+
'should trigger the callback after keepalive seconds',
48+
)
49+
clock.tick(keepalive * 1000 + 1)
50+
assert.equal(cb.callCount, 1, 'should not re-schedule')
51+
assert.ok(!pingTimer['timer'], 'timer should not exists')
52+
})
53+
})

0 commit comments

Comments
 (0)