Skip to content

Commit cde3203

Browse files
fix: graceful shutdown (#1994)
* Shutdown to revoke connection retries * SocksClient to use an existing connection to tor * ConnextClient disconnect to abort pending requests * Track reachability verification peer, for shutdown purposes * Pool disconnection to announce 'shutdown' to all peers * Fix jest test * Revert npm-shrinkwrap.json * Use Set instead of Map
1 parent 43e7c8e commit cde3203

File tree

4 files changed

+72
-11
lines changed

4 files changed

+72
-11
lines changed

Diff for: lib/connextclient/ConnextClient.ts

+21-1
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,9 @@ class ConnextClient extends SwapClient {
118118
private outboundAmounts = new Map<string, number>();
119119
private inboundAmounts = new Map<string, number>();
120120

121+
private pendingRequests = new Set<http.ClientRequest>();
122+
private criticalRequestPaths = ['/hashlock-resolve', '/hashlock-transfer'];
123+
121124
/** The minimum incremental quantity that we may use for collateral requests. */
122125
private static MIN_COLLATERAL_REQUEST_SIZES: { [key: string]: number | undefined } = {
123126
ETH: 0.1 * 10 ** 8,
@@ -866,6 +869,16 @@ class ConnextClient extends SwapClient {
866869
/** Connext client specific cleanup. */
867870
protected disconnect = async () => {
868871
this.setStatus(ClientStatus.Disconnected);
872+
873+
for (const req of this.pendingRequests) {
874+
if (this.criticalRequestPaths.includes(req.path)) {
875+
this.logger.warn(`critical request is pending: ${req.path}`);
876+
continue;
877+
}
878+
879+
this.logger.info(`aborting pending request: ${req.path}`);
880+
req.destroy();
881+
}
869882
}
870883

871884
/**
@@ -893,7 +906,11 @@ class ConnextClient extends SwapClient {
893906
}
894907

895908
this.logger.trace(`sending request to ${endpoint}${payloadStr ? `: ${payloadStr}` : ''}`);
896-
const req = http.request(options, async (res) => {
909+
910+
let req: http.ClientRequest;
911+
req = http.request(options, async (res) => {
912+
this.pendingRequests.delete(req);
913+
897914
let err: XudError | undefined;
898915
let body;
899916
switch (res.statusCode) {
@@ -935,6 +952,7 @@ class ConnextClient extends SwapClient {
935952
});
936953

937954
req.on('error', async (err: any) => {
955+
this.pendingRequests.delete(req);
938956
if (err.code === 'ECONNREFUSED') {
939957
await this.disconnect();
940958
}
@@ -945,7 +963,9 @@ class ConnextClient extends SwapClient {
945963
if (payloadStr) {
946964
req.write(payloadStr);
947965
}
966+
948967
req.end();
968+
this.pendingRequests.add(req);
949969
});
950970
}
951971
}

Diff for: lib/p2p/Peer.ts

+8-3
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,7 @@ class Peer extends EventEmitter {
331331
}
332332

333333
this.status = PeerStatus.Closed;
334+
this.revokeConnectionRetries();
334335

335336
if (this.socket) {
336337
if (!this.socket.destroyed) {
@@ -521,6 +522,8 @@ class Peer extends EventEmitter {
521522
this.connectionRetriesRevoked = false;
522523

523524
const connectViaProxy = () => {
525+
this.socket = net.connect(torport, 'localhost');
526+
524527
const proxyOptions: SocksClientOptions = {
525528
proxy: {
526529
host: 'localhost',
@@ -532,11 +535,11 @@ class Peer extends EventEmitter {
532535
host: this.address.host,
533536
port: this.address.port,
534537
},
538+
existing_socket: this.socket,
535539
};
536540
SocksClient.createConnection(proxyOptions)
537541
.then((info) => {
538-
// a raw net.Socket that is established to the destination host through the given proxy server
539-
this.socket = info.socket;
542+
assert(this.socket === info.socket);
540543
onConnect();
541544
})
542545
.catch(onError);
@@ -614,7 +617,9 @@ class Peer extends EventEmitter {
614617
}
615618

616619
private initStall = (): void => {
617-
assert(this.status !== PeerStatus.Closed);
620+
if (this.status !== PeerStatus.Closed) {
621+
return;
622+
}
618623
assert(!this.stallTimer);
619624

620625
this.stallTimer = setInterval(this.checkTimeout, Peer.STALL_INTERVAL);

Diff for: lib/p2p/Pool.ts

+14-6
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,11 @@ class Pool extends EventEmitter {
279279
if (this.loadingNodesPromise) {
280280
await this.loadingNodesPromise;
281281
}
282-
await Promise.all([this.unlisten(), this.closePendingConnections(), this.closePeers()]);
282+
await Promise.all([
283+
this.unlisten(),
284+
this.closePendingConnections(DisconnectionReason.Shutdown),
285+
this.closePeers(DisconnectionReason.Shutdown)],
286+
);
283287
this.connected = false;
284288
this.disconnecting = false;
285289
}
@@ -303,13 +307,17 @@ class Pool extends EventEmitter {
303307
this.logger.debug(`Verifying reachability of advertised address: ${externalAddress}`);
304308
try {
305309
const peer = new Peer(Logger.DISABLED_LOGGER, address, this.network);
310+
311+
this.pendingOutboundPeers.set(this.nodePubKey, peer);
306312
await peer.beginOpen({
307313
ownNodeState: this.nodeState,
308314
ownNodeKey: this.nodeKey,
309315
ownVersion: this.version,
310316
expectedNodePubKey: this.nodePubKey,
311317
torport: this.config.torport,
312318
});
319+
this.pendingOutboundPeers.delete(this.nodePubKey);
320+
313321
await peer.close();
314322
assert.fail();
315323
} catch (err) {
@@ -1002,21 +1010,21 @@ class Pool extends EventEmitter {
10021010
}
10031011
}
10041012

1005-
private closePeers = () => {
1013+
private closePeers = (reason?: DisconnectionReason) => {
10061014
const closePromises = [];
10071015
for (const peer of this.peers.values()) {
1008-
closePromises.push(peer.close(DisconnectionReason.Shutdown));
1016+
closePromises.push(peer.close(reason));
10091017
}
10101018
return Promise.all(closePromises);
10111019
}
10121020

1013-
private closePendingConnections = () => {
1021+
private closePendingConnections = (reason?: DisconnectionReason) => {
10141022
const closePromises = [];
10151023
for (const peer of this.pendingOutboundPeers.values()) {
1016-
closePromises.push(peer.close());
1024+
closePromises.push(peer.close(reason));
10171025
}
10181026
for (const peer of this.pendingInboundPeers) {
1019-
closePromises.push(peer.close());
1027+
closePromises.push(peer.close(reason));
10201028
}
10211029
return Promise.all(closePromises);
10221030
}

Diff for: test/jest/Connext.spec.ts

+29-1
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,20 @@ jest.mock('http', () => {
2727
statusCode: 404,
2828
});
2929
}
30+
31+
let errorCb: any;
3032
return {
33+
path: options.path,
3134
write: jest.fn(),
32-
on: jest.fn(),
3335
end: jest.fn(),
36+
on: jest.fn().mockImplementation((event, cb) => {
37+
if (event === 'error') {
38+
errorCb = cb;
39+
}
40+
}),
41+
destroy: jest.fn().mockImplementation(() => {
42+
errorCb();
43+
}),
3444
};
3545
}),
3646
};
@@ -55,6 +65,8 @@ describe('ConnextClient', () => {
5565
logger.trace = jest.fn();
5666
logger.error = jest.fn();
5767
logger.debug = jest.fn();
68+
logger.warn = jest.fn();
69+
logger.info = jest.fn();
5870
const currencyInstances = [
5971
{
6072
id: 'ETH',
@@ -427,4 +439,20 @@ describe('ConnextClient', () => {
427439
expect(connext['sendRequest']).toHaveBeenCalledTimes(0);
428440
});
429441
});
442+
443+
describe('disconnect', () => {
444+
it('aborts pending requests, except critical ones', async () => {
445+
expect(connext['pendingRequests'].size).toEqual(0);
446+
447+
connext['sendRequest'](connext['criticalRequestPaths'][0], '', {});
448+
connext['sendRequest']('/path1', '', {});
449+
connext['sendRequest']('/path1', '', {});
450+
connext['sendRequest']('/path2', '', {});
451+
connext['sendRequest'](connext['criticalRequestPaths'][1], '', {});
452+
expect(connext['pendingRequests'].size).toEqual(5);
453+
454+
connext['disconnect']();
455+
expect(connext['pendingRequests'].size).toEqual(2);
456+
});
457+
});
430458
});

0 commit comments

Comments
 (0)