Skip to content

protocol: pass negotiated ProtocolFeatures to message serialization#934

Merged
dkropachev merged 3 commits into
scylladb:masterfrom
nikagra:protocol-features-encoding-groundwork
Jul 16, 2026
Merged

protocol: pass negotiated ProtocolFeatures to message serialization#934
dkropachev merged 3 commits into
scylladb:masterfrom
nikagra:protocol-features-encoding-groundwork

Conversation

@nikagra

@nikagra nikagra commented Jul 14, 2026

Copy link
Copy Markdown

Summary

Groundwork extracted from the discussions on #770 (SCYLLA_USE_METADATA_ID) and #913 (TABLETS_ROUTING_V2): both extensions need to know, at message-serialization time, which protocol features the serving connection negotiated, so that extension fields are emitted exactly on the connections that negotiated them. This PR threads that information through the encode path once, so both feature PRs (and any future extension) can build on it instead of each changing the contract separately.

This implements the approach proposed by @Lorak-mmk in #770 (comment) and follows up on @dawmd's suggestion (#770 (comment)) to unify the mechanism between #770 and #913 — the plumbing mirrors what #913 already implements, hence the co-authorship.

Changes

  • Connection.send_msg passes the connection's negotiated ProtocolFeatures to the encoder — the single choke point used by every send path, including the control-connection fallback.
  • _ProtocolHandler.encode_message accepts a new required protocol_features argument (passed by keyword from send_msg, per review feedback — no None default) and forwards it to every message's send_body, which gains the same parameter. The encode side becomes symmetric with decode_message, which already receives protocol_features.
  • Design rule (now stated in the encode_message docstring): messages carry connection-independent request data; send_body decides the wire format from (protocol_version, protocol_features). This avoids per-send-site setup and mutating shared message objects per attempt.
  • ProtocolFeatures.__init__ becomes keyword-only, so independently developed extensions (DRIVER-153: negotiate and implement SCYLLA_USE_METADATA_ID extension #770 adds use_metadata_id, Implement TABLETS_ROUTING_V2 #913 adds tablets_routing_v2) can add fields without conflicting over positional-argument order.
  • API docs and CHANGELOG document the contract change.

Zero wire change

This is pure plumbing: no message consumes the parameter yet, so no bytes on the wire change. A new byte-identity test suite pins frames for representative messages (STARTUP/OPTIONS/REGISTER/AUTH_RESPONSE/PREPARE/QUERY/EXECUTE/BATCH across protocol v3/v4/v5) to the exact bytes produced before this change — both with no features and with all-default features.

Breaking change for custom protocol handlers

_ProtocolHandler.encode_message is a documented contract; custom handlers that override encode_message must accept the required protocol_features keyword argument — the parameter name matters, since send_msg passes it by keyword (adding **kwargs is recommended for future-proofing) — and custom encoders that delegate to msg.send_body should forward it. Handlers that only customize decoding — the advertised use case (LazyProtocolHandler, NumpyProtocolHandler, custom deserializers) — are unaffected.

There is deliberately no compatibility fallback for old-style encoders, and the argument is required rather than defaulted: extensions are negotiated per connection at STARTUP, before the per-request handler is known. A silently grandfathered old-style encoder would omit fields a negotiated extension requires — a data-dependent protocol error in production — whereas the signature change fails fast with a deterministic TypeError. Precedent: decode_message already gained protocol_features the same way.

Test plan

  • New unit tests: send_msg hands the connection's features to the encoder; encode_message forwards them into send_body (plain and compressed paths) and raises TypeError when the argument is omitted (assertion phrased to hold under both CPython's and Cython's error wording)
  • Byte-identity regression suite (15 pinned frames, v3/v4/v5)
  • Full unit suite passes (670 passed, 103 skipped)
  • Cython build (build_ext --inplace) compiles the new signatures; protocol tests pass against the compiled modules

Pre-review checklist

  • I have split my patch into logically separate commits.
  • All commit messages clearly explain what they change and why.
  • I added relevant tests for new features and bug fixes.
  • All commits compile, pass static checks and pass test.
  • PR description sums up the changes and reasons why they should be introduced.
  • I have provided docstrings for the public items that I want to introduce.
  • I have adjusted the documentation in ./docs/.
  • I added appropriate Fixes: annotations to PR description.

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@nikagra, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 45 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0ed451ed-5dc9-4e45-accd-9c65deefb6fb

📥 Commits

Reviewing files that changed from the base of the PR and between e0878f3 and 5dc0e6b.

📒 Files selected for processing (7)
  • CHANGELOG.rst
  • cassandra/connection.py
  • cassandra/protocol.py
  • cassandra/protocol_features.py
  • docs/api/cassandra/protocol.rst
  • tests/unit/test_connection.py
  • tests/unit/test_protocol.py
📝 Walkthrough

Walkthrough

Message serialization now receives negotiated ProtocolFeatures from Connection.send_msg. _ProtocolHandler.encode_message forwards the value to message send_body methods in compressed and uncompressed paths, including query parameter encoding. ProtocolFeatures construction is keyword-only. Documentation and changelog entries describe the updated encoder contract. Tests cover propagation, compression, omitted arguments, connection wiring, and frame-byte identity.

Sequence Diagram(s)

sequenceDiagram
  participant Connection
  participant ProtocolHandler
  participant Message
  Connection->>ProtocolHandler: encode_message(protocol_features=self.features)
  ProtocolHandler->>Message: send_body(protocol_features)
  Message-->>ProtocolHandler: serialized body
  ProtocolHandler-->>Connection: encoded frame
Loading

Possibly related PRs

Suggested labels: area/Driver_-_python-driver

Suggested reviewers: lorak-mmk

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title is concise and accurately summarizes the main change: threading negotiated ProtocolFeatures into message serialization.
Description check ✅ Passed The description follows the template closely and covers summary, changes, tests, docs, and checklist items; only Fixes annotations are left incomplete.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@nikagra
nikagra requested review from Lorak-mmk and dawmd July 14, 2026 18:01

@dawmd dawmd left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Looks good. Left just one question.

Comment thread CHANGELOG.rst
Comment thread cassandra/protocol.py
@dkropachev
dkropachev marked this pull request as ready for review July 15, 2026 11:07
@coderabbitai
coderabbitai Bot requested review from Lorak-mmk and mykaul July 15, 2026 11:08
@nikagra
nikagra force-pushed the protocol-features-encoding-groundwork branch from 5f108fa to e0878f3 Compare July 16, 2026 13:26

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
tests/unit/test_protocol.py (1)

19-24: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove duplicate import of UnsupportedOperation.

UnsupportedOperation is imported twice: once from cassandra (line 19) and once from cassandra.protocol (line 21). Remove the redundant import from the cassandra.protocol block to avoid redefinition and keep the list clean.

♻️ Proposed fix
 from cassandra import ConsistencyLevel, ProtocolVersion, UnsupportedOperation
 from cassandra.protocol import (
-    PrepareMessage, QueryMessage, ExecuteMessage, UnsupportedOperation,
+    PrepareMessage, QueryMessage, ExecuteMessage,
     BatchMessage, StartupMessage, OptionsMessage, RegisterMessage,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/test_protocol.py` around lines 19 - 24, Remove
UnsupportedOperation from the cassandra.protocol import list in
tests/unit/test_protocol.py, retaining the existing import from cassandra and
leaving all other protocol imports unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@tests/unit/test_protocol.py`:
- Around line 19-24: Remove UnsupportedOperation from the cassandra.protocol
import list in tests/unit/test_protocol.py, retaining the existing import from
cassandra and leaving all other protocol imports unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ca399a84-9a7f-4d9f-9180-8de6748670e7

📥 Commits

Reviewing files that changed from the base of the PR and between 5f108fa and e0878f3.

📒 Files selected for processing (7)
  • CHANGELOG.rst
  • cassandra/connection.py
  • cassandra/protocol.py
  • cassandra/protocol_features.py
  • docs/api/cassandra/protocol.rst
  • tests/unit/test_connection.py
  • tests/unit/test_protocol.py
🚧 Files skipped from review as they are similar to previous changes (6)
  • CHANGELOG.rst
  • cassandra/connection.py
  • tests/unit/test_connection.py
  • docs/api/cassandra/protocol.rst
  • cassandra/protocol_features.py
  • cassandra/protocol.py

nikagra and others added 3 commits July 16, 2026 15:40
Make ProtocolFeatures.__init__ keyword-only and build it by keyword in
parse_from_supported. Independently developed protocol extensions
(SCYLLA_USE_METADATA_ID, TABLETS_ROUTING_V2) each add fields to this
class; keyword construction lets them do so without conflicting over
positional-argument order. All existing callers already used keywords.
Connection.send_msg now passes the connection's negotiated
ProtocolFeatures to the encoder; _ProtocolHandler.encode_message accepts
it as a new required protocol_features argument (passed by keyword from
send_msg) and forwards it to every message's send_body, which gains the
same parameter. Messages carry connection-independent request data;
send_body decides the wire format from (protocol_version,
protocol_features), so fields belonging to a negotiated protocol
extension are emitted exactly on the connections that negotiated it — on
every send path, including the control-connection fallback, and without
mutating shared message objects per attempt.

This is pure plumbing: no message consumes the parameter yet, so no
bytes on the wire change. It is groundwork for the
SCYLLA_USE_METADATA_ID (scylladb#770) and TABLETS_ROUTING_V2 (scylladb#913) extensions,
which must serialize extension fields based on what the serving
connection negotiated. The encode side becomes symmetric with
decode_message, which already receives protocol_features.

This changes the contracted signature of encode_message: custom protocol
handlers overriding it must accept the protocol_features keyword
argument. The argument is deliberately required, with no default and no
fallback for old-style encoders: extensions are negotiated per
connection at STARTUP before the per-request handler is known, so an
encoder unaware of protocol_features could silently omit fields a
negotiated extension requires; omitting it fails fast with TypeError
instead.

Tests: send_msg hands the connection's features to the encoder;
encode_message forwards them into send_body (plain and compressed
paths) and raises TypeError when the argument is omitted; a
byte-identity suite pins frames for representative messages (v3/v4/v5)
to the exact bytes produced before this change, both without features
and with all-default features.

Co-authored-by: Dawid Mędrek <dawid.medrek@scylladb.com>
Note in the protocol API docs that both contracted _ProtocolHandler
methods receive the connection's negotiated ProtocolFeatures, spelling
out the calling conventions: decode_message receives it positionally,
encode_message as the required protocol_features keyword argument, so
overrides must keep that parameter name. Add a CHANGELOG entry with
upgrade guidance for custom protocol handlers (accept protocol_features,
prefer **kwargs for future-proofing, forward it when delegating to
send_body).
@nikagra
nikagra force-pushed the protocol-features-encoding-groundwork branch from e0878f3 to 5dc0e6b Compare July 16, 2026 13:41
@dkropachev
dkropachev merged commit e605de2 into scylladb:master Jul 16, 2026
21 checks passed
@nikagra
nikagra deleted the protocol-features-encoding-groundwork branch July 16, 2026 18:52
@dawmd dawmd mentioned this pull request Jul 20, 2026
8 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants