diff --git a/src/content/docs/how-to/synchronous-connection.mdx b/src/content/docs/how-to/synchronous-connection.mdx index 325eefce..13a30280 100644 --- a/src/content/docs/how-to/synchronous-connection.mdx +++ b/src/content/docs/how-to/synchronous-connection.mdx @@ -13,12 +13,12 @@ Starting with version 2.1, Glide introduces a new synchronous Python client, alo The following features were added to the sync client in subsequent releases: -| Feature | Version | -| ----------------------- | ----------------------------------------------------------------------- | -| Cluster Scan | [v2.2.0](https://github.com/valkey-io/valkey-glide/releases/tag/v2.2.0) | -| In-flight Request Limit | [v2.3.0](https://github.com/valkey-io/valkey-glide/releases/tag/v2.3.0) | -| OpenTelemetry | [v2.3.0](https://github.com/valkey-io/valkey-glide/releases/tag/v2.3.0) | -| PubSub | [v2.3.0](https://github.com/valkey-io/valkey-glide/releases/tag/v2.3.0) | +| Feature | Version | +| ------------------------ | ----------------------------------------------------------------------- | +| Cluster Scan | [v2.2.0](https://github.com/valkey-io/valkey-glide/releases/tag/v2.2.0) | +| In-flight Request Limit | [v2.3.0](https://github.com/valkey-io/valkey-glide/releases/tag/v2.3.0) | +| OpenTelemetry | [v2.3.0](https://github.com/valkey-io/valkey-glide/releases/tag/v2.3.0) | +| PubSub | [v2.3.0](https://github.com/valkey-io/valkey-glide/releases/tag/v2.3.0) | ## Installing @@ -78,9 +78,9 @@ The following example compares the synchronous vs asynchronous clients usage for ) addresses = [ - NodeAddress(host="primary.example.com", port=6379), - NodeAddress(host="replica1.example.com", port=6379), - NodeAddress(host="replica2.example.com", port=6379) + NodeAddress(host="primary.example.com", port=6379), + NodeAddress(host="replica1.example.com", port=6379), + NodeAddress(host="replica2.example.com", port=6379) ] client_config = GlideClientConfiguration(addresses) @@ -97,3 +97,107 @@ The following example compares the synchronous vs asynchronous clients usage for ``` + + +## Zero-Copy Buffers + +When reading large values (KV-cache blobs, embeddings, serialized tensors), the default `get` and `mget` commands allocate a new Python `bytes` object for each value returned. For high-throughput workloads this allocation overhead adds up. Zero-copy buffers let you pass pre-allocated memory so values are written directly into your buffers without intermediate allocations. + +:::caution[Sync Client Only] +Zero-copy buffer parameters are available only on the synchronous client (`glide_sync`). The async client does not support this feature. +::: + +### Single key: `get` with buffer + +Pass a writable `memoryview` to `get` to receive the value directly in your buffer: + +```python +buf = bytearray(4096) +view = memoryview(buf) + +result = client.get("my_key", buffer=view) +if result is not None: + bytes_written = int(result) + value = bytes(buf[:bytes_written]) +``` + +When a `buffer` is provided, `get` returns the number of bytes written as a byte string (e.g. `b'4096'`), or `None` if the key does not exist. + +### Multiple keys: `mget` with buffers + +The `buffers` parameter extends the same pattern to multi-key reads: + +```python +keys = ["embedding:1", "embedding:2", "embedding:3"] +BUFFER_SIZE = 8192 + +# Pre-allocate one buffer per key +buffers = [memoryview(bytearray(BUFFER_SIZE)) for _ in keys] + +results = client.mget(keys, buffers=buffers) + +for i, result in enumerate(results): + if result is None: + print(f"{keys[i]}: missing") + else: + bytes_written = int(result) + # Access the value directly from the buffer — no extra copy + value = bytes(buffers[i][:bytes_written]) + print(f"{keys[i]}: {bytes_written} bytes") +``` + +:::tip[Buffer Sizing] +Size buffers appropriately for your expected values. If a value exceeds its buffer capacity, a `RequestError` is raised. For variable-sized data, consider using a conservative upper bound or implementing retry logic with larger buffers. +::: + +Each element in the returned list is either: +- A byte string containing the number of bytes written (e.g. `b'8192'`) — read the value from `buffers[i][:int(result)]` +- `None` — the key does not exist + +### Requirements + +- **Writable and C-contiguous**: Each buffer must be a writable, C-contiguous `memoryview`. The simplest way to create one is `memoryview(bytearray(size))`. +- **Same length as keys**: The `buffers` list must have exactly the same number of elements as `keys`. + +### Error Handling + +- **Buffer too small**: If a value exceeds its buffer capacity, a `RequestError` with "exceeds buffer capacity" is raised. +- **Length mismatch**: If the `buffers` list length differs from `keys`, a `ValueError` is raised. +- **Invalid buffer type**: Non-writable or non-C-contiguous buffers raise a `TypeError`. + +```python +try: + small_buf = memoryview(bytearray(10)) # Too small for large values + result = client.get("large_key", buffer=small_buf) +except RequestError as e: + if "exceeds buffer capacity" in str(e): + # Retry with a larger buffer + larger_buf = memoryview(bytearray(8192)) + result = client.get("large_key", buffer=larger_buf) +``` + +### Complete example + +```python +from glide_sync import GlideClient, GlideClientConfiguration, NodeAddress + +config = GlideClientConfiguration([NodeAddress("localhost", 6379)]) +client = GlideClient.create(config) + +# Store some test data +client.mset({"vec:1": b"A" * 4096, "vec:2": b"B" * 2048}) + +# Read with zero-copy buffers +keys = ["vec:1", "vec:2", "vec:missing"] +bufs = [memoryview(bytearray(8192)) for _ in keys] + +results = client.mget(keys, buffers=bufs) +# results == [b'4096', b'2048', None] + +for i, r in enumerate(results): + if r is None: + print(f"{keys[i]} does not exist") + else: + n = int(r) + print(f"{keys[i]}: {n} bytes -> {bytes(bufs[i][:n])[:20]}...") +``` \ No newline at end of file