Skip to content

Add MIGRATE_MODULE_PROPAGATE_END event to propagate module commands at the end of slot migration - #472

Open
sundb wants to merge 270 commits into
unstablefrom
asm-propagate-end
Open

Add MIGRATE_MODULE_PROPAGATE_END event to propagate module commands at the end of slot migration#472
sundb wants to merge 270 commits into
unstablefrom
asm-propagate-end

Conversation

@sundb

@sundb sundb commented Jun 26, 2026

Copy link
Copy Markdown
Owner

Background

During atomic slot migration (ASM), modules can already propagate commands to
the destination node at the beginning of the migration via the
REDISMODULE_SUBEVENT_CLUSTER_SLOT_MIGRATION_MIGRATE_MODULE_PROPAGATE event and
the RM_ClusterPropagateForSlotMigration() API. Those commands are delivered as
the first part of the migration stream, before the slot snapshot.

There was no equivalent hook to propagate commands at the end of the
migration, which some modules need to keep their own state consistent on the
destination side.

What this PR does

Adds a new sub-event
REDISMODULE_SUBEVENT_CLUSTER_SLOT_MIGRATION_MIGRATE_MODULE_PROPAGATE_END,
fired on the source node at the very end of the migration — right before the
STREAM-EOF is sent to the destination. Modules use the same
RM_ClusterPropagateForSlotMigration() API to enqueue commands; they are
delivered as the last part of the migration stream.

Sub-event Context Delivery position
MIGRATE_MODULE_PROPAGATE RDB fork child, before snapshot first
MIGRATE_MODULE_PROPAGATE_END main process, before STREAM-EOF last

skaslev and others added 30 commits January 22, 2026 09:40
Add key allocation sizes histograms based on previous memory accounting work
in redis#14363 and redis#14451.

The histograms are exposed via `INFO keysizes` and use logarithmic (power-of-2) bins,
similar to current key sizes/length histogram implementation in the following fields:

    db0_distrib_lists_sizes:1=...,2=...,4=...
    db0_distrib_sets_sizes:1=...,2=...,4=...
    db0_distrib_hashes_sizes:1=...,2=...,4=...
    db0_distrib_zsets_sizes:1=...,2=...,4=...

To avoid confusion with existing distrib_strings_sizes histograms which are based on
string lengths we don't report allocation sizes histograms for strings.

So far per key and per slot memory accounting code has been relying type specific functions
(hashTypeAllocSize(), listTypeAllocSize(), zsetAllocSize(), etc) for computing data structure
allocation sizes since it's faster and we only need to track size deltas and not the complete
allocation size along with the kvobj and key length overhead. In order to keep the allocation
sizes histogram consistent, memory accounting code has been switched to use kvobjAllocSize()
instead which does return the total allocation size.

Note that the feature is enabled with `key-bytes-stats` or `cluster-slot-stats` config in redis
config file on startup.
…dis#14725)

## Summary
Adds missing IDMP configuration parameters to redis.conf, previously
ommitted in redis#14615

## Changes
- Added `stream-idmp-duration` configuration parameter with
documentation
- Added `stream-idmp-maxsize` configuration parameter with documentation
- Both parameters were already implemented in the code (src/config.c,
src/server.h) but were missing from redis.conf

## Configuration Parameters

### stream-idmp-duration
- **Purpose**: Duration (in seconds) to remember IDMP identifiers for
duplicate detection
- **Range**: 1 to 86400 seconds (1 second to 24 hours)
- **Default**: 100 seconds
- **Modifiable**: Yes, via CONFIG SET at runtime

### stream-idmp-maxsize
- **Purpose**: Maximum number of IDMP identifiers to track per producer
per stream
- **Range**: 1 to 10000 entries
- **Default**: 100 entries
- **Modifiable**: Yes, via CONFIG SET at runtime
…ock is available (redis#14713)

This PR reduces per-command `ustime()` syscalls in `call()` by reusing
cached time and batching wall-clock updates when HW monotonic time is
available.

### What changed
- Pass `server.ustime` to `enterExecutionUnit()` instead of calling
`ustime()`.
- Use HW monotonic clock to measure duration and accumulate it across
commands.
- Refresh cached time with `ustime()` only when accumulated duration >
**10µs** or after **25 commands**.
- Fallback to direct `ustime()` when HW monotonic clock isn’t available.

### Impact
- `ustime` CPU: **4.58% → 0.25%**, which leads to ~4% boost on max QPS

### Notes
- Time drift is bounded (≤10µs or 25 commands).
- No behavior change on non-HW-monotonic systems.

---------

Co-authored-by: Yuan Wang <yuan.wang@redis.com>
…redis#14727)

### Summary

Adds `expired_keys_active` and `expired_subkeys_active` counters to
track keys and hash fields expired by the active expiration cycle,
distinguishing them from lazy expirations.
These new metrics are exposed in INFO stats output.

### Motivation

Currently, Redis tracks the total number of expired keys (expired_keys)
and expired hash fields (expired_subkeys), but there's no way to
differentiate between expirations triggered by active expire and lazy
expire.

---------

Co-authored-by: Moti Cohen <moti.cohen@redis.com>
in redis#14440, we remove the refcount
check in
[tryDeferFreeClientObject](redis@235e688#diff-252bce0cc340542712f0c1adf62e9035ea47a4a064321fbf40ec3dd4b814aaf2R1509),
it is ok in 8.4 version, since after command execution, the refcount of
a kvobject always is 1.
but in redis#14608 (8.6 RC1) we change this assumption, increment refcount
when a client refer a kvobject in reply, so now if the refcount of
kvobject is more than 1, we may let the io thread call `decrRefCount`,
there is data race, maybe it causes memory leak.
Adds startup-time security warnings when the default user permits
unauthenticated access, with behavior dependent on protected-mode and
bind settings.
Warnings are skipped in Sentinel mode since it intentionally
disables protected-mode by design.

- No password + no protected-mode + no bind: warn about accepting
  connections from any IP/interface
- No password + no protected-mode: warn about accepting connections
  from any IP on configured interface
- No password + protected-mode enabled: warn about accepting
  connections from local clients
…ter (redis#14742)

Some hotkeys cpu metrics display time in milliseconds others in
microseconds.

Change the metrics showing time of command executions to all use
microseconds and use the `-us` postfix to show that.

Also, disable the `SLOTS` param for `HOTKEYS START` if we are not in
cluster mode.
…is#14739)

Optimizes handling of clients with referenced replies by embedding the
`pending_ref_reply_node` list node in `client` and avoiding
per-operation node alloc/free.

there is an improvement: ~2% on 4 and 16 io-threads. ~1% on 8 io-threads
This pull request vectorizes the 8-bit quantization vector-search path
in a similar was as the non-quantization path.
The assembly intrinsics are a bit more complicated than in the
non-quantization path, since we are operating on 8-bit integers and we
need to worry about preventing overflow. Thus, after loading the 8-bit
integers, they are extended into 16-bits before multiplying and
accumulating into 32-bit integers.

---------

Co-authored-by: debing.sun <debing.sun@redis.com>
## Overview
This PR optimizes Redis Streams consumer group performance by replacing
the `pel_by_time` rax tree with a doubly-linked list, delivering
significant performance improvements for NACK updates and XREADGROUP
CLAIM operations while also reducing memory usage.

## The Problem
Consumer groups maintain a time-ordered index of pending entries using a
radix tree (`pel_by_time`). Every time a pending entry is reclaimed or
delivered, we need to update its delivery time, which currently
requires:

```c
raxRemovePelByTime(group->pel_by_time, old_time, &id);  // O(k) where k=key length
nack->delivery_time = current_time;
raxInsertPelByTime(group->pel_by_time, current_time, &id); // O(k) where k=key length
```

## The Key Insight

**99% of delivery_time updates set the value to the current time** —
which means they're appending to the tail of a time-ordered structure.

We're using a radix tree (O(k) operations where k is key length, plus
tree traversal overhead) for what is essentially an append-only workload
(should be O(1)).

## The Solution

Replace the rax tree with a doubly-linked list embedded directly in each
`streamNACK`:

```c
typedef struct streamNACK {
    mstime_t delivery_time;
    uint64_t delivery_count;
    streamConsumer *consumer;
    listNode *cgroup_ref_node;
    streamID id;                    // NEW
    struct streamNACK *pel_prev;    // NEW
    struct streamNACK *pel_next;    // NEW
} streamNACK;
```

Now updating a NACK becomes:
```c
pelListUpdate(group, nack, current_time);  // O(1): unlink + append
```

## Why This Works

**Typical case (99%):** Delivery time = current time
- Unlink from current position: O(1) — just update 2-4 pointers
- Append to tail: O(1) — update tail pointer and link

**Edge case (1%):** XCLAIM with explicit past IDLE time
- Still handled correctly by `pelListInsertSorted()` which scans
backward from tail
- Rare enough that O(N) worst case doesn't matter

## Memory Reduction

The linked list approach uses less memory than the rax tree:

**What we add:**
- 3 new fields in `streamNACK`: `id` (16 bytes) + `pel_prev` (8 bytes) +
`pel_next` (8 bytes) = 32 bytes per entry

**What we remove:**
- Entire `pel_by_time` rax tree with its node overhead (~40-50 bytes per
entry)

**Net result:** Lower memory footprint per pending entry, plus better
cache locality from eliminating the separate tree structure.

## Performance Impact

### Theoretical Analysis

| Operation | Before | After |
|-----------|--------|-------|
| NACK update | O(k) × 2 + tree overhead | O(1) |
| CLAIM iteration | O(k) per entry + traversal | O(1) per entry |

*k = key length (32 bytes: timestamp + stream ID)*

For a consumer group with 10,000 pending entries claiming 100 oldest:
- **Before:** Tree traversal + key comparisons for each operation
- **After:** Simple pointer updates

**Key Findings:**
- **28% higher throughput** for XREADGROUP with CLAIM
- **22% lower average latency** (0.195ms → 0.152ms)
- **21% lower P99 latency** (0.212ms → 0.168ms)
- XADD performance unchanged (69K ops/sec both implementations)
The reason for the failure is that when starting server with bind *, the
host will be set to *. At this time, when reconnect, the client will not
recognize this host.
So this fix skipped checking whether the server was ready.
…inary vector distance (redis#13962)

This PR replaces the manual `popcount64()` implementation with
`__builtin_popcountll()` for computing Hamming distance in binary
vectors, when the underlying hardware supports the `POPCNT` instruction.

The built-in version simplifies the code and enables the compiler to
emit a single `POPCNT` instruction on supported CPUs, which is
significantly faster than the manual bitwise method. You can verify the
difference here:
[https://godbolt.org/z/TxWMcE8M3](https://godbolt.org/z/TxWMcE8M3) — the
manual version generates a long sequence of instructions (approximately
34 on modern HW) vs 1 instruction (popcnt) when using
__builtin_popcountll()

## Portability across platforms

This change maintains full portability across platforms and compilers.
The use of `__builtin_popcountll()` is guarded by the `HAVE_POPCNT`
macro, which is defined only when the compiler supports the
target("popcnt") attribute. At runtime, we also check
`__builtin_cpu_supports("popcnt")` to ensure the hardware provides
support for the instruction. If not available, the implementation safely
falls back to the original manual `popcount64()` logic.

---------

Co-authored-by: debing.sun <debing.sun@redis.com>
…edis#14749)

Follow redis#14680
Reply of `HOTKEYS GET` is an unordered collection of key-value pairs. It
is more reasonable to be a map in resp3 instead of flat array.
…4751)

Add LTRIM/LREM and RM_StringTruncate() memory tracking tests.
redis#14492)

This PR adds SIMD vectorization for binary quantization distance
calculation, similar to PR redis#14474.

---------

Co-authored-by: debing.sun <debing.sun@redis.com>
This PR continues the work redis#14645, to further ensure sensitive user
data is not exposed in logs when hide_user_data_from_log is enabled.

- Redact empty key notices during RDB load.
- Redact key names in eviction/expiration debug logs.
- Block DEBUG SCRIPT output and suppress raw string dump in crash object
debug when redaction is enabled.
- Redact malformed MODULE LOAD argument snippets and unresolved module
configuration logs.
- Redact empty key notices during RDB load.
- Redact key names during Lua globals allow‑list warnings.
Fix HOTKEYS to track each command in a MULTI/EXEC block.
… (redis#14729)

Fix RDB Channel connections mistakenly discovered by Sentinel

During fullsync, if the main replication connection is interrupted, but
the rdbchannel connection is still active, it will be visible in the
"info replication" output. Currently, the rdbchannel connection does not
send `REPLCONF ip-address`, and in a meshed scenario, when the source IP
addresses of both connections differ, Sentinel will treat them as
separate replicas. This commit adds `REPLCONF ip-address` to rdbchannel
replica handshake if `server.slave_announce_ip` is enabled.

fixes: redis#14728

Co-authored-by: Ozan Tezcan <ozantezcan@gmail.com>
…alid slots (redis#14761)

- When passing slots not within the range of a node to `HOTKEYS START
SLOTS ...` the hotkey command now returns error.
- Changed the cmd tips for the HOTKEYS subcommands so that they reflect
the special nature of the cmd in cluster mode - i.e command should be
issued against a single node only. Clients should not care about cluster
management and aggregation of results.
- Change reply schema to return Array of the maps. For a single node
this will return array of 1 element. Getting results from multiple nodes
will make it easy to concatenate the elements into one array.
<!-- CURSOR_SUMMARY -->
> [!NOTE]
> **Low Risk**
> Simple version bumps in build configuration; main risk is upstream
module behavior/compatibility changes when building `v8.6.0`.
> 
> **Overview**
> Bumps the pinned build versions of the Redis data type modules
`redisbloom`, `redisjson`, and `redistimeseries` from `v8.5.90` to
`v8.6.0` via their Makefiles.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
f1319fa. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Key in the result of HOTKEYS `sampled-command-selected-slots-us` changed
to `sampled-commands-selected-slots-us` in order to be aligned with the
other key names.
# Summary

Refactors IDMP producer defragmentation to use the generic
`defragRadixTree` helper function instead of manually implementing radix
tree iteration, improving code consistency and maintainability.

## Changes

* Replaced manual rax iteration in `defragStreamIdmpProducers` with
`defragRadixTree` call
* Added `defragIdmpProducerCallback` function following the
`raxDefragFunction` signature pattern
* Removed unnecessary wrapper function and inlined the call directly at
the call site
* Made the callback function static since it's only used within
`defrag.c`

## Benefits

**Code Consistency:** Now follows the same pattern used by other stream
defrag functions (`defragStreamConsumer`, `defragStreamConsumerGroup`,
etc.)
**Reduced Duplication:** Eliminates 20+ lines of boilerplate rax
iteration code by leveraging the existing `defragRadixTree` helper
**Maintainability:** All rax defragmentation logic (tree struct, head
node, internal nodes, data pointers) is now centralized in
`defragRadixTree`
**Correctness:** Uses the proven, well-tested radix tree defragmentation
logic consistently across all stream-related structures
- After redis#14440, the IO thread can
parse all command in pipeline instead of only the header, so we should
prefetch all pending commands when prefetching.

- Since the main thread will access the `io_deferred_objects`, we should
prefetch them.

Up to 8% improvement for 100% writing scenarios, about 2% for other scenarios
Before this PR, `pipe2()` is only enabled on Linux and FreeBSD while
`pipe2()` is available on *BSD.

This PR enables `pipe2()` for the rest of *BSD: DragonFlyBSD, NetBSD and
OpenBSD.

- [pipe2 on
DraonFlyBSD](https://man.dragonflybsd.org/?command=pipe&section=2)
- [__DragonFly_version for
pipe2](https://github.com/DragonFlyBSD/DragonFlyBSD/blob/7485684fa5c3fadb6c7a1da0d8bb6ea5da4e0f2f/sys/sys/param.h#L121)
- [pipe2 on  NetBSD](https://man.netbsd.org/pipe.2)
- [pipe2 on OpenBSD](https://man.openbsd.org/pipe.2)
Currently, redis uses select(2) on DragonFlyBSD while `kqueue` is
available on DragonFlyBSD since FreeBSD 4.1, and DragonFlyBSD was
originally forked from FreeBSD 4.8

`select(2)` is a pretty old technique that has many defects compared to
`kqueue`, we should switch to `kqueue` on DragonFlyBSD.

References:

[DragonflyBSD -
kqueue](https://man.dragonflybsd.org/?command=kqueue&section=2)

---------

Signed-off-by: Andy Pan <i@andypan.me>
Co-authored-by: debing.sun <debing.sun@redis.com>
Prevent unsigned integer underflow when `mem_total` exceeds
`zmalloc_used` in `getMemoryOverheadData()`.
This can occur during early startup or due to timing mismatches in
memory sampling.

Add conditional check to set dataset to 0 when underflow would occur,
matching the existing pattern used for `net_usage` calculation.
Currently, the SFLUSH command is permitted only when the specified slot
range fully covers all slot ranges owned by the node. We want to enable
it to perform partial slot flushes instead, and the reply should be list
of ranges that were flushed.

for implementation, we use TRIMSLOTS functionalities (added as part of
ASM work) to support flush async.

**NOTE**: Redis will reply `-TRYAGAIN Slot is being trimmed` error if
clients send write commands to given slots during SFLUSH execution,
clients should not send write commands to these slots before getting
reply of SFLUSH command.

And there is still an issue if we use active trim for `SFLUSH ASYNC`
redis#14750 (comment), so we
still mark this command `experimental`, we may remove this tag when we
deprecate active slot trimming or don't support ASYNC option.

---------

Co-authored-by: Ozan Tezcan <ozantezcan@gmail.com>
Use rax memory accounting for RedisModuleDict.

Suggested-by: debing.sun <debing.sun@redis.com>
…nabled (redis#14786)

Don't call kvobjAllocSize() in t_stream.c if memory tracking is not
enabled.

Reported-by: Sergei Georgiev <s_ggeorgiev@yahoo.com>
sggeorgiev and others added 29 commits June 5, 2026 11:56
…s#15263)

## Summary

The cluster bus PING/PONG/MEET packet parser validated extension padding
and total length but never checked that string-carrying extensions are
properly null-terminated, allowing a crafted packet to trigger
out-of-bounds reads when the payload is later consumed as a C string.

1. **Null-termination check for hostname and human-nodename extensions
(`cluster_legacy.c`)**

Added a check inside the existing extension-validation loop in
`clusterProcessPacket`: for `CLUSTERMSG_EXT_TYPE_HOSTNAME` and
`CLUSTERMSG_EXT_TYPE_HUMAN_NODENAME` extension types, it verifies that
the data portion is non-empty (`datalen > 0`) and that the last byte is
`'\0'`. Packets failing this check are rejected with a warning log and
an early return, the same way other malformed-extension cases are
handled.

2. **Test (`hostnames.tcl`)**

A new test exercises the rejection path by constructing a raw
cluster-bus PING packet with a 32-byte hostname extension that contains
no `'\0'`, sending it directly to a node's bus port, and verifying the
packet is dropped (warning logged, hostname not updated in `CLUSTER
NODES`). Two helper procs (`build_cluster_bus_ping` and
`build_hostname_extension`) build the binary packet from scratch in Tcl,
allowing fine-grained control over extension contents without needing a
modified Redis sender.
…is#14704)

Avoid zmalloc_size() in kvobjAllocSize() and use approximation instead.

Since for ongoing key allocation histograms work (redis#14695) we need to
call
kvobjAllocSize() more often on hot paths, using zmalloc_size() would
cause
unnecessary performance overhead.
…s#15262)

Fixes redis#15250

## Summary

When `redis-cli --cluster rebalance` is invoked with `--user <username>`
but without `-a <password>`, the MIGRATE command constructed by
redis-cli contains an extra unfilled argv slot that gets serialized as
an empty string. The server interprets this empty string as a key with
slot 0, triggering a CROSSSLOT error when it conflicts with the actual
keys' slot.

## Root Cause

In `src/redis-cli.c`, function `clusterManagerMigrateKeysInReply`:

```c
int c = (replace ? 8 : 7);
if (config.conn_info.auth) c += 2;
if (config.conn_info.user) c += 1;    // BUG: adds 1 for user even when auth is NULL
size_t argc = c + reply->elements;
```

When `config.conn_info.user` is set but `config.conn_info.auth` is NULL,
`c` is incremented by 1 for the user parameter. However, the argv
filling logic later only sets the user inside the `if
(config.conn_info.auth)` block (using AUTH2 with both user and
password). This mismatch causes:
- `argc` is 1 larger than the actual number of argv entries filled
- The unfilled argv slot is serialized as `$0\r\n\r\n` (empty string)
- Server's `migrateGetKeys` treats the empty string as a key →
`keyHashSlot("",0)` returns slot 0 → CROSSSLOT

## Fix

Consolidate the two separate increments into one:

```diff
- if (config.conn_info.auth) c += 2;
- if (config.conn_info.user) c += 1;
+ if (config.conn_info.auth)
+     c += config.conn_info.user ? 3 : 2;
```

This is consistent with the argv filling logic where both AUTH and AUTH2
cases are handled inside a single `if (config.conn_info.auth)` block.

---------

Co-authored-by: 2030XiaoGe <2030XiaoGe@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 <noreply@owtffssent.com>
Co-authored-by: debing.sun <debing.sun@redis.com>
Fixes redis#15269.

This keeps `VADD ... CAS SETATTR` in sync with the normal insert path by
incrementing `numattribs` when a CAS insert actually stores an
attribute.

Before this change, the CAS path attached the attribute to the node but
left the set-wide counter unchanged. That made `VINFO` under-report
`attributes-count`, and if the set-wide count stayed at zero, RDB save
skipped attribute serialization for the whole vector set.

---------

Co-authored-by: debing.sun <debing.sun@redis.com>
## Issue

The Codecov workflow failed while running
`codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2` (`v6`)
because the downloaded Codecov CLI could not be verified with GPG.

```text
Run codecov/codecov-action@57e3a13
==> Running Action version 6.0.0
...
gpg: Can't check signature: No public key
==> Could not verify signature. Please contact Codecov if problem continues
    Exiting...
```

## Change

Update the Codecov upload step to use `codecov/codecov-action v7.0.0`,
which includes the updated wrapper/signature verification behavior.
…#15308)

## Summary
The stream `entries_added` field is stored as `uint64_t` but is consumed
as `long long` in multiple downstream code paths - lag computation
(`streamReplyWithCGLag`), distance estimation
(`streamEstimateDistanceFromFirstEverEntry`), and `XINFO STREAM` output
(`addReplyLongLong`). The same problem applies to each consumer group's
`entries_read` field. A crafted RDB or RESTORE payload could set either
field to a value exceeding `LLONG_MAX`, causing sign-flip in lag
calculations and incorrect negative values reported to clients.

1. **Overflow and invariant check for `entries_added`**
Added a validation in `rdbLoadObject` during stream loading that rejects
payloads where `entries_added` exceeds `LLONG_MAX` (would overflow when
cast to signed) or is less than the stream's current `length` (violates
the invariant that total-ever-added must be >= live entries). This
mirrors the same invariant already enforced by `xsetidCommand` and
prevents corrupt values from propagating into lag arithmetic or AOF
rewrites (which also narrow to `long long` via `rioWriteBulkLongLong`).

2. **Overflow and invariant check for consumer group `entries_read`**
Added a validation during consumer group loading that rejects payloads
where a group's `entries_read` exceeds `LLONG_MAX` (would overflow when
cast to signed) or is greater than the stream's `entries_added` (a group
cannot have read more entries than were ever added). The check skips the
sentinel `SCG_INVALID_ENTRIES_READ` value, which legitimately represents
an unknown lag. This prevents corrupt `entries_read` values from
propagating into lag arithmetic.

3. **Tests**
- A new test sends a hand-crafted RESTORE payload containing a valid
3-entry stream whose `entries_added` field is patched to
`0xFFFFFFFF00000000` (exceeds `INT64_MAX`). The test disables checksum
validation and `sanitize-dump-payload` to reach the RDB object loader,
then verifies both the client-facing error (`Bad data format`) and the
server log message (`Stream entries_added inconsistent with length`).
- A second test sends a hand-crafted RESTORE payload containing a valid
2-entry stream with a consumer group whose `entries_read` field is
patched to `0xFFFFFFFF00000000` (exceeds `INT64_MAX` and the stream's
`entries_added`). It similarly verifies the client-facing error (`Bad
data format`) and the server log message (`Stream cgroup entries_read
inconsistent with entries_added`).
## Implement SUNIONCARD command

Add a new `SUNIONCARD` command that returns the cardinality (number of
distinct elements) of the union of multiple sets without returning the
elements themselves.

**Syntax:** `SUNIONCARD numkeys key [key ...] [APPROX] [LIMIT limit]`
- **Exact mode (default):** Builds a temporary union set (reusing
`sunionDiffGenericCommand` with new `cardinality_only` and `limit`
parameters) and returns its size. Supports `LIMIT` with true early
termination — stops adding elements as soon as cardinality reaches the
limit.

- **Approximate mode (`APPROX`):** Uses Redis's existing HyperLogLog
internals (`createHLLObject`/`hllAdd`/`hllCount`) to estimate the union
cardinality without materializing the full union. Standard HLL error of
~0.81%. With `LIMIT`, the HLL estimate is checked after processing
sufficient elements to cross the boundary (calculated based on how far
the current estimate is below LIMIT) for early termination.

## Changes

- **`src/t_set.c`**: Extended `sunionDiffGenericCommand` with
`cardinality_only`, `approx`, and `limit` parameters (following the same
pattern as `sinterGenericCommand/SINTERCARD`). The `SET_OP_UNION` loop
now accumulates into either a temporary set (exact) or a `HyperLogLog`
(approx), with early termination once limit is reached, plus a
`cardinality_only` reply branch that returns the count. All existing
callers (`SUNION`, `SUNIONSTORE`, `SDIFF`, `SDIFFSTORE`, `SPOP`) pass
`0, 0, 0` — no behavior change. Implemented `sunioncardCommand`: it
parses arguments and delegates to `sunionDiffGenericCommand`, with
`approx` selecting the HLL-based estimate.

- **`src/hyperloglog.h`** (new): Extracted `struct hllhdr` from
`hyperloglog.c` and exposes `createHLLObject`, `hllAdd`, `hllCount` so
`t_set.c` can use the HLL API.

---------

Co-authored-by: debing.sun <debing.sun@redis.com>
## Summary

Bumps the pinned `MODULE_VERSION` for **RedisTimeSeries** from `v8.8.0`
to `v8.9.80`. RedisBloom and RedisJSON are unchanged.

| Module          | From    | To       |
|-----------------|---------|----------|
| RedisTimeSeries | v8.8.0  | v8.9.80  |

> **Note:** `v8.9.80` is the **8.10 LTS branch cut** (targets Redis
8.10) and has diverged from `v8.8.0`. The `v8.8.0...v8.9.80` ancestry
diff contains 37 commits, but **7 are backports already shipped in
`v8.8.0`** (verified with `git cherry`, see below); the **30 genuinely
new** changes are listed here.

## Module changes ([v8.8.0 →
v8.9.80](RedisTimeSeries/RedisTimeSeries@v8.8.0...v8.9.80))

- MOD-14124 — m commands acl
([redis#1913](RedisTimeSeries/RedisTimeSeries#1913))
- MOD-14250 — missing artifacts logs fix
([redis#1932](RedisTimeSeries/RedisTimeSeries#1932))
- MOD-14289 — Increase LibMR execution timeout for Valgrind builds
([redis#1933](RedisTimeSeries/RedisTimeSeries#1933))
- MOD-12726 — unique snapshot name + new output params for nighly event
([redis#1937](RedisTimeSeries/RedisTimeSeries#1937))
- MOD-14674 — oss refresh list
([redis#1935](RedisTimeSeries/RedisTimeSeries#1935))
- nightly build, upload snapshot artifact
([redis#1954](RedisTimeSeries/RedisTimeSeries#1954))
- MOD-14902 — existence check to prevent Timeseries crash when Redis is
not aligned with new redis-core changes
([redis#1940](RedisTimeSeries/RedisTimeSeries#1940))
- MOD-14850 — Bump libmr
([redis#1962](RedisTimeSeries/RedisTimeSeries#1962))
- MOD-14715 — add apt retries/timeouts to Debian/Ubuntu Dockerfiles
([redis#1965](RedisTimeSeries/RedisTimeSeries#1965))
- MOD-15262 — Align TimeSeries with the RedisModule_GetUserUserName API
changes
([redis#1975](RedisTimeSeries/RedisTimeSeries#1975))
- RED-180951 RED-180027 — fixing bugs and improving the code
([redis#2004](RedisTimeSeries/RedisTimeSeries#2004))
- MOD-14239 — Ignore known init-rdbchannel migration timeout under
sanitizer
([redis#1979](RedisTimeSeries/RedisTimeSeries#1979))
- MOD-15105 — make one generic getUser function
([redis#1981](RedisTimeSeries/RedisTimeSeries#1981))
- MOD-15741 MOD-14091 — add dont-cache command tip to non-cacheable read
commands
([redis#2031](RedisTimeSeries/RedisTimeSeries#2031))
- MOD-15731 — modules modernization alignment
([redis#2027](RedisTimeSeries/RedisTimeSeries#2027))
- MOD-15728 — Tal.ba/feat/blocking get
([redis#2028](RedisTimeSeries/RedisTimeSeries#2028))
- MOD-8187 — fix the aggregation bugs (fix reverse iterator)
([redis#2036](RedisTimeSeries/RedisTimeSeries#2036))
- MOD-15749 — Link to new version of LibMR with short-form CLUSTERSET
and add tests
([redis#2034](RedisTimeSeries/RedisTimeSeries#2034))
- Fix REDISMODULE_MAIN typo + drop common.h forward-decl workarounds
([redis#2044](RedisTimeSeries/RedisTimeSeries#2044))
- CI: switch flow-linux arm64 runner to standard ubuntu-24.04-arm
([redis#2048](RedisTimeSeries/RedisTimeSeries#2048))
- MOD-15999 — Centralize Redis ref into a single source-of-truth file
for CI
([redis#2045](RedisTimeSeries/RedisTimeSeries#2045))
- CI: trigger OSS cluster (multi-shard) benchmarks on demand
([redis#2050](RedisTimeSeries/RedisTimeSeries#2050))
- MOD-15899 — oom try calloc
([redis#2051](RedisTimeSeries/RedisTimeSeries#2051))
- MOD-8187 — fix non twa aggregation
([redis#2053](RedisTimeSeries/RedisTimeSeries#2053))
- MOD-16224 — Fix filter-blind neighbor lookups in EMPTY aggregation
([redis#2056](RedisTimeSeries/RedisTimeSeries#2056))
- MOD-15893 — Nrange new command
([redis#2052](RedisTimeSeries/RedisTimeSeries#2052))
- MOD-16160 — update blocking get syntax
([redis#2054](RedisTimeSeries/RedisTimeSeries#2054))
- MOD-16160 — Fix client-side cache tips for TS.BGET and
TS.NRANGE/TS.NREVRANGE
([redis#2057](RedisTimeSeries/RedisTimeSeries#2057))
- Cut 8.10 LTS branch: bump version to 8.9.80, target Redis 8.10
([27484f7](RedisTimeSeries/RedisTimeSeries@27484f7))
- Fix event-nightly CI on 8.10: read Redis ref from dedicated redis_ref
field
([redis#2058](RedisTimeSeries/RedisTimeSeries#2058))

<details>
<summary>7 commits excluded — already shipped in v8.8.0 as
backports</summary>

These show up in the `v8.8.0...v8.9.80` ancestry diff with different
SHAs, but `git cherry` confirms their patch content is already in
`v8.8.0`: they were backported to the 8.8 release branch (the "in 8.8
via" PR below), so they are **not** new in this bump.

- MOD-9320 — Multiple aggregators single command
([redis#1916](RedisTimeSeries/RedisTimeSeries#1916))
→ in 8.8 via
[redis#1921](RedisTimeSeries/RedisTimeSeries#1921)
- MOD-9320 — rename AGGREGATION token in m commands
([redis#1926](RedisTimeSeries/RedisTimeSeries#1926))
→ in 8.8 via
[redis#1927](RedisTimeSeries/RedisTimeSeries#1927)
- MOD-13142 — Allow changing number of threads while pool hasn't started
([redis#1928](RedisTimeSeries/RedisTimeSeries#1928))
→ in 8.8 via
[redis#1934](RedisTimeSeries/RedisTimeSeries#1934)
- MOD-14439 — Detect cluster topology changes during a multi-shard
command
([redis#1930](RedisTimeSeries/RedisTimeSeries#1930))
→ in 8.8 under the same PR
- ci: skip Event CI when PR only changes src/version.h
([redis#1984](RedisTimeSeries/RedisTimeSeries#1984))
→ in 8.8 via
[redis#1986](RedisTimeSeries/RedisTimeSeries#1986)
- MOD-15020 — remove bionic os support
([redis#1983](RedisTimeSeries/RedisTimeSeries#1983))
→ in 8.8 via
[redis#1996](RedisTimeSeries/RedisTimeSeries#1996)
- MOD-14420 — fix count reducers return wrong NaN
([redis#2013](RedisTimeSeries/RedisTimeSeries#2013))
→ in 8.8 via
[redis#2016](RedisTimeSeries/RedisTimeSeries#2016)

</details>

## Test plan
- [ ] CI passes for RedisTimeSeries at the new pinned version
- [ ] `make all` builds RedisTimeSeries cleanly against `unstable`
- [ ] Module tests run under `make test`

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tick (redis#15330)

## Motivation

`getAllocatorFragmentation()` was called twice per `serverCron` tick:
once by `cronUpdateMemoryStats()` for `INFO MEMORY`, then again by
`computeDefragCycles()` for the threshold decision. Each call forces a
jemalloc epoch refresh (cross-thread sync via IPI) plus a per-arena
small-bin scan. The second call's result was almost always discarded
by the threshold check.

## Change

`struct defragFragCache` on `redisServer` (`src/server.h`) caches the
Lua-arena-subtracted `(frag_pct, frag_bytes)` plus `server.cronloops`
at the moment of measurement.

- `defragFragCachePut(frag_bytes, allocated)` — called from
  `cronUpdateMemoryStats()` (`src/server.c`) after the existing arena
  walk; mirrors `getAllocatorFragmentation()`'s Lua subtraction.
- `defragFragCacheTake(&pct, &bytes)` — called at the top of
  `computeDefragCycles()` (`src/defrag.c`); returns hit only when the
  recorded `cronloops` matches `server.cronloops` (same tick). Miss →
  falls through to a fresh `getAllocatorFragmentation()` call.

Freshness gate is `server.cronloops`: it advances at the top of
`serverCron()` and `whileBlockedCron()`, so any value published in a
previous tick is automatically stale on the next `Take`. No explicit
invalidate call needed. `initServerConfig()` seeds `cronloops = -1`
so the first `Take` is a clean miss.

`HAVE_DEFRAG=0` builds get no-op `Put`/`Take` stubs so the
unconditional call site in `cronUpdateMemoryStats()` always links.

## Observability

`DEBUG DEFRAG-FRAG-CACHE-STATS` exposes `defrag_frag_cache_hits`
(testing/diagnostic surface only; not in `INFO`).
Add a new CMD_SCRIPT_RUNNER flag (bit 30) to identify commands that
execute scripts or functions. The flag is applied to EVAL, EVALSHA,
EVAL_RO, EVALSHA_RO, FCALL, and FCALL_RO.

These marker help clients better understand command semantics,
particularly in the context of Client Side Caching: clients can use
the SCRIPT_RUNNER flag to decide which commands
should not have their results cached, since script execution and
consumer group reads are inherently stateful and non-repeatable.
Jira: https://redislabs.atlassian.net/browse/MOD-16291

Updates the bundled RediSearch module version used by Redis module
builds from `v8.8.0` to `v8.9.80`.

This picks up the tagged RediSearch 8.9.80 release.

Validation:
- Checked that `v8.9.80` exists in `RediSearch/RediSearch` and resolves
to `280c3ce1bd65fd7306cae75a1d54bce3a05518e4`.
- Checked the diff is limited to `modules/redisearch/Makefile`.
- Ran `git diff --check`.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Single Makefile version pin with no application logic changes; main
risk is behavioral differences in the upstream RediSearch 8.9.80 release
at runtime.
> 
> **Overview**
> **RediSearch module version bump** from `v8.8.0` to `v8.9.80` via
`MODULE_VERSION` in `modules/redisearch/Makefile`, so Redis module
builds fetch and compile the tagged 8.9.80 release instead of 8.8.0.
> 
> No other build flags or paths change in this PR.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
7bd4875. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
…le (redis#15305)

Corrects command introspection metadata for SET and DELEX so IFDEQ / IFDNE digest operands are documented as string, not integer.
…on (redis#15327)

Adds two `RedisModuleEvent_ForkChild` subevents so a multi-threaded
module can bring its background threads to a safe point before `fork()`:

- `REDISMODULE_SUBEVENT_FORK_CHILD_PRE` — fired synchronously on the
main thread just before `fork()`. The handler runs synchronously, so
returning from it acknowledges the module is ready to fork.
- `REDISMODULE_SUBEVENT_FORK_CHILD_CANCELLED` — fired if a fork preceded
by `_PRE` did not happen (`fork()` failed), so the module can resume. On
success the module resumes on the existing `FORK_CHILD_BORN`.

`redisFork()` fires `_PRE` before `fork()` and `_CANCELLED` on `fork()`
failure.

## Why
Between `fork()` and `exec()` only async-signal-safe functions are safe
to call. A multi-threaded module (search, timeseries/LibMR, gears…)
whose background thread holds a lock — e.g. the allocator (malloc arena)
lock — at the instant of `fork()` leaves the child holding that lock
with no owning thread, so the child **deadlocks** the first time it
takes it (e.g. during the RDB save it forked for). `_PRE` lets the
module quiesce its threads to a safe, lock-free point first.

Existing `FORK_CHILD` subscribers are unaffected — they ignore the new
subevents (default case); no existing subevent value or the event id
changes.


Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Ozan Tezcan <ozantezcan@gmail.com>
…edis#15242)

## What this PR does

Adds a new module API, `RedisModule_AddPostNotificationJobForKey`,
alongside
the existing `RedisModule_AddPostNotificationJob`. Both schedule work to
run
after a keyspace-notification (KSN) handler returns (so the handler
stays free
of write side-effects), but the new API binds each job to a specific key
and
differs from the existing one in three ways:

| | `AddPostNotificationJob` (existing) | `AddPostNotificationJobForKey`
(new) |
|---|---|---|
| **Callback** | `(ctx, pd)` | `(ctx, key, pd)` — receives the bound
key. |
| **Firing schedule** | Once, at the end of the outermost execution unit
(`firePostExecutionUnitJobs`). | At the tail of **every `call()`** — so
each MULTI/EXEC and EVAL/FCALL sub-command is observed in turn —
**plus** during AOF replay after each replayed command, **plus** the
end-of-execution-unit drain (for standalone commands). |
| **When allowed** | Refused while `server.loading` or on a read-only
replica. | Permitted during AOF replay and on replicas (see below).
Refused if called **outside a KSN handler**. |

Motivation: RED-197766. A module needs to react to each step of an
`HSET` + `HEXPIRE` sequence on the same hash inside a `MULTI/EXEC` block
—
the existing API can't deliver that because it only fires once at the
end of
EXEC.

## Design: one queue, distinguished by the bound key

Per-key jobs share the existing post-notification queue
(`modulePostExecUnitJobs`) rather than living in a separate one. A
queued job
(`RedisModulePostExecUnitJob`) carries an owned `key` reference and a
callback
union:

- **Regular job** (`RM_AddPostNotificationJob`): `key == NULL`,
`cb.callback`
is used. Fires once at the end of the outermost execution unit and may
write
  to the keyspace.
- **Per-key job** (`RM_AddPostNotificationJobForKey`): `key != NULL`,
  `cb.key_callback` is used and receives the bound key. Fires between
sub-commands and during AOF replay, and may **not** write to the
keyspace.

`key != NULL` is the sole discriminator — no separate type or flag.

### Firing sites

`firePerKeyJobsBetweenSubcommands()` drains only the per-key jobs (those
with
`key != NULL`), leaving regular jobs for the end-of-unit drain. It is
invoked
at three places, all off the universal `afterCommand` hot path so
standalone
commands that never register a per-key job pay nothing:

1. `execCommand()` (`multi.c`) — after each MULTI/EXEC sub-command's
`call()`,
   so per-key effects are observable between sibling sub-commands.
2. `scriptCall()` (`script.c`) — after each EVAL/FCALL sub-command's
`call()`.
3. `loadSingleAppendOnlyFile()` (`aof.c`) — after each replayed single
command,
plus a final `firePostExecutionUnitJobs()` at the `cleanup:` path so a
partially-applied command can't leave stragglers. EXEC's sub-commands go
through `call()` during replay and are covered by the per-`call()`
drain.

Per-key jobs registered by a standalone command (no MULTI/EXEC, no
script) are
drained by the existing end-of-execution-unit
`firePostExecutionUnitJobs()`,
which walks the single queue in submission order.

These drain sites are gated by an inline hint
`server.fire_keyed_jobs_between_subcommands`, armed when a per-key job
is
enqueued and cleared after a drain, so the no-jobs case is a single
well-predicted branch on a hot `server` field rather than a cross-TU
call.

## AOF replay & replica support

Unlike the regular API, `RM_AddPostNotificationJobForKey` does **not**
mirror
the `server.loading || repl_slave_ro` guard. The canonical consumer
attaches
**module key metadata** (`RM_SetKeyMeta` / `RM_GetKeyMeta`) to keys it
observes
via KSN. That metadata is neither stored in the AOF nor replicated; it
must be
rebuilt by re-running the same callback over the same KSN stream on
every
instance that applies the command — master, replica, and a process
replaying
its own AOF at startup. The per-key API is therefore permitted in all
those
contexts; the only registration guard is that it must be called from
inside a
KSN dispatch.

**RDB load is intentionally outside this pattern.** RDB load decodes
keys
directly without running commands, so no KSN fires and per-key callbacks
do not
run. A test (`perkey-rdb`) pins this boundary.

## Runtime contract enforcement

A per-key callback MUST only touch non-replicated, non-AOF-persisted
state
(canonically: module key metadata). Writing back into the keyspace would
amplify the AOF on replay and diverge a replica from its master. This is
now
**enforced at runtime**, not merely documented —
`server.firing_keyed_post_notif_jobs`
is set for the duration of each per-key callback, and while it is set:

- `RM_Call(...)` is refused: returns `NULL` with `errno == EINVAL`, or a
  `-ERR` call-reply when `CALL_REPLIES_AS_ERRORS` is requested.
- `RM_NotifyKeyspaceEvent` / `RM_NotifyKeyspaceEventWithSubkeys` are
refused
(return `REDISMODULE_ERR`) — a nested notification could enqueue further
  per-key jobs mid-drain.

Each refusal is logged once per drain (`LL_WARNING`, deduplicated via
`keyedPostNotifRMCallWarned` / `keyedPostNotifNotifyWarned`). The
canonical
`RM_SetKeyMeta` path is unaffected (metadata does not propagate).

## Implementation summary

- New per-key callback type `RedisModulePostNotifyJobPerKeyFunc`
  `(ctx, key, pd)`. The existing `RedisModulePostNotificationJobFunc` is
renamed to `RedisModulePostNotifyJobFunc` with a backward-compatible
typedef
  alias so existing module sources keep compiling.
- `RedisModulePostExecUnitJob` gains an owned `key` field and a callback
union.
- `firePerKeyJobsBetweenSubcommands()` drains per-key jobs;
`firePostExecutionUnitJobs()` drains the whole queue at end of unit. A
shared
`executePostExecUnitJob()` helper runs and frees a single job (and
decrefs
  the owned key for per-key jobs).
- New `server` fields:
- `firing_keyed_post_notif_jobs` — set while a per-key callback runs;
the
no-write guard backing the `RM_Call` / `RM_NotifyKeyspaceEvent`
refusals.
- `fire_keyed_jobs_between_subcommands` — fast-path hint gating the
explicit
    drains in `multi.c` / `script.c` / `aof.c`.
- `in_keyspace_notification` — counter, incremented around the dispatch
loop
    in `moduleNotifyKeyspaceEvent`. Defines the scope from which
`RM_AddPostNotificationJobForKey` may be called (nested notifications
nest
cleanly). Calling the API outside this scope returns `REDISMODULE_ERR`.
- `moduleUnregisterPostNotificationJobs()` drops any queued jobs
belonging to a
  module being unloaded (wired into `moduleUnregisterCleanup`).
- Because each KSN dispatch is single-key by construction, multi-key
commands
like `MSET`/`MGET` work transparently: they emit one
`notifyKeyspaceEvent`
  per key, so the handler can register one per-key job per affected key.

## Testing

A single test module, `pkmeta`
(`tests/modules/postnotifications_perkey_metadata.c`), uses the
canonical
pattern: a KSN handler enqueues a per-key job, and the job attaches
module key
metadata via `RM_SetKeyMeta`. Metadata is neither AOF-persisted nor
replicated,
so its presence after a reload / propagation is direct evidence the
callback
re-ran on that instance. Module-internal counters (fire count, fire log,
blocked-`RM_Call` count, blocked-notification count), kept out of the
keyspace
on purpose, carry the load-bearing assertions.

All tests live in one suite,
`tests/unit/moduleapi/postnotifications_perkey.tcl`:

| Group | Tests | What they pin |
|---|---|---|
| `perkey-aof` | single command rebuilds metadata via AOF reload;
MULTI/EXEC fires once per sub-command; HSET+HEXPIRE in MULTI/EXEC fires
twice (each run for both `debug loadaof` and full restart) | One drain
per replayed command/sub-command during AOF replay; the original
RED-197766 scenario end-to-end. |
| `perkey-rdb` | RDB-only restart does NOT rebuild metadata | Pins that
RDB load is outside the firing pattern. |
| `perkey-aof-replica` | AOF replay on a replica at startup rebuilds
metadata | The carve-out removing the `repl_slave_ro` guard. |
| `perkey-repl` | replica builds metadata from a propagated single
command; replica fires per sub-command for propagated MULTI/EXEC | Both
sides run the per-key job locally; no metadata crosses the replication
stream. |
| `perkey-misuse` | registration refused outside a KSN handler | The
`in_keyspace_notification` scope guard. |
| `perkey-order` | fires once per key in submission order across
MULTI/EXEC; fires between sub-commands inside MULTI/EXEC; fires between
commands inside a script (EVAL); multi-key command fires one job per key
| Ordering and per-sub-command granularity for MULTI/EXEC, scripts, and
multi-key commands. |
| `perkey-contract` | RM_Call refused; refusal repeats per firing and
never writes; RM_NotifyKeyspaceEvent refused; notification refusal
repeats per firing | Runtime enforcement of the no-write contract. |
| `perkey-expire` | lazy expire on read fires the per-key job; active
expire (cron) fires it without a read | Expire-path coverage. |
| `perkey-ptr-safety` | SMOVE first metadata attach must not UAF the
source set | Owned-key lifetime safety. |

The suite is registered in `runtest-moduleapi` and the module in
`tests/modules/Makefile`.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Touches module notification timing, MULTI/EXEC, scripting, and AOF
replay—areas where ordering bugs could affect replication consistency,
though keyspace writes from callbacks are blocked.
> 
> **Overview**
> Adds **`RedisModule_AddPostNotificationJobForKey`**, a sibling to
`AddPostNotificationJob` that binds deferred work to a specific key from
a keyspace-notification handler. Per-key jobs share the existing
post-notification queue (`key != NULL`); regular jobs still drain only
at the end of the execution unit, while per-key jobs also run after each
sub-command in **MULTI/EXEC**, **EVAL/FCALL**, and **single-command AOF
replay** (gated by `fire_keyed_jobs_between_subcommands` so the common
path stays cheap).
> 
> Runtime contract: per-key callbacks may update local module state
(e.g. **`RM_SetKeyMeta`**) but **`RM_Call`** and
**`RM_NotifyKeyspaceEvent`** are refused while they run; registration
outside KSN dispatch fails via **`in_keyspace_notification`**. Unlike
the regular API, per-key registration is allowed during AOF replay and
on replicas so metadata can be rebuilt from the same KSN stream. Module
unload now drops queued jobs for that module; AOF load **`cleanup`**
flushes remaining regular jobs.
> 
> New **`pkmeta`** test module and **`postnotifications_perkey`** suite
cover ordering, AOF/replica rebuild, contract violations, and
expire/evict edge cases.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
022ad9d. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Filipe Oliveira (Redis) <filipe@redis.com>
Co-authored-by: Moti Cohen <moticless@gmail.com>
…er (redis#15309)

Found by oranagra 

## The problem

`freeClient()` reverses the client's contribution to
`stat_clients_type_memory` (exposed as `mem_clients_normal`) only when
`c->conn` is set:

```c
if (c->conn)
    server.stat_clients_type_memory[c->last_memory_type] -= c->last_memory_usage;
```

A replica caches its master on disconnect to attempt a partial resync.
replicationCacheMaster() detaches the connection (unlinkClient) before
caching, so by the time the cached master is freed by
replicationDiscardCachedMaster(), `c->conn` is already NULL. The
subtraction is skipped, so the cached master's last accounted size is
never removed from mem_clients_normal.

## Fix

Include the cached master in the subtraction by also reversing the
contribution when the client being freed is `server.cached_master` (it
is still set at the point `replicationDiscardCachedMaster()` calls
`freeClient()`, and only cleared right after).
This PR is based on valkey-io/valkey#1213.

sdssplitargs() builds each argument one byte at a time via sdscatlen(),
which uses greedy doubling allocation. This leaves significant unused
space in each SDS string — for a 600-byte value, ~400 bytes are wasted.

Add sdsRemoveFreeSpace() after sdssplitargs() in processInlineBuffer()
to trim each argument before wrapping it in an robj.

Measured with 1M SET commands with 600-byte values piped via `redis-cli
--pipe` in inline format:
- Before: 1,021 MB vs 655 MB for RESP (55.9% overhead)
- After: 655 MB, matching RESP baseline

---------

Co-authored-by: muelstefamzn <muelstef@amazon.com>
Add a fixed-length-key rax mode that stores values directly in the leaf
parent's child slots, avoiding the final leaf raxNode allocation.

Introduce raxNewEx(metaSize, alloc_size, keyFixedLen) and keep the
existing representation unchanged for variable-length trees.

Update lookup, insert, remove, free, compression, and iterator paths to
handle virtual leaves, including value updates through
raxIteratorSetData().

Switch in-tree fixed-size rax users to the new mode. This reduces memory
for dense fixed-length trees, with throughput changes within benchmark
noise.
…redis#15362)

`rewriteConfig()` accepts a path and reads the existing config from that
path, but the writeback path used `server.configfile`.

Since current callers all pass `server.configfile`, this does not affect
the normal CONFIG REWRITE path, but using the passed-in path keeps the
function behavior consistent with its interface and comments.
…is#15356)

## Problem
`ACLLoadFromFile()` iterates `server.clients` to rebind users and drop
pub/sub clients whose channel ACLs changed. Before this fix the loop
only
skipped `CLIENT_MASTER` clients before dereferencing `c->user->name`.

Internal cluster connections (`AUTH "internal connection" <secret>`) set
`c->user = NULL` and `CLIENT_INTERNAL`, but **not** `CLIENT_MASTER`. So
if
an internal connection is open when `ACL LOAD` runs, the loop
dereferences
a NULL `c->user` and crashes the node.

## Fix
Guard on `c->user == NULL` instead of `CLIENT_MASTER`. This is a
superset:
master clients already carry `user = NULL`, so their handling is
unchanged,
while internal connections are now also skipped. Skipping internal
connections is also semantically correct — they run unrestricted (no ACL
user) and have no channel permissions to re-resolve.

---------

Co-authored-by: debing.sun <debing.sun@redis.com>
## Issue

`RedisModule_ConfigSetNumeric()` could crash when called with a
non-existent config name. `moduleSetNumericConfig()` did not check
whether `getMutableConfig()` returned `NULL` before dereferencing
`config->type`, unlike the existing bool config setter path.



## Change

Add the missing `NULL` check in `moduleSetNumericConfig()`, matching the
existing handling in `moduleSetBoolConfig()`.

Add TCL coverage for `configaccess.setnumeric nonexistent_config 1` to
verify the module API returns an error instead of crashing.

## Test
Passed:

```sh
make
./runtest --single unit/moduleapi/configaccess
```
Add `SDIFFCARD` command — a cardinality-only variant of `SDIFF` that
returns the number of elements in the set difference without sending the
actual elements of the result.

### Motivation

Similar to how `SINTERCARD` and `SUNIONCARD` provide cardinality queries
for intersection and union, `SDIFFCARD` fills the gap for set
difference. This is useful when clients only need to know the size of
the difference rather than the full set of elements.

### Implementation

- Reuses `sunionDiffGenericCommand` with `cardinality_only=1` and
`SET_OP_DIFF`, consistent with how `SUNIONCARD` and `SINTERCARD` work.
- For diff algorithm 1, skips `setTypeAddAux` entirely and just
increments a counter, with early termination when `LIMIT` is reached.
- For diff algorithm 2, positive-`LIMIT` cardinality-only requests use a
dedicated 2b path
that materializes the subtrahends, scans the first set, and stops at
`LIMIT`; unlimited
(`LIMIT 0` or omitted) requests use the normal algorithm-2 cardinality
path and compute
the full difference cardinality.
- Adds a dedicated `sdiffcardGetKeys` key-extraction function following
the same pattern as `sintercardGetKeys` / `sunioncardGetKeys`.
- `LIMIT 0` means no limit. A positive `LIMIT` caps the returned
cardinality, allowing early exit.

### Syntax
SDIFFCARD numkeys key [key ...] [LIMIT limit]

---------

Co-authored-by: debing.sun <debing.sun@redis.com>
## Module Update

This PR updates **RedisJSON** to version **8.9.80**.

## Changes

- Updated `modules/redisjson/Makefile` to set `MODULE_VERSION = v8.9.80`

## Release Information

- **Module**: RedisJSON
- **Version**: 8.9.80
- **Tag Date**: 2026-06-24
- **Makefile Path**: `modules/redisjson/Makefile`

## Verification

- [ ] Version updated correctly in Makefile
- [ ] Module builds successfully
- [ ] Tests pass
- [ ] No breaking changes

---

*This PR was automatically created by the Redis Module Release
Orchestrator.*

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Single Makefile version bump with no application or security logic
changes in this repository; risk is limited to upstream RedisJSON 8.9.80
behavior at build/runtime.
> 
> **Overview**
> Bumps the **RedisJSON** build pin from **v8.8.0** to **v8.9.80** by
updating `MODULE_VERSION` in `modules/redisjson/Makefile`.
> 
> That version is used by the shared module build (`common.mk`) to check
out the matching **redisjson/redisjson** tag when fetching sources and
producing `rejson.so`; no other files or build steps change in this PR.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
9bc0964. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
## Module Update

This PR updates **RedisBloom** to version **8.9.80**.

## Changes

- Updated `modules/redisbloom/Makefile` to set `MODULE_VERSION =
v8.9.80`

## Release Information

- **Module**: RedisBloom
- **Version**: 8.9.80
- **Tag Date**: 2026-06-24
- **Makefile Path**: `modules/redisbloom/Makefile`

## Verification

- [ ] Version updated correctly in Makefile
- [ ] Module builds successfully
- [ ] Tests pass
- [ ] No breaking changes

---

*This PR was automatically created by the Redis Module Release
Orchestrator.*

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Single version constant change with no application logic; risk is
limited to upstream RedisBloom 8.9.80 behavior or compatibility in
deployments that use this module.
> 
> **Overview**
> Bumps the **RedisBloom** build pin from **v8.8.0** to **v8.9.80** by
updating `MODULE_VERSION` in `modules/redisbloom/Makefile`.
> 
> That variable drives the upstream clone in `modules/common.mk`, so
builds will fetch and compile the **8.9.80** tag from
`redisbloom/redisbloom` instead of the previous release.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
0ebf5d7. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
## Summary
Bumps the pinned `MODULE_VERSION` for **RedisTimeSeries** from `v8.9.80`
to `v8.9.81`. RedisBloom and RedisJSON are unchanged.

| Module | From | To |
| --- | --- | --- |
| RedisTimeSeries | v8.9.80 | v8.9.81 |

> **Note:** the `v8.9.80...8.9.81` range is **diverged** — 10 new
commits on `8.9.81`, and `8.9.81` is 2 commits *behind* `v8.9.80`
(release-branch-only version/branch-cut commits, not forward-ported).
The ancestry diff contains 10 commits and **all 10 are genuinely new** —
`git cherry` marks every one `+`, so **0 are backports** already in
`v8.9.80`. One in-range note: **`TS.BGET` was renamed to `TS.READ`**
([redis#2081](RedisTimeSeries/RedisTimeSeries#2081))
with a reworked blocking syntax, so the net-new command surface is
`TS.READ`, not `TS.BGET`.

## Module changes ([v8.9.80 →
v8.9.81](RedisTimeSeries/RedisTimeSeries@v8.9.80...8.9.81))
* Use dedicated redis_ref field in ramp.yml for CI git ref
([redis#2059](RedisTimeSeries/RedisTimeSeries#2059))
* pass VG=1 flag to the build as well
([redis#2060](RedisTimeSeries/RedisTimeSeries#2060))
* MOD-16160 — wake parked TS.BGET clients on key deletion
([redis#2062](RedisTimeSeries/RedisTimeSeries#2062))
* CI: bump GitHub Actions to Node24-compatible versions
([redis#2035](RedisTimeSeries/RedisTimeSeries#2035))
* MOD-15749 — Short-form CLUSTERSET: RAMP capability + LibMR
error-return
([redis#2065](RedisTimeSeries/RedisTimeSeries#2065))
* CI: speed up Event CI (lean PR gate) and shard slow nightly jobs
([redis#2068](RedisTimeSeries/RedisTimeSeries#2068))
* MOD-6409 — TS.INFO memoryUsage (and MEMORY USAGE, via the mem_usage
callback)
([redis#2067](RedisTimeSeries/RedisTimeSeries#2067))
* MOD-16266 — fix ci hang
([redis#2066](RedisTimeSeries/RedisTimeSeries#2066))
* MOD-15565 — mrange aggregation execution in inner shards
([redis#2074](RedisTimeSeries/RedisTimeSeries#2074))
* MOD-16505 — change TS.BGET command to TS.READ
([redis#2081](RedisTimeSeries/RedisTimeSeries#2081))

**0 commits excluded — all 10 are genuinely new**
The `v8.9.80...8.9.81` ancestry diff contains exactly these 10 commits.
`git cherry -v v8.9.80 8.9.81` marks every one `+` (no patch-equivalent
change already in `v8.9.80`), so none are backports — unlike the `v8.8.0
→ v8.9.80` bump, there is nothing to exclude here.

## Test plan
* [ ]  CI passes for RedisTimeSeries at the new pinned version
* [ ]  `make all` builds RedisTimeSeries cleanly against `unstable`
* [ ]  Module tests run under `make test`
### Overview

This PR adds two new options, `MAXCOUNT` and `MAXSIZE`, to the `XREAD`
and `XREADGROUP` stream commands. They cap the **cumulative** reply
across all streams named in a single command: `MAXCOUNT` bounds the
total number of entries returned, and `MAXSIZE` bounds the total reply
size in bytes. Unlike the existing `COUNT` option — which limits entries
on a per-stream basis — these new options apply a global budget for the
whole command, giving clients a reliable way to bound a multi-stream
read.

### Problem Statement

`XREAD`/`XREADGROUP` accept multiple streams in one call, and `COUNT`
only limits entries **per stream**. A read over N streams with `COUNT C`
can therefore return up to `N * C` entries, and the total reply size is
effectively unbounded — a single command can return a very large payload
depending on how many streams match and how large each entry is.

For clients that need to bound the work and memory of a single round
trip (e.g. fan-in consumers reading many streams, or latency-sensitive
paths), there is no way today to say "give me at most K entries total"
or "at most B bytes total" across all streams in one command. Working
around this requires issuing per-stream reads and aggregating
client-side, costing extra round trips.

### Solution

Introduces two cumulative caps, applied across all streams in the
command:

- `XREAD [COUNT count] [MAXCOUNT maxcount] [MAXSIZE maxsize] [BLOCK
milliseconds] STREAMS key [key ...] id [id ...]`
- `XREADGROUP GROUP group consumer [COUNT count] [MAXCOUNT maxcount]
[MAXSIZE maxsize] [BLOCK milliseconds] [CLAIM min-idle-time] [NOACK]
STREAMS key [key ...] id [id ...]`

Semantics:

- **`MAXCOUNT`** caps the total number of entries returned across all
streams. It must be a positive integer, and must be `>= COUNT` when both
are given (since it is a cumulative cap over a per-stream limit). When
`COUNT` is omitted, `MAXCOUNT` alone bounds the total.
- **`MAXSIZE`** caps the total reply size in bytes (tracked via
`c->net_output_bytes_curr_cmd`). It must be a positive integer.
- **At least one entry is always returned.** The budget is never
enforced until at least one entry has been emitted across the whole
reply, so a single message larger than `MAXSIZE` is still returned
rather than yielding an empty reply.
- Both caps work for new (`>`) reads and for history/PEL reads in
`XREADGROUP`. For `MAXSIZE`, the limit is checked *before* delivering
the next PEL/new entry, so a skipped entry is neither sent nor added to
the consumer's PEL.

**Implementation details:**

`MAXCOUNT` is enforced in `xreadCommand()`: a running `total_entries`
counter shrinks each stream's effective `count` to the remaining budget
and stops scanning further streams once the cap is reached. `MAXSIZE` is
enforced inside the emit loops of `streamReplyWithRange()` and
`streamReplyWithRangeFromConsumerPEL()`, guarded by `(emitted_before +
arraylen) > 0` to preserve the "always serve one entry" exception. All
existing callers (`XRANGE`, `XCLAIM`, `XAUTOCLAIM`, `XINFO`, and the
consumer-PEL path) are migrated to the new struct form.

### Examples

Given three streams, each with 100 entries:

**`MAXCOUNT` caps the cumulative entry count (vs. per-stream `COUNT`):**

```
> XREAD COUNT 50 STREAMS s1 s2 s3 0 0 0
# returns 150 entries total (50 per stream)

> XREAD COUNT 50 MAXCOUNT 80 STREAMS s1 s2 s3 0 0 0
# returns 80 entries total — capped across all streams
```

**`MAXCOUNT` without `COUNT`** — bounds the total directly, filling from
the first stream onward:

```
> XREAD MAXCOUNT 7 STREAMS s1 s2 s3 0 0 0
# returns 7 entries total, all from s1
```

**`MAXSIZE` bounds the reply size in bytes:**

```
> XREAD MAXSIZE 200 STREAMS s1 s2 s3 0 0 0
# returns fewer entries than an unbounded read, but always >= 1
```

**`MAXSIZE` always returns a single oversized message:**

```
> XADD bigstream 1-1 f <5000-byte value>
> XREAD MAXSIZE 50 STREAMS bigstream 0
1) 1) "bigstream"
   2) 1) 1) "1-1"
         2) 1) "f"
            2) "<5000-byte value>"
# the single entry exceeds MAXSIZE but is still returned
```

**Both together — whichever bound triggers first wins:**

```
> XREAD MAXCOUNT 5 MAXSIZE 100000 STREAMS s1 s2 s3 0 0 0
# returns 5 entries (MAXCOUNT is the tighter bound)
```

`XREADGROUP` behaves identically for both new (`>`) reads and
history/PEL reads, and the caps are honored after a blocking client is
unblocked.

### Backward Compatibility

This is a fully additive change. `MAXCOUNT` and `MAXSIZE` are new
optional tokens recognized only within `XREAD`/`XREADGROUP`; when
omitted, behavior is exactly as before. No existing command, argument,
default, or reply format is modified, and no new command is introduced.
Co-authored-by: Ozan Tezcan <ozan.vx@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.