Fix memory leak in build_kv_error_map_info when building KV exception
Description
Environment
Gerrit Reviews
Release Notes Description
relates
Activity
Jared Casey July 21, 2022 at 2:51 PM
Fixed memory leak when building KV error map and fix memory leak related to exception_base custom type.
Jared Casey July 16, 2022 at 12:10 AMEdited
Attached both some tracemalloc outputs and valgrind outputs (valgrind outputs are ~20 MB, so all are attached in a zip).
tracemalloc
A simple program was used to loop a handful of times and do a basic key-value operation (or multi key-value operation) that looks up non-existent keys and for each iteration a diff of tracemalloc snapshots are taken (snapshot taken after manually running garbage collection to reduce noise) to see changes that might be happening during each iteration. In the output logs labeled orig_*, one can see that on each iteration more memory is taken up and the amount of blocks taken up correspond to a multiple of 3 to the number of exceptions raised (notice that for a multi result, you have 1 + the number of results in a batch because the multi-result is a result object itself and is used to store all the result objects for the key-value operation it ran). The reason for the multiple is due to the leaks in the build_kv_error_map_info() method (2 blocks) and then the tp_free() method not being called in the exception type's destructor (1 block).
The corresponding fix_* files show the same test run w/ the applied fix mentioned in this ticket.
Valgrind
In orig_simple_kv_fake_keys_loop100.xml OR orig_simple_multi_kv_fake_keys_b20_loop100.xml search for
exception_base_new_: shows the exception object leak
If the loop count is changed, the number of leaked blocks for the result will follow the loop count
In fix_simple_kv_fake_keys_loop100.xml OR fix_simple_multi_kv_fake_keys_b20_loop100.xml search results for the follow will come up empty (i.e. indicated the leak has been fixed)
exception_base_new_
Example valgrind output (loop of 100):
<error>
<unique>0xf23</unique>
<tid>1</tid>
<kind>Leak_DefinitelyLost</kind>
<xwhat>
<text>4,800 bytes in 100 blocks are definitely lost in loss record 3,874 of 3,892</text>
<leakedbytes>4800</leakedbytes>
<leakedblocks>100</leakedblocks>
</xwhat>
<stack>
<frame>
<ip>0x483B7F3</ip>
<obj>/usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so</obj>
<fn>malloc</fn>
</frame>
<frame>
<ip>0x5A55E7</ip>
<obj>/usr/bin/python3.8</obj>
<fn>PyType_GenericAlloc</fn>
<dir>/build/python3.8-uvizni/python3.8-3.8.10/build-static/../Objects</dir>
<file>typeobject.c</file>
<line>1017</line>
</frame>
<frame>
<ip>0x6073CF8</ip>
<obj>/memory_tests/couchbase-python-client/couchbase/pycbc_core.so</obj>
<fn>exception_base__new__(_typeobject*, _object*, _object*)</fn>
</frame>
<frame>
<ip>0x5F73E2</ip>
<obj>/usr/bin/python3.8</obj>
<fn>UnknownInlinedFun</fn>
<dir>/build/python3.8-uvizni/python3.8-3.8.10/build-static/../Objects</dir>
<file>typeobject.c</file>
<line>974</line>
</frame>
<frame>
<ip>0x5F73E2</ip>
<obj>/usr/bin/python3.8</obj>
<fn>_PyObject_MakeTpCall</fn>
<dir>/build/python3.8-uvizni/python3.8-3.8.10/build-static/../Objects</dir>
<file>call.c</file>
<line>159</line>
</frame>
<frame>
<ip>0x5F8999</ip>
<obj>/usr/bin/python3.8</obj>
<fn>_PyObject_FastCallDict</fn>
<dir>/build/python3.8-uvizni/python3.8-3.8.10/build-static/../Objects</dir>
<file>call.c</file>
<line>91</line>
</frame>
<frame>
<ip>0x5FF6A39</ip>
<obj>/memory_tests/couchbase-python-client/couchbase/pycbc_core.so</obj>
<fn>_object* build_exception_from_context<couchbase::error_context::key_value>(couchbase::error_context::key_value const&, char const*, int, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >)</fn>
</frame>
<frame>
<ip>0x6082F16</ip>
<obj>/memory_tests/couchbase-python-client/couchbase/pycbc_core.so</obj>
<fn>void create_result_from_get_operation_response<couchbase::operations::get_response>(char const*, couchbase::operations::get_response const&, _object*, _object*, std::shared_ptr<std::promise<_object*> >, result*)</fn>
</frame>
<frame>
<ip>0x6089D26</ip>
<obj>/memory_tests/couchbase-python-client/couchbase/pycbc_core.so</obj>
<fn>void couchbase::bucket::execute<couchbase::operations::get_request, void do_get<couchbase::operations::get_request>(connection&, couchbase::operations::get_request&, _object*, _object*, std::shared_ptr<std::promise<_object*> >, result*)::{lambda(couchbase::operations::get_response)#1}>(couchbase::operations::get_request, void do_get<couchbase::operations::get_request>(connection&, couchbase::operations::get_request&, _object*, _object*, std::shared_ptr<std::promise<_object*> >, result*)::{lambda(couchbase::operations::get_response)#1}&&)::{lambda(std::error_code, std::optional<couchbase::io::mcbp_message>)#1}::operator()(void do_get<couchbase::operations::get_request>(connection&, couchbase::operations::get_request&, _object*, _object*, std::shared_ptr<std::promise<_object*> >, result*)::{lambda(couchbase::operations::get_response)#1}&&, couchbase::io::mcbp_message)</fn>
</frame>
<frame>
<ip>0x608A611</ip>
<obj>/memory_tests/couchbase-python-client/couchbase/pycbc_core.so</obj>
<fn>std::_Function_handler<void (std::error_code, std::optional<couchbase::io::mcbp_message>), couchbase::utils::movable_function<void (std::error_code, std::optional<couchbase::io::mcbp_message>)>::wrapper<void couchbase::bucket::execute<couchbase::operations::get_request, void do_get<couchbase::operations::get_request>(connection&, couchbase::operations::get_request&, _object*, _object*, std::shared_ptr<std::promise<_object*> >, result*)::{lambda(couchbase::operations::get_response)#1}>(couchbase::operations::get_request, void do_get<couchbase::operations::get_request>(connection&, couchbase::operations::get_request&, _object*, _object*, std::shared_ptr<std::promise<_object*> >, result*)::{lambda(couchbase::operations::get_response)#1}&&)::{lambda(std::error_code, std::optional<couchbase::io::mcbp_message>)#1}, void> >::_M_invoke(std::_Any_data const&, std::error_code&&, std::optional<couchbase::io::mcbp_message>&&)</fn>
</frame>
<frame>
<ip>0x6092B4C</ip>
<obj>/memory_tests/couchbase-python-client/couchbase/pycbc_core.so</obj>
<fn>couchbase::operations::mcbp_command<couchbase::bucket, couchbase::operations::get_request>::invoke_handler(std::error_code, std::optional<couchbase::io::mcbp_message>)</fn>
</frame>
<frame>
<ip>0x60AD272</ip>
<obj>/memory_tests/couchbase-python-client/couchbase/pycbc_core.so</obj>
<fn>couchbase::operations::mcbp_command<couchbase::bucket, couchbase::operations::get_request>::send()::{lambda(std::error_code, couchbase::io::retry_reason, couchbase::io::mcbp_message&&)#1}::operator()(std::error_code, couchbase::io::retry_reason, couchbase::io::mcbp_message&&)</fn>
</frame>
<frame>
<ip>0x5FED3B6</ip>
<obj>/memory_tests/couchbase-python-client/couchbase/pycbc_core.so</obj>
<fn>operator()</fn>
<dir>/usr/include/c++/9/bits</dir>
<file>std_function.h</file>
<line>688</line>
</frame>
<frame>
<ip>0x5FED3B6</ip>
<obj>/memory_tests/couchbase-python-client/couchbase/pycbc_core.so</obj>
<fn>operator()<std::error_code, couchbase::io::retry_reason, couchbase::io::mcbp_message></fn>
<dir>/memory_tests/couchbase-python-client/deps/couchbase-cxx-client/couchbase/utils</dir>
<file>movable_function.hxx</file>
<line>40</line>
</frame>
<frame>
<ip>0x5FED3B6</ip>
<obj>/memory_tests/couchbase-python-client/couchbase/pycbc_core.so</obj>
<fn>std::_Function_handler<void (std::error_code, couchbase::io::retry_reason, couchbase::io::mcbp_message&&), couchbase::utils::movable_function<void (std::error_code, couchbase::io::retry_reason, couchbase::io::mcbp_message&&)>::wrapper<std::function<void (std::error_code, couchbase::io::retry_reason, couchbase::io::mcbp_message&&)>, void> >::_M_invoke(std::_Any_data const&, std::error_code&&, couchbase::io::retry_reason&&, couchbase::io::mcbp_message&&)</fn>
<dir>/usr/include/c++/9/bits</dir>
<file>std_function.h</file>
<line>300</line>
</frame>
</stack>
</error>
When building the KV error map, build_kv_error_map_info(), creates two memory leaks by adding strings directly to the err_info dict using the PyDict_SetItemString().
To fix the leak:
created a PyObject* that stores the string
add the created PyObject* to the err_info dict
decrement the created PyObject* after it has been added to the dict
Leak:
if (-1 == PyDict_SetItemString(err_info, "name", PyUnicode_FromString(error_info.name.c_str()))) { PyErr_Print(); PyErr_Clear(); } if (-1 == PyDict_SetItemString(err_info, "description", PyUnicode_FromString(error_info.description.c_str()))) { PyErr_Print(); PyErr_Clear(); }
Also, call tp_free() in the exception_base destructor (see associated ticket https://couchbasecloud.atlassian.net/browse/PYCBC-1382#icft=PYCBC-1382).