diff --git a/CHANGES/1328.bugfix.rst b/CHANGES/1328.bugfix.rst new file mode 100644 index 000000000..a08279333 --- /dev/null +++ b/CHANGES/1328.bugfix.rst @@ -0,0 +1,3 @@ +Fixed race when calling for a string representation of a :py:class:`~multidict.MultiDict` object. + +-- by :user:`Vizonex` \ No newline at end of file diff --git a/multidict/_multilib/hashtable.h b/multidict/_multilib/hashtable.h index f2ba9868a..4e76283e0 100644 --- a/multidict/_multilib/hashtable.h +++ b/multidict/_multilib/hashtable.h @@ -1769,6 +1769,7 @@ md_repr(MultiDictObject *md, PyObject *name, bool show_keys, bool show_values) PyObject *key = NULL; PyObject *value = NULL; + Py_BEGIN_CRITICAL_SECTION(md); bool comma = false; uint64_t version = md->version; @@ -1850,6 +1851,7 @@ md_repr(MultiDictObject *md, PyObject *name, bool show_keys, bool show_values) Py_CLEAR(key); Py_CLEAR(value); PyUnicodeWriter_Discard(writer); + Py_END_CRITICAL_SECTION(); return NULL; } diff --git a/tests/isolated/multidict_repr_ft.py b/tests/isolated/multidict_repr_ft.py new file mode 100644 index 000000000..b04b84311 --- /dev/null +++ b/tests/isolated/multidict_repr_ft.py @@ -0,0 +1,37 @@ +import os +import subprocess +import sys +import sysconfig +import textwrap + +FREETHREADED = bool(sysconfig.get_config_var("Py_GIL_DISABLED")) + + +if __name__ == "__main__" and FREETHREADED: + child = textwrap.dedent(""" + import signal, threading, time + signal.alarm(20) + from multidict import MultiDict + md = MultiDict([(f"k{i}", i) for i in range(50)]) + errors = [] + stop = threading.Event() + def repr_loop(): + while not stop.is_set(): + try: repr(md) + except Exception as e: errors.append(type(e).__name__) + def mutate_loop(): + i = 0 + while not stop.is_set(): md.add(f"m{i}", i); i += 1 + ts = [threading.Thread(target=repr_loop, daemon=True) for _ in range(2)] + ts += [threading.Thread(target=mutate_loop, daemon=True) for _ in range(2)] + for t in ts: t.start() + time.sleep(0.3); stop.set(); time.sleep(0.05) + print(f"repr errors: {len(errors)}") + """) + subprocess.run( + [sys.executable, "-c", child], + env={**os.environ, "PYTHON_GIL": "0"}, + capture_output=True, + timeout=60, + check=True, + ) diff --git a/tests/test_leaks.py b/tests/test_leaks.py index c39abe245..660fd0f43 100644 --- a/tests/test_leaks.py +++ b/tests/test_leaks.py @@ -11,6 +11,7 @@ @pytest.mark.parametrize( ("script"), ( + "multidict_repr_ft.py", "multidict_extend_dict.py", "multidict_extend_multidict.py", "multidict_extend_tuple.py",