From 75e33a333ec4c5dd59eddb8750a2f053c2996c04 Mon Sep 17 00:00:00 2001 From: Eric Froemling Date: Fri, 8 May 2026 21:40:53 -0700 Subject: [PATCH] Release the GIL across libmaxminddb lookup calls get_record() previously held the GIL across MMDB_lookup_sockaddr and MMDB_get_entry_data_list. These walk the read-only mmap and touch no Python state, but if the relevant pages aren't resident (cold cache, slow disk, memory pressure) a single page-fault inside the walk stalls every Python thread for the duration of the disk wait, not just the calling thread. Wrap both calls in Py_BEGIN_ALLOW_THREADS / Py_END_ALLOW_THREADS. The pthread_rwlock_t read lock is held throughout, and the module already declares Py_MOD_GIL_NOT_USED, so this just brings the GIL-build fast path in line with the existing free-threaded guarantees. --- extension/maxminddb.c | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/extension/maxminddb.c b/extension/maxminddb.c index c2dd73e7..13d35fc1 100644 --- a/extension/maxminddb.c +++ b/extension/maxminddb.c @@ -442,9 +442,18 @@ static int get_record(PyObject *self, PyObject *args, PyObject **record) { return -1; } + // Release the GIL across the libmaxminddb tree walk. The walk only + // touches the read-only mmap and stack-local state, the rwlock is + // already held, and the module is declared Py_MOD_GIL_NOT_USED - so + // no Python state is accessed here. On systems where the mmdb's + // pages are not resident (cold cache, slow disk, memory pressure) + // an individual page-in can stall this call for seconds; without + // releasing the GIL, the entire interpreter stalls with it. int mmdb_error = MMDB_SUCCESS; - MMDB_lookup_result_s result = - MMDB_lookup_sockaddr(mmdb, ip_address, &mmdb_error); + MMDB_lookup_result_s result; + Py_BEGIN_ALLOW_THREADS + result = MMDB_lookup_sockaddr(mmdb, ip_address, &mmdb_error); + Py_END_ALLOW_THREADS if (mmdb_error != MMDB_SUCCESS) { reader_release_read_lock(reader); @@ -478,8 +487,13 @@ static int get_record(PyObject *self, PyObject *args, PyObject **record) { return prefix_len; } + // Same reasoning as above: read the data section from the mmap + // without holding the GIL. MMDB_entry_data_list_s *entry_data_list = NULL; - int status = MMDB_get_entry_data_list(&result.entry, &entry_data_list); + int status; + Py_BEGIN_ALLOW_THREADS + status = MMDB_get_entry_data_list(&result.entry, &entry_data_list); + Py_END_ALLOW_THREADS if (status != MMDB_SUCCESS) { reader_release_read_lock(reader); char ipstr[INET6_ADDRSTRLEN] = {0};