Skip to content

Commit cbf08b5

Browse files
mykauldkropachev
authored andcommitted
Address LZ4 review feedback
1 parent c534cf6 commit cbf08b5

5 files changed

Lines changed: 35 additions & 22 deletions

File tree

benchmarks/bench_lz4.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,8 @@ def py_lz4_decompress(byts):
7979
def make_payload(size):
8080
"""Generate a pseudo-realistic compressible payload."""
8181
# Mix of repetitive and random-ish bytes to simulate CQL result rows.
82-
chunk = (b"row_value_" + os.urandom(6)) * (size // 16 + 1)
83-
return chunk[:size]
82+
blocks = [b"row_value_" + os.urandom(6) for _ in range(size // 16 + 1)]
83+
return b"".join(blocks)[:size]
8484

8585

8686
def bench(label, func, arg, inner=INNER, repeat=REPEAT):

cassandra/cython_lz4.pyx

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -55,23 +55,22 @@ cdef extern from *:
5555
# theoretical maximum is ~2 GiB. We use 256 MiB as a practical upper
5656
# bound (matching the server's default frame size limit) to avoid
5757
# accidentally allocating multi-GiB buffers on corrupt headers.
58-
DEF MAX_DECOMPRESSED_LENGTH = 268435456 # 256 MiB
58+
cdef enum:
59+
MAX_DECOMPRESSED_LENGTH = 268435456 # 256 MiB
5960

6061
# LZ4_MAX_INPUT_SIZE from lz4.h — the LZ4 C API uses C int (32-bit
6162
# signed) for sizes, so we must reject Python bytes objects that
6263
# exceed this before casting Py_ssize_t down to int.
63-
DEF LZ4_MAX_INPUT_SIZE = 0x7E000000 # 2 113 929 216 bytes
64+
cdef enum:
65+
LZ4_MAX_INPUT_SIZE = 0x7E000000 # 2 113 929 216 bytes
6466

6567
# Maximum LZ4_compressBound value for which we use a fixed-size buffer on
66-
# the C stack instead of malloc. 128 KiB is well within the default
67-
# 8 MiB thread stack size (POSIX) / 1 MiB (Windows) and covers CQL frames
68-
# up to ~127 KiB uncompressed — the vast majority of real traffic. Larger
69-
# frames fall back to heap allocation. A plain fixed-size array is used
70-
# (rather than alloca()) because alloca() is declared in <alloca.h>, which
71-
# does not exist on Windows/MSVC (it ships _alloca() in <malloc.h> instead
72-
# with subtly different semantics) — a fixed-size array avoids that
73-
# platform split entirely while remaining just as cheap.
74-
DEF STACK_ALLOC_THRESHOLD = 131072 # 128 KiB
68+
# the C stack instead of malloc. 16 KiB covers common CQL frames while
69+
# keeping the per-call stack frame small. Larger frames fall back to heap
70+
# allocation. A plain fixed-size array is used instead of alloca(), which
71+
# is not portable across Windows/MSVC and POSIX.
72+
cdef enum:
73+
STACK_ALLOC_THRESHOLD = 16384 # 16 KiB
7574

7675

7776
cdef extern from "lz4.h":

conanfile.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ def generate(self) -> None:
2626
build_req = self._conanfile.dependencies.build # tool_requires
2727
test_req = self._conanfile.dependencies.test
2828

29-
content_buffer = ""
29+
include_dirs = []
30+
library_dirs = []
3031

3132
# Filter the build_requires not activated for any requirement
3233
dependencies = [tup for tup in list(host_req.items()) + list(build_req.items()) + list(test_req.items()) if not tup[0].build]
@@ -37,17 +38,19 @@ def generate(self) -> None:
3738
continue
3839
include_dir = Path(dep.package_folder) / 'include'
3940
package_dir = Path(dep.package_folder) / 'lib'
40-
content_buffer += json.dumps(dict(include_dirs=str(include_dir), library_dirs=str(package_dir)))
41+
include_dirs.append(str(include_dir))
42+
library_dirs.append(str(package_dir))
4143

42-
save(self._conanfile, CONAN_COMMANDLINE_FILENAME, content_buffer)
44+
content = json.dumps(dict(include_dirs=include_dirs, library_dirs=library_dirs))
45+
save(self._conanfile, CONAN_COMMANDLINE_FILENAME, content)
4346
self._conanfile.output.info(f"Generated {CONAN_COMMANDLINE_FILENAME}")
4447

4548

4649
class python_driverConan(ConanFile):
4750
win_bash = False
4851

4952
settings = "os", "compiler", "build_type", "arch"
50-
requires = "libev/4.33"
53+
requires = "libev/4.33", "lz4/1.9.4"
5154

5255
def layout(self):
5356
basic_layout(self)

setup.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,8 +155,8 @@ def eval_env_var_as_array(varname):
155155
conan_envfile = Path(__file__).parent / 'build-release/conan/conandeps.env'
156156
if conan_envfile.exists():
157157
conan_paths = json.loads(conan_envfile.read_text())
158-
libev_includes.extend([conan_paths.get('include_dirs')])
159-
libev_libdirs.extend([conan_paths.get('library_dirs')])
158+
libev_includes.extend(conan_paths.get('include_dirs', []))
159+
libev_libdirs.extend(conan_paths.get('library_dirs', []))
160160

161161
libev_ext = Extension('cassandra.io.libevwrapper',
162162
sources=['cassandra/io/libevwrapper.c'],

tests/unit/cython/test_cython_lz4.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,11 +74,11 @@ def test_round_trip_64kb(self):
7474
self.assertEqual(lz4_decompress(lz4_compress(data)), data)
7575

7676
def test_round_trip_heap_buffer(self):
77-
"""Inputs above the 128 KiB stack threshold use the malloc path."""
78-
# LZ4_compressBound(131072) = 131072 + 131072/255 + 16 = 131602,
77+
"""Inputs above the 16 KiB stack threshold use the malloc path."""
78+
# LZ4_compressBound(16384) = 16384 + 16384/255 + 16 = 16464,
7979
# which exceeds STACK_ALLOC_THRESHOLD, so lz4_compress must fall
8080
# back to the heap buffer and free it on success.
81-
data = os.urandom(131072)
81+
data = os.urandom(16384)
8282
self.assertEqual(lz4_decompress(lz4_compress(data)), data)
8383

8484
def test_round_trip_empty(self):
@@ -207,6 +207,17 @@ def test_cross_compat_64kb(self):
207207
def test_cross_compat_empty(self):
208208
self._check_cross_compat(b"")
209209

210+
def test_connection_prefers_cython_codec(self):
211+
"""The connection layer selects the Cython codec when present."""
212+
from cassandra import connection
213+
214+
self.assertIs(connection.locally_supported_compressions['lz4'][0],
215+
lz4_compress)
216+
self.assertIs(connection.locally_supported_compressions['lz4'][1],
217+
lz4_decompress)
218+
self.assertIs(connection.segment_codec_lz4.compressor, lz4_compress)
219+
self.assertIs(connection.segment_codec_lz4.decompressor, lz4_decompress)
220+
210221

211222
if __name__ == "__main__":
212223
unittest.main()

0 commit comments

Comments
 (0)