-
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
Conversation
7f61ba6
to
0060adc
Compare
We were closing LevelDB before closing OrbitDB which resulted in an exception.
467d988
to
bf0ebb5
Compare
const connectionsManager = app.get<ConnectionsManagerService>(ConnectionsManagerService) | ||
await connectionsManager.pause() | ||
connectionsManager.pause() |
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
if (this.libp2pService) { | ||
this.logger('Stopping libp2p') | ||
await this.libp2pService.close() | ||
} | ||
} |
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.
Re-organizing. Data server should get closed first and then OrbitDB, then LibP2P
} | ||
|
||
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 comment
The reason will be displayed to describe this comment to others. Learn more.
I remove the log because there is a similar log in sockerService.close
, but we can keep it too, just let me know.
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 comment
The 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.
@@ -49,17 +61,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 comment
The reason will be displayed to describe this comment to others. Learn more.
This only needs to happen in sequence
|
||
// 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 comment
The reason will be displayed to describe this comment to others. Learn more.
This only needs to happen once (takeEvery
is not necessary and causes issues)
console.log('nativeServicesCallbacksSaga stopping') | ||
if (yield cancelled()) { | ||
console.log('nativeServicesCallbacksSaga cancelled') | ||
} |
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.
Just adding additional logging around all of these long-running sagas (anything that runs until cancelled... so anything with a takeEvery
in it or other while (true)
loop)
// Restart backend | ||
yield* putResolve(app.actions.closeServices()) | ||
|
||
yield takeLeading(initActions.canceledRootTask.type, clearReduxStore) |
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.
We never want to run takeLeading
or takeEvery
more than once because, for example, in this case it will result in clearReduxStore
being run multiple times. I moved this into the rootSaga
instead.
fork(setupCryptoSaga), | ||
fork(initMasterSaga), | ||
fork(navigationMasterSaga), | ||
fork(nativeServicesMasterSaga), |
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.
Running these multiple times without cancelling and restarting rootSaga
results in multiple sagas being run for each event. I didn't see any need to cancel and restart rootSaga
like we do with useIO
, so I just used fork
here.
@@ -223,47 +223,51 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI | |||
} | |||
} | |||
|
|||
public async closeAllServices(options: { saveTor: boolean } = { saveTor: false }) { | |||
public async closeAllServices( | |||
options: { saveTor: boolean; purgeLocalDb: boolean } = { saveTor: false, purgeLocalDb: false } |
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.
Feels a little clunky to add another parameter here since closeAllServices
is doing a little more than just closing things now, but it seems like the simplest change without a larger refactor. Also if closeAllServices
is purging local-db
then maybe it should also purge data on the filesystem.
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.
[question] Why do we need to add this parameter? Maybe I'm reading into the name localDbService
too much, but it seems like the data that would be cleared by this.localDbService.purge()
would be the same data that would be cleared by this.purgeData()
called in leaveCommunity()
since that purgeData()
seems to holistically remove all the local db data from the filesystem. Adding a side effect to closeAllServices()
seems like it would be a bit of a code smell.
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.
I'm not really sure, if we are sure that LevelDB would be totally cleared by purgeData
then I don't think this.localDbServer.purge()
is required - I just rearranged what was already there.
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.
If we close and then re-open the same LevelDB, is there data in memory that sticks around? Or does removing the LevelDB files clear everything?
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.
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.
From the link above "DestroyDB operation will be
faster for moderate to large databases (its implementation works
pretty much as Krzysztof Kowalczyk mentioned: it deletes all the
database files in the directory)." and then taking a look at the source code itself, looks like that is what's going on: https://github.com/google/leveldb/blob/068d5ee1a3ac40dabd00d211d5013af44be55bea/db/db_impl.cc#L1542
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.
I just tested this and it appears to work.
) { | ||
this.logger('Closing services') | ||
|
||
await this.closeSocket() |
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.
Close the data server first
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.
This looks great!
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.
I'm not entirely sure about the changes to the sagas, but I have some questions, some nitpicks, and some design reservations about the changes to the SocketService.
@@ -27,13 +27,17 @@ import { CONFIG_OPTIONS, SERVER_IO_PROVIDER } from '../const' | |||
import { ConfigOptions, ServerIoProviderTypes } from '../types' | |||
import { suspendableSocketEvents } from './suspendable.events' | |||
import Logger from '../common/logger' | |||
import { sleep } from '../common/sleep' |
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.
[nitpick] unused import
}) | ||
}) | ||
|
||
return () => sockets.forEach(s => s.destroy()) |
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.
[design] This feels very unintuitive to me. attachListeners()
doubling as closeSockets()
feels like a violation of the single responsibility principle. Why can't we have a separate closeSockets()
function? This would make the code easier to read and understand.
if (err) throw new Error(err.message) | ||
resolve(count) | ||
}) | ||
}) | ||
} | ||
|
||
public listen = async (port = this.configOptions.socketIOPort): Promise<void> => { |
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.
[refactor] port
is unused. We should either remove the parameter or use it in the function instead of hardcoding the port number.
// which didn't work for me. | ||
const sockets = new Set<net.Socket>() | ||
|
||
this.serverIoProvider.server.on('connection', conn => { |
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.
[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 comment
The 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 comment
The 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 net.Socket
. I'm really just following the advice here: socketio/socket.io#1602 and it worked for me in cleaning up the hanging connections I was seeing, so I just went with that.
return | ||
} | ||
|
||
this.serverIoProvider.io.close(err => { |
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.
[question] Does this suffer from the same issue mentioned in the cleanup function stuck in attachListeners
where you need to loop through each socket and disconnect? Also, does this remove all of the listeners that were attached in the attachListeners
function? Could this cause a memory leak and duplication of actions if the SocketService
is started back up after being close
and the listeners are attached again?
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.
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.
// `this.serverIoProvider.server.listening` is sufficient. | ||
if (this.listening) { | ||
const numConnections = await this.getConnections() | ||
this.logger('Failed to listen. Connections still open:', numConnections) |
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.
[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 comment
The 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.
this.serverIoProvider.server.listen(this.configOptions.socketIOPort, '127.0.0.1', () => { | ||
this.logger(`Data server running on port ${this.configOptions.socketIOPort}`) | ||
this.listening = true |
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.
[design] This seems like reinventing the wheel. I feel like this just introduces chances for desync between the state of the http server and the state of SocketService.listening
. I'd rather see us use the built in property and properly handle the cases related to no longer listening, but not having properly cleaned up connections.
resolve() | ||
}) | ||
this.logger('Disconnecting sockets') | ||
this.closeSockets() |
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.
[question] Shouldn't we also detach listeners when closing sockets? I mentioned it before, but I'd rather see a dedicated closeSockets()
and detachListeners()
method.
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.
I don't think we need to detach any listeners. They are just attached once and persist between opening and closing of the server.
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.
Just nitpicks that don't necessarily need addressed. I am concerned about the move of attachListeners()
from onModuleInit
to the constructor
, but I'm approving on the assumption that you have proven this doesn't cause any issues.
@@ -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 comment
The 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 comment
The reason will be displayed to describe this comment to others. Learn more.
I think not specifying void is generally the standard TS convention
|
||
this.sockets = new Set<net.Socket>() | ||
|
||
this.attachListeners() |
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.
[FYI] attachListeners() moved from onModuleInit()
to the constructor
. I'm not sure what side effects this would have. Might cause attachListeners
to trigger before init
?
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.
Yea it does trigger before init
and I think that's fine. I think it makes sense to put everything necessary to setup an object in the constructor and only using onModuleInit
when necessary and it doesn't seem like attachListeners
needs to be in onModuleInit
.
resolve() | ||
}) | ||
|
||
this.serverIoProvider.io.disconnectSockets(true) |
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.
[nitpick] I would put this in closeSockets
, but mostly inconsequential.
…times and close backend server properly (#2499)
Fixes:
We were closing LevelDB before closing OrbitDB which resulted in an exception.
Solution for Clear LevelDB after OrbitDB when leaving a community #2497
Sagas were running multiple times after leaving a community due to our use of
takeEvery
.Solution for Sagas run multiple times after leaving and rejoining community #2509
The backend server wasn't getting closed properly when the app went to background on iOS.
Solution for Stuck on 'Connecting process started' due to missing community on iOS #2500
There was a bug in the (old) version of OrbitDB that we are using that caused it to send heads in a loop.
Solution for App can get stuck on 'Loading messages' when joining #2496 and Stuck on splash screen when leaving community #2512
Pull Request Checklist
(Optional) Mobile checklist
Please ensure you completed the following checks if you did any changes to the mobile package: