Skip to content

RESP2 and RESP3 response unification#4025

Merged
petyaslavova merged 31 commits into
masterfrom
ps_unify_responces_resp2_resp3_commands
Apr 9, 2026
Merged

RESP2 and RESP3 response unification#4025
petyaslavova merged 31 commits into
masterfrom
ps_unify_responces_resp2_resp3_commands

Conversation

@petyaslavova
Copy link
Copy Markdown
Collaborator

@petyaslavova petyaslavova commented Apr 6, 2026

RESP2/RESP3 Response Unification

This PR unifies the return types of Redis commands across RESP2 and RESP3 protocols, ensuring that application code receives consistent data structures regardless of which protocol version the client is connected with. Previously, many commands returned fundamentally different types — tuples vs lists, bytes vs strings, flat arrays vs nested structures — forcing users to write protocol-aware branching logic. After this change, RESP2 callbacks normalize responses to match RESP3 native types, making the protocol version an internal transport detail rather than a leaky abstraction.

Core Commands

Sorted set commands (ZRANGE, ZPOPMAX, ZSCORE, ZRANDMEMBER, ZRANK, ZSCAN, etc.) now consistently return [member, score] lists with float scores, and score_cast_func receives a float in both protocols instead of raw bytes in RESP2. Blocking pop commands (BLPOP, BRPOP, BZPOPMAX, BZPOPMIN) return lists instead of tuples. ZMPOP/BZMPOP scores are always float. HRANDFIELD with values returns [[field, value], ...] lists in both protocols. LCS with IDX returns a dict in RESP2, matching RESP3's native map. str_if_bytes wrapping has been removed from RESP2 callbacks where RESP3 already returns raw types, eliminating unnecessary encoding discrepancies.

Structural & Module Commands

Stream commands (XINFO STREAM, XREAD, XREADGROUP), geo commands (GEOSEARCH, GEORADIUS), and client tracking info now return matching structures across protocols. Sentinel commands normalize flags to set and masters to consistent dict shapes. Complex introspection commands (COMMAND, COMMAND INFO, ACL LOG, ACL GETUSER, MEMORY STATS, DEBUG OBJECT) received dedicated RESP3 parsers that produce the same shape as their RESP2 counterparts. Module commands across Bloom/Cuckoo/CMS/TopK/TDigest, TimeSeries, JSON, and Search all follow the same pattern — RESP3 parsers were added or aligned to produce identical output types.

Search Module Refactoring

The Search module received significant internal cleanup beyond response unification. A shared decode_field_value utility in redis/utils.py replaces five duplicated decode-or-preserve-bytes blocks across Result, hybrid parsers, and load_document. Callback registration was deduplicated by moving _RESP2_MODULE_CALLBACKS / _RESP3_MODULE_CALLBACKS construction into a shared _init_module_callbacks() method on SearchCommands, eliminating three identical 20-line blocks. The pipeline adapter callbacks (_search_response_callback, _aggregate_response_callback, _hybrid_response_callback) were removed entirely — _parse_results now handles pipeline options directly via functools.partial, with a guard clause for commands queued without a query object.

Testing

The test suite was substantially refactored to remove protocol-conditional assertions. Over 2,500 lines of duplicated @pytest.mark.parametrize("protocol", [2, 3]) branches and if protocol == 3: blocks were replaced with unified assertions that validate both protocols produce the same types. New tests cover score_cast_func with scientific notation, load_document with field_encodings, and edge cases across all affected command families. The net result is 42 files changed with ~2,600 lines removed, reflecting the elimination of both production-side protocol branching and test-side conditional logic.

This change includes a fix for issue #4021


Note

High Risk
High risk because it introduces widespread breaking return-type/structure changes across many core and module commands (e.g., tuples→lists, bytes→str, lists→dicts, flag lists→sets) and updates pipeline callback behavior, which can break downstream code and tests that relied on protocol-specific shapes.

Overview
Unifies RESP2 and RESP3 command responses so callers receive the same Python types/structures regardless of protocol, including sorted-set score pairs (now list[list[member, float]] with score_cast_func always receiving float), blocking pops (now lists), streams (XREAD/XREADGROUP now dicts), and other structural normalizations (e.g., LCS idx → dict, CLIENT TRACKINGINFO → dict, COMMAND.flags → set with optional acl_categories, Sentinel flags → set and RESP3 SENTINEL MASTERS → dict keyed by name).

Adds/updates parsers and callbacks for RESP3 parity (e.g., MEMORY STATS, ACL LOG, DEBUG OBJECT, CLUSTER LINKS, CLUSTER SHARDS key decoding) and normalizes geo coordinate outputs to lists.

Extends the same unification to modules: Bloom/TopK/TDigest INFO commands now always return *Info objects; fixes incorrect numeric coercion for TOPK.ADD/INCRBY/LIST; JSON normalizes NUMINCRBY/NUMMULTBY to list results, converts JSON.RESP float strings to native floats, and aligns JSON.TYPE missing-key handling; Search adds RESP3 parsing to return rich result objects and refactors module pipeline callback registration so module pipelines inherit full client.response_callbacks and post-process consistently.

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

@petyaslavova petyaslavova added the breakingchange API or Breaking Change label Apr 6, 2026
Comment thread redis/commands/bf/__init__.py
Comment thread redis/commands/bf/__init__.py
@jit-ci
Copy link
Copy Markdown

jit-ci Bot commented Apr 6, 2026

🛡️ Jit Security Scan Results

CRITICAL HIGH MEDIUM

✅ No security findings were detected in this PR


Security scan by Jit

Comment thread redis/_parsers/helpers.py
Comment thread redis/_parsers/helpers.py Outdated
Comment thread redis/_parsers/helpers.py
Comment thread redis/commands/search/commands.py Outdated
Comment thread redis/commands/json/__init__.py
Comment thread redis/commands/search/__init__.py Outdated
Comment thread redis/_parsers/helpers.py
Comment thread redis/_parsers/helpers.py
Comment thread redis/_parsers/helpers.py
Comment thread redis/_parsers/helpers.py
Comment thread redis/commands/search/__init__.py Outdated
Comment thread redis/_parsers/helpers.py Outdated
Comment thread redis/_parsers/helpers.py
Comment thread redis/commands/search/commands.py
Comment thread redis/commands/search/__init__.py
Comment thread redis/commands/search/__init__.py Outdated
Comment thread redis/commands/search/__init__.py
Comment thread redis/commands/search/__init__.py Outdated
@petyaslavova petyaslavova force-pushed the ps_unify_responces_resp2_resp3_commands branch from e39c3ab to e20d0a7 Compare April 7, 2026 15:12
Comment thread redis/commands/core.py
Comment thread .agent/resp2_vs_resp3_command_analysis.md Outdated
Comment thread tests/test_timeseries.py
Comment thread redis/commands/search/commands.py
Comment thread redis/_parsers/commands.py
@petyaslavova petyaslavova force-pushed the ps_unify_responces_resp2_resp3_commands branch from 5fb3a71 to a08048c Compare April 8, 2026 13:26
Comment thread redis/_parsers/helpers.py
Comment thread redis/commands/search/commands.py
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 2301f33. Configure here.

Comment thread redis/_parsers/helpers.py
@petyaslavova petyaslavova merged commit de8a47a into master Apr 9, 2026
250 of 254 checks passed
@petyaslavova petyaslavova deleted the ps_unify_responces_resp2_resp3_commands branch April 9, 2026 14:20
petyaslavova added a commit that referenced this pull request Apr 30, 2026
petyaslavova added a commit that referenced this pull request May 5, 2026
… default config compatible with current RESP2 shape. Adding possibility to opt-in for unified responses for both protocol. (#4052)

* Revert "Changing the default RESP protocol configurations for Redis clients and connections to 3 (#4031)"

This reverts commit 0984334.

* Revert "RESP2 and RESP3 response unification (#4025)"

This reverts commit de8a47a.

* Fixing newly added test to be compatible with the old style responses

* Default to RESP3 wire protocol, add legacy_responses opt-out

The default RESP protocol on the wire is now RESP3 (was RESP2). To keep
existing applications working unchanged, response shapes still match
today's RESP2-style Python objects by default, controlled by a new
legacy_responses=True parameter on the public clients.

* redis/utils.py
  - DEFAULT_RESP_VERSION lives here (moved from redis.connection) and is
    set to 3
  - check_protocol_version() resolves protocol=None to
    DEFAULT_RESP_VERSION before comparison, so a missing protocol is
    treated as RESP3-capable

* redis/connection.py, redis/asyncio/connection.py
  - import DEFAULT_RESP_VERSION from redis.utils (still re-exported from
    redis.connection for backward compatibility)
  - AbstractConnection.__init__: protocol default 2 -> None, new
    legacy_responses: bool = True stored as self.legacy_responses
  - URL_QUERY_ARGUMENT_PARSERS accepts protocol and
    legacy_responses, so both can be passed in connection URLs via
    redis://...?protocol=3&legacy_responses=false

* redis/client.py, redis/asyncio/client.py
  - Redis.__init__: protocol default 2 -> None, new legacy_responses
    parameter, both forwarded into the connection pool kwargs
  - client-side caching and maintenance-notifications gating switched
    to check_protocol_version(...) so a missing protocol is treated
    as RESP3

* redis/asyncio/cluster.py
  - RedisCluster.__init__: protocol default 2 -> None, new
    legacy_responses parameter, both placed in the cluster connection
    kwargs

* redis/cluster.py
  - REDIS_ALLOWED_KEYS includes legacy_responses so it propagates
    through cleanup_kwargs() to per-node Redis instances

A user-supplied protocol=None is preserved on the pool / connection
kwargs; only the wire-level HELLO handshake resolves it to the concrete
DEFAULT_RESP_VERSION.

* Adding test fixes

* Adding changes for first several commands and test helpers

* Adding changes for batches 5 and 6

* Adding the rest of the base commands.

* Adding changes for probabilistic and timeseries modules

* Adding changes for search and json modules. Adding docs and test fixes. Updating some of the unified forms to the correct ones

* Adding test_xreadgroup_with_claim_min_idle_time fix

* Adding pipeline test coverage for all combinations of protocol and legacy_responses

* Applying review comments

* Applying review comments

* Applying review comments and fixing failing tests

* Applying review comments and fixing failing tests

* Add flaky test fix

* Add flaky test fix- second try, same test - fails for just one job

* Fixing unstable in CI tests

* Applying review comments

* Fixing linters and spelling error

* Applying review comments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breakingchange API or Breaking Change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants