Skip to content
This repository was archived by the owner on Nov 26, 2025. It is now read-only.

Commit f48ac1b

Browse files
authored
Add reconnection & backoff logic to client mode (#138)
* Adds randomized exponential backoff reconnection to server when running in client mode and the connection is closed * Splits business logic of index.js into initial rclnodejs setup and websocket+bridge connection handling * Fixes typo in README * Moves "shutdown" behavior into a single function * Simplifies bridge error reporting (no longer requiring bridge ID to be passed, since we know by context which bridge we're listening to).
1 parent 59163ba commit f48ac1b

File tree

3 files changed

+87
-54
lines changed

3 files changed

+87
-54
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ npm install
4444
node bin/rosbridge.js
4545
```
4646

47-
If you want to start the server in a "client" mode (i.e. connecting the bridge to an existing websocket server, do this instead:
47+
If you want to start in client mode (i.e. connecting the bridge to an existing websocket server), do this instead:
4848

4949
```bash
5050
node bin/rosbridge.js --address ws://<address>:<port>

index.js

Lines changed: 86 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,54 @@ const rclnodejs = require('rclnodejs');
1818
const WebSocket = require('ws');
1919
const Bridge = require('./lib/bridge.js');
2020
const debug = require('debug')('ros2-web-bridge:index');
21+
22+
// rclnodejs node
23+
let node;
24+
25+
// Websocket server (or client if client mode set via --address)
26+
let server;
27+
let connectionAttempts = 0;
28+
29+
// Map of bridge IDs to Bridge objects
30+
let bridgeMap = new Map();
31+
32+
function closeAllBridges() {
33+
bridgeMap.forEach((bridge, bridgeId) => {
34+
bridge.close();
35+
});
36+
}
37+
38+
function shutDown(error) {
39+
// Closing the server triggers the individual connections to be closed.
40+
if (server) {
41+
server.close();
42+
}
43+
if (!rclnodejs.isShutdown()) {
44+
rclnodejs.shutdown();
45+
}
46+
if (error) {
47+
throw error;
48+
}
49+
}
50+
2151
function createServer(options) {
2252
options = options || {};
2353
options.address = options.address || null;
24-
let server;
54+
process.on('exit', () => {
55+
debug('Application will exit.');
56+
shutDown();
57+
});
58+
return rclnodejs.init()
59+
.then(() => {
60+
node = rclnodejs.createNode('ros2_web_bridge');
61+
rclnodejs.spin(node);
62+
debug('ROS2 node started');
63+
createConnection(options);
64+
})
65+
.catch(error => shutDown(error));
66+
}
67+
68+
function createConnection(options) {
2569
if (options.address != null) {
2670
debug('Starting in client mode; connecting to ' + options.address);
2771
server = new WebSocket(options.address);
@@ -31,63 +75,53 @@ function createServer(options) {
3175
server = new WebSocket.Server({port: options.port});
3276
}
3377

34-
process.on('exit', () => {
35-
debug('Application will exit.');
36-
// Closing the server will trigger the individual connections to be closed and cleaned.
37-
server.close();
38-
});
78+
const makeBridge = (ws) => {
79+
let bridge = new Bridge(node, ws, options.status_level);
80+
bridgeMap.set(bridge.bridgeId, bridge);
3981

40-
return rclnodejs.init().then(() => {
41-
let node = rclnodejs.createNode('ros2_web_bridge');
42-
let bridgeMap = new Map();
82+
bridge.on('error', (error) => {
83+
debug(`Bridge ${bridge.bridgeId} closing with error: ${error}`);
84+
bridge.close();
85+
bridgeMap.delete(bridge.bridgeId);
86+
});
4387

44-
function closeAllBridges() {
45-
bridgeMap.forEach((bridge, bridgeId) => {
46-
bridge.close();
47-
});
48-
}
88+
bridge.on('close', (bridgeId) => {
89+
bridgeMap.delete(bridgeId);
90+
});
91+
};
4992

50-
const makeBridge = (ws) => {
51-
let bridge = new Bridge(node, ws, options.status_level);
52-
bridgeMap.set(bridge.bridgeId, bridge);
53-
54-
bridge.on('error', (error) => {
55-
let bridge = error.bridge;
56-
if (bridge) {
57-
debug(`Error happened, the bridge ${error.bridge.bridgeId} will be closed.`);
58-
bridge.close();
59-
bridgeMap.delete(bridge.bridgeId);
60-
} else {
61-
debug(`Unknown error happened: ${error}.`);
62-
}
63-
});
64-
65-
bridge.on('close', (bridgeId) => {
66-
bridgeMap.delete(bridgeId);
67-
});
68-
};
69-
server.on('open', () => debug('Connected as client'));
70-
if (options.address) {
71-
makeBridge(server);
72-
} else {
73-
server.on('connection', makeBridge);
74-
}
93+
server.on('open', () => {
94+
debug('Connected as client');
95+
connectionAttempts = 0;
96+
});
97+
98+
if (options.address) {
99+
makeBridge(server);
100+
} else {
101+
server.on('connection', makeBridge);
102+
}
75103

76-
server.on('error', (error) => {
77-
closeAllBridges();
78-
rclnodejs.shutdown();
79-
debug(`WebSocket server error: ${error}, the module will be terminated.`);
80-
});
104+
server.on('error', (error) => {
105+
closeAllBridges();
106+
debug(`WebSocket error: ${error}`);
107+
});
81108

82-
rclnodejs.spin(node);
83-
debug('The ros2-web-bridge has started.');
84-
let wsAddr = (options.address) ? options.address : `ws://localhost:${options.port}`;
85-
console.log(`Websocket started on ${wsAddr}`);
86-
}).catch(error => {
87-
debug(`Unknown error happened: ${error}, the module will be terminated.`);
88-
server.close();
89-
rclnodejs.shutdown();
109+
server.on('close', (event) => {
110+
debug(`Websocket closed: ${event}`);
111+
if (options.address) {
112+
closeAllBridges();
113+
connectionAttempts++;
114+
// Gradually increase reconnection interval to prevent
115+
// overwhelming the server, up to a maximum delay of ~1 minute
116+
// https://en.wikipedia.org/wiki/Exponential_backoff
117+
const delay = Math.pow(1.5, Math.min(10, Math.floor(Math.random() * connectionAttempts)));
118+
debug(`Reconnecting to ${options.address} in ${delay.toFixed(2)} seconds`);
119+
setTimeout(() => createConnection(options), delay*1000);
120+
}
90121
});
122+
123+
let wsAddr = options.address || `ws://localhost:${options.port}`;
124+
console.log(`Websocket started on ${wsAddr}`);
91125
}
92126

93127
module.exports = {

lib/bridge.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,6 @@ class Bridge extends EventEmitter {
9595
});
9696

9797
ws.on('error', (error) => {
98-
error.bridge = this;
9998
this.emit('error', error);
10099
debug(`Web socket of bridge ${this._bridgeId} error: ${error}`);
101100
});

0 commit comments

Comments
 (0)