Skip to content

Commit 1c1f21b

Browse files
committed
PYCBC-1810: Release the GIL with an RAII guard
Changes -------- * Add pycbc::gil_release_guard in src/gil_guard.hxx, a non-copyable, non-movable wrapper over PyEval_SaveThread/PyEval_RestoreThread that restores the GIL when a C++ exception unwinds out of the region * Replace all 17 Py_BEGIN/END_ALLOW_THREADS regions inside try blocks in connection.cxx and connection.hxx with the guard. The macro pair expands to a bare block open and close, so a throw between them skipped Py_END_ALLOW_THREADS entirely and the catch block, its PyErr_* calls and the return path all ran with the GIL released * Covers connect, close, open_bucket, close_bucket, diagnostics and ping in connection.cxx, and execute_op, dispatch_sync, execute_multi_op, execute_streaming_op and execute_mgmt_op in connection.hxx * Scope the guard to the dispatch and wait region only, so the barrier result is read back with the GIL held rather than relying on Py_END_ALLOW_THREADS landing before the return statement * Restore the intended brace layout at the sites where the macros had forced clang-format into a hanging if (barrier) indent Change-Id: I8483e939444a0cc15971c991131b687f9c6140af Reviewed-on: https://review.couchbase.org/c/couchbase-python-client/+/249733 Tested-by: Build Bot <build@couchbase.com> Reviewed-by: Sergey Avseyev <sergey.avseyev@gmail.com>
1 parent 4c63620 commit 1c1f21b

3 files changed

Lines changed: 112 additions & 58 deletions

File tree

src/connection.cxx

Lines changed: 36 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -177,23 +177,23 @@ Connection::connect(PyObject* kwargs)
177177
update_cluster_options_from_py(connstr.options, pyObj_options, pyObj_auth);
178178
couchbase::core::origin origin(creds, connstr);
179179

180-
Py_BEGIN_ALLOW_THREADS
181180
{
181+
gil_release_guard no_gil;
182182
cluster_.open(
183183
origin,
184184
[callback = pyObj_callback, errback = pyObj_errback, barrier, this](std::error_code ec) {
185185
handle_connection_operation_callback(
186186
ec, "connect", callback, errback, barrier, connection_state_action::set_connected);
187187
});
188188
}
189-
Py_END_ALLOW_THREADS
190189

191-
if (barrier)
192-
{
190+
if (barrier) {
193191
PyObject* result = nullptr;
194-
Py_BEGIN_ALLOW_THREADS result = fut.get();
195-
Py_END_ALLOW_THREADS if (result == nullptr)
196192
{
193+
gil_release_guard no_gil;
194+
result = fut.get();
195+
}
196+
if (result == nullptr) {
197197
// The IO thread's result construction failed; that failure was already
198198
// reported via PyErr_WriteUnraisable there, so nothing is pending on this
199199
// thread.
@@ -231,8 +231,8 @@ Connection::close(PyObject* kwargs)
231231
}
232232

233233
try {
234-
Py_BEGIN_ALLOW_THREADS
235234
{
235+
gil_release_guard no_gil;
236236
cluster_.close([callback = pyObj_callback, errback = pyObj_errback, barrier, this]() {
237237
handle_connection_operation_callback(std::error_code{},
238238
"close",
@@ -242,14 +242,14 @@ Connection::close(PyObject* kwargs)
242242
connection_state_action::set_disconnected);
243243
});
244244
}
245-
Py_END_ALLOW_THREADS
246245

247-
if (barrier)
248-
{
246+
if (barrier) {
249247
PyObject* result = nullptr;
250-
Py_BEGIN_ALLOW_THREADS result = fut.get();
251-
Py_END_ALLOW_THREADS if (result == nullptr)
252248
{
249+
gil_release_guard no_gil;
250+
result = fut.get();
251+
}
252+
if (result == nullptr) {
253253
// The IO thread's result construction failed; that failure was already
254254
// reported via PyErr_WriteUnraisable there, so nothing is pending on this
255255
// thread.
@@ -293,22 +293,22 @@ Connection::open_bucket(PyObject* kwargs)
293293
throw std::invalid_argument("bucket_name must be a valid UTF-8 string");
294294
}
295295

296-
Py_BEGIN_ALLOW_THREADS
297296
{
297+
gil_release_guard no_gil;
298298
cluster_.open_bucket(
299299
bucket_name,
300300
[callback = pyObj_callback, errback = pyObj_errback, barrier, this](std::error_code ec) {
301301
handle_connection_operation_callback(ec, "open_bucket", callback, errback, barrier);
302302
});
303303
}
304-
Py_END_ALLOW_THREADS
305304

306-
if (barrier)
307-
{
305+
if (barrier) {
308306
PyObject* result = nullptr;
309-
Py_BEGIN_ALLOW_THREADS result = fut.get();
310-
Py_END_ALLOW_THREADS if (result == nullptr)
311307
{
308+
gil_release_guard no_gil;
309+
result = fut.get();
310+
}
311+
if (result == nullptr) {
312312
// The IO thread's result construction failed; that failure was already
313313
// reported via PyErr_WriteUnraisable there, so nothing is pending on this
314314
// thread.
@@ -352,22 +352,22 @@ Connection::close_bucket(PyObject* kwargs)
352352
throw std::invalid_argument("bucket_name must be a valid UTF-8 string");
353353
}
354354

355-
Py_BEGIN_ALLOW_THREADS
356355
{
356+
gil_release_guard no_gil;
357357
cluster_.close_bucket(
358358
bucket_name,
359359
[callback = pyObj_callback, errback = pyObj_errback, barrier, this](std::error_code ec) {
360360
handle_connection_operation_callback(ec, "close_bucket", callback, errback, barrier);
361361
});
362362
}
363-
Py_END_ALLOW_THREADS
364363

365-
if (barrier)
366-
{
364+
if (barrier) {
367365
PyObject* result = nullptr;
368-
Py_BEGIN_ALLOW_THREADS result = fut.get();
369-
Py_END_ALLOW_THREADS if (result == nullptr)
370366
{
367+
gil_release_guard no_gil;
368+
result = fut.get();
369+
}
370+
if (result == nullptr) {
371371
// The IO thread's result construction failed; that failure was already
372372
// reported via PyErr_WriteUnraisable there, so nothing is pending on this
373373
// thread.
@@ -445,23 +445,23 @@ Connection::diagnostics(PyObject* kwargs)
445445
}
446446

447447
try {
448-
Py_BEGIN_ALLOW_THREADS
449448
{
449+
gil_release_guard no_gil;
450450
cluster_.diagnostics(report_id,
451451
[pyObj_callback, pyObj_errback, barrier, this](
452452
couchbase::core::diag::diagnostics_result resp) {
453453
handle_cluster_operation_callback(
454454
resp, pyObj_callback, pyObj_errback, barrier);
455455
});
456456
}
457-
Py_END_ALLOW_THREADS
458457

459-
if (barrier)
460-
{
458+
if (barrier) {
461459
PyObject* result = nullptr;
462-
Py_BEGIN_ALLOW_THREADS result = fut.get();
463-
Py_END_ALLOW_THREADS if (result == nullptr)
464460
{
461+
gil_release_guard no_gil;
462+
result = fut.get();
463+
}
464+
if (result == nullptr) {
465465
// The IO thread's result construction failed; that failure was already
466466
// reported via PyErr_WriteUnraisable there, so nothing is pending on this
467467
// thread.
@@ -508,8 +508,8 @@ Connection::ping(PyObject* kwargs)
508508
}
509509

510510
try {
511-
Py_BEGIN_ALLOW_THREADS
512511
{
512+
gil_release_guard no_gil;
513513
cluster_.ping(
514514
report_id,
515515
bucket_name,
@@ -519,14 +519,14 @@ Connection::ping(PyObject* kwargs)
519519
handle_cluster_operation_callback(resp, pyObj_callback, pyObj_errback, barrier);
520520
});
521521
}
522-
Py_END_ALLOW_THREADS
523522

524-
if (barrier)
525-
{
523+
if (barrier) {
526524
PyObject* result = nullptr;
527-
Py_BEGIN_ALLOW_THREADS result = fut.get();
528-
Py_END_ALLOW_THREADS if (result == nullptr)
529525
{
526+
gil_release_guard no_gil;
527+
result = fut.get();
528+
}
529+
if (result == nullptr) {
530530
// The IO thread's result construction failed; that failure was already
531531
// reported via PyErr_WriteUnraisable there, so nothing is pending on this
532532
// thread.

src/connection.hxx

Lines changed: 27 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
#include "Python.h"
2121
#include "error_contexts.hxx"
2222
#include "exceptions.hxx"
23+
#include "gil_guard.hxx"
2324
#include "operations_autogen.hxx"
2425
#include "pycbc_kv_request.hxx"
2526
#include "pytocbpp_defs.hxx"
@@ -234,8 +235,8 @@ private:
234235
std::optional<std::chrono::system_clock::time_point> start_time = std::nullopt)
235236
{
236237
using response_type = typename Request::response_type;
237-
Py_BEGIN_ALLOW_THREADS
238238
{
239+
gil_release_guard no_gil;
239240
cluster_.execute(
240241
req,
241242
[pyObj_callback, pyObj_errback, barrier, wrapper_span, start_time, this](
@@ -276,7 +277,6 @@ private:
276277
PyGILState_Release(state);
277278
});
278279
}
279-
Py_END_ALLOW_THREADS
280280
}
281281

282282
template<typename PyType>
@@ -366,11 +366,14 @@ Connection::dispatch_sync(Request&& req)
366366
using Response = typename Request::response_type;
367367
auto barrier = std::make_shared<std::promise<Response>>();
368368
auto fut = barrier->get_future();
369-
Py_BEGIN_ALLOW_THREADS cluster_.execute(std::forward<Request>(req), [barrier](Response resp) {
370-
barrier->set_value(std::move(resp));
371-
});
372-
fut.wait();
373-
Py_END_ALLOW_THREADS return fut.get();
369+
{
370+
gil_release_guard no_gil;
371+
cluster_.execute(std::forward<Request>(req), [barrier](Response resp) {
372+
barrier->set_value(std::move(resp));
373+
});
374+
fut.wait();
375+
}
376+
return fut.get();
374377
}
375378

376379
template<typename Request, typename Response>
@@ -523,20 +526,21 @@ Connection::execute_multi_op(PyObject* arg)
523526
std::move(fut) });
524527
}
525528

526-
Py_BEGIN_ALLOW_THREADS for (auto& s : staging)
527529
{
528-
auto barrier = s.barrier;
529-
cluster_.execute(s.req, [barrier](Response resp) {
530-
barrier->set_value(std::move(resp));
531-
});
532-
}
530+
gil_release_guard no_gil;
531+
for (auto& s : staging) {
532+
auto barrier = s.barrier;
533+
cluster_.execute(s.req, [barrier](Response resp) {
534+
barrier->set_value(std::move(resp));
535+
});
536+
}
533537

534-
for (auto& s : staging) {
535-
s.fut.wait();
538+
for (auto& s : staging) {
539+
s.fut.wait();
540+
}
536541
}
537-
Py_END_ALLOW_THREADS
538542

539-
bool all_okay = true;
543+
bool all_okay = true;
540544
for (auto& s : staging) {
541545
PyObject* res =
542546
finalize_kv_result<Request>(s.fut.get(), std::move(s.wrapper_span), std::move(s.start_time));
@@ -622,8 +626,8 @@ Connection::execute_streaming_op(PyObject* kwargs)
622626
// };
623627

624628
using response_type = typename Request::response_type;
625-
Py_BEGIN_ALLOW_THREADS
626629
{
630+
gil_release_guard no_gil;
627631
cluster_.execute(
628632
req,
629633
// we pass the shared_ptr rows separately b/c we need t o allow the rows queue to survive
@@ -708,9 +712,8 @@ Connection::execute_streaming_op(PyObject* kwargs)
708712
PyGILState_Release(state);
709713
});
710714
}
711-
Py_END_ALLOW_THREADS
712715

713-
return reinterpret_cast<PyObject*>(streamed_res);
716+
return reinterpret_cast<PyObject*>(streamed_res);
714717

715718
} catch (const std::exception& e) {
716719
Py_XDECREF(pyObj_callback);
@@ -764,9 +767,11 @@ Connection::execute_mgmt_op(PyObject* kwargs)
764767
execute_op(req, pyObj_callback, pyObj_errback, barrier, wrapper_span);
765768
if (barrier) {
766769
PyObject* result = nullptr;
767-
Py_BEGIN_ALLOW_THREADS result = fut.get();
768-
Py_END_ALLOW_THREADS if (result == nullptr)
769770
{
771+
gil_release_guard no_gil;
772+
result = fut.get();
773+
}
774+
if (result == nullptr) {
770775
// The IO thread's conversion failed; that failure was already reported via
771776
// PyErr_WriteUnraisable there, so nothing is pending on this thread.
772777
set_runtime_error_if_unset("Failed to process operation result.");

src/gil_guard.hxx

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright 2016-2026. Couchbase, Inc.
3+
* All Rights Reserved.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
#pragma once
19+
20+
#include "Python.h"
21+
22+
namespace pycbc
23+
{
24+
25+
// Releases the GIL for the guard's lifetime. Unlike Py_BEGIN/END_ALLOW_THREADS, the GIL is
26+
// restored when a C++ exception unwinds out of the region, so catch blocks run with it held.
27+
class gil_release_guard
28+
{
29+
public:
30+
gil_release_guard()
31+
: state_(PyEval_SaveThread())
32+
{
33+
}
34+
35+
~gil_release_guard()
36+
{
37+
PyEval_RestoreThread(state_);
38+
}
39+
40+
gil_release_guard(const gil_release_guard&) = delete;
41+
gil_release_guard& operator=(const gil_release_guard&) = delete;
42+
gil_release_guard(gil_release_guard&&) = delete;
43+
gil_release_guard& operator=(gil_release_guard&&) = delete;
44+
45+
private:
46+
PyThreadState* state_;
47+
};
48+
49+
} // namespace pycbc

0 commit comments

Comments
 (0)