Skip to content

Commit 820631f

Browse files
authored
Fix Memory leaks and add tests to prevent memory leaks during md_clear from passing (#1233)
Related issue number: #1232
1 parent 00e3803 commit 820631f

8 files changed

Lines changed: 92 additions & 2 deletions

File tree

.pre-commit-config.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,8 @@ repos:
130130
- pytest_codspeed
131131
- Sphinx >= 5.3.0
132132
- sphinxcontrib-spelling
133+
- types-psutil
134+
- psutil
133135
args:
134136
- --python-version=3.13
135137
- --txt-report=.tox/.tmp/.mypy/python-3.13
@@ -146,6 +148,8 @@ repos:
146148
- pytest_codspeed
147149
- Sphinx >= 5.3.0
148150
- sphinxcontrib-spelling
151+
- types-psutil
152+
- psutil
149153
args:
150154
- --python-version=3.11
151155
- --txt-report=.tox/.tmp/.mypy/python-3.11

CHANGES/1233.bugfix.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix ``MutliDict`` & ``CIMultiDict`` memory leak when deleting values or clearing them
2+
-- by :user:`Vizonex`

CHANGES/1233.contrib.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Added memory leak test for popping or deleting attributes from a multidict to prevent future issues or bogus claims.
2+
-- by :user:`Vizonex`

multidict/_multilib/hashtable.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1877,7 +1877,7 @@ md_traverse(MultiDictObject *md, visitproc visit, void *arg)
18771877
static inline int
18781878
md_clear(MultiDictObject *md)
18791879
{
1880-
if (md->used == 0) {
1880+
if (md->keys == NULL || md->keys == &empty_htkeys) {
18811881
return 0;
18821882
}
18831883
md->version = NEXT_VERSION(md->state);

pytest.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ doctest_optionflags = ALLOW_UNICODE ELLIPSIS
4141
# Marks tests with an empty parameterset as xfail(run=False)
4242
empty_parameter_set_mark = xfail
4343

44-
faulthandler_timeout = 60
44+
faulthandler_timeout = 90
4545

4646
filterwarnings =
4747
error

requirements/pytest.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ objgraph==3.6.2
22
pytest==8.4.0
33
pytest-codspeed==3.2.0
44
pytest-cov==6.1.0
5+
psutil==7.0.0

tests/isolated/multidict_pop.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# Test for memory leaks surrounding deletion of values or
2+
# bad cleanups.
3+
# SEE: https://github.com/aio-libs/multidict/issues/1232
4+
# We want to make sure that bad predictions or bougus claims
5+
# of memory leaks can be prevented in the future.
6+
7+
import gc
8+
import psutil
9+
import os
10+
from multidict import MultiDict
11+
12+
13+
def trim_ram() -> None:
14+
"""Forces python garbage collection."""
15+
gc.collect()
16+
17+
18+
process = psutil.Process(os.getpid())
19+
20+
21+
def get_memory_usage() -> int:
22+
memory_info = process.memory_info()
23+
return memory_info.rss / (1024 * 1024) # type: ignore[no-any-return]
24+
25+
26+
keys = [f"X-Any-{i}" for i in range(100)]
27+
headers = {key: key * 2 for key in keys}
28+
29+
30+
def check_for_leak() -> None:
31+
trim_ram()
32+
usage = get_memory_usage()
33+
assert usage < 50, f"Memory leaked at: {usage} MB"
34+
35+
36+
def _test_pop() -> None:
37+
for _ in range(10):
38+
for _ in range(100):
39+
result = MultiDict(headers)
40+
for k in keys:
41+
result.pop(k)
42+
check_for_leak()
43+
44+
45+
def _test_popall() -> None:
46+
for _ in range(10):
47+
for _ in range(100):
48+
result = MultiDict(headers)
49+
for k in keys:
50+
result.popall(k)
51+
check_for_leak()
52+
53+
54+
def _test_popone() -> None:
55+
for _ in range(10):
56+
for _ in range(100):
57+
result = MultiDict(headers)
58+
for k in keys:
59+
result.popone(k)
60+
check_for_leak()
61+
62+
63+
def _test_del() -> None:
64+
for _ in range(10):
65+
for _ in range(100):
66+
result = MultiDict(headers)
67+
for k in keys:
68+
del result[k]
69+
check_for_leak()
70+
71+
72+
def _run_isolated_case() -> None:
73+
_test_pop()
74+
_test_popall()
75+
_test_popone()
76+
_test_del()
77+
78+
79+
if __name__ == "__main__":
80+
_run_isolated_case()

tests/test_leaks.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"multidict_extend_multidict.py",
1616
"multidict_extend_tuple.py",
1717
"multidict_update_multidict.py",
18+
"multidict_pop.py",
1819
),
1920
)
2021
@pytest.mark.leaks

0 commit comments

Comments
 (0)