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

Memory leak in ThreadPoolExecutor + run_in_executor #82611

Closed
EvgenyNizhibitsky mannequin opened this issue Oct 10, 2019 · 17 comments
Closed

Memory leak in ThreadPoolExecutor + run_in_executor #82611

EvgenyNizhibitsky mannequin opened this issue Oct 10, 2019 · 17 comments
Labels
3.10 only security fixes 3.11 only security fixes 3.12 bugs and security fixes topic-asyncio type-bug An unexpected behavior, bug, or error

Comments

@EvgenyNizhibitsky
Copy link
Mannequin

EvgenyNizhibitsky mannequin commented Oct 10, 2019

BPO 38430
Nosy @asvetlov, @1st1, @eamanu
PRs
  • WIP: bpo-38430: Memory leak in ThreadPoolExecutor + run_in_executor #17038
  • Files
  • python-leak.zip: Minimal python script + dockerfile/makefile for reproducibility
  • Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.

    Show more details

    GitHub fields:

    assignee = None
    closed_at = None
    created_at = <Date 2019-10-10.09:41:34.118>
    labels = ['expert-asyncio', '3.7', '3.9', 'performance']
    title = 'Memory leak in ThreadPoolExecutor + run_in_executor'
    updated_at = <Date 2019-11-05.01:20:32.415>
    user = 'https://bugs.python.org/EvgenyNizhibitsky'

    bugs.python.org fields:

    activity = <Date 2019-11-05.01:20:32.415>
    actor = 'vstinner'
    assignee = 'none'
    closed = False
    closed_date = None
    closer = None
    components = ['asyncio']
    creation = <Date 2019-10-10.09:41:34.118>
    creator = 'Evgeny Nizhibitsky'
    dependencies = []
    files = ['48654']
    hgrepos = []
    issue_num = 38430
    keywords = ['patch']
    message_count = 14.0
    messages = ['354351', '354352', '354353', '354354', '354355', '354356', '354357', '354358', '354360', '355835', '355836', '355843', '355864', '355882']
    nosy_count = 4.0
    nosy_names = ['asvetlov', 'yselivanov', 'eamanu', 'Evgeny Nizhibitsky']
    pr_nums = ['17038']
    priority = 'normal'
    resolution = None
    stage = 'patch review'
    status = 'open'
    superseder = None
    type = 'resource usage'
    url = 'https://bugs.python.org/issue38430'
    versions = ['Python 3.6', 'Python 3.7', 'Python 3.9']

    @EvgenyNizhibitsky
    Copy link
    Mannequin Author

    EvgenyNizhibitsky mannequin commented Oct 10, 2019

    I have run into a memory leak caused by using run_in_executor + ThreadPoolExecutor while running some stability tests with custom web services.

    It was 1 MB leaked for 1k requests made for my case and I've extracted the root cause and converted it into minimal script with both mentioned parts + just NOP function to "run".

    The script can easily eat up to 1 GB of memory in less then 1 minute now. It uses external psutil library to report the memory allocated but it can be easily commented out and the leak will stay anyway.

    One can found that script attached + Dockerfile/Makefile for reproducibility. I've also reproduced it in my own conda-based 3.7 environment as well as the master branch of cpython.

    @EvgenyNizhibitsky EvgenyNizhibitsky mannequin added 3.7 (EOL) end of life 3.9 only security fixes topic-asyncio performance Performance or resource usage labels Oct 10, 2019
    @vstinner
    Copy link
    Member

    Well, that's a common issue when using asyncio: you forgot await.

    async def main(_loop):
        while True:
            with futures.ThreadPoolExecutor() as pool:
                _loop.run_in_executor(pool, nop)
            sys.stdout.write(f'\r{get_mem():0.3f}MB')

    It should be: "await _loop.run_in_executor(pool, nop)" ;-)

    Sadly, PYTHONASYNCIODEBUG=1 env var doesn't complain on this bug.

    See: https://docs.python.org/dev/library/asyncio-dev.html#debug-mode

    @asvetlov
    Copy link
    Contributor

    You MUST await a future returned from loop.run_in_executor() to avoid the leak.

    Yuri, what should we do with the issue? I see the second similar report in the last half of the year.
    Sure, we can add weakrefs somewhere in futures._chain_future() but I pretty sure that the proper fix is just enforcing await here, e.g.

    1. rename existing run_in_executor() into a private _run_in_executor() function.

    2. write async trampoline:
      async def run_in_executor(self, executor, func, *args):
      return await self._run_in_executor(executor, func, *args)

    @asvetlov asvetlov reopened this Oct 10, 2019
    @asvetlov asvetlov removed the invalid label Oct 10, 2019
    @asvetlov
    Copy link
    Contributor

    Victor answered the first :)

    @vstinner
    Copy link
    Member

    Yuri, what should we do with the issue?

    A Task emits a warning when it's not awaited. Can a Task be used instead of a Future in run_in_executor()?

    @asvetlov
    Copy link
    Contributor

    Can a Task be used instead of a Future in run_in_executor()?

    I don't think that the task is required here. The problem is that run_in_executor is a function that returns asyncio future; that is in turn a wrapper around concurrent future object.

    If we convert run_in_executor() into async function we'll get a warning about unawaited coroutine even without asyncio debug mode.

    @vstinner
    Copy link
    Member

    If we convert run_in_executor() into async function we'll get a warning about unawaited coroutine even without asyncio debug mode.

    That sounds like a good solution :-)

    @asvetlov
    Copy link
    Contributor

    The change is slightly not backward compatible but

    1. It's pretty visible. In the worst-case instead of the memory leak people see a RuntimeWarning
    2. We did it already for a part of API, e.g. loop.sock_read() and family

    @EvgenyNizhibitsky
    Copy link
    Mannequin Author

    EvgenyNizhibitsky mannequin commented Oct 10, 2019

    Oh, I see that in the initial code with leakage (it was heavy ThreadPoolExecutor + xgboost thing) there was an await but I must have lost it somewhere while reducing it to the minimal example and finished in the wrong direction.

    Glad too see that it raised a discussion to prevent others from getting into this silent trap.

    @1st1
    Copy link
    Member

    1st1 commented Nov 1, 2019

    The change is slightly not backward compatible but

    Yeah, that's my main problem with converting loop.run_in_executor() to a coroutine. When I attempted doing that I discovered that there's code that expects the method to return a Future, and so expects it have the cancel() method.

    If we convert it to a coroutine a lot of code will break, which might be OK if it's really necessary. Is it though? Can we return a special Future subclass that complains if it's not awaited? Would that fix the problem?

    @asvetlov
    Copy link
    Contributor

    asvetlov commented Nov 1, 2019

    I thought about returning a special subclass.
    Future has too rich API: get_loop(), done(), result() etc.

    What's about returning the proxy object with future instance embedded; the object raises DeprecationWarning for everythin except __repr__, __del__ and __await__, __getattr__ redirects to getattr(self._fut, name) for all other attributes access.

    It is a more complex solution but definitely 100% backward compatible; plus the solution we can prepare people for removing the deprecated code eventually.

    @1st1
    Copy link
    Member

    1st1 commented Nov 1, 2019

    It is a more complex solution but definitely 100% backward compatible; plus the solution we can prepare people for removing the deprecated code eventually.

    Yeah. Do you think it's worth it bothering with this old low-level API instead of making a new high-level one? I don't, but if you do the feel free to change it.

    @asvetlov
    Copy link
    Contributor

    asvetlov commented Nov 2, 2019

    The API exists, people use it and get the memory leak.
    We should either remove the API (not realistic dream at least for many years) or fix it. There is no choice actually.

    @1st1
    Copy link
    Member

    1st1 commented Nov 2, 2019

    We should either remove the API (not realistic dream at least for many years) or fix it. There is no choice actually.

    I don't understand. What happens if we don't await the future that run_in_executor returns? Does it get GCed eventually? Why is memory leaking?

    @ezio-melotti ezio-melotti transferred this issue from another repository Apr 10, 2022
    @iritkatriel iritkatriel added type-bug An unexpected behavior, bug, or error and removed performance Performance or resource usage labels Aug 24, 2022
    @kumaraditya303 kumaraditya303 added 3.11 only security fixes 3.10 only security fixes 3.12 bugs and security fixes and removed 3.9 only security fixes 3.7 (EOL) end of life labels Dec 2, 2022
    @ndvbd
    Copy link

    ndvbd commented Dec 5, 2022

    Any solutions? I'm having this too in python 2.7.18...

    @vstinner
    Copy link
    Member

    vstinner commented Dec 6, 2022

    Any solutions? I'm having this too in python 2.7.18...

    This issue is about asyncio which was added to Python 3.4. Moreover, Python 2.7 is no longer supported: you should upgrade to Python 3.11.

    @kumaraditya303
    Copy link
    Contributor

    Duplicate of #85865

    @kumaraditya303 kumaraditya303 marked this as a duplicate of #85865 Apr 19, 2023
    @kumaraditya303 kumaraditya303 closed this as not planned Won't fix, can't repro, duplicate, stale Apr 19, 2023
    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
    Labels
    3.10 only security fixes 3.11 only security fixes 3.12 bugs and security fixes topic-asyncio type-bug An unexpected behavior, bug, or error
    Projects
    Status: Done
    Development

    No branches or pull requests

    6 participants