Skip to content
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

Merged
merged 13 commits into from
May 9, 2024

Conversation

leblowl
Copy link
Collaborator

@leblowl leblowl commented May 1, 2024

Fixes:

Pull Request Checklist

  • I have linked this PR to a related GitHub issue.
  • I have added a description of the change (and Github issue number, if any) to the root CHANGELOG.md.

(Optional) Mobile checklist

Please ensure you completed the following checks if you did any changes to the mobile package:

  • I have run e2e tests for mobile
  • I have updated base screenshots for visual regression tests

@leblowl leblowl changed the title fix: Reorder the closing of services and add additional logs fix: Reorder the closing of services, prevent sagas running multiple times and close backend server properly May 6, 2024
@leblowl leblowl force-pushed the fix-close-services-order branch from 7f61ba6 to 0060adc Compare May 6, 2024 22:16
Lucas Leblow added 4 commits May 7, 2024 10:41
@leblowl leblowl force-pushed the fix-close-services-order branch from 467d988 to bf0ebb5 Compare May 7, 2024 16:47
const connectionsManager = app.get<ConnectionsManagerService>(ConnectionsManagerService)
await connectionsManager.pause()
connectionsManager.pause()
Copy link
Collaborator Author

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()
}
}
Copy link
Collaborator Author

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()
Copy link
Collaborator Author

@leblowl leblowl May 7, 2024

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()
Copy link
Collaborator Author

@leblowl leblowl May 7, 2024

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)
Copy link
Collaborator Author

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)
Copy link
Collaborator Author

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')
}
Copy link
Collaborator Author

@leblowl leblowl May 7, 2024

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)
Copy link
Collaborator Author

@leblowl leblowl May 7, 2024

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),
Copy link
Collaborator Author

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 }
Copy link
Collaborator Author

@leblowl leblowl May 7, 2024

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.

Copy link
Collaborator

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.

Copy link
Collaborator Author

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.

Copy link
Collaborator Author

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?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator Author

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

Copy link
Collaborator Author

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()
Copy link
Collaborator Author

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

Copy link
Collaborator

@islathehut islathehut left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great!

Copy link
Collaborator

@adrastaea adrastaea left a 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'
Copy link
Collaborator

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())
Copy link
Collaborator

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> => {
Copy link
Collaborator

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 => {
Copy link
Collaborator

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.

Copy link
Collaborator

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

Copy link
Collaborator Author

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 => {
Copy link
Collaborator

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?

Copy link
Collaborator Author

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)
Copy link
Collaborator

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?

Copy link
Collaborator Author

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
Copy link
Collaborator

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()
Copy link
Collaborator

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.

Copy link
Collaborator Author

@leblowl leblowl May 8, 2024

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.

Lucas Leblow added 2 commits May 8, 2024 12:34
Copy link
Collaborator

@adrastaea adrastaea left a 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 = () => {
Copy link
Collaborator

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

Copy link
Collaborator Author

@leblowl leblowl May 8, 2024

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()
Copy link
Collaborator

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?

Copy link
Collaborator Author

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)
Copy link
Collaborator

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.

@leblowl leblowl merged commit 134fbcb into 2.2.0 May 9, 2024
22 of 26 checks passed
leblowl pushed a commit that referenced this pull request May 10, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants