gh-153056: Fix a data race compiling the string.Template pattern in free-threading builds#153057
gh-153056: Fix a data race compiling the string.Template pattern in free-threading builds#153057tonghuaroot wants to merge 4 commits into
Conversation
…n in free-threading builds Template compiles its substitution pattern lazily and caches it on the class. On the free-threaded build two concurrent first uses could race: a thread that observed the pattern another thread had just compiled would try to recompile it, and re.compile() rejects flags on an already-compiled pattern, raising a spurious ValueError. Return the already-compiled pattern instead. As a side effect, a subclass that supplies an already-compiled pattern now works too; previously it raised the same ValueError at class definition.
|
I am not very fond of that but I don't see a better way of doing it except by allowing flags on a compiled pattern assuming those flags are the same. Are the flags retained by a pattern object or not? if they are not, I don't see another solution than the proposed one. |
|
It's actually a little more complicated because of what the docs actually say:
"regular expression object" implies a compiled regular expression, but that actually breaks in exactly the way you've identified in your "non-free-threading" comments and test. So the very thing that the docs tell you to do is unsupported, and I believe nothing actually tests that; all the tests set So your fix is correct on that basis alone. Bonus that it fixes the FT race condition! I'll take a closer look at the diff exactly, but since you're here, would you mind adding an update to the docs to describe that |
warsaw
left a comment
There was a problem hiding this comment.
Thanks for finding and fixing this! As mentioned, I believe this is a legitimate bug regardless of the FT race, so it's good to fix and back port. Please do add a doc fix and ping me for a re-review when you're ready, as the bot will instruct.
| import re # deferred import, for performance | ||
|
|
||
| pattern = cls.__dict__.get('pattern', _TemplatePattern) | ||
| if isinstance(pattern, re.Pattern): |
There was a problem hiding this comment.
I think I'd add a comment here just above this line, something to the effect of:
# `pattern` can be one of three objects: a `_TemplatePattern` indicating that the descriptor has not yet
# run, a compiled regular expression object as stated in the docs, or a string representing a regular
# expression pattern. If it's an already compiled regular expression object, we can just return that
# immediately. For the former two cases, we have to compile the pattern and cache it back on the
# attribute.| def test_precompiled_pattern(self): | ||
| # A subclass may supply an already-compiled pattern; it must be reused, | ||
| # not recompiled (re.compile() rejects flags on a compiled pattern). | ||
| import re |
There was a problem hiding this comment.
It'll be nice when we can get rid of all these and just use lazy imports 😄
| @@ -0,0 +1,2 @@ | |||
| Fix a race in :class:`string.Template` where concurrent first use on the | |||
There was a problem hiding this comment.
Also add that this fixes documented behavior that has always been broken? I'm not sure which to emphasize by mentioning first, but I'll leave that to you.
|
A Python core developer has requested some changes be made to your pull request before we can consider merging it. If you could please address their requests along with any other requests in other reviews from core developers that would be appreciated. Once you have made the requested changes, please leave a comment on this pull request containing the phrase |
|
Updated the docs for I have made the requested changes; please review again |
|
Thanks for making the requested changes! @warsaw: please review the changes made to this pull request. |
|
Also addressed the inline comments: added the comment on the three states of I have made the requested changes; please review again |
|
Thanks for making the requested changes! @warsaw: please review the changes made to this pull request. |
Documentation build overview
|
Fixes the free-threading data race in the linked issue.
Template._compile_pattern()could read a pattern that another thread had just compiled and cached, then hand that already-compiledre.Patterntore.compile(pattern, cls.flags | re.VERBOSE), which rejects a flags argument on a compiled pattern and raises a spuriousValueError. The fix returns the already-compiled pattern instead, which is idempotent and lock-free.Testing
Lib/test/test_free_threading/test_string_template_race.pyraces the lazy compile across many threads withthreading_helper.run_concurrently(barrier-synchronized). It uses a throwaway subclass re-armed to the uncompiled sentinel each round, so the sharedstring.Templateclass is never mutated. Without the fix, the first round raises theValueErroron many of the racing threads; with it, 0.Lib/test/test_string/test_string.py::TestTemplate::test_precompiled_patternis a deterministic, non-threaded regression that runs on every build: a subclass supplying an already-compiled pattern raised theValueErrorat class definition before the fix and works after it.Notes
re.Patterntopattern, where it previously raised at class definition. The commit message calls this out as a deliberate consequence rather than leaving it implicit.cls.flags(two threads may both storere.IGNORECASE); it does not affect correctness and is intentionally left out of scope to keep this change single-purpose.