Skip to content
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
633e9e8
add test for memory leakage during deletion of value
Vizonex Jul 27, 2025
8acea81
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 27, 2025
c06df7d
trim memory before checking if memory had been leaked to prevent boug…
Vizonex Jul 28, 2025
22c7d92
trim ram in multidict_pop.py when testing item deletion so that bougu…
Vizonex Jul 28, 2025
a1fe938
add contribution to timeline
Vizonex Jul 28, 2025
4ec1158
merge the two functions together so that mypy will accept it.
Vizonex Jul 28, 2025
2bf1cb7
typo fix
Vizonex Jul 28, 2025
45f4066
see if this works...
Vizonex Jul 30, 2025
7843457
revert
Vizonex Jul 30, 2025
9712cfe
Fixed Memory Leak with deleting and adding values
Vizonex Aug 7, 2025
9fa0bfc
fix crashing and ensure multidicts can actually clear entries
Vizonex Aug 7, 2025
1059ec8
remove malloc-trim there is not need for it here
Vizonex Aug 7, 2025
9f2903c
_md_refdump is not version compatable so I'll comment it out for now...
Vizonex Aug 7, 2025
ecb03e7
increase timeout so that workflow doesn't cut off
Vizonex Aug 7, 2025
fac4442
increase fault handler timeout so that debug versions can finish in time
Vizonex Aug 7, 2025
bd37797
revert this was the worng timeout
Vizonex Aug 7, 2025
4e67d13
lessen amount of time memory leak takes to test
Vizonex Aug 7, 2025
c500173
higher amount but print how big it was so we know where it should be …
Vizonex Aug 7, 2025
ad1be49
make sure code-coverage does its job and fix up md_clear so that all …
Vizonex Aug 8, 2025
e374329
label that we bugfixed a problem
Vizonex Aug 9, 2025
e1c5f14
cleanup debug artifacts
Vizonex Aug 10, 2025
fb25e16
Apply suggestions from code review
asvetlov Aug 11, 2025
0ccaf30
Apply suggestions from code review
asvetlov Aug 11, 2025
232806b
Apply suggestions from code review
asvetlov Aug 11, 2025
3a08bfc
Update multidict/_multilib/hashtable.h
asvetlov Aug 11, 2025
53b7bb6
Update multidict/_multilib/hashtable.h
asvetlov Aug 11, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ repos:
- pytest_codspeed
- Sphinx >= 5.3.0
- sphinxcontrib-spelling
- types-psutil
- psutil
args:
- --python-version=3.13
- --txt-report=.tox/.tmp/.mypy/python-3.13
Expand All @@ -146,6 +148,8 @@ repos:
- pytest_codspeed
- Sphinx >= 5.3.0
- sphinxcontrib-spelling
- types-psutil
- psutil
args:
- --python-version=3.11
- --txt-report=.tox/.tmp/.mypy/python-3.11
Expand Down
2 changes: 2 additions & 0 deletions CHANGES/1233.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fix ``MutliDict`` & ``CIMultiDict`` memory leak when deleting values or clearing them
-- by :user:`Vizonex`
2 changes: 2 additions & 0 deletions CHANGES/1233.contrib.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Added memory leak test for popping or deleting attributes from a multidict to prevent future issues or bogus claims.
-- by :user:`Vizonex`
2 changes: 1 addition & 1 deletion multidict/_multidict.c
Original file line number Diff line number Diff line change
Expand Up @@ -674,7 +674,7 @@ multidict_popone(MultiDictObject *self, PyObject *const *args,
if (md_pop_one(self, key, &ret_val) < 0) {
return NULL;
}

ASSERT_CONSISTENT(self, false);
if (ret_val == NULL) {
if (_default != NULL) {
Expand Down
19 changes: 9 additions & 10 deletions multidict/_multilib/hashtable.h
Original file line number Diff line number Diff line change
Expand Up @@ -1877,21 +1877,20 @@ md_traverse(MultiDictObject *md, visitproc visit, void *arg)
static inline int
md_clear(MultiDictObject *md)
{
if (md->used == 0) {
if ((md->keys == &empty_htkeys) || (md->state == NULL)) {
return 0;
}
md->version = NEXT_VERSION(md->state);

entry_t *entries = htkeys_entries(md->keys);
for (Py_ssize_t pos = 0; pos < md->keys->nentries; pos++) {
entry_t *entry = entries + pos;
if (entry->identity != NULL) {
Py_CLEAR(entry->identity);
Py_CLEAR(entry->key);
Py_CLEAR(entry->value);
}
// Py_CLEAR has null checks of it's own making it easier to free.
Py_CLEAR(entry->identity);
Py_CLEAR(entry->key);
Py_CLEAR(entry->value);
}

md->used = 0;
if (md->keys != &empty_htkeys) {
htkeys_free(md->keys);
Expand All @@ -1910,7 +1909,7 @@ _md_check_consistency(MultiDictObject *md, bool update)

#define CHECK(expr) assert(expr)
// do { if (!(expr)) { assert(0 && Py_STRINGIFY(expr)); } } while (0)

htkeys_t *keys = md->keys;
CHECK(keys != NULL);
Py_ssize_t calc_usable = USABLE_FRACTION(htkeys_nslots(keys));
Expand All @@ -1929,7 +1928,7 @@ _md_check_consistency(MultiDictObject *md, bool update)
Py_ssize_t ix = htkeys_get_index(keys, i);
CHECK(DKIX_DUMMY <= ix && ix <= calc_usable);
}

entry_t *entries = htkeys_entries(keys);
for (Py_ssize_t i = 0; i < calc_usable; i++) {
entry_t *entry = &entries[i];
Expand Down
2 changes: 1 addition & 1 deletion pytest.ini
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ doctest_optionflags = ALLOW_UNICODE ELLIPSIS
# Marks tests with an empty parameterset as xfail(run=False)
empty_parameter_set_mark = xfail

faulthandler_timeout = 60
faulthandler_timeout = 90

filterwarnings =
error
Expand Down
1 change: 1 addition & 0 deletions requirements/pytest.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ objgraph==3.6.2
pytest==8.4.0
pytest-codspeed==3.2.0
pytest-cov==6.1.0
psutil==7.0.0
80 changes: 80 additions & 0 deletions tests/isolated/multidict_pop.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Test for memory leaks surrounding deletion of values or
# bad cleanups.
# SEE: https://github.com/aio-libs/multidict/issues/1232
# We want to make sure that bad predictions or bougus claims
# of memory leaks can be prevented in the future.

import gc
import psutil
import os
from multidict import MultiDict


def trim_ram() -> None:
"""Forces python garbage collection."""
gc.collect()


process = psutil.Process(os.getpid())


def get_memory_usage() -> int:
memory_info = process.memory_info()
return memory_info.rss / (1024 * 1024) # type: ignore[no-any-return]


keys = [f"X-Any-{i}" for i in range(100)]
headers = {key: key * 2 for key in keys}


def check_for_leak() -> None:
trim_ram()
usage = get_memory_usage()
assert usage < 50, f"Memory leaked at: {usage} MB"


def _test_pop() -> None:
for _ in range(10):
for _ in range(100):
result = MultiDict(headers)
for k in keys:
result.pop(k)
check_for_leak()


def _test_popall() -> None:
for _ in range(10):
for _ in range(100):
result = MultiDict(headers)
for k in keys:
result.popall(k)
check_for_leak()


def _test_popone() -> None:
for _ in range(10):
for _ in range(100):
result = MultiDict(headers)
for k in keys:
result.popone(k)
check_for_leak()


def _test_del() -> None:
for _ in range(10):
for _ in range(100):
result = MultiDict(headers)
for k in keys:
del result[k]
check_for_leak()


def _run_isolated_case() -> None:
_test_pop()
_test_popall()
_test_popone()
_test_del()


if __name__ == "__main__":
_run_isolated_case()
1 change: 1 addition & 0 deletions tests/test_leaks.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"multidict_extend_multidict.py",
"multidict_extend_tuple.py",
"multidict_update_multidict.py",
"multidict_pop.py",
),
)
@pytest.mark.leaks
Expand Down
Loading