fix memory leak issue - #1179
Conversation
|
Hi @AKASHHMISHRA1, I plan to merge your fix using #1180. I will add you as a co-author to give proper credit. |
|
Thanks @juliannguyen4 |
|
I'll publish a version in our custom PyPI repo for you to test out. It should be ready by today. |
Thanks @juliannguyen4 , let me know when it’s done |
|
Here's a build available with the memory leak fix. Please let me know if it works
|
|
Hi @juliannguyen4 ,I can’t test this change unless it is available on pypi aerospike package. I see that 19.3.0 is not available there yet. |
|
That index URL shouldn't require credentials. Does running that command I posted work for you? Not all the changes for 19.3.0 are staged yet, so I cannot publish this to PyPI yet. (the command I linked is for installing an early preview build) Note: I may have to override the build at that URL I sent due to technical restraints with our JFrog build system, so I would recommend saving the build locally. |
cc: @juliannguyen4 @dwelch-spike
Summary
deserialize_based_on_as_bytes_type()insrc/main/serializer.cis missing abreak;statement at the end of theAS_BYTES_PYTHONcase, causing execution to fall through into theAS_BYTES_BLOBcase immediately below it. This results in a permanent, one-object-per-call native memory leak whenever a bin holding a legacyAS_BYTES_PYTHONvalue is read back and no custom deserializer is registered.Root cause
This case was rewritten in
13.0.0to remove the automatic pickle-based deserialization ofAS_BYTES_PYTHONvalues (per the documented breaking change in that release). The rewrite dropped the internalpickle.loads()call and replaced it with a directPyByteArray_FromStringAndSize()call — correct on its own — but the closingbreak;that the original, pre-13.0.0 block had (case AS_BYTES_PYTHON: { ... } break;) was not carried over:Because a C
switchonly stops at an explicitbreak, execution continues directly intoAS_BYTES_BLOB's handling. When no custom deserializer is registered (the common case), that branch allocates a second, independentPyObject(viaPyBytes_FromStringAndSize) from the same underlying bytes and overwrites*retvalwith it.The first object — the
PyByteArraycreated for theAS_BYTES_PYTHONcase — is never released.Py_DECREFis never called on it, and the only pointer to it (*retval) is clobbered before the caller ever receives it. It becomes permanently unreachable while its refcount remains at 1, so it can never be freed for the lifetime of the process.This fires on every single deserialization of an
AS_BYTES_PYTHON-typed bin with no registered custom deserializer — i.e., on every read of a value written by a pre-13.0.0 client (or a client relying on the legacy Python-object auto-serialization) that hasn't been rewritten in a newer format.Impact
In a long-running server process reading such bins at any meaningful request rate, this produces steady, unbounded RSS growth proportional to read volume — eventually leading to OOM.
How this was found
While investigating a memory leak in a production service, we bisected across aerospike-client-python versions under sustained load and found a clean, reproducible boundary:
All versions ≤ 12.0.0 retain the original, properly
break-terminatedAS_BYTES_PYTHONcase. All versions ≥ 13.0.0 tested contain the fallthrough. The leak rate observed was consistent with one orphaned object perAS_BYTES_PYTHONdeserialization, matching this code path exactly.We confirmed the same code path (missing
break) is still present as of the currentmaster/19.3.0rc1.Fix
Add the missing
break;at the end of theAS_BYTES_PYTHONcase, restoring the original control flow: convert to a bytearray, set the output, and stop — without falling into unrelated blob-deserialization handling.Testing
This is a minimal, single-line change restoring the pre-13.0.0 control flow for this case. No behavioral change is intended for any other bin type (
AS_BYTES_BLOB,AS_BYTES_LIST,AS_BYTES_MAP, etc.) — only the accidental fallthrough intoAS_BYTES_BLOBfromAS_BYTES_PYTHONis removed.