Skip to content

feat(pubsub): inline Pub/Sub APIs for exact channels - #308

Open
Aryex wants to merge 3 commits into
mainfrom
alexl/inline-pubsub
Open

feat(pubsub): inline Pub/Sub APIs for exact channels#308
Aryex wants to merge 3 commits into
mainfrom
alexl/inline-pubsub

Conversation

@Aryex

@Aryex Aryex commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

Summary

This adds the part 2 of PubSub APIs implementation plan.

Issue link

Related #135

Changes

  • PubSub implemented to support the APIs, including the ffi callback.
  • RESP3 is enforced client side.
  • unit and integration tests added.

Limitations

  • publish takes the message first — publish(message, channel) — matching the other GLIDE clients rather than redis-rb's publish(channel, message).
  • timeout: is documented and treated as milliseconds, not seconds. Worth confirming that's the intended public unit, since the rest of the client's timeouts are in seconds.

Testing

  • bundle exec rubocop — clean.
  • bundle exec rake test:unit — 227 tests, 480 assertions, 0 failures, 0 errors, 0 skips. test/unit/glide/pubsub_test.rb grows by ~234 lines covering the RESP3 guard, timeout_argument conversion and rejection, and the push handler's kind filtering / NUL-safe reads / exception swallowing.
  • test/integration/valkey/pubsub_test.rb — new, ~277 lines, wired into the standalone suite. Covers subscribe/publish/receive round trips, blocking vs non-blocking consume, and the negative paths.
  • Integration suite has not been run against a live server in this pass; that's the main gap before this leaves draft.

Checklist

Before submitting the PR make sure the following are checked:

  • This Pull Request is related to an issue.
  • Commit message describe your changes
  • Commits are signed off (git commit -s) per the DCO.
  • Tests are added or updated.
  • CHANGELOG.md and documentation files are updated.
  • Linters have been run (bundle exec rubocop) and pass.
  • Destination branch is correct - main.

Signed-off-by: Alex Le <alex.le@improving.com>
@Aryex
Aryex marked this pull request as ready for review September 8, 2026 14:23

@jamesx-improving jamesx-improving left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

CHANGELOG.md still needs a 1.1.0 entry for this — it's the largest user-facing change in the release and the checklist box is unchecked.

On your open question: milliseconds is the right wire unit — glide-core parses the last argument as a u64 ms count and Go's peer API names the parameter timeoutMs — but timeout: reads as seconds next to every other timeout in this client, so consider renaming the kwarg timeout_ms: while the API is still unreleased.

Comment thread lib/valkey/commands.rb
include FunctionCommands
# TODO: https://github.com/valkey-io/valkey-glide-ruby/issues/135
# include PubSubCommands
include PubSubCommands

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This also mixes PubSubCommands into Valkey::Pipeline (pipeline.rb:7 includes Commands), which never assigns @pubsub — so r.pipelined { |p| p.publish("m", "c") } raises NoMethodError: undefined method 'publish' for nil and aborts the whole batch, where PUBLISH would otherwise pipeline fine.

Suggested fix: have PubSubCommands#publish call send_command(RequestType::PUBLISH, [channel.to_s, message.to_s]) on self (keeping the sharded: guard in the mixin) so it works for both the client and a pipeline; the subscribe/consume methods aren't pipelineable and are worth an explicit Pipeline override that raises something readable.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This is a good point and raised an oversight in my design of the PubSub class. In order to support PubSub in Batch, we will need to refactor PubSub into a mixin. I will handle this in a follow up PR once this is merged.

@Aryex Aryex Sep 8, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I guess it is less of an in compatibility and more that it is an anti-pattern with how the current client work. See my comment.

My suggestions is to perform this refactor after this is merged. Then proceed with support for other APIs

Comment thread lib/valkey/glide/pubsub.rb Outdated

# glide-core takes the timeout as the last command argument, in whole
# milliseconds, and reads a zero as "no deadline".
def timeout_argument(timeout)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

timeout: nil"0", and glide-core reads 0 as no deadline: wait_for_sync sets deadline = None and falls through to a bare notified.await, and the pubsub interception bypasses request_timeout. Since command is blocking: true, a subscribe that never reconciles (server restarted right after connect) parks the thread permanently — Timeout.timeout and Thread#raise can't interrupt a thread inside a blocking FFI call — and this PR removed the "nil blocks indefinitely" line from every public doc comment.

Suggested fix: restore that wording on the blocking methods, or default timeout: to the client's request_timeout so the failure is a Valkey::TimeoutError instead of a hang.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 730dfaa

Comment thread AGENTS.md Outdated

- **Ruby 3.0+ Required:** Minimum per `valkey.gemspec`
- **FFI dependency:** `ffi ~> 1.17.0`; do not break ABI without rebuilding native lib
- **FFI bindings MUST pass `blocking: true`:** Every `attach_function` in `lib/valkey/bindings.rb` releases the GVL. Ruby-FFI defaults to `blocking: false`, which holds the GVL for the entire native call — that stalls every other Ruby thread, and deadlocks outright whenever the native call waits on a thread that needs the GVL to make progress.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

"Every attach_function" isn't accurate — 16 of the 21 in bindings.rb still default to blocking: false, including create_client and create_client_from_uri (bindings.rb:238, :245), the two that install this Pub/Sub callback and that hold the GVL for the entire connect-and-retry window.

Suggested fix: add blocking: true to both create_client* functions, and scope the rule to calls that can actually block (connect, command, batch, script, close) — the free_* and OTel span helpers don't need it.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good point. updated in 587e665

I have also raised #309 to fix the create_client methods.

Signed-off-by: Alex Le <alex.le@improving.com>
Signed-off-by: Alex Le <alex.le@improving.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants