anycable 1.0.0.preview1 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 65d07fe802d850f89351ed8c054d2cba9c393b36f7a5aa318183362f09d53e58
4
- data.tar.gz: b4167ba77ce930ddcb77a9e38d205215fb3ec085a93bfd3876f2bf8a5e669373
3
+ metadata.gz: 90f8b0fc45d4659234b7defd755b818c19dde55779a64cbdec794fc68441bf45
4
+ data.tar.gz: 2184541660e683c2206593636434b4a24578376936eb0bb63d4600602829c736
5
5
  SHA512:
6
- metadata.gz: 456af4dd28de6d32a8436ce8702ddb3ba5523b25b531bce8dac00c5bf6cd181e79c57e4d4a89d1a2c1ff3e21eda1108be3f023c65edcf1bbf5790594216ae971
7
- data.tar.gz: b25fa2a291a6cd6bbf4734c9c59dcf991dead19c0e25c004ac696ec524e6de63bb5ea4251f8d06ce1d21699a44e178a7e445bfedcd4d70179535c6648ed67854
6
+ metadata.gz: bddcd49f2e31f8a1c0fcf202e44779abe0fcf4092bc3358f3b4efe7b86e4db1454e083e2c352e651ff38fafcf0d4f3b1907a019fba5e262aaeba0b7fd6a09abb
7
+ data.tar.gz: fa7586b9a059dfb9584894d1f5540fd33dff26411f8199a0f837cf26d0d0c9c2af150602ebbadc775af3f2b9393ed5c2de0f7435a2d403f11081166ddcae88df
@@ -1,283 +1,57 @@
1
1
  # Change log
2
2
 
3
- ## 🚧 1.0.0 (_coming soon_)
3
+ ## master
4
4
 
5
- - **RPC schema has changed**. ([@palkan][])
6
-
7
- Using `anycable-go` v1.x is required.
8
-
9
- - **Ruby 2.5+ is required**. ([@palkan][])
10
-
11
- - Added RPC proto version check. ([@palkan][])
12
-
13
- Server must sent `protov` metadata with the supported versions (comma-separated list). If there is no matching version an exception is raised.
14
-
15
- Current RPC proto version is **v1**.
16
-
17
- - Added `request` support to channels. ([@palkan][])
18
-
19
- Now you can access `request` object in channels, too (e.g., to read headers/cookies/URL/etc).
20
-
21
- - Change default server address from `[::]:50051` to `127.0.0.1:50051`. ([@palkan][])
22
-
23
- See [#71](https://github.com/anycable/anycable/pull/71).
24
-
25
- ## 0.6.4 (2020-01-24)
26
-
27
- - Fix Ruby 2.7 warnings. ([@palkan])
28
-
29
- – Add `REMOTE_ADDR` socket env variable using a synthetic header passed from a websocket
30
- server. ([@sponomarev][])
31
-
32
- Recreating a request object in your custom connection factory using `Rack::Request` or
33
- `ActionDispatch::Request` (already implemented in [anycable-rails](https://github.com/anycable/anycable-rails))
34
- gives you an access to `request.ip` with the properly set IP address.
35
-
36
- - Align socket env to be more compatibile with Rack Spec ([@sponomarev][])
37
-
38
- Provide as much env details as possible to be able to reconstruct the full
39
- request object in a custom connection factory.
40
-
41
- ## 0.6.3 (2019-03-26)
42
-
43
- - Relax `redis` gem version requirement. ([@palkan][])
44
-
45
- Use the same restriction as Action Cable does (`>= 3`).
46
-
47
- ## 0.6.2 (2019-03-15)
48
-
49
- - Add GRPC service method name and message content to exception notifications ([@sponomarev][])
50
-
51
- `Anycable.capture_exception` allows accessing GRPC service method name and message content
52
- on which an exception was captured. It can be used for exceptions grouping in your tracker and
53
- providing additional data to investigate a root of a problem.
54
-
55
- Example:
56
-
57
- ```ruby
58
- AnyCable.capture_exception do |ex, method, message|
59
- Honeybadger.notify(ex, component: "any_cable", action: method, params: message)
60
- end
61
- ```
62
-
63
- Usage of a handler proc with just a single argument is preserved for the sake of compatibility.
64
-
65
- - Add deprecation warning to default host usage ([@sponomarev][])
66
-
67
- Exposing AnyCable publicly is considered to be harmful and planned to be changed
68
- in future versions.
69
-
70
- - Allow running the server as a detachable daemon ([@sponomarev][])
71
-
72
- Server is fully managed by the binary itself.
73
-
74
- ```
75
- # Start anycable daemon
76
- $ bundle exec anycabled start
77
-
78
- # Pass cli options to anycable through daemon. Separate daemon options and anycable options with `--`
79
- $ bundle exec anycabled start -- --rpc-host 127.0.0.1:31337
80
-
81
- # Stop anycable daemon
82
- $ bundle exec anycabled stop
83
-
84
- # See more anycable daemon options
85
- $ bundle exec anycabled
86
- ```
87
-
88
- ## 0.6.1 (2019-01-05)
89
-
90
- - [Fix #63](https://github.com/anycable/anycable-rails/issues/63) Load `anyway_config` after application boot to make sure that all frameworks dependent functionality is loaded. ([@palkan][])
91
-
92
- ## 0.6.0 (2018-11-15)
93
-
94
- ### Features
95
-
96
- #### Broadcast adapters
97
-
98
- AnyCable allows you to use custom broadcasting adapters (Redis is used by default):
99
-
100
- ```ruby
101
- # Specify by name (tries to load `AnyCable::BroadcastAdapters::MyAdapter` from
102
- # "anycable/broadcast_adapters/my_adapter")
103
- AnyCable.broadcast_adapter = :my_adapter, {option: "value"}
104
- # or provide an instance (should respond_to #broadcast)
105
- AnyCable.broadcast_adapter = MyAdapter.new
106
- ```
107
-
108
- **Breaking:** to use Redis adapter you must ensure that it is present in your Gemfile; AnyCable gem doesn't have `redis` as a dependency anymore.
109
-
110
- #### CLI
111
-
112
- AnyCable now ships with a CLI–`anycable`.
113
-
114
- Use it to run a gRPC server:
115
-
116
- ```sh
117
- # run anycable and load app from app.rb
118
- bundle exec anycable -r app.rb
119
- # or
120
- bundle exec anycable --require app.rb
121
- ```
5
+ ## 1.0.1 (2020-07-07)
122
6
 
123
- All configuration options are also supported as CLI options (see `anycable -h` for more information).
7
+ - Support providing passwords for Redis Sentinels. ([@palkan][])
124
8
 
125
- The only required options is the application file to load (`-r/--require`).
9
+ Use the following format: `ANYCABLE_REDIS_SENTINELS=:password1@my.redis.sentinel.first:26380,:password2@my.redis.sentinel.second:26380`.
126
10
 
127
- You can omit it if you want to load an app form `./config/environment.rb` (e.g. with Rails) or `./config/anycable.rb`.
11
+ ## 1.0.0 (2020-07-01)
128
12
 
129
- AnyCable CLI also allows you to run a separate command (process) from within a RPC server:
13
+ - Add `embedded` option to CLI runner. ([@palkan][])
130
14
 
131
- ```sh
132
- $ bundle exec anycable --server-command "anycable-go -p 3334"
133
- ```
15
+ - Add `Env#istate` and `EnvResponse#istate` to store channel state. ([@palkan][])
134
16
 
135
- #### Configuration
17
+ That would allow to mimic instance variables usage in Action Cable channels.
136
18
 
137
- - Default server host is changed from `localhost:50051` to `0.0.0.0:50051`
138
- - Expose gRPC server parameters via `rpc_*` config params:
19
+ - Add `CommandResponse#stopped_streams` to support unsubscribing from particular broadcastings. ([@palkan])
139
20
 
140
- ```ruby
141
- AnyCable.configure do |config|
142
- config.rpc_pool_size = 120
143
- config.rpc_max_waiting_requests = 10
144
- # etc
145
- end
146
- ```
147
- - `REDIS_URL` env is used by default if present (and no `ANYCABLE_REDIS_URL` specified)
148
- - Make HTTP health check url configurable
149
- - Add ability to pass Redis Sentinel config as array of string.
21
+ `Socket#unsubscribe` is now implemented as well.
150
22
 
151
- Now it's possible to pass Sentinel configuration via env vars:
23
+ - Add `AnyCable.broadcast_adapter#broadcast_command` method. ([@palkan][])
152
24
 
153
- ```sh
154
- ANYCABLE_REDIS_SENTINELS=127.0.0.1:26380,127.0.0.1:26381 bundle exec anycable
155
- ```
25
+ It could be used to send commands to WS server (e.g., remote disconnect).
156
26
 
157
- #### Other
27
+ - Add `:http` broadcasting adapter. ([@palkan][])
158
28
 
159
- - Added middlewares support
160
-
161
- See [docs](https://docs.anycable.io/#/./middlewares).
162
-
163
- - Added gRPC health checker.
164
-
165
- See [docs](https://docs.anycable.io/#/./health_checking).
166
-
167
- - Added hook to run code only within RPC server context.
168
-
169
- Use `AnyCable.configure_server { ... }` to run code only when RPC server is running.
170
-
171
- ### API changes
172
-
173
- **NOTE**: the old API is still working but deprecated (you'll see a notice).
174
-
175
- - Use `AnyCable` instead of `Anycable`
176
-
177
- - New API for registering error handlers:
178
-
179
- ```ruby
180
- AnyCable.capture_exception do |ex|
181
- Honeybadger.notify(ex)
182
- end
183
- ```
184
-
185
- - `AnyCable::Server.start` is deprecated
186
-
187
-
188
- ## 0.5.2 (2018-09-06)
189
-
190
- - [#48](https://github.com/anycable/anycable/pull/48) Add HTTP health server ([@DarthSim][])
191
-
192
- ## 0.5.1 (2018-06-13)
193
-
194
- Minor fixes.
195
-
196
- ## 0.5.0 (2017-10-21)
197
-
198
- - [#2](https://github.com/anycable/anycable/issues/2) Add support for [Redis Sentinel](https://redis.io/topics/sentinel). ([@accessd][])
199
-
200
- - [#28](https://github.com/anycable/anycable/issues/28) Support arbitrary headers. ([@palkan][])
201
-
202
- Previously we hardcoded only "Cookie" header. Now we add all passed headers by WebSocket server to request env.
203
-
204
- - [#27](https://github.com/anycable/anycable/issues/27) Add `error_msg` to RPC responses. ([@palkan][])
205
-
206
- Now RPC responses has 3 statuses:
207
-
208
- - `SUCCESS` – successful request, operation succeed
209
- - `FAILURE` – successful request, operation failed (e.g. authentication failed)
210
- - `ERROR` – request failed (exception raised).
211
-
212
- We provide `error_msg` only when request status is `ERROR`.
213
-
214
- - [#25](https://github.com/anycable/anycable/issues/25) Improve logging and exceptions handling. ([@palkan][])
215
-
216
- Default logger logs to STDOUT with `info` level by default but can be configured to log to file with
217
- any severity.
218
-
219
- GRPC logging is turned off by default (can be turned on through `log_grpc` configuration parameter).
220
-
221
- `ANYCABLE_DEBUG=1` acts as a shortcut to set `debug` level and turn on GRPC logging.
222
-
223
- Now it's possible to add custom exception handlers (e.g. to notify external exception tracking services).
224
-
225
- More on [Wiki](https://github.com/anycable/anycable/wiki/Logging-&-Exceptions-Handling).
226
-
227
- ## 0.4.6 (2017-05-20)
228
-
229
- - Add `Anycable::Server#stop` method. ([@sadovnik][])
230
-
231
- ## 0.4.5 (2017-03-17)
232
-
233
- - Fixed #11. ([@palkan][])
234
-
235
- ## 0.4.4 (2017-03-06)
236
-
237
- - Handle `StandardError` gracefully in RPC calls. ([@palkan][])
238
-
239
- ## 0.4.3 (2017-02-18)
240
-
241
- - Update `grpc` version dependency to support Ruby 2.4. ([@palkan][])
242
-
243
- ## 0.4.2 (2017-01-28)
244
-
245
- - Change socket streaming API. ([@palkan][])
246
-
247
- Add `Socket#subscribe`, `unsubscribe` and `unsubscribe_from_all` methods.
248
-
249
- ## 0.4.1 (2017-01-24)
250
-
251
- - Introduce _fake_ socket instance to handle transmissions and streams. ([@palkan][])
29
+ - **RPC schema has changed**. ([@palkan][])
252
30
 
253
- - Make commands handling more abstract. ([@palkan][])
31
+ Using `anycable-go` v1.x is required.
254
32
 
255
- We now do not explicitly call channels action but use the only one entrypoing for all commands:
33
+ - **Ruby 2.5+ is required**. ([@palkan][])
256
34
 
257
- ```ruby
258
- connection.handle_channel_command(identifier, command, data)
259
- ```
35
+ - Added RPC proto version check. ([@palkan][])
260
36
 
261
- This method should return `true` if command was successful and `false` otherwise.
37
+ Server must sent `protov` metadata with the supported versions (comma-separated list). If there is no matching version an exception is raised.
262
38
 
263
- ## 0.4.0 (2017-01-22)
39
+ Current RPC proto version is **v1**.
264
40
 
265
- - Refactor RPC API. ([@palkan][])
41
+ - Added `request` support to channels. ([@palkan][])
266
42
 
267
- Replace `Subscribe`, `Unsubscribe` and `Perform` methods with `Command` method.
43
+ Now you can access `request` object in channels, too (e.g., to read headers/cookies/URL/etc).
268
44
 
269
- - Extract Rails functionality to separate gem. ([@palkan][])
45
+ - Change default server address from `[::]:50051` to `127.0.0.1:50051`. ([@palkan][])
270
46
 
271
- All Rails specifics now live here https://github.com/anycable/anycable-rails.
47
+ See [#71](https://github.com/anycable/anycable/pull/71).
272
48
 
273
- ## 0.3.0 (2016-12-28)
49
+ - Fix building Redis Sentinel config. ([@palkan][])
274
50
 
275
- - Handle `Disconnect` requests. ([@palkan][])
51
+ ---
276
52
 
277
- Implement `Disconnect` handler, which invokes `Connection#disconnect` (along with `Channel#unsubscribed` for each subscription).
53
+ See [Changelog](https://github.com/anycable/anycable/blob/0-6-stable/CHANGELOG.md) for versions <1.0.0.
278
54
 
279
55
  [@palkan]: https://github.com/palkan
280
- [@sadovnik]: https://github.com/sadovnik
281
- [@accessd]: https://github.com/accessd
282
- [@DarthSim]: https://github.com/DarthSim
283
56
  [@sponomarev]: https://github.com/sponomarev
57
+ [@bibendi]: https://github.com/bibendi
@@ -1,4 +1,4 @@
1
- Copyright 2017-2019 Vladimir Dementyev
1
+ Copyright 2017-2020 Vladimir Dementyev
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.md CHANGED
@@ -1,30 +1,36 @@
1
- [![GitPitch](https://gitpitch.com/assets/badge.svg)](https://gitpitch.com/anycable/anycable/master?grs=github) [![Gem Version](https://badge.fury.io/rb/anycable.svg)](https://rubygems.org/gems/anycable) [![Build Status](https://travis-ci.org/anycable/anycable.svg?branch=master)](https://travis-ci.org/anycable/anycable)
1
+ [![GitPitch](https://gitpitch.com/assets/badge.svg)](https://gitpitch.com/anycable/anycable/master?grs=github)
2
+ [![Gem Version](https://badge.fury.io/rb/anycable.svg)](https://rubygems.org/gems/anycable)
3
+ [![Build](https://github.com/anycable/anycable/workflows/Build/badge.svg)](https://github.com/anycable/anycable/actions)
2
4
  [![Gitter](https://img.shields.io/badge/gitter-join%20chat%20%E2%86%92-brightgreen.svg)](https://gitter.im/anycable/Lobby)
3
- [![Documentation](https://img.shields.io/badge/docs-link-brightgreen.svg)](https://docs.anycable.io)
5
+ [![Documentation](https://img.shields.io/badge/docs-link-brightgreen.svg)](https://docs.anycable.io/v1)
4
6
 
5
7
  # AnyCable
6
8
 
7
9
  <img align="right" height="150" width="129"
8
10
  title="AnyCable logo" src="https://docs.anycable.io/assets/images/logo.svg">
9
11
 
10
- AnyCable allows you to use any WebSocket server (written in any language) as a replacement for your Ruby server (such as Faye, ActionCable, etc).
12
+ AnyCable allows you to use any WebSocket server (written in any language) as a replacement for your Ruby server (such as Faye, Action Cable, etc).
11
13
 
12
14
  AnyCable uses the same protocol as ActionCable, so you can use its [JavaScript client](https://www.npmjs.com/package/actioncable) without any monkey-patching.
13
15
 
16
+ **Important** This is a readme for the upcoming v1.0 release. For v0.6.x see the readme from the [0-6-stable](https://github.com/anycable/anycable/tree/0-6-stable) branch.
17
+
14
18
  <a href="https://evilmartians.com/">
15
19
  <img src="https://evilmartians.com/badges/sponsored-by-evil-martians.svg" alt="Sponsored by Evil Martians" width="236" height="54"></a>
16
20
 
17
21
  ## Requirements
18
22
 
19
23
  - Ruby >= 2.5
20
- - Redis (for broadcasting, [discuss other options](https://github.com/anycable/anycable/issues/2) with us!)
24
+ - Redis (for broadcasting **in production**, [discuss other options](https://github.com/anycable/anycable/issues/2) with us!)
21
25
 
22
26
  ## Usage
23
27
 
24
- Check out our 📑 [Documentation](https://docs.anycable.io).
28
+ Check out our 📑 [Documentation](https://docs.anycable.io/v1).
25
29
 
26
30
  ## Links
27
31
 
32
+ - [AnyCable 1.0: Four years of real-time web with Ruby and Go](https://evilmartians.com/chronicles/anycable-1-0-four-years-of-real-time-web-with-ruby-and-go)
33
+
28
34
  - [AnyCable: Action Cable on steroids!](https://evilmartians.com/chronicles/anycable-actioncable-on-steroids)
29
35
 
30
36
  - [Connecting LiteCable to Hanami](http://gabrielmalakias.com.br/ruby/hanami/iot/2017/05/26/websockets-connecting-litecable-to-hanami.html) by [@GabrielMalakias](https://github.com/GabrielMalakias)
@@ -43,12 +49,9 @@ Check out our 📑 [Documentation](https://docs.anycable.io).
43
49
 
44
50
  - RailsClub Moscow 2016 [slides](https://speakerdeck.com/palkan/railsclub-moscow-2016-anycable) and [video](https://www.youtube.com/watch?v=-k7GQKuBevY&list=PLiWUIs1hSNeOXZhotgDX7Y7qBsr24cu7o&index=4) (RU)
45
51
 
46
- ## Compatible WebSocket servers
47
-
48
- - [AnyCable Go](https://github.com/anycable/anycable-go)
49
- - [ErlyCable](https://github.com/anycable/erlycable)
52
+ ## Building
50
53
 
51
- ## Build
54
+ ### Generating gRPC files from `.proto`
52
55
 
53
56
  - Install required GRPC gems:
54
57
 
@@ -75,4 +78,4 @@ The gem is available as open source under the terms of the [MIT License](http://
75
78
 
76
79
  ## Security Contact
77
80
 
78
- To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure.
81
+ To report a security vulnerability, please contact us at `anycable@evilmartians.com`. We will coordinate the fix and disclosure.
@@ -67,7 +67,7 @@ module AnyCable
67
67
  end
68
68
 
69
69
  def broadcast_adapter
70
- self.broadcast_adapter = :redis unless instance_variable_defined?(:@broadcast_adapter)
70
+ self.broadcast_adapter = AnyCable.config.broadcast_adapter.to_sym unless instance_variable_defined?(:@broadcast_adapter)
71
71
  @broadcast_adapter
72
72
  end
73
73
 
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "anycable/broadcast_adapters/base"
4
+
3
5
  module AnyCable
4
6
  module BroadcastAdapters # :nodoc:
5
7
  module_function
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnyCable
4
+ module BroadcastAdapters
5
+ class Base
6
+ def raw_broadcast(_data)
7
+ raise NotImplementedError
8
+ end
9
+
10
+ def broadcast(stream, payload)
11
+ raw_broadcast({stream: stream, data: payload}.to_json)
12
+ end
13
+
14
+ def broadcast_command(command, **payload)
15
+ raw_broadcast({command: command, payload: payload}.to_json)
16
+ end
17
+
18
+ def announce!
19
+ logger.info "Broadcasting via #{self.class.name}"
20
+ end
21
+
22
+ private
23
+
24
+ def logger
25
+ AnyCable.logger
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "uri"
5
+ require "net/http"
6
+
7
+ module AnyCable
8
+ module BroadcastAdapters
9
+ # HTTP adapter for broadcasting.
10
+ #
11
+ # Example:
12
+ #
13
+ # AnyCable.broadast_adapter = :http
14
+ #
15
+ # It uses configuration from global AnyCable config
16
+ # by default.
17
+ #
18
+ # You can override these params:
19
+ #
20
+ # AnyCable.broadcast_adapter = :http, url: "http://ws.example.com/_any_cable_"
21
+ class Http < Base
22
+ # Taken from: https://github.com/influxdata/influxdb-ruby/blob/886058079c66d4fd019ad74ca11342fddb0b753d/lib/influxdb/errors.rb#L18
23
+ RECOVERABLE_EXCEPTIONS = [
24
+ Errno::ECONNABORTED,
25
+ Errno::ECONNREFUSED,
26
+ Errno::ECONNRESET,
27
+ Errno::EHOSTUNREACH,
28
+ Errno::EINVAL,
29
+ Errno::ENETUNREACH,
30
+ Net::HTTPBadResponse,
31
+ Net::HTTPHeaderSyntaxError,
32
+ Net::ProtocolError,
33
+ SocketError,
34
+ (OpenSSL::SSL::SSLError if defined?(OpenSSL))
35
+ ].compact.freeze
36
+
37
+ OPEN_TIMEOUT = 5
38
+ READ_TIMEOUT = 10
39
+
40
+ MAX_ATTEMPTS = 3
41
+ DELAY = 2
42
+
43
+ attr_reader :url, :headers, :authorized
44
+ alias authorized? authorized
45
+
46
+ def initialize(url: AnyCable.config.http_broadcast_url, secret: AnyCable.config.http_broadcast_secret)
47
+ @url = url
48
+ @headers = {}
49
+ if secret
50
+ headers["Authorization"] = "Bearer #{secret}"
51
+ @authorized = true
52
+ end
53
+
54
+ @uri = URI.parse(url)
55
+ @queue = Queue.new
56
+ end
57
+
58
+ def raw_broadcast(payload)
59
+ ensure_thread_is_alive
60
+ queue << payload
61
+ end
62
+
63
+ # Wait for background thread to process all the messages
64
+ # and stop it
65
+ def shutdown
66
+ queue << :stop
67
+ thread.join if thread&.alive?
68
+ rescue Exception => e # rubocop:disable Lint/RescueException
69
+ logger.error "Broadcasting thread exited with exception: #{e.message}"
70
+ end
71
+
72
+ def announce!
73
+ logger.info "Broadcasting HTTP url: #{url}#{authorized? ? " (with authorization)" : ""}"
74
+ end
75
+
76
+ private
77
+
78
+ attr_reader :uri, :queue, :thread
79
+
80
+ def ensure_thread_is_alive
81
+ return if thread&.alive?
82
+
83
+ @thread = Thread.new do
84
+ loop do
85
+ msg = queue.pop
86
+ break if msg == :stop
87
+
88
+ handle_response perform_request(msg)
89
+ end
90
+ end
91
+ end
92
+
93
+ def perform_request(payload)
94
+ build_http do |http|
95
+ req = Net::HTTP::Post.new(url, {"Content-Type" => "application/json"}.merge(headers))
96
+ req.body = payload
97
+ http.request(req)
98
+ end
99
+ end
100
+
101
+ def handle_response(response)
102
+ return unless response
103
+ return if Net::HTTPCreated === response
104
+
105
+ logger.error "Broadcast request responded with unexpected status: #{response.code}"
106
+ end
107
+
108
+ def build_http
109
+ retry_count = 0
110
+
111
+ begin
112
+ http = Net::HTTP.new(uri.host, uri.port)
113
+ http.open_timeout = OPEN_TIMEOUT
114
+ http.read_timeout = READ_TIMEOUT
115
+ http.use_ssl = url.match?(/^https/)
116
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
117
+ yield http
118
+ rescue Timeout::Error, *RECOVERABLE_EXCEPTIONS => e
119
+ retry_count += 1
120
+ return logger.error("Broadcast request failed: #{e.message}") if MAX_ATTEMPTS < retry_count
121
+
122
+ sleep((DELAY**retry_count) * retry_count)
123
+ retry
124
+ ensure
125
+ http.finish if http.started?
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
@@ -19,7 +19,7 @@ module AnyCable
19
19
  # You can override these params:
20
20
  #
21
21
  # AnyCable.broadcast_adapter = :redis, url: "redis://my_redis", channel: "_any_cable_"
22
- class Redis
22
+ class Redis < Base
23
23
  attr_reader :redis_conn, :channel
24
24
 
25
25
  def initialize(
@@ -31,11 +31,12 @@ module AnyCable
31
31
  @channel = channel
32
32
  end
33
33
 
34
- def broadcast(stream, payload)
35
- redis_conn.publish(
36
- channel,
37
- {stream: stream, data: payload}.to_json
38
- )
34
+ def raw_broadcast(payload)
35
+ redis_conn.publish(channel, payload)
36
+ end
37
+
38
+ def announce!
39
+ logger.info "Broadcasting Redis channel: #{channel}"
39
40
  end
40
41
  end
41
42
  end
@@ -20,17 +20,22 @@ module AnyCable
20
20
  # Wait for external process termination (s)
21
21
  WAIT_PROCESS = 2
22
22
 
23
- attr_reader :server, :health_server
23
+ attr_reader :server, :health_server, :embedded
24
+ alias embedded? embedded
25
+
26
+ def initialize(embedded: false)
27
+ @embedded = embedded
28
+ end
24
29
 
25
30
  # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
26
- def run(args = {})
31
+ def run(args = [])
27
32
  @at_stop = []
28
33
 
29
34
  extra_options = parse_cli_options!(args)
30
35
 
31
36
  # Boot app first, 'cause it might change
32
37
  # configuration, loggin settings, etc.
33
- boot_app!
38
+ boot_app! unless embedded?
34
39
 
35
40
  parse_gem_options!(extra_options)
36
41
 
@@ -40,7 +45,7 @@ module AnyCable
40
45
 
41
46
  print_versions!
42
47
 
43
- logger.info "Serving #{defined?(::Rails) ? "Rails " : ""}application from #{boot_file}"
48
+ logger.info "Serving #{defined?(::Rails) ? "Rails " : ""}application from #{boot_file}" unless embedded?
44
49
 
45
50
  verify_connection_factory!
46
51
 
@@ -66,6 +71,8 @@ module AnyCable
66
71
 
67
72
  run_custom_server_command! unless server_command.nil?
68
73
 
74
+ return if embedded?
75
+
69
76
  begin
70
77
  wait_till_terminated
71
78
  rescue Interrupt => e
@@ -81,7 +88,7 @@ module AnyCable
81
88
 
82
89
  def shutdown
83
90
  at_stop.each(&:call)
84
- server.stop
91
+ server&.stop
85
92
  end
86
93
 
87
94
  private
@@ -183,7 +190,7 @@ module AnyCable
183
190
  end
184
191
 
185
192
  def start_pubsub!
186
- logger.info "Broadcasting Redis channel: #{config.redis_channel}"
193
+ AnyCable.broadcast_adapter.announce!
187
194
  end
188
195
 
189
196
  # rubocop: disable Metrics/MethodLength, Metrics/AbcSize
@@ -313,9 +320,7 @@ module AnyCable
313
320
  -r, --require=path Location of application file to require, default: "config/environment.rb"
314
321
  --server-command=command Command to run WebSocket server
315
322
  --rpc-host=host Local address to run gRPC server on, default: "[::]:50051"
316
- --redis-url=url Redis URL for pub/sub, default: REDIS_URL or "redis://localhost:6379/5"
317
- --redis-channel=name Redis channel for broadcasting, default: "__anycable__"
318
- --redis-sentinels=<...hosts> Redis Sentinel followers addresses (as a comma-separated list), default: nil
323
+ --broadcast-adapter=type Pub/sub adapter type for broadcasts, default: redis
319
324
  --log-level=level Logging level, default: "info"
320
325
  --log-file=path Path to log file, default: <none> (log to STDOUT)
321
326
  --log-grpc Enable gRPC logging (disabled by default)
@@ -323,6 +328,15 @@ module AnyCable
323
328
  -v, --version Print version and exit
324
329
  -h, --help Show this help
325
330
 
331
+ REDIS PUB/SUB OPTIONS
332
+ --redis-url=url Redis URL for pub/sub, default: REDIS_URL or "redis://localhost:6379/5"
333
+ --redis-channel=name Redis channel for broadcasting, default: "__anycable__"
334
+ --redis-sentinels=<...hosts> Redis Sentinel followers addresses (as a comma-separated list), default: nil
335
+
336
+ HTTP PUB/SUB OPTIONS
337
+ --http-broadcast-url HTTP pub/sub endpoint URL, default: "http://localhost:8090/_broadcast"
338
+ --http-broadcast-secret HTTP pub/sub authorization secret, default: <none> (disabled)
339
+
326
340
  HTTP HEALTH CHECKER OPTIONS
327
341
  --http-health-port=port Port to run HTTP health server on, default: <none> (disabled)
328
342
  --http-health-path=path Endpoint to server health cheks, default: "/health"
@@ -3,6 +3,8 @@
3
3
  require "anyway_config"
4
4
  require "grpc"
5
5
 
6
+ require "uri"
7
+
6
8
  module AnyCable
7
9
  # AnyCable configuration.
8
10
  class Config < Anyway::Config
@@ -19,11 +21,18 @@ module AnyCable
19
21
  # See https://github.com/grpc/grpc/blob/f526602bff029b8db50a8d57134d72da33d8a752/include/grpc/impl/codegen/grpc_types.h#L292-L315
20
22
  rpc_server_args: {},
21
23
 
24
+ ## PubSub
25
+ broadcast_adapter: :redis,
26
+
22
27
  ### Redis options
23
28
  redis_url: ENV.fetch("REDIS_URL", "redis://localhost:6379/5"),
24
29
  redis_sentinels: nil,
25
30
  redis_channel: "__anycable__",
26
31
 
32
+ ### HTTP broadcasting options
33
+ http_broadcast_url: "http://localhost:8090/_broadcast",
34
+ http_broadcast_secret: nil,
35
+
27
36
  ### Logging options
28
37
  log_file: nil,
29
38
  log_level: :info,
@@ -43,19 +52,33 @@ module AnyCable
43
52
  ignore_options :rpc_server_args
44
53
  flag_options :log_grpc, :debug
45
54
 
46
- def log_level
47
- debug ? :debug : @log_level
48
- end
55
+ # Support both anyway_config 1.4 and 2.0.
56
+ # DEPRECATE: Drop <2.0 support in 1.1
57
+ if respond_to?(:on_load)
58
+ on_load { self.debug = debug != false }
49
59
 
50
- def log_grpc
51
- debug || @log_grpc
52
- end
60
+ def log_level
61
+ debug? ? :debug : super
62
+ end
53
63
 
54
- def debug
55
- @debug != false
56
- end
64
+ def log_grpc
65
+ debug? || super
66
+ end
67
+ else
68
+ def log_level
69
+ debug ? :debug : @log_level
70
+ end
57
71
 
58
- alias debug? debug
72
+ def log_grpc
73
+ debug || @log_grpc
74
+ end
75
+
76
+ def debug
77
+ @debug != false
78
+ end
79
+
80
+ alias debug? debug
81
+ end
59
82
 
60
83
  def http_health_port_provided?
61
84
  !http_health_port.nil? && http_health_port != ""
@@ -77,12 +100,11 @@ module AnyCable
77
100
  {url: redis_url}.tap do |params|
78
101
  next if redis_sentinels.nil?
79
102
 
80
- raise ArgumentError, "redis_sentinels must be an array; got #{redis_sentinels}" unless
81
- redis_sentinels.is_a?(Array)
103
+ sentinels = Array(redis_sentinels)
82
104
 
83
- next if redis_sentinels.empty?
105
+ next if sentinels.empty?
84
106
 
85
- params[:sentinels] = redis_sentinels.map(&method(:parse_sentinel))
107
+ params[:sentinels] = sentinels.map(&method(:parse_sentinel))
86
108
  end
87
109
  end
88
110
 
@@ -96,16 +118,14 @@ module AnyCable
96
118
 
97
119
  private
98
120
 
99
- SENTINEL_RXP = /^([\w\-_]*)\:(\d+)$/.freeze
100
-
101
121
  def parse_sentinel(sentinel)
102
- return sentinel if sentinel.is_a?(Hash)
122
+ return sentinel.transform_keys!(&:to_sym) if sentinel.is_a?(Hash)
103
123
 
104
- matches = sentinel.match(SENTINEL_RXP)
124
+ uri = URI.parse("redis://#{sentinel}")
105
125
 
106
- raise ArgumentError, "Invalid Sentinel value: #{sentinel}" if matches.nil?
107
-
108
- {"host" => matches[1], "port" => matches[2].to_i}
126
+ {host: uri.host, port: uri.port}.tap do |opts|
127
+ opts[:password] = uri.password if uri.password
128
+ end
109
129
  end
110
130
  end
111
131
  end
@@ -20,7 +20,7 @@ module AnyCable
20
20
 
21
21
  attr_reader :grpc_server, :port, :path, :server
22
22
 
23
- def initialize(grpc_server, port:, path: "/health", logger: AnyCable.logger)
23
+ def initialize(grpc_server, port:, logger: nil, path: "/health")
24
24
  @grpc_server = grpc_server
25
25
  @port = port
26
26
  @path = path
@@ -48,7 +48,9 @@ module AnyCable
48
48
 
49
49
  private
50
50
 
51
- attr_reader :logger
51
+ def logger
52
+ @logger ||= AnyCable.logger
53
+ end
52
54
 
53
55
  def build_server
54
56
  require "webrick"
@@ -5,7 +5,7 @@ require "grpc"
5
5
  module AnyCable
6
6
  # Middleware is a wrapper over gRPC interceptors
7
7
  # for request/response calls
8
- class Middleware < GRPC::Interceptor
8
+ class Middleware < GRPC::ServerInterceptor
9
9
  def request_response(request: nil, call: nil, method: nil)
10
10
  # Call middlewares only for AnyCable service
11
11
  return yield unless method.receiver.is_a?(AnyCable::RPCHandler)
@@ -15,6 +15,11 @@ module AnyCable
15
15
  end
16
16
  end
17
17
 
18
+ def server_streamer(**kwargs)
19
+ p kwargs
20
+ yield
21
+ end
22
+
18
23
  def call(*)
19
24
  raise NotImplementedError
20
25
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module AnyCable
4
4
  module Middlewares
5
- # Checks that RPC client version is compatibly with
5
+ # Checks that RPC client version is compatible with
6
6
  # the current RPC proto version
7
7
  class CheckVersion < Middleware
8
8
  attr_reader :version
@@ -35,6 +35,14 @@ module AnyCable
35
35
  def cstate=(val)
36
36
  env.cstate = val
37
37
  end
38
+
39
+ def istate
40
+ env.istate
41
+ end
42
+
43
+ def istate=(val)
44
+ env.istate = val
45
+ end
38
46
  end
39
47
 
40
48
  # Status predicates
@@ -11,9 +11,11 @@ Google::Protobuf::DescriptorPool.generated_pool.build do
11
11
  optional :url, :string, 1
12
12
  map :headers, :string, :string, 2
13
13
  map :cstate, :string, :string, 3
14
+ map :istate, :string, :string, 4
14
15
  end
15
16
  add_message "anycable.EnvResponse" do
16
17
  map :cstate, :string, :string, 1
18
+ map :istate, :string, :string, 2
17
19
  end
18
20
  add_message "anycable.ConnectionRequest" do
19
21
  optional :env, :message, 3, "anycable.Env"
@@ -40,6 +42,7 @@ Google::Protobuf::DescriptorPool.generated_pool.build do
40
42
  repeated :transmissions, :string, 5
41
43
  optional :error_msg, :string, 6
42
44
  optional :env, :message, 7, "anycable.EnvResponse"
45
+ repeated :stopped_streams, :string, 8
43
46
  end
44
47
  add_message "anycable.DisconnectRequest" do
45
48
  optional :identifiers, :string, 1
@@ -86,7 +86,8 @@ module AnyCable
86
86
  status: result ? AnyCable::Status::SUCCESS : AnyCable::Status::FAILURE,
87
87
  disconnect: socket.closed?,
88
88
  stop_streams: socket.stop_streams?,
89
- streams: socket.streams,
89
+ streams: socket.streams[:start],
90
+ stopped_streams: socket.streams[:stop],
90
91
  transmissions: socket.transmissions,
91
92
  env: build_env_response(socket)
92
93
  )
@@ -106,17 +107,18 @@ module AnyCable
106
107
  uri = URI.parse(request_env.url)
107
108
 
108
109
  env = base_rack_env
109
- env.merge!(
110
+ env.merge!({
110
111
  "PATH_INFO" => uri.path,
111
112
  "QUERY_STRING" => uri.query,
112
113
  "SERVER_NAME" => uri.host,
113
- "SERVER_PORT" => uri.port.to_s,
114
+ "SERVER_PORT" => uri.port,
114
115
  "HTTP_HOST" => uri.host,
115
116
  "REMOTE_ADDR" => request_env.headers.delete("REMOTE_ADDR"),
116
- "rack.url_scheme" => uri.scheme,
117
+ "rack.url_scheme" => uri.scheme&.sub(/^ws/, "http"),
117
118
  # AnyCable specific fields
118
- "anycable.raw_cstate" => request_env.cstate&.to_h
119
- )
119
+ "anycable.raw_cstate" => request_env.cstate&.to_h,
120
+ "anycable.raw_istate" => request_env.istate&.to_h
121
+ }.delete_if { |_k, v| v.nil? })
120
122
 
121
123
  env.merge!(build_headers(request_env.headers))
122
124
  end
@@ -125,6 +127,7 @@ module AnyCable
125
127
  # Minimum required variables according to Rack Spec
126
128
  # (not all of them though, just those enough for Action Cable to work)
127
129
  # See https://rubydoc.info/github/rack/rack/master/file/SPEC
130
+ # and https://github.com/rack/rack/blob/master/lib/rack/lint.rb
128
131
  {
129
132
  "REQUEST_METHOD" => "GET",
130
133
  "SCRIPT_NAME" => "",
@@ -133,7 +136,13 @@ module AnyCable
133
136
  "SERVER_NAME" => "",
134
137
  "SERVER_PORT" => "80",
135
138
  "rack.url_scheme" => "http",
136
- "rack.input" => ""
139
+ "rack.input" => StringIO.new("", "r").tap { |io| io.set_encoding(Encoding::ASCII_8BIT) },
140
+ "rack.version" => ::Rack::VERSION,
141
+ "rack.errors" => StringIO.new("").tap { |io| io.set_encoding(Encoding::ASCII_8BIT) },
142
+ "rack.multithread" => true,
143
+ "rack.multiprocess" => false,
144
+ "rack.run_once" => false,
145
+ "rack.hijack?" => false
137
146
  }
138
147
  end
139
148
 
@@ -151,7 +160,8 @@ module AnyCable
151
160
 
152
161
  def build_env_response(socket)
153
162
  AnyCable::EnvResponse.new(
154
- cstate: socket.cstate.changed_fields
163
+ cstate: socket.cstate.changed_fields,
164
+ istate: socket.istate.changed_fields
155
165
  )
156
166
  end
157
167
 
@@ -7,7 +7,7 @@ RSpec.shared_context "anycable:rpc:stub" do
7
7
 
8
8
  let(:service) { @service }
9
9
 
10
- let(:url) { "example.com/cable" }
10
+ let(:url) { "ws://example.anycable.com/cable" }
11
11
  let(:headers) { {} }
12
12
  let(:env) { AnyCable::Env.new(url: url, headers: headers) }
13
13
  end
@@ -9,6 +9,7 @@ RSpec.shared_context "anycable:rpc:server" do
9
9
  )
10
10
 
11
11
  @server.start
12
+ sleep 0.1
12
13
  end
13
14
 
14
15
  after(:all) { @server.stop }
@@ -23,7 +23,7 @@ module AnyCable
23
23
  class Server
24
24
  attr_reader :grpc_server, :host
25
25
 
26
- def initialize(host:, logger: AnyCable.logger, **options)
26
+ def initialize(host:, logger: nil, **options)
27
27
  @logger = logger
28
28
  @host = host
29
29
  @grpc_server = build_server(options)
@@ -70,7 +70,11 @@ module AnyCable
70
70
 
71
71
  private
72
72
 
73
- attr_reader :logger, :start_thread
73
+ attr_reader :start_thread
74
+
75
+ def logger
76
+ @logger ||= AnyCable.logger
77
+ end
74
78
 
75
79
  def build_server(options)
76
80
  GRPC::RpcServer.new(**options).tap do |server|
@@ -17,6 +17,8 @@ module AnyCable
17
17
  source&.[](key)
18
18
  end
19
19
 
20
+ alias [] read
21
+
20
22
  def write(key, val)
21
23
  return if source&.[](key) == val
22
24
 
@@ -26,18 +28,21 @@ module AnyCable
26
28
  source[key] = val
27
29
  end
28
30
 
31
+ alias []= write
32
+
29
33
  def changed_fields
30
34
  return unless source && dirty_keys
31
35
  source.slice(*dirty_keys)
32
36
  end
33
37
  end
34
38
 
35
- attr_reader :transmissions, :env, :cstate
39
+ attr_reader :transmissions, :env, :cstate, :istate
36
40
 
37
41
  def initialize(env: nil)
38
42
  @transmissions = []
39
43
  @env = env
40
44
  @cstate = env["anycable.cstate"] = State.new(env["anycable.raw_cstate"])
45
+ @istate = env["anycable.istate"] = State.new(env["anycable.raw_istate"])
41
46
  end
42
47
 
43
48
  def transmit(websocket_message)
@@ -45,11 +50,11 @@ module AnyCable
45
50
  end
46
51
 
47
52
  def subscribe(_channel, broadcasting)
48
- streams << broadcasting
53
+ streams[:start] << broadcasting
49
54
  end
50
55
 
51
- def unsubscribe(_channel, _broadcasting)
52
- raise NotImplementedError
56
+ def unsubscribe(_channel, broadcasting)
57
+ streams[:stop] << broadcasting
53
58
  end
54
59
 
55
60
  def unsubscribe_from_all(_channel)
@@ -57,7 +62,7 @@ module AnyCable
57
62
  end
58
63
 
59
64
  def streams
60
- @streams ||= []
65
+ @streams ||= {start: [], stop: []}
61
66
  end
62
67
 
63
68
  def close
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AnyCable
4
- VERSION = "1.0.0.preview1"
4
+ VERSION = "1.0.1"
5
5
  end
metadata CHANGED
@@ -1,27 +1,27 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: anycable
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0.preview1
4
+ version: 1.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - palkan
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-02-19 00:00:00.000000000 Z
11
+ date: 2020-07-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: anyway_config
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
19
  version: 1.4.2
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - "~>"
24
+ - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: 1.4.2
27
27
  - !ruby/object:Gem::Dependency
@@ -72,14 +72,14 @@ dependencies:
72
72
  requirements:
73
73
  - - ">="
74
74
  - !ruby/object:Gem::Version
75
- version: '10.0'
75
+ version: '13.0'
76
76
  type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
80
  - - ">="
81
81
  - !ruby/object:Gem::Version
82
- version: '10.0'
82
+ version: '13.0'
83
83
  - !ruby/object:Gem::Dependency
84
84
  name: rack
85
85
  requirement: !ruby/object:Gem::Requirement
@@ -109,47 +109,19 @@ dependencies:
109
109
  - !ruby/object:Gem::Version
110
110
  version: '3.5'
111
111
  - !ruby/object:Gem::Dependency
112
- name: rubocop-md
112
+ name: webmock
113
113
  requirement: !ruby/object:Gem::Requirement
114
114
  requirements:
115
115
  - - "~>"
116
116
  - !ruby/object:Gem::Version
117
- version: '0.3'
117
+ version: '3.8'
118
118
  type: :development
119
119
  prerelease: false
120
120
  version_requirements: !ruby/object:Gem::Requirement
121
121
  requirements:
122
122
  - - "~>"
123
123
  - !ruby/object:Gem::Version
124
- version: '0.3'
125
- - !ruby/object:Gem::Dependency
126
- name: simplecov
127
- requirement: !ruby/object:Gem::Requirement
128
- requirements:
129
- - - ">="
130
- - !ruby/object:Gem::Version
131
- version: 0.3.8
132
- type: :development
133
- prerelease: false
134
- version_requirements: !ruby/object:Gem::Requirement
135
- requirements:
136
- - - ">="
137
- - !ruby/object:Gem::Version
138
- version: 0.3.8
139
- - !ruby/object:Gem::Dependency
140
- name: standard
141
- requirement: !ruby/object:Gem::Requirement
142
- requirements:
143
- - - "~>"
144
- - !ruby/object:Gem::Version
145
- version: 0.1.7
146
- type: :development
147
- prerelease: false
148
- version_requirements: !ruby/object:Gem::Requirement
149
- requirements:
150
- - - "~>"
151
- - !ruby/object:Gem::Version
152
- version: 0.1.7
124
+ version: '3.8'
153
125
  description: AnyCable is a polyglot replacement for ActionCable-compatible servers
154
126
  email:
155
127
  - dementiev.vm@gmail.com
@@ -168,6 +140,8 @@ files:
168
140
  - bin/setup
169
141
  - lib/anycable.rb
170
142
  - lib/anycable/broadcast_adapters.rb
143
+ - lib/anycable/broadcast_adapters/base.rb
144
+ - lib/anycable/broadcast_adapters/http.rb
171
145
  - lib/anycable/broadcast_adapters/redis.rb
172
146
  - lib/anycable/cli.rb
173
147
  - lib/anycable/config.rb
@@ -207,9 +181,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
207
181
  version: 2.5.0
208
182
  required_rubygems_version: !ruby/object:Gem::Requirement
209
183
  requirements:
210
- - - ">"
184
+ - - ">="
211
185
  - !ruby/object:Gem::Version
212
- version: 1.3.1
186
+ version: '0'
213
187
  requirements: []
214
188
  rubygems_version: 3.0.6
215
189
  signing_key: