Skip to content

feat(streams): support explicit LIMIT 0 in XTRIM/XADD trimming via XTrimLimitDisabled sentinel#3848

Merged
ndyakov merged 1 commit into
redis:masterfrom
TheRealMal:feat/xtrim-limit-zero
Jun 11, 2026
Merged

feat(streams): support explicit LIMIT 0 in XTRIM/XADD trimming via XTrimLimitDisabled sentinel#3848
ndyakov merged 1 commit into
redis:masterfrom
TheRealMal:feat/xtrim-limit-zero

Conversation

@TheRealMal

@TheRealMal TheRealMal commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Motivation

Redis distinguishes between omitting the LIMIT clause and passing LIMIT 0 when trimming a stream with the approximate (~) strategy:

  • No LIMIT clause — Redis applies an implicit effort cap of 100 * stream-node-max-entries examined entries per call.
  • LIMIT 0 — the effort cap is disabled entirely; Redis trims as many macro nodes as the threshold requires in a single call.

(See the XADD / XTRIM docs: "Specifying the value 0 as count disables the limiting mechanism entirely.")

Today the library cannot express the second form. xTrim, xTrimMode, and XAdd all build the clause with:

if limit > 0 {
    args = append(args, "limit", limit)
}

so 0 is silently swallowed and there is no way to send XTRIM key MINID ~ <id> LIMIT 0 through the typed API. This matters in practice for periodic cleaners on busy streams: with the implicit default cap, a single XTRIM ... ~ call may not reclaim a large backlog, and the only workaround is dropping down to raw Do().

Changes

Introduces a documented negative-value sentinel, following the existing KeepTTL = -1 precedent in commands.go (the same "zero value already has a meaning, a third state is needed" problem for SET):

const XTrimLimitDisabled = -1

The limit parameter of XTrimMaxLenApprox, XTrimMinIDApprox, XTrimMaxLenApproxMode, XTrimMinIDApproxMode, and the XAddArgs.Limit field now have three documented states:

Value Emitted clause Notes
0 (none) unchanged historical behavior — Redis applies its implicit default cap
> 0 LIMIT <n> unchanged
< 0 (XTrimLimitDisabled) LIMIT 0 new — disables the effort cap

Implementation is a single shared helper, appendXTrimLimit, used by XAdd, xTrim, and xTrimMode, replacing the three duplicated if limit > 0 blocks.

Usage:

rdb.XTrimMinIDApprox(ctx, "stream", "1716000000000-0", redis.XTrimLimitDisabled)
// XTRIM stream MINID ~ 1716000000000-0 LIMIT 0

Backward compatibility

  • No public API changes: all exported signatures and struct fields are unchanged.
  • No behavior change for existing callers: limit = 0 still omits the clause; positive values are emitted as before. Only previously-meaningless negative values gain a meaning.
  • Exact-trim commands (XTrimMaxLen, XTrimMinID, and their Mode variants) take no limit parameter and still can never emit LIMIT — Redis rejects LIMIT without ~ (ERR syntax error, LIMIT cannot be used without the special ~ option). For XAddArgs the library keeps its existing stance of not validating the Approx/Limit combination client-side (matching how the conflicting MaxLen/MinID pair is handled); this is now stated explicitly in the Limit field docs instead of being implicit.

Testing

Added server-less unit tests asserting the constructed command args via the existing captureCmdable pattern (stream_commands_unit_test.go):

  • TestXTrim_LimitArgs — 13 cases covering omitted / positive / disabled limit across XTrimMaxLenApprox, XTrimMinIDApprox, their Mode variants, and pinning that exact-trim commands never emit LIMIT.
  • TestXAdd_TrimLimitArgs — 5 cases covering the XAddArgs.Limit path, including clause ordering relative to the entry ID.

go build ./..., go vet ./..., and gofmt are clean; the pre-existing unit test suite passes.


Note

Low Risk
Additive sentinel and command-building change only; existing limit 0 and positive values behave the same, with coverage for emitted Redis args.

Overview
Adds XTrimLimitDisabled (-1) so approximate stream trimming (XADD / XTRIM with ~) can send an explicit LIMIT 0 to Redis, disabling the default trimming effort cap—something the client could not express before because only limit > 0 ever emitted a LIMIT clause.

A shared appendXTrimLimit helper centralizes the three behaviors: omit LIMIT when limit == 0 (unchanged), LIMIT n when positive, and LIMIT 0 when negative. XAdd, xTrim, and xTrimMode use it instead of duplicated checks. XAddArgs.Limit and the *Approx* trim APIs are documented accordingly.

New stream_commands_unit_test.go tests assert built command args via captureCmdable for omitted, positive, and disabled limits across trim variants and XAdd, including that exact (=) trims never emit LIMIT.

Reviewed by Cursor Bugbot for commit 87acd5d. Bugbot is set up for automated code reviews on this repo. Configure here.

…rimLimitDisabled sentinel

Redis treats XTRIM/XADD trimming LIMIT 0 (valid only with the ~ approximate
flag) as "disable the trimming effort cap entirely", which is different from
omitting LIMIT (implicit default of 100 * stream-node-max-entries examined
entries). The command builders only emitted LIMIT when limit > 0, so callers
could never send an explicit LIMIT 0.

Following the KeepTTL = -1 precedent, add the XTrimLimitDisabled = -1
sentinel and a shared appendXTrimLimit helper used by XAdd, xTrim and
xTrimMode:

  - limit == 0 keeps the historical behavior: no LIMIT clause is sent;
  - limit > 0 emits LIMIT <limit> as before;
  - limit < 0 (XTrimLimitDisabled) emits an explicit LIMIT 0.

No public signatures or struct fields change, and existing callers passing 0
produce byte-identical commands. Exact ("=") trim commands (XTrimMaxLen,
XTrimMinID and their Mode variants) still never emit LIMIT; XAddArgs keeps
the existing upstream behavior of passing a user-supplied Limit through even
without Approx (Redis rejects it server-side), now explicitly documented on
the field.

Add server-less unit tests asserting the constructed command args for the
omitted/positive/disabled-limit and exact-trim cases across all XTRIM
variants and XAdd.
@jit-ci

jit-ci Bot commented Jun 11, 2026

Copy link
Copy Markdown

Hi, I’m Jit, a friendly security platform designed to help developers build secure applications from day zero with an MVS (Minimal viable security) mindset.

In case there are security findings, they will be communicated to you as a comment inside the PR.

Hope you’ll enjoy using Jit.

Questions? Comments? Want to learn more? Get in touch with us.

@ndyakov ndyakov left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello @TheRealMal and thank you for this. I like the approach and it is somewhat consistent with the options where we use -1 to disable a given option. Let me rerun the failed job (it was github runner issue) and I will review the code.

@ndyakov ndyakov left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me, thank you @TheRealMal

@ndyakov
ndyakov merged commit 641294c into redis:master Jun 11, 2026
69 of 70 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants