Skip to content

ElementTree.iterparse "leaks" file descriptor when not exhausted #101438

Description

@colesbury

The PR #31696 attempts to fix the "leak" of file descriptors when the iterator is not exhausted. That PR fixes the warning, but not the underlying issue that the files aren't closed until the next tracing garbage collection cycle.

Note that there isn't truly a leak of file descriptors. The file descriptors are eventually closed when the file object is finalized (at cyclic garbage collection). The point of the ResourceWarning (in my understanding) is that waiting until the next garbage collection cycle means that you may temporarily have a lot of unwanted open file descriptors, which could exhaust the global limit or prevent successful writes to those files on Windows.

# run with ulimit -Sn 1000
import xml.etree.ElementTree as ET
import tempfile

import gc
gc.disable()

def run():
    with tempfile.NamedTemporaryFile("w") as f:
        f.write("<document />junk")

        for i in range(10000):
            it = ET.iterparse(f.name)
            del it

run()

On my system, after lowering the file descriptor limit to 1000 (via ulimit -Sn 1000) I get:

OSError: [Errno 24] Too many open files: '/tmp/tmpwwmd9gp6'

Linked PRs

Activity

  1. ericvsmith commented on Jan 30, 2023

    @ericvsmith
    Member

    I think this would require something like PEP 533 “Deterministic cleanup for iterators“ to solve.

  2. colesbury commented on Jan 30, 2023

    @colesbury
    ContributorAuthor

    In this case, I think you can fix it just by avoiding reference cycles that involve the generator. Below is an example refactoring of iterparse.

    def iterparse(source, events=None, parser=None):
        # Use the internal, undocumented _parser argument for now; When the
        # parser argument of iterparse is removed, this can be killed.
        pullparser = XMLPullParser(events=events, _parser=parser)
        _root = None
    
        def iterator(source):
            nonlocal _root
            close_source = False
            try:
                if not hasattr(source, "read"):
                    source = open(source, "rb")
                    close_source = True
                yield None
                while True:
                    yield from pullparser.read_events()
                    # load event buffer
                    data = source.read(16 * 1024)
                    if not data:
                        break
                    pullparser.feed(data)
                root = pullparser._close_and_return_root()
                yield from pullparser.read_events()
                _root = root
            finally:
                if close_source:
                    source.close()
    
        class IterParseIterator(collections.abc.Iterator):
            def __init__(self, source):
                self.it = iterator(source)
            def __next__(self):
                return next(self.it)
            @property
            def root(self):
                nonlocal _root
                return _root
        it = IterParseIterator(source)
        del iterator, IterParseIterator
    
        next(it)
        return it

    This avoids two reference cycles that captured the generator created by iterator(source):

    1. Classes naturally form reference cycles, so move iterator(source) to the instance instead of a class attribute
    2. Move _root up so that iterator(source) doesn't refer to the instance it.

    I'm not sure about a few things including:

    • How do you test this?
    • Is it.root important? It's not documented. Is it only for testing?

    EDIT: the problem is not capturing of the file descriptor, but the generator iterator(source).

  3. rhettinger commented on Jan 30, 2023

    @rhettinger
    Contributor

    @colesbury Doe the issue persist if you add gc.collect() after the del it?

  4. colesbury commented on Jan 30, 2023

    @colesbury
    ContributorAuthor

    @rhettinger everything is cleaned up properly once the garbage collector runs. So adding a gc.collect() after the del it would collect the generator and close the file descriptor.

  5. added a commit that references this issue on Apr 19, 2023
  6. added
    stdlibStandard Library Python modules in the Lib/ directory
    on Nov 26, 2023
  7. added a commit that references this issue on Jan 18, 2024
  8. added a commit that references this issue on Jan 23, 2024
  9. added 2 commits that reference this issue on Jan 23, 2024
  10. added 2 commits that reference this issue on Jan 23, 2024
  11. added a commit that references this issue on Feb 11, 2024
  12. added a commit that references this issue on Sep 2, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    3.11only security fixes3.12only security fixes3.13only security fixesstdlibStandard Library Python modules in the Lib/ directorytype-bugAn unexpected behavior, bug, or error

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions