Replication streaming compression (per-replica) — review vs #3531 base#18
Merged
roshkhatri merged 37 commits intoJun 23, 2026
Merged
Conversation
Adds replication wire compression on top of valkey-io#3531 with lz4 as the first supported codec for the incremental replication stream. The replication stream from primary to replica is wrapped in a VKCS envelope (using STREAM_KIND_REPL) and compressed as a single long-lived frame at the per-replica buffer layer. Default behavior is unchanged with 'replcompression no'; existing replicas without the new capability stay uncompressed. Negotiation is per-replica via the existing PSYNC handshake; a new REPLICA_CAPA_COMPRESSION capability lets each side opt in independently. Compression runs inline on the IO thread that owns the replica's write job; no dedicated compression thread, no IPC, no reordering. Optional sticky thread affinity (lazy ownership + event-driven rebalance) keeps the long-lived LZ4 frame state on a single IO thread for cache locality. Configs: replcompression bool, default no repl-compression-thread-affinity bool, default yes Internal constants: REPLICA_CAPA_COMPRESSION (1 << 4) REPL_COMPRESSION_ALGO ALGO_LZ4 REPL_COMPRESSION_LEVEL 0 (LZ4 fast mode) REPL_COMPRESSION_BATCH_LIMIT 1 MB raw input per dispatch REPL_STREAM_DECODER_OUTPUT_MAX 256 MB INFO replication per-replica fields: compression=lz4, compressed_bytes, uncompressed_bytes, compression_ratio, compression_errors, compression_cpu_usec, debug_compression_pending_drains, debug_thread_switches INFO replication server-level (replica side): repl_decompression_errors, repl_decompression_cpu_usec, repl_decompressed_bytes_total, repl_apply_cpu_usec, repl_apply_batches CI adds a test-replication-compression job that runs the replication-tagged integration tests with replcompression=yes to exercise compression across the broader replication test surface. Tests: 18 streamReader push-mode unit tests + 3 replCompression unit tests + 27 integration tests. Performance (BlockMesh tweets, 3M keys x ~315 byte JSON values, 1,073 MB uncompressed per replica, 30 clients, pipeline 50, 2 cross-region replicas): LZ4 level 0 (default): 0.48 ratio, 52% bandwidth saved, 2.5s compression CPU per replica, <1% throughput overhead vs uncompressed baseline. Affinity ON vs OFF: throughput unchanged (118.6K vs 118.1K keys/s) but thread switches drop from ~800K to ~30 per replica. ZSTD support follows in valkey-io#3798. Related to valkey-io#3531. Signed-off-by: Roshan Khatri <rvkhatri@amazon.com>
# Conflicts: # src/io_threads.c # src/networking.c # src/replication.c
Signed-off-by: Roshan Khatri <rvkhatri@amazon.com>
The compression CI job (--config replcompression yes) ran replication-buffer.tcl and the dual-channel buffer-memory tests, which assert exact replication buffer/backlog memory and byte volumes. Compression legitimately changes those (per-replica codec buffers add ~1MB scratch; fewer wire bytes let replicas keep up), so the assertions fail under compression even though replication is correct. Drop the repl-compression tag from replication-buffer.tcl and the two dual-channel blocks holding the memory tests; they still run uncompressed in the regular job. Functional dual-channel coverage stays in the compression job. Signed-off-by: Roshan Khatri <rvkhatri@amazon.com>
…io#3897) ## Summary Fixes valkey-io#3008 > lets add assert checking that the object has a key in dbUntrackKeyWithVolatileItems and dbTrackKeyWithVolatileItems to be able to get a more explicit error in these cases This is addressed in this PR. Signed-off-by: ydsakshi <ydsakshi023@gmail.com>
clusterNode.shard_id is a fixed-size char[CLUSTER_NAMELEN] buffer that is not guaranteed to be NUL-terminated, so it must be printed with %.40s. This was introduced in valkey-io#2510. Signed-off-by: Binbin <binloveplay1314@qq.com>
…valkey-io#3941) Since valkey-io#2449 made the failover delay relative to cluster-node-timeout. Now delay = min(cluster-node-timeout / 30, 500), any cluster-node-timeout below 30, including the legal minimum 0 will collapses delay to zero, and `x % 0` is undefined behaviour. Signed-off-by: Binbin <binloveplay1314@qq.com>
…thakaggarwal97/valkey into replication-streaming-compression-pr # Conflicts: # src/compression_stream.c # src/compression_stream.h # src/config.c # src/rdb.c # src/server.h # src/unit/test_compression.cpp # tests/integration/rdb-compression.tcl # valkey.conf
…tate and plaintext passthrough Signed-off-by: Roshan Khatri <rvkhatri@amazon.com>
…alkey-io#3938) This regression is still present in 9.1 GA as the cherrypick of revert commit was missed during release. Re-applies valkey-io#3544 (reverted in valkey-io#3756 due to ~20% SET regression) with the performance fix from valkey-io#3760. **Root Cause:** The original valkey-io#3544 changed `tryOffloadFreeObjToIOThreads` to only offload the SDS buffer free to IO threads, freeing the `robj` shell on the main thread. I carried out profiling for the change and it showed that freeing the `robj` shell on the main thread became the prime main-thread hotspot (~10% CPU), while IO threads shifted from doing real `jemalloc` work to spinning idle on `spmcDequeue`. **Fix**: Keep `tryOffloadFreeObjToIOThreads` offloading the entire robj (`decrRefCount`) to the IO thread. Cross-thread `zfree` is safe with `jemalloc`. This PR includes all cleanup work from valkey-io#3544 so - - `trySendWriteToIOThreads`: defer clearing `last_header` until after successful enqueue - `evictClients`: simplified bookkeeping - Queue sizes as runtime parameters instead of compile-time macros - IO ignition policy using `stat_active_time` instead of `getrusage` - Function renames (`IOThreadFreeArgv` --> `ioThreadFreeArgv`, etc.) and doc comments **Benchmark** on (Graviton4 c8gb.metal-48xl): Config: SET, 128B values, 9 IO threads, pipeline=10, 1600 clients - Same as Valkey official method | Version | Throughput | |---------|-----------| | Unstable + original valkey-io#3544 | ~1,554K rps | | Unstable + this PR | ~2,116K rps | <details> <summary>Diff vs original valkey-io#3544 (perf fix)</summary> ```diff diff --git a/src/io_threads.c b/src/io_threads.c --- a/src/io_threads.c +++ b/src/io_threads.c @@ // IO thread handler case JOB_REQ_FREE_OBJ: - zfree(data); + decrRefCount(data); break; @@ // tryOffloadFreeObjToIOThreads - /* We offload only the free of the ptr that may be allocated by the I/O thread. - * The object itself was allocated by the main thread and will be freed by the main thread. */ - void *job = tagJob(sdsAllocPtr(objectGetVal(obj)), JOB_REQ_FREE_OBJ); + void *job = tagJob(obj, JOB_REQ_FREE_OBJ); if (unlikely(spmcEnqueue(&io_shared_inbox, job) == false)) return C_ERR; - objectSetVal(obj, NULL); - decrRefCount(obj); io_jobs_submitted++; ``` </details> --------- Signed-off-by: Roshan Khatri <rvkhatri@amazon.com>
…val (fix macOS -Werror) Signed-off-by: Roshan Khatri <rvkhatri@amazon.com>
…3949) This test runs `start_cluster 4 5` and manually attaches an extra replica with `CLUSTER REPLICATE`, so one primary ends up with two replicas while the others have one. It then pauses and restarts all 8 nodes and asserts that every node's shard id is unchanged. This is why the test fails: - A primary is termed "orphaned" when it owns slots and is flagged `MIGRATE_TO`, but currently has zero working replicas. - When a primary is orphaned, the cluster moves a spare replica to it. A replica is only spare if its current primary would still have more than `cluster-migration-barrier` replicas after it leaves (default barrier is 1). - A migrating replica adopts the target primary's shard id, so its shard id changes. During the staggered restart, a primary can be left with no reachable replica for a short window and become orphaned. Once the cluster returns to OK, the primary that has two replicas is the only one eligible to donate, so one of its replicas migrates to the orphaned primary and takes on its shard id. The fix is to set `cluster-allow-replica-migration no` on this block so replicas stay with their original primaries across the restart. Replica migration is not what this test is checking. Fixes valkey-io#3914 Signed-off-by: Sarthak Aggarwal <sarthagg@amazon.com>
…io#3947) The latency report told users to run 'echo never > .../enabled' to disable THP, but checkTHPEnabled recommends 'echo madvise' and notes that 'madvise' or 'never' are both valid. Make createLatencyReport consistent: suggest 'madvise' in the command and mention both values in the accompanying note. This advice was originally added in 1461f02. Signed-off-by: Binbin <binloveplay1314@qq.com>
…et (valkey-io#3950) When a hash has many fields whose expirations fall in the same volatile-set time-bucket (>127 entries), that bucket is encoded as a hashtable (HT). The active field-expiry path drains expired entries via vsetBucketRemoveExpired_HASHTABLE(), but it is bounded by the per-key expire quota (max_count, from dbReclaimExpiredFields). When the quota stops the drain one entry short, the function only collapsed the bucket to NONE at size 0 and left it as an HT bucket holding a single entry. That violates the invariant enforced by removeFromBucket_HASHTABLE(), which asserts hashtableSize(ht) > 0 after a delete and downgrades an HT bucket to SINGLE at size 1. A later normal removal of that lone field -- HDEL, or a cross-bucket HSETEX update routed through removeEntryFromRaxBucket() -- deletes the sole entry, drops the size 1 -> 0, and trips the assertion. Fix: Apply the same downgrade in the expiry path: when draining leaves exactly one entry, downgrade the HT bucket to SINGLE so the invariant holds. Behavior is unchanged when the bucket drains fully (-> NONE) or retains two or more entries (-> HT, as the normal removal path already tolerates). Add a regression test that fills one time-bucket past the HT threshold, expires all but one entry, then removes the survivor through the normal path; it crashed before this fix and passes after. Signed-off-by: Ran Shidlansik <ranshid@amazon.com>
…er of each shard (valkey-io#3946) We previously attempted to set `cluster-node-timeout` to 15000 in valkey-io#2793 but failed. This was because we did not explicitly specify it and relied on the default value, but `start_cluster` internally sets it to 3000. Closes valkey-io#3932. Signed-off-by: Binbin <binloveplay1314@qq.com>
valkey-io#3964) Database-level ACL valkey-io#2309 introduced `alldbs` rule that was explicit for all users and because of that previous versions no longer had the ability to parse ACL strings produced by later versions. Omit `alldbs` in `ACLDescribeSelector()`, that is used in `ACL SAVE/LOAD` and `CONFIG REWRITE` command paths so that downgrades would be possible if new feature was not used (`db=` and `resetdbs` rules). Keep `ACL GETUSER` command's output as is and return `alldbs` in `databases` field because of command's field-value format. Add test to check that `ACL LIST` omits implicit `alldbs` and add check to existing `ACL SAVE` and `CONFIG REWRITE` tests. Fixes valkey-io#3915 Signed-off-by: Daniil Kashapov <daniil.kashapov.ykt@gmail.com>
Sarthak Aggarwal (@sarthakaggarwal97) has been granted write permissions by maintainer consensus. This adds him to the Current Committers list in MAINTAINERS.md. Signed-off-by: Madelyn Olson <madelyneolson@gmail.com>
Signed-off-by: Roshan Khatri <rvkhatri@amazon.com>
…alkey-io#3920) ## Problem A crafted zipmap entry can set the value length to a value near `UINT32_MAX` so that the `l + e` sum (value length + one-byte free space) wraps in `unsigned int` arithmetic. The wrapped sum advances the validation cursor by a tiny amount, leaving `p` inside the buffer, so the `OUT_OF_RANGE` check passes and `zipmapValidateIntegrity` wrongly returns success. The field-length path has the same shape — advancing `p` by a ~4GB length wraps the pointer on 32-bit builds. `zipmapValidateIntegrity` is always called with `deep=1` from `rdb.c` when loading `RDB_TYPE_HASH_ZIPMAP`, **including via `RESTORE`**, so any client with `RESTORE` access can submit a payload that passes validation. On 32-bit platforms this leads to out-of-bounds access during the subsequent zipmap→listpack conversion. On 64-bit the downstream `lpSafeToAdd` cap happens to reject it (the raw ~4GB length exceeds `LISTPACK_MAX_SAFETY_SIZE`), but the validator should not accept a malformed payload in the first place — this is the function whose sole job is to reject it. ## Fix Bounds-check the attacker-controlled length against the bytes remaining in the zipmap, in 64-bit space, **before** any pointer arithmetic, for both the field-length and value-length paths. ## Testing - `tests/integration/corrupt-dump.tcl`: a `RESTORE`-path test exercising the full attack surface; asserts rejection and that the server stays up. - Verified the test **fails on the pre-fix code** (validator accepts the value-length payload) and **passes after the fix**, confirmed by stashing the fix during the integration run. - Full `integration/corrupt-dump` suite: 76 passed, 0 failed. > [!NOTE] > Found via structure-aware fuzzing of the RESTORE path. This issue was generated by AI but verified, with love, by a human. Signed-off-by: Madelyn Olson <madelyneolson@gmail.com>
…valkey-io#3921) ## Problem A crafted `RESTORE` payload can store a `NAN` score in a listpack-encoded sorted set. The integrity validation (`lpValidateIntegrityAndDups`) only checks the listpack *structure* and member uniqueness — it does not check score validity — so the payload is accepted on load. When the sorted set is later converted to a skiplist (e.g. when it grows past `zset-max-listpack-entries`, or via any operation that triggers conversion), `zslInsertNode()` asserts the score is not `NAN` (`t_zset.c:260`) and the server aborts. **Any client with `RESTORE` access can remotely crash the server.** The skiplist RDB format (`RDB_TYPE_ZSET` / `RDB_TYPE_ZSET_2`) already rejects `NAN` scores at load time (`rdb.c`, "Zset with NAN score detected"). The listpack format (`RDB_TYPE_ZSET_LISTPACK`) had no equivalent check. ## Reproduction ``` RESTORE k 0 "\x11\x19\x19\x00\x00\x00\x04\x00\x82m1\x03\x83nan\x04\x82m2\x03\x832.5\x04\xFF\x50\x00...." # loads OK, then: ZADD k 9 x # forces listpack->skiplist conversion -> serverAssert(!isnan(node->score)) -> SIGABRT ``` ## Fix Add `zzlValidateScores()`, which scans the scores of a listpack zset after structural validation and rejects the payload if any score is `NAN`. Mirrors the existing skiplist-format check. `inf`/`-inf` and large finite scores remain accepted (only `NAN` is rejected), matching normal `ZADD` semantics. ## Testing - `tests/integration/corrupt-dump.tcl`: a `RESTORE`-path test asserting rejection and that the server stays up. - Verified the test **fails on the pre-fix code** (server crashes on conversion) and **passes after the fix**, by stashing the fix during the run. - Confirmed valid zsets, including `inf`/`-inf`/large finite scores, still load and convert correctly. - Full `integration/corrupt-dump` suite: 74 passed, 0 failed. > [!NOTE] > Found via structure-aware fuzzing of the RESTORE path. This issue was generated by AI but verified, with love, by a human. Signed-off-by: Madelyn Olson <madelyneolson@gmail.com>
When `valkey-cli --rdb` trims the EOF marker, a failed `ftruncate` only printed a warning but still reported success and exited 0, leaving a corrupt RDB file. This makes it exit non-zero on that failure, matching how the `write()` failure is already handled in the same function. (Thank you, Madelyn, for tag teaming this with me.) Signed-off-by: Grace <glucier22@gmail.com>
…ey-io#3959) The Case 3 portion of the test was flaky: after a single round of `CLUSTER DELSLOTS 0` on R0/R1/R2, the cluster could stay in OK state and `wait_for_cluster_state fail` would time out with `Cluster node 1 cluster_state:ok`. The race is between R0's local DELSLOTS and the gossip already in flight from R0. After R1 locally clears slot 0, a stale pre-DELSLOTS packet from R0 (whose myslots still claims slot 0) hits the isSlotUnclaimed fast path in clusterUpdateSlotsConfigWith and rebinds slot 0 back to R0 on R1. See: ``` if (isSlotUnclaimed(j) || server.cluster->slots[j]->configEpoch < senderConfigEpoch || clusterSlotFailoverGranted(j)) { ... clusterDelSlot(j); clusterAddSlot(sender, j); ... } ``` R0's subsequent "no longer claiming" PINGs cannot undo this, because that path only sets owner_not_claiming_slot and never clears slots[j]: ``` if (server.cluster->slots[j] == sender) { /* The slot is currently bound to the sender but the sender is no longer * claiming it. We don't want to unbind the slot yet as it can cause the cluster * to move to FAIL state and also throw client error. Keeping the slot bound to * the previous owner will cause a few client side redirects, but won't throw * any errors. We will keep track of the uncertainty in ownership to avoid * propagating misinformation about this slot's ownership using UPDATE * messages. */ bitmapSetBit(server.cluster->owner_not_claiming_slot, j); } ``` Combined with clusterUpdateState's full-coverage check looking only at slots[j] == NULL, R1 stays at cluster OK forever. ``` if (server.cluster->slots[j] == NULL || ...) { new_state = CLUSTER_FAIL; ... } ``` Rather than fighting the protocol's intentional asymmetry around "soft delete" via gossip, just retry the DELSLOTS pass until all three nodes converge to FAIL. This keeps the test focused on the CLUSTERSCAN error semantics it actually wants to verify. This closes valkey-io#3891. The test was added in valkey-io#3674. Signed-off-by: Binbin <binloveplay1314@qq.com>
## Summary `addReplyCommandSubCommands` unconditionally called `addReplySetLen(c, 0)` when a command has no subcommands, emitting a RESP3 Set type prefix (`~0`) regardless of the `use_map` parameter. The non-empty path (below it) already branches correctly on `use_map` — the empty early-return was simply missing the same logic. In RESP3, `COMMAND INFO <cmd>` returns the subcommands field as a Set (`~0`) instead of an Array (`*0`) for any command without subcommands (e.g. PING). Strict RESP3 client libraries that dispatch on collection type will misinterpret the response. Not visible in RESP2 since both Set and Array use the `*` prefix there. ## Fix Apply the same `use_map` branch to the empty case: - `addReplyMapLen(c, 0)` when `use_map=1` - `addReplyArrayLen(c, 0)` otherwise ## Test Added a `readraw` integration test in `tests/unit/introspection-2.tcl` that inspects the raw wire-level type byte for the subcommands field of `COMMAND INFO ping` in RESP3 mode, asserting `*0` (Array) rather than `~0` (Set). Signed-off-by: Rick Ramsay <49293857+rickrams@users.noreply.github.com> Signed-off-by: rickrams <rickrams@amazon.com>
…ey-io#3811) `off_t` (64-bit), but were read into `int` (32-bit) locals in `genValkeyInfoString()` and `handleBioThreadFinishedRDBDownload()`. This causes INFO replication to report negative `master_sync_total_bytes` during bio disk-based sync when RDB exceeds 2GB. Fix: change the local variable types from `int` to `off_t`. Signed-off-by: chx9 <lovelypiska@outlook.com>
Multi-command parsing in parseMultibulkBuffer (introduced valkey-io#2092) was disabled for replicated clients because the per-command replication offset relied on c->qb_pos as the right boundary of the just-applied command. Replication stream is actually a big pipeline, so if it can be supported, the processing speed of the replica can be improved. Decouple parsing position from application position by introducing a single per-client field that tracks the currently processed command's right boundary in querybuf: client.qb_applied: right boundary of the current command in querybuf. Set by the parsers (for c->argv, = c->qb_pos) and advanced incrementally by consumeCommandQueue() using each queued command's input_bytes (qb_applied += p->input_bytes). parsedCommand.input_bytes already records the number of querybuf bytes consumed to parse one command (multibulk header + each bulk-length line + arg bytes + per-arg CRLFs), see parseMultibulk for mode details. It is a relative quantity, independent of where querybuf has been trimmed since parsing, so it is exactly what's needed to advance qb_applied across multi-command parsing, so no extra per-command snapshot field is required. commandProcessed() now uses c->qb_applied instead of c->qb_pos to advance reploff by exactly the bytes of the just-applied command. beforeNextClient shifts only c->qb_applied (a single variable) when the replicated client's querybuf is trimmed; pending queue entries carry only relative input_bytes and therefore need no fix-up. qb_applied is populated unconditionally on the first-command path so that a client transitioning to replicated mid-command (e.g. SYNCSLOTS ESTABLISH installs slot_migration_job inside its own handler) still has a valid value when commandProcessed() runs. Signed-off-by: Binbin <binloveplay1314@qq.com>
…r fuzzy provenance matches (valkey-io#3933) - update verify-provenance to require near-duplicate evidence for fuzzy provenance matches - normalize master/primary and slave/replica terminology. - print the captured provenance script log in the check workflow. --------- Signed-off-by: Ping Xie <pingxie@outlook.com>
…-io#3982) ## Issue Description `log_crashes` in `tests/instances.tcl` — the multi-instance harness used by both `make test-sentinel` and `make test-cluster` — scans every instance's `err.txt` with `find_valgrind_errors` **unconditionally**: ```tcl set logs [glob */err.txt] foreach log $logs { set res [find_valgrind_errors $log true] if {$res != ""} { puts $res incr ::failed } } ``` The main test framework runs the identical scan only behind a valgrind guard — in `tests/support/server.tcl`, `check_valgrind_errors` is called from `kill_server` inside `if {$::valgrind}`. `find_valgrind_errors`' fallback (in `tests/support/util.tcl`) treats any stderr that lacks a valgrind leak-free summary as an error: ```tcl # Look for the absence of a leak free summary # (happens when the server isn't terminated properly). if {(![regexp -- {definitely lost: 0 bytes} $buf] && ![regexp -- {no leaks are possible} $buf])} { return $buf } ``` When the suite is **not** running under valgrind, that summary is never present, so **any non-empty `err.txt` is flagged as a failure**. ## Concrete trigger On hosts whose CPU does not expose the `constant_tsc` flag (common inside VMs / LXD), every `valkey-server` and `valkey-sentinel` prints to stderr at startup (`src/monotonic.c`): ``` monotonic: x86 linux, 'constant_tsc' flag not present ``` The sentinel suite then reports exactly **10 failures** (5 sentinel + 5 valkey instances) on every run — even though every individual test passes and no assertion fails. The cluster suite shares this harness and is affected identically. On hosts that expose `constant_tsc` the message is never printed, so the bug is invisible — which is why it only surfaces in certain CI/VM environments. ## Fix Wrap the valgrind `err.txt` scan in `if {$::valgrind}`, mirroring `server.tcl`. The sanitizer scan immediately below is left unguarded, matching `check_sanitizer_errors`, which always runs. No test coverage is lost. ```tcl if {$::valgrind} { set logs [glob */err.txt] foreach log $logs { set res [find_valgrind_errors $log true] if {$res != ""} { puts $res incr ::failed } } } ``` Signed-off-by: Smail Kourta <smail.kourta@canonical.com>
…on (valkey-io#3968) Adds REUSE 3.3 structure (REUSE.toml, LICENSES/) covering all source files and vendored dependencies. Replaces the non-standard dual-license COPYING file with a standard BSD-3-Clause text. Updates the description of custom patches to Lua and Jemalloc in deps/README.md. Benefits: - GitHub and OpenSSF Scorecard correctly detect the project license - reuse spdx generates a complete SPDX SBOM covering first-party code and all vendored deps - CI check prevents future contributors from introducing invalid SPDX identifiers - Per-file license clarity for downstream consumers (distro packagers, enterprises) --------- Signed-off-by: Viktor Söderqvist <viktor.soderqvist@est.tech>
…ey-io#3803) New hashtable API hashtableScanHasPassedKey. New guarantees around hashtable scan. --------- Signed-off-by: Rain Valentine <rainval@amazon.com>
Updated `kvstoreScan` to more clearly show when a kvstore cursor was used vs. a hashtable cursor. Signed-off-by: Jim Brunner <brunnerj@amazon.com>
Signed-off-by: Roshan Khatri <rvkhatri@amazon.com>
Signed-off-by: Roshan Khatri <rvkhatri@amazon.com>
Signed-off-by: Roshan Khatri <rvkhatri@amazon.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Isolates the per-replica replication-stream compression work on top of the streaming-compression-rio (valkey-io#3531) base.
streaming-compression-rio-pr@ 0f15410 (the Streaming Compression support for RDB valkey-io/valkey#3531 commit this work is built on)replication-streaming-compression-pr@ 5144e12Review-only (both branches in this fork) — shows just the replication-compression diff, separated from the valkey-io#3531 base.