Skip to content

gh-136421: Load _datetime static types during interpreter initialization #136583

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Jul 21, 2025

Conversation

ZeroIntensity
Copy link
Member

@ZeroIntensity ZeroIntensity commented Jul 12, 2025

_datetime is a special module, because it's the only non-builtin C extension that contains static types. As such, it would initialize static types in the module's execution function, which can run concurrently. Since static type initialization is not thread-safe, this caused crashes. This fixes it by moving the initialization of _datetime's static types to interpreter startup (where all other static types are initialized), which is already properly protected through other locks.

I have no idea if this will fix the Windows build, but let's hope.
@ZeroIntensity ZeroIntensity requested a review from a team as a code owner July 12, 2025 14:22
@neonene
Copy link
Contributor

neonene commented Jul 12, 2025

My concern is unnecessarily losing portability between the builtin module and the non-builtin one.

This PR avoids a crash not by locking but forcing the main interpreter initialize the _datetime static types, which I think is almost equivalent to the following change in _datetimemodule.c:

PyMODINIT_FUNC
PyInit__datetime(void)
{
+   if (PyDateTime_DateType.tp_subclasses == NULL) {
+       PyInterpreterState *interp = PyInterpreterState_Get();
+       // main interp currently checks a module before subinterp imports
+       assert(_Py_IsMainInterpreter(interp));
+       init_static_types(interp, 0);
+   }
    return PyModuleDef_Init(&datetimemodule);
}

@ZeroIntensity
Copy link
Member Author

Hmm, that feels really hacky.

I wouldn't call the movement of _datetime to static being totally unnecessary, because _datetime exposes a C API via a capsule. If _datetime isn't included with the build of Python, things using the capsule are very likely to crash, so it makes more sense to me to require it. Are there people actually disabling _datetime in their builds?

@neonene
Copy link
Contributor

neonene commented Jul 12, 2025

Hmm, that feels really hacky.

The crash can be avoided even on pure Python level, and managed static extsension types are already special only for _datetime. The C API is likely to be redesigned in the future, which will be a standard way to shared extensions including _datetime.

Are there people actually disabling _datetime in their builds?

Not sure about disabling. On Windows, _datetime is a built-in module that can be switched to a DLL. I know someone who want to use such a DLL as a plugin.

@ZeroIntensity
Copy link
Member Author

Right, there are several ways to fix this that work, but I'm trying to find the fix that is the most correct. Relying on implementation details like tp_subclasses is a little more maintenance if we ever change type initialization, and it also muddies our general rules about how static types should work. I see several options:

  1. Make _datetime static, as it is in this PR. This makes the most sense, but I guess it could compromise compatibility in some niche cases.
  2. Initialize static types in the PyInit_ function. This works, but will probably lead to some other issues because it mixes single and multi-phase initialization, which isn't supposed to be work.
  3. Import _datetime in _interpreters. I really don't like this fix because it won't fix subinterpreters created via the C API.
  4. Import _datetime in new_interpreter. This slows down initialization and is also a bit of a hack.
  5. Make _PyStaticType_InitForExtension thread-safe. I think this will be needlessly complex.
  6. Serialize calls to _PyStaticType_InitForExtension (gh-136421: Fix crash when _datetime is been initialized in multiple sub-interpreters #136422). This works but again hurts the story about static types, and is technically more of a bottleneck.

We have to find a balance between correctness and simplicity, and I'm not yet sure which that is.

@neonene
Copy link
Contributor

neonene commented Jul 12, 2025

  1. Make _datetime static, as it is in this PR. This makes the most sense, but I guess it could compromise compatibility in some niche cases.

This is at least a feature change more than a bug fix.

@ZeroIntensity
Copy link
Member Author

Yeah, you're right. I'd be fine with a bandaid fix for 3.14 if you want to make a PR. The PyInit_ approach probably works best, but I don't fully understand why we need the tp_subclasses check.

@neonene
Copy link
Contributor

neonene commented Jul 12, 2025

Still rough example, but checking tp_subclasses is an alternative to PyType_HasFeature(PyDateTime_DateType, Py_TPFLAGS_READY). I'm not sure which is better on free-threading. Both must be kept in the main interpreter in its life, which means interp_count is at least 1 in the runtime state and the subinterp shutdown cannot invoke fini_static_type() as final.

During the _datetime exec phase, the main interpreter probably needs to skip init_static_types() to not increase interp_count.

@ZeroIntensity
Copy link
Member Author

PyType_HasFeature is definitely the better option. tp_subclasses being NULL is an implementation detail, and free-threading uses non-atomic loads for the type flags (I think it probably uses stop-the-world when modifying). People look to CPython for inspiration on their own C extensions, we should encourage them to use public APIs where possible.

@aisk
Copy link
Member

aisk commented Jul 13, 2025

A small concern is the startup time. Can you do a micro performance benchmark to measure the time cost of ./python -m 'pass' several times before and after this change? My expectation is that this will not add observable time cost.

@neonene
Copy link
Contributor

neonene commented Jul 13, 2025

#136620 is ready for review.

Can you do a micro performance benchmark

I haven't come up with how to measure so far.

Comment on lines +763 to +766
status = _PyDateTime_InitTypes(interp);
if (_PyStatus_EXCEPTION(status)) {
return status;
}
Copy link
Contributor

@neonene neonene Jul 14, 2025

Choose a reason for hiding this comment

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

Suggested change
status = _PyDateTime_InitTypes(interp);
if (_PyStatus_EXCEPTION(status)) {
return status;
}
if (!_Py_IsMainInterpreter(interp)) {
status = _PyDateTime_InitTypes(interp);
if (_PyStatus_EXCEPTION(status)) {
return status;
}
}

Reply from #136620 (comment)

Can you run test_concurrent_initialization() with this change? Based on the crashes that come from the change, I said this PR and my example are "almost equivalent." I'm not sure right now what this PR ensures.

Copy link
Member Author

Choose a reason for hiding this comment

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

Hm, what are you trying to achieve here? This will just break the types for the main interpreter.

Copy link
Contributor

@neonene neonene Jul 14, 2025

Choose a reason for hiding this comment

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

This will just break the types for the main interpreter.

Note that test_concurrent_initialization() does not load the _datetime in the main inter interpreter at all.

Correction: Run the script of the test without running test_datetime.

@ZeroIntensity
Copy link
Member Author

After discussion on DPO, the consensus is that we are okay with making _datetime static, as long as we fix the capsule in the process: https://discuss.python.org/t/datetime-should-be-a-static-module/98857.

@vstinner, would you mind doing a review?

Comment on lines 3654 to 3656
@support.cpython_only
def test_concurrent_initialization_subinterpreter(self):
# Run in a subprocess to ensure we get a clean version of _datetime
Copy link
Contributor

Choose a reason for hiding this comment

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

Please put an anchor instead of the well-known explanation of assert_python_ok(). Also, move the test to ExtensionModuleTests (@support.cpython_only is redundant there). TestDateTime is the place to test the datetime class.

Copy link
Member Author

Choose a reason for hiding this comment

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

Sorry, I'm not sure what you mean by "an anchor".

Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry about that. I meant the gh-issue number or the url.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ok, did both.

print('a', end='')

with InterpreterPoolExecutor() as executor:
for _ in range(8):
Copy link
Contributor

Choose a reason for hiding this comment

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

It's up to you, but range(10) is probably better than range(8) for the Windows x64 CI to reproduce the issue.

Copy link
Member Author

Choose a reason for hiding this comment

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

Eh, I think this is fine. Many other systems have 8 cores, not 10.

Copy link
Contributor

Choose a reason for hiding this comment

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

Okay, but it sounds like you are talking about the max_workers argument rather than the submit count here.

Copy link
Member Author

@ZeroIntensity ZeroIntensity Jul 20, 2025

Choose a reason for hiding this comment

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

max_workers just chooses the number of threads on the system when not set. We could try to submit os.cpu_count() number of futures, but we don't need to overcomplicate this; we just need something that stresses several subinterpreters trying to import _datetime concurrently.

Comment on lines 7338 to 7333

// `&...` is not a constant expression according to a strict reading
// of C standards. Fill tp_base at run-time rather than statically.
// See https://bugs.python.org/issue40777
Copy link
Contributor

Choose a reason for hiding this comment

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

This PR does not address the possible races in PyDateTime_*.tp_base = &PyDateTime_*Type; below, right?

Copy link
Member Author

Choose a reason for hiding this comment

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

I guess not. I don't think there's an easy way to do this here, because atomically storing tp_base will continue to race with all the non-atomic reads elsewhere.

Do we even need to load it at runtime like this? We have other examples of directly storing it in the PyTypeObject structure:

.tp_base = &PyCFunction_Type,

Copy link
Contributor

Choose a reason for hiding this comment

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

It is not needed now because the module is statically linked, that issue happens only with dynamic loaded modules so you can define it statically now.

Copy link
Contributor

Choose a reason for hiding this comment

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

I guess a _Py_IsMainInterPreter() check will suffice in this PR?

Copy link
Member Author

Choose a reason for hiding this comment

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

It is not needed now because the module is statically linked, that issue happens only with dynamic loaded modules so you can define it statically now.

Ah, TIL. That's definitely the best option here then.

I guess a _Py_IsMainInterPreter() check will suffice in this PR?

For what?

Copy link
Contributor

Choose a reason for hiding this comment

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

For ensuring tp_base is set only once after Py_Initialize(). But I missed the Kumar 's comment.

Copy link
Contributor

Choose a reason for hiding this comment

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

Please remove PyDateTime_DateTimeType.tp_base = &PyDateTime_DateType; as well.

@kumaraditya303 kumaraditya303 added the needs backport to 3.14 bugs and security fixes label Jul 21, 2025
@kumaraditya303 kumaraditya303 added the 🔨 test-with-buildbots Test PR w/ buildbots; report in status section label Jul 21, 2025
@bedevere-bot
Copy link

🤖 New build scheduled with the buildbot fleet by @kumaraditya303 for commit 3520514 🤖

Results will be shown at:

https://buildbot.python.org/all/#/grid?branch=refs%2Fpull%2F136583%2Fmerge

If you want to schedule another build, you need to add the 🔨 test-with-buildbots label again.

@bedevere-bot bedevere-bot removed the 🔨 test-with-buildbots Test PR w/ buildbots; report in status section label Jul 21, 2025
@ZeroIntensity
Copy link
Member Author

I'll merge this once buildbots pass, thanks for the reviews everyone!

@ZeroIntensity ZeroIntensity merged commit a109606 into python:main Jul 21, 2025
112 of 118 checks passed
@miss-islington-app
Copy link

Thanks @ZeroIntensity for the PR 🌮🎉.. I'm working now to backport this PR to: 3.14.
🐍🍒⛏🤖

@ZeroIntensity ZeroIntensity deleted the datetime-interp-init branch July 21, 2025 17:47
miss-islington pushed a commit to miss-islington/cpython that referenced this pull request Jul 21, 2025
…tialization (pythonGH-136583)

`_datetime` is a special module, because it's the only non-builtin C extension that contains static types. As such, it would initialize static types in the module's execution function, which can run concurrently. Since static type initialization is not thread-safe, this caused crashes. This fixes it by moving the initialization of `_datetime`'s static types to interpreter startup (where all other static types are initialized), which is already properly protected through other locks.
(cherry picked from commit a10960699a2b3e4e62896331c4f9cfd162ebf440)

Co-authored-by: Peter Bierma <zintensitydev@gmail.com>
@bedevere-app
Copy link

bedevere-app bot commented Jul 21, 2025

GH-136943 is a backport of this pull request to the 3.14 branch.

@bedevere-app bedevere-app bot removed the needs backport to 3.14 bugs and security fixes label Jul 21, 2025
ZeroIntensity added a commit that referenced this pull request Jul 21, 2025
…itialization (GH-136583) (GH-136943)

gh-136421: Load `_datetime` static types during interpreter initialization (GH-136583)

`_datetime` is a special module, because it's the only non-builtin C extension that contains static types. As such, it would initialize static types in the module's execution function, which can run concurrently. Since static type initialization is not thread-safe, this caused crashes. This fixes it by moving the initialization of `_datetime`'s static types to interpreter startup (where all other static types are initialized), which is already properly protected through other locks.
(cherry picked from commit a109606)

Co-authored-by: Peter Bierma <zintensitydev@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants