Compare commits

...
Author SHA1 Message Date
Antje WorringandClaude Opus 4.8 4a6c872ae3 docs: tidy LLM.md indexes; CLAUDE.md -> LLM.md symlink convention
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 09:59:49 -07:00
Hanzo DevandGitHub c4d7b32653 Merge branch 'redis:unstable' into unstable 2026-06-03 12:52:56 -07:00
Sergei GeorgievandGitHub 1d6888b20d Validate stream listpack first entry on RDB/RESTORE load (#15295)
**Summary**

The stream RDB/RESTORE loader read the first element of each stream
listpack node (the "valid entries" count) and immediately decoded it as
an integer, but under shallow validation (`sanitize-dump-payload no`)
only the listpack header had been checked — never the first entry
itself. A crafted payload whose first entry declares an oversized string
encoding (e.g. `LP_ENCODING_32BIT_STR` claiming `0x7FFFFFFF` bytes)
caused `lpGetIntegerValue()` to read encoding-dependent bytes past the
end of the listpack, triggering an out-of-bounds read / crash.

**Changes**

1. **First-entry validation before integer decode (`rdb.c`)**
In `rdbLoadObject`, the stream-loading path now obtains the first
element via `lpValidateFirst()` instead of `lpFirst()`, and then
validates that entry with `lpValidateNext()` before
`lpGetIntegerValue()` decodes it. If the entry is malformed, the load is
rejected cleanly via `rdbReportCorruptRDB("Stream listpack integrity
check failed.")` with proper cleanup (`sdsfree(nodekey)`,
`decrRefCount(o)`, `zfree(lp)`) and an early `NULL` return, matching how
other corrupt-payload cases are handled. This closes the gap where only
the listpack header — not its first entry — was checked under
`sanitize-dump-payload no`.

2. **Test (`corrupt-dump.tcl`)**
A new test (`corrupt payload: stream listpack entry with corrupt
encoding crashes lpFirst`) exercises the rejection path. It disables
payload sanitization and checksum validation, then issues a `RESTORE`
with a hand-built stream payload whose listpack has a valid header but a
first entry encoded as `LP_ENCODING_32BIT_STR` (`0xF0`) declaring a
`0x7FFFFFFF`-byte string that runs past the end of the listpack. The
test asserts the command fails with `*Bad data format*`, verifies the
`*Stream listpack integrity check failed*` warning is logged, and
confirms the server survives (`r ping`).
2026-06-03 10:03:25 +03:00
c0a83262a3 Fix integer overflow in cluster bus PUBLISH and MODULE message length validation (#15187)
## Summary

Add overflow checks for attacker-controlled `uint32_t` length fields in
`clusterProcessPacket()` before adding them to `explen` (`uint32_t`).
Without these checks, crafted PUBLISH/PUBLISHSHARD and MODULE cluster
bus messages can wrap `explen` to a small value, bypassing the `totlen
!= explen` validation and causing heap out-of-bounds reads or OOM aborts
in the processing path.

## Problem

In `clusterProcessPacket()`, the expected packet length for PUBLISH and
MODULE messages is computed by summing struct overhead with
variable-length fields read from the packet header via `ntohl()`:

```c
// PUBLISH (line 2841-2846)
explen += sizeof(clusterMsgDataPublish) - 8 +
    ntohl(hdr->data.publish.msg.channel_len) +
    ntohl(hdr->data.publish.msg.message_len);

// MODULE (line 2855-2858)
explen += sizeof(clusterMsgModule) - 3 + ntohl(hdr->data.module.msg.len);
```

Both `channel_len + message_len` (PUBLISH) and `len` (MODULE) are
`uint32_t` values from the network packet. Their addition to `explen`
can overflow `uint32_t`, wrapping to a small value that matches
`totlen`, passing the size validation at line 2864.

**Example (PUBLISH):** With `channel_len = 0x80000000` and `message_len
= 0x80000000`, their sum overflows to `0`, making `explen` equal to just
the base struct size. An attacker sets `totlen` to match. The validation
passes, and the processing path at line 3273 calls
`createStringObject()` with the original 2GB lengths, reading far past
the received buffer.

**Attack vector:** Reachable via the cluster bus port (default: data
port + 10000), which does not require authentication by default.

## Fix

Extract `ntohl` values into local variables and check each addition
against `UINT32_MAX` before performing it. Reject the packet with a
warning log if overflow is detected. The computed `explen` is identical
to the original for all non-overflowing (legitimate) inputs.

---------

Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
Co-authored-by: debing.sun <debing.sun@redis.com>
2026-06-03 12:09:49 +08:00
Hanzo AI b46813a86f merge: upstream/unstable (preserve Hanzo brand, 117 commits) 2026-06-02 10:00:55 -07:00
2df35f9fb0 Avoid double-walk on find-then-insert via raxFindLink + raxInsertAt (#15252)
Add `raxFindLink()` + `raxInsertAt()`: a two-step commit API that
lets a caller walk the rax once to find a key, then commit an insert
at the recorded position without re-walking. Today's
"lookup then insert" pattern (`raxFind` + `raxInsert` on miss) walks
the tree twice; this change collapses the worst case to a single
walk.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: debing.sun <debing.sun@redis.com>
2026-06-02 18:01:16 +03:00
GuyAv46andGitHub 1b3721eb22 Clarify RedisModule_GetClusterNodeInfo buffer sizes (#15268)
Updates the in-source documentation for RedisModule_GetClusterNodeInfo
so module authors size caller buffers correctly.
2026-06-02 17:24:44 +08:00
Vitah LinandGitHub 148de48334 Fix potential TCP deadlock in module datatype defrag test (#15274)
### Issue
The module datatype defrag test sends 20k commands through a deferred
client before reading any replies. On slower CI environments this can
cause replies to accumulate and fill TCP/socket buffers, leading to
flaky `I/O error reading reply` failures.

### Change

Fix by batching deferred writes and reply drains, following the same
approach used in #14886.
2026-06-02 09:37:29 +08:00
nuybandGitHub 09b5c95b98 Fix AUTH failure on URIs with empty username in redis-cli (#15255)
Partial fix #11085

## Bug

`parseRedisUri()` called `percentDecode(curr, 0)` for URIs of the form
`redis://:password@host` and stored the resulting **empty-but-non-NULL**
sds into `connInfo->user`.
`cliAuth()` only checks `user == NULL` when deciding between legacy
`AUTH <pass>` and ACL `AUTH <user> <pass>`, so the empty username was
sent as `AUTH "" <password>`, which the server rejects with `WRONGPASS`.

## Fix

In parseRedisUri(), treat explicitly empty username or password
components as NULL rather than empty SDS strings. This allows cliAuth()
to fall back to legacy single-argument AUTH when the username is empty,
and to skip AUTH entirely when the password is empty.

Before replacing URI-parsed credentials, existing connInfo->user and
connInfo->auth values are freed to avoid leaks and preserve the expected
"later arguments override earlier ones" behavior.

As a related cleanup, -a/--pass and --user now also free previously
assigned values before reassignment, fixing the same leak pattern when
those options are specified multiple times.
2026-06-02 09:29:56 +08:00
Hanzo AI 9cc798fa10 merge: ci/canonical-docker-build-1776995843 (-X theirs) 2026-06-01 18:10:36 -07:00
Cong ChenandGitHub 230c651c89 tests:remove flaky SAMPLE 1000 case from hotkeys biased-access test (#15285)
Fixes: test faulure in
https://github.com/redis/redis/actions/runs/26698983853/job/78688240089

While investigating the failure, I added temporary debug output:

```tcl
puts "cpu_time_array=$cpu_time_array"
puts "num_returned_cpu=$num_returned_cpu"
puts "res=$res"
```

The failing SAMPLE 1000 run produced:

```
cpu_time_array=key_010 3 key_000 3 key_015 2 key_005 2 key_013 2 key_056 2 key_054 2 key_046 1 key_044 1 key_049 1
num_returned_cpu=10
res=5
```

The test becomes statistically unstable at SAMPLE 1000. With only ~50
sampled operations out of 50k requests, the accumulated CPU times
collapse into a very narrow range (observed: 1–3 µs), causing frequent
ties between hot and cold keys near the Top-K cutoff. The resulting
failures are driven by sampling variance rather than HOTKEYS
correctness.

Since the test already covers sampling behavior with ratios 1, 100, and
500, removing the 1000 case preserves coverage while eliminating a
configuration that is too sparse to produce stable, reproducible
assertions.
2026-06-01 17:05:36 +03:00
nuybandGitHub 7f532eacdf Fix garbled "source node not found" error in atomic slot migration (#15284)
asmSyncWithSource() built the error with sdscatfmt() and a "%.40s"
specifier. sdscatfmt() is not printf: it parses only the single byte
after '%' and ignores width/precision, so "%.40s" emits a literal '.',
consumes no argument, and task->source is never printed. The message
rendered as "Source node .40s was not found", dropping the node name.

task->source is a CLUSTER_NAMELEN (40) byte, non-NUL-terminated buffer
(filled via memcpy and always read with an explicit length elsewhere),
so simply switching to sdscatfmt's "%s" would strlen() past the buffer.
Use sdscatprintf(), which honors the "%.40s" precision and bounds the
read to 40 bytes -- matching the sibling error paths in this function
that already use sdscatprintf().
2026-06-01 21:27:35 +08:00
Vitah LinandGitHub 8fcf3dc866 Fix vector set tests to use RESP2 for default clients (#15287)
## Issue

The vector set Python tests intentionally use two clients: 
- the default client (`self.redis`) for the existing RESP2-oriented test
expectations
- `self.redis3` for RESP3-specific coverage.

However, the default client did not explicitly set a protocol, so it
depended on redis-py's default behavior. With newer redis-py versions,
RESP3 is now the default
protocol(https://github.com/redis/redis-py/pull/4052). In particular,
vector set replies such as `VSIM ... WITHSCORES` may be parsed into
map/dict-like structures instead of the RESP2 flat-array shape assumed
by existing tests.

## Changes

Explicitly create the default primary and replica Redis clients with
`protocol=2`.
`self.redis3` is left unchanged and continues to use `protocol=3` for
RESP3-specific test coverage.
2026-06-01 13:31:20 +08:00
Hanzo DevandGitHub 789b1b7b95 Merge pull request #1 from hanzoai/upstream-sync-20260327
upstream: merge redis/redis unstable (58 commits)
2026-05-29 21:08:32 -07:00
65401042dc Offload length formatting in tryAvoidBulkStrCopyToReply() to IO thread (#14844)
## Summary

In the reply copy-avoidance path (bulkStrRef), the RESP bulk-string
prefix `$<len>\r\n` was formatted eagerly on the main thread inside
`_addBulkStrRefToBufferOrList()`. This PR defers that formatting to
write time via a new idempotent helper `formatBulkStrRefPrefix()`, so
the work happens on the IO thread for clients served by IO threads.

---------

Co-authored-by: Yuan Wang <yuan.wang@redis.com>
2026-05-29 11:01:10 +08:00
Vitah LinandGitHub a36cfc0829 Fix flaky slot-stats inline network-bytes-in test (#15279)
### Issue

CLUSTER SLOT-STATS network-bytes-in, in-line buffer processing can fail
because the test sends an inline SET through a deferring client and
immediately checks slot stats from another client.

$rd flush only guarantees the request was written to the socket; it does
not guarantee Redis has processed the command and updated
network-bytes-in.

### Change

Wait for the inline SET reply before reading CLUSTER SLOT-STATS.
2026-05-29 09:57:59 +08:00
30f57f32bd Optimize redis-cli interactive output waiting (#15264)
### Issue

This refines the fix from #15119.

The previous change increased the generic `read_cli` empty-read retry
threshold from 5 to 100 to reduce flakiness in the redis-cli reverse
search no-result test. While effective, that made every interactive CLI
read potentially wait longer.

### Changes

This restores the original generic `read_cli` behavior and uses targeted
pattern-based waiting only where needed.

---------

Co-authored-by: debing.sun <debing.sun@redis.com>
2026-05-28 09:20:15 +08:00
h.o.t. neglectedandGitHub 138263a1b4 Fix cache line false sharing on per-IO-thread stat counters (#14907)
`stat_io_reads_processed[]` and `stat_io_writes_processed[]` were
per-IO-thread arrays inside `struct redisServer` that suffer from false
sharing. This PR moves the two stat counters into the IOThread struct,
which is already `__attribute__((aligned(CACHE_LINE_SIZE)))`. Each IO
thread's counters now sit on a separate cache line, eliminating the
cross-thread contention.
- Added io_reads_processed and io_writes_processed fields to IOThread struct
- Removed stat_io_reads_processed[] and stat_io_writes_processed[] from struct redisServer
- Made IOThreads[] non-static with extern declaration in server.h
2026-05-26 11:55:38 +08:00
f023cf026b Adjust fastfloat tests for libc-dependent nan payload parsing (#15110)
## Problem

`strtod()` handles some `nan(n-char-sequence)` inputs differently across
libc implementations. For example, `nan(ab!c)` and `nan(ab c)` may be
accepted on some platforms but rejected on others.

The existing test treated these inputs as fixed invalid cases, which can
fail on platforms whose libc accepts them.

## Changes

- Move libc-dependent `nan(...)` cases out of the fixed invalid test
list.
- Add a helper to verify `fast_float_strtod()` matches the platform
`strtod()` behavior for these cases, including value, `endptr`, and
success/failure status.
- Keep the existing parser behavior unchanged.

---------

Co-authored-by: debing.sun <debing.sun@redis.com>
2026-05-26 09:35:01 +08:00
fa08257fcb Fix flaky redis-cli reverse search no-result test (#15119)
## Problem

The `redis-cli` reverse-search test for the no-result case can be flaky
in slower CI environments.

`read_cli` may return too early when CLI output is fragmented or
delayed. It currently gives up after only 5 consecutive empty reads,
with a 10ms delay between reads, which can make the test assert before
the expected `(empty array)` output is printed.

## Changes

Increase the `read_cli` consecutive empty-read threshold from `5` to
`100`.

This keeps the existing read behavior unchanged when data is available,
but allows the helper to wait longer for delayed/fragmented CLI output
before giving up.

---------

Co-authored-by: debing.sun <debing.sun@redis.com>
2026-05-26 08:57:13 +08:00
13d9553673 Add concurrency groups to cancel stale GH workflow runs (#15224)
This is based on
[valkey-io/valkey#849](https://github.com/valkey-io/valkey/pull/849)

Introduce `concurrency` and `group` keywords into workflows execpt from
`redis_docs_sync` (which is triggered on release event only) and
`daily`.

https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/control-workflow-concurrency

With this, only one workflow can run in a group at any time.

---------

Co-authored-by: Yury-Fridlyand <yury.fridlyand@improving.com>
2026-05-26 08:54:39 +08:00
dariaguyandGitHub 8757b125b2 Remove handle-werrors target from modules/Makefile (#15257) 2026-05-25 23:18:16 +03:00
1587755a22 Add RM_GetClusterNodeSlotRanges module API (#14953)
Add new module API `RM_GetClusterNodeSlotRanges` that allows modules
to query slot ranges for any cluster node by its node ID, not just the
local node:

```c
RedisModuleSlotRangeArray *RM_GetClusterNodeSlotRanges(RedisModuleCtx *ctx, const char *nodeid)
```

---------

Co-authored-by: Ozan Tezcan <ozantezcan@gmail.com>
Co-authored-by: Yuan Wang <yuan.wang@redis.com>
2026-05-25 22:56:44 +08:00
Vitah LinandGitHub 368786ead5 Update GH Actions dependency (Node.js 20 deprecation) (#15216)
This is a follow-up to
[redis/redis#14938](https://github.com/redis/redis/pull/14938), which
upgraded GitHub Actions to newer stable versions for the upcoming
Node.js 20 deprecation on GitHub Actions runners.

That PR missed two remaining action updates in `daily.yml`:

- `cross-platform-actions/action`
- `py-actions/py-dependency-install`

### Why replace `py-actions/py-dependency-install`

`py-actions/py-dependency-install` is no longer an actively maintained
dependency installation action, so keeping it in CI increases
maintenance and supply-chain risk over time.

The replacement uses GitHub's official `actions/setup-python` action,
which is actively maintained and supports built-in `pip` dependency
caching. Installing dependencies with `python -m pip install -r
./utils/req-res-validator/requirements.txt` also makes the workflow
behavior explicit and easier to debug.
2026-05-25 19:20:09 +08:00
f9b7fa0a96 Update DataType Modules to v8.8.0 (#15238)
## Summary

Bumps `MODULE_VERSION` for RedisBloom, RedisJSON, and RedisTimeSeries
from `v8.7.91` to `v8.8.0`.

| Module          | From    | To     |
|-----------------|---------|--------|
| RedisBloom      | v8.7.91 | v8.8.0 |
| RedisJSON       | v8.7.91 | v8.8.0 |
| RedisTimeSeries | v8.7.91 | v8.8.0 |

## Module changes (v8.7.91 → v8.8.0)

### RedisBloom
([v8.7.91...v8.8.0](https://github.com/RedisBloom/RedisBloom/compare/v8.7.91...v8.8.0))
- MOD-15418 — fix load rdb mem leak
([#1007](https://github.com/RedisBloom/RedisBloom/pull/1007))
- Revert "fix redis version"
([#1010](https://github.com/RedisBloom/RedisBloom/pull/1010))
- bump version to v8.8.0

### RedisJSON
([v8.7.91...v8.8.0](https://github.com/RedisJSON/RedisJSON/compare/v8.7.91...v8.8.0))
- Revert "fix redis version"
([#1597](https://github.com/RedisJSON/RedisJSON/pull/1597))
- bump version v8.8.0

### RedisTimeSeries
([v8.7.91...v8.8.0](https://github.com/RedisTimeSeries/RedisTimeSeries/compare/v8.7.91...v8.8.0))
- MOD-14439 — Detect cluster topology changes during a multi-shard
command and return an appropriate error
([#1930](https://github.com/RedisTimeSeries/RedisTimeSeries/pull/1930))
- Revert Docker and CI redis-ref from 8.8 back to unstable
([#2033](https://github.com/RedisTimeSeries/RedisTimeSeries/pull/2033))
- bump version v8.8.0

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

🤖 Generated with [Claude Code](https://claude.com/claude-code)



Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 11:52:41 +03:00
Omer ShadmiandGitHub ba1a4b2c8f Update RediSearch module to v8.8.0 (#15228)
Updates the bundled RediSearch module version used by the Redis 8.8
branch from `v8.7.91` to `v8.8.0`.
2026-05-20 15:45:06 +03:00
Vitah LinandGitHub 457a1a542d Test fix "too many open files" on macOS (#14853)
On macOS, running `make test` often fails with "too many open files" due
to the low default limit (usually 256).

This PR increases the limit by adding `ulimit -n 4096` so that the tests
have enough file descriptors for concurrent connections.
2026-05-20 19:01:45 +08:00
95040d61d5 Replace INCREX out-of-bounds policy to a single SATURATE option (#15237)
Follow https://github.com/redis/redis/issues/15045

## Summary

Simplify INCREX's out-of-bounds policy:

The original INCREX shipped with three out-of-bounds policies — OVERFLOW
FAIL, OVERFLOW SAT, OVERFLOW REJECT — but FAIL and REJECT are
functionally redundant: both leave the key untouched when the result is
out of bounds. They differ only in how the caller is notified (error
reply vs. [current_value, 0] array reply), which forces the user to make
a stylistic choice with no real semantic difference.

This PR collapses the three policies into one clear behavior:

* Default: the operation is rejected; the key value and TTL are left
unchanged, and the reply is [current_value, 0]. Callers detect
non-application by checking the applied-increment field; no
error-handling branch is required.
* SATURATE: the result is saturated to UBOUND / LBOUND, or to the type
limits (LLONG_MAX/MIN for BYINT, ±LDBL_MAX for BYFLOAT) when no explicit
bound is given.

New syntax:

    INCREX <key> [BYFLOAT increment | BYINT increment]
                 [LBOUND lowerbound] [UBOUND upperbound] [SATURATE]
[EX seconds | PX milliseconds | EXAT seconds-timestamp | PXAT
milliseconds-timestamp | PERSIST] [ENX]

---------

Co-authored-by: Ozan Tezcan <ozantezcan@gmail.com>
2026-05-20 18:07:14 +08:00
31896140d1 Fix diskless replicas drop during rdb pipe test (#15131)
This PR is based on: valkey-io/valkey#3511
Close https://github.com/redis/redis/issues/14983

## Summary

During diskless replication, if **any single replica** cannot accept a
write (TCP send buffer full / `EAGAIN`), the master stops reading the
RDB pipe entirely, stalling data delivery to **all** replicas —
including fast ones that are ready to receive data.

The failure reason is similar to
https://github.com/redis/redis/pull/14946, the socket buffer is more
easy to fill.

## Root Cause

In `rdbPipeReadHandler`, the master reads from the child's RDB pipe and
writes to all replica sockets in a loop. When `connWrite` to any replica
returns a partial write (socket send buffer full), the handler:

1. Installs a per-replica `rdbPipeWriteHandler` and increments
`rdb_pipe_numconns_writing`
2. **Removes the pipe read event** via `aeDeleteFileEvent(server.el,
server.rdb_pipe_read, AE_READABLE)`, stopping all pipe reads

The pipe read event is only re-enabled when **all** pending write
handlers complete (`rdb_pipe_numconns_writing == 0`), meaning the
**slowest replica dictates the throughput for all replicas**.

## Observed Behavior

With one slow replica (consuming at ~290 KB/s due to `key-load-delay`):

- Master bursts ~1.3 MB of RDB data until the slow replica's socket send
buffer fills
- `rdbPipeReadHandler` disables the pipe read event
- **All replicas starve for 4–5 seconds** while the slow replica drains
its buffer
- Cycle repeats: burst → stall → burst → stall

Ultimately, it leads to a very slow synchronization process of the
entire master and replica.

### Changes

1. Skip the entire `diskless replicas drop during rdb pipe` test under
Valgrind to avoid timing flakiness on slow env.

2. Move `start_server` inside the `foreach all_drop` loop so each
subcase gets a fresh master instead of sharing state across subcases.

3. For `no / slow / fast / all` subcases, replica 0 runs with
`key-load-delay 500`, which combined with the blocked-writer TCP
back-pressure can stall the RDB-saving child indefinitely; shrink the
dataset to ~40 MB so the transfer still exercises the blocked-writer
path but completes in reasonable time instead of hanging on the TCP
deadlock.
For the timeout subcase, replica 0 does not run with `key-load-delay
500`, so to avoid the TCP deadlock we still reduce the dataset somewhat,
but keep it larger than the other subcases. Otherwise the kernel TCP
send buffer can absorb the whole RDB, and we'd miss the
repl_last_partial_write != 0 "(full sync)" timeout path and only hit the
"(streaming sync)" path instead.

5. For the `all` subcase, set `rdb-key-save-delay 1000` on the master so
the RDB child keeps generating data while both replicas are killed,
ensuring the last-replica-drop path is exercised rather than racing with
normal completion.

6. Move the slow-replica `pause_process()` so it happens only in the
timeout subcase, not after killing replicas, so Redis observes the
disconnect promptly in non-timeout flows.

7. In the timeout subcase, set `repl-timeout` 2, wait inline for
`*Disconnecting timedout replica (full sync)*`, then restore
`repl-timeout` 60 so the remaining replica can finish the streamed RDB.

---------

Co-authored-by: Sarthak Aggarwal <sarthagg@amazon.com>  
Co-authored-by: debing.sun <debing.sun@redis.com>
2026-05-19 18:27:33 +08:00
Luca PalmieriandGitHub 842a28f4a1 Configure RediSearch to avoid regenerating C headers for Rust modules (#15220)
Fixes the nightly build breakage.
It requires https://github.com/RediSearch/RediSearch/pull/9669 to be
merged first.
2026-05-19 10:04:29 +03:00
0d06e515af Fix compile on some linux distros (#15225)
Fixes: 

1. After #15096, we pass -flto to jemalloc. On Azure Linux, the
resulting jemalloc library cannot be handled at link time and the build
fails. Adding -ffat-lto-objects so the compiler also emits regular
object code that the linker can fall back to when it cannot handle the
LTO-compiled library.
2. Fixed a warning about `path` being NULL in
`moduleLoadInternalModules()`.
3. Fixed compile warnings on older GCC versions introduced by #15162  
    (reported on Ubuntu 20.04)

Co-authored-by: debing.sun <debing.sun@redis.com>
2026-05-18 15:47:08 +03:00
Mincho PaskalevandGitHub b1a53ea21f Add error on enabling memory tracking in non-clustered mode (#15005)
Enabling memory tracking is forbidden during runtime if it is already
disabled. In non-clustered mode though the checks were incorrect so this
PR enforces the correct behavior in non-clustered environment.
2026-05-15 18:35:07 +03:00
stav-nachmiasandGitHub 0c1c747062 RED-196433 remove unused post release workflow (#15206) 2026-05-14 20:59:17 +03:00
6c3a8eccef Set default for INLINE_LSE_ATOMICS to 0 for compatibility across architectures (#15212)
Ensure backward compatibility and consistent behavior across different
architectures by explicitly setting the default value.

Fixes #15175


Co-authored-by: ofiryanai <ofiryanai1@gmail.com>
2026-05-14 17:14:15 +03:00
2e46d2e735 Hold GCRA out of the release (#15191)
After introducing GCRA algorithm into redis
https://github.com/redis/redis/pull/14826 and subsequent introduction of
new RATE_LIMIT object type - https://github.com/redis/redis/pull/14905.
It was internally decided not to introduce GCRA into the new release.

As still no decision is made on whether it will be kept or not in the
future, this PR only makes the code related to GCRA dead - commands are
inaccessible and AOF/RDB load+save is disabled.

---------

Co-authored-by: debing.sun <debing.sun@redis.com>
2026-05-14 16:31:25 +03:00
Lior KoganandGitHub c3db5254b7 Added Array, rename RQE to Search (#15186) 2026-05-14 14:58:48 +03:00
Omer ShadmiandGitHub 2ad98a4e3c Update RediSearch to v8.7.91 (#15199) 2026-05-14 14:23:32 +03:00
Tom GabsowandGitHub 5faaed6e6b updating Data Types to RC1 (#15201)
JSON - 8.7.91:
* Cherry-pick bug fixes to 8.8 (RedisJSON/RedisJSON#1557)
* MOD-6722 Fix mutation ordering for array commands with recursive paths
(RedisJSON/RedisJSON#1543)
* MOD-7266 make sure to end() deserialization, to disallow trailing c…
(RedisJSON/RedisJSON#1554)
* MOD-14664 Json Path evaluation - Allow multi-result nodelists in filt…
(RedisJSON/RedisJSON#1542)

Bloom - 8.7.91
* Lili: RED-180951 RED-181297 RED-184457 RED-184456 RED-184458
RED-184459 fixing bugs and improving the code
(RedisBloom/RedisBloom#1002)
* MOD-14675 - refresh os list to 8.8 (RedisBloom/RedisBloom#976)

Time Series - 8.7.91:
* MOD-15262 - Align TimeSeries with the RedisModule_GetUserUserName API…
(RedisTimeSeries/RedisTimeSeries#1985)
* MOD-14420 fix count reducers return wrong NaN
(RedisTimeSeries/RedisTimeSeries#2013)
(RedisTimeSeries/RedisTimeSeries#2016)
* Lili- RED-180951 RED-180027 fixing bugs and improving the code
(RedisTimeSeries/RedisTimeSeries#2003)
* MOD-14674 - refresh os list to 8.8
(RedisTimeSeries/RedisTimeSeries#1946)
2026-05-14 14:11:59 +03:00
Scott LasleyandYaacovHazan dbe5d1e605 Fix invalid repl-diskless-load value in replication test (#15178)
Close #15177
Follow [Fix use-after-free when fullsync happens while replica is
running a timed out script
(CVE-2026-23631)](https://github.com/redis/redis/commit/0cca172a174642bdae03b871615227896274d9bb)

Remove the `repl-diskless-load yes` test configuration because this
option exists only in the Redis fork and is not available in Redis OSS.

(cherry picked from commit 5033e15143d100bdc004bc69399b9d013d52ff23)
2026-05-14 14:10:41 +03:00
dannysheynandYaacovHazan 54ea50c029 Fix cluster-announce-ip rejecting hostnames (#15188)
Fixes [#15183](https://github.com/redis/redis/issues/15183).

## Motivation
Commit
[cf668f2c2](https://github.com/redis/redis/commit/cf668f2c2c782ea12dc88458bfd329cf6eb5d658)
tightened cluster-announce-ip validation to require a valid IPv4 or IPv6
address, which is a regression for users that legitimately announce a
hostname.

## Changes
* isValidClusterAnnounceIp() now accepts either:
   * A valid IPv4/IPv6 address
   * A valid hostname — same character rules as cluster-announce-hostname,
length-bounded by NET_IP_STR_LEN to match the storage buffer.

(cherry picked from commit 21f2569f9b577e2acb560f83652e2679c5bd6c92)
2026-05-14 14:10:41 +03:00
ofiryanaiandYaacovHazan c8f1ec959a Limit VADD REDUCE dim to not exceed original dim
* Limit VADD REDUCE dim to not exceed original dim

Enforce VADD key [REDUCE dim]  to reject dim that is bigger than the HNSW original dim, as dimension reduction makes no sense for reduce_dim > original_dim.
This also avoids OOM and possible heap overflow on later allocations using reduce_dim.

This should be backported to Redis version 8.0, 8.2 and 8.4.
2026-05-14 14:10:41 +03:00
bc904dab04 Vector Sets: Limit sane max dim
Co-authored-by: GuyAv46 <47632673+GuyAv46@users.noreply.github.com>
2026-05-14 14:10:41 +03:00
Stav-LeviandYaacovHazan 22f1ab6e27 Fix cluster AUX-field newline/control-character injection bricks a node on restart
* fix cluster AUX-field newline/control-character injection bricks a node on restart
2026-05-14 14:10:41 +03:00
debing.sunandYaacovHazan 5c355b68ec Fix use-after-free when evicting blocked client during unblock (CVE-2026-23479)
When re-executing a pending command after unblocking, check the return value
of `processCommandAndResetClient` and exit if needed.
2026-05-14 14:10:41 +03:00
Ozan TezcanandYaacovHazan 837ca7f8f4 Fix use-after-free when fullsync happens while replica is running a timed out script (CVE-2026-23631)
Fullsync triggers emptyData and scriptingReset which free the scripting/function engine. If a timed out script is still running on the replica, this causes a use-after-free. Delay fullsync processing in readSyncBulkPayload until the script finishes.
2026-05-14 14:10:41 +03:00
Stav-LeviandYaacovHazan 8cdf9391da Fix crash in Lua debugger when error object is not a string 2026-05-14 14:10:41 +03:00
Sergei GeorgievandYaacovHazan c4e3405704 Invalid Memory Access in Redis RESTORE Command (CVE-2026-25243) 2026-05-14 14:10:41 +03:00
sggeorgievandYaacovHazan 80621b1d0e Add DENYOOM flag to SUBSCRIBE, PSUBSCRIBE and SSUBSCRIBE commands
Add the DENYOOM flag to SUBSCRIBE, PSUBSCRIBE, and SSUBSCRIBE commands
to bring their memory protection behavior in line with other Redis commands.

Problem:
Currently, subscribe commands lack memory protection when Redis reaches its
memory limit. This becomes problematic in two specific scenarios:
1. When the eviction policy doesn't allow eviction (e.g., noeviction)
2. When there are no evictable keys remaining in the database

In these cases, memory usage from pub/sub subscribers can keep growing
unchecked, potentially causing the Redis server to run out of memory. This
behavior is inconsistent with other Redis commands, which are protected by
the DENYOOM flag.

Solution:
Add the DENYOOM flag to all subscribe commands. When memory limits are
reached, these commands will be rejected, preventing uncontrolled memory
growth and aligning their behavior with other Redis commands.
2026-05-14 14:10:41 +03:00
Shubham S TapleandGitHub fab099cdcf Fix double free when loading streams with duplicate consumer PEL entries (#15095)
Fixes #15082

## Problem
Loading a stream from RDB/RESTORE with a malformed consumer PEL (the
same pending ID listed twice for one consumer) hit an error path that
called streamFreeNACK() on a nack that was still referenced from the
group’s global PEL (cgroup->pel). Teardown then freed that nack again
while destroying the stream, causing a double free and a possible server
crash.

## Fix
On the duplicate consumer PEL branch in src/rdb.c, stop calling
streamFreeNACK(s, nack) when raxTryInsert(consumer->pel, …) fails. Keep
reporting corruption and rely on decrRefCount(o) for cleanup, consistent
with other paths where the nack is owned only by cgroup->pel.
2026-05-14 14:17:47 +08:00
0d9576435f Implement the new Redis Array type (#15162)
# Redis Array

For years, Redis has been missing a real indexed data structure for the
use cases where the index and the spatial relationship of elements are
semantic. Hashes give you random lookups, but you have to store an index
as a key, and have no range visibility. Lists give you appending and
trimming, but what is in the middle remains hard to access. Streams give
you append-only events, which is another (useful, indeed) beast. None of
these is what you want when the *position itself* has business meaning —
slot 37, step 4, row 18552, day from 2934 to 2949, file line 11, 12, 15
and so forth. And, all those types, for different reasons, are all
suboptimal when you want a **ring buffer** able to store the latest N
observed samples of something.

Up to now, users found ways (they always do \o/) using the fact that the
data structures that are obvious in this universe are also extremely
powerful, if well implemented. But this forces compromises. Arrays
handle these index-first requirements natively, and usually with much
better memory and CPU usage than the workarounds. If the use case is the
right one, Arrays often provide much better space, time and usability at
the same time.

## Internal encoding

1. When dense, an Array is essentially a more fancy C array. You don't
pay anything for storing the index.
2. Yet, instead of going really flat, arrays are sliced into
4096-element slices, and each slice, when it contains just a few
elements, uses a special sparse encoding. When a slice is empty it's
just a `NULL` stored in the directory.
3. Small ints, floats, and short strings are pointer-tagged, so they
cost zero additional memory beyond the pointer slot itself.
4. When very sparse, a super-directory of windowed directories is used.
This allows the data type to be safe, instead of exhibiting pathological
space or time behavior. This representation is only triggered when there
are more than 8 million elements or very high indexes set.

## Use cases

Arrays are mostly stateless if not for the fact that each array
remembers the index of the latest added item, allowing `ARINSERT` and
`ARRING` to work properly. Otherwise it is a set/get at this index game,
with solid support for both setting / getting ranges, server-side
scanning, returning only populated elements in a time which is
proportional not to the range size, but to the population size.

A few concrete examples, that may work as mental models for the set of
problems that are similar to them (from the POV of the data modeling).

**Thermometer.** A sensor reporting once per minute, with gaps:

```
ARSET       temp:room12:day7 123 22.3
ARGETRANGE  temp:room12:day7 600 660     # the 10:00–11:00 window, with NULLs
ARSCAN      temp:room12:day7 600 660     # only populated elements
AROP        temp:room12:day7 0 1439 MAX  # peak of the day, server-side
```

Missing minutes cost little to nothing. Numeric aggregation runs inside
Redis. Telemetry, IoT, meter readings, KPI rollups.

**Calendar.** A clinic with 96 fifteen-minute slots per day:

```
ARSET       sched:room12:day 32 booking:991
ARSCAN      sched:room12:day 0 95      # only occupied slots
ARGETRANGE  sched:room12:day 48 63     # the afternoon full view to render
```

The slot number is the business key in this case. Room booking, parking
spaces, warehouse bins, lockers, ...

**Ring buffer.** ARRING replaces the classic LPUSH+LTRIM pattern.
Imagine remote `dmesg`.

```
ARRING       machine:123 200 "[141087.430123]: arm_cpu_init(): cpu 14 online" # Capped to 200 entries
ARLASTITEMS  machine:123 50 REV       # 50 newest first
```

Faster than LPUSH+LTRIM, keep indexed access to past elements. Last-N
alarms, recent fraud scores, access history, remote logs, device events.
Ok here the use cases are mainly the ones of the old pattern: it is just
a better fit and allows to access random items in the middle, aggregate
server-side, and so forth.

**Workflow.** Step number is the index, value is the status. Gaps are
meaningful:

```
ARSET       claim:99172 0 received
ARSET       claim:99172 3 waiting:reviewer42
ARSET       claim:99172 5 approved
ARGETRANGE  claim:99172 0 5     # full workflow view, with NULLs for missing steps
ARSCAN      claim:99172 0 5     # only steps that have a state
ARCOUNT     claim:99172         # number of recorded steps
ARLEN       claim:99172         # highest reached step + 1
```

**Skills knowledge base for agents.** Arrays are good at representing /
grepping into Markdown files:

```
ARSET   skill:metal_gpu 0 "...."
ARSET   skill:metal_gpu 1 "...."
ARSET   skill:metal_gpu 2 "...."
ARGREP  skill:metal_gpu - + RE "M3|M4" WITHVALUES
```

ARGREP has EXACT, MATCH, GLOB, RE, you can have multiple predicates, can
select AND or OR behavior.

**Bulk import results.** Sparse row annotations over millions of rows /
CSV / ...:

```
ARSET  import:job551 18552 ERR:bad_email
ARSCAN import:job551 0 1000000        # Provides only rows that have something
```

## TLDR

If the position is part of the meaning, use an Array. If you want to
aggregate or grep remotely, use an Array. Feedback welcome :)

---------

Co-authored-by: debing.sun <debing.sun@redis.com>
Co-authored-by: Shubham S Taple <155555100+ShubhamTaple@users.noreply.github.com>
Co-authored-by: Yuan Wang <yuan.wang@redis.com>
Co-authored-by: Marc Gravell <marc.gravell@gmail.com>
2026-05-14 00:56:44 +08:00
Filipe Oliveira (Redis)andGitHub 3ab7fe0812 fast_float_strtod: fix ±1 ULP rounding mismatch in widened fast path (#15111)
## Summary

Fixes a ±1 ULP rounding mismatch between `fast_float_strtod()` and libc
`strtod()` in the widened (mantissa > 2^53) fast path introduced by
#15061. Reported by @vitahlin in
https://github.com/redis/redis/pull/14661#issuecomment-4320058616 with
two minimal reproducers:

```
input: 9007199255094284e-19
  fast_float_strtod: 0x3f4d83c94fbbcb8a
  libc strtod      : 0x3f4d83c94fbbcb8b   delta -1 ULP

input: 2489830482329185244e1
  fast_float_strtod: 0x43f59888c51e5b4c
  libc strtod      : 0x43f59888c51e5b4b   delta +1 ULP
```

Redis treats `strtod()` as the fallback for `fast_float_strtod()`, so
every fast-path-accepted input is contractually expected to be bit-exact
with `strtod()`. The two cases above are accepted by the widened branch
but produce a different IEEE-754 representation, breaking the contract.

## Root cause

The widened branch added in #15061 used a homebrew shortcut to convert a
128-bit integer product to a double:

```c
value = (double)hi * 18446744073709551616.0 + (double)lo;
```

This is **not** a single-rounding operation — `(double)hi` rounds when
`hi > 2^53`, and the subsequent `+ (double)lo` rounds again. For inputs
near the round-half-to-even boundary, the two roundings can compose into
the wrong direction.

The negative-exponent branch is doubly affected: integer division
`scaled / divisor` truncates the remainder before the conversion, so
even a hypothetical correct `hi*2^64+lo` step would round down on inputs
that should round up.

## Fix

Replace the homebrew widened branch with the **Eisel-Lemire** algorithm
from upstream `fast_float`. This is the same algorithm that
`fast_float`'s own widened path uses; bit-exact-with-strtod for inputs
with ≤19-digit mantissa is proved in:

> Noble Mushtak and Daniel Lemire, "Fast Number Parsing Without
Fallback".

Pieces ported (MIT-licensed, from
`fast_float/include/fast_float/fast_table.h`, `decimal_to_binary.h`,
`float_common.h`):

- 128-bit precomputed extended-precision powers of five (`5^-342 ...
5^308`, 651 entries) — pure data.
- `compute_product_approximation` — 128-bit multiply with high-half
rounding-boundary fixup.
- `compute_float` — the main algorithm; returns `(mantissa, power2)`
ready to be packed.
- `am_to_double` — IEEE-754 binary64 bit-pack.
- `__builtin_clzll` and `__uint128_t` wrappers (with a 32-bit fallback
for portability).

Clinger's strict fast path (`mantissa ≤ 2^53` and `|exp| ≤ 22`) is
**kept** unchanged — it's a single double multiply/divide and is faster
than Eisel-Lemire on its domain. Only the buggy widened branch is
replaced.

The port stays minimal:
- **double-only** (no `float`, no `long double`)
- No bigint slow path. The rare "indeterminate" inputs that upstream
resolves with `digit_comp` are unreachable from `parse_number_string`'s
≤19-digit mantissa per the Mushtak-Lemire proof, but a defensive
`am.power2 < 0` check is preserved that falls back to libc `strtod()` if
any future caller widens the input domain.

## Why not just revert #15061?

Considered. Reverting restores correctness at the cost of the +73-84 %
zset listpack-load wins #15061 measured on 17-19 digit double scores.
Eisel-Lemire is *the* algorithm that gives both correctness *and* the
wider mantissa range — preserving #15061's wins while fixing the
rounding regression.

A "tightened admission filter" (only accept widened-path inputs where
the conversion happens to be single-rounding) was also considered. The
math shows the filter conditions are essentially unsatisfied for typical
inputs (`lo == 0` requires the 128-bit product be divisible by 2^64;
only ~1 in 10^13 random inputs qualify), making it equivalent to a
revert with extra dead code. Eisel-Lemire is the only widened-path
solution that preserves perf on the typical case.
2026-05-13 19:38:08 +08:00
Sergei GeorgievandGitHub 5e155593d9 Reject corrupt stream payloads with mismatched entry counts (#15124)
## Summary

The stream RDB loader and listpack integrity validator had two gaps that
allowed corrupted payloads to be silently accepted, potentially leading
to crashes or incorrect behaviour at query time.

**1. `deleted_count` in the listpack header was trusted without
verification (`t_stream.c`)**

`streamValidateListpackIntegrity` already walks every entry in a stream
listpack during deep validation, inspecting each entry's flags. However,
the `deleted_count` value stored in the listpack header was never
cross-checked against the actual number of entries carrying
`STREAM_ITEM_FLAG_DELETED`. A corrupted or crafted payload could set an
arbitrary `deleted_count`, causing the `entry_count` (live) and
`deleted_count` to be inconsistent with the real data. This PR adds a
running `actual_deleted` counter during the entry walk and rejects the
listpack when it disagrees with the header.

**2. `s->length` was only loosely validated against the rax (`rdb.c`)**

The old check (`s->length && !raxSize(s->rax)`) only caught the
degenerate case where the stream claimed a non-zero length but had zero
rax nodes. It did not detect a mismatch where the stream's `length`
field differed from the sum of live (non-tombstone) entries across all
listpacks. A corrupted payload could, for example, report `length = 2`
while every listpack's live-entry count adds up to only 1, bypassing the
check entirely and causing incorrect results from `XLEN`, `XRANGE`,
`XREAD`, etc.

This PR accumulates `live_entries` from each listpack's first element
(the live-entry count) during the rax-loading loop and then performs an
exact equality check (`s->length != live_entries`).

The live-entry count itself is also validated against the in-memory
invariant maintained by `streamIteratorRemoveEntry`: when the last live
entry of a listpack is deleted, the whole rax node is removed, so a node
in the rax must have `lp_live >= 1`. The loader enforces this by
rejecting `lp_live <= 0` (not just `< 0`). Without this stricter bound,
a payload where every listpack has `lp_live = 0` and `s->length = 0`
would pass the equality check (`0 == 0`) and load into an inconsistent
state: non-empty rax containing only-tombstone listpacks, with `XLEN`
reporting 0.

**3. Tests**

Three new `corrupt-dump.tcl` tests exercise the new rejection paths:

- **stream listpack with wrong deleted count in header** — a crafted
payload where the header says `deleted_count = 1` but the only entry is
live, caught by the new `actual_deleted` check in
`streamValidateListpackIntegrity`. Requires `sanitize-dump-payload yes`
since the check runs only during deep validation.
- **stream length inconsistent with live entries** — a crafted payload
where the listpack reports `lp_live = 1` (so the `lp_live <= 0` guard
passes) but `s->length = 2`, caught by the `s->length != live_entries`
check in `rdbLoadObject`.
- **stream all-tombstone listpack with zero length** — a crafted payload
where the listpack header reports `lp_live = 0` and `s->length = 0`.
This case slips past the equality check (`0 == 0`) and is uniquely
caught by the tightened `lp_live <= 0` rejection at the listpack header.
2026-05-13 10:28:31 +03:00
a38493a70e hyperloglog: 4-way histogram accumulators for hllRawRegHisto (#15049)
## Summary

Optimize the HyperLogLog register histogram functions (`hllRawRegHisto` and
`hllDenseRegHisto`) by splitting the single histogram accumulator into 4
independent accumulators that are merged at the end. This breaks
store→load dependency chains when consecutive register bytes map to the same
histogram bin, allowing the CPU's out-of-order engine to overlap the increments.

Profiling shows `hllRawRegHisto` as the single hottest function on multi-key
PFCOUNT. TMA analysis on x86 Sapphire Rapids reveals Core_Bound at 25.1%
with Heavy_Operations at 9.6%, indicating serialized memory operations
from the histogram update pattern.

**When it helps:**
- Multi-key PFCOUNT (the primary use case — each key triggers a full
  16384-register histogram build)
- PFMERGE followed by PFCOUNT (same histogram path on the merged result)
- Any PFCOUNT on a dense HLL representation (most production HLLs)

**When it doesn't help:**
- PFADD (register updates, not histogram reads)
- Sparse HLL representations (small cardinalities use a different path)
- Single-key PFCOUNT on sparse encoding

## How it works

The original code builds one `reghisto[64]` array by incrementing bins for
each of the 16384 registers (processed 8 at a time from 64-bit words):

```c
reghisto[bytes[0]]++;  // store→load hazard if bytes[0] == bytes[1]
reghisto[bytes[1]]++;  // must wait for previous store to complete
...
```

When two bytes in the same word have the same value (common — register
values cluster around log2(cardinality)), the CPU serializes the increment
chain because each `reghisto[x]++` is a load-modify-store that depends
on the previous store to the same address.

The fix splits into 4 independent arrays — `h0` through `h3` — each handling
2 of the 8 bytes per word, interleaved so consecutive bytes go to different accumulators:

```c
h0[r[0]]++;  // independent
h1[r[1]]++;  // independent — different array, no hazard
h2[r[2]]++;
h3[r[3]]++;
h0[r[4]]++;
h1[r[5]]++;
h2[r[6]]++;
h3[r[7]]++;
```

`hllDenseRegHisto` applies the same pattern across 16 registers per
iteration, interleaved by index mod 4 (`r0,r4,r8,r12 → h0; r1,r5,r9,r13
→ h1; …`).

After the loop, the 4 arrays are summed into the final histogram. The 4×64
extra stack bytes are negligible, and the merge loop is ~1% of the function's cost.

## Benchmark Results

Tested on `x86-aws-m7i.metal-24xl-2` (Intel Sapphire Rapids, bare metal),
`oss-standalone` topology. **Median of 2 datapoints, ±0.4% std.dev on PFCOUNT.**

| Test | Baseline (`unstable @ 9c1ecd044`) | PR (`5401108`) | Change |
|------|---------------------------------:|---------------:|--------|
| **multiple-hll-pfcount-100B-values** | 63,338 | 78,809 ±0.4% |**+24.4%** |
| multiple-hll-pfmerge-100B-values | 106,881 | 106,832 ±2.0% | -0.0%(flat) |

PFMERGE is flat — the SIMD merge path is unchanged, only the histogram
accumulation is modified.

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 21:41:01 +08:00
b7d6ef6b5a Add slowlog entry truncation limits configs (#15182)
Add configurations for `SLOWLOG_ENTRY_MAX_ARGC` and
`SLOWLOG_ENTRY_MAX_STRING` values which are currently hardcoded in code.

Two new configurations:
* `slowlog-entry-max-argc` - maximum number of command arguments kept in
a slowlog entry. Default: 32
* `slowlog-entry-max-string-len` - maximum length of a command argument
in a slowlog entry. Default: 128

Useful for better diagnostics of slow commands with numerous and long
arguments.

---------

Co-authored-by: debing.sun <debing.sun@redis.com>
2026-05-12 16:21:23 +03:00
Moti CohenandGitHub cf27a1abe4 Fix vecClear() to invoke free method on elements (#15190)
vecClear() reset the logical size without releasing element ownership,
leaking elements whenever a free callback was registered via
vecSetFreeMethod(). This mirrors vecRelease()'s element-freeing loop
while still preserving the backing storage.
2026-05-12 12:07:16 +03:00
9c1ecd044e Add INCREX command for atomic increment with TTL and bounds (#15045)
Close #14278 

## Overview

Rate limiters, sliding windows, request counters, and numerous other
network-facing patterns share a common primitive: **atomically increment
a counter and set its expiration**. Achieving this in Redis requires
either multiple round-trips or a Lua script that bundles `INCR` /
`INCRBY` / `INCRBYFLOAT` with `EXPIRE` / `PEXPIRE`.

We propose a new command, **`INCREX`**, that collapses this two-step
pattern into a single, native, O(1) command. `INCREX` atomically:

1. Increments (or decrements) a key's numeric value — by integer or
float.
2. Optionally enforces lower and/or upper bounds, with a configurable
overflow policy (error out, saturate, or no-op), enabling built-in cap
enforcement (e.g., max request count) without additional client logic.
3. Optionally sets or removes the key's expiration.
4. Returns both the **new value** and the **actual increment applied**,
giving the caller immediate feedback on whether the operation was
saturated or skipped.

## Use Cases

### Basic Usage

```
# Increment by 1 (default) and set a 60-second TTL.
> SET mykey 10
> INCREX mykey EX 60
1) (integer) 11        # new value
2) (integer) 1         # actual increment

# Use 0 as initial value if the key doesn't exist.
> DEL mykey
> INCREX mykey
1) (integer) 1         # new value
2) (integer) 1         # actual increment

# Default policy (OVERFLOW FAIL): exceeding a bound returns an error.
> SET mykey 5
> INCREX mykey BYINT 20 UBOUND 10
(error) value is out of bounds

# Opt into saturation with OVERFLOW SAT.
> INCREX mykey BYINT 20 UBOUND 10 OVERFLOW SAT
1) (integer) 10     # saturated to upper bound
2) (integer) 5      # only 5 was actually applied

# Skip the operation with OVERFLOW REJECT — the key and its TTL are
# untouched, and the reply reports the current value with a zero delta.
> SET mykey 5
> INCREX mykey BYINT 20 UBOUND 10 OVERFLOW REJECT
1) (integer) 5      # current (unchanged) value
2) (integer) 0      # nothing was applied

# Increment by a float
> SET mykey 1
> INCREX mykey BYFLOAT 0.5
1) "1.5"
2) "0.5"
```

### Use Case: Rate Limiter

**Before (Lua script):**
```lua
-- KEYS[1] = rate limit key, ARGV[1] = limit, ARGV[2] = window in seconds
local current = redis.call('INCR', KEYS[1])
if current > tonumber(ARGV[1]) then
    return 0  -- rejected
end
if current == 1 then
    redis.call('EXPIRE', KEYS[1], ARGV[2])
end
return 1  -- allowed
```

Client invocation:

```python
result = redis.eval(LUA_SCRIPT, 1, f"ratelimit:{user_id}", 100, 60)
if result == 0:
    reject_request()
```

**After (INCREX):**

```python
new_val, actual_incr = redis.execute_command(
    "INCREX", f"ratelimit:{user_id}", "UBOUND", 100, "OVERFLOW", "REJECT", "EX", 60, "ENX"
)
if actual_incr == 0:
    # Rate limit exceeded — key left unchanged.
    reject_request()
```

`ENX` means: set expiration only if the key doesn't already have an
expiration. This ensures the sliding window's TTL is only set on the
first request.

### Use Case: Token Bucket Refill

Refill tokens periodically up to a capacity ceiling, saturating at the
cap instead of erroring:

```
> INCREX tokens:user123 BYINT 10 UBOUND 100 OVERFLOW SAT EX 3600 ENX
1) (integer) 10
2) (integer) 10
```

Tokens cannot exceed 100, and the key auto-expires after inactivity.

### Use Case: Countdown / Resource Consumption

Decrement a resource counter down to zero, saturating at the floor:

```
> SET credits:user123 50
> INCREX credits:user123 BYINT -1 LBOUND 0 OVERFLOW SAT
1) (integer) 49
2) (integer) -1
```

When credits are exhausted, `OVERFLOW SAT` prevents negative balances
without client-side checks.

## Parameter Reference

### Syntax

```
INCREX key
      [BYFLOAT increment | BYINT increment]
      [LBOUND lowerbound] [UBOUND upperbound] [OVERFLOW <FAIL | SAT | REJECT>]
      [EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | PERSIST] [ENX]
```

### Parameters

| Parameter | Description |
|-----------|-------------|
| `key` | The key to increment. Created with value `0` if it does not
exist. |
| `BYFLOAT increment` | Increment the value by the given long-double
float. |
| `BYINT increment` | Increment the value by the given 64-bit signed
integer. |
| `LBOUND lowerbound` | Set lower bound for the increment result.
Defaults to `LLONG_MIN` (integer) or `-LDBL_MAX` (float). |
| `UBOUND upperbound` | Set upper bound for the increment result.
Defaults to `LLONG_MAX` (integer) or `LDBL_MAX` (float). |
| `OVERFLOW <FAIL \| SAT \| REJECT>` | Set the overflow policy when the
result would be out of bounds. `FAIL` rejects the operation with an
error (default). `SAT` saturates the result to the bound. `REJECT`
leaves the key and its TTL untouched and replies with the current value
and a zero delta. |
| `EX seconds` | Set the key's TTL to `seconds` seconds. |
| `PX milliseconds` | Set the key's TTL to `milliseconds` milliseconds.
|
| `EXAT unix-time-seconds` | Set the key's expiration to the absolute
Unix timestamp in seconds. |
| `PXAT unix-time-milliseconds` | Set the key's expiration to the
absolute Unix timestamp in milliseconds. |
| `PERSIST` | Remove the key's existing TTL. |
| `ENX` | Set the key's TTL/expiration if it has No eXpiration |

If neither `BYINT` nor `BYFLOAT` is specified, the increment defaults to
integer `1`.

### Return Value

An **array of two elements**:

1. **New value** — the value of the key after the increment (or the
unchanged current value under `OVERFLOW REJECT`).
2. **Actual increment** — the increment that was actually applied. May
differ from the requested increment when `OVERFLOW SAT` saturates the
result to a bound, and is always `0` when `OVERFLOW REJECT` skipped the
operation.

- In integer mode (default or `BYINT`): both elements are **integers**.
- In float mode (`BYFLOAT`): both elements are **bulk strings**
representing the float values on RESP2, and **RESP3 Doubles** on RESP3.

### Overflow Policy (FAIL vs. SAT vs. REJECT)

Controlled by the optional `OVERFLOW` argument. A bound violation
includes both exceeding an explicit `LBOUND`/`UBOUND` and overflowing
the type limits when no explicit bound is given.

- **`OVERFLOW FAIL` (default)**: if the computed result would violate a
bound, the command returns an error and the key is left unchanged. This
matches the existing semantics of `INCRBY` / `INCRBYFLOAT` on overflow.
- **`OVERFLOW SAT`**: the result is silently capped at `UBOUND` /
floored at `LBOUND` (or saturated to the type limits when no explicit
bound is given). The second element of the reply reflects the saturated
delta. If the delta cannot be represented as a 64-bit signed
integer(default or `BYINT`), or would produce Infinity(`BYFLOAT`), an
error is returned.
- **`OVERFLOW REJECT`**: the operation is silently skipped — the key
value and its TTL are left unchanged, no keyspace notification is fired,
and nothing is replicated. The reply is `[current_value, 0]`, allowing
the caller to detect the rejection without handling an error.

### Notes
- If **no expiration option** is given, the key's existing TTL is
preserved (like `INCR`).
- `ENX` requires one of `EX`/`PX`/`EXAT`/`PXAT`.
- If the result is saturated by `OVERFLOW SAT`, the expiration is still
applied as specified.
- Under `OVERFLOW REJECT` the expiration option is ignored on the
rejected branch — TTL is preserved exactly as it was before the call.
- **`BYINT` requires an integer-typed existing value; `BYFLOAT` accepts
both.**
Integers can be promoted to floats losslessly, but a stored float (e.g.
`"1.5"`) cannot be parsed back as an integer. This is consistent with
`INCR`/`INCRBY` (integer-only) and `INCRBYFLOAT` (accepts both).

---------

Co-authored-by: debing.sun <debing.sun@redis.com>
Co-authored-by: Ozan Tezcan <ozantezcan@gmail.com>
Co-authored-by: Moti Cohen <moti.cohen@redis.com>
Co-authored-by: oranagra <oran@redislabs.com>
2026-05-11 18:22:27 +08:00
h.o.t. neglectedandGitHub 9302d27889 Fix MULTI queue memory accounting in multiStateMemOverhead (#15163)
PR #14440 changed `mstate.commands` from an array of `multiCmd` structs
to an array of `pendingCommand` pointers. This PR fixes the overhead
calculation in multiStateMemOverhead to account for both the pointer
array and the actual structs:
- The pointer array: `alloc_count * sizeof(pendingCommand*)`
- The individually allocated structs: `count * sizeof(pendingCommand)`
2026-05-11 10:53:18 +08:00
62551a7b12 Batched MGET/MSET dict prefetch with dictType-driven payload hints (#15133)
Reduce MGET / MSET latency by overlapping the dict-lookup memory accesses
across the keys of a single multi-key command. Builds on the cross-command
batched prefetch framework introduced in #14017 and the dict-prefetch state
machine in `memory_prefetch.c`, and lifts the kvobject-aware bits out of the
state machine into two new `dictType` callbacks so the same machinery can
be reused for other dict-encoded types later (hash hashtable, sets, sorted
sets) without paying for `kvobj`-specific code paths in the core loop.

Bundles the work originally proposed in #14899 (MGET prefetch framework,
by @mpozniak95) and #15043 (MSET batch prefetch).

## Design

Two new optional callbacks on `dictType`:

```c
typedef struct dictType {
    ...
    /* Bring the entry's key payload into cache before keyCompare runs.
     * Returns the address to prefetch, or NULL if the entry alone is enough. */
    void *(*prefetchEntryKey)(const dictEntry *de);

    /* Called only after a key match. Returns the value-side payload to
     * prefetch (or NULL). */
    void *(*prefetchEntryValue)(const dictEntry *de);
} dictType;
```

`dbDictType` registers both. The kv-aware logic — the `dictEntryIsKey()`
shortcut for embedded kvobjs, and `kv->ptr` for `OBJ_STRING` /
`OBJ_ENCODING_RAW` values — now lives in two small helpers in
`server.c`:

```c
static void *dbDictPrefetchEntryKey(const dictEntry *de) {
    return dictEntryIsKey(de) ? NULL : dictGetKey(de);
}

static void *dbDictPrefetchEntryValue(const dictEntry *de) {
    kvobj *kv = dictGetKey(de);
    return (kv->type == OBJ_STRING && kv->encoding == OBJ_ENCODING_RAW)
            ? kv->ptr : NULL;
}
```

The `PrefetchGetValueDataFunc` typedef and the per-call `get_val_data`
parameter on `dictPrefetchKeys()` / `dictPrefetch()` are removed — the
dict's own type drives both ends. This also removes the foot-gun where
callers (like `mgetCommand`) had to remember whether to pass
`prefetchGetObjectValuePtr` or `NULL`. `memory_prefetch.c` no longer
references `kvobj`, `kvobjGetKey`, or any specific value layout.

## State machine

Two file-local types in `memory_prefetch.c`:

| Type | Role |
|---|---|
| `dictPrefetchLookup` | Per-key snapshot of an in-flight,
software-pipelined `dictFind` (mirrors the locals a synchronous
`dictFind` would carry across one bucket walk). |
| `dictPrefetcher` | Driver that advances a batch of
`dictPrefetchLookup`s through the FSM, yielding to the next in-flight
lookup each time a prefetch is issued. |

Five-stage lifecycle for each lookup, driven by the prefetcher:

```text
                                                           │
                                                         start
                                                           │
                                                  ┌────────▼─────────┐
                                       ┌─────────►│  PREFETCH_BUCKET ├────►────────┐
                                       │          └────────┬─────────┘            no more tables
                                       │             bucket│found                  │
                                       │                   │                       │
        entry not found - goto next table         ┌────────▼────────┐              │
                                       └────◄─────┤ PREFETCH_ENTRY  │              ▼
                                    ┌────────────►└────────┬────────┘              │
                                    │                 entry│found                  │
                                    │                      │                       │
                                    │          ┌───────────▼─────────────┐         │
                                    │          │   PREFETCH_ENTRY_KEY    │ ◄── dictType->prefetchEntryKey(de)
                                    │          └───────────┬─────────────┘         │
                                    │                      │                       │
        key mismatch - goto next entry                     │                       │
                                    │          ┌───────────▼─────────────┐         │
                                    └──────◄───│   PREFETCH_ENTRY_VALUE  │ ◄── keyCompare; on match,
                                               └───────────┬─────────────┘     dictType->prefetchEntryValue(de)
                                                           │                       │
                                                 ┌─────────▼─────────────┐         │
                                                 │     PREFETCH_DONE     │◄────────┘
                                                 └───────────────────────┘
```

`PREFETCH_BUCKET` first picks `ht_table[0]`, then flips to `ht_table[1]`
if the dict is mid-rehash, then transitions to `PREFETCH_DONE` if no
more tables remain.

`memory_prefetch.c` exposes a small lifecycle that any caller can drive:

```c
dictPrefetcherInit(p, max_keys);                  /* one-shot heap alloc of lookups[] */
dictPrefetcherReset(p, dicts, keys, nkeys);       /* configure for one batch */
dictPrefetcherRun(p);                             /* drive FSM until all PREFETCH_DONE */
dictPrefetcherFree(p);                            /* release */
```

Each FSM stage is a named static function (`dictPrefetchBucket`,
`dictPrefetchEntry`, `dictPrefetchEntryKey`, `dictPrefetchEntryValue`),
so the `dictPrefetcherRun` driver is a four-line `switch` over the
state.

The state machine is dict-pure: no `kvobj` field on
`dictPrefetchLookup`,
no `kvobjGetKey` reach-through. Round-robin advance semantics — a state
only advances the cursor if a prefetch was actually issued — are
preserved, so the embedded-kvobj fast path
(`dictEntryIsKey(de) == 1` → callback returns NULL) still skips the
extra prefetch and falls straight into the compare on the next loop
iteration.

The cross-command path (`prefetchCommands` / `PrefetchCommandsBatch`)
embeds a `dictPrefetcher` initialized once at startup and reset per
batch, so cross-command prefetching no longer allocates per call.

## Intra-command API

```c
void dictPrefetchKeys(dict **dicts, void **keys, size_t nkeys);
```

A single multi-key command (e.g. MGET) can prefetch dict data for a
batch of its own keys, reusing the same state machine that the
cross-command path uses. Single-key calls (`nkeys <= 1`) early-return —
nothing to interleave with. The implementation stack-allocates a
fixed-size lookup array bounded by `DICT_PREFETCH_MAX_SIZE = 64` (no
VLA, predictable stack usage), so the intra-command path doesn't touch
the heap.

## Notes on the call sites

A shared helper picks the next prefetch batch and warms it via
`dictPrefetchKeys`:

```c
/* Pick the next prefetch batch starting at argv[start] and warm it via
 * dictPrefetchKeys. 'stride' is 1 for keys-only args (MGET) or 2 for
 * key/value pairs (MSET). Returns the chosen batch size in items. */
static int prefetchKeysBatch(client *c, int slot, int start, int stride);
```

Adaptive batch sizing inside the helper: if at least two full batches
(`PREFETCH_BATCH_SIZE * 2 = 32` items) remain, take one batch
(`PREFETCH_BATCH_SIZE = 16`); otherwise take all remaining items in one
call. This generalizes the small-request fast path so the trailing
batch of a large request also gets the single-call benefit.

- **MGET (`mgetCommand`)** — gated by
`do_prefetch = server.prefetch_batch_max_size && !already_prefetched && numkeys > 1`,
with `already_prefetched = c->current_pending_cmd &&
(c->current_pending_cmd->flags & PENDING_CMD_KEYS_PREFETCHED)`.
  When `do_prefetch` is set, each iteration calls
  `prefetchKeysBatch(c, slot, j, 1)` and then sequentially
  `lookupKeyRead`s + replies the chosen batch. When `do_prefetch` is
  clear (cross-command path already warmed the keys, or batch
  prefetching is off), the loop takes all remaining items in one go
  and skips the prefetch.

- **MSET / MSETNX (`msetGenericCommand`)** — same `do_prefetch` gate as
  MGET with `stride = 2`. For the NX flag the NX-check loop runs
  `lookupKeyWrite` (which already warmed everything via
  `prefetchKeysBatch`); the SET loop then disables further prefetch
  (`do_prefetch &&= !nx`) so we don't re-prefetch on the second pass.
  Going through the full state machine (rather than bucket-only) means
  `dbDictType`'s `prefetchEntryValue` callback runs on a key match —
  warming the old kvobj's payload, which `setKey -> dbReplaceValue ->
  updateKeysizesHist(oldlen, newlen)` then reads to compute the
  histogram delta. The slot dict is re-fetched per batch — in cluster
  mode the slot dict can be freed mid-MSET (`KVSTORE_FREE_EMPTY_DICTS`
  + `expireIfNeeded`), so a cached pointer would otherwise dangle.

- **Cross-command batch path (`addCommandToBatch`)** — sets
  `PENDING_CMD_KEYS_PREFETCHED` on every command added to the batch,
  even on partial-batch overflow (was: only when ALL keys fit). The
  intra-command path then uniformly skips supplemental prefetching for
  any command the batch touched. Rationale: running both paths
  (cross-command warm + intra-command supplement) caused a measured
  −9.6 % regression on x86 with pipeline-10, and the partial cross-
  command warmup is sufficient for the head of the keyset; the cold
  tail goes through normal lookup, which is still cheaper than running
  the FSM a second time on already-warm keys.

- **Future types**: each dict's `dictType` can register its own
  `prefetchEntryKey` / `prefetchEntryValue` (e.g. for the hashtable hash
  encoding, the field-sds and value-sds payloads), without touching
  `memory_prefetch.c`.

## Benchmark validation

On x86, performance improvements are significant for larger batch sizes:
  - 5Mkeys-string-mget-10B-100keys-pipeline-10: +89.44%
  - 5Mkeys-string-mget-100B-100keys: +37.33%
  - 5Mkeys-string-mget-100B-30keys: +22.40%
On ARM (Graviton4), the gains are even more pronounced:
  - 5Mkeys-string-mget-10B-100keys-pipeline-10: +128.34%
  - 5Mkeys-string-mget-100B-100keys-pipeline-10: +46.76%
Overall, the improvement scales with batch size, while a few small-batch cases show marginal gains or slight regressions.

---------

Co-authored-by: Marcin Poźniak <marcin.pozniak@intel.com>
Co-authored-by: Yuan Wang <yuan.wang@redis.com>
2026-05-11 10:22:25 +08:00
Shubham S TapleandGitHub 74cfe81543 Fix off-by-one in listpack backlen encoding (#15151)
## Problem

At the back-length width boundaries 16383 / 2097151 / 268435455,
lpEncodeBacklen falls through to the next-wider encoding instead of
using the narrower one, because the threshold checks use < where they
should use <=. The listpack stays valid (the decoder is self-delimiting)
but each such entry wastes one byte.

## Fix

Update the threshold checks to use inclusive ≤, ensuring boundary values
are encoded with the minimal number of bytes, consistent with
lpDecodeBacklen.
2026-05-11 09:44:47 +08:00
Vitah LinandGitHub 8bba074586 Upgrade GH actions to latest stable versions (Node.js 20 deprecation) (#14938)
### Problem

Node.js 20 actions are deprecated. The warning in CI like that:
> Node.js 20 actions are deprecated. The following actions are running
on Node.js 20 and may not work as expected: actions/checkout@v4. Actions
will be forced to run with Node.js 24 by default starting June 2nd,
2026. Please check if updated versions of these actions are available
that support Node.js 24. To opt into Node.js 24 now, set the
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24=true environment variable on the
runner or in your workflow file. Once Node.js 24 becomes the default,
you can temporarily opt out by setting
ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION=true. For more information see:
https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/

### Changed 

Upgrade actions to their latest stable versions:
1. `actions/upload-artifact` v4 => v7
2. `actions/checkout` v4 => v6
3. `actions/checkout` main => v6
4. `actions/create-github-app-token` v1 => v3
5. `github/codeql-action` v3 => v4
6. `actions/cache` v4 => v5
7. `actions/setup-node` v4 => v6
2026-05-11 09:34:46 +08:00
LeenearandGitHub bf432c98fd Fix incorrect memmove size in LDB breakpoint deletion (#15115)
# Description
There is an array corruption bug in LDB caused by an incorrect size
argument being passed to `memmove()` inside the `ldbDelBreakpoint()`
function.

When deleting a breakpoint, `memmove()` is used to shift the remaining
breakpoints in the ldb.bp integer array forward. However, the size
parameter passes the number of elements rather than the number of bytes.
Because ldb.bp is an array of type `int`, this results in an under-copy.
2026-05-10 13:01:08 +08:00
Ozan TezcanandGitHub 7bdab45ff1 Reduce memory allocation overhead (#15096)
While profiling command execution, I noticed that command argv object
alloc/free overhead is quite high for workloads with many small
arguments (e.g. `HSET` with many fields). The effect is much more
visible with pipelining when Redis becomes CPU bound.

I experimented with replacing argv object alloc/free with a simple
object pool and saw significant speedups.
(Note: related effort around this topic:
https://github.com/redis/redis/pull/13726)

In this PR, I tried to improve the main hotspots in the memory
allocation path (focusing on command arg allocations) to close the gap
with custom pool performance, so we can avoid having a dedicated memory
pools and let the whole codebase benefit from these optimizations.

## Changes

### 1) Faster dealloc via passing size hint to jemalloc (separate PR
#15071)
Jemalloc does more work than an object pool on free (a lookup on a tree
to find the allocation's size class). For some deallocations, we can
reduce free path overhead by passing a size hint to jemalloc (i.e.
`sdallocx()`) which can skip metadata lookup in the common case. This PR
introduces `zfree_with_size()` and uses it where we can know the
allocation size i.e. `OBJ_ENCODING_EMBSTR` objects in `decrRefCount()`
and SDS free path.

### 2) Reduce atomic operation cost for stat updates
`update_zmalloc_stat_alloc()` / `update_zmalloc_stat_free()` previously
used atomic read-modify-write (RMW) operations (`atomicIncrGet` /
`atomicDecr`) which can emit expensive locked instructions on x86.

When we can guarantee a single writer to a counter, we can use a cheaper
load+add+store sequence instead of a locked RMW. This PR gives the first
16 threads dedicated slots for used_memory stats (intended to cover the
main thread/ I/O threads) so they can use this single writer fast path.
Threads beyond that fall back to a shared pool and continue to use full
atomic RMW.

### 3) Improve jemalloc tcache hit rate 

With the default `lookahead=16` config, a pipelined HSET with ~20 fields
does ~40 small allocations per command (fields + values), so you can get
16 x 40 = ~640 allocations. When args are small, many of these land in
the 32 byte size class (often `EMBSTR`). Jemalloc’s default per-bin
tcache cap is 200, so this kind of burst overflows the cache and it does
frequent flushes. I raised the small-bin tcache limits
(lg_tcache_nslots_mul:3, tcache_nslots_small_max:1000) to handle these
bursts better. In the worst case, tcache may have a higher memory usage
due to this change. Perhaps, another option was lowering `lookahead` to
tune it differently.

### 4) Inlining
When you have a simple pool, it has a few small functions and it is easy
for compiler to inline them. Compared to that, jemalloc alloc/free path
has a deeper call stack. Also, jemalloc was not compiled with `-flto`
which was preventing inlining jemalloc functions. As part of this PR, I
added `-flto` flag to jemalloc when it is enabled for Redis.

Compiler also chooses not to inline some hot path functions in Redis.
This suggests PGO (profile-guided optimization) could provide additional
wins and perhaps we can start experimenting with it sometime. We could
try to force inlining with attributes like `always_inline` but it is
hard to apply across a deep call stack and misuse can cause code bloat.
So, rather than going in this direction, I added `inline` keyword to
some functions for now. This doesn't make compiler to inline all hot
path functions but at least it is a step ahead. (If we can further
improve this in future, performance gets very close to custom memory
pool implementation).

## Benchmark results

Commands were like:

```
memtier_benchmark   --command="HSET __key__ username john_doe email john@example.com password hashed_pwd_123 created_at 1709125200 updated_at 1709125200 first_name John last_name Doe phone_number +1234567890 address 123_Main_St city NewYork country USA postal_code 10001 company Acme_Corp job_title Engineer bio Loves_coding"   --command-ratio=1   --command-key-pattern=P   --key-prefix="hsetkey"   --key-minimum=1   --key-maximum=100000   -n 1000000   -c 50   -t 2   --hide-histogram --pipeline 50
```

| Benchmark | Improvement |
| --- | ---: |
| SET | +0% |
| SET (pipeline) | +8% |
| HSET 15 fields | +2% |
| HSET 15 fields (pipeline) | +17% |
| ZADD 15 elements| +3% |
| ZADD 15 elements (pipeline) | +15% |
2026-05-09 11:48:45 +03:00
7cf63635f0 Pass size hint to jemalloc for faster deallocation (#15071)
This PR is based on https://github.com/valkey-io/valkey/pull/453 and
https://github.com/valkey-io/valkey/pull/694

When jemalloc frees memory, it performs a lookup to find the
allocation's size class. `sdallocx()` lets us skip this lookup by
passing the size we already know. Introduced a new free function wrapper
for this: `zfree_with_size()`.
Note: Impact of this optimization is only visible on hot paths e.g. on
repeated memory deallocations.

For the initial phase, I integrated this at `sdsfree()` only. Over time,
we may expand the usage of this new API for other performance sensitive
paths.

For testing, added jemalloc config `--enable-opt-size-checks` to the
daily fortify build. This makes jemalloc validate that the size passed
to `sdallocx()` matches the actual allocation's size class, aborting on
mismatch.

----


Signed-off-by: Vadym Khoptynets <vadymkh@amazon.com>
Signed-off-by: Madelyn Olson <madelyneolson@gmail.com>
Co-authored-by: Madelyn Olson <madelyneolson@gmail.com>
Signed-off-by: ranshid <ranshid@amazon.com>
2026-05-08 12:16:56 +03:00
hristostaykov-delandGitHub d9b03bdb98 Add inline cleanup to sentinel CONFIG SET/GET tests (#15174)
Test 15-config-set-config-get.tcl was leaving announce-port and
announce-hostnames at non-default values, which breaks auto-discovery in
subsequent test units. Add reset lines at the end of each test that
modifies config. This PR fixes failures in Daily CI tests.
2026-05-08 10:58:15 +03:00
Hanzo AI 3b7d32069f ci: add id-token: write to caller permissions
Required for hanzoai/.github/.github/workflows/docker-build.yml@main —
without it the workflow_call dies as startup_failure with no jobs
dispatched. Caller permissions are a CEILING.
2026-05-07 09:11:20 -07:00
7ecc04f59d Fix memory leak on malformed legacy help entry in redis-cli (#15150)
Close #15134 

## Description
In cliLegacyIntegrateHelp(), malformed COMMAND entries could return
early and skip freeReplyObject(reply). Use break instead so the loop
exits and the existing cleanup still frees the reply.

Signed-off-by: huynhanx03 <157712338+huynhanx03@users.noreply.github.com>
Co-authored-by: IamShreshth <IamShreshth@users.noreply.github.com>
2026-05-06 09:55:14 +08:00
05859cdd7e Fix client output buffer memory tracking not accounting for copy-avoided bulk string references (#14934)
## Problem

After #14608 (Reply Copy Avoidance), when copy avoidance kicks in, bulk
string replies are sent by reference instead of being copied into the
output buffer.
The referenced bytes are not counted in  `reply_bytes`, which causes:

1. `getClientOutputBufferMemoryUsage()` underestimates the actual memory
usage, so output buffer limits may not be triggered in time, allowing
clients to consume unbounded memory.
2. Client eviction does not account for the referenced bytes, making it
ineffective when copy avoidance is used.
3. `omem` reported in `CLIENT LIST` / `CLIENT INFO` does not reflect the
true output buffer memory footprint.

## Solution

Track the bytes of referenced bulk strings in the output buffer with two
per-client counters:

1. reply_bytes_shared - the logical size of all BULK_STR_REF payloads in
the output buffer.
   Updated incrementally whenever a reference is added/removed.
Represents memory the client is "charged" for even though it is shared
with the keyspace.

2. reply_bytes_unshared — the subset of the above where the referenced
object's refcount == 1 (i.e. the key has been deleted from the
keyspace), so the memory is kept alive solely by this client's output
buffer and would actually be freed on disconnect.
Maintained as a lazy cache refreshed via
updateClientUnsharedReplyBytes().

## Info field

CLIENT LIST / CLIENT INFO — two new fields, plus refined semantics for
existing ones:

Field | Meaning
-- | --
omem | (semantics changed) logical output-buffer memory, now including
shared memory referenced from the keyspace. Still
excludes client->buf so static clients show 0.
omem-shared | (new) shared output-buffer memory (referenced bulk
strings, not solely owned by this client).
omem-unshared | (new) unshared output-buffer memory (referenced bulk
strings solely owned by this client; freed on disconnect).
tot-mem | (semantics refined) actual memory usage —
includes omem-unshared, excludes omem-shared to avoid double-counting
keyspace memory.

INFO memory — two new fields mirroring the above:

Field | Meaning
-- | --
mem_clients_normal | (semantics changed) actual memory usage of normal
clients (includes unshared, excludes shared).
mem_clients_normal_shared | (new) aggregate shared output-buffer memory
across normal clients.
mem_clients_normal_unshared | (new) aggregate unshared output-buffer
memory across normal clients.

MEMORY STATS — schema extended with the matching keys:

Field | Meaning
-- | --
clients.normal.shared | (new) aggregate shared output-buffer memory
across normal clients.
clients.normal.unshared | (new) aggregate unshared output-buffer memory
across normal clients.

## Bug Fix
Fix missing closeClientOnOutputBufferLimitReached() call when adding a
referenced robj to the reply

---------

Co-authored-by: oranagra <oran@redislabs.com>
2026-05-06 09:46:17 +08:00
Angel YanevandGitHub 2408a15ee6 Enable LTO for RediSearch module when Redis is build with modules (#15166)
<!-- CURSOR_SUMMARY -->
> [!NOTE]
> **Low Risk**
> Low risk Makefile change that only alters build flags for the
RediSearch module; primary risk is build/compatibility issues on some
toolchains when LTO is enabled.
> 
> **Overview**
> **RediSearch module builds now default to link-time optimization.**
The `modules/redisearch/Makefile` introduces `LTO ?= 1` and exports it
so the upstream RediSearch build can pick it up, with an escape hatch to
disable via `LTO=0`.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
de33dc7033fc5161a3c6870b754055219c6ea538. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
2026-05-05 21:17:37 +03:00
hristostaykov-delandGitHub 3e1afec688 Fix Sentinel config injection via SENTINEL SET (#14970)
Reject control characters (0x00-0x1F, 0x7F) in user-controlled string
arguments to SENTINEL SET, SENTINEL MONITOR, and SENTINEL CONFIG SET to
prevent newline injection into the persisted config file. An attacker
with access to SENTINEL SET could inject arbitrary config directives
(e.g. notification-script) by embedding \r\n in auth-pass or similar
fields, leading to code execution on restart.

As a defense-in-depth measure, config serialization now uses sdscatrepr
(via sentinelSdscatConfigArg) for all user-controlled string fields when
they contain characters that require escaping. Simple values remain
unquoted for backward compatibility with older config parsers.

Add comprehensive Sentinel tests (16-config-injection.tcl) covering
control character rejection for all affected commands, verification that
injection payloads do not pollute the config file, round-trip
persistence of values containing spaces and quotes through restart, and
backward compatibility with the old unquoted config format.
2026-05-05 11:04:28 +03:00
Vitah LinandGitHub 2432f55278 Fix CI Codecov v6 coverage upload configuration (#15147)
PR https://github.com/redis/redis/pull/14937 updates the Codecov
workflow configuration for `codecov/codecov-action` v6.

The action no longer accepts the singular `file` input, so this switches
to `files` to ensure `./src/redis.info` is uploaded correctly.
2026-04-30 21:38:25 +08:00
Vitah LinandGitHub 417cc6e4fc test: stabilize HOTKEYS MULTI/EXEC test by increasing iteration count (#15129)
## Problem

The test `HOTKEYS - commands inside MULTI/EXEC` in
`tests/unit/hotkeys.tcl` is flaky on fast hardware. This PR raises its
inner loop count from 7 to 30 to make `key2` reliably appear in the CPU
top-K.

Failed CI:
https://github.com/redis/redis/actions/runs/25051455424/job/73380034469?pr=15128

Inside `MULTI`/`EXEC`, each queued command's per-command CPU time is
recorded as `c->duration = ustime() - call_timer` (microseconds,
integer). Very fast commands such as `SET` against a small value can
complete in less than 1 µs and therefore be measured as `0`.

`hotkeyStatsUpdateCurrentCmd` then forwards that zero duration as the
weight to `chkTopKUpdate`, which has an explicit early return on `weight
== 0`:

```c
sds chkTopKUpdate(chkTopK *topk, char *item, int itemlen, counter_t weight) {
    if (weight == 0) return NULL;
    ...
}
```

In the original test, `key2` is `SET` only 7 times inside the
transaction. On fast hosts (the failure was observed on an ARM box with
`ustime()` ticking at 1 µs resolution) it is possible for all 7 calls to
be measured as 0 µs, which means `key2` is never inserted into the CPU
top-K and the assertion

```tcl
assert [dict exists $cpu_result $key2]
```

fails. `key1` has 21 calls and is statistically safe.

The author already anticipated this and left a comment ("Send multiple
commands to avoid <1us cpu for $key2"), but 7 iterations turned out to
be insufficient.

## Changes

Bump the iteration count from 7 to 30. With `key2` now `SET` 30 times
the probability of every single call being measured as 0 µs becomes
negligible on any realistic hardware.
2026-04-30 16:15:11 +03:00
0bbb196c46 Fix sharded pubsub unsubscribe lookup using cached command slot (#15094)
Fixes #15085 

## Problem
getKeySlot() may return `server.current_client->slot` while a command is
executing instead of computing the slot from the provided string.

The unsubscribe can be triggered by another client, in which case server.current_client is not the client being unsubscribed, so getKeySlot() would return that client's cached slot. Using this wrong slot would make the lookup in type.serverPubSubChannels miss the channel and ultimately trigger the assertion below.

## Fix
Always use keyHashSlot() instead of getKeySlot() on unsubscribe.

---------

Co-authored-by: debing.sun <debing.sun@redis.com>
2026-04-29 22:04:06 +08:00
Filipe Oliveira (Redis)andGitHub 48eaa75257 Add batched prefetch for HGETALL on hashtable encoding (#14988)
## Summary

Add a batched prefetch fast path for `HGETALL` on hashtable-encoded
hashes. When iterating large hash tables, pointer chasing through
scattered heap allocations (`dictEntry` → `Entry` → value SDS) causes
cache misses that dominate CPU time (~10% flat in `dictNext`).

The new path collects dict entries in batches of configured batch size,
issues software prefetches for the `Entry` structs and their value SDS
data, then emits replies while the data is cache-warm. This hides memory
latency by overlapping prefetch with reply generation.
2026-04-29 16:09:51 +08:00
Shubham S TapleandGitHub 247307de96 Pass context to RM_GetUserUsername() to support auto memory management (#15042)
Following #14890

## Problem
RM_GetUserUsername() documents that the returned RedisModuleString can
be freed via automatic memory management, but it always creates the
string with ctx=NULL so it cannot be tracked by RedisModule_AutoMemory.
Modules following the documentation may leak memory.

## Fix
Fixes `RedisModule_GetUserUsername` to accept a `RedisModuleCtx *` and create the returned `RedisModuleString` with that context, allowing RedisModule auto-memory management to track/free it as documented.
2026-04-28 16:45:31 +08:00
Moti CohenandGitHub 5a05863e97 t_string: rewrite SET GET propagation in place (#15114)
Optimize SET key value GET propagation rewriting in setGenericCommand() 
by removing GET arguments in-place with rewriteClientCommandArgument(). 
This avoids the overhead of allocating a new argv vector and 
incrementing reference counts for every retained argument.

The optimization is scoped to the no-expire SET ... GET rewrite path. 
It also adds test coverage for cases with repeated GET tokens to 
ensure robust string semantics and consistent replication behavior.

Changes:
- Use rewriteClientCommandArgument(c, j, NULL) for in-place removal.
- Eliminate redundant argv allocations and refcount increments.
- Improve performance of SET GET in high-throughput write streams.
2026-04-28 10:05:59 +03:00
Raj Kripal DandayandGitHub 625b6f58f6 tracking: fix self-overlap returning non-zero loop index (#15073)
Fixes checkPrefixCollisionsOrReply() to return 0 (failure) on any provided-prefix self-overlap, instead of accidentally returning a non-zero loop index for overlaps found after the first prefix.

Signed-off-by: Raj Danday <rajkripal.danday@gmail.com>
2026-04-28 09:24:47 +08:00
Curtis MeansandGitHub 861917603e Update SECURITY.md vulnerability reporting instructions (#15089)
Made-with: Cursor

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Documentation-only change; no code or runtime behavior is affected,
but it changes the official intake channels for vulnerability reports.
> 
> **Overview**
> Updates `SECURITY.md` to redirect vulnerability reporters from
emailing the core team to using the **Redis Vulnerability Disclosure
Program** link, with GitHub’s *Report a Vulnerability* as an
alternative.
> 
> Adds a dedicated security contact email (`security@redis.com`) for
questions and includes brief rationale for the new reporting path.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
eeaa8c4ada3152c7dc37f6338f3d6f9c178dfdb9. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
2026-04-27 19:03:31 +03:00
Filipe Oliveira (Redis)andGitHub 77628f370a perf+security: drop SCAN vector pre-allocation; rely on grow-by-doubling (#15118)
## Summary
Follow-up to #15065. The merged code calls `vecReserve(&keys, count)`
where `count` is user-supplied. A client can pass a giant `COUNT` (e.g.
`HSCAN k 0 COUNT 10000000000000`) and the server pre-allocates the
corresponding pointer slots before any work happens — ~80 TB on a 64-bit
build. Pre-reserve DoS surface flagged in code review.

## Fix
Drop the pre-reserve entirely. The vec already starts on a 256-pointer
stack buffer and grows-by-doubling driven by **actual cardinality** of
the dictionary, not by user-supplied `COUNT`.

## Why drop the pre-reserve (vs cap it)
The pre-reserve doesn't pay measurable performance — `vecPush()`'s
grow-by-doubling path is amortized O(1) and the dominant cost on SCAN
workloads is the per-entry callback work, not vector growth.
2026-04-27 23:13:14 +08:00
Filipe Oliveira (Redis)andGitHub 7a80aade96 perf: replace list with vec in scanGenericCommand key collection (#15065)
## Motivation

With the append-only pointer vector (`vec`) introduced in #15039, the
SCAN keys collection path is a natural consumer: `scanCallback` pushes
each key to a `list` via `listAddNodeTail`, which allocates a `listNode`
(~48 bytes + jemalloc overhead) per key. `scanGenericCommand` then
iterates the list once to emit replies and frees every node plus the
list itself. For `SCAN COUNT 500`, that is ~500 node allocations + frees
on the hot path of a single command.

This PR replaces that with `vec` — a 256-element stack buffer covers
typical `COUNT` values without any heap allocation, and larger scans
grow to a single heap allocation via `vecPush`.

## Change

Single-file diff in `src/db.c` — ~30 touched lines, net +6 LOC:

- `scanData.keys`: `list *` → `vec *`
- `scanCallback`: `listAddNodeTail(keys, key)` → `vecPush(keys, key)`
- `scanGenericCommand`:
  - `listCreate()` → stack-backed `vec` with 256-element `keys_stack[]`
- Reply loop: `listFirst / listNodeValue / listDelNode` → `vecGet` index
loop
- The old `listSetFreeMethod(keys, sdsfreegeneric)` was only active for
ZSET (which allocates temporary sds for scores) and listpack paths; we
    track that via a `free_collected` flag and do an explicit `sdsfree`
    loop before `vecRelease`. The listpack early-return paths (OBJ_SET,
    listpack, listpack_ex) call `vecRelease(&keys)` directly since they
    never called the callback.
- `#include "vector.h"` added

No algorithmic changes — SCAN cursor iteration, pattern matching, expiry
filtering, type filtering and reply formatting are unchanged.

## Benchmarks

Run via `redis-benchmarks-specification` on `x86-aws-m7i.metal-24xl`
(Intel Sapphire Rapids) and `arm-aws-m8g.metal-24xl` (Neoverse-V2
Graviton4). `unstable` baseline is `n=5`; PR is `n=2-3` on commit
`56458ce42` (the first push of this branch — the rebased commit
`6e4aff26f` is a no-op rebase over upstream, identical tree).

### x86-aws-m7i.metal-24xl

| Test | unstable | PR | Δ |
|------|---------:|---:|--:|
| `memtier_benchmark-1Mkeys-generic-scan-count-10-incremental-iteration`
| 176,929 (n=5) | 181,185 (n=3) | **+2.4%** |
|
`memtier_benchmark-1Mkeys-generic-scan-count-10-incremental-iteration-high-cursor-count`
| 157,405 (n=5) | 164,025 (n=3) | **+4.2%** |
| `memtier_benchmark-1Mkeys-generic-scan-count-50-incremental-iteration`
| 99,770 (n=5) | 110,862 (n=2) | **+11.1%** |
|
`memtier_benchmark-1Mkeys-generic-scan-count-100-incremental-iteration`
| 61,722 (n=5) | 71,445 (n=3) | **+15.8%** |
|
`memtier_benchmark-1Mkeys-generic-scan-count-500-incremental-iteration`
| 18,994 (n=5) | 22,594 (n=2) | **+19.0%** |
| `memtier_benchmark-1Mkeys-generic-scan-count-500-pipeline-10` | 25,677
(n=5) | 35,442 (n=2) | **+38.0%** |
| `memtier_benchmark-1Mkeys-generic-scan-pipeline-10` | 824,033 (n=5) |
920,415 (n=2) | **+11.7%** |
| `memtier_benchmark-1Mkeys-generic-scan-type-pipeline-10` | 764,420
(n=5) | 852,255 (n=2) | **+11.5%** |
| `memtier_benchmark-1Mkeys-generic-scan-cursor-count-500-pipeline-10` |
15,264 (n=5) | 19,688 (n=2) | **+29.0%** |
| `memtier_benchmark-1Mkeys-generic-scan-cursor-pipeline-10` | 491,250
(n=5) | 564,721 (n=2) | **+15.0%** |

### arm-aws-m8g.metal-24xl

| Test | unstable | PR | Δ |
|------|---------:|---:|--:|
| `memtier_benchmark-1Mkeys-generic-scan-count-10-incremental-iteration`
| 195,917 (n=5) | 204,520 (n=3) | **+4.4%** |
|
`memtier_benchmark-1Mkeys-generic-scan-count-10-incremental-iteration-high-cursor-count`
| 177,644 (n=5) | 182,682 (n=3) | **+2.8%** |
| `memtier_benchmark-1Mkeys-generic-scan-count-50-incremental-iteration`
| 103,337 (n=5) | 118,119 (n=2) | **+14.3%** |
|
`memtier_benchmark-1Mkeys-generic-scan-count-100-incremental-iteration`
| 66,199 (n=5) | 77,436 (n=3) | **+17.0%** |
|
`memtier_benchmark-1Mkeys-generic-scan-count-500-incremental-iteration`
| 18,869 (n=5) | 21,790 (n=2) | **+15.5%** |
| `memtier_benchmark-1Mkeys-generic-scan-count-500-pipeline-10` | 27,621
(n=5) | 38,585 (n=2) | **+39.7%** |
| `memtier_benchmark-1Mkeys-generic-scan-pipeline-10` | 789,621 (n=5) |
893,041 (n=2) | **+13.1%** |
| `memtier_benchmark-1Mkeys-generic-scan-type-pipeline-10` | 725,833
(n=5) | 878,881 (n=2) | **+21.1%** |
| `memtier_benchmark-1Mkeys-generic-scan-cursor-count-500-pipeline-10` |
11,061 (n=5) | 13,996 (n=2) | **+26.5%** |
| `memtier_benchmark-1Mkeys-generic-scan-cursor-pipeline-10` | 411,119
(n=5) | 483,889 (n=2) | **+17.7%** |

Pattern is consistent across both architectures: gains scale with
`COUNT` (more keys collected per call → more `listNode` allocations
avoided). The ~+40% peak on `count-500-pipeline-10` is where the
per-call allocator overhead dominated the previous implementation.

No test regresses. Every delta is positive.

## Tests

- `./runtest --single unit/scan` — 0 exceptions
- `./runtest --single unit/type/hash` — 0 exceptions (exercises HSCAN
path)

## References

- **#15039** — @moticless introduced `vec` (append-only pointer vector
with optional stack-backed storage).
- **#14958** (Subkey notification for hash fields, @ShooterIT) is the
first in-tree consumer of `vec`; this PR is a second, small consumer.
2026-04-27 14:16:36 +03:00
sggeorgievandGitHub c61099eaa0 Replace recursive rax tree freeing with iterative traversal (#15103)
`raxRecursiveFree` and `raxRecursiveFreeWithCtx` used C call-stack
recursion to walk the entire radix tree during `raxFree`. On trees with
pathologically deep paths (long keys with no shared prefixes) this could
overflow the thread stack and crash the process.

This PR replaces both recursive functions with a single unified
iterative helper (`raxFreeNodesWithCallback`) that maintains an explicit
heap-allocated `raxStack` — the same stack structure already used
elsewhere in the rax code (e.g. `raxIterator`). The helper accepts both
callback variants (with and without a user-supplied context) so the two
public entry points `raxFreeWithCallback` and `raxFreeWithCbAndContext`
now both delegate to it. Child pointers are now enumerated forward from
`raxNodeFirstChildPtr` instead of backward from `raxNodeLastChildPtr`,
which is simpler and consistent with how the rest of the codebase
traverses children. No functional change: every node is still visited
exactly once, its optional data callback is still invoked before the
node is freed, and `rax->numnodes` is decremented identically.
2026-04-27 08:47:43 +03:00
Hanzo DevandGitHub 05c26399ec ci: migrate to canonical hanzoai/.github/docker-build.yml reusable (#2) 2026-04-23 18:58:52 -07:00
Hanzo AI 80a5137ee9 ci: migrate to canonical hanzoai/.github/docker-build.yml reusable 2026-04-23 18:57:40 -07:00
sggeorgievandGitHub 47c51369ee Reject corrupt stream RDB with shared NACK across consumers (#15081)
**Summary**

Detects and rejects corrupt stream RDB payloads where the same NACK
(pending entry) is referenced by more than one consumer, which violates
a stream data-structure.

**Changes**

- **`rdbLoadObject` (stream consumer PEL loading)**: Added a guard that
checks `nack->consumer != NULL` before assigning the consumer pointer.
When a second consumer's PEL references a NACK that was already claimed
by a prior consumer, the loader now reports a corrupt RDB error and
aborts instead of silently overwriting the pointer. Without this check,
two consumers share the same `streamNACK`, and freeing the first
consumer's PEL leaves the second with a dangling pointer.
- **`corrupt-dump.tcl`**: Added a regression test that crafts a stream
with two consumers (`consumerA`, `consumerB`) whose PELs both reference
the same entry (`1-0`). The `RESTORE` command is expected to fail with
`"Bad data format"`, and the server must remain responsive (`PING`
succeeds).

**Benefits**

- **Fail-fast on corrupt data**: The invariant violation is caught at
load time with a clear diagnostic message rather than manifesting as a
crash later during normal operation.
- **Regression coverage**: The crafted payload in the test ensures this
class of corruption is permanently guarded against.
2026-04-23 15:46:48 +03:00
Vitah LinandGitHub fafc47251a Fix signed integer overflow in scan count parameter (#14982)
### Problem 
In `scanGenericCommand`, `maxiterations = count * 10` overflows when
`count > LONG_MAX / 10`, causing undefined behavior.

### Changed 
1. Use saturating arithmetic to prevent overflow.
2. Added a test to trigger the overflow path, detectable by UBSan.
2026-04-23 17:38:42 +08:00
303667a40c Fix use-after-free in RM_RegisterClusterMessageReceiver() (#15059)
RM_RegisterClusterMessageReceiver() unlinks a receiver node from the
clusterReceivers[type] linked list when the callback is set to NULL, but
when removing the head node (prev == NULL), the code updates
clusterReceivers[type]->next instead of clusterReceivers[type] itself.

This leaves clusterReceivers[type] pointing to the freed node, so any
later traversal through clusterReceivers[type] dereferences a dangling
pointer.

Fix by updating clusterReceivers[type] directly when prev == NULL.

Fixes #15057

---------

Co-authored-by: debing.sun <debing.sun@redis.com>
2026-04-23 16:44:36 +08:00
sggeorgievandGitHub 63f02e7876 Fix double ERR prefix in XNACK error replies (#15091)
Several `addReplyError` and `addReplyErrorFormat` calls in
`xnackCommand` included a redundant `"ERR "` prefix in the message
string. Since `addReplyErrorLength` already prepends `-ERR ` to the RESP
reply, clients received `ERR ERR ...` for these error paths.

This PR removes the redundant prefix from all five affected calls and
tightens the corresponding test patterns to match from the beginning of
the error message (`"ERR ..."` instead of `"*...*"`), so any future
double-prefix regression will be caught.
2026-04-22 09:12:04 +03:00
0fa78fd8fd perf: widen fast_float_strtod fast path to 17-19 digit mantissas (#15061)
## Root cause

Roughly 50% of random double scores generated by the ZADD listpack
workload have 17-19 significant digits, which exceed
`MAX_MANTISSA_FAST_PATH` (`2^53`). These inputs fall through to the
`strtod()` fallback:

```c
char static_buf[128];
memcpy(buf, nptr, len);           /* memcpy back! */
buf[len] = '\0';                   /* null-term */
double result = strtod(buf, ...);  /* glibc strtod — ~10× slower on ARM */
```

The original C++ `fast_float` library handled the same 17-19 digit
inputs with Eisel-Lemire / bigint arithmetic without falling back to
`strtod()`. That is what the pure-C replacement lost.

## Fix

Compute `mantissa * 10^exponent` in 128-bit integer arithmetic using
`__uint128_t`, then convert to double with a single IEEE
round-to-nearest-even cast. Supported for `|exp| in [0, 19]` where
`10^|exp|` fits in `uint64`; cases outside that range (or otherwise
outside the fast path's preconditions) still fall through to `strtod()`.

---------

Co-authored-by: debing.sun <debing.sun@redis.com>
2026-04-20 20:45:49 +08:00
Omer ShadmiandGitHub 58dc4f3c85 Update RediSearch to 8.8 RC1 (v8.7.90) (#15072)
Update RediSearch module version to 8.8 RC1 (v8.7.90)


Made with [Cursor](https://cursor.com)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Low risk: a single version bump that changes which RediSearch git tag
is cloned/built; main risk is build/runtime incompatibility from the
upstream RC update.
> 
> **Overview**
> Updates the RediSearch module build configuration to fetch and build
upstream `redisearch` tag `v8.7.90` (8.8 RC1) instead of `v8.5.90`.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
21e121c7380568130846f9d202c90f72ad93e0f3. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
2026-04-19 10:49:34 +03:00
8677971360 Remove unnecessary -ERR and \r\n for addReplyErrorFormat in extractLongLatOrReply() (#14995)
In addReplyErrorLength and addReplyErrorFormatInternal, `-ERR` is
automatically prepended if the message doesn’t start with `-`, so the
initial `-ERR` is unnecessary. Also, trailing `\r\n` will be trimmed, so
it doesn’t need to be included.

---------

Signed-off-by: charsyam <charsyam@naver.com>
Signed-off-by: DaeMyung Kang <charsyam@gmail.com>
Co-authored-by: debing.sun <debing.sun@redis.com>
2026-04-17 17:28:13 +08:00
Vitah LinandGitHub 8aeea8c210 Increase threshold for HPEXPIRETIME persists after RDB reload test (#15047) 2026-04-17 16:36:02 +08:00
Vitah LinandGitHub 15cb40dac2 Fix command-docs and corrupt-dump-fuzzer of OBJ_GCRA type (#15055)
### Problem 

While the new type `OBJ_GCRA` was added, several related code paths were
not updated accordingly, leading to failures in the
`reply-schemas-validator` CI job and `corrupt-dump-fuzzer.tcl`

##### reply-schemas-validator

Failed CI:
https://github.com/redis/redis/actions/runs/24485248057/job/71558533290#step:10:903
```shell
Traceback (most recent call last):
  File "/home/runner/work/redis/redis/./utils/req-res-log-validator.py", line 238, in process_file
    jsonschema.validate(instance=res.json, schema=req.schema, cls=schema_validator)
  File "/home/runner/.local/lib/python3.12/site-packages/jsonschema/validators.py", line 1121, in validate
    raise error
jsonschema.exceptions.ValidationError: 'rate_limit' is not valid under any of the given schemas

Failed validating 'oneOf' in schema['patternProperties']['^.*$']['properties']['group']:
    {'description': 'the functional group to which the command belongs',
     'oneOf': [{'const': 'bitmap'},
               {'const': 'cluster'},
               {'const': 'connection'},
               {'const': 'generic'},
               {'const': 'geo'},
               {'const': 'hash'},
               {'const': 'hyperloglog'},
               {'const': 'list'},
               {'const': 'module'},
               {'const': 'pubsub'},
               {'const': 'scripting'},
               {'const': 'sentinel'},
               {'const': 'server'},
               {'const': 'set'},
               {'const': 'sorted-set'},
               {'const': 'stream'},
               {'const': 'string'},
               {'const': 'transactions'}]}

On instance['gcrasetvalue']['group']:
    'rate_limit'
```


##### `corrupt-dump-fuzzer.tcl`

Also fixed `: Fuzzer corrupt restore payloads - sanitize_dump: yes in
tests/integration/corrupt-dump-fuzzer.tcl`

Failed daily test :
https://github.com/redis/redis/actions/runs/24485248057/job/71558533312#step:6:8652
```shell
Server crashed (by signal: 0, err: key "gcra" not known in dictionary), with payload: "\x1C\x0A\x02\x5F\x37\xC0\x06\xC0\x00\x02\x5F\x39\xC0\x08\x02\x5F\x33\x02\x5F\x35\x02\x5F\x31\xC0\x02\xC0\x04\x0E\x00\xA9\x71\xBF\xEE\x6F\x46\xEF\xA6"
violating commands:
Done 1434 cycles in 600 seconds.
RESTORE: successful: 601, rejected: 833
Total commands sent in traffic: 1194776, crashes during traffic: 1 (0 by signal).
[: Fuzzer corrupt restore payloads - sanitize_dump: yes in tests/integration/corrupt-dump-fuzzer.tcl
Expected '1' to be equal to '0' (context: type eval line 155 cmd {assert_equal $stat_terminated_in_traffic 0} proc ::test)
[147/147 done]: integration/corrupt-dump-fuzzer (1201 seconds)
```

### Changed

This change completes the necessary updates across all relevant
components to ensure consistent handling of the rate_limit group and
restores CI stability.
2026-04-17 10:30:43 +03:00
Yuan WangandGitHub 4757561861 Subkey notification for hash fields (#14958)
## Motivation

Redis's existing keyspace notification system operates at the **key
level** only — when a hash field is modified via `HSET`, `HDEL`, or
`HEXPIRE`, the subscriber receives the key name and the event type, but
not **which fields** were affected, therefore, these notifications has
very little practical value.

This PR introduces a subkey notification system that extends keyspace
events to include field-level (subkey) details for hash operations,
through both Pub/Sub channels and the Module API.

## New Pub/Sub Notification Channels

Four new channels are added:

|Channel Format | Payload |
|---------------|---------|
| `__subkeyspace@<db>__:<key>` | `<event>\|<len>:<subkey>[,...]` |
|`__subkeyevent@<db>__:<event>` |
`<key_len>:<key>\|<len>:<subkey>[,...]` |
| `__subkeyspaceitem@<db>__:<key>\n<subkey>` | `<event>` |
|`__subkeyspaceevent@<db>__:<event>\|<key>` | `<len>:<subkey>[,...]` |

**Design rationale for 4 channels:**
- **Subkeyspace**: Subscribe to a specific key, receive all field
changes in a single message — efficient for key-centric consumers.
- **Subkeyevent**: Subscribe to a specific event type, receive
key+fields — efficient for event-centric consumers.
- **Subkeyspaceitem**: Subscribe to a specific key+field combination —
the most selective, one message per field, no parsing needed.
- **Subkeyspaceevent**: Subscribe to event+key combination, receiving
only the affected fields — server-side filtering on both dimensions.

Subkeys are encoded in a length-prefixed format (`<len>:<subkey>`) to
support binary-safe field names containing delimiters.

**Safety guards:**
- Events containing `|` are skipped for `__subkeyspace` and
`__subkeyspaceevent ` channels (to avoid parsing ambiguity).
- Keys containing `\n` are skipped for the `__subkeyspaceitem` channel
(newline is the key/subkey separator).
- Subkeys channels are only published when `subkeys != NULL && count >
0`.

## Hash Command Integration

The following hash operations now emit subkey level notifications with
the affected field names:

| Command | Event | Subkeys |
|---------|-------|---------|
| `HSET` / `HMSET` | `hset` | All fields being set |
| `HSETNX` | `hset` | The field (if set) |
| `HDEL` | `hdel` | All fields deleted |
| `HGETDEL` | `hdel` / `hexpired` | Deleted or lazily expired fields |
| `HGETEX` | `hexpire` / `hpersist` / `hdel` / `hexpired` | Affected
fields per event |
| `HINCRBY` | `hincrby` | The field |
| `HINCRBYFLOAT` | `hincrbyfloat` | The field |
| `HEXPIRE` / `HPEXPIRE` / `HEXPIREAT` / `HPEXPIREAT` | `hexpire` |
Updated fields |
| `HPERSIST` | `hpersist` | Persisted fields |
| `HSETEX` | `hset` / `hdel` / `hexpire` / `hexpired` | Affected fields
per event |
| Field expiration (active/lazy) | `hexpired` | All expired fields
(batched) |

For field expiration, expired fields are collected into a dynamic array
and sent as a single batched notification after the expiration loop,
rather than one notification per field.

## Module API

Three new APIs and one new callback type:

```c
/* Function pointer type for keyspace event notifications with subkeys from modules. */
typedef void (*RedisModuleNotificationWithSubkeysFunc)(
    RedisModuleCtx *ctx, int type, const char *event,
    RedisModuleString *key, RedisModuleString **subkeys, int count);

/* Subscribe to keyspace notifications with subkey information.
 *
 * This is the extended version of RM_SubscribeToKeyspaceEvents. When subkeys
 * are available, the `subkeys` array and `count` are passed to the callback.
 * `subkeys` contains only the names of affected subkeys (values are not included),
 * and `count` is the number of elements. The array may contain duplicates when
 * the same subkey appears more than once in a command (e.g. HSET key f1 v1 f1 v2
 * produces subkeys=["f1","f1"], count=2). When no subkeys are present, `subkeys`
 * will be NULL and `count` will be 0. Whether events without subkeys are delivered
 * depends on the `flags` parameter (see below).
 *
 * `types` is a bit mask of event types the module is interested in
 * (using the same REDISMODULE_NOTIFY_* flags as RM_SubscribeToKeyspaceEvents).
 *
 * `flags` controls delivery filtering:
 *  - REDISMODULE_NOTIFY_FLAG_NONE: The callback is invoked for all matching
 *    events regardless of whether subkeys are present, so a separate
 *    RM_SubscribeToKeyspaceEvents registration can be omitted.
 *  - REDISMODULE_NOTIFY_FLAG_SUBKEYS_REQUIRED: The callback is only invoked
 *    when subkeys are not empty. Events without subkey information (e.g. SET,
 *    EXPIRE, DEL) are skipped.
 *
 * The callback signature is:
 *   void callback(RedisModuleCtx *ctx, int type, const char *event,
 *                 RedisModuleString *key, RedisModuleString **subkeys, int count);
 *
 * The subkeys array and its contents are only valid during the callback.
 * The underlying objects may be stack-allocated or temporary, so
 * RM_RetainString must NOT be used on them. To keep a subkey beyond
 * the callback (e.g. in a RM_AddPostNotificationJob callback), use
 * RM_HoldString (which handles static objects by copying) or
 * RM_CreateStringFromString to make a deep copy before returning.
 */
int RM_SubscribeToKeyspaceEventsWithSubkeys(RedisModuleCtx *ctx, int types, int flags, RedisModuleNotificationWithSubkeysFunc callback);

/* Unregister a module's callback from keyspace notifications with subkeys
 * for specific event types.
 *
 * This function removes a previously registered subscription identified by
 * the event mask, delivery flags, and the callback function.
 *
 * Parameters:
 *  - ctx: The RedisModuleCtx associated with the calling module.
 *  - types: The event mask representing the notification types to unsubscribe from.
 *  - flags: The delivery flags that were used during registration.
 *  - callback: The callback function pointer that was originally registered.
 *
 * Returns:
 *  - REDISMODULE_OK on successful removal of the subscription.
 *  - REDISMODULE_ERR if no matching subscription was found. */ 
int RM_UnsubscribeFromKeyspaceEventsWithSubkeys(
    RedisModuleCtx *ctx, int types, int flags,
    RedisModuleNotificationWithSubkeysFunc cb);

/* Like RM_NotifyKeyspaceEvent, but also triggers subkey-level notifications
 * when subkeys are provided. Both key-level (keyspace/keyevent) and
 * subkey-level (subkeyspace/subkeyevent/subkeyspaceitem/subkeyspaceevent)
 * channels are published to, depending on the server configuration.
 *
 * This is the extended version of RM_NotifyKeyspaceEvent and can actually
 * replace it. When called with subkeys=NULL and count=0, it behaves
 * identically to RM_NotifyKeyspaceEvent. */
int RM_NotifyKeyspaceEventWithSubkeys(
    RedisModuleCtx *ctx, int type, const char *event,
    RedisModuleString *key, RedisModuleString **subkeys, int count);
```

## Configuration

Subkey notifications are controlled via the existing
`notify-keyspace-events` configuration string with four new characters:
`notify-keyspace-events` "STIV"

**S** -> Subkeyspace events, published with `__subkeyspace@<db>__:<key>`
prefix.
**T** -> Subkeyevent events, published with
`__subkeyevent@<db>__:<event>` prefix.
**I** -> Subkeyspaceitem events, published per subkey with
`__subkeyspaceitem@<db>__:<key>\n<subkey>` prefix.
**V** -> Subkeyspaceevent events, published with
`__subkeyspaceevent@<db>__:<event>|<key>` prefix.

These flags are **independent** from the existing key-level flags (`K`,
`E`, etc.). Enabling subkey notifications does **not** implicitly enable
or depend on keyspace/keyevent notifications, and vice versa.

## Known Limitations

- **Duplicate fields in subkey notifications**: Subkey notification
payloads may contain duplicate field names when the same field is
affected more than once within a single command. Since duplicate fields
are not the common case and deduplication would introduce significant
overhead on every notification, we chose not to deduplicate at this
time.
- **Subkey is sds encoding object**: We assume the subkey is sds
encoding object, and access it by `subkey->ptr`, and there is an assert,
redis will crash if not.
2026-04-17 13:39:04 +08:00
debing.sunandGitHub ca6e471a3f Fix decrRefCount on NULL robj on corrupt KEY_META payload (#15034)
## Summary

This PR fixes two issues when processing corrupt data in
rdbLoadCheckModuleValue():

1. When handling `RDB_MODULE_OPCODE_STRING` opcode,
rdbGenericLoadStringObject() can return NULL on a corrupt payload. The
code called decrRefCount(o) unconditionally without a NULL check,
resulting in a NULL pointer dereference crash.

2. The while loop condition was `!= RDB_MODULE_OPCODE_EOF`, which means
a truncated payload (causing rdbLoadLen to return RDB_LENERR) would
never exit the loop, since `RDB_LENERR != RDB_MODULE_OPCODE_EOF` is
always true, potentially causing an infinite hang.
2026-04-16 21:50:49 +08:00
Aviv DavidandGitHub 6339fd739e DataTypes update 8.8 RC1 (#15036) 2026-04-16 13:43:10 +03:00
Moti CohenandGitHub fa6d4c3d63 Fix SIGABRT in HSETEX when a field appears twice in the FIELDS list (#14956)
HSETEX crashed on assert() with a SIGABRT when the same field appeared
more than once in the FIELDS list and an expiry time was given
(EX/PX/EXAT/PXAT).

Root cause: hfieldPersist() and the KEEP_TTL path in hashTypeSet() both
asserted that dictExpireMeta->expireMeta.trash == 0, meaning the hash
must be globally registered in the HFE DS. This is incorrect during
HSETEX execution because hashTypeSetExDone(), which registers the hash
globally and clears trash, called only at the end of flow. The private
per-field ebuckets are fully valid regardless of the global registration state.

Fix: Remove both incorrect assertions. The operations on the private
ebuckets (ebRemove in hfieldPersist, ebAdd in the KEEP_TTL path) are
correct and do not require the hash to be globally registered.

Tests: Added two regression tests covering the crash scenarios:
- HSETEX EX with a duplicate field (existing field, expiry given)
- HSETEX FNX EX with a duplicate field (no prior field, FNX condition
passes)
2026-04-16 13:16:52 +03:00
Ozan TezcanandGitHub eb74450fca Log node address when ASM starts (#15056)
Log source/destination address on import/migrate start events for easier
debugging.
2026-04-16 12:13:01 +03:00
3bcfbbe92a Add new OBJ_GCRA type (#14905)
[PR ](https://github.com/redis/redis/pull/14826) introduced a new rate
limiting command which stores its internal implementation-detail data
into a string key.

Since this will prevent a client from detecting type errors or
accidental overwrites or value invalidations, f.e via SET or INCR this
PR introduces a new data type - OBJ_GCRA specifically created for that
new command.

Furthermore, a new RATE_LIMIT KSN type was introduced for emitting "gcra" events on such keys.

GCRASETTAT was renamed to GCRASETVALUE.

---------

Co-authored-by: debing.sun <debing.sun@redis.com>
2026-04-15 17:46:22 +03:00
b89bc044a3 Reduce overhead in command propagation (#15003)
Refactor command propagation code to reduce overhead on master

Currently, the main bottleneck is `feedReplicationBuffer()`. It is
called for each argument in the command and has bookkeeping overhead on
every call (e.g. checking whether to attach replicas to the replication
backlog). It is also not inlined by the compiler. These costs become
more visible with pipelining and commands with many arguments (e.g. HSET
with many fields).

Changes:

- Defer all bookkeeping to be done once per command instead of once per
command argument.
- Refactor the hot path so the compiler can inline
`replBufWriterAppend()`.
- Add `replBufWritterAppendBulkLen()` that uses shared RESP headers for
small values, avoiding formatting overhead.

These changes should not introduce any behavioral change.

**TODO:** In a follow-up PR, explore forwarding the exact command from
the client querybuf to avoid re-serialization. Many commands are
propagated without modification and can benefit from this.

--


| Benchmark | Before (ops/s) | After (ops/s) | Improvement |
|---|---|---|---|
| SET | 256,048 | 265,131 | **+3%** |
| SET (pipeline) | 1,477,310 | 1,671,272 | **+13%** |
| HSET 10 fields | 145,000 | 158,000 | **+9%** |
| HSET 10 fields (pipeline) | 363,483 | 430,855 | **+18%** |
| HSET 10 fields, 15B values (pipeline) | 387,443 | 487,135 | **+26%** |
| ZADD 5 members | 180,700 | 193,519 | **+7%** |
| ZADD 5 members (pipeline) | 466,453 | 564,872 | **+21%** |

------
Co-authored-by: Yuan Wang <yuan.wang@redis.com>
2026-04-15 17:08:36 +03:00
Yuan WangandGitHub 2f1a8b2bad Dismiss dict bucket arrays in fork child to reduce CoW (#14979)
During RDB saving and AOF rewriting, the fork child already dismisses
(madvise(MADV_DONTNEED)) individual key-value objects after serializing them.
However, the hash table bucket arrays of each dict were never dismissed,
leaving large contiguous allocations subject to CoW when the parent
modifies them.

This PR extends the dismiss mechanism to cover dict bucket arrays,
reducing CoW memory overhead.

- **Expires kvstore** — dismissed upfront before saving starts, since the
child never accesses expires directly, after embeding expire time in the key object.
- **Slot dicts** (cluster mode) — dismissed per-slot as the iterator moves
   to the next slot during RDB saving or AOF rewriting.
- **DB keys kvstore** (standalone mode) — dismissed per-DB after each DB is
   fully serialized during RDB saving or AOF rewriting.
2026-04-15 20:34:36 +08:00
670993a89d Replace fast_float C++ library with pure C implementation (#14661)
The fast_float dependency required C++ (libstdc++) to build Redis. This
commit replaces the 3800-line C++ template library with a minimal pure C
implementation (~360 lines) that provides the same functionality needed
by Redis.

This is **very important** because Redis build process would fail
without g++ installed, a common situation in Linux distributions even
after installing the basic build tools: we want the build process of
Redis to be the simplest possible. Also Redis sometimes is compiled in
embedded systems lacking the g++ toolchain. There is no reason to depend
on C++ in a project written in C.

## The C implementation uses
1. Fast path (Clinger's algorithm) for numbers with mantissa <= 2^53 and
exponent in [-22, 22], covering ~99% of real-world cases.
2. Fallback to strtod() for complex cases to ensure correctly-rounded
results.

## Changes
- Move new fast_float_strtod.c(C implementation) from deps into Redis
core since it is now a single file and no longer needs a separate
directory.
- Remove all c++ dependencies

The implementation was tested against both strtod and the original C++
implementation with 10,000+ test cases including edge cases, special
values (inf/nan), and random inputs.

---------

Co-authored-by: debing.sun <debing.sun@redis.com>
Co-authored-by: Mincho Paskalev <minchopaskal@gmail.com>
Co-authored-by: Moti Cohen <moti.cohen@redis.com>
2026-04-15 20:33:55 +08:00
Vitah LinandGitHub 3cd464263b Fix gen_write_load error on MOVED/ASK during atomic-slot-migration tests (#15016) 2026-04-15 08:34:40 +08:00
Moti CohenandGitHub 3f810d35bf Introduce internal append-only pointer vector DS (#15039)
Refactoring work for follow-ups (e.g. subkey notifications
#14958), splitting reusable infrastructure from feature logic.

Optimized for stack allocation with optional growth to heap. Usage:

Start on stack (grow to heap):
  vec v;
  void *vstack[8];
  vecInit(&v, vstack, 8);

Start embedded (grow to heap):
  typedef struct {
    vec v;
    void *vembedded[8];
  } obj;
  vecInit(&obj.v, obj.vembedded, 8);

Heap only (capacity 8 or 0):
  vecInit(&v, NULL, 8);
  vecInit(&v, NULL, 0);

Reserve based on size:
  vecInit(&v, vstack, 8);
  vecReserve(&v, varsize); // <=8 uses stack, else heap
2026-04-14 18:45:48 +03:00
debing.sunandGitHub 2049c7fe32 Fix wrong argv index in xinfoReplyWithStreamInfo for slot alloc size tracking (#15037)
`xinfoReplyWithStreamInfo` passed the wrong key(c->argv[1]) instead of
`c->argv[2]` to `updateSlotAllocSize` when updating per-slot memory
tracking.

Fix by passing the key explicitly to `xinfoReplyWithStreamInfo` instead
of relying on a hardcoded argv index.
Also, add the `-DDEBUG_ASSERTIONS` flag to the test-ubuntu-jemalloc CI
to cover this debug assertion.
2026-04-14 19:26:42 +08:00
Sergei GeorgievandGitHub 80f1ebda88 Add AGGREGATE COUNT option to ZUNION, ZINTER, ZUNIONSTORE, and ZINTERSTORE (#14892)
### Overview

This PR adds a new `COUNT` aggregation mode to the `ZUNIONSTORE`,
`ZINTERSTORE`, `ZUNION`, and `ZINTER` sorted set commands. When
`AGGREGATE COUNT` is specified, the resulting score for each element
reflects how many input sets contain it (optionally scaled by
`WEIGHTS`), rather than combining the actual scores of the elements.
This enables a common use case — counting set membership frequency —
directly at the command level, without application-side workarounds.

### Problem Statement

For developers who need to know **how many input sorted sets contain
each element**, there is no single-command solution today.

**Example:** given several game leaderboards, find how many leaderboards
each player appears in.

The existing aggregation modes (`SUM`, `MIN`, `MAX`) all operate on the
elements' scores. To ignore scores and just count set membership, you'd
currently need to copy each sorted set with all scores set to 1, then
run `ZUNIONSTORE`/`ZINTERSTORE` with `SUM` — requiring multiple round
trips, temporary keys, and application-level locking to avoid races.

A `COUNT` aggregation mode solves this directly.

### Solution

Introduces `AGGREGATE COUNT` as a fourth aggregation mode:

- `ZINTER numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE
<SUM | MIN | MAX | COUNT>] [WITHSCORES]`
- `ZINTERSTORE destination numkeys key [key ...] [WEIGHTS weight [weight
...]] [AGGREGATE <SUM | MIN | MAX | COUNT>]`
- `ZUNION numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE
<SUM | MIN | MAX | COUNT>] [WITHSCORES]`
- `ZUNIONSTORE destination numkeys key [key ...] [WEIGHTS weight [weight
...]] [AGGREGATE <SUM | MIN | MAX | COUNT>]`

When `COUNT` is specified, **the scores in the input sets are ignored**.
Note that `WEIGHTS` is **not** ignored — each set contributes its weight
(default 1) per element, and the contributions are summed.

**Implementation details:**

A new helper function `zuiWeightedScore()` computes the per-set
contribution:

```c
inline static double zuiWeightedScore(double score, double weight, int aggregate) {
    return (aggregate == REDIS_AGGR_COUNT) ? weight : weight * score;
}
```

The `zunionInterAggregate()` function treats `COUNT` identically to
`SUM` — it adds the per-set contributions. All four call sites where
`weight * score` was previously computed inline are updated to use
`zuiWeightedScore()`.

### Examples

```
> ZADD s1 1 foo 1 bar
> ZADD s2 2 foo 2 bar
> ZADD s3 3 foo
```

**With `SUM` (existing behavior, for comparison):**

```
> ZINTERSTORE t1 3 s1 s2 s3 WEIGHTS 10 5 3 AGGREGATE SUM
(integer) 1
> ZRANGE t1 0 -1 WITHSCORES
1) "foo"
2) "29"

> ZUNIONSTORE t1 3 s1 s2 s3 WEIGHTS 10 5 3 AGGREGATE SUM
(integer) 2
> ZRANGE t1 0 -1 WITHSCORES
1) "bar"
2) "20"
3) "foo"
4) "29"
```

**With `COUNT` and `WEIGHTS`:**

```
> ZINTERSTORE t1 3 s1 s2 s3 WEIGHTS 10 5 3 AGGREGATE COUNT
(integer) 1
> ZRANGE t1 0 -1 WITHSCORES
1) "foo"
2) "18"

> ZUNIONSTORE t1 3 s1 s2 s3 WEIGHTS 10 5 3 AGGREGATE COUNT
(integer) 2
> ZRANGE t1 0 -1 WITHSCORES
1) "bar"
2) "15"
3) "foo"
4) "18"
```

**With `COUNT` and no specified `WEIGHTS`** — resulting score equals the
number of input sorted sets containing the element:

```
> ZINTERSTORE t1 3 s1 s2 s3 AGGREGATE COUNT
(integer) 1
> ZRANGE t1 0 -1 WITHSCORES
1) "foo"
2) "3"

> ZUNIONSTORE t1 3 s1 s2 s3 AGGREGATE COUNT
(integer) 2
> ZRANGE t1 0 -1 WITHSCORES
1) "bar"
2) "2"
3) "foo"
4) "3"
```

### Backward Compatibility

This is a fully additive change. The new `COUNT` keyword is only
recognized after the `AGGREGATE` token in the four affected commands.
Existing commands, arguments, and default behavior (`AGGREGATE SUM`) are
completely unchanged. No new command is introduced, and no existing
response format is modified.
2026-04-14 09:21:53 +03:00
Moti CohenandGitHub e1d35aca01 Fix HEXPIRE numfields overflow (#15021)
Validate HEXPIRE-family field counts without parser overflow
keep flexible option order; only require fields fit in argv
add tests for INT_MAX numfields across HEXPIRE/HPEXPIRE/HEXPIREAT/HPEXPIREAT
2026-04-13 09:46:46 +03:00
h.o.t. neglectedandGitHub e8da0e5b47 Fix brittle assert_match patterns for unexpected slowlog fields (#14948) 2026-04-13 14:45:14 +08:00
0d85627bf0 Use no_value dict type for stream_idmp_keys to explicitly mark it as a key-only set (#14987)
Fixes #14985

### Problem

dict stream_idmp_keys was using objectKeyPointerValueDictType, in this
dict type dicts are expected to have RObj as keys and Pointers as
values, but stream_idmp_keys was not using the value field at all.

### Solution
This PR fixes the above issue by implementing new dict type
(objectKeyNoValueDictType) for stream_idmp_keys

---------

Co-authored-by: debing.sun <debing.sun@redis.com>
2026-04-10 23:25:56 +08:00
Momchil MarinovandGitHub ae9552663d RED-183356: Automate tarball creation (#14911)
This PR implements the tarball creation job by reusing 01 script. 
It splits the original job to smaller jobs and moves the gate and test
jobs before the upload job.
The job outputs the SHA of the tar and the size.  
Link to a run:
https://github.com/m-marinov/redis/actions/runs/23437802059
2026-04-09 17:58:37 +03:00
dageckoandGitHub e97fe246aa Pin third-party action to commit SHA and move secrets to step env (#14937) 2026-04-09 10:17:39 +08:00
Sergei GeorgievandGitHub 0be39e5032 Fix missing consumer propagation on empty XREADGROUP (#14963)
## Summary

Fixes consumer replication inconsistency when `XREADGROUP` is called for
a new consumer but no `XCLAIM` commands are propagated to the replica.
Previously, consumer creation was only propagated to replicas when
`noack=true`, relying on `XCLAIM` propagation to implicitly create the
consumer in the non-NOACK path. However, if no messages exist to read,
no `XCLAIM` is generated, and the consumer is silently lost on the
replica.

This is a follow-up to the original fix in
[redis/redis#7140](https://github.com/redis/redis/issues/7140) /
[redis/redis#7526](https://github.com/redis/redis/pull/7526), which
introduced `XGROUP CREATECONSUMER` propagation but only for the `NOACK`
case.

## Changes

- **`xreadgroupCommand` (src/t_stream.c):** Replaced the `if (noack)`
guard around the `streamPropagateConsumerCreation()` call with a
deferred check after `streamReplyWithRange()`. Consumer creation is now
propagated when `noack || propCount == 0` — that is, only when no
`XCLAIM` commands were generated. This avoids redundant propagation in
the common case where `XCLAIM` already implicitly creates the consumer
on the replica, while correctly handling both the NOACK path (where
PEL/XCLAIM is skipped entirely) and the no-messages path (where there is
nothing to XCLAIM).
- **Test (tests/unit/type/stream-cgroups.tcl):** Added replication test
`"XREADGROUP propagates new consumer to replica"` that sets up a
master-replica pair and verifies consumer propagation in two cases: (1)
without NOACK when no messages are available to deliver, and (2) with
NOACK when messages are delivered but XCLAIM is skipped.

## Benefits

- **Master-replica consistency:** Consumers created by `XREADGROUP` are
now visible on replicas whenever no `XCLAIM` would otherwise create them
— covering both the NOACK path and the empty-stream path.
- **No redundant propagation:** The noack || propCount == 0 condition
avoids emitting a superfluous XGROUP CREATECONSUMER when XCLAIM commands
are already propagated and would implicitly create the consumer on the
replica.
2026-04-08 14:59:22 +03:00
charsyamandGitHub c77d60d6b8 fix trivial double-free issue in rdbLoadObject (#15011) 2026-04-08 09:50:51 +08:00
Sergei GeorgievandGitHub 747dfe578e Add XNACK command for releasing stream messages back to the group (#14797)
### Overview

This PR enhances Redis Streams consumer groups by adding a new `XNACK`
command that allows consumers to explicitly release pending messages
back to the group without acknowledging them. Released (NACKed) entries
become immediately available for re-delivery to other consumers,
eliminating the idle-timeout delay currently required for message
recovery. The command supports three modes — SILENT, FAIL, and FATAL —
giving consumers fine-grained control over delivery counter semantics to
handle graceful shutdowns, transient failures, and poison messages
respectively.

### Problem Statement

For developers using Redis Streams with consumer groups, there are
several common scenarios where a consumer needs to release a message it
has claimed without acknowledging it:

1. **Transient internal failures**: A consumer may fail to process a
message because of problems unrelated to the message itself — for
example, it cannot connect to an external service to fetch required
context. The message is perfectly valid and should be retried promptly
by another consumer.

2. **Resource pressure**: A consumer under resource stress (low CPU, low
memory) may be unable to handle a specific message (e.g., a complex or
large message) within acceptable QoS. It should leave the opportunity to
other consumers in the group, with minimal delay.

3. **Graceful shutdown**: A consumer about to shut down would like to
immediately release all unprocessed messages it has claimed, so they can
be picked up by remaining consumers without waiting for idle timeouts.

4. **Poison / malicious messages**: A consumer may detect or suspect
that a claimed message is invalid or malicious and wants to mark it as
permanently failed (for dead-letter queue processing when available).

**Currently, a consumer cannot NACK a message.** It can either:

- **XACK** it — marks it as "processed" and removes it from the PEL
entirely, losing the ability to redeliver it
- **Leave it pending** — requires other consumers to discover it via
`XPENDING` and claim it via `XCLAIM`/`XAUTOCLAIM` or `XREADGROUP CLAIM`
after the idle timeout expires, introducing a long, unnecessary delay

In all these cases, the logic that applications must implement
introduces **message handling delays**, **implementation complexity**,
and **code duplication** across consumer implementations.

### Solution

Introduces a new `XNACK` (Negative ACKnowledge) command that explicitly
releases pending messages from their owning consumer back to the group's
PEL, making them immediately claimable via `XCLAIM` and `XAUTOCLAIM`,
and prioritized for re-delivery in `XREADGROUP CLAIM`:

```
XNACK key group <SILENT|FAIL|FATAL> IDS numids id [id ...] [RETRYCOUNT count] [FORCE]
```

When executed, the command:

1. **Disassociates** the entry from its owning consumer (`consumer =
NULL`)
2. **Repositions** the entry to the head of the PEL time-ordered list
(`delivery_time = 0`), making it immediately claimable with any
`min-idle-time` threshold
3. **Adjusts the delivery counter** based on the specified mode, giving
consumers fine-grained control over retry semantics
4. **Returns** the count of successfully NACKed entries

**Mode** controls the delivery counter adjustment and communicates the
reason for the NACK:

| Mode | Delivery Counter Behavior | Use Case |

|----------|---------------------------------------------------|---------------------------------------------|
| `SILENT` | Decrement by 1 (undo the delivery increment) | Consumer
shutdown / transient internal error — the delivery "didn't count" |
| `FAIL` | No change (keep the incremented value) | Message too complex
for this consumer, but may work for others — count this as an attempt |
| `FATAL` | Set to `LLONG_MAX` | Invalid / suspected malicious message —
mark as permanently failed |

The three modes map directly to the real-world scenarios above:

- **SILENT** for graceful shutdown or transient failures unrelated to
the message
- **FAIL** for resource-constrained consumers that cannot handle a
specific message
- **FATAL** for poison message detection and dead-letter queue
integration

**Optional parameters:**

- **`RETRYCOUNT count`**: Directly sets `delivery_count` to the
specified value, overriding the mode-based adjustment
- **`FORCE`**: Creates new unowned PEL entries for IDs that are not
already in the group PEL (the entry must exist in the stream). When
`FORCE` creates an entry, the delivery counter is set to `0` (or to
`RETRYCOUNT` if specified, or to `LLONG_MAX` if mode is `FATAL`). This
is used internally for AOF rewrite and replication.

### Response Format

The command returns an integer — the number of messages successfully
NACKed (released back to the group PEL):

```
127.0.0.1:6379> XADD mystream 1-0 f v1
"1-0"
127.0.0.1:6379> XADD mystream 2-0 f v2
"2-0"
127.0.0.1:6379> XGROUP CREATE mystream grp 0
OK
127.0.0.1:6379> XREADGROUP GROUP grp c1 STREAMS mystream >
1) 1) "mystream"
   2) 1) 1) "1-0"
         2) 1) "f"
            2) "v1"
      2) 1) "2-0"
         2) 1) "f"
            2) "v2"
127.0.0.1:6379> XNACK mystream grp FAIL IDS 2 1-0 2-0
(integer) 2
```

After XNACK, the entries appear with an empty consumer in XPENDING
output:

```
127.0.0.1:6379> XPENDING mystream grp - + 10
1) 1) "1-0"
   2) ""
   3) (integer) -1
   4) (integer) 1
2) 1) "2-0"
   2) ""
   3) (integer) -1
   4) (integer) 1
```

### NACK Zone: Data Structure Extension

To support unowned PEL entries and ensure they are prioritized for
re-delivery, a **NACK zone** is introduced at the head of the existing
PEL time-ordered doubly-linked list. A new `pel_nack_tail` pointer is
added to the `streamCG` structure:

**PEL ordering:**

```
[pel_time_head] <-> ... <-> [pel_nack_tail] <-> [owned entries...] <-> [pel_time_tail]
|_____________ NACK zone ______________|   |_______ normal PEL ________|
```

The head of the PEL contains all NACKed messages (FIFO-ordered),
followed by all delivered messages that were not NACKed (same order as
today). This ensures NACKed messages are always prioritized over idle
pending messages.

The delivery order for `XREADGROUP` is therefore:
1. If `CLAIM` was specified: first deliver NACKed messages, then deliver
due pending messages (current behavior)
2. Deliver new entries after the group's last-delivered-id (current
behavior)

**Structure Design:**

- NACKed entries occupy positions from `pel_time_head` to
`pel_nack_tail` in the time-ordered list
- Their `delivery_time` is set to `0`, ensuring they always appear
"oldest" and are immediately claimable
- Their `consumer` pointer is set to `NULL`, marking them as unowned
- `pel_nack_tail` is `NULL` when no NACKed entries exist

**Key Properties:**

- **O(1) insertion**: New NACKed entries are inserted right after
`pel_nack_tail` (or at the list head if the zone is empty)
- **FIFO ordering** among NACKed entries: entries are NACKed in the
order they are released
- **Immediate claimability**: Since `delivery_time = 0`, NACKed entries
have maximum idle time and satisfy any `min-idle-time` threshold in
`XCLAIM` and `XAUTOCLAIM`, In `XREADGROUP CLAIM`, NACKed entries are
also prioritized over other pending entries due to their position at the
head of the PEL.
- **Zone integrity**: The `pelListInsertSorted` function is updated to
stop scanning at the `pel_nack_tail` boundary, ensuring owned entries
are never placed inside the NACK zone

### Impact on Existing Commands

All commands that interact with the PEL are updated to handle unowned
(`consumer = NULL`) entries:

- **XPENDING**: Shows NACKed entries with an empty consumer name
- **XCLAIM / XAUTOCLAIM**: Can claim NACKed entries (they satisfy any
min-idle-time since `delivery_time = 0`)
- **XREADGROUP CLAIM**: NACKed entries are picked up by the claim phase
- **XACK**: Works correctly on NACKed entries (removes from group PEL)
- **XINFO STREAM FULL**: Displays NACKed entries with an empty consumer
name
- **XGROUP DELCONSUMER**: Unaffected — NACKed entries are not in any
consumer's PEL

Propagation is also updated: when `XCLAIM` or `XAUTOCLAIM` encounters a
deleted stream entry for an unowned NACK, it propagates `XACK` (instead
of `XCLAIM`) to replicas and AOF, since there is no source consumer to
reference.

### Persistence

**RDB:**

- A new RDB type `RDB_TYPE_STREAM_LISTPACKS_5` (type 27) is introduced
- After saving consumer PEL entries, the NACK zone stream IDs are saved
separately (count + encoded IDs)
- On load, NACK zone entries are reconstructed by looking them up in the
group PEL, unlinking from their sorted position, and re-inserting into
the NACK zone via `pelListInsertNacked`
- Backward compatibility is preserved: old RDB types continue to load
with the existing validation (all entries must have consumers)

**AOF:**

- AOF rewrite emits `XNACK <key> <group> FAIL IDS <n> <id...> RETRYCOUNT
<cnt> FORCE` commands for entries in the NACK zone
- Consecutive entries with the same `delivery_count` are batched into a
single command (up to `AOF_REWRITE_ITEMS_PER_CMD` IDs per command)

### Defragmentation

The defragmentation logic is restructured to handle unowned entries:

- **`defragStreamCGPendingEntry`** (new): Walks the group-level PEL rax,
defragments each NACK, updates the doubly-linked list pointers
(`pel_prev`, `pel_next`), `pel_time_head`, `pel_time_tail`,
`pel_nack_tail`, and the consumer PEL back-pointer for owned entries
- **`defragStreamConsumerPendingEntry`** (simplified): Only fixes up
back-pointers to the possibly-relocated consumer and CG, since actual
defragmentation is now done at the group-level walk. Unowned (NACK zone)
entries have no consumer PEL walk, so the group-level pass is their only
chance

### Key Benefits

- **Immediate re-delivery**: NACKed entries are instantly claimable by
other consumers via `XCLAIM` and `XAUTOCLAIM` (since `delivery_time = 0`
satisfies any `min-idle-time`), and prioritized for re-delivery in
`XREADGROUP CLAIM`, eliminating idle-time delays that can range from
seconds to minutes
- **Explicit release semantics**: Consumers can release messages
intentionally, with fine-grained control over retry behavior — a
capability that exists in competing systems like RabbitMQ
- **Flexible retry control**: Three modes (SILENT, FAIL, FATAL) plus
RETRYCOUNT cover the full spectrum of failure handling strategies, from
graceful shutdown to poison message detection
- **Reduced application complexity**: Eliminates the need for
application-level workarounds involving XPENDING polling, arbitrary idle
timeouts, and manual XCLAIM orchestration
- **Dead-letter queue readiness**: FATAL mode + delivery count enables
straightforward poison message detection and future DLQ integration
- **Backward compatibility**: Fully optional new command with zero
breaking changes to existing behavior
2026-04-07 14:17:53 +03:00
Moti CohenandGitHub 153b79a290 keymeta: add DEBUG flag for runtime keymeta class registration (#14968)
M_CreateKeyMetaClass() allows registration only on:
- 'DEBUG enable-module-keymeta-runtime-registration 1' (replaces server.enable_debug_cmd)
- REDISMODULE_CTX_FLAGS_SERVER_STARTUP, in addition to module->onload
2026-04-07 12:31:53 +03:00
Moti CohenandGitHub d22c68f904 Partial support set keymeta on ksn (#15004)
As part of KSN, modules must not modify keys. However, RediSearch
modifies key metadata in some flows, which may invalidate the local
kvobj pointer.

Introduce KSN_INVALIDATE_KVOBJ() to explicitly invalidate kvobj after
notifications, preventing further access by Redis core. Currently
relevant for hash keys without HFE.

Changes:
- Add KSN_INVALIDATE_KVOBJ() to guard unsafe flows
- Apply invalidation beyond hash-specific paths
- Extend KSN side-effect coverage for DELEX and MOVE
- Rearrange flows to avoid kvobj access after notification
- Include additional tests from @JoanFM (#14939)

Behavior:
No intended behavior change and no reordering of notifications.
2026-04-07 12:15:26 +03:00
Hanzo AI 8e65ce37df chore: symlink AGENTS.md + CLAUDE.md → LLM.md 2026-04-06 15:07:39 -07:00
Huihui HuangandGitHub fe3ae0ccac Fix memory leak in clusterManagerFixSlotsCoverage() in src/redis-cli.c (#14863) 2026-04-03 22:34:32 +08:00
Yuan WangandGitHub 5aa6440cbc Disable memory tracking in child processes (#14928)
Disable memory tracking in child processes (forked for RDB operations)
to reduce unnecessary overhead.
2026-04-03 15:10:13 +08:00
Sergei GeorgievandGitHub 8d2b548f84 Fix IDMP tracking not cleared when resetting stream IDMP config (#14955)
## Summary

Unregisters stream keys from `db->stream_idmp_keys` when IDMP
configuration is changed via `XCFGSET`. Previously, changing
`IDMP-DURATION` or `IDMP-MAXSIZE` would clear all IDMP producers but
leave the key registered in the tracking dictionary, causing the cron
job `handleExpiredIdmpEntries()` to needlessly iterate over streams with
no IDMP data.

## Changes

- **`xcfgsetCommand` (src/t_stream.c):** Added
`dictDelete(c->db->stream_idmp_keys, key)` in the `if (changed)` block
to immediately untrack the key when IDMP configuration changes clear all
producers.

## Benefits

- **Immediate cleanup of tracking state:** The stream key is removed
from `stream_idmp_keys` when configuration changes, rather than relying
on the cron to detect and clean up the stale entry on a subsequent pass.
- **Reduced unnecessary cron work:** The cron no longer wastes cycles
inspecting streams that have no IDMP producers.
2026-04-03 09:17:11 +03:00
Gagan DhakreyandGitHub c5287c0b0d Fix memory leak in helloacl module test (#14974) 2026-04-03 11:09:40 +08:00
Lior KoganandGitHub a0bad9a048 Update descriptions in gcra.json (#14969)
Low risk documentation-only change updating `src/commands/gcra.json`
reply field descriptions with no functional or behavioral impact.
2026-04-01 12:58:51 +03:00
debing.sunandGitHub effcb5a03c Fix flaky cluster pubsubshard test in 26-pubsubshard (#14962)
In the "PUBSUB channels/shardchannels" test, we call sunsubscribe
without channels, but the number of loops in
consume_subscribe_messages() is determined by the size of channels.
When channels are empty, the loop will loop 0 times and will not read
the sunsubscribe response message returned by the server.
This means that when verifying the channel length, the previous command
might not have been complete yet, so this PR added a read after
sunsubscribe.
2026-04-01 06:50:58 +08:00
Moti CohenandGitHub 8f3b6990dd keymeta: rename void *value to void *reserved in rdb_save/aof_rewrite callbacks (#14964)
Rename the `void *value` parameter to `void *reserved` in keymeta
`rdb_save` and `aof_rewrite` module callbacks, and pass `NULL` at call
sites.

Originally the `value` parameter was planned to pass the internal kvobj
for core use of key metadata, but since modules cannot use it in any
meaningful way, it should not be exposed. The parameter is kept as a
reserved slot for potential future use.
2026-03-31 15:09:06 +03:00
Mincho PaskalevandGitHub ef536f48fd GCRA param renaming (#14950)
Renames the `GCRA` command interface to use token terminology:
`requests-per-period` becomes `tokens-per-period`, and the optional
`NUM_REQUESTS` argument becomes `TOKENS` (with corresponding error
messages/documentation updates).
2026-03-31 11:57:42 +03:00
debing.sunandGitHub b22ce4abb5 Fix sds unit test and improve unittest output formatting (#14927)
### Summary

1. Fix sdsResize() unit tests
Before this PR, we forgot to call test_report() after the test ended,
resulting in that even if there were failures, we couldn't detect them.
In this PR, the test_report() call has been fixed, and the missed test
failures in sds.c have been resolved.
Since only the allocated size of jemalloc is deterministic, the
allocation size related sds unittests are only run with Jemalloc.

2. Improve the unit test framework output
Add ANSI color highlighting to test_cond (green PASSED / red FAILED) and
test_report, and refactor the test summary in server.c to show per-group
pass/total counts with a formatted summary table.
2026-03-31 16:05:35 +08:00
a6a27f56f2 Test tcp deadlock fixes (#14946)
This fix follows #14667 and #14886

Several tests pipelined large numbers of commands on deferring clients
without draining replies. That can fill buffers and stall progress.

Fix by draining replies every 500 pipelined requests to avoid TCP
stalls.

---------

Co-authored-by: oranagra <oran@redislabs.com>
2026-03-30 19:50:47 +08:00
Antoni DikovandGitHub 5f5ddfd1a1 Fix COMMAND GETKEYS for PFMERGE with no source keys (#14942)
PFMERGE's second key spec (source keys) produces an empty range when
called with only a dest key (e.g. PFMERGE dest). getKeysUsingKeySpecs
treats that as invalid_spec, which discards all previously found keys
and returns an error. Add pfmergeGetKeys as a getkeys callback so the
command correctly falls back to it when key specs fail on the edge case.
2026-03-30 14:07:07 +08:00
Vallabh MahajanandGitHub f2adcdedbc Fix memory leak in lpBatchInsert() (#14866) 2026-03-30 10:05:24 +08:00
Oran AgraandGitHub 3dc246cd53 fixes around recent keyModified changes (#14925)
These areas were modified as part of the recent LRM eviction policy, and
i think they're wrong;
1. DEBUG POPULATE and a few others, have the value at hand, so they can pass it to
keyModified. It's harmless sine the key was just created and LRM is set, but its better for consistency to pass it anyway.
2. RM_SignalModifiedKey now calls lookupKey, but it should use NOEFFECTS
to avoid incrementing stats, key miss notification, and others.
2026-03-29 22:49:16 +03:00
Hanzo Dev 5f45bdf542 Merge remote-tracking branch 'upstream/unstable' into upstream-sync-20260327 2026-03-27 08:11:21 -07:00
GuimuandGitHub 2ba0194fbe Fix memory leak in ZDIFF algorithm 2 on early exit (#14932)
## Problem

`zdiffAlgorithm2()` can break out early once the destination cardinality
reaches zero. In that path, a temporary SDS created by
`zuiSdsFromValue()` is left dirty and never released, because the
cleanup normally happens on the next `zuiNext()` call which is skipped
due to the early `break`.

`zuiClearIterator()` called after the loop does **not** clean up dirty
values — only `zuiNext()` or explicit `zuiDiscardDirtyValue()` does.

## Fix

Add `zuiDiscardDirtyValue(&zval)` before the early `break` to ensure the
temporary SDS is freed on all exit paths.
2026-03-27 22:09:57 +08:00
Zijie ZhaoandGitHub 8e89e0b89f Fix VSIM FILTER memory leaks on duplicate option and error paths (#14898)
`VSIM_RedisCommand` in `vset.c` had two memory leak bugs related to the
compiled FILTER expression (`exprstate` allocated by `exprCompile`):

1. Duplicate FILTER: When two FILTER options are provided in a single
VSIM command, the second `exprCompile` overwrites `filter_expr` without
freeing the first. Only the last one is freed in `VSIM_execute cleanup`.
   Fix: call `exprFree` on the existing `filter_expr` before reassigning.

2. Error path leaks: When FILTER is parsed successfully but a later
option fails validation (invalid COUNT/EF/EPSILON/FILTER-EF or unknown
option), the error return frees `vec` but not `filter_expr`.
   Fix: add `exprFree(filter_expr)` to all five error return paths.
2026-03-27 12:27:24 +08:00
Vitah LinandGitHub f12001e0cc Fix missing ksn_notify_side_effect module test in runtest-moduleapi (#14935) 2026-03-27 10:33:57 +08:00
Luca PalmieriandGitHub f887464840 Upgrade Rust toolchain from 1.93.1 to 1.94.0 2026-03-26 21:26:43 +02:00
Oran AgraandGitHub ef741a95a2 fix handleExpiredIdmpEntries to mark keys as modified (#14933)
refactor code to use streamKeyLoaded and streamKeyRemoved instead of
handling stream internal concerns direction from random places.
2026-03-26 15:39:29 +02:00
40c140bf16 Fix heap-use-after-free in restoreCommand when module sets key metadata during notification (#14929)
### Problem

When a module's keyspace notification callback calls
`RedisModule_SetKeyMeta` to attach metadata for the first time,
`kvobjSet` reallocates the kvobj (to add a metadata slot) and frees the
old one. The local `kv` pointer in `restoreCommand` becomes dangling,
and the subsequent access to `kv->type` reads freed memory.

Related PR: https://github.com/redis/redis/pull/14918

### Changed

Save `kv->type` to a local variable before the notification call.

---------

Co-authored-by: debing.sun <debing.sun@redis.com>
2026-03-26 20:52:11 +08:00
Vitah LinandGitHub bbc0dcbb9a Fix potential TCP deadlock in Active defrag IDMP streams test (#14886)
The IDMP streams defrag test sends all commands (100k) before reading
any replies, which can cause TCP deadlock when buffers fill up.

Fix by batching writes and reads (1000 iterations per batch), consistent
with the approach already used in the script defrag test above.
2026-03-26 14:59:17 +08:00
Joan FontanalsandGitHub 78e21daafe HSETNX notify after accessing KV (#14918)
On HSETNX command handling, the key space notification is sent before
the complete usage of kvobject in stack unlike in the rest of the HSET*
family of command handlers.

The problem is that when a Module writes potentially to Key Metadata,
this KVObject may be reallocated and the usage of this after the
notifciation becomes dangerous and can potentially lead to a crash.

This issue appeared because we started integration Key Metadata support
on a module and observed that when handling HSETNX to update some
metadata, we observed a crash.
2026-03-25 21:19:48 +02:00
1f062ee4a9 Test fix ASM test failures under DEBUG_ASSERT_KEYSPACE builds (#14913)
Under `DEBUG_ASSERT_KEYSPACE`, every command triggers $O(n)$ full key
scans, which causes ASM migrations to stall due to excessive per-command
overhead on the destination node.This PR fixes the issue by skipping
debug assertions during ASM import or background trimming.

---------

Co-authored-by: Ozan Tezcan <ozantezcan@gmail.com>
2026-03-25 16:48:37 +03:00
Vitah LinandGitHub f9b9140f10 Fix test failures under Valgrind caused by unexpected slowlog fields in cmdstat output (#14914)
The recent PR https://github.com/redis/redis/pull/14896 introduced new
slowlog statistics in `INFO` commandstats. This causes `assert_match` to
fail in CI (especially under Valgrind) when commands trigger the
slowlog, adding extra fields after failed_calls.

This PR updates the test patterns to append a trailing `*` to tolerate
these optional fields.
2026-03-25 21:46:52 +08:00
Oran AgraandGitHub 50ed2b4cb5 allow RO CLUSTER commands while loading (#14924)
I noticed that CLUSTER INFO, and CLUSTER NODES fail during loading
(failed some tests in runtest-cluster in ROF which is slow).
the fact is that these commands check the cluster state, and there's no
reason not to allow them during loading (same as we allow them during
STALE, and similarly to INFO).
2026-03-25 11:34:20 +02:00
1abd489d07 Skip memory prefetch during loading to avoid crash in dictEmpty callback (#14848)
Fixes #14838

## Summary

Fix a crash in `prefetchCommands()` that occurs during replica full sync
when the replica has existing data that needs to be emptied.

## Problem Description

During `emptyData()` → `kvstoreEmpty()` → `dictEmpty()` →
`_dictClear()`, the first hash table is cleared and `d->ht_table[0]` is
set to NULL via `_dictReset`. Then while clearing the second hash table,
every 65536 buckets it invokes `replicationEmptyDbCallback()` →
`processEventsWhileBlocked()` → `readQueryFromClient()` →
`prefetchCommands()`.
At this point, `dictSize() > 0` still holds (because the second hash
table isn't fully cleared yet), but `ht_table[0]` is already NULL. The
prefetch code assumed `ht_table[0]` is always valid when `dictSize() >
0`, leading to a crash.

## Solution
1. **Skip prefetch during loading**: Added a `server.loading` check at
the top of `prefetchCommands()` to return early. During RDB loading, the
main dictionary is being rebuilt, so prefetching keys from it is useless
anyway.
2. **Add defensive assertion**: Added
`serverAssert(batch->current_dicts[i]->ht_table[0])` in
`initBatchInfo()` to catch any future cases where `ht_table[0]` is NULL
while `dictSize() > 0` (which should only happen mid-`dictEmpty` via
`_dictReset`).

---------

Co-authored-by: kairosci <kairosci@users.noreply.github.com>
Co-authored-by: debing.sun <debing.sun@redis.com>
Co-authored-by: Yuan Wang <yuan.wang@redis.com>
2026-03-24 10:22:58 +08:00
Sergei GeorgievandGitHub 63d841c3ae Optimize rax insert and lookup for sequential key patterns (#14885)
**Summary**

Optimizes rax tree insert performance by reducing unnecessary reallocs
and shortcutting sorted-array scans in `raxAddChild` and `raxLowWalk`,
targeting the common case of sequential or append-heavy key insertion
(e.g., stream IDs).

**Changes**

- **`raxAddChild` — skip realloc when allocation already fits**: Before
reallocating the node, checks `rax_malloc_usable_size(n) < newlen`.
Jemalloc size-class rounding often leaves enough usable bytes in the
existing allocation to accommodate the extra child byte and pointer,
making the realloc a no-op that can be avoided entirely.
- **`raxAddChild` — fast-path insertion position for append case**: When
the new child character `c` is greater than the last existing child
(`n->data[n->size - 1]`), sets `pos = n->size` directly instead of
scanning the full sorted edge array. This is the hot path for sequential
stream ID insertion where new keys are always lexicographically largest.
- **`raxLowWalk` — last-child-first lookup for non-compressed nodes**:
Checks the last child in the sorted edge array before falling back to
the linear scan. For sequential inserts the match is almost always at
the tail. If the search byte is greater than the last child, breaks
immediately (early miss), avoiding the full O(n) scan.

**Benefits**

- **Fewer reallocs**: Avoids `raxNodeRealloc` calls when the allocator
already provided enough space, reducing allocator pressure and
pointer-update overhead.
- **O(1) insert position for sequential keys**: The append fast-path in
`raxAddChild` turns the child-position search from O(k) to O(1) for the
dominant stream-ID pattern.
- **Faster tree walks**: The last-child-first check in `raxLowWalk` cuts
the per-node scan cost for sequential workloads from O(k) to O(1), and
adds at most one extra comparison for random workloads.
- **No memory overhead**: All optimizations operate on the existing node
layout and sorted-children invariant. No additional fields, flags, or
auxiliary data structures are introduced — memory usage is identical to
before.
- **Zero behavioral change**: All three optimizations are purely
performance shortcuts; no data structure or API semantics are altered.
2026-03-23 16:09:49 +02:00
Mincho PaskalevandGitHub 0f5190f103 Add global and per-command stats for slowlog metrics (#14896)
# What

Add global and per-command slowlog metrics.
`INFO STATS` now shows:
- slowlog_commands_count - total count of commands written to slowlog
(including trimmed ones)
- slowlog_commands_time_ms_sum - sum of execution times of the commands
from the slowlog.
- slowlog_commands_time_ms_max - maximum execution time of a command
from the slowlog (useful for calculating averege values)

`INFO COMMANDSTATS` adds the equivalent 3 metrics, but per command. Only
shown for a command if it was added at least once in the slowlog:
- slowlog_count - how many times the command was written in the slowlog
- slowlog_time_ms_sum - sum of execution time of the command (only from
the slowlog)
- slowlog_time_ms_max - maximum execution time of the command (only from
the slowlog)

# Why

More fine-grained slowlog metrics, easy of alert creation regarding
slowlog.
2026-03-22 18:09:20 +02:00
Moti CohenandGitHub f4d176b3b7 KEYSIZES/ASM: simplify histograms, fix background trim, and refactor debug assertions (#14877)
- Simplify KEYSIZES tracking and saving memory by removing per‑slot histogram state in kvstoreDictMetadata and routing all updates through kvsUpdateHistogram + updateKeysizesHist(db, type, ...) (per‑DB only).
- Fix KEYSIZES consistency during ASM background trim by introducing asmTrimCtx, passing it through emptyDbDataAsync/BIO lazy‑free, computing histogram deltas in the background, and applying them on completion; add bg_trim_running to coordinate with validation.
- Refactor and relax debug assertions into a unified dbg_assert_flags bitmask and dbgRunAssertions(db), and skip KEYSIZES/ALLOCSIZE checks during nested execution, RDB load, ASM import, and ASM background trim.
- Update commands, module APIs, tests (including UNLINK async deletion coverage), and daily CI workflow to reflect the new histogram behavior and re‑enable ASM/cluster tests.
- Revise the daily CI “debug-assert-keyspace” workflow to run ASM and slot-stats unit tests again

Potential edge case with histogram accuracy:
The histogram could become inaccurate if the database is flushed while ASM trim is running in the background. I considered adding a generation counter to detect this, but decided against it since this is purely an INFO/diagnostic feature and the edge case is quite rare. The target_kvstore pointer check prevents applying stale deltas to the wrong kvstore, and if the histogram does become incorrect we have debugServerAssert() to catch negative values - it won't cause crashes in production. This is a known limitation we can document and revisit if it becomes a real issue in practice.
2026-03-22 15:26:06 +02:00
Cong ChenandGitHub 9accf8bd24 Refine error message for HSETEX command with PERSIST (#14880)
Fixes #14879 

> PERSIST can only be given for HGETEX and KEEPTTL for HSETEX but the
code doesn’t verify it.

**Behavior currently:**
 ```
127.0.0.1:5555> HSETEX h ex 100 persist fields 1 k v
(error) ERR Only one of EX, PX, EXAT, PXAT or KEEPTTL arguments can be
specified
```

**With this PR, behavior would be:**

```
127.0.0.1:5555> HSETEX h ex 100 persist fields 1 k v
(error) ERR unknown argument: persist
```
2026-03-20 10:19:00 +08:00
Tom GabsowandGitHub 7d5708180e DataTypes update 8.8 milestone 1 (#14904)
Update data type modules to 8.7.80 (8.8 M01)
2026-03-19 22:11:45 +02:00
stav-nachmiasandGitHub 36a1f02835 Skeleton of gh workflow for published release (#14801)
<!-- CURSOR_SUMMARY -->
> [!NOTE]
> **Low Risk**
> Low risk since it only introduces a new workflow with mostly
placeholder commands and no deployment/credential usage yet; it will run
on `release.published` events in `redis/redis`.
> 
> **Overview**
> Adds a new `Post-Release Automation` GitHub Actions workflow triggered
on `release.published` (guarded to only run on `redis/redis`).
> 
> The workflow extracts the release tag and determines whether it is the
latest release, then stubs out the post-release pipeline (tarball
creation/upload, tarball testing, and updating hashes) with a
conditional *latest-only* approval gate and stable-symlink update
placeholder, plus a run summary and placeholder Slack notification.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
d7c9e373a5d745495370231f0fbc8f651964b36a. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
2026-03-19 18:05:06 +02:00
Sergei GeorgievandGitHub 462e603a1f Fix stream_idmp_keys missing from database lifecycle ops (#14897)
**Summary**

Ensures `db->stream_idmp_keys` is managed consistently with `keys`,
`expires`, and `subexpires` across every database lifecycle operation —
flush, swap, temp-DB init/discard, lazy-free, and cluster slot
migration.

Without this fix, the dict was absent from these code paths, causing
three classes of bugs: a SIGSEGV during diskless replication in `swapdb`
mode (NULL pointer dereference in `initTempDb`), silently lost IDMP
tracking after `SWAPDB` and diskless replication (pointers never
swapped), and stale IDMP entries surviving `FLUSHDB` (dict never
cleared).

**Changes**

- **`emptyDbStructure`** (`src/db.c`) — Clear `stream_idmp_keys` on
flush so stale entries don't persist across `FLUSHDB`/`FLUSHALL`.
- **`initTempDb` / `discardTempDb`** (`src/db.c`) — Create and release
`stream_idmp_keys` for temp databases used during diskless replication
RDB load. Fixes the SIGSEGV when `rdbLoadRioWithLoadingCtx` calls
`dictAddRaw` on a NULL pointer.
- **`dbSwapDatabases`** (`src/db.c`) — Swap `stream_idmp_keys` alongside
the other per-DB dicts so IDMP tracking follows the data during
`SWAPDB`.
- **`swapMainDbWithTempDb`** (`src/db.c`) — Swap `stream_idmp_keys` when
promoting a temp database after diskless replication, so the cron can
discover and expire IDMP entries on replicas.
- **`streamMoveIdmpKeys`** (`src/db.c`) — New helper that migrates IDMP
entries by slot from one dict to another, used during cluster slot
migration.
- **`emptyDbAsync` / `emptyDbDataAsync`** (`src/lazyfree.c`) — Replace
the dict with a fresh one and hand the old one to the background
lazy-free job.
- **`lazyfreeFreeDatabase`** (`src/lazyfree.c`) — Free
`stream_idmp_keys` in the background job (now takes 4 args instead of
3).
- **`asmTriggerBackgroundTrim`** (`src/cluster_asm.c`) — Move matching
IDMP entries by slot into a temporary dict during background trim, then
pass it to `emptyDbDataAsync` for async cleanup.
- **`server.h`** — Updated `emptyDbDataAsync` signature; added
`streamMoveIdmpKeys` declaration.
- **Tests** (`tests/unit/type/stream.tcl`) — Four new integration tests
covering IDMP expiry after `SAVE` + restart, tracking survival across
`SWAPDB`, cleanup on `FLUSHDB`, and diskless replication in `swapdb`
mode (both `rdbchannel=yes` and `rdbchannel=no`).

**What this fixes**

- **No crash on diskless replication** — `initTempDb` now initializes
the dict, eliminating the NULL dereference during RDB load.
- **Tracking preserved across swaps** — Both `SWAPDB` and diskless
replication correctly transfer the dict, so the cron keeps expiring
entries in the right database.
- **Clean state after flush** — `FLUSHDB`/`FLUSHALL` clear the dict,
preventing ghost entries from interfering with new streams.
- **Correct cluster migration cleanup** — IDMP entries for migrated
slots are moved and freed alongside the key data.
2026-03-19 14:37:19 +02:00
Cong ChenandGitHub 1b615c774d Fix FIELDS argument validation in HSETEX/HGETEX (#14883)
Fixes #14879 

Improve validation of the FIELDS argument in HSETEX and HGETEX to ensure
exactly one field is provided, rejecting both missing and multiple
fields with consistent and accurate error messages.

Align behavior across both commands.
2026-03-19 19:54:22 +08:00
judengandGitHub 8a6ae0e4dd Refactor asmTask: remove source_node, use clusterLookupNode (#14766)
Description
This PR refactors the Async Slot Migration (ASM) implementation to
improve the encapsulation and isolation between the ASM module and the
core Cluster module.

Motivation
Previously, the `asmTask` structure maintained a long-lived raw pointer
(clusterNode *source_node) to the source node of a migration. This
introduced a tight coupling between the ASM and cluster impl, that has
caused some trouble for my attempt to replace the cluster impl.
2026-03-19 17:04:35 +08:00
Cong ChenandGitHub bd1b241faa Add ARM64 jobs to daily workflow (#14891) 2026-03-19 16:27:09 +08:00
Zijie ZhaoandGitHub c4d74587b5 Fix ACL OOB for wrong-arity KEYNUM commands (#14847)
`luaRedisAclCheckCmdPermissionsCommand` and
`RM_ACLCheckCommandPermissions` now call `commandCheckArity()` to check
command arity before calling `ACLCheckAllUserCommandPerm`, matching the
behavior of `processCommand`, `scriptCall`, and `RM_Call`. Without this,
KEYNUM keyspec commands like EVAL with wrong arity cause out-of-bounds
argv access during key extraction.

Also fix KEYNUM index calculation (`first + keynumidx`) and add a bounds
check in genericGetKeys().

Add scripting and module ACL tests for wrong-arity `EVAL` to lock in the
non-crashing behavior.

Fixes #14843
2026-03-19 09:30:56 +08:00
4ecc07fcff Fix divide-by-zero in redis-benchmark and redis-cli (#14371)
Resolve https://github.com/redis/redis/issues/14358 and
https://github.com/redis/redis/issues/14359. Fix division by zero in
redis-benchmark and redis-cli when histograms are empty or elapsed time
is zero. Guard all affected divisions in showLatencyReport(),
showThroughput(), and displayKeyStatsSizeDist(). Added integration tests
for both cases.

---------

Co-authored-by: debing.sun <debing.sun@redis.com>
2026-03-18 19:57:58 +08:00
31a4356ac0 GCRA Rate Limiter (#14826)
# What

Implement rate limiting functionality via GCRA algorithm. 

Introduce a new command `GCRA` to facilitate it.

The implementation is heavily based on the popular
[redis-cell](https://github.com/brandur/redis-cell) module (by
[brandur](https://github.com/brandur)) with small changes in the API.

# Why

Rate limiting is a very common use case of redis and GCRA is one of the
most popular algorithms used because of its simplicity and speed.
Currently rate limiting with GCRA is possible via lua scripts or even
client libraries via the relatively recent `SET IFEQ`/`DIGEST` commands
([redis-py
example](https://gist.github.com/minchopaskal/b7acd4550f7144b88e2d0f86568a0d7b)).
Implementing it directly inside redis gives us even faster performance.

# API

```
GCRA key max_burst requests_per_period period [NUM_REQUESTS count]
```

## Description

Rate limit via GCRA. `requests_per_period` are allowed per `period` at a
sustained rate. Thus we have a minimum spacing(emission interval) of
`period`/`requests_per_period` seconds between each request. `max_burst`
allows for occasional spikes by granting up to `max_burst` additional
requests to be consumed at once. See more in the [GCRA
wiki](https://en.wikipedia.org/wiki/Generic_cell_rate_algorithm).

## Options

**KEY** - key related to specific rate limiting case
**MAX_BURST** - maximum number of tokens allowed as a burst (in addition
to the sustained rate). Min: 0
**REQUESTS_PER_PERIOD** - number of requests allowed per PERIOD. Min: 1
**PERIOD** - period in seconds as floating point number used for
calculating the sustained rate. Min: 1.0
**NUM_REQUESTS** - cost (or weight) of this rate-limiting request. A
higher cost drains the allowance faster. Default: 1

### Note

In redis-cell module and most other modules that are based on it PERIOD
is given in seconds as integer. We decided to use floating point for
greater flexibility. Internally time periods are calculated in
microsecond granularity.

## Reply

Reply is identical to reply of redis-cell 

```
127.0.0.1:6379> GCRA <key> <max_burst> <requests_per_period> <period> NUM_REQUESTS <count>
1) <limited> # 0 or 1
2) <max-req-num> # max number of request. Always equal to max_burst+1
3) <num-avail-req> # number of requests available immediately
4) <reply-after> # number of seconds after which caller should retry. Always returns -1 if request isn't limited.
5) <full-burst-after> # number of seconds after which a full burst will be allowed
```

---------

Co-authored-by: debing.sun <debing.sun@redis.com>
2026-03-18 13:28:05 +02:00
Tal Bar YakarandGitHub 44157ee35e Add RM_GetContextUser and RM_GetUserUserName module APIs (#14890)
Add RM_GetContextUser to retrieve the RedisModuleUser set via
RM_SetContextUser, allowing modules to access the user associated
with RM_Call ACL checks.
in addition, add new api to get the user name from RM_RedisModuleUser
2026-03-17 20:28:13 +08:00
Yuan WangandGitHub 220a577d3c Use flush db if sflush all owned slots (#14887)
SFLUSH now detects when the requested slot ranges exactly match the
node’s local slot coverage and, in that case, skips slot trimming and
instead performs a full DB flush via flushCommandCommon (while still
replying with the flushed slot ranges).
2026-03-17 09:33:37 +08:00
Guy KorlandandGitHub 7e866b47e3 Fix safety check in lpSafeToAdd size_t Addition Overflow Bypass (#14888)
Fixes an integer-overflow bypass in `lpSafeToAdd` by replacing the
`len + add` limit check with an overflow-safe comparison (`add > max` or
`len > max - add`). This ensures listpack growth is consistently
rejected when the requested addition would exceed the 1GB safety cap,
even when `add` is near `SIZE_MAX`.
2026-03-16 19:51:37 +08:00
Sergei GeorgievandGitHub 8b9fd4b752 Filter expired IDMP entries during RDB save and load (#14862)
**Summary**

Filters expired IDMP entries during RDB save and load, reducing RDB file
size and memory usage on restart by not persisting or loading entries
that have already exceeded their `idmp_duration`.

**Changes**

- **`rdbSaveStreamIdmpEntries`**: Added single-pass expiration filtering
that skips expired entries when writing. Leverages the timestamp-ordered
linked list to find the first valid entry, then counts and writes only
non-expired entries. Producers with all entries expired are written with
`count=0`.
- **`rdbLoadStreamIdmpEntries`**: Added expiration filtering that skips
individual entries whose `id.ms <= expire_time`. Producers loaded with
`count=0` are skipped entirely. Producers that become empty after
entry-level filtering are removed from the rax tree. If no producers
remain, the rax tree is freed.

**Benefits**

- **Smaller RDB files**: Expired entries are no longer serialized,
reducing file size proportional to the number of stale IDMP entries.
- **Faster restarts**: Loading skips expired entries and empty
producers, avoiding unnecessary allocations that would be immediately
cleaned up by the cron.
- **Defense in depth**: Filtering on both save *and* load handles the
case where an RDB was written by an older version (without save
filtering) and loaded by a newer version.
2026-03-16 09:04:28 +02:00
Zijie ZhaoandGitHub 4f0d3118e1 Fix listpack memory leak in zipmap-to-hash conversion on error path (#14878)
When loading RDB_TYPE_HASH_ZIPMAP objects, a listpack is allocated for
the zipmap-to-listpack conversion. If the conversion loop encounters a
duplicate key, allocation failure, or oversized listpack, the error
handler frees the dict, field, encoded buffer, and object — but not the
listpack. Add the missing lpFree(lp) call and a regression test that
RESTOREs a zipmap with duplicate keys to exercise this path.
2026-03-16 14:12:03 +08:00
Varun ChawlaandGitHub a441d3db44 Fix rss_extra_bytes type from size_t to ssize_t in redisMemOverhead (#14793)
The `rss_extra_bytes` field in `struct redisMemOverhead` was declared as
`size_t` (unsigned), while all other similar fields (`total_frag_bytes`,
`allocator_frag_bytes`, `allocator_rss_bytes`) use `ssize_t` (signed).

This causes two issues when `process_rss < allocator_resident` (which
happens in practice, visible when `rss_overhead_ratio < 1.0`):

1. The unsigned subtraction wraps to a very large value, making the
`MEMORY DOCTOR` check `mh->rss_extra_bytes > 10<<20` a potential false
positive (though currently masked by the ratio check).

2. The `INFO memory` output uses `%zd` (signed) format for this field,
which is technically undefined behavior with a `size_t` argument. It
happens to print correctly on most platforms due to two's complement
representation, but the mismatch is still a latent bug.

The fix changes `size_t` to `ssize_t` to match the other `*_bytes`
fields in the same struct. No behavioral change for the common case
where `process_rss >= allocator_resident`.
2026-03-15 19:34:35 +08:00
Sergei GeorgievandGitHub ee376cdc3c Fix IDMP cron expiration not working after RDB load (#14869)
**Summary**

Registers streams with IDMP producers in `db->stream_idmp_keys` during
RDB load, so that the periodic cron job can expire stale IDMP entries
after a server restart. Without this fix, streams loaded from RDB were
never registered, causing IDMP entries to accumulate indefinitely and
never be cleaned up by the cron.

**Changes**

- **`rdbLoadRioWithLoadingCtx` (src/rdb.c):** After loading each
key-value pair, added a check for `OBJ_STREAM` type with a non-NULL
`idmp_producers` rax. When found, creates a string object for the key
and inserts it into `db->stream_idmp_keys` via `dictAddRaw`. If the key
already exists (duplicate insert), the reference is decremented to avoid
a leak.
- **Test `XADD IDMP cron expiration works after RDB load`
(tests/unit/type/stream.tcl):** Added an integration test that creates a
stream with IDMP producers, sets a short `IDMP-DURATION`, saves and
restarts the server, then verifies that the cron eventually expires the
entries (pids-tracked and iids-tracked drop to 0) and that
previously-expired IIDs can be re-added as new entries.

**Benefits**

- **Cron expiration works after restart:** Streams with IDMP producers
are now discoverable by the periodic cleanup cron after an RDB load,
ensuring expired entries are reaped as expected.
- **No memory leak on stale entries:** Without registration, IDMP
entries that outlived their duration would persist in memory forever
after a restart, growing unboundedly. This fix ensures they are cleaned
up.
- **Consistency between runtime and post-load behavior:** IDMP
expiration now behaves identically whether the stream was created at
runtime or restored from an RDB snapshot.
2026-03-13 14:00:28 +02:00
Yuan WangandGitHub 708bfd5de5 No checksum performed on diskless replication (#14851)
In #14575, we disable rdb compression when using diskless replication.
Actually rdb checksum also costs much CPU, and it is used to check RDB
disk file instead of replication stream, so when using diskless
replication (`diskless sync` is enabled on master, `diskless load` is
enabled on replica), we don't store/load any rdb file into/from disk, so
we can skip the checksum in this scenario to accelerate rdb delivery.

People may argue the checksum for replication stream can recognize the
network data corruption, but actually we can not recognize the network
data corruption issue for incremental replication-stream/commands, so it
is limited, we should use `tls` feature to ensure the data correctness.

I tested on AWS, ec2 type is c8g.2xlarge, data size is 12.37GB, all keys
are string type, keys size is 1024.
- If master and replica are different ec2 instances but in the same
placement group, the full synchronization time reduced from 16s to 11s.
- If the master and replica are in the same ec2 instance, the full
synchronization time reduced from 15s to 10s.
- If master and replica are different ec2 instances but not in the same
placement group, there is no change.

It seems network latency is the bottleneck now.

And after #14575 and this PR, the full synchronization time reduced from
35s to 11s when master and replica instances are in the same placement
group. A decrease of nearly 68.5%!
2026-03-12 17:10:43 +08:00
Hanzo Dev 28bebc2a51 docs: add LLM.md project guide 2026-03-11 10:32:58 -07:00
Martin DimitrovandGitHub 3f588a31af Optimize BITOP operations with AVX512 (#14770)
The AVX512 optimizations help only for large values (e.g. 10K bytes -
based on my testing). Thus, the AVX512 implementation is only utilized
for larger values. For smaller values, it may cause performance
degradation. For some large values (e.g. 10MB) it could help a lot - up
to 80% in my experiments.

Performance results attached. The results were conducted on an IceLake
server. The redis process and the memtier benchmark were pinned to
separate CPUs for more consistent results.


[avx2_vs_avx512_8keysmin.txt](https://github.com/user-attachments/files/25111126/avx2_vs_avx512_8keysmin.txt)

The following script was used for benchmarking:
https://github.com/slice4e/bench_script
This is very similar to the original script used to evaluate the bitop
commands: https://github.com/minchopaskal/bench_script
2026-03-11 16:17:51 +02:00
udi-speedbandGitHub fb5230dd3c RED-188967: Port active clients stats to OSS (#14841)
## Overview
Add monitoring metrics that provide visibility into client activity and
command pipeline behavior. These include a real-time count of active
clients, per-client read and pipeline statistics, and server-level
counters for input buffer processing and pipeline depth.

## Changes
### Active Clients Sliding Window (`server.c`, `server.h`)
A circular buffer of 4 slots (128ms each, 512ms total window) tracks
unique clients with recent read activity. Each client is counted at most
once via timestamp-based deduplication. `serverCron()` calls the update
function periodically to clear stale slots during idle periods.
- New INFO clients field: `active_clients`
- New client struct field: `last_ts_when_counted_as_active`

Sliding-window implementation details:
- The code clears all skipped slots when window advancement jumps more
than one slot.
- Old-slot decrement checks use slot-aligned timestamp comparison, so
the code does not decrement slots that were already cleared after
wraparound.

Threading behavior:
- `statsUpdateActiveClients(c)` runs only when the client is running on
the main thread (in `processInputBuffer`), avoiding unsynchronized
updates of the static window state from IO threads.

### Per-Client Statistics (`networking.c`, `server.h`)
Three new fields in CLIENT LIST / CLIENT INFO output:
- `read-events` — number of `readQueryFromClient()` calls for this
client
- `avg-pipeline-len-sum` — cumulative sum of commands parsed per batch
- `avg-pipeline-len-cnt` — number of parse batches

Average pipeline length = sum / cnt. For non-pipelined clients the
average is 1.0; for pipelined clients it reflects the effective batching
depth (capped by the `lookahead` config).

### Server-Level Statistics (`server.c`, `networking.c`, `server.h`)
New INFO stats fields:
- `total_client_processing_events` — total `processInputBuffer()` calls
- `eventloop_cycles_with_clients_processing` — event loop cycles where
client input buffers were processed (uses before/after-sleep snapshot)
- `avg_pipeline_length_sum`, `avg_pipeline_length_cnt`,
`avg_pipeline_length` — global pipeline depth aggregates

IO-thread behavior for server-level counters:
- The shared global counters updated from `processInputBuffer()` are
stored as atomic server fields and updated with Redis atomic helpers.
- Snapshot/reset/read paths (`beforeSleep`, `afterSleep`,
`resetServerStats`, INFO generation) use atomic loads/stores
consistently.

All server-level counters are reset by `CONFIG RESETSTAT`. Per-client
counters are connection-lifetime.
2026-03-11 22:00:18 +08:00
h.o.t. neglectedandGitHub 753c9f616f Fix X509 memory leak in connTLSGetPeerCert (#14855)
`connTLSGetPeerCert()` calls `SSL_get_peer_certificate()` which
increments the X509 reference count, but never calls `X509_free()` to
release it. This leaks an X509 object on each call to
`RM_GetClientCertificate()`. This PR adds `X509_free(cert)` before
returns.

The existing `test module getclientcert api` test in
`tests/unit/moduleapi/misc.tcl` exercises this code path. But the leak
was not caught because the ASAN CI job does not enable TLS.
2026-03-10 15:35:44 +08:00
Vitah LinandGitHub 1ba5799f7b Fix missing keymeta module test in runtest-moduleapi (#14860) 2026-03-10 10:59:39 +08:00
Oran AgraandGitHub 20f163eb3d XADD, update keyModified and dirty counter when increasing iids_duplicates (#14858)
we're saving iids_duplicates to the rdb file, so when it changes, we
need to increment the dirty counter, and also mark the key as modified
(being metadata, no signal is needed)
2026-03-09 09:53:37 +02:00
udi-speedbandGitHub e86882efe9 RED-184929: Auto-backups and restore test configuration (#14753)
# Automatic Test Configuration Restoration

This PR introduces an infrastructure improvement in the TCL testing
framework.

## Problem Statement

When running multiple tests under the same server, or in external server
mode, we always have the issue of potential leak of configuration
parameters set by one test polluting another.

This has caused us many problems in the past, and it's a nuisance having
to fix these when they occur. Alternatively, handling the backup and
restoration manually and explicitly when writing the test adds "noise"
that makes tests longer (lines of code wise) than necessary.

Note also that even if there is explicit configuration restoration code
at the end of a test, that code will not run in case the test fails and
exits before reaching the restoration code.

## Objective

Every test should be completely isolated (unless explicitly designed to
depend on previous tests): It should set up its environment from
scratch, and clean up after itself in case it might affect subsequent
tests. Preferably, this should happen automatically on behalf of the
test writer.

The purpose is to have a mechanism at the level of the individual test
that will perform automatic restoration of the configuration to what it
was before the test started.

This improvement should be an opt-in option. It shouldn't change
existing tests (there are existing cases in which there is a sequence of
tests, wherein the latter tests depend on changes made by the former
ones). It should allow explicit update of existing tests to use the new
mechanism, but this will occur only when explicitly triggered.

## The Solution

This PR introduces such a mechanism.

To trigger automatic restoration of changed configuration, a new
`config:restore` tag should be attached to the test. Once the tag is
attached, the configuration will be automatically restored to its state
at the beginning of the test.

### Usage Example

```tcl
test "Modify maxmemory temporarily" {
    r config set maxmemory 100000000
    # ... test that needs specific maxmemory ...
} {} {config:restore}
# maxmemory automatically restored to original value
```

## Covered Scenarios

- Basic Single-Server Tests
- Cluster Scenarios
- Multi-Server (Nested `start_server`) Scenarios
- Failure Handling (tests failing due to various reasons)

## Implementation Details

### Infrastructure Changes

**`save_server_configs`** (test.tcl) - Captures configuration state for
all servers in the `::servers` stack:
- Iterates through all active servers
- Executes `CONFIG GET *` on each server
- Returns a list of `[server_index, config_dict]` pairs

**`restore_server_configs`** (test.tcl) - Restores configurations using
diff-based detection:
- Compares current config to saved config
- Only restores parameters that actually changed (optimization)
- Checks server responsiveness before attempting restoration

**`test` procedure changes** (test.tcl):
- Detects `config:restore` tag in the tags list
- Calls `save_server_configs` before test execution
- Calls `restore_server_configs` after test completion (success path)
and before re-raising errors (failure path)

**`ping_server_with_timeout`** (server.tcl) - Non-blocking server
responsiveness check:
- Prevents restoration from hanging on unresponsive servers
2026-03-09 15:44:32 +08:00
Cong ChenandGitHub b3ce4c28ca Fix test assertion except from TSAN case (#14852)
Fix #14835

Executed `failover` with `force` argument in slow environments (TSAN, IO
threads, etc) can force a full resync instead of a partial one.
e3c38aab6 adds TSAN-only workaround in integration/failover test, test
failure occurs in `test‑ubuntu‑io‑threads` job, drop TSAN-only condition
and always check the sum of partial+full syncs will fix it.

Steps to reproduce:
My setup: Hardware: MacBook Pro (Apple chip)

1. Build docker image with Dockerfile below and run the container:
```
FROM ubuntu:22.04

# avoid interactive prompts
ENV DEBIAN_FRONTEND=noninteractive

RUN apt-get update && \
    apt-get install -y --no-install-recommends \
    build-essential \
    tcl8.6 \
    tclx \
    tcl-tls \
    pkg-config \
    ca-certificates \
    git \
    bash \
    vim \
    wget \
    curl \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /opt/redis

# if you prefer to copy the source during build uncomment the next line
# COPY . /opt/redis

# configure a non-root user if desired; we stay root for simplicity

# default command keeps you in a shell
CMD ["/bin/bash"]

```

Execute the command:

```
cd /path/to/redis
docker build -f Dockerfile.io-threads -t redis-test-io .
docker run --rm -it --cpuset-cpus=0 --cpus=0.1 -v $(pwd):/opt/redis redis-test-io /bin/bash
```
2. Inside the container you can then run the same commands as the CI
job:
```
make REDIS_CFLAGS='-Werror'
./runtest --single integration/failover --config io-threads 4 --tags "failover" --verbose --accurate --loop --stop
```
If the container still seems too fast, lower `--cpus` further (e.g.
0.05), or run the whole driver under `cpulimit`:
```
apt-get update && apt-get install -y cpulimit
cpulimit -l 1 -- ./runtest --single integration/failover --config io-threads 4 --tags "failover" --verbose --accurate --loop --stop
```
2026-03-06 13:11:38 +02:00
Vitah LinandGitHub 62059a2438 Chore complete Tcl 9 support and fix regressions in test suite (#14845)
## Problem

PR https://github.com/redis/redis/pull/14787 introduced **Tcl 9 support
for the test suite**, but it still fails on my machine (**macOS 26.3,
Tcl 9.0.3**). Some tests fail and the runner may hang.

Example:

```bash
$ tclsh <<<'puts $tcl_version'
9.0.3

$ make test
[err]: BITCOUNT against test vector #2
Expected [r bitcount str] == 4
```

This is caused by **behavior changes in Tcl 9**, including:

- `string length` returning **character count** instead of **byte
count**
- binary sockets rejecting characters with code points **>255**
- differences in `string is wideinteger`

Parts of the Redis Tcl test framework rely on **byte-oriented
behavior**, which breaks under Tcl 9.



## Changes

### 1. Fix IPC payload encoding in test runner

`tests/unit/memefficiency.tcl` contains a **non-ASCII quote character**:


https://github.com/redis/redis/blob/fe16003e667407973296ae11b039baa2f9d088c2/tests/unit/memefficiency.tcl#L826

Under Tcl 9 this can corrupt the IPC stream when the test runner
serializes Tcl code blocks, causing the runner to hang.

Instead of fixing only this character, the IPC payload is now explicitly
encoded using:

```tcl
encoding convertto utf-8
```

This makes the protocol robust against future non-ASCII characters.



### 2. Avoid Tcl 9 glob backtracking issue

Replaced:

```tcl
string match "*[^\u0000-\u00ff]*" $a
```

with:

```tcl
regexp {[^\u0000-\u00ff]} $a
```

This avoids a **catastrophic backtracking issue in Tcl 9's glob
matcher** while preserving the same behavior.


### 3. Update DIGEST validation

The previous check relied on:`string is wideinteger` which behaves
differently in Tcl 9.

The assertion now validates the **expected DIGEST format directly**.
2026-03-06 13:27:56 +08:00
h.o.t. neglectedandGitHub fe16003e66 Fix potential sds buffer leak in RM_SaveDataTypeToString (#14831)
Fixes memory leak in `RM_SaveDataTypeToString()` when `io.error` is set.
The sds buffer from `rioInitWithBuffer(&payload,sdsempty())` was not
freed on the error path.

Note: this is a defensive fix. This bug is unlikely to trigger
currently. With buffer-based rio, writes cannot fail through normal
error propagation - the error path only occurs if a module's `rdb_save`
callback explicitly sets `io.error`. However, future changes that add
error checking to `rioBufferWrite()` or introduce new failure paths in
buffer-based rio could trigger this leak.
2026-03-05 11:29:37 +08:00
Hanzo AI b7aa8b5219 chore: remove stale LLM.md 2026-03-03 14:00:00 -08:00
Moti CohenandGitHub 8a65b65d63 Fix setModuleEnumConfig() to pass unprefixed name to callbacks (#14816)
`setModuleEnumConfig()` was passing the prefixed config name to module
callbacks instead of the unprefixed name, inconsistent with other
config types. Fixed by using getRegisteredConfigName() like bool,
numeric, and string configs do.

Added assertions to all test module config callbacks to validate
correct unprefixed names are received.

Issue was introduced by #13656
2026-03-03 13:56:58 +02:00
Vitah LinandGitHub 6088387b6f Capture and report server crash after RESTORE in corrupt-dump-fuzzer test (#14836)
## Problem

In `corrupt-dump-fuzzer.tcl`, after a successful `RESTORE` with a
corrupted payload, the test did a bare `r ping` to check whether the
server was still alive:

If the server crashed at this point, the uncaught exception would
**silently terminate the entire test** without printing the payload that
caused the crash, making the issue extremely hard to reproduce and
debug.

## Fix

Wrap `r ping` with catch to ensure failures trigger the same reporting
and restart logic as `RESTORE`. This ensures offending payloads are
logged for reproduction.
2026-03-03 09:33:52 +08:00
Moti CohenandGitHub 67187687b1 Rename test module from test_metakey to test_keymeta (#14834) 2026-03-01 13:09:23 +02:00
debing.sunandGitHub 3de7fa257f Don't use reply copy avoidance for module to prevent potential UAF (#14824)
This PR disables reply copy avoidance for module strings.
The module string's lifecycle is not controlled by Redis, it may belong
to a datatype element that gets lazy-freed by BIO thread. If copy
avoidance holds a reference to it in the reply list, this causes UAF
when IO thread accesses the freed robj.
2026-02-28 14:24:51 +08:00
Alessio AttilioandGitHub 707757e478 Support Tcl 9.0 in Redis test suite (#14787)
## Summary
This PR adds support for running the Redis test suite using Tcl 9.0.

## Changes
- **runtest**: Added `9.0` to the list of Tcl versions the script
searches for.
- **Version Requirements**: Updated `package require Tcl` from `8.5` to
`8.5-10` in key test files. In Tcl, a simple version requirement like
`8.5` is interpreted as "8.5 or higher within major version 8".
Specifying the range `8.5-10` allows Tcl 9.0 to be used if the code is
compatible.
- **Tcl Precision**: Wrapped `set tcl_precision 17` in a conditional
check `if {$tcl_version < 9.0}`. In Tcl 9.0, `tcl_precision` has been
removed as double-to-string conversions are now lossless by default.
- Adjusts the Tcl Redis client to preserve binary arguments under Tcl 9
by only UTF-8 converting strings that contain non-byte characters before
building the RESP command.

## Testing
Verified that `tclsh9.0` successfully parses the updated `package
require` and handles the conditional `tcl_precision` assignment. This
allows the test suite to run on modern Linux distributions where Tcl 9.0
is the default version.
2026-02-28 14:07:25 +08:00
Vitah LinandGitHub 33319b80a7 Fix compilation warning: Use macro for seq_buffer_max_length (#14815)
## Description

This PR fixes a compilation warning in `deps/linenoise/linenoise.c` by
converting the local constant variable `seq_buffer_max_length` to an
enum constant `SEQ_BUFFER_MAX_LENGTH`.

### Changes Made
- Replaced the local `const int seq_buffer_max_length = 8;` declaration
with a macro constant.
2026-02-27 09:31:10 +08:00
Vitah LinandGitHub 50f1469961 Fix memory leak in trackingRememberKeys() for PUBSUB commands (#14817)
Fixes a memory leak in `trackingRememberKeys()` by returning early for
`CMD_PUBSUB` commands *before* calling `getKeysFromCommand()`, avoiding
an allocated `getKeysResult` that previously wasn’t freed on the PUBSUB
fast path.
2026-02-26 20:20:40 +08:00
Mincho PaskalevandGitHub 9152df61da Add HOTKEYS HELP command coverage (#14814)
Add coverage for HOTKEYS HELP command. Now reply schema test in Daily CI
does not fail.
2026-02-25 16:02:35 +02:00
Yuan WangandGitHub dd81afa2c8 Do not add a key into db if it has a past expiration time (#14784)
For **RESTORE** and **SET** command, if the expiration time is already
elapsed, we skip adding it to the DB. However, we still increment the
`expiredkeys` counter. From stats perspective we behave as if we
inserted a new key (possibly an overwrite) and later expired it, but
from the per-key KSN observability, we reflect what we've actually done
in the db (deletion of old key, and no insertion of new one), so we
don't confuse modules.

**Changes**:
- **RESTORE**: Increment the `expiredkeys` counter if the key has an
expiration time in the past.
- **SET**: If the key has an expiration time in the past, do not add it
to the main DB, increment the `expiredkeys` counter instead. And we also
delete the old key if it exists.

For reference, **EXPIREAT** with TTL in the past, which implicitly
deletes the key and return success. Now **SET** command has the same
behavior.
2026-02-25 20:44:13 +08:00
Ali-Akber SaifeeandGitHub b6ea0dd2a3 Fix XADD/XCFGSET IDMP argument types (#14788)
# Description 
Recently added parameters in `XADD` and the new `XCFGSET` command use
pure tokens for arguments
when they should be regular tokens as they have associated values. 

## XADD
- `IDMPAUTO` should not be a block argument and instead just a `string`
argument as it only contains one value
- `IDMP` should be a block argument with two arguments (not including
the token itself)

## XCFGSET
- `IDMP-DURATION` should not be a block argument and instead an integer
argument
- `IDMP-MAXSIZE` should not be a block argument and instead an integer
argument


# Side effect
- Added `since` attributed for `IDMP` block in `XADD`

# Related PR:
- #14615
2026-02-25 11:00:09 +02:00
Ozan TezcanandGitHub c6f190a7ab Improve error log for ASM slot handoff failure (#14791)
This PR improves the wording of a specific error message in an ASM
scenario. It also includes minor comment fixes.
2026-02-25 11:54:29 +03:00
280dab0b8a Fix implicit conversion warning in timeout.c for newer Clang versions (#14811)
## Problem

Build fails on macOS with Xcode 26.4 beta (Clang 21):

```
timeout.c:157:21: error: implicit conversion from 'long long' to 'long double' changes value from 9223372036854775807 to 9223372036854775808 [-Werror,-Wimplicit-const-int-float-conversion]
  157 |         if (ftval > LLONG_MAX) {
      |                   ~ ^~~~~~~~~
/Applications/Xcode_26.4_beta.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/21/include/limits.h:109:20: note: expanded from macro 'LLONG_MAX'
  109 | #define LLONG_MAX  __LONG_LONG_MAX__
      |                    ^~~~~~~~~~~~~~~~~
<built-in>:64:27: note: expanded from macro '__LONG_LONG_MAX__'
   64 | #define __LONG_LONG_MAX__ 9223372036854775807LL
      |                           ^~~~~~~~~~~~~~~~~~~~~
1 error generated.
make[1]: *** [timeout.o] Error 1
make: *** [all] Error 2
```
## Cause

Comparing `long double` with `LLONG_MAX` causes implicit conversion with
precision loss. Clang 21 has stricter warnings for this, and `-Werror`
treats it as an error.

## Fix

Add explicit cast to make the conversion intentional.

---------

Co-authored-by: debing.sun <debing.sun@redis.com>
2026-02-25 10:03:17 +08:00
Sergei GeorgievandGitHub 1f7bcecc94 Add XIDMPRECORD command and AOFRW emission to restore stream IDMP state (#14794)
## Summary

- Introduce a new XIDMPRECORD <key> <pid> <iid> <streamID> command that
attaches IDMP (idempotency) metadata — a producer ID and an idempotency
ID — to an existing stream message. This is an internal command used to
replay IDMP state during AOF loading.
- Modify rewriteStreamObject() in AOF rewrite (AOFRW) to emit
XIDMPRECORD commands after the XADD and consumer-group commands,
ensuring IDMP deduplication state is fully preserved across AOF rewrites
and server restarts.
- Additionally emit XCFGSET during AOFRW when per-stream IDMP
configuration (IDMP-DURATION, IDMP-MAXSIZE) differs from server
defaults, so custom settings survive rewrites.

## New command: XIDMPRECORD
Syntax: `XIDMPRECORD <key> <pid> <iid> <streamID>`

- Validates that the key exists, is a stream, the stream ID refers to an
existing (non-deleted) entry, and both pid and iid are non-empty.
- If the (pid, iid) pair already maps to the same stream ID, the command
is idempotent and returns OK.
- If the (pid, iid) pair already maps to a different stream ID, an error
is returned.

## AOF rewrite changes (src/aof.c)

- New helper rioWriteStreamIdmpEntry() writes a single XIDMPRECORD bulk
command to the AOF rio.
- After consumer groups are emitted, iterates all producers in
s->idmp_producers and emits an XIDMPRECORD for every IDMP entry (in
linked-list insertion order).
- Emits XCFGSET for per-stream IDMP config when it diverges from server
defaults.
2026-02-24 11:51:48 +02:00
Varun ChawlaandGitHub 53e4631e79 Initialize strOldSize in bitfieldGeneric to avoid undefined behavior (#14790)
In `bitfieldGeneric()`, `strOldSize` is declared but only `strGrowSize`
gets initialized to 0.

This just initializes `strOldSize` to 0 alongside `strGrowSize` for
safety and clarity.

Fixes #14315
2026-02-24 13:04:14 +08:00
TheBitBrineandGitHub c7c278c6c1 Allow comments in ACL files (#14461)
This PR allows ACL files to include comment lines starting with `#`,
addressing issue #14403.

Updated documentation to reflect comment support and note that
comments are lost on ACL SAVE.

Added test case in `tests/unit/acl.tcl` to verify comments work correctly.
2026-02-24 11:29:33 +08:00
Mincho PaskalevandGitHub 6ec7b16cc1 Add HOTKEYS HELP subcommand and fix hotkeys INFO section (#14785)
Each command having subcommands needs a HELP subcommand which is
currently missing for HOTKEYS.
Also the newly added section "Hotkeys" for INFO was messing up modules
INFOs in some cases.
Fixed both issues in this PR.
2026-02-23 11:04:14 +02:00
Hanzo Dev b90a1df833 fix: add g++ build dependency for fast_float compilation 2026-02-22 15:13:13 -08:00
Hanzo Dev 95a46111ff Use GH_PAT for GHCR login to fix permission_denied on push
GITHUB_TOKEN cannot push to GHCR packages that are not linked
to the source repository. Use the org-level GH_PAT secret which
has write:packages scope and bypasses this restriction.
2026-02-22 14:51:18 -08:00
Zach Kelling b89e644ec3 Add Dockerfile and GHCR deploy workflow
Build Redis from source for multi-platform (amd64/arm64) container
images. Deploy workflow triggers on push to unstable, pushes to
ghcr.io/hanzoai/memory:latest and :$SHA.
2026-02-22 13:08:49 -08:00
Zach Kelling fb208c0792 Merge remote-tracking branch 'upstream/unstable' into unstable 2026-02-22 00:54:30 -08:00
Kalin StaykovandGitHub 832c723f9c fix Rust binary checksums (#14802)
<!-- CURSOR_SUMMARY -->
> [!NOTE]
> **Low Risk**
> Build-script-only change that updates pinned checksums and a
verification flag; main risk is breaking Rust toolchain installation if
the new hashes are incorrect.
> 
> **Overview**
> Updates the `modules/Makefile` Rust toolchain bootstrap to use new
SHA256 checksums for the Rust 1.93.1 standalone installers across
`x86_64`/`aarch64` and `musl`/`gnu` targets.
> 
> Also changes the checksum verification command to use `sha256sum -c
--status` (instead of `--quiet`) while keeping the same failure
behavior, reducing noisy output during successful installs.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
30ab5a3ba63cd77e7ce10ab66f771e69bad9db77. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
2026-02-20 13:01:21 +02:00
Luca PalmieriandGitHub 5072fe0c2d Update to latest Rust stable toolchain (#14796) 2026-02-19 21:03:46 +02:00
Moti CohenandGitHub b5f728f6a0 Refine CI timeout from 14400 to 360 minutes (#14798)
This change reduces the CI timeout from 14400 minutes (10 days) to 360
minutes (6 hours), which matches the GitHub-hosted runner execution
time limit.

Changes:
- Updated timeout-minutes in .github/workflows/daily.yml (31 occurrences)
- Updated timeout-minutes in .github/workflows/external.yml (3 occurrences)

The 6-hour limit aligns with GitHub Actions' hard limit for GitHub-hosted
runners, making this a meaningful safety cap rather than an unreachable
value. For self-hosted runners (which have a 5-day limit), this still
provides a reasonable timeout to catch hung jobs.
2026-02-19 11:31:08 +02:00
Moti CohenandGitHub 6ef4b00925 Add DEBUG_ASSERT_KEYSPACE build flag and daily CI test (#14795)
Introduce DEBUG_ASSERT_KEYSPACE compile-time flag that enables runtime
assertions for keyspace consistency after each command invocation:
- Info keysizes histogram validation
- Cluster slot allocation size tracking

Changes:
- New daily CI job (test-debug-assert-keyspace) that builds with
  -DDEBUG_ASSERT_KEYSPACE and runs tests
- Skip assertions during nested command execution (intermediate state
  may be inconsistent) and RDB loading (database may be in inconsistent
  state)
- Fix RM_StringTruncate to update keysizes histogram when truncating
  strings

Known limitation (TODO):
ASM (atomic slot migration) tests are skipped due to known compatibility
issues with keysizes histogram tracking. To be fixed separately.
2026-02-18 11:45:38 +02:00
ee64353a7b redis-cli --keystats percentile update and all key names between double quotes (#14703)
This PR fixes two issues in redis-cli --keystats output to improve
clarity and consistency:

1. Percentile calculation 

The key size percentile now uses manual calculation (cumulative_count /
total_count * 100) instead of hdr_histogram's built-in percentile
values. This fixes misleading percentages when keys fall into histogram
buckets unevenly. For example, with 100 keys, if 63 keys have been
processed, the output now correctly shows "63.0000%" instead of
"50.0000%" (which was based on hdr_histogram's predefined bucket). This
makes the key size percentile consistent with the key name length
percentile calculation.

2. Key name formatting
         
All key names are now displayed within double quotes using sdscatrepr()
instead of sdsnewlen(), ensuring proper escaping of special characters
and whitespace for better readability and parseability.

---------

Co-authored-by: Yves LeBras <yves.lebras@redis.com>
2026-02-15 21:20:33 +08:00
Zhijun LiaoandGitHub 07c5e5640b redis-cli: Add word-jump navigation (Alt/Option + ←/→, Ctrl + ←/→) (#14331)
solves issue #14322 

# summary

Interactive use of redis-cli often involves working with long keys (e.g.
MY:INCREDIBLY:LONG:keythattakesalongtimetotype). In shells like bash,
zsh, or psql, users can quickly move the cursor word by word with
**Alt/Option+Left/Right** or **Ctrl+Left/Right**. This makes editing
long commands much more efficient.

Until now, redis-cli (via linenoise) only supported single-character
cursor moves, which is painful for frequent key editing.

This patch adds such support, with simple code changes in linenoise. It
now suppports both the Meta (Alt/Option) styles and CSI (control
sequence introducer) style:

|                 | Meta style | CSI style | Alt style |
| --------------- | ---------- | --------- | --------- |
| move word left  | ESC b      | ESC [1;5D | ESC [1;3D |
| move word right | ESC f      | ESC [1;5C | ESC [1;3C |

---------

Signed-off-by: Zhijun <dszhijun@gmail.com>
2026-02-14 16:09:52 +08:00
h.o.t. neglectedandGitHub 7842df622f Clarify RM_BlockClientOnKeys thread safety in documentation (#14693)
This PR closes https://github.com/redis/redis/issues/14677.
2026-02-14 15:38:39 +08:00
Yuan WangandGitHub 099203cb2a Fix DB hash tables not expanding during RDB load (#14789)
In standalone mode, we create dicts on demand. by default, there is no
dicts, and RESIZEDB hint doesn't expand dict actually. so we should
create dict first when expanding if the dict does not exist.
2026-02-14 15:18:08 +08:00
h.o.t. neglectedandGitHub 95314a93e5 Fix missing getKeysFreeResult() in cross-slot error path (#14774)
This fix adds the missing cleanup for code consistency and to prevent
potential memory leak from future code changes.

The potential leak can't be reproduced at the moment because it requires
two conditions:
1. use_cache_keys_result == false, so getKeysFromCommand allocates heap
memory
2. pcmd->read_error == CLIENT_READ_CROSS_SLOT, to trigger early return

However, the two conditions aren't met in practice:
- In preprocessCommand() (server.c), when CLIENT_READ_CROSS_SLOT is set,
PENDING_CMD_KEYS_RESULT_VALID is always set too. This causes
getClientCachedKeyResult() to return cached result, making
use_cache_keys_result true and skipping heap allocation in
getNodeByQuery().
- The other callers of getNodeByQuery() in script.c and module.c pass
read_error = 0, so the condition 2 above is never met.
2026-02-14 14:59:00 +08:00
98328ae2ec Pause dict auto-resize during multi-field deletion (#14783)
The idea comes directly from ValKey:
https://github.com/valkey-io/valkey/pull/3144

Deleting many fields from a hash/zset/set stored as a dict can trigger
repeated shrink/rehash work during the loop.

---------

Co-authored-by: Binbin <binloveplay1314@qq.com>
Co-authored-by: debing.sun <debing.sun@redis.com>
2026-02-14 14:45:54 +08:00
Vitah LinandGitHub a9838e6a4b Fix minor inconsistency in connFormatAddr size argument (#14731)
**Type**: Refactoring / Code Cleanup

**Description**: While reviewing the connection layer logic, I noticed a
minor inconsistency in how buffer sizes are passed to `connFormatAddr`.

In the error handling block of `acceptCommonHandler`, `sizeof(addr)` is
currently used for both the remote address and the local address
buffers. While both addr and laddr are defined with the same size
(`NET_ADDR_STR_LEN`), it is more idiomatic and safer to use the sizeof
of the actual buffer being populated.

**This change ensures:**
- Defensive Programming: Prevents potential buffer overflows or
truncation issues if the array sizes are changed independently in the
future.
- Code Clarity: Removes ambiguity for future maintainers and ensures the
logic is self-consistent.
2026-02-13 14:47:14 +08:00
Slavomir KaslevandGitHub 3cc7e9956f Don't call kvobjAllocSize() in t_stream.c if memory tracking is not enabled (#14786)
Don't call kvobjAllocSize() in t_stream.c if memory tracking is not
enabled.

Reported-by: Sergei Georgiev <s_ggeorgiev@yahoo.com>
2026-02-12 14:43:44 +02:00
Slavomir KaslevandGitHub 8e8483aac4 Use rax memory accounting for RedisModuleDict (#14779)
Use rax memory accounting for RedisModuleDict.

Suggested-by: debing.sun <debing.sun@redis.com>
2026-02-12 10:37:08 +02:00
e8887dc44c Support SFLUSH to flush slots partially (#14750)
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`
https://github.com/redis/redis/pull/14750#discussion_r2791904891, 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>
2026-02-12 16:20:14 +08:00
Paulo SousaandGitHub 9601c31125 Fix integer underflow in used_memory_dataset calculation (#14771)
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.
2026-02-12 15:39:55 +08:00
ded623d8ea Use kqueue as the backend of AE on DragonFlyBSD (#14555)
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>
2026-02-12 11:44:38 +08:00
Andy PanandGitHub 21b0f4ed4e Enable pipe2() on *BSD (#14556)
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)
2026-02-12 11:09:58 +08:00
Yuan WangandGitHub 23e58f5107 Optimize prefetching commands (#14754)
- After https://github.com/redis/redis/pull/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
2026-02-11 21:44:30 +08:00
Sergei GeorgievandGitHub 8387f9e164 Refactor idmp defrag (#14730)
# 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
2026-02-11 14:15:32 +02:00
Mincho PaskalevandGitHub bf6a5688ad Fix hotkeys result key name (#14780)
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.
2026-02-10 11:50:34 +02:00
Tom GabsowandGitHub d30b7255cb update datatypes modules to v8.6.0 (#14776)
<!-- 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
f1319fafa29bb8d149f70bcabaaed9ccb79ad109. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
2026-02-09 19:29:01 +02:00
Lior KoganandGitHub 3339b753b5 SECURITY.md: update Redis versions; clarifies OSes/CPUs/compilers in-scope for vulnerability reports (#14747) 2026-02-04 20:29:23 +02:00
Mincho PaskalevandGitHub b5a37c0e42 Add cmd tips for HOTKEYS. Return err when hotkeys START specifies invalid slots (#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.
2026-02-03 17:54:32 +02:00
02700f11cd RDB Channel connections mistakenly discovered by Sentinel (#14728) (#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: https://github.com/redis/redis/issues/14728

Co-authored-by: Ozan Tezcan <ozantezcan@gmail.com>
2026-02-02 14:02:44 +03:00
Slavomir KaslevandGitHub bafaec5b6a Fix HOTKEYS to track each command in a MULTI/EXEC block (#14756)
Fix HOTKEYS to track each command in a MULTI/EXEC block.
2026-02-02 09:50:44 +02:00
RoyBenMosheandGitHub bf6287d087 Redact user input in selected logs. (#14748)
This PR continues the work #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.
2026-01-29 23:42:19 +08:00
0024d5dfde Vectorize binary quantization path for vectorsets distance calculation (#14492)
This PR adds SIMD vectorization for binary quantization distance
calculation, similar to PR #14474.

---------

Co-authored-by: debing.sun <debing.sun@redis.com>
2026-01-29 19:59:48 +08:00
Slavomir KaslevandGitHub ca681f997e Add LTRIM/LREM and RM_StringTruncate() memory tracking tests (#14751)
Add LTRIM/LREM and RM_StringTruncate() memory tracking tests.
2026-01-29 13:04:46 +02:00
Mincho PaskalevandGitHub 591fc90263 Change reply schema for hotkeys get to use map instead of flat array (#14749)
Follow #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.
2026-01-29 11:21:05 +02:00
319153fe46 [Vector sets] Replace manual popcount with __builtin_popcountll for binary vector distance (#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>
2026-01-28 09:42:46 +08:00
debing.sunandGitHub beb75e40bf Fix test failure when using bind "*" in introspection.tcl (#14745)
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.
2026-01-27 20:50:06 +08:00
Sergei GeorgievandGitHub cecdc99873 Optimize Redis XREADGROUP CLAIM (#14726)
## 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)
2026-01-27 20:16:16 +08:00
37f685908e Vectorized the quantized 8-bit vector distance calculation (#14474)
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>
2026-01-27 09:25:59 +08:00
Yuan WangandGitHub 48aa1ce524 Avoid allocating and releasing list node in reply copy avoidance (#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
2026-01-26 19:49:36 +08:00
Mincho PaskalevandGitHub b209e8afde Fix hotkey info metric names. Disable HOTKEY SLOTS param for non-cluster (#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.
2026-01-26 13:32:05 +02:00
Stav-LeviandGitHub a765ee8238 Add security configuration warnings at startup (#14708)
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
2026-01-26 16:58:53 +08:00
Yuan WangandGitHub 6e2cbd51c3 Fix deferring free object that refcount is more than 1 (#14738)
in https://github.com/redis/redis/pull/14440, we remove the refcount
check in
[tryDeferFreeClientObject](https://github.com/redis/redis/commit/235e688b010b38496ea1de06b0bfc2786b1ebc63#diff-252bce0cc340542712f0c1adf62e9035ea47a4a064321fbf40ec3dd4b814aaf2R1509),
it is ok in 8.4 version, since after command execution, the refcount of
a kvobject always is 1.
but in #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.
2026-01-25 18:41:16 +08:00
18538461d1 Add separate statistics for active expiration of keys and hash fields (#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>
2026-01-22 22:30:25 +08:00
3ff37ea815 Reduce per command syscalls by reusing cached time when HW monotic clock is available (#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>
2026-01-22 17:45:45 +08:00
Sergei GeorgievandGitHub d099c10581 added: stream-idmp-duration and stream-idmp-maxsize to redis.conf (#14725)
## Summary
Adds missing IDMP configuration parameters to redis.conf, previously
ommitted in #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
2026-01-22 15:40:57 +08:00
Slavomir KaslevandGitHub 5dec7d3675 Add key allocation sizes histogram (#14695)
Add key allocation sizes histograms based on previous memory accounting work
in #14363 and #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.
2026-01-22 09:40:04 +02:00
cuiandGitHub 262ed50201 fix: two typos (#14655) 2026-01-22 10:33:58 +08:00
Paulo SousaandGitHub c4baa64ea8 Optimize peak memory stats by switching from per-command checks to threshold-based (#14692)
This PR optimizes peak memory tracking by moving from **per-command
checks** to a **threshold-based mechanism** in `zmalloc`.

Instead of updating peak memory on every command, peak tracking is now
triggered only when a thread's memory delta exceeds **100KB**. This
reduces runtime overhead while keeping peak memory accuracy acceptable.

## Implementation Details

- Peak memory is tracked atomically in `zmalloc` when a thread's memory
delta exceeds 100KB
- Thread-safe peak updates using CAS
- Peak tracking considers both:
  - current used memory
  - zmalloc-reported peak memory

## Performance Results (ARM AArch64)

All performance numbers were obtained on an **AWS m8g.metal (ARM
AArch64)** instance.

The database was pre-populated with **1M keys**, each holding a **1KB
value**.
Benchmarks were executed using memtier with a **10 SET : 90 GET ratio**
and **pipeline = 10** ([full benchmark spec.
here](https://github.com/redis/redis-benchmarks-specification/blob/main/redis_benchmarks_specification/test-suites/memtier_benchmark-1Mkeys-string-setget200c-1KiB-pipeline-10.yml)).

| Environment | Baseline `redis/redis` unstable (median ± std.dev) |
Comparison `paulorsousa/redis`
`f05a4bd273cb4d63ff03d33e6207837b6e51de86` (median) | % change (higher
better) | Note |

|------------------------------|----------------------------------------------------|----------------------------------------------------------------------------------:|--------------------------|-----------------------|
| oss-standalone | 802,830 ± 0.2% (7 datapoints) | 796,660 | -0.8% | No
change |
| oss-standalone-02-io-threads | 982,698 ± 0.6% (7 datapoints) | 980,520
| -0.2% | No change |
| oss-standalone-04-io-threads | 2,573,244 ± 1.9% (7 datapoints) |
2,630,931 | +2.2% | Potential improvement |
| oss-standalone-08-io-threads | 2,343,609 ± 1.6% (7 datapoints) |
2,455,630 | +4.8% | Improvement |
2026-01-21 22:52:31 +08:00
e3c38aab66 Handle primary/replica clients in IO threads (#14335)
# Problem

While introducing Async IO
threads(https://github.com/redis/redis/pull/13695) primary and replica
clients were left to be handled inside main thread due to data race and
synchronization issues. This PR solves this issue with the additional
hope it increases performance of replication.

# Overview

## Moving the clients to IO threads

Since clients first participate in a handshake and an RDB replication
phases it was decided they are moved to IO-thread after RDB replication
is done. For primary client this was trivial as the master client is
created only after RDB sync (+ some additional checks one can see in
`isClientMustHandledByMainThread`). Replica clients though are moved to
IO threads immediately after connection (as are all clients) so
currently in `unstable` replication happens while this client is in
IO-thread. In this PR it was moved to main thread after receiving the
first `REPLCONF` message from the replica, but it is a bit hacky and we
can remove it. I didn't find issues between the two versions.

## Primary client (replica node)

We have few issues here:
- during `serverCron` a `replicationCron` is ran which periodically
sends `REPLCONF ACK` message to the master, also checks for timed-out
master. In order to prevent data races we utilize`IOThreadClientsCron`.
The client is periodically sent to main thread and during
`processClientsFromIOThread` it's checked if it needs to run the
replication cron behaviour.

- data races with main thread - specifically `lastinteraction` and
`read_reploff` members of the primary client that are written to in
`readQueryFromClient` could be accessed at the same time from main
thread during execution of `INFO REPLICATION`(`genRedisInfoString`). To
solve this the members were duplicated so if the client is in IO-thread
it writes to the duplicates and they are synced with the original
variables each time the client is send to main thread ( that means `INFO
REPLICATION` could potentially return stale values).

- During `freeClient` the primary client is fetched to main thread but
when caching it(`replicationCacheMaster`) the thread id will remain the
id of the IO thread it was from. This creates problems when resurrecting
the master client. Here the call to `unbindClientFromIOThreadEventLoop`
in `freeClient` was rewritten to call `keepClientInMainThread` which
automatically fixes the problem.

- During `exitScriptTimedoutMode` the master is queued for reprocessing
(specifically process any pending commands ASAP after it's unblocked).
We do that by putting it in the `server.unblocked_clients` list, which
are processed in the next `beforeSleep` cycle in main thread. Since this
will create a contention between main and IO thread, we just skip this
queueing in `unblocked_clients` and just queue the client to main thread
- the `processClientsFromIOThread` will process the pending commands
just as main would have.

## Replica clients (primary node)

We move the client after RDB replication is done and after replication
backlog is fed with its first message.
We do that so that the client's reference to the first replication
backlog node is initialized before it's read from IO-thread, hence no
contention with main thread on it.

### Shared replication buffer

Currently in unstable the replication buffer is shared amongst clients.
This is done via clients holding references to the nodes inside the
buffer. A node from the buffer can be trimmed once each replica client
has read it and send its contents. The reference is
`client->ref_repl_buf_node`. The replication buffer is written to by
main thread in `feedReplicationBuffer` and the refcounting is intrusive
- it's inside the replication-buffer nodes themselves.

Since the replica client changes the refcount (decreases the refcount of
the node it has just read, and increases the refcount of the next node
it starts to read) during `writeToClient` we have a data race with main
thread when it feeds the replication buffer. Moreover, main thread also
updates the `used` size of the node - how much it has written to it,
compared to its capacity which the replica client relies on to know how
much to read. Obviously replica being in IO-thread creates another data
race here. To mitigate these issues a few new variables were added to
the client's struct:

- `io_curr_repl_node` - starting node this replica is reading from
inside IO-thread
- `io_bound_repl_node` - the last node in the replication buffer the
replica sees before being send to IO-thread.

These values are only allowed to be updated in main thread. The client
keeps track of how much it has read into the buffer via the old
`ref_repl_buf_node`. Generally while in IO-thread the replica client
will now keep refcount of the `io_curr_repl_node` until it's processed
all the nodes up to `io_bound_repl_node` - at that point its returned to
main thread which can safely update the refcounts.
The `io_bound_repl_node` reference is there so the replica knows when to
stop reading from the repl buffer - imagine that replica reads from the
last node of the replication buffer while main thread feeds data to it -
we will create a data race on the `used` value
(`_writeToClientSlave`(IO-thread) vs `feedReplicationBuffer`(main)).
That's why this value is updated just before the replica is being send
to IO thread.
*NOTE*, this means that when replicas are handled by IO threads they
will hold more than one node at a time (i.e `io_curr_repl_node` up to
`io_bound_repl_node`) meaning trimming will happen a bit less
frequently. Tests show no significant problems with that.
(tnx to @ShooterIT for the `io_curr_repl_node` and `io_bound_repl_node`
mechanism as my initial implementation had similar semantics but was way
less clear)

Example of how this works:

* Replication buffer state at time N:
   | node 0| ... | node M, used_size K |
* replica caches `io_curr_repl_node`=0, `io_bound_repl_node`=M and
`io_bound_block_pos`=K
* replica moves to IO thread and processes all the data it sees
* Replication buffer state at time N + 1:
| node 0| ... | node M, used_size Full | |node M + 1| |node M + 2,
used_size L|, where Full > M
* replica moves to main thread at time N + 1, at this point following
happens
   - refcount to node 0 (io_curr_repl_node) is decreased
- `ref_repl_buf_node` becomes node M(io_bound_repl_node) (we still have
size-K bytes to process from there)
- refcount to node M is increased (now all nodes from 0 up to M-1
including can be trimmed unless some other replica holds reference to
them)
- And just before the replica is send back to IO thread the following
are updated:
   - `io_bound_repl_node` ref becomes node M+2
   - `io_bound_block_pos` becomes L

Note that replica client is only moved to main if it has processed all
the data it knows about (i.e up to `io_bound_repl_node` +
`io_bound_block_pos`)

### Replica clients kept in main as much as possible

During implementation an issue arose - how fast is the replica client
able to get knowledge about new data from the replication buffer and how
fast can it trim it. In order for that to happen ASAP whenever a replica
is moved to main it remains there until the replication buffer is fed
new data. At that point its put in the pending write queue and special
cased in handleClientsWithPendingWrites so that its send to IO thread
ASAP to write the new data to replica. Also since each time the replica
writes its whole repl data it knows about that means after it's send to
main thread `processClientsFromIOThread` is able to immediately update
the refcounts and trim whatever it can.

### ACK messages from primary

Slave clients need to periodically read `REPLCONF ACK` messages from
client. Since replica can remain in main thread indefinitely if no DB
change occurs, a new atomic `pending_read` was added during
`readQueryFromClient`. If a replica client has a pending read it's
returned back to IO-thread in order to process the read even if there is
no pending repl data to write.

### Replicas during shutdown

During shutdown the main thread pauses write actions and periodically
checks if all replicas have reached the same replication offset as the
primary node. During `finishShutdown` that may or may not be the case.
Either way a client data may be read from the replicas and even we may
try to write any pending data to them inside `flushSlavesOutputBuffers`.
In order to prevent races all the replicas from IO threads are moved to
main via `fetchClientFromIOThread`. A cancel of the shutdown should be
ok, since the mechanism employed by `handleClientsWithPendingWrites`
should return the client back to IO thread when needed.

## Notes

While adding new tests timing issues with Tsan tests were found and
fixed.

Also there is a data race issue caught by Tsan on the `last_error`
member of the `client` struct. It happens when both IO-thread and main
thread make a syscall using a `client` instance - this can happen only
for primary and replica clients since their data can be accessed by
commands send from other clients. Specific example is the `INFO
REPLICATION` command.
Although other such races were fixed, as described above, this once is
insignificant and it was decided to be ignored in `tsan.sup`.

---------

Co-authored-by: Yuan Wang <wangyuancode@163.com>
Co-authored-by: Yuan Wang <yuan.wang@redis.com>
2026-01-21 16:19:12 +02:00
Slavomir KaslevandGitHub b9c00b27f8 Make cluster-slot-stats-enabled config multivalued (#14719)
This allows users to specify exactly what per slot statistics are to be
collected -- CPU, network traffic and/or memory used.

The config accepts multiple values as a space-separated list:
  - cpu: Track CPU usage per slot (cpu-usec metric)
  - net: Track network bytes per slot (network-bytes-in, network-bytes-out metrics)
  - mem: Track memory usage per slot (memory-bytes metric)
  - yes: Enable all tracking (equivalent to "cpu net mem")
  - no: Disable all tracking (default)

Note: Memory tracking (mem) can ONLY be enabled at startup. If you try to enable
memory tracking via CONFIG SET when it wasn't enabled at startup, the command will
fail. However, you can disable memory tracking at runtime by removing the 'mem' flag.
Once disabled, memory tracking cannot be re-enabled without restarting the server.
2026-01-21 15:36:03 +02:00
EmilyZHANG00andGitHub 5656e99c7c Modify the condition for adding CLIENT_IO_CLOSE_ASAP flag (#14709)
1. CLIENT_IO_CLOSE_ASAP is a flag for c->io_flags, which does not match
c->flags. The flag corresponding to c->flags is CLIENT_CLOSE_ASAP.
2. If we want to asynchronously free a client running on an io-thread,
we should check its c->io_flags to determine if CLIENT_IO_CLOSE_ASAP has
already been added. If it hasn't been added before, then
CLIENT_IO_CLOSE_ASAP should be added.
2026-01-21 19:39:15 +08:00
Yuan WangandGitHub a2e901c93d Fix inaccurate IO thread client count due to delayed freeing (#14723)
There is a failure in CI:
```
*** [err]: Clients are evenly distributed among io threads in tests/unit/introspection.tcl
Expected '2' to be equal to '1' (context: type eval line 3 cmd {assert_equal $cur_clients 1} proc ::start_server)
```

There might be a client used for health checks (to detect if the server
is up)
that has not been freed timely. This can lead to an inaccurate count of
connected clients processed by IO threads. So we wait it to close
completely.
2026-01-21 18:13:40 +08:00
Stav-LeviandGitHub 25f780b662 Fix crash when calling internal container command without arguments (#14690)
Addresses crash and clarifies errors around container commands.

- Update server.c to handle container commands with no subcommand: emit
"missing subcommand. Try HELP."; keep "unknown subcommand" for invalid
subcommands; for unknown commands, include args preview only when
present
- Add a test module command subcommands.internal_container with a
subcommand for validation
- Add unit test asserting missing subcommand error when calling the
internal container command without arguments
2026-01-21 08:38:04 +02:00
Yuan WangandGitHub 5c5c7c5a2c Quick user ACL permission verification (#14714)
Optimizes ACL evaluation by adding a fast path for fully privileged users.
2026-01-21 14:34:53 +08:00
Slavomir KaslevandGitHub 818046d031 Avoid memory allocation on quicklist iteration (#14720)
This change is the last in the series (see #14200 and #14473) where we
store iterators
on the stack rather than allocating them the heap.
2026-01-21 08:09:49 +02:00
Yuan WangandGitHub e8240017fd Fix prefetch size (#14715)
Fixes prefetch sizing so when the remaining work is smaller than
the effective max batch (2× configured), Redis prefetches it all
at once instead of splitting into an inefficient tiny tail batch.
2026-01-20 19:43:57 +08:00
debing.sunandGitHub e76e3af5b7 Fix some test timing issues in replication.tcl and maxmemory.tcl (#14718)
1) Replace fixed sleep with wait_for_condition to avoid flaky test
failures when checking master_current_sync_attempts counter.

2) Similar to https://github.com/redis/redis/pull/14674, use
assert_lessthan_equal instead of assert_lessthan to verify the idle
time.
2026-01-20 19:25:15 +08:00
debing.sunandGitHub d2da5cca37 Fix timeout waiting for blocked clients in pause test (#14716)
To verify the pause duration, we need to wait for the client to be
unpause and the command to complete, so add `$rd read` to wait for the
command to finish.

The test failure was caused by $rd still being blocked and not closed in
the previous test, so the next test would get 2 blocked clients instead
of 1 client, causing the test to fail.
2026-01-20 17:12:22 +08:00
Omer ShadmiandGitHub 8816bcd973 MOD-13504: Update Search to RC1 8.5.90 (#14717)
Update Search to 8.6 RC1 version 8.5.90
2026-01-20 11:07:15 +02:00
Mincho PaskalevandGitHub 1ab0cd228f Add hotkeys memory to memory-overhead and fix hotkeys info keys (#14711)
Add the memory overhead of the hotkeyStats structure to
`used_memory_overhead`, add `hotkeys-` prefix to hotkey keys in INFO and
remove `used_memory` in the hotkeys info section as it's unneeded (too
little memory for us to care about).

Tnx @oranagra for pointing
[this](https://github.com/redis/redis/pull/14680#discussion_r2702151804)
out.
2026-01-20 10:35:00 +02:00
Yuan WangandGitHub cfa6129040 Minor fixes for ASM (#14707)
- **TCL test failure**

https://github.com/redis/redis/actions/runs/21121021310/job/60733781853#step:6:5705
```
[err]: Test cluster module notifications when replica restart with RDB during importing
in tests/unit/cluster/atomic-slot-migration.tcl
Expected '{sub: cluster-slot-migration-import-started, source_node_id:28c64b3f462f3c29aa3c96c2ba5dff948dfe315b, destination_node_id:1382a4b4ca86621e39068ee8b25524a44a21bbc1, task_id:4d185a5398be94edac0dd77fff094eb7f5c73ec4, slots:0-100}' to be equal to '{sub: cluster-slot-migration-import-started, source_node_id:28c64b3f462f3c29aa3c96c2ba5dff948dfe315b, destination_node_id:1382a4b4ca86621e39068ee8b25524a44a21bbc1, task_id:4d185a5398be94edac0dd77fff094eb7f5c73ec4, slots:0-100} {sub: cluster-slot-migration-import-completed, source_node_id:28c64b3f462f3c29aa3c96c2ba5dff948dfe315b, destination_node_id:1382a4b4ca86621e39068ee8b25524a44a21bbc1, task_id:4d185a5398be94edac0dd77fff094eb7f5c73ec4, slots:0-100}' (context: type eval line 29 cmd {assert_equal  [list  "sub: cluster-slot-migration-import-started, source_node_id:$src_id, destination_node_id:$dest_id, task_id:$task_id, slots:0-100"  ] [R 4 asm.get_cluster_event_log]} proc ::test)
```
If there is a delay to work to check, the ASM task may complete, so we
will get `started & completed` ASM log instead of only `started` log, it
feels fragile, so delete the check, we will check all logs later.
```
                restart_server -4 true false true save ;# rdb save
---> if there is a delay, the ASM task should complete
                # the asm task info in rdb will fire module event
                assert_equal  [list \
                    "sub: cluster-slot-migration-import-started, source_node_id:$src_id, destination_node_id:$dest_id, task_id:$task_id, slots:0-100" \
                ] [R 4 asm.get_cluster_event_log]
```
- **Start BGSAVE for slot snapshot ASAP**
Since we consider the migrating client as a replica that wants diskless
replication, so it will wait for repl-diskless-sync-delay` to start a
new fork after the last child exits. But actually slot snapshot can not
be shared with other slaves, so we can start BGSAVE for it immediately.

  also resolve internal ticket RED-177974.
2026-01-19 19:57:20 +08:00
Zach Kelling 2761a2bd7b feat: add Hanzo Redis config with caching patterns
- Add redis.conf with performance tuning
- Add compose.yml for Redis Stack + exporter
- Add LLM.md with caching patterns documentation
- Configure for sessions, rate limiting, LLM caching
2026-01-19 02:58:19 -08:00
Tom GabsowandGitHub c42d07a76e MOD-13505 Update DataType Modules to 8.5.90 (#14705)
update data type modules to 8.6 RC1
time series v8.5.90
bloom v8.5.90
json v8.5.90
2026-01-19 09:37:07 +02:00
Filipe Oliveira (Redis)andGitHub c118f91b25 Optimize lpDecodeBacklen() fast paths by removing loop-based decoding (#14662)
## Optimization details

The current `lpDecodeBacklen()` implementation decodes the backlen using
a loop with backward pointer mutation and a branch-heavy termination
condition:

```c
do {
    val |= (uint64_t)(p[0] & 127) << shift;
    if (!(p[0] & 128)) break;
    shift += 7;
    p--;
    if (shift > 28) return UINT64_MAX;
} while(1);
```

While correct, this structure introduces avoidable overhead in hot
paths:

- repeated loop control
- unpredictable branch (if (!(p[0] & 128)) break)
- increased front-end pressure and bad speculation

### Optimization

This PR replaces the loop with a straight-line implementation optimized
for the common case:

- explicit fast paths for 1–2 byte backlen encodings (dominant in
practice)
- unrolled handling up to the maximum 5-byte encoding
- no pointer mutation, no loop, fewer branches
- identical encoding semantics and validation behavior

leading to a 5.3% boost on listpack iterator heavy benchmark on HASH
datatype.
2026-01-19 14:23:15 +08:00
39881fa6f2 Reply Copy Avoidance (#14608)
This PR is based on https://github.com/valkey-io/valkey/pull/2078

# Reply Copy Avoidance Optimization

This PR introduces an optimization to avoid unnecessary memory copies
when sending replies to clients in Redis.

## Overview

Currently, Redis copies reply data into client output buffers before
sending responses. This PR implements a mechanism to avoid these copies
in certain scenarios, improving performance and reducing memory
overhead.

### Key Changes
* Added capability to reply construction allowing to interleave regular
replies with copy avoid replies in client reply buffers
* Extended write-to-client handlers to support copy avoid replies
* Added copy avoidance of string bulk replies when copy avoidance
indicated by I/O threads
* Copy avoidance is beneficial for performance despite object size only
starting certain number of threads. So it will be enabled only starting
certain number of threads.

**Note**: When copy avoidance disabled content and handling of client
reply buffers remains as before this PR

---------

Signed-off-by: Alexander Shabanov <alexander.shabanov@gmail.com>
Signed-off-by: xbasel <103044017+xbasel@users.noreply.github.com>
Signed-off-by: Madelyn Olson <madelyneolson@gmail.com>
Co-authored-by: Alexander Shabanov <alexander.shabanov@gmail.com>
Co-authored-by: xbasel <103044017+xbasel@users.noreply.github.com>
Co-authored-by: Madelyn Olson <madelyneolson@gmail.com>
Co-authored-by: Slavomir Kaslev <slavomir.kaslev@gmail.com>
Co-authored-by: moticless <moticless@github.com>
Co-authored-by: Yuan Wang <yuan.wang@redis.com>
2026-01-19 11:09:16 +08:00
Moti CohenandGitHub 609acaad02 Optimize zset to use dict with no_value=1 (#14701)
* Embed sds element inside skiplist nodes: Changed zset dict to store
zskiplistNode* as keys (with no_value=1) instead of storing sds keys and
double* values, eliminating redundant sds storage and enabling
single-allocation nodes
* Single allocation for skiplist nodes: Each node now contains: fixed
fields + level[] array + embedded sds, reducing memory fragmentation and
allocation overhead. This optimization is based on https://github.com/valkey-io/valkey/pull/1427
* Optimize lookups with dictFindLink: Use dictFindLink in zsetAdd to
avoid double hash table lookup when inserting new elements (find + add
becomes single operation)
* Simplify score updates
2026-01-18 14:52:50 +02:00
7f541b9607 Prefetch client fields before prefetching command-related data (#14700)
This PR refines the prefetch strategy by removing ineffective (to close
on the pipeline) dictionary-level prefetching and improving prefetch
usage in IO threads. The goal is to better aligning prefetches with
predictable access patterns.

## Changes

- Removed speculative prefetching from `dictFindLinkInternal()`,
simplifying the dictionary lookup hot path.
- Introduced a two-phase prefetch approach in
`prefetchIOThreadCommands()`:
  - Phase 1: Prefetch client structures and `pending_cmds`
- Phase 2: Add commands to the batch and prefetch follow-up fields
(`reply`, `mem_usage_bucket`)

## Performance

Measured with
`memtier_benchmark-1Mkeys-string-setget2000c-1KiB-pipeline-16`.

| Environment                  | % change |
|-----------------------------|----------|
| oss-standalone               | -0.1%    |
| oss-standalone-02-io-threads | +0.4%    |
| oss-standalone-04-io-threads | +1.6%    |
| oss-standalone-08-io-threads | +2.3%    |
| oss-standalone-12-io-threads | +0.7%    |
| oss-standalone-16-io-threads | +1.9%    |

Overall, this shows an ~2% throughput improvement on IO-threaded
configurations, with no meaningful impact on non-IO-threaded setups.

---------

Co-authored-by: Yuan Wang <wangyuancode@163.com>
2026-01-18 20:14:39 +08:00
c93e4a62c6 Add hotkeys detection (#14680)
# Description

Introducing a new method for identifying hotkeys inside a redis server
during a tracking time period.

Hotkeys in this context are defined by two metrics:
* Percentage of time spend by cpu on the key from the total time during
the tracking period
* Percentage of network bytes (input+output) used for the key from the
total network bytes used by redis during the tracking period

## Usage

Although the API is subject to change the general idea is for the user
to initiate a hotkeys tracking process which should run for some time.
The keys' metrics are recorded inside a probabilistic structure and
after that the user is able to fetch the top K of them.

### Current API

```
HOTKEYS START
            <METRICS count [CPU] [NET]>
            [COUNT k] 
            [DURATION duration]
            [SAMPLE ratio]
            [SLOTS count slot…]

HOTKEYS GET
HOTKEYS STOP
HOTKEYS RESET

```

### HOTKEYS START

Start a tracking session if either no is already started, or one was
stopped or reset. Return error if one is in progress.

* METRICS count [CPU] [NET] - chose one or more metrics to track
* COUNT k - track top K keys
* DURATION duration - preset how long the tracking session should last
* SAMPLE ratio - a key is tracked with probability 1/ratio
* SLOTS count slot... - Only track a key if it's in a slot amongst the
chosen ones

### HOTKEYS GET

Return array of the chosen metrics to track and various other metadata.
(nil) if no tracking was started or it was reset.

```
127.0.0.1:6379> hotkeys get
1) "tracking-active"
2) 1
3) "sample-ratio"
4) <ratio>
5) "selected-slots" (empty array if no slots selected)
6) 1) 0
   2) 5
   3) 6
7) "sampled-command-selected-slots-ms" (show on condition sample-ratio > 1 and selected-slots != empty-array)
8) <time-in-milliseconds>
9) "all-commands-selected-slots-ms" (show on condition selected-slots != empty-array)
10) <time-in-milliseconds>
11) "all-commands-all-slots-ms"
12) <time-in-milliseconds>
13) "net-bytes-sampled-commands-selected-slots" (show on condition sample-ratio > 1 and selected-slots != empty-array)
14) <num-bytes>
15) "net-bytes-all-commands-selected-slots" (show on condition selected-slots != empty-array)
16) <num-bytes>
17) "net-bytes-all-commands-all-slots"
18) <num-bytes>
19) "collection-start-time-unix-ms"
20) <start-time-unix-timestamp-in-ms>
21) "collection-duration-ms"
22) <duration-in-milliseconds>
23) "used-cpu-sys-ms"
24) <duration-in-millisec>
25) "used-cpu-user-ms"
26) <duration-in-millisec>
27) "total-net-bytes"
28) <num-bytes>
29) "by-cpu-time"
30) 1) key-1_1
    2) <millisec>
    ...
    19) key-10_1
    20) <millisec>
31) 1) "by-net-bytes"
32) 1) key-1_2
    2) <num-bytes>
    ...
    19) key-10_2
    20) <num-bytes>

```

### HOTKEYS STOP

Stop tracking session but user can still get results from `HOTKEYS GET`.

### HOTKEYS RESET

Release resources used for hotkeys tracking only when it is stopped.
Return error if a tracking is active.

## Additional changes

The `INFO` command now has a "hotkeys" section with 3 fields
* tracking_active - a boolean flag indicating whether or not we
currently track hotkeys.
* used-memory - memory overhead of the structures used for hotkeys
tracking.
* cpu-time - time in ms spend updating the hotkey structure. 

## Implementation

Independent of API, implementation is based on a probabilistic structure
- [Cuckoo Heavy
Keeper](https://dl.acm.org/doi/abs/10.14778/3746405.3746434) structure
with added min-heap to keep track of top K hotkey's names. CHK is an
loosely based on
[HeavyKeeper](https://www.usenix.org/conference/atc18/presentation/gong)
which is used in RedisBloom's TopK but has higher throughput.

Random fixed probability sampling via the `HOTKEYS start sample <ratio>`
param. Each key is sampled with probability `1/ratio`.

## Performance implications

With low enough sample rate (controlled by `HOTKEYS start sample
<ratio>`) there is negligible performance hit. Tracking every key though
can incur up to 15% hit in [the worst
case](https://github.com/redis/redis-benchmarks-specification/blob/main/redis_benchmarks_specification/test-suites/memtier_benchmark-1Mkeys-string-get-10B-pipeline-500.yml)
after running the tests in this
[bench](https://github.com/redis/redis-benchmarks-specification/).

---------

Co-authored-by: Ozan Tezcan <ozantezcan@gmail.com>
Co-authored-by: Slavomir Kaslev <slavomir.kaslev@gmail.com>
Co-authored-by: debing.sun <debing.sun@redis.com>
2026-01-16 17:15:28 +02:00
Filipe Oliveira (Redis)andGitHub 9bbeaa312d Avoid unnecessary command rewriting for SET command with expiration (#14699)
This PR optimizes SET commands with expiration options (EX/PX/EXAT) by
using in-place argument replacement instead of full command vector
rewrite during replication propagation/AOF.

We've moved from rewriteClientCommandVector taking ~6% of CPU cycles on
mixed SET+GET benchmark with expiration
2026-01-16 09:57:37 +08:00
Moti CohenandGitHub 11e73c66a8 Modules KeyMeta (Keys Metadata) (#14445)
Modules KeyMeta (Keys Metadata)

Redis modules often need to associate additional metadata with keys in
the keyspace. The objective is to create a unified and extensible
interface, usable by modules, Redis core, and maybe later by the users,
that facilitate the association and management of metadata with keys.
While extending RedisModuleTypes might be an easier path, this proposal
goes one step further: a general-purpose mechanism that lets attach
metadata to any key, independent of underlying data type.

A major part of this feature involves defining how metadata is managed
throughout a key’s lifecycle. Modules will be able to optionally
register distinct metadata classes, each with its own lifecycle
callbacks and capable of storing arbitrary 8-byte value per key. These
metadata values will be embedded directly within Redis’s core key-value
objects to ensure fast access and automatic callback execution as keys
are created, updated, or deleted. Each 8 bytes of metadata can represent
either a simple primitive value or a pointer/handle to more complex,
externally managed data by the module and RDB serialized along with the
key.

Key Features:
- Modules can register up to 7 metadata classes (8 total, 1 reserved)
- Each class: 4-char name + 5-bit version (e.g., "SRC1" v1)
- Each class attaches 8 bytes per key (value or pointer/handle)
- Separate namespace from module data types

Module API:
- RedisModule_CreateKeyMetaClass() - Register metadata class
- RedisModule_ReleaseKeyMetaClass() - Release metadata class
- RedisModule_SetKeyMeta() - Attach/update metadata
- RedisModule_GetKeyMeta() - Retrieve metadata

Lifecycle Callbacks:
- copy, rename, move - Handle key operations
- unlink, free - Handle key deletion/expiration
- rdb_save, rdb_load - RDB persistence
- aof_rewrite - AOF rewrite support

Implementation:
- Metadata slots allocated before kvobj in reverse class ID order
- 8-bit metabits bitmap tracks active classes per key
- Minimal memory overhead - only allocated slots consume memory

RDB Serialization (v13):
- New opcode RDB_OPCODE_KEY_METADATA
- Compact 32-bit class spec: 24-bit name + 5-bit ver + 3-bit flags
- Self-contained format: [META,] TYPE, KEY, VALUE
- Portable across cluster nodes

Integration:
- Core ops: dbAdd, dbSet, COPY, MOVE, RENAME, DELETE
- DUMP/RESTORE support
- AOF rewrite via module callbacks
- Defragmentation support
- Module type I/O refactored to ModuleEntityId
2026-01-15 23:11:17 +02:00
Sergei GeorgievandGitHub 221409788a Add idempotency support to XADD via IDMPAUTO and IDMP parameters (#14615)
# Overview

This PR introduces idempotency support to Redis Streams' XADD command,
enabling automatic deduplication of duplicate message submissions
through optional IDMPAUTO and IDMP parameters with producer
identification. This enables reliable at-least-once delivery while
preventing duplicate entries in streams.

## Problem Statement

Current Redis Streams implementations lack built-in idempotency
mechanisms, making reliable at-least-once delivery impossible without
accepting duplicates:

- **Application-level tracking**: Developers must maintain separate data
structures to track submitted messages
- **Race conditions**: Network failures and retries can result in
duplicate stream entries
- **Complexity overhead**: Each producer must implement custom
deduplication logic
- **Memory inefficiency**: External deduplication systems duplicate
Redis's storage capabilities

This lack of native idempotency support creates reliability challenges
in distributed systems where at-least-once delivery semantics are
required but exactly-once processing is desired.

## Solution

Extends XADD with optional idempotency parameters that include producer
identification:

```
XADD key [NOMKSTREAM] [KEEPREF | DELREF | ACKED] [IDMPAUTO pid | IDMP pid iid] [MAXLEN | MINID [= | ~] threshold [LIMIT count]] <* | id> field value [field value ...]
```

### Producer ID (pid)

- **pid** (producer id): A unique identifier for each producer
- Must be unique per producer instance
- Producers must use the same pid after restart to access their
persisted idempotency tracking
- Enables per-producer idempotency tracking, isolating duplicate
detection between different producers

**Format**: Binary or string, recommended max 36 bytes

**Generation**: 
- **Recommended**: UUID v4 for globally unique identification
- **Alternative**: `hostname:process_id` or application-assigned IDs

### Idempotency Modes

**IDMPAUTO pid (Automatic Idempotency)**:

- Producer specifies its pid, Redis automatically calculates a unique
idempotent ID (iid) based on entry content
- Hash calculation combines XXH128 hashing of individual field-value
pairs using an order-independent Sum + XOR approach with rotation (each
pair: `XXH128(field || field_length || value)`)
- 16-byte binary iid with extremely low accidental collision probability
- XXH128 is a non-cryptographic hash function: fast and
well-distributed, but does NOT prevent intentional collision attacks
- For protection against adversarial collision crafting, use IDMP mode
with cryptographically-signed idempotent IDs
- Order-independent: field ordering does not affect the calculated iid
- If (pid, iid) pair exists in producer's IDMP map: returns existing
entry ID without creating duplicate entry
- Generally slower than manual mode due to hash calculation overhead

**IDMP pid iid (Manual Idempotency)**:

- Caller provides explicit producer id (pid) and idempotent ID (iid) for
deduplication
- iid must be unique per message (either globally or per pid)
- Faster processing than IDMPAUTO (no hash calculation overhead)
- Enables shorter iids for reduced memory footprint
- If (pid, iid) pair exists in producer's IDMP map: returns existing
entry ID without comparing field contents
- Caller responsible for iid uniqueness and consistency across retries

Both modes can only be specified when entry ID is `*` (auto-generated).

### Deduplication Logic

When XADD is called with idempotency parameters:

1. Redis checks if the message was recently added to the stream based on
the (pid, iid) pair
2. If the (pid, iid) pair matches a recently-seen pair for that
producer, the message is assumed to be identical
3. No duplicate message is added to the stream; the existing entry ID is
returned
4. With **IDMP pid iid**: Redis does not compare the specified fields
and their values—two messages with the same (pid, iid) are assumed
identical
5. With **IDMPAUTO pid**: Redis calculates the iid from message content
and checks for duplicates

## IDMP Map: Per-Producer Time and Capacity-Based Expiration

Each producer with idempotency enabled maintains its own isolated IDMP
map (iid → entry_id) with dual expiration criteria:

**Time-based expiration (duration)**:

- Each iid expires automatically after duration seconds from insertion
- Provides operational guarantee: Redis will not forget an iid before
duration elapses (unless capacity reached)
- Configurable per-stream via XCFGSET

**Capacity-based expiration (maxsize)**:

- Each producer's map enforces maximum capacity of maxsize entries
- When capacity reached, oldest iids for that producer are evicted
regardless of remaining duration
- Prevents unbounded memory growth during extended usage

### Configuration Commands

**XINFO STREAM**: View current configuration and metrics

Use `XINFO STREAM key` to retrieve idempotency configuration
(idmp-duration, idmp-maxsize) along with tracking metrics.

**XCFGSET**: Configure expiration parameters

```
XCFGSET key [IDMP-DURATION duration] [IDMP-MAXSIZE maxsize]
```

- **duration**: Seconds to retain each iid (range: 1- 86400 seconds)
- **maxsize**: Maximum iids to track per producer (range: 1-10,000
entries)
- Calling XCFGSET clears all existing producer IDMP maps for the stream

**Default Configuration** (when XCFGSET not called):

- Duration: 100 seconds
- Maxsize: 100 iids per producer
- Runtime configurable via: `stream-idmp-duration` and
`stream-idmp-maxsize`

## Response Behavior

**On first submission** (pid, iid) pair not in producer's map:

- Entry added to stream with generated entry ID
- (pid, iid) pair stored in producer's IDMP map with current timestamp
- Returns new entry ID

**On duplicate submission** (pid, iid) pair exists in producer's map:

- No entry added to stream
- Returns existing entry ID from producer's IDMP map
- Identical response to original submission (client cannot distinguish)

## Stream Metadata

XINFO STREAM extended with idempotency metrics and configuration:

- **idmp-duration**: The duration value (in seconds) configured for the
stream's IDMP map
- **idmp-maxsize**: The maxsize value configured for the stream's IDMP
map
- **pids-tracked**: Current number of producers with active IDMP maps
- **iids-tracked**: Current total number of iids across all producers'
IDMP maps (reflects active iids that haven't expired or been evicted)
- **iids-added**: Lifetime cumulative count of entries added with
idempotency parameters
- **iids-duplicates**: Lifetime cumulative count of duplicate iids
detected across all producers

## Persistence and Restart Behavior

**IDMP maps are fully persisted and restored across Redis restarts**:

- **RDB/AOF**: All pid-iid pairs, timestamps, and configuration are
included in snapshots and AOF logs
- **Recovery**: On restart, all tracked (pid, iid) pairs remain valid
and operational
- **Producer Requirement**: Producers must reuse the same pid after
restart to access their persisted IDMP map
- **Configuration**: Stream-level settings (duration, maxsize) persist
across restarts
- **Important**: Calling XCFGSET after restart clears restored IDMP maps
(same behavior as during runtime)

## Key Benefits

- **Enables At-most-once Producer Semantics**: Makes it possible to
safely retry message submissions without creating duplicates
- **Automatic Retry Safety**: Network failures and retries cannot create
duplicate entries
- **Producer Isolation**: Each producer maintains independent
idempotency tracking
- **Memory Efficient**: Time and capacity-based expiration per producer
prevents unbounded growth
- **Flexible Implementation**: Choose automatic (IDMPAUTO) or manual
(IDMP) based on performance needs
- **Backward Compatible**: Fully optional parameters with zero impact on
existing XADD behavior
- **Collision Resistant**: XXH128 with Sum + XOR combination and
field-length separators provides high-quality non-cryptographic hashing
for IDMPAUTO with extremely low collision probability and prevents
ambiguous concatenation attacks
2026-01-15 21:58:44 +08:00
Moti CohenandGitHub 8da9c04bb8 Eliminate zslGetRank calls from ZCOUNT/ZLEXCOUNT + simplify zslGetRankByNode() (#14684)
- Eliminate redundant zslGetRank calls in ZCOUNT/ZLEXCOUNT
- Simplify and clarify zslGetRankByNode()
- Add UT for skiplist
2026-01-15 11:48:16 +02:00
Filipe Oliveira (Redis)andGitHub 7e7c7b0558 Fix flaky test failures in caused by clock precision issues with monotonic clock. (#14697)
Fix flaky test failures in `tests/unit/moduleapi/blockedclient.tcl`
caused by
clock precision issues with monotonic clock.

The test runs a command that blocks for 200ms and then asserts the
elapsed time
is >= 200ms. Due to clock skew and timing precision differences, the
measured
time occasionally comes back as 199ms, causing spurious test failures.
2026-01-14 19:44:05 +08:00
Kalin StaykovandYaacovHazan 23a947ee3d fix Rust installation checksums 2026-01-14 13:41:20 +02:00
LukeMathWalkerandYaacovHazan cb842d6cc2 chore: Bump Rust stable version from 1.88.0 to 1.92.0 2026-01-14 12:10:53 +02:00
cuiandGitHub b23122d002 Remove unused comment in ebuckets.c (#14694) 2026-01-14 14:25:58 +08:00
Filipe Oliveira (Redis)andGitHub 3c96680cfb Enable hardware clock by default on ARM AArch64. (#14676)
Redis can already use a processor-provided hardware counter as a
high-performance monotonic clock. On some architectures this must be
enabled carefully, but on ARM AArch64 the situation is different:

- The ARM Generic Timer is architecturally mandatory for all processors
that implement the AArch64 execution state.
- The system counter (`CNTVCT_EL0`) and its frequency (`CNTFRQ_EL0`) are
guaranteed to exist and provide a monotonic time source (per the *“The
Generic Timer in AArch64 state”* section of the *Arm® Architecture
Reference Manual for Armv8-A* —
https://developer.arm.com/documentation/ddi0487/latest).

Because of this architectural guarantee, it is safe to enable the
hardware clock by default on ARM AArch64.
Like detailed bellow, this gives us around 5% boost on io-thread
deployments for a simple strings benchmark.
2026-01-13 20:12:04 +08:00
Salvatore SanfilippoandGitHub 60a4fa2e4b Vsets: Remove stale note about replication from README. (#14528) 2026-01-13 16:13:59 +08:00
Moti CohenandGitHub cc1660abdd Refactor dict key encoding and fix defrag tag bit bug (#14682)
Introduce encodeEntryKey() helper to centralize key encoding logic for
no_value dicts, replacing 4 instances of duplicated code.

This also fixes a bug in dictDefragBucket() where:
- Before: *bucketref = newkey (loses ENTRY_PTR_IS_EVEN_KEY tag)
- After: *bucketref = encodeEntryKey(d, newkey) (preserves tag bits)

The bug affects dicts with no_value=1 and keys_are_odd=0 when defragKey
callback returns a relocated pointer. Currently theoretical as main DB
dict uses defragKey=NULL.
2026-01-12 13:19:03 +02:00
391530cd15 [Vector sets]: redis-cli recall testing abilities (#14408)
Vector sets have the ability to also ask for ground truth performing an
O(N) scan.
This allows to perform a recall test against any key holding a vector
set, allowing users to verify what is the best EF value to use and how
HNSW performs depending on the data set on a given key (the level of
clustering changes significantly how vectors near/far a cluster will
behave).

---------

Co-authored-by: debing.sun <debing.sun@redis.com>
2026-01-12 12:40:39 +08:00
Vitah LinandGitHub e396dd3385 Fix flaky stream LRM test due to timing precision (#14674) 2026-01-09 10:14:44 +08:00
Yuan WangandGitHub 858a8800e2 Propagate migrate task info to replicas (#14672)
- Allow replicas to track master's migrate task state
Previously, we only propagate import task info to replicas, but now we
also support propagating migrate task info, so the new master can
initiate slots trimming again if needed after failover, this can avoid
data redundancy.

- Prevent replicas from initiating slot trimming actively
Lack of data cleaning mechanism on source side, so we allow replicas to
continue pending slot trimming, but it is not good idea to let replicas
trim actively. As we introduce above feature, we can delete this logic
2026-01-08 19:06:57 +08:00
Moti CohenandGitHub 29f733484a Optimize ZRANK by avoiding string comparisons during skiplist traversal (#14636)
This optimization is based on Valkey valkey-io/valkey#1389

ZRANK no longer performs per-level string comparisons when walking
the skiplist. Instead, it retrieves the skiplist node directly from
the hash table entry via pointer arithmetic.

Rank is computed by walking upward from the node and summing spans
using stored node heights, eliminating costly byte-wise comparisons.

This improves ZRANK throughput by 2–14% depending on score
distribution.

--------- 
Co-authored-by: Ran Shidlansik <ranshid@amazon.com>
2026-01-08 11:20:52 +02:00
Slavomir KaslevandGitHub 5aa47347e7 Fix CLUSTER SLOT-STATS test Lua scripts (#14671)
Fix hard-coded keys in test Lua scripts which is incompatible with
cluster-mode.

Reported-by: Oran Agra <oran@redis.com>
2026-01-08 11:16:50 +02:00
Stav-LeviandGitHub 73249497d4 Fix ACL key-pattern bypass in MSETEX command (#14659)
MSETEX doesn't properly check ACL key permissions for all keys - only
the first key is validated.

MSETEX arguments look like: MSETEX <numkeys> key1 val1 key2 val2 ... EX
seconds

Keys are at every 2nd position (step=2). When Redis extracts keys for
ACL checking, it calculates where the last key is:

last = first + numkeys - 1;        => calculation ignores step
last = first + (numkeys-1) * step; 
With 2 keys starting at position 2:

Bug: last = 2 + 2 - 1 = 3 → only checks position 2
Fix: last = 2 + (2-1)*2 = 4 → checks positions 2 and 4

Fixes #14657
2026-01-08 08:41:55 +02:00
debing.sunandGitHub 85ab4cab58 Fix UBSan error in stream trim when processing last entry (#14669)
## Summary

This bus was introduced by https://github.com/redis/redis/pull/14623

Before PR #14623, when a stream node was going to be fully removed, we
would just delete the whole node directly instead of iterating through
and deleting each entry.

Now, with the XTRIM/XADD flags, we have to iterate and delete entries
one by one. However, the implementation in issue #8169 didn’t consider
the case where all entries are removed, so `p` can end up being NULL.

Fixes an UndefinedBehaviorSanitizer error in `streamTrim()` when marking
the last entry in a listpack as deleted. The issue occurs when
performing pointer arithmetic on a NULL pointer after `lpNext()` reaches
the end of the listpack.

## Solution
If p is NULL, we skip the delta calculation and the calculation of
new `p`.
2026-01-07 20:51:41 +08:00
154fdcee01 Test tcp deadlock fixes (#14667)
**Disclaimer: this patch was created with the help of AI**

My experience with the Redis test not passing on older hardware didn't
stop just with the other PR opened with the same problem. There was
another deadlock happening when the test was writing a lot of commands
without reading it back, and the cause seems related to the fact that
such tests have something in common. They create a deferred client (that
does not read replies at all, if not asked to), flood the server with 1
million of requests without reading anything back. This results in a
networking issue where the TCP socket stops accepting more data, and the
test hangs forever.

To read those replies from time to time allows to run the test on such
older hardware.

Ping oranagra that introduced at least one of the bulk writes tests.
AFAIK there is no problem in the test, if we change it in this way,
since the slave buffer is going to be filled anyway. But better to be
sure that it was not intentional to write all those data without reading
back for some reason I can't see.

IMPORTANT NOTE: **I am NOT sure at all** that the TCP socket senses
congestion in one side and also stops the other side, but anyway this
fix works well and is likely a good idea in general. At the same time, I
doubt there is a pending bug in Redis that makes it hang if the output
buffer is too large, or we are flooding the system with too many
commands without reading anything back. So the actual cause remains
cloudy. I remember that Redis, when the output limit is reached, could
kill the client, and not lower the priority of command processing. Maybe
Oran knows more about this.

## LLM commit message.

The test "slave buffer are counted correctly" was hanging indefinitely
on slow machines. The test sends 1M pipelined commands without reading
responses, which triggers a TCP-level deadlock.

Root cause: When the test client sends commands without reading
responses:
1. Server processes commands and sends responses
2. Client's TCP receive buffer fills (client not reading)
3. Server's TCP send buffer fills
4. Packets get dropped due to buffer pressure
5. TCP congestion control interprets this as network congestion
6. cwnd (congestion window) drops to 1, RTO increases exponentially
7. After multiple backoffs, RTO reaches ~100 seconds
8. Connection becomes effectively frozen

This was confirmed by examining TCP socket state showing cwnd:1,
backoff:9, rto:102912ms, and rwnd_limited:100% on the client side.

The fix interleaves reads with writes by processing responses every
10,000 commands. This prevents TCP buffers from filling to the point
where congestion control triggers the pathological backoff behavior.

The test still validates the same functionality (slave buffer memory
accounting) since the measurement happens after all commands complete.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 14:26:22 +08:00
Moti CohenandGitHub da4c5eec82 Replace fragile dict stored-key API with getKeyId callback (#14646)
This change simplifies the dictionary API for handling stored keys by
replacing the previous dict stored-key mechanism with a cleaner
`keyFromStoredKey` callback approach.
2026-01-06 18:57:28 +02:00
0cb1ee0dc1 New eviction policies - least recently modified (#14624)
### Summary

This PR introduces two new maxmemory eviction policies: `volatile-lrm`
and `allkeys-lrm`.
LRM (Least Recently Modified) is similar to LRU but only updates the
timestamp on write operations, not read operations. This makes it useful
for evicting keys that haven't been modified recently, regardless of how
frequently they are read.

### Core Implementation

The LRM implementation reuses the existing LRU infrastructure but with a
key difference in when timestamps are updated:

- **LRU**: Updates timestamp on both read and write operations
- **LRM**: Updates timestamp only on write operations via `updateLRM()`

### Key changes:
Add `keyModified()` to accept an optional `robj *val` parameter and call
`updateLRM()` when a value is provided. Since `keyModified()` serves as
the unified entry point for all key modifications, placing the LRM
update here ensures timestamps are consistently updated across all write
operations

---------

Co-authored-by: oranagra <oran@redislabs.com>
Co-authored-by: Yuan Wang <yuan.wang@redis.com>
2026-01-06 20:57:31 +08:00
debing.sunandGitHub 9ca860be9e Fix XTRIM/XADD with approx not deletes entries for DELREF/ACKED strategies (#14623)
This bug was introduced by #14130 and found by guybe7 

When using XTRIM/XADD with approx mode (~) and DELREF/ACKED delete
strategies, if a node was eligible for removal but couldn't be removed
directly (because consumer group references need to be checked), the
code would incorrectly break out of the loop instead of continuing to
process entries within the node. This fix allows the per-entry deletion
logic to execute for eligible nodes when using non-KEEPREF strategies.
2026-01-05 21:17:36 +08:00
debing.sunandGitHub 4eda670de9 Fix infinite loop during reverse iteration due to invalid numfields of corrupted stream (#14472)
Follow https://github.com/redis/redis/pull/14423

In https://github.com/redis/redis/pull/14423,
I thought the last lpNext operation of the iterator occurred at the end
of streamIteratorGetID.
However, I overlooked the fact that after calling
`streamIteratorGetID()`, we might still use `streamIteratorGetField()`
to continue moving within the current entry.
This means that during reverse iteration, the iterator could move back
to a previous entry position.

To fix this, in this PR I record the current position at the beginning
of streamIteratorGetID().
When we enter it again next time, we ensure that the entry position does
not exceed the previous one,
that is, during forward iteration the entry must be greater than the
last entry position,
and during reverse iteration it must be smaller than the last entry
position.

Note that the fix for https://github.com/redis/redis/pull/14423 has been
replaced by this fix.
2026-01-05 21:16:53 +08:00
Andy PanandGitHub 7511a1919b Sanitize TCP_KEEPINTVL and simplify TCP_KEEPALIVE_ABORT_THRESHOLD on Solaris (#13142)
This PR fixes a bug on Solaris where setsockopt() fails with EINVAL
when TCP keepalive parameters fall below the kernel's 10-second
minimum. When the user-configured interval is divided by 3 to
calculate TCP_KEEPINTVL, values below 30 seconds result in
intervals less than 10 seconds (e.g., interval=25 → intvl=8),
causing connection failures.

The fix adds if (intvl < 10) intvl = 10; to enforce the minimum and
simplifies the TCP_KEEPALIVE_ABORT_THRESHOLD calculation for older
Solaris versions. This behavior is documented in the Oracle
Solaris TCP manual.
2026-01-05 09:57:33 +02:00
Moti CohenandGitHub 16068d6b63 Fix: Use dictSetKeyAtLink in activeDefragHfieldDictCallback (#14654)
Problem:
The activeDefragHfieldDictCallback was wrongly using dictSetKey() which
set key and value. However, hash field dictionaries use no_value=1
(since PR #14595), causing assertion `assert(!d->type->no_value)` to
fail

Solution:
* Replace `dictSetKey(d, (dictEntry *)de, newEntry)` with
`dictSetKeyAtLink(d, newEntry, &plink, 0)` which properly handles both
regular dictEntry and the optimized `no_value=1` case where keys are
stored directly in the hash table. The callback already receives the
plink parameter pointing to the exact location that needs updating.
* Following PR #14595 value can be now optionally embedded in `entry`.
As a result, `activeDefragEntry()` refines and defragments an entry’s
value only when `entryGetValuePtrRef(entry) != NULL`.
2026-01-04 14:38:08 +02:00
igalperelmanandGitHub ea72406275 Updated readme: added a Redis Cloud paragraph (#14651)
Making Redis Cloud a little more visible in the readme file.
2026-01-04 09:09:14 +02:00
Andy PanandGitHub eb2661a46d Detect accept4() on specific versions of various platforms (#14558)
This PR has mainly done three things:
1. Enable `accept4()` on DragonFlyBSD 4.3+
2. Fix the failures of determining the presence of `accept4()` due to
the missing <sys/param.h> on two OSs: NetBSD, OpenBSD
3. Drop the support of FreeBSD <10.0 for `redis`, FreeBSD 10 is past
EOL, as are the two major versions following it, so defined(__FreeBSD__)
is sufficient.

- [param.h in
DragonFlyBSD](https://github.com/DragonFlyBSD/DragonFlyBSD/blob/7485684fa5c3fadb6c7a1da0d8bb6ea5da4e0f2f/sys/sys/param.h#L129-L257)
- [param.h in
FreeBSD](https://github.com/freebsd/freebsd-src/blob/main/sys/sys/param.h#L46-L76)
- [param.h in
NetBSD](https://github.com/NetBSD/src/blob/b5f8d2f930b7ef226d4dc1b4f7017e998c0e5cde/sys/sys/param.h#L53-L70)
- [param.h in
OpenBSD](https://github.com/openbsd/src/blob/d9c286e032a7cee9baaecdd54eb0d43f658ae696/sys/sys/param.h#L40-L45)

---------

Signed-off-by: Andy Pan <i@andypan.me>
2026-01-04 15:05:07 +08:00
zzjandGitHub 0ef4a4e7e3 Fix some comment spelling typos (#14648) 2026-01-04 10:38:11 +08:00
RoyBenMosheandGitHub 29346eb7dd Hide PII from ACL log (#14645)
This PR continues the work from
[#13400](https://github.com/redis/redis/pull/13400), following the
discussion in
[#11747](https://github.com/redis/redis/pull/11747#discussion_r1094418111),
to further ensure sensitive user data is not exposed in logs when
hide_user_data_from_log is enabled.

- Introduce redactLogCstr() helper for safe, centralized log redaction.
- Update ACL and networking log messages to use redacted values where
appropriate.
- Prevent leaking raw query buffer contents.
2026-01-04 10:35:30 +08:00
Yueyang (Terry) TaoandGitHub 174307530b Expand hash dicts using original length when rdb loading (#14635)
During hash RDB loading (`rdbLoadObject`), the element count `len` is
consumed as entries are read.
In the listpack -> hashtable (HT) spillover path, we later used the
*remaining* `len` for `dictTryExpand`.
By that point `len` may no longer represent the original cardinality
(and can be 0), which can skip/undersize the pre-sizing and lead to
extra rehash/expansion work while loading large hashes.

The same issue existed in the hash-with-metadata (field expire) load
path.
2025-12-26 14:22:40 +08:00
Moti CohenandGitHub e4b69f9a13 Remove dead code leftover (#14640)
Flags defined (mutually exclusive):
plainFlag = flags & RDB_LOAD_PLAIN
sdsFlag = flags & RDB_LOAD_SDS
robjFlag = !(plainFlag || sdsFlag)

If robjFlag is true, the function returns early. Otherwise we are in the
plain/sds path:
plainFlag → allocate with ztrymalloc_usable()
sdsFlag → allocate with sdstrynewlen()

Thus, in error handling only two cases exist:
plainFlag → zfree(buf)
else → sdsFlag → sdsfree(buf)

The hfldFlag branch assumed a third allocation path that no longer exists
after PR #14595, making entryFree(buf, NULL) unreachable.
2025-12-25 14:14:24 +02:00
Stav-LeviandGitHub 860b8c772a Add TLS certificate-based automatic client authentication (#14610)
This PR implements support for automatic client authentication based on
a field in the client's TLS certificate.
We adopt ValKey’s PR: https://github.com/valkey-io/valkey/pull/1920

API Changes:

Add New configuration tls-auth-clients-user  
  -  Allowed values: `off` (default), `CN`.
  - `off` – disable TLS certificate–based auto-authentication.
- `CN` – derive the ACL username from the Common Name (CN) field of the
client certificate.
 
New INFO stat
  - `acl_access_denied_tls_cert`
- Counts failed TLS certificate–based authentication attempts, i.e. TLS
connections where a client certificate was presented, a username was
derived from it, but no matching ACL user was found.

New ACL LOG reason
  - Reason string: `"tls-cert"`
- Emitted when a client certificate’s Common Name fails to match any
existing ACL user.


Implementation Details:

- Added getCertFieldByName() utility to extract fields from peer
certificates.

- Added autoAuthenticateClientFromCert() to handle automatic login logic
post-handshake.

- Integrated automatic authentication into the TLSAccept function after
handshake completion.

- Updated test suite (tests/integration/tls.tcl) to validate the
feature.
2025-12-25 14:07:58 +02:00
itayTzivandGitHub 877c09f662 incrRefCount off-by-one error (#14647)
The condition for blocking `o->refcount++` in `incrRefCount` is `if
(o->refcount < OBJ_FIRST_SPECIAL_REFCOUNT)`, meaning refcount can
accidentally reach the first special refcount (`OBJ_STATIC_REFCOUNT`
currently).
Fixed the condition to be `if (o->refcount < OBJ_FIRST_SPECIAL_REFCOUNT
- 1)`
2025-12-25 18:51:57 +08:00
Moti CohenandGitHub 238a626859 Hash - Unify Field-Value into a single struct along with dict no_value=1 (#14595)
Unifies field–value pairs and optional expiration into a single allocation, 
removing the generic mstr abstraction (mstr was discarded because its overly 
generic design added complexity and runtime overhead without clear benefits 
for hash workloads). The new Entry layout supports embedded values (≤128B) 
and pointer-based values, with expiration metadata integrated for per-field hash 
TTLs. Update hash dictionaries to no_value=1 and apply optimizations to avoid 
regressions. This significantly reduces hash memory usage (~30–50%) with minimal 
performance impact.
2025-12-23 12:19:00 +02:00
fde3576f88 Fix adjacent slot range behavior in ASM operations (#14637)
This PR containts a few changes for ASM:

**Bug fix:** 
- Fixes an issue in ASM when adjacent slot ranges are provided in
CLUSTER MIGRATION IMPORT command (e.g. 0-10 11-100). ASM task keeps the
original slot ranges as given, but later the source node reconstructs
the slot ranges from the config update as a single range (e.g. 0-100).
This causes asmLookupTaskBySlotRangeArray() to fail to match the task,
and the source node incorrectly marks the ASM task as failed. Although
the migration completes successfully, the source node performs a
blocking trim operation for these keys, assuming the slot ownership
changed outside of an ASM operation. With this PR, redis merges adjacent
slot ranges in a slot range array to avoid this problem.
 
 **Other improvements:**
- Indicates imported/migrated key count in the log once asm operation is
completed.
 - Use error return value instead of assert in parseSlotRangesOrReply()
- Validate slot range array that is given by cluster implementation on
ASM_EVENT_IMPORT_START.

---------

Co-authored-by: Yuan Wang <yuan.wang@redis.com>
2025-12-23 11:54:12 +03:00
h.o.t. neglectedandGitHub c5f3d3e11c Fix use-after-free in hnsw_cursor_free (#14627)
Close https://github.com/redis/redis/issues/14626.

Note that this method hasn't been used by any place.
2025-12-22 10:34:50 +08:00
JohnandGitHub 0d5d75e04d Fix incorrect comment about LRU clock resolution in initObjectLRUOrLFU (#14582)
Because the LRU_CLOCK_RESOLUTION macro is 1000 and its comment is LRU
clock resolution in ms
2025-12-20 19:30:15 +08:00
1e974e6311 Fix kvstoreGetFirstNonEmptyDictIndex() and kvstoreIteratorReset() for empty kvstore (#14625)
These bugs was located by @rantidhar 

This PR fixes two related issues in kvstore iterator handling when
dealing with empty kvstores:

1. If the kvstore is empty, kvstoreGetFirstNonEmptyDictIndex() may
return 0. For example, during defragmentation, it may only be when
calling kvstoreGetNextNonEmptyDictIndex() that the invalid slot is
detected. This fix ensures that kvstoreGetFirstNonEmptyDictIndex() will
eventually return -1 and terminate the defragmentation process.
However, currently, when the kvstore is created, the number of
dictionary arrays is at least 1, so this is just a defensive fix.

2. If a kvstoreIterator is initialized but not used by calling
kvstoreIteratorNextDict() before it is released, then during the
kvstoreIteratorReset(), using didx(-1) to access the dictionary array
could lead to an out-of-bounds access. However, in the current code,
there will never be a situation where kvstoreIteratorNextDict() is not
called, so this is just a defensive fix.

---------

Co-authored-by: rantidhar <ran.tidhar@redis.com>
2025-12-19 11:40:01 +08:00
Andy PanandGitHub a9f0f07b7c Merge kqueue events to reduce system calls (#14557)
`kqueue` has the capability of batch applying events:

> The kevent,() kevent64() and kevent_qos() system calls are used to
register events with the queue, and return any pending events to the
     user.  The changelist argument is a pointer to an array of kevent,
kevent64_s or kevent_qos_s structures, as defined in <sys/event.h>. All
changes contained in the changelist are applied before any pending
events
     are read from the queue.  The nchanges argument gives the size of
     changelist.

This PR implements this functionality for `kqueue` with which we're able
to reduce plenty of system calls of `kevent(2)`.

## References

[FreeBSD - kqueue](https://man.freebsd.org/cgi/man.cgi?kqueue)
2025-12-18 19:51:02 +08:00
Yuan WangandGitHub dd67275033 Unify slot migration logs across cluster implementations (#14628)
Using a different cluster implementation instead of the legacy one may
result in inconsistent slot migration logs, which can cause confusion.
Therefore, we should centralize these logs within the slot migration
process itself rather than relying on the specific cluster
implementation.
2025-12-18 18:22:25 +08:00
Moti CohenandGitHub 2e69130ea3 Improve dict pointer tagging doc (#14616)
Clarifies the pointer tagging scheme used in Redis dicts, particularly
for the no_value=1 optimization introduced in #11595.
2025-12-18 09:24:45 +02:00
JohnandGitHub 081693f32e Fix incorrect comment about STATS_METRIC_* Macro in server.h (#14620) 2025-12-18 14:43:05 +08:00
fanpei91andGitHub e6e0cf5764 Fix incorrect stream ID comparison in streamReplyWithRangeFromConsumerPEL() (#14619)
Since all commands that invoke streamReplyWithRange with a group
argument always pass end as NULL, therefore will not trigger incorrect
stream ID comparisons. In other words, even if this bug remains unfixed,
no incident would occur.
2025-12-16 17:00:22 +08:00
33391a7b61 Support delay trimming slots after finishing migrating slots (#14567)
This PR introduces a mechanism that allows a module to temporarily
disable trimming after an ASM migration operation so it can safely
finish ongoing asynchronous jobs that depend on keys in migrating (and
about to be trimmed) slots.

1. **ClusterDisableTrim/ClusterEnableTrim**
We introduce `ClusterDisableTrim/ClusterEnableTrim` Module APIs to allow
module to disable/enable slot migration
    ```
    /* Disable automatic slot trimming. */
    int RM_ClusterDisableTrim(RedisModuleCtx *ctx)

    /* Enable automatic slot trimming */
    int RM_ClusterEnableTrim(RedisModuleCtx *ctx)
    ```

**Please notice**: Redis will not start any subsequent import or migrate
ASM operations while slot trimming is disabled, so modules must
re-enable trimming immediately after completing their pending work.

The only valid and meaningful time for a module to disable trimming
appears to be after the MIGRATE_COMPLETED event.

2. **REDISMODULE_OPEN_KEY_ACCESS_TRIMMED**
Added REDISMODULE_OPEN_KEY_ACCESS_TRIMMED to RM_OpenKey() so that module
can operate with these keys in the unowned slots after trim is paused.

And now we don't delete the key if it is in trim job when we access it.
And `expireIfNeeded` returns `KEY_VALID` if
`EXPIRE_ALLOW_ACCESS_TRIMMED` is set, otherwise, returns `KEY_TRIMMED`
without deleting key.

3. **REDISMODULE_CTX_FLAGS_TRIM_IN_PROGRESS**
We also extend RM_GetContextFlags() to include a flag
REDISMODULE_CTX_FLAGS_TRIM_IN_PROGRESS indicating whether a trimming job
is pending (due to trim pause) or in progress. Modules could
periodically poll this flag to synchronize their internal state, e.g.,
if a trim job was delayed or if the module incorrectly assumed trimming
was still active.

Bugfix: RM_SetClusterFlags could not clear a flag after enabling it first.

---------

Co-authored-by: Ozan Tezcan <ozantezcan@gmail.com>
2025-12-16 16:30:56 +08:00
Rushabh MehtaandGitHub ddbd96d8ae Add --name flag to redis-cli for setting client name (#14588)
​This PR introduces a new flag `--name <client-name>` to `redis-cli`.
This allows users to specify a persistent client name that remains
associated with the connection.

​Implementation Details:
- ​Configuration: Added `client_name` field to the global config struct.
- ​Argument Parsing: Updated `parseOptions` to handle the `--name` flag.
- ​Unified Logic (`cliSetName`): Introduced a helper function cliSetName
that sends `CLIENT SETNAME <name>` immediately after the connection is
established. This ensures the name is set consistently for both RESP2
and RESP3 modes.
- ​Documentation: Updated `redis-cli --help` output to include the new
flag.

This PR can close #14585
2025-12-15 21:43:51 +08:00
Yuan WangandGitHub f3316c3a1a Introduce flushdb option for repl-diskless-load (#14596)
`repl-diskless-load` feature can effectively reduce the time of full
synchronization, but maybe it is not widely used.
`swapdb` option needs double `maxmemory`, and `on-empty-db` only works
on the first full sync (the replica must have no data).

This PR introduce a new option: `flushdb` - Always flush the entire
dataset before diskless load. If the diskless load fails, the replica
will lose all existing data.

Of course, it brings the risk of data loss, but it provides a choice if
you want to reduce full sync time and accept this risk.
2025-12-15 11:25:53 +08:00
Stav-LeviandGitHub 23aca15c8c Fix the flexibility of argument positions in the Redis API's (#14416)
This PR implements flexible keyword-based argument parsing for all 12
hash field expiration commands, allowing users to specify arguments in
any logical order rather than being constrained by rigid positional
requirements.
This enhancement follows Redis's modern design of keyword-based flexible
argument ordering and significantly improves user experience.

Commands with Flexible Parsing
HEXPIRE, HPEXPIRE, HEXPIREAT, HPEXPIREAT, HGETEX, HSETEX

some examples: 
HEXPIRE: 
* All these are equivalent and valid:
HEXPIRE key EX 60 NX FIELDS 2 f1 f2
HEXPIRE key NX EX 60 FIELDS 2 f1 f2  
HEXPIRE key FIELDS 2 f1 f2 EX 60 NX
HEXPIRE key FIELDS 2 f1 f2 NX EX 60
HEXPIRE key NX FIELDS 2 f1 f2 EX 60

HGETEX:
* All these are equivalent and valid:
HGETEX key EX 60 FIELDS 2 f1 f2
HGETEX key FIELDS 2 f1 f2 EX 60

HSETEX:
* All these are equivalent and valid:
HSETEX key FNX EX 60 FIELDS 2 f1 v1 f2 v2
HSETEX key EX 60 FNX FIELDS 2 f1 v1 f2 v2
HSETEX key FIELDS 2 f1 v1 f2 v2 FNX EX 60
HSETEX key FIELDS 2 f1 v1 f2 v2 EX 60 FNX
HSETEX key FNX FIELDS 2 f1 v1 f2 v2 EX 60
2025-12-14 09:35:12 +02:00
Lior KoganandGitHub 9b7254c810 Clarify that BUILD_WITH_MODULES=yes is not supported on 32 bit systems. (#14606)
Following #14618 , This PR Update the readme file
2025-12-11 11:07:47 +02:00
YaacovHazanandGitHub ec84bd6143 Prevent building with modules on 32-bit systems (#14618)
Redis modules do not support 32-bit architectures. The build now fails
early when modules are enabled on such systems.
2025-12-11 11:04:30 +02:00
debing.sunandGitHub 679e009b73 Add daily CI for vectorset (#14302) 2025-12-10 08:52:43 +08:00
Vitah LinandGitHub 4499d68748 Cleanup redundant declaration of getSlotOrReply() (#14576) 2025-12-09 17:58:19 +08:00
3bcacd8a21 Upgrade GitHub Actions macOS runner (#14613)
1. GitHub has deprecated older macOS runners, and macos-13 is no longer
supported. Updating to macos-26 ensures that CI workflows continue to
run without interruption.
2. Previously, cross-platform-actions/action@v0.22.0 used runs-on:
macos-13. I checked the latest version of cross-platform-actions, and
the official examples now use runs-on: ubuntu. I think we can switch
from macOS to Ubuntu.

---------

Co-authored-by: debing.sun <debing.sun@redis.com>
2025-12-09 15:01:58 +08:00
5299ccf2a9 Add kvstore type and decouple kvstore from its metadata (#14543)
Decouple kvstore from its metadata by introducing `kvstoreType` structure of
callbacks. This resolves the abstraction layer violation of having kvstore
include `server.h` directly.

Move (again) cluster slot statistics to per slot dicts' metadata. The callback
`canFreeDict` is used to prevent freeing empty per slot dicts from losing per
slot statistics.

Co-authored-by: Ran Tidhar <ran.tidhar@redis.com>
2025-12-08 21:12:33 +02:00
dd57b141b9 Clean up lookahead-related code (#14562)
## Summary

Clean up lookahead-related(https://github.com/redis/redis/issues/14440)
code by consolidating slot extraction logic.

## Changes

* Replace `GETSLOT_NOKEYS` with `INVALID_CLUSTER_SLOT`
* Refactor `getSlotFromCommand()` to reuse `extractSlotFromKeysResult()`
* Let extractSlotFromKeysResult () behavior more unified and more
readable
* Fix comment alignment

---------

Co-authored-by: Ozan Tezcan <ozantezcan@gmail.com>
2025-12-08 14:47:39 +08:00
cb71dec0c3 Disable RDB compression when diskless replication is used (#14575)
Fixes #14538

If the master uses diskless synchronization and the replica uses
diskless load, we can disable RDB compression to reduce full sync time.
I tested on AWS and found we could reduce time by 20-40%.

In terms of implementation, when the replica can use diskless load, the
replica will send `replconf rdb-no-compress 1` to master to deliver a
RDB without compression.

If your network is slow, please disable repl-diskless-load, and maybe
even repl-diskless-sync

---------

Co-authored-by: Ozan Tezcan <ozantezcan@gmail.com>
2025-12-04 09:24:23 +08:00
Ozan TezcanandGitHub 08b63b6ceb Fix flaky ASM tests (#14604)
1. Fix "Simple slot migration with write load" by introducing artificial
delay to traffic generator to slow down it for tsan builds. Failed test:
https://github.com/redis/redis/actions/runs/19720942981/job/56503213650

2. Fix "Test RM_ClusterCanAccessKeysInSlot returns false for unowned
slots" by waiting config propagation before checking it on a replica.
Failed test:
https://github.com/redis/redis/actions/runs/19841852142/job/56851802772
2025-12-03 12:12:48 +03:00
Ozan TezcanandGitHub 3c57a8fc92 Retry an ASM import step when the source node is temporarily not ready (#14599)
The cluster implementation may be temporarily unavailable and return an
error to the `ASM_EVENT_MIGRATE_PREP` event to prevent starting a new
migration. Although this is most likely a transient condition, the
source node has no way to distinguish it from a real error, so it must
fail the import attempt and start a new one.

In Redis, failing an attempt is cheap, but in other cluster
implementations it may require cleaning up resources and can cause
unnecessary disruption.

This PR introduces a new `-NOTREADY` error reply for the `CLUSTER
SYNCSLOTS SYNC` command. When the source replies with `-NOTREADY`, the
destination can recognize the condition as transient and retry sending
`CLUSTER SYNCSLOTS SYNC` step periodically instead of failing the
attempt.
2025-12-02 13:38:22 +03:00
Ozan TezcanandGitHub 86c63588b0 Refactor some of ASM and slot-stats functions (#14587)
This PR does not introduce any behavioral changes.

- Refactored and moved verifyClusterConfigWithData() into cluster.c.
- Refactored and centralized ASM and slot-stats initialization
functions.

These changes place shared logic in a common location so it can be
reused by different cluster implementations.
2025-11-29 22:41:58 +03:00
Oran AgraandGitHub f56e0115fb Fix rare server hang at shutdown (#14581)
In case we have to kill an rdb child at shutdown, we wait for the child
process to exit, and then resume with the shutodwn, and we did not clear
the child_pid variable, since we're going to terminate anyway. but if
the shutdown is then aborted due to another issue further down that
function, we will try to kill that child again, and the waitpid will
never get released.

Reproduced in the test "SHUTDOWN can proceed if shutdown command was
with nosave"
2025-11-27 07:59:11 +02:00
Oran AgraandGitHub 82fbf213eb fix test tag leakage that can result in skipping tests (#14572)
some error handling paths didn't remove the tags they added, but most
importantly, if the start_server proc is given the "tags" argument more
than once, on exit, it only removed the last one.

this problem exists in start_cluster in list.tcl, and the result was
that the "external:skip cluster modules" were not removed
2025-11-26 09:13:21 +02:00
Ozan TezcanandGitHub 8d542434bd Fix delayed config broadcast after ASM operations (#14573)
An incorrect constant value prevented the configuration from being
broadcast immediately. As a result, the config was only broadcast later
as part of periodic ping messages, causing other nodes to learn about
the configuration change with a delay.

Introduced by https://github.com/redis/redis/pull/14504.
2025-11-26 08:50:12 +03:00
c8e1e833cd Validate existence of backup directory for cluster backup in redis-cli (#14448)
Currently, there is a `TODO` regarding adding a check if the backup
directory exists in `redis-cli`.

This PR adds a check to improve usability:

- If the backup directory does not exist, the user is informed with an
error message.
- If the specified path exists but is not a directory, an error is now
properly reported.

---------

Co-authored-by: debing.sun <debing.sun@redis.com>
2025-11-25 20:05:33 +08:00
RoyBenMosheandGitHub 39200596f4 SCAN: restore original filter order (#14537)
In #14121, the SCAN filters order was changed, before #14121the order
was - pattern, expiration and type, after #14121pattern became last,
this break change broke the original behavior, which will cause scan
with pattern also to remove the expired keys.
This PR reorders the filters to be consistent with the original behavior
and extends a test to cover this scenario.
2025-11-25 15:30:43 +08:00
lihpandGitHub 0288d70820 Fixes an issue where EXEC checks ACL during AOF loading (#14545)
This PR fixes an issue(#14541) where EXEC’s ACL recheck was still being
performed during AOF loading, that may cause AOF loading failed, if ACL
rules are changed and don't allow some commands in MULTI-EXEC.
2025-11-22 11:52:31 +08:00
Ozan TezcanandGitHub 5ee05d8de4 Broadcast config change immediately to the other nodes in cluster (#14504)
Broadcast configuration changes immediately to the other cluster nodes.
This ensures that all nodes are aware of configuration updates and the
node’s latest epoch immediately, preventing subsequent messages from
being incorrectly rejected as stale.

This issue was observed in an Atomic Slot Migration (ASM) scenario:
- Node B joins the cluster, but there is a config epoch collision with
Node A.
- Node A increments its epoch but has not yet broadcast the new
configuration.
- Meanwhile, Node B starts an import operation. When it finishes, Node B
bumps its epoch and broadcasts the new configuration immediately.
- However, since Node A already has a higher epoch, it ignores Node B’s
update, causing the import operation to fail.
2025-11-22 03:38:29 +03:00
bb6389e823 Fix min_cgroup_last_id cache not updated when destroying consumer group (#14552)
## Problem

When destroying a consumer group with `XGROUP DESTROY`, the cached
`min_cgroup_last_id` was not being invalidated. This caused incorrect
behavior when using `XDELEX` with the `ACKED` option, as the cache still
referenced the destroyed group's `last_id`.

## Solution

Invalidate the `min_cgroup_last_id` cache when the destroyed group's
`last_id` equals the cached minimum. The cache will be recalculated on
the next call to `streamEntryIsReferenced()`.

---------

Co-authored-by: guybe7 <guy.benoish@redislabs.com>
2025-11-21 22:37:17 +08:00
Slavomir KaslevandGitHub 1102415f46 Avoid allocation when iterating over hashes, lists, sets and kvstores (#14473)
The PR is follow up on #14200 where we prefer storing iterators on the stack
rather than allocating on the heap. Here we continue this for iterators over
hashes, lists, sets and kvstores.

Quicklist's iterators are still using heap allocation and will be addressed
soon. The reason is that `NULL` is perfectly valid quicklist iterator value and
handling this would be better reviewed separately from the mostly mechanical
changes here.
2025-11-21 12:28:26 +02:00
Ozan TezcanandGitHub b632e9df6a Fix flaky ASM write load test (#14551)
Extend write pause timeout to stabilize ASM write load test under TSAN.

Failing test for reference:
https://github.com/redis/redis/actions/runs/19520561209/job/55882882951
2025-11-21 12:18:28 +03:00
Yuan WangandGitHub 7a3cb3b4b3 Fix CI flaky tests (#14531)
- https://github.com/redis/redis/actions/runs/19200504999/job/54887625884
   avoid calling `start_write_load` before pausing the destination node

- https://github.com/redis/redis/actions/runs/18958533020/job/54140746904
maybe the replica did not sync with master, then the replica did not update the counter
2025-11-19 17:10:57 +08:00
Mincho PaskalevandGitHub 837b14c89a Fix ASan Daily (#14527)
After https://github.com/redis/redis/pull/14226 module tests started
running with ASan enabled.

`auth.c` blocks the user on auth and spawns a thread that sleeps for
0.5s before unblocking the client and returning.

A tcl tests unloads the module which may happen just after the spawned
thread unblocks the client. In that case if the unloading finishes fast
enough the spawned thread may try to execute code from the module's
dynamic library that is already unloaded resulting in sefault.

Fix: just wait on the thread during module's OnUnload method.
2025-11-19 10:56:18 +02:00
alonre24andGitHub 42f36755ed Update RediSearch to v8.4.2 (#14542)
[8.4] Validate CLUSTER SHARDS Reply - [MOD-12432]
(https://github.com/RediSearch/RediSearch/commit/b6301f5c7b02cca98dfed000878c39d6a4833406)
[8.4] Fix TLS config check - [MOD-12408]
(https://github.com/RediSearch/RediSearch/commit/6606e8112bb34265c8bafd6575ff77a67b612d80)

[MOD-12432]:
https://redislabs.atlassian.net/browse/MOD-12432?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
[MOD-12408]:
https://redislabs.atlassian.net/browse/MOD-12408?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-11-17 20:48:32 +02:00
342 changed files with 57819 additions and 9753 deletions
+15 -12
View File
@@ -2,12 +2,16 @@ name: CI
on: [push, pull_request]
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.head_ref || github.ref }}
cancel-in-progress: true
jobs:
test-ubuntu-latest:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: make
# Fail build if there are warnings
# build with TLS just for compilation coverage
@@ -26,7 +30,7 @@ jobs:
test-sanitizer-address:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: make
# build with TLS module just for compilation coverage
run: make SANITIZER=address REDIS_CFLAGS='-Werror -DDEBUG_ASSERTIONS -DREDIS_TEST' BUILD_TLS=module
@@ -39,7 +43,7 @@ jobs:
runs-on: ubuntu-latest
container: debian:buster
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: make
run: |
sed -i 's|http://deb.debian.org/debian|http://archive.debian.org/debian|g' /etc/apt/sources.list
@@ -50,7 +54,7 @@ jobs:
build-macos-latest:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: make
# Fail build if there are warnings
# build with TLS just for compilation coverage
@@ -59,16 +63,16 @@ jobs:
build-32bit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: make
run: |
sudo apt-get update && sudo apt-get install libc6-dev-i386 gcc-multilib g++-multilib
sudo apt-get update && sudo apt-get install libc6-dev-i386 gcc-multilib
make REDIS_CFLAGS='-Werror' 32bit
build-libc-malloc:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: make
run: make REDIS_CFLAGS='-Werror' MALLOC=libc
@@ -76,17 +80,17 @@ jobs:
runs-on: ubuntu-latest
container: quay.io/centos/centos:stream9
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: make
run: |
dnf -y install which gcc gcc-c++ make
dnf -y install which gcc make
make REDIS_CFLAGS='-Werror'
build-old-chain-jemalloc:
runs-on: ubuntu-latest
container: ubuntu:20.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: make
run: |
apt-get update
@@ -96,7 +100,6 @@ jobs:
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 40976EAF437D05B5
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 3B4FE6ACC0B21F32
apt-get update
apt-get install -y make gcc-4.8 g++-4.8
apt-get install -y make gcc-4.8
update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-4.8 100
update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-4.8 100
make CC=gcc REDIS_CFLAGS='-Werror'
+17 -5
View File
@@ -4,13 +4,21 @@ name: "Codecov"
# where each PR needs to be compared against the coverage of the head commit
on: [push, pull_request]
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.head_ref || github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
code-coverage:
runs-on: ubuntu-22.04
if: ${{ github.repository == 'redis/redis' }}
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Install lcov and run test
run: |
@@ -18,7 +26,11 @@ jobs:
make lcov
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
token: ${{ secrets.CODECOV_TOKEN }}
file: ./src/redis.info
files: ./src/redis.info
disable_search: true
fail_ci_if_error: true
+8 -4
View File
@@ -6,6 +6,10 @@ on:
# run weekly new vulnerability was added to the database
- cron: '0 0 * * 0'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.head_ref || github.ref }}
cancel-in-progress: true
jobs:
analyze:
name: Analyze
@@ -19,15 +23,15 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
- name: Autobuild
uses: github/codeql-action/autobuild@v3
uses: github/codeql-action/autobuild@v4
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
uses: github/codeql-action/analyze@v4
+14 -4
View File
@@ -6,17 +6,24 @@ on:
- cron: '0 0 * * *'
# Support manual execution
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.head_ref || github.ref }}
cancel-in-progress: true
jobs:
coverity:
if: github.repository == 'redis/redis'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@main
- uses: actions/checkout@v6
- name: Download and extract the Coverity Build Tool
run: |
wget -q https://scan.coverity.com/download/cxx/linux64 --post-data "token=${{ secrets.COVERITY_SCAN_TOKEN }}&project=redis-unstable" -O cov-analysis-linux64.tar.gz
wget -q https://scan.coverity.com/download/cxx/linux64 --post-data "token=${COVERITY_SCAN_TOKEN}&project=redis-unstable" -O cov-analysis-linux64.tar.gz
mkdir cov-analysis-linux64
tar xzf cov-analysis-linux64.tar.gz --strip 1 -C cov-analysis-linux64
env:
COVERITY_SCAN_TOKEN: ${{ secrets.COVERITY_SCAN_TOKEN }}
- name: Install Redis dependencies
run: sudo apt install -y gcc tcl8.6 tclx procps libssl-dev
- name: Build with cov-build
@@ -26,7 +33,10 @@ jobs:
tar czvf cov-int.tgz cov-int
curl \
--form project=redis-unstable \
--form email=${{ secrets.COVERITY_SCAN_EMAIL }} \
--form token=${{ secrets.COVERITY_SCAN_TOKEN }} \
--form email="${COVERITY_SCAN_EMAIL}" \
--form token="${COVERITY_SCAN_TOKEN}" \
--form file=@cov-int.tgz \
https://scan.coverity.com/builds
env:
COVERITY_SCAN_EMAIL: ${{ secrets.COVERITY_SCAN_EMAIL }}
COVERITY_SCAN_TOKEN: ${{ secrets.COVERITY_SCAN_TOKEN }}
+278 -95
View File
@@ -11,7 +11,7 @@ on:
inputs:
skipjobs:
description: 'jobs to skip (delete the ones you wanna keep, do not leave empty)'
default: 'valgrind,sanitizer,tls,freebsd,macos,alpine,32bit,iothreads,ubuntu,centos,malloc,specific,fortify,reply-schema,oldTC,defrag'
default: 'valgrind,sanitizer,tls,freebsd,macos,alpine,32bit,iothreads,ubuntu,centos,malloc,specific,fortify,reply-schema,oldTC,defrag,vectorset,assert-keyspace,arm'
skiptests:
description: 'tests to skip (delete the ones you wanna keep, do not leave empty)'
default: 'redis,modules,sentinel,cluster,unittest'
@@ -36,7 +36,7 @@ jobs:
if: |
(github.event_name == 'workflow_dispatch' || (github.event_name != 'workflow_dispatch' && github.repository == 'redis/redis')) &&
!contains(github.event.inputs.skipjobs, 'ubuntu')
timeout-minutes: 14400
timeout-minutes: 360
steps:
- name: prep
if: github.event_name == 'workflow_dispatch'
@@ -47,12 +47,12 @@ jobs:
echo "skiptests: ${{github.event.inputs.skiptests}}"
echo "test_args: ${{github.event.inputs.test_args}}"
echo "cluster_test_args: ${{github.event.inputs.cluster_test_args}}"
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
repository: ${{ env.GITHUB_REPOSITORY }}
ref: ${{ env.GITHUB_HEAD_REF }}
- name: make
run: make REDIS_CFLAGS='-Werror -DREDIS_TEST'
run: make REDIS_CFLAGS='-Werror -DREDIS_TEST -DDEBUG_ASSERTIONS'
- name: testprep
run: sudo apt-get install tcl8.6 tclx
- name: test
@@ -68,12 +68,129 @@ jobs:
if: true && !contains(github.event.inputs.skiptests, 'unittest')
run: ./src/redis-server test all --accurate
test-ubuntu-jemalloc-fortify:
test-ubuntu-arm:
runs-on: ubuntu-24.04-arm
if: |
(github.event_name == 'workflow_dispatch' || (github.event_name != 'workflow_dispatch' && github.repository == 'redis/redis')) &&
!contains(github.event.inputs.skipjobs, 'arm')
timeout-minutes: 360
steps:
- name: prep
if: github.event_name == 'workflow_dispatch'
run: |
echo "GITHUB_REPOSITORY=${{github.event.inputs.use_repo}}" >> $GITHUB_ENV
echo "GITHUB_HEAD_REF=${{github.event.inputs.use_git_ref}}" >> $GITHUB_ENV
echo "skipjobs: ${{github.event.inputs.skipjobs}}"
echo "skiptests: ${{github.event.inputs.skiptests}}"
echo "test_args: ${{github.event.inputs.test_args}}"
echo "cluster_test_args: ${{github.event.inputs.cluster_test_args}}"
- uses: actions/checkout@v6
with:
repository: ${{ env.GITHUB_REPOSITORY }}
ref: ${{ env.GITHUB_HEAD_REF }}
- name: make
run: make REDIS_CFLAGS='-Werror -DREDIS_TEST'
- name: testprep
run: |
sudo apt-get update
sudo apt-get install -y tcl8.6 tclx
- name: test
if: true && !contains(github.event.inputs.skiptests, 'redis')
run: ./runtest --accurate --verbose --dump-logs ${{github.event.inputs.test_args}}
- name: sentinel tests
if: true && !contains(github.event.inputs.skiptests, 'sentinel')
run: ./runtest-sentinel ${{github.event.inputs.cluster_test_args}}
- name: cluster tests
if: true && !contains(github.event.inputs.skiptests, 'cluster')
run: ./runtest-cluster ${{github.event.inputs.cluster_test_args}}
- name: unittest
if: true && !contains(github.event.inputs.skiptests, 'unittest')
run: ./src/redis-server test all --accurate
test-ubuntu-arm-libc-malloc:
runs-on: ubuntu-24.04-arm
if: |
(github.event_name == 'workflow_dispatch' || (github.event_name != 'workflow_dispatch' && github.repository == 'redis/redis')) &&
(!contains(github.event.inputs.skipjobs, 'arm') || !contains(github.event.inputs.skipjobs, 'malloc'))
timeout-minutes: 360
steps:
- name: prep
if: github.event_name == 'workflow_dispatch'
run: |
echo "GITHUB_REPOSITORY=${{github.event.inputs.use_repo}}" >> $GITHUB_ENV
echo "GITHUB_HEAD_REF=${{github.event.inputs.use_git_ref}}" >> $GITHUB_ENV
echo "skipjobs: ${{github.event.inputs.skipjobs}}"
echo "skiptests: ${{github.event.inputs.skiptests}}"
echo "test_args: ${{github.event.inputs.test_args}}"
echo "cluster_test_args: ${{github.event.inputs.cluster_test_args}}"
- uses: actions/checkout@v6
with:
repository: ${{ env.GITHUB_REPOSITORY }}
ref: ${{ env.GITHUB_HEAD_REF }}
- name: make
run: make MALLOC=libc REDIS_CFLAGS='-Werror -DREDIS_TEST'
- name: testprep
run: |
sudo apt-get update
sudo apt-get install -y tcl8.6 tclx
- name: test
if: true && !contains(github.event.inputs.skiptests, 'redis')
run: ./runtest --accurate --verbose --dump-logs ${{github.event.inputs.test_args}}
- name: sentinel tests
if: true && !contains(github.event.inputs.skiptests, 'sentinel')
run: ./runtest-sentinel ${{github.event.inputs.cluster_test_args}}
- name: cluster tests
if: true && !contains(github.event.inputs.skiptests, 'cluster')
run: ./runtest-cluster ${{github.event.inputs.cluster_test_args}}
test-ubuntu-arm-tls:
runs-on: ubuntu-24.04-arm
if: |
(github.event_name == 'workflow_dispatch' || (github.event_name != 'workflow_dispatch' && github.repository == 'redis/redis')) &&
(!contains(github.event.inputs.skipjobs, 'arm') || !contains(github.event.inputs.skipjobs, 'tls'))
timeout-minutes: 360
steps:
- name: prep
if: github.event_name == 'workflow_dispatch'
run: |
echo "GITHUB_REPOSITORY=${{github.event.inputs.use_repo}}" >> $GITHUB_ENV
echo "GITHUB_HEAD_REF=${{github.event.inputs.use_git_ref}}" >> $GITHUB_ENV
echo "skipjobs: ${{github.event.inputs.skipjobs}}"
echo "skiptests: ${{github.event.inputs.skiptests}}"
echo "test_args: ${{github.event.inputs.test_args}}"
echo "cluster_test_args: ${{github.event.inputs.cluster_test_args}}"
- uses: actions/checkout@v6
with:
repository: ${{ env.GITHUB_REPOSITORY }}
ref: ${{ env.GITHUB_HEAD_REF }}
- name: make
run: make BUILD_TLS=yes REDIS_CFLAGS='-Werror -DREDIS_TEST'
- name: testprep
run: |
sudo apt-get update
sudo apt-get install -y tcl8.6 tclx tcl-tls
./utils/gen-test-certs.sh
- name: test
if: true && !contains(github.event.inputs.skiptests, 'redis')
run: ./runtest --accurate --verbose --tls --dump-logs ${{github.event.inputs.test_args}}
- name: sentinel tests
if: true && !contains(github.event.inputs.skiptests, 'sentinel')
run: ./runtest-sentinel --tls ${{github.event.inputs.cluster_test_args}}
- name: cluster tests
if: true && !contains(github.event.inputs.skiptests, 'cluster')
run: ./runtest-cluster --tls ${{github.event.inputs.cluster_test_args}}
# Test with DEBUG_ASSERT_KEYSPACE enabled to verify keyspace consistency.
# This enables additional runtime checks after each command for:
# - Info keysizes histogram
# - Cluster slot stats
# Skips slow and defrag tests to avoid timeouts while maintaining good coverage.
test-debug-assert-keyspace:
runs-on: ubuntu-latest
if: |
(github.event_name == 'workflow_dispatch' || (github.event_name != 'workflow_dispatch' && github.repository == 'redis/redis')) &&
!contains(github.event.inputs.skipjobs, 'fortify')
timeout-minutes: 14400
!contains(github.event.inputs.skipjobs, 'assert-keyspace')
timeout-minutes: 360
steps:
- name: prep
if: github.event_name == 'workflow_dispatch'
@@ -84,14 +201,48 @@ jobs:
echo "skiptests: ${{github.event.inputs.skiptests}}"
echo "test_args: ${{github.event.inputs.test_args}}"
echo "cluster_test_args: ${{github.event.inputs.cluster_test_args}}"
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
repository: ${{ env.GITHUB_REPOSITORY }}
ref: ${{ env.GITHUB_HEAD_REF }}
- name: make
run: make REDIS_CFLAGS='-Werror -DREDIS_TEST -DDEBUG_ASSERT_KEYSPACE -DDEBUG_ASSERTIONS'
- name: testprep
run: sudo apt-get install tcl8.6 tclx
- name: test
if: true && !contains(github.event.inputs.skiptests, 'redis')
run: |
./runtest --verbose --tags "-slow -defrag" \
--dump-logs ${{github.event.inputs.test_args}}
- name: cluster tests
if: true && !contains(github.event.inputs.skiptests, 'cluster')
run: ./runtest-cluster ${{github.event.inputs.cluster_test_args}}
test-ubuntu-jemalloc-fortify:
runs-on: ubuntu-latest
if: |
(github.event_name == 'workflow_dispatch' || (github.event_name != 'workflow_dispatch' && github.repository == 'redis/redis')) &&
!contains(github.event.inputs.skipjobs, 'fortify')
timeout-minutes: 360
steps:
- name: prep
if: github.event_name == 'workflow_dispatch'
run: |
echo "GITHUB_REPOSITORY=${{github.event.inputs.use_repo}}" >> $GITHUB_ENV
echo "GITHUB_HEAD_REF=${{github.event.inputs.use_git_ref}}" >> $GITHUB_ENV
echo "skipjobs: ${{github.event.inputs.skipjobs}}"
echo "skiptests: ${{github.event.inputs.skiptests}}"
echo "test_args: ${{github.event.inputs.test_args}}"
echo "cluster_test_args: ${{github.event.inputs.cluster_test_args}}"
- uses: actions/checkout@v6
with:
repository: ${{ env.GITHUB_REPOSITORY }}
ref: ${{ env.GITHUB_HEAD_REF }}
- name: make
run: |
apt-get update && apt-get install -y make gcc g++
make CC=gcc REDIS_CFLAGS='-Werror -DREDIS_TEST -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3'
apt-get update && apt-get install -y make gcc
# Also enables jemalloc's sized deallocation checks to catch sdallocx()/zfree_with_size() misuse.
make CC=gcc REDIS_CFLAGS='-Werror -DREDIS_TEST -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3' JEMALLOC_CONFIGURE_OPTS='--enable-opt-size-checks'
- name: testprep
run: sudo apt-get install -y tcl8.6 tclx procps
- name: test
@@ -112,7 +263,7 @@ jobs:
if: |
(github.event_name == 'workflow_dispatch' || (github.event_name != 'workflow_dispatch' && github.repository == 'redis/redis')) &&
!contains(github.event.inputs.skipjobs, 'malloc')
timeout-minutes: 14400
timeout-minutes: 360
steps:
- name: prep
if: github.event_name == 'workflow_dispatch'
@@ -123,7 +274,7 @@ jobs:
echo "skiptests: ${{github.event.inputs.skiptests}}"
echo "test_args: ${{github.event.inputs.test_args}}"
echo "cluster_test_args: ${{github.event.inputs.cluster_test_args}}"
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
repository: ${{ env.GITHUB_REPOSITORY }}
ref: ${{ env.GITHUB_HEAD_REF }}
@@ -146,7 +297,7 @@ jobs:
if: |
(github.event_name == 'workflow_dispatch' || (github.event_name != 'workflow_dispatch' && github.repository == 'redis/redis')) &&
!contains(github.event.inputs.skipjobs, 'malloc')
timeout-minutes: 14400
timeout-minutes: 360
steps:
- name: prep
if: github.event_name == 'workflow_dispatch'
@@ -157,7 +308,7 @@ jobs:
echo "skiptests: ${{github.event.inputs.skiptests}}"
echo "test_args: ${{github.event.inputs.test_args}}"
echo "cluster_test_args: ${{github.event.inputs.cluster_test_args}}"
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
repository: ${{ env.GITHUB_REPOSITORY }}
ref: ${{ env.GITHUB_HEAD_REF }}
@@ -180,7 +331,7 @@ jobs:
if: |
(github.event_name == 'workflow_dispatch' || (github.event_name != 'workflow_dispatch' && github.repository == 'redis/redis')) &&
!contains(github.event.inputs.skipjobs, '32bit')
timeout-minutes: 14400
timeout-minutes: 360
steps:
- name: prep
if: github.event_name == 'workflow_dispatch'
@@ -191,13 +342,13 @@ jobs:
echo "skiptests: ${{github.event.inputs.skiptests}}"
echo "test_args: ${{github.event.inputs.test_args}}"
echo "cluster_test_args: ${{github.event.inputs.cluster_test_args}}"
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
repository: ${{ env.GITHUB_REPOSITORY }}
ref: ${{ env.GITHUB_HEAD_REF }}
- name: make
run: |
sudo apt-get update && sudo apt-get install libc6-dev-i386 g++ gcc-multilib g++-multilib
sudo apt-get update && sudo apt-get install libc6-dev-i386 gcc-multilib
make 32bit REDIS_CFLAGS='-Werror -DREDIS_TEST'
make -C tests/modules 32bit # the script below doesn't have an argument, we must build manually ahead of time
- name: testprep
@@ -220,7 +371,7 @@ jobs:
if: |
(github.event_name == 'workflow_dispatch' || (github.event_name != 'workflow_dispatch' && github.repository == 'redis/redis')) &&
!contains(github.event.inputs.skipjobs, 'tls')
timeout-minutes: 14400
timeout-minutes: 360
steps:
- name: prep
if: github.event_name == 'workflow_dispatch'
@@ -231,7 +382,7 @@ jobs:
echo "skiptests: ${{github.event.inputs.skiptests}}"
echo "test_args: ${{github.event.inputs.test_args}}"
echo "cluster_test_args: ${{github.event.inputs.cluster_test_args}}"
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
repository: ${{ env.GITHUB_REPOSITORY }}
ref: ${{ env.GITHUB_HEAD_REF }}
@@ -260,7 +411,7 @@ jobs:
if: |
(github.event_name == 'workflow_dispatch' || (github.event_name != 'workflow_dispatch' && github.repository == 'redis/redis')) &&
!contains(github.event.inputs.skipjobs, 'tls')
timeout-minutes: 14400
timeout-minutes: 360
steps:
- name: prep
if: github.event_name == 'workflow_dispatch'
@@ -271,7 +422,7 @@ jobs:
echo "skiptests: ${{github.event.inputs.skiptests}}"
echo "test_args: ${{github.event.inputs.test_args}}"
echo "cluster_test_args: ${{github.event.inputs.cluster_test_args}}"
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
repository: ${{ env.GITHUB_REPOSITORY }}
ref: ${{ env.GITHUB_HEAD_REF }}
@@ -300,7 +451,7 @@ jobs:
if: |
(github.event_name == 'workflow_dispatch' || (github.event_name != 'workflow_dispatch' && github.repository == 'redis/redis')) &&
!contains(github.event.inputs.skipjobs, 'iothreads')
timeout-minutes: 14400
timeout-minutes: 360
steps:
- name: prep
if: github.event_name == 'workflow_dispatch'
@@ -311,7 +462,7 @@ jobs:
echo "skiptests: ${{github.event.inputs.skiptests}}"
echo "test_args: ${{github.event.inputs.test_args}}"
echo "cluster_test_args: ${{github.event.inputs.cluster_test_args}}"
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
repository: ${{ env.GITHUB_REPOSITORY }}
ref: ${{ env.GITHUB_HEAD_REF }}
@@ -322,7 +473,7 @@ jobs:
run: sudo apt-get install tcl8.6 tclx
- name: test
if: true && !contains(github.event.inputs.skiptests, 'redis')
run: ./runtest --config io-threads 4 --accurate --verbose --tags network --dump-logs ${{github.event.inputs.test_args}}
run: ./runtest --config io-threads 4 --accurate --verbose --tags "network iothreads psync2 repl failover" --dump-logs ${{github.event.inputs.test_args}}
- name: cluster tests
if: true && !contains(github.event.inputs.skiptests, 'cluster')
run: ./runtest-cluster --config io-threads 4 ${{github.event.inputs.cluster_test_args}}
@@ -332,7 +483,7 @@ jobs:
if: |
(github.event_name == 'workflow_dispatch' || (github.event_name != 'workflow_dispatch' && github.repository == 'redis/redis')) &&
!contains(github.event.inputs.skipjobs, 'specific')
timeout-minutes: 14400
timeout-minutes: 360
steps:
- name: prep
if: github.event_name == 'workflow_dispatch'
@@ -343,7 +494,7 @@ jobs:
echo "skiptests: ${{github.event.inputs.skiptests}}"
echo "test_args: ${{github.event.inputs.test_args}}"
echo "cluster_test_args: ${{github.event.inputs.cluster_test_args}}"
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
repository: ${{ env.GITHUB_REPOSITORY }}
ref: ${{ env.GITHUB_HEAD_REF }}
@@ -410,7 +561,7 @@ jobs:
if: |
(github.event_name == 'workflow_dispatch' || (github.event_name != 'workflow_dispatch' && github.repository == 'redis/redis')) &&
!contains(github.event.inputs.skipjobs, 'valgrind') && !contains(github.event.inputs.skiptests, 'redis')
timeout-minutes: 14400
timeout-minutes: 360
steps:
- name: prep
if: github.event_name == 'workflow_dispatch'
@@ -421,7 +572,7 @@ jobs:
echo "skiptests: ${{github.event.inputs.skiptests}}"
echo "test_args: ${{github.event.inputs.test_args}}"
echo "cluster_test_args: ${{github.event.inputs.cluster_test_args}}"
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
repository: ${{ env.GITHUB_REPOSITORY }}
ref: ${{ env.GITHUB_HEAD_REF }}
@@ -430,17 +581,20 @@ jobs:
- name: testprep
run: |
sudo apt-get update
sudo apt-get install tcl8.6 tclx valgrind g++ -y
sudo apt-get install tcl8.6 tclx valgrind -y
- name: test
if: true && !contains(github.event.inputs.skiptests, 'redis')
run: ./runtest --valgrind --no-latency --verbose --clients 1 --timeout 2400 --dump-logs ${{github.event.inputs.test_args}}
# Note that valgrind's overhead doesn't pair well with io-threads so we
# explicitly disable tests tagged with 'iothreads' - these are tests that
# always run with io-threads enabled.
run: ./runtest --valgrind --no-latency --verbose --clients 1 --timeout 2400 --tags -iothreads --dump-logs ${{github.event.inputs.test_args}}
test-valgrind-misc:
runs-on: ubuntu-latest
if: |
(github.event_name == 'workflow_dispatch' || (github.event_name != 'workflow_dispatch' && github.repository == 'redis/redis')) &&
!contains(github.event.inputs.skipjobs, 'valgrind') && !(contains(github.event.inputs.skiptests, 'modules') && contains(github.event.inputs.skiptests, 'unittest'))
timeout-minutes: 14400
timeout-minutes: 360
steps:
- name: prep
if: github.event_name == 'workflow_dispatch'
@@ -451,7 +605,7 @@ jobs:
echo "skiptests: ${{github.event.inputs.skiptests}}"
echo "test_args: ${{github.event.inputs.test_args}}"
echo "cluster_test_args: ${{github.event.inputs.cluster_test_args}}"
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
repository: ${{ env.GITHUB_REPOSITORY }}
ref: ${{ env.GITHUB_HEAD_REF }}
@@ -472,7 +626,7 @@ jobs:
if: |
(github.event_name == 'workflow_dispatch' || (github.event_name != 'workflow_dispatch' && github.repository == 'redis/redis')) &&
!contains(github.event.inputs.skipjobs, 'valgrind') && !contains(github.event.inputs.skiptests, 'redis')
timeout-minutes: 14400
timeout-minutes: 360
steps:
- name: prep
if: github.event_name == 'workflow_dispatch'
@@ -483,7 +637,7 @@ jobs:
echo "skiptests: ${{github.event.inputs.skiptests}}"
echo "test_args: ${{github.event.inputs.test_args}}"
echo "cluster_test_args: ${{github.event.inputs.cluster_test_args}}"
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
repository: ${{ env.GITHUB_REPOSITORY }}
ref: ${{ env.GITHUB_HEAD_REF }}
@@ -492,17 +646,17 @@ jobs:
- name: testprep
run: |
sudo apt-get update
sudo apt-get install tcl8.6 tclx valgrind g++ -y
sudo apt-get install tcl8.6 tclx valgrind -y
- name: test
if: true && !contains(github.event.inputs.skiptests, 'redis')
run: ./runtest --valgrind --no-latency --verbose --clients 1 --timeout 2400 --dump-logs ${{github.event.inputs.test_args}}
run: ./runtest --valgrind --tags -iothreads --no-latency --verbose --clients 1 --timeout 2400 --dump-logs ${{github.event.inputs.test_args}}
test-valgrind-no-malloc-usable-size-misc:
runs-on: ubuntu-latest
if: |
(github.event_name == 'workflow_dispatch' || (github.event_name != 'workflow_dispatch' && github.repository == 'redis/redis')) &&
!contains(github.event.inputs.skipjobs, 'valgrind') && !(contains(github.event.inputs.skiptests, 'modules') && contains(github.event.inputs.skiptests, 'unittest'))
timeout-minutes: 14400
timeout-minutes: 360
steps:
- name: prep
if: github.event_name == 'workflow_dispatch'
@@ -513,7 +667,7 @@ jobs:
echo "skiptests: ${{github.event.inputs.skiptests}}"
echo "test_args: ${{github.event.inputs.test_args}}"
echo "cluster_test_args: ${{github.event.inputs.cluster_test_args}}"
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
repository: ${{ env.GITHUB_REPOSITORY }}
ref: ${{ env.GITHUB_HEAD_REF }}
@@ -534,7 +688,7 @@ jobs:
if: |
(github.event_name == 'workflow_dispatch' || (github.event_name != 'workflow_dispatch' && github.repository == 'redis/redis')) &&
!contains(github.event.inputs.skipjobs, 'sanitizer')
timeout-minutes: 14400
timeout-minutes: 360
strategy:
matrix:
compiler: [ gcc, clang ]
@@ -550,7 +704,7 @@ jobs:
echo "skiptests: ${{github.event.inputs.skiptests}}"
echo "test_args: ${{github.event.inputs.test_args}}"
echo "cluster_test_args: ${{github.event.inputs.cluster_test_args}}"
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
repository: ${{ env.GITHUB_REPOSITORY }}
ref: ${{ env.GITHUB_HEAD_REF }}
@@ -578,9 +732,9 @@ jobs:
if: |
(github.event_name == 'workflow_dispatch' || (github.event_name != 'workflow_dispatch' && github.repository == 'redis/redis')) &&
!contains(github.event.inputs.skipjobs, 'sanitizer')
timeout-minutes: 14400
timeout-minutes: 360
env:
CC: clang # MSan work only with clang
CC: clang # MSan works only with clang
steps:
- name: prep
if: github.event_name == 'workflow_dispatch'
@@ -591,7 +745,7 @@ jobs:
echo "skiptests: ${{github.event.inputs.skiptests}}"
echo "test_args: ${{github.event.inputs.test_args}}"
echo "cluster_test_args: ${{github.event.inputs.cluster_test_args}}"
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
repository: ${{ env.GITHUB_REPOSITORY }}
ref: ${{ env.GITHUB_HEAD_REF }}
@@ -619,7 +773,7 @@ jobs:
if: |
(github.event_name == 'workflow_dispatch' || (github.event_name != 'workflow_dispatch' && github.repository == 'redis/redis')) &&
!contains(github.event.inputs.skipjobs, 'sanitizer')
timeout-minutes: 14400
timeout-minutes: 360
strategy:
matrix:
compiler: [ gcc, clang ]
@@ -635,7 +789,7 @@ jobs:
echo "skiptests: ${{github.event.inputs.skiptests}}"
echo "test_args: ${{github.event.inputs.test_args}}"
echo "cluster_test_args: ${{github.event.inputs.cluster_test_args}}"
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
repository: ${{ env.GITHUB_REPOSITORY }}
ref: ${{ env.GITHUB_HEAD_REF }}
@@ -663,8 +817,9 @@ jobs:
if: |
(github.event_name == 'workflow_dispatch' || (github.event_name != 'workflow_dispatch' && github.repository == 'redis/redis')) &&
!contains(github.event.inputs.skipjobs, 'sanitizer')
timeout-minutes: 14400
timeout-minutes: 360
strategy:
fail-fast: false # let gcc and clang both run until the end even if one of them fails
matrix:
compiler: [ gcc, clang ]
env:
@@ -679,7 +834,7 @@ jobs:
echo "skiptests: ${{github.event.inputs.skiptests}}"
echo "test_args: ${{github.event.inputs.test_args}}"
echo "cluster_test_args: ${{github.event.inputs.cluster_test_args}}"
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
repository: ${{ env.GITHUB_REPOSITORY }}
ref: ${{ env.GITHUB_HEAD_REF }}
@@ -696,7 +851,7 @@ jobs:
run: ./runtest --tsan --clients 1 --config io-threads 4 --accurate --verbose --dump-logs ${{github.event.inputs.test_args}}
- name: sentinel tests
if: true && !contains(github.event.inputs.skiptests, 'sentinel')
run: ./runtest-sentinel --config io-threads 2 ${{github.event.inputs.cluster_test_args}}
run: ./runtest-sentinel --tsan ${{github.event.inputs.cluster_test_args}}
- name: cluster tests
if: true && !contains(github.event.inputs.skiptests, 'cluster')
run: ./runtest-cluster --config io-threads 2 ${{github.event.inputs.cluster_test_args}}
@@ -707,7 +862,7 @@ jobs:
(github.event_name == 'workflow_dispatch' || (github.event_name != 'workflow_dispatch' && github.repository == 'redis/redis')) &&
!contains(github.event.inputs.skipjobs, 'centos')
container: quay.io/centos/centos:stream9
timeout-minutes: 14400
timeout-minutes: 360
steps:
- name: prep
if: github.event_name == 'workflow_dispatch'
@@ -718,13 +873,13 @@ jobs:
echo "skiptests: ${{github.event.inputs.skiptests}}"
echo "test_args: ${{github.event.inputs.test_args}}"
echo "cluster_test_args: ${{github.event.inputs.cluster_test_args}}"
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
repository: ${{ env.GITHUB_REPOSITORY }}
ref: ${{ env.GITHUB_HEAD_REF }}
- name: make
run: |
dnf -y install which gcc make g++
dnf -y install which gcc make
make REDIS_CFLAGS='-Werror'
- name: testprep
run: |
@@ -746,7 +901,7 @@ jobs:
(github.event_name == 'workflow_dispatch' || (github.event_name != 'workflow_dispatch' && github.repository == 'redis/redis')) &&
!contains(github.event.inputs.skipjobs, 'tls')
container: quay.io/centos/centos:stream9
timeout-minutes: 14400
timeout-minutes: 360
steps:
- name: prep
if: github.event_name == 'workflow_dispatch'
@@ -757,13 +912,13 @@ jobs:
echo "skiptests: ${{github.event.inputs.skiptests}}"
echo "test_args: ${{github.event.inputs.test_args}}"
echo "cluster_test_args: ${{github.event.inputs.cluster_test_args}}"
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
repository: ${{ env.GITHUB_REPOSITORY }}
ref: ${{ env.GITHUB_HEAD_REF }}
- name: make
run: |
dnf -y install which gcc make openssl-devel openssl g++
dnf -y install which gcc make openssl-devel openssl
make BUILD_TLS=module REDIS_CFLAGS='-Werror'
- name: testprep
run: |
@@ -789,7 +944,7 @@ jobs:
(github.event_name == 'workflow_dispatch' || (github.event_name != 'workflow_dispatch' && github.repository == 'redis/redis')) &&
!contains(github.event.inputs.skipjobs, 'tls')
container: quay.io/centos/centos:stream9
timeout-minutes: 14400
timeout-minutes: 360
steps:
- name: prep
if: github.event_name == 'workflow_dispatch'
@@ -800,13 +955,13 @@ jobs:
echo "skiptests: ${{github.event.inputs.skiptests}}"
echo "test_args: ${{github.event.inputs.test_args}}"
echo "cluster_test_args: ${{github.event.inputs.cluster_test_args}}"
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
repository: ${{ env.GITHUB_REPOSITORY }}
ref: ${{ env.GITHUB_HEAD_REF }}
- name: make
run: |
dnf -y install which gcc make openssl-devel openssl g++
dnf -y install which gcc make openssl-devel openssl
make BUILD_TLS=module REDIS_CFLAGS='-Werror'
- name: testprep
run: |
@@ -831,7 +986,7 @@ jobs:
if: |
(github.event_name == 'workflow_dispatch' || (github.event_name != 'workflow_dispatch' && github.repository == 'redis/redis')) &&
!contains(github.event.inputs.skipjobs, 'macos') && !(contains(github.event.inputs.skiptests, 'redis') && contains(github.event.inputs.skiptests, 'modules'))
timeout-minutes: 14400
timeout-minutes: 360
steps:
- name: prep
if: github.event_name == 'workflow_dispatch'
@@ -842,7 +997,7 @@ jobs:
echo "skiptests: ${{github.event.inputs.skiptests}}"
echo "test_args: ${{github.event.inputs.test_args}}"
echo "cluster_test_args: ${{github.event.inputs.cluster_test_args}}"
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
repository: ${{ env.GITHUB_REPOSITORY }}
ref: ${{ env.GITHUB_HEAD_REF }}
@@ -857,7 +1012,7 @@ jobs:
if: |
(github.event_name == 'workflow_dispatch' || (github.event_name != 'workflow_dispatch' && github.repository == 'redis/redis')) &&
!contains(github.event.inputs.skipjobs, 'macos') && !contains(github.event.inputs.skiptests, 'sentinel')
timeout-minutes: 14400
timeout-minutes: 360
steps:
- name: prep
if: github.event_name == 'workflow_dispatch'
@@ -868,7 +1023,7 @@ jobs:
echo "skiptests: ${{github.event.inputs.skiptests}}"
echo "test_args: ${{github.event.inputs.test_args}}"
echo "cluster_test_args: ${{github.event.inputs.cluster_test_args}}"
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
repository: ${{ env.GITHUB_REPOSITORY }}
ref: ${{ env.GITHUB_HEAD_REF }}
@@ -883,7 +1038,7 @@ jobs:
if: |
(github.event_name == 'workflow_dispatch' || (github.event_name != 'workflow_dispatch' && github.repository == 'redis/redis')) &&
!contains(github.event.inputs.skipjobs, 'macos') && !contains(github.event.inputs.skiptests, 'cluster')
timeout-minutes: 14400
timeout-minutes: 360
steps:
- name: prep
if: github.event_name == 'workflow_dispatch'
@@ -894,7 +1049,7 @@ jobs:
echo "skiptests: ${{github.event.inputs.skiptests}}"
echo "test_args: ${{github.event.inputs.test_args}}"
echo "cluster_test_args: ${{github.event.inputs.cluster_test_args}}"
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
repository: ${{ env.GITHUB_REPOSITORY }}
ref: ${{ env.GITHUB_HEAD_REF }}
@@ -907,12 +1062,12 @@ jobs:
build-macos:
strategy:
matrix:
os: [macos-13, macos-15]
os: [macos-14, macos-26]
runs-on: ${{ matrix.os }}
if: |
(github.event_name == 'workflow_dispatch' || (github.event_name != 'workflow_dispatch' && github.repository == 'redis/redis')) &&
!contains(github.event.inputs.skipjobs, 'macos')
timeout-minutes: 14400
timeout-minutes: 360
steps:
- uses: maxim-lobanov/setup-xcode@v1
with:
@@ -926,7 +1081,7 @@ jobs:
echo "skiptests: ${{github.event.inputs.skiptests}}"
echo "test_args: ${{github.event.inputs.test_args}}"
echo "cluster_test_args: ${{github.event.inputs.cluster_test_args}}"
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
repository: ${{ env.GITHUB_REPOSITORY }}
ref: ${{ env.GITHUB_HEAD_REF }}
@@ -934,26 +1089,23 @@ jobs:
run: make REDIS_CFLAGS='-Werror -DREDIS_TEST'
test-freebsd:
runs-on: macos-13
runs-on: ubuntu-latest
if: |
(github.event_name == 'workflow_dispatch' || (github.event_name != 'workflow_dispatch' && github.repository == 'redis/redis')) &&
!contains(github.event.inputs.skipjobs, 'freebsd')
timeout-minutes: 14400
env:
CC: clang
CXX: clang++
timeout-minutes: 360
steps:
- name: prep
if: github.event_name == 'workflow_dispatch'
run: |
echo "GITHUB_REPOSITORY=${{github.event.inputs.use_repo}}" >> $GITHUB_ENV
echo "GITHUB_HEAD_REF=${{github.event.inputs.use_git_ref}}" >> $GITHUB_ENV
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
repository: ${{ env.GITHUB_REPOSITORY }}
ref: ${{ env.GITHUB_HEAD_REF }}
- name: test
uses: cross-platform-actions/action@v0.22.0
uses: cross-platform-actions/action@v1.0.0
with:
operating_system: freebsd
environment_variables: MAKE
@@ -980,7 +1132,7 @@ jobs:
echo "skiptests: ${{github.event.inputs.skiptests}}"
echo "test_args: ${{github.event.inputs.test_args}}"
echo "cluster_test_args: ${{github.event.inputs.cluster_test_args}}"
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
repository: ${{ env.GITHUB_REPOSITORY }}
ref: ${{ env.GITHUB_HEAD_REF }}
@@ -1016,7 +1168,7 @@ jobs:
echo "skiptests: ${{github.event.inputs.skiptests}}"
echo "test_args: ${{github.event.inputs.test_args}}"
echo "cluster_test_args: ${{github.event.inputs.cluster_test_args}}"
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
repository: ${{ env.GITHUB_REPOSITORY }}
ref: ${{ env.GITHUB_HEAD_REF }}
@@ -1038,7 +1190,7 @@ jobs:
reply-schemas-validator:
runs-on: ubuntu-latest
timeout-minutes: 14400
timeout-minutes: 360
if: |
(github.event_name == 'workflow_dispatch' || (github.event_name != 'workflow_dispatch' && github.repository == 'redis/redis')) &&
!contains(github.event.inputs.skipjobs, 'reply-schema')
@@ -1052,7 +1204,7 @@ jobs:
echo "skiptests: ${{github.event.inputs.skiptests}}"
echo "test_args: ${{github.event.inputs.test_args}}"
echo "cluster_test_args: ${{github.event.inputs.cluster_test_args}}"
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
repository: ${{ env.GITHUB_REPOSITORY }}
ref: ${{ env.GITHUB_HEAD_REF }}
@@ -1069,10 +1221,14 @@ jobs:
- name: cluster tests
if: true && !contains(github.event.inputs.skiptests, 'cluster')
run: ./runtest-cluster --log-req-res --dont-clean --force-resp3 ${{github.event.inputs.cluster_test_args}}
- name: Install Python dependencies
uses: py-actions/py-dependency-install@v4
- name: Set up Python
uses: actions/setup-python@v6
with:
path: "./utils/req-res-validator/requirements.txt"
python-version: "3.x"
cache: "pip"
cache-dependency-path: "./utils/req-res-validator/requirements.txt"
- name: Install Python dependencies
run: python -m pip install -r ./utils/req-res-validator/requirements.txt
- name: validator
run: ./utils/req-res-log-validator.py --verbose --fail-missing-reply-schemas ${{ (!contains(github.event.inputs.skiptests, 'redis') && !contains(github.event.inputs.skiptests, 'module') && !contains(github.event.inputs.sentinel, 'redis') && !contains(github.event.inputs.skiptests, 'cluster')) && github.event.inputs.test_args == '' && github.event.inputs.cluster_test_args == '' && '--fail-commands-not-all-hit' || '' }}
@@ -1082,7 +1238,7 @@ jobs:
(github.event_name == 'workflow_dispatch' || (github.event_name != 'workflow_dispatch' && github.repository == 'redis/redis')) &&
!contains(github.event.inputs.skipjobs, 'oldTC')
container: ubuntu:20.04
timeout-minutes: 14400
timeout-minutes: 360
steps:
- name: prep
if: github.event_name == 'workflow_dispatch'
@@ -1093,7 +1249,7 @@ jobs:
echo "skiptests: ${{github.event.inputs.skiptests}}"
echo "test_args: ${{github.event.inputs.test_args}}"
echo "cluster_test_args: ${{github.event.inputs.cluster_test_args}}"
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
repository: ${{ env.GITHUB_REPOSITORY }}
ref: ${{ env.GITHUB_HEAD_REF }}
@@ -1106,9 +1262,8 @@ jobs:
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 40976EAF437D05B5
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 3B4FE6ACC0B21F32
apt-get update
apt-get install -y make gcc-4.8 g++-4.8
apt-get install -y make gcc-4.8
update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-4.8 100
update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-4.8 100
make CC=gcc REDIS_CFLAGS='-Werror'
- name: testprep
run: apt-get install -y tcl tcltls tclx
@@ -1128,7 +1283,7 @@ jobs:
(github.event_name == 'workflow_dispatch' || (github.event_name != 'workflow_dispatch' && github.repository == 'redis/redis')) &&
!contains(github.event.inputs.skipjobs, 'tls') && !contains(github.event.inputs.skipjobs, 'oldTC')
container: ubuntu:20.04
timeout-minutes: 14400
timeout-minutes: 360
steps:
- name: prep
if: github.event_name == 'workflow_dispatch'
@@ -1139,7 +1294,7 @@ jobs:
echo "skiptests: ${{github.event.inputs.skiptests}}"
echo "test_args: ${{github.event.inputs.test_args}}"
echo "cluster_test_args: ${{github.event.inputs.cluster_test_args}}"
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
repository: ${{ env.GITHUB_REPOSITORY }}
ref: ${{ env.GITHUB_HEAD_REF }}
@@ -1152,10 +1307,9 @@ jobs:
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 40976EAF437D05B5
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 3B4FE6ACC0B21F32
apt-get update
apt-get install -y make gcc-4.8 g++-4.8 openssl libssl-dev
apt-get install -y make gcc-4.8 openssl libssl-dev
update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-4.8 100
update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-4.8 100
make CC=gcc CXX=g++ BUILD_TLS=module REDIS_CFLAGS='-Werror'
make CC=gcc BUILD_TLS=module REDIS_CFLAGS='-Werror'
- name: testprep
run: |
apt-get install -y tcl tcltls tclx
@@ -1179,7 +1333,7 @@ jobs:
(github.event_name == 'workflow_dispatch' || (github.event_name != 'workflow_dispatch' && github.repository == 'redis/redis')) &&
!contains(github.event.inputs.skipjobs, 'tls') && !contains(github.event.inputs.skipjobs, 'oldTC')
container: ubuntu:20.04
timeout-minutes: 14400
timeout-minutes: 360
steps:
- name: prep
if: github.event_name == 'workflow_dispatch'
@@ -1190,7 +1344,7 @@ jobs:
echo "skiptests: ${{github.event.inputs.skiptests}}"
echo "test_args: ${{github.event.inputs.test_args}}"
echo "cluster_test_args: ${{github.event.inputs.cluster_test_args}}"
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
repository: ${{ env.GITHUB_REPOSITORY }}
ref: ${{ env.GITHUB_HEAD_REF }}
@@ -1203,9 +1357,8 @@ jobs:
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 40976EAF437D05B5
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 3B4FE6ACC0B21F32
apt-get update
apt-get install -y make gcc-4.8 g++-4.8 openssl libssl-dev
apt-get install -y make gcc-4.8 openssl libssl-dev
update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-4.8 100
update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-4.8 100
make BUILD_TLS=module CC=gcc REDIS_CFLAGS='-Werror'
- name: testprep
run: |
@@ -1229,7 +1382,7 @@ jobs:
if: |
(github.event_name == 'workflow_dispatch' || (github.event_name != 'workflow_dispatch' && github.repository == 'redis/redis')) &&
!contains(github.event.inputs.skipjobs, 'defrag')
timeout-minutes: 14400
timeout-minutes: 360
steps:
- name: prep
if: github.event_name == 'workflow_dispatch'
@@ -1240,7 +1393,7 @@ jobs:
echo "skiptests: ${{github.event.inputs.skiptests}}"
echo "test_args: ${{github.event.inputs.test_args}}"
echo "cluster_test_args: ${{github.event.inputs.cluster_test_args}}"
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
repository: ${{ env.GITHUB_REPOSITORY }}
ref: ${{ env.GITHUB_HEAD_REF }}
@@ -1251,3 +1404,33 @@ jobs:
- name: test
if: true && !contains(github.event.inputs.skiptests, 'redis')
run: ./runtest --debug-defrag --verbose --clients 1 ${{github.event.inputs.test_args}}
test-vectorset:
runs-on: ubuntu-latest
if: |
(github.event_name == 'workflow_dispatch' || (github.event_name != 'workflow_dispatch' && github.repository == 'redis/redis')) &&
!contains(github.event.inputs.skipjobs, 'vectorset')
timeout-minutes: 360
steps:
- name: prep
if: github.event_name == 'workflow_dispatch'
run: |
echo "GITHUB_REPOSITORY=${{github.event.inputs.use_repo}}" >> $GITHUB_ENV
echo "GITHUB_HEAD_REF=${{github.event.inputs.use_git_ref}}" >> $GITHUB_ENV
echo "skipjobs: ${{github.event.inputs.skipjobs}}"
echo "skiptests: ${{github.event.inputs.skiptests}}"
echo "test_args: ${{github.event.inputs.test_args}}"
echo "cluster_test_args: ${{github.event.inputs.cluster_test_args}}"
- uses: actions/checkout@v6
with:
repository: ${{ env.GITHUB_REPOSITORY }}
ref: ${{ env.GITHUB_HEAD_REF }}
- name: make
run: make REDIS_CFLAGS='-Werror -DREDIS_TEST'
- name: testprep
run: |
sudo apt-get install tcl8.6 tclx
sudo pip install redis
- name: test
if: true && !contains(github.event.inputs.skiptests, 'redis')
run: ./runtest --accurate --verbose --dump-logs --single vectorset/vectorset ${{github.event.inputs.test_args}}
+16
View File
@@ -0,0 +1,16 @@
name: Docker
on:
workflow_dispatch:
push:
branches: [main, dev, test, unstable]
tags: ['v*']
permissions:
contents: read
packages: write
id-token: write
jobs:
docker:
uses: hanzoai/.github/.github/workflows/docker-build.yml@main
with:
image: ghcr.io/hanzoai/memory
secrets: inherit
+13 -9
View File
@@ -6,13 +6,17 @@ on:
schedule:
- cron: '0 0 * * *'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.head_ref || github.ref }}
cancel-in-progress: true
jobs:
test-external-standalone:
runs-on: ubuntu-latest
if: github.event_name != 'schedule' || github.repository == 'redis/redis'
timeout-minutes: 14400
timeout-minutes: 360
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Build
run: make REDIS_CFLAGS=-Werror
- name: Start redis-server
@@ -27,7 +31,7 @@ jobs:
--tags -slow
- name: Archive redis log
if: ${{ failure() }}
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: test-external-redis-log
path: external-redis.log
@@ -35,9 +39,9 @@ jobs:
test-external-cluster:
runs-on: ubuntu-latest
if: github.event_name != 'schedule' || github.repository == 'redis/redis'
timeout-minutes: 14400
timeout-minutes: 360
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Build
run: make REDIS_CFLAGS=-Werror
- name: Start redis-server
@@ -55,7 +59,7 @@ jobs:
--tags -slow
- name: Archive redis log
if: ${{ failure() }}
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: test-external-cluster-log
path: external-redis-cluster.log
@@ -63,9 +67,9 @@ jobs:
test-external-nodebug:
runs-on: ubuntu-latest
if: github.event_name != 'schedule' || github.repository == 'redis/redis'
timeout-minutes: 14400
timeout-minutes: 360
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Build
run: make REDIS_CFLAGS=-Werror
- name: Start redis-server
@@ -79,7 +83,7 @@ jobs:
--tags "-slow -needs:debug"
- name: Archive redis log
if: ${{ failure() }}
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: test-external-redis-nodebug-log
path: external-redis-nodebug.log
+1 -1
View File
@@ -11,7 +11,7 @@ jobs:
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@v1
uses: actions/create-github-app-token@v3
with:
app-id: ${{ secrets.DOCS_APP_ID }}
private-key: ${{ secrets.DOCS_APP_PRIVATE_KEY }}
+6 -2
View File
@@ -8,13 +8,17 @@ on:
paths:
- 'src/commands/*.json'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.head_ref || github.ref }}
cancel-in-progress: true
jobs:
reply-schemas-linter:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Setup nodejs
uses: actions/setup-node@v4
uses: actions/setup-node@v6
- name: Install packages
run: npm install ajv
- name: linter
+6 -2
View File
@@ -9,6 +9,10 @@ on:
push:
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.head_ref || github.ref }}
cancel-in-progress: true
jobs:
build:
name: Spellcheck
@@ -16,10 +20,10 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: pip cache
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
+7
View File
@@ -0,0 +1,7 @@
name: Workflow Sanity
on:
pull_request:
paths: ['.github/workflows/**']
jobs:
sanity:
uses: hanzoai/.github/.github/workflows/workflow-sanity.yml@main
+1 -1
View File
@@ -30,7 +30,7 @@ deps/lua/src/luac
deps/lua/src/liblua.a
deps/hdr_histogram/libhdrhistogram.a
deps/fpconv/libfpconv.a
deps/fast_float/libfast_float.a
deps/tre/libtre.a
tests/tls/*
.make-*
.prerequisites
Symlink
+1
View File
@@ -0,0 +1 @@
LLM.md
Symlink
+1
View File
@@ -0,0 +1 @@
LLM.md
+37
View File
@@ -0,0 +1,37 @@
FROM --platform=$TARGETPLATFORM alpine:latest AS builder
RUN apk add --no-cache gcc g++ make musl-dev linux-headers
WORKDIR /src
COPY . /src
RUN make -j$(nproc) BUILD_TLS=no MALLOC=libc
FROM --platform=$TARGETPLATFORM alpine:latest
LABEL maintainer="dev@hanzo.ai"
LABEL org.opencontainers.image.title="Hanzo Memory"
LABEL org.opencontainers.image.description="Redis-compatible in-memory store — Hanzo Memory"
LABEL org.opencontainers.image.vendor="Hanzo AI"
LABEL org.opencontainers.image.source="https://github.com/hanzoai/redis"
RUN addgroup -S memory && adduser -S memory -G memory
COPY --from=builder /src/src/redis-server /usr/local/bin/redis-server
COPY --from=builder /src/src/redis-cli /usr/local/bin/redis-cli
COPY --from=builder /src/src/redis-sentinel /usr/local/bin/redis-sentinel
COPY --from=builder /src/src/redis-benchmark /usr/local/bin/redis-benchmark
RUN mkdir -p /data && chown memory:memory /data
VOLUME /data
WORKDIR /data
EXPOSE 6379
USER memory
HEALTHCHECK --interval=15s --timeout=3s --start-period=10s --retries=3 \
CMD redis-cli ping | grep -q PONG || exit 1
ENTRYPOINT ["redis-server"]
CMD ["--bind", "0.0.0.0", "--dir", "/data", "--maxmemory-policy", "allkeys-lru", "--protected-mode", "no"]
+7
View File
@@ -0,0 +1,7 @@
# redis
[![codecov](https://codecov.io/github/redis/redis/graph/badge.svg?token=6bVHb5fRuz)](https://codecov.io/github/redis/redis)
This document serves as both a quick start guide to Redis and a detailed resource for building it from source.
- New to Redis? Start with [What is Redis](#what-is-redis) and [Getting Started](#getting-started)
+3
View File
@@ -2,6 +2,9 @@
SUBDIRS = src
ifeq ($(BUILD_WITH_MODULES), yes)
ifeq ($(MAKECMDGOALS),32bit)
$(error BUILD_WITH_MODULES=yes is not supported on 32 bit systems)
endif
SUBDIRS += modules
endif
+22 -11
View File
@@ -20,6 +20,7 @@ This document serves as both a quick start guide to Redis and a detailed resourc
- [Using Redis with redis-cli](#using-redis-with-redis-cli)
- [Using Redis with Redis Insight](#using-redis-with-redis-insight)
- [Redis data types, processing engines, and capabilities](#redis-data-types-processing-engines-and-capabilities)
- [Cloud hosted Redis](#cloud-hosted-redis)
- [Community](#community)
- [Build Redis from source](#build-redis-from-source)
- [Build and run Redis with all data structures - Ubuntu 20.04 (Focal)](#build-and-run-redis-with-all-data-structures---ubuntu-2004-focal)
@@ -52,7 +53,7 @@ Redis excels in various applications, including:
- **Distributed Session Store:** Offers flexible session data modeling (string, JSON, hash).
- **Data Structure Server:** Provides low-level data structures (strings, lists, sets, hashes, sorted sets, JSON, etc.) with high-level semantics (counters, queues, leaderboards, rate limiters) and supports transactions & scripting.
- **NoSQL Data Store:** Key-value, document, and time series data storage.
- **Search and Query Engine:** Indexing for hash/JSON documents, supporting vector search, full-text search, geospatial queries, ranking, and aggregations via Redis Query Engine.
- **Search and Query Engine:** Indexing for hash/JSON documents, supporting vector search, full-text search, geospatial queries, ranking, and aggregations via Redis Search.
- **Event Store & Message Broker:** Implements queues (lists), priority queues (sorted sets), event deduplication (sets), streams, and pub/sub with probabilistic stream processing capabilities.
- **Vector Store for GenAI:** Integrates with AI applications (e.g. LangGraph, mem0) for short-term memory, long-term memory, LLM response caching (semantic caching), and retrieval augmented generation (RAG).
- **Real-Time Analytics:** Powers personalization, recommendations, fraud detection, and risk assessment.
@@ -171,9 +172,10 @@ Redis provides a variety of data types, processing engines, and capabilities to
**Important:** Features marked with an asterisk (\*) require Redis to be compiled with the `BUILD_WITH_MODULES=yes` flag when [building Redis from source](#build-redis-from-source)
- [**String:**](https://redis.io/docs/latest/develop/data-types/strings) Sequences of bytes, including text, serialized objects, and binary arrays used for caching, counters, and bitwise operations.
- [**JSON:**](https://redis.io/docs/latest/develop/data-types/json/) Nested JSON documents that are indexed and searchable using JSONPath expressions and with [Redis Query Engine](https://redis.io/docs/latest/develop/interact/search-and-query/)
- [**JSON:**](https://redis.io/docs/latest/develop/data-types/json/) Nested JSON documents that are indexed and searchable using JSONPath expressions and with [Redis Search](https://redis.io/docs/latest/develop/ai/search-and-query/)
- [**Array:**](https://redis.io/docs/latest/develop/data-types/arrays/) Sparse, index-addressable collection of string values
- [**Hash:**](https://redis.io/docs/latest/develop/data-types/hashes/) Field-value maps used to represent basic objects and store groupings of key-value pairs with support for [hash field expiration (TTL)](https://redis.io/docs/latest/develop/data-types/hashes/#field-expiration)
- [**Redis Query Engine:**](https://redis.io/docs/latest/develop/interact/search-and-query/) Use Redis as a document database, a vector database, a secondary index, and a search engine. Define indexes for hash and JSON documents and then use a rich query language for vector search, full-text search, geospatial queries, and aggregations.
- [**Redis Search:**](https://redis.io/docs/latest/develop/ai/search-and-query/) Use Redis as a document database, a vector database, a secondary index, and a search engine. Define indexes for hash and JSON documents and then use a rich query language for vector search, full-text search, geospatial queries, and aggregations.
- [**List:**](https://redis.io/docs/latest/develop/data-types/lists/) Linked lists of string values used as stacks, queues, and for queue management.
- [**Set:**](https://redis.io/docs/latest/develop/data-types/sets/) Unordered collection of unique strings used for tracking unique items, relations, and common set operations (intersections, unions, differences).
- [**Sorted set:**](https://redis.io/docs/latest/develop/data-types/sorted-sets/) Collection of unique strings ordered by an associated score used for leaderboards and rate limiters.
@@ -194,6 +196,12 @@ Redis provides a variety of data types, processing engines, and capabilities to
- [**Transaction:**](https://redis.io/docs/latest/develop/interact/transactions/) Allows the execution of a group of commands in a single step. A request sent by another client will never be served in the middle of the execution of a transaction. This guarantees that the commands are executed as a single isolated operation.
- [**Programmability:**](https://redis.io/docs/latest/develop/interact/programmability/eval-intro/) Upload and execute Lua scripts on the server. Scripts can employ programmatic control structures and use most of the commands while executing to access the database. Because scripts are executed on the server, reading and writing data from scripts is very efficient.
## Cloud hosted Redis
Fully-managed Redis with real-time performance at scale.
[**Redis Cloud**](https://redis.io/cloud/)
## Community
[**Redis Community Resources**](https://redis.io/community/)
@@ -265,7 +273,7 @@ Tested with the following Docker image:
```sh
cd /usr/src/redis-<version>
export BUILD_TLS=yes BUILD_WITH_MODULES=yes INSTALL_RUST_TOOLCHAIN=yes DISABLE_WERRORS=yes
export BUILD_TLS=yes BUILD_WITH_MODULES=yes INSTALL_RUST_TOOLCHAIN=yes
make -j "$(nproc)" all
```
@@ -331,7 +339,7 @@ Tested with the following Docker image:
```sh
cd /usr/src/redis-<version>
export BUILD_TLS=yes BUILD_WITH_MODULES=yes INSTALL_RUST_TOOLCHAIN=yes DISABLE_WERRORS=yes
export BUILD_TLS=yes BUILD_WITH_MODULES=yes INSTALL_RUST_TOOLCHAIN=yes
make -j "$(nproc)" all
```
@@ -385,7 +393,7 @@ Tested with the following Docker image:
```sh
cd /usr/src/redis-<version>
export BUILD_TLS=yes BUILD_WITH_MODULES=yes INSTALL_RUST_TOOLCHAIN=yes DISABLE_WERRORS=yes
export BUILD_TLS=yes BUILD_WITH_MODULES=yes INSTALL_RUST_TOOLCHAIN=yes
make -j "$(nproc)" all
```
@@ -442,7 +450,7 @@ Tested with the following Docker images:
```sh
cd /usr/src/redis-<version>
export BUILD_TLS=yes BUILD_WITH_MODULES=yes INSTALL_RUST_TOOLCHAIN=yes DISABLE_WERRORS=yes
export BUILD_TLS=yes BUILD_WITH_MODULES=yes INSTALL_RUST_TOOLCHAIN=yes
make -j "$(nproc)" all
```
@@ -561,7 +569,7 @@ Tested with the following Docker images:
```sh
source /etc/profile.d/gcc-toolset-13.sh
cd /usr/src/redis-<version>
export BUILD_TLS=yes BUILD_WITH_MODULES=yes INSTALL_RUST_TOOLCHAIN=yes DISABLE_WERRORS=yes
export BUILD_TLS=yes BUILD_WITH_MODULES=yes INSTALL_RUST_TOOLCHAIN=yes
make -j "$(nproc)" all
```
@@ -678,7 +686,7 @@ Tested with the following Docker images:
```sh
source /etc/profile.d/gcc-toolset-13.sh
cd /usr/src/redis-<version>
export BUILD_TLS=yes BUILD_WITH_MODULES=yes INSTALL_RUST_TOOLCHAIN=yes DISABLE_WERRORS=yes
export BUILD_TLS=yes BUILD_WITH_MODULES=yes INSTALL_RUST_TOOLCHAIN=yes
make -j "$(nproc)" all
```
@@ -750,7 +758,6 @@ Tested with the following Docker images:
export HOMEBREW_PREFIX="$(brew --prefix)"
export BUILD_WITH_MODULES=yes
export BUILD_TLS=yes
export DISABLE_WERRORS=yes
PATH="$HOMEBREW_PREFIX/opt/libtool/libexec/gnubin:$HOMEBREW_PREFIX/opt/llvm@18/bin:$HOMEBREW_PREFIX/opt/make/libexec/gnubin:$HOMEBREW_PREFIX/opt/gnu-sed/libexec/gnubin:$HOMEBREW_PREFIX/opt/coreutils/libexec/gnubin:$PATH"
export LDFLAGS="-L$HOMEBREW_PREFIX/opt/llvm@18/lib"
export CPPFLAGS="-I$HOMEBREW_PREFIX/opt/llvm@18/include"
@@ -784,6 +791,8 @@ To build Redis with all the data structures (including JSON, time series, Bloom
make BUILD_WITH_MODULES=yes
```
Note: `BUILD_WITH_MODULES=yes` is not supported on 32 bit systems.
To build Redis with just the core data structures, use:
```sh
@@ -873,7 +882,9 @@ make MALLOC=jemalloc
By default, Redis will build using the POSIX clock_gettime function as the monotonic clock source. On most modern systems, the internal processor clock can be used to improve performance. Cautions can be found here: http://oliveryang.net/2015/09/pitfalls-of-TSC-usage/
To build with support for the processor's internal instruction clock, use:
On ARM aarch64 systems, the hardware clock is enabled by default because the ARM Generic Timer is architecturally guaranteed to be available and monotonic on all ARMv8-A processors (see the *“The Generic Timer in AArch64 state”* section of the *Arm Architecture Reference Manual for Armv8-A*).
To build with support for the processor's internal instruction clock on other architectures, use:
```sh
make CFLAGS="-DUSE_PROCESSOR_CLOCK"
+28 -6
View File
@@ -11,20 +11,32 @@ unless this is not possible or feasible with a reasonable effort.
| Version | Supported |
|---------|------------------------------------------------------------------------|
| 8.6.x | :white_check_mark: |
| 8.4.x | :white_check_mark: |
| 8.2.x | :white_check_mark: |
| 8.0.x | :white_check_mark: |
| 8.0.x | :x: |
| 7.4.x | :white_check_mark: |
| 7.2.x | :white_check_mark: support extended till 7.4 end of support |
| < 7.2.x | :x: |
| 6.2.x | :white_check_mark: support extended - may be removed after end of 2025 |
| 6.2.x | :white_check_mark: support extended |
| < 6.2.x | :x: |
## Reporting a Vulnerability
If you believe you've discovered a serious vulnerability, please contact the
Redis core team at redis@redis.io. We will evaluate your report and if
necessary issue a fix and an advisory. If the issue was previously undisclosed,
we'll also mention your name in the credits.
If you believe you have found a security vulnerability, to ensure proper review
and assessment, we kindly ask vulnerability reports be submitted through
our [Redis Vulnerability Disclosure Program.](https://redis.io/redis-responsible-vulnerability-disclosure/)
We have found this path to be beneficial for both researchers and us for
a number of reasons. Including, offering fast response times to researchers and
opportunities for us to invite those with exceptional reports into closed paid
engagements.
For those averse to using our chosen platform, we will also accept reports directly
via GitHub's "Report a Vulnerability".
To contact the security team directly with questions use: [security@redis.com](mailto:security@redis.com)
## Responsible Disclosure
@@ -40,6 +52,16 @@ embargo on public disclosure.
If you believe you should be on the list, please contact us and we will
consider your request based on the above criteria.
## Support across Operating Systems, Architectures, and Compilers
Redis is primarily tested on modern Linux distributions, using contemporary
Intel and AMD x86_64 CPUs, as well as ARM-based CPUs, and recent versions of
the GCC compiler.
Vulnerability reports that rely on unsupported or uncommon environments
(for example, 32-bit architectures, non-Linux operating systems, or outdated
toolchains) may be considered out of scope, even if the issue is technically
valid. Such reports will be evaluated on a case-by-case basis at our discretion.
## License Compatibility
For security vulnerability patches released under Redis Open Source 7.4 and
+7 -6
View File
@@ -59,7 +59,7 @@ distclean:
-(cd jemalloc && [ -f Makefile ] && $(MAKE) distclean) > /dev/null || true
-(cd hdr_histogram && $(MAKE) clean) > /dev/null || true
-(cd fpconv && $(MAKE) clean) > /dev/null || true
-(cd fast_float && $(MAKE) clean) > /dev/null || true
-(cd tre && $(MAKE) clean) > /dev/null || true
-(cd xxhash && $(MAKE) clean) > /dev/null || true
-(rm -f .make-*)
@@ -95,11 +95,12 @@ fpconv: .make-prerequisites
.PHONY: fpconv
fast_float: .make-prerequisites
tre: .make-prerequisites
@printf '%b %b\n' $(MAKECOLOR)MAKE$(ENDCOLOR) $(BINCOLOR)$@$(ENDCOLOR)
cd fast_float && $(MAKE) libfast_float CFLAGS="$(DEPS_CFLAGS)" LDFLAGS="$(DEPS_LDFLAGS)"
cd tre && $(MAKE) CFLAGS="$(DEPS_CFLAGS)" LDFLAGS="$(DEPS_LDFLAGS)"
.PHONY: tre
.PHONY: fast_float
XXHASH_CFLAGS = -fPIC $(DEPS_CFLAGS)
xxhash: .make-prerequisites
@@ -136,8 +137,8 @@ lua: .make-prerequisites
.PHONY: lua
JEMALLOC_CFLAGS=$(CFLAGS)
JEMALLOC_LDFLAGS=$(LDFLAGS)
JEMALLOC_CFLAGS=$(ENABLE_LTO) $(CFLAGS)
JEMALLOC_LDFLAGS=$(ENABLE_LTO) $(LDFLAGS)
ifneq ($(DEB_HOST_GNU_TYPE),)
JEMALLOC_CONFIGURE_OPTS += --host=$(DEB_HOST_GNU_TYPE)
-27
View File
@@ -1,27 +0,0 @@
# Fallback to gcc/g++ when $CC or $CXX is not in $PATH.
CC ?= gcc
CXX ?= g++
WARN=-Wall
OPT=-O3
STD=-std=c++11
DEFS=-DFASTFLOAT_ALLOWS_LEADING_PLUS
FASTFLOAT_CFLAGS=$(WARN) $(OPT) $(STD) $(DEFS) $(CFLAGS)
FASTFLOAT_LDFLAGS=$(LDFLAGS)
libfast_float: fast_float_strtod.o
$(AR) -r libfast_float.a fast_float_strtod.o
32bit: FASTFLOAT_CFLAGS += -m32
32bit: FASTFLOAT_LDFLAGS += -m32
32bit: libfast_float
fast_float_strtod.o: fast_float_strtod.cpp
$(CXX) $(FASTFLOAT_CFLAGS) -c fast_float_strtod.cpp $(FASTFLOAT_LDFLAGS)
clean:
rm -f *.o
rm -f *.a
rm -f *.h.gch
rm -rf *.dSYM
-21
View File
@@ -1,21 +0,0 @@
README for fast_float v6.1.4
----------------------------------------------
We're using the fast_float library[1] in our (compiled-in)
floating-point fast_float_strtod implementation for faster and more
portable parsing of 64 decimal strings.
The single file fast_float.h is an amalgamation of the entire library,
which can be (re)generated with the amalgamate.py script (from the
fast_float repository) via the command
```
git clone https://github.com/fastfloat/fast_float
cd fast_float
git checkout v6.1.4
python3 ./script/amalgamate.py --license=MIT \
> $REDIS_SRC/deps/fast_float/fast_float.h
```
[1]: https://github.com/fastfloat/fast_float
-3838
View File
File diff suppressed because it is too large Load Diff
-32
View File
@@ -1,32 +0,0 @@
#include "fast_float.h"
#include <iostream>
#include <string>
#include <system_error>
#include <cerrno>
/* Convert NPTR to a double using the fast_float library.
*
* This function behaves similarly to the standard strtod function, converting
* the initial portion of the string pointed to by `nptr` to a `double` value,
* using the fast_float library for high performance. If the conversion fails,
* errno is set to EINVAL error code.
*
* @param nptr A pointer to the null-terminated byte string to be interpreted.
* @param endptr A pointer to a pointer to character. If `endptr` is not NULL,
* it will point to the character after the last character used
* in the conversion.
* @return The converted value as a double. If no valid conversion could
* be performed, returns 0.0.
* If ENDPTR is not NULL, a pointer to the character after the last one used
* in the number is put in *ENDPTR. */
extern "C" double fast_float_strtod(const char *nptr, char **endptr) {
double result = 0.0;
auto answer = fast_float::from_chars(nptr, nptr + strlen(nptr), result);
if (answer.ec != std::errc()) {
errno = EINVAL; // Fallback to for other errors
}
if (endptr != NULL) {
*endptr = (char *)answer.ptr;
}
return result;
}
-15
View File
@@ -1,15 +0,0 @@
#ifndef __FAST_FLOAT_STRTOD_H__
#define __FAST_FLOAT_STRTOD_H__
#if defined(__cplusplus)
extern "C"
{
#endif
double fast_float_strtod(const char *in, char **out);
#if defined(__cplusplus)
}
#endif
#endif /* __FAST_FLOAT_STRTOD_H__ */
+81 -8
View File
@@ -120,6 +120,7 @@
#include <assert.h>
#include "linenoise.h"
#define SEQ_BUFFER_MAX_LENGTH 8
#define LINENOISE_DEFAULT_HISTORY_MAX_LEN 100
#define LINENOISE_MAX_LINE 4096
static char *unsupported_term[] = {"dumb","cons25","emacs",NULL};
@@ -793,6 +794,30 @@ void linenoiseEditMoveRight(struct linenoiseState *l) {
}
}
/* Consider letters/digits/underscore as “word”; others as delimiters. */
static int isWordChar(char c) {
return (c == '_' || (c >= '0' && c <= '9') ||
(c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'));
}
static void linenoiseEditMoveWordLeft(struct linenoiseState *l) {
if (l->pos == 0) return;
/* Skip any delimiters, then move left over the previous word */
while (l->pos > 0 && !isWordChar(l->buf[l->pos - 1])) l->pos--;
/* Then move to the start of that word */
while (l->pos > 0 && isWordChar(l->buf[l->pos - 1])) l->pos--;
refreshLine(l);
}
static void linenoiseEditMoveWordRight(struct linenoiseState *l) {
if (l->pos == l->len) return;
/* Skip the current word to the right */
while (l->pos < l->len && isWordChar(l->buf[l->pos])) l->pos++;
/* Then skip any delimiters to reach the next word */
while (l->pos < l->len && !isWordChar(l->buf[l->pos])) l->pos++;
refreshLine(l);
}
/* Move cursor to the start of the line. */
void linenoiseEditMoveHome(struct linenoiseState *l) {
if (l->pos != 0) {
@@ -1012,6 +1037,18 @@ static int linenoiseEdit(int stdin_fd, int stdout_fd, char *buf, size_t buflen,
* Use two calls to handle slow terminals returning the two
* chars at different times. */
if (read(l.ifd,seq,1) == -1) break;
/* Handle Meta-b / Meta-f directly because it's a 2-byte sequence */
if (seq[0] == 'b' || seq[0] == 'f') {
if (reverse_search_mode_enabled) {
disableReverseSearchMode(&l, buf, buflen, 1);
break;
}
if (seq[0] == 'b') linenoiseEditMoveWordLeft(&l); /* ESC b → word left */
else linenoiseEditMoveWordRight(&l); /* ESC f → word right */
break;
}
if (read(l.ifd,seq+1,1) == -1) break;
if (reverse_search_mode_enabled) {
@@ -1022,14 +1059,50 @@ static int linenoiseEdit(int stdin_fd, int stdout_fd, char *buf, size_t buflen,
/* ESC [ sequences. */
if (seq[0] == '[') {
if (seq[1] >= '0' && seq[1] <= '9') {
/* Extended escape, read additional byte. */
if (read(l.ifd,seq+2,1) == -1) break;
if (seq[2] == '~') {
switch(seq[1]) {
case '3': /* Delete key. */
linenoiseEditDelete(&l);
break;
}
/* Extended escape, read additional bytes.
* Examples: ESC [1;5C ESC [3~ */
char seq_buffer[SEQ_BUFFER_MAX_LENGTH];
int i = 0;
seq_buffer[i++] = seq[1];
/* Read more bytes until we see a CSI final byte (range @..~).
* Use SEQ_BUFFER_MAX_LENGTH-1 to reserve one position for '\0'. */
char seq_char;
while (i < SEQ_BUFFER_MAX_LENGTH - 1 && read(l.ifd, &seq_char, 1) == 1) {
seq_buffer[i++] = seq_char;
if (seq_char >= '@' && seq_char <= '~') break; /* CSI final byte */
}
seq_buffer[i] = '\0';
/* The exact key mapping behavior depends on your keyboard/terminal setup.
* For example, in MacOS terminal you can go to the profile keyboard setting
* to see or configure the current mapping.
*
* Take action `[1;5C` (Ctrl + →) or `[1;3D` (Alt + ←) as examples:
* [ indicates a CSI (Control Sequence Introducer), telling the terminal
* "What follows is a control command, not text"
* 1 is how many units to move (default is 1 if omitted)
* ; is the separator between parameters
* 5 is the modifier mask for Ctrl. Other possible values include 1 (no modifier),
* 2 (Shift), 3 (Alt), 4 (Shift + Alt), 6 (Shift + Ctrl), 7 (Alt + Ctrl), etc.
* C is the cursor right command. Other commands include A (cursor up), B (cursor
* down), D (cursor left).
*/
/* Word left: Ctrl + ← (modifier 5) or Alt + ← (modifier 3) */
if (strcmp(seq_buffer, "1;5D") == 0 || strcmp(seq_buffer, "1;3D") == 0) {
linenoiseEditMoveWordLeft(&l);
break;
}
/* Word right: Ctrl + → (modifier 5) or Alt + → (modifier 3) */
if (strcmp(seq_buffer, "1;5C") == 0 || strcmp(seq_buffer, "1;3C") == 0) {
linenoiseEditMoveWordRight(&l);
break;
}
/* Delete key */
if (strcmp(seq_buffer, "3~") == 0) {
linenoiseEditDelete(&l);
break;
}
} else {
switch(seq[1]) {
+29
View File
@@ -0,0 +1,29 @@
This is the license, copyright notice, and disclaimer for TRE, a regex
matching package (library and tools) with support for approximate
matching.
Copyright (c) 2001-2009 Ville Laurikari <vl@iki.fi>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+79
View File
@@ -0,0 +1,79 @@
STD= -std=c99
WARN= -Wall
OPT= -Os
ifeq ($(SANITIZER),address)
CFLAGS+=-fsanitize=address -fno-sanitize-recover=all -fno-omit-frame-pointer
LDFLAGS+=-fsanitize=address
else
ifeq ($(SANITIZER),undefined)
CFLAGS+=-fsanitize=undefined -fno-sanitize-recover=all -fno-omit-frame-pointer
LDFLAGS+=-fsanitize=undefined
else
ifeq ($(SANITIZER),thread)
CFLAGS+=-fsanitize=thread -fno-sanitize-recover=all -fno-omit-frame-pointer
LDFLAGS+=-fsanitize=thread
else
ifeq ($(SANITIZER),memory)
CFLAGS+=-fsanitize=memory -fsanitize-memory-track-origins=2 -fno-sanitize-recover=all -fno-omit-frame-pointer
LDFLAGS+=-fsanitize=memory
endif
endif
endif
endif
R_CFLAGS= $(STD) $(WARN) $(OPT) $(DEBUG) $(CFLAGS) -DTRE_REGEX_T_FIELD=value -Ilocal_includes -Ilib
R_LDFLAGS= $(LDFLAGS)
DEBUG= -g
R_CC=$(CC) $(R_CFLAGS)
R_LD=$(CC) $(R_LDFLAGS)
AR= ar
ARFLAGS= rcs
TRE_OBJ=lib/regcomp.o lib/regerror.o lib/regexec.o lib/tre-ast.o lib/tre-compile.o \
lib/tre-filter.o lib/tre-match-backtrack.o lib/tre-match-parallel.o \
lib/tre-mem.o lib/tre-parse.o lib/tre-stack.o lib/xmalloc.o
TRE_TESTS=tests/retest tests/test-str-source tests/test-literal-opt tests/test-malformed-regn
libtre.a: $(TRE_OBJ)
$(AR) $(ARFLAGS) $@ $+
check: $(TRE_TESTS)
@set -e; \
for test in $(TRE_TESTS); do \
echo "TEST $$test"; \
./$$test; \
done
tests/retest: tests/retest.c libtre.a
$(R_LD) $(R_CFLAGS) -DHAVE_REGNEXEC -DHAVE_REGNCOMP -o $@ $< libtre.a
tests/test-str-source: tests/test-str-source.c libtre.a
$(R_LD) $(R_CFLAGS) -o $@ $< libtre.a
tests/test-literal-opt: tests/test-literal-opt.c libtre.a
$(R_LD) $(R_CFLAGS) -o $@ $< libtre.a
tests/test-malformed-regn: tests/test-malformed-regn.c libtre.a
$(R_LD) $(R_CFLAGS) -o $@ $< libtre.a
lib/regcomp.o: lib/regcomp.c local_includes/tre.h local_includes/tre-config.h lib/tre-internal.h lib/xmalloc.h
lib/regerror.o: lib/regerror.c local_includes/tre.h
lib/regexec.o: lib/regexec.c local_includes/tre.h lib/tre-internal.h lib/xmalloc.h
lib/tre-ast.o: lib/tre-ast.c lib/tre-ast.h lib/tre-internal.h
lib/tre-compile.o: lib/tre-compile.c lib/tre-compile.h lib/tre-internal.h lib/tre-mem.h lib/tre-parse.h lib/tre-stack.h lib/xmalloc.h
lib/tre-filter.o: lib/tre-filter.c lib/tre-filter.h lib/tre-internal.h
lib/tre-match-backtrack.o: lib/tre-match-backtrack.c lib/tre-internal.h lib/tre-match-utils.h lib/tre-mem.h lib/tre-stack.h
lib/tre-match-parallel.o: lib/tre-match-parallel.c lib/tre-internal.h lib/tre-match-utils.h lib/tre-mem.h
lib/tre-mem.o: lib/tre-mem.c lib/tre-mem.h
lib/tre-parse.o: lib/tre-parse.c lib/tre-ast.h lib/tre-compile.h lib/tre-filter.h lib/tre-internal.h lib/tre-mem.h lib/tre-parse.h lib/tre-stack.h lib/xmalloc.h
lib/tre-stack.o: lib/tre-stack.c lib/tre-internal.h lib/tre-stack.h
lib/xmalloc.o: lib/xmalloc.c lib/xmalloc.h
.c.o:
$(R_CC) -c -o $@ $<
clean:
rm -f $(TRE_OBJ) libtre.a $(TRE_TESTS)
+276
View File
@@ -0,0 +1,276 @@
Introduction
============
TRE is a lightweight, robust, and efficient POSIX compliant regexp
matching library with some exciting features such as approximate
(fuzzy) matching.
The matching algorithm used in TRE uses linear worst-case time in
the length of the text being searched, and quadratic worst-case
time in the length of the used regular expression.
In other words, the time complexity of the algorithm is O(M^2N), where
M is the length of the regular expression and N is the length of the
text. The used space is also quadratic on the length of the regex,
but does not depend on the searched string. This quadratic behaviour
occurs only on pathological cases which are probably very rare in
practice.
Hacking
=======
Here's how to work with this code.
Prerequisites
-------------
You will need the following tools installed on your system:
- autoconf
- automake
- gettext (including autopoint)
- libtool
- zip (optional)
Building
--------
First, prepare the tree. Change to the root of the source directory
and run
./utils/autogen.sh
This will regenerate various things using the prerequisite tools so
that you end up with a buildable tree.
After this, you can run the configure script and build TRE as usual:
./configure
make
make check
make install
Building a source code package
------------------------------
In a prepared tree, this command creates a source code tarball:
./configure && make dist
Alternatively, you can run
./utils/build-sources.sh
which builds the source code packages and puts them in the `dist`
subdirectory. This script needs a working `zip` command.
Features
========
TRE is not just yet another regexp matcher. TRE has some features
which are not there in most free POSIX compatible implementations.
Most of these features are not present in non-free implementations
either, for that matter.
Approximate matching
--------------------
Approximate pattern matching allows matches to be approximate, that
is, allows the matches to be close to the searched pattern under some
measure of closeness. TRE uses the edit-distance measure (also known
as the Levenshtein distance) where characters can be inserted,
deleted, or substituted in the searched text in order to get an exact
match.
Each insertion, deletion, or substitution adds the distance, or cost,
of the match. TRE can report the matches which have a cost lower than
some given threshold value. TRE can also be used to search for
matches with the lowest cost.
TRE includes a version of the agrep (approximate grep) command line
tool for approximate regexp matching in the style of grep. Unlike
other agrep implementations (like the one by Sun Wu and Udi Manber
from University of Arizona) TRE agrep allows full regexps of any
length, any number of errors, and non-uniform costs for insertion,
deletion and substitution.
Strict standard conformance
---------------------------
POSIX defines the behaviour of regexp functions precisely. TRE
attempts to conform to these specifications as strictly as possible.
TRE always returns the correct matches for subpatterns, for example.
Very few other implementations do this correctly. In fact, the only
other implementations besides TRE that I am aware of (free or not)
that get it right are Rx by Tom Lord, Regex++ by John Maddock, and the
AT&T ast regex by Glenn Fowler and Doug McIlroy.
The standard TRE tries to conform to is the IEEE Std 1003.1-2001, or
Open Group Base Specifications Issue 6, commonly referred to as
“POSIX”. The relevant parts are the base specifications on regular
expressions (and the rationale) and the description of the `regcomp()`
API.
For an excellent survey on POSIX regexp matchers, see the testregex
pages by Glenn Fowler of AT&T Labs Research.
Predictable matching speed
--------------------------
Because of the matching algorithm used in TRE, the maximum time
consumed by any `regexec()` call is always directly proportional to
the length of the searched string. There is one exception: if back
references are used, the matching may take time that grows
exponentially with the length of the string. This is because matching
back references is an NP complete problem, and almost certainly
requires exponential time to match in the worst case.
Predictable and modest memory consumption
-----------------------------------------
A `regexec()` call never allocates memory from the heap. TRE allocates
all the memory it needs during a `regcomp()` call, and some temporary
working space from the stack frame for the duration of the `regexec()`
call. The amount of temporary space needed is constant during
matching and does not depend on the searched string. For regexps of
reasonable size TRE needs less than 50K of dynamically allocated
memory during the `regcomp()` call, less than 20K for the compiled
pattern buffer, and less than two kilobytes of temporary working space
from the stack frame during a `regexec()` call. There is no time /
memory tradeoff. TRE is also small in code size; statically linking
with TRE increases the executable size less than 30K (gcc-3.2, x86,
GNU/Linux).
Wide character and multibyte character set support
--------------------------------------------------
TRE supports multibyte character sets. This makes it possible to use
regexps seamlessly with, for example, Japanese locales. TRE also
provides a wide character API.
Binary pattern and data support
-------------------------------
TRE provides APIs which allow binary zero characters both in regexps
and searched strings. The standard API cannot be easily used to, for
example, search for printable words from binary data (although it is
possible with some hacking). Searching for patterns which contain
binary zeroes embedded is not possible at all with the standard API.
Completely thread safe
----------------------
TRE is completely thread safe. All the exported functions are
re-entrant, and a single compiled regexp object can be used
simultaneously in multiple contexts; e.g. in `main()` and a signal
handler, or in many threads of a multithreaded application.
Portable
--------
TRE is portable across multiple platforms. Below is a table of
platforms and compilers used to develop and test TRE:
<table>
<tr><th>Platform</th> <th>Compiler</th></tr>
<tr><td>FreeBSD 14.1</td> <td>Clang 18</td></tr>
<tr><td>Ubuntu 22.04</td> <td>GCC 11</td></tr>
<tr><td>macOS 14.6</td> <td>Clang 14</td></tr>
<tr><td>Windows 11</td> <td>Microsoft Visual Studio 2022</td></tr>
</table>
TRE should compile without changes on most modern POSIX-like
platforms, and be easily portable to any platform with a hosted C
implementation.
Depending on the platform, you may need to install libutf8 to get
wide character and multibyte character set support.
Free
----
TRE is released under a license which is essentially the same as the
“2 clause” BSD-style license used in NetBSD. See the file LICENSE for
details.
Roadmap
-------
There are currently two features, both related to collating elements,
missing from 100% POSIX compliance. These are:
* Support for collating elements (e.g. `[[.\<X>.]]`, where `\<X>` is a
collating element). It is not possible to support multi-character
collating elements portably, since POSIX does not define a way to
determine whether a character sequence is a multi-character
collating element or not.
* Support for equivalence classes, for example `[[=\<X>=]]`, where
`\<X>` is a collating element. An equivalence class matches any
character which has the same primary collation weight as `\<X>`.
Again, POSIX provides no portable mechanism for determining the
primary collation weight of a collating element.
Note that other portable regexp implementations don't support
collating elements either. The single exception is Regex++, which
comes with its own database for collating elements for different
locales. Support for collating elements and equivalence classes has
not been widely requested and is not very high on the TODO list at the
moment.
These are other features I'm planning to implement real soon now:
* All the missing GNU extensions enabled in GNU regex, such as
`[[:<:]]` and `[[:>:]]`.
* A `REG_SHORTEST` `regexec()` flag for returning the shortest match
instead of the longest match.
* Perl-compatible syntax:
* `[:^class:]`
Matches anything but the characters in class. Note that
`[^[:class:]]` works already, this would be just a convenience
shorthand.
* `\A`
Match only at beginning of string.
* `\Z`
Match only at end of string, or before newline at the end.
* `\z`
Match only at end of string.
* `\l`
Lowercase next char (think vi).
* `\u`
Uppercase next char (think vi).
* `\L`
Lowercase till `\E` (think vi).
* `\U`
Uppercase till `\E` (think vi).
* `(?=pattern)`
Zero-width positive look-ahead assertions.
* `(?!pattern)`
Zero-width negative look-ahead assertions.
* `(?<=pattern)`
Zero-width positive look-behind assertions.
* `(?<!pattern)`
Zero-width negative look-behind assertions.
Documentation especially for the nonstandard features of TRE, such as
approximate matching, is a work in progress (with “progress” loosely
defined...) If you want to find an extension to use, reading the
`include/tre/tre.h` header might provide some additional hints if you
are comfortable with C source code.
+188
View File
@@ -0,0 +1,188 @@
/*
tre_regcomp.c - TRE POSIX compatible regex compilation functions.
This software is released under a BSD-style license.
See the file LICENSE for details and copyright.
*/
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif /* HAVE_CONFIG_H */
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include "tre-internal.h"
#include "xmalloc.h"
int
tre_regncomp(regex_t *preg, const char *regex, size_t n, int cflags)
{
int ret;
if (n > TRE_MAX_RE)
return REG_ESPACE;
#if TRE_WCHAR
tre_char_t *wregex;
size_t wlen;
wregex = xmalloc(sizeof(tre_char_t) * (n + 1));
if (wregex == NULL)
return REG_ESPACE;
/* If the current locale uses the standard single byte encoding of
characters, we don't do a multibyte string conversion. If we did,
many applications which use the default locale would break since
the default "C" locale uses the 7-bit ASCII character set, and
all characters with the eighth bit set would be considered invalid. */
#if TRE_MULTIBYTE
if (TRE_MB_CUR_MAX == 1)
#endif /* TRE_MULTIBYTE */
{
size_t i;
const unsigned char *str = (const unsigned char *)regex;
tre_char_t *wstr = wregex;
for (i = 0; i < n; i++)
*(wstr++) = *(str++);
wlen = n;
}
#if TRE_MULTIBYTE
else
{
size_t consumed;
tre_char_t *wcptr = wregex;
#ifdef HAVE_MBSTATE_T
mbstate_t state;
memset(&state, '\0', sizeof(state));
#endif /* HAVE_MBSTATE_T */
while (n > 0)
{
consumed = tre_mbrtowc(wcptr, regex, n, &state);
switch (consumed)
{
case 0:
if (*regex == '\0')
consumed = 1;
else
{
xfree(wregex);
return REG_BADPAT;
}
break;
case -1:
DPRINT(("mbrtowc: error %d: %s.\n", errno, strerror(errno)));
xfree(wregex);
return REG_BADPAT;
case -2:
/* The last character wasn't complete. Let's not call it a
fatal error. */
consumed = n;
break;
}
regex += consumed;
n -= consumed;
wcptr++;
}
wlen = wcptr - wregex;
}
#endif /* TRE_MULTIBYTE */
wregex[wlen] = L'\0';
ret = tre_compile(preg, wregex, wlen, cflags);
xfree(wregex);
#else /* !TRE_WCHAR */
ret = tre_compile(preg, (const tre_char_t *)regex, n, cflags);
#endif /* !TRE_WCHAR */
return ret;
}
/* this version takes bytes literally, to be used with raw vectors */
int
tre_regncompb(regex_t *preg, const char *regex, size_t n, int cflags)
{
int ret;
if (n > TRE_MAX_RE)
return REG_ESPACE;
#if TRE_WCHAR /* wide chars = we need to convert it all to the wide format */
tre_char_t *wregex;
size_t i;
wregex = xmalloc(sizeof(tre_char_t) * n);
if (wregex == NULL)
return REG_ESPACE;
for (i = 0; i < n; i++)
wregex[i] = (tre_char_t) ((unsigned char) regex[i]);
ret = tre_compile(preg, wregex, n, cflags | REG_USEBYTES);
xfree(wregex);
#else /* !TRE_WCHAR */
ret = tre_compile(preg, (const tre_char_t *)regex, n, cflags | REG_USEBYTES);
#endif /* !TRE_WCHAR */
return ret;
}
int
tre_regcomp(regex_t *preg, const char *regex, int cflags)
{
size_t n = regex ? strlen(regex) : 0;
if (n > TRE_MAX_RE)
return REG_ESPACE;
return tre_regncomp(preg, regex, n, cflags);
}
int
tre_regcompb(regex_t *preg, const char *regex, int cflags)
{
int ret;
tre_char_t *wregex;
size_t i, n = regex ? strlen(regex) : 0;
const unsigned char *str = (const unsigned char *)regex;
tre_char_t *wstr;
if (n > TRE_MAX_RE)
return REG_ESPACE;
wregex = xmalloc(sizeof(tre_char_t) * (n + 1));
if (wregex == NULL) return REG_ESPACE;
wstr = wregex;
for (i = 0; i < n; i++)
*(wstr++) = *(str++);
wregex[n] = L'\0';
ret = tre_compile(preg, wregex, n, cflags | REG_USEBYTES);
xfree(wregex);
return ret;
}
#ifdef TRE_WCHAR
int
tre_regwncomp(regex_t *preg, const wchar_t *regex, size_t n, int cflags)
{
if (n > TRE_MAX_RE)
return REG_ESPACE;
return tre_compile(preg, regex, n, cflags);
}
int
tre_regwcomp(regex_t *preg, const wchar_t *regex, int cflags)
{
size_t n = regex ? wcslen(regex) : 0;
if (n > TRE_MAX_RE)
return REG_ESPACE;
return tre_compile(preg, regex, n, cflags);
}
#endif /* TRE_WCHAR */
void
tre_regfree(regex_t *preg)
{
tre_free(preg);
}
/* EOF */
+86
View File
@@ -0,0 +1,86 @@
/*
tre_regerror.c - POSIX tre_regerror() implementation for TRE.
This software is released under a BSD-style license.
See the file LICENSE for details and copyright.
*/
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif /* HAVE_CONFIG_H */
#include <string.h>
#ifdef HAVE_WCHAR_H
#include <wchar.h>
#endif /* HAVE_WCHAR_H */
#ifdef HAVE_WCTYPE_H
#include <wctype.h>
#endif /* HAVE_WCTYPE_H */
#include "tre-internal.h"
#ifdef HAVE_GETTEXT
#include <libintl.h>
#else
#define dgettext(p, s) s
#define gettext(s) s
#endif
#define _(String) dgettext(PACKAGE, String)
#define gettext_noop(String) String
#define xstr(s) str(s)
#define str(s) #s
/* Error message strings for error codes listed in `tre.h'. This list
needs to be in sync with the codes listed there, naturally. */
static const char *tre_error_messages[] =
{ gettext_noop("No error"), /* REG_OK */
gettext_noop("No match"), /* REG_NOMATCH */
gettext_noop("Invalid regexp"), /* REG_BADPAT */
gettext_noop("Unknown collating element"), /* REG_ECOLLATE */
gettext_noop("Unknown character class name"), /* REG_ECTYPE */
gettext_noop("Trailing backslash"), /* REG_EESCAPE */
gettext_noop("Invalid back reference"), /* REG_ESUBREG */
gettext_noop("Missing ']'"), /* REG_EBRACK */
gettext_noop("Missing ')'"), /* REG_EPAREN */
gettext_noop("Missing '}'"), /* REG_EBRACE */
gettext_noop("Invalid contents of {}"), /* REG_BADBR */
gettext_noop("Invalid character range"), /* REG_ERANGE */
gettext_noop("Out of memory"), /* REG_ESPACE */
gettext_noop("Invalid use of repetition operators"), /* REG_BADRPT */
gettext_noop("Maximum repetition in {} larger than " xstr(RE_DUP_MAX)), /* REG_BADMAX */
};
size_t
tre_regerror(int errcode, const regex_t *preg, char *errbuf, size_t errbuf_size)
{
const char *err;
size_t err_len;
/*LINTED*/(void)&preg;
if (errcode >= 0
&& errcode < (int)(sizeof(tre_error_messages)
/ sizeof(*tre_error_messages)))
err = gettext(tre_error_messages[errcode]);
else
err = gettext("Unknown error");
err_len = strlen(err) + 1;
if (errbuf_size > 0 && errbuf != NULL)
{
if (err_len > errbuf_size)
{
strncpy(errbuf, err, errbuf_size - 1);
errbuf[errbuf_size - 1] = '\0';
}
else
{
strcpy(errbuf, err);
}
}
return err_len;
}
/* EOF */
+584
View File
@@ -0,0 +1,584 @@
/*
tre_regexec.c - TRE POSIX compatible matching functions (and more).
This software is released under a BSD-style license.
See the file LICENSE for details and copyright.
*/
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif /* HAVE_CONFIG_H */
#ifdef TRE_USE_ALLOCA
/* AIX requires this to be the first thing in the file. */
#ifndef __GNUC__
# if HAVE_ALLOCA_H
# include <alloca.h>
# else
# ifdef _AIX
#pragma alloca
# else
# ifndef alloca /* predefined by HP cc +Olibcalls */
char *alloca ();
# endif
# endif
# endif
#endif
#endif /* TRE_USE_ALLOCA */
#include <assert.h>
#include <stdlib.h>
#include <string.h>
#ifdef HAVE_WCHAR_H
#include <wchar.h>
#endif /* HAVE_WCHAR_H */
#ifdef HAVE_WCTYPE_H
#include <wctype.h>
#endif /* HAVE_WCTYPE_H */
#ifndef TRE_WCHAR
#include <ctype.h>
#endif /* !TRE_WCHAR */
#ifdef HAVE_MALLOC_H
#include <malloc.h>
#endif /* HAVE_MALLOC_H */
#include <limits.h>
#include "tre-internal.h"
#include "xmalloc.h"
/* Literal alternatives are grouped by the first byte so the matcher can
* reach the relevant candidates in O(1). In nocase mode the lookup uses the
* same folded byte mapping that was applied at compile time. */
static void
tre_litopt_candidate_range(const tre_literal_opt_t *opt, unsigned char first_byte,
size_t *start, size_t *end)
{
unsigned char key = opt->nocase ? opt->fold_map[first_byte] : first_byte;
*start = opt->start_offsets[key];
*end = opt->start_offsets[key + 1];
}
static int
tre_litopt_bytes_equal(const unsigned char *haystack,
const unsigned char *needle, size_t len,
const unsigned char *fold_map)
{
size_t i;
if (fold_map == NULL)
return memcmp(haystack, needle, len) == 0;
for (i = 0; i < len; i++)
if (fold_map[haystack[i]] != needle[i])
return 0;
return 1;
}
static int
tre_litopt_contains_case(const unsigned char *haystack, size_t hay_len,
const unsigned char *needle, size_t needle_len,
int *match_end_ofs)
{
const unsigned char *p;
size_t remaining;
if (needle_len > hay_len)
return 0;
p = haystack;
remaining = hay_len;
while (remaining >= needle_len)
{
p = memchr(p, needle[0], remaining - needle_len + 1);
if (p == NULL)
return 0;
if (memcmp(p, needle, needle_len) == 0)
{
if (match_end_ofs != NULL)
*match_end_ofs = (int)(p - haystack + needle_len);
return 1;
}
remaining = hay_len - (size_t)(p - haystack) - 1;
p++;
}
return 0;
}
/* Nocase substring matching is still byte-oriented, but scanning once and
* only checking literals that share the same folded first byte avoids the
* old O(haystack * literals) restart pattern. */
static int
tre_litopt_contains_nocase(const tre_literal_opt_t *opt,
const unsigned char *haystack, size_t hay_len,
int *match_end_ofs)
{
size_t i, start, end, j;
for (i = 0; i < hay_len; i++)
{
tre_litopt_candidate_range(opt, haystack[i], &start, &end);
for (j = start; j < end; j++)
{
const tre_literal_opt_literal_t *lit = &opt->literals[j];
if (lit->len <= hay_len - i
&& tre_litopt_bytes_equal(haystack + i, lit->data, lit->len,
opt->fold_map))
{
if (match_end_ofs != NULL)
*match_end_ofs = (int)(i + lit->len);
return 1;
}
}
}
return 0;
}
static reg_errcode_t
tre_match_literal_opt(const tre_tnfa_t *tnfa, const char *string, size_t len,
int eflags, int *match_end_ofs)
{
const tre_literal_opt_t *opt = &tnfa->literal_opt;
const unsigned char *haystack = (const unsigned char *)string;
size_t start = 0, end = opt->num_literals, i;
const unsigned char *fold_map = opt->nocase ? opt->fold_map : NULL;
if ((opt->mode == TRE_LITERAL_OPT_PREFIX
|| opt->mode == TRE_LITERAL_OPT_EXACT)
&& (eflags & REG_NOTBOL))
return REG_NOMATCH;
if ((opt->mode == TRE_LITERAL_OPT_SUFFIX
|| opt->mode == TRE_LITERAL_OPT_EXACT)
&& (eflags & REG_NOTEOL))
return REG_NOMATCH;
if ((opt->mode == TRE_LITERAL_OPT_EXACT
|| opt->mode == TRE_LITERAL_OPT_PREFIX)
&& len > 0)
tre_litopt_candidate_range(opt, haystack[0], &start, &end);
if (opt->mode == TRE_LITERAL_OPT_CONTAINS)
{
if (opt->nocase)
return tre_litopt_contains_nocase(opt, haystack, len, match_end_ofs)
? REG_OK : REG_NOMATCH;
for (i = 0; i < opt->num_literals; i++)
{
const tre_literal_opt_literal_t *lit = &opt->literals[i];
if (tre_litopt_contains_case(haystack, len, lit->data, lit->len,
match_end_ofs))
return REG_OK;
}
return REG_NOMATCH;
}
for (i = start; i < end; i++)
{
const tre_literal_opt_literal_t *lit = &opt->literals[i];
switch (opt->mode)
{
case TRE_LITERAL_OPT_EXACT:
if (len == lit->len
&& tre_litopt_bytes_equal(haystack, lit->data, len, fold_map))
{
if (match_end_ofs != NULL)
*match_end_ofs = (int)len;
return REG_OK;
}
break;
case TRE_LITERAL_OPT_PREFIX:
if (len >= lit->len
&& tre_litopt_bytes_equal(haystack, lit->data, lit->len,
fold_map))
{
if (match_end_ofs != NULL)
*match_end_ofs = (int)lit->len;
return REG_OK;
}
break;
case TRE_LITERAL_OPT_SUFFIX:
if (len >= lit->len
&& tre_litopt_bytes_equal(haystack + len - lit->len, lit->data,
lit->len, fold_map))
{
if (match_end_ofs != NULL)
*match_end_ofs = (int)len;
return REG_OK;
}
break;
case TRE_LITERAL_OPT_CONTAINS:
case TRE_LITERAL_OPT_NONE:
break;
}
}
return REG_NOMATCH;
}
/* Fills the POSIX.2 regmatch_t array according to the TNFA tag and match
endpoint values. */
void
tre_fill_pmatch(size_t nmatch, regmatch_t pmatch[], int cflags,
const tre_tnfa_t *tnfa, int *tags, int match_eo)
{
tre_submatch_data_t *submatch_data;
unsigned int i, j;
int *parents;
i = 0;
if (match_eo >= 0 && !(cflags & REG_NOSUB))
{
/* Construct submatch offsets from the tags. */
DPRINT(("end tag = t%d = %d\n", tnfa->end_tag, match_eo));
submatch_data = tnfa->submatch_data;
while (i < tnfa->num_submatches && i < nmatch)
{
if (submatch_data[i].so_tag == tnfa->end_tag)
pmatch[i].rm_so = match_eo;
else
pmatch[i].rm_so = tags[submatch_data[i].so_tag];
if (submatch_data[i].eo_tag == tnfa->end_tag)
pmatch[i].rm_eo = match_eo;
else
pmatch[i].rm_eo = tags[submatch_data[i].eo_tag];
/* If either of the endpoints were not used, this submatch
was not part of the match. */
if (pmatch[i].rm_so == -1 || pmatch[i].rm_eo == -1)
pmatch[i].rm_so = pmatch[i].rm_eo = -1;
DPRINT(("pmatch[%d] = {t%d = %d, t%d = %d}\n", i,
submatch_data[i].so_tag, pmatch[i].rm_so,
submatch_data[i].eo_tag, pmatch[i].rm_eo));
i++;
}
/* Reset all submatches that are not within all of their parent
submatches. */
i = 0;
while (i < tnfa->num_submatches && i < nmatch)
{
if (pmatch[i].rm_eo == -1)
assert(pmatch[i].rm_so == -1);
assert(pmatch[i].rm_so <= pmatch[i].rm_eo);
parents = submatch_data[i].parents;
if (parents != NULL)
for (j = 0; parents[j] >= 0; j++)
{
DPRINT(("pmatch[%d] parent %d\n", i, parents[j]));
if (pmatch[i].rm_so < pmatch[parents[j]].rm_so
|| pmatch[i].rm_eo > pmatch[parents[j]].rm_eo)
pmatch[i].rm_so = pmatch[i].rm_eo = -1;
}
i++;
}
}
while (i < nmatch)
{
pmatch[i].rm_so = -1;
pmatch[i].rm_eo = -1;
i++;
}
}
/*
Wrapper functions for POSIX compatible regexp matching.
*/
int
tre_have_backrefs(const regex_t *preg)
{
tre_tnfa_t *tnfa = (void *)preg->TRE_REGEX_T_FIELD;
return tnfa->have_backrefs;
}
int
tre_have_approx(const regex_t *preg)
{
tre_tnfa_t *tnfa = (void *)preg->TRE_REGEX_T_FIELD;
return tnfa->have_approx;
}
static int
tre_match(const tre_tnfa_t *tnfa, const void *string, ssize_t len,
tre_str_type_t type, size_t nmatch, regmatch_t pmatch[],
int eflags)
{
reg_errcode_t status;
int *tags = NULL, eo;
if (tnfa->num_tags > 0 && nmatch > 0)
{
#ifdef TRE_USE_ALLOCA
tags = alloca(sizeof(*tags) * tnfa->num_tags);
#else /* !TRE_USE_ALLOCA */
tags = xmalloc(sizeof(*tags) * tnfa->num_tags);
#endif /* !TRE_USE_ALLOCA */
if (tags == NULL)
return REG_ESPACE;
}
if (type == STR_BYTE
&& tnfa->literal_opt.mode != TRE_LITERAL_OPT_NONE
&& (nmatch == 0 || (tnfa->cflags & REG_NOSUB))
#ifdef TRE_APPROX
&& !(eflags & REG_APPROX_MATCHER)
#endif /* TRE_APPROX */
&& !(eflags & REG_BACKTRACKING_MATCHER))
{
size_t byte_len = (len >= 0) ? (size_t)len : strlen((const char *)string);
status = tre_match_literal_opt(tnfa, string, byte_len, eflags, &eo);
/* Even when the caller asked for no submatches, regexec() still has to
* clear any pmatch entries it was handed. The normal matcher path does
* this through tre_fill_pmatch(), so mirror that behavior here. */
if (status == REG_OK && nmatch > 0)
tre_fill_pmatch(nmatch, pmatch, tnfa->cflags, tnfa, NULL, eo);
#ifndef TRE_USE_ALLOCA
if (tags)
xfree(tags);
#endif /* !TRE_USE_ALLOCA */
return status;
}
/* Dispatch to the appropriate matcher. */
if (tnfa->have_backrefs || eflags & REG_BACKTRACKING_MATCHER)
{
/* The regex has back references, use the backtracking matcher. */
if (type == STR_USER)
{
const tre_str_source *source = string;
if (source->rewind == NULL || source->compare == NULL)
{
/* The backtracking matcher requires rewind and compare
capabilities from the input stream. */
#ifndef TRE_USE_ALLOCA
if (tags)
xfree(tags);
#endif /* !TRE_USE_ALLOCA */
return REG_BADPAT;
}
}
status = tre_tnfa_run_backtrack(tnfa, string, len, type,
tags, eflags, &eo);
}
#ifdef TRE_APPROX
else if (tnfa->have_approx || eflags & REG_APPROX_MATCHER)
{
/* The regex uses approximate matching, use the approximate matcher. */
regamatch_t match;
regaparams_t params;
tre_regaparams_default(&params);
params.max_err = 0;
params.max_cost = 0;
status = tre_tnfa_run_approx(tnfa, string, len, type, tags,
&match, params, eflags, &eo);
}
#endif /* TRE_APPROX */
else
{
/* Exact matching, no back references, use the parallel matcher. */
status = tre_tnfa_run_parallel(tnfa, string, len, type,
tags, eflags, &eo);
}
if (status == REG_OK)
/* A match was found, so fill the submatch registers. */
tre_fill_pmatch(nmatch, pmatch, tnfa->cflags, tnfa, tags, eo);
#ifndef TRE_USE_ALLOCA
if (tags)
xfree(tags);
#endif /* !TRE_USE_ALLOCA */
return status;
}
int
tre_regnexec(const regex_t *preg, const char *str, size_t len,
size_t nmatch, regmatch_t pmatch[], int eflags)
{
tre_tnfa_t *tnfa = (void *)preg->TRE_REGEX_T_FIELD;
tre_str_type_t type = (TRE_MB_CUR_MAX == 1) ? STR_BYTE : STR_MBS;
return tre_match(tnfa, str, len, type, nmatch, pmatch, eflags);
}
#ifdef TRE_USE_GNUC_REGEXEC_FPL
int
tre_regexec(const regex_t *preg, const char *str,
size_t nmatch, regmatch_t pmatch[_Restrict_arr_ _REGEX_NELTS (nmatch)],
int eflags)
#else
int
tre_regexec(const regex_t *preg, const char *str,
size_t nmatch, regmatch_t pmatch[], int eflags)
#endif
{
return tre_regnexec(preg, str, -1, nmatch, pmatch, eflags);
}
int
tre_regexecb(const regex_t *preg, const char *str,
size_t nmatch, regmatch_t pmatch[], int eflags)
{
tre_tnfa_t *tnfa = (void *)preg->TRE_REGEX_T_FIELD;
return tre_match(tnfa, str, -1, STR_BYTE, nmatch, pmatch, eflags);
}
int
tre_regnexecb(const regex_t *preg, const char *str, size_t len,
size_t nmatch, regmatch_t pmatch[], int eflags)
{
tre_tnfa_t *tnfa = (void *)preg->TRE_REGEX_T_FIELD;
return tre_match(tnfa, str, len, STR_BYTE, nmatch, pmatch, eflags);
}
#ifdef TRE_WCHAR
int
tre_regwnexec(const regex_t *preg, const wchar_t *str, size_t len,
size_t nmatch, regmatch_t pmatch[], int eflags)
{
tre_tnfa_t *tnfa = (void *)preg->TRE_REGEX_T_FIELD;
return tre_match(tnfa, str, len, STR_WIDE, nmatch, pmatch, eflags);
}
int
tre_regwexec(const regex_t *preg, const wchar_t *str,
size_t nmatch, regmatch_t pmatch[], int eflags)
{
return tre_regwnexec(preg, str, -1, nmatch, pmatch, eflags);
}
#endif /* TRE_WCHAR */
int
tre_reguexec(const regex_t *preg, const tre_str_source *str,
size_t nmatch, regmatch_t pmatch[], int eflags)
{
tre_tnfa_t *tnfa = (void *)preg->TRE_REGEX_T_FIELD;
return tre_match(tnfa, str, -1, STR_USER, nmatch, pmatch, eflags);
}
#ifdef TRE_APPROX
/*
Wrapper functions for approximate regexp matching.
*/
static int
tre_match_approx(const tre_tnfa_t *tnfa, const void *string, ssize_t len,
tre_str_type_t type, regamatch_t *match, regaparams_t params,
int eflags)
{
reg_errcode_t status;
int *tags = NULL, eo;
/* If the regexp does not use approximate matching features, the
maximum cost is zero, and the approximate matcher isn't forced,
use the exact matcher instead. */
if (params.max_cost == 0 && !tnfa->have_approx
&& !(eflags & REG_APPROX_MATCHER))
return tre_match(tnfa, string, len, type, match->nmatch, match->pmatch,
eflags);
/* Back references are not supported by the approximate matcher. */
if (tnfa->have_backrefs)
return REG_BADPAT;
if (tnfa->num_tags > 0 && match->nmatch > 0)
{
#if TRE_USE_ALLOCA
tags = alloca(sizeof(*tags) * tnfa->num_tags);
#else /* !TRE_USE_ALLOCA */
tags = xmalloc(sizeof(*tags) * tnfa->num_tags);
#endif /* !TRE_USE_ALLOCA */
if (tags == NULL)
return REG_ESPACE;
}
status = tre_tnfa_run_approx(tnfa, string, len, type, tags,
match, params, eflags, &eo);
if (status == REG_OK)
tre_fill_pmatch(match->nmatch, match->pmatch, tnfa->cflags, tnfa, tags, eo);
#ifndef TRE_USE_ALLOCA
if (tags)
xfree(tags);
#endif /* !TRE_USE_ALLOCA */
return status;
}
int
tre_reganexec(const regex_t *preg, const char *str, size_t len,
regamatch_t *match, regaparams_t params, int eflags)
{
tre_tnfa_t *tnfa = (void *)preg->TRE_REGEX_T_FIELD;
tre_str_type_t type = (TRE_MB_CUR_MAX == 1) ? STR_BYTE : STR_MBS;
return tre_match_approx(tnfa, str, len, type, match, params, eflags);
}
int
tre_regaexec(const regex_t *preg, const char *str,
regamatch_t *match, regaparams_t params, int eflags)
{
return tre_reganexec(preg, str, -1, match, params, eflags);
}
int
tre_regaexecb(const regex_t *preg, const char *str,
regamatch_t *match, regaparams_t params, int eflags)
{
tre_tnfa_t *tnfa = (void *)preg->TRE_REGEX_T_FIELD;
return tre_match_approx(tnfa, str, -1, STR_BYTE, match, params, eflags);
}
#ifdef TRE_WCHAR
int
tre_regawnexec(const regex_t *preg, const wchar_t *str, size_t len,
regamatch_t *match, regaparams_t params, int eflags)
{
tre_tnfa_t *tnfa = (void *)preg->TRE_REGEX_T_FIELD;
return tre_match_approx(tnfa, str, len, STR_WIDE,
match, params, eflags);
}
int
tre_regawexec(const regex_t *preg, const wchar_t *str,
regamatch_t *match, regaparams_t params, int eflags)
{
return tre_regawnexec(preg, str, -1, match, params, eflags);
}
#endif /* TRE_WCHAR */
void
tre_regaparams_default(regaparams_t *params)
{
memset(params, 0, sizeof(*params));
params->cost_ins = 1;
params->cost_del = 1;
params->cost_subst = 1;
params->max_cost = INT_MAX;
params->max_ins = INT_MAX;
params->max_del = INT_MAX;
params->max_subst = INT_MAX;
params->max_err = INT_MAX;
}
#endif /* TRE_APPROX */
/* EOF */
+226
View File
@@ -0,0 +1,226 @@
/*
tre-ast.c - Abstract syntax tree (AST) routines
This software is released under a BSD-style license.
See the file LICENSE for details and copyright.
*/
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif /* HAVE_CONFIG_H */
#include <assert.h>
#include "tre-ast.h"
#include "tre-mem.h"
tre_ast_node_t *
tre_ast_new_node(tre_mem_t mem, tre_ast_type_t type, size_t size)
{
tre_ast_node_t *node;
node = tre_mem_calloc(mem, sizeof(*node));
if (!node)
return NULL;
node->obj = tre_mem_calloc(mem, size);
if (!node->obj)
return NULL;
node->type = type;
node->nullable = -1;
node->submatch_id = -1;
return node;
}
tre_ast_node_t *
tre_ast_new_literal(tre_mem_t mem, int code_min, int code_max)
{
tre_ast_node_t *node;
tre_literal_t *lit;
node = tre_ast_new_node(mem, LITERAL, sizeof(tre_literal_t));
if (!node)
return NULL;
lit = node->obj;
lit->code_min = code_min;
lit->code_max = code_max;
lit->position = -1;
return node;
}
tre_ast_node_t *
tre_ast_new_iter(tre_mem_t mem, tre_ast_node_t *arg, int min, int max,
int minimal)
{
tre_ast_node_t *node;
tre_iteration_t *iter;
node = tre_ast_new_node(mem, ITERATION, sizeof(tre_iteration_t));
if (!node)
return NULL;
iter = node->obj;
iter->arg = arg;
iter->min = min;
iter->max = max;
iter->minimal = minimal;
node->num_submatches = arg->num_submatches;
return node;
}
tre_ast_node_t *
tre_ast_new_union(tre_mem_t mem, tre_ast_node_t *left, tre_ast_node_t *right)
{
tre_ast_node_t *node;
node = tre_ast_new_node(mem, UNION, sizeof(tre_union_t));
if (node == NULL)
return NULL;
((tre_union_t *)node->obj)->left = left;
((tre_union_t *)node->obj)->right = right;
node->num_submatches = left->num_submatches + right->num_submatches;
return node;
}
tre_ast_node_t *
tre_ast_new_catenation(tre_mem_t mem, tre_ast_node_t *left,
tre_ast_node_t *right)
{
tre_ast_node_t *node;
node = tre_ast_new_node(mem, CATENATION, sizeof(tre_catenation_t));
if (node == NULL)
return NULL;
((tre_catenation_t *)node->obj)->left = left;
((tre_catenation_t *)node->obj)->right = right;
node->num_submatches = left->num_submatches + right->num_submatches;
return node;
}
#ifdef TRE_DEBUG
static void
tre_findent(FILE *stream, int i)
{
while (i-- > 0)
fputc(' ', stream);
}
void
tre_print_params(int *params)
{
int i;
if (params)
{
DPRINT(("params ["));
for (i = 0; i < TRE_PARAM_LAST; i++)
{
if (params[i] == TRE_PARAM_UNSET)
DPRINT(("unset"));
else if (params[i] == TRE_PARAM_DEFAULT)
DPRINT(("default"));
else
DPRINT(("%d", params[i]));
if (i < TRE_PARAM_LAST - 1)
DPRINT((", "));
}
DPRINT(("]"));
}
}
static void
tre_do_print(FILE *stream, tre_ast_node_t *ast, int indent)
{
int code_min, code_max, pos;
int num_tags = ast->num_tags;
tre_literal_t *lit;
tre_iteration_t *iter;
tre_findent(stream, indent);
switch (ast->type)
{
case LITERAL:
lit = ast->obj;
code_min = lit->code_min;
code_max = lit->code_max;
pos = lit->position;
if (IS_EMPTY(lit))
{
fprintf(stream, "literal empty\n");
}
else if (IS_ASSERTION(lit))
{
int i;
char *assertions[] = { "bol", "eol", "ctype", "!ctype",
"bow", "eow", "wb", "!wb" };
if (code_max >= ASSERT_LAST << 1)
assert(0);
fprintf(stream, "assertions: ");
for (i = 0; (1 << i) <= ASSERT_LAST; i++)
if (code_max & (1 << i))
fprintf(stream, "%s ", assertions[i]);
fprintf(stream, "\n");
}
else if (IS_TAG(lit))
{
fprintf(stream, "tag %d\n", code_max);
}
else if (IS_BACKREF(lit))
{
fprintf(stream, "backref %d, pos %d\n", code_max, pos);
}
else if (IS_PARAMETER(lit))
{
tre_print_params(lit->u.params);
fprintf(stream, "\n");
}
else
{
fprintf(stream, "literal (%c, %c) (%d, %d), pos %d, sub %d, "
"%d tags\n", code_min, code_max, code_min, code_max, pos,
ast->submatch_id, num_tags);
}
break;
case ITERATION:
iter = ast->obj;
fprintf(stream, "iteration {%d, %d}, sub %d, %d tags, %s\n",
iter->min, iter->max, ast->submatch_id, num_tags,
iter->minimal ? "minimal" : "greedy");
tre_do_print(stream, iter->arg, indent + 2);
break;
case UNION:
fprintf(stream, "union, sub %d, %d tags\n", ast->submatch_id, num_tags);
tre_do_print(stream, ((tre_union_t *)ast->obj)->left, indent + 2);
tre_do_print(stream, ((tre_union_t *)ast->obj)->right, indent + 2);
break;
case CATENATION:
fprintf(stream, "catenation, sub %d, %d tags\n", ast->submatch_id,
num_tags);
tre_do_print(stream, ((tre_catenation_t *)ast->obj)->left, indent + 2);
tre_do_print(stream, ((tre_catenation_t *)ast->obj)->right, indent + 2);
break;
default:
assert(0);
break;
}
}
static void
tre_ast_fprint(FILE *stream, tre_ast_node_t *ast)
{
tre_do_print(stream, ast, 0);
}
void
tre_ast_print(tre_ast_node_t *tree)
{
printf("AST:\n");
tre_ast_fprint(stdout, tree);
}
#endif /* TRE_DEBUG */
/* EOF */
+128
View File
@@ -0,0 +1,128 @@
/*
tre-ast.h - Abstract syntax tree (AST) definitions
This software is released under a BSD-style license.
See the file LICENSE for details and copyright.
*/
#ifndef TRE_AST_H
#define TRE_AST_H 1
#include "tre-mem.h"
#include "tre-internal.h"
#include "tre-compile.h"
/* The different AST node types. */
typedef enum {
LITERAL,
CATENATION,
ITERATION,
UNION
} tre_ast_type_t;
/* Special subtypes of TRE_LITERAL. */
#define EMPTY -1 /* Empty leaf (denotes empty string). */
#define ASSERTION -2 /* Assertion leaf. */
#define TAG -3 /* Tag leaf. */
#define BACKREF -4 /* Back reference leaf. */
#define PARAMETER -5 /* Parameter. */
#define IS_SPECIAL(x) ((x)->code_min < 0)
#define IS_EMPTY(x) ((x)->code_min == EMPTY)
#define IS_ASSERTION(x) ((x)->code_min == ASSERTION)
#define IS_TAG(x) ((x)->code_min == TAG)
#define IS_BACKREF(x) ((x)->code_min == BACKREF)
#define IS_PARAMETER(x) ((x)->code_min == PARAMETER)
/* A generic AST node. All AST nodes consist of this node on the top
level with `obj' pointing to the actual content. */
typedef struct {
tre_ast_type_t type; /* Type of the node. */
void *obj; /* Pointer to actual node. */
int nullable;
int submatch_id;
unsigned int num_submatches;
unsigned int num_tags;
tre_pos_and_tags_t *firstpos;
tre_pos_and_tags_t *lastpos;
} tre_ast_node_t;
/* A "literal" node. These are created for assertions, back references,
tags, matching parameter settings, and all expressions that match one
character. */
typedef struct {
long code_min;
long code_max;
int position;
union {
tre_ctype_t class;
int *params;
} u;
tre_ctype_t *neg_classes;
} tre_literal_t;
/* A "catenation" node. These are created when two regexps are concatenated.
If there are more than one subexpressions in sequence, the `left' part
holds all but the last, and `right' part holds the last subexpression
(catenation is left associative). */
typedef struct {
tre_ast_node_t *left;
tre_ast_node_t *right;
} tre_catenation_t;
/* An "iteration" node. These are created for the "*", "+", "?", and "{m,n}"
operators. */
typedef struct {
/* Subexpression to match. */
tre_ast_node_t *arg;
/* Minimum number of consecutive matches. */
int min;
/* Maximum number of consecutive matches. */
int max;
/* If 0, match as many characters as possible, if 1 match as few as
possible. Note that this does not always mean the same thing as
matching as many/few repetitions as possible. */
unsigned int minimal:1;
/* Approximate matching parameters (or NULL). */
int *params;
} tre_iteration_t;
/* An "union" node. These are created for the "|" operator. */
typedef struct {
tre_ast_node_t *left;
tre_ast_node_t *right;
} tre_union_t;
tre_ast_node_t *
tre_ast_new_node(tre_mem_t mem, tre_ast_type_t type, size_t size);
tre_ast_node_t *
tre_ast_new_literal(tre_mem_t mem, int code_min, int code_max);
tre_ast_node_t *
tre_ast_new_iter(tre_mem_t mem, tre_ast_node_t *arg, int min, int max,
int minimal);
tre_ast_node_t *
tre_ast_new_union(tre_mem_t mem, tre_ast_node_t *left, tre_ast_node_t *right);
tre_ast_node_t *
tre_ast_new_catenation(tre_mem_t mem, tre_ast_node_t *left,
tre_ast_node_t *right);
#ifdef TRE_DEBUG
void
tre_ast_print(tre_ast_node_t *tree);
/* XXX - rethink AST printing API */
void
tre_print_params(int *params);
#endif /* TRE_DEBUG */
#endif /* TRE_AST_H */
/* EOF */
+2673
View File
File diff suppressed because it is too large Load Diff
+27
View File
@@ -0,0 +1,27 @@
/*
tre-compile.h: Regex compilation definitions
This software is released under a BSD-style license.
See the file LICENSE for details and copyright.
*/
#ifndef TRE_COMPILE_H
#define TRE_COMPILE_H 1
typedef struct {
int position;
int code_min;
int code_max;
int *tags;
int assertions;
tre_ctype_t class;
tre_ctype_t *neg_classes;
int backref;
int *params;
} tre_pos_and_tags_t;
#endif /* TRE_COMPILE_H */
/* EOF */
+73
View File
@@ -0,0 +1,73 @@
/*
tre-filter.c: Histogram filter to quickly find regexp match candidates
This software is released under a BSD-style license.
See the file LICENSE for details and copyright.
*/
/* The idea of this filter is quite simple. First, let's assume the
search pattern is a simple string. In order for a substring of a
longer string to match the search pattern, it must have the same
numbers of different characters as the pattern, and those
characters must occur in the same order as they occur in pattern. */
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif /* HAVE_CONFIG_H */
#include <stdio.h>
#include "tre-internal.h"
#include "tre-filter.h"
int
tre_filter_find(const unsigned char *str, size_t len, tre_filter_t *filter)
{
unsigned short counts[256];
unsigned int i;
unsigned int window_len = filter->window_len;
tre_filter_profile_t *profile = filter->profile;
const unsigned char *str_orig = str;
DPRINT(("tre_filter_find: %.*s\n", len, str));
for (i = 0; i < elementsof(counts); i++)
counts[i] = 0;
i = 0;
while (*str && i < window_len && i < len)
{
counts[*str]++;
i++;
str++;
len--;
}
while (len > 0)
{
tre_filter_profile_t *p;
counts[*str]++;
counts[*(str - window_len)]--;
p = profile;
while (p->ch)
{
if (counts[p->ch] < p->count)
break;
p++;
}
if (!p->ch)
{
DPRINT(("Found possible match at %d\n",
str - str_orig));
return str - str_orig;
}
else
{
DPRINT(("No match so far...\n"));
}
len--;
str++;
}
DPRINT(("This string cannot match.\n"));
return -1;
}
+19
View File
@@ -0,0 +1,19 @@
typedef struct {
unsigned char ch;
unsigned char count;
} tre_filter_profile_t;
typedef struct {
/* Length of the window where the character counts are kept. */
int window_len;
/* Required character counts table. */
tre_filter_profile_t *profile;
} tre_filter_t;
int
tre_filter_find(const unsigned char *str, size_t len, tre_filter_t *filter);
+319
View File
@@ -0,0 +1,319 @@
/*
tre-internal.h - TRE internal definitions
This software is released under a BSD-style license.
See the file LICENSE for details and copyright.
*/
#ifndef TRE_INTERNAL_H
#define TRE_INTERNAL_H 1
#ifdef HAVE_WCHAR_H
#include <wchar.h>
#endif /* HAVE_WCHAR_H */
#ifdef HAVE_WCTYPE_H
#include <wctype.h>
#endif /* HAVE_WCTYPE_H */
#ifdef HAVE_SYS_TYPES_H
#include <sys/types.h>
#endif /* HAVE_SYS_TYPES_H */
#include <limits.h>
#include <ctype.h>
#include "../local_includes/tre.h"
#define TRE_MAX_RE 65536
#define TRE_MAX_STRING INT_MAX
#define TRE_MAX_STACK 1048576
#ifdef TRE_DEBUG
#include <stdio.h>
#define DPRINT(msg) do {printf msg; fflush(stdout);} while(/*CONSTCOND*/(void)0,0)
#else /* !TRE_DEBUG */
#define DPRINT(msg) do { } while(/*CONSTCOND*/(void)0,0)
#endif /* !TRE_DEBUG */
#define elementsof(x) ( sizeof(x) / sizeof(x[0]) )
#ifdef HAVE_MBRTOWC
#define tre_mbrtowc(pwc, s, n, ps) (mbrtowc((pwc), (s), (n), (ps)))
#else /* !HAVE_MBRTOWC */
#ifdef HAVE_MBTOWC
#define tre_mbrtowc(pwc, s, n, ps) (mbtowc((pwc), (s), (n)))
#endif /* HAVE_MBTOWC */
#endif /* !HAVE_MBRTOWC */
#ifdef TRE_MULTIBYTE
#ifdef HAVE_MBSTATE_T
#define TRE_MBSTATE
#endif /* TRE_MULTIBYTE */
#endif /* HAVE_MBSTATE_T */
/* Define the character types and functions. */
#ifdef TRE_WCHAR
/* Wide characters. */
typedef wint_t tre_cint_t;
#if WCHAR_MAX <= INT_MAX
#define TRE_CHAR_MAX WCHAR_MAX
#else /* WCHAR_MAX > INT_MAX */
#define TRE_CHAR_MAX INT_MAX
#endif
#ifdef TRE_MULTIBYTE
#define TRE_MB_CUR_MAX MB_CUR_MAX
#else /* !TRE_MULTIBYTE */
#define TRE_MB_CUR_MAX 1
#endif /* !TRE_MULTIBYTE */
#define tre_isalnum iswalnum
#define tre_isalpha iswalpha
#ifdef HAVE_ISWBLANK
#define tre_isblank iswblank
#endif /* HAVE_ISWBLANK */
#define tre_iscntrl iswcntrl
#define tre_isdigit iswdigit
#define tre_isgraph iswgraph
#define tre_islower iswlower
#define tre_isprint iswprint
#define tre_ispunct iswpunct
#define tre_isspace iswspace
#define tre_isupper iswupper
#define tre_isxdigit iswxdigit
#define tre_tolower towlower
#define tre_toupper towupper
#define tre_strlen wcslen
#else /* !TRE_WCHAR */
/* 8 bit characters. */
typedef short tre_cint_t;
#define TRE_CHAR_MAX 255
#define TRE_MB_CUR_MAX 1
#define tre_isalnum isalnum
#define tre_isalpha isalpha
#ifdef HAVE_ISASCII
#define tre_isascii isascii
#endif /* HAVE_ISASCII */
#ifdef HAVE_ISBLANK
#define tre_isblank isblank
#endif /* HAVE_ISBLANK */
#define tre_iscntrl iscntrl
#define tre_isdigit isdigit
#define tre_isgraph isgraph
#define tre_islower islower
#define tre_isprint isprint
#define tre_ispunct ispunct
#define tre_isspace isspace
#define tre_isupper isupper
#define tre_isxdigit isxdigit
#define tre_tolower(c) (tre_cint_t)(tolower(c))
#define tre_toupper(c) (tre_cint_t)(toupper(c))
#define tre_strlen(s) (strlen((const char*)s))
#endif /* !TRE_WCHAR */
#if defined(TRE_WCHAR) && defined(HAVE_ISWCTYPE) && defined(HAVE_WCTYPE)
#define TRE_USE_SYSTEM_WCTYPE 1
#endif
#ifdef TRE_USE_SYSTEM_WCTYPE
/* Use system provided iswctype() and wctype(). */
typedef wctype_t tre_ctype_t;
#define tre_isctype iswctype
#define tre_ctype wctype
#else /* !TRE_USE_SYSTEM_WCTYPE */
/* Define our own versions of iswctype() and wctype(). */
typedef int (*tre_ctype_t)(tre_cint_t);
#define tre_isctype(c, type) ( (type)(c) )
tre_ctype_t tre_ctype(const char *name);
#endif /* !TRE_USE_SYSTEM_WCTYPE */
typedef enum { STR_WIDE, STR_BYTE, STR_MBS, STR_USER } tre_str_type_t;
/* Returns number of bytes to add to (char *)ptr to make it
properly aligned for the type. */
#define ALIGN(ptr, type) \
((((long)ptr) % sizeof(type)) \
? (sizeof(type) - (((long)ptr) % sizeof(type))) \
: 0)
#undef MAX
#undef MIN
#define MAX(a, b) (((a) >= (b)) ? (a) : (b))
#define MIN(a, b) (((a) <= (b)) ? (a) : (b))
/* Define STRF to the correct printf formatter for strings. */
#ifdef TRE_WCHAR
#define STRF "ls"
#else /* !TRE_WCHAR */
#define STRF "s"
#endif /* !TRE_WCHAR */
/* TNFA transition type. A TNFA state is an array of transitions,
the terminator is a transition with NULL `state'. */
typedef struct tnfa_transition tre_tnfa_transition_t;
struct tnfa_transition {
/* Range of accepted characters. */
tre_cint_t code_min;
tre_cint_t code_max;
/* Pointer to the destination state. */
tre_tnfa_transition_t *state;
/* ID number of the destination state. */
int state_id;
/* -1 terminated array of tags (or NULL). */
int *tags;
/* Matching parameters settings (or NULL). */
int *params;
/* Assertion bitmap. */
int assertions;
/* Assertion parameters. */
union {
/* Character class assertion. */
tre_ctype_t class;
/* Back reference assertion. */
int backref;
} u;
/* Negative character class assertions. */
tre_ctype_t *neg_classes;
};
/* Assertions. */
#define ASSERT_AT_BOL 1 /* Beginning of line. */
#define ASSERT_AT_EOL 2 /* End of line. */
#define ASSERT_CHAR_CLASS 4 /* Character class in `class'. */
#define ASSERT_CHAR_CLASS_NEG 8 /* Character classes in `neg_classes'. */
#define ASSERT_AT_BOW 16 /* Beginning of word. */
#define ASSERT_AT_EOW 32 /* End of word. */
#define ASSERT_AT_WB 64 /* Word boundary. */
#define ASSERT_AT_WB_NEG 128 /* Not a word boundary. */
#define ASSERT_BACKREF 256 /* A back reference in `backref'. */
#define ASSERT_LAST 256
/* Tag directions. */
typedef enum {
TRE_TAG_MINIMIZE = 0,
TRE_TAG_MAXIMIZE = 1
} tre_tag_direction_t;
/* Parameters that can be changed dynamically while matching. */
typedef enum {
TRE_PARAM_COST_INS = 0,
TRE_PARAM_COST_DEL = 1,
TRE_PARAM_COST_SUBST = 2,
TRE_PARAM_COST_MAX = 3,
TRE_PARAM_MAX_INS = 4,
TRE_PARAM_MAX_DEL = 5,
TRE_PARAM_MAX_SUBST = 6,
TRE_PARAM_MAX_ERR = 7,
TRE_PARAM_DEPTH = 8,
TRE_PARAM_LAST = 9
} tre_param_t;
/* Unset matching parameter */
#define TRE_PARAM_UNSET -1
/* Signifies the default matching parameter value. */
#define TRE_PARAM_DEFAULT -2
/* Instructions to compute submatch register values from tag values
after a successful match. */
struct tre_submatch_data {
/* Tag that gives the value for rm_so (submatch start offset). */
int so_tag;
/* Tag that gives the value for rm_eo (submatch end offset). */
int eo_tag;
/* List of submatches this submatch is contained in. */
int *parents;
};
typedef struct tre_submatch_data tre_submatch_data_t;
typedef enum {
TRE_LITERAL_OPT_NONE = 0,
TRE_LITERAL_OPT_CONTAINS,
TRE_LITERAL_OPT_PREFIX,
TRE_LITERAL_OPT_SUFFIX,
TRE_LITERAL_OPT_EXACT
} tre_literal_opt_mode_t;
typedef struct {
unsigned char *data;
size_t len;
} tre_literal_opt_literal_t;
typedef struct {
tre_literal_opt_mode_t mode;
int nocase;
size_t num_literals;
/* Folded byte mapping used by the nocase fast path. */
unsigned char fold_map[256];
/* Literal index ranges grouped by the first literal byte. */
size_t start_offsets[257];
tre_literal_opt_literal_t *literals;
} tre_literal_opt_t;
/* TNFA definition. */
typedef struct tnfa tre_tnfa_t;
struct tnfa {
tre_tnfa_transition_t *transitions;
unsigned int num_transitions;
tre_tnfa_transition_t *initial;
tre_tnfa_transition_t *final;
tre_submatch_data_t *submatch_data;
char *firstpos_chars;
int first_char;
unsigned int num_submatches;
tre_tag_direction_t *tag_directions;
int *minimal_tags;
int num_tags;
int num_minimals;
int end_tag;
int num_states;
int cflags;
int have_backrefs;
int have_approx;
int params_depth;
tre_literal_opt_t literal_opt;
};
int
tre_compile(regex_t *preg, const tre_char_t *regex, size_t n, int cflags);
void
tre_free(regex_t *preg);
void
tre_fill_pmatch(size_t nmatch, regmatch_t pmatch[], int cflags,
const tre_tnfa_t *tnfa, int *tags, int match_eo);
reg_errcode_t
tre_tnfa_run_parallel(const tre_tnfa_t *tnfa, const void *string, ssize_t len,
tre_str_type_t type, int *match_tags, int eflags,
int *match_end_ofs);
reg_errcode_t
tre_tnfa_run_backtrack(const tre_tnfa_t *tnfa, const void *string, ssize_t len,
tre_str_type_t type, int *match_tags, int eflags,
int *match_end_ofs);
#ifdef TRE_APPROX
reg_errcode_t
tre_tnfa_run_approx(const tre_tnfa_t *tnfa, const void *string, ssize_t len,
tre_str_type_t type, int *match_tags, regamatch_t *match,
regaparams_t params, int eflags, int *match_end_ofs);
#endif /* TRE_APPROX */
#endif /* TRE_INTERNAL_H */
/* EOF */
+676
View File
@@ -0,0 +1,676 @@
/*
tre-match-backtrack.c - TRE backtracking regex matching engine
This software is released under a BSD-style license.
See the file LICENSE for details and copyright.
*/
/*
This matcher is for regexps that use back referencing. Regexp matching
with back referencing is an NP-complete problem on the number of back
references. The easiest way to match them is to use a backtracking
routine which basically goes through all possible paths in the TNFA
and chooses the one which results in the best (leftmost and longest)
match. This can be spectacularly expensive and may run out of stack
space, but there really is no better known generic algorithm. Quoting
Henry Spencer from comp.compilers:
<URL: http://compilers.iecc.com/comparch/article/93-03-102>
POSIX.2 REs require longest match, which is really exciting to
implement since the obsolete ("basic") variant also includes
\<digit>. I haven't found a better way of tackling this than doing
a preliminary match using a DFA (or simulation) on a modified RE
that just replicates subREs for \<digit>, and then doing a
backtracking match to determine whether the subRE matches were
right. This can be rather slow, but I console myself with the
thought that people who use \<digit> deserve very slow execution.
(Pun unintentional but very appropriate.)
*/
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif /* HAVE_CONFIG_H */
#ifdef TRE_USE_ALLOCA
/* AIX requires this to be the first thing in the file. */
#ifndef __GNUC__
# if HAVE_ALLOCA_H
# include <alloca.h>
# else
# ifdef _AIX
#pragma alloca
# else
# ifndef alloca /* predefined by HP cc +Olibcalls */
char *alloca ();
# endif
# endif
# endif
#endif
#endif /* TRE_USE_ALLOCA */
#include <assert.h>
#include <stdlib.h>
#include <string.h>
#ifdef HAVE_WCHAR_H
#include <wchar.h>
#endif /* HAVE_WCHAR_H */
#ifdef HAVE_WCTYPE_H
#include <wctype.h>
#endif /* HAVE_WCTYPE_H */
#ifndef TRE_WCHAR
#include <ctype.h>
#endif /* !TRE_WCHAR */
#ifdef HAVE_MALLOC_H
#include <malloc.h>
#endif /* HAVE_MALLOC_H */
#include "tre-internal.h"
#include "tre-mem.h"
#include "tre-match-utils.h"
#include "xmalloc.h"
typedef struct {
int pos;
const char *str_byte;
#ifdef TRE_WCHAR
const wchar_t *str_wide;
#endif /* TRE_WCHAR */
tre_tnfa_transition_t *state;
int state_id;
int next_c;
int *tags;
#ifdef TRE_MBSTATE
mbstate_t mbstate;
#endif /* TRE_MBSTATE */
} tre_backtrack_item_t;
typedef struct tre_backtrack_struct {
tre_backtrack_item_t item;
struct tre_backtrack_struct *prev;
struct tre_backtrack_struct *next;
} *tre_backtrack_t;
#ifdef TRE_WCHAR
#define BT_STACK_WIDE_IN(_str_wide) stack->item.str_wide = (_str_wide)
#define BT_STACK_WIDE_OUT (str_wide) = stack->item.str_wide
#else /* !TRE_WCHAR */
#define BT_STACK_WIDE_IN(_str_wide)
#define BT_STACK_WIDE_OUT
#endif /* !TRE_WCHAR */
#ifdef TRE_MBSTATE
#define BT_STACK_MBSTATE_IN stack->item.mbstate = (mbstate)
#define BT_STACK_MBSTATE_OUT (mbstate) = stack->item.mbstate
#else /* !TRE_MBSTATE */
#define BT_STACK_MBSTATE_IN
#define BT_STACK_MBSTATE_OUT
#endif /* !TRE_MBSTATE */
#ifdef TRE_USE_ALLOCA
#define tre_bt_mem_new tre_mem_newa
#define tre_bt_mem_alloc tre_mem_alloca
#define tre_bt_mem_destroy(obj) do { } while (0)
#define xafree(obj) do { } while (0) /* do nothing, obj was obtained with alloca() */
#else /* !TRE_USE_ALLOCA */
#define tre_bt_mem_new tre_mem_new
#define tre_bt_mem_alloc tre_mem_alloc
#define tre_bt_mem_destroy tre_mem_destroy
#define xafree(obj) xfree(obj)
#endif /* !TRE_USE_ALLOCA */
#define BT_STACK_PUSH(_pos, _str_byte, _str_wide, _state, _state_id, _next_c, _tags, _mbstate) \
do \
{ \
int i; \
if (!stack->next) \
{ \
tre_backtrack_t s; \
s = tre_bt_mem_alloc(mem, sizeof(*s)); \
if (!s) \
{ \
tre_bt_mem_destroy(mem); \
if (tags) \
xafree(tags); \
if (pmatch) \
xafree(pmatch); \
if (states_seen) \
xafree(states_seen); \
return REG_ESPACE; \
} \
s->prev = stack; \
s->next = NULL; \
s->item.tags = tre_bt_mem_alloc(mem, \
sizeof(*tags) * tnfa->num_tags); \
if (!s->item.tags) \
{ \
tre_bt_mem_destroy(mem); \
if (tags) \
xafree(tags); \
if (pmatch) \
xafree(pmatch); \
if (states_seen) \
xafree(states_seen); \
return REG_ESPACE; \
} \
stack->next = s; \
stack = s; \
} \
else \
stack = stack->next; \
stack->item.pos = (_pos); \
stack->item.str_byte = (_str_byte); \
BT_STACK_WIDE_IN(_str_wide); \
stack->item.state = (_state); \
stack->item.state_id = (_state_id); \
stack->item.next_c = (_next_c); \
for (i = 0; i < tnfa->num_tags; i++) \
stack->item.tags[i] = (_tags)[i]; \
BT_STACK_MBSTATE_IN; \
} \
while (/*CONSTCOND*/(void)0,0)
#define BT_STACK_POP() \
do \
{ \
int i; \
assert(stack->prev); \
pos = stack->item.pos; \
if (type == STR_USER) \
str_source->rewind(pos + pos_add_next, str_source->context); \
str_byte = stack->item.str_byte; \
BT_STACK_WIDE_OUT; \
state = stack->item.state; \
next_c = (tre_char_t) stack->item.next_c; \
for (i = 0; i < tnfa->num_tags; i++) \
tags[i] = stack->item.tags[i]; \
BT_STACK_MBSTATE_OUT; \
stack = stack->prev; \
} \
while (/*CONSTCOND*/(void)0,0)
#undef MIN
#define MIN(a, b) ((a) <= (b) ? (a) : (b))
reg_errcode_t
tre_tnfa_run_backtrack(const tre_tnfa_t *tnfa, const void *string,
ssize_t len, tre_str_type_t type, int *match_tags,
int eflags, int *match_end_ofs)
{
/* State variables required by GET_NEXT_WCHAR. */
tre_char_t prev_c = 0, next_c = 0;
const char *str_byte = string;
ssize_t pos = 0;
unsigned int pos_add_next = 1;
#ifdef TRE_WCHAR
const wchar_t *str_wide = string;
#ifdef TRE_MBSTATE
mbstate_t mbstate;
#endif /* TRE_MBSTATE */
#endif /* TRE_WCHAR */
int reg_notbol = eflags & REG_NOTBOL;
int reg_noteol = eflags & REG_NOTEOL;
int reg_newline = tnfa->cflags & REG_NEWLINE;
int str_user_end = 0;
/* These are used to remember the necessary values of the above
variables to return to the position where the current search
started from. */
int next_c_start;
const char *str_byte_start;
int pos_start = -1;
#ifdef TRE_WCHAR
const wchar_t *str_wide_start;
#endif /* TRE_WCHAR */
#ifdef TRE_MBSTATE
mbstate_t mbstate_start;
#endif /* TRE_MBSTATE */
reg_errcode_t ret;
/* End offset of best match so far, or -1 if no match found yet. */
int match_eo = -1;
/* Tag arrays. */
int *next_tags, *tags = NULL;
/* Current TNFA state. */
tre_tnfa_transition_t *state;
int *states_seen = NULL;
/* Memory allocator to for allocating the backtracking stack. */
tre_mem_t mem = tre_bt_mem_new();
/* The backtracking stack. */
tre_backtrack_t stack;
tre_tnfa_transition_t *trans_i;
regmatch_t *pmatch = NULL;
/*
* TRE internals tend to use int instead of size_t for positions or
* lengths and don't check for overflow. This will take time to fix
* properly. In the meantime, simply limit the input to what we can
* handle.
*/
if (len > TRE_MAX_STRING)
len = TRE_MAX_STRING;
#ifdef TRE_MBSTATE
memset(&mbstate, '\0', sizeof(mbstate));
#endif /* TRE_MBSTATE */
if (!mem)
return REG_ESPACE;
stack = tre_bt_mem_alloc(mem, sizeof(*stack));
if (!stack)
{
ret = REG_ESPACE;
goto error_exit;
}
stack->prev = NULL;
stack->next = NULL;
DPRINT(("tnfa_execute_backtrack, input type %d\n", type));
DPRINT(("len = %zd\n", len));
#ifdef TRE_USE_ALLOCA
tags = alloca(sizeof(*tags) * tnfa->num_tags);
pmatch = alloca(sizeof(*pmatch) * tnfa->num_submatches);
states_seen = alloca(sizeof(*states_seen) * tnfa->num_states);
#else /* !TRE_USE_ALLOCA */
if (tnfa->num_tags)
{
tags = xmalloc(sizeof(*tags) * tnfa->num_tags);
if (!tags)
{
ret = REG_ESPACE;
goto error_exit;
}
}
if (tnfa->num_submatches)
{
pmatch = xmalloc(sizeof(*pmatch) * tnfa->num_submatches);
if (!pmatch)
{
ret = REG_ESPACE;
goto error_exit;
}
}
if (tnfa->num_states)
{
states_seen = xmalloc(sizeof(*states_seen) * tnfa->num_states);
if (!states_seen)
{
ret = REG_ESPACE;
goto error_exit;
}
}
#endif /* !TRE_USE_ALLOCA */
retry:
{
int i;
for (i = 0; i < tnfa->num_tags; i++)
{
tags[i] = -1;
if (match_tags)
match_tags[i] = -1;
}
for (i = 0; i < tnfa->num_states; i++)
states_seen[i] = 0;
}
state = NULL;
pos = pos_start;
if (type == STR_USER)
str_source->rewind(pos + pos_add_next, str_source->context);
GET_NEXT_WCHAR();
pos_start = pos;
next_c_start = next_c;
str_byte_start = str_byte;
#ifdef TRE_WCHAR
str_wide_start = str_wide;
#endif /* TRE_WCHAR */
#ifdef TRE_MBSTATE
mbstate_start = mbstate;
#endif /* TRE_MBSTATE */
/* Handle initial states. */
next_tags = NULL;
for (trans_i = tnfa->initial; trans_i->state; trans_i++)
{
DPRINT(("> init %p, prev_c %lc\n", trans_i->state, (tre_cint_t)prev_c));
if (trans_i->assertions && CHECK_ASSERTIONS(trans_i->assertions))
{
DPRINT(("assert failed\n"));
continue;
}
if (state == NULL)
{
/* Start from this state. */
state = trans_i->state;
next_tags = trans_i->tags;
}
else
{
/* Backtrack to this state. */
DPRINT(("saving state %d for backtracking\n", trans_i->state_id));
BT_STACK_PUSH(pos, str_byte, str_wide, trans_i->state,
trans_i->state_id, next_c, tags, mbstate);
{
int *tmp = trans_i->tags;
if (tmp)
while (*tmp >= 0)
stack->item.tags[*tmp++] = pos;
}
}
}
if (next_tags)
for (; *next_tags >= 0; next_tags++)
tags[*next_tags] = pos;
DPRINT(("entering match loop, pos %zd, str_byte %p\n", pos, str_byte));
DPRINT(("pos:chr/code | state and tags\n"));
DPRINT(("-------------+------------------------------------------------\n"));
if (state == NULL)
goto backtrack;
while (/*CONSTCOND*/(void)1,1)
{
tre_tnfa_transition_t *next_state;
int empty_br_match;
DPRINT(("start loop\n"));
if (state == tnfa->final)
{
DPRINT((" match found, %d %zd\n", match_eo, pos));
if (match_eo < pos
|| (match_eo == pos
&& match_tags
&& tre_tag_order(tnfa->num_tags, tnfa->tag_directions,
tags, match_tags)))
{
int i;
/* This match wins the previous match. */
DPRINT((" win previous\n"));
match_eo = pos;
if (match_tags)
for (i = 0; i < tnfa->num_tags; i++)
match_tags[i] = tags[i];
}
/* Our TNFAs never have transitions leaving from the final state,
so we jump right to backtracking. */
goto backtrack;
}
#ifdef TRE_DEBUG
DPRINT(("%3zd:%2lc/%05d | %p ", pos, (tre_cint_t)next_c, (int)next_c,
state));
{
int i;
for (i = 0; i < tnfa->num_tags; i++)
DPRINT(("%d%s", tags[i], i < tnfa->num_tags - 1 ? ", " : ""));
DPRINT(("\n"));
}
#endif /* TRE_DEBUG */
/* Go to the next character in the input string. */
empty_br_match = 0;
trans_i = state;
if (trans_i->state && trans_i->assertions & ASSERT_BACKREF)
{
/* This is a back reference state. All transitions leaving from
this state have the same back reference "assertion". Instead
of reading the next character, we match the back reference. */
int so, eo, bt = trans_i->u.backref;
int bt_len;
int result;
DPRINT((" should match back reference %d\n", bt));
/* Get the substring we need to match against. Remember to
turn off REG_NOSUB temporarily. */
tre_fill_pmatch(bt + 1, pmatch, tnfa->cflags & ~REG_NOSUB,
tnfa, tags, pos);
so = pmatch[bt].rm_so;
eo = pmatch[bt].rm_eo;
bt_len = eo - so;
#ifdef TRE_DEBUG
{
int slen;
if (len < 0)
slen = bt_len;
else
slen = MIN(bt_len, len - pos);
if (type == STR_BYTE)
{
DPRINT((" substring (len %d) is [%d, %d[: '%.*s'\n",
bt_len, so, eo, bt_len, (char*)string + so));
DPRINT((" current string is '%.*s'\n", slen, str_byte - 1));
}
#ifdef TRE_WCHAR
else if (type == STR_WIDE)
{
DPRINT((" substring (len %d) is [%d, %d[: '%.*" STRF "'\n",
bt_len, so, eo, bt_len, (wchar_t*)string + so));
DPRINT((" current string is '%.*" STRF "'\n",
slen, str_wide - 1));
}
#endif /* TRE_WCHAR */
}
#endif
if (len < 0)
{
if (type == STR_USER)
result = str_source->compare((unsigned)so, (unsigned)pos,
(unsigned)bt_len,
str_source->context);
#ifdef TRE_WCHAR
else if (type == STR_WIDE)
result = wcsncmp((const wchar_t*)string + so, str_wide - 1,
(size_t)bt_len);
#endif /* TRE_WCHAR */
else
result = strncmp((const char*)string + so, str_byte - 1,
(size_t)bt_len);
}
else if (len - pos < bt_len)
result = 1;
#ifdef TRE_WCHAR
else if (type == STR_WIDE)
result = wmemcmp((const wchar_t*)string + so, str_wide - 1,
(size_t)bt_len);
#endif /* TRE_WCHAR */
else
result = memcmp((const char*)string + so, str_byte - 1,
(size_t)bt_len);
if (result == 0)
{
/* Back reference matched. Check for infinite loop. */
if (bt_len == 0)
empty_br_match = 1;
if (empty_br_match && states_seen[trans_i->state_id])
{
DPRINT((" avoid loop\n"));
goto backtrack;
}
states_seen[trans_i->state_id] = empty_br_match;
/* Advance in input string and resync `prev_c', `next_c'
and pos. */
DPRINT((" back reference matched\n"));
str_byte += bt_len - 1;
#ifdef TRE_WCHAR
str_wide += bt_len - 1;
#endif /* TRE_WCHAR */
pos += bt_len - 1;
GET_NEXT_WCHAR();
DPRINT((" pos now %zd\n", pos));
}
else
{
DPRINT((" back reference did not match\n"));
goto backtrack;
}
}
else
{
/* Check for end of string. */
if (len < 0)
{
if (type == STR_USER)
{
if (str_user_end)
goto backtrack;
}
else if (next_c == L'\0' || pos >= TRE_MAX_STRING)
goto backtrack;
}
else
{
if (pos >= len)
goto backtrack;
}
/* Read the next character. */
GET_NEXT_WCHAR();
}
next_state = NULL;
for (trans_i = state; trans_i->state; trans_i++)
{
DPRINT((" transition %d-%d (%c-%c) %d to %d\n",
trans_i->code_min, trans_i->code_max,
trans_i->code_min, trans_i->code_max,
trans_i->assertions, trans_i->state_id));
if (trans_i->code_min <= (tre_cint_t)prev_c
&& trans_i->code_max >= (tre_cint_t)prev_c)
{
if (trans_i->assertions
&& (CHECK_ASSERTIONS(trans_i->assertions)
|| CHECK_CHAR_CLASSES(trans_i, tnfa, eflags)))
{
DPRINT((" assertion failed\n"));
continue;
}
if (next_state == NULL)
{
/* First matching transition. */
DPRINT((" Next state is %d\n", trans_i->state_id));
next_state = trans_i->state;
next_tags = trans_i->tags;
}
else
{
/* Second matching transition. We may need to backtrack here
to take this transition instead of the first one, so we
push this transition in the backtracking stack so we can
jump back here if needed. */
DPRINT((" saving state %d for backtracking\n",
trans_i->state_id));
BT_STACK_PUSH(pos, str_byte, str_wide, trans_i->state,
trans_i->state_id, next_c, tags, mbstate);
{
int *tmp;
for (tmp = trans_i->tags; tmp && *tmp >= 0; tmp++)
stack->item.tags[*tmp] = pos;
}
#if 0 /* XXX - it's important not to look at all transitions here to keep
the stack small! */
break;
#endif
}
}
}
if (next_state != NULL)
{
/* Matching transitions were found. Take the first one. */
state = next_state;
/* Update the tag values. */
if (next_tags)
while (*next_tags >= 0)
tags[*next_tags++] = pos;
}
else
{
backtrack:
/* A matching transition was not found. Try to backtrack. */
if (stack->prev)
{
DPRINT((" backtracking\n"));
if (stack->item.state->assertions & ASSERT_BACKREF)
{
DPRINT((" states_seen[%d] = 0\n",
stack->item.state_id));
states_seen[stack->item.state_id] = 0;
}
BT_STACK_POP();
}
else if (match_eo < 0)
{
/* Try starting from a later position in the input string. */
/* Check for end of string. */
if (len < 0)
{
if (next_c_start == L'\0' || pos_start >= TRE_MAX_STRING)
{
DPRINT(("end of string.\n"));
break;
}
}
else
{
if (pos_start >= len)
{
DPRINT(("end of string.\n"));
break;
}
}
DPRINT(("restarting from next start position\n"));
next_c = (tre_char_t) next_c_start;
#ifdef TRE_MBSTATE
mbstate = mbstate_start;
#endif /* TRE_MBSTATE */
str_byte = str_byte_start;
#ifdef TRE_WCHAR
str_wide = str_wide_start;
#endif /* TRE_WCHAR */
goto retry;
}
else
{
DPRINT(("finished\n"));
break;
}
}
}
ret = match_eo >= 0 ? REG_OK : REG_NOMATCH;
*match_end_ofs = match_eo;
error_exit:
tre_bt_mem_destroy(mem);
#ifndef TRE_USE_ALLOCA
if (tags)
xafree(tags);
if (pmatch)
xafree(pmatch);
if (states_seen)
xafree(states_seen);
#endif /* !TRE_USE_ALLOCA */
return ret;
}
+538
View File
@@ -0,0 +1,538 @@
/*
tre-match-parallel.c - TRE parallel regex matching engine
This software is released under a BSD-style license.
See the file LICENSE for details and copyright.
*/
/*
This algorithm searches for matches basically by reading characters
in the searched string one by one, starting at the beginning. All
matching paths in the TNFA are traversed in parallel. When two or
more paths reach the same state, exactly one is chosen according to
tag ordering rules; if returning submatches is not required it does
not matter which path is chosen.
The worst case time required for finding the leftmost and longest
match, or determining that there is no match, is always linearly
dependent on the length of the text being searched.
This algorithm cannot handle TNFAs with back referencing nodes.
See `tre-match-backtrack.c'.
*/
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif /* HAVE_CONFIG_H */
#ifdef TRE_USE_ALLOCA
/* AIX requires this to be the first thing in the file. */
#ifndef __GNUC__
# if HAVE_ALLOCA_H
# include <alloca.h>
# else
# ifdef _AIX
#pragma alloca
# else
# ifndef alloca /* predefined by HP cc +Olibcalls */
char *alloca ();
# endif
# endif
# endif
#endif
#endif /* TRE_USE_ALLOCA */
#include <assert.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#ifdef HAVE_WCHAR_H
#include <wchar.h>
#endif /* HAVE_WCHAR_H */
#ifdef HAVE_WCTYPE_H
#include <wctype.h>
#endif /* HAVE_WCTYPE_H */
#ifndef TRE_WCHAR
#include <ctype.h>
#endif /* !TRE_WCHAR */
#ifdef HAVE_MALLOC_H
#include <malloc.h>
#endif /* HAVE_MALLOC_H */
#include "tre-internal.h"
#include "tre-match-utils.h"
#include "xmalloc.h"
typedef struct {
tre_tnfa_transition_t *state;
int *tags;
} tre_tnfa_reach_t;
typedef struct {
int pos;
int **tags;
} tre_reach_pos_t;
#ifdef TRE_DEBUG
static void
tre_print_reach(const tre_tnfa_reach_t *reach, int num_tags)
{
int i;
while (reach->state != NULL)
{
DPRINT((" %p", (void *)reach->state));
if (num_tags > 0)
{
DPRINT(("/"));
for (i = 0; i < num_tags; i++)
{
DPRINT(("%d:%d", i, reach->tags[i]));
if (i < (num_tags-1))
DPRINT((","));
}
}
reach++;
}
DPRINT(("\n"));
}
#endif /* TRE_DEBUG */
reg_errcode_t
tre_tnfa_run_parallel(const tre_tnfa_t *tnfa, const void *string, ssize_t len,
tre_str_type_t type, int *match_tags, int eflags,
int *match_end_ofs)
{
/* State variables required by GET_NEXT_WCHAR. */
tre_char_t prev_c = 0, next_c = 0;
const char *str_byte = string;
ssize_t pos = -1;
unsigned int pos_add_next = 1;
#ifdef TRE_WCHAR
const wchar_t *str_wide = string;
#ifdef TRE_MBSTATE
mbstate_t mbstate;
#endif /* TRE_MBSTATE */
#endif /* TRE_WCHAR */
reg_errcode_t ret;
int reg_notbol = eflags & REG_NOTBOL;
int reg_noteol = eflags & REG_NOTEOL;
int reg_newline = tnfa->cflags & REG_NEWLINE;
int str_user_end = 0;
char *buf;
tre_tnfa_transition_t *trans_i;
tre_tnfa_reach_t *reach, *reach_next, *reach_i, *reach_next_i;
tre_reach_pos_t *reach_pos;
int *tag_i;
int num_tags, i;
int match_eo = -1; /* end offset of match (-1 if no match found yet) */
int new_match = 0;
int *tmp_tags = NULL;
int *tmp_iptr;
/*
* TRE internals tend to use int instead of size_t for positions or
* lengths and don't check for overflow. This will take time to fix
* properly. In the meantime, simply limit the input to what we can
* handle.
*/
if (len > TRE_MAX_STRING)
len = TRE_MAX_STRING;
#ifdef TRE_MBSTATE
memset(&mbstate, '\0', sizeof(mbstate));
#endif /* TRE_MBSTATE */
DPRINT(("tre_tnfa_run_parallel, input type %d\n", type));
if (!match_tags)
num_tags = 0;
else
num_tags = tnfa->num_tags;
/* Allocate memory for temporary data required for matching. This needs to
be done for every matching operation to be thread safe. This allocates
everything in a single large block from the stack frame using alloca()
or with malloc() if alloca is unavailable. */
{
size_t tbytes, rbytes, pbytes, xbytes, total_bytes;
size_t num_states = (size_t)tnfa->num_states;
size_t state_tag_bytes, reach_bytes;
size_t padding = (sizeof(long) - 1) * 4;
char *tmp_buf;
if (num_states > SIZE_MAX / sizeof(*reach_pos))
return REG_ESPACE;
pbytes = sizeof(*reach_pos) * num_states;
if (num_states + 1 > SIZE_MAX / sizeof(*reach_next))
return REG_ESPACE;
rbytes = sizeof(*reach_next) * (num_states + 1);
if ((size_t)num_tags > SIZE_MAX / sizeof(*tmp_tags))
return REG_ESPACE;
tbytes = sizeof(*tmp_tags) * (size_t)num_tags;
if ((size_t)num_tags > SIZE_MAX / sizeof(int))
return REG_ESPACE;
xbytes = sizeof(int) * (size_t)num_tags;
if (num_states > 0 && xbytes > SIZE_MAX / num_states)
return REG_ESPACE;
state_tag_bytes = xbytes * num_states;
if (rbytes > SIZE_MAX - state_tag_bytes)
return REG_ESPACE;
reach_bytes = rbytes + state_tag_bytes;
if (reach_bytes > (SIZE_MAX - padding - tbytes - pbytes) / 2)
return REG_ESPACE;
/* Compute the length of the block we need. */
total_bytes =
padding + reach_bytes * 2 + tbytes + pbytes;
/* Allocate the memory. */
#ifdef TRE_USE_ALLOCA
buf = alloca(total_bytes);
#else /* !TRE_USE_ALLOCA */
buf = xmalloc(total_bytes);
#endif /* !TRE_USE_ALLOCA */
if (buf == NULL)
return REG_ESPACE;
memset(buf, 0, total_bytes);
/* Get the various pointers within tmp_buf (properly aligned). */
tmp_tags = (void *)buf;
tmp_buf = buf + tbytes;
tmp_buf += ALIGN(tmp_buf, long);
reach_next = (void *)tmp_buf;
tmp_buf += rbytes;
tmp_buf += ALIGN(tmp_buf, long);
reach = (void *)tmp_buf;
tmp_buf += rbytes;
tmp_buf += ALIGN(tmp_buf, long);
reach_pos = (void *)tmp_buf;
tmp_buf += pbytes;
tmp_buf += ALIGN(tmp_buf, long);
for (i = 0; i < tnfa->num_states; i++)
{
reach[i].tags = (void *)tmp_buf;
tmp_buf += xbytes;
reach_next[i].tags = (void *)tmp_buf;
tmp_buf += xbytes;
}
}
for (i = 0; i < tnfa->num_states; i++)
reach_pos[i].pos = -1;
/* If only one character can start a match, find it first. */
if (tnfa->first_char >= 0 && type == STR_BYTE && str_byte)
{
const char *orig_str = str_byte;
int first = tnfa->first_char;
if (len >= 0)
str_byte = memchr(orig_str, first, (size_t)len);
else
str_byte = strchr(orig_str, first);
if (str_byte == NULL)
{
#ifndef TRE_USE_ALLOCA
if (buf)
xfree(buf);
#endif /* !TRE_USE_ALLOCA */
return REG_NOMATCH;
}
DPRINT(("skipped %lu chars\n", (unsigned long)(str_byte - orig_str)));
if (str_byte >= orig_str + 1)
prev_c = (unsigned char)*(str_byte - 1);
next_c = (unsigned char)*str_byte;
pos = str_byte - orig_str;
if (len < 0 || pos < len)
str_byte++;
}
else
{
GET_NEXT_WCHAR();
pos = 0;
}
#if 0
/* Skip over characters that cannot possibly be the first character
of a match. */
if (tnfa->firstpos_chars != NULL)
{
char *chars = tnfa->firstpos_chars;
if (len < 0)
{
const char *orig_str = str_byte;
/* XXX - use strpbrk() and wcspbrk() because they might be
optimized for the target architecture. Try also strcspn()
and wcscspn() and compare the speeds. */
while (next_c != L'\0' && !chars[next_c])
{
next_c = *str_byte++;
}
prev_c = *(str_byte - 2);
pos += str_byte - orig_str;
DPRINT(("skipped %d chars\n", str_byte - orig_str));
}
else
{
while (pos <= len && !chars[next_c])
{
prev_c = next_c;
next_c = (unsigned char)(*str_byte++);
pos++;
}
}
}
#endif
DPRINT(("length: %zd\n", len));
DPRINT(("pos:chr/code | states and tags\n"));
DPRINT(("-------------+------------------------------------------------\n"));
reach_next_i = reach_next;
while (/*CONSTCOND*/(void)1,1)
{
/* If no match found yet, add the initial states to `reach_next'. */
if (match_eo < 0)
{
DPRINT((" init >"));
trans_i = tnfa->initial;
while (trans_i->state != NULL)
{
if (reach_pos[trans_i->state_id].pos < pos)
{
if (trans_i->assertions
&& CHECK_ASSERTIONS(trans_i->assertions))
{
DPRINT(("assertion failed\n"));
trans_i++;
continue;
}
DPRINT((" %p", (void *)trans_i->state));
reach_next_i->state = trans_i->state;
for (i = 0; i < num_tags; i++)
reach_next_i->tags[i] = -1;
tag_i = trans_i->tags;
if (tag_i)
while (*tag_i >= 0)
{
if (*tag_i < num_tags)
reach_next_i->tags[*tag_i] = pos;
tag_i++;
}
if (reach_next_i->state == tnfa->final)
{
DPRINT((" found empty match\n"));
match_eo = pos;
new_match = 1;
for (i = 0; i < num_tags; i++)
match_tags[i] = reach_next_i->tags[i];
}
reach_pos[trans_i->state_id].pos = pos;
reach_pos[trans_i->state_id].tags = &reach_next_i->tags;
reach_next_i++;
}
trans_i++;
}
DPRINT(("\n"));
reach_next_i->state = NULL;
}
else
{
if (num_tags == 0 || reach_next_i == reach_next)
/* We have found a match. */
break;
}
/* Check for end of string. */
if (len < 0)
{
if (type == STR_USER)
{
if (str_user_end)
break;
}
else if (next_c == L'\0' || pos >= TRE_MAX_STRING)
break;
}
else
{
if (pos >= len)
break;
}
GET_NEXT_WCHAR();
#ifdef TRE_DEBUG
DPRINT(("%3zd:%2lc/%05d |", pos - 1, (tre_cint_t)prev_c, (int)prev_c));
tre_print_reach(reach_next, num_tags);
DPRINT(("%3zd:%2lc/%05d |", pos, (tre_cint_t)next_c, (int)next_c));
tre_print_reach(reach_next, num_tags);
#endif /* TRE_DEBUG */
/* Swap `reach' and `reach_next'. */
reach_i = reach;
reach = reach_next;
reach_next = reach_i;
/* For each state in `reach', weed out states that don't fulfill the
minimal matching conditions. */
if (tnfa->num_minimals && new_match)
{
new_match = 0;
reach_next_i = reach_next;
for (reach_i = reach; reach_i->state; reach_i++)
{
int skip = 0;
for (i = 0; tnfa->minimal_tags[i] >= 0; i += 2)
{
int end = tnfa->minimal_tags[i];
int start = tnfa->minimal_tags[i + 1];
DPRINT((" Minimal start %d, end %d\n", start, end));
if (end >= num_tags)
{
DPRINT((" Throwing %p out.\n", reach_i->state));
skip = 1;
break;
}
else if (reach_i->tags[start] == match_tags[start]
&& reach_i->tags[end] < match_tags[end])
{
DPRINT((" Throwing %p out because t%d < %d\n",
reach_i->state, end, match_tags[end]));
skip = 1;
break;
}
}
if (!skip)
{
reach_next_i->state = reach_i->state;
tmp_iptr = reach_next_i->tags;
reach_next_i->tags = reach_i->tags;
reach_i->tags = tmp_iptr;
reach_next_i++;
}
}
reach_next_i->state = NULL;
/* Swap `reach' and `reach_next'. */
reach_i = reach;
reach = reach_next;
reach_next = reach_i;
}
/* For each state in `reach' see if there is a transition leaving with
the current input symbol to a state not yet in `reach_next', and
add the destination states to `reach_next'. */
reach_next_i = reach_next;
for (reach_i = reach; reach_i->state; reach_i++)
{
for (trans_i = reach_i->state; trans_i->state; trans_i++)
{
/* Does this transition match the input symbol? */
if (trans_i->code_min <= (tre_cint_t)prev_c &&
trans_i->code_max >= (tre_cint_t)prev_c)
{
if (trans_i->assertions
&& (CHECK_ASSERTIONS(trans_i->assertions)
|| CHECK_CHAR_CLASSES(trans_i, tnfa, eflags)))
{
DPRINT(("assertion failed\n"));
continue;
}
/* Compute the tags after this transition. */
for (i = 0; i < num_tags; i++)
tmp_tags[i] = reach_i->tags[i];
tag_i = trans_i->tags;
if (tag_i != NULL)
while (*tag_i >= 0)
{
if (*tag_i < num_tags)
tmp_tags[*tag_i] = pos;
tag_i++;
}
if (reach_pos[trans_i->state_id].pos < pos)
{
/* Found an unvisited node. */
reach_next_i->state = trans_i->state;
tmp_iptr = reach_next_i->tags;
reach_next_i->tags = tmp_tags;
tmp_tags = tmp_iptr;
reach_pos[trans_i->state_id].pos = pos;
reach_pos[trans_i->state_id].tags = &reach_next_i->tags;
if (reach_next_i->state == tnfa->final
&& (match_eo == -1
|| (num_tags > 0
&& reach_next_i->tags[0] <= match_tags[0])))
{
DPRINT((" found match %p\n", trans_i->state));
match_eo = pos;
new_match = 1;
for (i = 0; i < num_tags; i++)
match_tags[i] = reach_next_i->tags[i];
}
reach_next_i++;
}
else
{
assert(reach_pos[trans_i->state_id].pos == pos);
/* Another path has also reached this state. We choose
the winner by examining the tag values for both
paths. */
if (tre_tag_order(num_tags, tnfa->tag_directions,
tmp_tags,
*reach_pos[trans_i->state_id].tags))
{
/* The new path wins. */
tmp_iptr = *reach_pos[trans_i->state_id].tags;
*reach_pos[trans_i->state_id].tags = tmp_tags;
if (trans_i->state == tnfa->final)
{
DPRINT((" found better match\n"));
match_eo = pos;
new_match = 1;
for (i = 0; i < num_tags; i++)
match_tags[i] = tmp_tags[i];
}
tmp_tags = tmp_iptr;
}
}
}
}
}
reach_next_i->state = NULL;
}
DPRINT(("match end offset = %d\n", match_eo));
*match_end_ofs = match_eo;
ret = match_eo >= 0 ? REG_OK : REG_NOMATCH;
#ifndef TRE_USE_ALLOCA
if (buf)
xfree(buf);
#endif /* !TRE_USE_ALLOCA */
return ret;
}
/* EOF */
+215
View File
@@ -0,0 +1,215 @@
/*
tre-match-utils.h - TRE matcher helper definitions
This software is released under a BSD-style license.
See the file LICENSE for details and copyright.
*/
#define str_source ((const tre_str_source*)string)
#ifdef TRE_WCHAR
#ifdef TRE_MULTIBYTE
/* Wide character and multibyte support. */
#define GET_NEXT_WCHAR() \
do { \
prev_c = next_c; \
if (type == STR_BYTE) \
{ \
pos++; \
if (len >= 0 && pos >= len) \
next_c = '\0'; \
else \
next_c = (unsigned char)(*str_byte++); \
} \
else if (type == STR_WIDE) \
{ \
pos++; \
if (len >= 0 && pos >= len) \
next_c = L'\0'; \
else \
next_c = *str_wide++; \
} \
else if (type == STR_MBS) \
{ \
pos += pos_add_next; \
if (str_byte == NULL) \
next_c = L'\0'; \
else \
{ \
size_t w; \
size_t max; \
if (len >= 0) \
max = len - pos; \
else \
max = 32; \
if (max <= 0) \
{ \
next_c = L'\0'; \
pos_add_next = 1; \
} \
else \
{ \
w = tre_mbrtowc(&next_c, str_byte, (size_t)max, &mbstate); \
if (w == (size_t)-1 || w == (size_t)-2) \
return REG_NOMATCH; \
if (w == 0 && len >= 0) \
{ \
pos_add_next = 1; \
next_c = 0; \
str_byte++; \
} \
else \
{ \
pos_add_next = w; \
str_byte += w; \
} \
} \
} \
} \
else if (type == STR_USER) \
{ \
pos += pos_add_next; \
str_user_end = str_source->get_next_char(&next_c, &pos_add_next, \
str_source->context); \
} \
} while(/*CONSTCOND*/(void)0,0)
#else /* !TRE_MULTIBYTE */
/* Wide character support, no multibyte support. */
#define GET_NEXT_WCHAR() \
do { \
prev_c = next_c; \
if (type == STR_BYTE) \
{ \
pos++; \
if (len >= 0 && pos >= len) \
next_c = '\0'; \
else \
next_c = (unsigned char)(*str_byte++); \
} \
else if (type == STR_WIDE) \
{ \
pos++; \
if (len >= 0 && pos >= len) \
next_c = L'\0'; \
else \
next_c = *str_wide++; \
} \
else if (type == STR_USER) \
{ \
pos += pos_add_next; \
str_user_end = str_source->get_next_char(&next_c, &pos_add_next, \
str_source->context); \
} \
} while(/*CONSTCOND*/(void)0,0)
#endif /* !TRE_MULTIBYTE */
#else /* !TRE_WCHAR */
/* No wide character or multibyte support. */
#define GET_NEXT_WCHAR() \
do { \
prev_c = next_c; \
if (type == STR_BYTE) \
{ \
pos++; \
if (len >= 0 && pos >= len) \
next_c = '\0'; \
else \
next_c = (unsigned char)(*str_byte++); \
} \
else if (type == STR_USER) \
{ \
pos += pos_add_next; \
str_user_end = str_source->get_next_char(&next_c, &pos_add_next, \
str_source->context); \
} \
} while(/*CONSTCOND*/(void)0,0)
#endif /* !TRE_WCHAR */
#define IS_WORD_CHAR(c) ((c) == L'_' || tre_isalnum(c))
#define CHECK_ASSERTIONS(assertions) \
(((assertions & ASSERT_AT_BOL) \
&& (pos > 0 || reg_notbol) \
&& (prev_c != L'\n' || !reg_newline)) \
|| ((assertions & ASSERT_AT_EOL) \
&& (next_c != L'\0' || reg_noteol) \
&& (next_c != L'\n' || !reg_newline)) \
|| ((assertions & ASSERT_AT_BOW) \
&& (IS_WORD_CHAR(prev_c) || !IS_WORD_CHAR(next_c))) \
|| ((assertions & ASSERT_AT_EOW) \
&& (!IS_WORD_CHAR(prev_c) || IS_WORD_CHAR(next_c))) \
|| ((assertions & ASSERT_AT_WB) \
&& (pos != 0 && next_c != L'\0' \
&& IS_WORD_CHAR(prev_c) == IS_WORD_CHAR(next_c))) \
|| ((assertions & ASSERT_AT_WB_NEG) \
&& (pos == 0 || next_c == L'\0' \
|| IS_WORD_CHAR(prev_c) != IS_WORD_CHAR(next_c))))
#define CHECK_CHAR_CLASSES(trans_i, tnfa, eflags) \
(((trans_i->assertions & ASSERT_CHAR_CLASS) \
&& !(tnfa->cflags & REG_ICASE) \
&& !tre_isctype((tre_cint_t)prev_c, trans_i->u.class)) \
|| ((trans_i->assertions & ASSERT_CHAR_CLASS) \
&& (tnfa->cflags & REG_ICASE) \
&& !tre_isctype(tre_tolower((tre_cint_t)prev_c),trans_i->u.class) \
&& !tre_isctype(tre_toupper((tre_cint_t)prev_c),trans_i->u.class)) \
|| ((trans_i->assertions & ASSERT_CHAR_CLASS_NEG) \
&& tre_neg_char_classes_match(trans_i->neg_classes,(tre_cint_t)prev_c,\
tnfa->cflags & REG_ICASE)))
/* Returns 1 if `t1' wins `t2', 0 otherwise. */
inline static int
tre_tag_order(int num_tags, tre_tag_direction_t *tag_directions,
int *t1, int *t2)
{
int i;
for (i = 0; i < num_tags; i++)
{
if (tag_directions[i] == TRE_TAG_MINIMIZE)
{
if (t1[i] < t2[i])
return 1;
if (t1[i] > t2[i])
return 0;
}
else
{
if (t1[i] > t2[i])
return 1;
if (t1[i] < t2[i])
return 0;
}
}
/* assert(0);*/
return 0;
}
inline static int
tre_neg_char_classes_match(tre_ctype_t *classes, tre_cint_t wc, int icase)
{
DPRINT(("neg_char_classes_test: %p, %d, %d\n", classes, wc, icase));
while (*classes != (tre_ctype_t)0)
if ((!icase && tre_isctype(wc, *classes))
|| (icase && (tre_isctype(tre_toupper(wc), *classes)
|| tre_isctype(tre_tolower(wc), *classes))))
return 1; /* Match. */
else
classes++;
return 0; /* No match. */
}
+155
View File
@@ -0,0 +1,155 @@
/*
tre-mem.c - TRE memory allocator
This software is released under a BSD-style license.
See the file LICENSE for details and copyright.
*/
/*
This memory allocator is for allocating small memory blocks efficiently
in terms of memory overhead and execution speed. The allocated blocks
cannot be freed individually, only all at once. There can be multiple
allocators, though.
*/
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif /* HAVE_CONFIG_H */
#include <stdlib.h>
#include <string.h>
#include "tre-internal.h"
#include "tre-mem.h"
#include "xmalloc.h"
/* Returns a new memory allocator or NULL if out of memory. */
tre_mem_t
tre_mem_new_impl(int provided, void *provided_block)
{
tre_mem_t mem;
if (provided)
{
mem = provided_block;
memset(mem, 0, sizeof(*mem));
}
else
mem = xcalloc(1, sizeof(*mem));
if (mem == NULL)
return NULL;
return mem;
}
/* Frees the memory allocator and all memory allocated with it. */
void
tre_mem_destroy(tre_mem_t mem)
{
tre_list_t *tmp, *l = mem->blocks;
while (l != NULL)
{
xfree(l->data);
tmp = l->next;
xfree(l);
l = tmp;
}
xfree(mem);
}
/* Allocates a block of `size' bytes from `mem'. Returns a pointer to the
allocated block or NULL if an underlying malloc() failed. */
void *
tre_mem_alloc_impl(tre_mem_t mem, int provided, void *provided_block,
int zero, size_t size)
{
void *ptr;
if (mem->failed)
{
DPRINT(("tre_mem_alloc: oops, called after failure?!\n"));
return NULL;
}
#ifdef MALLOC_DEBUGGING
if (!provided)
{
ptr = xmalloc(1);
if (ptr == NULL)
{
DPRINT(("tre_mem_alloc: xmalloc forced failure\n"));
mem->failed = 1;
return NULL;
}
xfree(ptr);
}
#endif /* MALLOC_DEBUGGING */
if (mem->n < size)
{
/* We need more memory than is available in the current block.
Allocate a new block. */
tre_list_t *l;
if (provided)
{
DPRINT(("tre_mem_alloc: using provided block\n"));
if (provided_block == NULL)
{
DPRINT(("tre_mem_alloc: provided block was NULL\n"));
mem->failed = 1;
return NULL;
}
mem->ptr = provided_block;
mem->n = TRE_MEM_BLOCK_SIZE;
}
else
{
size_t block_size;
if (size * 8 > TRE_MEM_BLOCK_SIZE)
block_size = size * 8;
else
block_size = TRE_MEM_BLOCK_SIZE;
DPRINT(("tre_mem_alloc: allocating new %zu byte block\n",
block_size));
l = xmalloc(sizeof(*l));
if (l == NULL)
{
mem->failed = 1;
return NULL;
}
l->data = xmalloc(block_size);
if (l->data == NULL)
{
xfree(l);
mem->failed = 1;
return NULL;
}
l->next = NULL;
if (mem->current != NULL)
mem->current->next = l;
if (mem->blocks == NULL)
mem->blocks = l;
mem->current = l;
mem->ptr = l->data;
mem->n = block_size;
}
}
/* Make sure the next pointer will be aligned. */
size += ALIGN(mem->ptr + size, long);
/* Allocate from current block. */
ptr = mem->ptr;
mem->ptr += size;
mem->n -= size;
/* Set to zero if needed. */
if (zero)
memset(ptr, 0, size);
return ptr;
}
/* EOF */
+66
View File
@@ -0,0 +1,66 @@
/*
tre-mem.h - TRE memory allocator interface
This software is released under a BSD-style license.
See the file LICENSE for details and copyright.
*/
#ifndef TRE_MEM_H
#define TRE_MEM_H 1
#include <stdlib.h>
#define TRE_MEM_BLOCK_SIZE 1024
typedef struct tre_list {
void *data;
struct tre_list *next;
} tre_list_t;
typedef struct tre_mem_struct {
tre_list_t *blocks;
tre_list_t *current;
char *ptr;
size_t n;
int failed;
void **provided;
} *tre_mem_t;
tre_mem_t tre_mem_new_impl(int provided, void *provided_block);
void *tre_mem_alloc_impl(tre_mem_t mem, int provided, void *provided_block,
int zero, size_t size);
/* Returns a new memory allocator or NULL if out of memory. */
#define tre_mem_new() tre_mem_new_impl(0, NULL)
/* Allocates a block of `size' bytes from `mem'. Returns a pointer to the
allocated block or NULL if an underlying malloc() failed. */
#define tre_mem_alloc(mem, size) tre_mem_alloc_impl(mem, 0, NULL, 0, size)
/* Allocates a block of `size' bytes from `mem'. Returns a pointer to the
allocated block or NULL if an underlying malloc() failed. The memory
is set to zero. */
#define tre_mem_calloc(mem, size) tre_mem_alloc_impl(mem, 0, NULL, 1, size)
#ifdef TRE_USE_ALLOCA
/* alloca() versions. Like above, but memory is allocated with alloca()
instead of malloc(). */
#define tre_mem_newa() \
tre_mem_new_impl(1, alloca(sizeof(struct tre_mem_struct)))
#define tre_mem_alloca(mem, size) \
((mem)->n >= (size) \
? tre_mem_alloc_impl((mem), 1, NULL, 0, (size)) \
: tre_mem_alloc_impl((mem), 1, alloca(TRE_MEM_BLOCK_SIZE), 0, (size)))
#endif /* TRE_USE_ALLOCA */
/* Frees the memory allocator and all memory allocated with it. */
void tre_mem_destroy(tre_mem_t mem);
#endif /* TRE_MEM_H */
/* EOF */
+1758
View File
File diff suppressed because it is too large Load Diff
+52
View File
@@ -0,0 +1,52 @@
/*
tre-parse.c - Regexp parser definitions
This software is released under a BSD-style license.
See the file LICENSE for details and copyright.
*/
#ifndef TRE_PARSE_H
#define TRE_PARSE_H 1
/* Parse context. */
typedef struct {
/* Memory allocator. The AST is allocated using this. */
tre_mem_t mem;
/* Stack used for keeping track of regexp syntax. */
tre_stack_t *stack;
/* The parse result. */
tre_ast_node_t *result;
/* The regexp to parse and its length. */
const tre_char_t *re;
/* The first character of the entire regexp. */
const tre_char_t *re_start;
/* The first character after the end of the regexp. */
const tre_char_t *re_end;
size_t len;
/* Current submatch ID. */
int submatch_id;
/* The highest back reference or -1 if none seen so far. */
int max_backref;
/* This flag is set if the regexp uses approximate matching. */
int have_approx;
/* This flag is set if the regexp changes cflags inline using (?...) */
int have_inline_cflags;
/* Compilation flags. */
int cflags;
/* If this flag is set the top-level submatch is not captured. */
int nofirstsub;
/* The currently set approximate matching parameters. */
int params[TRE_PARAM_LAST];
/* the MB_CUR_MAX in use */
int mb_cur_max;
} tre_parse_ctx_t;
/* Parses a wide character regexp pattern into a syntax tree. This parser
handles both syntaxes (BRE and ERE), including the TRE extensions. */
reg_errcode_t
tre_parse(tre_parse_ctx_t *ctx);
#endif /* TRE_PARSE_H */
/* EOF */
+123
View File
@@ -0,0 +1,123 @@
/*
tre-stack.c - Simple stack implementation
This software is released under a BSD-style license.
See the file LICENSE for details and copyright.
*/
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif /* HAVE_CONFIG_H */
#include <stdlib.h>
#include <assert.h>
#include "tre-internal.h"
#include "tre-stack.h"
#include "xmalloc.h"
union tre_stack_item {
void *voidptr_value;
int int_value;
};
struct tre_stack_rec {
size_t size;
size_t max_size;
size_t ptr;
union tre_stack_item *stack;
};
tre_stack_t *
tre_stack_new(size_t size, size_t max_size)
{
tre_stack_t *s;
s = xmalloc(sizeof(*s));
if (s != NULL)
{
s->stack = xmalloc(sizeof(*s->stack) * size);
if (s->stack == NULL)
{
xfree(s);
return NULL;
}
s->size = size;
s->max_size = max_size;
s->ptr = 0;
}
return s;
}
void
tre_stack_destroy(tre_stack_t *s)
{
xfree(s->stack);
xfree(s);
}
size_t
tre_stack_num_items(tre_stack_t *s)
{
return s->ptr;
}
static reg_errcode_t
tre_stack_push(tre_stack_t *s, union tre_stack_item value)
{
if (s->ptr < s->size)
{
s->stack[s->ptr] = value;
s->ptr++;
}
else
{
if (s->size >= s->max_size)
{
DPRINT(("tre_stack_push: stack full\n"));
return REG_ESPACE;
}
else
{
union tre_stack_item *new_buffer;
size_t new_size;
DPRINT(("tre_stack_push: trying to realloc more space\n"));
new_size = s->size + s->size;
if (new_size > s->max_size)
new_size = s->max_size;
new_buffer = xrealloc(s->stack, sizeof(*new_buffer) * new_size);
if (new_buffer == NULL)
{
DPRINT(("tre_stack_push: realloc failed.\n"));
return REG_ESPACE;
}
DPRINT(("tre_stack_push: realloc succeeded.\n"));
assert(new_size > s->size);
s->size = new_size;
s->stack = new_buffer;
tre_stack_push(s, value);
}
}
return REG_OK;
}
#define define_pushf(typetag, type) \
declare_pushf(typetag, type) { \
union tre_stack_item item; \
item.typetag ## _value = value; \
return tre_stack_push(s, item); \
}
define_pushf(int, int)
define_pushf(voidptr, void *)
#define define_popf(typetag, type) \
declare_popf(typetag, type) { \
return s->stack[--s->ptr].typetag ## _value; \
}
define_popf(int, int)
define_popf(voidptr, void *)
/* EOF */
+76
View File
@@ -0,0 +1,76 @@
/*
tre-stack.h: Stack definitions
This software is released under a BSD-style license.
See the file LICENSE for details and copyright.
*/
#ifndef TRE_STACK_H
#define TRE_STACK_H 1
#include "../local_includes/tre.h"
typedef struct tre_stack_rec tre_stack_t;
/* Creates a new stack object with initial size `size' and maximum size
`max_size'. Pushing an additional item onto a full stack will resize
the stack to double its capacity until the maximum is reached. Returns
the stack object or NULL if out of memory. */
tre_stack_t *
tre_stack_new(size_t size, size_t max_size);
/* Frees the stack object. */
void
tre_stack_destroy(tre_stack_t *s);
/* Returns the current number of items on the stack. */
size_t
tre_stack_num_items(tre_stack_t *s);
/* Each tre_stack_push_*(tre_stack_t *s, <type> value) function pushes
`value' on top of stack `s'. Returns REG_ESPACE if out of memory.
This tries to realloc() more space before failing if maximum size
has not yet been reached. Returns REG_OK if successful. */
#define declare_pushf(typetag, type) \
reg_errcode_t tre_stack_push_ ## typetag(tre_stack_t *s, type value)
declare_pushf(voidptr, void *);
declare_pushf(int, int);
/* Each tre_stack_pop_*(tre_stack_t *s) function pops the topmost
element off of stack `s' and returns it. The stack must not be
empty. */
#define declare_popf(typetag, type) \
type tre_stack_pop_ ## typetag(tre_stack_t *s)
declare_popf(voidptr, void *);
declare_popf(int, int);
/* Just to save some typing. */
#define STACK_PUSH(s, typetag, value) \
do \
{ \
status = tre_stack_push_ ## typetag(s, value); \
} \
while (/*CONSTCOND*/(void)0,0)
#define STACK_PUSHX(s, typetag, value) \
{ \
status = tre_stack_push_ ## typetag(s, value); \
if (status != REG_OK) \
break; \
}
#define STACK_PUSHR(s, typetag, value) \
{ \
reg_errcode_t _status; \
_status = tre_stack_push_ ## typetag(s, value); \
if (_status != REG_OK) \
return _status; \
}
#endif /* TRE_STACK_H */
/* EOF */
+362
View File
@@ -0,0 +1,362 @@
/*
xmalloc.c - Simple malloc debugging library implementation
This software is released under a BSD-style license.
See the file LICENSE for details and copyright.
*/
/*
TODO:
- red zones
- group dumps by source location
*/
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif /* HAVE_CONFIG_H */
#include <stdint.h>
#include <stdlib.h>
#include <assert.h>
#include <stdio.h>
#define XMALLOC_INTERNAL 1
#include "xmalloc.h"
/*
Internal stuff.
*/
typedef struct hashTableItemRec {
void *ptr;
size_t bytes;
const char *file;
int line;
const char *func;
struct hashTableItemRec *next;
} hashTableItem;
typedef struct {
hashTableItem **table;
} hashTable;
static int xmalloc_peak;
int xmalloc_current;
static int xmalloc_peak_blocks;
int xmalloc_current_blocks;
static int xmalloc_fail_after;
#define TABLE_BITS 8
#define TABLE_MASK ((1 << TABLE_BITS) - 1)
#define TABLE_SIZE (1 << TABLE_BITS)
static hashTable *
hash_table_new(void)
{
hashTable *tbl;
tbl = malloc(sizeof(*tbl));
if (tbl != NULL)
{
tbl->table = calloc(TABLE_SIZE, sizeof(*tbl->table));
if (tbl->table == NULL)
{
free(tbl);
return NULL;
}
}
return tbl;
}
static unsigned int
hash_void_ptr(void *ptr)
{
unsigned int hash;
unsigned int i;
/* I took this hash function just off the top of my head, I have
no idea whether it is bad or very bad. */
hash = 0;
for (i = 0; i < sizeof(ptr) * 8 / TABLE_BITS; i++)
{
hash ^= (uintptr_t)ptr >> i * 8;
hash += i * 17;
hash &= TABLE_MASK;
}
return hash;
}
static void
hash_table_add(hashTable *tbl, void *ptr, size_t bytes,
const char *file, int line, const char *func)
{
unsigned int i;
hashTableItem *item, *new;
i = hash_void_ptr(ptr);
item = tbl->table[i];
if (item != NULL)
while (item->next != NULL)
item = item->next;
new = malloc(sizeof(*new));
assert(new != NULL);
new->ptr = ptr;
new->bytes = bytes;
new->file = file;
new->line = line;
new->func = func;
new->next = NULL;
if (item != NULL)
item->next = new;
else
tbl->table[i] = new;
xmalloc_current += bytes;
if (xmalloc_current > xmalloc_peak)
xmalloc_peak = xmalloc_current;
xmalloc_current_blocks++;
if (xmalloc_current_blocks > xmalloc_peak_blocks)
xmalloc_peak_blocks = xmalloc_current_blocks;
}
static void
#if defined(__GNUC__) && __GNUC__ >= 11
__attribute__((access(none, 2)))
#endif
hash_table_del(hashTable *tbl, void *ptr)
{
int i;
hashTableItem *item, *prev;
i = hash_void_ptr(ptr);
item = tbl->table[i];
if (item == NULL)
{
printf("xfree: invalid ptr %p\n", ptr);
abort();
}
prev = NULL;
while (item->ptr != ptr)
{
prev = item;
item = item->next;
}
if (item->ptr != ptr)
{
printf("xfree: invalid ptr %p\n", ptr);
abort();
}
xmalloc_current -= item->bytes;
xmalloc_current_blocks--;
if (prev != NULL)
{
prev->next = item->next;
free(item);
}
else
{
tbl->table[i] = item->next;
free(item);
}
}
static hashTable *xmalloc_table = NULL;
static void
xmalloc_init(void)
{
if (xmalloc_table == NULL)
{
xmalloc_table = hash_table_new();
xmalloc_peak = 0;
xmalloc_peak_blocks = 0;
xmalloc_current = 0;
xmalloc_current_blocks = 0;
xmalloc_fail_after = -1;
}
assert(xmalloc_table != NULL);
assert(xmalloc_table->table != NULL);
}
/*
Public API.
*/
void
xmalloc_configure(int fail_after)
{
xmalloc_init();
xmalloc_fail_after = fail_after;
}
int
xmalloc_dump_leaks(void)
{
unsigned int i;
unsigned int num_leaks = 0;
size_t leaked_bytes = 0;
hashTableItem *item;
xmalloc_init();
for (i = 0; i < TABLE_SIZE; i++)
{
item = xmalloc_table->table[i];
while (item != NULL)
{
printf("%s:%d: %s: %zu bytes at %p not freed\n",
item->file, item->line, item->func, item->bytes, item->ptr);
num_leaks++;
leaked_bytes += item->bytes;
item = item->next;
}
}
if (num_leaks == 0)
printf("No memory leaks.\n");
else
printf("%u unfreed memory chuncks, total %zu unfreed bytes.\n",
num_leaks, leaked_bytes);
printf("Peak memory consumption %d bytes (%.1f kB, %.1f MB) in %d blocks ",
xmalloc_peak, (double)xmalloc_peak / 1024,
(double)xmalloc_peak / (1024*1024), xmalloc_peak_blocks);
printf("(average ");
if (xmalloc_peak_blocks)
printf("%d", ((xmalloc_peak + xmalloc_peak_blocks / 2)
/ xmalloc_peak_blocks));
else
printf("N/A");
printf(" bytes per block).\n");
return num_leaks;
}
void *
xmalloc_impl(size_t size, const char *file, int line, const char *func)
{
void *ptr;
xmalloc_init();
assert(size > 0);
if (xmalloc_fail_after == 0)
{
xmalloc_fail_after = -2;
#if 0
printf("xmalloc: forced failure %s:%d: %s\n", file, line, func);
#endif
return NULL;
}
else if (xmalloc_fail_after == -2)
{
printf("xmalloc: called after failure from %s:%d: %s\n",
file, line, func);
assert(0);
}
else if (xmalloc_fail_after > 0)
xmalloc_fail_after--;
ptr = malloc(size);
if (ptr != NULL)
hash_table_add(xmalloc_table, ptr, (int)size, file, line, func);
return ptr;
}
void *
xcalloc_impl(size_t nmemb, size_t size, const char *file, int line,
const char *func)
{
void *ptr;
xmalloc_init();
assert(size > 0);
if (xmalloc_fail_after == 0)
{
xmalloc_fail_after = -2;
#if 0
printf("xcalloc: forced failure %s:%d: %s\n", file, line, func);
#endif
return NULL;
}
else if (xmalloc_fail_after == -2)
{
printf("xcalloc: called after failure from %s:%d: %s\n",
file, line, func);
assert(0);
}
else if (xmalloc_fail_after > 0)
xmalloc_fail_after--;
ptr = calloc(nmemb, size);
if (ptr != NULL)
hash_table_add(xmalloc_table, ptr, (int)(nmemb * size), file, line, func);
return ptr;
}
void
xfree_impl(void *ptr, const char *file, int line, const char *func)
{
/*LINTED*/(void)&file;
/*LINTED*/(void)&line;
/*LINTED*/(void)&func;
xmalloc_init();
if (ptr != NULL)
hash_table_del(xmalloc_table, ptr);
free(ptr);
}
void *
xrealloc_impl(void *ptr, size_t new_size, const char *file, int line,
const char *func)
{
void *new_ptr;
xmalloc_init();
assert(ptr != NULL);
assert(new_size > 0);
if (xmalloc_fail_after == 0)
{
xmalloc_fail_after = -2;
return NULL;
}
else if (xmalloc_fail_after == -2)
{
printf("xrealloc: called after failure from %s:%d: %s\n",
file, line, func);
assert(0);
}
else if (xmalloc_fail_after > 0)
xmalloc_fail_after--;
new_ptr = realloc(ptr, new_size);
if (new_ptr != NULL && new_ptr != ptr)
{
#if defined(__GNUC__) && !defined(__clang__) && __GNUC__ >= 12
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wuse-after-free"
#endif
hash_table_del(xmalloc_table, ptr);
#if defined(__GNUC__) && !defined(__clang__) && __GNUC__ >= 12
#pragma GCC diagnostic pop
#endif
hash_table_add(xmalloc_table, new_ptr, (int)new_size, file, line, func);
}
return new_ptr;
}
/* EOF */
+77
View File
@@ -0,0 +1,77 @@
/*
xmalloc.h - Simple malloc debugging library API
This software is released under a BSD-style license.
See the file LICENSE for details and copyright.
*/
#ifndef _XMALLOC_H
#define _XMALLOC_H 1
void *xmalloc_impl(size_t size, const char *file, int line, const char *func);
void *xcalloc_impl(size_t nmemb, size_t size, const char *file, int line,
const char *func);
void xfree_impl(void *ptr, const char *file, int line, const char *func);
void *xrealloc_impl(void *ptr, size_t new_size, const char *file, int line,
const char *func);
int xmalloc_dump_leaks(void);
void xmalloc_configure(int fail_after);
#ifndef XMALLOC_INTERNAL
#ifdef MALLOC_DEBUGGING
/* Version 2.4 and later of GCC define a magical variable `__PRETTY_FUNCTION__'
which contains the name of the function currently being defined.
# define __XMALLOC_FUNCTION __PRETTY_FUNCTION__
This is broken in G++ before version 2.6.
C9x has a similar variable called __func__, but prefer the GCC one since
it demangles C++ function names. */
# ifdef __GNUC__
# if __GNUC__ > 2 || (__GNUC__ == 2 \
&& __GNUC_MINOR__ >= (defined __cplusplus ? 6 : 4))
# define __XMALLOC_FUNCTION __PRETTY_FUNCTION__
# else
# define __XMALLOC_FUNCTION ((const char *) 0)
# endif
# else
# if defined __STDC_VERSION__ && __STDC_VERSION__ >= 199901L
# define __XMALLOC_FUNCTION __func__
# else
# define __XMALLOC_FUNCTION ((const char *) 0)
# endif
# endif
#define xmalloc(size) xmalloc_impl(size, __FILE__, __LINE__, \
__XMALLOC_FUNCTION)
#define xcalloc(nmemb, size) xcalloc_impl(nmemb, size, __FILE__, __LINE__, \
__XMALLOC_FUNCTION)
#define xfree(ptr) xfree_impl(ptr, __FILE__, __LINE__, __XMALLOC_FUNCTION)
#define xrealloc(ptr, new_size) xrealloc_impl(ptr, new_size, __FILE__, \
__LINE__, __XMALLOC_FUNCTION)
#undef malloc
#undef calloc
#undef free
#undef realloc
#define malloc USE_XMALLOC_INSTEAD_OF_MALLOC
#define calloc USE_XCALLOC_INSTEAD_OF_CALLOC
#define free USE_XFREE_INSTEAD_OF_FREE
#define realloc USE_XREALLOC_INSTEAD_OF_REALLOC
#else /* !MALLOC_DEBUGGING */
#include <stdlib.h>
#define xmalloc(size) malloc(size)
#define xcalloc(nmemb, size) calloc(nmemb, size)
#define xfree(ptr) free(ptr)
#define xrealloc(ptr, new_size) realloc(ptr, new_size)
#endif /* !MALLOC_DEBUGGING */
#endif /* !XMALLOC_INTERNAL */
#endif /* _XMALLOC_H */
/* EOF */
+48
View File
@@ -0,0 +1,48 @@
/*
regex.h - TRE legacy API
This software is released under a BSD-style license.
See the file LICENSE for details and copyright.
This header is for source level compatibility with old code using
the <tre/regex.h> header which defined the TRE API functions without
a prefix. New code should include <tre/tre.h> instead.
*/
#ifndef TRE_REXEX_H
#define TRE_REGEX_H 1
#ifdef USE_LOCAL_TRE_H
/* Use the header(s) from the TRE package that this file is part of.
(Yes, this file is in local_include too, but the explict path
means there is no way to get a system tre.h by accident.) */
#include "../local_includes/tre.h"
#else
/* Use the header(s) from an installed version of the TRE package
(so that this application matches the installed libtre),
not the one(s) in the local_includes directory. */
#include <tre/tre.h>
#endif
#ifndef TRE_USE_SYSTEM_REGEX_H
#define regcomp tre_regcomp
#define regerror tre_regerror
#define regexec tre_regexec
#define regfree tre_regfree
#endif /* TRE_USE_SYSTEM_REGEX_H */
#define regacomp tre_regacomp
#define regaexec tre_regaexec
#define regancomp tre_regancomp
#define reganexec tre_reganexec
#define regawncomp tre_regawncomp
#define regawnexec tre_regawnexec
#define regncomp tre_regncomp
#define regnexec tre_regnexec
#define regwcomp tre_regwcomp
#define regwexec tre_regwexec
#define regwncomp tre_regwncomp
#define regwnexec tre_regwnexec
#endif /* TRE_REGEX_H */
+14
View File
@@ -0,0 +1,14 @@
/* Minimal TRE configuration for Redis.
*
* We use TRE as a byte-oriented regex matcher for ARGREP. Redis SDS values are
* binary-safe byte strings, so we intentionally keep the dependency build
* simple: no wide-char path, no multibyte locale handling, and no approximate
* matching engine.
*/
#define HAVE_SYS_TYPES_H 1
#define TRE_VERSION "redis-vendored"
#define TRE_VERSION_1 0
#define TRE_VERSION_2 0
#define TRE_VERSION_3 0
+344
View File
@@ -0,0 +1,344 @@
/*
tre.h - TRE public API definitions
This software is released under a BSD-style license.
See the file LICENSE for details and copyright.
*/
#ifndef TRE_H
#define TRE_H 1
#ifdef USE_LOCAL_TRE_H
/* Make certain to use the header(s) from the TRE package that this
file is part of by giving the full path to the header from this directory. */
#include "../local_includes/tre-config.h"
#else
/* Use the header in the same directory as this file if there is one. */
#include "tre-config.h"
#endif
#ifdef HAVE_SYS_TYPES_H
#include <sys/types.h>
#endif /* HAVE_SYS_TYPES_H */
#ifdef HAVE_LIBUTF8_H
#include <libutf8.h>
#endif /* HAVE_LIBUTF8_H */
#ifdef TRE_USE_SYSTEM_REGEX_H
/* Include the system regex.h to make TRE ABI compatible with the
system regex. */
#include TRE_SYSTEM_REGEX_H_PATH
#define tre_regcomp regcomp
#define tre_regexec regexec
#define tre_regerror regerror
#define tre_regfree regfree
/* The GNU C regex has a number of refinements to the POSIX standard for the
formal parameter list of the regexec() function, and some of those fail to
compile when using LLVM. The refinements seem to be opt-out rather than
opt-in when using a recent gcc, and they produce a warning when TRE tries
to mimic the API without the refinements. The TRE code still works but
the warnings are distracting, so try to #define a flag to indicate when to
add the refinements to TRE's parameter list too. */
#ifdef __GNUC__
/* Try to test something that looks pretty REGEX specific and hope we don't
need a zillion different platform+compiler specific tests to deal with this. */
#ifdef _REGEX_NELTS
/* Define a TRE specific flag here so that:
1) there is only one place where code has to be changed if the test above is not adequate, and
2) the flag can be used in any other parts of the TRE source that might be affected by the
GNUC refinements.
Note that this flag is only defined when all of TRE_USE_SYSTEM_REGEX_H, __GNUC__, and _REGEX_NELTS are defined. */
#define TRE_USE_GNUC_REGEXEC_FPL 1
#endif
#endif
#endif /* TRE_USE_SYSTEM_REGEX_H */
#ifdef __cplusplus
extern "C" {
#endif
#ifdef TRE_USE_SYSTEM_REGEX_H
#ifndef REG_OK
#define REG_OK 0
#endif /* !REG_OK */
#ifndef HAVE_REG_ERRCODE_T
typedef int reg_errcode_t;
#endif /* !HAVE_REG_ERRCODE_T */
#if !defined(REG_NOSPEC) && !defined(REG_LITERAL)
#define REG_LITERAL 0x1000
#endif
/* Extra tre_regcomp() return error codes. */
#define REG_BADMAX REG_BADBR
/* Extra tre_regcomp() flags. */
#ifndef REG_BASIC
#define REG_BASIC 0
#endif /* !REG_BASIC */
#define REG_RIGHT_ASSOC (REG_LITERAL << 1)
#ifdef REG_UNGREEDY
/* We're going to use TRE code, so we need the TRE define (dodge problem in MacOS). */
#undef REG_UNGREEDY
#endif
#define REG_UNGREEDY (REG_RIGHT_ASSOC << 1)
#define REG_USEBYTES (REG_UNGREEDY << 1)
/* Extra tre_regexec() flags. */
#define REG_APPROX_MATCHER 0x1000
#ifdef REG_BACKTRACKING_MATCHER
/* We're going to use TRE code, so we need the TRE define (dodge problem in MacOS). */
#undef REG_BACKTRACKING_MATCHER
#endif
#define REG_BACKTRACKING_MATCHER (REG_APPROX_MATCHER << 1)
#else /* !TRE_USE_SYSTEM_REGEX_H */
/* If the we're not using system regex.h, we need to define the
structs and enums ourselves. */
typedef int regoff_t;
typedef struct {
size_t re_nsub; /* Number of parenthesized subexpressions. */
void *value; /* For internal use only. */
} regex_t;
typedef struct {
regoff_t rm_so;
regoff_t rm_eo;
} regmatch_t;
typedef enum {
REG_OK = 0, /* No error. */
/* POSIX tre_regcomp() return error codes. (In the order listed in the
standard.) */
REG_NOMATCH, /* No match. */
REG_BADPAT, /* Invalid regexp. */
REG_ECOLLATE, /* Unknown collating element. */
REG_ECTYPE, /* Unknown character class name. */
REG_EESCAPE, /* Trailing backslash. */
REG_ESUBREG, /* Invalid back reference. */
REG_EBRACK, /* "[]" imbalance */
REG_EPAREN, /* "\(\)" or "()" imbalance */
REG_EBRACE, /* "\{\}" or "{}" imbalance */
REG_BADBR, /* Invalid content of {} */
REG_ERANGE, /* Invalid use of range operator */
REG_ESPACE, /* Out of memory. */
REG_BADRPT, /* Invalid use of repetition operators. */
REG_BADMAX, /* Maximum repetition in {} too large */
} reg_errcode_t;
/* POSIX tre_regcomp() flags. */
#define REG_EXTENDED 1
#define REG_ICASE (REG_EXTENDED << 1)
#define REG_NEWLINE (REG_ICASE << 1)
#define REG_NOSUB (REG_NEWLINE << 1)
/* Extra tre_regcomp() flags. */
#define REG_BASIC 0
#define REG_LITERAL (REG_NOSUB << 1)
#define REG_RIGHT_ASSOC (REG_LITERAL << 1)
#define REG_UNGREEDY (REG_RIGHT_ASSOC << 1)
#define REG_USEBYTES (REG_UNGREEDY << 1)
/* POSIX tre_regexec() flags. */
#define REG_NOTBOL 1
#define REG_NOTEOL (REG_NOTBOL << 1)
/* Extra tre_regexec() flags. */
#define REG_APPROX_MATCHER (REG_NOTEOL << 1)
#define REG_BACKTRACKING_MATCHER (REG_APPROX_MATCHER << 1)
#endif /* !TRE_USE_SYSTEM_REGEX_H */
/* REG_NOSPEC and REG_LITERAL mean the same thing. */
#if defined(REG_LITERAL) && !defined(REG_NOSPEC)
#define REG_NOSPEC REG_LITERAL
#elif defined(REG_NOSPEC) && !defined(REG_LITERAL)
#define REG_LITERAL REG_NOSPEC
#endif /* defined(REG_NOSPEC) */
/* The maximum number of iterations in a bound expression. */
#undef RE_DUP_MAX
#define RE_DUP_MAX 255
/* The POSIX.2 regexp functions */
extern int
tre_regcomp(regex_t *preg, const char *regex, int cflags);
#ifdef TRE_USE_GNUC_REGEXEC_FPL
extern int
tre_regexec(const regex_t *preg, const char *string,
size_t nmatch, regmatch_t pmatch[_Restrict_arr_ _REGEX_NELTS (nmatch)],
int eflags);
#else
extern int
tre_regexec(const regex_t *preg, const char *string, size_t nmatch,
regmatch_t pmatch[], int eflags);
#endif
extern int
tre_regcompb(regex_t *preg, const char *regex, int cflags);
extern int
tre_regexecb(const regex_t *preg, const char *string, size_t nmatch,
regmatch_t pmatch[], int eflags);
extern size_t
tre_regerror(int errcode, const regex_t *preg, char *errbuf,
size_t errbuf_size);
extern void
tre_regfree(regex_t *preg);
#ifdef TRE_WCHAR
#ifdef HAVE_WCHAR_H
#include <wchar.h>
#endif /* HAVE_WCHAR_H */
/* Wide character versions (not in POSIX.2). */
extern int
tre_regwcomp(regex_t *preg, const wchar_t *regex, int cflags);
extern int
tre_regwexec(const regex_t *preg, const wchar_t *string,
size_t nmatch, regmatch_t pmatch[], int eflags);
#endif /* TRE_WCHAR */
/* Versions with a maximum length argument and therefore the capability to
handle null characters in the middle of the strings (not in POSIX.2). */
extern int
tre_regncomp(regex_t *preg, const char *regex, size_t len, int cflags);
extern int
tre_regnexec(const regex_t *preg, const char *string, size_t len,
size_t nmatch, regmatch_t pmatch[], int eflags);
/* regn*b versions take byte literally as 8-bit values */
extern int
tre_regncompb(regex_t *preg, const char *regex, size_t n, int cflags);
extern int
tre_regnexecb(const regex_t *preg, const char *str, size_t len,
size_t nmatch, regmatch_t pmatch[], int eflags);
#ifdef TRE_WCHAR
extern int
tre_regwncomp(regex_t *preg, const wchar_t *regex, size_t len, int cflags);
extern int
tre_regwnexec(const regex_t *preg, const wchar_t *string, size_t len,
size_t nmatch, regmatch_t pmatch[], int eflags);
#endif /* TRE_WCHAR */
#ifdef TRE_APPROX
/* Approximate matching parameter struct. */
typedef struct {
int cost_ins; /* Default cost of an inserted character. */
int cost_del; /* Default cost of a deleted character. */
int cost_subst; /* Default cost of a substituted character. */
int max_cost; /* Maximum allowed cost of a match. */
int max_ins; /* Maximum allowed number of inserts. */
int max_del; /* Maximum allowed number of deletes. */
int max_subst; /* Maximum allowed number of substitutes. */
int max_err; /* Maximum allowed number of errors total. */
} regaparams_t;
/* Approximate matching result struct. */
typedef struct {
size_t nmatch; /* Length of pmatch[] array. */
regmatch_t *pmatch; /* Submatch data. */
int cost; /* Cost of the match. */
int num_ins; /* Number of inserts in the match. */
int num_del; /* Number of deletes in the match. */
int num_subst; /* Number of substitutes in the match. */
} regamatch_t;
/* Approximate matching functions. */
extern int
tre_regaexec(const regex_t *preg, const char *string,
regamatch_t *match, regaparams_t params, int eflags);
extern int
tre_reganexec(const regex_t *preg, const char *string, size_t len,
regamatch_t *match, regaparams_t params, int eflags);
extern int
tre_regaexecb(const regex_t *preg, const char *string,
regamatch_t *match, regaparams_t params, int eflags);
#ifdef TRE_WCHAR
/* Wide character approximate matching. */
extern int
tre_regawexec(const regex_t *preg, const wchar_t *string,
regamatch_t *match, regaparams_t params, int eflags);
extern int
tre_regawnexec(const regex_t *preg, const wchar_t *string, size_t len,
regamatch_t *match, regaparams_t params, int eflags);
#endif /* TRE_WCHAR */
/* Sets the parameters to default values. */
extern void
tre_regaparams_default(regaparams_t *params);
#endif /* TRE_APPROX */
#ifdef TRE_WCHAR
typedef wchar_t tre_char_t;
#else /* !TRE_WCHAR */
typedef unsigned char tre_char_t;
#endif /* !TRE_WCHAR */
typedef struct {
int (*get_next_char)(tre_char_t *c, unsigned int *pos_add, void *context);
void (*rewind)(size_t pos, void *context);
int (*compare)(size_t pos1, size_t pos2, size_t len, void *context);
void *context;
} tre_str_source;
extern int
tre_reguexec(const regex_t *preg, const tre_str_source *string,
size_t nmatch, regmatch_t pmatch[], int eflags);
/* Returns the version string. The returned string is static. */
extern char *
tre_version(void);
/* Returns the value for a config parameter. The type to which `result'
must point to depends of the value of `query', see documentation for
more details. */
extern int
tre_config(int query, void *result);
enum {
TRE_CONFIG_APPROX,
TRE_CONFIG_WCHAR,
TRE_CONFIG_MULTIBYTE,
TRE_CONFIG_SYSTEM_ABI,
TRE_CONFIG_VERSION
};
/* Returns 1 if the compiled pattern has back references, 0 if not. */
extern int
tre_have_backrefs(const regex_t *preg);
/* Returns 1 if the compiled pattern uses approximate matching features,
0 if not. */
extern int
tre_have_approx(const regex_t *preg);
#ifdef __cplusplus
}
#endif
#endif /* TRE_H */
/* EOF */
+1871
View File
File diff suppressed because it is too large Load Diff
+303
View File
@@ -0,0 +1,303 @@
/*
test-literal-opt.c - Validate TRE literal optimization against the
generic matcher.
This software is released under a BSD-style license.
See the file LICENSE for details and copyright.
*/
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif /* HAVE_CONFIG_H */
#include <locale.h>
#include <stdio.h>
#include <string.h>
#include "tre-internal.h"
#define PMATCH_SLOTS 4
#define RC_ANY -9999
typedef struct {
const char *name;
const char *pattern;
size_t pattern_len;
int cflags;
const char *string;
size_t string_len;
int eflags;
int expected_rc;
tre_literal_opt_mode_t expected_mode;
} litopt_case_t;
static void
init_pmatch(regmatch_t pmatch[], size_t count)
{
size_t i;
for (i = 0; i < count; i++)
{
pmatch[i].rm_so = 111;
pmatch[i].rm_eo = 222;
}
}
static int
same_pmatch(const regmatch_t a[], const regmatch_t b[], size_t count)
{
size_t i;
for (i = 0; i < count; i++)
if (a[i].rm_so != b[i].rm_so || a[i].rm_eo != b[i].rm_eo)
return 0;
return 1;
}
static int
pmatch_cleared(const regmatch_t pmatch[], size_t count)
{
size_t i;
for (i = 0; i < count; i++)
if (pmatch[i].rm_so != -1 || pmatch[i].rm_eo != -1)
return 0;
return 1;
}
static int
run_case(const litopt_case_t *tc)
{
regex_t preg;
tre_tnfa_t *tnfa;
regmatch_t fast[PMATCH_SLOTS], slow[PMATCH_SLOTS];
tre_literal_opt_mode_t saved_mode;
char errbuf[256];
int errcode, fast_rc, slow_rc;
memset(&preg, 0, sizeof(preg));
errcode = tre_regncompb(&preg, tc->pattern, tc->pattern_len, tc->cflags);
if (errcode != REG_OK)
{
tre_regerror(errcode, &preg, errbuf, sizeof(errbuf));
fprintf(stderr, "%s: compile failed: %s\n", tc->name, errbuf);
return 1;
}
tnfa = (tre_tnfa_t *)preg.value;
if (tnfa->literal_opt.mode != tc->expected_mode)
{
fprintf(stderr, "%s: optimizer mode %d, expected %d\n",
tc->name, (int)tnfa->literal_opt.mode, (int)tc->expected_mode);
tre_regfree(&preg);
return 1;
}
init_pmatch(fast, PMATCH_SLOTS);
init_pmatch(slow, PMATCH_SLOTS);
fast_rc = tre_regnexecb(&preg, tc->string, tc->string_len,
PMATCH_SLOTS, fast, tc->eflags);
saved_mode = tnfa->literal_opt.mode;
tnfa->literal_opt.mode = TRE_LITERAL_OPT_NONE;
slow_rc = tre_regnexecb(&preg, tc->string, tc->string_len,
PMATCH_SLOTS, slow, tc->eflags);
tnfa->literal_opt.mode = saved_mode;
if (fast_rc != slow_rc)
{
fprintf(stderr, "%s: fast rc %d, slow rc %d\n",
tc->name, fast_rc, slow_rc);
tre_regfree(&preg);
return 1;
}
if (tc->expected_rc != RC_ANY && fast_rc != tc->expected_rc)
{
fprintf(stderr, "%s: rc %d, expected %d\n",
tc->name, fast_rc, tc->expected_rc);
tre_regfree(&preg);
return 1;
}
if (!same_pmatch(fast, slow, PMATCH_SLOTS))
{
fprintf(stderr, "%s: fast and slow pmatch differ\n", tc->name);
tre_regfree(&preg);
return 1;
}
if ((tc->cflags & REG_NOSUB) && fast_rc == REG_OK
&& !pmatch_cleared(fast, PMATCH_SLOTS))
{
fprintf(stderr, "%s: REG_NOSUB match did not clear pmatch\n", tc->name);
tre_regfree(&preg);
return 1;
}
tre_regfree(&preg);
return 0;
}
int
main(void)
{
static const char nonascii_pattern[] = { (char)0xc0, '|', (char)0xe0 };
static const char nonascii_haystack[] = { 'x', (char)0xe0, 'y' };
static const litopt_case_t cases[] = {
{
"contains basic",
"foo|bar|baz",
sizeof("foo|bar|baz") - 1,
REG_EXTENDED | REG_NOSUB,
"xxbaryy",
sizeof("xxbaryy") - 1,
0,
REG_OK,
TRE_LITERAL_OPT_CONTAINS
},
{
"contains ignores bol/eol flags",
"foo|bar|baz",
sizeof("foo|bar|baz") - 1,
REG_EXTENDED | REG_NOSUB,
"xxbaryy",
sizeof("xxbaryy") - 1,
REG_NOTBOL | REG_NOTEOL,
REG_OK,
TRE_LITERAL_OPT_CONTAINS
},
{
"prefix basic",
"^(foo|bar|baz)",
sizeof("^(foo|bar|baz)") - 1,
REG_EXTENDED | REG_NOSUB,
"barrier",
sizeof("barrier") - 1,
0,
REG_OK,
TRE_LITERAL_OPT_PREFIX
},
{
"prefix respects REG_NOTBOL",
"^(foo|bar|baz)",
sizeof("^(foo|bar|baz)") - 1,
REG_EXTENDED | REG_NOSUB,
"barrier",
sizeof("barrier") - 1,
REG_NOTBOL,
REG_NOMATCH,
TRE_LITERAL_OPT_PREFIX
},
{
"suffix basic",
"(foo|bar|baz)$",
sizeof("(foo|bar|baz)$") - 1,
REG_EXTENDED | REG_NOSUB,
"crowbar",
sizeof("crowbar") - 1,
0,
REG_OK,
TRE_LITERAL_OPT_SUFFIX
},
{
"suffix respects REG_NOTEOL",
"(foo|bar|baz)$",
sizeof("(foo|bar|baz)$") - 1,
REG_EXTENDED | REG_NOSUB,
"crowbar",
sizeof("crowbar") - 1,
REG_NOTEOL,
REG_NOMATCH,
TRE_LITERAL_OPT_SUFFIX
},
{
"exact basic",
"^(foo|bar|baz)$",
sizeof("^(foo|bar|baz)$") - 1,
REG_EXTENDED | REG_NOSUB,
"bar",
sizeof("bar") - 1,
0,
REG_OK,
TRE_LITERAL_OPT_EXACT
},
{
"exact respects REG_NOTBOL",
"^(foo|bar|baz)$",
sizeof("^(foo|bar|baz)$") - 1,
REG_EXTENDED | REG_NOSUB,
"bar",
sizeof("bar") - 1,
REG_NOTBOL,
REG_NOMATCH,
TRE_LITERAL_OPT_EXACT
},
{
"exact respects REG_NOTEOL",
"^(foo|bar|baz)$",
sizeof("^(foo|bar|baz)$") - 1,
REG_EXTENDED | REG_NOSUB,
"bar",
sizeof("bar") - 1,
REG_NOTEOL,
REG_NOMATCH,
TRE_LITERAL_OPT_EXACT
},
{
"empty alternation disables optimization",
"(|foo|bar)",
sizeof("(|foo|bar)") - 1,
REG_EXTENDED | REG_NOSUB,
"",
0,
0,
REG_OK,
TRE_LITERAL_OPT_NONE
},
{
"inline flag disable stays generic",
"foo(?-i:zap)zot",
sizeof("foo(?-i:zap)zot") - 1,
REG_EXTENDED | REG_ICASE | REG_NOSUB,
"FoOzApZOt",
sizeof("FoOzApZOt") - 1,
0,
REG_NOMATCH,
TRE_LITERAL_OPT_NONE
},
{
"inline flag disable still matches exact scoped bytes",
"foo(?-i:zap)zot",
sizeof("foo(?-i:zap)zot") - 1,
REG_EXTENDED | REG_ICASE | REG_NOSUB,
"FoOzapZOt",
sizeof("FoOzapZOt") - 1,
0,
REG_OK,
TRE_LITERAL_OPT_NONE
},
{
"nocase non-ascii bytes stay in sync",
nonascii_pattern,
sizeof(nonascii_pattern),
REG_EXTENDED | REG_ICASE | REG_NOSUB,
nonascii_haystack,
sizeof(nonascii_haystack),
0,
RC_ANY,
TRE_LITERAL_OPT_CONTAINS
}
};
size_t i;
int failures = 0;
setlocale(LC_CTYPE, "en_US.ISO-8859-1");
for (i = 0; i < elementsof(cases); i++)
failures += run_case(&cases[i]);
return failures;
}
+85
View File
@@ -0,0 +1,85 @@
/*
test-malformed-regn.c - Verify exact-length edge-case regexps compile or fail
cleanly both with and without a trailing NUL byte.
This software is released under a BSD-style license.
See the file LICENSE for details and copyright.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "tre.h"
typedef struct {
const char *name;
const char *pattern;
int expected_err;
} malformed_case_t;
static int
run_case(const malformed_case_t *tc, int nul_terminated)
{
regex_t preg;
size_t len = strlen(tc->pattern);
size_t alloc_len = len + (nul_terminated ? 1 : 0);
char *pattern = malloc(alloc_len ? alloc_len : 1);
int errcode;
if (pattern == NULL)
{
fprintf(stderr, "%s: out of memory\n", tc->name);
return 1;
}
if (len > 0)
memcpy(pattern, tc->pattern, len);
if (nul_terminated)
pattern[len] = '\0';
memset(&preg, 0, sizeof(preg));
errcode = tre_regncompb(&preg, pattern, len, REG_EXTENDED | REG_NOSUB);
if (errcode == REG_OK)
tre_regfree(&preg);
free(pattern);
if (errcode != tc->expected_err)
{
char errbuf[128];
memset(&preg, 0, sizeof(preg));
tre_regerror(errcode, &preg, errbuf, sizeof(errbuf));
fprintf(stderr, "%s (%s): got %d (%s), expected %d\n",
tc->name, nul_terminated ? "nul" : "exact",
errcode, errbuf, tc->expected_err);
return 1;
}
return 0;
}
int
main(void)
{
static const malformed_case_t cases[] = {
{ "open paren", "(", REG_EPAREN },
{ "open bracket", "[", REG_EBRACK },
{ "unterminated comment", "(?#", REG_BADPAT },
{ "unterminated inline flags", "(?i", REG_BADPAT },
{ "short hex escape", "\\x", REG_OK },
{ "unterminated wide hex", "\\x{", REG_EBRACE },
{ "empty wide hex", "\\x{}", REG_OK }
};
size_t i;
for (i = 0; i < sizeof(cases) / sizeof(*cases); i++)
{
if (run_case(&cases[i], 0))
return 1;
if (run_case(&cases[i], 1))
return 1;
}
return 0;
}
+192
View File
@@ -0,0 +1,192 @@
/*
test-str-source.c - Sample program for using tre_reguexec()
This software is released under a BSD-style license.
See the file LICENSE for details and copyright.
*/
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif /* HAVE_CONFIG_H */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* look for getopt in order to use a -o option for output. */
#if defined(HAVE_UNISTD_H)
#include <unistd.h>
#elif defined(HAVE_GETOPT_H)
#include <getopt.h>
#endif
#include "tre-internal.h"
static FILE *outf = NULL;
/* Context structure for the tre_str_source wrappers. */
typedef struct {
/* Our string. */
const char *str;
/* Current position in the string. */
size_t pos;
} str_handler_ctx;
/* The get_next_char() handler. Sets `c' to the value of the next character,
and increases `pos_add' by the number of bytes read. Returns 1 if the
string has ended, 0 if there are more characters. */
static int
str_handler_get_next(tre_char_t *c, unsigned int *pos_add, void *context)
{
str_handler_ctx *ctx = context;
unsigned char ch = ctx->str[ctx->pos];
#ifdef TRE_DEBUG
fprintf(outf, "str[%lu] = %d\n", (unsigned long)ctx->pos, ch);
#endif /* TRE_DEBUG */
*c = ch;
if (ch)
ctx->pos++;
*pos_add = 1;
return ch == '\0';
}
/* The rewind() handler. Resets the current position in the input string. */
static void
str_handler_rewind(size_t pos, void *context)
{
str_handler_ctx *ctx = context;
#ifdef TRE_DEBUG
fprintf(outf, "rewind to %lu\n", (unsigned long)pos);
#endif /* TRE_DEBUG */
ctx->pos = pos;
}
/* The compare() handler. Compares two substrings in the input and returns
0 if the substrings are equal, and a nonzero value if not. */
static int
str_handler_compare(size_t pos1, size_t pos2, size_t len, void *context)
{
str_handler_ctx *ctx = context;
#ifdef TRE_DEBUG
fprintf(outf, "comparing %lu-%lu and %lu-%lu\n",
(unsigned long)pos1, (unsigned long)pos1 + len,
(unsigned long)pos2, (unsigned long)pos2 + len);
#endif /* TRE_DEBUG */
return strncmp(ctx->str + pos1, ctx->str + pos2, len);
}
/* Creates a tre_str_source wrapper around the string `str'. Returns the
tre_str_source object or NULL if out of memory. */
static tre_str_source *
make_str_source(const char *str)
{
tre_str_source *s;
str_handler_ctx *ctx;
s = calloc(1, sizeof(*s));
if (!s)
return NULL;
ctx = malloc(sizeof(str_handler_ctx));
if (!ctx)
{
free(s);
return NULL;
}
ctx->str = str;
ctx->pos = 0;
s->context = ctx;
s->get_next_char = str_handler_get_next;
s->rewind = str_handler_rewind;
s->compare = str_handler_compare;
return s;
}
/* Frees the memory allocated for `s'. */
static void
free_str_source(tre_str_source *s)
{
free(s->context);
free(s);
}
/* Run one test with tre_reguexec. Returns 1 if the regex matches, 0 if
it doesn't, and -1 if an error occurs. */
static int
test_reguexec(const char *str, const char *regex)
{
regex_t preg;
tre_str_source *source;
regmatch_t pmatch[5];
int ret;
if ((source = make_str_source(str)) == NULL)
{
fprintf(stderr, "Out of memory\n");
ret = -1;
}
else
{
if (tre_regcomp(&preg, regex, REG_EXTENDED) != REG_OK)
{
fprintf(stderr, "Failed to compile /%s/\n", regex);
ret = -1;
}
else
{
if (tre_reguexec(&preg, source, elementsof(pmatch), pmatch, 0) == 0)
{
fprintf(outf, "Match: /%s/ matches \"%.*s\" in \"%s\"\n", regex,
(int)(pmatch[0].rm_eo - pmatch[0].rm_so),
str + pmatch[0].rm_so, str);
ret = 1;
}
else
{
fprintf(outf, "No match: /%s/ in \"%s\"\n", regex, str);
ret = 0;
}
tre_regfree(&preg);
}
free_str_source(source);
}
return ret;
}
int
main(int argc, char **argv)
{
int ret = 0;
outf = stdout;
#if defined(HAVE_UNISTD_H) || defined(HAVE_GETOPT_H)
int opt;
while ((opt = getopt(argc, argv, "o:")) != EOF)
{
switch (opt)
{
case 'o':
if ((outf = fopen(optarg, "w")) == NULL)
{
perror(optarg);
exit(1);
}
break;
default:
/* getopt() will have printed an error message already */
exit(1);
}
}
#endif
ret += test_reguexec("xfoofofoofoo", "(foo)\\1") != 1;
ret += test_reguexec("catcat", "(cat|dog)\\1") != 1;
ret += test_reguexec("catdog", "(cat|dog)\\1") != 0;
ret += test_reguexec("dogdog", "(cat|dog)\\1") != 1;
ret += test_reguexec("dogcat", "(cat|dog)\\1") != 0;
return ret;
}
+167
View File
@@ -0,0 +1,167 @@
# Hanzo Memory (Redis Fork)
## Overview
**Hanzo Memory** is a fork of Redis optimized for the Hanzo AI platform's caching and real-time needs. It provides:
- **Session Storage** - User sessions, API keys
- **Rate Limiting** - Token bucket, sliding window
- **Real-time PubSub** - WebSocket notifications
- **Caching Layer** - LLM responses, embeddings
- **Queue Management** - Background job processing
Repository: https://github.com/hanzoai/redis
## Quick Start
```bash
# Start Redis with Hanzo config
cd hanzo
docker compose up -d
# Connect to Redis
docker exec -it hanzo-redis redis-cli
# Test connection
PING
```
## Hanzo Modules
Pre-configured modules:
- **RedisJSON** - Native JSON support
- **RediSearch** - Full-text search
- **RedisTimeSeries** - Time-series data
- **RedisBloom** - Probabilistic data structures
## Key Namespaces
```
hanzo:session:{session_id} - User sessions
hanzo:api_key:{key_hash} - API key data
hanzo:rate:{org_id}:{endpoint} - Rate limit counters
hanzo:cache:llm:{hash} - LLM response cache
hanzo:cache:embed:{hash} - Embedding cache
hanzo:queue:{queue_name} - Job queues
hanzo:pubsub:{channel} - Real-time channels
```
## Integration Points
### With hanzo/console (LangFuse fork)
Console uses Redis for caching:
```env
REDIS_URL=redis://localhost:6379
```
### With hanzo/llm (LiteLLM fork)
LLM Gateway caches responses:
```env
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=hanzo_dev
```
### With hanzo/iam (Casdoor fork)
IAM uses for session storage:
```env
redisEndpoint=localhost:6379
```
## Syncing with Upstream
```bash
# Fetch upstream changes
git fetch upstream
# Merge upstream unstable
git merge upstream/unstable
# Keep hanzo/ directory
git checkout --ours hanzo/
git push origin unstable
```
## Performance Tuning
### Memory Management
```
# redis.conf
maxmemory 2gb
maxmemory-policy allkeys-lru
# Persistence
save 900 1
save 300 10
save 60 10000
appendonly yes
appendfsync everysec
```
### Client Limits
```
# Connection limits
maxclients 10000
timeout 300
# Slow log
slowlog-log-slower-than 10000
slowlog-max-len 128
```
## Docker Compose
See `hanzo/compose.yml` for local development with:
- Redis Stack (includes modules)
- RedisInsight for management
- Prometheus metrics export
## Caching Patterns
### LLM Response Caching
```python
# Cache key: sha256(model + prompt + params)
cache_key = f"hanzo:cache:llm:{hash}"
ttl = 3600 # 1 hour
# Set with JSON
redis.json().set(cache_key, "$", response)
redis.expire(cache_key, ttl)
```
### Rate Limiting (Sliding Window)
```python
# Key: hanzo:rate:{org_id}:{endpoint}
key = f"hanzo:rate:{org_id}:chat_completions"
window_seconds = 60
max_requests = 100
# Lua script for atomic operation
```
### Session Storage
```python
# Key: hanzo:session:{session_id}
session_key = f"hanzo:session:{session_id}"
ttl = 86400 # 24 hours
redis.json().set(session_key, "$", session_data)
redis.expire(session_key, ttl)
```
## Related Repositories
- **hanzo/console** - AI observability (caching)
- **hanzo/llm** - LLM Gateway (response cache)
- **hanzo/iam** - Identity management (sessions)
- **hanzo/datastore** - ClickHouse fork (OLAP)
- **hanzo/relational** - PostgreSQL fork (OLTP)
+42
View File
@@ -0,0 +1,42 @@
services:
redis:
image: redis/redis-stack:latest
container_name: hanzo-redis
ports:
- "6379:6379" # Redis
- "8001:8001" # RedisInsight
volumes:
- redis-data:/data
- ./redis.conf:/usr/local/etc/redis/redis.conf:ro
environment:
REDIS_ARGS: "--requirepass ${REDIS_PASSWORD:-hanzo_dev}"
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD:-hanzo_dev}", "ping"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
command: >
redis-stack-server
--appendonly yes
--maxmemory 2gb
--maxmemory-policy allkeys-lru
exporter:
image: oliver006/redis_exporter:latest
container_name: hanzo-redis-exporter
ports:
- "9121:9121"
environment:
REDIS_ADDR: redis://redis:6379
REDIS_PASSWORD: ${REDIS_PASSWORD:-hanzo_dev}
depends_on:
- redis
restart: unless-stopped
volumes:
redis-data:
networks:
default:
name: hanzo-memory
+68
View File
@@ -0,0 +1,68 @@
# Hanzo Memory - Redis Configuration
# Network
bind 0.0.0.0
port 6379
tcp-backlog 511
timeout 300
tcp-keepalive 300
# Memory
maxmemory 2gb
maxmemory-policy allkeys-lru
maxmemory-samples 5
# Persistence
save 900 1
save 300 10
save 60 10000
stop-writes-on-bgsave-error yes
rdbcompression yes
rdbchecksum yes
dbfilename dump.rdb
# Append-only file
appendonly yes
appendfilename "appendonly.aof"
appendfsync everysec
no-appendfsync-on-rewrite no
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
# Logging
loglevel notice
logfile ""
# Clients
maxclients 10000
# Slow log
slowlog-log-slower-than 10000
slowlog-max-len 128
# Event notifications (for pub/sub)
notify-keyspace-events "Ex"
# Lua scripting
lua-time-limit 5000
# Latency monitoring
latency-monitor-threshold 100
# Active defragmentation
activedefrag yes
active-defrag-ignore-bytes 100mb
active-defrag-threshold-lower 10
active-defrag-threshold-upper 100
active-defrag-cycle-min 1
active-defrag-cycle-max 25
# Thread I/O
io-threads 4
io-threads-do-reads yes
# Module loading (redis-stack includes these)
# loadmodule /opt/redis-stack/lib/rejson.so
# loadmodule /opt/redis-stack/lib/redisearch.so
# loadmodule /opt/redis-stack/lib/redistimeseries.so
# loadmodule /opt/redis-stack/lib/redisbloom.so
+9 -22
View File
@@ -11,7 +11,7 @@ all: prepare_source
get_source:
$(call submake,$@)
prepare_source: get_source handle-werrors setup_environment
prepare_source: get_source setup_environment
clean:
$(call submake,$@)
@@ -25,14 +25,14 @@ pristine:
install:
$(call submake,$@)
setup_environment: install-rust handle-werrors
setup_environment: install-rust
clean_environment: uninstall-rust
# Keep all of the Rust stuff in one place
install-rust:
ifeq ($(INSTALL_RUST_TOOLCHAIN),yes)
@RUST_VERSION=1.88.0; \
@RUST_VERSION=1.94.0; \
ARCH="$$(uname -m)"; \
if ldd --version 2>&1 | grep -q musl; then LIBC_TYPE="musl"; else LIBC_TYPE="gnu"; fi; \
echo "Detected architecture: $${ARCH} and libc: $${LIBC_TYPE}"; \
@@ -40,24 +40,24 @@ ifeq ($(INSTALL_RUST_TOOLCHAIN),yes)
'x86_64') \
if [ "$${LIBC_TYPE}" = "musl" ]; then \
RUST_INSTALLER="rust-$${RUST_VERSION}-x86_64-unknown-linux-musl"; \
RUST_SHA256="200bcf3b5d574caededba78c9ea9d27e7afc5c6df4154ed0551879859be328e1"; \
RUST_SHA256="9a358120ce1491a4d5b7f71a41e4e97b380b5db5d4ec31f7110f5b3090bd3d55"; \
else \
RUST_INSTALLER="rust-$${RUST_VERSION}-x86_64-unknown-linux-gnu"; \
RUST_SHA256="7b5437c1d18a174faae253a18eac22c32288dccfc09ff78d5ee99b7467e21bca"; \
RUST_SHA256="e8fa4185f3ef6ae32725ff638b1ecdbff28f5d651dc0b3111e2539350d03b15a"; \
fi ;; \
'aarch64') \
if [ "$${LIBC_TYPE}" = "musl" ]; then \
RUST_INSTALLER="rust-$${RUST_VERSION}-aarch64-unknown-linux-musl"; \
RUST_SHA256="f8b3a158f9e5e8cc82e4d92500dd2738ac7d8b5e66e0f18330408856235dec35"; \
RUST_SHA256="008b3f0fc4175c956ecbfa4e0c48865ec3f953741b2926e75e8ded7e3adfdb19"; \
else \
RUST_INSTALLER="rust-$${RUST_VERSION}-aarch64-unknown-linux-gnu"; \
RUST_SHA256="d5decc46123eb888f809f2ee3b118d13586a37ffad38afaefe56aa7139481d34"; \
RUST_SHA256="c6fd6d1c925ed986df3b2c0b89bbc90ce15afb62e4d522a054e7d50c856b3c1a"; \
fi ;; \
*) echo >&2 "Unsupported architecture: '$${ARCH}'"; exit 1 ;; \
esac; \
echo "Downloading and installing Rust standalone installer: $${RUST_INSTALLER}"; \
wget --quiet -O $${RUST_INSTALLER}.tar.xz https://static.rust-lang.org/dist/$${RUST_INSTALLER}.tar.xz; \
echo "$${RUST_SHA256} $${RUST_INSTALLER}.tar.xz" | sha256sum -c --quiet || { echo "Rust standalone installer checksum failed!"; exit 1; }; \
echo "$${RUST_SHA256} $${RUST_INSTALLER}.tar.xz" | sha256sum -c --status || { echo "Rust standalone installer checksum failed!"; exit 1; }; \
tar -xf $${RUST_INSTALLER}.tar.xz; \
(cd $${RUST_INSTALLER} && ./install.sh); \
rm -rf $${RUST_INSTALLER}
@@ -74,17 +74,4 @@ ifeq ($(INSTALL_RUST_TOOLCHAIN),yes)
fi
endif
handle-werrors: get_source
ifeq ($(DISABLE_WERRORS),yes)
@echo "Disabling -Werror for all modules"
@for dir in $(SUBDIRS); do \
echo "Processing $$dir"; \
find $$dir/src -type f \
\( -name "Makefile" \
-o -name "*.mk" \
-o -name "CMakeLists.txt" \) \
-exec sed -i 's/-Werror//g' {} +; \
done
endif
.PHONY: all clean distclean install $(SUBDIRS) setup_environment clean_environment install-rust uninstall-rust handle-werrors
.PHONY: all clean distclean install $(SUBDIRS) setup_environment clean_environment install-rust uninstall-rust
+1 -1
View File
@@ -1,5 +1,5 @@
SRC_DIR = src
MODULE_VERSION = v8.4.0
MODULE_VERSION = v8.8.0
MODULE_REPO = https://github.com/redisbloom/redisbloom
TARGET_MODULE = $(SRC_DIR)/bin/$(FULL_VARIANT)/redisbloom.so
+15 -2
View File
@@ -1,7 +1,20 @@
SRC_DIR = src
MODULE_VERSION = v8.4.1
MODULE_VERSION = v8.8.0
MODULE_REPO = https://github.com/redisearch/redisearch
TARGET_MODULE = $(SRC_DIR)/bin/$(FULL_VARIANT)/search-community/redisearch.so
include ../common.mk
# Enable link-time optimization for RediSearch by default. Override with LTO=0.
LTO ?= 1
export LTO
# Use the committed C headers for Rust modules, rather than regenerating them
# from Rust source. Override with REDISEARCH_GENERATE_HEADERS=1.
REDISEARCH_GENERATE_HEADERS ?= 0
export REDISEARCH_GENERATE_HEADERS
# Set INLINE_LSE_ATOMICS=1 for perf improvement on common ARM CPUs (i.e. Graviton2/3/4); no effect on x86 or macOS.
# Default 0 keeps the binary runnable on pre-Armv8.1-a cores (Cortex-A72, Graviton1, RPi4) that would otherwise SIGILL at module load.
INLINE_LSE_ATOMICS ?= 0
export INLINE_LSE_ATOMICS
include ../common.mk
+1 -1
View File
@@ -1,5 +1,5 @@
SRC_DIR = src
MODULE_VERSION = v8.4.0
MODULE_VERSION = v8.8.0
MODULE_REPO = https://github.com/redisjson/redisjson
TARGET_MODULE = $(SRC_DIR)/bin/$(FULL_VARIANT)/rejson.so
+1 -1
View File
@@ -1,5 +1,5 @@
SRC_DIR = src
MODULE_VERSION = v8.4.0
MODULE_VERSION = v8.8.0
MODULE_REPO = https://github.com/redistimeseries/redistimeseries
TARGET_MODULE = $(SRC_DIR)/bin/$(FULL_VARIANT)/redistimeseries.so
+1 -5
View File
@@ -54,7 +54,7 @@ performed in the background, while the command is executed in the main thread.
`NOQUANT` forces the vector to be created (in the first VADD call to a given key) without integer 8 quantization, which is otherwise the default.
`BIN` forces the vector to use binary quantization instead of int8. This is much faster and uses less memory, but has impacts on the recall quality.
`BIN` forces the vector to use binary quantization instead of int8. This is much faster and uses less memory, but has impacts on the recall quality. The distance is computed as normalized Hamming distance (`hamming_bits * 2 / dim`), yielding values in [0, 2] consistent with cosine distance semantics, not raw Hamming bit counts.
`Q8` forces the vector to use signed 8 bit quantization. This is the default, and the option only exists in order to make sure to check at insertion time if the vector set is of the same format.
@@ -721,10 +721,6 @@ During Vector Sets testing, we discovered that often clients introduce considera
Switching from `VALUES` to `FP32` as a method for transmitting vectors may easily provide 10-20x speedups.
# Known bugs
* Replication code is pretty much untested, and very vanilla (replicating the commands verbatim).
# Implementation details
Vector sets are based on the `hnsw.c` implementation of the HNSW data structure with extensions for speed and functionality.
+330 -29
View File
@@ -47,28 +47,45 @@
#include "hnsw.h"
#include "mixer.h"
/* Check if we can compile SIMD code with function attributes */
#if defined (__x86_64__) && ((defined(__GNUC__) && __GNUC__ >= 5) || (defined(__clang__) && __clang_major__ >= 4))
#if defined(__has_attribute) && __has_attribute(target)
#define HAVE_AVX2
#define HAVE_AVX512
#endif
/* Check if we can compile SIMD code with function attributes.
* This defines HAVE_AVX2, HAVE_AVX512, and HAVE_POPCNT when the compiler
* supports the target() attribute for runtime CPU feature dispatch. */
#if defined(__x86_64__) && ((defined(__GNUC__) && __GNUC__ >= 5) || (defined(__clang__) && __clang_major__ >= 4))
#if defined(__has_attribute) && __has_attribute(target)
#define HAVE_AVX2
#define HAVE_AVX512
#define HAVE_POPCNT
#endif
#endif
#if defined (HAVE_AVX2)
#if defined(HAVE_POPCNT)
#define ATTRIBUTE_TARGET_POPCNT __attribute__((target("popcnt")))
#define VSET_USE_POPCNT __builtin_cpu_supports("popcnt")
#else
#define ATTRIBUTE_TARGET_POPCNT
#define VSET_USE_POPCNT 0
#endif
#if defined(HAVE_AVX2)
#define ATTRIBUTE_TARGET_AVX2 __attribute__((target("avx2,fma")))
#define ATTRIBUTE_TARGET_AVX2_POPCNT __attribute__((target("avx2,fma,popcnt")))
#define VSET_USE_AVX2 (__builtin_cpu_supports("avx2") && __builtin_cpu_supports("fma"))
#else
#define ATTRIBUTE_TARGET_AVX2
#define ATTRIBUTE_TARGET_AVX2_POPCNT
#define VSET_USE_AVX2 0
#endif
#if defined (HAVE_AVX512)
#define ATTRIBUTE_TARGET_AVX512 __attribute__((target("avx512f,fma")))
#define VSET_USE_AVX512 (__builtin_cpu_supports("avx512f"))
#define ATTRIBUTE_TARGET_AVX512 __attribute__((target("avx512f,avx512bw,fma")))
#define ATTRIBUTE_TARGET_AVX512_VPOPCNT __attribute__((target("avx512f,fma,avx512vpopcntdq,popcnt")))
#define VSET_USE_AVX512 (__builtin_cpu_supports("avx512f") && __builtin_cpu_supports("avx512bw"))
#define VSET_USE_AVX512_VPOPCNT (__builtin_cpu_supports("avx512f") && __builtin_cpu_supports("avx512vpopcntdq"))
#else
#define ATTRIBUTE_TARGET_AVX512
#define ATTRIBUTE_TARGET_AVX512_VPOPCNT
#define VSET_USE_AVX512 0
#define VSET_USE_AVX512_VPOPCNT 0
#endif
/* Include SIMD headers when supported */
@@ -88,6 +105,15 @@
#define MIN(a,b) ((a) < (b) ? (a) : (b))
/* Define likely macro if not already defined */
#ifndef likely
#if __GNUC__ >= 3
#define likely(x) __builtin_expect(!!(x), 1)
#else
#define likely(x) (x)
#endif
#endif
/* Algorithm parameters. */
#define HNSW_P 0.25 /* Probability of level increase. */
@@ -221,6 +247,55 @@ float pq_max_distance(pqueue *pq) {
/* ============================ HNSW algorithm ============================== */
/* Check if CPU supports POPCNT instruction - cached per thread */
static inline int hnsw_cpu_supports_popcnt(void) {
#if defined(HAVE_POPCNT)
static __thread int popcnt_supported = -1;
if (popcnt_supported == -1) {
popcnt_supported = __builtin_cpu_supports("popcnt");
}
return popcnt_supported;
#else
return 0; /* Assume CPU does not support POPCNT if __builtin_cpu_supports() is not available. */
#endif
}
/* Manual popcount implementation for platforms without POPCNT support */
static inline int hnsw_popcount64(uint64_t x) {
x = (x & 0x5555555555555555) + ((x >> 1) & 0x5555555555555555);
x = (x & 0x3333333333333333) + ((x >> 2) & 0x3333333333333333);
x = (x & 0x0F0F0F0F0F0F0F0F) + ((x >> 4) & 0x0F0F0F0F0F0F0F0F);
x = (x & 0x00FF00FF00FF00FF) + ((x >> 8) & 0x00FF00FF00FF00FF);
x = (x & 0x0000FFFF0000FFFF) + ((x >> 16) & 0x0000FFFF0000FFFF);
x = (x & 0x00000000FFFFFFFF) + ((x >> 32) & 0x00000000FFFFFFFF);
return x;
}
/* Optimized popcount function that uses hardware POPCNT instruction when available,
* falling back to a software implementation when necessary. The CPU feature detection
* result is cached per thread for better performance. */
ATTRIBUTE_TARGET_POPCNT
static inline int hnsw_popcount(uint64_t x) {
if (likely(hnsw_cpu_supports_popcnt())) {
return __builtin_popcountll(x);
} else {
return hnsw_popcount64(x);
}
}
/* Binary vectors distance function that uses POPCNT when available */
ATTRIBUTE_TARGET_POPCNT
static inline float hnsw_vectors_distance_bin(const uint64_t *x, const uint64_t *y, uint32_t dim) {
uint32_t len = (dim+63)/64;
uint32_t opposite = 0;
for (uint32_t j = 0; j < len; j++) {
uint64_t xor = x[j]^y[j];
opposite += hnsw_popcount(xor);
}
return (float)opposite*2/dim;
}
#if defined(HAVE_AVX512)
/* AVX512 optimized dot product for float vectors */
ATTRIBUTE_TARGET_AVX512
@@ -347,8 +422,155 @@ float vectors_distance_float(const float *x, const float *y, uint32_t dim) {
}
/* Q8 quants dotproduct. We do integer math and later fix it by range. */
#if defined(HAVE_AVX512)
/* AVX512 optimized dot product for Q8 vectors */
ATTRIBUTE_TARGET_AVX512
float vectors_distance_q8_avx512(const int8_t *x, const int8_t *y, uint32_t dim,
float range_a, float range_b) {
// Handle zero vectors special case.
if (range_a == 0 || range_b == 0) {
return 1.0f;
}
const float scale_product = (range_a/127) * (range_b/127);
__m512i sum = _mm512_setzero_si512();
uint32_t i;
/* Process 64 int8 elements at a time with AVX512 */
for (i = 0; i + 63 < dim; i += 64) {
/* Load 64 int8 values */
__m512i vx = _mm512_loadu_si512((__m512i*)&x[i]);
__m512i vy = _mm512_loadu_si512((__m512i*)&y[i]);
/* Unpack and multiply-add in 32-bit precision
* This is done in two steps: lower 32 bytes and upper 32 bytes */
/* Process lower 32 bytes (256 bits) */
__m256i vx_lo = _mm512_extracti64x4_epi64(vx, 0);
__m256i vy_lo = _mm512_extracti64x4_epi64(vy, 0);
/* Extend int8 to int16 */
__m512i vx_lo_16 = _mm512_cvtepi8_epi16(vx_lo);
__m512i vy_lo_16 = _mm512_cvtepi8_epi16(vy_lo);
/* Multiply and accumulate to int32 */
__m512i prod_lo = _mm512_madd_epi16(vx_lo_16, vy_lo_16);
sum = _mm512_add_epi32(sum, prod_lo);
/* Process upper 32 bytes (256 bits) */
__m256i vx_hi = _mm512_extracti64x4_epi64(vx, 1);
__m256i vy_hi = _mm512_extracti64x4_epi64(vy, 1);
__m512i vx_hi_16 = _mm512_cvtepi8_epi16(vx_hi);
__m512i vy_hi_16 = _mm512_cvtepi8_epi16(vy_hi);
__m512i prod_hi = _mm512_madd_epi16(vx_hi_16, vy_hi_16);
sum = _mm512_add_epi32(sum, prod_hi);
}
/* Horizontal sum of the 16 int32 elements in sum */
int32_t dot = _mm512_reduce_add_epi32(sum);
/* Handle remaining elements */
for (; i < dim; i++) {
dot += ((int32_t)x[i]) * ((int32_t)y[i]);
}
/* Convert to original range */
float dotf = dot * scale_product;
float distance = 1.0f - dotf;
/* Clamp distance to [0, 2] */
if (distance < 0) distance = 0;
else if (distance > 2) distance = 2;
return distance;
}
#endif /* HAVE_AVX512 */
#if defined(HAVE_AVX2)
/* AVX2 optimized dot product for Q8 vectors */
ATTRIBUTE_TARGET_AVX2
float vectors_distance_q8_avx2(const int8_t *x, const int8_t *y, uint32_t dim,
float range_a, float range_b) {
// Handle zero vectors special case.
if (range_a == 0 || range_b == 0) {
return 1.0f;
}
const float scale_product = (range_a/127) * (range_b/127);
__m256i sum = _mm256_setzero_si256();
uint32_t i;
/* Process 32 int8 elements at a time with AVX2 */
for (i = 0; i + 31 < dim; i += 32) {
/* Load 32 int8 values */
__m256i vx = _mm256_loadu_si256((__m256i*)&x[i]);
__m256i vy = _mm256_loadu_si256((__m256i*)&y[i]);
/* Split into lower and upper 16 bytes */
__m128i vx_lo = _mm256_extracti128_si256(vx, 0);
__m128i vy_lo = _mm256_extracti128_si256(vy, 0);
__m128i vx_hi = _mm256_extracti128_si256(vx, 1);
__m128i vy_hi = _mm256_extracti128_si256(vy, 1);
/* Extend int8 to int16 for lower half */
__m256i vx_lo_16 = _mm256_cvtepi8_epi16(vx_lo);
__m256i vy_lo_16 = _mm256_cvtepi8_epi16(vy_lo);
/* Multiply and accumulate (madd does multiply adjacent pairs and add) */
__m256i prod_lo = _mm256_madd_epi16(vx_lo_16, vy_lo_16);
sum = _mm256_add_epi32(sum, prod_lo);
/* Extend int8 to int16 for upper half */
__m256i vx_hi_16 = _mm256_cvtepi8_epi16(vx_hi);
__m256i vy_hi_16 = _mm256_cvtepi8_epi16(vy_hi);
__m256i prod_hi = _mm256_madd_epi16(vx_hi_16, vy_hi_16);
sum = _mm256_add_epi32(sum, prod_hi);
}
/* Horizontal sum of the 8 int32 elements in sum */
__m128i sum_hi = _mm256_extracti128_si256(sum, 1);
__m128i sum_lo = _mm256_castsi256_si128(sum);
__m128i sum_128 = _mm_add_epi32(sum_hi, sum_lo);
sum_128 = _mm_hadd_epi32(sum_128, sum_128);
sum_128 = _mm_hadd_epi32(sum_128, sum_128);
int32_t dot = _mm_cvtsi128_si32(sum_128);
/* Handle remaining elements */
for (; i < dim; i++) {
dot += ((int32_t)x[i]) * ((int32_t)y[i]);
}
/* Convert to original range */
float dotf = dot * scale_product;
float distance = 1.0f - dotf;
/* Clamp distance to [0, 2] */
if (distance < 0) distance = 0;
else if (distance > 2) distance = 2;
return distance;
}
#endif /* HAVE_AVX2 */
/* Q8 dot product: automatically selects best available implementation */
float vectors_distance_q8(const int8_t *x, const int8_t *y, uint32_t dim,
float range_a, float range_b) {
#if defined(HAVE_AVX512)
if (dim >= 64 && VSET_USE_AVX512) {
return vectors_distance_q8_avx512(x, y, dim, range_a, range_b);
}
#endif
#if defined(HAVE_AVX2)
if (dim >= 32 && VSET_USE_AVX2) {
return vectors_distance_q8_avx2(x, y, dim, range_a, range_b);
}
#endif
/* Fallback to scalar implementation */
// Handle zero vectors special case.
if (range_a == 0 || range_b == 0) {
/* Zero vector distance from anything is 1.0
@@ -389,25 +611,103 @@ float vectors_distance_q8(const int8_t *x, const int8_t *y, uint32_t dim,
return distance;
}
static inline int popcount64(uint64_t x) {
x = (x & 0x5555555555555555) + ((x >> 1) & 0x5555555555555555);
x = (x & 0x3333333333333333) + ((x >> 2) & 0x3333333333333333);
x = (x & 0x0F0F0F0F0F0F0F0F) + ((x >> 4) & 0x0F0F0F0F0F0F0F0F);
x = (x & 0x00FF00FF00FF00FF) + ((x >> 8) & 0x00FF00FF00FF00FF);
x = (x & 0x0000FFFF0000FFFF) + ((x >> 16) & 0x0000FFFF0000FFFF);
x = (x & 0x00000000FFFFFFFF) + ((x >> 32) & 0x00000000FFFFFFFF);
return x;
}
/* Binary vectors distance. */
float vectors_distance_bin(const uint64_t *x, const uint64_t *y, uint32_t dim) {
#if defined(HAVE_AVX512) && defined(HAVE_POPCNT)
/* AVX-512 vectorized binary distance calculation using VPOPCNTDQ.
* Processes 8 uint64_t (512 bits) per iteration.
*
* Uses _mm512_popcnt_epi64 hardware popcount instruction which requires
* AVX512VPOPCNTDQ extension
*/
ATTRIBUTE_TARGET_AVX512_VPOPCNT
static float vectors_distance_bin_avx512_vpopcnt(const uint64_t *x, const uint64_t *y, uint32_t dim) {
uint32_t len = (dim+63)/64;
uint32_t opposite = 0;
for (uint32_t j = 0; j < len; j++) {
uint64_t xor = x[j]^y[j];
opposite += popcount64(xor);
uint32_t j = 0;
/* Process 8 uint64_t (512 bits) at a time with hardware popcount */
if (len >= 8) {
__m512i sum = _mm512_setzero_si512();
for (; j + 7 < len; j += 8) {
__m512i vx = _mm512_loadu_si512((__m512i*)&x[j]);
__m512i vy = _mm512_loadu_si512((__m512i*)&y[j]);
__m512i vxor = _mm512_xor_si512(vx, vy);
/* Hardware popcount for 64-bit integers (AVX512VPOPCNTDQ) */
__m512i popcnt = _mm512_popcnt_epi64(vxor);
sum = _mm512_add_epi64(sum, popcnt);
}
/* Horizontal sum: reduce 8x 64-bit integers to scalar */
opposite = _mm512_reduce_add_epi64(sum);
}
return (float)opposite*2/dim;
/* Handle remaining elements */
for (; j < len; j++) {
uint64_t xor = x[j] ^ y[j];
opposite += __builtin_popcountll(xor);
}
return (float)opposite * 2.0f / dim;
}
#endif
#if defined(HAVE_AVX2) && defined(HAVE_POPCNT)
/* AVX2 vectorized binary distance calculation.
* Processes 4 uint64_t (256 bits) per iteration. */
ATTRIBUTE_TARGET_AVX2_POPCNT
static float vectors_distance_bin_avx2(const uint64_t *x, const uint64_t *y, uint32_t dim) {
uint32_t len = (dim+63)/64;
uint32_t opposite = 0;
uint32_t j = 0;
/* Process 4 uint64_t (256 bits) at a time */
if (len >= 4) {
for (; j + 3 < len; j += 4) {
__m256i vx = _mm256_loadu_si256((__m256i*)&x[j]);
__m256i vy = _mm256_loadu_si256((__m256i*)&y[j]);
__m256i vxor = _mm256_xor_si256(vx, vy);
/* Extract and use hardware POPCNT instruction */
uint64_t xor_vals[4];
_mm256_storeu_si256((__m256i*)xor_vals, vxor);
opposite += __builtin_popcountll(xor_vals[0]);
opposite += __builtin_popcountll(xor_vals[1]);
opposite += __builtin_popcountll(xor_vals[2]);
opposite += __builtin_popcountll(xor_vals[3]);
}
}
/* Handle remaining elements */
for (; j < len; j++) {
uint64_t xor = x[j] ^ y[j];
opposite += __builtin_popcountll(xor);
}
return (float)opposite * 2.0f / dim;
}
#endif
/* Binary vectors distance with SIMD dispatch. */
ATTRIBUTE_TARGET_POPCNT
float vectors_distance_bin(const uint64_t *x, const uint64_t *y, uint32_t dim) {
#if defined(HAVE_AVX512) && defined(HAVE_POPCNT)
/* AVX-512 with VPOPCNTDQ */
if (dim >= 512 && VSET_USE_AVX512_VPOPCNT) {
return vectors_distance_bin_avx512_vpopcnt(x, y, dim);
}
#endif
#if defined(HAVE_AVX2) && defined(HAVE_POPCNT)
/* AVX2 path: processes 4 uint64_t (256 bits) per iteration */
if (dim >= 256 && VSET_USE_AVX2 && VSET_USE_POPCNT) {
return vectors_distance_bin_avx2(x, y, dim);
}
#endif
/* Fallback to scalar implementation with runtime POPCNT detection */
return hnsw_vectors_distance_bin(x, y, dim);
}
/* Dot product between nodes. Will call the right version depending on the
@@ -2582,26 +2882,27 @@ hnswCursor *hnsw_cursor_init(HNSW *index) {
/* Free the cursor. Can be called both at the end of the iteration, when
* hnsw_cursor_next() returned NULL, or before. */
void hnsw_cursor_free(hnswCursor *cursor) {
if (pthread_rwlock_wrlock(&cursor->index->global_lock) != 0) {
HNSW *index = cursor->index;
if (pthread_rwlock_wrlock(&index->global_lock) != 0) {
// No easy way to recover from that. We will leak memory.
return;
}
hnswCursor *x = cursor->index->cursors;
hnswCursor *x = index->cursors;
hnswCursor *prev = NULL;
while(x) {
if (x == cursor) {
if (prev)
prev->next = cursor->next;
else
cursor->index->cursors = cursor->next;
index->cursors = cursor->next;
hfree(cursor);
break;
}
prev = x;
x = x->next;
}
pthread_rwlock_unlock(&cursor->index->global_lock);
pthread_rwlock_unlock(&index->global_lock);
}
/* Acquire a lock to use the cursor. Returns 1 if the lock was acquired
+5 -3
View File
@@ -96,10 +96,10 @@ class TestCase:
self.error_details = None
self.test_key = f"test:{self.__class__.__name__.lower()}"
# Primary Redis instance
self.redis = redis.Redis(port=primary_port,db=9)
self.redis = redis.Redis(port=primary_port,protocol=2,db=9)
self.redis3 = redis.Redis(port=primary_port,protocol=3,db=9)
# Replica Redis instance
self.replica = redis.Redis(port=replica_port,db=9)
self.replica = redis.Redis(port=replica_port,protocol=2,db=9)
# Replication status
self.replication_setup = False
# Ports
@@ -174,7 +174,8 @@ class TestCase:
def find_test_classes(primary_port, replica_port):
test_classes = []
tests_dir = 'tests'
script_dir = os.path.dirname(os.path.abspath(__file__))
tests_dir = os.path.join(script_dir, 'tests')
if not os.path.exists(tests_dir):
return []
@@ -285,6 +286,7 @@ def run_tests():
else:
if total-skipped-passed > 0:
print(colored(f"{total-skipped-passed} TESTS FAILED!", "red"))
sys.exit(1)
if skipped > 0:
print(colored(f"{skipped} TESTS SKIPPED!", "yellow"))
@@ -0,0 +1,54 @@
from test import TestCase
class BinVectorization(TestCase):
def getname(self):
return "Binary quantization: verify vectorized vs scalar paths produce consistent results"
def test(self):
# Test with different dimensions to exercise different code paths:
# - dim=1: Edge case for minimal valid dimension (scalar path)
# - dim=64: Exact alignment boundary, one uint64_t word (scalar path)
# - dim=128: Scalar path (< 256)
# - dim=384: AVX2 path if available (>= 256, < 512)
# - dim=768: AVX512 path if available (>= 512)
# Note: dim=0 is not tested as it's invalid input (division by zero)
test_dims = [1, 64, 128, 384, 768]
for dim in test_dims:
# Add two very similar vectors, one different
vec1 = [1.0] * dim
vec2 = [0.99] * dim # Very similar to vec1
vec3 = [-1.0] * dim # Opposite direction - should have low similarity
# Add vectors with binary quantization
self.redis.execute_command('VADD', f'{self.test_key}:dim{dim}', 'VALUES', dim,
*[str(x) for x in vec1], f'{self.test_key}:dim{dim}:item:1', 'BIN')
self.redis.execute_command('VADD', f'{self.test_key}:dim{dim}', 'VALUES', dim,
*[str(x) for x in vec2], f'{self.test_key}:dim{dim}:item:2', 'BIN')
self.redis.execute_command('VADD', f'{self.test_key}:dim{dim}', 'VALUES', dim,
*[str(x) for x in vec3], f'{self.test_key}:dim{dim}:item:3', 'BIN')
# Query similarity
result = self.redis.execute_command('VSIM', f'{self.test_key}:dim{dim}', 'VALUES', dim,
*[str(x) for x in vec1], 'WITHSCORES')
# Convert results to dictionary
results_dict = {}
for i in range(0, len(result), 2):
key = result[i].decode()
score = float(result[i+1])
results_dict[key] = score
# Verify results are consistent across dimensions
# Self-similarity should be very high (binary quantization is less precise)
assert results_dict[f'{self.test_key}:dim{dim}:item:1'] > 0.99, \
f"Dim {dim}: Self-similarity too low: {results_dict[f'{self.test_key}:dim{dim}:item:1']}"
# Similar vector should have high similarity (binary quant loses some precision)
assert results_dict[f'{self.test_key}:dim{dim}:item:2'] > 0.95, \
f"Dim {dim}: Similar vector similarity too low: {results_dict[f'{self.test_key}:dim{dim}:item:2']}"
# Opposite vector should have very low similarity
assert results_dict[f'{self.test_key}:dim{dim}:item:3'] < 0.1, \
f"Dim {dim}: Opposite vector similarity too high: {results_dict[f'{self.test_key}:dim{dim}:item:3']}"
@@ -0,0 +1,129 @@
from test import TestCase, generate_random_vector
import struct
import redis.exceptions
MAX_DIM = 65536
class DimensionMaxLimitVaddAtLimit(TestCase):
def getname(self):
return "[regression] VADD VALUES dim == MAX_DIM accepted"
def estimated_runtime(self):
return 0.5
def test(self):
dim = MAX_DIM
vec = generate_random_vector(dim)
result = self.redis.execute_command(
'VADD', self.test_key,
'VALUES', dim,
*[str(x) for x in vec],
f"{self.test_key}:item:maxdim")
assert result == 1, "VADD with dimension at the limit should succeed"
class DimensionMaxLimitVaddAboveLimit(TestCase):
def getname(self):
return "[regression] VADD VALUES dim > MAX_DIM rejected"
def estimated_runtime(self):
return 0.1
def test(self):
too_big_dim = MAX_DIM + 1
too_big_vec = generate_random_vector(16)
try:
self.redis.execute_command(
'VADD', self.test_key,
'VALUES', too_big_dim,
*[str(x) for x in too_big_vec],
f"{self.test_key}:item:toolarge")
assert False, "VADD with dimension above the limit should fail"
except redis.exceptions.ResponseError as e:
# parseVector returns NULL so caller uses the generic invalid spec error
assert "invalid vector specification" in str(e), (
f"Expected invalid vector specification error, got: {e}")
class DimensionMaxLimitVsimAtLimit(TestCase):
def getname(self):
return "[regression] VSIM VALUES dim == MAX_DIM accepted"
def estimated_runtime(self):
return 0.5
def test(self):
# Insert a vector at the maximum allowed dimension, then query at the same dimension.
dim = MAX_DIM
base_vec = generate_random_vector(dim)
result = self.redis.execute_command(
'VADD', self.test_key,
'VALUES', dim,
*[str(x) for x in base_vec],
f"{self.test_key}:item:1")
assert result == 1, "VADD with dimension at the limit should succeed"
query = generate_random_vector(dim)
res = self.redis.execute_command(
'VSIM', self.test_key,
'VALUES', dim,
*[str(x) for x in query],
'COUNT', 1)
assert isinstance(res, list), "VSIM with dimension at the limit should return a list"
class DimensionMaxLimitVsimAboveLimit(TestCase):
def getname(self):
return "[regression] VSIM VALUES dim > MAX_DIM rejected"
def estimated_runtime(self):
return 0.1
def test(self):
# Create a small index, then issue a VSIM with an over-limit dimension.
base_dim = 16
base_vec = generate_random_vector(base_dim)
result = self.redis.execute_command(
'VADD', self.test_key,
'VALUES', base_dim,
*[str(x) for x in base_vec],
f"{self.test_key}:item:1")
assert result == 1, "VADD with base_dim should succeed"
too_big_dim = MAX_DIM + 1
too_big_vec = generate_random_vector(16)
try:
self.redis.execute_command(
'VSIM', self.test_key,
'VALUES', too_big_dim,
*[str(x) for x in too_big_vec],
'COUNT', 1)
assert False, "VSIM with dimension above the limit should fail"
except redis.exceptions.ResponseError as e:
assert "invalid vector specification" in str(e), (
f"Expected invalid vector specification error in VSIM, got: {e}")
class DimensionMaxLimitHugeDimension(TestCase):
def getname(self):
return "[regression] VADD VALUES absurdly large dim rejected"
def estimated_runtime(self):
return 0.1
def test(self):
# Extremely large dimension close to LLONG_MAX should also be rejected safely.
huge_dim = 9223372036854775807 # LLONG_MAX from the original report
try:
self.redis.execute_command(
'VADD', self.test_key,
'VALUES', huge_dim,
'0') # Just a dummy value; parseVector should reject based on dimension alone
assert False, "VADD with absurdly large dimension should fail"
except redis.exceptions.ResponseError as e:
assert "invalid vector specification" in str(e), (
f"Expected invalid vector specification error for huge dim, got: {e}")
@@ -65,3 +65,33 @@ class DimensionValidation(TestCase):
assert False, "VSIM with wrong dimension should fail"
except redis.exceptions.ResponseError as e:
assert "Input dimension mismatch for projection" in str(e), f"Expected dimension mismatch error in VSIM, got: {e}"
class ReduceDimConstraintValidation(TestCase):
def getname(self):
return "[regression] VADD enforces reduce_dim <= dim"
def estimated_runtime(self):
return 0.1
def test(self):
import struct
dim = 16
reduce_dim = dim + 1 # Intentionally larger than dim
# Build a simple FP32 vector of the given dimension.
vec = [0.0] * dim
vec_bytes = struct.pack(f'{dim}f', *vec)
try:
self.redis.execute_command(
'VADD', self.test_key,
'REDUCE', reduce_dim,
'FP32', vec_bytes,
f'{self.test_key}:item:reducemismatch')
assert False, "VADD with reduce_dim > dim should fail"
except redis.exceptions.ResponseError as e:
# Same generic validation error path as other vector spec problems.
assert "invalid vector specification" in str(e), (
f"Expected invalid vector error, got: {e}")
@@ -0,0 +1,71 @@
from test import TestCase
class Q8Similarity(TestCase):
def getname(self):
return "Q8 quantization: VSIM reported distance makes sense with 4D vectors"
def test(self):
# Add two very similar vectors, one different
# Using same test vectors as basic_similarity.py for comparison
vec1 = [1, 0, 0, 0]
vec2 = [0.99, 0.01, 0, 0]
vec3 = [0.1, 1, -1, 0.5]
# Add vectors using VALUES format with Q8 quantization
self.redis.execute_command('VADD', self.test_key, 'VALUES', 4,
*[str(x) for x in vec1], f'{self.test_key}:item:1', 'Q8')
self.redis.execute_command('VADD', self.test_key, 'VALUES', 4,
*[str(x) for x in vec2], f'{self.test_key}:item:2', 'Q8')
self.redis.execute_command('VADD', self.test_key, 'VALUES', 4,
*[str(x) for x in vec3], f'{self.test_key}:item:3', 'Q8')
# Query similarity with vec1
result = self.redis.execute_command('VSIM', self.test_key, 'VALUES', 4,
*[str(x) for x in vec1], 'WITHSCORES')
# Convert results to dictionary
results_dict = {}
for i in range(0, len(result), 2):
key = result[i].decode()
score = float(result[i+1])
results_dict[key] = score
# Verify results (same expectations as float32, allowing for quantization error)
assert results_dict[f'{self.test_key}:item:1'] > 0.99, "Self-similarity should be very high (Q8)"
assert results_dict[f'{self.test_key}:item:2'] > 0.99, "Similar vector should have high similarity (Q8)"
assert results_dict[f'{self.test_key}:item:3'] < 0.80, "Not very similar vector should have low similarity (Q8)"
# Test extreme values with 512 dimensions to stress-test overflow safety
vec4 = [1.0] * 512 # All +127 after quantization
vec5 = [-1.0] * 512 # All -127 after quantization
vec6 = [1.0, -1.0] * 256 # Alternating +127, -127
# Add vectors using VALUES format with Q8 quantization
self.redis.execute_command('VADD', f'{self.test_key}:extreme', 'VALUES', 512,
*[str(x) for x in vec4], f'{self.test_key}:extreme:vec4', 'Q8')
self.redis.execute_command('VADD', f'{self.test_key}:extreme', 'VALUES', 512,
*[str(x) for x in vec5], f'{self.test_key}:extreme:vec5', 'Q8')
self.redis.execute_command('VADD', f'{self.test_key}:extreme', 'VALUES', 512,
*[str(x) for x in vec6], f'{self.test_key}:extreme:vec6', 'Q8')
# Query vec4 against itself - worst-case positive accumulation (512 * 127 * 127 = 8,258,048)
result_vec4 = self.redis.execute_command('VSIM', f'{self.test_key}:extreme', 'VALUES', 512,
*[str(x) for x in vec4], 'WITHSCORES')
results_vec4 = {}
for i in range(0, len(result_vec4), 2):
key = result_vec4[i].decode()
score = float(result_vec4[i+1])
results_vec4[key] = score
# Verify extreme value handling
# VSIM returns similarity = 1.0 - distance/2.0, so:
# - Distance 0 (identical) → similarity 1.0
# - Distance 2 (opposite) → similarity 0.0
assert results_vec4[f'{self.test_key}:extreme:vec4'] > 0.999, \
f"vec4 self-similarity should be very high, got {results_vec4[f'{self.test_key}:extreme:vec4']}"
assert results_vec4[f'{self.test_key}:extreme:vec5'] < 0.01, \
f"vec4 vs vec5 (opposite extremes) should be near 0, got {results_vec4[f'{self.test_key}:extreme:vec5']}"
# Alternating pattern should result in mid-range similarity (perpendicular)
assert 0.4 < results_vec4[f'{self.test_key}:extreme:vec6'] < 0.6, \
f"vec4 vs vec6 (alternating) should be near 0.5, got {results_vec4[f'{self.test_key}:extreme:vec6']}"
@@ -0,0 +1,85 @@
from test import TestCase
class Q8Vectorization(TestCase):
def getname(self):
return "Q8 quantization: verify vectorized vs scalar paths produce consistent results"
def test(self):
# Test with different dimensions to exercise different code paths and boundaries:
# - dim=16: Scalar path (< 32)
# - dim=31: Largest scalar-only dimension (boundary)
# - dim=32: Smallest AVX2 dimension, no remainder (boundary)
# - dim=33: AVX2 with 1-element remainder
# - dim=63: AVX2 with 31-element remainder (largest AVX2-only)
# - dim=64: Smallest AVX512 dimension, no remainder (boundary)
# - dim=65: AVX512 with 1-element remainder
# - dim=128: AVX512 path with no remainder
# - dim=256, dim=512: Large dimensions to test overflow prevention
test_dims = [16, 31, 32, 33, 63, 64, 65, 128, 256, 512]
for dim in test_dims:
key = f'{self.test_key}:dim{dim}'
# Test vectors with extreme values to verify overflow prevention:
# vec1: all +1.0 -> quantizes to +127 (max positive int8)
# vec2: all +0.99 -> quantizes to ~+126 (similar to vec1)
# vec3: all -1.0 -> quantizes to -127/-128 (max negative int8)
# vec4: alternating +1.0/-1.0 -> alternating +127/-127 (tests mixed signs)
vec1 = [1.0] * dim # All max positive
vec2 = [0.99] * dim # Similar to vec1
vec3 = [-1.0] * dim # All max negative (opposite direction)
vec4 = [1.0 if i % 2 == 0 else -1.0 for i in range(dim)] # Alternating extreme values
# Add vectors with Q8 quantization
self.redis.execute_command('VADD', key, 'VALUES', dim,
*[str(x) for x in vec1], f'{key}:item:1', 'Q8')
self.redis.execute_command('VADD', key, 'VALUES', dim,
*[str(x) for x in vec2], f'{key}:item:2', 'Q8')
self.redis.execute_command('VADD', key, 'VALUES', dim,
*[str(x) for x in vec3], f'{key}:item:3', 'Q8')
self.redis.execute_command('VADD', key, 'VALUES', dim,
*[str(x) for x in vec4], f'{key}:item:4', 'Q8')
# Query similarity using vec1 (all max positive values)
# This exercises worst-case positive accumulation: dim * 127 * 127
result = self.redis.execute_command('VSIM', key, 'VALUES', dim,
*[str(x) for x in vec1], 'WITHSCORES')
# Convert results to dictionary
results_dict = {}
for i in range(0, len(result), 2):
k = result[i].decode()
score = float(result[i+1])
results_dict[k] = score
# Verify results - these would be wrong if overflow occurred
# Self-similarity should be ~1.0 (identical vectors)
assert results_dict[f'{key}:item:1'] > 0.99, \
f"Dim {dim}: Self-similarity too low: {results_dict[f'{key}:item:1']}"
# Similar vector should have high similarity
assert results_dict[f'{key}:item:2'] > 0.99, \
f"Dim {dim}: Similar vector similarity too low: {results_dict[f'{key}:item:2']}"
# Opposite vector should have very low similarity (~0.0)
# With overflow bug, this could give incorrect positive values
assert results_dict[f'{key}:item:3'] < 0.1, \
f"Dim {dim}: Opposite vector similarity too high: {results_dict[f'{key}:item:3']}"
# Alternating vector: dot product sums to ~0, so similarity ~0.5
# (127*127) + (127*-127) + ... = 0, normalized gives ~0.5
assert 0.4 < results_dict[f'{key}:item:4'] < 0.6, \
f"Dim {dim}: Alternating vector similarity unexpected: {results_dict[f'{key}:item:4']}"
# Also query with the alternating pattern to verify its self-similarity
result_alt = self.redis.execute_command('VSIM', key, 'VALUES', dim,
*[str(x) for x in vec4], 'WITHSCORES')
results_alt = {}
for i in range(0, len(result_alt), 2):
k = result_alt[i].decode()
score = float(result_alt[i+1])
results_alt[k] = score
assert results_alt[f'{key}:item:4'] > 0.99, \
f"Dim {dim}: Alternating self-similarity too low: {results_alt[f'{key}:item:4']}"
@@ -0,0 +1,19 @@
from test import TestCase
class VSIMDuplicateFilterLeak(TestCase):
def getname(self):
return "[regression] VSIM duplicate FILTER should not leak memory"
def test(self):
self.redis.execute_command('VADD', self.test_key, 'VALUES', 3, 0.5774, 0.5774, 0.5774, 'elem1')
self.redis.execute_command('VADD', self.test_key, 'VALUES', 3, 0.7071, 0.7071, 0.0, 'elem2')
self.redis.execute_command('VSETATTR', self.test_key, 'elem1', '{"a": 1, "b": 2}')
self.redis.execute_command('VSETATTR', self.test_key, 'elem2', '{"a": 2, "b": 3}')
# Duplicate FILTER: before the fix the first exprstate was
# overwritten without exprFree(), leaking ~760 bytes per call.
# Under ASAN/valgrind this shows up as a leak at server exit.
for _ in range(100):
self.redis.execute_command(
'VSIM', self.test_key, 'VALUES', 3, 0.5774, 0.5774, 0.5774,
'FILTER', '.a == 1', 'FILTER', '.b >= 1')
@@ -0,0 +1,32 @@
from test import TestCase
import redis as redis_module
class VSIMFilterLeakOnOptionError(TestCase):
def getname(self):
return "[regression] VSIM FILTER expr freed on option parse error"
def test(self):
self.redis.execute_command('VADD', self.test_key, 'VALUES', 3, 1, 0, 0, 'elem1')
# Valid FILTER followed by invalid option values. Before the fix,
# error paths freed vec but not filter_expr, leaking the compiled
# exprstate. Under ASAN/valgrind this shows up at server exit.
error_cmds = [
# invalid COUNT (0)
['VSIM', self.test_key, 'VALUES', 3, 0, 0, 0, 'FILTER', '.a > 0', 'COUNT', 0],
# invalid EF (0)
['VSIM', self.test_key, 'VALUES', 3, 0, 0, 0, 'FILTER', '.a > 0', 'EF', 0],
# invalid EPSILON (0)
['VSIM', self.test_key, 'VALUES', 3, 0, 0, 0, 'FILTER', '.a > 0', 'EPSILON', 0],
# invalid FILTER-EF (0)
['VSIM', self.test_key, 'VALUES', 3, 0, 0, 0, 'FILTER', '.a > 0', 'FILTER-EF', 0],
# unknown option
['VSIM', self.test_key, 'VALUES', 3, 0, 0, 0, 'FILTER', '.a > 0', 'BADOPT', 1],
]
for cmd in error_cmds:
for _ in range(20):
try:
self.redis.execute_command(*cmd)
except redis_module.exceptions.ResponseError:
pass
+61 -6
View File
@@ -134,6 +134,9 @@ static uint64_t VectorSetTypeNextId = 0;
// Default num elements returned by VSIM.
#define VSET_DEFAULT_COUNT 10
// Maximum allowed vector dimension for input vectors and sets.
#define VSET_MAX_VECTOR_DIM (1<<16)
/* ========================== Internal data structure ====================== */
/* Our abstract data type needs a dual representation similar to Redis
@@ -408,6 +411,7 @@ float *parseVector(RedisModuleString **argv, int argc, int start_idx,
// Must be 4 bytes per component.
if (vec_raw_len % 4 || vec_raw_len < 4) return NULL;
*dim = vec_raw_len/4;
if (*dim > VSET_MAX_VECTOR_DIM) return NULL;
vec = RedisModule_Alloc(vec_raw_len);
if (!vec) return NULL;
@@ -417,7 +421,7 @@ float *parseVector(RedisModuleString **argv, int argc, int start_idx,
if (argc < start_idx + 2) return NULL; // Need at least the dimension.
long long vdim; // Vector dimension passed by the user.
if (RedisModule_StringToLongLong(argv[start_idx+1],&vdim)
!= REDISMODULE_OK || vdim < 1) return NULL;
!= REDISMODULE_OK || vdim < 1 || vdim > VSET_MAX_VECTOR_DIM) return NULL;
// Check that all the arguments are available.
if (argc < start_idx + 2 + vdim) return NULL;
@@ -441,6 +445,12 @@ float *parseVector(RedisModuleString **argv, int argc, int start_idx,
return NULL; // Unknown format.
}
// reduce_dim must be <= dim
if (reduce_dim && *reduce_dim && *reduce_dim > *dim) {
if (vec) RedisModule_Free(vec);
return NULL;
}
if (consumed_args) *consumed_args = consumed;
return vec;
}
@@ -1064,6 +1074,7 @@ int VSIM_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
!= REDISMODULE_OK || count <= 0)
{
RedisModule_Free(vec);
if (filter_expr) exprFree(filter_expr);
return RedisModule_ReplyWithError(ctx, "ERR invalid COUNT");
}
j += 2;
@@ -1072,6 +1083,7 @@ int VSIM_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
REDISMODULE_OK || epsilon <= 0)
{
RedisModule_Free(vec);
if (filter_expr) exprFree(filter_expr);
return RedisModule_ReplyWithError(ctx, "ERR invalid EPSILON");
}
j += 2;
@@ -1080,6 +1092,7 @@ int VSIM_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
REDISMODULE_OK || ef <= 0 || ef > 1000000)
{
RedisModule_Free(vec);
if (filter_expr) exprFree(filter_expr);
return RedisModule_ReplyWithError(ctx, "ERR invalid EF");
}
j += 2;
@@ -1088,6 +1101,7 @@ int VSIM_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
REDISMODULE_OK || filter_ef <= 0)
{
RedisModule_Free(vec);
if (filter_expr) exprFree(filter_expr);
return RedisModule_ReplyWithError(ctx, "ERR invalid FILTER-EF");
}
j += 2;
@@ -1096,6 +1110,7 @@ int VSIM_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
size_t exprlen;
char *exprstr = (char*)RedisModule_StringPtrLen(exprarg,&exprlen);
int errpos;
if (filter_expr) exprFree(filter_expr);
filter_expr = exprCompile(exprstr,&errpos);
if (filter_expr == NULL) {
if ((size_t)errpos >= exprlen) errpos = 0;
@@ -1107,6 +1122,7 @@ int VSIM_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
j += 2;
} else {
RedisModule_Free(vec);
if (filter_expr) exprFree(filter_expr);
return RedisModule_ReplyWithError(ctx,
"ERR syntax error in VSIM command");
}
@@ -1960,6 +1976,15 @@ void *VectorSetRdbLoad(RedisModuleIO *rdb, int encver) {
uint32_t quant_type = hnsw_config & 0xff;
uint32_t hnsw_m = (hnsw_config >> 8) & 0xffff;
/* Validate dimension loaded from RDB to enforce invariants and
* avoid absurd allocations or inconsistent state. */
if (dim == 0 || dim > VSET_MAX_VECTOR_DIM) {
RedisModule_LogIOError(rdb, "warning",
"Invalid vector dimension in RDB: dim=%u (max allowed %u)",
(unsigned)dim, (unsigned)VSET_MAX_VECTOR_DIM);
return NULL;
}
/* Check that the quantization type is correct. Otherwise
* return ASAP signaling the error. */
if (quant_type != HNSW_QUANT_NONE &&
@@ -1981,14 +2006,44 @@ void *VectorSetRdbLoad(RedisModuleIO *rdb, int encver) {
uint32_t input_dim = RedisModule_LoadUnsigned(rdb);
if (RedisModule_IsIOError(rdb)) goto ioerr;
uint32_t output_dim = dim;
size_t matrix_size = sizeof(float) * input_dim * output_dim;
/* Sanity check projection dimensions. */
if (input_dim == 0 || output_dim == 0 || input_dim > VSET_MAX_VECTOR_DIM || output_dim > input_dim) {
RedisModule_LogIOError(rdb, "warning",
"Invalid projection matrix dimensions: input_dim=%u, output_dim=%u (max allowed %u)",
(unsigned)input_dim, (unsigned)output_dim,
(unsigned)VSET_MAX_VECTOR_DIM);
goto ioerr;
}
/* Check for overflow in matrix_size = sizeof(float) * input_dim * output_dim. */
#if SIZE_MAX == UINT32_MAX
uint64_t product = (uint64_t) output_dim * (uint64_t) input_dim * sizeof(float);
if (product > SIZE_MAX) {
RedisModule_LogIOError(rdb, "warning",
"Projection matrix size overflow (output_dim too large): input_dim=%u, output_dim=%u",
(unsigned)input_dim, (unsigned)output_dim);
goto ioerr;
}
#endif
size_t matrix_size = sizeof(float) * (size_t)input_dim * (size_t)output_dim;
/* Load projection matrix as a binary blob and validate length. */
size_t blob_len = 0;
char *matrix_blob = RedisModule_LoadStringBuffer(rdb, &blob_len);
if (matrix_blob == NULL) goto ioerr;
if (blob_len != matrix_size) {
RedisModule_LogIOError(rdb, "warning",
"Mismatching projection matrix length: expected=%zu, got=%zu",
matrix_size, blob_len);
RedisModule_Free(matrix_blob);
goto ioerr;
}
vset->proj_matrix = RedisModule_Alloc(matrix_size);
vset->proj_input_size = input_dim;
// Load projection matrix as a binary blob
char *matrix_blob = RedisModule_LoadStringBuffer(rdb, NULL);
if (matrix_blob == NULL) goto ioerr;
memcpy(vset->proj_matrix, matrix_blob, matrix_size);
RedisModule_Free(matrix_blob);
}
+127 -4
View File
@@ -247,6 +247,23 @@ tcp-keepalive 300
# tls-auth-clients no
# tls-auth-clients optional
# Automatically authenticate TLS clients as Redis users based on their
# certificates.
#
# If set to a field like "CN", the server will extract the corresponding field
# from the client's TLS certificate and attempt to find a Redis user with the
# same name. If a matching user is found, the client is automatically
# authenticated as that user during the TLS handshake. If no matching user is
# found, the client is connected as the unauthenticated default user. Set to
# "off" to disable automatic user authentication via certificate fields.
#
# Supported values: CN, off. Default: off.
#
# Matches certificate CN to Redis username (exact match only).
# Example: Cert CN=myapp -> authenticates as user "myapp"
#
# tls-auth-clients-user CN
# By default, a Redis replica does not attempt to establish a TLS connection
# with its master.
#
@@ -662,6 +679,9 @@ repl-diskless-sync-max-replicas 0
# replication history.
# Note that this requires sufficient memory, if you don't have it,
# you risk an OOM kill.
# "flushdb" - Always flush the entire dataset before diskless load.
# Note that if the diskless load fails, the replica will lose all
# existing data.
# "on-empty-db" - Use diskless load only when current dataset is empty. This is
# safer and avoid having old and new dataset loaded side by side
# during replication.
@@ -1157,6 +1177,8 @@ acllog-max-len 128
# allkeys-lru -> Evict any key using approximated LRU.
# volatile-lfu -> Evict using approximated LFU, only keys with an expire set.
# allkeys-lfu -> Evict any key using approximated LFU.
# volatile-lrm -> Evict using approximated LRM, only keys with an expire set.
# allkeys-lrm -> Evict any key using approximated LRM.
# volatile-random -> Remove a random key having an expire set.
# allkeys-random -> Remove a random key, any key.
# volatile-ttl -> Remove the key with the nearest expire time (minor TTL)
@@ -1164,10 +1186,17 @@ acllog-max-len 128
#
# LRU means Least Recently Used
# LFU means Least Frequently Used
# LRM means Least Recently Modified (only write operations update the timestamp)
#
# Both LRU, LFU and volatile-ttl are implemented using approximated
# LRU, LFU, LRM and volatile-ttl are implemented using approximated
# randomized algorithms.
#
# LRU vs LRM: Both use similar eviction logic based on access time, but:
# - LRU updates the timestamp on both read (GET) and write (SET) operations
# - LRM only updates the timestamp on write (SET, INCR, etc.) operations
# This makes LRM useful when you want to evict keys that haven't been updated
# recently, regardless of how often they are read.
#
# Note: with any of the above policies, when there are no suitable keys for
# eviction, Redis will return an error on write operations that require
# more memory. These are usually commands that create new keys, add data or
@@ -1824,8 +1853,20 @@ aof-timestamp-enabled no
# By enabling the 'cluster-slot-stats-enabled' config, the cluster will begin to capture advanced statistics.
# These statistics can be leveraged to assess general slot usage trends, identify hot / cold slots,
# migrate slots for a balanced cluster workload, and / or re-write application logic to better utilize slots.
# Note that per slot **memory** statistics are collected only if 'cluster-slot-stats-enabled' is enabled
# at startup, and later config changes don't affect it.
#
# The config accepts multiple values as a space-separated list:
# - cpu: Track CPU usage per slot (cpu-usec metric)
# - net: Track network bytes per slot (network-bytes-in, network-bytes-out metrics)
# - mem: Track memory usage per slot (memory-bytes metric)
# - yes: Enable all tracking (equivalent to "cpu net mem")
# - no: Disable all tracking (default)
#
# Example: cluster-slot-stats-enabled "cpu net"
#
# Note: Memory tracking (mem) can ONLY be enabled at startup. If you try to enable
# memory tracking via CONFIG SET when it wasn't enabled at startup, the command will
# fail. However, you can disable memory tracking at runtime by removing the 'mem' flag.
# Once disabled, memory tracking cannot be re-enabled without restarting the server.
#
# cluster-slot-stats-enabled no
@@ -1930,6 +1971,17 @@ slowlog-log-slower-than 10000
# You can reclaim memory used by the slow log with SLOWLOG RESET.
slowlog-max-len 128
# When a command is written to the slowlog we check how many arguments it has
# and if it has more than slowlog-entry-max-argc we trim the excess ones. The
# last of the non-trimmed arguments is overwritten with an info string about
# how many args were trimmed. That's why slowlog-entry-max-argc has minimum
# value of 2, so we can always preserve the command name.
# Moreover, each individual argument string is also trimmed depending on
# slowlog-entry-max-string-len. Default values:
#
# slowlog-entry-max-argc 32
# slowlog-entry-max-string-len 128
################################ LATENCY MONITOR ##############################
# The Redis latency monitoring subsystem samples different operations
@@ -1992,13 +2044,20 @@ latency-monitor-threshold 0
# e Evicted events (events generated when a key is evicted for maxmemory)
# n New key events (Note: not included in the 'A' class)
# t Stream commands
# a Array commands
# d Module key type events
# m Key-miss events (Note: It is not included in the 'A' class)
# o Overwritten events generated every time a key is overwritten.
# (Note: not included in the 'A' class)
# c Type-changed events generated every time a key's type changes
# (Note: not included in the 'A' class)
# A Alias for g$lshzxetd, so that the "AKE" string means all the events
# S Subkeyspace events, published with __subkeyspace@<db>__:<key> prefix.
# T Subkeyevent events, published with __subkeyevent@<db>__:<event> prefix.
# I Subkeyspaceitem events, published per subkey with
# __subkeyspaceitem@<db>__:<key>\n<subkey> prefix.
# V Subkeyspaceevent events, published with
# __subkeyspaceevent@<db>__:<event>|<key> prefix.
# A Alias for g$lshzxetad, so that the "AKE" string means all the events
# except key-miss, new key, overwritten and type-changed.
#
# The "notify-keyspace-events" takes as argument a string that is composed
@@ -2104,6 +2163,61 @@ hll-sparse-max-bytes 3000
stream-node-max-bytes 4096
stream-node-max-entries 100
# Redis Streams support Idempotent Message Producer (IDMP) tracking to prevent
# duplicate message delivery. When producers send messages with IDMP identifiers
# (using XADD with IDMP or IDMPAUTO parameters), Redis tracks these identifiers to
# detect and reject duplicates within a configurable time window.
#
# stream-idmp-duration: Specifies how long (in seconds) Redis should remember
# IDMP identifiers for duplicate detection. After this duration expires, old
# identifiers are automatically removed. This prevents unbounded memory growth
# while allowing reasonable duplicate detection windows.
# Valid range: 1 to 86400 seconds (1 second to 24 hours)
# Default: 100 seconds
#
# stream-idmp-maxsize: Maximum number of IDMP identifiers to track per producer
# per stream. Once this limit is reached, the oldest identifiers are evicted to
# make room for new ones. This caps memory usage for IDMP tracking.
# Valid range: 1 to 10000 entries
# Default: 100 entries
#
# Note: These are default values for new streams. Individual streams can override
# these settings using the XCFGSET command.
#
# stream-idmp-duration 100
# stream-idmp-maxsize 100
# Arrays use a sliced directory structure for O(1) access. The slice size
# controls the granularity of memory allocation - each slice covers a range
# of indices. Must be a power of two between 256 and 65536.
#
# Smaller slices (1024-2048): Better for sparse data with large gaps between
# indices, or many small arrays. Uses less memory per slice but more directory
# entries.
#
# Larger slices (8192-16384): Better for dense/contiguous data. Fewer directory
# entries but may waste memory if data is sparse within slices.
#
# Default 4096 works well for mixed workloads. If you change this setting via
# CONFIG SET, existing arrays retain their original slice size.
#
# IMPORTANT CONSIDERATION: Redis arrays, for slices with very few elements, are
# able to use a sparse representation, where the slice is not really
# materialized into an actual contiguous allocation. See the next configuration
# parameters for more information.
array-slice-size 4096
# Arrays start with sparse slices (sorted key-value pairs) for memory efficiency
# when elements are scattered. When a sparse slice exceeds array-sparse-kmax
# entries, it promotes to a dense slice (direct array). When a dense slice's
# element count drops below array-sparse-kmin and demotion would save memory,
# it demotes back to sparse. Set kmax to 0 to disable sparse encoding entirely.
# Set kmin to 0 if you never want dense slices to be demoted to sparse (useful
# when in your work load arrays reach an almost empty state to be filled again
# and so forth).
array-sparse-kmax 10
array-sparse-kmin 5
# Active rehashing uses 1 millisecond every 100 milliseconds of CPU time in
# order to help rehashing the main Redis hash table (the one mapping top-level
# keys to values). The hash table implementation Redis uses (see dict.c)
@@ -2418,3 +2532,12 @@ jemalloc-bg-thread yes
# to suppress
#
# ignore-warnings ARM64-COW-BUG
# When enabled, 'key-memory-histograms' adds per-type memory allocation histograms
# (distrib_*_sizes fields in bytes) to INFO keysizes output, complementing the
# existing size/item histograms with actual memory usage data.
#
# This setting must be enabled at startup; later config changes have no effect.
# It is also implicitly enabled when 'cluster-slot-stats-enabled' is set.
#
# key-memory-histograms no
+6 -1
View File
@@ -1,5 +1,10 @@
#!/bin/sh
TCL_VERSIONS="8.5 8.6 8.7"
# Raise open files limit (macOS default 256 is too low for tests).
OPEN_FILE_LIMIT=$(ulimit -n 2>/dev/null)
if [ -n "$OPEN_FILE_LIMIT" ] && [ "$OPEN_FILE_LIMIT" != "unlimited" ] && [ "$OPEN_FILE_LIMIT" -lt 1024 ]; then
ulimit -n 1024 2>/dev/null || true
fi
TCL_VERSIONS="8.5 8.6 8.7 9.0"
TCLSH=""
for VERSION in $TCL_VERSIONS; do
+6 -1
View File
@@ -1,5 +1,10 @@
#!/bin/sh
TCL_VERSIONS="8.5 8.6 8.7"
# Raise open files limit (macOS default 256 is too low for tests).
OPEN_FILE_LIMIT=$(ulimit -n 2>/dev/null)
if [ -n "$OPEN_FILE_LIMIT" ] && [ "$OPEN_FILE_LIMIT" != "unlimited" ] && [ "$OPEN_FILE_LIMIT" -lt 1024 ]; then
ulimit -n 1024 2>/dev/null || true
fi
TCL_VERSIONS="8.5 8.6 8.7 9.0"
TCLSH=""
for VERSION in $TCL_VERSIONS; do
+8 -1
View File
@@ -1,5 +1,10 @@
#!/bin/sh
TCL_VERSIONS="8.5 8.6 8.7"
# Raise open files limit (macOS default 256 is too low for tests).
OPEN_FILE_LIMIT=$(ulimit -n 2>/dev/null)
if [ -n "$OPEN_FILE_LIMIT" ] && [ "$OPEN_FILE_LIMIT" != "unlimited" ] && [ "$OPEN_FILE_LIMIT" -lt 1024 ]; then
ulimit -n 1024 2>/dev/null || true
fi
TCL_VERSIONS="8.5 8.6 8.7 9.0"
TCLSH=""
[ -z "$MAKE" ] && MAKE=make
@@ -59,4 +64,6 @@ $TCLSH tests/test_helper.tcl \
--single unit/moduleapi/crash \
--single unit/moduleapi/internalsecret \
--single unit/moduleapi/configaccess \
--single unit/moduleapi/keymeta \
--single unit/moduleapi/ksn_notify_side_effect \
"${@}"
+6 -1
View File
@@ -1,5 +1,10 @@
#!/bin/sh
TCL_VERSIONS="8.5 8.6 8.7"
# Raise open files limit (macOS default 256 is too low for tests).
OPEN_FILE_LIMIT=$(ulimit -n 2>/dev/null)
if [ -n "$OPEN_FILE_LIMIT" ] && [ "$OPEN_FILE_LIMIT" != "unlimited" ] && [ "$OPEN_FILE_LIMIT" -lt 1024 ]; then
ulimit -n 1024 2>/dev/null || true
fi
TCL_VERSIONS="8.5 8.6 8.7 9.0"
TCLSH=""
for VERSION in $TCL_VERSIONS; do
+10 -8
View File
@@ -25,17 +25,19 @@ CLANG := $(findstring clang,$(shell sh -c '$(CC) --version | head -1'))
# some automatic defaults are added to it. To specify optimization flags
# explicitly without any defaults added, pass the OPT variable instead.
OPTIMIZATION?=-O3
ENABLE_LTO?=
ifeq ($(OPTIMIZATION),-O3)
ifeq (clang,$(CLANG))
OPTIMIZATION+=-flto
ENABLE_LTO=-flto
else
OPTIMIZATION+=-flto=auto
ENABLE_LTO=-flto=auto -ffat-lto-objects
endif
OPTIMIZATION+=$(ENABLE_LTO)
endif
ifneq ($(OPTIMIZATION),-O0)
OPTIMIZATION+=-fno-omit-frame-pointer
endif
DEPENDENCY_TARGETS=hiredis linenoise lua hdr_histogram fpconv fast_float xxhash
DEPENDENCY_TARGETS=hiredis linenoise lua hdr_histogram fpconv xxhash tre
NODEPS:=clean distclean
# Default settings
@@ -149,7 +151,7 @@ endif
FINAL_CFLAGS=$(STD) $(WARN) $(OPT) $(DEBUG) $(CFLAGS) $(REDIS_CFLAGS)
FINAL_LDFLAGS=$(LDFLAGS) $(OPT) $(REDIS_LDFLAGS) $(DEBUG)
FINAL_LIBS=-lm -lstdc++
FINAL_LIBS=-lm
DEBUG=-g -ggdb
# Linux ARM32 needs -latomic at linking time
@@ -257,7 +259,7 @@ ifdef OPENSSL_PREFIX
endif
# Include paths to dependencies
FINAL_CFLAGS+= -I../deps/hiredis -I../deps/linenoise -I../deps/lua/src -I../deps/hdr_histogram -I../deps/fpconv -I../deps/fast_float -I../deps/xxhash
FINAL_CFLAGS+= -I../deps/hiredis -I../deps/linenoise -I../deps/lua/src -I../deps/hdr_histogram -I../deps/fpconv -I../deps/xxhash
# Determine systemd support and/or build preference (defaulting to auto-detection)
BUILD_WITH_SYSTEMD=no
@@ -382,7 +384,7 @@ endif
REDIS_SERVER_NAME=redis-server$(PROG_SUFFIX)
REDIS_SENTINEL_NAME=redis-sentinel$(PROG_SUFFIX)
REDIS_SERVER_OBJ=threads_mngr.o memory_prefetch.o adlist.o quicklist.o ae.o anet.o dict.o ebuckets.o eventnotifier.o iothread.o mstr.o kvstore.o fwtree.o estore.o server.o sds.o zmalloc.o lzf_c.o lzf_d.o pqsort.o zipmap.o sha1.o ziplist.o release.o networking.o util.o object.o db.o replication.o rdb.o t_string.o t_list.o t_set.o t_zset.o t_hash.o config.o aof.o pubsub.o multi.o debug.o sort.o intset.o syncio.o cluster.o cluster_asm.o cluster_legacy.o cluster_slot_stats.o crc16.o endianconv.o slowlog.o eval.o bio.o rio.o rand.o memtest.o syscheck.o crcspeed.o crccombine.o crc64.o bitops.o sentinel.o notify.o setproctitle.o blocked.o hyperloglog.o latency.o sparkline.o redis-check-rdb.o redis-check-aof.o geo.o lazyfree.o module.o evict.o expire.o geohash.o geohash_helper.o childinfo.o defrag.o siphash.o rax.o t_stream.o listpack.o localtime.o lolwut.o lolwut5.o lolwut6.o lolwut8.o acl.o tracking.o socket.o tls.o sha256.o timeout.o setcpuaffinity.o monotonic.o mt19937-64.o resp_parser.o call_reply.o script_lua.o script.o functions.o function_lua.o commands.o strl.o connection.o unix.o logreqres.o
REDIS_SERVER_OBJ=threads_mngr.o memory_prefetch.o adlist.o quicklist.o ae.o anet.o dict.o ebuckets.o eventnotifier.o iothread.o mstr.o entry.o kvstore.o fwtree.o estore.o server.o sds.o zmalloc.o lzf_c.o lzf_d.o pqsort.o zipmap.o sha1.o ziplist.o release.o networking.o util.o object.o db.o replication.o rdb.o t_string.o t_list.o t_set.o t_zset.o t_hash.o t_array.o sparsearray.o config.o aof.o pubsub.o multi.o debug.o sort.o intset.o syncio.o cluster.o cluster_asm.o cluster_legacy.o cluster_slot_stats.o crc16.o endianconv.o slowlog.o eval.o bio.o rio.o rand.o memtest.o syscheck.o crcspeed.o crccombine.o crc64.o bitops.o sentinel.o notify.o setproctitle.o blocked.o hyperloglog.o latency.o sparkline.o redis-check-rdb.o redis-check-aof.o geo.o lazyfree.o module.o evict.o expire.o geohash.o geohash_helper.o childinfo.o defrag.o siphash.o rax.o t_stream.o listpack.o localtime.o lolwut.o lolwut5.o lolwut6.o lolwut8.o acl.o tracking.o socket.o tls.o sha256.o timeout.o setcpuaffinity.o monotonic.o mt19937-64.o resp_parser.o call_reply.o script_lua.o script.o functions.o function_lua.o commands.o strl.o connection.o unix.o logreqres.o keymeta.o chk.o hotkeys.o gcra.o vector.o fast_float_strtod.o
REDIS_CLI_NAME=redis-cli$(PROG_SUFFIX)
REDIS_CLI_OBJ=anet.o adlist.o dict.o redis-cli.o zmalloc.o release.o ae.o redisassert.o crcspeed.o crccombine.o crc64.o siphash.o crc16.o monotonic.o cli_common.o mt19937-64.o strl.o cli_commands.o
REDIS_BENCHMARK_NAME=redis-benchmark$(PROG_SUFFIX)
@@ -423,7 +425,7 @@ persist-settings: distclean
echo REDIS_LDFLAGS=$(REDIS_LDFLAGS) >> .make-settings
echo PREV_FINAL_CFLAGS=$(FINAL_CFLAGS) >> .make-settings
echo PREV_FINAL_LDFLAGS=$(FINAL_LDFLAGS) >> .make-settings
-(cd ../deps && $(MAKE) $(DEPENDENCY_TARGETS))
-(cd ../deps && $(MAKE) $(DEPENDENCY_TARGETS) ENABLE_LTO="$(ENABLE_LTO)")
.PHONY: persist-settings
@@ -442,7 +444,7 @@ endif
# redis-server
$(REDIS_SERVER_NAME): $(REDIS_SERVER_OBJ) $(REDIS_VEC_SETS_OBJ)
$(REDIS_LD) -o $@ $^ ../deps/hiredis/libhiredis.a ../deps/lua/src/liblua.a ../deps/hdr_histogram/libhdrhistogram.a ../deps/fpconv/libfpconv.a ../deps/fast_float/libfast_float.a ../deps/xxhash/libxxhash.a $(FINAL_LIBS)
$(REDIS_LD) -o $@ $^ ../deps/hiredis/libhiredis.a ../deps/lua/src/liblua.a ../deps/hdr_histogram/libhdrhistogram.a ../deps/fpconv/libfpconv.a ../deps/xxhash/libxxhash.a ../deps/tre/libtre.a $(FINAL_LIBS)
# redis-sentinel
$(REDIS_SENTINEL_NAME): $(REDIS_SERVER_NAME)
+31 -12
View File
@@ -2,6 +2,9 @@
* Copyright (c) 2018-Present, Redis Ltd.
* All rights reserved.
*
* Copyright (c) 2024-present, Valkey contributors.
* All rights reserved.
*
* Licensed under your choice of (a) the Redis Source Available License 2.0
* (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the
* GNU Affero General Public License v3 (AGPLv3).
@@ -54,6 +57,7 @@ struct ACLCategoryItem {
{"list", ACL_CATEGORY_LIST},
{"hash", ACL_CATEGORY_HASH},
{"string", ACL_CATEGORY_STRING},
{"array", ACL_CATEGORY_ARRAY},
{"bitmap", ACL_CATEGORY_BITMAP},
{"hyperloglog", ACL_CATEGORY_HYPERLOGLOG},
{"geo", ACL_CATEGORY_GEO},
@@ -67,6 +71,9 @@ struct ACLCategoryItem {
{"connection", ACL_CATEGORY_CONNECTION},
{"transaction", ACL_CATEGORY_TRANSACTION},
{"scripting", ACL_CATEGORY_SCRIPTING},
#ifdef ENABLE_GCRA
{"ratelimit", ACL_CATEGORY_RATE_LIMIT},
#endif
{NULL,0} /* Terminator. */
};
@@ -802,7 +809,7 @@ sds ACLDescribeSelectorCommandRules(aclSelector *selector) {
{
serverLog(LL_WARNING,
"CRITICAL ERROR: User ACLs don't match final bitmap: '%s'",
rules);
redactLogCstr(rules));
serverPanic("No bitmap match in ACLDescribeSelectorCommandRules()");
}
ACLFreeSelector(fake_selector);
@@ -1181,7 +1188,7 @@ int ACLSetSelector(aclSelector *selector, const char* op, size_t oplen) {
/* Add the first-arg to the list of valid ones. */
serverLog(LL_WARNING, "Deprecation warning: Allowing a first arg of an otherwise "
"blocked command is a misuse of ACL and may get disabled "
"in the future (offender: +%s)", op+1);
"in the future (offender: +%s)", redactLogCstr(op+1));
ACLAddAllowedFirstArg(selector,cmd->id,sub);
}
ACLUpdateCommandRules(selector,op+1,1);
@@ -1854,6 +1861,15 @@ int ACLCheckAllUserCommandPerm(user *u, struct redisCommand *cmd, robj **argv, i
/* If there is no associated user, the connection can run anything. */
if (u == NULL) return ACL_OK;
/* Quick check if the user has all permissions, return early if so. */
if (likely(listFirst(u->selectors) != NULL)) {
aclSelector *s = listNodeValue(listFirst(u->selectors));
const uint32_t all_perms = SELECTOR_FLAG_ALLCOMMANDS |
SELECTOR_FLAG_ALLKEYS |
SELECTOR_FLAG_ALLCHANNELS;
if ((s->flags & all_perms) == all_perms) return ACL_OK;
}
/* We have to pick a single error to log, the logic for picking is as follows:
* 1) If no selector can execute the command, return the command.
* 2) Return the last key or channel that no selector could match. */
@@ -2247,7 +2263,7 @@ int ACLLoadConfiguredUsers(void) {
const char *errmsg = ACLSetUserStringError();
serverLog(LL_WARNING,"Error loading ACL rule '%s' for "
"the user named '%s': %s",
aclrules[j],aclrules[0],errmsg);
redactLogCstr(aclrules[j]),redactLogCstr(aclrules[0]),errmsg);
return C_ERR;
}
}
@@ -2258,22 +2274,22 @@ int ACLLoadConfiguredUsers(void) {
serverLog(LL_NOTICE, "The user '%s' is disabled (there is no "
"'on' modifier in the user description). Make "
"sure this is not a configuration error.",
aclrules[0]);
redactLogCstr(aclrules[0]));
}
}
return C_OK;
}
/* This function loads the ACL from the specified filename: every line
* is validated and should be either empty or in the format used to specify
* users in the redis.conf configuration or in the ACL file, that is:
* is validated and should be either empty, a comment, or in the format
* used to specify users in the redis.conf configuration or in the ACL file,
* that is:
*
* user <username> ... rules ...
*
* Note that this function considers comments starting with '#' as errors
* because the ACL file is meant to be rewritten, and comments would be
* lost after the rewrite. Yet empty lines are allowed to avoid being too
* strict.
* Lines starting with '#' are treated as comments and ignored. Note that
* comments will be lost after ACL SAVE rewrites the file. Empty lines are
* also allowed.
*
* One important part of implementing ACL LOAD, that uses this function, is
* to avoid ending with broken rules if the ACL file is invalid for some
@@ -2325,8 +2341,8 @@ sds ACLLoadFromFile(const char *filename) {
lines[i] = sdstrim(lines[i]," \t\r\n");
/* Skip blank lines */
if (lines[i][0] == '\0') continue;
/* Skip blank lines and comments */
if (lines[i][0] == '\0' || lines[i][0] == '#') continue;
/* Split into arguments */
argv = sdssplitlen(lines[i],sdslen(lines[i])," ",1,&argc);
@@ -2647,6 +2663,8 @@ void ACLUpdateInfoMetrics(int reason){
server.acl_info.invalid_key_accesses++;
} else if (reason == ACL_DENIED_CHANNEL) {
server.acl_info.invalid_channel_accesses++;
} else if (reason == ACL_INVALID_TLS_CERT_AUTH) {
server.acl_info.acl_access_denied_tls_cert++;
} else {
serverPanic("Unknown ACL_DENIED encoding");
}
@@ -3091,6 +3109,7 @@ void aclCommand(client *c) {
case ACL_DENIED_KEY: reasonstr="key"; break;
case ACL_DENIED_CHANNEL: reasonstr="channel"; break;
case ACL_DENIED_AUTH: reasonstr="auth"; break;
case ACL_INVALID_TLS_CERT_AUTH: reasonstr = "tls-cert"; break;
default: reasonstr="unknown";
}
addReplyBulkCString(c,reasonstr);
+1 -10
View File
@@ -196,22 +196,13 @@ void listUnlinkNode(list *list, listNode *node) {
* call to listNext() will return the next element of the list.
*
* This function can't fail. */
listIter *listGetIterator(list *list, int direction)
void listInitIterator(listIter *iter, list *list, int direction)
{
listIter *iter;
if ((iter = zmalloc(sizeof(*iter))) == NULL) return NULL;
if (direction == AL_START_HEAD)
iter->next = list->head;
else
iter->next = list->tail;
iter->direction = direction;
return iter;
}
/* Release the iterator memory */
void listReleaseIterator(listIter *iter) {
zfree(iter);
}
/* Create an iterator in the list private iterator structure */
+1 -2
View File
@@ -58,9 +58,8 @@ list *listAddNodeHead(list *list, void *value);
list *listAddNodeTail(list *list, void *value);
list *listInsertNode(list *list, listNode *old_node, void *value, int after);
void listDelNode(list *list, listNode *node);
listIter *listGetIterator(list *list, int direction);
void listInitIterator(listIter *iter, list *list, int direction);
listNode *listNext(listIter *iter);
void listReleaseIterator(listIter *iter);
list *listDup(list *orig);
listNode *listSearchKey(list *list, void *key);
listNode *listIndex(list *list, long index);
+12 -19
View File
@@ -101,31 +101,24 @@ static void aeApiFree(aeEventLoop *eventLoop) {
static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) {
aeApiState *state = eventLoop->apidata;
struct kevent ke;
struct kevent evs[2];
int nch = 0;
if (mask & AE_READABLE) {
EV_SET(&ke, fd, EVFILT_READ, EV_ADD, 0, 0, NULL);
if (kevent(state->kqfd, &ke, 1, NULL, 0, NULL) == -1) return -1;
}
if (mask & AE_WRITABLE) {
EV_SET(&ke, fd, EVFILT_WRITE, EV_ADD, 0, 0, NULL);
if (kevent(state->kqfd, &ke, 1, NULL, 0, NULL) == -1) return -1;
}
return 0;
if (mask & AE_READABLE) EV_SET(evs + nch++, fd, EVFILT_READ, EV_ADD, 0, 0, NULL);
if (mask & AE_WRITABLE) EV_SET(evs + nch++, fd, EVFILT_WRITE, EV_ADD, 0, 0, NULL);
return kevent(state->kqfd, evs, nch, NULL, 0, NULL);
}
static void aeApiDelEvent(aeEventLoop *eventLoop, int fd, int mask) {
aeApiState *state = eventLoop->apidata;
struct kevent ke;
struct kevent evs[2];
int nch = 0;
if (mask & AE_READABLE) {
EV_SET(&ke, fd, EVFILT_READ, EV_DELETE, 0, 0, NULL);
kevent(state->kqfd, &ke, 1, NULL, 0, NULL);
}
if (mask & AE_WRITABLE) {
EV_SET(&ke, fd, EVFILT_WRITE, EV_DELETE, 0, 0, NULL);
kevent(state->kqfd, &ke, 1, NULL, 0, NULL);
}
if (mask & AE_READABLE) EV_SET(evs + nch++, fd, EVFILT_READ, EV_DELETE, 0, 0, NULL);
if (mask & AE_WRITABLE) EV_SET(evs + nch++, fd, EVFILT_WRITE, EV_DELETE, 0, 0, NULL);
kevent(state->kqfd, evs, nch, NULL, 0, NULL);
}
static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
+3 -4
View File
@@ -174,6 +174,7 @@ int anetKeepAlive(char *err, int fd, int interval)
}
intvl = idle/3;
if (intvl < 10) intvl = 10; /* kernel expects at least 10 seconds */
if (setsockopt(fd, IPPROTO_TCP, TCP_KEEPINTVL, &intvl, sizeof(intvl))) {
anetSetError(err, "setsockopt TCP_KEEPINTVL: %s\n", strerror(errno));
return ANET_ERR;
@@ -196,9 +197,7 @@ int anetKeepAlive(char *err, int fd, int interval)
/* Note that the consequent probes will not be sent at equal intervals on Solaris,
* but will be sent using the exponential backoff algorithm. */
intvl = idle/3;
cnt = 3;
int time_to_abort = intvl * cnt;
int time_to_abort = idle;
if (setsockopt(fd, IPPROTO_TCP, TCP_KEEPALIVE_ABORT_THRESHOLD, &time_to_abort, sizeof(time_to_abort))) {
anetSetError(err, "setsockopt TCP_KEEPCNT: %s\n", strerror(errno));
return ANET_ERR;
@@ -716,7 +715,7 @@ error:
* and one of the use cases is O_CLOEXEC|O_NONBLOCK. */
int anetPipe(int fds[2], int read_flags, int write_flags) {
int pipe_flags = 0;
#if defined(__linux__) || defined(__FreeBSD__)
#ifdef HAVE_PIPE2
/* When possible, try to leverage pipe2() to apply flags that are common to both ends.
* There is no harm to set O_CLOEXEC to prevent fd leaks. */
pipe_flags = O_CLOEXEC | (read_flags & write_flags);
+314 -50
View File
@@ -1920,9 +1920,10 @@ int rioWriteBulkObject(rio *r, robj *obj) {
int rewriteListObject(rio *r, robj *key, robj *o) {
long long count = 0, items = listTypeLength(o);
listTypeIterator *li = listTypeInitIterator(o,0,LIST_TAIL);
listTypeIterator li;
listTypeEntry entry;
while (listTypeNext(li,&entry)) {
listTypeInitIterator(&li, o, 0, LIST_TAIL);
while (listTypeNext(&li, &entry)) {
if (count == 0) {
int cmd_items = (items > AOF_REWRITE_ITEMS_PER_CMD) ?
AOF_REWRITE_ITEMS_PER_CMD : items;
@@ -1930,7 +1931,7 @@ int rewriteListObject(rio *r, robj *key, robj *o) {
!rioWriteBulkString(r,"RPUSH",5) ||
!rioWriteBulkObject(r,key))
{
listTypeReleaseIterator(li);
listTypeResetIterator(&li);
return 0;
}
}
@@ -1941,19 +1942,19 @@ int rewriteListObject(rio *r, robj *key, robj *o) {
vstr = listTypeGetValue(&entry,&vlen,&lval);
if (vstr) {
if (!rioWriteBulkString(r,(char*)vstr,vlen)) {
listTypeReleaseIterator(li);
listTypeResetIterator(&li);
return 0;
}
} else {
if (!rioWriteBulkLongLong(r,lval)) {
listTypeReleaseIterator(li);
listTypeResetIterator(&li);
return 0;
}
}
if (++count == AOF_REWRITE_ITEMS_PER_CMD) count = 0;
items--;
}
listTypeReleaseIterator(li);
listTypeResetIterator(&li);
return 1;
}
@@ -1961,11 +1962,12 @@ int rewriteListObject(rio *r, robj *key, robj *o) {
* The function returns 0 on error, 1 on success. */
int rewriteSetObject(rio *r, robj *key, robj *o) {
long long count = 0, items = setTypeSize(o);
setTypeIterator *si = setTypeInitIterator(o);
setTypeIterator si;
char *str;
size_t len;
int64_t llval;
while (setTypeNext(si, &str, &len, &llval) != -1) {
setTypeInitIterator(&si, o);
while (setTypeNext(&si, &str, &len, &llval) != -1) {
if (count == 0) {
int cmd_items = (items > AOF_REWRITE_ITEMS_PER_CMD) ?
AOF_REWRITE_ITEMS_PER_CMD : items;
@@ -1973,20 +1975,20 @@ int rewriteSetObject(rio *r, robj *key, robj *o) {
!rioWriteBulkString(r,"SADD",4) ||
!rioWriteBulkObject(r,key))
{
setTypeReleaseIterator(si);
setTypeResetIterator(&si);
return 0;
}
}
size_t written = str ?
rioWriteBulkString(r, str, len) : rioWriteBulkLongLong(r, llval);
if (!written) {
setTypeReleaseIterator(si);
setTypeResetIterator(&si);
return 0;
}
if (++count == AOF_REWRITE_ITEMS_PER_CMD) count = 0;
items--;
}
setTypeReleaseIterator(si);
setTypeResetIterator(&si);
return 1;
}
@@ -2040,8 +2042,9 @@ int rewriteSortedSetObject(rio *r, robj *key, robj *o) {
dictInitIterator(&di, zs->dict);
while((de = dictNext(&di)) != NULL) {
sds ele = dictGetKey(de);
double *score = dictGetVal(de);
zskiplistNode *znode = dictGetKey(de);
sds ele = zslGetNodeElement(znode);
double score = znode->score;
if (count == 0) {
int cmd_items = (items > AOF_REWRITE_ITEMS_PER_CMD) ?
@@ -2055,7 +2058,7 @@ int rewriteSortedSetObject(rio *r, robj *key, robj *o) {
return 0;
}
}
if (!rioWriteBulkDouble(r,*score) ||
if (!rioWriteBulkDouble(r,score) ||
!rioWriteBulkString(r,ele,sdslen(ele)))
{
dictResetIterator(&di);
@@ -2104,14 +2107,14 @@ static int rioWriteHashIteratorCursor(rio *r, hashTypeIterator *hi, int what) {
int rewriteHashObject(rio *r, robj *key, robj *o) {
int res = 0; /*fail*/
hashTypeIterator *hi;
hashTypeIterator hi;
long long count = 0, items = hashTypeLength(o, 0);
int isHFE = hashTypeGetMinExpire(o, 0) != EB_EXPIRE_TIME_INVALID;
hi = hashTypeInitIterator(o);
hashTypeInitIterator(&hi, o);
if (!isHFE) {
while (hashTypeNext(hi, 0) != C_ERR) {
while (hashTypeNext(&hi, 0) != C_ERR) {
if (count == 0) {
int cmd_items = (items > AOF_REWRITE_ITEMS_PER_CMD) ?
AOF_REWRITE_ITEMS_PER_CMD : items;
@@ -2121,31 +2124,31 @@ int rewriteHashObject(rio *r, robj *key, robj *o) {
goto reHashEnd;
}
if (!rioWriteHashIteratorCursor(r, hi, OBJ_HASH_KEY) ||
!rioWriteHashIteratorCursor(r, hi, OBJ_HASH_VALUE))
if (!rioWriteHashIteratorCursor(r, &hi, OBJ_HASH_KEY) ||
!rioWriteHashIteratorCursor(r, &hi, OBJ_HASH_VALUE))
goto reHashEnd;
if (++count == AOF_REWRITE_ITEMS_PER_CMD) count = 0;
items--;
}
} else {
while (hashTypeNext(hi, 0) != C_ERR) {
while (hashTypeNext(&hi, 0) != C_ERR) {
char hmsetCmd[] = "*4\r\n$5\r\nHMSET\r\n";
if ( (!rioWrite(r, hmsetCmd, sizeof(hmsetCmd) - 1)) ||
(!rioWriteBulkObject(r, key)) ||
(!rioWriteHashIteratorCursor(r, hi, OBJ_HASH_KEY)) ||
(!rioWriteHashIteratorCursor(r, hi, OBJ_HASH_VALUE)) )
(!rioWriteHashIteratorCursor(r, &hi, OBJ_HASH_KEY)) ||
(!rioWriteHashIteratorCursor(r, &hi, OBJ_HASH_VALUE)) )
goto reHashEnd;
if (hi->expire_time != EB_EXPIRE_TIME_INVALID) {
if (hi.expire_time != EB_EXPIRE_TIME_INVALID) {
char cmd[] = "*6\r\n$10\r\nHPEXPIREAT\r\n";
if ( (!rioWrite(r, cmd, sizeof(cmd) - 1)) ||
(!rioWriteBulkObject(r, key)) ||
(!rioWriteBulkLongLong(r, hi->expire_time)) ||
(!rioWriteBulkLongLong(r, hi.expire_time)) ||
(!rioWriteBulkString(r, "FIELDS", 6)) ||
(!rioWriteBulkString(r, "1", 1)) ||
(!rioWriteHashIteratorCursor(r, hi, OBJ_HASH_KEY)) )
(!rioWriteHashIteratorCursor(r, &hi, OBJ_HASH_KEY)) )
goto reHashEnd;
}
}
@@ -2154,7 +2157,7 @@ int rewriteHashObject(rio *r, robj *key, robj *o) {
res = 1; /* success */
reHashEnd:
hashTypeReleaseIterator(hi);
hashTypeResetIterator(&hi);
return res;
}
@@ -2194,6 +2197,35 @@ int rioWriteStreamPendingEntry(rio *r, robj *key, const char *groupname, size_t
return 1;
}
/* Helper for rewriteStreamObject(): emit a single XNACK FORCE command that
* reconstructs one or more NACKed (unowned) PEL entries sharing the same
* delivery_count. `ids` points to an array of `count` streamIDs (at most
* AOF_REWRITE_ITEMS_PER_CMD). Returns 0 on error, 1 on success. */
int rioWriteStreamNackedEntries(rio *r, robj *key, const char *groupname,
size_t groupname_len, streamID *ids,
int count, uint64_t delivery_count) {
serverAssert(count > 0 && count <= AOF_REWRITE_ITEMS_PER_CMD);
/* XNACK <key> <group> FAIL IDS <n> <id..> RETRYCOUNT <cnt> FORCE
* 6 fixed tokens before IDs + count IDs + 3 fixed tokens after. */
if (rioWriteBulkCount(r,'*',6+count+3) == 0) return 0;
if (rioWriteBulkString(r,"XNACK",5) == 0) return 0;
if (rioWriteBulkObject(r,key) == 0) return 0;
if (rioWriteBulkString(r,groupname,groupname_len) == 0) return 0;
if (rioWriteBulkString(r,"FAIL",4) == 0) return 0;
if (rioWriteBulkString(r,"IDS",3) == 0) return 0;
if (rioWriteBulkLongLong(r,count) == 0) return 0;
for (int i = 0; i < count; i++) {
if (rioWriteBulkStreamID(r,&ids[i]) == 0) return 0;
}
if (rioWriteBulkString(r,"RETRYCOUNT",10) == 0) return 0;
if (rioWriteBulkLongLong(r,delivery_count) == 0) return 0;
if (rioWriteBulkString(r,"FORCE",5) == 0) return 0;
return 1;
}
/* Helper for rewriteStreamObject(): emit the XGROUP CREATECONSUMER is
* needed in order to create consumers that do not have any pending entries.
* All this in the context of the specified key and group. */
@@ -2208,17 +2240,31 @@ int rioWriteStreamEmptyConsumer(rio *r, robj *key, const char *groupname, size_t
return 1;
}
/* Helper for rewriteStreamObject(): emit the XIDMPRECORD needed to
* restore an IDMP entry for the given producer in the context of the
* specified key. */
int rioWriteStreamIdmpEntry(rio *r, robj *key, const char *pid, size_t pid_len, idmpEntry *entry) {
/* XIDMPRECORD <key> <pid> <iid> <streamID> */
if (rioWriteBulkCount(r,'*',5) == 0) return 0;
if (rioWriteBulkString(r,"XIDMPRECORD",11) == 0) return 0;
if (rioWriteBulkObject(r,key) == 0) return 0;
if (rioWriteBulkString(r,pid,pid_len) == 0) return 0;
if (rioWriteBulkString(r,entry->iid,entry->iid_len) == 0) return 0;
if (rioWriteBulkStreamID(r,&entry->id) == 0) return 0;
return 1;
}
/* Emit the commands needed to rebuild a stream object.
* The function returns 0 on error, 1 on success. */
int rewriteStreamObject(rio *r, robj *key, robj *o) {
stream *s = o->ptr;
streamIterator si;
streamIteratorStart(&si,s,NULL,NULL,0);
streamID id;
int64_t numfields;
if (s->length) {
/* Reconstruct the stream data using XADD commands. */
streamIterator si;
int64_t numfields;
streamIteratorStart(&si,s,NULL,NULL,0);
while(streamIteratorGetID(&si,&id,&numfields)) {
/* Emit a two elements array for each item. The first is
* the ID, the second is an array of field-value pairs. */
@@ -2244,6 +2290,7 @@ int rewriteStreamObject(rio *r, robj *key, robj *o) {
}
}
}
streamIteratorStop(&si);
} else {
/* Use the XADD MAXLEN 0 trick to generate an empty stream if
* the key we are serializing is an empty string, which is possible
@@ -2258,7 +2305,6 @@ int rewriteStreamObject(rio *r, robj *key, robj *o) {
!rioWriteBulkString(r,"x",1) ||
!rioWriteBulkString(r,"y",1))
{
streamIteratorStop(&si);
return 0;
}
}
@@ -2274,11 +2320,9 @@ int rewriteStreamObject(rio *r, robj *key, robj *o) {
!rioWriteBulkString(r,"MAXDELETEDID",12) ||
!rioWriteBulkStreamID(r,&s->max_deleted_entry_id))
{
streamIteratorStop(&si);
return 0;
}
/* Create all the stream consumer groups. */
if (s->cgroups) {
raxIterator ri;
@@ -2297,7 +2341,6 @@ int rewriteStreamObject(rio *r, robj *key, robj *o) {
!rioWriteBulkLongLong(r,group->entries_read))
{
raxStop(&ri);
streamIteratorStop(&si);
return 0;
}
@@ -2316,7 +2359,6 @@ int rewriteStreamObject(rio *r, robj *key, robj *o) {
{
raxStop(&ri_cons);
raxStop(&ri);
streamIteratorStop(&si);
return 0;
}
continue;
@@ -2335,21 +2377,110 @@ int rewriteStreamObject(rio *r, robj *key, robj *o) {
raxStop(&ri_pel);
raxStop(&ri_cons);
raxStop(&ri);
streamIteratorStop(&si);
return 0;
}
}
raxStop(&ri_pel);
}
raxStop(&ri_cons);
/* Emit XNACK FORCE for NACKed (unowned) entries from the
* NACK zone of the PEL time-ordered list
* (pel_time_head..pel_nack_tail). Consecutive entries with
* the same delivery_count are batched into a single command.
*
* nack_stop is the first node outside the NACK zone (or NULL
* when the zone extends to the end of the PEL). When
* pel_nack_tail is NULL (no NACKed entries) the guard below
* skips the whole block. */
streamNACK *nack_end = group->pel_nack_tail;
if (nack_end != NULL) {
streamID batch_ids[AOF_REWRITE_ITEMS_PER_CMD];
streamNACK *nack_stop = nack_end->pel_next;
streamNACK *nack = group->pel_time_head;
int batch_count = 0;
uint64_t batch_dc = 0;
while (nack && nack != nack_stop) {
if (batch_count == 0) batch_dc = nack->delivery_count;
batch_ids[batch_count++] = nack->id;
streamNACK *next = nack->pel_next;
if (batch_count >= AOF_REWRITE_ITEMS_PER_CMD ||
!next || next == nack_stop ||
next->delivery_count != batch_dc)
{
if (rioWriteStreamNackedEntries(r,key,(char*)ri.key,
ri.key_len,batch_ids,
batch_count,batch_dc) == 0)
{
raxStop(&ri);
return 0;
}
batch_count = 0;
}
nack = next;
}
}
}
raxStop(&ri);
}
streamIteratorStop(&si);
/* Emit XCFGSET to restore per-stream IDMP configuration if it differs
* from the server defaults, so that AOF rewrite preserves custom settings. */
if (s->idmp_duration != (uint64_t)server.stream_idmp_duration ||
s->idmp_max_entries != (uint64_t)server.stream_idmp_maxsize)
{
if (!rioWriteBulkCount(r,'*',6) ||
!rioWriteBulkString(r,"XCFGSET",7) ||
!rioWriteBulkObject(r,key) ||
!rioWriteBulkString(r,"IDMP-DURATION",13) ||
!rioWriteBulkLongLong(r,s->idmp_duration) ||
!rioWriteBulkString(r,"IDMP-MAXSIZE",12) ||
!rioWriteBulkLongLong(r,s->idmp_max_entries))
{
return 0;
}
}
/* Emit XIDMPRECORD for each IDMP entry. Entries whose stream ID no
* longer exists (removed by XDEL/trim) are skipped, since
* xidmprecordCommand() rejects references to missing IDs and would
* cause AOF replay errors. */
if (s->idmp_producers) {
raxIterator ri_idmp;
raxStart(&ri_idmp,s->idmp_producers);
raxSeek(&ri_idmp,"^",NULL,0);
while(raxNext(&ri_idmp)) {
idmpProducer *producer = ri_idmp.data;
for (idmpEntry *entry = producer->idmp_head; entry != NULL; entry = entry->next) {
if (!streamEntryExists(s, &entry->id)) continue;
if (rioWriteStreamIdmpEntry(r,key,(char*)ri_idmp.key,
ri_idmp.key_len,entry) == 0)
{
raxStop(&ri_idmp);
return 0;
}
}
}
raxStop(&ri_idmp);
}
return 1;
}
#ifdef ENABLE_GCRA
int rewriteGCRAObject(rio *r, robj *key, robj *o) {
long long val;
getLongLongFromGCRAObject(o, &val);
/* GCRASETVALUE <key> <tat> */
if (rioWriteBulkCount(r,'*',3) == 0) return 0;
if (rioWriteBulkString(r,"GCRASETVALUE",12) == 0) return 0;
if (rioWriteBulkObject(r,key) == 0) return 0;
if (rioWriteBulkLongLong(r,val) == 0) return 0;
return 1;
}
#endif
/* Call the module type callback in order to rewrite a data type
* that is exported by a module and is not handled by Redis itself.
* The function returns 0 on error, 1 on success. */
@@ -2357,7 +2488,7 @@ int rewriteModuleObject(rio *r, robj *key, robj *o, int dbid) {
RedisModuleIO io;
moduleValue *mv = o->ptr;
moduleType *mt = mv->type;
moduleInitIOContext(io,mt,r,key,dbid);
moduleInitIOContext(&io, &mt->entity, r, key, dbid);
mt->aof_rewrite(&io,key,mv->value);
if (io.ctx) {
moduleFreeContext(io.ctx);
@@ -2386,6 +2517,116 @@ werr:
return 0;
}
/* Write unsigned 64-bit integer as bulk string.
* Unlike rioWriteBulkLongLong which uses signed representation,
* this correctly handles values >= 2^63 (e.g., array indices). */
static int rioWriteBulkUnsignedLongLong(rio *r, uint64_t value) {
char buf[24];
int len = ull2string(buf, sizeof(buf), value);
return rioWriteBulkString(r, buf, len);
}
/* Helper to emit a single array element for AOF rewrite.
* Returns 0 on error, 1 on success. Updates count and items. */
static int aofEmitArrayElement(rio *r, robj *key, uint64_t idx, void *v,
long long *count, long long *items) {
if (*count == 0) {
int cmd_items = (*items > AOF_REWRITE_ITEMS_PER_CMD/2) ?
AOF_REWRITE_ITEMS_PER_CMD/2 : *items; /* pairs of idx+val */
if (!rioWriteBulkCount(r,'*',2+cmd_items*2) ||
!rioWriteBulkString(r,"ARMSET",6) ||
!rioWriteBulkObject(r,key))
{
return 0;
}
}
/* Write index (unsigned to handle indices >= 2^63) */
if (!rioWriteBulkUnsignedLongLong(r, idx)) return 0;
/* Write value - inline types use scratch space, arString aliases directly. */
char buf[AR_INLINE_BUFSIZE];
size_t len;
const char *data = arDecode(v, buf, sizeof(buf), &len);
if (!rioWriteBulkString(r, data, len)) return 0;
if (++(*count) == AOF_REWRITE_ITEMS_PER_CMD/2) *count = 0;
(*items)--;
return 1;
}
/* Helper to emit all elements from a slice for AOF rewrite. */
static int aofEmitSliceElements(rio *r, robj *key, arSlice *s, uint64_t slice_id,
uint32_t slice_size, long long *count, long long *items) {
if (s->encoding == AR_SLICE_DENSE) {
for (uint32_t i = 0; i < s->layout.dense.winsize; i++) {
void *v = s->layout.dense.items[i];
if (arIsEmpty(v)) continue;
uint64_t idx = arMakeIdx(slice_id, s->layout.dense.offset + i, slice_size);
if (!aofEmitArrayElement(r, key, idx, v, count, items)) return 0;
}
} else {
/* Sparse slice */
uint16_t *offsets = s->layout.sparse.offsets;
void **values = s->layout.sparse.values;
for (uint32_t i = 0; i < s->count; i++) {
uint64_t idx = arMakeIdx(slice_id, offsets[i], slice_size);
if (!aofEmitArrayElement(r, key, idx, values[i], count, items)) return 0;
}
}
return 1;
}
/* Emit the commands needed to rebuild an array object.
* The function returns 0 on error, 1 on success. */
int rewriteArrayObject(rio *r, robj *key, robj *o) {
redisArray *ar = o->ptr;
long long count = 0, items = ar->count;
if (items == 0) return 1;
/* Iterate through all slices, handling both flat directory mode and
* superdir mode. This mirrors the iteration logic in rdb.c. */
if (ar->superdir) {
/* Superdir mode: iterate through blocks */
for (uint32_t bi = 0; bi < ar->sdir_len; bi++) {
arSDirEntry *e = ar->superdir + bi;
uint64_t block_base = e->block_id * AR_SUPER_BLOCK_SLOTS;
for (uint32_t si = 0; si < AR_SUPER_BLOCK_SLOTS; si++) {
arSlice *s = e->slots[si];
if (!s) continue;
uint64_t slice_id = block_base + si;
if (!aofEmitSliceElements(r, key, s, slice_id, ar->slice_size,
&count, &items)) return 0;
}
}
} else {
/* Flat directory mode */
for (uint64_t slice_id = 0; slice_id <= ar->dir_highest_used && slice_id < ar->dir_alloc; slice_id++) {
arSlice *s = ar->dir[slice_id];
if (!s) continue;
if (!aofEmitSliceElements(r, key, s, slice_id, ar->slice_size,
&count, &items)) return 0;
}
}
/* If insert_idx is set, emit ARSEEK command to restore it.
* When insert_idx == UINT64_MAX-1, we emit ARSEEK UINT64_MAX which
* correctly sets insert_idx back to UINT64_MAX-1 (terminal state). */
if (ar->insert_idx != AR_INSERT_IDX_NONE) {
/* ARSEEK key insert_idx+1 (ARSEEK sets position for next insert) */
if (!rioWriteBulkCount(r,'*',3) ||
!rioWriteBulkString(r,"ARSEEK",6) ||
!rioWriteBulkObject(r,key) ||
!rioWriteBulkUnsignedLongLong(r, ar->insert_idx + 1))
{
return 0;
}
}
return 1;
}
int rewriteObject(rio *r, robj *key, robj *o, int dbid, long long expiretime) {
/* Save the key and associated value */
if (o->type == OBJ_STRING) {
@@ -2405,6 +2646,12 @@ int rewriteObject(rio *r, robj *key, robj *o, int dbid, long long expiretime) {
if (rewriteHashObject(r,key,o) == 0) return C_ERR;
} else if (o->type == OBJ_STREAM) {
if (rewriteStreamObject(r,key,o) == 0) return C_ERR;
#ifdef ENABLE_GCRA
} else if (o->type == OBJ_GCRA) {
if (rewriteGCRAObject(r,key,o) == 0) return C_ERR;
#endif
} else if (o->type == OBJ_ARRAY) {
if (rewriteArrayObject(r,key,o) == 0) return C_ERR;
} else if (o->type == OBJ_MODULE) {
if (rewriteModuleObject(r,key,o,dbid) == 0) return C_ERR;
} else {
@@ -2419,6 +2666,10 @@ int rewriteObject(rio *r, robj *key, robj *o, int dbid, long long expiretime) {
if (rioWriteBulkLongLong(r,expiretime) == 0) return C_ERR;
}
/* If modules metadata is available */
if ((getModuleMetaBits(o->metabits)) && (keyMetaOnAof(r, key, o, dbid) == 0))
return C_ERR;
return C_OK;
}
@@ -2428,7 +2679,7 @@ int rewriteAppendOnlyFileRio(rio *aof) {
long key_count = 0;
long long updated_time = 0;
unsigned long long skipped = 0;
kvstoreIterator *kvs_it = NULL;
kvstoreIterator kvs_it;
/* Record timestamp at the beginning of rewriting AOF. */
if (server.aof_timestamp_enabled) {
@@ -2448,11 +2699,21 @@ int rewriteAppendOnlyFileRio(rio *aof) {
if (rioWrite(aof,selectcmd,sizeof(selectcmd)-1) == 0) goto werr;
if (rioWriteBulkLongLong(aof,j) == 0) goto werr;
kvs_it = kvstoreIteratorInit(db->keys);
kvstoreIteratorInit(&kvs_it, db->keys);
int last_slot = -1;
/* Iterate this DB writing every entry */
while((de = kvstoreIteratorNext(kvs_it)) != NULL) {
while((de = kvstoreIteratorNext(&kvs_it)) != NULL) {
long long expiretime;
size_t aof_bytes_before_key = aof->processed_bytes;
int curr_slot = kvstoreIteratorGetCurrentDictIndex(&kvs_it);
/* In cluster mode, dismiss bucket arrays of the previous slot
* which won't be accessed again, to avoid CoW. */
if (server.cluster_enabled && curr_slot != last_slot) {
if (server.in_fork_child && last_slot != -1)
dismissDictBucketsMemory(kvstoreGetDict(db->keys, last_slot));
last_slot = curr_slot;
}
/* Get the value object (of type kvobj) */
kvobj *o = dictGetKV(de);
@@ -2461,25 +2722,23 @@ int rewriteAppendOnlyFileRio(rio *aof) {
expiretime = kvobjGetExpire(o);
/* Skip keys that are being trimmed */
if (server.cluster_enabled) {
int curr_slot = kvstoreIteratorGetCurrentDictIndex(kvs_it);
if (isSlotInTrimJob(curr_slot)) {
skipped++;
continue;
}
if (server.cluster_enabled && isSlotInTrimJob(curr_slot)) {
skipped++;
continue;
}
/* Set on stack string object for key */
robj key;
initStaticStringObject(key, kvobjGetKey(o));
if (rewriteObject(aof, &key, o, j, expiretime) == C_ERR) goto werr;
if (rewriteObject(aof, &key, o, j, expiretime) == C_ERR) goto werr2;
/* In fork child process, we can try to release memory back to the
* OS and possibly avoid or decrease COW. We give the dismiss
* mechanism a hint about an estimated size of the object we stored. */
size_t dump_size = aof->processed_bytes - aof_bytes_before_key;
if (server.in_fork_child) dismissObject(o, dump_size);
if (server.in_fork_child && dump_size > server.page_size/2)
dismissObject(o, dump_size);
/* Update info every 1 second (approximately).
* in order to avoid calling mstime() on each iteration, we will
@@ -2496,13 +2755,18 @@ int rewriteAppendOnlyFileRio(rio *aof) {
if (server.rdb_key_save_delay)
debugDelay(server.rdb_key_save_delay);
}
kvstoreIteratorRelease(kvs_it);
kvstoreIteratorReset(&kvs_it);
/* Dismiss bucket arrays of kvstore in standalone mode. */
if (server.in_fork_child && !server.cluster_enabled)
dismissKvstoreBucketsMemory(db->keys);
}
serverLog(LL_NOTICE, "AOF rewrite done, %ld keys saved, %llu keys skipped.", key_count, skipped);
return C_OK;
werr2:
kvstoreIteratorReset(&kvs_it);
werr:
if (kvs_it) kvstoreIteratorRelease(kvs_it);
return C_ERR;
}
+30
View File
@@ -11,6 +11,7 @@
* atomicSet(var,value) -- Set the atomic counter value
* atomicGetWithSync(var,value) -- 'atomicGet' with inter-thread synchronization
* atomicSetWithSync(var,value) -- 'atomicSet' with inter-thread synchronization
* atomicCompareExchange(type,var,expected_var,desired) -- Compare and exchange (CAS) operation
*
* Atomic operations on flags.
* Flag type can be int, long, long long or their unsigned counterparts.
@@ -110,6 +111,8 @@
} while(0)
#define atomicSetWithSync(var,value) \
atomic_store_explicit(&var,value,memory_order_seq_cst)
#define atomicCompareExchange(type,var,expected_var,desired) \
atomic_compare_exchange_weak_explicit(&var,&expected_var,desired,memory_order_relaxed,memory_order_relaxed)
#define atomicFlagGetSet(var,oldvalue_var) \
oldvalue_var = atomic_exchange_explicit(&var,1,memory_order_relaxed)
#define REDIS_ATOMIC_API "c11-builtin"
@@ -135,6 +138,8 @@
} while(0)
#define atomicSetWithSync(var,value) \
__atomic_store_n(&var,value,__ATOMIC_SEQ_CST)
#define atomicCompareExchange(type,var,expected_var,desired) \
__atomic_compare_exchange_n(&var,&expected_var,desired,1,__ATOMIC_RELAXED,__ATOMIC_RELAXED)
#define atomicFlagGetSet(var,oldvalue_var) \
oldvalue_var = __atomic_exchange_n(&var,1,__ATOMIC_RELAXED)
#define REDIS_ATOMIC_API "atomic-builtin"
@@ -164,6 +169,12 @@
ANNOTATE_HAPPENS_BEFORE(&var); \
while(!__sync_bool_compare_and_swap(&var,var,value,__sync_synchronize)); \
} while(0)
#define atomicCompareExchange(type,var,expected_var,desired) ({ \
type _old = __sync_val_compare_and_swap(&var,expected_var,desired); \
int _success = (_old == expected_var); \
if (!_success) expected_var = _old; \
_success; \
})
#define atomicFlagGetSet(var,oldvalue_var) \
oldvalue_var = __sync_val_compare_and_swap(&var,0,1)
#define REDIS_ATOMIC_API "sync-builtin"
@@ -172,4 +183,23 @@
#error "Unable to determine atomic operations for your platform"
#endif
/* atomicIncrGetSingleWriter(var, delta, newvalue_var)
*
* Adds `delta` to `var` and writes the resulting value to `newvalue_var`.
* Same end result as atomicIncrGet() but implemented as load+add+store instead
* of an atomic read-modify-write. This avoids the `lock` prefix on x86
* (~20-40 cycles vs ~2-3 for plain load+store).
*
* SAFETY: the caller MUST guarantee that no other thread ever writes to `var`
* (no atomicIncr, no atomicSet, no other call to this macro from a different
* thread). Concurrent writers cause silent lost updates. Readers on other
* threads using atomicGet are fine: they will observe either the pre or
* post update value. */
#define atomicIncrGetSingleWriter(var, delta, newvalue_var) do { \
atomicGet((var), (newvalue_var)); \
(newvalue_var) += (delta); \
atomicSet((var), (newvalue_var)); \
} while(0)
#endif /* __ATOMIC_VAR_H */
+202 -44
View File
@@ -37,9 +37,11 @@
/* AArch64 NEON support is determined at compile time via HAVE_AARCH64_NEON */
#ifdef HAVE_AVX512
#define BITOP_USE_AVX512 (__builtin_cpu_supports("avx512f") && __builtin_cpu_supports("avx512vpopcntdq"))
#define BITOP_USE_AVX512 (__builtin_cpu_supports("avx512f"))
#define BITOPS_USE_AVX512_POPCOUNT (__builtin_cpu_supports("avx512f") && __builtin_cpu_supports("avx512vpopcntdq"))
#else
#define BITOP_USE_AVX512 0
#define BITOPS_USE_AVX512_POPCOUNT 0
#endif
@@ -364,7 +366,7 @@ long long redisPopCountAvx2(void *s, long count) {
/* Automatically select the best available popcount implementation */
static inline long long redisPopcountAuto(const unsigned char *p, long count) {
#ifdef HAVE_AVX512
if (BITOP_USE_AVX512) {
if (BITOPS_USE_AVX512_POPCOUNT) {
return redisPopCountAvx512((void*)p, count);
}
#endif
@@ -796,11 +798,11 @@ static kvobj *lookupStringForBitCommand(client *c, uint64_t maxbit,
} else {
o = dbUnshareStringValue(c->db,c->argv[1],o);
*strOldSize = sdslen(o->ptr);
if (server.memory_tracking_per_slot)
oldAllocSize = stringObjectAllocSize(o);
if (server.memory_tracking_enabled)
oldAllocSize = kvobjAllocSize(o);
o->ptr = sdsgrowzero(o->ptr,byte+1);
if (server.memory_tracking_per_slot)
updateSlotAllocSize(c->db, getKeySlot(c->argv[1]->ptr), oldAllocSize, stringObjectAllocSize(o));
if (server.memory_tracking_enabled)
updateSlotAllocSize(c->db, getKeySlot(c->argv[1]->ptr), o, oldAllocSize, kvobjAllocSize(o));
*strGrowSize = sdslen(o->ptr) - *strOldSize;
}
return o;
@@ -875,7 +877,7 @@ void setbitCommand(client *c) {
byteval &= ~(1 << bit);
byteval |= ((on & 0x1) << bit);
((uint8_t*)o->ptr)[byte] = byteval;
signalModifiedKey(c,c->db,c->argv[1]);
keyModified(c,c->db,c->argv[1],o,1);
notifyKeyspaceEvent(NOTIFY_STRING,"setbit",c->argv[1],c->db->id);
server.dirty++;
@@ -883,8 +885,7 @@ void setbitCommand(client *c) {
* update the keysizes histogram. Otherwise, the histogram already
* updated in lookupStringForBitCommand() by calling dbAdd(). */
if ((strOldSize > 0) && (strGrowSize != 0))
updateKeysizesHist(c->db, getKeySlot(c->argv[1]->ptr), OBJ_STRING,
strOldSize, strOldSize + strGrowSize);
updateKeysizesHist(c->db, OBJ_STRING, strOldSize, strOldSize + strGrowSize);
}
/* Return original value. */
@@ -923,7 +924,7 @@ void getbitCommand(client *c) {
* 256-bit registers so if `minlen` is not a multiple of 32 some of the bytes
* will be skipped. They will be taken care for in the unoptimized loop in the
* main bitopCommand function. */
ATTRIBUTE_TARGET_AVX2_POPCOUNT
ATTRIBUTE_TARGET_AVX2
unsigned long bitopCommandAVX(unsigned char **keys, unsigned char *res,
unsigned long op, unsigned long numkeys,
unsigned long minlen)
@@ -939,21 +940,13 @@ unsigned long bitopCommandAVX(unsigned char **keys, unsigned char *res,
return 0;
}
/* Unlike other operations that do the same with all source keys
* DIFF, DIFF1 and ANDOR all compute the disjunction of all the source keys
* but the first one. We first store that disjunction in `lres` and later
* compute the final operation using the first source key. */
if (op != BITOP_DIFF && op != BITOP_DIFF1 && op != BITOP_ANDOR) {
memcpy(res, keys[0], minlen);
}
const __m256i max256 = _mm256_set1_epi64x(-1);
const __m256i zero256 = _mm256_set1_epi64x(0);
switch (op) {
case BITOP_AND:
while (minlen >= step) {
__m256i lres = _mm256_lddqu_si256((__m256i*)res);
__m256i lres = _mm256_lddqu_si256((__m256i*)(keys[0]+processed));
for (i = 1; i < numkeys; i++) {
__m256i lkey = _mm256_lddqu_si256((__m256i*)(keys[i]+processed));
@@ -965,12 +958,18 @@ unsigned long bitopCommandAVX(unsigned char **keys, unsigned char *res,
minlen -= step;
}
break;
/* Unlike other operations that do the same with all source keys
* DIFF, DIFF1 and ANDOR all compute the disjunction of all the source keys
* but the first one. We first store that disjunction in `lres` and later
* compute the final operation using the first source key. */
case BITOP_DIFF:
case BITOP_DIFF1:
case BITOP_ANDOR:
case BITOP_OR:
while (minlen >= step) {
__m256i lres = _mm256_lddqu_si256((__m256i*)res);
__m256i lres = (op == BITOP_OR) ?
_mm256_lddqu_si256((__m256i*)(keys[0]+processed)) :
zero256;
for (i = 1; i < numkeys; i++) {
__m256i lkey = _mm256_lddqu_si256((__m256i*)(keys[i]+processed));
@@ -984,7 +983,7 @@ unsigned long bitopCommandAVX(unsigned char **keys, unsigned char *res,
break;
case BITOP_XOR:
while (minlen >= step) {
__m256i lres = _mm256_lddqu_si256((__m256i*)res);
__m256i lres = _mm256_lddqu_si256((__m256i*)(keys[0]+processed));
for (i = 1; i < numkeys; i++) {
__m256i lkey = _mm256_lddqu_si256((__m256i*)(keys[i]+processed));
@@ -998,7 +997,7 @@ unsigned long bitopCommandAVX(unsigned char **keys, unsigned char *res,
break;
case BITOP_NOT:
while (minlen >= step) {
__m256i lres = _mm256_lddqu_si256((__m256i*)res);
__m256i lres = _mm256_lddqu_si256((__m256i*)(keys[0]+processed));
lres = _mm256_xor_si256(lres, max256);
_mm256_storeu_si256((__m256i*)res, lres);
res += step;
@@ -1008,7 +1007,7 @@ unsigned long bitopCommandAVX(unsigned char **keys, unsigned char *res,
break;
case BITOP_ONE:
while (minlen >= step) {
__m256i lres = _mm256_lddqu_si256((__m256i*)res);
__m256i lres = _mm256_lddqu_si256((__m256i*)(keys[0]+processed));
__m256i common_bits = zero256;
for (i = 1; i < numkeys; i++) {
@@ -1075,6 +1074,162 @@ unsigned long bitopCommandAVX(unsigned char **keys, unsigned char *res,
}
#endif /* HAVE_AVX2 */
#ifdef HAVE_AVX512
/* Compute the given bitop operation using AVX512 intrinsics.
* Return how many bytes were successfully processed, as AVX512 operates on
* 512-bit registers so if `minlen` is not a multiple of 64 some of the bytes
* will be skipped. They will be taken care for in the unoptimized loop in the
* main bitopCommand function. */
ATTRIBUTE_TARGET_AVX512
unsigned long bitopCommandAVX512(unsigned char **keys, unsigned char *res,
unsigned long op, unsigned long numkeys,
unsigned long minlen)
{
const unsigned long step = sizeof(__m512i); /* 64 bytes */
unsigned long i;
unsigned long processed = 0;
unsigned char *res_start = res;
unsigned char *fst_key = keys[0];
if (minlen < step) {
return 0;
}
const __m512i max512 = _mm512_set1_epi64(-1);
const __m512i zero512 = _mm512_set1_epi64(0);
switch (op) {
case BITOP_AND:
while (minlen >= step) {
__m512i lres = _mm512_loadu_si512((__m512i*)(keys[0]+processed));
for (i = 1; i < numkeys; i++) {
__m512i lkey = _mm512_loadu_si512((__m512i*)(keys[i]+processed));
lres = _mm512_and_si512(lres, lkey);
}
_mm512_storeu_si512((__m512i*)res, lres);
res += step;
processed += step;
minlen -= step;
}
break;
/* Unlike other operations that do the same with all source keys
* DIFF, DIFF1 and ANDOR all compute the disjunction of all the source keys
* but the first one. We first store that disjunction in `lres` and later
* compute the final operation using the first source key. */
case BITOP_DIFF:
case BITOP_DIFF1:
case BITOP_ANDOR:
case BITOP_OR:
while (minlen >= step) {
__m512i lres = (op == BITOP_OR) ?
_mm512_loadu_si512((__m512i*)(keys[0]+processed)) :
zero512;
for (i = 1; i < numkeys; i++) {
__m512i lkey = _mm512_loadu_si512((__m512i*)(keys[i]+processed));
lres = _mm512_or_si512(lres, lkey);
}
_mm512_storeu_si512((__m512i*)res, lres);
res += step;
processed += step;
minlen -= step;
}
break;
case BITOP_XOR:
while (minlen >= step) {
__m512i lres = _mm512_loadu_si512((__m512i*)(keys[0]+processed));
for (i = 1; i < numkeys; i++) {
__m512i lkey = _mm512_loadu_si512((__m512i*)(keys[i]+processed));
lres = _mm512_xor_si512(lres, lkey);
}
_mm512_storeu_si512((__m512i*)res, lres);
res += step;
processed += step;
minlen -= step;
}
break;
case BITOP_NOT:
while (minlen >= step) {
__m512i lres = _mm512_loadu_si512((__m512i*)(keys[0]+processed));
lres = _mm512_xor_si512(lres, max512);
_mm512_storeu_si512((__m512i*)res, lres);
res += step;
processed += step;
minlen -= step;
}
break;
case BITOP_ONE:
while (minlen >= step) {
__m512i lres = _mm512_loadu_si512((__m512i*)(keys[0]+processed));
__m512i common_bits = zero512;
for (i = 1; i < numkeys; i++) {
__m512i lkey = _mm512_loadu_si512((__m512i*)(keys[i]+processed));
/* common_bits |= (lres & lkey): ternary-logic with imm8 0xEA == c|(a&b)
* (a=lres, b=lkey, c=common_bits), replacing a separate AND+OR. */
common_bits = _mm512_ternarylogic_epi32(lres, lkey, common_bits, 0xEA);
lres = _mm512_xor_si512(lres, lkey);
}
lres = _mm512_andnot_si512(common_bits, lres);
_mm512_storeu_si512((__m512i*)res, lres);
res += step;
processed += step;
minlen -= step;
}
break;
default:
break;
}
res = res_start;
switch (op) {
case BITOP_DIFF:
for (i = 0; i < processed; i += step) {
__m512i lres = _mm512_loadu_si512((__m512i*)res);
__m512i fkey = _mm512_loadu_si512((__m512i*)fst_key);
lres = _mm512_andnot_si512(lres, fkey);
_mm512_storeu_si512((__m512i*)res, lres);
res += step;
fst_key += step;
}
break;
case BITOP_DIFF1:
for (i = 0; i < processed; i += step) {
__m512i lres = _mm512_loadu_si512((__m512i*)res);
__m512i fkey = _mm512_loadu_si512((__m512i*)fst_key);
lres = _mm512_andnot_si512(fkey, lres);
_mm512_storeu_si512((__m512i*)res, lres);
res += step;
fst_key += step;
}
break;
case BITOP_ANDOR:
for (i = 0; i < processed; i += step) {
__m512i lres = _mm512_loadu_si512((__m512i*)res);
__m512i fkey = _mm512_loadu_si512((__m512i*)fst_key);
lres = _mm512_and_si512(fkey, lres);
_mm512_storeu_si512((__m512i*)res, lres);
res += step;
fst_key += step;
}
break;
default:
break;
}
return processed;
}
#endif /* HAVE_AVX512 */
/* BITOP op_name target_key src_key1 src_key2 src_key3 ... src_keyN */
REDIS_NO_SANITIZE("alignment")
void bitopCommand(client *c) {
@@ -1163,36 +1318,40 @@ void bitopCommand(client *c) {
res = (unsigned char*) sdsnewlen(NULL,maxlen);
unsigned char output, byte, disjunction, common_bits;
unsigned long i;
int useAVX2 = 0;
int useAVX = 0;
/* Number of bytes processed from each source key */
j = 0;
#if defined(HAVE_AVX512)
if (BITOP_USE_AVX512 && (minlen >= 10000) && (numkeys >= 8)) {
j = bitopCommandAVX512(src, res, op, numkeys, minlen);
serverAssert(minlen >= j);
minlen -= j;
useAVX = 1;
}
#endif
#if defined(HAVE_AVX2)
if (BITOP_USE_AVX2) {
if (!useAVX && BITOP_USE_AVX2) {
j = bitopCommandAVX(src, res, op, numkeys, minlen);
serverAssert(minlen >= j);
minlen -= j;
useAVX2 = 1;
useAVX = 1;
}
#endif
#if !defined(USE_ALIGNED_ACCESS)
/* We don't have AVX2 but we still have fast path:
* as far as we have data for all the input bitmaps we
* can take a fast path that performs much better than the
* vanilla algorithm. On ARM we skip the fast path since it will
* result in GCC compiling the code using multiple-words load/store
* operations that are not supported even in ARM >= v6. */
if (minlen >= sizeof(unsigned long)*4) {
/* We can't have entered the AVX2 path since minlen >= sizeof(unsigned long)*4
* AVX2 path operates on steps of sizeof(__m256i) which for 64-bit
* machines (the only ones supporting AVX2) is equal to
* sizeof(unsigned long)*4. That means after the AVX2
* path minlen will necessarily be < sizeof(unsigned long)*4. */
serverAssert(!useAVX2);
/* If no SIMD path was used (no AVX2/AVX512), fall back
* to a word-at-a-time fast path that is still much better
* than the byte-by-byte loop below. On ARM we skip this since
* it would cause GCC to emit multiple-word load/store ops
* not supported even on ARM >= v6. */
if (!useAVX && minlen >= sizeof(unsigned long)*4) {
unsigned long **lp = (unsigned long**)src;
unsigned long *lres = (unsigned long*) res;
@@ -1447,7 +1606,7 @@ void bitopCommand(client *c) {
notifyKeyspaceEvent(NOTIFY_STRING,"set",targetkey,c->db->id);
server.dirty++;
} else if (dbDelete(c->db,targetkey)) {
signalModifiedKey(c,c->db,targetkey);
keyModified(c,c->db,targetkey,NULL,1);
notifyKeyspaceEvent(NOTIFY_GENERIC,"del",targetkey,c->db->id);
server.dirty++;
}
@@ -1722,7 +1881,7 @@ void bitfieldGeneric(client *c, int flags) {
kvobj *o;
uint64_t bitoffset;
int j, numops = 0, changes = 0;
size_t strOldSize, strGrowSize = 0;
size_t strOldSize = 0, strGrowSize = 0;
struct bitfieldOp *ops = NULL; /* Array of ops to execute at end. */
int owtype = BFOVERFLOW_WRAP; /* Overflow type. */
int readonly = 1;
@@ -1948,10 +2107,9 @@ void bitfieldGeneric(client *c, int flags) {
* update the keysizes histogram. Otherwise, the histogram already
* updated in lookupStringForBitCommand() by calling dbAdd(). */
if ((strOldSize > 0) && (strGrowSize != 0))
updateKeysizesHist(c->db, getKeySlot(c->argv[1]->ptr), OBJ_STRING,
strOldSize, strOldSize + strGrowSize);
updateKeysizesHist(c->db, OBJ_STRING, strOldSize, strOldSize + strGrowSize);
signalModifiedKey(c,c->db,c->argv[1]);
keyModified(c,c->db,c->argv[1],o,1);
notifyKeyspaceEvent(NOTIFY_STRING,"setbit",c->argv[1],c->db->id);
server.dirty += changes;
}
+17 -3
View File
@@ -237,7 +237,11 @@ int blockedClientMayTimeout(client *c) {
* unblockClient() will be called with the same client as argument. */
void replyToBlockedClientTimedOut(client *c) {
if (c->bstate.btype == BLOCKED_LAZYFREE) {
addReply(c, shared.ok); /* No reason lazy-free to fail */
/* SFLUSH: reply with empty array, FLUSH*: reply with OK */
if (c->cmd && c->cmd->proc == sflushCommand)
addReplyArrayLen(c, 0);
else
addReply(c, shared.ok); /* No reason lazy-free to fail */
} else if (c->bstate.btype == BLOCKED_LIST ||
c->bstate.btype == BLOCKED_ZSET ||
c->bstate.btype == BLOCKED_STREAM) {
@@ -297,7 +301,11 @@ void disconnectAllBlockedClients(void) {
continue;
if (c->bstate.btype == BLOCKED_LAZYFREE) {
addReply(c, shared.ok); /* No reason lazy-free to fail */
/* SFLUSH: reply with empty array, FLUSH*: reply with OK */
if (c->cmd && c->cmd->proc == sflushCommand)
addReplyArrayLen(c, 0);
else
addReply(c, shared.ok);
updateStatsOnUnblock(c, 0, 0, 0);
c->flags &= ~CLIENT_PENDING_COMMAND;
unblockClient(c, 1);
@@ -691,7 +699,13 @@ static void unblockClientOnKey(client *c, robj *key) {
client *old_client = server.current_client;
server.current_client = c;
enterExecutionUnit(1, 0);
processCommandAndResetClient(c);
if (processCommandAndResetClient(c) == C_ERR) {
/* Client was freed during command processing, exit immediately */
exitExecutionUnit();
server.current_client = old_client;
return;
}
if (!(c->flags & CLIENT_BLOCKED)) {
if (c->flags & CLIENT_MODULE) {
moduleCallCommandUnblockedHandler(c);
+822
View File
@@ -0,0 +1,822 @@
/* Implementation of a topK structure using CuckooHeavyKeeper algorithm
*
* Implementation is based on the paper "Cuckoo Heavy Keeper and the balancing
* act of maintaining heavy hitters in stream processing" by Vinh Quang Ngo and
* Marina Papatriantafilou. Also, the accompanying C++ implementation was used
* as a reference point: https://github.com/vinhqngo5/Cuckoo_Heavy_Keeper
* Main changes are addition of a min-heap so we can keep names of the top K
* elements - idea comes from RedisBloom's TopK structure.
*
* Copyright (c) 2026-Present, Redis Ltd.
* All rights reserved.
*
* Licensed under your choice of (a) the Redis Source Available License 2.0
* (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the
* GNU Affero General Public License v3 (AGPLv3).
*/
#include "chk.h"
#include "redisassert.h"
#include "zmalloc.h"
#include "xxhash.h"
#include <math.h>
#include <stdlib.h>
#include <string.h>
/* Lobby to heavy item promotion threshold */
#define LOBBY_PROMOTION_THRESHOLD 16
#ifndef static_assert
#define static_assert(expr, lit) extern char __static_assert_failure[(expr) ? 1:-1]
#endif
static_assert(LOBBY_PROMOTION_THRESHOLD < CHK_LUT_SIZE,
"Lobby promotion threshold should be less then the LUT size to "
"ensure constant operations during decayCounter!");
/* After a heavy item is demoted is starts recursively kicking out other heavy
* items in the case it should stay heavy (defined by isHeavyHitter). In
* principle this process could go over all the items in the chkTopK's tables
* so it's artificially limited by this constant. */
#define MAX_KICKS 16
/* An item is defined as heavy hitter if its count is more or equal to x * N
* where x is a threshold constant (HEAVY_RATIO) and N is the total count the
* chkTopK structure has accumulated. See the paper for more info. */
#define HEAVY_RATIO 0.008
/* A unique seed for the items when storing them in the heap so it's not related
* to the cuckoo's hashes. Also, we don't need the less-bit hash here as the
* heap does not take much memory so we avoid needless possible collisions. */
#define HEAP_SEED 1919
typedef struct {
size_t idx[CHK_NUM_TABLES];
fingerprint_t fp;
} fpAndIdx;
#define min(a, b) ((a) < (b) ? (a) : (b))
/* Heap operations */
static chkHeapBucket *chkCheckExistInHeap(chkTopK *topk, const char *item, int itemlen, uint64_t fp) {
for (int32_t i = topk->k - 1; i >= 0; --i) {
chkHeapBucket *bucket = topk->heap + i;
if (bucket->fp == fp && bucket->item &&
sdslen(bucket->item) == (size_t)itemlen &&
memcmp(bucket->item, item, itemlen) == 0)
{
return bucket;
}
}
return NULL;
}
void chkHeapifyDown(chkHeapBucket *array, size_t len, size_t start) {
size_t child = start;
if (len < 2 || (len - 2) / 2 < child) {
return;
}
child = 2 * child + 1;
if ((child + 1) < len && (array[child].count > array[child + 1].count)) {
++child;
}
if (array[child].count > array[start].count) {
return;
}
chkHeapBucket top = {0};
top = array[start];
do {
memcpy(&array[start], &array[child], sizeof(chkHeapBucket));
start = child;
if ((len - 2) / 2 < child) {
break;
}
child = 2 * child + 1;
if ((child + 1) < len && (array[child].count > array[child + 1].count)) {
++child;
}
} while (array[child].count < top.count);
memcpy(&array[start], &top, sizeof(chkHeapBucket));
}
/*-----------------------------------------------------------------------------
* chkTopK operations
*----------------------------------------------------------------------------*/
/* Create the chkTopK structure. Note, CHK paper recommends decay=1.08.
* numbuckets must be a power of 2. Recommended size for numbuckets is at least
* 7 or 8 times k. */
chkTopK *chkTopKCreate(int k, int numbuckets, double decay) {
/* Number of buckets need to be a power of 2 for better performance - we
* have better cache locality of the tables and faster table indices
* calculations. */
assert(k > 0 && (numbuckets & (numbuckets - 1)) == 0);
size_t usable = 0;
chkTopK *topk = zcalloc_usable(sizeof(chkTopK), &usable);
topk->alloc_size += usable;
for (int i = 0; i < CHK_NUM_TABLES; ++i) {
topk->tables[i] = zcalloc_usable(sizeof(chkBucket) * numbuckets, &usable);
topk->alloc_size += usable;
}
topk->heap = zcalloc_usable(sizeof(chkHeapBucket) * k, &usable);
topk->alloc_size += usable;
topk->decay = decay;
topk->inv_decay = 1. / decay;
topk->k = k;
topk->numbuckets = numbuckets;
topk->lut_decay_exp[0] = 0;
topk->lut_min_decay[0] = 0;
topk->lut_decay_prob[0] = 0;
for (int i = 1; i < CHK_LUT_SIZE + 1; ++i) {
topk->lut_decay_exp[i] = topk->lut_decay_exp[i - 1] + pow(topk->decay, i - 1);
topk->lut_min_decay[i] = topk->lut_decay_exp[i] - topk->lut_decay_exp[i - 1];
topk->lut_decay_prob[i] = pow(topk->inv_decay, i);
}
return topk;
}
/* Release chkTopK resources */
void chkTopKRelease(chkTopK *topk) {
size_t usable;
for (int i = 0; i < CHK_NUM_TABLES; ++i) {
zfree_usable(topk->tables[i], &usable);
topk->alloc_size -= usable;
}
for (int i = 0; i < topk->k; ++i) {
if (topk->heap[i].item) {
topk->alloc_size -= sdsAllocSize(topk->heap[i].item);
sdsfree(topk->heap[i].item);
}
}
zfree_usable(topk->heap, &usable);
topk->alloc_size -= usable;
debugAssert(topk->alloc_size == zmalloc_usable_size(topk));
zfree(topk);
}
static inline int generateAltIdx(fingerprint_t fp, int idx, int numbuckets) {
return (idx ^ (0x5bd1e995 * (size_t)fp)) & (numbuckets - 1);
}
fpAndIdx generateItemFpAndIdxs(chkTopK *topk, char *item, int itemlen) {
uint64_t hash = XXH3_64bits_withSeed(item, itemlen, 0);
fpAndIdx res;
res.fp = (hash & 0xFFFF); /* Only use 16 bits for fingerprint */
/* Note numbuckets are a power of 2 so we don't use modulo for index calc */
res.idx[0] = (hash >> 32) & (topk->numbuckets - 1);
for (int i = 1; i < CHK_NUM_TABLES; ++i) {
res.idx[i] = generateAltIdx(res.fp, res.idx[i-1], topk->numbuckets);
}
return res;
}
typedef struct {
int table_idx;
int pos;
} checkEntryRes;
/* Check if `item` is a heavy entry. If so we bump its count. If not - we make
* it a heavy entry immediately if there is an empty spot, thus skipping the
* lobby as an optimization. */
checkEntryRes checkHeavyEntries(chkTopK *topk, fpAndIdx item, counter_t weight) {
int empty_table_idx = -1;
int empty_pos = -1;
for (int i = 0; i < CHK_NUM_TABLES; ++i) {
int idx = item.idx[i];
chkBucket *bucket = &topk->tables[i][idx];
for (int j = 0; j < CHK_HEAVY_ENTRIES_PER_BUCKET; ++j) {
chkHeavyEntry *e = &bucket->heavy_entries[j];
if (e->count > 0) {
if (e->fp == item.fp) {
e->count += weight;
checkEntryRes res = { i, j };
return res;
}
} else if (empty_table_idx == -1) {
empty_table_idx = i;
empty_pos = j;
}
}
}
if (empty_table_idx == -1) {
checkEntryRes res = { -1, -1 };
return res;
}
/* If there is an empty slot in the heavy entries just put the item there
* instead of going through the lobby first (optimization as per the paper) */
int idx = item.idx[empty_table_idx];
chkHeavyEntry *e = &topk->tables[empty_table_idx][idx].heavy_entries[empty_pos];
e->fp = item.fp;
e->count = weight;
checkEntryRes res = {empty_table_idx, empty_pos};
return res;
}
/* A heavy hitter is defined by the paper as an item with counter more or equal
* to phi * N, where phi is a constant and N is the total count the structure
* has recorded up to that point */
int isHeavyHitter(chkTopK *topk, counter_t cnt) {
return cnt >= (topk->total * HEAVY_RATIO);
}
/* After a lobby item is promoted it may be placed on a heavy item's spot. The
* latter is kicked out, but it may recursively kick out another heavy item.
* The process is limited by MAX_KICKS and also by the fact that during updates
* one of the kicked out items may have its counter decayed so much - it's not
* passing the heavy item threshold (see isHeavyHitter). */
void kickout(chkTopK *topk, chkHeavyEntry entry, int idx, int table_idx) {
for (int i = 0; i < MAX_KICKS; ++i) {
/* Do not try to swap with any entries if we don't reach the heavy
* hitter threshold */
if (!isHeavyHitter(topk, entry.count)) return;
/* Find the heavy entry in the alt bucket in the other table with
* minimum count. If there is empty entry there just occupy it, else
* recursively kick the minimal one out.
* To find the alt bucket we need to compute the alt index from the
* fingerprint of the kicked-out entry. */
table_idx = 1 - table_idx;
idx = generateAltIdx(entry.fp, idx, topk->numbuckets);
chkBucket *bucket = &topk->tables[table_idx][idx];
counter_t min = (counter_t)-1;
int min_pos = -1;
for (int j = 0; j < CHK_HEAVY_ENTRIES_PER_BUCKET; ++j) {
chkHeavyEntry *e = &bucket->heavy_entries[j];
if (e->count == 0) {
*e = entry;
return;
}
if (e->count < min) {
min = e->count;
min_pos = j;
}
}
chkHeavyEntry old_entry = bucket->heavy_entries[min_pos];
bucket->heavy_entries[min_pos] = entry;
entry = old_entry;
}
}
/* When a lobby entry's counter passes the promotion threshold we try to promote
* it with some probability. See the paper for more details. If promotion is
* successful the lobby entry may kick out a heavy one - see kickout() */
int tryPromoteAndKickout(chkTopK *topk, fpAndIdx item, counter_t new_count,
int table_idx)
{
int idx = item.idx[table_idx];
chkBucket *bucket = &topk->tables[table_idx][idx];
counter_t min = (counter_t)-1; /* counter_t is unsigned */
int min_idx = -1;
/* We search for heavy item bucket of the promoted lobby entry. We may have
* an empty space which we immediately occupy. Otherwise we choose the
* bucket with lowest counter */
for (int i = 0; i < CHK_HEAVY_ENTRIES_PER_BUCKET; ++i) {
if (bucket->heavy_entries[i].count == 0) {
bucket->heavy_entries[i].fp = item.fp;
bucket->heavy_entries[i].count = new_count;
return i;
}
if (bucket->heavy_entries[i].count < min) {
min = bucket->heavy_entries[i].count;
min_idx = i;
}
}
/* If the heavy entry that is going to be kicked out has a counter lower
* than the lobby's one we always kick it out */
if (min > new_count) {
double prob = (new_count - LOBBY_PROMOTION_THRESHOLD) /
(double)(min - LOBBY_PROMOTION_THRESHOLD);
if ((rand() / (double)RAND_MAX) >= prob) return -1;
}
chkHeavyEntry to_kickout = bucket->heavy_entries[min_idx];
/* Note, that here the promoted item keeps the old count as per the paper */
bucket->heavy_entries[min_idx].fp = bucket->lobby_entry.fp;
bucket->lobby_entry.count = 0;
bucket->lobby_entry.fp = 0;
kickout(topk, to_kickout, idx, table_idx);
return min_idx;
}
/* Check if an item is a lobby entry */
checkEntryRes checkLobbyEntries(chkTopK *topk, fpAndIdx item, counter_t weight) {
for (int i = 0; i < CHK_NUM_TABLES; ++i) {
int idx = item.idx[i];
chkBucket *bucket = &topk->tables[i][idx];
chkLobbyEntry *e = &bucket->lobby_entry;
/* No match or empty lobby entry */
if (e->fp != item.fp || e->count == 0) continue;
/* If we don't cross the threshold just update the counter */
uint64_t new_count = (uint64_t)e->count + weight;
if (new_count < LOBBY_PROMOTION_THRESHOLD) {
e->count = (uint16_t)new_count;
checkEntryRes res = { i, -1 };
return res;
}
/* Try to promote the entry to heavy entry if we crossed the threshold.
* Else just set the counter to the value of the threshold */
int kickout_pos = tryPromoteAndKickout(topk, item, new_count, i);
if (kickout_pos != -1) {
checkEntryRes res = {i, kickout_pos};
return res;
}
e->count = LOBBY_PROMOTION_THRESHOLD;
checkEntryRes res = { i, -1 };
return res;
}
checkEntryRes res = { -1, -1 };
return res;
}
/* Probability to decay cnt with 1.
* Equal to pow(decay, -cnt) */
static inline double getDecayProb(chkTopK *topk, counter_t cnt) {
if (cnt < CHK_LUT_SIZE) {
return topk->lut_decay_prob[cnt];
}
return pow(topk->lut_decay_prob[CHK_LUT_SIZE],
((double)cnt / (CHK_LUT_SIZE))) *
topk->lut_decay_prob[cnt % (CHK_LUT_SIZE)];
}
/* Expected decay steps to decay cnt to 0.
* Equal to sum(pow(decay, i)) for i in [0; cnt] */
static inline double getExpDecayCount(chkTopK *topk, lobby_counter_t cnt) {
return topk->lut_decay_exp[cnt];
}
/* Expected minimum decay steps to decay cnt with 1. Since probability is
* pow(decay, -cnt) it's equal to pow(decay, cnt) */
static inline double getMinDecayCount(chkTopK *topk, counter_t cnt) {
if (cnt < CHK_LUT_SIZE) {
return topk->lut_min_decay[cnt];
}
return pow(topk->lut_min_decay[CHK_LUT_SIZE],
((double)cnt / (CHK_LUT_SIZE))) *
topk->lut_min_decay[cnt % (CHK_LUT_SIZE)];
}
/* When there is a hash-collission between lobby entries we decay the existing
* lobby entry with the weight of the new one. Return the counter after decaying. */
lobby_counter_t chkDecayCounter(chkTopK *topk, lobby_counter_t cnt, counter_t weight) {
if (weight == 0) return cnt;
/* Unweighted update - just decay with probability pow(decay, -cnt) */
if (weight == 1) {
double prob = getDecayProb(topk, (counter_t)cnt);
if ((rand() / (double)RAND_MAX) < prob) {
return cnt - 1;
}
return cnt;
}
/* For weighted updates we simulate multiple unweighted ones */
/* Weight is smaller than the minimum amount of decay steps required to
* decay the counter with probability of 100% so again we roll the dice */
double min_decay = getMinDecayCount(topk, cnt);
if (weight < (counter_t)min_decay) {
double prob = weight / min_decay;
if ((rand() / (double)RAND_MAX) < prob) {
return cnt - 1;
}
return cnt;
}
/* Weight is more than the expected amount of decay steps to decay the
* counter to 0. */
double exp_decays = getExpDecayCount(topk, cnt);
if (weight >= (counter_t)exp_decays)
return 0;
/* Weight is large enough to decay the counter to cnt - X where 0 < X < cnt.
* We binary search for the largest value `C` such that:
*
* (expected decay ops for `C`) >= (expected decay ops for `cnt`) - `weight`
* i.e lut_decay_exp[C] + weight >= lut_decay_exp[cnt]
*
* Note that since cnt is a lobby counter it will necessarily be less or
* equal than LOBBY_PROMOTION_THRESHOLD, so although we binary search this
* is a O(1) operation */
int left = 0;
int right = cnt;
while (left < right) {
int mid = left + (right - left) / 2;
if (topk->lut_decay_exp[mid] + weight >= topk->lut_decay_exp[cnt]) {
right = mid;
} else {
left = mid + 1;
}
}
return left;
}
/* Update weighted item. If another one was expelled from the topK list -
* return it. Caller is responsible for releasing it */
sds chkTopKUpdate(chkTopK *topk, char *item, int itemlen, counter_t weight)
{
if (weight == 0) return NULL;
topk->total += weight;
/* Generate a fingerprint and indices for both cuckoo tables. */
fpAndIdx itemFpIdx = generateItemFpAndIdxs(topk, item, itemlen);
/* Check if the item is amongst the heavy entries. If so we just update its
* counter. */
checkEntryRes res = checkHeavyEntries(topk, itemFpIdx, weight);
if (res.table_idx != -1) {
goto update_heap;
}
/* If the item is not already heavy it may be in the lobby. If so we'll
* increase its counter and promote it to a heavy entry if it passes the
* threshold */
res = checkLobbyEntries(topk, itemFpIdx, weight);
if (res.table_idx != -1) {
goto update_heap;
}
/* Item is not tracked at all. Check for empty lobby entries - if there is
* any - place the item there. The weight may be higher than the promotional
* threshold in which case we'll try to promote it. */
for (int i = 0; i < CHK_NUM_TABLES; ++i) {
int idx = itemFpIdx.idx[i];
chkBucket *bucket = &topk->tables[i][idx];
if (bucket->lobby_entry.count == 0) {
bucket->lobby_entry.fp = itemFpIdx.fp;
res.table_idx = i;
res.pos = -1;
if (weight < LOBBY_PROMOTION_THRESHOLD) {
bucket->lobby_entry.count = weight;
} else {
int kickout_pos = tryPromoteAndKickout(topk, itemFpIdx, weight, i);
if (kickout_pos != -1) {
res.pos = kickout_pos;
} else {
bucket->lobby_entry.count = LOBBY_PROMOTION_THRESHOLD;
}
}
goto update_heap;
}
}
/* If there are no empty lobby entries choose a table deterministically,
* decay its lobby counter and update */
int table_idx = itemFpIdx.fp & 1;
int idx = itemFpIdx.idx[table_idx];
chkLobbyEntry *e = &topk->tables[table_idx][idx].lobby_entry;
/* new_count is the count of `e` after decaying it with weight */
lobby_counter_t new_count = chkDecayCounter(topk, e->count, weight);
/* if the chosen lobby entry has decayed its counter to 0, it's replaced by
* the new entry. Note, in that case the new entry has it's weight
* decreased by the approximate amount of decay operations needed to decay
* the old entry. */
if (new_count == 0) {
e->fp = itemFpIdx.fp;
counter_t exp_decay_cnt = getExpDecayCount(topk, e->count);
e->count = exp_decay_cnt >= weight ?
1 : (lobby_counter_t)min(255, weight - exp_decay_cnt);
} else {
e->count = new_count;
}
if (e->count >= LOBBY_PROMOTION_THRESHOLD) {
int kickout_pos = tryPromoteAndKickout(topk, itemFpIdx, e->count, table_idx);
if (kickout_pos != -1) {
res.table_idx = table_idx;
res.pos = kickout_pos;
}
}
/* After a change in the structure has occurred we check if we also need to
* update the heap - i.e bump a new item in it, or reorder an old item if
* it's counter went up. */
update_heap:
if (res.table_idx == -1 || res.pos == -1)
return NULL;
table_idx = res.table_idx;
idx = itemFpIdx.idx[table_idx];
counter_t heap_min = topk->heap[0].count;
chkHeavyEntry *entry = &topk->tables[table_idx][idx].heavy_entries[res.pos];
if (entry->count < heap_min)
return NULL;
/* Heap uses different hash than the cuckoo tables */
uint64_t fp = XXH3_64bits_withSeed(item, itemlen, HEAP_SEED);
chkHeapBucket *itemHeapPtr = chkCheckExistInHeap(topk, item, itemlen, fp);
if (itemHeapPtr != NULL) {
itemHeapPtr->count = entry->count;
chkHeapifyDown(topk->heap, topk->k, itemHeapPtr - topk->heap);
} else {
/* We know the new entry has bigger count than the min-element so it's
* safe to expel it. */
sds expelled = topk->heap[0].item;
if (expelled) topk->alloc_size -= sdsAllocSize(expelled);
topk->heap[0].count = entry->count;
topk->heap[0].fp = fp;
topk->heap[0].item = sdsnewlen(item, itemlen);
topk->alloc_size += sdsAllocSize(topk->heap[0].item);
chkHeapifyDown(topk->heap, topk->k, 0);
return expelled;
}
return NULL;
}
int cmpchkHeapBucket(const void *tmp1, const void *tmp2) {
const chkHeapBucket *res1 = tmp1;
const chkHeapBucket *res2 = tmp2;
return res1->count < res2->count ? 1 : res1->count > res2->count ? -1 : 0;
}
/* Get an ordered by count list of topk->k elements inside the topk object.
*
* NOTE, the returned array is a copy of the internal heap stored by `topk`. The
* caller is responsible for releasing it after use. The elements of the array
* share their `item` pointers with the internal topk->heap buckets so one must
* not use it after `topk` is released. */
chkHeapBucket *chkTopKList(chkTopK *topk) {
chkHeapBucket *list = zmalloc(sizeof(chkHeapBucket) * topk->k);
memcpy(list, topk->heap, sizeof(chkHeapBucket) * topk->k);
qsort(list, topk->k, sizeof(*list), cmpchkHeapBucket);
return list;
}
size_t chkTopKGetMemoryUsage(chkTopK *topk) {
if (!topk) return 0;
return topk->alloc_size;
}
#ifdef REDIS_TEST
#include <stdio.h>
#include "testhelp.h"
#define UNUSED(x) (void)(x)
static int findItemInList(chkHeapBucket *list, int k, const char *item, int itemlen) {
for (int i = 0; i < k; i++) {
if (list[i].item != NULL &&
sdslen(list[i].item) == (size_t)itemlen &&
memcmp(list[i].item, item, itemlen) == 0) {
return i;
}
}
return -1;
}
static int verifyListSorted(chkHeapBucket *list, int k) {
for (int i = 0; i < k - 1; i++) {
if (list[i].item == NULL) continue;
if (list[i + 1].item == NULL) continue;
if (list[i].count < list[i + 1].count) {
return 0;
}
}
return 1;
}
static void chkTopKUpdateAndFreeExpelled(chkTopK *topk, const char *item, int itemlen, counter_t weight) {
sds expelled = chkTopKUpdate(topk, (char *)item, itemlen, weight);
if (expelled) sdsfree(expelled);
}
static void testBasicTopK(void) {
int k = 5;
int numbuckets = 64;
double decay = 0.9;
chkTopK *topk = chkTopKCreate(k, numbuckets, decay);
test_cond("Create topk structure", topk != NULL);
if (topk == NULL) return;
chkTopKUpdateAndFreeExpelled(topk, "item1", 5, 100);
chkTopKUpdateAndFreeExpelled(topk, "item2", 5, 200);
chkTopKUpdateAndFreeExpelled(topk, "item3", 5, 150);
chkTopKUpdateAndFreeExpelled(topk, "item4", 5, 50);
chkTopKUpdateAndFreeExpelled(topk, "item5", 5, 300);
chkTopKUpdateAndFreeExpelled(topk, "item6", 5, 75);
chkHeapBucket *list = chkTopKList(topk);
test_cond("chkTopKList returns non-NULL", list != NULL);
if (list == NULL) {
chkTopKRelease(topk);
return;
}
test_cond("TopK list is sorted in descending order", verifyListSorted(list, k));
int idx1 = findItemInList(list, k, "item5", 5);
int idx2 = findItemInList(list, k, "item2", 5);
int idx3 = findItemInList(list, k, "item3", 5);
test_cond("Heaviest items are in the list", idx1 != -1 && idx2 != -1 && idx3 != -1);
test_cond("item5 has the highest count", idx1 == 0);
zfree(list);
chkTopKRelease(topk);
}
static void testHeavierElementsReplaceLighter(void) {
int k = 5;
int numbuckets = 64;
double decay = 0.9;
chkTopK *topk = chkTopKCreate(k, numbuckets, decay);
test_cond("Create topk structure for replacement test", topk != NULL);
if (topk == NULL) return;
chkTopKUpdateAndFreeExpelled(topk, "light1", 6, 50);
chkTopKUpdateAndFreeExpelled(topk, "light2", 6, 60);
chkTopKUpdateAndFreeExpelled(topk, "light3", 6, 70);
chkTopKUpdateAndFreeExpelled(topk, "light4", 6, 80);
chkTopKUpdateAndFreeExpelled(topk, "light5", 6, 90);
chkHeapBucket *list1 = chkTopKList(topk);
test_cond("Initial topk list is not NULL", list1 != NULL);
if (list1 == NULL) {
chkTopKRelease(topk);
return;
}
int light1_idx = findItemInList(list1, k, "light1", 6);
int light2_idx = findItemInList(list1, k, "light2", 6);
int light3_idx = findItemInList(list1, k, "light3", 6);
int light4_idx = findItemInList(list1, k, "light4", 6);
int light5_idx = findItemInList(list1, k, "light5", 6);
test_cond("light1 is in initial topk list", light1_idx != -1);
test_cond("light2 is in initial topk list", light2_idx != -1);
test_cond("light3 is in initial topk list", light3_idx != -1);
test_cond("light4 is in initial topk list", light4_idx != -1);
test_cond("light5 is in initial topk list", light5_idx != -1);
zfree(list1);
chkTopKUpdateAndFreeExpelled(topk, "heavy1", 6, 500);
chkTopKUpdateAndFreeExpelled(topk, "heavy2", 6, 600);
chkHeapBucket *list2 = chkTopKList(topk);
test_cond("Updated topk list is not NULL", list2 != NULL);
if (list2 == NULL) {
chkTopKRelease(topk);
return;
}
int heavy1_idx = findItemInList(list2, k, "heavy1", 6);
int heavy2_idx = findItemInList(list2, k, "heavy2", 6);
test_cond("heavy1 is in updated topk list", heavy1_idx != -1);
test_cond("heavy2 is in updated topk list", heavy2_idx != -1);
light1_idx = findItemInList(list2, k, "light1", 6);
light2_idx = findItemInList(list2, k, "light2", 6);
light3_idx = findItemInList(list2, k, "light3", 6);
light4_idx = findItemInList(list2, k, "light4", 6);
light5_idx = findItemInList(list2, k, "light5", 6);
int light_items_remaining = (light1_idx != -1 ? 1 : 0) +
(light2_idx != -1 ? 1 : 0) +
(light3_idx != -1 ? 1 : 0) +
(light4_idx != -1 ? 1 : 0) +
(light5_idx != -1 ? 1 : 0);
test_cond("Some lighter items remain in the list after adding heavier ones",
light_items_remaining > 0);
zfree(list2);
chkTopKRelease(topk);
}
static void testManySmallWeightUpdates(void) {
int k = 2;
int numbuckets = 64;
double decay = 0.9;
chkTopK *topk = chkTopKCreate(k, numbuckets, decay);
test_cond("Create topk structure for small weight updates test", topk != NULL);
if (topk == NULL) return;
chkTopKUpdateAndFreeExpelled(topk, "item0", 5, 50);
chkTopKUpdateAndFreeExpelled(topk, "item1", 5, 100);
chkHeapBucket *list1 = chkTopKList(topk);
test_cond("Topk list after adding item0 and item1 is not NULL", list1 != NULL);
if (list1 == NULL) {
chkTopKRelease(topk);
return;
}
int item0_idx1 = findItemInList(list1, k, "item0", 5);
int item1_idx1 = findItemInList(list1, k, "item1", 5);
test_cond("item0 and item1 are in topk after initial updates",
item0_idx1 != -1 && item1_idx1 != -1);
zfree(list1);
for (int i = 0; i < 100; i++) {
chkTopKUpdateAndFreeExpelled(topk, "item2", 5, 1);
}
chkHeapBucket *list2 = chkTopKList(topk);
test_cond("Topk list after many small updates is not NULL", list2 != NULL);
if (list2 == NULL) {
chkTopKRelease(topk);
return;
}
int item0_idx2 = findItemInList(list2, k, "item0", 5);
int item1_idx2 = findItemInList(list2, k, "item1", 5);
int item2_idx2 = findItemInList(list2, k, "item2", 5);
test_cond("item1 and item2 are in topk, item0 is not",
item1_idx2 != -1 && item2_idx2 != -1 && item0_idx2 == -1);
counter_t item1_count = 0;
counter_t item2_count = 0;
if (item1_idx2 != -1) item1_count = list2[item1_idx2].count;
if (item2_idx2 != -1) item2_count = list2[item2_idx2].count;
test_cond("item1 and item2 have similar weights", item1_count > 0 && item2_count > 0 &&
(item1_count > item2_count ? item1_count - item2_count : item2_count - item1_count) < 5);
zfree(list2);
chkTopKRelease(topk);
}
int chkTopKTest(int argc, char *argv[], int flags) {
UNUSED(argc);
UNUSED(argv);
UNUSED(flags);
testBasicTopK();
testHeavierElementsReplaceLighter();
testManySmallWeightUpdates();
return 0;
}
#endif /* REDIS_TEST */
+89
View File
@@ -0,0 +1,89 @@
/* Implementation of a topK structure using CuckooHeavyKeeper algorithm
*
* Implementation is based on the paper "Cuckoo Heavy Keeper and the balancing
* act of maintaining heavy hitters in stream processing" by Vinh Quang Ngo and
* Marina Papatriantafilou. Also, the accompanying C++ implementation was used
* as a reference point: https://github.com/vinhqngo5/Cuckoo_Heavy_Keeper
* Main changes are addition of a min-heap so we can keep names of the top K
* elements - idea comes from RedisBloom's TopK structure.
*
* Copyright (c) 2026-Present, Redis Ltd.
* All rights reserved.
*
* Licensed under your choice of (a) the Redis Source Available License 2.0
* (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the
* GNU Affero General Public License v3 (AGPLv3).
*/
#pragma once
#include "sds.h"
#include <stddef.h>
#include <stdint.h>
#define CHK_LUT_SIZE 256
#define CHK_HEAVY_ENTRIES_PER_BUCKET 2
#define CHK_NUM_TABLES 2
typedef uint64_t counter_t;
typedef uint16_t fingerprint_t;
typedef uint8_t lobby_counter_t;
typedef struct {
counter_t count;
fingerprint_t fp;
} chkHeavyEntry;
typedef struct {
fingerprint_t fp;
lobby_counter_t count;
} chkLobbyEntry;
typedef struct {
chkHeavyEntry heavy_entries[CHK_HEAVY_ENTRIES_PER_BUCKET];
chkLobbyEntry lobby_entry;
} chkBucket;
typedef struct {
counter_t count;
sds item;
uint64_t fp; /* Fingerprint used to identify the item. Internal use only */
} chkHeapBucket;
typedef struct chkTopK {
chkBucket *tables[CHK_NUM_TABLES]; /* Cuckoo tables */
chkHeapBucket *heap; /* Min-heap for storing top-K item's names */
size_t alloc_size; /* Used for memory tracking only */
/* Expected number of operations to decay count i to 0 */
double lut_decay_exp[CHK_LUT_SIZE + 1];
/* Minimum number of decay operations to decay count i with 1 */
double lut_min_decay[CHK_LUT_SIZE + 1];
/* Probability of decaying i with 1. As per paper probability is decay^-i
* but we actually store (1/decay)^i for faster computation. */
double lut_decay_prob[CHK_LUT_SIZE + 1];
double decay; /* Decay constant */
double inv_decay; /* Cache 1/decay for faster computations */
counter_t total; /* Total recorded count for all updates */
int k;
int numbuckets;
} chkTopK;
chkTopK *chkTopKCreate(int k, int numbuckets, double decay);
void chkTopKRelease(chkTopK *topk);
sds chkTopKUpdate(chkTopK *topk, char *item, int itemlen, counter_t weight);
chkHeapBucket *chkTopKList(chkTopK *topk);
size_t chkTopKGetMemoryUsage(chkTopK *topk);
#ifdef REDIS_TEST
int chkTopKTest(int argc, char *argv[], int flags);
#endif /* REDIS_TEST */
+10 -3
View File
@@ -324,11 +324,18 @@ void parseRedisUri(const char *uri, const char* tool_name, cliConnInfo *connInfo
/* Extract user info. */
if ((userinfo = strchr(curr,'@'))) {
if ((username = strchr(curr, ':')) && username < userinfo) {
connInfo->user = percentDecode(curr, username - curr);
/* Free any value previously set via --user / -a (later
* parameters override earlier ones) and use NULL for an
* explicitly empty component, so cliAuth() falls back to the
* legacy single-argument AUTH (empty username) or skips AUTH
* entirely (empty password) instead of sending an empty ACL
* component, which the server rejects. */
sdsfree(connInfo->user);
connInfo->user = (username > curr) ? percentDecode(curr, username - curr) : NULL;
curr = username + 1;
}
connInfo->auth = percentDecode(curr, userinfo - curr);
sdsfree(connInfo->auth);
connInfo->auth = (userinfo > curr) ? percentDecode(curr, userinfo - curr) : NULL;
curr = userinfo + 1;
}
if (curr == end) return;
+301 -68
View File
@@ -24,6 +24,7 @@
#include "cluster_slot_stats.h"
#include <ctype.h>
#include "bio.h"
/* -----------------------------------------------------------------------------
* Key space handling
@@ -92,6 +93,10 @@ void createDumpPayload(rio *payload, robj *o, robj *key, int dbid, int skip_chec
/* Serialize the object in an RDB-like format. It consist of an object type
* byte followed by the serialized object. This is understood by RESTORE. */
rioInitWithBuffer(payload,sdsempty());
/* Save key metadata if present without (handles TTL separately via command args) */
if (getModuleMetaBits(o->metabits))
serverAssert(rdbSaveKeyMetadata(payload, key, o, dbid) != -1);
serverAssert(rdbSaveObjectType(payload,o));
serverAssert(rdbSaveObject(payload,o,key,dbid));
@@ -240,9 +245,28 @@ void restoreCommand(client *c) {
}
rioInitWithBuffer(&payload,c->argv[3]->ptr);
if (((type = rdbLoadObjectType(&payload)) == -1) ||
((obj = rdbLoadObject(type,&payload,key->ptr,c->db->id,NULL)) == NULL))
/* Initialize metadata spec to collect metadata+expiry from payload. */
KeyMetaSpec keymeta;
keyMetaSpecInit(&keymeta);
/* Compute TTL early so we can add it to metadata spec in correct order */
if (ttl) {
if (!absttl) ttl+=commandTimeSnapshot();
keyMetaSpecAdd(&keymeta, KEY_META_ID_EXPIRE, ttl);
}
/* With metadata, type = RDB_OPCODE_KEY_META. Layout: [<META>,]<TYPE>,<KEY>,<VALUE> */
type = rdbLoadType(&payload);
if (rdbResolveKeyType(&payload, &type, c->db->id, &keymeta) == -1) {
addReplyError(c,"Bad data format");
return;
}
/* Load the object */
if ((obj = rdbLoadObject(type,&payload,key->ptr,c->db->id,NULL)) == NULL)
{
keyMetaSpecCleanup(&keymeta);
addReplyError(c,"Bad data format");
return;
}
@@ -252,31 +276,39 @@ void restoreCommand(client *c) {
if (replace)
deleted = dbDelete(c->db,key);
if (ttl && !absttl) ttl+=commandTimeSnapshot();
if (ttl && checkAlreadyExpired(ttl)) {
if (deleted) {
robj *aux = server.lazyfree_lazy_server_del ? shared.unlink : shared.del;
rewriteClientCommandVector(c, 2, aux, key);
signalModifiedKey(c,c->db,key);
keyModified(c,c->db,key,NULL,1);
notifyKeyspaceEvent(NOTIFY_GENERIC,"del",key,c->db->id);
server.dirty++;
}
/* Update the stats, see setGenericCommand for details. */
server.stat_expiredkeys++;
keyMetaSpecCleanup(&keymeta);
decrRefCount(obj);
addReply(c, shared.ok);
return;
}
/* Create the key and set the TTL if any */
kvobj *kv = dbAddInternal(c->db, key, &obj, NULL, ttl ? ttl : -1);
kvobj *kv = dbAddInternal(c->db, key, &obj, NULL, &keymeta);
/* Save type: kv may be reallocated by module callbacks during notifyKeyspaceEvent below. */
int kvtype = kv->type;
/* If minExpiredField was set, then the object is hash with expiration
* on fields and need to register it in global HFE DS */
if (kv->type == OBJ_HASH) {
if (kvtype == OBJ_HASH) {
uint64_t minExpiredField = hashTypeGetMinExpire(kv, 1);
if (minExpiredField != EB_EXPIRE_TIME_INVALID)
estoreAdd(c->db->subexpires, getKeySlot(key->ptr), kv, minExpiredField);
}
if (kvtype == OBJ_STREAM)
streamKeyLoaded(c->db, key, kv);
if (ttl) {
if (!absttl) {
/* Propagate TTL as absolute timestamp */
@@ -287,14 +319,15 @@ void restoreCommand(client *c) {
}
}
objectSetLRUOrLFU(kv, lfu_freq, lru_idle, lru_clock, 1000);
signalModifiedKey(c,c->db,key);
keyModified(c,c->db,key,NULL,1);
notifyKeyspaceEvent(NOTIFY_GENERIC,"restore",key,c->db->id);
KSN_INVALIDATE_KVOBJ(kv);
/* If we deleted a key that means REPLACE parameter was passed and the
* destination key existed. */
if (deleted) {
notifyKeyspaceEvent(NOTIFY_OVERWRITTEN, "overwritten", key, c->db->id);
if (oldtype != kv->type) {
if (oldtype != kvtype) {
notifyKeyspaceEvent(NOTIFY_TYPE_CHANGED, "type_changed", key, c->db->id);
}
}
@@ -660,7 +693,7 @@ void migrateCommand(client *c) {
if (!copy) {
/* No COPY option: remove the local key, signal the change. */
dbDelete(c->db,keyArray[j]);
signalModifiedKey(c,c->db,keyArray[j]);
keyModified(c,c->db,keyArray[j],NULL,1);
notifyKeyspaceEvent(NOTIFY_GENERIC,"del",keyArray[j],c->db->id);
server.dirty++;
@@ -771,7 +804,12 @@ int verifyClusterNodeId(const char *name, int length) {
}
int isValidAuxChar(int c) {
return isalnum(c) || (strchr("!#$%&()*+:;<>?@[]^{|}~", c) == NULL);
/* Reject control characters (0x00-0x1F and 0x7F). */
if (iscntrl(c)) {
return 0;
}
/* Reject forbidden characters including nodes.conf delimiters and special parsing characters */
return isalnum(c) || (strchr("!#$%&()*+:;<>?@[]^{|}~,= \"'\\", c) == NULL);
}
int isValidAuxString(char *s, unsigned int length) {
@@ -1054,16 +1092,16 @@ void clusterCommand(client *c) {
unsigned int keys_in_slot = countKeysInSlot(slot);
unsigned int numkeys = maxkeys > keys_in_slot ? keys_in_slot : maxkeys;
addReplyArrayLen(c,numkeys);
kvstoreDictIterator *kvs_di = NULL;
kvstoreDictIterator kvs_di;
dictEntry *de = NULL;
kvs_di = kvstoreGetDictIterator(server.db->keys, slot);
kvstoreInitDictIterator(&kvs_di, server.db->keys, slot);
for (unsigned int i = 0; i < numkeys; i++) {
de = kvstoreDictIteratorNext(kvs_di);
de = kvstoreDictIteratorNext(&kvs_di);
serverAssert(de != NULL);
sds sdskey = kvobjGetKey(dictGetKV(de));
addReplyBulkCBuffer(c, sdskey, sdslen(sdskey));
}
kvstoreReleaseDictIterator(kvs_di);
kvstoreResetDictIterator(&kvs_di);
} else if ((!strcasecmp(c->argv[1]->ptr,"slaves") ||
!strcasecmp(c->argv[1]->ptr,"replicas")) && c->argc == 3) {
/* CLUSTER SLAVES <NODE ID> */
@@ -1089,6 +1127,10 @@ void clusterCommand(client *c) {
addReplyBulkCString(c,ni);
sdsfree(ni);
}
} else if (!strcasecmp(c->argv[1]->ptr, "migration")) {
clusterMigrationCommand(c);
} else if (!strcasecmp(c->argv[1]->ptr,"syncslots") && c->argc >= 3) {
clusterSyncSlotsCommand(c);
} else if(!clusterCommandSpecial(c)) {
addReplySubcommandSyntaxError(c);
return;
@@ -1096,16 +1138,14 @@ void clusterCommand(client *c) {
}
/* Extract slot number from keys in a keys_result structure and return to caller.
* Returns INVALID_CLUSTER_SLOT if keys belong to different slots (cross-slot error),
* or if there are no keys.
*/
* Returns:
* - The slot number if all keys belong to the same slot
* - INVALID_CLUSTER_SLOT if there are no keys or cluster is disabled
* - CLUSTER_CROSSSLOT if keys belong to different slots (cross-slot error) */
int extractSlotFromKeysResult(robj **argv, getKeysResult *keys_result) {
if (keys_result->numkeys == 0)
if (keys_result->numkeys == 0 || !server.cluster_enabled)
return INVALID_CLUSTER_SLOT;
if (!server.cluster_enabled)
return 0;
int first_slot = INVALID_CLUSTER_SLOT;
for (int j = 0; j < keys_result->numkeys; j++) {
robj *this_key = argv[keys_result->keys[j].pos];
@@ -1114,7 +1154,7 @@ int extractSlotFromKeysResult(robj **argv, getKeysResult *keys_result) {
if (first_slot == INVALID_CLUSTER_SLOT)
first_slot = this_slot;
else if (first_slot != this_slot) {
return INVALID_CLUSTER_SLOT;
return CLUSTER_CROSSSLOT;
}
}
return first_slot;
@@ -1150,6 +1190,8 @@ int extractSlotFromKeysResult(robj **argv, getKeysResult *keys_result) {
* already "down" but it is fragile to rely on the update of the global state,
* so we also handle it here.
*
* CLUSTER_REDIR_TRIMMING if the request addresses a slot that is being trimmed.
*
* CLUSTER_REDIR_DOWN_STATE and CLUSTER_REDIR_DOWN_RO_STATE if the cluster is
* down but the user attempts to execute a command that addresses one or more keys. */
clusterNode *getNodeByQuery(client *c, struct redisCommand *cmd, robj **argv, int argc, int *hashslot,
@@ -1240,6 +1282,7 @@ clusterNode *getNodeByQuery(client *c, struct redisCommand *cmd, robj **argv, in
/* The command has keys and was checked for cross-slot between its keys in preprocessCommand() */
if (pcmd->read_error == CLIENT_READ_CROSS_SLOT) {
/* Error: multiple keys from different slots. */
if (!use_cache_keys_result) getKeysFreeResult(&result);
if (error_code)
*error_code = CLUSTER_REDIR_CROSS_SLOT;
return NULL;
@@ -1391,6 +1434,15 @@ clusterNode *getNodeByQuery(client *c, struct redisCommand *cmd, robj **argv, in
return myself;
}
/* If this node is responsible for the slot and is currently trimming it,
* SFLUSH may have triggered active trimming and it could still be in progress.
* Here we reject any write commands as no writes should be accepted for
* trimming slots while active trimming is in progress. */
if (n == myself && is_write_command && isSlotInTrimJob(slot)) {
if (error_code) *error_code = CLUSTER_REDIR_TRIMMING;
return NULL;
}
/* Base case: just return the right node. However, if this node is not
* myself, set error_code to MOVED since we need to issue a redirection. */
if (n != myself && error_code) *error_code = CLUSTER_REDIR_MOVED;
@@ -1427,6 +1479,8 @@ void clusterRedirectClient(client *c, clusterNode *n, int hashslot, int error_co
"-%s %d %s:%d",
(error_code == CLUSTER_REDIR_ASK) ? "ASK" : "MOVED",
hashslot, clusterNodePreferredEndpoint(n), port));
} else if (error_code == CLUSTER_REDIR_TRIMMING) {
addReplyError(c,"-TRYAGAIN Slot is being trimmed");
} else {
serverPanic("getNodeByQuery() unknown error.");
}
@@ -1670,16 +1724,16 @@ unsigned int clusterDelKeysInSlot(unsigned int hashslot, int by_command) {
if (!kvstoreDictSize(server.db->keys, (int) hashslot))
return 0;
kvstoreDictIterator *kvs_di = NULL;
kvstoreDictIterator kvs_di;
dictEntry *de = NULL;
kvs_di = kvstoreGetDictSafeIterator(server.db->keys, (int) hashslot);
while((de = kvstoreDictIteratorNext(kvs_di)) != NULL) {
kvstoreInitDictSafeIterator(&kvs_di, server.db->keys, (int) hashslot);
while((de = kvstoreDictIteratorNext(&kvs_di)) != NULL) {
enterExecutionUnit(1, 0);
sds sdskey = kvobjGetKey(dictGetKV(de));
robj *key = createStringObject(sdskey, sdslen(sdskey));
dbDelete(&server.db[0], key);
signalModifiedKey(NULL, &server.db[0], key);
keyModified(NULL, &server.db[0], key, NULL, 1);
if (by_command) {
/* Keys are deleted by a command (trimslots), we need to notify the
* keyspace event. Though, we don't need to propagate the DEL
@@ -1692,7 +1746,7 @@ unsigned int clusterDelKeysInSlot(unsigned int hashslot, int by_command) {
* just moved to another node. The modules needs to know that these
* keys are no longer available locally, so just send the keyspace
* notification to the modules, but not to clients. */
moduleNotifyKeyspaceEvent(NOTIFY_GENERIC, "del", key, server.db[0].id);
moduleNotifyKeyspaceEvent(NOTIFY_GENERIC, "del", key, server.db[0].id, NULL, 0);
}
exitExecutionUnit();
postExecutionUnitOperations();
@@ -1700,7 +1754,7 @@ unsigned int clusterDelKeysInSlot(unsigned int hashslot, int by_command) {
j++;
server.dirty++;
}
kvstoreReleaseDictIterator(kvs_di);
kvstoreResetDictIterator(&kvs_di);
return j;
}
@@ -1719,18 +1773,18 @@ int clusterIsMySlot(int slot) {
return getMyClusterNode() == getNodeBySlot(slot);
}
void replySlotsFlushAndFree(client *c, slotRangeArray *slots) {
void replySlotsFlush(client *c, slotRangeArray *slots) {
addReplyArrayLen(c, slots->num_ranges);
for (int i = 0 ; i < slots->num_ranges ; i++) {
addReplyArrayLen(c, 2);
addReplyLongLong(c, slots->ranges[i].start);
addReplyLongLong(c, slots->ranges[i].end);
}
slotRangeArrayFree(slots);
}
/* Checks that slot ranges are well-formed and non-overlapping. */
int validateSlotRanges(slotRangeArray *slots, sds *err) {
/* Normalizes (sorts and merges adjacent ranges), checks that slot ranges are
* well-formed and non-overlapping. */
int slotRangeArrayNormalizeAndValidate(slotRangeArray *slots, sds *err) {
unsigned char used_slots[CLUSTER_SLOTS] = {0};
if (slots->num_ranges <= 0 || slots->num_ranges >= CLUSTER_SLOTS) {
@@ -1738,6 +1792,9 @@ int validateSlotRanges(slotRangeArray *slots, sds *err) {
return C_ERR;
}
/* Sort and merge adjacent slot ranges. */
slotRangeArraySortAndMerge(slots);
for (int i = 0; i < slots->num_ranges; i++) {
if (slots->ranges[i].start >= CLUSTER_SLOTS ||
slots->ranges[i].end >= CLUSTER_SLOTS)
@@ -1787,6 +1844,7 @@ void slotRangeArraySet(slotRangeArray *slots, int idx, int start, int end) {
/* Create a slot range string in the format of: "1000-2000 3000-4000 ..." */
sds slotRangeArrayToString(slotRangeArray *slots) {
sds s = sdsempty();
if (slots == NULL || slots->num_ranges == 0) return s;
for (int i = 0; i < slots->num_ranges; i++) {
slotRange *sr = &slots->ranges[i];
@@ -1824,7 +1882,7 @@ slotRangeArray *slotRangeArrayFromString(sds data) {
/* Validate all ranges */
sds err_msg = NULL;
if (validateSlotRanges(slots, &err_msg) != C_OK) {
if (slotRangeArrayNormalizeAndValidate(slots, &err_msg) != C_OK) {
if (err_msg) sdsfree(err_msg);
goto err;
}
@@ -1845,13 +1903,32 @@ static int compareSlotRange(const void *a, const void *b) {
return 0;
}
/* Sort slot ranges by start slot and merge adjacent ranges.
* Adjacent means: prev.end + 1 == next.start.
* e.g. 1000-2000 2001-3000 0-100 => 0-100 1000-3000
*
* Note: Overlapping ranges are not merged.*/
void slotRangeArraySortAndMerge(slotRangeArray *slots) {
if (!slots || slots->num_ranges <= 1) return;
qsort(slots->ranges, slots->num_ranges, sizeof(slotRange), compareSlotRange);
int idx = 0;
for (int i = 1; i < slots->num_ranges; i++) {
if (slots->ranges[idx].end + 1 == slots->ranges[i].start)
slots->ranges[idx].end = slots->ranges[i].end;
else
slots->ranges[++idx] = slots->ranges[i];
}
slots->num_ranges = idx + 1;
}
/* Compare two slot range arrays, return 1 if equal, 0 otherwise */
int slotRangeArrayIsEqual(slotRangeArray *slots1, slotRangeArray *slots2) {
if (slots1->num_ranges != slots2->num_ranges) return 0;
slotRangeArraySortAndMerge(slots1);
slotRangeArraySortAndMerge(slots2);
/* Sort slot ranges first */
qsort(slots1->ranges, slots1->num_ranges, sizeof(slotRange), compareSlotRange);
qsort(slots2->ranges, slots2->num_ranges, sizeof(slotRange), compareSlotRange);
if (slots1->num_ranges != slots2->num_ranges) return 0;
for (int i = 0; i < slots1->num_ranges; i++) {
if (slots1->ranges[i].start != slots2->ranges[i].start ||
@@ -1924,6 +2001,19 @@ void slotRangeArrayFreeGeneric(void *slots) {
slotRangeArrayFree(slots);
}
/* Returns the number of keys in the given slot ranges. */
unsigned long long getKeyCountInSlotRangeArray(slotRangeArray *slots) {
if (!slots) return 0;
unsigned long long key_count = 0;
for (int i = 0; i < slots->num_ranges; i++) {
for (int j = slots->ranges[i].start; j <= slots->ranges[i].end; j++) {
key_count += countKeysInSlot(j);
}
}
return key_count;
}
/* Slot range array iterator */
slotRangeArrayIter *slotRangeArrayGetIterator(slotRangeArray *slots) {
slotRangeArrayIter *it = zmalloc(sizeof(*it));
@@ -1957,13 +2047,18 @@ void slotRangeArrayIteratorFree(slotRangeArrayIter *it) {
zfree(it);
}
/* Parse slot ranges from the command arguments. Returns NULL on error. */
/* Parse slot range pairs from argv starting at `pos`.
* `argc` is the argument count, `pos` is the first slot argument index.
* Returns a slotRangeArray or NULL on error. */
slotRangeArray *parseSlotRangesOrReply(client *c, int argc, int pos) {
int start, end, count;
slotRangeArray *slots;
serverAssert(pos <= argc);
serverAssert((argc - pos) % 2 == 0);
/* Ensure there is at least one (start,end) slot range pairs. */
if (argc < 0 || pos < 0 || pos >= argc || (argc - pos) < 2 || ((argc - pos) % 2) != 0) {
addReplyErrorArity(c);
return NULL;
}
count = (argc - pos) / 2;
slots = slotRangeArrayCreate(count);
@@ -1981,7 +2076,7 @@ slotRangeArray *parseSlotRangesOrReply(client *c, int argc, int pos) {
}
sds err = NULL;
if (validateSlotRanges(slots, &err) != C_OK) {
if (slotRangeArrayNormalizeAndValidate(slots, &err) != C_OK) {
addReplyErrorSds(c, err);
slotRangeArrayFree(slots);
return NULL;
@@ -2012,17 +2107,26 @@ int clusterCanAccessKeysInSlot(int slot) {
return 0;
}
/* Return the slot ranges that belong to the current node or its master. */
/* Return the slot ranges that belong to the current node or its master.
* In non-cluster mode, returns the full slot range (0-16383). */
slotRangeArray *clusterGetLocalSlotRanges(void) {
slotRangeArray *slots = NULL;
if (!server.cluster_enabled) {
slots = slotRangeArrayCreate(1);
slotRangeArray *slots = slotRangeArrayCreate(1);
slotRangeArraySet(slots, 0, 0, CLUSTER_SLOTS - 1);
return slots;
}
clusterNode *master = clusterNodeGetMaster(getMyClusterNode());
return clusterGetNodeSlotRanges(getMyClusterNode());
}
/* Returns the slot ranges owned by the given node.
* If the node is a replica, the master's slot ranges are returned.
* Returns an empty array if the node has no slots. */
slotRangeArray *clusterGetNodeSlotRanges(clusterNode *node) {
slotRangeArray *slots = NULL;
serverAssert(server.cluster_enabled && node != NULL);
clusterNode *master = clusterNodeGetMaster(node);
if (master) {
for (int i = 0; i < CLUSTER_SLOTS; i++) {
if (clusterNodeCoversSlot(master, i))
@@ -2036,16 +2140,18 @@ slotRangeArray *clusterGetLocalSlotRanges(void) {
*
* Usage: SFLUSH <start-slot> <end slot> [<start-slot> <end slot>]* [SYNC|ASYNC]
*
* This is an initial implementation of SFLUSH (slots flush) which is limited to
* flushing a single shard as a whole, but in the future the same command may be
* used to partially flush a shard based on hash slots. Currently only if provided
* slots cover entirely the slots of a node, the node will be flushed and the
* return value will be pairs of slot ranges. Otherwise, a single empty set will
* be returned. If possible, SFLUSH SYNC will be run as blocking ASYNC as an
* Redis will flush the slots that belong to this node and reply with the flushed
* slot ranges. If no slot is flushed, an empty array will be returned.
*
* e.g. Node owns slot 100-200, user issues SFLUSH 50 150
* Redis will flush slot 100-150 and reply with [100,150]
*
* If possible, SFLUSH SYNC will be run as blocking ASYNC as an
* optimization.
*/
void sflushCommand(client *c) {
int flags = EMPTYDB_NO_FLAGS, argc = c->argc;
int trim_method = ASM_TRIM_METHOD_NONE;
if (server.cluster_enabled == 0) {
addReplyError(c,"This instance has cluster support disabled");
@@ -2073,40 +2179,87 @@ void sflushCommand(client *c) {
slotRangeArray *slots = parseSlotRangesOrReply(c, argc, 1);
if (!slots) return;
/* If client is AOF or master, we must obey the slot ranges. */
int must_obey = mustObeyClient(c);
/* Iterate and find the slot ranges that belong to this node. Save them in
* a new slotRangeArray. It is allocated on heap since there is a chance
* that FLUSH SYNC will be running as blocking ASYNC and only later reply
* with slot ranges */
unsigned char slots_to_flush[CLUSTER_SLOTS] = {0}; /* Requested slots to flush */
slotRangeArray *myslots = NULL;
for (int i = 0; i < slots->num_ranges; i++) {
for (int j = slots->ranges[i].start; j <= slots->ranges[i].end; j++) {
if (clusterIsMySlot(j)) {
if (must_obey || clusterIsMySlot(j)) {
myslots = slotRangeArrayAppend(myslots, j);
slots_to_flush[j] = 1;
}
}
}
/* Verify that all slots of mynode got covered. See sflushCommand() comment. */
int all_slots_covered = 1;
for (int i = 0; i < CLUSTER_SLOTS; i++) {
if (clusterIsMySlot(i) && !slots_to_flush[i]) {
all_slots_covered = 0;
break;
}
}
if (myslots == NULL || !all_slots_covered) {
/* If no slots belong to this node, return empty array. */
if (myslots == NULL) {
addReplyArrayLen(c, 0);
slotRangeArrayFree(slots);
slotRangeArrayFree(myslots);
return;
}
slotRangeArrayFree(slots);
/* takes ownership of myslots */
asmTrimCtx *trim_ctx = asmTrimCtxCreate(myslots, server.db[0].keys);
/* Flush selected slots. If not flush as blocking async, then reply immediately */
if (flushCommandCommon(c, FLUSH_TYPE_SLOTS, flags, myslots) == 0)
replySlotsFlushAndFree(c, myslots);
/* If the selected slots are exactly the same as the local slots, we can
* simply flush the entire DB by flushCommandCommon. */
slotRangeArray *local_slots = clusterGetLocalSlotRanges();
int all_slots_covered = slotRangeArrayIsEqual(myslots, local_slots);
slotRangeArrayFree(local_slots);
if (all_slots_covered) {
/* If not flush as blocking async, then reply immediately */
if (flushCommandCommon(c, FLUSH_TYPE_SLOTS, flags, trim_ctx) == 0) {
replySlotsFlush(c, trim_ctx->slots);
}
asmTrimCtxRelease(trim_ctx);
return;
}
/* Cancel all ASM tasks that overlap with the given slot ranges. */
clusterAsmCancelBySlotRangeArray(myslots, c->argv[0]->ptr);
/* In case of SYNC, check if we can optimize and run it in bg as blocking ASYNC */
int blocking_async = 0;
if ((!(flags & EMPTYDB_ASYNC)) && (!(c->flags & CLIENT_AVOID_BLOCKING_ASYNC_FLUSH))) {
flags |= EMPTYDB_ASYNC; /* Run as ASYNC */
blocking_async = 1;
}
/* Trim the slots if running in async mode and not loading from AOF,
* otherwise delete the keys synchronously. */
if (flags & EMPTYDB_ASYNC && server.loading == 0) {
/* Update dirty stats before trimming. */
server.dirty += getKeyCountInSlotRangeArray(myslots);
/* Pass client id for active trim to unblock client when trim completes. */
trim_method = asmTrimSlots(trim_ctx, blocking_async ? c->id : CLIENT_ID_NONE, 0);
} else {
clusterDelKeysInSlotRangeArray(myslots, 1);
}
/* Without the forceCommandPropagation, when DB was already empty,
* SFLUSH will not be replicated nor put into the AOF. */
forceCommandPropagation(c, PROPAGATE_REPL | PROPAGATE_AOF);
/* Handle waiting for trim job to complete in case of blocking async flush.
* Block the client and schedule completion callback based on trim method:
* - BG trim uses BIO lazyfree worker to trim the slots, so schedule a new
* BIO lazyfree worker to wait for completion, then unblock client and reply.
* - Active trim works in cron job of the main thread, it will automatically
* unblock client and reply in active trim completion. */
if (blocking_async && trim_method != ASM_TRIM_METHOD_NONE) {
blockClientForAsyncFlush(c);
} else {
/* Reply with slot ranges that were flushed. SYNC and ASYNC mode will be
* replied here immediately. */
replySlotsFlush(c, trim_ctx->slots);
}
asmTrimCtxRelease(trim_ctx); /* if bg trim, released later by kvsAsyncFreeDoneCB() */
}
/* The READWRITE command just clears the READONLY command state. */
@@ -2127,3 +2280,83 @@ void resetClusterStats(void) {
clusterSlotStatResetAll();
}
/* This function is called at server startup in order to initialize cluster data
* structures that are shared between the different cluster implementations. */
void clusterCommonInit(void) {
resetClusterStats();
asmInit();
}
/* This function is called after the node startup in order to check if there
* are any slots that we have keys for, but are not assigned to us. If so,
* we delete the keys. */
void clusterDeleteKeysInUnownedSlots(void) {
if (clusterNodeIsSlave(getMyClusterNode())) return;
/* Check that all the slots we have keys for are assigned to us. Otherwise,
* delete the keys. */
for (int i = 0; i < CLUSTER_SLOTS; i++) {
/* Skip if: no keys in the slot, it's our slot, or we are importing it. */
if (!countKeysInSlot(i) ||
clusterIsMySlot(i) ||
getImportingSlotSource(i))
{
continue;
}
serverLog(LL_NOTICE, "I have keys for slot %d, but the slot is "
"assigned to another node. "
"Deleting keys in the slot.", i);
/* With atomic slot migration, it is safe to drop keys from slots
* that are not owned. This will not result in data loss under the
* legacy slot migration approach either, since the importing state
* has already been persisted in node.conf. */
clusterDelKeysInSlot(i, 0);
}
}
/* This function is called after the node startup in order to verify that data
* loaded from disk is in agreement with the cluster configuration:
*
* 1) If we find keys about hash slots we have no responsibility for, the
* following happens:
* A) If no other node is in charge according to the current cluster
* configuration, we add these slots to our node.
* B) If according to our config other nodes are already in charge for
* this slots, we set the slots as IMPORTING from our point of view
* in order to justify we have those slots, and in order to make
* redis-cli aware of the issue, so that it can try to fix it.
* 2) If we find data in a DB different than DB0 we return C_ERR to
* signal the caller it should quit the server with an error message
* or take other actions.
*
* The function always returns C_OK even if it will try to correct
* the error described in "1". However if data is found in DB different
* from DB0, C_ERR is returned.
*
* The function also uses the logging facility in order to warn the user
* about desynchronizations between the data we have in memory and the
* cluster configuration. */
int verifyClusterConfigWithData(void) {
/* Return ASAP if a module disabled cluster redirections. In that case
* every master can store keys about every possible hash slot. */
if (server.cluster_module_flags & CLUSTER_MODULE_FLAG_NO_REDIRECTION)
return C_OK;
/* If this node is a slave, don't perform the check at all as we
* completely depend on the replication stream. */
if (clusterNodeIsSlave(getMyClusterNode())) return C_OK;
/* Make sure we only have keys in DB0. */
for (int i = 1; i < server.dbnum; i++) {
if (kvstoreSize(server.db[i].keys)) return C_ERR;
}
/* Take over slots that we have keys for, but are assigned to no one. */
clusterClaimUnassignedSlots();
/* Delete keys in unowned slots */
clusterDeleteKeysInUnownedSlots();
return C_OK;
}
+8 -9
View File
@@ -23,6 +23,7 @@
#define CLUSTER_SLOTS (1<<CLUSTER_SLOT_MASK_BITS) /* Total number of slots in cluster mode, which is 16384. */
#define CLUSTER_SLOT_MASK ((unsigned long long)(CLUSTER_SLOTS - 1)) /* Bit mask for slot id stored in LSB. */
#define INVALID_CLUSTER_SLOT (-1) /* Invalid slot number. */
#define CLUSTER_CROSSSLOT (-2)
#define CLUSTER_OK 0 /* Everything looks ok */
#define CLUSTER_FAIL 1 /* The cluster can't work */
#define CLUSTER_NAMELEN 40 /* sha1 hex length */
@@ -36,17 +37,11 @@
#define CLUSTER_REDIR_DOWN_STATE 5 /* -CLUSTERDOWN, global state. */
#define CLUSTER_REDIR_DOWN_UNBOUND 6 /* -CLUSTERDOWN, unbound slot. */
#define CLUSTER_REDIR_DOWN_RO_STATE 7 /* -CLUSTERDOWN, allow reads. */
#define CLUSTER_REDIR_TRIMMING 8 /* -TRYAGAIN, slot is being trimmed. */
typedef struct _clusterNode clusterNode;
struct clusterState;
/* Struct used for storing slot statistics. */
typedef struct clusterSlotStat {
uint64_t cpu_usec; /* CPU time (in microseconds) spent on given slot */
uint64_t network_bytes_in; /* Network ingress (in bytes) received for given slot */
uint64_t network_bytes_out; /* Network egress (in bytes) sent for given slot */
} clusterSlotStat;
/* Flags that a module can set in order to prevent certain Redis Cluster
* features to be enabled. Useful when implementing a different distributed
* system on top of Redis Cluster message bus, using modules. */
@@ -86,8 +81,10 @@ static inline unsigned int keyHashSlot(const char *key, int keylen) {
/* functions requiring mechanism specific implementations */
void clusterInit(void);
void clusterInitLast(void);
void clusterCommonInit(void);
void clusterCron(void);
void clusterBeforeSleep(void);
void clusterClaimUnassignedSlots(void);
int verifyClusterConfigWithData(void);
int clusterSendModuleMessageToTarget(const char *target, uint64_t module_id, uint8_t type, const char *payload, uint32_t len);
@@ -157,6 +154,7 @@ int getSlotOrReply(client *c, robj *o);
int clusterIsMySlot(int slot);
int clusterCanAccessKeysInSlot(int slot);
struct slotRangeArray *clusterGetLocalSlotRanges(void);
struct slotRangeArray *clusterGetNodeSlotRanges(clusterNode *node);
/* functions with shared implementations */
clusterNode *getNodeByQuery(client *c, struct redisCommand *cmd, robj **argv, int argc, int *hashslot,
@@ -166,7 +164,6 @@ int clusterRedirectBlockedClientIfNeeded(client *c);
void clusterRedirectClient(client *c, clusterNode *n, int hashslot, int error_code);
void migrateCloseTimedoutSockets(void);
int patternHashSlot(char *pattern, int length);
int getSlotOrReply(client *c, robj *o);
int isValidAuxString(char *s, unsigned int length);
void migrateCommand(client *c);
void clusterCommand(client *c);
@@ -189,6 +186,7 @@ slotRangeArray *slotRangeArrayDup(slotRangeArray *slots);
void slotRangeArraySet(slotRangeArray *slots, int idx, int start, int end);
sds slotRangeArrayToString(slotRangeArray *slots);
slotRangeArray *slotRangeArrayFromString(sds data);
void slotRangeArraySortAndMerge(slotRangeArray *slots);
int slotRangeArrayIsEqual(slotRangeArray *slots1, slotRangeArray *slots2);
slotRangeArray *slotRangeArrayAppend(slotRangeArray *slots, int slot);
int slotRangeArrayContains(slotRangeArray *slots, unsigned int slot);
@@ -198,8 +196,9 @@ slotRangeArrayIter *slotRangeArrayGetIterator(slotRangeArray *slots);
int slotRangeArrayNext(slotRangeArrayIter *it);
int slotRangeArrayGetCurrentSlot(slotRangeArrayIter *it);
void slotRangeArrayIteratorFree(slotRangeArrayIter *it);
int validateSlotRanges(slotRangeArray *slots, sds *err);
int slotRangeArrayNormalizeAndValidate(slotRangeArray *slots, sds *err);
slotRangeArray *parseSlotRangesOrReply(client *c, int argc, int pos);
unsigned long long getKeyCountInSlotRangeArray(slotRangeArray *slots);
unsigned int clusterDelKeysInSlot(unsigned int hashslot, int by_command);
unsigned int clusterDelKeysInSlotRangeArray(slotRangeArray *slots, int by_command);
+556 -210
View File
File diff suppressed because it is too large Load Diff
+15 -2
View File
@@ -15,6 +15,10 @@ struct asmTask;
struct slotRangeArray;
struct slotRange;
#define ASM_TRIM_METHOD_NONE 0
#define ASM_TRIM_METHOD_BG 1
#define ASM_TRIM_METHOD_ACTIVE 2
void asmInit(void);
void asmBeforeSleep(void);
void asmCron(void);
@@ -33,7 +37,7 @@ struct slotRangeArray *asmTaskGetSlotRanges(const char *task_id);
int asmNotifyConfigUpdated(struct asmTask *task, sds *err);
size_t asmGetPeakSyncBufferSize(void);
size_t asmGetImportInputBufferSize(void);
size_t asmGetMigrateOutputBufferSize(void);
size_t asmGetMigrateOutputMemoryUsage(void);
int clusterAsmCancel(const char *task_id, const char *reason);
int clusterAsmCancelBySlot(int slot, const char *reason);
int clusterAsmCancelBySlotRangeArray(struct slotRangeArray *slots, const char *reason);
@@ -51,7 +55,16 @@ void asmFinalizeMasterTask(void);
int asmIsTrimInProgress(void);
int asmGetTrimmingSlotForCommand(struct redisCommand *cmd, robj **argv, int argc);
void asmActiveTrimCycle(void);
int asmActiveTrimDelIfNeeded(redisDb *db, robj *key, kvobj *kv);
int asmIsKeyInTrimJob(sds keyname);
int asmModulePropagateBeforeSlotSnapshot(struct redisCommand *cmd, robj **argv, int argc);
int asmTrimSlots(struct asmTrimCtx *ctx, uint64_t client_id, int migration_cleanup);
int asmIsBgTrimRunning(void);
void asmBgTrimCounterDecr(void);
void asmBgTrimCounterIncr(void);
/* Context for ASM background trim */
struct asmTrimCtx *asmTrimCtxCreate(struct slotRangeArray *slots, kvstore *target_kvstore);
void asmTrimCtxRetain(struct asmTrimCtx *ctx);
void asmTrimCtxRelease(struct asmTrimCtx *ctx);
#endif
+77 -119
View File
@@ -1036,10 +1036,8 @@ void clusterInit(void) {
clusterUpdateMyselfIp();
clusterUpdateMyselfHostname();
clusterUpdateMyselfHumanNodename();
resetClusterStats();
getRandomHexChars(server.cluster->internal_secret, CLUSTER_INTERNALSECRETLEN);
asmInit();
}
void clusterInitLast(void) {
@@ -1729,8 +1727,10 @@ int clusterBumpConfigEpochWithoutConsensus(void) {
{
server.cluster->currentEpoch++;
myself->configEpoch = server.cluster->currentEpoch;
/* Save the new config epoch and broadcast it to the other nodes. */
clusterDoBeforeSleep(CLUSTER_TODO_SAVE_CONFIG|
CLUSTER_TODO_FSYNC_CONFIG);
CLUSTER_TODO_FSYNC_CONFIG|
CLUSTER_TODO_BROADCAST_PONG);
serverLog(LL_NOTICE,
"New configEpoch set to %llu",
(unsigned long long) myself->configEpoch);
@@ -1796,6 +1796,8 @@ void clusterHandleConfigEpochCollision(clusterNode *sender) {
server.cluster->currentEpoch++;
myself->configEpoch = server.cluster->currentEpoch;
clusterSaveConfigOrDie(1);
/* Broadcast new config epoch to the other nodes. */
clusterDoBeforeSleep(CLUSTER_TODO_BROADCAST_PONG);
serverLog(LL_VERBOSE,
"WARNING: configEpoch collision with node %.40s (%s)."
" configEpoch set to %llu",
@@ -2472,9 +2474,11 @@ void clusterUpdateSlotsConfigWith(clusterNode *sender, uint64_t senderConfigEpoc
"Configuration change detected. Reconfiguring myself "
"as a replica of %.40s (%s)", sender->name, sender->human_nodename);
clusterSetMaster(sender);
/* Save the new config and broadcast it to the other nodes. */
clusterDoBeforeSleep(CLUSTER_TODO_SAVE_CONFIG|
CLUSTER_TODO_UPDATE_STATE|
CLUSTER_TODO_FSYNC_CONFIG);
CLUSTER_TODO_FSYNC_CONFIG|
CLUSTER_TODO_BROADCAST_PONG);
} else if (myself->slaveof && myself->slaveof->slaveof &&
/* In some rare case when CLUSTER FAILOVER TAKEOVER is used, it
* can happen that myself is a replica of a replica of myself. If
@@ -2489,9 +2493,11 @@ void clusterUpdateSlotsConfigWith(clusterNode *sender, uint64_t senderConfigEpoc
"I'm a sub-replica! Reconfiguring myself as a replica of grandmaster %.40s (%s)",
myself->slaveof->slaveof->name, myself->slaveof->slaveof->human_nodename);
clusterSetMaster(myself->slaveof->slaveof);
/* Save the new config and broadcast to the other nodes. */
clusterDoBeforeSleep(CLUSTER_TODO_SAVE_CONFIG|
CLUSTER_TODO_UPDATE_STATE|
CLUSTER_TODO_FSYNC_CONFIG);
CLUSTER_TODO_FSYNC_CONFIG|
CLUSTER_TODO_BROADCAST_PONG);
} else if (dirty_slots_count && !asm_task) {
/* If we are here, we received an update message which removed
* ownership for certain slots we still have keys about, but still
@@ -2833,11 +2839,16 @@ int clusterProcessPacket(clusterLink *link) {
explen = sizeof(clusterMsg)-sizeof(union clusterMsgData);
explen += sizeof(clusterMsgDataFail);
} else if (type == CLUSTERMSG_TYPE_PUBLISH || type == CLUSTERMSG_TYPE_PUBLISHSHARD) {
uint32_t ch_len = ntohl(hdr->data.publish.msg.channel_len);
uint32_t msg_len = ntohl(hdr->data.publish.msg.message_len);
explen = sizeof(clusterMsg)-sizeof(union clusterMsgData);
explen += sizeof(clusterMsgDataPublish) -
8 +
ntohl(hdr->data.publish.msg.channel_len) +
ntohl(hdr->data.publish.msg.message_len);
explen += sizeof(clusterMsgDataPublish) - 8;
if (ch_len > UINT32_MAX - explen || msg_len > UINT32_MAX - explen - ch_len) {
serverLog(LL_WARNING, "Received invalid %s packet with overflow in length fields "
"(channel_len:%u, message_len:%u)", clusterGetMessageTypeString(type), ch_len, msg_len);
return 1;
}
explen += ch_len + msg_len;
} else if (type == CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST ||
type == CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK ||
type == CLUSTERMSG_TYPE_MFSTART)
@@ -2847,9 +2858,15 @@ int clusterProcessPacket(clusterLink *link) {
explen = sizeof(clusterMsg)-sizeof(union clusterMsgData);
explen += sizeof(clusterMsgDataUpdate);
} else if (type == CLUSTERMSG_TYPE_MODULE) {
uint32_t module_len = ntohl(hdr->data.module.msg.len);
explen = sizeof(clusterMsg)-sizeof(union clusterMsgData);
explen += sizeof(clusterMsgModule) -
3 + ntohl(hdr->data.module.msg.len);
explen += sizeof(clusterMsgModule) - 3;
if (module_len > UINT32_MAX - explen) {
serverLog(LL_WARNING, "Received invalid %s packet with overflow in length field "
"(len:%u)", clusterGetMessageTypeString(type), module_len);
return 1;
}
explen += module_len;
} else {
/* We don't know this type of packet, so we assume it's well formed. */
explen = totlen;
@@ -4272,7 +4289,7 @@ void clusterFailoverReplaceYourMaster(void) {
/* 4) Pong all the other nodes so that they can update the state
* accordingly and detect that we switched to master role. */
clusterBroadcastPong(CLUSTER_BROADCAST_ALL);
clusterDoBeforeSleep(CLUSTER_TODO_BROADCAST_PONG);
/* 5) If there was a manual failover in progress, clear the state. */
resetManualFailover();
@@ -4569,6 +4586,10 @@ void clusterHandleSlaveMigration(int max_slaves) {
serverLog(LL_NOTICE,"Migrating to orphaned master %.40s",
target->name);
clusterSetMaster(target);
/* Save the new config and broadcast it to the other nodes. */
clusterDoBeforeSleep(CLUSTER_TODO_SAVE_CONFIG|
CLUSTER_TODO_FSYNC_CONFIG|
CLUSTER_TODO_BROADCAST_PONG);
}
}
@@ -4922,9 +4943,6 @@ void clusterCron(void) {
if (update_state || server.cluster->state == CLUSTER_FAIL)
clusterUpdateState();
/* Atomic slot migration cron */
asmCron();
}
/* This function is called before the event handler returns to sleep for
@@ -4963,7 +4981,9 @@ void clusterBeforeSleep(void) {
clusterSaveConfigOrDie(fsync);
}
asmBeforeSleep();
/* Broadcast a PONG to all the nodes. */
if (flags & CLUSTER_TODO_BROADCAST_PONG)
clusterBroadcastPong(CLUSTER_BROADCAST_ALL);
}
void clusterDoBeforeSleep(int flags) {
@@ -5236,82 +5256,6 @@ void clusterUpdateState(void) {
}
}
/* This function is called after the node startup in order to verify that data
* loaded from disk is in agreement with the cluster configuration:
*
* 1) If we find keys about hash slots we have no responsibility for, the
* following happens:
* A) If no other node is in charge according to the current cluster
* configuration, we add these slots to our node.
* B) If according to our config other nodes are already in charge for
* this slots, we set the slots as IMPORTING from our point of view
* in order to justify we have those slots, and in order to make
* redis-cli aware of the issue, so that it can try to fix it.
* 2) If we find data in a DB different than DB0 we return C_ERR to
* signal the caller it should quit the server with an error message
* or take other actions.
*
* The function always returns C_OK even if it will try to correct
* the error described in "1". However if data is found in DB different
* from DB0, C_ERR is returned.
*
* The function also uses the logging facility in order to warn the user
* about desynchronizations between the data we have in memory and the
* cluster configuration. */
int verifyClusterConfigWithData(void) {
int j;
int update_config = 0;
/* Return ASAP if a module disabled cluster redirections. In that case
* every master can store keys about every possible hash slot. */
if (server.cluster_module_flags & CLUSTER_MODULE_FLAG_NO_REDIRECTION)
return C_OK;
/* If this node is a slave, don't perform the check at all as we
* completely depend on the replication stream. */
if (nodeIsSlave(myself)) return C_OK;
/* Make sure we only have keys in DB0. */
for (j = 1; j < server.dbnum; j++) {
if (kvstoreSize(server.db[j].keys)) return C_ERR;
}
/* Check that all the slots we see populated memory have a corresponding
* entry in the cluster table. Otherwise fix the table. */
for (j = 0; j < CLUSTER_SLOTS; j++) {
if (!countKeysInSlot(j)) continue; /* No keys in this slot. */
/* Check if we are assigned to this slot or if we are importing it.
* In both cases check the next slot as the configuration makes
* sense. */
if (server.cluster->slots[j] == myself ||
server.cluster->importing_slots_from[j] != NULL) continue;
/* If we are here data and cluster config don't agree, and we have
* slot 'j' populated even if we are not importing it, nor we are
* assigned to this slot. Fix this condition. */
update_config++;
/* Case A: slot is unassigned. Take responsibility for it. */
if (server.cluster->slots[j] == NULL) {
serverLog(LL_NOTICE, "I have keys for unassigned slot %d. "
"Taking responsibility for it.",j);
clusterAddSlot(myself,j);
} else {
serverLog(LL_NOTICE, "I have keys for slot %d, but the slot is "
"assigned to another node. "
"Deleting keys in the slot.", j);
/* With atomic slot migration, it is safe to drop keys from slots
* that are not owned. This will not result in data loss under the
* legacy slot migration approach either, since the importing state
* has already been persisted in node.conf. */
clusterDelKeysInSlot(j, 0);
}
}
if (update_config) clusterSaveConfigOrDie(1);
return C_OK;
}
/* Remove all the shard channel related information not owned by the current shard. */
static inline void removeAllNotOwnedShardChannelSubscriptions(void) {
if (!kvstoreSize(server.pubsubshard_channels)) return;
@@ -5323,6 +5267,33 @@ static inline void removeAllNotOwnedShardChannelSubscriptions(void) {
}
}
/* This function is called after the node startup in order to check if there
* are any slots that we have keys for, but are assigned to no one. If so,
* we take ownership of them. */
void clusterClaimUnassignedSlots(void) {
if (nodeIsSlave(myself)) return;
int update_config = 0;
for (int i = 0; i < CLUSTER_SLOTS; i++) {
/* Skip if: no keys, already has an owner, or we are importing it. */
if (!countKeysInSlot(i) ||
server.cluster->slots[i] != NULL ||
server.cluster->importing_slots_from[i] != NULL)
{
continue;
}
/* If we are here data and cluster config don't agree, and we have
* slot 'i' populated even if we are not importing it, nor anyone else
* is assigned to it. Fix this condition by taking ownership. */
update_config++;
serverLog(LL_NOTICE, "I have keys for unassigned slot %d. "
"Taking responsibility for it.", i);
clusterAddSlot(myself, i);
}
if (update_config) clusterSaveConfigOrDie(1);
}
/* -----------------------------------------------------------------------------
* SLAVE nodes handling
* -------------------------------------------------------------------------- */
@@ -6219,9 +6190,11 @@ int clusterCommandSpecial(client *c) {
"Configuration change detected. Reconfiguring myself "
"as a replica of %.40s (%s)", n->name, n->human_nodename);
clusterSetMaster(n);
/* Save the new config and broadcast it to the other nodes. */
clusterDoBeforeSleep(CLUSTER_TODO_SAVE_CONFIG |
CLUSTER_TODO_UPDATE_STATE |
CLUSTER_TODO_FSYNC_CONFIG);
CLUSTER_TODO_FSYNC_CONFIG |
CLUSTER_TODO_BROADCAST_PONG);
}
/* If this node was importing this slot, assigning the slot to
@@ -6244,7 +6217,7 @@ int clusterCommandSpecial(client *c) {
server.cluster->importing_slots_from[slot] = NULL;
/* After importing this slot, let the other nodes know as
* soon as possible. */
clusterBroadcastPong(CLUSTER_BROADCAST_ALL);
clusterDoBeforeSleep(CLUSTER_TODO_BROADCAST_PONG);
}
} else {
addReplyError(c,
@@ -6325,7 +6298,10 @@ int clusterCommandSpecial(client *c) {
/* Set the master. */
clusterSetMaster(n);
clusterDoBeforeSleep(CLUSTER_TODO_UPDATE_STATE|CLUSTER_TODO_SAVE_CONFIG);
/* Save the new config and broadcast it to the other nodes. */
clusterDoBeforeSleep(CLUSTER_TODO_UPDATE_STATE|
CLUSTER_TODO_SAVE_CONFIG|
CLUSTER_TODO_BROADCAST_PONG);
addReply(c,shared.ok);
} else if (!strcasecmp(c->argv[1]->ptr,"count-failure-reports") &&
c->argc == 3)
@@ -6460,10 +6436,6 @@ int clusterCommandSpecial(client *c) {
} else if (!strcasecmp(c->argv[1]->ptr,"links") && c->argc == 2) {
/* CLUSTER LINKS */
addReplyClusterLinksDescription(c);
} else if (!strcasecmp(c->argv[1]->ptr, "migration")) {
clusterMigrationCommand(c);
} else if (!strcasecmp(c->argv[1]->ptr,"syncslots") && c->argc >= 3) {
clusterSyncSlotsCommand(c);
} else {
return 0;
}
@@ -6575,22 +6547,17 @@ void clusterPromoteSelfToMaster(void) {
}
int clusterAsmOnEvent(const char *task_id, int event, void *arg) {
UNUSED(arg);
sds str = NULL;
slotRangeArray *slots = asmTaskGetSlotRanges(task_id);
if (slots) str = slotRangeArrayToString(slots);
else if (arg) str = slotRangeArrayToString(arg);
serverLog(LL_VERBOSE, "Slot migration task %s received event %d for slots: %s",
task_id, event, str ? str : "unknown");
switch (event) {
case ASM_EVENT_IMPORT_STARTED:
serverLog(LL_NOTICE, "Import task %s started for slots: %s", task_id, str);
break;
case ASM_EVENT_IMPORT_FAILED:
serverLog(LL_NOTICE, "Import task %s failed for slots: %s", task_id, str);
break;
case ASM_EVENT_TAKEOVER:
serverLog(LL_NOTICE, "Import task %s is ready to takeover slots: %s", task_id, str);
for (int i = 0; i < slots->num_ranges; i++) {
slotRange *sr = &slots->ranges[i];
for (int j = sr->start; j <= sr->end; j++) {
@@ -6598,31 +6565,22 @@ int clusterAsmOnEvent(const char *task_id, int event, void *arg) {
clusterAddSlot(myself, j);
}
}
/* New config and Bump new config */
/* Bump config epoch and broadcast the new config to the other nodes. */
clusterBumpConfigEpochWithoutConsensus();
clusterSaveConfigOrDie(1);
clusterBroadcastPong(CLUSTER_BROADCAST_ALL);
clusterDoBeforeSleep(CLUSTER_TODO_BROADCAST_PONG);
clusterAsmProcess(task_id, ASM_EVENT_DONE, NULL, NULL);
break;
case ASM_EVENT_IMPORT_COMPLETED:
serverLog(LL_NOTICE, "Import task %s completed for slots: %s", task_id, str);
break;
case ASM_EVENT_MIGRATE_STARTED:
serverLog(LL_NOTICE, "Migrate task %s started for slots: %s", task_id, str);
break;
case ASM_EVENT_MIGRATE_FAILED:
serverLog(LL_NOTICE, "Migrate task %s failed for slots: %s", task_id, str);
unpauseActions(PAUSE_DURING_SLOT_HANDOFF);
break;
case ASM_EVENT_HANDOFF_PREP:
serverLog(LL_NOTICE, "Migrate task %s preparing to handoff for slots: %s", task_id, str);
pauseActions(PAUSE_DURING_SLOT_HANDOFF,
LLONG_MAX,
PAUSE_ACTIONS_CLIENT_WRITE_SET);
clusterAsmProcess(task_id, ASM_EVENT_HANDOFF, NULL, NULL);
break;
case ASM_EVENT_MIGRATE_COMPLETED:
serverLog(LL_NOTICE, "Migrate task %s completed for slots: %s", task_id, str);
unpauseActions(PAUSE_DURING_SLOT_HANDOFF);
break;
default:
+1
View File
@@ -39,6 +39,7 @@
#define CLUSTER_TODO_SAVE_CONFIG (1<<2)
#define CLUSTER_TODO_FSYNC_CONFIG (1<<3)
#define CLUSTER_TODO_HANDLE_MANUALFAILOVER (1<<4)
#define CLUSTER_TODO_BROADCAST_PONG (1<<5)
/* clusterLink encapsulates everything needed to talk with a remote node. */
typedef struct clusterLink {

Some files were not shown because too many files have changed in this diff Show More