Skip to content

Conversation

@ttypic
Copy link
Contributor

@ttypic ttypic commented Sep 4, 2025

  • Added ChannelOptions class to handle channel parameters and cipher configurations.
  • Updated RealtimeChannel to support ChannelOptions

Summary by CodeRabbit

  • New Features

    • Added ChannelOptions to configure per-channel params and encryption; Channels.get now accepts options (dict or ChannelOptions) when creating/retrieving channels.
    • RealtimeChannel.set_options to update channel options at runtime; params exposed via a params property.
  • Bug Fixes

    • Improved attach/serial handling for more reliable resumed state and message ordering.
    • Safer option-update behavior with clear error when reattach would be required.
  • Tests

    • Added tests for ChannelOptions, params handling, dict interop, and get() behavior.

@ttypic ttypic requested review from VeskeR and sacOO7 September 4, 2025 17:53
@coderabbitai
Copy link

coderabbitai bot commented Sep 4, 2025

Walkthrough

Adds ChannelOptions for per-channel cipher/params, persists options in RealtimeChannel, includes params in ATTACH, tracks attach/channel serials, exposes set_options and params, and updates Channels.get to accept/validate ChannelOptions or dicts and enforce safe option changes.

Changes

Cohort / File(s) Summary
Realtime channel options & control flow updates
ably/realtime/realtime_channel.py
- Add ChannelOptions (cipher, params; validation, __eq__, __hash__, to_dict/from_dict).
- RealtimeChannel.__init__(..., channel_options: Optional[ChannelOptions]=None) stores __channel_options, __attach_serial, __channel_serial, and forwards options dict to base Channel.
- Add RealtimeChannel.set_options(channel_options: ChannelOptions) and set_options_without_reattach; add should_reattach_to_set_options logic; may reattach or raise on unsafe change.
- Include params from __channel_options in ATTACH payload; update _on_message to update attach/channel serials and store params; add params property.
Channels.get signature & validation
ably/realtime/realtime_channel.py
- `Channels.get(name, options: Optional[ChannelOptions
Tests: per-channel params & behavior
test/ably/realtime/realtimechannel_test.py
- Import and use ChannelOptions.
- Tests cover params propagation from ChannelOptions and dicts, unknown params ignored, equality when same params, and raising when getting existing channel with different params.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Developer
  participant Channels
  participant RealtimeChannel
  participant AblyServer

  Developer->>Channels: get(name, options)
  alt channel does not exist
    Channels->>RealtimeChannel: new RealtimeChannel(name, ChannelOptions)
    RealtimeChannel->>AblyServer: ATTACH { params: ChannelOptions.params }
    AblyServer-->>RealtimeChannel: ATTACHED { attachSerial, channelSerial, params }
    RealtimeChannel->>RealtimeChannel: store __attach_serial, __channel_serial, __params
  else channel exists
    Channels->>RealtimeChannel: compare options
    alt reattach required
      Channels-->>Developer: AblyException (use set_options)
    else safe to update without reattach
      Channels->>RealtimeChannel: set_options_without_reattach(newOptions)
    else set_options triggers reattach
      RealtimeChannel->>AblyServer: ATTACH { params: newOptions.params }
      AblyServer-->>RealtimeChannel: ATTACHED { attachSerial, channelSerial, params }
      RealtimeChannel->>RealtimeChannel: update serials/params
    end
  end

  Note over AblyServer,RealtimeChannel: On MESSAGE, update __channel_serial before emitting
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

I nibbled params from a tiny packet,
Packed them safely in my channel jacket.
Serials counted, ATTACH sent on cue,
If options change, I'll hop and renew.
A rabbit keeps your channels true. 🐇✨

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.

Pre-merge checks (2 passed, 1 warning)

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 56.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title succinctly and accurately highlights the primary change by announcing the introduction of ChannelOptions for improved channel configuration, avoiding extraneous detail and remaining concise and clear.
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/channel-options

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 and usage tips.

@github-actions github-actions bot temporarily deployed to staging/pull/622/features September 4, 2025 17:53 Inactive
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (2)
ably/realtime/realtime_channel.py (2)

530-534: Consider more granular reattachment logic

The current implementation triggers reattachment whenever options differ. Consider if certain option changes (like cipher changes) might not require reattachment depending on the protocol requirements.

If the protocol allows certain option changes without reattachment, consider implementing more granular logic:

     def should_reattach_to_set_options(self, old_options: ChannelOptions, new_options: ChannelOptions) -> bool:
         if self.state != ChannelState.ATTACHING and self.state != ChannelState.ATTACHED:
             return False
-        return old_options != new_options
+        # Only reattach if params changed - cipher changes might not require reattachment
+        if old_options.params != new_options.params:
+            return True
+        # Add other conditions as per protocol requirements
+        return False

38-38: Consider moving error messages to exception class

The static analysis suggests avoiding long messages outside the exception class. While not critical, consider defining these as constants in the AblyException class or using shorter inline messages.

Consider refactoring to use exception message constants or factory methods in the AblyException class to improve maintainability and consistency of error messages across the codebase.

Also applies to: 59-59, 91-91, 586-586, 594-599

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 3cc9a9a and 16f3ec3.

📒 Files selected for processing (1)
  • ably/realtime/realtime_channel.py (8 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
ably/realtime/realtime_channel.py (5)
ably/rest/channel.py (7)
  • ably (143-144)
  • cipher (155-156)
  • get (182-193)
  • name (147-148)
  • Channel (22-174)
  • options (159-160)
  • options (167-174)
ably/util/crypto.py (1)
  • CipherParams (16-42)
ably/util/exceptions.py (1)
  • AblyException (9-84)
ably/realtime/realtime.py (1)
  • AblyRealtime (12-140)
ably/util/eventemitter.py (1)
  • once_async (168-181)
🪛 Ruff (0.12.2)
ably/realtime/realtime_channel.py

38-38: Avoid specifying long messages outside the exception class

(TRY003)


59-59: Avoid specifying long messages outside the exception class

(TRY003)


91-91: Avoid specifying long messages outside the exception class

(TRY003)


586-586: Avoid specifying long messages outside the exception class

(TRY003)


594-599: Avoid specifying long messages outside the exception class

(TRY003)

🪛 GitHub Actions: Linting check
ably/realtime/realtime_channel.py

[error] 63-63: flake8: E303 too many blank lines (2). Command: poetry run flake8


[error] 595-595: flake8: E501 line too long (117 > 115 characters). Command: poetry run flake8

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: check (3.13)
  • GitHub Check: check (3.11)
  • GitHub Check: check (3.12)
  • GitHub Check: check (3.10)
  • GitHub Check: check (3.7)
  • GitHub Check: check (3.9)
  • GitHub Check: check (3.8)
🔇 Additional comments (5)
ably/realtime/realtime_channel.py (5)

22-97: Well-structured ChannelOptions implementation with proper validation

The ChannelOptions class is well-designed with appropriate validation, property access patterns, and conversion utilities. The implementation correctly validates params as a dictionary and provides both equality checking and hashability for consistent behavior.


136-144: Clean integration of ChannelOptions with inheritance hierarchy

The channel options are properly initialized with a default instance, and correctly passed to the parent Channel class as a dictionary. This maintains backward compatibility while adding the new functionality.


199-215: Channel parameters correctly included in ATTACH message

The implementation properly includes channel parameters in the ATTACH protocol message, ensuring they're sent to the server during channel attachment.


410-411: Consistent serial tracking implementation

The serial tracking for ATTACHED and MESSAGE actions is implemented correctly. When ATTACHED is received in an already attached state with the resumed flag, both __attach_serial and __channel_serial are updated appropriately. For MESSAGE actions, only __channel_serial is updated as expected.

Also applies to: 425-425


145-157: Add proper async implementation for set_options

The set_options method should be properly marked as async since it contains await statements and handles asynchronous state changes.

-    async def set_options(self, channel_options: ChannelOptions) -> None:
+    async def set_options(self, channel_options: ChannelOptions) -> None:
         """Set channel options"""
         old_channel_options = self.__channel_options
         self.__channel_options = channel_options
         # Update parent class options
         self.options = channel_options.to_dict()
 
         if self.should_reattach_to_set_options(old_channel_options, channel_options):
-            self._attach_impl()
+            await self._attach_impl()
             state_change = await self.__internal_state_emitter.once_async()
             if state_change.current in (ChannelState.SUSPENDED, ChannelState.FAILED):
                 raise state_change.reason

Also, _attach_impl needs to be made async to match this usage pattern, or the call should be wrapped appropriately.

Likely an incorrect or invalid review comment.

@ttypic ttypic force-pushed the feat/channel-options branch from 16f3ec3 to 5bec2a7 Compare September 4, 2025 18:06
@github-actions github-actions bot temporarily deployed to staging/pull/622/features September 4, 2025 18:07 Inactive
@ttypic ttypic force-pushed the fix/fix-tests-update-ci branch from 3cc9a9a to 9bfa4db Compare September 4, 2025 18:24
@ttypic ttypic force-pushed the feat/channel-options branch from 5bec2a7 to 3671ec8 Compare September 4, 2025 18:25
@github-actions github-actions bot temporarily deployed to staging/pull/622/features September 4, 2025 18:26 Inactive
@ttypic ttypic force-pushed the feat/channel-options branch 2 times, most recently from b0d7781 to 9032c63 Compare September 4, 2025 18:47
@github-actions github-actions bot temporarily deployed to staging/pull/622/features September 4, 2025 18:48 Inactive
@ttypic ttypic force-pushed the feat/channel-options branch from 9032c63 to f84e7e6 Compare September 4, 2025 23:10
@github-actions github-actions bot temporarily deployed to staging/pull/622/features September 4, 2025 23:10 Inactive
@ttypic ttypic force-pushed the fix/fix-tests-update-ci branch from 490767a to 0659f89 Compare September 4, 2025 23:12
@ttypic ttypic force-pushed the feat/channel-options branch from f84e7e6 to 22aecd6 Compare September 4, 2025 23:13
@github-actions github-actions bot temporarily deployed to staging/pull/622/features September 4, 2025 23:13 Inactive
@ttypic ttypic force-pushed the fix/fix-tests-update-ci branch from 0659f89 to a1dfdd7 Compare September 4, 2025 23:30
@ttypic ttypic force-pushed the feat/channel-options branch from 22aecd6 to 29bb3a3 Compare September 4, 2025 23:31
@github-actions github-actions bot temporarily deployed to staging/pull/622/features September 4, 2025 23:32 Inactive
@ttypic ttypic force-pushed the fix/fix-tests-update-ci branch from a1dfdd7 to 05054a8 Compare September 4, 2025 23:39
@ttypic ttypic force-pushed the feat/channel-options branch from 29bb3a3 to 01c3725 Compare September 4, 2025 23:47
@github-actions github-actions bot temporarily deployed to staging/pull/622/features September 4, 2025 23:48 Inactive
Copy link
Collaborator

@sacOO7 sacOO7 left a comment

Choose a reason for hiding this comment

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

Since we are introducing ChannelOptions, can we add tests for the same.

  1. Add params property to RealtimeChannel class and set it from received ProtocolMessage similar to channel.params = protoMessage.params. Add test for the same, check if sent params from attachMessage are received and set on channel params.
  2. Optional, but you can also introduce modes to ChannelOptions similar to ChannelOptions#modes. Add test for the same.

@ttypic
Copy link
Contributor Author

ttypic commented Sep 5, 2025

thanks @sacOO7, I'll add tests for params, but I think modes is out of the scope, we'll add them later

@ttypic ttypic force-pushed the fix/fix-tests-update-ci branch 2 times, most recently from 23d1036 to 30fdc5d Compare September 7, 2025 10:58
@ttypic ttypic force-pushed the feat/channel-options branch from 01c3725 to 28135ae Compare September 7, 2025 12:49
@github-actions github-actions bot temporarily deployed to staging/pull/622/features September 7, 2025 12:49 Inactive
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
test/ably/realtime/realtimechannel_test.py (1)

521-533: Tighten the negative case assertion

Optionally assert on the exception’s code/message to avoid false positives (any AblyException would currently pass).

Example:

with pytest.raises(AblyException) as exc:
    ably.channels.get(channel_name, {"params": {"delta": "vcdiff"}})
assert getattr(exc.value, "code", None) in {40000, 40010, 400}  # adjust to the actual code raised
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between f84e7e6 and 28135ae.

📒 Files selected for processing (2)
  • ably/realtime/realtime_channel.py (9 hunks)
  • test/ably/realtime/realtimechannel_test.py (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • ably/realtime/realtime_channel.py
🧰 Additional context used
🪛 GitHub Actions: Linting check
test/ably/realtime/realtimechannel_test.py

[error] 475-475: flake8 E251: unexpected spaces around keyword / parameter equals.


[error] 475-475: flake8 E251: unexpected spaces around keyword / parameter equals.


[error] 486-486: flake8 E251: unexpected spaces around keyword / parameter equals.


[error] 486-486: flake8 E251: unexpected spaces around keyword / parameter equals.


[error] 500-500: flake8 E201: whitespace after '{'.


[error] 500-500: flake8 E202: whitespace before '}'.


[error] 511-511: flake8 E201: whitespace after '{'.


[error] 511-511: flake8 E202: whitespace before '}'.


[error] 515-515: flake8 E201: whitespace after '{'.


[error] 515-515: flake8 E202: whitespace before '}'.


[error] 525-525: flake8 E201: whitespace after '{'.


[error] 525-525: flake8 E202: whitespace before '}'.


[error] 528-528: flake8 W293: blank line contains whitespace.


[error] 530-530: flake8 F841: local variable 'same_channel' is assigned to but never used.


[error] 531-531: flake8 E201: whitespace after '{'.


[error] 531-531: flake8 E202: whitespace before '}'.


[error] 534-534: flake8 E201: whitespace after '{'.


[error] 534-534: flake8 E202: whitespace before '}'.


[error] 537-537: flake8 W391: blank line at end of file.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: check (3.11)
  • GitHub Check: check (3.13)
  • GitHub Check: check (3.12)
  • GitHub Check: check (3.10)
  • GitHub Check: check (3.9)
  • GitHub Check: check (3.7)
  • GitHub Check: check (3.8)
🔇 Additional comments (1)
test/ably/realtime/realtimechannel_test.py (1)

3-3: Confirm public import path for ChannelOptions

Make sure ChannelOptions is actually defined/exported from ably.realtime.realtime_channel; if it moved (e.g., ably.types), the tests will fail to import.

Run to verify definition and import sites:

#!/bin/bash
# Locate ChannelOptions definition and any public exports
rg -nP 'class\s+ChannelOptions\b|ChannelOptions\s*=' -C2

# Check all import statements referencing ChannelOptions
rg -nP 'from\s+ably\.[\w\.]+\s+import\s+.*\bChannelOptions\b' -C1

@ttypic ttypic force-pushed the feat/channel-options branch from 28135ae to a06a6da Compare September 7, 2025 12:56
@github-actions github-actions bot temporarily deployed to staging/pull/622/features September 7, 2025 12:57 Inactive
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (1)
ably/realtime/realtime_channel.py (1)

589-597: Fix AblyException code/status order and await async set_options in Channels.get

  • Arguments are reversed (should be code=40000, status=400).
  • set_options is async and currently not awaited, so updates won’t apply. Schedule it.
             if options and channel.should_reattach_to_set_options(options):
                 raise AblyException(
                     'Channels.get() cannot be used to set channel options that would cause the channel to '
                     'reattach. Please, use RealtimeChannel.setOptions() instead.',
-                    400,
-                    40000
+                    40000,
+                    400
                 )
             elif options:
-                channel.set_options(options)
+                asyncio.create_task(channel.set_options(options))
🧹 Nitpick comments (4)
ably/realtime/realtime_channel.py (4)

4-4: Import Mapping for safer type hints

ChannelOptions would benefit from Mapping to accept any mapping-like object and to support copying.

-from typing import Optional, TYPE_CHECKING, Dict, Any, Union
+from typing import Optional, TYPE_CHECKING, Dict, Any, Union, Mapping

111-132: Initialize RealtimeChannel params from ChannelOptions for immediate visibility

Optional: seed __params from provided options so channel.params reflects configured params before first ATTACH.

-        self.__channel_options = channel_options or ChannelOptions()
-        self.__params: Optional[Dict[str, str]] = None
+        self.__channel_options = channel_options or ChannelOptions()
+        self.__params: Optional[Dict[str, str]] = (
+            dict(self.__channel_options.params) if self.__channel_options.params else None
+        )

415-416: Guard channelSerial update on MESSAGE

Prevent resetting to None when the field is absent.

-            self.__channel_serial = channel_serial
+            if channel_serial is not None:
+                self.__channel_serial = channel_serial

520-524: Simplify and clarify reattach predicate

Equivalent logic, more readable.

-    def should_reattach_to_set_options(self, new_options: ChannelOptions) -> bool:
-        if self.state != ChannelState.ATTACHING and self.state != ChannelState.ATTACHED:
-            return False
-        return self.__channel_options != new_options
+    def should_reattach_to_set_options(self, new_options: ChannelOptions) -> bool:
+        return (
+            self.state in (ChannelState.ATTACHING, ChannelState.ATTACHED)
+            and self.__channel_options != new_options
+        )
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 28135ae and a06a6da.

📒 Files selected for processing (2)
  • ably/realtime/realtime_channel.py (9 hunks)
  • test/ably/realtime/realtimechannel_test.py (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/ably/realtime/realtimechannel_test.py
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: check (3.11)
  • GitHub Check: check (3.13)
  • GitHub Check: check (3.9)
  • GitHub Check: check (3.8)
  • GitHub Check: check (3.12)
  • GitHub Check: check (3.10)
  • GitHub Check: check (3.7)
🔇 Additional comments (2)
ably/realtime/realtime_channel.py (2)

387-390: Guard serial/params updates; don’t overwrite with None; use attachSerial if available

Setting __attach_serial to channelSerial looks incorrect; also avoid clobbering existing values with None.

-            self.__attach_serial = channel_serial
-            self.__channel_serial = channel_serial
-            self.__params = proto_msg.get('params')
+            # Prefer attachSerial when provided, and avoid overwriting with None
+            attach_serial = proto_msg.get('attachSerial')
+            if attach_serial is not None:
+                self.__attach_serial = attach_serial
+            if channel_serial is not None:
+                self.__channel_serial = channel_serial
+            if 'params' in proto_msg:
+                self.__params = proto_msg.get('params')

Would you like me to verify the Ably Realtime protocol fields for ATTACHED messages and confirm attachSerial/channelSerial semantics?


547-551: Fix type hint and copy-out for RealtimeChannel.params

The property may be None; return a copy to avoid external mutation.

     @property
-    def params(self) -> Dict[str, str]:
+    def params(self) -> Optional[Dict[str, str]]:
         """Get channel parameters"""
-        return self.__params
+        return dict(self.__params) if self.__params is not None else None

Likely an incorrect or invalid review comment.

Base automatically changed from fix/fix-tests-update-ci to main September 8, 2025 12:34
@ttypic ttypic requested a review from sacOO7 September 8, 2025 12:34
@ttypic ttypic force-pushed the feat/channel-options branch from a06a6da to 85c8e70 Compare September 9, 2025 12:25
@github-actions github-actions bot temporarily deployed to staging/pull/622/features September 9, 2025 12:26 Inactive
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (6)
ably/realtime/realtime_channel.py (6)

565-582: Consider accepting only ChannelOptions for consistency

As per the spec RTS3c, the signature should be get(String, ChannelOptions) -> ChannelType. The current implementation accepts both dict and ChannelOptions which may lead to inconsistency.

Consider strictly accepting ChannelOptions to align with the specification and maintain consistency, especially since future fields like attachOnSubscribe (boolean) can't be properly represented in a plain dict.


65-73: to_dict() exposes internal mutable reference

The to_dict() method returns the internal __params dict directly, allowing external code to mutate the channel options.

     def to_dict(self) -> Dict[str, Any]:
         """Convert to dictionary representation"""
         result = {}
         if self.__cipher is not None:
             result['cipher'] = self.__cipher
         if self.__params:
-            result['params'] = self.__params
+            result['params'] = dict(self.__params)
         return result

133-145: set_options() can hang indefinitely when channel is ATTACHED

Calling _attach_impl() directly won't emit a state change when the current state is ATTACHED, causing once_async() to wait forever. Use _request_state(ATTACHING) to properly trigger state transitions.

     async def set_options(self, channel_options: ChannelOptions) -> None:
         """Set channel options"""
         should_reattach = self.should_reattach_to_set_options(channel_options)
         self.__channel_options = channel_options
         # Update parent class options
         self.options = channel_options.to_dict()
 
         if should_reattach:
-            self._attach_impl()
+            self._request_state(ChannelState.ATTACHING)
             state_change = await self.__internal_state_emitter.once_async()
             if state_change.current in (ChannelState.SUSPENDED, ChannelState.FAILED):
                 raise state_change.reason

588-597: Async method called without await

The set_options method is async but called without await on line 596, which will create an unawaited coroutine warning.

             # Update options if channel is not attached or currently attaching
             if options and channel.should_reattach_to_set_options(options):
                 raise AblyException(
                     'Channels.get() cannot be used to set channel options that would cause the channel to '
                     'reattach. Please, use RealtimeChannel.setOptions() instead.',
                     400,
                     40000
                 )
             elif options:
-                channel.set_options(options)
+                asyncio.create_task(channel.set_options(options))

22-39: Potential mutability issues with ChannelOptions params

The params parameter accepts a mutable dict but stores it by reference, which could lead to external mutations affecting the channel's behavior unexpectedly. Additionally, the type hint for the params property suggests it's never None, but it can be.

Apply these changes to ensure immutability:

-    def __init__(self, cipher: Optional[CipherParams] = None, params: Optional[dict] = None):
+    def __init__(self, cipher: Optional[CipherParams] = None, params: Optional[Dict[str, str]] = None):
         self.__cipher = cipher
-        self.__params = params
+        # Copy params to prevent external mutation
+        self.__params = dict(params) if params else None
         # Validate params
         if self.__params and not isinstance(self.__params, dict):
             raise AblyException("params must be a dictionary", 40000, 400)

And update the property:

     @property
-    def params(self) -> Dict[str, str]:
+    def params(self) -> Optional[Dict[str, str]]:
         """Get channel parameters"""
-        return self.__params
+        return dict(self.__params) if self.__params else None

58-63: Unsafe hash implementation with mutable params

The __hash__ method can crash at runtime if cipher is unhashable, and having __hash__ with mutable params violates Python's hashability contract (objects that compare equal must have the same hash value).

Since params can be mutated externally (current implementation) and CipherParams may not be hashable, remove the __hash__ method:

-    def __hash__(self):
-        """Make ChannelOptions hashable"""
-        return hash((
-            self.__cipher,
-            tuple(sorted(self.__params.items())) if self.__params else None,
-        ))

If hashability is required, ensure both immutability and that all components are hashable.

🧹 Nitpick comments (2)
ably/realtime/realtime_channel.py (2)

191-195: Avoid sending null params in ATTACH message

The ATTACH message always includes params even when it's None, which may not be desired.

         attach_msg = {
             "action": ProtocolMessageAction.ATTACH,
-            "params": self.__channel_options.params,
             "channel": self.name,
         }
+        if self.__channel_options.params:
+            attach_msg["params"] = self.__channel_options.params

547-551: params property potentially returns None

The params property can return None but the type hint suggests it always returns a Dict.

     @property
-    def params(self) -> Dict[str, str]:
+    def params(self) -> Optional[Dict[str, str]]:
         """Get channel parameters"""
         return self.__params
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between a06a6da and 85c8e70.

📒 Files selected for processing (2)
  • ably/realtime/realtime_channel.py (9 hunks)
  • test/ably/realtime/realtimechannel_test.py (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/ably/realtime/realtimechannel_test.py
🧰 Additional context used
🧬 Code graph analysis (1)
ably/realtime/realtime_channel.py (5)
ably/rest/channel.py (7)
  • ably (143-144)
  • cipher (155-156)
  • get (182-193)
  • name (147-148)
  • Channel (22-174)
  • options (159-160)
  • options (167-174)
ably/util/crypto.py (1)
  • CipherParams (16-42)
ably/util/exceptions.py (2)
  • AblyException (9-84)
  • from_dict (83-84)
ably/util/eventemitter.py (1)
  • once_async (168-181)
ably/types/channelstate.py (1)
  • ChannelState (7-14)
🪛 Ruff (0.12.2)
ably/realtime/realtime_channel.py

38-38: Avoid specifying long messages outside the exception class

(TRY003)


78-78: Avoid specifying long messages outside the exception class

(TRY003)


581-581: Avoid specifying long messages outside the exception class

(TRY003)


589-594: Avoid specifying long messages outside the exception class

(TRY003)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: check (3.7)
  • GitHub Check: check (3.8)
  • GitHub Check: check (3.11)
  • GitHub Check: check (3.9)
  • GitHub Check: check (3.10)
  • GitHub Check: check (3.12)
  • GitHub Check: check (3.13)
🔇 Additional comments (2)
ably/realtime/realtime_channel.py (2)

387-389: Correct implementation of attach serial handling

The code correctly updates both __attach_serial and __channel_serial when receiving an ATTACHED message, and stores the server-provided params.


520-524: LGTM - Clean reattachment logic

The should_reattach_to_set_options method properly checks if reattachment is needed only when the channel is ATTACHING or ATTACHED and options have changed.

@ttypic ttypic force-pushed the feat/channel-options branch from 85c8e70 to a5e34dc Compare September 10, 2025 11:31
@github-actions github-actions bot temporarily deployed to staging/pull/622/features September 10, 2025 11:32 Inactive
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (4)
ably/realtime/realtime_channel.py (4)

4-4: Fix AblyException arg order; make params immutable; drop unsafe hash; tighten types.

  • AblyException calls use wrong arg order at Lines 37-38 and 77-78 (status_code, code).
  • Expose/retain params by reference; later mutation breaks equality semantics.
  • hash is unsafe (mutable params; cipher may be unhashable).
  • Type of params should be Mapping[str, str]; property should return Optional[Dict[str, str]] and copy.

Apply:

-from typing import Optional, TYPE_CHECKING, Dict, Any
+from typing import Optional, TYPE_CHECKING, Dict, Any, Mapping
@@
-class ChannelOptions:
+class ChannelOptions:
@@
-    def __init__(self, cipher: Optional[CipherParams] = None, params: Optional[dict] = None):
-        self.__cipher = cipher
-        self.__params = params
-        # Validate params
-        if self.__params and not isinstance(self.__params, dict):
-            raise AblyException("params must be a dictionary", 40000, 400)
+    def __init__(self, cipher: Optional[CipherParams] = None, params: Optional[Mapping[str, str]] = None):
+        self.__cipher = cipher
+        if params is not None and not isinstance(params, Mapping):
+            raise AblyException("params must be a dictionary", 400, 40000)
+        # store a copy to prevent external mutation
+        self.__params = dict(params) if params is not None else None
@@
-    def params(self) -> Dict[str, str]:
-        """Get channel parameters"""
-        return self.__params
+    def params(self) -> Optional[Dict[str, str]]:
+        """Get channel parameters (copy)"""
+        return dict(self.__params) if self.__params is not None else None
@@
-    def __hash__(self):
-        """Make ChannelOptions hashable"""
-        return hash((
-            self.__cipher,
-            tuple(sorted(self.__params.items())) if self.__params else None,
-        ))
+    # Note: __hash__ intentionally omitted due to mutability and potential unhashable cipher.
@@
-    def to_dict(self) -> Dict[str, Any]:
+    def to_dict(self) -> Dict[str, Any]:
         """Convert to dictionary representation"""
         result = {}
         if self.__cipher is not None:
             result['cipher'] = self.__cipher
-        if self.__params:
-            result['params'] = self.__params
+        if self.__params:
+            result['params'] = dict(self.__params)
         return result
@@
-    def from_dict(cls, options_dict: Dict[str, Any]) -> 'ChannelOptions':
+    def from_dict(cls, options_dict: Dict[str, Any]) -> 'ChannelOptions':
         """Create ChannelOptions from dictionary"""
         if not isinstance(options_dict, dict):
-            raise AblyException("options must be a dictionary", 40000, 400)
+            raise AblyException("options must be a dictionary", 400, 40000)
 
         return cls(
             cipher=options_dict.get('cipher'),
             params=options_dict.get('params'),
         )

I can add unit tests asserting immutability and correct AblyException codes. Want me to open an issue and provide tests?

Also applies to: 22-39, 45-49, 58-63, 65-73, 74-83


133-143: set_options() can hang; request ATTACHING instead of calling _attach_impl() directly.

Calling _attach_impl() while ATTACHED won’t emit a state change; once_async() may await forever. Trigger the state machine.

         if should_reattach:
-            self._attach_impl()
-            state_change = await self.__internal_state_emitter.once_async()
+            self._request_state(ChannelState.ATTACHING)
+            state_change = await self.__internal_state_emitter.once_async()
             if state_change.current in (ChannelState.SUSPENDED, ChannelState.FAILED):
                 raise state_change.reason

195-199: Avoid sending "params": None in ATTACH frame.

Only include params when present.

         attach_msg = {
             "action": ProtocolMessageAction.ATTACH,
-            "params": self.__channel_options.params,
             "channel": self.name,
         }
+        if self.__channel_options.params:
+            attach_msg["params"] = self.__channel_options.params

552-556: params property should be Optional and return a copy.

Prevents accidental external mutation and matches None default.

-    def params(self) -> Dict[str, str]:
-        """Get channel parameters"""
-        return self.__params
+    def params(self) -> Optional[Dict[str, str]]:
+        """Get channel parameters"""
+        return dict(self.__params) if self.__params is not None else None
🧹 Nitpick comments (2)
ably/realtime/realtime_channel.py (2)

578-580: Docstring: accept ChannelOptions only (per spec).

The signature is strict; update docs.

-        options: ChannelOptions or dict, optional
-            Channel options for the channel
+        options: ChannelOptions, optional
+            Channel options for the channel

586-594: Nit: quell TRY003 by extracting the long error message.

Optional style tweak for Ruff; logic is fine.

-            if options and channel.should_reattach_to_set_options(options):
-                raise AblyException(
-                    'Channels.get() cannot be used to set channel options that would cause the channel to '
-                    'reattach. Please, use RealtimeChannel.setOptions() instead.',
-                    400,
-                    40000
-                )
+            if options and channel.should_reattach_to_set_options(options):
+                msg = (
+                    'Channels.get() cannot be used to set channel options that would cause the channel to '
+                    'reattach. Please, use RealtimeChannel.setOptions() instead.'
+                )
+                raise AblyException(msg, 400, 40000)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between a5e34dc and 204138f.

📒 Files selected for processing (2)
  • ably/realtime/realtime_channel.py (9 hunks)
  • test/ably/realtime/realtimechannel_test.py (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/ably/realtime/realtimechannel_test.py
🧰 Additional context used
🧬 Code graph analysis (1)
ably/realtime/realtime_channel.py (4)
ably/rest/channel.py (7)
  • ably (143-144)
  • cipher (155-156)
  • get (182-193)
  • name (147-148)
  • Channel (22-174)
  • options (159-160)
  • options (167-174)
ably/util/crypto.py (1)
  • CipherParams (16-42)
ably/util/exceptions.py (2)
  • AblyException (9-84)
  • from_dict (83-84)
ably/util/eventemitter.py (1)
  • once_async (168-181)
🪛 Ruff (0.12.2)
ably/realtime/realtime_channel.py

38-38: Avoid specifying long messages outside the exception class

(TRY003)


78-78: Avoid specifying long messages outside the exception class

(TRY003)


587-592: Avoid specifying long messages outside the exception class

(TRY003)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: check (3.13)
  • GitHub Check: check (3.8)
  • GitHub Check: check (3.9)
  • GitHub Check: check (3.10)
  • GitHub Check: check (3.7)
  • GitHub Check: check (3.12)
  • GitHub Check: check (3.11)

Copy link
Collaborator

@sacOO7 sacOO7 left a comment

Choose a reason for hiding this comment

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

LGTM

@ttypic ttypic merged commit 91d5759 into main Sep 10, 2025
16 of 17 checks passed
@ttypic ttypic deleted the feat/channel-options branch September 10, 2025 12:41
@VeskeR VeskeR mentioned this pull request Sep 19, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants