Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
dcbc141
add atomic global counter system
Vizonex Apr 21, 2026
df0477a
add change to timeline
Vizonex Apr 21, 2026
6a35f59
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Apr 21, 2026
b785c82
Merge branch 'global-state' of https://github.com/Vizonex/multidict i…
Vizonex Apr 21, 2026
adcec68
add changes to setup.py for windows users
Vizonex Apr 21, 2026
aaf0b25
fix mypy issues with global_counter test
Vizonex Apr 21, 2026
ae8b776
fix global counter test again
Vizonex Apr 22, 2026
d949beb
fix compiler issues with mac
Vizonex Apr 22, 2026
89214fa
Update multidict/_multilib/state.h
Vizonex Apr 22, 2026
6024eaa
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Apr 22, 2026
d9e4b00
fine tune atomics to enusre all tests pass and add comment about why …
Vizonex Apr 22, 2026
3ac82ec
Update multidict/_multilib/state.h
Vizonex Apr 22, 2026
9a73bd4
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Apr 22, 2026
b5f675d
fix syntax error
Vizonex Apr 22, 2026
a7fad51
fix setup.py and customize flags a little bit more
Vizonex Apr 22, 2026
e872f37
Update tests/isolated/multidict_global_counter.py
Vizonex Apr 22, 2026
8bb2038
Update setup.py
Vizonex Apr 22, 2026
abdbf7f
Merge branch 'master' into global-state
bdraco Apr 25, 2026
7c4b6b7
Update CHANGES/1328.bugfix.rst
Vizonex Apr 30, 2026
fbc5351
Merge branch 'master' into global-state
Vizonex Apr 30, 2026
0298263
Merge branch 'master' into global-state
Vizonex May 14, 2026
2ab5cb1
Merge branch 'master' into global-state
Vizonex May 16, 2026
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
2 changes: 2 additions & 0 deletions CHANGES/1328.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fixed global counter system using an atomic variable
-- by :user:`Vizonex`.
10 changes: 8 additions & 2 deletions multidict/_multilib/state.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
extern "C" {
#endif

#include <stdatomic.h>

/* State of the _multidict module */
typedef struct {
PyTypeObject *IStrType;
Expand All @@ -26,7 +28,7 @@ typedef struct {
PyObject *str_lower;
PyObject *str_name;

uint64_t global_version;
_Atomic uint64_t global_version;
} mod_state;

static inline mod_state *
Expand Down Expand Up @@ -128,7 +130,11 @@ get_mod_state_by_def(PyObject *self)
static inline uint64_t
NEXT_VERSION(mod_state *state)
{
return ++state->global_version;
/* relaxed is fine here as we only care about the atomicity of the RMW
* itself */
return atomic_fetch_add_explicit(
&state->global_version, 1, memory_order_relaxed) +
1;
}

#ifdef __cplusplus
Expand Down
50 changes: 33 additions & 17 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,51 @@
import os
import platform
import sys

from setuptools import Extension, setup
from setuptools.command.build_ext import build_ext

NO_EXTENSIONS = bool(os.environ.get("MULTIDICT_NO_EXTENSIONS"))
DEBUG_BUILD = bool(os.environ.get("MULTIDICT_DEBUG_BUILD"))

if sys.implementation.name != "cpython":
NO_EXTENSIONS = True

CFLAGS = ["-O0", "-g3", "-UNDEBUG"] if DEBUG_BUILD else ["-O3", "-DNDEBUG"]

if platform.system() != "Windows":
CFLAGS.extend(
[
"-std=c11",
"-Wall",
"-Wsign-compare",
"-Wconversion",
"-fno-strict-aliasing",
"-Wno-conversion",
"-Werror",
]
)
BASE_CFLAGS = ["O0", "g3", "UNDEBUG"] if DEBUG_BUILD else ["O3", "DNDEBUG"]

UNIX_CFLAGS = [
"-std=c11",
"-Wall",
"-Wsign-compare",
"-Wconversion",
"-fno-strict-aliasing",
"-Wno-conversion",
"-Werror",
]

MSVC_CFLAGS = ["/std:c11", "/experimental:c11atomics"]


class BuildExt(build_ext):
def build_extensions(self):
if self.compiler.compiler_type == "msvc":
for ext in self.extensions:
ext.extra_compile_args.extend(MSVC_CFLAGS)
for flag in BASE_CFLAGS:
# XXX: MSVC Doesn't have a /O3 flag only O2 is possible...
ext.extra_compile_args.append("/O2" if flag == "O3" else f"/{flag}")
else:
Comment on lines +33 to +36
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

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

MSVC handling currently prefixes BASE_CFLAGS values with / (e.g. O0 -> /O0, g3 -> /g3). These aren't valid MSVC flags (/Od and /Zi//Z7 are the usual equivalents), so Windows builds with MULTIDICT_DEBUG_BUILD=1 will fail. Consider defining a separate MSVC-specific debug/release flag list instead of reusing BASE_CFLAGS verbatim.

Copilot uses AI. Check for mistakes.
for ext in self.extensions:
ext.extra_compile_args.extend(UNIX_CFLAGS)
for flag in BASE_CFLAGS:
ext.extra_compile_args.append(f"-{flag}")
super().build_extensions()


extensions = [
Extension(
"multidict._multidict",
["multidict/_multidict.c"],
Comment thread
Vizonex marked this conversation as resolved.
extra_compile_args=CFLAGS,
extra_compile_args=[],
),
]

Expand All @@ -38,7 +54,7 @@
print("*********************")
print("* Accelerated build *")
print("*********************")
setup(ext_modules=extensions)
setup(ext_modules=extensions, cmdclass={"build_ext": BuildExt})
else:
print("*********************")
print("* Pure Python build *")
Expand Down
33 changes: 33 additions & 0 deletions tests/isolated/multidict_global_counter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import sysconfig
import threading

import multidict

Check notice

Code scanning / CodeQL

Module is imported with 'import' and 'import from' Note test

Module 'multidict' is imported with both 'import' and 'import from'.
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
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.

Should probably resolve this, no reason to import it twice.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Concur — easiest fix is from multidict import MultiDict, getversion and then call getversion(md) directly. That also removes one of the two type: ignore[arg-type] comments below, since getversion would be imported with its real signature rather than accessed through the bare module namespace.

from multidict import MultiDict

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


md: MultiDict[int] = MultiDict()
N, M = 3, 100
baseline = multidict.getversion(md) # type: ignore[arg-type]
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.

why ignore?

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.

If there's an error here, then maybe my strict Multidict[object] is causing problems. Might need to be changed to Multidict[Any]. Probably a contravariant thing I missed.



def worker(tid: int) -> None:
for i in range(M):
md[f"k{tid}_{i}"] = i


if (__name__ == "__main__") and FREETHREADED:
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.

Same concern as in other PRs: we shouldn't need to gate on FT.

cc @Dreamsorcerer

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Agreeing with this concern. Now that global_version is atomic, the invariant delta == N*M should hold under the GIL too — it just becomes a less interesting test. Gating on FREETHREADED means non‑FT CI runs the script for no benefit (still pays a subprocess + import) and any future regression that reverts NEXT_VERSION back to a plain ++ won't be caught unless someone happens to run an FT build locally. Unless there's a concrete __setitem__ path on the GIL build that legitimately skips a NEXT_VERSION call, I'd drop the gate. If the assertion does fail on a GIL build today, that's a separate bug that this PR is masking by gating it out.

threads = [threading.Thread(target=worker, args=(tid,)) for tid in range(N)]
for t in threads:
t.start()
for t in threads:
t.join()

observed = multidict.getversion(md) - baseline # type: ignore[arg-type]
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.

why ignore?

Copy link
Copy Markdown
Member Author

@Vizonex Vizonex Apr 30, 2026

Choose a reason for hiding this comment

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

MultiDict[object] is what getversion(..) wants but here we have a completely different type hence type: ignore[arg-type]

expected = N * M
assert expected == observed, (
f"expected delta: {expected}"
f" observed: {observed} "
f"lost: {expected - observed}"
)
1 change: 1 addition & 0 deletions tests/test_leaks.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"multidict_type_leak.py",
"multidict_type_leak_items_values.py",
"multidict_pop.py",
"multidict_global_counter.py",
),
)
@pytest.mark.leaks
Expand Down
Loading