-
Notifications
You must be signed in to change notification settings - Fork 88
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: Reorder the closing of services, prevent sagas running multiple times and close backend server properly #2499
Changes from 8 commits
e1a97d4
c8718c5
a3951e0
bf0ebb5
f7b370c
cec2c0d
de2c55a
9758408
06dee86
c98b93e
6a0a5a8
05390e6
4483f71
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
--- node_modules/ipfs-pubsub-peer-monitor/src/ipfs-pubsub-peer-monitor.js 2024-05-08 12:44:48 | ||
+++ node_modules/ipfs-pubsub-peer-monitor/src/ipfs-pubsub-peer-monitor.backup.js 2024-05-08 12:44:25 | ||
@@ -55,7 +55,7 @@ | ||
async _pollPeers () { | ||
try { | ||
const peers = await this._pubsub.peers(this._topic) | ||
- IpfsPubsubPeerMonitor._emitJoinsAndLeaves(new Set(this._peers), new Set(peers), this) | ||
+ IpfsPubsubPeerMonitor._emitJoinsAndLeaves(new Set(this._peers.map(p => p.toString())), new Set(peers.map(p => p.toString())), this) | ||
this._peers = peers | ||
} catch (err) { | ||
clearInterval(this._interval) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -224,46 +224,44 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI | |
} | ||
|
||
public async closeAllServices(options: { saveTor: boolean } = { saveTor: false }) { | ||
this.logger('Closing services') | ||
|
||
await this.closeSocket() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Close the data server first |
||
|
||
if (this.tor && !options.saveTor) { | ||
this.logger('Killing tor') | ||
await this.tor.kill() | ||
} else if (options.saveTor) { | ||
this.logger('Saving tor') | ||
} | ||
if (this.storageService) { | ||
this.logger('Stopping orbitdb') | ||
this.logger('Stopping OrbitDB') | ||
await this.storageService?.stopOrbitDb() | ||
} | ||
if (this.serverIoProvider?.io) { | ||
this.logger('Closing socket server') | ||
this.serverIoProvider.io.close() | ||
} | ||
if (this.localDbService) { | ||
this.logger('Closing local storage') | ||
await this.localDbService.close() | ||
} | ||
if (this.libp2pService) { | ||
this.logger('Stopping libp2p') | ||
await this.libp2pService.close() | ||
} | ||
if (this.localDbService) { | ||
this.logger('Closing local DB') | ||
await this.localDbService.close() | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Re-organizing. Data server should get closed first and then OrbitDB, then LibP2P |
||
|
||
public closeSocket() { | ||
this.serverIoProvider.io.close() | ||
public async closeSocket() { | ||
await this.socketService.close() | ||
} | ||
|
||
public async pause() { | ||
this.logger('Pausing!') | ||
this.logger('Closing socket!') | ||
this.closeSocket() | ||
await this.closeSocket() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I remove the log because there is a similar log in |
||
this.logger('Pausing libp2pService!') | ||
this.peerInfo = await this.libp2pService?.pause() | ||
this.logger('Found the following peer info on pause: ', this.peerInfo) | ||
} | ||
|
||
public async resume() { | ||
this.logger('Resuming!') | ||
this.logger('Reopening socket!') | ||
await this.openSocket() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Again, I remove the log because there is a similar log in socketService.listen. We can keep both also, just let me know. |
||
this.logger('Attempting to redial peers!') | ||
if (this.peerInfo && (this.peerInfo?.connected.length !== 0 || this.peerInfo?.dialed.length !== 0)) { | ||
|
@@ -289,21 +287,14 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI | |
public async leaveCommunity(): Promise<boolean> { | ||
this.logger('Running leaveCommunity') | ||
|
||
this.logger('Resetting tor') | ||
this.tor.resetHiddenServices() | ||
|
||
this.logger('Closing the socket') | ||
this.closeSocket() | ||
|
||
this.logger('Purging local DB') | ||
await this.localDbService.purge() | ||
|
||
this.logger('Closing services') | ||
await this.closeAllServices({ saveTor: true }) | ||
|
||
this.logger('Purging data') | ||
await this.purgeData() | ||
|
||
this.logger('Resetting Tor') | ||
this.tor.resetHiddenServices() | ||
|
||
this.logger('Resetting state') | ||
await this.resetState() | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,13 +27,15 @@ import { CONFIG_OPTIONS, SERVER_IO_PROVIDER } from '../const' | |
import { ConfigOptions, ServerIoProviderTypes } from '../types' | ||
import { suspendableSocketEvents } from './suspendable.events' | ||
import Logger from '../common/logger' | ||
import type net from 'node:net' | ||
|
||
@Injectable() | ||
export class SocketService extends EventEmitter implements OnModuleInit { | ||
private readonly logger = Logger(SocketService.name) | ||
|
||
public resolveReadyness: (value: void | PromiseLike<void>) => void | ||
public readyness: Promise<void> | ||
private sockets: Set<net.Socket> | ||
|
||
constructor( | ||
@Inject(SERVER_IO_PROVIDER) public readonly serverIoProvider: ServerIoProviderTypes, | ||
|
@@ -44,12 +46,15 @@ export class SocketService extends EventEmitter implements OnModuleInit { | |
this.readyness = new Promise<void>(resolve => { | ||
this.resolveReadyness = resolve | ||
}) | ||
|
||
this.sockets = new Set<net.Socket>() | ||
|
||
this.attachListeners() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [FYI] attachListeners() moved from There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yea it does trigger before |
||
} | ||
|
||
async onModuleInit() { | ||
this.logger('init: Started') | ||
|
||
this.attachListeners() | ||
await this.init() | ||
|
||
this.logger('init: Finished') | ||
|
@@ -71,7 +76,7 @@ export class SocketService extends EventEmitter implements OnModuleInit { | |
this.logger('init: Frontend connected') | ||
} | ||
|
||
private readonly attachListeners = (): void => { | ||
private readonly attachListeners = () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [nitpick] lost the return type There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think not specifying void is generally the standard TS convention |
||
// Attach listeners here | ||
this.serverIoProvider.io.on(SocketActionTypes.CONNECTION, socket => { | ||
this.logger('Socket connection') | ||
|
@@ -173,7 +178,7 @@ export class SocketService extends EventEmitter implements OnModuleInit { | |
} | ||
) | ||
|
||
socket.on(SocketActionTypes.LEAVE_COMMUNITY, async (callback: (closed: boolean) => void) => { | ||
socket.on(SocketActionTypes.LEAVE_COMMUNITY, (callback: (closed: boolean) => void) => { | ||
this.logger('Leaving community') | ||
this.emit(SocketActionTypes.LEAVE_COMMUNITY, callback) | ||
}) | ||
|
@@ -195,25 +200,77 @@ export class SocketService extends EventEmitter implements OnModuleInit { | |
this.emit(SocketActionTypes.LOAD_MIGRATION_DATA, data) | ||
}) | ||
}) | ||
|
||
// Ensure the underlying connections get closed. See: | ||
// https://github.com/socketio/socket.io/issues/1602 | ||
// | ||
// I also tried `this.serverIoProvider.io.disconnectSockets(true)` | ||
// which didn't work for me. | ||
this.serverIoProvider.server.on('connection', conn => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [bug] This seems like a confusion of the server and the socket io. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. And in my testing never seems to be called There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This gets called for me. There are two different sockets, the socket.io Socket (https://socket.io/docs/v4/server-api/#socket) and the Node |
||
this.sockets.add(conn) | ||
conn.on('close', () => { | ||
this.sockets.delete(conn) | ||
}) | ||
}) | ||
} | ||
|
||
public getConnections = (): Promise<number> => { | ||
return new Promise(resolve => { | ||
this.serverIoProvider.server.getConnections((err, count) => { | ||
if (err) throw new Error(err.message) | ||
resolve(count) | ||
}) | ||
}) | ||
} | ||
|
||
// Ensure the underlying connections get closed. See: | ||
// https://github.com/socketio/socket.io/issues/1602 | ||
public closeSockets = () => { | ||
this.logger('Disconnecting sockets') | ||
this.sockets.forEach(s => s.destroy()) | ||
} | ||
|
||
public listen = async (port = this.configOptions.socketIOPort): Promise<void> => { | ||
return await new Promise(resolve => { | ||
if (this.serverIoProvider.server.listening) resolve() | ||
public listen = async (): Promise<void> => { | ||
this.logger(`Opening data server on port ${this.configOptions.socketIOPort}`) | ||
|
||
if (this.serverIoProvider.server.listening) { | ||
this.logger('Failed to listen. Server already listening.') | ||
return | ||
} | ||
|
||
const numConnections = await this.getConnections() | ||
|
||
if (numConnections > 0) { | ||
this.logger('Failed to listen. Connections still open:', numConnections) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [question] Wouldn't it be better to just clean up the existing connections and proceed with listening? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure. I thought about that, but there might be some trickiness with timing due to the listen and close being async. I'll have to look into it more, so just went with the simplest option for now. |
||
return | ||
} | ||
|
||
return new Promise(resolve => { | ||
this.serverIoProvider.server.listen(this.configOptions.socketIOPort, '127.0.0.1', () => { | ||
this.logger(`Data server running on port ${this.configOptions.socketIOPort}`) | ||
resolve() | ||
}) | ||
}) | ||
} | ||
|
||
public close = async (): Promise<void> => { | ||
this.logger(`Closing data server on port ${this.configOptions.socketIOPort}`) | ||
return await new Promise(resolve => { | ||
this.serverIoProvider.server.close(err => { | ||
public close = (): Promise<void> => { | ||
return new Promise(resolve => { | ||
this.logger(`Closing data server on port ${this.configOptions.socketIOPort}`) | ||
|
||
if (!this.serverIoProvider.server.listening) { | ||
this.logger('Data server is not running.') | ||
resolve() | ||
return | ||
} | ||
|
||
this.serverIoProvider.io.close(err => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [question] Does this suffer from the same issue mentioned in the cleanup function stuck in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yea, this doesn't appear to close all connections properly. I don't think we need to remove the listeners ever. So those just get added once in the constructor. |
||
if (err) throw new Error(err.message) | ||
this.logger('Data server closed') | ||
resolve() | ||
}) | ||
|
||
this.serverIoProvider.io.disconnectSockets(true) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [nitpick] I would put this in |
||
this.closeSockets() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [question] Shouldn't we also detach listeners when closing sockets? I mentioned it before, but I'd rather see a dedicated There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think we need to detach any listeners. They are just attached once and persist between opening and closing of the server. |
||
}) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,18 @@ | ||
import { io } from 'socket.io-client' | ||
import { select, put, call, cancel, fork, takeEvery, FixedTask, delay, apply, putResolve } from 'typed-redux-saga' | ||
import { | ||
select, | ||
put, | ||
putResolve, | ||
call, | ||
cancel, | ||
fork, | ||
take, | ||
takeLeading, | ||
takeEvery, | ||
FixedTask, | ||
delay, | ||
apply, | ||
} from 'typed-redux-saga' | ||
import { PayloadAction } from '@reduxjs/toolkit' | ||
import { socket as stateManager, Socket } from '@quiet/state-manager' | ||
import { encodeSecret } from '@quiet/common' | ||
|
@@ -49,17 +62,20 @@ export function* startConnectionSaga( | |
}) | ||
yield* fork(handleSocketLifecycleActions, socket, action.payload) | ||
// Handle opening/restoring connection | ||
yield* takeEvery(initActions.setWebsocketConnected, setConnectedSaga, socket) | ||
yield* takeLeading(initActions.setWebsocketConnected, setConnectedSaga, socket) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This only needs to happen in sequence |
||
} | ||
|
||
function* setConnectedSaga(socket: Socket): Generator { | ||
console.log('Frontend is ready. Forking state-manager sagas and starting backend...') | ||
|
||
const task = yield* fork(stateManager.useIO, socket) | ||
console.log('WEBSOCKET', 'Forking state-manager sagas', task) | ||
// Handle suspending current connection | ||
yield* takeEvery(initActions.suspendWebsocketConnection, cancelRootTaskSaga, task) | ||
console.log('Frontend is ready. Starting backend...') | ||
|
||
// @ts-ignore - Why is this broken? | ||
yield* apply(socket, socket.emit, [SocketActionTypes.START]) | ||
|
||
// Handle suspending current connection | ||
const suspendAction = yield* take(initActions.suspendWebsocketConnection) | ||
yield* call(cancelRootTaskSaga, task, suspendAction) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This only needs to happen once ( |
||
} | ||
|
||
function* handleSocketLifecycleActions(socket: Socket, socketIOData: WebsocketConnectionPayload): Generator { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No need for async here since nothing awaits the event handler