Skip to content

Commit 8107d5a

Browse files
committed
[GR-61894] Eagerly initialize native thread state.
PullRequest: graalpython/4350
2 parents 0c32235 + 15f82a2 commit 8107d5a

14 files changed

Lines changed: 366 additions & 110 deletions

File tree

graalpython/com.oracle.graal.python.cext/src/capi.c

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2018, 2026, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* The Universal Permissive License (UPL), Version 1.0
@@ -272,9 +272,13 @@ PyObject* _Py_NotImplementedStructReference;
272272
*/
273273
THREAD_LOCAL PyThreadState *tstate_current = NULL;
274274

275-
static void initialize_globals() {
276-
// store the thread state into a thread local variable
277-
tstate_current = GraalPyPrivate_ThreadState_Get(&tstate_current);
275+
PyAPI_FUNC(PyThreadState **) GraalPyPrivate_InitThreadStateCurrent(PyThreadState *tstate) {
276+
tstate_current = tstate;
277+
return &tstate_current;
278+
}
279+
280+
static void initialize_globals(PyThreadState *tstate) {
281+
GraalPyPrivate_InitThreadStateCurrent(tstate);
278282
_Py_NoneStructReference = GraalPyPrivate_None();
279283
_Py_NotImplementedStructReference = GraalPyPrivate_NotImplemented();
280284
_Py_EllipsisObjectReference = GraalPyPrivate_Ellipsis();
@@ -667,7 +671,7 @@ Py_LOCAL_SYMBOL TruffleContext* TRUFFLE_CONTEXT;
667671
*/
668672
Py_LOCAL_SYMBOL int8_t *_graalpy_finalizing = NULL;
669673

670-
PyAPI_FUNC(void) initialize_graal_capi(TruffleEnv* env, void **builtin_closures, GCState *gc) {
674+
PyAPI_FUNC(PyThreadState **) initialize_graal_capi(TruffleEnv* env, void **builtin_closures, GCState *gc, PyThreadState *tstate) {
671675
clock_t t = clock();
672676

673677
if (env) {
@@ -706,7 +710,7 @@ PyAPI_FUNC(void) initialize_graal_capi(TruffleEnv* env, void **builtin_closures,
706710

707711
initialize_builtin_types_and_structs();
708712
// initialize global variables like '_Py_NoneStruct', etc.
709-
initialize_globals();
713+
initialize_globals(tstate);
710714
initialize_exceptions();
711715
initialize_hashes();
712716
initialize_bufferprocs();
@@ -717,6 +721,7 @@ PyAPI_FUNC(void) initialize_graal_capi(TruffleEnv* env, void **builtin_closures,
717721
Py_FileSystemDefaultEncoding = "utf-8"; // strdup(PyUnicode_AsUTF8(GraalPyPrivate_FileSystemDefaultEncoding()));
718722

719723
GraalPyPrivate_Log(PY_TRUFFLE_LOG_FINE, "initialize_graal_capi: %fs", ((double) (clock() - t)) / CLOCKS_PER_SEC);
724+
return &tstate_current;
720725
}
721726

722727
/*

graalpython/com.oracle.graal.python.cext/src/pystate.c

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/* Copyright (c) 2024, 2025, Oracle and/or its affiliates.
1+
/* Copyright (c) 2024, 2026, Oracle and/or its affiliates.
22
* Copyright (C) 1996-2024 Python Software Foundation
33
*
44
* Licensed under the PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
@@ -84,9 +84,16 @@ static inline PyThreadState *
8484
_get_thread_state() {
8585
PyThreadState *ts = tstate_current;
8686
if (UNLIKELY(ts == NULL)) {
87-
ts = GraalPyPrivate_ThreadState_Get(&tstate_current);
88-
tstate_current = ts;
87+
/*
88+
* Very unlikely fallback: this can happen if another thread initializes the C API while
89+
* the current thread is attached to Python but blocked and therefore misses eager
90+
* initialization of its native 'tstate_current' TLS slot.
91+
*/
92+
ts = GraalPyPrivate_ThreadState_Get(&tstate_current);
93+
assert(ts != NULL);
94+
tstate_current = ts;
8995
}
96+
assert(ts != NULL);
9097
return ts;
9198
}
9299

graalpython/com.oracle.graal.python.test/src/tests/test_datetime.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@
3838
# SOFTWARE.
3939

4040
import datetime
41+
import os
42+
import subprocess
43+
import sys
44+
import textwrap
45+
import time
4146
import unittest
4247

4348
class DateTest(unittest.TestCase):
@@ -542,12 +547,29 @@ def test_strptime(self):
542547
actual = datetime.datetime.strptime("+00:00 GMT", "%z %Z")
543548
self.assertEqual(actual.tzinfo.tzname(None), "GMT")
544549

545-
import time
546550
timezone_name = time.localtime().tm_zone
547551
self.assertIsNotNone(timezone_name)
548552
actual = datetime.datetime.strptime(f"+00:00 {timezone_name}", "%z %Z")
549553
self.assertEqual(actual.tzinfo.tzname(None), timezone_name)
550554

555+
if hasattr(time, "tzset") and sys.executable:
556+
proc = subprocess.run(
557+
[sys.executable, "-c", textwrap.dedent("""\
558+
import datetime
559+
import time
560+
561+
time.tzset()
562+
timezone_name = time.localtime().tm_zone
563+
actual = datetime.datetime.strptime(f"+00:00 {timezone_name}", "%z %Z")
564+
assert actual.tzinfo.tzname(None) == timezone_name
565+
""")],
566+
env={**os.environ, "TZ": "Etc/GMT-1"},
567+
stdout=subprocess.PIPE,
568+
stderr=subprocess.PIPE,
569+
text=True,
570+
)
571+
self.assertEqual(proc.returncode, 0, proc.stderr)
572+
551573
# time zone name without utc offset is ignored
552574
actual = datetime.datetime.strptime("UTC", "%Z")
553575
self.assertIsNone(actual.tzinfo)

graalpython/com.oracle.graal.python.test/src/tests/test_socket.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved.
1+
# Copyright (c) 2020, 2026, Oracle and/or its affiliates. All rights reserved.
22
# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
33
#
44
# The Universal Permissive License (UPL), Version 1.0
@@ -59,6 +59,13 @@ def test_inet_aton_errs(self):
5959
self.assertRaises(OSError, lambda : socket.inet_aton('255.255.256.1'))
6060
self.assertRaises(TypeError, lambda : socket.inet_aton(255))
6161

62+
63+
class TestHostLookupErrors(unittest.TestCase):
64+
def test_gethostbyname_ex_invalid_host_raises_gaierror(self):
65+
with self.assertRaises(socket.gaierror):
66+
socket.gethostbyname_ex("nonexistent.invalid")
67+
68+
6269
def test_get_name_info():
6370
import socket
6471
try :

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/ImpModuleBuiltins.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,7 @@ private static int doExec(Node node, PythonContext context, PythonModule extensi
287287
return 0;
288288
}
289289

290-
if (!context.hasCApiContext()) {
290+
if (context.getCApiState() != PythonContext.CApiState.INITIALIZED) {
291291
throw PRaiseNode.raiseStatic(node, PythonBuiltinClassType.SystemError, ErrorMessages.CAPI_NOT_YET_INITIALIZED);
292292
}
293293

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/SocketModuleBuiltins.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2018, 2026, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* The Universal Permissive License (UPL), Version 1.0
@@ -385,7 +385,7 @@ static Object get(VirtualFrame frame, Object nameObj,
385385
addrInfoCursorLib.release(cursor);
386386
}
387387
} catch (GetAddrInfoException e) {
388-
throw constructAndRaiseNode.get(inliningTarget).executeWithArgsOnly(frame, SocketHError, new Object[]{e.getMessageAsTruffleString()});
388+
throw constructAndRaiseNode.get(inliningTarget).executeWithArgsOnly(frame, SocketGAIError, new Object[]{e.getErrorCode(), e.getMessageAsTruffleString()});
389389
} catch (PosixException e) {
390390
throw constructAndRaiseNode.get(inliningTarget).raiseOSErrorFromPosixException(frame, e);
391391
}

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextPyStateBuiltins.java

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2021, 2026, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* The Universal Permissive License (UPL), Version 1.0
@@ -71,16 +71,13 @@
7171
import com.oracle.graal.python.runtime.GilNode;
7272
import com.oracle.graal.python.runtime.PythonContext;
7373
import com.oracle.graal.python.runtime.PythonContext.PythonThreadState;
74-
import com.oracle.graal.python.runtime.object.PFactory;
7574
import com.oracle.graal.python.util.OverflowException;
7675
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
7776
import com.oracle.truffle.api.ThreadLocalAction;
7877
import com.oracle.truffle.api.TruffleLogger;
7978
import com.oracle.truffle.api.dsl.Bind;
8079
import com.oracle.truffle.api.dsl.Cached;
8180
import com.oracle.truffle.api.dsl.Specialization;
82-
import com.oracle.truffle.api.interop.InteropLibrary;
83-
import com.oracle.truffle.api.library.CachedLibrary;
8481
import com.oracle.truffle.api.nodes.Node;
8582
import com.oracle.truffle.api.nodes.RootNode;
8683

@@ -116,19 +113,39 @@ static Object restore(
116113
}
117114
}
118115

119-
@CApiBuiltin(ret = PyThreadState, args = {Pointer}, call = Ignored)
116+
/**
117+
* Very unlikely fallback for threads that were already attached when another thread initialized
118+
* the C API, but were blocked at that time and therefore could not process the thread-local
119+
* action that eagerly initializes their native 'tstate_current' TLS slot.
120+
*/
121+
@CApiBuiltin(ret = PyThreadState, args = {Pointer}, acquireGil = false, call = Ignored)
120122
abstract static class GraalPyPrivate_ThreadState_Get extends CApiUnaryBuiltinNode {
123+
private static final TruffleLogger LOGGER = CApiContext.getLogger(GraalPyPrivate_ThreadState_Get.class);
121124

122-
@Specialization(limit = "1")
123-
static Object get(Object tstateCurrentPtr,
124-
@Bind Node inliningTarget,
125-
@Bind PythonContext context,
126-
@CachedLibrary("tstateCurrentPtr") InteropLibrary lib) {
127-
PythonThreadState pythonThreadState = context.getThreadState(context.getLanguage(inliningTarget));
128-
if (!lib.isNull(tstateCurrentPtr)) {
129-
pythonThreadState.setNativeThreadLocalVarPointer(tstateCurrentPtr);
125+
@Specialization
126+
@TruffleBoundary
127+
static Object get(Object tstateCurrentPtr) {
128+
PythonContext context = PythonContext.get(null);
129+
PythonThreadState threadState = context.getThreadState(context.getLanguage());
130+
131+
/*
132+
* The C caller may have observed 'tstate_current == NULL' before entering this upcall.
133+
* While entering this builtin, the same thread may process a queued thread-local action
134+
* from C API initialization and initialize its native thread state eagerly. So the
135+
* fallback decision made in C can be stale by the time we get here.
136+
*/
137+
if (threadState.isNativeThreadStateInitialized()) {
138+
LOGGER.fine(() -> String.format("Lazy initialization attempt of native thread state for thread %s aborted. Was initialized in the meantime.", Thread.currentThread()));
139+
Object nativeThreadState = PThreadState.getNativeThreadState(threadState);
140+
assert nativeThreadState != null;
141+
return nativeThreadState;
130142
}
131-
return PThreadState.getOrCreateNativeThreadState(pythonThreadState);
143+
144+
LOGGER.fine(() -> "Lazy (fallback) initialization of native thread state for thread " + Thread.currentThread());
145+
assert PThreadState.getNativeThreadState(threadState) == null;
146+
Object nativeThreadState = PThreadState.getOrCreateNativeThreadState(threadState);
147+
threadState.setNativeThreadLocalVarPointer(tstateCurrentPtr);
148+
return nativeThreadState;
132149
}
133150
}
134151

@@ -151,12 +168,7 @@ static PDict get(
151168
@Bind Node inliningTarget,
152169
@Bind PythonContext context) {
153170
PythonThreadState threadState = context.getThreadState(context.getLanguage(inliningTarget));
154-
PDict threadStateDict = threadState.getDict();
155-
if (threadStateDict == null) {
156-
threadStateDict = PFactory.createDict(context.getLanguage());
157-
threadState.setDict(threadStateDict);
158-
}
159-
return threadStateDict;
171+
return PThreadState.getOrCreateThreadStateDict(context, threadState);
160172
}
161173
}
162174

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/datetime/DateTimeBuiltins.java

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2371,22 +2371,14 @@ private static Object parse(String string, String format, PythonContext context,
23712371
TimeZone timeZone = TimeModuleBuiltins.getGlobalTimeZone(context);
23722372
String zoneName = timeZone.getDisplayName(false, TimeZone.SHORT);
23732373
String zoneNameDaylightSaving = timeZone.getDisplayName(true, TimeZone.SHORT);
2374+
String matchedZoneName = matchTimeZoneName(string, i, zoneName, zoneNameDaylightSaving, "UTC", "GMT");
23742375

2375-
if (string.startsWith("UTC", i)) {
2376-
builder.setTimeZoneName("UTC");
2377-
i += 3;
2378-
} else if (string.startsWith("GMT", i)) {
2379-
builder.setTimeZoneName("GMT");
2380-
i += 3;
2381-
} else if (string.startsWith(zoneName, i)) {
2382-
builder.setTimeZoneName(zoneName);
2383-
i += zoneName.length();
2384-
} else if (string.startsWith(zoneNameDaylightSaving, i)) {
2385-
builder.setTimeZoneName(zoneNameDaylightSaving);
2386-
i += zoneNameDaylightSaving.length();
2387-
} else {
2376+
if (matchedZoneName == null) {
23882377
throw PRaiseNode.raiseStatic(inliningTarget, ValueError, ErrorMessages.TIME_DATA_S_DOES_NOT_MATCH_FORMAT_S, string, format);
23892378
}
2379+
2380+
builder.setTimeZoneName(matchedZoneName);
2381+
i += matchedZoneName.length();
23902382
}
23912383
case 'j' -> {
23922384
var pos = new ParsePosition(i);
@@ -2548,6 +2540,16 @@ private static Integer parseDigits(String source, int from, int digitsCount) {
25482540
return result;
25492541
}
25502542

2543+
private static String matchTimeZoneName(String string, int from, String... candidates) {
2544+
String matched = null;
2545+
for (String candidate : candidates) {
2546+
if (candidate != null && string.startsWith(candidate, from) && (matched == null || candidate.length() > matched.length())) {
2547+
matched = candidate;
2548+
}
2549+
}
2550+
return matched;
2551+
}
2552+
25512553
@TruffleBoundary
25522554
private static Integer parseDigitsUpTo(String source, ParsePosition from, int maxDigitsCount) {
25532555
int result = 0;

0 commit comments

Comments
 (0)