Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

[Engine] the sampler and the HIP properties probe never call close() - ResourceWarning lines on every Minimal serving runtime run

Open Beginner friendly
#4,654 0 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 3 days

Nobody has claimed this yet.

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
76/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
python
Domain
backend

Research direction

Start with the six open() sites named in serve/telemetry.py and serve/server.py, then run python -X dev -m unittest discover -s serve -p 'test_*.py' to see the warnings. Close each file after reading while preserving the existing results and exception handling. Done when the suite passes and the six readers no longer emit ResourceWarning lines.

Written by the indexing model from the issue text.

Description

Problem and result. The Minimal serving runtime job prints the same ResourceWarning lines on
every run. Six open() calls sit inside an expression, so nothing ever calls close() on them:

serve/telemetry.py:495: ResourceWarning: unclosed file <_io.TextIOWrapper name='/proc/stat' mode='r' encoding='UTF-8'>
serve/telemetry.py:478: ResourceWarning: unclosed file <_io.TextIOWrapper name='/proc/cpuinfo' ...>
serve/telemetry.py:458: ResourceWarning: unclosed file <... physical_package_id ...>      (one per CPU)
serve/server.py:3364 / :3401: ResourceWarning: unclosed file <... /tmp/tmpXXXX/0/properties ...>

The patch closes all six and changes no answer.

Where they are (line numbers at v0.2.8):

  • serve/telemetry.py:458 _cpu_topology() - one leaked handle per cpu*/topology/physical_package_id
    glob hit. A 4-CPU runner prints four lines; this 16-CPU box leaks sixteen per call. Read from the
    code, not measured as a cost: _cpu_topology() is called twice in the same static dict
    (sockets and numa_nodes), so every CPU directory is read twice at each sampler start.
  • serve/telemetry.py:476 _cpu_name() - the /proc/cpuinfo scan. It reports at 478, the return
    inside the loop, not at the open(): the loop's iterator still holds the file when the frame is
    released. Worth knowing, because the line number in a report points away from the line to fix.
  • serve/telemetry.py:495 _CpuRamFallback._times() - /proc/stat. sample() reaches the fallback
    once a second when psutil is absent (serve/telemetry.py:633), and _CpuRamFallback.cpu() calls
    _times() again, so a second of sampling is two /proc/stat reads plus one /proc/meminfo.
  • serve/telemetry.py:522 _CpuRamFallback.ram() - /proc/meminfo. Same defect, and it never
    printed in the log above: the log understates the set by one reader.
  • serve/server.py:3364 hip_speed_scores() and serve/server.py:3401 hip_gpu_archs() - the
    properties probe, both copies. In the test job these read a faked sysfs root
    (/tmp/tmpXXXX/0/properties in the log), which is why they print on a machine with no AMD card.

Measured on this box (Arch Linux, CPython 3.14.8, AMD Ryzen 7 9800X3D, 16 logical CPUs; both
versions loaded into one process so the reads are back to back against the same live /proc, and
the same probe re-run against the pure v0.2.8 tag in a throwaway worktree):

  • 40 rounds of the five telemetry readers with warnings captured: 800 before, 0 after. The 800
    is arithmetic rather than a guess: 40 rounds x 20 files, where 20 = 16 per-CPU topology files +
    cpuinfo + stat + stat again through cpu() + meminfo. On the pure tag: 800 as released, 0 with the
    patch applied, and git apply --check lands clean on upstream's bytes.
  • Same answers before and after: _cpu_name() returns AMD Ryzen 7 9800X3D 8-Core Processor,
    _cpu_topology() returns (1, 1), cpu() returns 0.0, ram() returns
    (119748005888, 134679015424).
  • It is not a descriptor leak on CPython, and I want to be precise about that. 1,000 rounds of
    _times() + ram() with no collection at all hold +0 descriptors on /proc/self/fd, before
    and after: CPython decrefs the temporary at the end of the statement and the descriptor goes with
    it. The warnings do not need the collector either - 40 rounds with gc.collect() and 40 rounds
    without it both give 800. So ResourceWarning here is the interpreter's "nobody called close()"
    complaint, i.e. hygiene and log noise, not fd pressure. On an interpreter without prompt
    deallocation the same six would be a real leak; this box runs CPython only, so that is not tested.
  • The arms that reach these paths - serve.test_parallel, test_replicas, test_responses,
    test_security, test_server, test_session_save_retry, test_gpu_roles_server, test_vram:
    Ran 504 tests ... OK (skipped=1) in 202.9 s.
    • The full serve suite on the patched tree, run the way the job runs it (unittest discover -s serve -p 'test_*.py'): Ran 1589 tests ... OK (skipped=17) in 464.2 s, and its log carries 5 unclosed file lines where the same command on the unpatched tree carried 121 - 108 of them on a /proc or /sys path. A 4-CPU runner prints 4 of those, which is what the quoted log shows; a 16-CPU box prints 108. The 5 that remain are 4 serve/lifeline.py:59 pipe descriptors and one stdlib frame, none of them a site this patch touches.

The patch. Six with-statements, nothing else. Sites that had no encoding argument were left
without one on purpose, so this is a lifetime change only and not a locale change; both
try/except (OSError, ValueError) shapes still catch what they caught, and a missing properties
file behaves as before.

diff --git a/serve/server.py b/serve/server.py
index 43e973f8..08a720f0 100644
--- a/serve/server.py
+++ b/serve/server.py
@@ -4055,7 +4055,8 @@ def hip_speed_scores(indices: list[int], root: str = "/sys/class/kfd/kfd/topolog
     cards = []
     for n in nodes:
         try:
-            props = dict(line.split() for line in open(os.path.join(root, str(n), "properties")) if len(line.split()) == 2)
+            with open(os.path.join(root, str(n), "properties")) as fh:
+                props = dict(line.split() for line in fh if len(line.split()) == 2)
             simd, clk = int(props.get("simd_count", 0)), int(props.get("max_engine_clk_fcompute", 0))
         except (OSError, ValueError):
             continue
@@ -4092,7 +4093,8 @@ def hip_gpu_archs(root: str = "/sys/class/kfd/kfd/topology/nodes") -> dict[int,
     out: dict[int, str] = {}
     for n in nodes:
         try:
-            props = dict(line.split() for line in open(os.path.join(root, str(n), "properties")) if len(line.split()) == 2)
+            with open(os.path.join(root, str(n), "properties")) as fh:
+                props = dict(line.split() for line in fh if len(line.split()) == 2)
             ver, simd = int(props.get("gfx_target_version", 0)), int(props.get("simd_count", 0))
         except (OSError, ValueError):
             continue
diff --git a/serve/telemetry.py b/serve/telemetry.py
index f56da090..8722f62f 100644
--- a/serve/telemetry.py
+++ b/serve/telemetry.py
@@ -455,7 +455,8 @@ def _cpu_topology(root: str = "/sys/devices/system"):
         packages = set()
         for f in glob.glob(os.path.join(root, "cpu", "cpu[0-9]*", "topology", "physical_package_id")):
             try:
-                packages.add(open(f, encoding="utf-8").read().strip())
+                with open(f, encoding="utf-8") as fh:
+                    packages.add(fh.read().strip())
             except OSError:
                 continue
         nodes = len(glob.glob(os.path.join(root, "node", "node[0-9]*")))
@@ -473,9 +474,10 @@ def _cpu_name():
         except OSError:
             pass
     elif os.path.exists("/proc/cpuinfo"):
-        for line in open("/proc/cpuinfo", encoding="utf-8", errors="replace"):
-            if line.startswith("model name"):
-                return line.split(":", 1)[1].strip()
+        with open("/proc/cpuinfo", encoding="utf-8", errors="replace") as fh:
+            for line in fh:
+                if line.startswith("model name"):
+                    return line.split(":", 1)[1].strip()
     return platform.processor() or None
 
 
@@ -492,7 +494,8 @@ class _CpuRamFallback:
                 return idle.value, kern.value + user.value           # kernel time includes idle
             return None
         try:
-            f = [int(x) for x in open("/proc/stat").readline().split()[1:]]
+            with open("/proc/stat") as fh:
+                f = [int(x) for x in fh.readline().split()[1:]]
             return f[3] + f[4], sum(f)
         except (OSError, ValueError):
             return None
@@ -519,7 +522,8 @@ class _CpuRamFallback:
                 return m.ullTotalPhys - m.ullAvailPhys, m.ullTotalPhys
             return None, None
         try:
-            info = dict(line.split(":", 1) for line in open("/proc/meminfo"))
+            with open("/proc/meminfo") as fh:
+                info = dict(line.split(":", 1) for line in fh)
             total = int(info["MemTotal"].split()[0]) * 1024
             avail = int(info["MemAvailable"].split()[0]) * 1024
             return total - avail, total

Limits, and what this does not claim.

  • Windows and macOS were not tested. These are the /proc and /sys branches; the registry and
    ctypes arms above them and the macOS sysctl arm are untouched.
  • The serve/lifeline.py:59 warning that prints in the same job names an unnamed descriptor
    (fd 8, 9, 10 - pipes) and points at whatever frame released it. Different shape, not claimed here.
  • #3590 already fixed the watcher's start_of() handle in this same family. This is the sampler and
    the HIP probe, which that thread did not cover - its own body says "the sampler half has no
    existing thread", and ResourceWarning appears in no issue title (search: repo:Niko1221/Strata ResourceWarning in:title returns 0).

How to see it. python -X dev -m unittest discover -s serve -p 'test_*.py' prints them; or
40 rounds of the five readers with warnings.simplefilter("always") and count
ResourceWarning.

— posted from the Strata-Mnemosyne fork (memory-core, merged to v0.2.8 today). Numbers are ours,
produced on the hardware named above.

Dominant language
C++
Stars
11.6k
Forks
1k
PR merge metrics
No merged PRs in 30d

Getting set up

This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from Niko1221/Strata

All issues in Niko1221/Strata

Similar issues

More C++ issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.