Skip to content

Fix __repr__ race (Free-Threaded)#1328

Open
Vizonex wants to merge 14 commits into
aio-libs:masterfrom
Vizonex:md-repr-ft
Open

Fix __repr__ race (Free-Threaded)#1328
Vizonex wants to merge 14 commits into
aio-libs:masterfrom
Vizonex:md-repr-ft

Conversation

@Vizonex

@Vizonex Vizonex commented Apr 28, 2026

Copy link
Copy Markdown
Member

What do these changes do?

Fixes another race condition seen in https://gist.github.com/devdanzin/fa00e03ef041c030660f083116a7e1b6#7-repr-race--md_repr Item 7.

Are there changes in behavior for the user?

Related issue number

#1321

Checklist

  • I think the code is well written
  • Unit tests for the changes exist
  • Documentation reflects the changes

@Vizonex
Vizonex requested a review from asvetlov as a code owner April 28, 2026 20:34
@Vizonex
Vizonex requested a review from webknjaz as a code owner April 28, 2026 20:35
@psf-chronographer psf-chronographer Bot added the bot:chronographer:provided There is a change note present in this PR label Apr 28, 2026
@Vizonex Vizonex changed the title fix __repr__ race (Free-Threaded) Fix __repr__ race (Free-Threaded) Apr 28, 2026
@codspeed-hq

codspeed-hq Bot commented Apr 28, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 242 untouched benchmarks


Comparing Vizonex:md-repr-ft (3093e13) with master (e71a593)

Open in CodSpeed

@webknjaz webknjaz left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Structural comments.

Comment thread CHANGES/1328.bugfix.rst
Comment thread CHANGES/1328.bugfix.rst Outdated
import sysconfig
import textwrap

FREETHREADED = bool(sysconfig.get_config_var("Py_GIL_DISABLED"))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be moved to the skipif mark in the pytest param.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do also since we seem to have a ton of bugs to patch. I was thinking about a new module called test_free_threaded.py when #1327 is merged to just have both of them in a new file.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's see if @Dreamsorcerer and @bdraco have opinions on this, then.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good. :)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Surely the tests should pass on regular versions of Python?

Vizonex and others added 3 commits April 28, 2026 15:54
Co-authored-by: 🇺🇦 Sviatoslav Sydorenko (Святослав Сидоренко) <wk.cvs.github@sydorenko.org.ua>
Comment thread CHANGES/1328.bugfix.rst


if __name__ == "__main__" and FREETHREADED:
child = textwrap.dedent("""

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd also get rid of the double-subprocess indirectly here. If we want to invoke a bunch of Python code with different Python flags, we can put those flags into the test parametrization. Additionally, a chunk of Python in a string is not lintable, and the coverage is not getting measured. It should definitely live in a Python module as a first-class citizen.

cc @Dreamsorcerer

Vizonex and others added 4 commits April 29, 2026 20:45
Co-authored-by: 🇺🇦 Sviatoslav Sydorenko (Святослав Сидоренко) <wk.cvs.github@sydorenko.org.ua>
Comment thread CHANGES/1328.bugfix.rst Outdated
Co-authored-by: 🇺🇦 Sviatoslav Sydorenko (Святослав Сидоренко) <wk.cvs.github@sydorenko.org.ua>
@Vizonex Vizonex added the free-threading FT-related discussion, issue or PR label May 1, 2026
@devdanzin

Copy link
Copy Markdown

Thanks for tackling this @Vizonex. I think there's a lock-release bug in the current form, though: the critical section is opened at the top of md_repr, but Py_END_CRITICAL_SECTION() is only in the fail: block. md_repr has three returns that don't go through fail: — the success return return PyUnicodeWriter_Finish(writer);, the "MultiDict changed during iteration" return NULL;, and the early if (writer == NULL) return NULL; — so on the common (successful) path the section is entered and never ended, which leaves md locked and unbalances the thread's critical-section stack.

On a free-threaded build that should deadlock as soon as the same object is repr()'d a second time (the first repr() never released the lock):

# free-threaded build (Py_GIL_DISABLED), single thread is enough:
from multidict import MultiDict
md = MultiDict([("a", 1)])
repr(md)   # success path returns WITHOUT Py_END_CRITICAL_SECTION -> md stays locked
repr(md)   # re-enters Py_BEGIN_CRITICAL_SECTION(md) on the same thread -> hangs

If CI is green it's worth checking that tests/isolated/multidict_repr_ft.py actually re-reprs the same object after a successful one — the loop should hit this. A single exit, or a Py_END_CRITICAL_SECTION() before each return, fixes it. Two smaller notes: the "changed during iteration" path also still return NULLs without PyUnicodeWriter_Discard(writer), so it leaks the writer (a separate, pre-existing bug from the #1306 report); and this may be redundant with #1317, which already wraps every md_repr caller in a critical section — if #1317 lands, md_repr is covered at the call site and this PR might not be needed, so it's worth deciding which one owns it so they don't double up.

@Vizonex

Vizonex commented Jul 23, 2026

Copy link
Copy Markdown
Member Author

Thanks for tackling this @Vizonex. I think there's a lock-release bug in the current form, though: the critical section is opened at the top of md_repr, but Py_END_CRITICAL_SECTION() is only in the fail: block. md_repr has three returns that don't go through fail: — the success return return PyUnicodeWriter_Finish(writer);, the "MultiDict changed during iteration" return NULL;, and the early if (writer == NULL) return NULL; — so on the common (successful) path the section is entered and never ended, which leaves md locked and unbalances the thread's critical-section stack.

On a free-threaded build that should deadlock as soon as the same object is repr()'d a second time (the first repr() never released the lock):

# free-threaded build (Py_GIL_DISABLED), single thread is enough:
from multidict import MultiDict
md = MultiDict([("a", 1)])
repr(md)   # success path returns WITHOUT Py_END_CRITICAL_SECTION -> md stays locked
repr(md)   # re-enters Py_BEGIN_CRITICAL_SECTION(md) on the same thread -> hangs

If CI is green it's worth checking that tests/isolated/multidict_repr_ft.py actually re-reprs the same object after a successful one — the loop should hit this. A single exit, or a Py_END_CRITICAL_SECTION() before each return, fixes it. Two smaller notes: the "changed during iteration" path also still return NULLs without PyUnicodeWriter_Discard(writer), so it leaks the writer (a separate, pre-existing bug from the #1306 report); and this may be redundant with #1317, which already wraps every md_repr caller in a critical section — if #1317 lands, md_repr is covered at the call site and this PR might not be needed, so it's worth deciding which one owns it so they don't double up.

@devdanzin The other maintainers and I are still debating on Free-Threaded support, while we don't necessary see a need for it to be included I'm glad your still keeping us going. I might return to this bug fix if I feel more encouragement though. There was also an idea to move to Rust (Pyo3) to stop a lot of these noticeable memory leaks and other related bugs but the probability of that happing is very small as I see rust hindering the current performance benefits.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bot:chronographer:provided There is a change note present in this PR free-threading FT-related discussion, issue or PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants