Skip to content

Fix thread leak in FileDownloadDelegate #614

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

Merged
merged 12 commits into from
Aug 18, 2022

Conversation

dnadoba
Copy link
Collaborator

@dnadoba dnadoba commented Aug 12, 2022

Fixes #364

Modification

  • remember if we own the thread pool or not.
  • shutdown the thread pool if we own it after we are done with it

Result

we no longer leak one thread per download

@dnadoba dnadoba force-pushed the dn-fix-file-download-thread-leak branch from c6a1a3d to ac67b22 Compare August 12, 2022 14:00
@dnadoba
Copy link
Collaborator Author

dnadoba commented Aug 12, 2022

API breakage failure is a false positive

16:14:03 1 breaking change detected in AsyncHTTPClient:
16:14:03   💔 API breakage: constructor FileDownloadDelegate.init(path:pool:reportHead:reportProgress:) has removed default argument from parameter 1

The change in question can be simplified to:

struct Foo {
    // old init
    init(bar: Int = 0) {}
    // new inits
    init() {}
    init(bar: Int) {}
}

replacing a default argument with two inits, one with the argument and one without, is an ABI breaking change but not an API breaking change.

@dnadoba dnadoba added the 🔨 semver/patch No public API change. label Aug 12, 2022
break
case .owned(let pool):
// if the delegate is unused we still need to shutdown the thread pool
try! pool.syncShutdownGracefully()
Copy link
Contributor

Choose a reason for hiding this comment

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

we shouldn't destroy this in deinit, you don't even have a real guarantee that deinit isn't running in a thread owned by the pool (which would make it deadlock).

I'd suggest to add a lifecycle hook (shutdown or so) to the delegate and have AHC invoke that. That new shutdown method can have an empty default implementation to not break the API.

Separately, I think AHC should provide the thread pool or else we spin up/down a thread per download and that kinda makes it pointless to do asynchronous I/O if we need a new thread each time.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Is a lifecycle hook compatible with AHC providing the thread pool?

I'm inclined to say that if the user doesn't provide a thread pool we should just use a global static. Users can override the thread pool for testing if they need to shut stuff down, but the default case is that they don't care and the threads can be cleaned up at termination.

How do you feel about that?

Copy link
Contributor

Choose a reason for hiding this comment

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

actually you could add two delegate methods (with default empty impls):

  • provideThreadPool(ioPool) where AHC provides a running thread pool to the delegate
  • shutdown() where AHC tells the delegate that it its job is done

After that, we can change the default here and have the delegate not even have the option to accept an io pool from the user.

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 AHC provides the default shared thread pool we don't need the shutdown() lifecycle hook because the delegate doesn't need to shutdown the thread pool anymore but the AHC client.

Copy link
Contributor

Choose a reason for hiding this comment

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

that's true

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 have added a shared file IO thread pool to the HTTPClient which is passed to the delegate through a call to provideSharedThreadPool(fileIOPool:)
1afc151

@dnadoba dnadoba force-pushed the dn-fix-file-download-thread-leak branch from 554c0d5 to 1afc151 Compare August 15, 2022 14:18
Copy link
Contributor

@weissi weissi left a comment

Choose a reason for hiding this comment

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

awesome, thank you!

@dnadoba dnadoba force-pushed the dn-fix-file-download-thread-leak branch from 6586cf0 to ac0b9f7 Compare August 16, 2022 12:21
@dnadoba
Copy link
Collaborator Author

dnadoba commented Aug 16, 2022

I have changed the implementation because we would otherwise only ever use one thread for all file IO. This is not good because file IO can block the thread and there is no easy way of making it non-blocking. We therefore initialize the thread pool with ProcessInfo.processInfo.processorCount threads. As this is potentially much more expensive operation compared to just creating one thread we also delay initialization until the first actual write.

@dnadoba dnadoba requested a review from weissi August 16, 2022 13:09
Copy link
Collaborator

@Lukasa Lukasa left a comment

Choose a reason for hiding this comment

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

Generally looks good, thanks, but we'll want some tests of the functionality I think.


init(eventLoop: EventLoop, logger: Logger) {
public var fileIOThreadPool: NIOThreadPool {
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 realized that we actually don't need to make this public. @Lukasa any objections to me making it internal?

Copy link
Collaborator

Choose a reason for hiding this comment

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

None at all.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done in c3c68b2

@dnadoba
Copy link
Collaborator Author

dnadoba commented Aug 17, 2022

@Lukasa test added in 9ee5c82

@dnadoba dnadoba merged commit fc510a3 into swift-server:main Aug 18, 2022
@dnadoba dnadoba deleted the dn-fix-file-download-thread-leak branch August 18, 2022 09:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🔨 semver/patch No public API change.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

fix FileDownloadDelegate
3 participants