Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
da0e917
Make the _json module thread safe
eendebakpt May 22, 2024
3797dfa
Update Modules/_json.c
eendebakpt May 22, 2024
366654c
handle goto and return statements
eendebakpt May 24, 2024
5b72cdf
Apply suggestions from code review
eendebakpt May 25, 2024
c4c24c3
Update Include/internal/pycore_critical_section.h
eendebakpt May 25, 2024
370191b
rename macro
eendebakpt May 31, 2024
93c4466
Merge branch 'main' into json_ft
eendebakpt May 31, 2024
eafd3c1
fix typo
eendebakpt May 31, 2024
daeec46
Merge branch 'json_ft' of github.com:eendebakpt/cpython into json_ft
eendebakpt May 31, 2024
d54baf2
fix missing to exit critical section
eendebakpt Jun 4, 2024
e5fa305
revert changes to tests
eendebakpt Jun 4, 2024
d4ddf5d
📜🤖 Added by blurb_it.
blurb-it[bot] Jun 4, 2024
67d942f
Merge branch 'main' into json_ft
eendebakpt Jun 4, 2024
4ffc1b2
Merge branch 'main' into json_ft
eendebakpt Aug 14, 2024
384ca59
sync with main
eendebakpt Aug 14, 2024
64e20aa
sync with main
eendebakpt Aug 14, 2024
e6ce9c9
update news entry
eendebakpt Aug 14, 2024
34885a0
fix normal build
eendebakpt Aug 14, 2024
2fe760b
Merge branch 'main' into json_ft
eendebakpt Aug 14, 2024
eebccac
add lock around result of PyMapping_Items
eendebakpt Aug 15, 2024
db8947c
add tests
eendebakpt Aug 15, 2024
c19ad14
fix argument of Py_END_CRITICAL_SECTION_SEQUENCE_FAST
eendebakpt Aug 15, 2024
8b12e0f
Merge branch 'main' into json_ft
eendebakpt Feb 10, 2025
78d3595
avoid Py_EXIT_CRITICAL_SECTION_SEQUENCE_FAST
eendebakpt Feb 10, 2025
6e8615f
use barriers in test
eendebakpt Feb 10, 2025
39ebc00
typo
eendebakpt Feb 10, 2025
7c5b185
whitespace
eendebakpt Feb 10, 2025
adf78c7
Merge branch 'main' into json_ft
eendebakpt Jun 24, 2025
acd0ad1
resolve merge conflicts
eendebakpt Jun 24, 2025
41e3dee
Update Misc/NEWS.d/next/Core_and_Builtins/2024-06-04-20-26-21.gh-issu…
eendebakpt Jun 24, 2025
75884cb
cleanup
eendebakpt Jun 24, 2025
0424c58
Merge branch 'json_ft' of github.com:eendebakpt/cpython into json_ft
eendebakpt Jun 24, 2025
9c964f9
Merge branch 'main' into json_ft
eendebakpt Jun 24, 2025
5acc999
Merge branch 'main' into json_ft
eendebakpt Jul 28, 2025
5173373
format
eendebakpt Jul 28, 2025
7d00562
only use items locally in encoder_listencode_dict
eendebakpt Aug 6, 2025
00f4d7f
Merge branch 'main' into json_ft
eendebakpt Aug 14, 2025
b95c07f
use strong references in iteration
eendebakpt Aug 24, 2025
cedc2c3
Merge branch 'main' into json_ft
eendebakpt Aug 24, 2025
0ca453d
replace Py_IncRef with Py_INCREF
eendebakpt Aug 26, 2025
45544f3
Update Modules/_json.c
eendebakpt Aug 27, 2025
dcb088e
Apply suggestion from @kumaraditya303
kumaraditya303 Aug 27, 2025
5f8ad07
add header
kumaraditya303 Aug 27, 2025
61bc8d1
Apply suggestions from code review
eendebakpt Aug 27, 2025
f85b695
review comments
eendebakpt Aug 28, 2025
2c0cd18
use _Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED
eendebakpt Aug 28, 2025
c133b71
remove testing code
eendebakpt Aug 29, 2025
7609117
Merge branch 'main' into json_ft
eendebakpt Aug 29, 2025
7656a2a
add incref/decref to fast seq iteration
eendebakpt Aug 30, 2025
1920aed
Merge branch 'json_ft' of github.com:eendebakpt/cpython into json_ft
eendebakpt Aug 30, 2025
d5d65ab
fix
eendebakpt Aug 30, 2025
4a16e18
fix
eendebakpt Aug 30, 2025
00139f3
Update Modules/_json.c
kumaraditya303 Aug 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions Lib/test/test_free_threading/test_json.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
from threading import Barrier, Thread
from test.test_json import CTest
from test.support import threading_helper


def encode_json_helper(
json, worker, data, number_of_threads=12, number_of_json_encodings=100
):
worker_threads = []
barrier = Barrier(number_of_threads)
for index in range(number_of_threads):
worker_threads.append(
Thread(target=worker, args=[barrier, data, index])
)
for t in worker_threads:
t.start()
for ii in range(number_of_json_encodings):
json.dumps(data)
data.clear()
for t in worker_threads:
t.join()


class MyMapping(dict):
def __init__(self):
self.mapping = []

def items(self):
return self.mapping


@threading_helper.reap_threads
@threading_helper.requires_working_threading()
class TestJsonEncoding(CTest):
# Test encoding json with concurrent threads modifying the data cannot
# corrupt the interpreter

def test_json_mutating_list(self):
def worker(barrier, data, index):
barrier.wait()
while data:
for d in data:
if len(d) > 5:
d.clear()
else:
d.append(index)

data = [[], []]
encode_json_helper(self.json, worker, data)

def test_json_mutating_exact_dict(self):
def worker(barrier, data, index):
barrier.wait()
while data:
for d in data:
if len(d) > 5:
try:
key = list(d)[0]
d.pop()
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't this be d.pop(key)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes. See #138339

except (KeyError, IndexError):
pass
else:
d[index] = index

data = [{}, {}]
encode_json_helper(self.json, worker, data)

def test_json_mutating_mapping(self):
def worker(barrier, data, index):
barrier.wait()
while data:
for d in data:
if len(d.mapping) > 3:
d.mapping.clear()
else:
d.mapping.append((index, index))

data = [MyMapping(), MyMapping()]
encode_json_helper(self.json, worker, data)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Make the module :mod:`json` safe to use under the free-threading build.
175 changes: 134 additions & 41 deletions Modules/_json.c
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

#include "Python.h"
#include "pycore_ceval.h" // _Py_EnterRecursiveCall()
#include "pycore_critical_section.h" // Py_BEGIN_CRITICAL_SECTION_SEQUENCE_FAST()
#include "pycore_global_strings.h" // _Py_ID()
#include "pycore_pyerrors.h" // _PyErr_FormatNote
#include "pycore_runtime.h" // _PyRuntime
Expand Down Expand Up @@ -1456,7 +1457,7 @@ write_newline_indent(PyUnicodeWriter *writer,
static PyObject *
encoder_call(PyObject *op, PyObject *args, PyObject *kwds)
{
/* Python callable interface to encode_listencode_obj */
/* Python callable interface to encoder_listencode_obj */
static char *kwlist[] = {"obj", "_current_indent_level", NULL};
PyObject *obj;
Py_ssize_t indent_level;
Expand Down Expand Up @@ -1743,15 +1744,84 @@ encoder_encode_key_value(PyEncoderObject *s, PyUnicodeWriter *writer, bool *firs
return 0;
}

static inline int
_encoder_iterate_mapping_lock_held(PyEncoderObject *s, PyUnicodeWriter *writer,
bool *first, PyObject *dct, PyObject *items,
Py_ssize_t indent_level, PyObject *indent_cache,
PyObject *separator)
{
_Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(items);
PyObject *key, *value;
for (Py_ssize_t i = 0; i < PyList_GET_SIZE(items); i++) {
PyObject *item = PyList_GET_ITEM(items, i);
#ifdef Py_GIL_DISABLED
// gh-119438: in the free-threading build the critical section on items can get suspended
Py_INCREF(item);
#endif
if (!PyTuple_Check(item) || PyTuple_GET_SIZE(item) != 2) {
PyErr_SetString(PyExc_ValueError, "items must return 2-tuples");
#ifdef Py_GIL_DISABLED
Py_DECREF(item);
#endif
return -1;
}

key = PyTuple_GET_ITEM(item, 0);
value = PyTuple_GET_ITEM(item, 1);
if (encoder_encode_key_value(s, writer, first, dct, key, value,
indent_level, indent_cache,
separator) < 0) {
#ifdef Py_GIL_DISABLED
Py_DECREF(item);
#endif
return -1;
}
#ifdef Py_GIL_DISABLED
Py_DECREF(item);
#endif
}

return 0;
}

static inline int
_encoder_iterate_dict_lock_held(PyEncoderObject *s, PyUnicodeWriter *writer,
bool *first, PyObject *dct, Py_ssize_t indent_level,
PyObject *indent_cache, PyObject *separator)
{
_Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(dct);
PyObject *key, *value;
Py_ssize_t pos = 0;
while (PyDict_Next(dct, &pos, &key, &value)) {
#ifdef Py_GIL_DISABLED
// gh-119438: in the free-threading build the critical section on dct can get suspended
Py_INCREF(key);
Py_INCREF(value);
#endif
if (encoder_encode_key_value(s, writer, first, dct, key, value,
indent_level, indent_cache,
separator) < 0) {
#ifdef Py_GIL_DISABLED
Py_DECREF(key);
Py_DECREF(value);
#endif
return -1;
}
#ifdef Py_GIL_DISABLED
Py_DECREF(key);
Py_DECREF(value);
#endif
}
return 0;
}

static int
encoder_listencode_dict(PyEncoderObject *s, PyUnicodeWriter *writer,
PyObject *dct,
PyObject *dct,
Py_ssize_t indent_level, PyObject *indent_cache)
{
/* Encode Python dict dct a JSON term */
PyObject *ident = NULL;
PyObject *items = NULL;
PyObject *key, *value;
bool first = true;

if (PyDict_GET_SIZE(dct) == 0) {
Expand Down Expand Up @@ -1788,34 +1858,30 @@ encoder_listencode_dict(PyEncoderObject *s, PyUnicodeWriter *writer,
}

if (s->sort_keys || !PyDict_CheckExact(dct)) {
items = PyMapping_Items(dct);
if (items == NULL || (s->sort_keys && PyList_Sort(items) < 0))
PyObject *items = PyMapping_Items(dct);
if (items == NULL || (s->sort_keys && PyList_Sort(items) < 0)) {
Py_XDECREF(items);
goto bail;
}

for (Py_ssize_t i = 0; i < PyList_GET_SIZE(items); i++) {
PyObject *item = PyList_GET_ITEM(items, i);

if (!PyTuple_Check(item) || PyTuple_GET_SIZE(item) != 2) {
PyErr_SetString(PyExc_ValueError, "items must return 2-tuples");
goto bail;
}

key = PyTuple_GET_ITEM(item, 0);
value = PyTuple_GET_ITEM(item, 1);
if (encoder_encode_key_value(s, writer, &first, dct, key, value,
indent_level, indent_cache,
separator) < 0)
goto bail;
int result;
Py_BEGIN_CRITICAL_SECTION_SEQUENCE_FAST(items);
result = _encoder_iterate_mapping_lock_held(s, writer, &first, dct,
items, indent_level, indent_cache, separator);
Py_END_CRITICAL_SECTION_SEQUENCE_FAST();
Py_DECREF(items);
if (result < 0) {
goto bail;
}
Py_CLEAR(items);

} else {
Py_ssize_t pos = 0;
while (PyDict_Next(dct, &pos, &key, &value)) {
if (encoder_encode_key_value(s, writer, &first, dct, key, value,
indent_level, indent_cache,
separator) < 0)
goto bail;
int result;
Py_BEGIN_CRITICAL_SECTION(dct);
result = _encoder_iterate_dict_lock_held(s, writer, &first, dct,
indent_level, indent_cache, separator);
Py_END_CRITICAL_SECTION();
if (result < 0) {
goto bail;
}
}

Expand All @@ -1837,22 +1903,52 @@ encoder_listencode_dict(PyEncoderObject *s, PyUnicodeWriter *writer,
return 0;

bail:
Py_XDECREF(items);
Py_XDECREF(ident);
return -1;
}

static inline int
_encoder_iterate_fast_seq_lock_held(PyEncoderObject *s, PyUnicodeWriter *writer,
PyObject *seq, PyObject *s_fast,
Py_ssize_t indent_level, PyObject *indent_cache, PyObject *separator)
{
for (Py_ssize_t i = 0; i < PySequence_Fast_GET_SIZE(s_fast); i++) {
PyObject *obj = PySequence_Fast_GET_ITEM(s_fast, i);
#ifdef Py_GIL_DISABLED
// gh-119438: in the free-threading build the critical section on s_fast can get suspended
Py_INCREF(obj);
#endif
if (i) {
if (PyUnicodeWriter_WriteStr(writer, separator) < 0) {
#ifdef Py_GIL_DISABLED
Py_DECREF(obj);
#endif
return -1;
}
}
if (encoder_listencode_obj(s, writer, obj, indent_level, indent_cache)) {
_PyErr_FormatNote("when serializing %T item %zd", seq, i);
#ifdef Py_GIL_DISABLED
Py_DECREF(obj);
#endif
return -1;
}
#ifdef Py_GIL_DISABLED
Py_DECREF(obj);
#endif
}
return 0;
}

static int
encoder_listencode_list(PyEncoderObject *s, PyUnicodeWriter *writer,
PyObject *seq,
Py_ssize_t indent_level, PyObject *indent_cache)
{
PyObject *ident = NULL;
PyObject *s_fast = NULL;
Py_ssize_t i;

ident = NULL;
s_fast = PySequence_Fast(seq, "_iterencode_list needs a sequence");
s_fast = PySequence_Fast(seq, "encoder_listencode_list needs a sequence");
if (s_fast == NULL)
return -1;
if (PySequence_Fast_GET_SIZE(s_fast) == 0) {
Expand Down Expand Up @@ -1890,16 +1986,13 @@ encoder_listencode_list(PyEncoderObject *s, PyUnicodeWriter *writer,
goto bail;
}
}
for (i = 0; i < PySequence_Fast_GET_SIZE(s_fast); i++) {
PyObject *obj = PySequence_Fast_GET_ITEM(s_fast, i);
if (i) {
if (PyUnicodeWriter_WriteStr(writer, separator) < 0)
goto bail;
}
if (encoder_listencode_obj(s, writer, obj, indent_level, indent_cache)) {
_PyErr_FormatNote("when serializing %T item %zd", seq, i);
goto bail;
}
int result;
Py_BEGIN_CRITICAL_SECTION_SEQUENCE_FAST(seq);
result = _encoder_iterate_fast_seq_lock_held(s, writer, seq, s_fast,
indent_level, indent_cache, separator);
Py_END_CRITICAL_SECTION_SEQUENCE_FAST();
if (result < 0) {
goto bail;
}
if (ident != NULL) {
if (PyDict_DelItem(s->markers, ident))
Expand Down
Loading