--- url: /rails/compatibility.md --- # Action Cable Compatibility This compatibility table shows which Action Cable features are supported by AnyCable Rails. | Feature | Status| |--------------------------|--------| | Connection identifiers | ✅\* | | Connection request data (cookies, params) | ✅ | | Disconnect handling | ✅ | | Subscribe to channels | ✅ | | Parameterized subscriptions | ✅ | | Unsubscribe from channels | ✅ | | [Subscription Instance Variables](http://edgeapi.rubyonrails.org/classes/ActionCable/Channel/Streams.html) | ✅ \*\* | | Performing Channel Actions | ✅ | | Streaming | ✅ | | [Custom stream callbacks](http://edgeapi.rubyonrails.org/classes/ActionCable/Channel/Streams.html) | 🚫 | | Broadcasting | ✅ | | Periodical timers | 🚫 | | Disconnect remote clients | ✅ | | Command callbacks | ✅ \*\*\* | \* See [restoring state objects](../architecture.md#restoring-state-objects) for more information on how identifiers work. \*\* See [channel state](./channels_state.md) for more information on subscription instance variables support. \*\*\* AnyCable (via `anycable-rails`) also supports [command callbacks](https://github.com/rails/rails/pull/44696) (`before_command`, `after_command`, `around_command`) for older Rails versions (event when not using AnyCable). ## Runtime checks AnyCable Rails provides a way to enforce compatibility through runtime checks. Runtime checks are monkey-patches which raise exceptions (`AnyCable::CompatibilityError`) when AnyCable-incompatible code is called. To enabled runtime checks add the following file to your configuration (e.g. `config/.rb` or `config/initializers/anycable.rb`): ```ruby require "anycable/rails/compatibility" ``` **NOTE:** compatibility checks could be used with Action Cable (i.e. w/o AnyCable) and don't affect compatible functionality; thus it makes sense to add runtime checks in development and test environments. For example, the following channel class: ```ruby class ChatChannel < ApplicationCable::Channel def subscribed @room = ChatRoom.find(params[:id]) end end ``` raises `AnyCable::CompatibilityError` when client tries to subscribe to the channel, 'cause AnyCable doesn't support storing channel's state in instance variables. ## RuboCop cops AnyCable Rails comes with [RuboCop](https://github.com/rubocop-hq/rubocop) rules to detect incompatible code in your application. Add to your `.rubocop.yml`: ```yml require: - "anycable/rails/rubocop" # ... ``` And run `rubocop`: ```sh $ bundle exec rubocop #=> app/channels/bad_channel.rb:5:5: C: AnyCable/InstanceVars: Channel instance variables are not supported in AnyCable. Use state_attr_accessor instead. #=> @bad_var = "bad" #=> ^^^^^^^^^^^^^^^^ ``` Or you can require AnyCable Rails cops dynamically: ```sh bundle exec rubocop -r 'anycable/rails/rubocop' --only AnyCable ``` **NOTE**: If you have `DisabledByDefault: true` in your RuboCop config, you need to specify all AnyCable Rails cops explicitly: ```sh bundle exec rubocop -r 'anycable/rails/rubocop' \ --only AnyCable/InstanceVars,AnyCable/PeriodicalTimers,AnyCable/InstanceVars ``` You can also install AnyCable Rails RuboCop extension as a separate gem (for example, if you use a dedicated RuboCop Gemfile): ```sh gem "rubocop-anycable-rails" ``` ### Cops #### `AnyCable/InstanceVars` Checks for instance variable usage inside channels: ```ruby # bad class MyChannel < ApplicationCable::Channel def subscribed @post = Post.find(params[:id]) stream_from @post end end # good class MyChannel < ApplicationCable::Channel def subscribed post = Post.find(params[:id]) stream_from post end end ``` #### `AnyCable/StreamFrom` Checks for `stream_from` calls with custom callbacks or coders: ```ruby # bad class MyChannel < ApplicationCable::Channel def follow stream_from("all") {} end end class MyChannel < ApplicationCable::Channel def follow stream_from("all", -> {}) end end class MyChannel < ApplicationCable::Channel def follow stream_from("all", coder: SomeCoder) end end # good class MyChannel < ApplicationCable::Channel def follow stream_from "all" end end ``` #### `AnyCable/PeriodicalTimers` Checks for periodical timers usage: ```ruby # bad class MyChannel < ApplicationCable::Channel periodically(:do_something, every: 2.seconds) end ``` --- --- url: /rails/extensions.md --- # Action Cable extensions AnyCable comes with useful Action Cable API extensions which you may use with (and sometimes without) AnyCable server: * [Presence tracking](#presence-tracking) * [Broadcast API extensions](#broadcast-api) * [Whispering](#whispering) ## Presence tracking AnyCable provides built-in [presence tracking](https://docs.anycable.io/edge/anycable-go/presence) support. You can join (or leave) the presence set either by calling `channel.presence.join()` (or `channel.presence.leave()`) from the client-side (see [AnyCable JS client](https://github.com/anycable/anycable-client)) or by performing the corresponding actions in the server-side channel classes. To control presence from server, you must first include the `AnyCable::Rails::Channel::Presence` module in your channel class. Them, you'll be able to perform presence actions as follows: ```ruby class ChatChannel < ApplicationCable::Channel def subscribed room = Chat::Room.find(params[:id]) stream_for room join_presence( # presence set is associated with the stream, # which is also used for broadcasting join/leave events broadcasting_for(room), # you must provide a unique user identifier id: current_user.id, # (optional) additional user info that will be available # to clients via the presence API info: {name: current_user.name} ) end end ``` You can provide the default values for presence ID and info by overriding the `#user_presence_id` and `#user_presence_info` methods in your channel class. For example, in your base channel class: ```ruby class ApplicationCable::Channel < ActionChannel::Channel::Base include AnyCable::Rails::Channel::Presence private def user_presence_id = current_user.id def user_presence_info = {name: current_user.name} end ``` You can also omit the `stream` argument when calling the `#join_presence` method. In this case, the presence set will be associated with the first stream you've subscribed to **within the action**. Thus, the example above could be rewritten as follows (given that `#user_presence_id` and `#user_presence_info` are defined in the base class): ```ruby class ChatChannel < ApplicationCable::Channel def subscribed room = Chat::Room.find(params[:id]) stream_for room join_presence end end ``` In most cases, you don't need to leave the presence set manually; it happens automatically on unsubscribe or disconnect (with some configurable delay). However, if you want to do that, you can call `leave_presence` method: ```ruby class ChatChannel < ApplicationCable::Channel def leave leave_presence current_user.id end end ``` You can omit the `id` argument and fallback to the default value provided by `#user_presence_id`. ## Broadcast API ### Broadcast to objects > See the [demo](https://github.com/anycable/anycable_rails_demo/pull/34) of using this feature. AnyCable allows to pass not only strings but arbitrary to `ActionCable.server.broadcast` to represent streams, for example: ```ruby user = User.first ActionCable.server.broadcast(user, data) # or multiple objects ActionCable.server.broadcast([user, :notifications], data) ``` This is useful in conjunction with AnyCable [signed streams](../anycable-go/signed_streams.md), since you can pass the same objects to the `AnyCable::Rails.signed_stream_name` method or `#signed_stream_name` helper: ```ruby signed_stream_name([user, :notifications]) ``` This feature is available when using AnyCable Rails with the Action Cable server. ### Broadcast to others AnyCable provides a functionality to deliver broadcasts to all clients except from the one initiated the action (e.g., when you need to broadcast a message to all users in a chat room except the one who sent the message). > **NOTE:** This feature is not available in Action Cable. It relies on [Action Cable protocol extensions](../misc/action_cable_protocol.md) currently only supported by AnyCable. To do so, you need to obtain a unique socket identifier. For example, using [AnyCable JS client](https://github.com/anycable/anycable-client), you can access it via the `cable.sessionId` property. Then, you must attach this identifier to HTTP request as a `X-Socket-ID` header value. AnyCable Rails uses this value to populate the `AnyCable::Rails.current_socket_id` value. If this value is set, you can implement broadcasting to other using one of the following methods: * Calling `ActionCable.server.broadcast stream, data, to_others: true` * Calling `MyChannel.broadcast_to stream, data, to_others: true` Finally, if you perform broadcasts indirectly, you can wrap the code with `AnyCable::Rails.broadcasting_to_others` to enable this feature. For example, when using Turbo Streams: ```ruby AnyCable::Rails.broadcasting_to_others do Turbo::StreamsChannel.broadcast_remove_to workspace, target: item end ``` You can also pass socket ID explicitly (if obtained from another source): ```ruby AnyCable::Rails.broadcasting_to_others(socket_id: my_socket_id) do # ... end # or ActionCable.server.broadcast stream, data, exclude_socket: my_socket_id ``` **IMPORTANT:** AnyCable Rails automatically pass the current socket ID to Active Job, so you can use `broadcast ..., to_others: true` in your background jobs without any additional configuration. ### Batching broadcasts automatically AnyCable supports publishing [broadcast messages in batches](../ruby/broadcast_adapters.md#batching) (to reduce the number of round-trips and ensure delivery order). You can enable automatic batching of broadcasts by setting `ANYCABLE_BROADCAST_BATCHING=true` (or `broadcast_batching: true` in the config file). Auto-batching uses [Rails executor](https://guides.rubyonrails.org/threading_and_code_execution.html#executor) under the hood, so broadcasts are aggregated within Rails *units of work*, such as HTTP requests, background jobs, etc. This feature is only supported when using AnyCable. ## Whispering > See the [demo](https://github.com/anycable/anycable_rails_demo/pull/34) of using whispering with Rails. AnyCable supports *whispering*, or client-initiated broadcasts. A typical use-case for whispering is sending typing notifications in messaging apps or sharing cursor positions. Here is an example client-side code leveraging whispers (using [AnyCable JS][anycable-client]): ```js let channel = cable.subscribeTo("ChatChannel", {id: 42}); channel.on("message", (msg) => { if (msg.event === "typing") { console.log(`user ${msg.name} is typing`); } }) // publishing whispers const { user } = getCurrentUser(); channel.whisper({event: "typing", name}) ``` You MUST explicitly enable whispers in your channel class as follows: ```ruby class ChatChannel < ApplicationCable::Channel def subscribed room = Chat::Room.find(params[:id]) stream_for room, whisper: true end end ``` Adding `whisper: true` to the stream subscription enables **sending** broadcasts for this client; all subscribed client receive whispers (as regular broadcasts). **IMPORTANT:** There can be only one whisper stream per channel subscription (since from the protocol perspective clients don't know about streams). **NOTE:** This feature is partially supported by Action Cable server (when `anycable-rails` is loaded). The difference is that *whispers* are broadcasted to all clients, including the initiator. ## Helpers AnyCable provides a few helpers you can use in your views: * `action_cable_with_jwt_meta_tag`: an alternative to `action_cable_meta_tag` with [JWT support](./authentication.md#jwt-authentication). * `signed_stream_name`: generates a signed stream name for [AnyCable signed streams](../anycable-go/signed_streams.md). If you want to generate signed stream names outside of views, you can use the `AnyCable::Rails.signed_stream_name` method. ## Connection mixins AnyCable Rails comes with a couple of module you can add to your `ApplicationCable::Connection` class to bring AnyCable-specific features to Action Cable (i.e., to use them without AnyCable): * `AnyCable::Rails::Ext::JWT`: adds AnyCable JWT support (see [more](./authentication.md#jwt-authentication)). ## Signed streams [AnyCable signed streams](../anycable-go/signed_streams.md) is automatically enabled as soon as you load the `anycable-rails` gem. That means, that you can subscribe to a specific "$pubsub" channel and provide a signed stream name to subscribe to Action Cable updates without creating any channels (similar to how Hotwire works): ```js const consumer = createConsumer(); const channel = consumer.subscriptions.create({channel: "$pubsub", signed_stream_name: ""}); ``` You can enable public (unsigned) streams support by overriding the `#allow_public_streams?` method in your connection class: ```ruby module ApplicationCable class Connection < ActionCable::Connection::Base # ... def allow_public_streams? true end end end ``` [anycable-client]: https://github.com/anycable/anycable-client --- --- url: /misc/action_cable_protocol.md --- # Action Cable Protocol [Action Cable](https://guides.rubyonrails.org/action_cable_overview.html) is a framework that allows you to integrate WebSockets with the rest of your Rails application easily. It uses a simple JSON-based protocol for client-server communication. AnyCable also implements an [extended version of the protocol](#action-cable-extended-protocol) to provide better consistency guarantees. ## Messages Communication is based on messages. Every message is an object. Protocol-related messages from server to client MUST have `type` field (string). Possible types: * \[`welcome`] * \[`disconnect`] * \[`ping`] * \[`confirm_subscription`] * \[`reject_subscription`] There are also *data* messages–broadcasts and transmissions–they MUST have `message` field. Protocol-related messages from client to server MUST have `command` field (string). Possible commands: * \[`subscribe`] * \[`unsubscribe`] * \[`message`] ## Handshake When client connects to server one of the following two could happen: * server accepts the connection and responds with `welcome` message (`{"type":"welcome"}`) * server rejects the connection and responds with a `disconnect` message, which may include fields `reason` and `reconnect` (`{"type":"disconnect", "reason":"unauthorized", "reconnect":false}`)\* Server MUST respond with either a `welcome` message or a `disconnect` message. \* `disconnect` message only exists in Rails 6.0 and later. Prior to 6.0, server would drop the connection without sending anything. ## Subscriptions & identifiers Data messages, client-to-server messages and some server-to-client messages (`confirm_subscription`, `reject_subscription`) MUST contain `identifier` field (string) which is used to route data to the specified *channel*. It's up to server and client how to generate and resolve identifiers. Rails identifiers schema is the following: `{ channel: "MyChannelClass", **params }.to_json`. For example, to subscribe to `ChatChannel` with `id: 42` client should send the following message: ```json { "identifier": "{\"channel\":\"ChatChannel\",\"id\":42}", "command": "subscribe" } ``` The response from server MUST contain the same identifier, e.g.: ```json { "identifier": "{\"channel\":\"ChatChannel\",\"id\":42}", "type": "confirm_subscription" } ``` To unsubscribe from the channel client should send the following message: ```json { "identifier": "{\"channel\":\"ChatChannel\",\"id\":42}", "command": "unsubscribe" } ``` There is no *unsubscription* confirmation sent (see [PR#24900](https://github.com/rails/rails/pull/24900)). ## Receive messages Data message from server to client MUST contain `identifier` field and `message` field with the data itself. ## Perform actions *Action* message from client to server MUST contain `command` ("message"), `identifier` fields, and `data` field containing a JSON-encoded value. The `data` field MAY contain `action` field. For example, in Rails to invoke a method on a channel class, you should send: ```json { "identifier": "{\"channel\":\"ChatChannel\",\"id\":42}", "command": "message", "data": "{\"action\":\"speak\",\"text\":\"hello!\"}" } ``` ## Ping Although [WebSocket protocol](https://tools.ietf.org/html/rfc6455#section-5.5.2) describes low-level `ping`/`pong` frames to detect dropped connections, some implementation (e.g. browsers) don't provide an API for using them. That's why Action Cable protocol has its own, protocol-level pings support. Server sends `ping` messages (`{ "type": "ping", "message": }`) every X seconds (3 seconds in Rails). Client MAY track this messages and decide to re-connect if no `ping` messages have been observed in the last Y seconds. For example, default Action Cable client reconnects if no `ping` messages have been received in 6 seconds. ## Action Cable Extended protocol **NOTE:** This protocol extension is only supported by AnyCable-Go v1.4+. The `actioncable-v1-ext-json` protocol adds new message types and extends the existing ones. > You can find the example implementation of the protocol in the [anycable-client library](https://github.com/anycable/anycable-client/blob/master/packages/core/action_cable_ext/index.js). ### New command: `history` The new command type is added, `history`. It is used by the client to request historical messages for the channel. It MUST contain the `identifier`, `command` ("history"), and `history` fields, where `history` contains the *history request* object: ```js { "identifier": "{\"channel\":\"ChatChannel\",\"id\":42}", "command": "history", "history": { "since": 1681828329, "streams": { "stream_id_1": { "offset": 32, "epoch": "x123" }, "stream_id_2": { "offset": 54, "epoch": "x123" } } } } ``` A history request contains two fields: * `since` is a UNIX timestamp in seconds indicating the time since when to fetch the history. Optional. It is used only for streams with no offset specified (usually, during the initial subscription). * `streams` is a map of stream IDs to observed offsets. Stream IDs, offsets, and epochs are received along with the messages. It's the responsibility of the client to track them and use for `history` requests. The `epoch` parameter specified the current state of the memory backend; if the current server's epoch doesn't match the requested one, the server fail to retrieve the history. For example, if in-memory backend is used to store streams history, every time a server restarts a new epoch starts. In response to a `history` request, the server MUST respond with the requested historical messages (sent one by one, like during normal broadcasts, so the client shouldn't handle them specifically). Then, the server sends an acknowledgment message (`confirm_history`). If case messages couldn't be retrieved from the server (e.g., history has been evicted for a stream), the server MUST respond with the `reject_history` message. ### Requesting history during subscription It's possible to request history along with the `subscribe` request by adding the `history` field to the command payload: ```js { "identifier": "{\"channel\":\"ChatChannel\",\"id\":42}", "command": "subscribe", "history": { "since": 1681828329 } } ``` Usually, in this case we can only specify the `since` parameter. In response, the server MUST first confirm the subscription and then execute the history request. If the subscription is rejected, no history request is made. ### New message types: `confirm_history` and `reject_history` Two new message types (server-client) are added: * \[`confirm_history`] * \[`reject_history`] Both messages act as acknowledgments for the `history` command and contain the `identifier` key. The `confirm_history` message is sent to the client to indicate that the requested historical messages for the channel have been successfully sent to the client. The `reject_history` indicates that the server failed to retrieve the requested messages and no historical message have been sent (the client must implement a fallback mechanism to restore the consistency). ### History metadata in broadcasted messages Broadcasted messages MAY contain metadata regarding their position in the stream. This information MUST be used with the subsequent `history` requests: ```js { "identifier": "{\"channel\":\"ChatChannel\",\"id\":42}", "message": { "text": "hello!", "user_id": 43 }, // NEW FIELDS: "stream_id": "chat_42", "epoch": "x123", "offset": 32 } ``` **NOTE:** Offsets are assumed to be in order (however, some history backends may lift this requirement, check the corresponding documentation). There is a small chance that the same message may arrive twice (from broadcast and from the history); to provide exactly-once delivery guarantees, the client MUST keep track of seen offsets and ignore duplicates. ### Handshake metadata During the handshake, the server MAY send a unique *session id* along with the welcome message: ```js { "type": "welcome", "sid": "rcl245" } ``` The client MAY use this ID during re-connection to restore the session state (subscriptions, channel states, etc.) and avoid re-subscribing to the channels. For that, the previously obtained session ID must be provided either as a query parameter (`?sid=rcl245`) or via an HTTP header (`X-ANYCABLE-RESTORE-SID`). If the server's attempt to restore the session from the *sid* succeeds, it MUST respond with the `welcome` message with the additional fields indicating that the session was restored: ```js { "type": "welcome", "sid": "yi421", // Note that session ID has changed "restored": true, "restored_ids": [ "{\"channel\":\"ChatChannel\",\"id\":42}" ] } ``` The `restored` flag indicates whether the session state has been restored. **NOTE:** In this case, no `connect` method is invoked at the Action Cable side. The optional `restored_ids` field contains the list of channel identifiers that has been re-subscribed automatically at the server side. The client MUST NOT try to resubscribe to the specified channels and consider them connected. It's recommended to perform `history` requests for all the restored channels to catch up with the messages. ### New command: `pong` The `pong` command MAY be sent in response to the `ping` message if the server requires pongs. It could be used to improve broken connections detection. ### New command: `whisper` The `whisper` can be used to publish broadcast messages from the client (if the whisper stream has been configured for it) to a particular *channel*. The payload MUST contain `command` ("whisper"), `identifier` fields, and `data` fields. The `data` field MAY contain a string or an object. For example: ```json { "identifier": "{\"channel\":\"ChatChannel\",\"id\":42}", "command": "whisper", "data": { "event":"typing", "user":"Jack" } } ``` **IMPORTANT**: Unlike actions (`message` command), the data is not JSON-serialized. It's broadcasted to connected clients as is. ### New commands: `join` and `leave` Presence commands allow the client to join or leave the presence set for a given channel: ```js { "command": "join", "identifier": "", "presence": { "id": "", "info": "..." // any object or string } } { "command": "leave", "identifier": "", "presence": { "id": "" } } ``` ### New commands: `presence` The presence command is used to request the current presence state for a given channel: ```js { "command": "presence", "identifier": "", } ``` You can also provide the "data" field with some configuration options: ```js { "command": "presence", "identifier": "", "data": { "return_records": true // set to false to return only totals, see below } } ``` ### New message type: `presence` Presence messages have the following formats: ```js { "type": "presence", "identifier": "", "message": { "id": "", "info": "..." // any object or string "type": "join" } } { "type": "presence", "identifier": "", "message": { "id": "", "type": "leave" } } { "type": "presence", "identifier": "", "message": { "total": 123, // total number of present users "records": [ { "id": "...", "info": "..." }, ... ], # presence records (missing if return_records is false) "type": "info" } } ``` --- --- url: /ruby/cli.md --- # AnyCable CLI [AnyCable Ruby](https://github.com/anycable/anycable) comes with a gRPC server for AnyCable and a CLI to run this server along with your Ruby application. Run `anycable` CLI to start a gRPC server: ```sh $ bundle exec anycable --require "./path/to/app.rb" #> Starting AnyCable gRPC server (pid: 85746, workers_num: 30) #> AnyCable version: 1.5.0 #> gRPC version: 1.57.0 #> Serving Rails application from ./path/to/app.rb ... #> ... ``` You only have to tell AnyCable where to find your application code via the `--require` (`-r`) option. By default, we check for the `config/anycable.rb` and `config/environment.rb` files presence (in the specified order). Thus, you don't need to specify any options when using with Ruby on Rails. Run `anycable -h` to see the list of all available options and their defaults. See also [configuration documentation](./configuration.md). ## Running AnyCable server along with RPC AnyCable CLI provides an option to run any arbitrary command along with the RPC server. That could be useful for local development and even in production (e.g. for [Heroku deployment](../deployment/heroku.md)). For example: ```sh $ bundle exec anycable --server-command "anycable-go -p 8080" #> Starting AnyCable gRPC server (pid: 85746, workers_num: 30) #> AnyCable version: 1.5.0 #> gRPC version: 1.57.0 #> Serving Rails application from ./path/to/app.rb ... #> ... #> Started command: anycable-go --port 8080 (pid: 13710) #> ... ``` --- --- url: /anycable-go/instrumentation.md --- # AnyCable Instrumentation AnyCable server provides useful statistical information about the service (such as the number of connected clients, received messages, etc.). > Read the ["Real-time stress: AnyCable, k6, WebSockets, and Yabeda"](https://evilmartians.com/chronicles/real-time-stress-anycable-k6-websockets-and-yabeda) post to learn more about AnyCable observability and see example Grafana dashboards. ## Metrics and what can we learn from them Instrumentation exists to help us preventing and identifying performance issues. Here we provide the list of the most crucial metrics and how to interpret their values. **NOTE:** Some values are updated at real time, others (*interval metrics*) are updated periodically (every 5 seconds by default, could be configured via the `stats_refresh_interval` configuration parameter). Interval metrics are marked with the ⏱ icon below. **NOTE:** The `*_total` metrics are *counters*; when printing metrics to logs, the delta between two subsequent metrics collections is displayed; Prometheus works with absolute (i.e., cumulative) values and interpolates them on its own. ### ⏱ `clients_num` / `clients_uniq_num` The `clients_num` shows the current number of *active* sessions (WebSocket connections). A session is considered activated as soon as it has been **authenticated** and until the connection is closed. The `clients_uniq_num` shows the current number of unique *connection identifiers* across the active sessions. A connection identifier is a combination of `identified_by` values for the corresponding Connection object. One the useful derivative of these two metrics is the `clients_uniq_num` / `clients_num` ratio. If it's much less than 1 and is decreasing, that could be an indicator of an improper connection managements at the client side (e.g., creating a *client* per a component mount or a Turbo navigation instead of re-using a singleton). ### `rpc_call_total`, `rpc_error_total`, `rpc_retries_total`, `rpc_timeouts_total`, `rpc_pending_num`, etc These are the vital metrics of the RPC communication channel. The `rpc_error_total` describes the number of failed RPC calls. This is the actual number of *commands* that failed. The most common reason for the is a lack of network connectivity with the RPC service. Another potential reason is the RPC schema incompatibility (in that case, most RPC requests would fail, i.e., `rpc_call_total / rpc_error_total` tends to 1). The `rpc_retries_total` describes the number of retried RPC calls. Retries could happen if the RPC server is exhausted or unavailable (no network connectivity). The former indicates that **concurrency settings for RPC and anycable-go went out of sync** (see [here](./configuration.md)). The `rpc_timeouts_total` shows the number of timed out RPC requests. Use this metric to tune the RPC timeout settings (`--rpc_request_timeout`). The `rpc_canceled_total` shows the number of RPC calls that were discarded due to the initiator (the connection) being terminated. Higher values could indicate either misbehaving clients (connecting, subscribing and disconnecting too quickly) or lack of RPC capacity to process all incoming commands (in this case, it should correlate with timeouts). The `rpc_pending_num` is the **key latency metrics** of AnyCable-Go. We limit the number of concurrent RPC requests (to prevent the RPC server exhaustion and retries). If the number of pending requests grows (which means we can not keep up with the rate of incoming messages), you should consider either tuning concurrency settings or scale up your cluster. ### `publications_total` / `broadcast_msg_total` / `remote_commands_total` We provide various metrics around broadcasting functionality that could help you to identify problems in different parts of the AnyCable's pub/sub architecture. The `publications_total` describes the number of publication requests received from the application through a non-distributed broadcasting interface (e.g., HTTP or Redis Streams). Each publication is processed only by a single node in the cluster, so the values could tell you how the publications are distributed and whether there is a connectivity between the application and the AnyCable cluster. Both message broadcasts and remote commands are considered publications for this metrics. The `broadcast_msg_total` describes the number of broadcasts (i.e., attempts to send a message to clients connected to a given stream) performed by the server instance. The value is incremented by each server in the cluster for each publication. Note that when using HTTP or Redis Streams broadcaster, this value is only updated when there are registered clients for the stream. When using legacy NATS or Redis Pub/Sub broadcasters, the value is incremented for each publication. ### `failed_auths_total` This `failed_auths_total` indicates the total number of unauthenticated connection attempts and has a special purpose: it helps you identify misconfigured client credentials and malicious behaviour. Ideally, the change rate of this number should be low comparing to the `clients_num`.) ### ⏱ `disconnect_queue_size` The `disconnect_queue_size` shows the current number of pending Disconnect calls. AnyCable-Go performs Disconnect calls in the background with some throttling (by default, 100 calls per second). During the normal operation, the value should be close to zero most of the a time. Larger values or growth could indicate inefficient client-side connection management (high re-connection rate). Spikes could indicate mass disconnect events. ### ⏱ `goroutines_num` The `goroutines_num` metrics is meant for debugging Go routines leak purposes. The number should be O(N), where N is the `clients_num` value for the OSS version and should be O(1) for the PRO version (unless IO polling is disabled). ### `mem_sys_bytes` The total bytes of memory obtained from the OS (according to [`runtime.MemStats.Sys`](https://golang.org/pkg/runtime/#MemStats)). You can also enabled detailed memory usage metrics by providing the `ANYCABLE_MEMORY_METRICS=1` environment variable. That would add additional metrics as in the example below: ``` heap_alloc_total=2226352 heap_idle_bytes=4104192 heap_released_bytes=4104192 heap_sys_bytes=7569408 mem_sys_bytes=13912328stack_sys_bytes=819200 ``` ## Prometheus To enable a HTTP endpoint to serve [Prometheus](https://prometheus.io)-compatible metrics (disabled by default) you must specify `--metrics_http` option (e.g. `--metrics_http="/metrics"`). You can also change a listening port and listening host through `--metrics_port` and `--metrics_host` options respectively (by default the same as the main (websocket) server port and host, i.e., using the same server). The exported metrics format is the following (NOTE: the list above is just an example and could be incomplete): ```sh # HELP anycable_go_clients_num The number of active clients # TYPE anycable_go_clients_num gauge anycable_go_clients_num 0 # HELP anycable_go_clients_uniq_num The number of unique clients (with respect to connection identifiers) # TYPE anycable_go_clients_uniq_num gauge anycable_go_clients_uniq_num 0 # HELP anycable_go_client_msg_total The total number of received messages from clients # TYPE anycable_go_client_msg_total counter anycable_go_client_msg_total 5906 # HELP anycable_go_failed_client_msg_total The total number of unrecognized messages received from clients # TYPE anycable_go_failed_client_msg_total counter anycable_go_failed_client_msg_total 0 # HELP anycable_go_broadcast_msg_total The total number of messages received through PubSub (for broadcast) # TYPE anycable_go_broadcast_msg_total counter anycable_go_broadcast_msg_total 956 # HELP anycable_go_failed_broadcast_msg_total The total number of unrecognized messages received through PubSub # TYPE anycable_go_failed_broadcast_msg_total counter anycable_go_failed_broadcast_msg_total 0 # HELP anycable_go_broadcast_streams_num The number of active broadcasting streams # TYPE anycable_go_broadcast_streams_num gauge anycable_go_broadcast_streams_num 0 # HELP anycable_go_rpc_call_total The total number of RPC calls # TYPE anycable_go_rpc_call_total counter anycable_go_rpc_call_total 15808 # HELP anycable_go_rpc_error_total The total number of failed RPC calls # TYPE anycable_go_rpc_error_total counter anycable_go_rpc_error_total 0 # HELP anycable_go_rpc_retries_total The total number of RPC call retries # TYPE anycable_go_rpc_retries_total counter anycable_go_rpc_retries_total 0 # HELP anycable_go_rpc_pending_num The number of pending RPC calls # TYPE anycable_go_rpc_pending_num gauge anycable_go_rpc_pending_num 0 # HELP anycable_go_failed_auths_total The total number of failed authentication attempts # TYPE anycable_go_failed_auths_total counter anycable_go_failed_auths_total 0 # HELP anycable_go_goroutines_num The number of Go routines # TYPE anycable_go_goroutines_num gauge anycable_go_goroutines_num 5222 # HELP anycable_go_disconnect_queue_size The size of delayed disconnect # TYPE anycable_go_disconnect_queue_size gauge anycable_go_disconnect_queue_size 0 # HELP anycable_go_server_msg_total The total number of messages sent to clients # TYPE anycable_go_server_msg_total counter anycable_go_server_msg_total 453 # HELP anycable_go_failed_server_msg_total The total number of messages failed to send to clients # TYPE anycable_go_failed_server_msg_total counter anycable_go_failed_server_msg_total 0 # HELP anycable_go_data_sent_total The total amount of bytes sent to clients # TYPE anycable_go_data_sent_total counter anycable_go_data_sent_total 1232434334 # HELP anycable_go_data_rcvd_total The total amount of bytes received from clients # TYPE anycable_go_data_rcvd_total counter anycable_go_data_rcvd_total 434334 ``` ## StatsD AnyCable also supports emitting real-time metrics to [StatsD](https://github.com/statsd/statsd). For that, you must specify the StatsD server UDP host: ```sh anycable-go -statsd_host=localhost:8125 ``` Metrics are pushed with the `anycable_go.` prefix by default. You can override it by specifying the `statsd_prefix` parameter. Find more info about StatsD metric types [here](https://github.com/statsd/statsd/blob/master/docs/metric_types.md). Example payload: ```sh anycable_go.mem_sys_bytes:15516936|g anycable_go.clients_num:0|g anycable_go.clients_uniq_num:0|g anycable_go.broadcast_streams_num:0|g anycable_go.disconnect_queue_size:0|g anycable_go.rpc_pending_num:0|g anycable_go.failed_server_msg_total:1|c anycable_go.rpc_call_total:1|c anycable_go.rpc_retries_total:1|c anycable_go.rpc_error_total:1|c ``` ## Default metrics tags You can define global tags (added to every reported metric by default) for Prometheus (reported as labels) and StatsD. For example, we can add environment and node information: ```sh anycable-go --metrics_tags=environment:production,node_id:xyz # or via environment variables ANYCABLE_METRICS_TAGS=environment:production,node_id:xyz anycable-go ``` For StatsD, you can specify tags format: "datadog" (default), "influxdb", or "graphite". Use the `statsd_tag_format` configuration parameter for that. ## Logging Another option is to periodically write stats to log (with `info` level). To enable metrics logging pass `--metrics_log` flag. Your logs should contain something like this: ```sh INFO 2018-03-06T14:16:27.872Z broadcast_msg_total=0 broadcast_streams_num=0 client_msg_total=0 clients_num=0 clients_uniq_num=0 context=metrics disconnect_queue_size=0 failed_auths_total=0 failed_broadcast_msg_total=0 failed_client_msg_total=0 goroutines_num=35 rpc_call_total=0 rpc_error_total=0 ``` By default, metrics are logged every 15 seconds (you can change this behavior through `--metrics_rotate_interval` option). By default, all available metrics are logged. You can specify a subset of metrics to print to logs via the `--metrics_log_filter` option. For example: ```sh $ anycable-go --metrics_log_filter=clients_num,rpc_call_total,rpc_error_total ... INFO 2023-02-21T15:49:25.744Z context=metrics Log metrics every 15s (only selected fields: clients_num, rpc_call_total, rpc_error_total) ... ``` ### Custom loggers with mruby > 👨‍🔬 This is an experimental API and could change in the future 👩‍🔬 AnyCable-Go allows you to write custom log formatters using an embedded [mruby](http://mruby.org) engine. mruby is the lightweight implementation of the Ruby language. Hence it is possible to use Ruby to write metrics exporters. First, you should download the version of `anycable-go` with mruby (it's not included by default): these binaries have `-mrb` suffix right after the version (i.e. `anycable-go-1.0.0-mrb-linux-amd64`). **NOTE**: only MacOS and Linux are supported. **NOTE**: when a server with mruby support is starting you should the following message: ```sh $ anycable-go INFO 2019-08-07T16:37:46.387Z context=main Starting AnyCable v0.6.2-13-gd421927 (with mruby 1.2.0 (2015-11-17)) (pid: 1362) ``` Secondly, write a Ruby script implementing a simple interface: ```ruby # Module MUST be named MetricsFormatter module MetricsFormatter # The only required method is .call. # # It accepts the metrics Hash and MUST return a string def self.call(data) data.to_json end end ``` Finally, specify `--metrics_log_formatter` when running a server: ```sh anycable-go --metrics_log_formatter path/to/custom_printer.rb ``` #### Example This a [Librato](https://www.librato.com)-compatible printer: ```ruby module MetricsFormatter def self.call(data) parts = [] data.each do |key, value| parts << "sample##{key}=#{value}" end parts.join(" ") end end ``` ```sh INFO 2018-04-27T14:11:59.701Z sample#clients_num=0 sample#clients_uniq_num=0 sample#goroutines_num=0 ``` --- --- url: /rails/getting_started.md --- # AnyCable on Rails AnyCable can be used as a drop-in replacement for Action Cable in Rails applications. It supports most Action Cable features (see [Compatibility](./compatibility.md) for more) and can be used with any Action Cable client. Moreover, AnyCable brings additional power-ups for your real-time features, such as [streams history support](../guides/reliable_streams.md) and [API extensions](./extensions.md). > See also the [demo](https://github.com/anycable/anycable_rails_demo/pull/2) of migrating from Action Cable to AnyCable. ## Requirements * Ruby >= 2.7 * Rails >= 6.0 See also requirements for [broadcast adapters](../ruby/broadcast_adapters.md) (You can start with HTTP to avoid additional dependencies). ## Installation Add AnyCable Rails gem to your Gemfile: ```ruby # If you plan to use gRPC gem "anycable-rails", "~> 1.5" # If you plan to use HTTP RPC or no RPC at all gem "anycable-rails-core", "~> 1.5" ``` Read more about different RPC modes [here](../anycable-go/rpc.md). Then, run the interactive configuration wizard via Rails generators: ```sh bin/rails g anycable:setup ``` The command above asks you a few questions to configure AnyCable for your application. Want more control? Check out the [manual setup section](#manual-setup) below. ## Configuration AnyCable Rails uses [Anyway Config][] for configuration. Thus, you can store configuration parameters whenever you want: YAML files, credentials, environment variables, whatever. We recommend keeping non-sensitive and *stable* parameters in `config/anycable.yml`, e.g., broadcast adapter, default JWT TTL, etc. For secrets (`secret`, `broadcast_key`, etc.), we recommend using Rails credentials. The most important configuration settings are: * **secret**: a common secret used to secure AnyCable features (signed streams, JWT, etc.). Make sure the value is the same for your Rails application and AnyCable server. * **broadcast\_adapter**: defines how to deliver broadcast messages from the Rails application to AnyCable server (so it can transmit them to connected clients). See [broadcasting docs](../ruby/broadast_adapters.md) for available options and their configuration. See AnyCable Ruby [configuration](../ruby/configuration.md) for more information. ### Forgery protection AnyCable respects [Action Cable configuration](https://guides.rubyonrails.org/action_cable_overview.html#allowed-request-origins) regarding forgery protection if and only if `ORIGIN` header is proxied by AnyCable server, i.e.: ```sh anycable-go --headers=cookie,origin --port=8080 ``` However, we recommend performing the origin check at the AnyCable server side (via the `--allowed_origins` option). See [AnyCable configuration](../anycable-go/configuration.md). ### Embedded gRPC server It is possible to run AnyCable gRPC server within another Ruby process (Rails server or tests runner). We recommend using this option in development and test environments only or in single-process production environments. To automatically start a gRPC server every time you run `rails s`, add `embedded: true` to your configuration. For example: ```yml # config/anycable.yml development: embedded: true ``` **NOTE:** Make sure you have `Rails.application.load_server` in your `config.ru`. The feature is available since Rails 6.1. ## Manual setup ### Prerequisites Make sure you have `require "action_cable/engine"` or `require "rails/all"` in your `config/application.rb` (AnyCable relies Action Cable abstractions). ### Development First, activate AnyCable in your Rails application by specifying it as an adapter for Action Cable: ```yml # config/cable.yml development: adapter: any_cable # ... ``` Then, create `config/anycable.yml` with basic AnyCable configuration: ```yml # config/anycable.yml development: broadcast_adapter: http websocket_url: ws://localhost:8080/cable ``` Install [AnyCable server](#server-installation) and add the following commands to your `Procfile.dev` file\*: ```sh web: bin/rails s # ... ws: anycable-go # When using gRPC rpc: bundle exec anycable ``` Now, run your application via your process manager (or `bin/dev`, if any). You are AnyCable-ready! \* If you don't have a process manager yet, we recommend using [Overmind][]. [Foreman][] works, too. **IMPORTANT**: Despite AnyCable providing multiple RPC modes, we recommend having similar development and production setups. Thus, if you use gRPC in production, use it in development, too. ### Production > The quickest way to get AnyCable server for production usage is to use our managed (and free) solution: [plus.anycable.io](https://plus.anycable.io) Whenever you're ready to push your AnyCable-backed Rails application to production (or staging), make sure your application is configured the right way: * Configure Action Cable adapter for production: ```yml # config/cable.yml production: adapter: any_cable # ... ``` * Provide AnyCable WebSocket URL via the `ANYCABLE_WEBSOCKET_URL` environment variable. Alternatively, you can use Rails credentials or YAML configuration. **IMPORTANT:** The URL configuration is used by the `#action_cable_meta_tag` helper. Make sure you have it in your HTML layout. * Make sure you configured secrets obtained from your AnyCable server (`secret`, `broadcast_key`, etc.) * When using gRPC server, make sure you have a corresponding new process added to your deployment. Check out our [deployment guides](../deployment) to learn more about your deployment methods and AnyCable. ## Server installation For your convenience, we provide a binstub (`bin/anycable-go`) which automatically downloads an AnyCable server binary (and caches it) and launches it. Run the following command to add it to your project: ```sh $ bundle exec rails g anycable:bin ... ``` You can also install AnyCable server yourself using one of the [multiple ways](../anycable-go/getting_started.md#installation). ## Testing with AnyCable If you'd like to run AnyCable gRPC server in tests (for example, in system tests), we recommend to start it manually only when necessary (i.e., when dependent tests are executed) and use the embedded mode. You can also run AnyCable server automatically when starting a gRPC server. That's how we do it with RSpec: ```ruby # spec/support/anycable_setup.rb RSpec.configure do |config| cli = nil config.before(:suite) do examples = RSpec.world.filtered_examples.values.flatten has_no_system_tests = examples.none? { |example| example.metadata[:type] == :system } # Only start RPC server if system tests are included into the run next if has_no_system_tests require "anycable/cli" $stdout.puts "\n⚡️ Starting AnyCable RPC server...\n" AnyCable::CLI.embed!(%w[--server-command=bin/anycable-go]) end end ``` To use `:test` Action Cable adapter along with AnyCable, you can extend it in the configuration: ```rb # config/environments/test.rb Rails.application.configure do config.after_initialize do # Don't forget to configure URL in your anycable.yml or via ANYCABLE_WEBSOCKET_URL config.action_cable.url = ActionCable.server.config.url = AnyCable.config.websocket_url # Make test adapter AnyCable-compatible AnyCable::Rails.extend_adapter!(ActionCable.server.pubsub) end # ... end ``` ## Gradually migrating from Action Cable It's possible to run AnyCable along with Action Cable, so you can still serve legacy connections (or perform gradual roll-out, A/B testing, etc.). A common use-case is switching from Action Cable to AnyCable while updating the WebSocket URL (e.g., when you have no control over a load balancer or ingress, so you can't just switch `/cable` traffic to a different service). To achieve a smooth migration, you need to accomplish the following steps: * Continue using your current pub/sub adapter for Action Cable (say, `redis`) but extend it with AnyCable broadcasting capabilities by adding the following code: ```ruby # config/initializers/anycable.rb AnyCable::Rails.extend_adapter!(ActionCable.server.pubsub) unless AnyCable::Rails.enabled? ``` * You can also add AnyCable JWT support to Action Cable. See [authentication docs](./authentication.md#jwt-authentication). That's it! Now you can serve Action Cable clients via both `ws:///cable` and `ws:///cable`, and they should be able to communicate with each other. **NOTE:** If you use `graphql-anycable`, things become more complicated. You will need to schemas with different subscriptions providers and a similar dual adapter to support both *cables*. [Overmind]: https://github.com/DarthSim/overmind [Foreman]: https://github.com/ddollar/foreman [Anyway Config]: https://github.com/palkan/anyway_config --- --- url: /anycable-go/rpc.md --- # AnyCable RPC AnyCable allows you to control all the real-time communication logic from your backend application. For that, AnyCable uses a *remote procedure call* (RPC) mechanism to delegate handling of connection lifecycle events and processing of incoming messages (subscriptions, arbitrary actions). Using RPC is required if you design your real-time logic using *Channels* (like in Rails Action Cable). For primitive pub/sub, you can run AnyCable in a [standalone mode](./getting_started.md#standalone-mode-pubsub-only), i.e., without RPC. ## RPC over gRPC AnyCable is built for performance. Hence, it defaults to gRPC as a transport/protocol for RPC communication. By default, AnyCable tries to connect to a gRPC server at `localhost:50051`: ```sh $ anycable-go 2024-03-06 14:09:23.532 INF Starting AnyCable 1.6.0-4f16b99 (pid: 21540, open file limit: 122880, gomaxprocs: 8) nodeid=6VV3mO ... 2024-03-06 14:09:23.533 INF RPC controller initialized: localhost:50051 (concurrency: 28, impl: grpc, enable_tls: false, proto_versions: v1) nodeid=6VV3mO context=rpc ``` When having multiple RPC servers, we recommend using gRPC's built-in DNS-based client-side load balancing. For that, you must explicitly specify the `dns:///` scheme for the RPC host parameter. You can also specify a fixed list RPC services to use, e.g., `--rpc_host=grpc-list://rpc1.example.com:50051,rpc2.example.com:50051`. See more in the [load balancing docs](/deployment/load_balancing). In addition to using the `--rpc_host` option, you can use the `ANYCABLE_RPC_HOST` env variable. [AnyCable Ruby][anycable-ruby] library comes with AnyCable gRPC server out-of-the-box. For other platforms, you can use definitions for the AnyCable gRPC service ([rpc.proto][proto]) to write your custom RPC server. ## RPC over HTTP AnyCable also supports RPC communication over HTTP. It's a good alternative if you don't want to deal with separate gRPC servers or you are using a platform that doesn't support gRPC (e.g., Heroku, Google Cloud Run). To connect to an HTTP RPC server, you must specify the `--rpc_host` (or `ANYCABLE_RPC_HOST`) with the explicit `http://` (or `https://`) scheme: ```sh $ anycable-go --rpc_host=http://localhost:3000/_anycable 2024-03-06 14:21:37.231 INF Starting AnyCable 1.6.0-4f16b99 (pid: 26540, open file limit: 122880, gomaxprocs: 8) nodeid=VkaKtV ... 2024-03-06 14:21:37.232 INF RPC controller initialized: http://localhost:3000/_anycable (concurrency: 28, impl: http, enable_tls: false, proto_versions: v1) nodeid=VkaKtV context=rpc ``` [AnyCable Ruby][anycable-ruby] library allows you to mount AnyCable HTTP RPC right into your Rack-compatible web server. [AnyCable JS][anycable-server-js] provides HTTP handlers for processing HTTP RPC requests. For other platforms, check out our Open API specification with examples on how to implement AnyCable HTTP RPC endpoint yourself: [anycable.spotlight.io](https://anycable.stoplight.io). ### Configuration and security If HTTP RPC endpoint is open to public (which is usually the case, since HTTP RPC is often embedded into the main application web server), it MUST be protected from unauthorized access. AnyCable can be configured to pass an authentication key along RPC requests in the `Authorization: Bearer ` header. You can either configure the RPC server key explicitly via the `--http_rpc_secret` (or `ANYCABLE_HTTP_RPC_SECRET`) parameter or use the application secret (`--secret`) to generate one using the following formula (in Ruby): ```ruby rpc_secret_key = OpenSSL::HMAC.hexdigest("SHA256", "", "rpc-cable") ``` Alternatively, using `openssl`: ```sh echo -n 'rpc-cable' | openssl dgst -sha256 -hmac '' | awk '{print $2}' ``` If you use official AnyCable libraries at the RPC server side, you don't need to worry about these details yourself (the shared application secret is used to generate tokens at both sides). Just make sure both sides share the same application or HTTP RPC secret. Other available configuration options: * `http_rpc_timeout`: timeout for HTTP RPC requests (default: 3s). ## Concurrency settings AnyCable uses a single Go gRPC client\* to communicate with AnyCable RPC servers (see [the corresponding PR](https://github.com/anycable/anycable-go/pull/88)). We limit the number of concurrent RPC calls to avoid flooding servers (and getting `ResourceExhausted` exceptions in response). \* A single *client* doesn't necessary mean a single connection; a Go gRPC client could maintain multiple HTTP2 connections, for example, when using [DNS-based load balancing](../deployment/load_balancing). We limit the number of concurrent RPC calls at the application level (to prevent RPC servers overload). By default, the concurrency limit is equal to **28**, which is intentionally less than the default RPC pool size of **30** (for example, in Ruby gRPC server implementation): there is a tiny lag between the times when the response is received by the client and the corresponding worker is returned to the pool. Thus, whenever you update the concurrency settings, make sure that the AnyCable value is *slightly less* than one we use by default for AnyCable Ruby gRPC server. You can change this value via `--rpc_concurrency` (`ANYCABLE_RPC_CONCURRENCY`) parameter. ### Adaptive concurrency AnyCable Pro provides the **adaptive concurrency** feature. When it is enabled, AnyCable automatically adjusts its RPC concurrency limit depending on the two factors: the number of `ResourceExhausted` errors (indicating that the current concurrency limit is greater than RPC servers capacity) and the number of pending RPC calls (indicating the current concurrency is too small to process incoming messages). The first factor (exhausted errors) has a priority (so if we have both a huge backlog and a large number of errors we decrease the concurrency limit). You can enable the adaptive concurrency by specifying 0 as the `--rpc_concurrency` value: ```sh $ anycable-go --rpc_concurrency=0 ... 2024-03-06 14:21:37.232 INF RPC controller initialized: \ localhost:50051 (concurrency: auto (initial=25, min=5, max=100), enable_tls: false, proto_versions: v1) \ nodeid=VkaKtV context=rpc ``` You should see the `(concurrency: auto (...))` in the logs. You can also specify the upper and lower bounds for concurrency via the following parameters: ```sh $ anycable-go \ --rpc_concurrency=0 \ --rpc_concurrency_initial=30 \ --rpc_concurrency_max=50 \ --rpc_concurrency_min=5 ``` You can also monitor the current concurrency value via the `rpc_capacity_num` metrics. Read more about [AnyCable instrumentation](./instrumentation.md). ## Request timeouts Long-running RPC calls may negatively affect the performance of your application, because they block the client from processing new messages. We recommend setting the timeout for RPC requests to prevent this (for historical reasons, it's not set for gRPC and is set and equal to 3s for HTTP RPC). You can do the via the `--rpc_request_timeout` parameter: ```sh # 5s timeout anycable-go --rpc_request_timeout=5000 # or ANYCABLE_RPC_REQUEST_TIMEOUT=5000 anycable-go ``` [proto]: ../misc/rpc_proto.md [anycable-ruby]: https://github.com/anycable/anycable [anycable-server-js]: https://github.com/anycable/anycable-serverless-js --- --- url: /misc/rpc_proto.md --- # AnyCable RPC Protobuf > Source code is available in [the repo](https://github.com/anycable/anycable/blob/master/protos/rpc.proto). This is a `.proto` file that should be used to generate AnyCable clients/servers: ```protobuf syntax = "proto3"; package anycable; service RPC { // Connect is called when a client connection is established to authenticate it rpc Connect (ConnectionRequest) returns (ConnectionResponse) {} // Command is called when authenticated client sends a message (subscribe, unsubscribe, perform) rpc Command (CommandMessage) returns (CommandResponse) {} // Disconnect is called when a client connection is closed rpc Disconnect (DisconnectRequest) returns (DisconnectResponse) {} } // Every response contains a status field with one of the following values enum Status { // RPC called failed unexpectedly ERROR = 0; // RPC called succeed, action was performed SUCCESS = 1; // RPC called succeed but actions was rejected by the application (e.g., rejected subscription/connection) FAILURE = 2; } // Env represents a client connection information passed to RPC server message Env { // Underlying HTTP request URL string url = 1; // Underlying HTTP request headers map headers = 2; // Connection-level metadata map cstate = 3; // Channel-level metadata (only set for Command calls, contains data for the affected subscription) map istate = 4; } // EnvResponse contains the changes made to the connection or channel state, // that must be applied to the state message EnvResponse { map cstate = 1; map istate = 2; } // ConnectionRequest describes a payload for the Connect call message ConnectionRequest { Env env = 3; } // ConnectionResponse describes a response of the Connect call message ConnectionResponse { Status status = 1; // Connection identifiers passed as string (in most cases, JSON) string identifiers = 2; // Messages to be sent to the client repeated string transmissions = 3; // Error message in case status is ERROR or FAILURE string error_msg = 4; EnvResponse env = 5; } // ConnectionMesssage describes a payload for the Command call message CommandMessage { // Name of the command ("subscribe", "unsubscribe", "message" for Action Cable) string command = 1; // Subscription identifier (channel id, channel params) string identifier = 2; // Client's connection identifiers (received in ConnectionResponse) string connection_identifiers = 3; // Command payload string data = 4; Env env = 5; } // CommandResponse describes a response of the Command call message CommandResponse { Status status = 1; // If true, the client must be disconencted bool disconnect = 2; // If true, the client must be unsubscribed from all streams from this subscription bool stop_streams = 3; // List of the streams to subscribe the client to repeated string streams = 4; // Messages to be sent to the client repeated string transmissions = 5; string error_msg = 6; EnvResponse env = 7; // List of the stream to unsubscribe the client from repeated string stopped_streams = 8; } // DisconnectRequest describes a payload for the Disconnect call message DisconnectRequest { string identifiers = 1; // List of a client's subscriptions (identifiers). // Required to call `unsubscribe` callbacks. repeated string subscriptions = 2; Env env = 5; } // DisconnectResponse describes a response of the Disconnect call message DisconnectResponse { Status status = 1; string error_msg = 2; } ``` --- --- url: /anycable-go/configuration.md --- # AnyCable server configuration You can configure AnyCable server via CLI options, e.g.: ```sh $ anycable-go --rpc_host=localhost:50051 --headers=cookie \ --redis_url=redis://localhost:6379/5 --redis_channel=__anycable__ \ --host=localhost --port=8080 ``` Or via the corresponding environment variables (i.e. `ANYCABLE_RPC_HOST`, `ANYCABLE_REDIS_URL`, etc.). Finally, you can also store configuration in a `.toml` file (see [configuration files](#configuration-files)). ## Primary settings Here is the list of the most commonly used configuration parameters. **NOTE:** To see all available options run `anycable-go -h`. *** **--host**, **--port** (`ANYCABLE_HOST`, `ANYCABLE_PORT` or `PORT`) Server host and port (default: `"localhost:8080"`). **--path** (`ANYCABLE_PATH`) WebSocket endpoint path (default: `"/cable"`). You can specify multiple paths separated by commas. You can also use wildcards (at the end of the paths) or path placeholders: ```sh anycable-go --path="/cable,/admin/cable/*,/accounts/{tenant}/cable" ``` **--allowed\_origins** (`ANYCABLE_ALLOWED_ORIGINS`) Comma-separated list of hostnames to check the Origin header against during the WebSocket Upgrade. Supports wildcards, e.g., `--allowed_origins=*.evilmartians.io,www.evilmartians.com`. **--broadcast\_adapter** (`ANYCABLE_BROADCAST_ADAPTER`, default: `redis`) [Broadcasting adapter](./broadcasting.md) to use. Available options: `redis` (default), `redisx`, `nats`, and `http`. When HTTP adapter is used, AnyCable-Go accepts broadcasting requests on `:8090/_broadcast`. You can also enable multiple adapters at once by specifying them separated by commas. **--broker** (`ANYCABLE_BROKER`, default: `none`) [Broker](./broker.md) adapter to use. **--pubsub** (`ANYCABLE_PUBSUB`, default: `none`) Pub/Sub adapter to use to distribute broadcasted messages within the cluster (when non-distributed broadcasting adapter is used). **Required for broker**. **--streams\_secret** (`ANYCABLE_STREAMS_SECRET`) A secret key used to verify [signed\_streams](./signed_streams.md). If not set, the `--secret` setting is used (see below). ## RPC settings **--rpc\_host** (`ANYCABLE_RPC_HOST`) RPC service address (default: `"localhost:50051"`). You can also specify the scheme part to indicate which RPC protocol to use, gRPC or HTTP (gRPC is assumed by default). See below for more details on [HTTP RPC](./rpc.md#rpc-over-http). **--norpc** (`ANYCABLE_NORPC=true`) This setting disables the RPC component completely. That means, you can only use AnyCable in a standalone mode (with [JWT authentication](./jwt_identification.md) and [signed streams](./signed_streams.md)). **--headers** (`ANYCABLE_HEADERS`) Comma-separated list of headers to proxy to RPC (default: `"cookie"`). **--proxy-cookies** (`ANYCABLE_PROXY_COOKIES`) Comma-separated list of cookies to proxy to RPC (default: all cookies). ## Security/access settings **--secret** (`ANYCABLE_SECRET`) A common secret key used by the following components (unless a specific key is specified): [JWT authentication](./jwt_identification.md), [signed streams](./signed_streams.md). **--broadcast\_key** (`ANYCABLE_BROADCAST_KEY`) A secret key used to authenticate broadcast requests. See [broadcasting docs](./broadcasting.md). You can use the special "none" value to disable broadcasting authentication. **--noauth** (`ANYCABLE_NOAUTH=true`) This setting disables client authentication checks (so, anyone is allowed to connect). Use it with caution. **NOTE**: if you use *enforced* JWT authentication, the `--noauth` option has no effect. **--public\_streams** (`ANYCABLE_PUBLIC_STREAMS=true`) Setting this value allows direct subscribing to streams using unsigned names (see more in the [signed streams docs](./signed_streams.md)). **--public** (`ANYCABLE_PUBLIC=true`) This is a shortcut to specify both `--noauth`, `--public_streams` and `--broadcast_key=none`, so you can use AnyCable without any protection. **Do not do this in production**. ## HTTP API **--http\_broadcast\_port** (`ANYCABLE_HTTP_BROADCAST_PORT`, default: `8090`) You can specify on which port to receive broadcasting requests (NOTE: it could be the same port as the main HTTP server listens to). ## Redis configuration **--redis\_url** (`ANYCABLE_REDIS_URL` or `REDIS_URL`) Redis URL to connect to (default: `"redis://localhost:6379/5"`). Used by the corresponding pub/sub, broadcasting, and broker adapters. **--redis\_channel** (`ANYCABLE_REDIS_CHANNEL`) Redis channel for broadcasting (default: `"__anycable__"`). When using the `redisx` adapter, it's used as a name of the Redis stream. **--redis\_disable\_cache** (`ANYCABLE_REDIS_DISABLE_CACHE`) Disable [`CLIENT TRACKING`](https://redis.io/commands/client-tracking/) (it could be blocked by some managed Redis providers). **--redis\_tls\_ca\_cert\_path** (`ANYCABLE_REDIS_TLS_CA_CERT_PATH`) Path to the CA certificate file to verify the Redis server. This is useful when your Redis server uses a certificate signed by a private CA. **--redis\_tls\_client\_cert\_path** (`ANYCABLE_REDIS_TLS_CLIENT_CERT_PATH`) Path to the client TLS certificate file for mutual TLS authentication with Redis. **--redis\_tls\_client\_key\_path** (`ANYCABLE_REDIS_TLS_CLIENT_KEY_PATH`) Path to the client TLS private key file for mutual TLS authentication with Redis. ## NATS configuration **--nats\_servers** (`ANYCABLE_NATS_SERVERS`) The list of [NATS][] servers to connect to (default: `"nats://localhost:4222"`). Used by the corresponding pub/sub, broadcasting, and broker adapters. **--nats\_channel** (`ANYCABLE_NATS_CHANNEL`) NATS channel for broadcasting (default: `"__anycable__"`). ## Logging settings **--log\_level** (`ANYCABLE_LOG_LEVEL`) Logging level (default: `"info"`). **--debug** (`ANYCABLE_DEBUG`) Enable debug mode (more verbose logging). ## Presets AnyCable-Go comes with a few built-in configuration presets for particular deployments environments, such as Heroku or Fly. The presets are detected and activated automatically. As an indication, you can find a line in the logs: ```sh INFO ... context=config Loaded presets: fly ``` To disable automatic presets activation, provide `ANYCABLE_PRESETS=none` environment variable (or pass the corresponding option to the CLI: `anycable-go --presets=none`). **NOTE:** Presets do not override explicitly provided configuration values. ### Preset: fly Automatically activated if all of the following environment variables are defined: `FLY_APP_NAME`, `FLY_REGION`, `FLY_ALLOC_ID`. The preset provide the following defaults: * `host`: "0.0.0.0" * `http_broadcast_port`: `$PORT` (set to the same value as the main HTTP port). * `broadcast_adapter`: "http" (unless Redis is configured) * `enats_server_addr`: "nats://0.0.0.0:4222" * `enats_cluster_addr`: "nats://0.0.0.0:5222" * `enats_cluster_name`: "\-\-cluster" * `enats_cluster_routes`: "nats://\.\.internal:5222" * `enats_gateway_advertise`: "\.\.internal:7222" (**NOTE:** You must set `ANYCABLE_ENATS_GATEWAY` to `nats://0.0.0.0:7222` and configure at least one gateway address manually to enable gateways). Also, [embedded NATS](./embedded_nats.md) is enabled automatically if no other pub/sub adapter neither Redis is configured. Similarly, pub/sub, broker and broadcast adapters using embedded NATS are configured automatically, too. Thus, by default, AnyCable-Go setups a NATS cluster automatically (within a single region), no configuration is required. If the `ANYCABLE_FLY_RPC_APP_NAME` env variable is provided, the following defaults are configured as well: * `rpc_host`: "dns:///\.\.internal:50051" ### Preset: heroku Automatically activated if all of the following environment variables are defined: `HEROKU_DYNO_ID`, `HEROKU_APP_ID`. **NOTE:** These env vars are defined only if the [Dyno Metadata feature](https://devcenter.heroku.com/articles/dyno-metadata) is enabled. The preset provides the following defaults: * `host`: "0.0.0.0". * `http_broadcast_port`: `$PORT` (to make HTTP endpoint accessible from other applications). ## Per-client settings A client MAY override default values for the settings listed below by providing the corresponding parameters in the WebSocket URL query string: * `?pi=`: ping interval (overrides `--ping_interval`). * `?ptp=`: ping timestamp precision (overrides `--ping_timestamp_precision`). For example, using the following URL, you can set the ping interval to 10 seconds and the timestamp precision to milliseconds: ```txt ws://localhost:8080/cable?pi=10&ptp=ms ``` ## TLS To secure your `anycable-go` server provide the paths to SSL certificate and private key: ```sh anycable-go --port=443 -ssl_cert=path/to/ssl.cert -ssl_key=path/to/ssl.key => INFO time context=http Starting HTTPS server at 0.0.0.0:443 ``` If your RPC server requires TLS you can enable it via `--rpc_enable_tls` (`ANYCABLE_RPC_ENABLE_TLS`). If RPC server uses certificate issued by private CA, then you can pass either its file path or PEM contents with `--rpc_tls_root_ca` (`ANYCABLE_RPC_TLS_ROOT_CA`). If RPC uses self-signed certificate, you can disable RPC server certificate verification by setting `--rpc_tls_verify` (`ANYCABLE_RPC_TLS_VERIFY`) to `false`, but this is insecure, use only in test/development. ## Disconnect settings AnyCable-Go notifies an RPC server about disconnected clients asynchronously with a rate limit. We do that to allow other RPC calls to have higher priority (because *live* clients are usually more important) and to avoid load spikes during mass disconnects (i.e., when a server restarts). That could lead to the situation when the *disconnect queue* is overwhelmed, and we cannot perform all the `Disconnect` calls during server shutdown. Thus, **RPC server may not receive all the disconnection events** (i.e., `disconnect` and `unsubscribed` callbacks in your code). If you rely on `disconnect` callbacks in your code, you can tune the default disconnect queue settings to provide better guarantees\*: **--disconnect\_rate** (`ANYCABLE_DISCONNECT_RATE`) The max number of `Disconnect` calls per-second (default: 100). **--disconnect\_mode** (`ANYCABLE_DISCONNECT_MODE`) This parameter defines when a Disconnect call should be made for a session. The default is "auto", which means that the Disconnect call is made only if we detected the client *interest* in it. Currently, we only skip Disconnect calls for sessions authenticated via [JWT](./jwt_identification.md) and using [signed streams](./signed_streams.md) (Hotwire or CableReady). Other available modes are "always" and "never". Thus, to disable Disconnect call completely, use `--disconnect_mode=never`. Using `--disconnect_mode=always` is useful when you have some logic in the `ApplicationCable::Connetion#disconnect` method and you want to invoke it even for JWT and signed streams sessions. **NOTE:** AnyCable tries to make a Disconnect call for active sessions during the server shutdown. However, if the server is killed with `kill -9` or crashes, the disconnect queue is not flushed, and some disconnect events may be lost. If you experience higher queue sizes during deployments, consider increasing the shutdown timeout by tuning the `--shutdown_timeout` parameter. ### Slow drain mode AnyCable-Go PRO provides the **slow drain** mode for disconnecting clients during shutdown. When it is enabled, AnyCable do not try to disconnect all active clients as soon as a shutdown signal is received. Instead, spread the disconnects over the graceful shutdown period. This way, you can reduce the load on AnyCable servers during deployments (i.e., avoid the *thundering herd* situation). You can enable this feature by providing the `--shutdown_slowdrain` option or setting the `ANYCABLE_SHUTDOWN_SLOWDRAIN` environment variable to `true`. You should see the following log message on shutdown indicating that the slow drain mode is enabled: ```sh INFO 2023-08-04T07:16:14.339Z context=node Draining 1234 active connections slowly for 24.7s ``` The actual *drain period* is slightly less than the shutdown timeout—we need to reserve some time to complete RPC calls. Also, there is a maximum interval between disconnects (500ms), so we don't wait too long when the number of clients is not that big. ## GOMAXPROCS We use [automaxprocs][] to automatically set the number of OS threads to match Linux container CPU quota in a virtualized environment, not a number of *visible* CPUs (which is usually much higher). This feature is enabled by default. You can opt-out by setting `GOMAXPROCS=0` (in this case, the default Go mechanism of defining the number of threads is used). You can find the actual value for GOMAXPROCS in the starting logs: ```sh INFO 2022-06-30T03:31:21.848Z context=main Starting AnyCable 1.2.0-c4f1c6e (with mruby 1.2.0 (2015-11-17)) (pid: 39705, open file limit: 524288, gomaxprocs: 8) ``` ## Configuration files Since v1.5.4, you can also provide configuration via a TOML file. This is recommended for applications that require a lot of settings or have complex configurations. AnyCable will look for a configuration file in the following locations: * `./anycable.toml` * `/ets/anycable/anycable.toml` You can also specify the path to the configuration file using the `--config-path` option, e.g.: ```sh $ anycable-go --config-path=/path/to/anycable.toml 2024-10-07 17:52:37.139 INF Starting AnyCable 1.6.0-87217bb (pid: 80235, open file limit: 122880, gomaxprocs: 8) nodeid=BzeSHV 2024-10-07 17:52:37.139 INF Using configuration from file: ./path/to/anycable.toml nodeid=BzeSHV ``` You can generate a sample configuration file (including the currently provided configuration) using the `--print-config` option: ```sh anycable-go --print-config > anycable.toml ``` Finally, if there is a configuration file but you don't want to use it, you can disable it using the `--ignore-config-path` option. [automaxprocs]: https://github.com/uber-go/automaxprocs [NATS]: https://nats.io --- --- url: /pro.md --- # AnyCable-Go AnyCable-Go Pro aims to bring AnyCable to the next level of efficient resources usage and developer ~~experience~~ happiness. > Read also AnyCable Goes Pro: Fast WebSockets for Ruby, at scale. ## Memory usage Pro version uses a different memory model under the hood, which gives you yet another **30-50% RAM usage reduction**. Here is the results of running [websocket-bench][] `broadcast` and `connect` benchmarks and measuring RAM used: | version | broadcast 5k | connect 10k | connect 15k | |---|----|---|---| | 1.3.0-pro | 142MB | 280MB | 351MB | | 1.3.0-pro (w/o poll)\* | 207MB | 343MB | 480MB | | 1.3.0 | 217MB | 430MB | 613MB | \* AnyCable-Go Pro uses epoll/kqueue to react on incoming messages by default. In most cases, that should work the same way as with non-Pro version; however, if you have a really high rate of incoming messages, you might want to fallback to the *actor-per-connection* model (you can do this by specifying `--netpoll_enabled=false`). **NOTE:** Currently, using net polling is not compatible with WebSocket per-message compression (it's disabled even if you enabled it explicitly). ## More features * [Adaptive RPC concurrency](anycable-go/rpc.md#adaptive-concurrency) * [Multi-node streams history](anycable-go/reliable_streams.md#redis) * [Slow drain mode for disconnecting clients on shutdown](anycable-go/configuration.md#slow-drain-mode) * [Binary messaging formats](anycable-go/binary_formats.md) * [Apollo GraphQL protocol support](anycable-go/apollo.md) * [Long polling support](anycable-go/long_polling.md) * [OCCP support](anycable-go/occp.md) ## Installation Read our [installation guide](pro/install.md). [websocket-bench]: https://github.com/anycable/websocket-bench --- --- url: /anycable-go/health_checking.md --- # AnyCable-Go Health Checking Health check endpoint is enabled by default and accessible at `/health` path. You can configure the path via the `--health-path` option (or `ANYCABLE_HEALTH_PATH` env var). You can use this endpoint as readiness/liveness check (e.g. for load balancers). --- --- url: /anycable-go/tracing.md --- # AnyCable-Go Tracing AnyCable-Go assigns a random unique `sid` (*session ID*) or use the one provided in the `X-Request-ID` HTTP header to each websocket connection and passes it with requests to RPC service. This identifier is also available in logs and you can use it to trace a request's pathway through the whole Load Balancer -> WS Server -> RPC stack. Logs example: ```sh D 2019-04-25T18:41:07.172Z context=node sid=FQQS_IltswlTJK60ncf9Cm Incoming message: &{subscribe {"channel":"PresenceChannel"} } D 2019-04-25T18:41:08.074Z context=pubsub Incoming pubsub message from Redis: {"stream":"presence:Z2lkOi8vbWFuYWdlYmFjL1NjaG9vbC8xMDAwMjI3Mw","data":"{\"type\":\"presence\",\"event\":\"user-presence-changed\",\"user_id\":1,\"status\":\"online\"}"} ``` ## Using with Heroku Heroku assigns `X-Request-ID` [automatically at the router level](https://devcenter.heroku.com/articles/http-request-id). ## Using with NGINX If you use AnyCable-Go behind NGINX server, you can assign request id with the provided configuration example: ```nginx # Сonfiguration is shortened for the sake of brevity log_format trace '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" "$http_user_agent" ' '"$http_x_forwarded_for" $request_id'; # `trace` logger server { add_header X-Request-ID $request_id; # Return `X-Request-ID` to client location /cable { proxy_set_header X-Request-ID $request_id; # Pass X-Request-ID` to AnyCable-GO server access_log /var/log/nginx/access_trace.log trace; # Use `trace` log } } ``` --- --- url: /anycable-go/apollo.md --- # Apollo GraphQL support AnyCable can act as a *translator* between Apollo GraphQL and Action Cable protocols (used by [GraphQL Ruby][graphql-ruby]). That allows us to use the variety of tools compatible with Apollo: client-side libraries, IDEs (such as Apollo Studio). > See also the [demo](https://github.com/anycable/anycable_rails_demo/pull/18) of using AnyCable with Apollo GraphQL React Native application. ## Usage Run `anycable-go` with `graphql_path` option specified as follows: ```sh $ anycable-go -graphql_path=/graphql ... INFO 2021-05-11T11:53:01.186Z context=main Handle GraphQL WebSocket connections at http://localhost:8080/graphql ... # or using env var $ ANYCABLE_GRAPHQL_PATH=/graphql anycable-go ``` Now your Apollo-compatible\* GraphQL clients can connect to the `/graphql` endpoint to consume your GraphQL API. \* Currently, there are two protocol implementations supported by AnyCable: [graphql-ws][] and (legacy) [subscriptions-transport-ws][]. GraphQL Ruby code stays unchanged (make sure you use [graphql-anycable][] plugin). Other configuration options: **--graphql\_channel** (`ANYCABLE_GRAPHQL_CHANNEL`) GraphQL Ruby channel class name (default: `"GraphqlChannel"`). **--graphql\_action** (`ANYCABLE_GRAPHQL_ACTION`) GraphQL Ruby channel action name (default: `"execute"`). ## Client configuration We test our implementation against the official Apollo WebSocket link configuration described here: [Get real-time updates from your GraphQL server][apollo-subscriptions]. ## Authentication Apollo GraphQL supports passing additional connection params during the connection establishment. For example: ```js import { GraphQLWsLink } from '@apollo/client/link/subscriptions'; import { createClient } from 'graphql-ws'; const wsLink = new GraphQLWsLink(createClient({ url: 'ws://localhost:8080/graphql', connectionParams: { token: 'some-token', }, })); ``` AnyCable passes these params via the `x-apollo-connection` HTTP header, which you can access in your `ApplicationCable::Connection#connect` method: ```ruby module ApplicationCable class Connection < ActionCable::Connection::Base identified_by :user def connect user = find_user reject_unauthorized_connection unless user self.user = user end private def find_user header = request.headers["x-apollo-connection"] return unless header # Header contains JSON-encoded params payload = JSON.parse(header) User.find_by_token(payload["token"]) end end end ``` Note that the header contains JSON-encoded connection params object. ### Using with JWT identification You can use [JWT identification](./jwt_identification.md) along with Apollo integration by specifying the token either via query params (e.g., `ws://localhost:8080/graphql?jid=`) or by passing it along with connection params like this: ```js const wsLink = new GraphQLWsLink(createClient({ url: 'ws://localhost:8080/graphql', connectionParams: { jid: '', }, })); ``` [subscriptions-transport-ws]: https://github.com/apollographql/subscriptions-transport-ws [apollo-subscriptions]: https://www.apollographql.com/docs/react/data/subscriptions/ [graphql-ruby]: https://graphql-ruby.org [graphql-anycable]: https://github.com/anycable/graphql-anycable [graphql-ws]: https://github.com/enisdenjo/graphql-ws --- --- url: /architecture.md --- # Architecture ## Overview AnyCable **real-time server** (WS, or WebSocket, since it's a primary transport) is responsible for handling clients, or connections. That includes: * low-level connections management * subscriptions management * broadcasting messages to clients AnyCable can be used in a standalone mode as a typical pub/sub server. However, it was primarily designed to act as a *business-logic proxy* allowing you to avoid duplicating real-time logic between multiple apps. For that, we use an [RPC protocol](/anycable-go/rpc) to delegate subscriptions, authentication and authorization logic to your backend. The application publish broadcast messages to the WebSocket server (directly via HTTP or via some **queuing service**, see [broadcast adapters](/ruby/broadcast_adapters.md)). In case of running a WebSocket cluster (multiple nodes), there is also can be a **Pub/Sub service** responsible for re-transmitting broadcast messages between nodes. You can use [embedded NATS](/anycable-go/embedded_nats.md) as a pub/sub service to miminalize the number of infrastructure dependencies. See [Pub/Sub documentation](/anycable-go/pubsub.md) for other options. ## State management AnyCable's is different to the most WebSocket servers in the way connection states are stored: all the information about client connections is kept in WebSocket server; an RPC server operates on temporary, short-lived, objects passed with every gRPC request. That means, for example, that you cannot rely on instance variables in your channel and connection classes. Instead, you should use specified *state* objects, provided by AnyCable (read more about [channel states](rails/channels_state.md)). A client's state consists of three parts: * **connection identifiers**: populated once during the connection, read-only (correspond to `identified_by` in Action Cable); * **connection state**: key-value store for arbitrary connection metadata (e.g., tagged logger tags stored this way); * **channel states**: key-value store for channels (for each subscription). This is how AnyCable manages these states under the hood: * A client connects to the WebSocket server, `Connect` RPC is made, which returns *connection identifiers* and the initial *connection state*. All subsequent RPC calls contain this information (as long as underlying HTTP request data). * Every time a client performs an action for a specific channel, the *channel state* for the corresponding subscription is provided in the RPC payload. * If during RPC invocation connection or channel state has been changed, the **changes** are returned to the WebSocket server to get merge with the full state. * When a client disconnects, the full channel state (i.e., for all subscriptions) is included into the corresponding RPC payload. Thus, the amount of state data passed in each RPC request is minimized. ### Restoring state objects A state is stored in a serialized form in WebSocket server and deserialized (lazily) during each RPC request (in Rails, we rely on [GlobalID](https://github.com/rails/globalid) for that). This results in a slightly different behaviour comparing to persistent, long-lived, state. For example, if you use an Active Record object as an identifier (e.g., `user`), it's *reloaded* in every RPC action it's used. To use arbitrary Ruby objects as identifiers, you must add GlobalID support for them (see [AnyCable setup demo](https://github.com/anycable/anycable_rails_demo/pull/2)). --- --- url: /rails/authentication.md --- # Authentication ## JWT authentication [AnyCable JWT](../anycable-go/jwt_identification.md) is the best (both secure and fast) way to authenticate your real-time connections. With AnyCable Rails, all you need is to configure the AnyCable application secret (`anycable.secret` in credentials or `ANYCABLE_SECRET` env var) and replace the `#action_cable_meta_tag` with `#action_cable_with_jwt_meta_tag`: ```erb <%= action_cable_with_jwt_meta_tag(user: current_user, tenant: Current.tenant) %> # => ``` You MUST pass current user's **connection identifiers** as keyword arguments to provide identity information. You can also use a separate `#anycable_token_meta_tag` helper to inject the token into the page: ```erb <%= anycable_token_meta_tag(user: current_user, tenant: Current.tenant) %> # => ``` [AnyCable JS client](https://github.com/anycable/anycable-client) will automatically pick up the token from the `cable-token` meta tag. *Connection identifiers* are the connection class parameters you define via the `.identified_by` method and set in the `#connect` method. For example: ```ruby module ApplicationCable class Connection < ActionCable::Connection::Base identified_by :user, :tenant def connect self.current_user = find_verified_user self.tenant = find_current_tenant end end end ``` **IMPORTANT:** When using AnyCable JWT, the `Connection#connect` method is never called (for clients using JWT tokens). We associate the connection identifiers with the client at the AnyCable server side and make them accessible in subsequent commands. However, if you have some additional logic in your `Connection#connect` method (e.g., tracking users activity), it won't be preserved. By default, tokens are valid for **1 hour**. You can change this value by specifying the `jwt_ttl` configuration parameter. ### Manually generating tokens If you're not using HTML, you can generate AnyCable JWT by using the following API: ```ruby token = AnyCable::JWT.encode({user: current_user}) # you can also override the global TTL setting via expires_at option token = AnyCable::JWT.encode({user: current_user}, expires_at: 10.minutes.from_now) ``` ### Using AnyCable JWT with Action Cable You can use AnyCable JWT authentication with Rails Action Cable (especially useful when you're gradually migrating to AnyCable). For that, update your `ApplicationCable::Connection` class as follows: ```diff module ApplicationCable class Connection < ActionCable::Connection::Base + prepend AnyCable::Rails::Ext::JWT + identified_by :user, :tenant def connect + return identify_from_anycable_jwt! if anycable_jwt_present? + self.current_user = find_verified_user self.tenant = find_current_tenant end end end ``` ### Tokens expiration AnyCable server checks a token's TTL, and in case the token is expired, the server disconnects the client with a specific reason: `token_expired`. You can learn more about how to refresh the token in [this post](https://anycable.io/blog/jwt-identification-and-hot-streams/). ## Cookies & session Cookies and Rails sessions are supported by AnyCable Rails. If you run AnyCable server on a different domain from your Rails application, make sure your cookie store is configured to share cookies between domains. For example, to share cookies with subdomains: ```ruby config.session_store :cookie_store, key: "__sid", domain: :all ``` ## Rack middlewares If your authentication method relies on non-standard Rack request properties (e.g., `request.env["something"]`) for authentication, you MUST configure AnyCable Rack middleware stack to include required Rack middlewares. ### Devise/Warden Devise relies on [`warden`](https://github.com/wardencommunity/warden) Rack middleware to authenticate users. By default, this middleware is automatically added to the AnyCable middleware stack when Devise is present. You can edit `config/anycable.yml` to disable this behavior by changing the `use_warden_manager` parameter. ```yml # config/anycable.yml development: use_warden_manager: false ``` And then, you can manually put this code, for example, into an initializer (`config/initializers/anycable.rb`) or any other configuration file. ```ruby AnyCable::Rails::Rack.middleware.use Warden::Manager do |config| Devise.warden_config = config end ``` Then, you can access the current user via `env["warden"].user(scope)` in your connection class (where `scope` is [Warden scope](https://github.com/wardencommunity/warden/wiki/Scopes), usually, `:user`). --- --- url: /benchmarks.md --- # Benchmarks > The latest benchmark results are available at [the main repo](https://github.com/anycable/anycable/blob/master/benchmarks/2020-06-30.md). **NOTE:** We run Action Cable with **eight** Puma workers. Using lower number of processes results in a much higher latency during broadcasting as well as connection timeout errors. ## Broadcasting RTT Broadcasting round-trip time benchmark (based on [Hashrocket's bench](https://github.com/hashrocket/websocket-shootout)) measures how much time does it take for the server to re-transmit the message to all the connected clients–the less the time, the better the *real-time-ness* of the server. The results of this benchmark could be seen below. ## Memory usage Memory usage of AnyCable is significantly lower than of Action Cable. That's achieved by moving memory-intensive operations into (storing connection states and subscriptions maps, serializing data into a standalone WebSocket server. ## CPU usage Below you can see the snapshot of CPU usage during the RTT benchmark. --- --- url: /anycable-go/binary_formats.md --- # Binary messaging formats AnyCable Pro allows you to use Msgpack or Protobufs instead of JSON to serialize incoming and outgoing data. Using binary formats bring the following benefits: faster (de)serialization and less data passing through network (see comparisons below). ## Msgpack ### Usage In order to initiate Msgpack-encoded connection, a client MUST use `"actioncable-v1-msgpack"` or `"actioncable-v1-ext-msgpack"` subprotocol during the connection. A client MUST encode outgoing and incoming messages using Msgpack. ### Using Msgpack with AnyCable JS client [AnyCable JavaScript client][anycable-client] supports Msgpack out-of-the-box: ```js // cable.js import { createCable } from '@anycable/web' import { MsgpackEncoder } from '@anycable/msgpack-encoder' export default createCable({protocol: 'actioncable-v1-msgpack', encoder: new MsgpackEncoder()}) // or for the extended Action Cable protocol // export default createCable({protocol: 'actioncable-v1-ext-msgpack', encoder: new MsgpackEncoder()}) ``` ### Action Cable JavaScript client patch Here is how you can patch the built-in Action Cable JavaScript client library to support Msgpack: ```js import { createConsumer, logger, adapters, INTERNAL } from "@rails/actioncable"; // Make sure you added msgpack library to your frontend bundle: // // yarn add @ygoe/msgpack // import msgpack from "@ygoe/msgpack"; let consumer; // This is an application specific function to create an Action Cable consumer. // Use it everywhere you need to connect to Action Cable. export const createCable = () => { if (!consumer) { consumer = createConsumer(); // Extend the connection object (see extensions code below) Object.assign(consumer.connection, connectionExtension); Object.assign(consumer.connection.events, connectionEventsExtension); } return consumer; } // Msgpack support // Patches this file: https://github.com/rails/rails/blob/main/actioncable/app/javascript/action_cable/connection.js // Replace JSON protocol with msgpack const supportedProtocols = [ "actioncable-v1-msgpack" ] const protocols = supportedProtocols const { message_types } = INTERNAL const connectionExtension = { // We have to override the `open` function, since we MUST provide custom WS sub-protocol open() { if (this.isActive()) { logger.log(`Attempted to open WebSocket, but existing socket is ${this.getState()}`) return false } else { logger.log(`Opening WebSocket, current state is ${this.getState()}, subprotocols: ${protocols}`) if (this.webSocket) { this.uninstallEventHandlers() } this.webSocket = new adapters.WebSocket(this.consumer.url, protocols) this.webSocket.binaryType = "arraybuffer" this.installEventHandlers() this.monitor.start() return true } }, isProtocolSupported() { return supportedProtocols[0] == this.getProtocol() }, send(data) { if (this.isOpen()) { const encoded = msgpack.encode(data); this.webSocket.send(encoded) return true } else { return false } } } // Incoming messages are handled by the connection.events.message function. // There is no way to patch it, so, we have to copy-paste :( const connectionEventsExtension = { message(event) { if (!this.isProtocolSupported()) { return } const {identifier, message, reason, reconnect, type} = msgpack.decode(new Uint8Array(event.data)) switch (type) { case message_types.welcome: this.monitor.recordConnect() return this.subscriptions.reload() case message_types.disconnect: logger.log(`Disconnecting. Reason: ${reason}`) return this.close({allowReconnect: reconnect}) case message_types.ping: return this.monitor.recordPing() case message_types.confirmation: return this.subscriptions.notify(identifier, "connected") case message_types.rejection: return this.subscriptions.reject(identifier) default: return this.subscriptions.notify(identifier, "received", message) } }, }; ``` > See the [demo](https://github.com/anycable/anycable_rails_demo/pull/17) of using Msgpack in a Rails project with AnyCable Rack server. ## Protobuf We squeeze a bit more space by using Protocol Buffers. AnyCable uses the following schema: ```proto syntax = "proto3"; package action_cable; enum Type { no_type = 0; welcome = 1; disconnect = 2; ping = 3; confirm_subscription = 4; reject_subscription = 5; confirm_history = 6; reject_history = 7; } enum Command { unknown_command = 0; subscribe = 1; unsubscribe = 2; message = 3; history = 4; pong = 5; } message StreamHistoryRequest { string epoch = 2; int64 offset = 3; } message HistoryRequest { int64 since = 1; map streams = 2; } message Message { Type type = 1; Command command = 2; string identifier = 3; // Data is a JSON encoded string. // This is by Action Cable protocol design. string data = 4; // Message has no structure. // We use Msgpack to encode/decode it. bytes message = 5; string reason = 6; bool reconnect = 7; HistoryRequest history = 8; } message Reply { Type type = 1; string identifier = 2; bytes message = 3; string reason = 4; bool reconnect = 5; string stream_id = 6; string epoch = 7; int64 offset = 8; string sid = 9; bool restored = 10; repeated string restored_ids = 11; } ``` When using the standard Action Cable protocol (v1), both incoming and outgoing messages are encoded as `action_cable.Message` type. When using the extended version, incoming messages are encoded as `action_cable.Reply` type. Note that `Message.message` field and `Reply.message` have the `bytes` type. This field carries the information sent from a server to clients, which could be of any form. We Msgpack to encode/decode this data. Thus, AnyCable Protobuf protocol is actually a mix of Protobufs and Msgpack. ### Using Protobuf with AnyCable JS client [AnyCable JavaScript client][anycable-client] supports Protobuf encoding out-of-the-box: ```js // cable.js import { createCable } from '@anycable/web' import { ProtobufEncoder } from '@anycable/protobuf-encoder' export default createCable({protocol: 'actioncable-v1-protobuf', encoder: new ProtobufEncoder()}) ``` > See the [demo](https://github.com/anycable/anycable_rails_demo/pull/24) of using Protobuf encoder in a Rails project with AnyCable JS client. To use Protobuf with the extended Action Cable protocol, use the following configuration: ```js // cable.js import { createCable } from '@anycable/web' import { ProtobufEncoderV2 } from '@anycable/protobuf-encoder' export default createCable({protocol: 'actioncable-v1-ext-protobuf', encoder: new ProtobufEncoderV2()}) ``` ## Formats comparison Here is the in/out traffic comparison: | Encoder | Sent | Rcvd | |----------|------|-------| | protobuf | 315.32MB | 327.1KB | | msgpack | 339.58MB | 473.6KB | | json | 502.45MB | 571.8KB | The data above were captured while running a [websocket-bench][] benchmark with the following parameters: ```sh websocket-bench broadcast ws://0.0.0.0:8080/cable —server-type=actioncable —origin http://0.0.0.0 —sample-size 100 —step-size 1000 —total-steps 5 —steps-delay 2 —wait-broadcasts=5 —payload-padding=100 ``` **NOTE:** The numbers above depend on the messages structure. Binary formats are more efficient for *objects* (JSON-like) and less efficient when you broadcast long strings (e.g., HTML fragments). Here is the encode/decode speed comparison: | Encoder | Decode (ns/op) | Encode (ns/op) | |--------|------|-------| | protobuf (base) | 425 | 1153 | | msgpack (base) | 676 | 1512 | | json (base) | 1386 | 1266 | |||| | protobuf (long) | 479 | 2370 | | msgpack (long) | 763 | 2506 | | json (long) | 2457 | 2319 | Where base payload is: ```json { "command": "message", "identifier": "{\"channel\":\"test_channel\",\"channelId\":\"23\"}", "data": "hello world" } ``` And the long one is: ```json { "command": "message", // x10 means repeat string 10 times "identifier": "{\"channel\":\"test_channel..(x10)\",\"channelId\":\"123..(x10)\"}", // message is the base message from above "message": { "command": "message", "identifier": "{\"channel\":\"test_channel\",\"channelId\":\"23\"}", "data": "hello world" } } ``` [websocket-bench]: https://github.com/anycable/websocket-bench [anycable-client]: https://github.com/anycable/anycable-client --- --- url: /anycable-go/broadcasting.md --- # Broadcasting Publishing messages from your application to connected clients (aka *broadcasting*) is an essential component of any real-time application. AnyCable comes with multiple options on how to broadcast messages. We call them *broadcasters*. Currently, we support HTTP, Redis, and NATS-based broadcasters. **NOTE:** The default broadcaster is Redis Pub/Sub for backward-compatibility reasons. This is going to change in v2. ## HTTP > Enable via `--broadcast_adapter=http` (or `ANYCABLE_BROADCAST_ADAPTER=http`). HTTP broadcaster has zero-dependencies and, thus, allows you to quickly start using AnyCable, and it's good enough to keep using it at scale. By default, HTTP broadcaster accepts publications as POST requests to the `/_broadcast` path of your server\*. The request body MUST contain the publication payload (see below to learn about [the format](#publication-format)). Here is a basic cURL example: ```bash curl -X POST -H "Content-Type: application/json" -d '{"stream":"my_stream","data":"{\"text\":\"Hello, world!\"}"}' http://localhost:8090/_broadcast ``` \* If neither the broadcast key nor the application secret is specified, we configure HTTP broadcaster to use a different port by default (`:8090`) for security reasons. You can handle broadcast requests at the main AnyCable port by specifying it explicitly (via the `http_broadcast_port` option). If the broadcast key is specified or explicitly set to "none" or auto-generated from the application secret (see below), we run it on the main port. You will see the notice in the startup logs telling you how the HTTP broadcaster endpoint was configured: ```sh 2024-03-06 10:35:39.297 INF Accept broadcast requests at http://localhost:8090/_broadcast (no authorization) nodeid=uE3mZ7 context=broadcast provider=http # OR 2024-03-06 10:35:39.297 INF Accept broadcast requests at http://localhost:8080/_broadcast (authorization required) nodeid=uE3mZ7 context=broadcast provider=http ``` ### Securing HTTP endpoint We automatically secure the HTTP broadcaster endpoint if the application broadcast key (`--broadcast_key`) is specified or inferred\* from the application secret (`--secret`) and the server is not running in the public mode (`--public`). Every request MUST include an "Authorization" header with the `Bearer ` value: ```sh # Run AnyCable $ anycable-go --broadcast_key=my-secret-key 2024-03-06 10:35:39.296 INF Starting AnyCable 1.6.0-a7aa9b4 (pid: 57260, open file limit: 122880, gomaxprocs: 8) nodeid=uE3mZ7 ... 2024-03-06 10:35:39.297 INF Accept broadcast requests at http://localhost:8080/_broadcast (authorization required) nodeid=uE3mZ7 context=broadcast provider=http # Broadcast a message $ curl -X POST -H "Content-Type: application/json" -H "Authorization: Bearer my-secret-key" -d '{"stream":"my_stream","data":"{\"text\":\"Hello, world!\"}"}' http://localhost:8080/_broadcast -w "%{http_code}" 201 ``` \* When the broadcast key is missing but the application secret is present, we automatically generate a broadcast key using the following formula (in Ruby): ```ruby broadcast_key = OpenSSL::HMAC.hexdigest("SHA256", "", "broadcast-cable") ``` When using official AnyCable server libraries, you don't need to calculate it yourself (they all use the same inference mechanism). But if you want to publish broadcasts using a custom implementation, you can generate a broadcast key for your secret key as follows: ```sh echo -n 'broadcast-cable' | openssl dgst -sha256 -hmac '' | awk '{print $2}' ``` ## Redis Pub/Sub > Enable via `--broadcast_adapter=redis` (or `ANYCABLE_BROADCAST_ADAPTER=redis`). This broadcaster uses Redis [Pub/Sub](https://redis.io/topics/pubsub) feature under the hood, and, thus, publications are delivered to all subscribed AnyCable servers simultaneously. All broadcast messages are published to a single channel (configured via the `--redis_channel`, defaults to `__anycable__`) as follows: ```sh $ redis-cli PUBLISH __anycable__ '{"stream":"my_stream","data":"{\"text\":\"Hello, world!\"}"}' (integer) 1 ``` Note that since all AnyCable server receive each publication, we cannot use [broker](./broker.md) to provide stream history support when using Redis Pub/Sub. See [configuration](./configuration.md#redis-configuration) for available Redis options. ## Redis X > Enable via `--broadcast_adapter=redisx` (or `ANYCABLE_BROADCAST_ADAPTER=redisx`). **IMPORTANT:** Redis v6.2+ is required. Redis X broadcaster uses [Redis Streams][redis-streams] instead of Publish/Subscribe to *consume* publications from your application. That gives us the following benefits: * **Broker compatibility**. This broadcaster uses a [broker](/anycable-go/broker.md) to store messages in a cache and distribute them within a cluster. This is possible due to the usage of Redis Streams consumer groups. * **Better delivery guarantees**. Even if there is no AnyCable server available at the broadcast time, the message will be stored in Redis and delivered to an AnyCable server once it is available. In combination with the [broker feature](./broker.md), you can achieve at-least-once delivery guarantees (compared to at-most-once provided by Redis Pub/Sub). To broadcast a message, you publish it to a dedicated Redis stream (configured via the `--redis_channel` option, defaults to `__anycable__`) with the publication JSON provided as the `payload` field value: ```sh $ redis-cli XADD __anycable__ "*" payload '{"stream":"my_stream","data":"{\"text\":\"Hello, world!\"}"}' "1709754437079-0" ``` See [configuration](./configuration.md#redis-configuration) for available Redis options. ## NATS Pub/Sub > Enable via `--broadcast_adapter=nats` (or `ANYCABLE_BROADCAST_ADAPTER=nats`). NATS broadcaster uses [NATS publish/subscribe](https://docs.nats.io/nats-concepts/core-nats/pubsub) functionality and supports cluster features out-of-the-box. It works to Redis Pub/Sub: distribute publications to all subscribed AnyCable servers. Thus, it's incompatible with [broker](./broker.md) (stream history support), too. To broadcast a message, you publish it to a NATS stream (configured via the `--nats_channel` option, defaults to `__anycable__`) as follows: ```sh $ nats pub __anycable__ '{"stream":"my_stream","data":"{\"text\":\"Hello, world!\"}"}' 12:03:39 Published 60 bytes to "__anycable__" ``` NATS Pub/Sub is useful when you want to set up an AnyCable cluster using our [embedded NATS](./embedded_nats.md) feature, so you can avoid having additional infrastructure components. See [configuration](./configuration.md#nats-configuration) for available NATS options. ## Publication format AnyCable accepts broadcast messages encoded as JSON and having the following properties: ```js { "stream": "", // string "data": "", // string, usually a JSON-encoded object, but not necessarily "meta": "{}" // object, publication metadata, optional } ``` It's also possible to publish multiple messages at once. For that, you just send them as an array of publications: ```js [ { "stream": "...", "data": "...", }, { "stream": "...", "data": "..." } ] ``` The `meta` field MAY contain additional instructions for servers on how to deliver the publication. Currently, the following fields are supported: * `exclude_socket`: you can specify a unique client identifier (returned by the server in the `welcome` message as `sid`) to remove this client from the list of recipients. All other meta fields are ignored for now. Here is a JSON Schema describing this format: ```json { "$schema": "http://json-schema.org/draft-07/schema", "definitions": { "publication": { "type": "object", "properties": { "stream": { "type": "string", "description": "Publication stream name" }, "data": { "type": "string", "description": "Payload, usually a JSON-encoded object, but not necessarily" }, "meta": { "type": "object", "description": "Publication metadata, optional", "properties": { "exclude_socket": { "type": "string", "description": "Unique client identifier to remove this client from the list of recipients" } }, "additionalProperties": true } }, "required": ["stream", "data"] } }, "anyOf": [ { "$ref": "#/definitions/publication" }, { "type": "array", "items":{"$ref": "#/definitions/publication"} } ] } ``` [redis-streams]: https://redis.io/docs/data-types/streams-tutorial/ --- --- url: /ruby/broadcast_adapters.md --- # Broadcasting AnyCable supports multiple ways of publishing messages from your backend to connected clients: HTTP API, Redis and [NATS][]-backed. AnyCable Ruby provides a universal API to publish broadcast messages from your Ruby/Rails applications independently of which underlying technology you would like to use. All you need is to pick and configure an adapter. Learn more about different broadcasting options and when to prefer one over another in the [AnyCable broadcasting documentation](../anycable-go/broadcasting.md). ## Configuration By default, AnyCable uses Redis Pub/Sub adapter (`redis`). Use the `broadcast_adapter` (`ANYCABLE_BROADCAST_ADAPTER`) configuration parameter to use another one. ### HTTP > Enable via `broadcast_adapter: http` in `anycable.yml` or `ANYCABLE_BROADCAST_ADAPTER=http`. The following configuration options are available: * **http\_broadcast\_url** (`ANYCABLE_HTTP_BROADCAST_URL`) Specify AnyCable HTTP broadcasting endpoint. Defaults to `http://localhost:8090/_broadcast`. If your HTTP broadcasting endpoint is secured, use the `broadcast_key` option to provide the key or the application secret (`secret`) to auto-generate it (the configuration MUST match your AnyCable server configuration). ### Redis Pub/Sub > Enable via `broadcast_adapter: redis` in `anycable.yml` or `ANYCABLE_BROADCAST_ADAPTER=redis`. **NOTE:** To use Redis adapters, you MUST add the `redis` gem to your Gemfile yourself. The following configuration options are available: * **redis\_url** (`REDIS_URL`, `ANYCABLE_REDIS_URL`) Redis connection URL (MAY include auth credentials) (default: `"redis://localhost:6379"`). * **redis\_channel** (`ANYCABLE_REDIS_CHANNEL`) Redis channel used for broadcasting (default: `"__anycable__"`). * **redis\_tls\_verify** (`ANYCABLE_REDIS_TLS_VERIFY`) Whether to validate Redis server TLS certificate if `rediss://` protocol is used (default: `false`). * **redis\_tls\_client\_cert\_path** (`ANYCABLE_REDIS_TLS_CLIENT_CERT_PATH`, `--redis-tls-client_cert-path`) Path to the file with a client TLS certificate in PEM format if the Redis server requires client authentication. * **redis\_tls\_client\_key\_path** (`ANYCABLE_REDIS_TLS_CLIENT_KEY_PATH`) Path to the file with a private key for the client TLS certificate if the Redis server requires client authentication. **NOTE:** Redis broadcast adapter uses a single connection to Redis. #### Redis Sentinel support AnyCable could be used with Redis Sentinel out-of-the-box. For that, you should configure it the following way: * `redis_url` MUST contain a master name (e.g., `ANYCABLE_REDIS_URL=redis://mymaster`) * `redis_sentinels` MUST contain a comma separated list of sentinel hosts (e.g., `ANYCABLE_REDIS_SENTINELS=my.redis.sentinel.first:26380,my.redis.sentinel.second:26380`). If your sentinels are protected with passwords, use the following format: `:password1@my.redis.sentinel.first:26380,:password2@my.redis.sentinel.second:26380`. > See the [demo](https://github.com/anycable/anycable_rails_demo/pull/8) of using Redis with Sentinels in a local Docker dev environment. ### Redis Streams > Enable via `broadcast_adapter: redisx` in `anycable.yml` or `ANYCABLE_BROADCAST_ADAPTER=redisx`. Redis Streams broadcaster shares configuration settings with Redis Pub/Sub (see above). The `redis_channel` value used as the name of the Redis Stream to publish broadcasts to. ### NATS Pub/Sub > Enable via `broadcast_adapter: nats` in `anycable.yml` or `ANYCABLE_BROADCAST_ADAPTER=nats`. **NOTE:** Make sure you added [`nats-pure` gem][nats-pure] to your Gemfile. The following configuration options are available: * **nats\_servers** (`ANYCABLE_NATS_SERVERS`, `--nats-servers`) A comma-separated list of NATS server addresses (default: `"nats://localhost:4222"`). * **nats\_channel** (`ANYCABLE_NATS_CHANNEL`, `--redis-channel`) NATS pus/sub channel for broadcasting (default: `"__anycable__"`). With [embedded NATS](../anycable-go/embedded_nats.md) feature of AnyCable, you can minimize the number of required components to deploy an AnyCable-backed application. ## Broadcasting API To publish a message to a stream via AnyCable, you can use the following API: ```ruby AnyCable.broadcast("my_stream", {text: "hoi"}) # or directly via the singleton broadcast adapter instance AnyCable.broadcast_adapter.broadcast("my_stream", {text: "hoi"}) ``` ### Batching AnyCable-Go v1.4.5+ supports publishing broadcast messages in batches. This is especially useful if you want to guarantee the order of delivered messages to clients (to be the same as the broadcasts order). To batch-broadcast messages, wrap your code with the `.batching` method of the broadcast adapter: ```ruby AnyCable.broadcast_adapter.batching do AnyCable.broadcast("my_stream", {text: "hoi"}) AnyCable.broadcast("my_stream", {text: "wereld"}) end #=> the actual publishing happens as we exit the block ``` The `.batching` method supports nesting, if you need to broadcast some messages immediately: ```ruby AnyCable.broadcast_adapter.batching do AnyCable.broadcast("my_stream", {text: "hoi"}) # added to the current batch AnyCable.broadcast_adapter.batching(false) do AnyCable.broadcast("another_stream", {text: "some other story"}) #=> publish immediately AnyCable.broadcast_adapter.batching do AnyCable.broadcast("my_stream", {text: "wereld"}) # added to the current batch end end end #=> the current batch is published ``` ### Broadcast options AnyCable v1.4.5+ supports additional broadcast options. You can pass them as the third argument to the `AnyCable.broadcast` method: ```ruby AnyCable.broadcast("my_stream", {text: "hoi"}, {exclude_socket: "some-socket-id"}) ``` The following options are supported: * `exclude_socket`: pass an AnyCable socket ID to exclude it from the broadcast recipients list. Useful if you want to broadcast to all clients except the one that initiated the broadcast. [NATS]: https://nats.io [nats-pure]: https://github.com/nats-io/nats-pure.rb --- --- url: /anycable-go/broker.md --- # Broker deep dive Broker is a component of AnyCable-Go responsible for keeping streams, sessions and presence information in a cache-like storage. It drives the [Reliable Streams](./reliable_streams.md) and [Presence](./presence.md) features. Broker implements features that can be characterized as *hot cache utilities*: * Handling incoming broadcast messages and storing them in a cache—that could help clients to receive missing broadcasts (triggered while the client was offline, for example). * Persisting client states—to make it possible to restore on re-connection (by providing a *session id* of the previous connection). * Keeping per-channel presence information. ## Client-server communication Below you can see the diagram demonstrating how clients can use the broker-backed features to keep up with the stream messages and restore their state: ```mermaid sequenceDiagram participant Client participant Server participant RPC participant Publisher Publisher--)Server: '{"stream":"chat_42","data":{"text":"Hi"}}' Client->>Server: CONNECT /cable activate Client Server->>RPC: Connect RPC->>Server: SUCCESS Server->>Client: '{"type":"welcome","sid":"a431"}' Client->>Server: '{"command":"subscribe","identifier":"ChatChannel/42","history":{"since":163213232}}' Server->>RPC: Subscribe RPC->>Server: SUCCESS Server->>Client: '{"type":"confirm_subscription"}}' Server->>Client: '{"message":{"text":"Hi"},"stream_id":"chat_42",offset: 42, epoch: "y2023"}' Server->>Client: '{"type":"confirm_history"}' Publisher--)Server: '{"stream":"chat_42","data":{"text":"What's up?"}}' Server->>Client: '{"message":{"text":"What's up?"},"stream_id":"chat_42",offset: 43, epoch: "y2023"}' Client-x Client: DISCONNECT deactivate Client Server--)RPC: Disconnect Publisher--)Server: '{"stream":"chat_42","data":{"text":"Where are you?"}}' Client->>Server: CONNECT /cable?sid=a431 activate Client Note over Server,RPC: No RPC calls made here Server->>Client: '{"type":"welcome", "sid":"h542", "restored":true,"restored_ids":["ChatChannel/42"]}' Note over Client,Server: No need to re-subscribe, we only request history Client->>Server: '{"type":"history","identifier":"ChatChannel/42","history":{"streams": {"chat_42": {"offset":43,"epoch":"y2023"}}}}' Server->>Client: '{"message":{"text":"Where are you?"},"stream_id":"chat_42",offset: 44, epoch: "y2023"}' Server->>Client: '{"type":"confirm_history"}' deactivate Client ``` To support these features, an [extended Action Cable protocol](/misc/action_cable_protocol.md#action-cable-extended-protocol) is used for communication. You can use [AnyCable JS client](https://github.com/anycable/anycable-client) library at the client-side to use the extended protocol. ## Broadcasting messages Broker is responsible for **registering broadcast messages**. Each message MUST be registered once; thus, we MUST use a broadcasting method which publishes messages to a single node in a cluster (see [broadcasting](./broadcasting.md)). Currently, `http` and `redisx` adapters are supported. **NOTE:** When legacy adapters are used, enabling a broker has no effect. To re-transmit registered messages within a cluster, we need a pub/sub component. See [Pub/Sub](./pubsub.md) for more information. The overall broadcasting message flow looks as follows: ```mermaid graph LR Publisher[Publisher] subgraph node2[Node 2] PubSub2[Pub/Sub 2] ClientC[Client C] ClientD[Client D] end subgraph node1[Node 1] Broadcaster[Broadcaster] Broker[Broker] BrokerBackend[Broker Backend] PubSub[Pub/Sub] ClientA[Client A] ClientB[Client B] end class node1 lightbg class node2 lightbg classDef lightbg fill:#ffe,stroke:#333,stroke-width:2px Publisher -.->|Message| Broadcaster Broadcaster -->|Message| Broker Broker -->|Cache Message| BrokerBackend BrokerBackend --> Broker Broker -->|Registered Message| PubSub PubSub -->|Registered Message| ClientA PubSub -->|Registered Message| ClientB PubSub -.-> PubSub2 PubSub2 -->|Registered Message| ClientC PubSub2 -->|Registered Message| ClientD ``` --- --- url: /deployment/capistrano.md --- # Capistrano Deployment You can deploy AnyCable using the provided Capistrano recipes. See [anycable/capistrano-anycable](https://github.com/anycable/capistrano-anycable) for more details. --- --- url: /rails/channels_state.md --- # Channel state Channel objects are ephemeral when using AnyCable (see [Architecture](../architecture.md)) compared to Action Cable. Thus, the following example wouldn't work in AnyCable as expected: ```ruby class RoomChannel < ApplicationCable::Channel def subscribed @room = Room.find(params["room_id"]) stream_for @room end def speak(data) broadcast_to @room, message: data["text"] end end ``` The instance variable `@room` lives only during the `#subscribed` call, it's not set when the `#speak` action is performed, 'cause it happens in the context of the new RoomChannel instance. The are two ways to fix this: using `params` or using *channel state accessors*. ## Subscription `params` Subscription parameters are included into the *subscription identifier*, and thus accessible to all RPC requests. **NOTE**: `params` are read-only. We can refactor our channel to rely on params instead of instance variables: ```ruby class RoomChannel < ApplicationCable::Channel def subscribed stream_for params["room_id"] end def speak(data) broadcast_to params["room_id"], message: data["text"] end end ``` ## Using `state_attr_accessor` In case `params` is not enough and you want to mutate the channel state or store non-primitive values, you can use *state accessors*. State accessor behaves like `attr_accessor` but also persists the data in AnyCable for subsequent calls: ```ruby class RoomChannel < ApplicationCable::Channel state_attr_accessor :room def subscribed self.room = Room.find(params["room_id"]) stream_for room end def speak(data) broadcast_to room, message: data["text"] end end ``` Read more about how the state is passed from a WebSocket server and restored in an RPC server in the [architecture overview](../architecture.md#restoring-state-objects). ### Connection state In addition to persisting channel states, we provide an ability to store data in the connection itself using a similar `.state_attr_accessor` interface: ```ruby module ApplicationCable class Connection < ActionCable::Connection::Base identified_by :current_user state_attr_accessor :url def connect self.current_user = verify_user # URL is accesible in subsequent RPC calls just like current_user self.url = request.url if current_user logger.add_tags "ActionCable", current_user.name end end end ``` Why having a separate storage when we already have `identifiers`? Identifiers are meant what they say: they should be used to identify the connection (e.g., when used for remote disconnects). If you want to store some additional information in the connection, use state accessors instead. --- --- url: /ruby/configuration.md --- # Configuration AnyCable Ruby uses [`anyway_config`](https://github.com/palkan/anyway_config) gem for configuration. Thus, it is possible to set configuration parameters through environment vars, `config/anycable.yml` file, etc. When running a gRPC server via our CLI, you can also pass configuration variables as follows: ```sh $ bundle exec anycable --rpc-host 0.0.0.0:50120 \ --redis-channel my_redis_channel \ --log-level debug ``` **NOTE:** CLI options take precedence over parameters from other sources (files, env). ## Primary settings **secret** (`ANYCABLE_SECRET`) (*@since v1.5.0*) The application secret used to secure AnyCable features: signed streams, JWT authentication, etc. We recommend setting this value as a single AnyCable-related application secret and rely on libraries to glue pieces together. **streams\_secret** (`ANYCABLE_STREAMS_SECRET`) (*@since v1.5.0*) A dedicated secret key used to [sign streams](../anycable-go/signed_streams.md). If none specified, the application secret is used. **broadcast\_adapter** (`ANYCABLE_BROADCAST_ADAPTER`) Broadcasting adapter to use. Available options out-of-the-box: `redis` (default), `http` (will be default in v2), `nats`, `redisx`. For adapter specific options, see [broadcast adapters documentation](./broadcast_adapters.md). **broadcast\_key** (`ANYCABLE_BROADCAST_KEY`) (*@since v1.5.0*) A secret key used to authorize broadcast requests. Currently, only used by the HTTP adapter. If not set, the value is inferred from the application secret. See the [broadcast adapters documentation](./broadcast_adapters.md). ## JWT settings AnyCable supports [JWT authentication](../anycable-go/jwt_identification.md) out-of-the-box. AnyCable Ruby provides an API for generating tokens relying on the following configuration parameters: * **jwt\_secret** (`ANYCABLE_JWT_SECRET`) (*@since v1.5.0*) The secret key used to sign JWT tokens. Optional (the application secret is used if no JWT secret specified) * **jwt\_ttl** (`ANYCABLE_JWT_TTL`) (*@since v1.5.0*) The time-to-live (TTL) for tokens in seconds. Default: 3600 (1 hour). ## Presets AnyCable Ruby comes with a few built-in configuration presets for particular deployments environments, such as Fly. The presets are detected and activated automatically To disable automatic presets activation, provide `ANYCABLE_PRESETS=none` environment variable (or pass the corresponding option to the CLI: `bundle exec anycable --presets=none`). **NOTE:** Presets do not override explicitly provided configuration values. ### Preset: fly Automatically activated if all of the following environment variables are defined: `FLY_APP_NAME`, `FLY_REGION`, `FLY_ALLOC_ID`. The preset provide the following defaults: * `rpc_host`: "0.0.0.0:50051" If the `ANYCABLE_FLY_WS_APP_NAME` env variable is provided, the following defaults are configured as well: * `nats_servers`: `"nats://..internal:4222"` * `http_broadcast_url`: `"http://..internal:8090/_broadcast"` ## gRPC settings **rpc\_host** (`ANYCABLE_RPC_HOST`, `--rpc-host`) Local address to run gRPC server on (default: `"127.0.0.1:50051"`). Set it to `0.0.0.0:50051` to make gRPC server accessible to the outside world (for example, when using containerized environment). **rpc\_tls\_cert** (`ANYCABLE_RPC_TLS_CERT`, `--rpc-tls-cert`) and **rpc\_tls\_key** (`ANYCABLE_RPC_TLS_KEY`, `--rpc-tls-key`) Specify file paths or contents for TLS certificate and private key for gRPC server. ### Concurrency settings AnyCable gRPC server maintains a pool of worker threads to execute commands. We rely on the `grpc` gem [default pool size](https://github.com/grpc/grpc/blob/80e834abab5dff45e16e9a1e3b98f20eae5f91ad/src/ruby/lib/grpc/generic/rpc_server.rb#L163), which is equal to **30**. You can configure the pool size via `rpc_pool_size` parameter (or `ANYCABLE_RPC_POOL_SIZE` env var). Increasing pool size makes sense if you have a lot of IO operations in your channels (DB, HTTP, etc.). **NOTE**: Make sure the gRPC pool size is aligned with concurrency limits you have in your application, such as database pool size. **IMPORTANT**: AnyCable server concurrency limit must correlate to the RPC server pool size (read more [here](../anycable-go/rpc.md#concurrency-settings)). ### Alternative gRPC implementations AnyCable Ruby uses the `grpc` gem to run its gRPC server by default. The gem heavily relies on native extensions, which may lead to complications during installation (e.g., on Alpine Linux) and compatibility issues with modern Ruby versions. To be closer to Ruby and depend less on extensions, AnyCable Ruby also supports an alternative gRPC implementation—[grpc\_kit](https://github.com/cookpad/grpc_kit). You can opt-in to use `grpc_kit` by setting `ANYCABLE_GRPC_IMPL=grpc_kit` environment variable for your `bundle exec anycable` process. You also need to update your `Gemfile` to include the `grpc_kit` gem and gRPC-less versions of AnyCable gems: ```ruby # For Rails applications gem "anycable-rails-core", require: ["anycable-rails"] gem "grpc_kit" # For non-Rails applications gem "anycable-core", require: ["anycable"] gem "grpc_kit" ``` --- --- url: /deployment.md --- # Deployment Check out the following guides for deploying AnyCable: * [Heroku](./deployment/heroku.md) * [Fly.io](./deployment/fly.md) * [Render](./deployment/render.md) * [Kubernetes](./deployment/kubernetes.md) * [Docker](./deployment/docker.md) * [Capistrano](./deployment/capistrano.md) * [Systemd](./deployment/systemd.md) * Hatchbox 🔗 * AWS Beanstalk 🔗 * AWS ECS 🔗 Other deployment-related topics: * [Load balancing](./deployment/load_balancing.md) * [Load testing](./deployment/load_testing.md) --- --- url: /deployment/Readme.md --- # Deployment Check out the following guides for deploying AnyCable: * [Heroku](./heroku.md) * [Fly.io](./fly.md) * [Render](./render.md) * [Kubernetes](./kubernetes.md) * [Docker](docker.md) * [Capistrano](capistrano.md) * [Systemd](systemd.md) Other deployment-related topics: * [Load balancing](./load_balancing.md) * [Load testing](./load_testing.md) --- --- url: /deployment/docker.md --- # Docker Deployment ## Ruby RPC server Deployment using Docker could be tricky since we rely on several gems with native extensions (`grpc`,`google-protobuf`). Here is the list of useful resources: * Segmentation faults with alpine ([anycable-rails#70](https://github.com/anycable/anycable-rails/issues/70) and [anycable#47](https://github.com/anycable/anycable/issues/47)) * [Example Dockerfile (alpine)](https://github.com/anycable/anycable/blob/master/etc/Dockerfile.alpine) ## AnyCable-Go Official docker images are available at [DockerHub](https://hub.docker.com/r/anycable/anycable-go/). --- --- url: /anycable-go/durable_streams.md --- # Durable Streams AnyCable supports the [Durable Streams](https://github.com/durable-streams/durable-streams) specification, enabling HTTP-based clients to consume real-time data streams using catch-up reads, long-polling, and Server-Sent Events (SSE). > **Note**: AnyCable implements only the **read** portion of the Durable Streams specification. Write operations are not supported. ## Overview Durable Streams provides a standardized HTTP API for consuming real-time data with built-in support for: * **Catch-up reads**: Fetch historical messages from a stream * **Long-polling**: Wait for new messages with automatic timeout * **SSE streaming**: Continuous real-time updates via Server-Sent Events This is particularly useful for clients that cannot use WebSockets or prefer HTTP-based communication. Or for platforms/environments, where you can't use AnyCable or Action Cable client SDKs. ## Configuration Enable Durable Streams support by setting the `--ds` flag or `ANYCABLE_DS=true` environment variable: ```sh $ anycable-go --ds INFO ... Handle Durable Streams requests at http://localhost:8080/ds ``` ### Configuration options | Option | Env variable | Default | Description | |--------|--------------|---------|-------------| | `--ds` | `ANYCABLE_DS` | `false` | Enable Durable Streams support | | `--ds_path` | `ANYCABLE_DS_PATH` | `/ds` | URL path for DS requests | | `--ds_skip_auth` | `ANYCABLE_DS_SKIP_AUTH` | `false` | Skip client authentication (only authorize streams) | | `--ds_poll_interval` | `ANYCABLE_DS_POLL_INTERVAL` | `10` | Long-poll timeout in seconds | | `--ds_sse_ttl` | `ANYCABLE_DS_SSE_TTL` | `60` | Maximum SSE connection lifetime in seconds | ## Client usage Use the official [@durable-streams/client](https://www.npmjs.com/package/@durable-streams/client) SDK to consume streams: ```js import { stream } from "@durable-streams/client"; const baseUrl = "http://localhost:8080"; const streamName = "chat/room-42"; // Catch-up read (fetch existing messages) const res = await stream({ url: `${baseUrl}/ds/${streamName}`, offset: "-1", // Start from beginning }); const messages = await res.json(); console.log("Messages:", messages); console.log("Next offset:", res.offset); ``` ### Reading modes #### Catch-up mode Fetch historical messages without waiting for new ones: ```js const res = await stream({ url: `${baseUrl}/ds/${streamName}`, offset: "-1", // -1 means start from beginning }); const messages = await res.json(); // Use res.offset for subsequent requests ``` #### Long-poll mode Wait for new messages with automatic timeout: ```js const res = await stream({ url: `${baseUrl}/ds/${streamName}`, offset: lastOffset, // Required for live modes live: "long-poll", }); const messages = await res.json(); ``` #### SSE mode Continuous streaming with automatic reconnection support: ```js const res = await stream({ url: `${baseUrl}/ds/${streamName}`, offset: "-1", live: "sse", json: true, }); res.subscribeJson(async (batch) => { for (const item of batch.items) { console.log("Received:", item); } }); ``` ## Authentication and authorization AnyCable DS supports two layers of security: **client authentication** and **stream authorization**. ### Client authentication By default, DS requests go through the same authentication flow as WebSocket connections. You can use [JWT authentication](./jwt_identification.md) for stateless authentication: ```js const res = await stream({ url: `${baseUrl}/ds/${streamName}`, offset: "-1", headers: { "X-JID": jwtToken, // Or use the configured header name }, }); ``` To skip client authentication and only perform stream authorization, set `--ds_skip_auth=true`. ### Stream authorization Stream access is controlled using [signed streams](./signed_streams.md). Provide a signed stream token via query parameter or header: ```js // Via query parameter const res = await stream({ url: `${baseUrl}/ds/${streamName}?signed=${signedToken}`, offset: "-1", }); // Via header const res = await stream({ url: `${baseUrl}/ds/${streamName}`, offset: "-1", headers: { "X-Signed": signedToken, }, }); ``` Generate signed tokens using the same mechanism as for WebSocket signed streams: ```ruby # Ruby/Rails signed_token = AnyCable::Streams.signed("chat/room-42") ``` ```js // Node.js (using @anycable/serverless-js) import { createHmac } from 'crypto'; const encoded = Buffer.from(JSON.stringify(streamName)).toString('base64'); const digest = createHmac('sha256', streamsSecret).update(encoded).digest('hex'); const signedToken = `${encoded}--${digest}`; ``` If public streams are enabled (`--public_streams`), unsigned stream names are also accepted. ## Requirements Durable Streams requires a [broker](./broker.md) to be configured for message history: ```sh anycable-go --ds --presets=broker ``` See [reliable streams](./reliable_streams.md) for more information on broker configuration and cache settings. ## Limitations * Only `application/json` content type is supported * Write operations (appends, stream creation) are not implemented; use general AnyCable [broadcasting](./broadcasting.md) capabilities. * Offsets are opaque tokens specific to AnyCable; clients should not parse them --- --- url: /anycable-go/embedded_nats.md --- # Embedded NATS AnyCable supports running a NATS server as a part of the `anycable-go` WebSocket server. Thus, you don't need any *external* pub/sub services to build AnyCable clusters (i.e., having multiple WebSocket nodes). > 🎥 Check out this [AnyCasts episode](https://anycable.io/anycasts/flying-multi-regionally-with-nats/) to learn how to use AnyCable with embedded NATS on [Fly.io][fly] There are multiple ways to use this functionality: * [Single-server configuration](#single-server-configuration) * [Cluster configuration](#cluster-configuration) ## Single-server configuration The easiest way to start using embedded NATS in AnyCable is to run a single `anycable-go` instance with *eNATS* (this is how we call "Embedded NATS") enabled and connecting all other instances to it. This is how you can do that locally: ```sh # first instance with NATS embedded $ anycable-go --broadcast_adapter=nats --embed_nats --enats_addr=nats://0.0.0.0:4242 INFO 2023-02-28T00:06:45.618Z context=main Starting AnyCable 1.3.0 INFO 2023-02-28T00:06:45.649Z context=main Embedded NATS server started: nats://127.0.0.1:4242 ``` Now you can run another WebSocket server connected to the first one: ```sh anycable-go --port 8081 --broadcast_adapter=nats --nats_servers=nats://0.0.0.0:4242 ``` RPC servers can also connect to the first AnyCable-Go server: ```sh bundle exec anycable --broadcast_adapter=nats --nats_servers=nats://0.0.0.0:4242 ``` This setup is similar to running a single NATS server independently. ## Cluster configuration Alternatively, you can form a cluster from embedded NATS instances. For that, you should start each `anycable-go` instance with a NATS cluster address and connect them together via the routes table: ```sh # first instance $ anycable-go --broadcast_adapter=nats --embed_nats --enats_addr=nats://0.0.0.0:4242 --enats_cluster=nats://0.0.0.0:4243 INFO 2023-02-28T00:06:45.618Z context=main Starting AnyCable 1.3.0 INFO 2023-02-28T00:06:45.649Z context=main Embedded NATS server started: nats://127.0.0.1:4242 (cluster: nats://0.0.0.0:4243, cluster_name: anycable-cluster) # other instances $ anycable-go --port 8081 --broadcast_adapter=nats --embed_nats --enats_addr=nats://0.0.0.0:4342 --enats_cluster=nats://0.0.0.0:4343 --enats_cluster_routes=nats://0.0.0.0:4243 INFO 2023-02-28T00:06:45.618Z context=main Starting AnyCable 1.3.0 INFO 2023-02-28T00:06:45.649Z context=main Embedded NATS server started: nats://127.0.0.1:4342 (cluster: nats://0.0.0.0:4343, cluster_name: anycable-cluster, routes: nats://0.0.0.0:4243) ``` See more information in the [NATS documentation](https://docs.nats.io/running-a-nats-service/configuration/clustering). ### Using on Fly.io AnyCable automatically infers sensible default configuration values for applications deployed to Fly.io. To configure a cluster from embedded NATS servers, all you need is to turn the embedded NATS feature on and use the defaults. AnyCable automatically configures cluster addresses and routes to build a cluster **within the current region**. See also [Fly deployment documentation](../deployment/fly.md). ### Super-cluster You can also setup a super-cluster by configuring gateways: ```sh # first cluster $ anycable-go --broadcast_adapter=nats --embed_nats \ --enats_addr=nats://0.0.0.0:4242 --enats_cluster=nats://0.0.0.0:4243 \ --enats_gateway=nats://0.0.0.0:7222 # second cluster $ anycable-go --port 8081 --broadcast_adapter=nats --embed_nats \ --enats_addr=nats://0.0.0.0:4342 --enats_cluster=nats://0.0.0.0:4343 \ --enats_gateway=nats://0.0.0.0:7322 \ --enats_gateways=anycable-cluster:nats://0.0.0.0:7222 ``` **NOTE**: The value of the `--enats_gateways` parameter must be have a form `:,;:,`. **IMPORTANT**: All servers in the cluster must have the same gateway configuration. You can also specify the advertised address for the gateway (in case your cluster is behind a NAT) via the `--enats_gateway_advertise` parameter. See more information in the [NATS documentation](https://docs.nats.io/running-a-nats-service/configuration/clustering). [fly]: https://fly.io --- --- url: /ruby/exceptions.md --- # Exceptions handling AnyCable Ruby RPC server (both gRPC and HTTP) captures all exceptions during your application code (your *connection* and *channels*) execution. The default behaviour is to log the exceptions with `"error"` level. > AnyCable Rails automatically integrates with Rails 7+ error reporting interface (`Rails.error.report(...)`), so you don't need to configure anything yourself. You can attach your own exceptions handler, for example, to send notifications somewhere (Honeybadger, Sentry, Airbrake, etc.): ```ruby # with Honeybadger AnyCable.capture_exception do |ex, method, message| Honeybadger.notify(ex, component: "any_cable", action: method, params: message) end # with Sentry (new SDK)... AnyCable.capture_exception do |ex, method, message| Sentry.with_scope do |scope| scope.set_tags transaction: "AnyCable#{method}", extra: message Sentry.capture_exception(ex) end end # ...or Raven (legacy Sentry SDK) AnyCable.capture_exception do |ex, method, message| Raven.capture_exception(ex, transaction: "AnyCable##{method}", extra: message) end # with Airbrake AnyCable.capture_exception do |ex, method, message| Airbrake.notify(ex) do |notice| notice[:context][:component] = "any_cable" notice[:context][:action] = method notice[:params] = message end end # with Datadog AnyCable.capture_exception do |ex, method, message| Datadog.tracer.trace("any_cable") do |span| span.set_error(ex) span.set_tag("method", method) span.set_tag("message", message) ensure span.finish end end ``` --- --- url: /deployment/fly.md --- # Fly.io Deployment > 🎥 Check out AnyCasts episode to learn how to deploy AnyCable applications to [Fly.io][fly]: [Learn to Fly.io with AnyCable](https://anycable.io/anycasts/learn-to-fly-io-with-anycable/) and [Flying multi-regionally with NATS](https://anycable.io/anycasts/flying-multi-regionally-with-nats/). The recommended way to deploy AnyCable apps to [Fly.io][fly] is to have two applications: one with a Rails app and another one with `anycable-go` (backed by the official Docker image). ## Deploying Rails app Follow the [official documentation][fly-docs-rails] on how to deploy a Rails app. Then, we need to configure AnyCable broadcast adapter. For multi-node applications (i.e., if you want to scale WebSocket servers horizontally), you need a distributed pub/sub engine, such as Redis or NATS. The quickest way to deploy AnyCable on Fly is to use [embedded NATS](../anycable-go/embedded_nats.md), so we'll be using it for the rest of the article. Thus, upgrade your `anycable.yml` by specifying `nats` as a broadcast adapter: ```yml # config/anycable.yml production: <<: *default # Use NATS in production broadcast_adapter: nats ``` Using Redis is similar to other deployment methods, please, check the corresponding [documentation][fly-docs-redis] on how to create a Redis instance on Fly. ### Configuration AnyCable can automatically infer sensible defaults for applications running on Fly.io. You only need to [link Rails and AnyCable-Go apps with each other](#linking-rails-and-anycable-go-apps). We will rely on [client-side load balancing](./load_balancing.md), so make sure max connection age is set to some short period (minutes). The default value of 5 minutes is a good starting point, so you shouldn't change anything in the configuration. ### Standalone RPC process (default) You can define multiple processes in your `fly.toml` like this: ```toml # fly.toml [processes] web = "bundle exec puma" # or whatever command you use to run a web server rpc = "bundle exec anycable" ``` Don't forget to update the `services` definition: ```diff [[services]] - processes = ["app"] + processes = ["web"] ``` **NOTE**: Keep in mind that each process is executed within its own [Firecracker VM](https://fly.io/docs/reference/machines/). This brings a benefit of independent scaling, e.g., `fly scale count web=2 rpc=1`. ### Embedded RPC You can run RPC server along with the Rails web server by using the embedded mode. This way you can reduce the number of VMs used (and hence, reduce the costs or fit into the free tier). Just add the following to your configuration: ```toml # fly.toml [env] # ... ANYCABLE_EMBEDDED = "true" ``` Embedding the RPC server could help to reduce the overall RAM usage (since there is a single Ruby process), but would increase the GVL contention (since more threads would compete for Ruby VM). ## Deploying AnyCable-Go To deploy AnyCable-Go server, we need to create a separate Fly application. Following [the official docs][fly-multiple-apps], we should do the following: * Create a `.fly/applications/anycable-go` folder and use it as a working directory for subsequent commands: ```sh mkdir -p .fly/applications/anycable-go cd .fly/applications/anycable-go ``` * Run the following command: ```sh fly launch --image anycable/anycable-go:1 --no-deploy --name my-cable ``` * Create a configuration file, `fly.toml`: ```toml # .fly/applications/anycable-go/fly.toml app = "my-cable" # use the name you chose on creation kill_signal = "SIGINT" kill_timeout = 5 processes = [] [build] image = "anycable/anycable-go:1" [env] PORT = "8080" [experimental] allowed_public_ports = [] auto_rollback = true [[services]] http_checks = [] internal_port = 8080 processes = ["app"] protocol = "tcp" script_checks = [] [services.concurrency] # IMPORTANT: Specify concurrency limits hard_limit = 10000 soft_limit = 10000 type = "connections" [[services.ports]] handlers = ["tls", "http"] port = 443 [[services.tcp_checks]] grace_period = "1s" interval = "15s" restart_limit = 0 timeout = "2s" ``` * If you use Redis, add `REDIS_URL` obtained during the Rails application configuration to the *cable* app: ```sh fly secrets set REDIS_URL= ``` You can always look up your `REDIS_URL` by running the following command: `fly redis status `. Now you can run `fly deploy` to deploy your AnyCable-Go server. ## Linking Rails and AnyCable-Go apps Finally, we need to *connect* both parts to each other. At the Rails app side, we need to provide the URL of our WebSocket server. For example: ```toml [env] # ... CABLE_URL = "my-cable.fly.dev" ``` And in your `production.rb` (added automatically if you used `rails g anycable:setup`): ```ruby Rails.application.configure do # Specify AnyCable WebSocket server URL to use by JS client config.after_initialize do config.action_cable.url = ActionCable.server.config.url = ENV.fetch("CABLE_URL", "/cable") if AnyCable::Rails.enabled? end end ``` When using embedded NATS or HTTP broadcast adapter, we also need to specify the AnyCable-Go application name, so it can locate WebSocket servers automatically: ```toml # fly.toml [env] ANYCABLE_FLY_WS_APP_NAME = "my-cable" ``` **NOTE:** By default, AnyCable resolves the address within the current region. For example, if you run Rails application in the `lhr` region, than the resulting NATS url will be `nats://lhr.my-cable.internal:4222`. At the AnyCable-Go side, we must provide the name of the Rails application: ```toml # .fly/applications/anycable-go/fly.toml [env] # ... FLY_ANYCABLE_RPC_APP_NAME="my-app" ``` The name will be used, for example, to generate an RPC address: `my-app -> dns:///lhr.my-app.internal:50051`. **NOTE:** The generated RPC url points to the instances located in the same region as the AnyCable-Go server. ## Authentication The described approach assumes running two Fly applications on two separate domains. If you're using cookie-based authentication, make sure you configured your Rails cookie settings accordingly: ```ruby # session_store.rb Rails.application.config.session_store :cookie_store, key: "_any_cable_session", domain: :all # or domain: '.example.com' # anywhere setting cookie cookies[:val] = {value: "1", domain: :all} ``` **IMPORTANT:** It's impossible to share cookies between `.fly.dev` domains, so cookie-based authentication wouldn't work. We recommend using [JWT authentication instead][jwt-id]. [fly]: https://fly.io [fly-docs-rails]: https://fly.io/docs/rails/ [fly-docs-redis]: https://fly.io/docs/reference/redis/ [fly-multiple-apps]: https://fly.io/docs/laravel/advanced-guides/multiple-applications/#creating-a-fly-application-within-a-fly-application [jwt-id]: /anycable-go/jwt_identification --- --- url: /getting_started.md --- # Getting Started with AnyCable AnyCable is a language-agnostic real-time server focused on performance and reliability written in Go. > The quickest way to get AnyCable is to use our managed (and free) solution: [plus.anycable.io](https://plus.anycable.io) ## Installation The easiest way to install AnyCable-Go is to [download](https://github.com/anycable/anycable/releases) a pre-compiled binary (for versions < 1.6.0 use our [legacy repository](https://github.com/anycable/anycable-go/releases)). MacOS users could install it with [Homebrew](https://brew.sh/) ```sh brew install anycable-go ``` Arch Linux users can install [anycable-go package from AUR](https://aur.archlinux.org/packages/anycable-go/). ### Via NPM For JavaScript projects, there is also an option to install AnyCable-Go via NPM: ```sh npm install --save-dev @anycable/anycable-go pnpm install --save-dev @anycable/anycable-go yarn add --dev @anycable/anycable-go # and run as follows npx anycable-go ``` **NOTE:** The version of the NPM package is the same as the version of the AnyCable server binary (which is downloaded automatically on the first run). ## Usage After installation, you can run AnyCable as follows: ```sh $ anycable-go 2024-03-06 13:38:07.545 INF Starting AnyCable 1.6.0-4f16b99 (pid: 8289, open file limit: 122880, gomaxprocs: 8) nodeid=hj2mXN ... 2024-03-06 13:38:56.490 INF RPC controller initialized: localhost:50051 (concurrency: 28, impl: grpc, enable_tls: false, proto_versions: v1) nodeid=FlCtwf context=rpc ``` By default, AnyCable tries to connect to a gRPC server listening at `localhost:50051` (the default host for the Ruby gem). AnyCable is designed as a logic-less proxy for your real-time features relying on a backend server to authenticate connections, authorize subscriptions and process incoming messages. That's why our default configuration assumes having an RPC server to handle all this logic. You can read more about AnyCable RPC in the [corresponding documentation](./rpc.md). ### Standalone mode (pub/sub only) For pure pub/sub functionality, you can use AnyCable in a standalone mode, without any RPC servers. For that, you must configure the following features: * [JWT authentication](./jwt_identification.md) or disable authentication completely (`--noauth`). **NOTE:** You can still add minimal protection via the `--allowed_origins` option (see [configuration](./configuration.md#primary-settings)). * Enable [signed streams](./signed_streams.md) or allow public streams via the `--public_streams` option. There is also a shortcut option `--public` to enable both `--noauth` and `--public_streams` options. **Use it with caution**. You can also explicitly disable the RPC component by specifying the `--norpc` option. Thus, to run AnyCable real-time server in an insecure standalone mode, use the following command: ```sh $ anycable-go --public 2024-03-06 14:00:12.549 INF Starting AnyCable 1.6.0-4f16b99 (pid: 17817, open file limit: 122880, gomaxprocs: 8) nodeid=wAhWDB 2024-03-06 14:00:12.549 WRN Server is running in the public mode nodeid=wAhWDB ... ``` To secure access to AnyCable server, specify either the `--jwt_secret` or `--streams_secret` option. There is also the `--secret` shortcut: ```sh anycable-go --secret=VERY_SECRET_VALUE --norpc ``` Read more about pub/sub mode in the [signed streams documentation](./signed_streams.md). ### Connecting to AnyCable AnyCable uses the [Action Cable protocol][protocol] for client-server communication. We recommend using our official [JavaScript client library][anycable-client] for all JavaScript/TypeScript runtimes: ```js import { createCable } from '@anycable/web' const cable = createCable(CABLE_URL) const subscription = cable.subscribeTo('ChatChannel', { roomId: '42' }) const _ = await subscription.perform('speak', { msg: 'Hello' }) subscription.on('message', msg => { if (msg.type === 'typing') { console.log(`User ${msg.name} is typing`) } else { console.log(`${msg.name}: ${msg.text}`) } }) ``` **Note**: The snippet above assumes having a "ChatChannel" defined in your application (which is connected to AnyCable via RPC). You can also use: * Third-party Action Cable-compatible clients. * EventSource (Server-Sent Events) connections ([more info](./sse.md)). * Custom WebSocket clients following the [Action Cable protocol][protocol]. AnyCable Pro also supports: * Apollo GraphQL WebSocket clients ([more info](./apollo.md)) * HTTP streaming (long-polling) ([more info](./long_polling.md)) * OCPP WebSocket clients ([more info](./ocpp.md)) ### Broadcasting messages Finally, to broadcast messages to connected clients via the name pub/sub streams, you can use one of the provided [broadcast adapters](./broadcasting.md). [anycable-client]: https://github.com/anycable/anycable-client [protocol]: ../misc/action_cable_protocol.md --- --- url: /ruby/health_checking.md --- # Health checking AnyCable Ruby gRPC server comes with two types of health checks: HTTP and gRPC. ## HTTP You can run a health check server along with the RPC server by specifying the `http_health_port`: ```sh # via CLI options $ bundle exec anycable --http-health-port=54321 #> ... #> HTTP health server is listening on localhost:54321 and mounted at "/health" # or via env $ ANYCABLE_HTTP_HEALTH_PORT=54321 bundle exec anycable ``` You can also specify the mount path: ```sh $ bundle exec anycable --http-health-port=54321 --http-health-path="/check" #> ... #> HTTP health server is listening on localhost:54321 and mounted at "/check" ``` The health check server responds with 200 when the gRPC server is running and with 503 when it isn't. HTTP health check server can be used for readiness and liveness checks (e.g., in Kubernetes environment). ## gRPC AnyCable includes a standard gRPC health checker (v1). See official [documentation](https://github.com/grpc/grpc/blob/master/doc/health-checking.md). Please, use the `anycable.RPC` service name for a health check (responds with `SERVING`). Omitting a service name would result in `NOT_SERVING` response. --- --- url: /deployment/heroku.md --- # Heroku Deployment ## Simplified (with HTTP RPC) Since v1.4, AnyCable supports [RPC over HTTP](../ruby/http_rpc.md) which allows us to use a single Heroku application for both regular and AnyCable RPC HTTP requests. All you need is to deploy `anycable-go` as a separate Heroku application, configure it to use HTTP RPC and point it to your main application. > See the [demo](https://github.com/anycable/anycable_rails_demo/pull/32) of preparing the app for a simplified Heroku deployment. ### Deploying AnyCable-Go Deploy AnyCable-Go by simply clicking the button below: [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://www.heroku.com/deploy?template=https://github.com/anycable/anycable-go) **NOTE:** To recreate the button-deployed application later, you must create [`heroku.yml`](https://github.com/anycable/anycable-go/blob/master/heroku.yml) and a [Dockerfile](https://github.com/anycable/anycable-go/blob/master/.docker/Dockerfile.heroku) and put them into your repository. Fill the required information: * `ANYCABLE_RPC_HOST`: the URL of your web application containing the mount path of the AnyCable HTTP RPC server (e.g., `https://my-app.herokuapp.com/_anycable`). * `ANYCABLE_SECRET`: A secret that will be used to generate an authentication token for HTTP RPC requests. Make sure to set the same values in your web application configuration (e.g., for Rails, `http_rpc_mount_path: "/_anycable"` in `config/anycable.yml` and the `ANYCABLE_SECRET` env var). We recommend enabling [Dyno Metadata](https://devcenter.heroku.com/articles/dyno-metadata) to activate the [Heroku configuration preset](../anycable-go/configuration.md#presets). Otherwise, don't forget to set `ANYCABLE_HOST=0.0.0.0` in the application configuration. Other configuration parameters depend on the features you use. ### Deploying AnyCable-Go PRO Heroku supports only public Docker registries when deploying using the `heroku.yml` config. To deploy AnyCable-Go PRO, you can use Heroku *Container Registry and Runtime*: pull an AnyCable-Go PRO image from our private registry, push it to your Heroku registry and deploy. See [the official documentation](https://devcenter.heroku.com/articles/container-registry-and-runtime). ### Configuring web application At the web application (Rails) side, you must also configure: * `config.action_cable.url`: the URL of your AnyCable-Go application (e.g., `wss://anycable-go.herokuapp.com/cable`). * `config.session_store :cookie_store, key: "__sid", domain: :all` to share the session between the web and AnyCable-Go applications. **NOTE:** Sharing cookies across domains doesn't work with `*.herokuapp.com` domains; you must use custom domains (or put a CDN in front of your applications to server both from the same hostname). ## Full mode (with gRPC) Deploying applications using AnyCable with gRPC on Heroku is a little bit tricky due to the following limitations: * **Missing HTTP/2 support.** AnyCable relies on HTTP/2 ('cause it uses [gRPC](https://grpc.io)). * **The only `web` service.** It is not possible to have two HTTP services within one application (only `web` service is open the world). The only way (for now) to run AnyCable applications on Heroku is to have two separate applications sharing some resources: the first one is a typical web application responsible for general HTTP and the second contains AnyCable WebSocket and RPC servers. For convenience, we recommend adding a WebSocket app instance to the same [pipeline](https://devcenter.heroku.com/articles/pipelines) as the original one. You can create a pipeline via Web UI or using a CLI: ```sh heroku pipelines:create -a example-pipeline ``` ### Preparing the source code > See also the [demo](https://github.com/anycable/anycable_rails_demo/pull/4) of preparing the app for Heroku deployment. We have to use the same `Procfile` for both applications ('cause we're using the same repo) but run different commands for the `web` service. We can use an environment variable to toggle the application behaviour, for example: ```sh # Procfile web: [[ "$ANYCABLE_DEPLOYMENT" == "true" ]] && bundle exec anycable --server-command="anycable-go" || bundle exec rails server -p $PORT -b 0.0.0.0 ``` If you have a `release` command in your `Procfile`, we recommend to ignore it for AnyCable deployment as well and let the main app take care of it. For example: ```sh release: [[ "$ANYCABLE_DEPLOYMENT" == "true" ]] && echo "Skip release script" || bundle exec rails db:migrate ``` ### Preparing Heroku apps Here is the step-by-step guide on how to deploy AnyCable application on Heroku from scratch using [Heroku CLI](https://devcenter.heroku.com/articles/heroku-cli#download-and-install). First, we need to create an app for the *main* application (skip this step if you already have a Heroku app): ```sh # Create a new heroku application heroku create example-app # Add to the pipeline heroku pipelines:add example-pipeline -a example-app # Add necessary add-ons # NOTE: we need at least Redis heroku addons:create heroku-postgresql heroku addons:create heroku-redis # Deploy application git push heroku master # Run migrations or other postdeployment scripts heroku run "rake db:migrate" ``` See also the [official Heroku guide](https://devcenter.heroku.com/articles/getting-started-with-rails6#create-a-new-rails-app-or-upgrade-an-existing-one) for setting up Rails applications. Secondly, create a new Heroku application for the same repository to host the WebSocket server: ```sh # Create a new application and name the git remote as "anycable" heroku create example-app-anycable --remote anycable # Add this application to the pipeline (if you have one) heroku pipelines:add example-pipeline -a example-app-anycable ``` Now we need to add `anycable-go` to this new app. There is a buildpack, [anycable/heroku-anycable-go](https://github.com/anycable/heroku-anycable-go), for that: ```sh # Add anycable-go buildpack heroku buildpacks:add https://github.com/anycable/heroku-anycable-go -a example-app-anycable ``` Also, to run RPC server ensure that you have a Ruby buildpack installed as well: ```sh # Add ruby buildpack heroku buildpacks:add heroku/ruby -a example-app-anycable ``` Now, **the most important** part: linking one application to another. First, we need to link the shared Heroku resources (databases, caches, other add-ons). Let's get a list of the main app add-ons: ```sh # Get the list of the first app add-ons $ heroku addons -a example-app ``` Find the ones you want to share with the AnyCable app and *attach* them to it: ```sh # Attach add-ons to the second app heroku addons:attach postgresql-closed-12345 -a example-app-anycable heroku addons:attach redis-regular-12345 -a example-app-anycable ``` **NOTE:** Make sure you have a Redis instance shared and the database as well. You might also want to share other add-ons depending on your configuration. ### Configuring the apps Finally, we need to add the configuration variables to both apps. For AnyCable app: ```sh # Make our heroku/web script run `bundle exec anycable` heroku config:set ANYCABLE_DEPLOYMENT=true -a example-app-anycable # Configure anycable-go to listen at 0.0.0.0 (to make it accessible to Heroku router) heroku config:set ANYCABLE_HOST=0.0.0.0 -a example-app-anycable # Don't forget to add RAILS_ENV if using Rails heroku config:set RAILS_ENV=production -a example-app-anycable ``` You may also want to explicitly specify AnyCable-Go version (the latest release is used by default): ```sh heroku config:set HEROKU_ANYCABLE_GO_VERSION ``` **IMPORTANT:** You also need to copy all (or most) the application-specific variables from `example-app` to `example-app-anycable` to make sure that applications have the same environment. For example, you **must** use the same `SECRET_KEY_BASE` if you're going to use cookies for authentication or utilize some other encryption-related functionality in your channels code. Here is an example Rake task to sync env vars between two applications on Heroku: [heroku.rake][]. We recommend using Rails credentials (or alternative secure store implementation, e.g., [chamber](https://github.com/thekompanee/chamber)) to store the application configuration. This way you won't need to think about manual and even automated env syncing. Next, we need to *tell* the main app where to point Action Cable clients. If you configured AnyCable via `rails g anycable:setup`, you have something like this in your `production.rb`: ```ruby # config/environments/production.rb config.after_initialize do config.action_cable.url = ActionCable.server.config.url = ENV.fetch("CABLE_URL") if AnyCable::Rails.enabled? end ``` And set the `CABLE_URL` var to point to the AnyCable endpoint in the AnyCable app: ```sh # with the default Heroku domain heroku config:set CABLE_URL="wss://example-app-anycable.herokuapp.com/cable" # or with a custom domain heroku config:set CABLE_URL="ws://anycable.example.com/cable" ``` **NOTE:** with default `.herokuapp.com` domains you won't be able to use cookies for authentication. Read more in [troubleshooting](../troubleshooting.md#my-websocket-connection-fails-with-quotauth-failedquot-error). ### Pushing code To keep the applications in sync, you need to deploy them simultaneously. The easiest way to do that is to configure [automatic deploys](https://devcenter.heroku.com/articles/github-integration#automatic-deploys). If you prefer the manual `git push` approach, don't forget to push code to the AnyCable app every time you push the code to the main app: ```sh git push anycable master ``` ## Using with review apps Creating a separate AnyCable app for every Heroku review app seems to be an unnecessary overhead. You can avoid this by using one of the following techniques. ### Use AnyCable Rack server [AnyCable Rack](https://github.com/anycable/anycable-rack-server) server could be mounted into a Rack/Rails app and run from the same process and handle WebSocket clients at the same HTTP endpoint as the app. On the other hand, it has the same architecture involving the RPC server, and thus provides the same experience as another, standalone, AnyCable implementations. ### Use Action Cable You can use the *standard* Action Cable in review apps with [enforced runtime compatibility checks](../rails/compatibility.md#runtime-checks). In your `cable.yml` use the following code to conditionally load the adapter: ```yml production: adapter: <%= ENV.fetch('CABLE_ADAPTER', 'any_cable') %> ``` And set `"CABLE_ADAPTER": "redis"` (or any other built-in adapter, e.g. `"CABLE_ADAPTER": "async"`) in your `app.json`. ## Choosing the right formation How do to choose the right dyno type and the number of AnyCable dynos? Let's consider the full mode (with gRPC server). Since we run both RPC and WebSocket servers within the same dyno, we need to think about the resources usage carefully. The following formula could be used to estimate the necessary formation configuration: $$ N = \frac{\mu\frac{C}{1000}}{\phi(D - R)} $$ $\mu$ — MiB required to serve 1k connections by AnyCable (currently, it equals to 50MiB for most use-cases) $\phi$ — fill factor ($0 \le \phi \le 1$): what portion of all the available RAM we want to use (to leave a room for load spikes) $C$ — the expected number of simultaneous connections at peak times $D$ — RAM available for the dyno type (512MiB for 1X and 1GiB for 2x) $R$ — the size of the RPC (Rails) application. For a hypothetical app with $R = 350$, $C = 6000$ and $\phi = 0.8$: $$ N = \frac{50\frac{6000}{1000}}{0.8(512 - 350)} = \frac{300}{130} = 2.31 $$ Thus, the theoretical number for required 1X dynos is 3. For 2X dynos it’s just 1 (the formula above gives 0.56). We recommend to analyze the application size and try to reduce it (e.g., drop unused gems, disable parts of the application for the AnyCable process) in order to leave more RAM for WebSocket connections. For HTTP RPC mode, the formula is the same, but with $R=0$. ### Preboot and load balancing The formula above doesn’t take into account load balancing with [Preboot](https://devcenter.heroku.com/articles/preboot), which could result in a non-uniform distribution of the connections across the dynos. We noticed the difference up to 2x-3x between the number of connections after deployment with Preboot. From the [Preboot docs](https://devcenter.heroku.com/articles/preboot#preboot-in-manual-or-automatic-dyno-restarts): > The new dynos will start receiving requests as soon as it binds to its assigned port. At this point, both the old and new dynos are receiving requests. Thus, the lag between new dynos startup causes some dynos to receive new connections before others. If it’s possible, it’s better to perform deployments during off-peak hours to minimize the unbalancing effect of Preboot and avoid having one dyno serving much more connections than others. ### Open files limit One thing you should also take into account is OS-level limits. One such limit is the open files limit (the total number of allowed file descriptors, including sockets). For 1X/2X dynos this limit is 10k. That means that the max number of connections is ~9500 (we need some room for RPC connections, logs, DB connections, etc). **NOTE:** It’s impossible to change system limits on Heroku. Thus, the max practical number of connections per dyno is 9k. ## Integration ### Datadog 1. Install [Datadog agent to Heroku](https://docs.datadoghq.com/agent/basic_agent_usage/heroku) 2. Add environment variable for AnyCable to send metrics via StatsD to Datadog ```sh heroku config:add ANYCABLE_STATSD_HOST=localhost:8125 ``` In case, you've changed default port of Datadog agent set your port. 3. Restart the application 4. Open `https://DATADOG_SITE/metric/explorer`. `DATADOG_SITE` is the domain where your Datadog account is registered. [Read more](https://docs.datadoghq.com/getting_started/site/) 5. Type any `anycable_go.*` metric name ## Links * [Demo application](https://github.com/anycable/anycable_rails_demo/pull/4) * [Deployed application](http://demo.anycable.io/) [heroku.rake]: https://github.com/anycable/anycable_rails_demo/blob/demo/heroku/lib/tasks/heroku.rake --- --- url: /pro/install.md --- # Install AnyCable Pro AnyCable Pro is distributed in two forms: a Docker image and pre-built binaries. **NOTE:** All distribution methods, currently, relies on GitHub **personal access tokens**. We can either grant an access to the packages/projects to your users or generate a token for you. You MUST enable the following permissions: `read:packages` to download Docker images and/or `repo` (full access) to download binary releases. **IMPORTANT**: Make sure you accepted the invitation to the AnyCable releases repository (if you haven't received it, please contact us). ## Docker We use [GitHub Container Registry][ghcr] to host images. See the [official documentation][ghcr-auth] on how to authenticate Docker to pull images from GHCR. Once authenticated, you can pull images using the following identifier: `ghcr.io/anycable/anycable-go-pro`. For example: ```yml # docker-compose.yml services: ws: image: ghcr.io/anycable/anycable-go-pro:1.5 ports: - '8080:8080' environment: ANYCABLE_HOST: "0.0.0.0" ``` ## Pre-built binaries We use a dedicated GitHub repo to host pre-built binaries via GitHub Releases: [github.com/anycable/anycable-go-pro-releases][releases-repo]. We recommend using [`fetch`][fetch] to download releases via command line: ```sh fetch --repo=https://github.com/anycable/anycable-go-pro-releases --tag="v1.4.0" --release-asset="anycable-go-linux-amd64" --github-oauth-token="" /tmp ``` ## Heroku ### Using buildpacks Our [heroku buildpack][buildpack] supports downloading binaries from the private GitHub releases repo. You need to provide the following configuration parameters: * `HEROKU_ANYCABLE_GO_REPO=https://github.com/anycable/anycable-go-pro-releases` * `HEROKU_ANYCABLE_GO_GITHUB_TOKEN=` Currently, you also need to specify the version as well: `HEROKU_ANYCABLE_GO_VERSION=1.3.0`. Make sure you're not using cached `anycable-go` binary by purging the Heroku cache: `heroku builds:cache:purge -a `. See [documentation](https://help.heroku.com/18PI5RSY/how-do-i-clear-the-build-cache) for more details. ### Using Docker images You can use Heroku *Container Registry and Runtime* feature to deploy AnyCable-Go as a standalone service (i.e., when using [RPC-less setup with Hotwire](../guides/hotwire.md) or [HTTP RPC](../ruby/http_rpc.md)). The basic steps are: pull an AnyCable-Go PRO image from our private registry, push it to your Heroku registry and deploy. See [the official documentation](https://devcenter.heroku.com/articles/container-registry-and-runtime). ## AnyCable Thruster Pro We also ship Pro versions of [AnyCable Thruster][thruster] binaries via the same [releases repo][releases-repo]. You can download them using `fetch` as follows: ```sh fetch --repo=https://github.com/anycable/anycable-go-pro-releases --tag="v1.6.5" --release-asset="anycable-thruster-linux-amd64" --github-oauth-token="" /tmp ``` Then, when running the `thrust` command (provided by the `anycable-thruster` gem), specify the path to the downloaded binary as `ANYCABLE_THRUSTER_BIN_PATH=path/to/anycable-thruster`. ### Installing AnyCable Thruster Pro on Heroku Our buildpack supports downloading custom binaries. To download `anycable-thruster` binaries, set the following environment vars: ``` HEROKU_ANYCABLE_GO_BINARY_NAME=anycable-thruster HEROKU_ANYCABLE_GO_SKIP_VERSION_CHECK=1 // skip the default version check on installation, since Thruster has no -v flag ``` [ghcr]: https://ghcr.io [ghcr-auth]: https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry#authenticating-to-the-container-registry [releases-repo]: https://github.com/anycable/anycable-go-pro-releases/ [fetch]: https://github.com/gruntwork-io/fetch [buildpack]: https://github.com/anycable/heroku-anycable-go [thruster]: https://github.com/anycable/thruster --- --- url: /anycable-go/jwt_identification.md --- # JWT authentication AnyCable provides support for [JWT][jwt]-based authentication and identification. We use the term "identification", because you can also pass a properly structured information as a part of the token to not only authentication the connection but also set up *identifiers* (in terms of Action Cable). This approach brings the following benefits: * **Performance**. No RPC call is required during the connection initiation, since we already have identification information. Thus, less load on the RPC server, much faster connection time (at least, 2x faster). * **Usability**. Universal way of dealing with credentials (no need to deal with cookies for web and whatever else for mobile apps). * **Security**. CSRF-safe by design. Configurable life time for tokens makes it easier to keep access under control. ## Usage > See the [demo](https://github.com/anycable/anycable_rails_demo/pull/23) of using JWT identification in a Rails app with [AnyCable JS client library][anycable-client]. **NOTE**: Currently, we only support the HMAC signing algorithms. By default, the `--secret` configuration parameter is used as a JWT secret key. If you want to use a custom key for JWT, you can specify it via the `--jwt_secret` (`ANYCABLE_JWT_SECRET`) parameter. Other configuration options are: * (*Optional*) **--jwt\_param** (`ANYCABLE_ID_PARAM`, default: "jid"): the name of a query string param or an HTTP header, which carries a token. The header name is prefixed with `X-`. * (*Optional*) **--enforce\_jwt** (`ANYCABLE_ENFORCE_JWT`, default: false): whether to require all connection requests to contain a token. Connections without a token would be rejected right away. If not set, the servers fallbacks to the RPC call (if RPC is configured) or would be accepted if authentication is disabled (`--noauth`). A client must provide an identification token either via a query param or via an HTTP header (if possible). For example: ```js import { createCable } from '@anycable/web' let cable = createCable('ws://cable.example.com/cable?jid=[JWT_TOKEN]') ``` The token MUST include the `ext` claim with the JSON-encoded connection identifiers. WebSocket clients may also provide tokens using *sub-protocols*. For that, specify the `anycable-token.` sub-protocol in addition to the actual protocol (e.g., `actioncable-v1-json`): ```js const ws = new WebSocket( 'wss://cable.example.com/cable', ['actioncable-v1-json', 'anycable-token.'] ); ``` When using AnyCable JS client, all you need is to specify the authentication strategy for the cable instance: ```js import { createCable } from '@anycable/web' export default createCable( 'ws://cable.example.com/cable', { auth: {token: "secret-value"}, websocketAuthStrategy: 'sub-protocol' } }); ``` ## Generating tokens ### Rails/Ruby When using AnyCable Ruby/Rails SDK, you can generate tokens as follows: ```ruby token = AnyCable::JWT.encode({user: current_user}) # Setting TTL is also possible token = AnyCable::JWT.encode({user: current_user}, expires_at: 10.minutes.from_now) ``` If you don't want to use our SDKs (why?), here is how you can generate tokens yourself: ```ruby require "jwt" require "json" ENCRYPTION_KEY = "some-sercret-key" # !!! Expiration is the responsibility of the token issuer exp = Time.now.to_i + 30 # Provides the serialized values for identifiers (`identified_by` in Action Cable) identifiers = {user_id: 42} # JWT payload payload = {ext: identifiers.to_json, exp: exp} puts JWT.encode payload, ENCRYPTION_KEY, "HS256" ``` ## JavaScript/TypeScript You can use [AnyCable server-side JS SDK](https://github.com/anycable/anycable-serverless-js) to generate tokens as follows: ```js import { identificator } from "@anycable/serverless-js"; const jwtSecret = "very-secret"; const jwtTTL = "1h"; export const identifier = identificator(jwtSecret, jwtTTL); // Then, somewhere in your code, generate a token and provide it to the client const userId = authenticatedUser.id; const token = await identifier.generateToken({ userId }); ``` ## PHP You can use the following snippet to generate tokens in PHP: ```php use Firebase\JWT\JWT; $identifiers = ['user_id' => 42]; $payload = ['ext' => json_encode($identifiers), 'exp' => time() + 300]; $jwt = JWT::encode($payload, $ENCRYPTION_KEY, 'HS256'); ``` ## Python Here is an example Python code to generate AnyCable tokens: ```python import json import jwt import time identifiers = {'user_id': 42} payload = {'ext': json.dumps(identifiers), 'exp': int(time.time()) + 300} jwt.encode(payload, ENCRYPTION_KEY, algorithm='HS256') ``` ### Handling expired tokens > 🎥 Check out this [AnyCasts episode](https://anycable.io/blog/anycasts-using-anycable-client/) to learn more about the expiration problem and how to solve it using [anycable-client](https://github.com/anycable/anycable-client). Whenever a server encounters a token that has expired, it rejects the connection and send the `disconnect` message with `reason: "token_expired"`. It's a client responsibility to handle this situation and refresh the token. See, for example, how [anycable-client handles this](https://github.com/anycable/anycable-client#refreshing-authentication-tokens). [jwt]: https://jwt.io [anycable-client]: https://github.com/anycable/anycable-client --- --- url: /deployment/kamal.md --- # Kamal [Kamal](https://kamal-deploy.org/) is a deployment tool from Basecamp that makes it easy to deploy Rails applications with Docker. This guide covers different approaches to deploying AnyCable web server and RPC servers (if required) with Kamal 2. **NOTE:** This guide assumes that the primary application framework is Ruby on Rails. However, most ideas could be applied to other frameworks and stacks. There is a number of ways you can run AnyCable with Kamal depending on your needs. Here is the table describing recommended setups based on such factors as expected load, the number of servers (machines), whether you need an RPC server or not: | Setup | Load | Servers | RPC | Recommended Approach | |-------|------|---------|--------------|---------------------| | Small | Low | 1 | No | Anycable Thruster | | Small | Low | 1 | Yes | AnyCable Thruster + Embedded gRPC or HTTP RPC | | Small | Medium | 1 | Yes | AnyCable Thruster + RPC role | | Medium | Medium | 1-2 | No | AnyCable accessory (single server) | | Medium | Medium | 1-2 | Yes | AnyCable accessory (single server) + RPC role (each server) | | Large | High | 3+ | No | AnyCable accessory (many servers) + Redis/NATS | | Large | High | 3+ | Yes | AnyCable accessory (many servers) + Redis/NATS + RPC role (each server) | ## Deploying AnyCable server ### Using Thruster The simplest way to deploy AnyCable with Kamal is using the [anycable-thruster](https://github.com/anycable/thruster) gem, which allows you to run AnyCable alongside your Rails web server in a single container. Rails' default Dockerfile already uses Thruster as its proxy server, so no additional changes required. With this setup, we recommend getting started with an [embedded gRPC server](/rails/getting_started?id=embedded-grpc-server) or [HTTP RPC](https://docs.anycable.io/ruby/http_rpc), so you can keep the Kamal configuration untouched. ### Deploying AnyCable as an Accessory For applications that need more control or better resource isolation, you can deploy AnyCable server separately as a Kamal *accessory*. With this approach, running `kamal setup` should be sufficient to make AnyCable server up and running. One particular benefit AnyCable benefit this approach brings is **zero-disconnect deployments** (WebSocket connections are kept between application restarts). However, there is a trade-off of having to use a separate domain name for AnyCable server (e.g., `ws.myapp.whatever`). That might require taking additional care of authentication (e.g., cookie-sharing). We recommend using AnyCable's built-in [JWT authentication](/anycable-go/jwt_identification) to not worry about that. > See this [demo PR](https://github.com/anycable/anycable_rails_demo/pull/37) for a complete configuration example. Here is a `config/deploy.yml` example with the AnyCable accessory: ```yaml # ... accessories: # ... anycable-go: image: anycable/anycable-go:1.6 host: 192.168.0.1 proxy: host: ws.demo.anycable.io ssl: true app_port: 8080 healthcheck: path: /health env: clear: ANYCABLE_HOST: "0.0.0.0" ANYCABLE_PORT: 8080 ANYCABLE_BROADCAST_ADAPTER: http ANYCABLE_HTTP_BROADCAST_PORT: 8080 secret: - ANYCABLE_SECRET ``` The important bits are: * `proxy` configuration for `anycable` accessory; it's required to server incoming traffic via Kamal * we configure AnyCable to receive broadcast HTTP requests on the same port served by Kamal Proxy to avoid publishing any additional ports; specifying `ANYCABLE_SECRET` is required to ensure your HTTP broadcasting endpoint is secured. The example above uses HTTP broadcasting. If you want to use Redis, it will look as follows: ```yaml # Name of your service defines accessory service names service: anycable_rails_demo # ... accessories: # ... redis: image: redis:7.0 host: 192.168.0.1 directories: - data:/data anycable-go: image: anycable/anycable-go:1.6 host: 192.168.0.1 proxy: host: ws.demo.anycable.io ssl: true app_port: 8080 healthcheck: path: /health env: clear: ANYCABLE_HOST: "0.0.0.0" ANYCABLE_PORT: 8080 ANYCABLE_REDIS_URL: "redis://anycable_rails_demo-redis:6379/0" ``` Note that if you want to run AnyCable servers on multiple hosts and use Redis for pub/sub, you must provide the same static Redis address for all AnyCable accessories (and better protect it at least via a password): ```yaml accessories: # ... redis: host: <%= ENV.fetch('REDIS_HOST') %> image: redis:8.0-alpine port: "6379:6379" cmd: redis-server --requirepass <%= ENV.fetch("REDIS_PASSWORD") %> volumes: - redisdata:/data anycable-go: image: anycable/anycable-go:1.6 host: <%= ENV.fetch("ANYCABLE_HOST") %> proxy: # .. env: clear: ANYCABLE_HOST: "0.0.0.0" ANYCABLE_PORT: 8080 ANYCABLE_REDIS_URL: "redis://:<%= ENV.fetch("REDIS_PASSWORD") %>@<%= ENV.fetch("REDIS_HOST") %>:6379/0" ``` The example above assumes that we store various configuration parameters such as IP addresses in the `.env` file (so, the actual configuration is *parameterized*). See the full example [here](https://github.com/anycable/anycable_rails_demo/pull/39). #### Using Embedded NATS AnyCable can run with an embedded NATS server, eliminating the need for Redis: ```yaml accessories: # ... anycable-go: host: <%= ENV.fetch("ANYCABLE_HOST") %> image: anycable/anycable-go:1.6.2-alpine env: clear: <<: *default_env ANYCABLE_HOST: "0.0.0.0" ANYCABLE_PORT: "8080" ANYCABLE_EMBED_NATS: "true" ANYCABLE_PUBSUB: nats ANYCABLE_BROADCAST_ADAPTER: "http" ANYCABLE_HTTP_BROADCAST_PORT: 8080 ANYCABLE_ENATS_ADDR: "nats://0.0.0.0:4242" ANYCABLE_ENATS_CLUSTER: "nats://0.0.0.0:4243" secret: - ANYCABLE_SECRET options: publish: - "4242:4242" - "4243:4243" proxy: host: <%= ENV.fetch("WS_PROXY_HOST") %> ssl: true app_port: 8080 healthcheck: path: /health interval: 1 timeout: 5 ``` The complete example of deploying AnyCable with embedded NATS via Kamal can be found in [this PR](https://github.com/anycable/anycasts_demo/pull/19). ## Deploying gRPC servers AnyCable RPC server using gRPC transport should be deployed as separate *server role* (not an accessory), since it serves your application. Thus, you must add to the list of servers as follows: ```yaml service: anycable_rails_demo servers: web: - 192.168.0.1 anycable-rpc: hosts: - 192.168.0.1 cmd: bundle exec anycable proxy: false options: network-alias: anycable_rails_demo-rpc accessories: # ... anycable-go: # ... env: clear: ANYCABLE_HOST: "0.0.0.0" ANYCABLE_PORT: 8080 ANYCABLE_RPC_HOST: anycable_rails_demo-rpc:50051 secret: - ANYCABLE_SECRET ``` The important bits are: * `proxy: false` is required to skip Kamal Proxy (it doesn't support gRPC) * `network-alias: anycable_rails_demo-rpc` allows us to use an fixed Docker service name to access the RPC server container from the accessory. ### Scaling gRPC servers horizontally > See this [demo PR](https://github.com/anycable/anycable_rails_demo/pull/39) for a complete configuration example. AnyCable-Go 1.6.2+ supports the `grpc-list://` scheme to connect to multiple RPC endpoints. This way, you can spread RPC traffic across machines: ```yaml # ... servers: web: # ... rpc: hosts: <%= ENV.fetch("RPC_HOSTS").split(",") %> cmd: bundle exec anycable env: clear: <<: *default_env ANYCABLE_RPC_HOST: "0.0.0.0:50051" options: publish: - "50051:50051" proxy: false accessories: # ... anycable-go: host: <%= ENV.fetch("WS_HOSTS") %> image: anycable/anycable-go:1.6.2-alpine env: clear: <<: *default_env ANYCABLE_HOST: "0.0.0.0" ANYCABLE_PORT: "8080" # Using a fixed list of RPC addresses https://docs.anycable.io/deployment/load_balancing?id=using-a-fixed-list-of-rpc-addresses ANYCABLE_RPC_HOST: "grpc-list://<%= ENV.fetch("RPC_HOSTS").split(",").map { "#{_1}:50051" }.join(",") %>" proxy: # ... ``` **IMPORTANT**: The setup above expose the gRPC server to the public (so it's reachable from other machines). We recommend securing access either by setting up firewall rules / virtual network within the cluster or using TLS with a private certificate for gRPC (see [configuration docs](https://docs.anycable.io/anycable-go/configuration?id=tls)). The setup above has one caveat: since we publish RPC port (`50051`) to the host system, default Kamal rolling updates would fail with the `port is already in use` after Kamal would have tried to launch a new copy of the `rpc` container. To avoid that, we can use the `pre-app-boot` hook to stop RPC containers (it's okay to have a short downtime here, AnyCable server would take care of recovering). This is an example `.kamal/hooks/pre-app-boot` code: ```sh #!/bin/bash # This script is a Kamal pre-app-boot hook. # It uses 'kamal app stop' to stop old containers of the 'rpc' role # This helps prevent "port already allocated" errors for services with fixed published ports. # Exit immediately if any command fails. set -e KAMAL_CMD="kamal" # Or "./bin/kamal" or "/path/to/kamal_executable" # The role whose containers need to be stopped. # This must match the role name in your deploy.yml ROLES_TO_STOP="rpc" KAMAL_ARGS=(--roles "$ROLES_TO_STOP") if "$KAMAL_CMD" app stop --roles "${ROLES_TO_STOP}"; then echo "'kamal app stop --roles $ROLES_TO_STOP' completed successfully." else exit_code=$? echo "Error: '$KAMAL_CMD app stop --roles $ROLES_TO_STOP' failed with exit code $exit_code." >&2 exit $exit_code fi ``` --- --- url: /deployment/kubernetes.md --- # Kubernetes Deployment > Check out our [Kuby plugin][kuby-anycable] and read the [Kubing Rails: stressless Kubernetes deployments with Kuby](https://evilmartians.com/chronicles/kubing-rails-stressless-kubernetes-deployments-with-kuby) blog post. ## AnyCable-Go AnyCable-Go can be easily deployed to your Kubernetes cluster using Helm and [our official Helm chart][anycable-helm]. * Add it as a dependency to your main application: ```yaml # Chart.yaml for Helm 3 dependencies: - name: anycable-go version: 0.2.4 repository: https://helm.anycable.io/ ``` ```` Check the latest Helm chart version at [github.com/anycable/anycable-helm/releases](https://github.com/anycable/anycable-helm/releases). And execute ```sh helm dependencies update ```` * And then configure it in your application values within `anycable-go` section: ```yaml # values.yaml # Configuration for the external Helm chart "anycable/anycable-go" anycable-go: env: # Assuming that Ruby RPC is available in K8s in the same namespace as anycable-rpc service (see next chapter) anycableRpcHost: anycable-rpc:50051 ingress: enable: true path: /cable # values/production.yaml anycable-go: env: # Assuming that Redis is available in K8s in the same namespace as redis-anycable service anycableRedisUrl: redis://:CHANGE-THE-PASSWORD@redis-anycable:6379/0 ingress: acme: # if you're using Let's Encrypt hosts: - your-app.com ``` Read the [chart’s README][anycable-helm] for more info. ## AnyCable-Go Pro Installation process for Pro version is almost identical to the non-Pro one. There are the following changes: * Use Helm chart version `>= 0.5.1`. * The `image` section of configuration values MUST contain `pullSecrets` section where you place credentials for private docker repository access: ```yaml # values.yaml anycable-go: image: repository: ghcr.io/anycable/anycable-go-pro tag: edge pullSecrets: enabled: true registry: "ghcr.io" username: "username" password: "github-token-here" ``` You can get a list of available `anycable-go-pro` image versions using the following command: ```sh curl -X GET -H "Authorization: Bearer $(echo "github-token-here" | base64)" https://ghcr.io/v2/anycable/anycable-go-pro/tags/list ``` Read the [chart’s README][anycable-helm] for more info. ## RPC server To run Ruby counterpart of AnyCable which will handle connection authentication and execute your business logic we need to create a separate deployment and a corresponding service for it. * [**Deployment**](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/) that will spin up a required number of pods and handle rolling restarts on deploys ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: anycable-rpc labels: component: anycable-rpc spec: replicas: 1 strategy: type: RollingUpdate rollingUpdate: maxUnavailable: 1 maxSurge: 0 selector: matchLabels: component: anycable-rpc template: metadata: labels: component: anycable-rpc spec: containers: - name: anycable-rpc image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" imagePullPolicy: IfNotPresent command: - bundle - exec - anycable # you should define these parameters in the values.yml file, we give them here directly for readability - --rpc-host=0.0.0.0:50051 env: - name: ANYCABLE_REDIS_URL valueFrom: secretKeyRef: name: "anycable-go-secrets" key: anycableRedisUrl # And all your application ENV like DATABASE_URL etc ``` * [**Service**](https://kubernetes.io/docs/concepts/services-networking/service/) to connect anycable-go with RPC server. ```yaml apiVersion: v1 kind: Service metadata: name: anycable-rpc labels: component: anycable-rpc spec: selector: component: anycable-rpc type: ClusterIP # Uncomment this line if you're using the DNS-based load balancing # clusterIP: None ports: # you should define these parameters in the values.yml file, we give them here directly for readability - port: 50051 targetPort: 50051 protocol: TCP ``` * (Optional) [**network policy**](https://kubernetes.io/docs/concepts/services-networking/network-policies/) will restrict access to pods running RPC service to only those that run AnyCable-Go daemon in the same namespace. ```yaml kind: NetworkPolicy apiVersion: networking.k8s.io/v1 metadata: name: anycable-go-and-rpc-connectivity spec: podSelector: matchLabels: component: anycable-rpc ingress: - from: - podSelector: matchLabels: component: anycable-go ``` See detailed explanation in the docs and in this example: [Kubernetes network policy recipes: deny traffic from other namespaces](https://github.com/ahmetb/kubernetes-network-policy-recipes/blob/60f5b12f274472901ce79463ce0ba3a8f98b9a48/04-deny-traffic-from-other-namespaces.md) [anycable-helm]: https://github.com/anycable/anycable-helm/ "Helm charts for installing any cables into a Kubernetes cluster" [kuby-anycable]: https://github.com/anycable/kuby-anycable --- --- url: /deployment/load_balancing.md --- # Load balancing ## RPC load balancing You can use load balancers to scale your application and/or perform zero-disconnect deployments (by doing a rolling update of RPC servers without restarting WebSocket servers). ### Using Linkerd Check out this blog post: [Scaling Rails web sockets in Kubernetes with AnyCable](https://blog.anycable.io/p/scaling-rails-websockets-in-kubernetes). ### Using Envoy [Envoy](https://envoyproxy.io) is a modern proxy server which supports HTTP2 and gRPC. See [the example configuration](https://github.com/anycable/anycable-go/tree/master/etc/envoy) in the `anycable-go` repo. ### Using NGINX You can use NGINX [gRPC module](http://nginx.org/en/docs/http/ngx_http_grpc_module.html) to distribute traffic across multiple RPC servers. The minimalist configuration looks like this (credits goes to [avlazarov](https://gist.github.com/avlazarov/9503c23d81c75f760e14b30e38847356#file-grpc-confe)): ```conf upstream grpcservers { server 0.0.0.0:50051; server 0.0.0.0:50052; } server { listen 50050 http2; server_name localhost; access_log /var/log/nginx/grpc_log.json; error_log /var/log/nginx/grpc_error_log.json debug; location / { grpc_pass grpc://grpcservers; } } ``` ### Client-side load balancing gRPC clients (more precisely, [grpc-go](https://github.com/grpc/grpc-go) used by `anycable-go`) provide client-level load balancing via DNS resolving. If the provided hostname resolves to multiple A records, a client connect to all of them and use round-robin strategy to distribute the requests. To activate this mechanism, you MUST provide use the following schema to build an URI: `dns://[authority]/host[:port]`. For example, when using Docker, you can rely on its internal DNS server and omit the `authority` part altogether: `ANYCABLE_RPC_HOST=dns:///rpc:50051` (**three** slashes!). See the [docs](https://github.com/grpc/grpc/blob/master/doc/naming.md). Since gRPC clients performs the DNS resolution only during the connection initialization, newly added servers (in case of auto-scaling) are not picked up. To resolve this issue, you can configure a max connection lifetime at the server side, so, connections are recreated periodically (that also triggers re-resolution). You can control gRPC connection lifetimes via the `rpc_max_connection_age` configuration option for AnyCable RPC server (could be also configured via the `ANYCABLE_RPC_MAX_CONNECTION_AGE` env variable). It's set to 300 (seconds, thus, 5 minutes) by default, so you're likely don't want to change it. You can also monitor the current number of gRPC connections by looking at the AnyCable-Go's `grpc_active_conn_num` metrics value. ### Using a fixed list of RPC addresses You can also provide a static list of gRPC servers to spread out the calls using the special `grpc-list://` scheme: ```sh $ anycable-go --rpc_host=grpc-list://grpc-list://rpc1.example.com:50051,rpc2.example.com:50051 ... RPC controller initialized: grpc-list://grpc-list://rpc1.example.com:50051,rpc2.example.com:50051 (concurrency: 28, impl: grpc, enable_tls: false, proto_versions: v1, proxy_headers: cookie, proxy_cookies: ) context=rpc ``` This is useful when you run AnyCable in environments without service discovery and a known list of server addresses (e.g., when using Kamal). ## WebSocket load balancing There is nothing specific in load balancing AnyCable WebSocket server comparing to other WebSocket applications. See, for example, [NGINX documentation](https://www.nginx.com/blog/websocket-nginx/). **NOTE:** We recommend to use a *least connected* strategy for WebSockets to have more uniform clients distribution (see, for example, [NGINX](http://nginx.org/en/docs/http/load_balancing.html#nginx_load_balancing_with_least_connected)). --- --- url: /deployment/load_testing.md --- # Load testing AnyCable > Read the ["Real-time stress: AnyCable, k6, WebSockets, and Yabeda"](https://evilmartians.com/chronicles/real-time-stress-anycable-k6-websockets-and-yabeda) post to learn more about WebSockets load testing. We maintain a [k6][] extension to load-test (or stress-test) Action Cable and AnyCable compatible WebSocket servers—[xk6-cable][]. See the project's Readme for further instructions. [k6]: https://k6.io [xk6-cable]: https://github.com/anycable/xk6-cable --- --- url: /ruby/logging.md --- # Logging By default, AnyCable Ruby logs to STDOUT with `INFO` level but can be easily configured (see [Configuration](configuration.md#parameters)), for example: ```sh $ bundle exec anycable --log-file=logs/anycable.log --log-level debug # or $ ANYCABLE_LOG_FILE=logs/anycable.log ANYCABLE_LOG_LEVEL=debug bundle exec anycable ``` You can also specify your own logger instance for full control: ```ruby # AnyCable invokes this code before initializing the configuration AnyCable.logger = MyLogger.new ``` **IMPORTANT:** When using with Rails, AnyCable automatically sets its logger to `Rails.logger`, so AnyCable-specific logging options are no-op. ## gRPC logging AnyCable does not log any GRPC internal events by default. You can turn GRPC logger on by setting `log_grpc` parameter to true: ```sh $ bundle exec anycable --log-grpc # or $ ANYCABLE_LOG_GRPC=t bundle exec anycable ``` ## Debug mode You can turn on verbose logging (with gRPC logging turned on and log level set to `"debug"`) by using a shortcut parameter–`debug`: ```sh $ bundle exec anycable --debug # or $ ANYCABLE_DEBUG=1 bundle exec anycable ``` ## Log tracing When using with Rails, AnyCable adds a *session ID* tag (`sid`) to each log entry produced during the RPC message handling. You can use it to trace the request's pathway through the whole Load Balancer -> WS Server -> RPC stack. Logs example: ```sh [AnyCable sid=FQQS_IltswlTJK60ncf9Cm] RPC Command: > [AnyCable sid=FQQS_IltswlTJK60ncf9Cm] User Load (0.6ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1 ``` --- --- url: /anycable-go/long_polling.md --- # Long polling support AnyCable Pro supports alternative transport protocols, such as long polling. Even though WebSockets are widely supported, they still can be blocked by corporate firewalls and proxies. Long polling is a simplest alternative for such cases, especially if you want to support legacy browsers or clients without official client SDKs. **IMPORTANT:** Long-polling sessions are not distributed by design (at least for now). For AnyCable-Go clusters, **sticky sessions must be used** for polling connections. ## Usage First, you need to enable long polling support in `anycable-go`: ```sh $ anycable-go --poll INFO 2023-06-29T03:44:22.460Z context=main Starting AnyCable 1.4.0-pro-72d0c60 (with mruby 1.2.0 (2015-11-17)) (pid: 34235, open file limit: 122880, gomaxprocs: 8, netpoll: true) ... INFO 2023-06-29T03:44:22.462Z context=main Handle long polling requests at http://0.0.0.0:8080/lp (poll_interval: 15, keepalive_timeout: 5) ``` Now you can use the `/lp` endpoint to establish a long polling connection. Let's see how we can do that at the client side. ### Using with AnyCable JS SDK [AnyCable JS client][anycable-client] provides long polling support by the means of the [@anycable/long-polling][] plugin. You can use it as follows: ```js import { createCable } from '@anycable/web' import { LongPollingTransport } from '@anycable/long-polling' // Create a transport object and pass the URL to the AnyCable server's long polling endpoint const lp = new LongPollingTransport('http://my.anycable.host/lp') // Pass the transport to the createCable or createConsumer function via the `fallbacks` option export default createCable({fallbacks: [lp]}) ``` That's it! Now your client will fallback to long polling if WebSocket connection can't be established. See full documentation [here][@anycable/long-polling] ### Using with a custom client You can use any HTTP client to communicate with AnyCable via a long polling endpoint. #### Establishing a connection To establish a connection, client MUST send a `POST` request to the `/lp` endpoint. The authentication is performed based on the request data (cookies, headers, etc.), i.e., similar to WebSocket connections. Client MAY send commands along with the initial request. The commands are processed by server only if authentication is successful. See below for the commands format. If authentication is successful, the server MUST respond with a 20x status code and a unique poll session identifier in the `X-Anycable-Poll-ID` response header. The response body MAY include messages for the client. If authentication is unsuccessful, the server MUST respond with a 401 status code. The response body MAY include messages for the client. All other status codes are considered as errors. #### Polling Client MUST send a `POST` request to the `/lp` endpoint with the `X-Anycable-Poll-ID` header set to the poll session identifier received during the initial connection to receive messages from the server. Client MAY send commands along with the poll request. #### Stale session If client doesn't send a poll request for a certain period of time (see `--poll_keepalive_timeout` option below), the server MUST close the poll session and respond with a 401 status code to the next polling request with the session's ID. Server MAY send a `session_expired` disconnect message to the client. #### Communication format Both client and server MUST use JSONL (JSON Lines) format for communication. JSONL is a sequence of JSON objects separated by newlines (`\n`). The last object MUST be followed by a newline. For example, client MAY send the following commands along the initial request: ```json {"command":"subscribe", "identifier":"chat_1"} {"command":"subscribe","identifier":"presence_1"} ``` Server MAY respond with the following messages in the response body: ```json {"type":"welcome"} {"type":"confirm_subscription","identifier":"chat_1"} {"type":"confirm_subscription","identifier":"presence_1"} ``` ## Configuration The following options are available: * `--poll_path` (`ANYCABLE_POLL_PATH`) (default: `/lp`): a long polling endpoint path. * `--poll_interval` (`ANYCABLE_POLL_INTERVAL`) (default: 15): polling interval in seconds. * `--poll_flush_interval` (`ANYCABLE_POLL_FLUSH_INTERVAL`) (default: 500): defines for how long to buffer server-to-client messages before flushing them to the client (in milliseconds). * `--poll_max_request_size` (`ANYCABLE_POLL_MAX_REQEUEST_SIZE`) (default: 64kB): maximum acceptable request body size (in bytes). * `--poll_keepalive_timeout` (`ANYCABLE_POLL_KEEPALIVE_TIMEOUT`) (default: 5): defines for how long to keep a poll session alive between requests (in seconds). ## CORS Server responds with the `Access-Control-Allow-Origin` header set to the value of the `--allowed_origins` option (default: `*`). If you want to restrict the list of allowed origins, you can pass a comma-separated list of domains to the option. See [documentation](./configuration.md). ## Instrumentation When long polling enabled, the following metrics are available: * `long_poll_clients_num`: number of active long polling clients. * `long_poll_stale_requests_total`: number of stale requests (i.e., requests that were sent after the poll session was closed). [anycable-client]: https://github.com/anycable/anycable-client [@anycable/long-polling]: https://github.com/anycable/anycable-client/tree/master/packages/long-polling --- --- url: /misc/Readme.md --- # Misc * [Action Cable Protocol](action_cable_protocol.md) * [Protobuf Definitions](rpc_proto.md) * [How to write AnyCable-compatible server](how_to_anycable_server.md) --- --- url: /anycable-go/ocpp.md --- # OCPP support (*alpha*) [OCPP][] (Open Charge Point Protocol) is a communication protocol for electric vehicle charging stations. It defines a WebSocket-based RPC communication protocol to manage station and receive status updates. AnyCable-Go Pro supports OCPP and allows you to *connect* your charging stations to Ruby or Rails applications and control everything using Action Cable at the backend. **NOTE:** Currently, AnyCable-Go Pro supports OCPP v1.6 only. Please, contact us if you need support for other versions. ## How it works * EV charging station connects to AnyCable-Go via WebSocket * The station sends a `BootNotification` request to initialize the connection * AnyCable transforms this request into several AnyCable RPC calls to match the Action Cable interface: 1. `Authenticate -> Connection#connect` to authenticate the station. 2. `Command{subscribe} -> OCCPChannel#subscribed` to initialize a channel entity to association with this station. 3. `Command{perform} -> OCCPChannel#boot_notification` to handle the `BootNotification` request. * Subsequent requests from the station are converted into `OCCPChannel` action calls (e.g., `Authorize -> OCCPChannel#authorize`, `StartTransaction -> OCCPChannel#start_transaction`). AnyCable also takes care of heartbeats and acknowledgment messages (unless you send them manually, see below). ## Usage To enable OCPP support, you need to specify the `--ocpp_path` flag (or `ANYCABLE_OCPP_PATH` environment variable) specify the prefix for OCPP connections: ```sh $ anycable-go --ocpp_path=/ocpp ... INFO 2023-03-28T19:06:58.725Z context=main Handle OCPP v1.6 WebSocket connections at http://localhost:8080/ocpp/{station_id} ... ``` AnyCable automatically adds the `/:station_id` part to the path. You can use it to identify the station in your application. ## Example Action Cable channel class Now, to manage EV connections at the Ruby side, you need to create a channel class. Here is an example: ```ruby class OCPPChannel < ApplicationCable::Channel def subscribed # You can subscribe the station to its personal stream to # send remote comamnds to it # params["sn"] contains the station's serial number # (meterSerialNumber from the BootNotification request) stream_for "ev/#{params["sn"]}" end def boot_notification(data) # Data contains the following fields: # - id - a unique message ID # - command - an original command name # - payload - a hash with the original request data id, payload = data.values_at("id", "payload") logger.info "BootNotification: #{payload}" # By default, if not ack sent, AnyCable sends the following: # [3, , {"status": "Accepted"}] # # For boot notification response, the "interval" is also added. end def status_notification(data) id, payload = data.values_at("id", "payload") logger.info "Status Notification: #{payload}" end def authorize(data) id, payload = data.values_at("id", "payload") logger.info "Authorize: idTag — #{payload["idTag"]}" # For some actions, you may want to send a custom response. transmit_ack(id:, idTagInfo: {status: "Accepted"}) end def start_transaction(data) id, payload = data.values_at("id", "payload") id_tag, connector_id = payload.values_at("idTag", "connectorId") logger.info "StartTransaction: idTag — #{id_tag}, connectorId — #{connector_id}" transmit_ack(id:, transactionId: rand(1000), idTagInfo: {status: "Accepted"}) end def stop_transaction(data) id, payload = data.values_at("id", "payload") id_tag, connector_id, transaction_id = payload.values_at("idTag", "connectorId", "transactionId") logger.info "StopTransaction: transcationId - #{transaction_id}, idTag — #{id_tag}" transmit_ack(id:, idTagInfo: {status: "Accepted"}) end # These are special methods to handle OCPP errors and acks def error(data) id, code, message, details = data.values_at("id", "code", "message", "payload") logger.error "Error from EV: #{code} — #{message} (#{details})" end def ack(data) logger.info "ACK from EV: #{data["id"]} — #{data.dig("payload", "status")}" end private def transmit_ack(id:, **payload) # IMPORTANT: You must use "Ack" as the command for acks, # so AnyCable can correctly translate them into OCPP acks. transmit({command: :Ack, id:, payload:}) end end ``` ### Single-action variant It's possible to handle all OCCP commands with a single `#receive` method at the channel class. For that, you must configure `anycable-go` to not use granular actions for OCPP: ```sh anycable-go --ocpp_granular_actions=false # or ANYCABLE_OCPP_GRANULAR_ACTIONS=false anycable-go ``` In your channel class: ```ruby class OCPPChannel < ApplicationCable::Channel def subscribed stream_for "ev/#{params["sn"]}" end def receive(data) id, command, payload = data.values_at("id", "command", "payload") logger.info "[#{id}] #{command}: #{payload}" end end ``` ### Remote commands You can send remote commands to stations via Action Cable broadcasts: ```ruby OCCPChannel.broadcast_to( "ev/#{serial_number}", { command: "TriggerMessage", id: "", payload: { requestedMessage: "BootNotification" } } ) ``` [OCPP]: https://en.wikipedia.org/wiki/Open_Charge_Point_Protocol --- --- url: /anycable-go/os_tuning.md --- # OS Tuning ## Open files limit The most important thing you should take into account is to set a big enough open files limit. It defines how many file descriptors a process can keep open, and a socket is also a file descriptor. Thus, you cannot handle more connections than this limit (and event less, since the process uses a few file descriptors for its own purposes). AnyCable-Go prints the current open files limit on boot: ```sh $ anycable-go INFO 2022-06-07T19:30:33.059Z context=main Starting AnyCable v1.2.1 (with mruby 1.2.0 (2015-11-17)) (pid: 29333, open file limit: 524288, gomaxprocs: 8) ... ``` Alternatively, you can run `ulimit -n` for the user which runs `anycable-go` or check the running process limits by `cat /proc//limits`. Changing this limit depends on the OS and the way you deploy the server (e.g., for [systemd](../deployment/systemd.md) you can set a limit using `LimitNOFILE` directive). ### Heroku Heroku sets the open files limit to 10k, and it's not possible to change it. See more [here](../deployment/heroku.md). ### AWS ECS ECS has a default limit of 1024 open files. Make sure you configured [the `ulimit` setting](https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_Ulimit.html) in your task definition. For example: ```yml HardLimit: 1048576 Name: nofile SoftLimit: 1048576 ``` ## TCP keepalive WebSockets are implemented on top of the TCP protocol. Normally, closing a connection is happening via 4-step handshake. But what happens if there is no more network to send handshake packets? How TCP detects that connection was lost if there is no network? By using [*keepalive*](http://tldp.org/HOWTO/TCP-Keepalive-HOWTO/overview.html) feature. Keepalive works the following way: if no data has been transmitting via socket for X seconds, the server sends N probe packets every Y seconds, and only if all of these packets failed to deliver, the server closes the socket. Thus, the `CLOSE` event could happen in minutes or even hours (depending on the OS settings) after the network failure. The recommended configuration is the following (add it to `/etc/sysctl.conf`): ```sysctl net.ipv4.tcp_keepalive_intvl = 10 net.ipv4.tcp_keepalive_probes = 5 net.ipv4.tcp_keepalive_time = 300 ``` The configuration above will allow you to “catch” dead connection in ~6min. **NOTE**: Don’t forget to reload the configuration by running sudo `sysctl -p /etc/sysctl.conf`. ## Resources We recommend to check out these articles on the details of how to tune OS settings for *zillions* of connections: * [The Road to 2 Million Websocket Connections in Phoenix](https://phoenixframework.org/blog/the-road-to-2-million-websocket-connections) * [Benchmarking and Scaling WebSockets: Handling 60000 concurrent connections](http://kemalcr.com/blog/2016/11/13/benchmarking-and-scaling-websockets-handling-60000-concurrent-connections/) --- --- url: /anycable-go/presence.md --- # Presence tracking AnyCable comes with a built-in presence tracking support for your real-time applications. No need to write custom code and deal with storage mechanisms to know who's online in your channels. ## Overview AnyCable presence allows channel subscribers to share their presence information with other clients and track the changes in the channel's presence set. Presence data can be used to display a list of online users, track user activity, etc. ## Quick start Presence is a part of the [broker](./broker.md) component, so you must enable it either via the `broker` configuration preset or manually: ```sh $ anycable-go --presets=broker # or $ anycable-go --broker=memory ``` If you use AnyCable Pro, you can also use the Redis broker: ```sh anycable-go --broker=redis ``` **NOTE:** Redis broker is the only broker that works in the cluster mode. **Redis 7.4+** or **Valkey 9.0+** is required for Redis-backed presence to work. Now, you can use the presence API in your application. For example, using [AnyCable JS client](https://github.com/anycable/anycable-client): ```js import { createCable } from '@anycable/web' // or for non-web projects // import { createCable } from '@anycable/core' const cable = createCable({protocol: 'actioncable-v1-ext-json'}) const channel = cable.streamFrom('room/42'); // join the channel's presence set channel.presence.join(user.id, { name: user.name }) // get the current presence state const presence = await chatChannel.presence.info() // subscribe to presence events channel.on("presence", (event) => { const { type, info, id } = event if (type === "join") { console.log(`${info.name} joined the channel`) } else if (type === "leave") { console.log(`${id} left the channel`) } }) ``` ## Presence lifecycle Clients join the presence set explicitly by performing the `presence` command. The `join` event is sent to all subscribers (including the initiator) with the presence information, but only if the **presence ID** (provided by the client) hasn't been registered yet. Thus, multiple sessions with the same ID are treated as a single presence record. Clients may explicitly leave the presence set by performing the `leave` command or by unsubscribing from the channel. The `leave` event is sent to all subscribers only if no other sessions with the same ID are left in the presence set. When a client disconnects without explicitly leaving or unsubscribing the channel, it's present information stays in the set for a short period of time. That prevents the burst of `join` / `leave` events when the client reconnects frequently. ## Configuration You can configure the presence expiration time (for disconnected clients) via the `--presence_ttl` option. The default value is 15 seconds. ## Presence for channels Alternatively to joining and leaving the channel's presence set from the client, you can use control the presence behaviour from the server-side when using channels. For that, you can provide presence commands (`join` and `leave`) in subscription callbacks and channel actions. ### Ruby on Rails integration Here is a quick overview of using Presence API in Rails' Action Cable: ```ruby class ChatChannel < ApplicationCable::Channel def subscribed room = Chat::Room.find(params[:id]) stream_for room join_presence(id: current_user.id, info: {name: current_user.name}) end end ``` The full documentation could be found [here](https://docs.anycable.io/edge/rails/extensions?id=presence-tracking). ### JavaScript integration > 🚧 Presence support in [anycable-serverless-js](https://github.com/anycable/anycable-serverless-js) is coming soon. ## Presence for Hotwire > Read more in the ["Simple Declarative Presence for Hotwire apps with AnyCable"](https://evilmartians.com/chronicles/simple-declarative-presence-for-hotwire-apps-with-anycable) blog post. For Hotwire applications, our `@anycable/turbo-stream` package (>= 0.8.0) provides a custom `` element to add presence information on the page without needing to write any custom client-side code. The complete documentation is coming soon; for now, you can check out this [example](https://github.com/anycable/anycasts_demo/pull/17). ## Presence API You can get the list of currently present users for a stream using our REST API: [documentation](./api.md). ## Presence webhooks > 🚧 Presence webhooks are to be implemented, too. Please, reach out to us if you need to use it and share your use cases. --- --- url: /anycable-go/pubsub.md --- # Pub/Sub for node-node communication When running multiple instances of AnyCable-Go, you have two options to deliver broadcast messages to all nodes (and, thus, clients connected to each node). The first, legacy option is to use a fan-out, or distributed broadcasting adapter (Redis or NATS), i.e., deliver messages to all nodes simultaneously and independently. The second option is to publish a message to a single node (picked randomly), and then let AnyCable-Go to re-transmit it to other nodes from the cluster. The latter is **required** for multi-node setups with a [broker](./broker.md). Although, we do not plan to sunset legacy, distributed adapters in the nearest future, we recommend switching to a new broadcaster+pubsub (*B+P/S*) architecture for the following reasons: * Broker (aka *streams history*) requires the new architecture. * The new architecture scales better by avoiding sending **all** messages to **all** nodes; only the nodes *interested* in a particular stream (i.e., having active subscribers) receive broadcast messages via pub/sub. **NOTE:** The new architecture will be the default one since v1.5. ## Usage By default, pub/sub is disabled (since the default broadcast adapter is legacy, fan-out Redis). To enable the pub/sub layer, you must provide the name of the provider via the `--pubsub` option. You also need to enable a compatible broadcasting adapter. See [broadcasting](./broadcasting.md). **NOTE**: It's safe to enable `--pubsub` even if you're still using legacy broadcasting adapters (they do not pass messages through the pub/sub layer). ## Supported adapters ### Redis The Redis pub/sub adapter uses the Publish/Subscribe Redis feature to re-transmit messages within a cluster. To enable it, set the value of the`pubsub` parameter to `redis`: ```sh $ anycable-go --pubsub=redis # or $ ANYCABLE_PUBSUB=redis anycable-go INFO 2023-04-18T20:46:00.692Z context=main Starting AnyCable 1.4.0-36a43e5 (with mruby 1.2.0 (2015-11-17)) (pid: 16574, open file limit: 122880, gomaxprocs: 8) INFO 2023-04-18T20:46:00.693Z context=pubsub Starting Redis pub/sub: localhost:6379 ... ``` See [configuration](./configuration.md) for available Redis configuration settings. ### NATS ```sh $ anycable-go --pubsub=nats # or $ ANYCABLE_PUBSUB=nats anycable-go INFO 2023-04-18T20:28:38.410Z context=main Starting AnyCable 1.4.0-36a43e5 (with mruby 1.2.0 (2015-11-17)) (pid: 9125, open file limit: 122880, gomaxprocs: 8) INFO 2023-04-18T20:28:38.411Z context=pubsub Starting NATS pub/sub: nats://127.0.0.1:4222 ... ``` You can use it with the [embedded NATS](./embedded_nats.md), too: ```sh $ anycable-go --embed_nats --pubsub=nats INFO 2023-04-18T20:30:58.724Z context=main Starting AnyCable 1.4.0-36a43e5 (with mruby 1.2.0 (2015-11-17)) (pid: 9615, open file limit: 122880, gomaxprocs: 8) INFO 2023-04-18T20:30:58.753Z context=main Embedded NATS server started: nats://127.0.0.1:4222 INFO 2023-04-18T20:30:58.755Z context=pubsub Starting NATS pub/sub: nats://127.0.0.1:4222 ``` See [configuration](./configuration.md) for available NATS configuration settings. --- --- url: /anycable-go/pusher.md --- # Pusher Compatibility AnyCable supports [Pusher protocol](https://pusher.com/docs/channels/library_auth_reference/pusher-websockets-protocol) meaning that it can be used as a drop-in replacement for Pusher (or another Pusher-speaking WebSocket server such as Laravel Reverb, Soketi, etc.). ## Configuration To enable Pusher compatibility mode, you must specify the following parameters: * `--pusher_app_id` (or `ANYCABLE_PUSHER_APP_ID`): Pusher application ID * `--pusher_app_key` (or `ANYCABLE_PUSHER_APP_KEY`): Pusher application key * `--pusher_secret` (or `ANYCABLE_PUSHER_SECRET`): Pusher secret for signing (falls back to `--secret` if not specified) Example: ```sh $ anycable-go --pusher_app_id=my-app-id --pusher_app_key=my-app-key --pusher_secret=my-secret ... INFO 2025-01-20 12:00:00.000 INF Handle Pusher WebSocket connections at http://localhost:8080/app/my-app-key INFO 2025-01-20 12:00:00.000 INF Handle Pusher API requests at http://localhost:8080/apps/my-app-id/ ``` In the logs, you will see the Pusher WebSocket and HTTP API endpoints. Configure your Pusher client library and server-side SDKs accordingly. ### Dedicated API port By default, the Pusher HTTP API is served on the same port as WebSocket connections. You can configure a separate port for the HTTP API using the `--pusher_api_port` option: ```sh $ anycable-go \ --pusher_app_id=my-app-id \ --pusher_app_key=my-app-key \ --pusher_secret=my-secret \ --pusher_api_port=8081 ... INFO Handle Pusher WebSocket connections at http://localhost:8080/app/my-app-key INFO Handle Pusher API requests at http://localhost:8081/apps/my-app-id/ ``` This is useful when you want to expose the HTTP API only within a private network while keeping WebSocket connections publicly accessible. ## HTTP API AnyCable implements a subset of Pusher HTTP API for server-to-server communication. All API requests require authentication using the [Pusher signature scheme](https://pusher.com/docs/channels/library_auth_reference/rest-api/#authentication). ### Trigger events **Endpoint:** `POST /apps/{app_id}/events` Broadcast an event to one or more channels: ```sh curl -X POST "http://localhost:8080/apps/my-app-id/events?auth_key=my-app-key&auth_timestamp=$(date +%s)&auth_version=1.0&body_md5=$(echo -n '{"name":"my-event","channel":"my-channel","data":"{}"}' | md5sum | cut -d' ' -f1)&auth_signature=" \ -H "Content-Type: application/json" \ -d '{"name":"my-event","channel":"my-channel","data":"{}"}' ``` We recommend using official Pusher server SDKs which handle authentication automatically. ### Get channel users **Endpoint:** `GET /apps/{app_id}/channels/{channel_name}/users` Retrieve the list of users subscribed to a presence channel: ```sh curl "http://localhost:8080/apps/my-app-id/channels/presence-my-channel/users?auth_key=my-app-key&auth_timestamp=$(date +%s)&auth_version=1.0&auth_signature=" ``` **Response:** ```json { "users": [ {"id": "user-1"}, {"id": "user-2"} ] } ``` **Notes:** * This endpoint only works with presence channels (channels prefixed with `presence-`) * Returns `400 Bad Request` for non-presence channels * Returns an empty list for unknown channels **Example using Pusher Ruby SDK:** ```go require "pusher" client = Pusher::Client.new( app_id: "my-app-id", key: "my-app-key", secret: "my-secret" ) # Request the users for a presence channel: response = pusher.channel_users("presence-my-channel") response["users"].each do |user| puts "User ID: #{user['id']}" end ``` ## Client configuration Configure your Pusher client to connect to AnyCable: ### JavaScript ```js import Pusher from 'pusher-js'; const pusher = new Pusher('my-app-key', { wsHost: 'localhost', wsPort: 8080, forceTLS: false, disableStats: true, enabledTransports: ['ws', 'wss'], }); ``` ### Laravel Echo ```js import Echo from 'laravel-echo'; import Pusher from 'pusher-js'; window.Pusher = Pusher; window.Echo = new Echo({ broadcaster: 'pusher', key: 'my-app-key', wsHost: 'localhost', wsPort: 8080, forceTLS: false, disableStats: true, enabledTransports: ['ws', 'wss'], }); ``` ## Compatibility AnyCable doesn't aim to provide 100% compatibility with the current or future Pusher versions. The primary purpose of this compatibility layer is to allow applications to use AnyCable as a Pusher replacement and, optionally, gradually migrate to the AnyCable protocol to benefit from its features (such as [reliable streams](./reliable_streams.md)). ### Supported features | Feature | Status | Notes | |---------|--------|-------| | Public channels | ✅ | | | Private channels | ✅ | | | Presence channels | ✅ | | | Client events (whispers) | ✅ | | | `POST /events` | ✅ | Trigger events API | | `GET /channels/{channel}/users` | ✅ | Get presence channel users | | `POST /batch_events` | ⚙️ | Can be added if needed | | Webhooks | ⏳ | Planned | | Watchlist events | ❌ | Not planned | | Encrypted channels | ❓ | | | Authentication (`pusher:signin`) | ❓ | | --- --- url: /release_notes.md --- # Release Notes This page contains combined release notes for major and minor releases of all AnyCable libraries. ## 1.6.0 **IMPORTANT**: the development of the AnyCable server has moved to [anycable/anycable](https://github.com/anycable/anycable). Ruby SDK now lives in [anycable/anycable-rb](https://github.com/anycable/anycable-rb). ### Highlights * **Presence tracking** We continued extending Action Cable protocol and added presence tracking support, so you can keep track channel/stream subscribers in real-time. See [docs](https://docs.anycable.io/anycable-go/presence). ### Features * Token-based authentication using a WebSocket sub-protocol. You can pass a token as a `anycable-token.` subprotocol to a WebSocket client. This way, token is not exposed in the URL. [AnyCable JS SDK](https://github.com/anycable/anycable-client) supports this feature out of the box. ### Improvements * Better handling of slow clients. AnyCable server now buffers outgoing messages using size-limited queues. Tuning the new configuration settings, `ws_write_timeout` and `ws_max_pending_size`, can help you deal with slow clients the way you want (let them buffer a lot of messages or disconnect). ## 1.5.0 ### Highlights * **Signed streams and public streams** We made signed streams functionality previously available for Hotwire applications (Turbo Streams) generic and available to everyone. Thus, it's now possible to use AnyCable without RPC as a regular pub/sub server with plain channels (but with all other features, like reliability, available 😉). This feature also comes with the initial support for **client-initiated broadcasts**, or *whispers*. See [docs](/anycable-go/signed_streams). * **One secret to rule them all** Now a single secret is enough to secure all AnyCable features; we call it an **application secret**. You can provide it via the `--secret` flag or the `ANYCABLE_SECRET=` env var. It's used as is for JWT and signed streams (unless specific secrets specified) and as a secret base for HTTP RPC and HTTP broadcasting (again, unless specific keys specified). There is also new `--broadast_key` (`ANYCABLE_BROADCAST_KEY`) that is meant to be used to authenticate broadcast actions. Currently, it's only used by HTTP broadcasting (as a replacement for `http_broadcast_secret`). ### Features * **Public mode**. You can run AnyCable in an insecure mode (at your own risk): no authentication (unless JWT specified), public streams, no HTTP broadcasting authentication. You can enable this mode via the `--public` toggle or by setting `ANYCABLE_PUBLIC=true`. It's also possible to partially disable protections via `--noauth` and `--public_streams` parameters. * **Embedding**. AnyCable Go library now provides interface that allows you to embed an AnyCable server into an existing Go web applications and use its HTTP handlers (for WebSockets, SSE, broadcasting). See [docs](https://docs.anycable.io/edge/anycable-go/library?id=embedding). #### Rails * Added `websocket_url` parameter to provide a WebSocket server address for clients. This new parameter automatically updates the `config.action_cable.url` to provide the WebSocket server information to clients via the `#action_cable_meta_tag` (or `#action_cable_with_jwt_meta_tag`) helpers. Thus, all you need to point your clients to AnyCable is configure the `websocket_url` (or `ANYCABLE_WEBSOCKET_URL`) value, no code changes required. * Broadcasting to objects. Extended `ActionCable.server.broadcast` to accept not only strings but objects (similar to `Channel.broadcast_to(...)`). See [docs](/rails/extensions?id=broadcast-to-objects). * Whispering support. You can specify the whispering stream (for client-initiated broadcasts) by using the `#stream_from(..., whisper: true)` or `#stream_for(..., whisper: true)` in your channel classes. See [docs](/rails/extensions?id=whispering). * Added `rails g anycable:bin`. This generator creates a `bin/anycable-go` script to run (and install) AnyCable server locally. ### Changes #### AnyCable server * Logging format has changed. We migrated to Go `log/slog` package for performance and DX reasons and decided to stick to the default Go log formatting. * HTTP broadcasting endpoint is enabled by default. Depending on security settings (whether the application secret or broadcast key is present), we expose HTTP broadcasting endpoint on the main application port (when secured) or `:8090` (when no authentication required, previous behaviour). * Multiple configuration parameters name changes. You will see deprecation warning on the server start with instructions on how to migrate. #### AnyCable Ruby/Rails * The `anycable-rails-jwt` gem has been merged into the `anycable` and `anycable-rails` gems. ## 1.4.0 ### Highlights * **Reliable streams and resumable sessions**. AnyCable-Go improves data consistency of your real-time applications by allowing clients to request the missed messages on re-connection and restore their state without re-authentication and re-subscription to channels. The features require using the [extended version of Action Cable protocol](./misc/action_cable_protocol.md#action-cable-extended-protocol), which is supported by the [AnyCable JS client](https://github.com/anycable/anycable-client) out-of-the-box—no application level changes required. See [documentation](./anycable-go/reliable_streams.md) for details. * **RPC over HTTP**. AnyCable now supports RPC over HTTP communication as an alternative to gRPC one. This allows you to embed AnyCable RPC part into your web server (e.g., Puma) without requiring a separate process or port. This is especially useful for Heroku deployments. See [documentation](./ruby/http_rpc.md) for details. ### Features * **Redis X** broadcasting adapter. Redis X is a new broadcasting adapter that use Redis Streams instead of Publish/Subscribe to deliver broadcasting messages from your application to WebSocket servers. This is another step towards improved consistency: no message broadcasted from your application will be lost, even if WebSocket servers are temporarily unavailable. This is especially useful in combination with reliable streams. See [documentation](./ruby/broadcast_adapters.md#redis-x) for details. ### Changes * Broadcasted messages are now delivered in the order they were received by the server. Previously, we used an executor pool internally to deliver broadcasted messages concurrently (to reduce the latency). That led to nonderterministic order of messages within a single stream delivered in a short period of time. Now, we preserve the order of messages within a stream—the delivered as they were accepted by the server. That means, with a single AnyCable-Go server, the following snippet will result in clients receiving the messages in the same order they were broadcasted: ```ruby 10.times { ActionCable.server.broadcast "test", {text: "Count: #{_1}"} } # Client will receive the following messages: # #=> {"text"=>"Count: 0"} #=> {"text"=>"Count: 1"} #=> {"text"=>"Count: 2"} # ... #=> {"text"=>"Count: 9"} ``` **NOTE:** In a clustered setup, the order of messages is not always guaranteed. For example, when using `http` or `redisx` adapter, each broadcasted message is handled by a single AnyCable-Go server independently, thus, there can be race conditions. #### AnyCable-Go * New disconnect modes and `--disable_disconnect` deprecation. AnyCable-Go becomes smarter with regards to performing Disconnect calls. In the default mode ("auto"), clients not relying on `#disconnect` / `#unsubscribed` callbacks do not trigger Disconnect RPC calls on connection close. Thus, if you use JWT identification and Hotwire signed streams with AnyCable-Go, you don't need to worry about the `--disable_disconnect` option to use AnyCable in the RPC-less mode. The previous `--disable_disconnect` behaviour can be achieved by setting `--disconnect_mode=never`. ## 1.3.0 ### Features #### Common * Configuration presets (aka sensible defaults). AnyCable now automatically detects known platforms (Heroku, Fly) and tunes configuration accordingly. Right now, Fly.io support is the most comprehensive and allows you to automatically connect Ruby and AnyCable-Go apps to each other (by setting correct RPC and broadcasting URLs). See documentation for [AnyCable](./ruby/configuration.md#presets) and [AnyCable-Go](./anycable-go/configuration.md#presets). #### AnyCable-Go * Added adaptive concurrency support. Users of AnyCable had to scale and balance resources on two sides: RPC and AnyCable-Go. Now AnyCable-Go can adjust its concurrency limit automatically to minimize errors (`ResourcesExhausted`) and maximize throughput (thus, reduce the backlog size) if possible. This means, you only have to scale the Rails application, and AnyCable-Go will balance itself alongside automatically. See [documentation](./anycable-go/rpc.md#adaptive-concurrency). #### AnyCable-Go * **Embedded NATS** support. Now it's possible to run a NATS server within an AnyCable-Go process, so you don't need to deploy a pub/sub engine yourself. See [documentation](./anycable-go/embedded_nats.md). * StatsD and metric tags are now generally available (dowstreamed from PRO). See [documentation](./anycable-go/instrumentation.md#statsd). * Added support for WebSocket endpoint paths. Now you can specify wildcards and placeholders in a WS endpoint for `anycable-go`: ```sh anycable-go --path="/{tenant}/cable ``` This could be helpful to differentiate between clients or even different Action Cable Connection class instances at a Ruby side. * Added `grpc_active_conn_num` metrics. Now you can monitor the actual number of gRPC connections established between a WebSocket server and RPC servers. #### AnyCable Ruby * Added experimental support for [grpc\_kit](https://github.com/cookpad/grpc_kit) as a gRPC server implementation. Add `grpc_kit` to your Gemfile and specify `ANYCABLE_GRPC_IMPL=grpc_kit` env var to use it. * Added mutual TLS support for connections to Redis. #### AnyCable Rails **NOTE:** Changes below are for v1.3.7 of the `anycable-rails` gem. * Added Rails 7+ error reporting interface integration. If your error reporting software supports Rails built-in error reporting (e.g., Sentry does), you no longer need to configure `AnyCable.capture_exception { ... }` yourself. ### Changes #### AnyCable Ruby * A new configuration paramter, `rpc_max_connection_age`, has been added to replace the previous `rpc_server_args.max_connection_age_ms` (or `ANYCABLE_RPC_SERVER_ARGS__MAX_CONNECTION_AGE_MS`). It comes with the **default value of 300 (5 minutes)**. **NOTE:** The `rpc_max_connection_age` accepts seconds, not milliseconds. *** For full list of changes see the corresponding change logs: * [AnyCable Ruby gem](https://github.com/anycable/anycable/blob/v1.3.0/CHANGELOG.md) * [AnyCable Rails gem](https://github.com/anycable/anycable-rails/blob/v1.3.0/CHANGELOG.md) * [AnyCable Go](https://github.com/anycable/anycable-go/blob/v1.3.0/CHANGELOG.md) ## 1.2.0 ### Features * Add fastlane subscribing for Hotwire (Turbo Streams) and CableReady. Make it possible to terminate subscription requests at AnyCable Go without performing RPC calls. See [documentation](./anycable-go/signed_streams.md). * Add JWT authentication/identification support. You can pass a properly structured token along the connection request to authorize the connection and set up *identifiers* without peforming an RPC call. See [documentation](./anycable-go/jwt_identification.md). ## 1.1.0 **tl;dr** Housekeeping and internals refactoring, prepare for non-gRPC RPC, minor but useful additions. See also [upgrade notes](./upgrade-notes/1_0_0_to_1_1_0.md). ### Features * Added ability to embed AnyCable RPC into any Ruby process. When using `anycable-rails`, set `embedded: true` in the configuration to launch RPC along with `rails s` (only for Rails 6.1+). For any other Ruby process, drop the following snippet to launch an RPC server: ```ruby require "anycable/cli" AnyCable::CLI.embed!(*args) # args is a space-separated list of CLI args ``` * New metrics for `anycable-go`: * `server_msg_total` and `failed_server_msg_total`: the total number of messages sent (or failed to send) by server. * `data_sent_bytes_total` and `data_rcvd_bytes_total`: the total amount of bytes sent to (or received from) clients. * New configuration parameters for `anycable-go`: * `--max-conn`: hard-limit the number of simultaneous server connections. * `--allowed_origins`: a comma-separated list of hostnames to check the Origin header against during the WebSocket Upgrade; supports wildcards, e.g., `--allowed_origins=*.evl.ms,www.evlms.io`. * `--ping_timestamp_precision`: define the precision for timestamps in ping messages (s, ms, ns). ### Changes * Ruby 2.6+ is required for all Ruby gems (`anycable`, `anycable-rails`, `anycable-rack-server`). * Rails 6.0+ is required for `anycable-rails`. * Dropped deprecated AnyCable RPC v0.6 support. * The `anycable` gem has been split into `anycable-core` and `anycable`. The first one contains an abstract RPC implementation and all the supporting tools (CLI, Protobuf), the second one adds the gRPC implementation. * **BREAKING** Middlewares are no longer inherited from gRPC interceptors. That allowed us to have *real* middlewares with ability to modify responses, intercept exceptions, etc. The API changed a bit: ```diff class SomeMiddleware < AnyCable::Middleware - def call(request, rpc_call, rpc_handler) + def call(rpc_method_name, request, metadata) yield end end ``` * Broadcasting messages is now happening concurrently. Now new broadcast messages are handled (and re-transmitted) concurrently by a pool of workers (Go routines). You can control the size of the pool via the `--hub_gopool_size` configuration parameter of the `anycable-go` server (defaults to 16). *** For internal changes see the corresponding change logs: * [AnyCable Ruby gem](https://github.com/anycable/anycable/blob/v1.1.0/CHANGELOG.md) * [AnyCable Rails gem](https://github.com/anycable/anycable-rails/blob/v1.1.0/CHANGELOG.md) * [AnyCable Go](https://github.com/anycable/anycable-go/blob/v1.1.0/CHANGELOG.md) * [AnyCable Rack Server](https://github.com/anycable/anycable-rack-server/blob/v0.4.0/CHANGELOG.md) *** ## 1.0.0 **tl;dr** API stabilization, better Action Cable compatibility, [Stimulus Reflex][stimulus_reflex] compatibility, improved RPC communication, state persistence, HTTP broadcast adapter, Rails generators. > Read more about the first major release of AnyCable in [Evil Martians chronicles](https://evilmartians.com/chronicles/anycable-1-0-four-years-of-real-time-web-with-ruby-and-go). See also [upgrade notes](./upgrade-notes/0_6_0_to_1_0_0.md). ### Features * Configure AnyCable for Rails apps via `rails g anycable:setup`. This interactive generator guides you through all the required steps to make AnyCable up and running for development and production. * Channel state, or `state_attr_accessor`. Similarly to connection identifiers, it is now possible to store arbitrary\* data for *subscriptions* (channel instances). Using `state_attr_accessor :a, :b` (from `anycable-rails`) you can define readers and writers to keep channel state between commands. When AnyCable is not activated (i.e., a different adapter is used for Action Cable), this method behaves like `attr_accessor`. \* GlobalID is used for serialization and deserialization of non-primitive objects. * Rack middlewares support in Rails. You can use Rack middlewares to *enhance* AnyCable `request` object. For that, add required middlewares to `AnyCable::Rails::Rack.middleware` using the same API as for Rails middleware. By default, only session store middleware is included, which allows you to access `request.session` without any hacks. A typical use-case is adding a Warden middleware for Devise-backed authentication. See [documentation](./rails/authentication.md). * Underlying HTTP request data in now accessible in all RPC methods. That is, you can access `request` object in channels, too (e.g., headers/cookies/URL/etc). * Remote disconnects. Disconnecting remote clients via `ActionCable.server.remote_connections.where(...).disconnect` is now supported. * Rails session persistence. Now `request.session` could be persisted between RPC calls, and hence be used as a per-connection store. Originally added for [Stimulus Reflex][stimulus_reflex] compatibility. **NOTE:** This feature is optional and should be enabled explicitly in `anycable-rails` configuration. See [documentation](./rails/stimulus_reflex.md). * HTTP broadcast adapter. Now you can experiment with AnyCable without having to install Redis. See [documentation](./ruby/broadcast_adapters.md#http-adapter). **NOTE:** Supported by `anycable` gem and `anycable-go`. * Unsubscribing from a particular stream. See the corresponding [Rails PR](https://github.com/rails/rails/pull/37171). * Redis Sentinel support. Both `anycable` gem and `anycable-go` now support using Redis with Sentinels. See [documentation](./ruby/broadcast_adapters.md#redis-sentinel-support). * New metrics for `anycable-go`: * `mem_sys_bytes`: the total bytes of memory obtained from the OS * `rpc_retries_total`: the total number of retried RPC calls (higher number could indicate incorrect concurrency configuration) * New configuration parameters for `anycable-go`: * `rpc_concurrency`: the limit on the number of concurrent RPC calls (read [documentation](./anycable-go/configuration.md#concurrency-settings)). * `enable_ws_compression`: enable WebSocket per-message compression (disabled by default). * `disconnect_timeout`: specify the timeout for graceful shutdown of the disconnect queue (read [documentation](./anycable-go/configuration.md#disconnect-events-settings)) * `disable_disconnect`: disable calling disconnect/unsubscribe callbacks. ### Changes * New RPC schema. Check out the annotated [new schema](./misc/rpc_proto.md). * Ruby 2.5+ is required for all Ruby gems (`anycable`, `anycable-rails`, `anycable-rack-server`). * Docker versioning changed from `vX.Y.Z` to `X.Y.Z` for `anycable-go`. Now you can specify only the part of the version, e.g. `anycable-go:1.0` instead of the full `anycable-go:v1.0.0`. *** For internal changes see the corresponding change logs: * [AnyCable Ruby gem](https://github.com/anycable/anycable/blob/v1.0.0/CHANGELOG.md) * [AnyCable Rails gem](https://github.com/anycable/anycable-rails/blob/v1.0.0/CHANGELOG.md) * [AnyCable Go](https://github.com/anycable/anycable-go/blob/v1.0.0/CHANGELOG.md) * [AnyCable Rack Server](https://github.com/anycable/anycable-rack-server/blob/v0.2.0/CHANGELOG.md) [stimulus_reflex]: https://github.com/hopsoft/stimulus_reflex --- --- url: /anycable-go/reliable_streams.md --- # Reliable streams and resumable sessions Since v1.4, AnyCable allows you to enhance the consistency of your real-time data and go from **at-most-once** to **at-least-once** and even **exactly-once delivery**. > 🎥 Learn more about the consistency pitfalls of Action Cable from [The pitfalls of realtime-ification](https://noti.st/palkan/MeBUVe/the-pitfalls-of-realtime-ification) talk (RailsConf 2022). ## Overview The next-level delivery guarantees are achieved by introducing **reliable streams**. AnyCable keeps a *hot cache* \* of the messages sent to the streams and allows clients to request the missed messages on re-connection. In addition to reliable streams, AnyCable v1.4 also introduces **resumable sessions**. This feature allows clients to restore their state on re-connection and avoid re-authentication and re-subscription to channels. \* The "hot cache" here means that the cache is short-lived and is not intended to be used as a long-term storage. The primary purpose of this cache is to improve the reliability of the stream delivery for clients with unstable network connections. ## Quick start The easiest way to try the streams history feature is to use the `broker` preset for AnyCable-Go (named after the underlying component, [Broker](./broker.md)): ```sh $ anycable-go --presets=broker INFO 2023-04-14T00:31:55.548Z context=main Starting AnyCable 1.4.0-d8939df (with mruby 1.2.0 (2015-11-17)) (pid: 87410, open file limit: 122880, gomaxprocs: 8) INFO 2023-04-14T00:31:55.548Z context=main Using in-memory broker (epoch: vRXl, history limit: 100, history ttl: 300s, sessions ttl: 300s) INFO 2023-04-18T20:46:00.693Z context=pubsub Starting Redis pub/sub: localhost:6379 INFO 2023-04-19T16:22:55.776Z context=pubsub provider=http Accept broadcast requests at http://localhost:8090/_broadcast ... ``` Now, at the Ruby/Rails side, switch to the `http` or `redisx` broadcasting adapter (if you use Redis). For example, in `config/anycable.yml`: ```yaml default: &default # ... broadcast_adapter: http ``` Finally, at the client-side, you MUST use the [AnyCable JS client](https://github.com/anycable/anycable-client) and configure it to use the `actioncable-v1-ext-json` protocol: ```js import { createCable } from '@anycable/web' // or for non-web projects // import { createCable } from '@anycable/core' export default createCable({protocol: 'actioncable-v1-ext-json'}) ``` That's it! Now your clients will automatically catch-up with the missed messages and restore their state on re-connection. ## Manual configuration The `broker` preset is good for a quick start. Let's see how to configure the broker and other components manually. First, you need to provide the `--broker` option with a broker adapter name: ```sh $ anycable-go --broker=memory INFO 2023-04-14T00:31:55.548Z context=main Starting AnyCable 1.4.0-d8939df (with mruby 1.2.0 (2015-11-17)) (pid: 87410, open file limit: 122880, gomaxprocs: 8) INFO 2023-04-14T00:31:55.548Z context=main Using in-memory broker (epoch: vRXl, history limit: 100, history ttl: 300s, sessions ttl: 300s) ... ``` Then, you MUST configure a compatible broadcasting adapter (currently, `http` and `redisx` are available). For example, when using Redis: ```sh $ anycable-go --broker=memory --broadcast_adapter=redisx ... INFO 2023-07-04T02:00:24.386Z consumer=s2IbkM context=broadcast id=s2IbkM provider=redisx stream=__anycable__ Starting Redis broadcaster at localhost:6379 ... ``` See [broadcasting documentation](./broadcasting.md) for more information. Finally, to re-transmit *registered* messages within a cluster, you MUST also configure a pub/sub adapter (via the `--pubsub` option). The command will look as follows: ```sh $ anycable-go --broker=memory --broadcast_adapter=redisx --pubsub=redis INFO 2023-07-04T02:02:10.548Z context=main Starting AnyCable 1.4.0-d8939df (with mruby 1.2.0 (2015-11-17)) (pid: 87410, open file limit: 122880, gomaxprocs: 8) INFO 2023-07-04T02:02:10.548Z context=main Using in-memory broker (epoch: vRXl, history limit: 100, history ttl: 300s, sessions ttl: 300s) INFO 2023-07-04T02:02:10.586Z consumer=s2IbkM context=broadcast id=s2IbkM provider=redisx stream=__anycable__ Starting Redis broadcaster at localhost:6379 INFO 2023-07-04T02:02:10.710Z context=pubsub Starting Redis pub/sub: localhost:6379 ... ``` See [Pub/Sub documentation](./pubsub.md) for available options. ### Cache settings There are several configuration options to control how to store messages and sessions: * `--history_limit`: Max number of messages to keep in the stream's history. Default: `100`. * `--history_ttl`: Max time to keep messages in the stream's history. Default: `300s`. * `--sessions_ttl`: Max time to keep sessions in the cache. Default: `300s`. Setting `sessions_ttl` to zero disables sessions cache. Currently, the configuration is global. We plan to add support for granular (per-stream) settings in the following releases. ## Resumed sessions vs. disconnect callbacks AnyCable WebSocket server notifies a main application about the client disconnection via the `Disconnect` RPC call (which translates into `Connection#disconnect` and `Channel#unsubscribed` calls in Rails). Currently, when the client's session is restored, no callbacks are invoked in the main application. Keep this limitation in mind when designing your business logic (i.e., if you rely on connect/disconnect callbacks, you should consider disabling sessions cache by setting `sessions_ttl` to 0). **NOTE:** We consider introducing a new RPC method, `Restore`, along with the corresponding Ruby-side callbacks (`Connection#restored` and `Channel#resubscribed`) to handle this situation. Feel free to join [the discussion](https://github.com/orgs/anycable/discussions/209) and share your thoughts! ## Cache backends ### Memory The default broker adapter. It stores all data in memory. It can be used **only for single node installations**. **IMPORTANT**: Since the data is stored in memory, it's getting lost during restarts. **NOTE:** Storing data in memory may result into the increased RAM usage of an AnyCable-Go process. ### NATS *🧪 This adapter is currently in the experimental stage. Please, report any issues you may encounter.* This adapter uses [NATS JetStream](https://nats.io/) as a shared distributed storage for sessions and streams cache and also keeps a local snapshot in memory (using the in-memory adapter described above). Usage: ```sh $ anycable-go --broker=nats --nats_servers=nats://localhost:4222 INFO 2023-10-28T00:57:53.937Z context=main Starting AnyCable 1.4.6-c31c153 (with mruby 1.2.0 (2015-11-17)) (pid: 29874, open file limit: 122880, gomaxprocs: 8) INFO 2023-10-28T00:57:53.937Z context=main Starting NATS broker: nats://localhost:4222 (history limit: 100, history ttl: 300s, sessions ttl: 300s) ... ``` **NOTE:** You MUST have JetStream enabled in your NATS server. See [NATS JetStream documentation](https://docs.nats.io/nats-concepts/jetstream) for more information. ### Using with embedded NATS [Embedded NATS](./embedded_nats.md) automatically enables JetStream if the NATS broker is being used: ```sh $ anycable-go --embed_nats --broker=nats INFO 2023-10-28T00:59:01.177Z context=main Starting AnyCable 1.4.6-c31c153 (with mruby 1.2.0 (2015-11-17)) (pid: 30693, open file limit: 122880, gomaxprocs: 8) INFO 2023-10-28T00:59:01.177Z context=main Starting NATS broker: nats://127.0.0.1:4222 (history limit: 100, history ttl: 300s, sessions ttl: 300s) INFO 2023-10-28T00:59:01.205Z context=main Embedded NATS server started: nats://127.0.0.1:4222 ... ``` **IMPORTANT:** Your multi-node cluster MUST have at least **3 nodes** to use NATS JetStream. Super-cluster mode hasn't been tested yet. ### Redis AnyCable Pro comes with a Redis-based broker adapter. It stores all data in Redis and, thus, can be used in multi-node installations. To use Redis broker, you need to provide the `--broker` option with the `redis` adapter name: ```sh $ anycable-go --broker=redis INFO 2023-07-08T00:46:55.491Z context=main Starting AnyCable 1.6.0-pro-eed05bc (with mruby 1.2.0 (2015-11-17)) (pid: 78585, open file limit: 122880, gomaxprocs: 8, netpoll: true) INFO 2023-07-08T00:46:55.492Z context=main Using Redis broker at localhost:6379 (history limit: 100, history ttl: 300s, sessions ttl: 300s, presence ttl: 15s) ... ``` When you use the `broker` preset with AnyCable, it automatically configures the Redis broker (if Redis credentials are configured). To estimate the required amount of memory for Redis, you can use the following formula: $$ M\_H = (325 + M\_i) \times L \times N\_{streams} $$ $$ M\_S = 350 \times N\_{clients} $$ $$ M = M\_S + M\_H $$ $M$ — total memory required $M\_H$ — memory required to store streams history $M\_S$ — memory required to store sessions cache $M\_i$ — size of a broadcast message $L$ — history size limit (per stream) $N\_{streams}$ — number of unique streams (see the `broadcast_streams_num` metrics) $N\_{clients}$ — total number of sessions (see the `clients_num` metrics) Note that the session cache size depends on the number of subscriptions and channel states, so could vary. Similarly, presence cache stored in Redis heavily depends on the size of a presence information attached to a user-channel pair. #### Streams history expiration We use [Redis Streams](https://redis.io/docs/data-types/streams/) to store messages history. Redis doesn't support expiring individual messages in a stream, so we expire the whole stream instead. In other words, the `--history_ttl` option controls the expiration of the whole stream. ## Further reading For in-depth information about the feature and its internals, see the following articles: * [Broker](./broker.md) * [Pub/Sub](./pubsub.md) --- --- url: /deployment/render.md --- # Render Deployment > Original version of this guide could be found in the PR: [anycable/docs.anycable.io#35](https://github.com/anycable/docs.anycable.io/pull/35). The easiest way to deploy AnyCable + Rails apps to Render.com is by having three services: * **Rails app** -- Web Service (public) -- Ruby environment * **Same rails app running gRPC** - Private Service -- Ruby environment * **AnyCable-Go server** -- Web Service (public) -- Docker environment It's likely doable to combine the two Rails app instances (public and grpc) into a single server, but I'll leave that exercise to the reader. ## Assumptions * You've followed the standard Rails setup and run the excellent installer script (see [Getting started with Rails](../rails/getting_started.md)). * You are using a custom domain and have DNS control, so you can create a subdomain. * You are using Devise/Warden for authentication in your Rails app (if you're using something else, I'll leave it to you to figure out what is needed to make AnyCable work with your auth scheme.) * You are using `redis_session_store` for sessions. ## Rails Web Service We're going to call our Rails app **"xylophone"** for the remainder of this guide. Provision the normal public Rails app web service as you usually would on Render. I'm also assuming you've provisioned a Redis server; remember the `REDIS_URL` because we'll need it later. We're going to assume you set up your custom domain under *Settings* (e.g. `xylophone.com`) ## gRPC Private Service The gRPC app is just going to be your same Rails app again, but this time running on a *private* service, communicating internally with your AnyCable service. We're going to provision it with the name `xylophone-grpc`. Under **Environment**, you will likely want to set the following: * `RAILS_MASTER_KEY` (same as on your public app service) * `REDIS_URL` (same as on your public app service) Under **Settings**, set the following: * Build Command: `bundle install` * Start Command: `bundle exec anycable --rpc-host 0.0.0.0:50051` Once your grpc app service is provisioned, it should provide an *internal* service name like `xylophone-grpc:50051` ... remember this because you'll need it later. ## AnyCable-Go Web Service AnyCable is going to be deployed as a simple Docker application on Render. The easiest way to do this is create a `anycable-go` directory on your local machine that literally *only* has a `Dockerfile` in it. ### `Dockerfile` ```dockerfile FROM anycable/anycable-go:1.2 ``` Now push that directory to a git repo so Render will be able to connect to it. Back to Render dashboard, click New+ and create a `Web Service` (it's going to be publicly available because this is the server that your clients' browser will hit up with websocket requests.) Make sure to connect to your new anycable-go repo with the Dockerfile. Render should auto-detect that it's a Docker app and build it for you. You'll need some settings though... ### Render Settings Under **Environment**: * set `ANYCABLE_HOST` to `0.0.0.0` * set `ANYCABLE_RPC_HOST` to `xylophone-grpc:50051` (the internal service name from before) * set `REDIS_URL` to the same redis url for your other services (e.g. `redis://red-abc123abc123abc123:6379`) Under **Settings**: You shouldn't really need to change much here. Just make sure you've set up your custom domain with a reasonable subdomain, e.g. `ws.xylophone.com`. ## Wiring some things together Now that you've got all three services running, you need to go back to the public Rails web service and tell it what ActionCable URL to give to clients. This assumes that your `production.rb` has something like this: ```ruby config.after_initialize do config.action_cable.url = ENV.fetch("CABLE_URL", "/cable") if AnyCable::Rails.enabled? end ``` If so, all you need to do on Render is change the Rails app **Environment** variables again: * Set `CABLE_URL` to your new anycable service (e.g. `wss://ws.xylophone.com/cable`) (*Don't forget the **/cable** at the end!*) With that, your services should now all be able to talk to each other. However, you may run into some issues: ## cable.yml If you haven't already, make sure you've setup `cable.yml` properly for production: ```yml production: adapter: any_cable ``` ## redis-session-store config If, like me, you're using the `redis-session-store` gem for session handling, you may need to tweak the config some... ```ruby Rails.application.config.session_store( :redis_session_store, key: "_session_#{Rails.env}", serializer: :json, domain: :all, # <-- THIS IS IMPORTANT redis: { expire_after: 1.year, ttl: 1.year, key_prefix: "xylophone:session:", url: ENV["REDIS_URL"] } ) ``` ⚠️ Note the change to `domain: :all`. This ensures that your clients' session cookie key can be shared between your primary domain (`xylophone.com`) and your websocket subdomain (`ws.xylophone.com`) See also [Authentication](../rails/authentication.md). --- --- url: /anycable-go/api.md --- # REST API AnyCable provides a REST API to perform actions and get information about AnyCable installation. ## Authentication When the API secret is configured (either explicitly via `--api_secret` or derived from `--secret`), all API requests must include an `Authorization` header with a Bearer token: ``` Authorization: Bearer ``` ### Secret derivation If `--api_secret` is not explicitly set but `--secret` is provided, the API secret is automatically derived using HMAC-SHA256. This feature is used by AnyCable SDKs and usually you don't need to worry about. However, if you're not using any of the official SDKs, you can generate an API secret based on the private AnyCable secret as follows: ```sh echo -n 'api-cable' | openssl dgst -sha256 -hmac '' | awk '{print $2}' ``` Or, for example, in Ruby: ```ruby api_secret = OpenSSL::HMAC.hexdigest("SHA256", "", "api-cable") ``` ## Endpoints ### POST /api/publish Publish a broadcast message to connected clients. #### Request * **Method:** `POST` * **Path:** `/api/publish` (or `/publish` if custom path is configured) * **Content-Type:** `application/json` * **Authorization:** `Bearer ` (when authentication is enabled) #### Request Body The request body must be a JSON object (or an array of objects) with the following structure: ```json { "stream": "", "data": "", "meta": {} } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `stream` | string | Yes | The name of the stream to publish to | | `data` | string | Yes | The message payload (typically JSON-encoded) | | `meta` | object | No | Additional metadata for the publication | ##### Meta fields | Field | Type | Description | |-------|------|-------------| | `exclude_socket` | string | Client identifier (`sid` from welcome message) to exclude from recipients | #### Response | Status Code | Description | |-------------|-------------| | `201 Created` | Message published successfully | | `401 Unauthorized` | Missing or invalid authentication | | `422 Unprocessable Entity` | Invalid request method or malformed body | | `501 Not Implemented` | Server failed to process the broadcast | #### Examples **Basic publish:** ```sh curl -X POST \ -H "Content-Type: application/json" \ -H "Authorization: Bearer your-api-secret" \ -d '{"stream":"chat/1","data":"{\"message\":\"Hello, world!\"}"}' \ http://localhost:8080/api/publish ``` **Publish multiple messages (*batch*):** ```sh curl -X POST \ -H "Content-Type: application/json" \ -H "Authorization: Bearer your-api-secret" \ -d '[ {"stream":"chat/1","data":"{\"message\":\"First\"}"}, {"stream":"chat/2","data":"{\"message\":\"Second\"}"} ]' \ http://localhost:8080/api/publish ``` ### GET /presence/:stream/users Retrieve presence information for a specific stream. **NOTE:** This endpoint requires [presence](./presence.md) support to be enabled (available when using the Memory or Redis broker). #### Request * **Method:** `GET` * **Path:** `/api/presence/:stream/users` (or `/presence/:stream/users` if custom path is configured) * **Authorization:** `Bearer ` (when authentication is enabled) #### URL Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `stream` | string | Yes | The name of the stream to get presence information for | #### Response | Status Code | Description | |-------------|-------------| | `200 OK` | Presence information retrieved successfully | | `401 Unauthorized` | Missing or invalid authentication | | `404 Not Found` | Invalid path format | | `422 Unprocessable Entity` | Invalid request method (non-GET) | | `501 Not Implemented` | Presence is not supported by the current broker | #### Response Body The response body is a JSON object with the following structure: ```json { "type": "info", "total": 2, "records": [ { "id": "user-1", "info": { "name": "Alice" } }, { "id": "user-2", "info": { "name": "Bob" } } ] } ``` | Field | Type | Description | |-------|------|-------------| | `type` | string | Always `"info"` | | `total` | integer | Total number of unique presence records in the stream | | `records` | array | List of presence records (omitted if empty) | Each record in the `records` array contains: | Field | Type | Description | |-------|------|-------------| | `id` | string | Unique presence identifier for the user | | `info` | object | User-defined presence information | #### Examples **Get presence for a stream:** ```sh curl -X GET \ -H "Authorization: Bearer your-api-secret" \ http://localhost:8080/api/presence/chat%2F1/users ``` **Example response:** ```json { "type": "info", "total": 2, "records": [ { "id": "user-123", "info": { "name": "Alice", "status": "online" } }, { "id": "user-456", "info": { "name": "Bob", "status": "away" } } ] } ``` **Empty stream response:** ```json { "total": 0 } ``` ## Configuration The API server can be configured using the following options: | Option | Environment Variable | Default | Description | |--------|---------------------|---------|-------------| | `--api_port` | `ANYCABLE_API_PORT` | `0` | API server port. When set to `0`, the API runs on the main server port | | `--api_path` | `ANYCABLE_API_PATH` | `/api` | Base path for API endpoints | | `--api_secret` | `ANYCABLE_API_SECRET` | - | Secret token for API authentication (derived from the main secret if not passed) | **IMPORTANT:** The API is not available if all of the below holds: * No secret provided for AnyCable * No dedicated port configured for the API server (`--api_port`) * No public mode. In other words, API is not available when it's not protected one way or another. When the API is enabled, you will see a log message on startup: ```sh INFO Handle API requests at http://localhost:8080/api (authorization required) ``` Or, if running without authentication (on a separate port or in a public mode): ```sh INFO Handle API requests at http://localhost:8080/api (no authorization) INFO API server is running without authentication ``` ### CORS The API supports CORS (Cross-Origin Resource Sharing) for browser-based requests. CORS headers are automatically added when the server is configured with CORS support. Preflight `OPTIONS` requests are handled automatically and return a `200 OK` response with appropriate CORS headers. --- --- url: /ruby/middlewares.md --- # RPC middlewares AnyCable Ruby allows to add custom *middlewares* to the RPC server (both standalone gRPC and embedded HTTP versions). For example, `anycable-rails` ships with [the middleware](https://github.com/anycable/anycable-rails/blob/1-4-stable/lib/anycable/rails/middlewares/executor.rb) that integrate [Rails Executor](https://guides.rubyonrails.org/v7.1.0/threading_and_code_execution.html#framework-behavior) into RPC server. Exceptions handling is also implemented via [AnyCable middleware](https://github.com/anycable/anycable/blob/1-4-stable/lib/anycable/middlewares/exceptions.rb). ## Adding a custom middleware AnyCable middleware is a class inherited from `AnyCable::Middleware` and implementing `#call` method: ```ruby class PrintMiddleware < AnyCable::Middleware # request - is a request payload (incoming message) # handler - is a method (Symbol) of RPC handler which is called # meta - is a metadata (Hash) provided along with request def call(handler, request, meta) p request yield end end ``` **NOTE**: you MUST yield the execution to continue calling middlewares and the RPC handler itself. Activate your middleware by adding it to the middleware chain: ```ruby # anywhere in your app before AnyCable server starts AnyCable.middleware.use(PrintMiddleware) # or using instance AnyCable.middleware.use(ParameterizedMiddleware.new(params)) ``` --- --- url: /ruby/http_rpc.md --- # RPC over HTTP AnyCable supports RPC communication between a real-time server and your web application over HTTP. Although the default gRPC communication is more performant and, thus, preferred, it comes with a price of additional infrastructure complexity—you have to manage a separate process/service. AnyCable Ruby comes with a Rack middleware that implements AnyCable RPC API. You can mount it into your Rack-compatible application. > See the [demo](https://github.com/anycable/anycable_rails_demo/pull/1) of using HTTP RPC in a Rails application. Learn more about different RPC modes in the [AnyCable documentation](../anycable-go/rpc.md). ## Using with Rails To enable HTTP RPC in your Rails application, all you need is to configure the `http_rpc` or `http_rpc_mount_path` parameter. For example, in your `config/anycable.yml`: ```yml development: http_rpc: true # uses default /_anycable path production: http_rpc_mount_path: "/__some_other_anycable_path" ``` That's it! Now configure your AnyCable server to perform RPC over HTTP at your mount path (e.g., `/_anycable`). **NOTE:** If you don't use AnyCable gRPC server in any environment, you can avoid installing gRPC dependencies by using the `anycable-rails-core` gem instead of `anycable-rails`. ## Security You can (and MUST in production) protect your HTTP RPC server with basic token-based authorization. To do so, you need to set the `http_rpc_secret` parameter (in YAML or via the `ANYCABLE_HTTP_RPC_SECRET` environment variable). Don't forget to set the same value in your WebSocket server configuration. ## Considerations * **Performance**. HTTP/1 has a higher overhead than HTTP/2 used by gRPC, so you should expect a higher latency and lower throughput. Keep this in mind when choosing between HTTP RPC and gRPC. * **Shared web server resources**. Rails applications have a limited HTTP concurrency (based on the total number of threads used by a web server, such as Puma), serving both regular HTTP requests and AnyCable RPC requests can result into a race for shared resources, and, in the worst case, longer request queuing times for user-facing HTTP operations. * **Scalability**. It's not possible to scale AnyCable RPC requests separately from the main web application. If you need to scale AnyCable RPC requests independently, you should use gRPC. ## Using with Rack You can mount AnyCable HTTP RPC server into your Rack application using the Rack Builder interface: ```ruby Rack::Builder.new do map "/anycable" do run AnyCable::HTTPRC::Server.new end end ``` --- --- url: /anycable-go/sse.md --- # Server-sent events In addition to WebSockets and [long polling](./long_polling.md)), AnyCable also allows you to use [Server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) (SSE) as a transport for receiving live updates. SSE is supported by all modern browsers (see [caniuse](https://caniuse.com/eventsource)) and is a good alternative to WebSockets if you don't need to send messages from the client to the server and don't want to deal with Action Cable or AnyCable SDKs: you can use native browsers `EventSource` API to establish a reliable connection or set up HTTP streaming manually using your tool of choice (e.g., `fetch` in a browser, `curl` in a terminal, etc.). ## Configuration You must opt-in to enable SSE support in AnyCable. To do so, you must provide the `--sse` or set the `ANYCABLE_SSE` environment variable to `true`: ```sh $ anycable-go --sse INFO 2023-09-06T22:52:04.229Z context=main Starting GoBenchCable 1.4.4 (with mruby 1.2.0 (2015-11-17)) (pid: 39193, open file limit: 122880, gomaxprocs: 8) ... INFO 2023-09-06T22:52:04.229Z context=main Handle WebSocket connections at http://localhost:8080/cable INFO 2023-09-06T22:52:04.229Z context=main Handle SSE requests at http://localhost:8080/events ... ``` The default path for SSE connections is `/events`, but you can configure it via the `--sse_path` configuration option. ## Usage with EventSource The easiest way to use SSE is to use the native `EventSource` API. For that, you MUST provide a URL to the SSE endpoint and pass the channel information in query parameters (EventSource only supports GET requests, so we cannot use the body). For example: ```js const source = new EventSource("http://localhost:8080/events?channel=ChatChannel"); // Setup an event listener to handle incoming messages source.addEventListener("message", (e) => { // e.data contains the message payload as a JSON string, so we need to parse it console.log(JSON.parse(e.data)); }); ``` The snippet above will establish a connection to the SSE endpoint and subscribe to the `ChatChannel` channel. If you need to subscribe to a channel with parameters, you MUST provide the fully qualified channel identifier via the `identifier` query parameter: ```js const identifier = JSON.stringify({ channel: "BenchmarkChannel", room_id: 42, }); const source = new EventSource( `http://localhost:8080/events?identifier=${encodeURIComponent(identifier)}` ); // ... ``` **IMPORTANT**: You MUST specify either `channel` or `identifier` query parameters. If you don't, the connection will be rejected. ### Usage with signed/public streams > @since v1.5.1 When using with [signed streams](./signed_streams.md), you can provide the public or signed stream name via the `stream` or `signed_stream` parameter respectively: ```js const publicSource = new EventSource( `http://localhost:8080/events?stream=${encodeURIComponent(myStreamName)}` ); const signedSource = new EventSource( `http://localhost:8080/events?signed_stream=${encodeURIComponent(mySecretStreamName)}` ); ``` ### Reliability EventSource is a reliable transport, which means that it will automatically reconnect if the connection is lost. EventSource also keeps track of received messages and sends the last consumed ID on reconnection. To leverage this feature, you MUST enable AnyCable [reliable streams](./reliable_streams.md) functionality. No additional client-side configuration is required. **IMPORTANT**: EventSource is assumed to be used with a single stream of data. If you subscribe a client to multiple Action Cable streams (e.g., multiple `stream_from` calls), the last consumed ID will be sent only for the last observed stream. ### Requesting initial history > @since v1.5.1 You can also specify the timestamp (Unix seconds) from which request initial stream history (if any): ```js // Last 5 minutes const ts = ((Date.now() / 1000) - 5*60) | 0; const publicSourceWithHistory = new EventSource( `http://localhost:8080/events?stream=${encodeURIComponent(myStreamName)}&history_since=${ts}` ); ``` ### Unauthorized connections or rejected subscriptions If the connection is unauthorized or the subscription is rejected, the server will respond with a `401` status code and close the connection. EventSource will automatically reconnect after a short delay. Please, make sure you handle `error` events and close the connection if you don't want to reconnect. ## Usage with other HTTP clients You can also use any other HTTP client to establish a connection to the SSE endpoint. For example, you can use `curl`: ```sh $ curl -N "http://localhost:8080/events?channel=ChatChannel" event: welcome data: {"type":"welcome"} event: confirm_subscription data: {"type":"confirm_subscription","identifier":"{\"channel\":\"ChatChannel\"}"} event: ping data: {"type":"ping","message":1694041735} data: {"message":"hello"} ... ``` AnyCable also supports setting up a streaming HTTP connection via POST requests. In this case, you can provide a list of client-server commands in the request body using the JSONL (JSON lines) format. Note that you must process different event types yourself. See below for the format. ## Action Cable over SSE format The server-client communication format is designed as follows: * The `data` field contains the message payload. **IMPORTANT**: for clients connecting via a GET request, the payload only contains the `message` part of the original Action Cable payload; clients connecting via POST requests receive the full payload (e.g., `{"identifier":, "message": {"foo":1}}`). * The optional `event` field contains the message type (if any); for example, `welcome`, `confirm_subscription`, `ping` * The optional `id` field contains the message ID if reliable streaming is enabled. The message ID has a form or `//` (see [Extended Action Cable protocol](/misc/action_cable_protocol.md#action-cable-extended-protocol)) * The optional `retry` field contains the reconnection interval in milliseconds. We only set this field for `disconnect` messages with `reconnect: false` (it's set to a reasonably high number to prevent automatic reconnection attempts by EventSource). Here is an example of a stream of messages from the server: ```txt event: welcome data: {"type":"welcome"} event: confirm_subscription data: {"type":"confirm_subscription","identifier":"{\"channel\":\"ChatChannel\"}"} event: ping data: {"type":"ping","message":1694041735} data: {"message":"hello"} id: 1/chat_42/y2023 data: {"message":"good-bye"} id: 2/chat_42/y2023 data: {"identifier":"{\"channel\":\"ChatChannel\"}","message":{"message":"hello"}} id: 1/chat_42/y2023 data: {"identifier":"{\"channel\":\"ChatChannel\"}","message":{"message":"good-bye"}} id: 2/chat_42/y2023 event: ping data: {"type":"ping","message":1694044435} event: disconnect data: {"type":"disconnect","reason":"remote","reconnect":false} retry: 31536000000 ``` ### Raw data streaming > @since v1.5.2 In some cases, you may not want to receive protocol-level events (`welcome`, `confirm_subscription`) via an SSE stream (e.g., using with legacy clients). To consume only data messages, you can add an additional `?raw=1` option to the URL: ```sh $ curl -N "http://localhost:8080/events?channel=ChatChannel&raw=1" // no welcome or confirm_subscription or ping messages data: {"message":"hello"} ... ``` **NOTE:** This is only applicable to GET requests. --- --- url: /anycable-go/signed_streams.md --- # Signed streams AnyCable allows you to subscribe to *streams* without using *channels* (in Action Cable terminology). Channels is a great way to encapsulate business-logic for a given real-time feature, but in many cases all we need is a good old explicit pub/sub. That's where the **signed streams** feature comes into play. > You read more about the Action Cable abstract design, how it compares to direct pub/sub and what are the pros and cons from this [Any Cables Monthly issue](https://anycable.substack.com/p/any-cables-monthly-18). Don't forget to subscribe! Signed streams work as follows: * Given a stream name, say, "chat/2024", you generate its signed version using a **secret key** (see below on the signing algorithm) * On the client side, you subscribe to the "$pubsub" channel and provide the signed stream name as a `signed_stream_name` parameter * AnyCable process the subscribe command, verifies the stream name and completes the subscription (if verified). For verification, you MUST provide the **secret key** via the `--streams_secret` (`ANYCABLE_STREAMS_SECRET`) parameter for AnyCable. ## Full-stack example: Rails Let's consider an example of using signed stream in a Rails application. Assume that we want to subscribe a user with ID=17 to their personal notifications channel, "notifications/17". First, we need to generate a signed stream name: ```ruby signed_name = AnyCable::Streams.signed("notifications/17") ``` Or you can use the `#signed_stream_name` helper in your views ```erb
">
``` By default, AnyCable uses `Rails.application.secret_key_base` to sign streams. We recommend configuring a custom secret though (so you can easily rotate values at both ends, the Rails app and AnyCable servers). You can specify it via the `streams_secret` configuration parameter (in `anycable.yml`, credentials, or environment). Then, on the client side, you can subscribe to this stream as follows: ```js // using @rails/actioncable let subscription = consumer.subscriptions.create( {channel: "$pubsub", signed_stream_name: stream}, { received: (msg) => { // handle notification msg } } ) // using @anycable/web let channel = cable.streamFromSigned(stream); channel.on("message", (msg) => { // handle notification }) ``` Now you can broadcast messages to this stream as usual: ```ruby ActionCable.server.broadcast "notifications/#{user.id}", payload ``` ## Public (unsigned) streams Sometimes you may want to skip all the signing ceremony and use plain stream names instead. With AnyCable, you can do that by enabling the `--public_streams` option (or `ANYCABLE_PUBLIC_STREAMS=true`) for the AnyCable server: ```sh $ anycable-go --public_streams # or $ ANYCABLE_PUBLIC_STREAMS=true anycable-go ``` With public streams enabled, you can subscribe to them as follows: ```js // using @rails/actioncable let subscription = consumer.subscriptions.create( {channel: "$pubsub", stream_name: "notifications/17"}, { received: (msg) => { // handle notification msg } } ) // using @anycable/web let channel = cable.streamFrom("notifications/17"); channel.on("message", (msg) => { // handle notification }) ``` ## Signing algorithm We use the same algorithm as Rails uses in its [MessageVerifier](https://api.rubyonrails.org/v7.1.3/classes/ActiveSupport/MessageVerifier.html): 1. Encode the stream name by first converting it into a JSON string and then encoding in Base64 format. 2. Calculate a HMAC digest using the SHA256 hash function from the secret and the encoded stream name. 3. Concatenate the encoded stream name, a double dash (`--`), and the digest. Here is the Ruby version of the algorithm: ```ruby encoded = ::Base64.strict_encode64(JSON.dump(stream_name)) digest = OpenSSL::HMAC.hexdigest("SHA256", SECRET_KEY, encoded) signed_stream_name = "#{encoded}--#{digest}" ``` The JavaScript (Node.js) version: ```js import { createHmac } from 'crypto'; const encoded = Buffer.from(JSON.stringify(stream_name)).toString('base64'); const digest = createHmac('sha256', SECRET_KEY).update(encoded).digest('hex'); const signedStreamName = `${encoded}--${digest}`; ``` The Python version looks as follows: ```python import base64 import json import hmac import hashlib encoded = base64.b64encode(json.dumps(stream_name).encode('utf-8')).decode('utf-8') digest = hmac.new(SECRET_KEY.encode('utf-8'), encoded.encode('utf-8'), hashlib.sha256).hexdigest() signed_stream_name = f"{encoded}--{digest}" ``` The PHP version is as follows: ```php $encoded = base64_encode(json_encode($stream_name)); $digest = hash_hmac('sha256', $encoded, $SECRET_KEY); $signed_stream_name = $encoded . '--' . $digest; ``` ## Whispering *Whispering* is an ability to publish *transient* broadcasts from clients, i.e., without touching your backend. This is useful when you want to share client-only information from one connection to others. Typical examples include typing indicators, cursor position sharing, etc. Whispering must be enabled explicitly for signed streams via the `--streams_whisper` (`ANYCABLE_STREAMS_WHISPER=true`) option. Public streams always allow whispering. Here is an example client code using AnyCable JS SDK: ```js let channel = cable.streamFrom("chat/22"); channel.on("message", (msg) => { if (msg.event === "typing") { console.log(`user ${msg.name} is typing`); } }) // publishing whispers channel.whisper({event: "typing", name: user.name}) ``` ## Hotwire and CableReady support AnyCable can be used to serve Hotwire ([Turbo Streams](https://turbo.hotwired.dev/handbook/streams)) and [CableReady](https://cableready.stimulusreflex.com) (v5+) subscriptions right at the real-time server using the same signed streams functionality under the hood (and, thus, without performing any RPC calls to authorize subscriptions). In combination with [JWT authentication](./jwt_identification.md), this feature makes it possible to run AnyCable in a standalone mode for Hotwire/CableReady applications. > 🎥 Check out this [AnyCasts episode](https://anycable.io/blog/anycasts-rails-7-hotwire-and-anycable/) to learn how to use AnyCable with Hotwire Rails application in a RPC-less way. You must explicitly enable Turbo Streams or CableReady signed streams support at the AnyCable server side by specifying the `--turbo_streams` (`ANYCABLE_TURBO_STREAMS=true`) or `--cable_ready_streams` (`ANYCABLE_CABLE_READY_STREAMS=true`) option respectively. You must also provide the `--streams_secret` corresponding to the secret you use for Turbo/CableReady. You can configure them in your Rails application as follows: ```ruby # Turbo configuration # config/environments/production.rb config.turbo.signed_stream_verifier_key = "" # CableReady configuration # config/initializers/cable_ready.rb CableReady.configure do |config| config.verifier_key = "" end ``` You can also specify custom secrets for Turbo Streams and CableReady via the `--turbo_streams_secret` and `--cable_ready_secret` parameters respectively. --- --- url: /deployment/systemd.md --- # Systemd configurations If you prefer to run AnyCable without containerization, we recommend running it as a system service for better manageability. On most modern Linux distributions this can be done by declaring a [systemd](https://www.freedesktop.org/wiki/Software/systemd/) service like this: 1. Edit as needed and save the following script to `/etc/systemd/system/anycable-.service`. 2. Reload systemd configuration via `sudo systemctl daemon-reload`. 3. Start the service: `sudo systemctl start anycable-go anycable-rpc`. ## AnyCable RPC ```ini # /etc/systemd/system/anycable-rpc.service [Unit] Description=AnyCable gRPC Server After=syslog.target network.target [Service] Type=simple Environment=RAILS_ENV=production WorkingDirectory=/path-to-your-project/current/ ExecStart=bundle exec anycable # or if you're using rbenv/rvm # ExecStart=/bin/bash -lc 'bundle exec anycable' ExecStop=/bin/kill -TERM $MAINPID # Set user/group # User=www # Group=www # UMask=0002 # Set memory limits # MemoryHigh=2G # MemoryMax=3G # MemoryAccounting=true Restart=on-failure # Configure WebSocket server using env vars (see Configuration guide) # Environment=ANYCABLE_REDIS_URL=redis://localhost:6379/5 # Environment=ANYCABLE_REDIS_CHANNEL=__anycable__ [Install] WantedBy=multi-user.target ``` ## AnyCable-Go ```ini [Unit] Description=AnyCable Go WebSocket Server After=network.target [Service] Type=simple ExecStart=/usr/local/bin/anycable-go ExecStop=/bin/kill -TERM $MAINPID # User=xxx # Group=xxx UMask=0002 LimitNOFILE=16384 # increase open files limit (see OS Tuning guide) Restart=on-failure # Configure WebSocket server using env vars # Environment=ANYCABLE_HOST=localhost # Environment=ANYCABLE_PORT=8080 # Environment=ANYCABLE_PATH=/cable # Environment=ANYCABLE_REDIS_URL=redis://localhost:6379/5 # Environment=ANYCABLE_REDIS_CHANNEL=__anycable__ # Environment=ANYCABLE_RPC_HOST=localhost:50051 # Environment=ANYCABLE_METRICS_HTTP=/metrics [Install] WantedBy=multi-user.target ``` ## Resources * [Deploy AnyCable with Capistrano and systemd](https://jetrockets.com/blog/deploy-anycable-with-capistrano-and-systemd) by [JetRockets](https://jetrockets.com). --- --- url: /anycable-go/telemetry.md --- # Telemetry AnyCable v1.4+ collects **anonymous usage** information. Users are notified on the server start. Why collecting telemetry? One of the biggest issues in the open-source development is the lack of feedback (especially, when everything works as expected). Getting more insights on how AnyCable is used in the wild will help us to prioritize work on new features and improvements. ## Opting out You can disable telemetry by setting the `ANYCABLE_DISABLE_TELEMETRY` environment variable to `true`. ## What is collected We collect the following information: * AnyCable version. * OS name. * CI name (to distinguish CI runs from other runs). * Deployment platform (e.g., Heroku, Fly, etc.). * Specific features enabled (e.g., JWT identification, signed streams, etc.). * Max observed amount of RAM used by the process. * Max observed number of concurrent connections (this helps us to distinguish development/test runs from production ones). We **do not collect** personally-identifiable or sensitive information, such as: hostnames, file names, environment variables, or IP addresses. We use [Posthog](https://posthog.com/) to store and visualize data. --- --- url: /troubleshooting.md --- # Troubleshooting 🔥 ## `Failed to connect to Redis: unknown command 'CLIENT', with args beginning with: 'TRACKING'` Some managed Redis (e.g., Google Cloud) providers block many Redis commands, including [client-side server tracking](https://redis.io/commands/client-tracking/), which is enabled in AnyCable by default. If you experience this issue, set the `--redis_disable_cache` flag (or `ANYCABLE_REDIS_DISABLE_CACHE=true`). ## `ActionCable.server.broadcast` doesn't send messages The most common problem is using different Redis channels within RPC instance and `anycable-go`. Find the following line in the logs: ```sh # for AnyCable-Go $ anycable-go ... INFO time context=pubsub Subscribed to Redis channel: __anycable__ # for RPC $ bundle exec anycable ... I, [2019-03-06T10:08:03.915310 #7922] INFO -- : Broadcasting Redis channel: __anycable__ ``` Make sure that both servers use the same Redis channel (`__anycable__` in the example above). Related issues: [#78](https://github.com/anycable/anycable/issues/78), [#45](https://github.com/anycable/anycable/issues/45). ## Server raises an `NotImplementedError: nil` exception when client tries to connect If you encounter an exception like this: ```sh There was an exception - NotImplementedError(NotImplementedError) .../gems/actioncable-7.0.4/lib/action_cable/subscription_adapter/base.rb:22:in `unsubscribe':in ... ``` that means you're client tries to connect to the built-in Action Cable server, and not the AnyCable one. Check that: * `config.action_cable.url` points to the AnyCable server and not to the Rails one * make sure your client is configured to connect to AnyCable server * drop `mount ActionCable.server => "/cable"` from your `routes.rb` * in case of using a reverse proxy (e.g. Nginx), check that it points to the correct server as well. Related issues: [#181](https://github.com/orgs/anycable/discussions/181), [#88](https://github.com/anycable/anycable-rails/issues/88), [#22](https://github.com/anycable/anycable-rails/issues/22). ## Authentication fails with `undefined method 'protocol' for nil:NilClass` That could happen if there is a monkey-patch overriding the default Action Cable behaviour. For example, `lograge` [does this](https://github.com/roidrage/lograge/pull/257#issuecomment-525690256) (at least versions <= 0.11.2). Related issues: [#103](https://github.com/anycable/anycable-rails/issues/103). ## My WebSocket connection fails with "Auth failed" error It's likely that you're using cookie-based authentication. There are several things that could break here. 1. Cross-domain cookies. Make sure that your cookies are accessible from both domain (HTTP server and WebSocket server). For example: ```ruby # session_store.rb Rails.application.config.session_store :cookie_store, key: "_any_cable_session", domain: :all # or domain: '.example.com' # anywhere setting cookie cookies[:val] = {value: "1", domain: :all} ``` **NOTE**: It's impossible to set cookies for `.herokuapp.com`. [Read more](https://devcenter.heroku.com/articles/cookies-and-herokuapp-com). 2. `SECRET_KEY_BASE` vs. encrypted cookies. Make sure both RPC and web apps use the same `Rails.application.secret_key_base` (usually provided via credentials or `ENV['SECRET_KEY_BASE']`). If they don't match, cookies decryption would silently fail. Related issues: [#135](https://github.com/anycable/anycable-rails/issues/135). 3. Middlewares configuration. There could be a situation when AnyCable middleware chain behaves differently from Rails middleware chain. Make sure you include only the necessary middlewares in the right order and with correct parameters. Keep in mind that `anycable-rails` automatically includes the corresponding session store middleware in the beginning of the AnyCable chain, so you don't need to add it yourself. Related issues: [#156](https://github.com/anycable/anycable-rails/issues/156). ## I see a lot of `too many open files` errors in the log Congratulations! You have a lot (thousands) of simultaneous connections (and you hit the max open files limit). For example, Heroku *standard* dynos have a limit (both soft and hard) of 10000 open files, and *performance* dynos have a limit of 1048576. Does this mean that the theoretical limit of connections is 10k for standard dynos? Yes, but only theoretical. But in practice, the process doesn't free open files (sockets in our case) immediately after disconnection; it waits for TCP close handshake to finish (and you can find such sockets in `CLOSE_WAIT` state). So, if a lot of clients dropping connections, the actual limit on the number of active connections could be much less at the specific moment. See the [OS tuning](anycable-go/os_tuning.md) guide for possible solutions. Related issues: [#79](https://github.com/anycable/anycable-rails/issues/79). ## Problems with Docker alpine images AnyCable ruby gem relies on [`google-protobuf`](https://rubygems.org/gems/google-protobuf) and [`grpc`](https://rubygems.org/gems/grpc) gems, which use native extensions. That could bring some problems installing `anycable` on alpine Docker images due to the incompatibility of the pre-built binaries. Usually, building the gems from source: ```ruby # use a gem from git gem "google-protobuf", git: "https://github.com/google/protobuf" ``` Another option is to force Bundler to build native extensions during the installation: ```sh BUNDLE_FORCE_RUBY_PLATFORM=1 bundle install ``` Or on per-gem basis using [this hack](https://github.com/grpc/grpc/issues/21514#issuecomment-581417788). See the [example Dockerfile](https://github.com/anycable/anycable/blob/master/etc/Dockerfile.alpine). Another option to consider is switching to `grpc_kit` gem. See [documentation](./ruby/configuration.md#alternative-grpc-implementations) for more details. Related issues: [#70](https://github.com/anycable/anycable-rails/issues/70), [#47](https://github.com/anycable/anycable/issues/47). ## Client connection fails with `ActionController::RoutingError (No route matches [GET] "/cable")` This exception means that your client attempts to connect to the Rails server not to AnyCable WebSocket server. Check that: * `config.action_cable.url` points to the AnyCable server and not to the Rails one. * Make sure that `action_cable_meta_tag` is called before JS script is loaded. * Make sure you do not pass incorrect URL to JS `createConsumer` question. * In case of using a reverse proxy (e.g. Nginx), check that it points to the correct server as well. Related issues: [#115](https://github.com/anycable/anycable-rails/issues/115) ## Websocket connections are not closed by load balancer Check out the [#83](https://github.com/anycable/anycable-go/issues/83) and [this comment](https://github.com/anycable/anycable-go/issues/83#issuecomment-597769178) in particular. ## RPC error: `missing selected ALPN property` In the recent versions, `grpc-go` introduced APLN enforcement which may cause connectivity issues. A quick fix is to provide the `GRPC_ENFORCE_ALPN_ENABLED=false` environment variable. See issues: [#256](https://github.com/anycable/anycable/issues/256). ## Abnormal socket closure spikes (AWS ALB) You may experience abnormal socket closure spikes (the `abnormal_socket_closure_total` metrics) leading to RPC load spikes when using AWS ALB. AWS ALBs scale in and out based on traffic patterns: they disconnect all remaining connections after a grace period (unknown), thus, persistent websocket connections are killed and showing up as abnormal socket closures. That could be 100s of killed connections per second causing load on gRPC servers (unless disconnect notices are disabled). AWS does not expose any ALB scale related metrics or events, so there is currently no good workaround for this problem. --- --- url: /websocket_servers.md --- --- --- url: /upgrade-notes/Readme.md --- # Upgrade notes * [From v1.0.x to v1.1.0](1_0_0_to_1_1_0.md) * [From v0.6.x to v1.0.0](0_6_0_to_1_0_0.md) --- --- url: /upgrade-notes/0_6_0_to_1_0_0.md --- # Upgrading from 0.6.x to 1.0.0 This document contains only the changes comparing to v0.6.x releases. For the new features see the [release notes](../release_notes.md). ## Upgrade process Since AnyCable-Go v1.0 is backward-compatible with Ruby gems of 0.6.x series, we recommend the following upgrade process: 1. Upgrade AnyCable-Go. 2. Upgrade Ruby gems. ## Rails/Ruby ### General Ruby 2.5+ is required. AnyCable-Go 1.0+ is required. ### Default RPC server host changed We used `[::]:50051` by default which could be harmful (see [#71](https://github.com/anycable/anycable/pull/71)). Now AnyCable RPC uses a loopback interface as the default (`127.0.0.1:50051`). ### Session access hacks are not required anymore Rails `session` object in Action Cable connection is supported out-of-the-box. No additional hacking is needed. ### Use AnyCable Rack middleware to support `env["warden"]` For example, for Devise all you need is to add the following configuration: ```ruby AnyCable::Rails::Rack.middleware.use Warden::Manager do |config| Devise.warden_config = config end ``` See also [Authentication](../rails/authentication.md). ## `anycable-go` ### Default host changed The default value for HTTP server host has changed from `0.0.0.0` to `localhost`. If you want to continue using `0.0.0.0`, configure your server explicitly: ```sh $ anycable-go --host=0.0.0.0 # or via env vars $ ANYCABLE_HOST=0.0.0.0 anycable-go ``` ### Docker images versioning Docker images versioning changed from `vX.Y.Z` to `X.Y.Z`. Now you can specify only the part of the version, e.g. `anycable-go:1.0` instead of the full `anycable-go:v1.0.0`. --- --- url: /upgrade-notes/1_0_0_to_1_1_0.md --- # Upgrading from 1.0.x to 1.1.0 This document contains only the changes comparing to v1.0.x releases. For the new features see the [release notes](../release_notes.md). ## Upgrade process You can upgrade Ruby gems and Go server in any order. ## Rails/Ruby ### General Ruby 2.6+ is required. Rails 6.0+ is required. Anyway Config 2.1+ is required. ### RPC middlewares API change Middlewares are no longer inherited from gRPC interceptors. That allowed us to have *real* middlewares with ability to modify responses, intercept exceptions, etc. The API changed a bit: ```diff class SomeMiddleware < AnyCable::Middleware - def call(request, rpc_call, rpc_handler) + def call(rpc_method_name, request, metadata) yield end end ``` See [built-in middlewares](https://github.com/anycable/anycable/tree/master/lib/anycable/middlewares), for example. ## `anycable-go` ### Configuration changes Renamed `metrics_log_interval` to `metrics_rotate_interval`. The older name is deprecated and will be removed in the next major/minor release. --- --- url: /upgrade-notes/1_2_0_to_1_3_0.md --- # Upgrading from 1.1.x/1.2.x to 1.3.0 This document contains only the changes comparing to v1.1.x releases. For the new features see the [release notes](../release_notes.md). ## Upgrade process You can upgrade Ruby gems and Go server in any order. ## Rails/Ruby ### Max connection age for RPC AnyCable now sets the max connection age for gRPC connections to 5 minutes by default. It's important to re-validate gRPC connections in case of DNS-based load balancing to react on formation changes (e.g., adding new RPC servers). If you set `ANYCABLE_RPC_SERVER_ARGS__MAX_CONNECTION_AGE_MS` (or `rpc_server_args.max_connection_age_ms`) to 300000 (5 minutes), you can remove this setting. If you used a different value for max connection age, you can provide it via the new configuration parameter—`rpc_max_connection_age` (or `ANYCABLE_RPC_MAX_CONNECTION_AGE`). **NOTE:** The new parameter accepts seconds, not milliseconds. ### Rails 7 reporter integration The `anycable-rails` library now integrates with Rails 7+ error reporter interface automatically (since v1.3.6). If your error reporting software supports Rails built-in error reporting (e.g., Sentry does), you no longer need to configure `AnyCable.capture_exception { ... }` yourself. --- --- url: /upgrade-notes/1_3_0_to_1_4_0.md --- # Upgrading from 1.3.x to 1.4.0 This document contains only the changes compared to v1.3.x releases. For the new features see the [release notes](../release_notes.md). ## Upgrade process You can upgrade Ruby gems and Go server in any order. However, if you want to migrate to the new broadcasting architecture, you should follow the steps described below. ## New broadcasting architecture AnyCable-Go v1.4 ships with a [new broadcasting architecture](../anycable-go/pubsub.md) aimed to provide better consistency for Action Cable applications. If you were using the Redis broadcast adapter, you can migrate to the new [Redis X](../ruby/broadcasting.md#redis-x) adapter by following the steps below: * Make sure you use Redis 6.2+. * Configure AnyCable-Go to use both `redis` and `redisx` adapters as well as `redis` pubsub (if you have more than one AnyCable-Go instance): ```sh ANYCABLE_BROADCAST_ADAPTER=redisx,redis \ ANYCABLE_PUBSUB=redis anycable-go # or anycable-go --broadcast_adapter=redisx,redis --pubsub=redis ``` * Upgrade Rails application to use the `redisx` adapter. * Remove the `redis` adapter from the AnyCable-Go configuration. ## AnyCable-Go ### Disconnect mode We introduce new configuration option, `--disconnect_mode`, in favor of `--disable_disconnect`. The new option has three possible values: "auto" (default), "never" and "always". If you used `--disable_disconnect`, you should either switch to `--disconnect_mode=never` or remove the option and use the default one depending on your use case. See [documentation](../anycable-go/configuration.md#disconnect-events-settings) for more details. --- --- url: /upgrade-notes/1_4_0_to_1_5_0.md --- # Upgrading from 1.4.x to 1.5.0 This document contains only the changes compared to v1.4.x releases. For the new features see the [release notes](../release_notes.md). ## Upgrade process You can upgrade AnyCable server and your backend SDK (Ruby, JS) in any order. As soon you upgrade both, you can migrate your secrets (see below). ## Secrets management AnyCable now supports a common **application secret**—a single secret that can be used to secure all the features: HTTP broadcasts, HTTP RPC, JWT tokens, Turbo streams. You MAY continue using separate secrets for all the features. However, if you'd like to migrate to the application secret, we recommend to plan the migration thoroughly to avoid service disruptions. There is no one recipe to fit all AnyCable setups. Feel free to contact us if you need help! ## AnyCable Rails ### JWT The `anycable-rails-jwt` has been merged into `anycable` and `anycable-rails` gems. Remove `anycable-rails-jwt` from your Gemfile. If you used `AnyCable::Rails::JWT` module explicitly in your code, update it to `AnyCable::JWT` and pass identifiers as a Hash: ```diff - AnyCable::Rails::JWT.encode(current_user:, expires_at: 10.minutes.from_now) + AnyCable::JWT.encode({current_user:}, expires_at: 10.minutes.from_now) ``` ### Configuration changes Some configuration parameters has been renamed as follows: * `http_broadcast_secret` -> `broadcast_key` * `jwt_id_key` -> `jwt_secret` * `jwt_id_ttl` -> `jwt_ttl` * `jwt_id_param` -> `jwt_param` ## AnyCable server (Go) Some configuration parameters has been renamed as follows: * `http_broadcast_secret` -> `broadcast_key` * `jwt_id_key` -> `jwt_secret` * `jwt_id_ttl` -> `jwt_ttl` * `jwt_id_param` -> `jwt_param` * `jwt_id_enforce` -> `enforce_jwt` * `turbo_rails_key` -> `turbo_secret` + `turbo_streams` * `cable_ready_key` -> `cable_ready_secret` + `cable_ready` Older parameter names are still supported but deprecated and will be removed in v2. --- --- url: /guides/client-side.md --- # Using AnyCable JS SDK > See the full documentation at [anycable/anycable-client](https://github.com/anycable/anycable-client). Even though AnyCable server utilizes Action Cable protocol and, thus, can be used with the existing Action Cable client libraries (such as `@rails/actioncable`), we recommend using AnyCable JS SDK for the following reasons: * Multi-platform out-of-the-box (web, workers, React Native, Node.js). * TypeScript support. * Extended protocol support (e.g., [binary formats](./anycable-go/binary_formats.md)). * AnyCable-specific features support (e.g., [reliable streams](./anycable-go/reliable_streams.md) and [signed streams](./anycable-go/signed_streams.md)). * Better [Turbo Streams support](#hotwire-integration) * ... and more. ## Quick start You can install AnyCable JS SDK via npm/yard/pnpm: ```bash npm install @anycable/web yarn add @anycable/web pnpm install @anycable/web ``` The `@anycable/web` package is assumed to be used in the browser environment. If you want to use AnyCable client in a non-web environment (e.g., Node.js), you should use `@anycable/core` package. Then you can use it in your application. First, you need to create a *cable* (or *consumer* as it's called in Action Cable): ```js // cable.js import { createCable } from '@anycable/web' export default createCable({ // There are various options available. For example: // - Enable verbose logging logLevel: 'debug', // - Use the extended Action Cable protocol protocol: 'actioncable-v1-ext-json', }) ``` Typically, the cable is a singleton in your application. You create it once for the whole lifetime of your application. ### Pub/Sub You can subscribe to data streams as follows: ```js import cable from 'cable'; const chatChannel = cable.streamFrom('room/42'); chatChannel.on('message', (msg) => { // ... }); ``` In most cases, however, you'd prefer to use secured (*signed*) stream names generated by your backend (see [signed streams](./anycable-go/signed_streams.md)): ```js const cable = createCable(); const signedName = await obtainSignedStreamNameFromWhenever(); const chatChannel = cable.streamFromSigned(signedName); // ... ``` ### Channels AnyCable client provides multiple ways to subscribe to channels: class-based subscriptions and *headless* subscriptions. > \[!TIP] > Read more about the concept of channels and how AnyCable uses it [here](./anycable-go/rpc). #### Class-based subscriptions Class-based APIs allows provides an abstraction layer to hide implementation details of subscriptions. You can add additional API methods, dispatch custom events, etc. Let's consider an example: ```js import { Channel } from '@anycable/web' // channels/chat.js export default class ChatChannel extends Channel { // Unique channel identifier (channel class for Action Cable) static identifier = 'ChatChannel' async speak(message) { return this.perform('speak', { message }) } receive(message) { if (message.type === 'typing') { // Emit custom event when message type is 'typing' return this.emit('typing', message) } // Fallback to the default behaviour super.receive(message) } } ``` Then, you can you this class to create a channel instance and subscribe to it: ```js import cable from 'cable' import { ChatChannel } from 'channels/chat' // Build an instance of a ChatChannel class. const channel = new ChatChannel({ roomId: '42' }) // Subscribe to the server channel via the client. cable.subscribe(channel) // return channel itself for chaining // Wait for subscription confirmation or rejection // NOTE: it's not necessary to do that, you can perform actions right away, // the channel would wait for connection automatically await channel.ensureSubscribed() // Perform an action let _ = await channel.speak('Hello') // Handle incoming messages channel.on('message', msg => console.log(`${msg.name}: ${msg.text}`)) // Handle custom typing messages channel.on('typing', msg => console.log(`User ${msg.name} is typing`)) // Or subscription close events channel.on('close', () => console.log('Disconnected from chat')) // Or temporary disconnect channel.on('disconnect', () => console.log('No chat connection')) // Unsubscribe from the channel (results in a 'close' event) channel.disconnect() ``` #### Headless subscriptions *Headless* subscriptions are very similar to Action Cable client-side subscriptions except from the fact that no mixins are allowed (you classes in case you need them). Let's rewrite the same example using headless subscriptions: ```js import cable from 'cable' const subscription = cable.subscribeTo('ChatChannel', { roomId: '42' }) const _ = await subscription.perform('speak', { msg: 'Hello' }) subscription.on('message', msg => { if (msg.type === 'typing') { console.log(`User ${msg.name} is typing`) } else { console.log(`${msg.name}: ${msg.text}`) } }) ``` ## Migrating from @rails/actioncable AnyCable JS SDK comes with a compatibility layer that allows you to use it as a drop-in replacement for `@rails/actioncable`. All you need is to change the imports: ```diff - import { createConsumer } from "@rails/actioncable"; + import { createConsumer } from "@anycable/web"; // createConsumer accepts all the options available to createCable export default createConsumer(); ``` Then you can use `consumer.subscriptions.create` as before (under the hood a headless channel would be create). ## Hotwire integration You can also use AnyCable JS SDK with Hotwire (Turbo Streams) to provide better real-time experience and benefit from AnyCable features. For that, you must install the [`@anycable/turbo-stream` package](https://github.com/anycable/anycable-client/tree/master/packages/turbo-stream). Here is how to switch `@hotwired/turbo` to use AnyCable client: ```js // IMPORTANT: Do not import turbo-rails, just turbo // import "@hotwired/turbo-rails"; import "@hotwired/turbo"; import { start } from "@anycable/turbo-stream"; import cable from "cable" start(cable, { delayedUnsubscribe: true }) ``` --- --- url: /ruby/non_rails.md --- # Using AnyCable Ruby without Rails AnyCable Ruby can be used without Rails, thus, allowing you to bring real-time and Action Cable-like functionality into your Ruby application. ## Installation Add `anycable` gem to your `Gemfile`: ```ruby gem "anycable", "~> 1.1" # when using Redis-backed broadcast adapter gem "redis", ">= 4.0" # when using NATS-backed broadcast adapter gem "nats-pure", "~> 2" ``` If you don't plan to use *channels* (RPC), you can go with the `anycable-core` gem instead of `anycable`. ## Pub/Sub only mode To use AnyCable in a standalone (pub/sub only) mode (see [docs](../anycable-go/getting_started.md)), you need to use the following APIs: * Broadcasting. See the [corresponding documentation](./broadcast_adapters.md). * JWT authentication. You can generate AnyCable JWT tokens as follows: ```ruby # Client entitity identifiers (usually, user ID or similar) identifiers = {user_id: User.first.id} token = AnyCable::JWT.encode(identifiers) ``` * Signed streams. You the following API to sign streams: ```ruby signed_name = AnyCable::Streams.signed("chat/42") ``` **NOTE:** For JWT authentication and signed streams, the application secret or dedicated secrets MUST be provided. The values MUST match the ones configured at the AnyCable server side. ## Using channels via Lite Cable There is a ready-to-go framework – [Lite Cable](https://github.com/palkan/litecable) – which can be used for application logic. It also supports AnyCable out-of-the-box. > Learn how to use AnyCable with Hanami in the ["AnyCable off Rails: connecting Twilio streams with Hanami"](https://evilmartians.com/chronicles/anycable-goes-off-rails-connecting-twilio-streams-with-hanami) blog post. ## Custom channels implementation You can build your own framework to use as *logic-handler* for AnyCable. AnyCable initiates a *connection* object for every request using user-provided factory: ```ruby # Specify factory AnyCable.connection_factory = MyConnectionFactory # And then AnyCable calls .call method on your factory connection = factory.call(socket, **options) ``` Where: * `socket` – is an object, representing client's socket (say, *socket stub*) (see [socket.rb](https://github.com/anycable/anycable/blob/master/lib/anycable/socket.rb)) * `options` may contain: * `identifiers`: a JSON string returned by `connection.identifiers_json` on connection (see below) * `subscriptions`: a list of channels identifiers for the connection. Connection interface: ```ruby class Connection # Called on connection def handle_open end # Called on disconnection def handle_close end # Called on incoming message. # Client send a JSON-encoded message of the form { "identifier": ..., "command": ..., "data" ... }. # - identifier – channel identifier (e.g. `{"channel":"chat","id":1}`) # - command – e.g. "subscribe", "unsubscribe", "message" # - any additional data def handle_channel_command(identifier, command, data) # ... end # Returns any string which can be used later in .create function to initiate connection. def identifiers_json end end ``` `Connection#handle_channel_command` should return truthy value on success (i.e., when a subscription is confirmed, or action is called). *NOTE*: connection instance is initiated on every request, so it should be stateless (except `identifiers_json`). To send a message to a client, you should call `socket#transmit`. For manipulating with streams use `socket#subscribe`, `socket#unsubscribe` and `socket#unsubscribe_from_all`. To persist client states between RPC calls you can use `socket#cstate` (connection state) and `socket#istate` (per-channel state), which are key-value stores (keys and values must both be strings). See [test factory](https://github.com/anycable/anycable/blob/master/spec/support/test_factory.rb) for example. --- --- url: /guides/serverless.md --- # Using AnyCable to power serverless JavaScript applications AnyCable is a great companion for your serverless JavaScript (and TypeScript) applications needing real-time features. It can be used as a real-time server with no strings attached: no vendor lock-in, no microservices spaghetti, no unexpected PaaS bills. Keep your logic in one place (your JS application) and let AnyCable handle the low-level stuff. ## Overview To use AnyCable with a serverless JS application, you need to: * Deploy AnyCable server to a platform of your choice (see [below](#deploying-anycable)). * Configure AnyCable API handler in your JS application. * Use [AnyCable Client SDK][anycable-client] to communicate with the AnyCable server from your client. AnyCable will handle WebSocket/SSE connections and translate incoming commands into API calls to your serverless functions, where you can manage subscriptions and respond to commands. Broadcasting real-time updates is as easy as performing POST requests to AnyCable. Luckily, you don't need to write all this code from scratch. Our JS SDK makes it easy to integrate AnyCable with your serverless application. ### Standalone real-time server You can run AnyCable in a standalone mode by using [signed pub/sub streams](../anycable-go/signed_streams.md) and [JWT authentication](../anycable-go/jwt_identification.md). In this case, all real-time actions are pre-authorized and no API handlers are required. > Check out this Next.js demo chat application running fully within Stackblitz and backed by AnyCable pub/sub streams: [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/edit/anycable-pubsub?file=README.md) ## AnyCable Serverless SDK [AnyCable Serverless SDK][anycable-serverless-js] is a Node.js package that provides a set of helpers to integrate AnyCable into your JavaScript backend application. > Check out our demo Next.js application to see the complete example: [vercel-anycable-demo][] AnyCable Serverless SDK contains the following components: * JWT authentication and signed streams. * Broadcasting. * Channels. ### JWT authentication AnyCable support [JWT-based authentication](../anycable-go/jwt_identification.md). With the SDK, you can generate tokens as follows: ```js import { identificator } from "@anycable/serverless-js"; const jwtSecret = "very-secret"; const jwtTTL = "1h"; export const identifier = identificator(jwtSecret, jwtTTL); // Then, somewhere in your code, generate a token and provide it to the client const userId = authenticatedUser.id; const token = await identifier.generateToken({ userId }); ``` ### Signed streams SDK provides functionality to generate [signed stream names](/anycable-go/signed_streams). For that, you can create a *signer* instance with the corresponding secret: ```js import { signer } from "@anycable/serverless-js"; const streamsSecret = process.env.ANYCABLE_STREAMS_SECRET; const sign = signer(secret); const signedStreamName = sign("room/13"); ``` Then, you can use the generated stream name with your client (using [AnyCable JS client SDK](https://github.com/anycable/anycable-client)): ```js import { createCable } from "@anycable/web"; const cable = createCable(WEBSOCKET_URL); const stream = await fetchStreamForRoom("13"); const channel = cable.streamFromSigned(stream); channel.on("message", (msg) => { // handle notification }) ``` ### Broadcasting SDK provides utilities to publish messages to AnyCable streams via HTTP: ```js import { broadcaster } from "@anycable/serverless-js"; // Broadcasting configuration const broadcastURL = process.env.ANYCABLE_BROADCAST_URL || "http://127.0.0.1:8090/_broadcast"; const broadcastKey = process.env.ANYCABLE_BROADCAST_KEY || ""; // Create a broadcasting function to send broadcast messages via HTTP API export const broadcastTo = broadcaster(broadcastURL, broadcastKey); // Now, you can use the initialized broadcaster to publish messages broadcastTo("chat/42", message); ``` Learn more about [broadcasting](../anycable-go/broadcasting.md). ### Channels Channels help to encapsulate your real-time logic and enhance typical pub/sub capabilities with the ability to handle incoming client commands. For example, a channel representing a chat room may be defined as follows: ```js import { Channel } from "@anycable/serverless-js"; export default class ChatChannel extends { // The `subscribed` method is called when the client subscribes to the channel // You can use it to authorize the subscription and set up streaming async subscribed(handle, params) { // Subscribe requests may contain additional parameters. // Here, we require the `roomId` parameter. if (!params?.roomId) { handle.reject(); return; } // We set up a subscription; now, the client will receive real-time updates // sent to the `room:${params.roomId}` stream. handle.streamFrom(`room:${params.roomId}`); } // This method is called by the client async sendMessage(handle, params, data) { const { body } = data; if (!body) { throw new Error("Body is required"); } const message = { id: Math.random().toString(36).substr(2, 9), body, createdAt: new Date().toISOString(), }; // Broadcast the message to all subscribers (see below) await broadcastTo(`room:${params.roomId}`, message); } } ``` Channels are registered within an *application* instance, which can also be used to authenticate connections (if JWT is not being used): ```js import { Application } from "@anycable/serverless-js"; import ChatChannel from "./channels/chat"; // Application instance handles connection lifecycle events class CableApplication extends Application { async connect(handle) { // You can access the original WebSocket request data via `handle.env` const url = handle.env.url; const params = new URL(url).searchParams; if (params.has("token")) { const payload = await verifyToken(params.get("token")!); if (payload) { const { userId } = payload; // Here, we associate user-specific data with the connection handle.identifiedBy({ userId }); } return; } // Reject connection if not authenticated handle.reject(); } async disconnect(handle: ConnectionHandle) { // Here you can perform any cleanup work console.log(`User ${handle.identifiers?.userId} disconnected`); } } // Create an instance of the class to use in HTTP handlers (see the next section) const app = new CableApplication(); // Register channel app.register("chat", ChatChannel); ``` To connect your channels to an AnyCable server, you MUST add AnyCable API endpoint to your HTTP server (or serverless function). The SDK provides HTTP handlers for that. Here is an example setup for Next.js via [Vercel serverless functions](https://vercel.com/docs/functions/serverless-functions): ```js // api/anycable/route.ts import { NextResponse } from "next/server"; import { handler, Status } from "@anycable/serverless-js"; // Your cable application instance import app from "../../cable"; export async function POST(request: Request) { try { const response = await handler(request, app); return NextResponse.json(response, { status: 200, }); } catch (e) { console.error(e); return NextResponse.json({ status: Status.ERROR, error_msg: "Server error", }); } } ``` You can use our [AnyCable Client SDK][anycable-client] on the client side. The corresponding code may look like this: ```js import { createCable, Channel } from "@anycable/web"; //Set up a connection export const cable = createCable(); //Define a client-side class for the channel export class ChatChannel extends Channel { static identifier = "chat"; sendMessage(message: SentMessage) { this.perform("sendMessage", message); } } // create a channel instance const channel = new ChatChannel({ roomId }); // subscribe to the server-side channel cable.subscribe(channel); channel.on("message", (message) => { console.log("New message", message); }); // perform remote commands channel.sendMessage({ body: "Hello, world!" }); ``` **NOTE:** Both serverless and client SDKs support TypeScript so that you can leverage the power of static typing in your real-time application. ## Deploying AnyCable > The quickest way to get AnyCable is to use our managed (and free) solution: [plus.anycable.io](https://plus.anycable.io) AnyCable can be deployed anywhere from modern clouds to good old bare-metal servers. Check out the [deployment guide](../deployment.md) for more details. We recommend using [Fly][], as you can deploy AnyCable in a few minutes with just a single command: ```sh fly launch --image anycable/anycable-go:1.5 --generate-name --ha=false \ --internal-port 8080 --env PORT=8080 \ --env ANYCABLE_SECRET= \ --env ANYCABLE_PRESETS=fly,broker \ --env ANYCABLE_RPC_HOST=https:///api/anycable ``` ## Running AnyCable locally There are plenty of ways of installing `anycable-go` binary on your machine (see \[../anycable-go/getting\_started.md]). For your convenience, we also provide an NPM package that can be used to install and run `anycable-go`: ```sh npm install --save-dev @anycable/anycable-go pnpm install --save-dev @anycable/anycable-go yarn add --dev @anycable/anycable-go # and run as follows npx anycable-go ``` **NOTE:** The version of the NPM package is the same as the version of the AnyCable-Go binary (which is downloaded automatically on the first run). [vercel-anycable-demo]: https://github.com/anycable/vercel-anycable-demo [Fly]: https://fly.io [anycable-serverless-js]: https://github.com/anycable/anycable-serverless-js [anycable-client]: https://github.com/anycable/anycable-client --- --- url: /guides/hotwire.md --- # Using AnyCable with Hotwire AnyCable be used as a [Turbo Streams][] backend for **any application**, not only Ruby or Rails. ## Rails applications Since [turbo-rails][] uses Action Cable under the hood, no additional configuration is required to use AnyCable with Hotwired Rails applications. See the [getting started guide](../rails/getting_started.md) for instructions. We recommend using AnyCable in a *standalone mode* (i.e., without running an [RPC server](../anycable-go/rpc.md)) for applications only using Action Cable for Turbo Streams. For that, you must accomplish the following steps: * Generate AnyCable **application secret** for your application and store it in the credentials (`anycable.secret`) or the environment variable (`ANYCABLE_SECRET`). * Enable JWT authentication by using the `action_cable_with_jwt_meta_tag(**identifiers)` helper instead of the `action_cable_meta_tag` (see [docs](../rails/authentication.md)). * Configure Turbo to use your AnyCable application secret for signing streams: ```ruby # config/environments/*.rb config.turbo.signed_stream_verifier_key = "" # or # config/application.rb config.turbo.signed_stream_verifier_key = AnyCable.config.secret ``` * Enable Turbo Streams support for AnyCable server: ```sh ANYCABLE_SECRET=your-secret \ ANYCABLE_TURBO_STREAMS=true \ anycable-go # or via cli args anycable-go --secret=your-secret --turbo_streams ``` That's it! Now you Turbo Stream connections are served solely by AnyCable server. ## Other frameworks and languages Hotwire is not limited to Ruby on Rails. You can use Turbo with any backend. Live updates via Turbo Streams, however, require a *connection* to receive the updates. This is where AnyCable comes into play. You can use AnyCable as a real-time server for Turbo Streams as follows: * Use [JWT authentication](../anycable-go/jwt_identification.md) to authenticate connections (or run your AnyCable server with authentication disabled via the `--noauth` option) * Enable [Turbo signed streams](../anycable-go/signed_streams.md#hotwire-and-cableready-support) support. * Configure your backend to broadcast Turbo Streams updates via AnyCable (see [broadcasting documentation](../anycable-go/broadcasting.md)). With this setup, you can use `@hotwired/turbo-rails` or [@anycable/turbo-stream][] JavaScript libraries in your application without any modification. ## Turbo Streams over Server-Sent Events AnyCable supports [Server-Sent Events](../anycable-go/sse.md) (SSE) as a transport protocol. This means that you can use Turbo Streams with AnyCable without WebSockets and Action Cable (or AnyCable) client libraries—just with the help of the browser native `EventSource` API. To create a Turbo Stream subscription over SSE, you must provide an URL to AnyCable SSE endpoint with the signed stream name as a query parameter when adding a `` element on the page: ```html ``` That's it! Now you can broadcast Turbo Stream updates from your backend. Moreover, AnyCable supports the `Last-Event-ID` feature of EventSource, which means your **connection is reliable** and you won't miss any updates even if network is unstable. Don't forget to enable the [reliable streams](../anycable-go/reliable_streams.md) feature. [Turbo Streams]: https://turbo.hotwired.dev/handbook/streams [turbo-rails]: https://github.com/hotwired/turbo-rails [@anycable/turbo-stream]: https://github.com/anycable/anycable-client/tree/master/packages/turbo-stream --- --- url: /guides/laravel.md --- # Using AnyCable with Laravel AnyCable can be used as a WebSocket server for Laravel applications using Laravel Echo and Laravel Broadcasting capabilities. Consider it a drop-in replacement for Laravel Reverb, or Pusher, or whatever you use today. Why choosing AnyCable over Reverb et al? AnyCable is a battle-proofed real-time server that's been in production at scale for many years. It comes with extensive features set (reliability, various protocols support, observability tools, etc.) and it's **free to use**. You can use AnyCable server in a [Pusher mode](/anycable-go/pusher.md) or *natively* using a custom broadcasting and Echo adapter. In the latter, you can benefit from such AnyCable features as streams history and resumeable sessions (see [Reliable streams](/anycable-go/reliable_streams.md)). ## Pusher mode Switching to AnyCable can be as simple as replacing the command to run a WebSocket server (if you use Reverb): ```sh # before $ REVERB_APP_ID=app-id \ REVERB_APP_KEY=app-key \ REVERB_APP_SECRET=app-secret \ php artisan reverb:start # after $ ANYCABLE_PUSHER_APP_ID=app-id \ ANYCABLE_PUSHER_APP_KEY=app-key \ ANYCABLE_SECRET=app-secret \ anycable-go ``` And that's it! AnyCable also runs on port 8080 by default, so no changes required. > To give AnyCable a quick try, consider using our free managed service [AnyCable+](https://plus.anycable.io/cables). To run AnyCable locally, you can use the [anycable-laravel][] package that provides the following command: ```sh php artisan anycable:server ``` When running with this command, AnyCable automatically recognizes the following Reverb environment variables and uses them: `REVERB_APP_ID`, `REVERB_APP_KEY`, `REVERB_APP_SECRET`. ## AnyCable mode > Check out our demo Laravel application to see the complete example: [laravel-anycable-demo][] To fully benefit from AnyCable features, we recommend switching to use our [client library][anycable-client]. We also provide an Echo adapter that provides a familiar interface while using AnyCable JS SDK under the hood. First, install the [anycable-laravel][] package and configure the broadcasting backend—you will need to authorize private and presence channels and publish events: ```sh composer require anycable/laravel-broadcaster ``` Then, configure the application to use `anycable` broadcasting driver. For that, add the AnyCable service provider to the `bootstrap/providers.php` file: ```diff [ 'driver' => 'anycable', ], ``` Now, install the `@anycable/echo` JS package and configure your Echo instance: ```js import Echo from "laravel-echo"; import { EchoCable } from "@anycable/echo"; window.Echo = new Echo({ broadcaster: EchoCable, cableOptions: { url: url: import.meta.env.VITE_WEBSOCKET_URL || 'ws://localhost:8080/cable', }, // other configuration options such as auth, etc }); ``` Finally, install AnyCable server. We provide a convenient Artisan command that automatically downloads (when necessary) and runs the server: ```sh php artisan anycable:server ``` You can specify AnyCable configuration in the `.env` file: * `ANYCABLE_SECRET=secret` * `ANYCABLE_PUBLIC=true`: This MUST be set to true to allow connection (however, we highly recommend looking at the [JWT Authentication feature](/anycable-go/jwt_identification.md)). * `ANYCABLE_PUBLIC_STREAMS=true`: Enables public channels—they're disabled by default. You can also create an `anycable.toml` configuration file to fine-tune your AnyCable server (see [docs](/anycable-go/configuration?id=configuration-files)). **NOTE:** The Artisan command automatically configures [AnyCable broadcasting adapter](/anycable-go/broadcasting.md) to HTTP and enables [the "broker" preset](/anycable-go/reliable_streams.md) (streams history). Alternatively, you can install AnyCable using [other available options](/anycable-go/getting_started?id=installation). That's it! Run your Laravel application, launch AnyCable server, and you should see your Echo client connecting to it and receiving updates. ## Benchmarks You can find the benchmarks here: https://github.com/anycable/anycable-laravel/tree/master/benchmarks **tl;dr** AnyCable shows slightly better performance and lesser memory usage during broadcast benchmarks compared to Reverb; however, AnyCable handles connection avalanches much better. [anycable-laravel]: https://github.com/anycable/anycable-laravel [laravel-anycable-demo]: https://github.com/anycable/larachat [anycable-client]: https://github.com/anycable/anycable-client/tree/master/packages/echo --- --- url: /anycable-go/library.md --- # Using anycable-go as a library You can use AnyCable-Go as a library to build custom real-time applications. > Read ["AnyCable off Rails: connecting Twilio streams with Hanami"](https://evilmartians.com/chronicles/anycable-goes-off-rails-connecting-twilio-streams-with-hanami) to learn how we've integrated Twilio Streams with a Hanami application via AnyCable-Go. Why building a WebSocket application with AnyCable-Go (and not other Go libraries)? * Connect your application to Ruby/Rails apps with ease by using AnyCable RPC protocol. * Many features out-of-the-box including different pub/sub adapters (including [embedded NATS](./embedded_nats.md)), built-in instrumentation. * Bulletproof code, which has been used production for years. To get started with an application development with AnyCable-Go, you can use our template repository: [anycable-go-scaffold](https://github.com/anycable/anycable-go-scaffold). ## Embedding You can also embed AnyCable into your existing web application in case you want to serve AnyCable WebSocket/SSE connections via the same HTTP server as other requests (e.g., if you build a smart reverse-proxy). Here is a minimal example Go code (you can find the full and up-to-date version [here](https://github.com/anycable/anycable/blob/master/cmd/embedded-cable/main.go)): ```go package main import ( "net/http" "github.com/anycable/anycable-go/cli" ) func main() { opts := []cli.Option{ cli.WithName("AnyCable"), cli.WithDefaultRPCController(), cli.WithDefaultBroker(), cli.WithDefaultSubscriber(), cli.WithDefaultBroadcaster(), } c := cli.NewConfig() runner, _ := cli.NewRunner(c, opts) anycable, _ := runner.Embed() wsHandler, _ := anycable.WebSocketHandler() http.Handle("/cable", wsHandler) http.ListenAndServe(":8080", nil) } ``` --- --- url: /rails/stimulus_reflex.md --- # Using with Stimulus Reflex AnyCable v1.0+ works with Stimulus Reflex with some additional considerations: * For Stimulus Reflex <3, you should add `persistent_session_enabled: true` to your configuration (e.g., `anycable.yml`) * Stimulus Reflex 3+ persists session by itself but requires you to use a cache store for sessions. **Using memory store is not compatible with AnyCable**. Memory store is only accessible by the owner process, and with AnyCable you have two processes at least (web server and RPC server). Thus, you need to use a distributed cache store such as Redis cache store. ## Links * [Stimulus Reflex AnyCable deployment documentation](https://docs.stimulusreflex.com/deployment#anycable) * [Stimulus Reflex Expo configured to run on AnyCable](https://github.com/anycable/stimulus_reflex_expo) * [Original issue & discussion](https://github.com/hopsoft/stimulus_reflex/issues/46) * [Issue with a cache store](https://github.com/anycable/anycable-rails/issues/127) --- --- url: /misc/how_to_anycable_server.md --- # Writing Custom AnyCable Server You can write your own server to handle *cable clients* and connect them to your business logic through AnyCable. Saying "cable clients" we want to underline that AnyCable doesn't depend on any transport protocol (e.g., WebSockets), you can use any protocol for client-server communication (e.g., RTMP, custom TCP, long-polling, etc.). ## Requirements The server should be able to: * Communicate with [gRPC](https://grpc.io) server as a gRPC client gRPC provides libraries for most popular languages (see [docs](http://www.grpc.io/docs/)). If there is no gRPC support for your favorite language, you can build it yourself (the minimal implementation for AnyCable)–it's just HTTP2 + [Protocol Buffers](https://developers.google.com/protocol-buffers/). See [erlgrpc](https://github.com/palkan/erlgrpc) for the example of a minimal gRPC client. * Subscribe to Redis channels. We use Redis to receive broadcast events from the application by default (see [Broadcast adapters](../ruby/broadcast_adapters.md)). **NOTE**: You can build a custom broadcast adapter (for both–your server and `anycable` gem). For the rest of this article, we consider that we want to use Redis. ## Step-by-step Let's go through all steps to implement a custom server (using abstract language). ### Step 1. Server First of all, you need a *server*–the entry point for clients connections – which can handle incoming data and disconnection events. ```js interface Server { # Invoked on socket connection. # socket_handle is an entity (object/record/whatever) representing connection socket func socket_conn(socket_handle); # Invoked on socket disconnection func socket_disconn(socket_handle); # Invoker on incoming message func socket_data(socket_handle, msg); } ``` ### Step 2. Hub *Hub* stores information about clients subscriptions and has the following interface: ```js interface Hub { # Subscribe socket to the stream. # We also need a channel_id to sign messages with it (see below) func add(socket_handle, channel_id, stream); # Unsubscribe socket from a stream for the given channel func remove(socket_handle, channel_id, stream); # Unsubscribe socket from all streams for the given channel func removeAll(socket_handle, channel_id); # Broadcast a message to all subscribed sockets func broadcast(stream, msg); } ``` Why do we need a `channel_id`? This is required by Action Cable client. The JS client doesn't know about streams, only about channels. So it needs a channel identifier to be present in incoming messages to resolve channels. Moreover, there are no uniqueness restrictions on streams names–the same stream name can be used for different channels. Thus, our `broadcast` function may look like this: ```js func broadcast(stream, msg) { # Assume that we have a nested structure to store subscriptions: # sockets2streams # | # stream1 # | | # | channel1 - (socket1, ..., socketN) # | | # | channel2 – ( ... ) # | # stream2 ... # for (channel in channels_for_stream(stream)) { channel_msg = msg_for_channel(msg, channel.id()) for (socket in channel.sockets()) { socket.transmit(channel_msg) } } } # msg – JSON encoded string # We should transform into another JSON "{"identifier":,"message": }" func msg_for_channel(msg, identifier) { return json_encode(['identifier', 'message'], [identifier, json_decode(msg)]); } ``` ### Step 3. Pinger Action Cable clients assume that a server sends a special message–ping–every 3 seconds (configurable). Thus we should implement a *pinger*. Pinger is a simple entity that holds a list of active sockets and broadcast a message to them every X seconds. ```js interface Pinger { # Add socket to the active list func register(socket_handle); # Remove socket from the active list func unregister(socket_handle); } ``` And we need a kind of `loop` method: ```js func loop() { while(true) { var msg = ping_message() for (socket in active_sockets) { socket.transmit(msg) } sleep(INTERVAL) } } func ping_message() { return json_encode(['type', 'message'], ['ping', time.utc()]) } ``` **NOTE**: *ping* could be implemented in a different way (e.g. via a timer attached to a *client* session). ### Step 4. gRPC Client Then you have to build a gRPC client using a [Protobuf service definition](rpc_proto.md). It has a simple interface with only three methods: `Connect`, `Disconnect` and `Command`. Let's go to Step 5 to see, how to use these methods and their return values. ### Step 5. Server – RPC communication Now, when we already have a server and RPC client, let's fit them together. **NOTE**: see also [Action Cable protocol spec](action_cable_protocol.md). #### Client Connection Every time a client is connected to our server we should invoke `Connect` method to authorize connection: ```js func socket_conn(socket_handle) { # We need request URL and cookies (if we want to use cookie-based authentication) var url = socket_handle.url() # Extract Cookie header and build a map { 'Cookie' => cookie_val } # NOTE: you MAY provide more headers if you want var headers = header('Cookie', socket_handle.header('Cookie')); # Keep header for subsequent calls socket_handle.setFilteredHeaders(headers); # Then generate a payload (build protobuf msg) # ConnectionRequest contains fields: # env: # url - string - request URL # headers - map var env = pb::SessionEnv(url, headers) var payload = pb::ConnectionRequest(env) # Make a call and get a response – ConnectionResponse: # status – Status::SUCCESS | Status::ERROR – status enum is a part of rpc.proto # identifiers – string (connection identifiers string used by the app) # transmissions - list of strings (repeated string) var response = rpc::Connect(payload) # handle response if (response.status() == pb::Status::SUCCESS) { # store identifiers for the socket # we will use them in later calls socket_handle.setIdentifiers(response.identifiers()) # update a client's connection state socket_handle.setState(response.env().cstate()) # transmit messages to socket # NOTE: typically Connect returns only "welcome" message socket_handle.transmit(response.transmissions()) # register socket to pinger pinger.register(socket_handle) } else { # if Status is not SUCCESS we should disconnect the socket socket_handle.close() # non-SUCCESS status could be: # - ERROR - there was an en exception during the call; in this case we also have response.error_msg() # - FAILURE - application-level "rejection" (e.g. authentication failed) } } ``` #### Client Commands *Command* is an incoming message from the client. We should distinguish "subscribe" and "unsubscribe" command from others, 'cause they're responsible for subscriptions. ```js func socket_data(socket_handle, msg) { var decoded = json_decode(msg) var type = decoded.key("type") # Every command is associated with the specified channel var identifier = decoded.key("identifier") var data = decoded.key("data") # Generate a payload (build protobuf msg) # CommandMessage contains fields: # command - string # identifier - string (channel identifier) # connection_identifiers - string (identifiers from Connect call) # data – string (additional provided data) # env: # url - string - request URL # headers - map # cstate - map — connection state obtained in socket_conn # istate - map — channel state for the identifier var env = pb::SessionEnv(socket_handle.url(), socket.filtered_headers(), socket.state(), socket.channel_state(identifier)) var payload = pb::CommandMessage(type, identifier, socket_handle.identifiers(), data, env) # Make a call and get a response – ConnectionResponse: # status – Status::SUCCESS | Status::FAILURE | Status::ERROR– status enum is a part of rpc.proto # disconnect – bool – whether to disconnect the client or not # stop_streams – bool – whether to stop all existing subscriptions for the channel # streams – list of strings – new subscriptions # stopped_streams - list of strings — # transmissions - list of strings – messages to send to the client # error_msg – error message in case of ERROR # env: # cstate - map — connection state changed/new fields # istate — map — channel state changed/new fields var response = rpc::Command(payload) # handle response if (response.status() == pb::Status::SUCCESS) { # First, handle subscription commands # We should track client subscriptions in order to call `#unsubscribe` callbacks on disconnection if (type == "subscribe") { socket_handle.addSubscripton(identifier) } if (type == "unsubscribe") { socket_handle.removeSubscription(identifier) } # Then handle other response information # If response contains disconnect flag set to true # The we immediately disconnect the client if (response.disconnect()) { return socket_handle.close() } # update connection state if (response.env().cstate()) { socket_handle.mergeState(response.env().cstate()) } # update channel state if (response.env().istate()) { # socket_handle.channel_state has a form of map>, # where first-level keys are subscription identifiers, and values are the # corresponding channels states socket_handle.mergeChannelState(identifier, response.env().istate()) } if (response.stop_streams()) { # Stop all subscriptions for the channel hub.removeAll(socket_handle, identifier) } # Add new subscriptions for (stream in response.streams()) { hub.add(socket_handle, identifier, stream) } # Remove old subscriptions for (stream in response.stopped_streams()) { hub.add(socket_handle, identifier, stream) } # And, finally, transmit messages socket_handle.transmit(response.transmissions()) } else { # in case of failure you may want to disconnect the client socket_handle.close() } } ``` #### Client Disconnection When a client disconnects, we should remove its subscriptions, de-register from pinger and invoke `#disconnect`/`#unsubscribe` callbacks in the app. ```js func socket_disconn(socket_handle) { # De-register socket from pinger pinger.unregister(socket_handle) # Remove subscriptions var subscriptions = socket_handle.subscriptions() for (channel in subscriptions) { hub.removeAll(socket_handle, channel) } # And only after that notify the app thru RPC # Then generate a payload (build protobuf msg) # DisconnectRequest contains fields: # identifiers – string – connection identifiers # subscriptions – list of strings – connections channels # env: # url – string – request URL # headers - map # cstate - map — connection state # istate - map — channel states for all subscriptions # We need to encode channel states to strings to pass them as istate (which is a string-string map) var channel_states = socket.channel_state().transform_values( (v) => JSON.encode(v) ) var env = pb::SessionEnv(socket_handle.url(), socket.filtered_headers(), socket.state(), channel_states) var payload = pb::DisconnectRequest(socket_handle.identifier(), subscriptions, env) # Make a call and get a response – DisconnectResponse: # status – Status::SUCCESS | Status::ERROR – status enum is a part of rpc.proto # Actually, response status does not matter here, we should cleanup rpc::Disconnect(payload) } ``` **NOTE**: It makes sense to call `Disconnect` asynchronously or using a queue in order to avoid RPC calls spikes caused by mass-disconnection. ### Step 6. Testing You can use [AnyT](https://github.com/anycable/anyt)–AnyCable conformance testing tool–for integration tests.