anycable 1.0.0.preview2 → 1.0.2

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: 61d002d899e5ef0c6c8e17b131a569dee4e93d948a9a2d401e20c764ee573ae2
4
- data.tar.gz: 20743c021aa6c79177cd727d31468b1823ed492c11da1967513fa7cd5bc18f0a
3
+ metadata.gz: dd11b4e0403da1e162132375055bc2815c48ee57c3554a04ed72683d91fb0fde
4
+ data.tar.gz: '0568d54349b5e0a452f00119678b37964bb8c0102f512010e015c0b836a2ef6f'
5
5
  SHA512:
6
- metadata.gz: a5d8ce6d031bfa1b65e12cbe49a1bb2813267997d090126a09fe55d30dc7e44a62c5315bf3e1f430bb6c8a67909245db2a9e03d3654ac7922cc08d6d09a7c14a
7
- data.tar.gz: eb9ecd34150e474fbb8c914d3f0b95e3c23752fe141fc6acf5819fefbaface12d27103bc0a62487d42ec3d76e2228c43f0c3e9b3c34423fc71ce422ad0200131
6
+ metadata.gz: 22ded17aa347fe2c26d0d0aa6ceeb4958244a0d82a9d50f358f0c6c0b8433004ea71f62810bd757e9cf11c896596896c31859e6af62716b218f61f72b6d4d3fe
7
+ data.tar.gz: 3f52eade080b1de1ecdc39624a87b6840239392ebcf30bada85e44706cbd7c819cecc566a1eb5b7e8fa9cff5c567ab5929c3ec2189c4fb300e94357cbbb4ca42
@@ -1,288 +1,61 @@
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.5 (2020-04-01)
26
-
27
- - Relax `anyway_config` dependency. ([@palkan][])
28
-
29
- ## 0.6.4 (2020-01-24)
30
-
31
- - Fix Ruby 2.7 warnings. ([@palkan][])
32
-
33
- – Add `REMOTE_ADDR` socket env variable using a synthetic header passed from a websocket
34
- server. ([@sponomarev][])
35
-
36
- Recreating a request object in your custom connection factory using `Rack::Request` or
37
- `ActionDispatch::Request` (already implemented in [anycable-rails](https://github.com/anycable/anycable-rails))
38
- gives you an access to `request.ip` with the properly set IP address.
39
-
40
- - Align socket env to be more compatible with Rack Spec ([@sponomarev][])
41
-
42
- Provide as much env details as possible to be able to reconstruct the full
43
- request object in a custom connection factory.
44
-
45
- ## 0.6.3 (2019-03-26)
46
-
47
- - Relax `redis` gem version requirement. ([@palkan][])
48
-
49
- Use the same restriction as Action Cable does (`>= 3`).
50
-
51
- ## 0.6.2 (2019-03-15)
52
-
53
- - Add GRPC service method name and message content to exception notifications ([@sponomarev][])
54
-
55
- `Anycable.capture_exception` allows accessing GRPC service method name and message content
56
- on which an exception was captured. It can be used for exceptions grouping in your tracker and
57
- providing additional data to investigate a root of a problem.
58
-
59
- Example:
60
-
61
- ```ruby
62
- AnyCable.capture_exception do |ex, method, message|
63
- Honeybadger.notify(ex, component: "any_cable", action: method, params: message)
64
- end
65
- ```
66
-
67
- Usage of a handler proc with just a single argument is preserved for the sake of compatibility.
68
-
69
- - Add deprecation warning to default host usage ([@sponomarev][])
70
-
71
- Exposing AnyCable publicly is considered to be harmful and planned to be changed
72
- in future versions.
73
-
74
- - Allow running the server as a detachable daemon ([@sponomarev][])
75
-
76
- Server is fully managed by the binary itself.
77
-
78
- ```sh
79
- # Start anycable daemon
80
- $ bundle exec anycabled start
81
-
82
- # Pass cli options to anycable through daemon. Separate daemon options and anycable options with `--`
83
- $ bundle exec anycabled start -- --rpc-host 127.0.0.1:31337
84
-
85
- # Stop anycable daemon
86
- $ bundle exec anycabled stop
87
-
88
- # See more anycable daemon options
89
- $ bundle exec anycabled
90
- ```
91
-
92
- ## 0.6.1 (2019-01-05)
93
-
94
- - [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][])
95
-
96
- ## 0.6.0 (2018-11-15)
97
-
98
- ### Features
99
-
100
- #### Broadcast adapters
101
-
102
- AnyCable allows you to use custom broadcasting adapters (Redis is used by default):
103
-
104
- ```ruby
105
- # Specify by name (tries to load `AnyCable::BroadcastAdapters::MyAdapter` from
106
- # "anycable/broadcast_adapters/my_adapter")
107
- AnyCable.broadcast_adapter = :my_adapter, {option: "value"}
108
- # or provide an instance (should respond_to #broadcast)
109
- AnyCable.broadcast_adapter = MyAdapter.new
110
- ```
111
-
112
- **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.
113
-
114
- #### CLI
115
-
116
- AnyCable now ships with a CLI–`anycable`.
5
+ ## 1.0.2
117
6
 
118
- Use it to run a gRPC server:
7
+ - Handle TLS Redis connections by using VERIFY_NONE mode. ([@palkan][])
119
8
 
120
- ```sh
121
- # run anycable and load app from app.rb
122
- bundle exec anycable -r app.rb
123
- # or
124
- bundle exec anycable --require app.rb
125
- ```
9
+ ## 1.0.1 (2020-07-07)
126
10
 
127
- All configuration options are also supported as CLI options (see `anycable -h` for more information).
11
+ - Support providing passwords for Redis Sentinels. ([@palkan][])
128
12
 
129
- The only required options is the application file to load (`-r/--require`).
13
+ Use the following format: `ANYCABLE_REDIS_SENTINELS=:password1@my.redis.sentinel.first:26380,:password2@my.redis.sentinel.second:26380`.
130
14
 
131
- You can omit it if you want to load an app form `./config/environment.rb` (e.g. with Rails) or `./config/anycable.rb`.
15
+ ## 1.0.0 (2020-07-01)
132
16
 
133
- AnyCable CLI also allows you to run a separate command (process) from within a RPC server:
17
+ - Add `embedded` option to CLI runner. ([@palkan][])
134
18
 
135
- ```sh
136
- bundle exec anycable --server-command "anycable-go -p 3334"
137
- ```
19
+ - Add `Env#istate` and `EnvResponse#istate` to store channel state. ([@palkan][])
138
20
 
139
- #### Configuration
21
+ That would allow to mimic instance variables usage in Action Cable channels.
140
22
 
141
- - Default server host is changed from `localhost:50051` to `0.0.0.0:50051`
142
- - Expose gRPC server parameters via `rpc_*` config params:
23
+ - Add `CommandResponse#stopped_streams` to support unsubscribing from particular broadcastings. ([@palkan])
143
24
 
144
- ```ruby
145
- AnyCable.configure do |config|
146
- config.rpc_pool_size = 120
147
- config.rpc_max_waiting_requests = 10
148
- # etc
149
- end
150
- ```
25
+ `Socket#unsubscribe` is now implemented as well.
151
26
 
152
- - `REDIS_URL` env is used by default if present (and no `ANYCABLE_REDIS_URL` specified)
153
- - Make HTTP health check url configurable
154
- - Add ability to pass Redis Sentinel config as array of string.
27
+ - Add `AnyCable.broadcast_adapter#broadcast_command` method. ([@palkan][])
155
28
 
156
- Now it's possible to pass Sentinel configuration via env vars:
29
+ It could be used to send commands to WS server (e.g., remote disconnect).
157
30
 
158
- ```sh
159
- ANYCABLE_REDIS_SENTINELS=127.0.0.1:26380,127.0.0.1:26381 bundle exec anycable
160
- ```
31
+ - Add `:http` broadcasting adapter. ([@palkan][])
161
32
 
162
- #### Other
163
-
164
- - Added middlewares support
165
-
166
- See [docs](https://docs.anycable.io/#/./middlewares).
167
-
168
- - Added gRPC health checker.
169
-
170
- See [docs](https://docs.anycable.io/#/./health_checking).
171
-
172
- - Added hook to run code only within RPC server context.
173
-
174
- Use `AnyCable.configure_server { ... }` to run code only when RPC server is running.
175
-
176
- ### API changes
177
-
178
- **NOTE**: the old API is still working but deprecated (you'll see a notice).
179
-
180
- - Use `AnyCable` instead of `Anycable`
181
-
182
- - New API for registering error handlers:
183
-
184
- ```ruby
185
- AnyCable.capture_exception do |ex|
186
- Honeybadger.notify(ex)
187
- end
188
- ```
189
-
190
- - `AnyCable::Server.start` is deprecated
191
-
192
- ## 0.5.2 (2018-09-06)
193
-
194
- - [#48](https://github.com/anycable/anycable/pull/48) Add HTTP health server ([@DarthSim][])
195
-
196
- ## 0.5.1 (2018-06-13)
197
-
198
- Minor fixes.
199
-
200
- ## 0.5.0 (2017-10-21)
201
-
202
- - [#2](https://github.com/anycable/anycable/issues/2) Add support for [Redis Sentinel](https://redis.io/topics/sentinel). ([@accessd][])
203
-
204
- - [#28](https://github.com/anycable/anycable/issues/28) Support arbitrary headers. ([@palkan][])
205
-
206
- Previously we hardcoded only "Cookie" header. Now we add all passed headers by WebSocket server to request env.
207
-
208
- - [#27](https://github.com/anycable/anycable/issues/27) Add `error_msg` to RPC responses. ([@palkan][])
209
-
210
- Now RPC responses has 3 statuses:
211
-
212
- 1) `SUCCESS` – successful request, operation succeed
213
- 2) `FAILURE` – successful request, operation failed (e.g. authentication failed)
214
- 3) `ERROR` – request failed (exception raised).
215
-
216
- We provide `error_msg` only when request status is `ERROR`.
217
-
218
- - [#25](https://github.com/anycable/anycable/issues/25) Improve logging and exceptions handling. ([@palkan][])
219
-
220
- Default logger logs to STDOUT with `info` level by default but can be configured to log to file with
221
- any severity.
222
-
223
- GRPC logging is turned off by default (can be turned on through `log_grpc` configuration parameter).
224
-
225
- `ANYCABLE_DEBUG=1` acts as a shortcut to set `debug` level and turn on GRPC logging.
226
-
227
- Now it's possible to add custom exception handlers (e.g. to notify external exception tracking services).
228
-
229
- More on [Wiki](https://github.com/anycable/anycable/wiki/Logging-&-Exceptions-Handling).
230
-
231
- ## 0.4.6 (2017-05-20)
232
-
233
- - Add `Anycable::Server#stop` method. ([@sadovnik][])
234
-
235
- ## 0.4.5 (2017-03-17)
236
-
237
- - Fixed #11. ([@palkan][])
238
-
239
- ## 0.4.4 (2017-03-06)
240
-
241
- - Handle `StandardError` gracefully in RPC calls. ([@palkan][])
242
-
243
- ## 0.4.3 (2017-02-18)
244
-
245
- - Update `grpc` version dependency to support Ruby 2.4. ([@palkan][])
246
-
247
- ## 0.4.2 (2017-01-28)
248
-
249
- - Change socket streaming API. ([@palkan][])
250
-
251
- Add `Socket#subscribe`, `unsubscribe` and `unsubscribe_from_all` methods.
252
-
253
- ## 0.4.1 (2017-01-24)
254
-
255
- - Introduce _fake_ socket instance to handle transmissions and streams. ([@palkan][])
33
+ - **RPC schema has changed**. ([@palkan][])
256
34
 
257
- - Make commands handling more abstract. ([@palkan][])
35
+ Using `anycable-go` v1.x is required.
258
36
 
259
- We now do not explicitly call channels action but use the only one entrypoint for all commands:
37
+ - **Ruby 2.5+ is required**. ([@palkan][])
260
38
 
261
- ```ruby
262
- connection.handle_channel_command(identifier, command, data)
263
- ```
39
+ - Added RPC proto version check. ([@palkan][])
264
40
 
265
- This method should return `true` if command was successful and `false` otherwise.
41
+ Server must sent `protov` metadata with the supported versions (comma-separated list). If there is no matching version an exception is raised.
266
42
 
267
- ## 0.4.0 (2017-01-22)
43
+ Current RPC proto version is **v1**.
268
44
 
269
- - Refactor RPC API. ([@palkan][])
45
+ - Added `request` support to channels. ([@palkan][])
270
46
 
271
- Replace `Subscribe`, `Unsubscribe` and `Perform` methods with `Command` method.
47
+ Now you can access `request` object in channels, too (e.g., to read headers/cookies/URL/etc).
272
48
 
273
- - Extract Rails functionality to separate gem. ([@palkan][])
49
+ - Change default server address from `[::]:50051` to `127.0.0.1:50051`. ([@palkan][])
274
50
 
275
- All Rails specifics now live here [https://github.com/anycable/anycable-rails](https://github.com/anycable/anycable-rails).
51
+ See [#71](https://github.com/anycable/anycable/pull/71).
276
52
 
277
- ## 0.3.0 (2016-12-28)
53
+ - Fix building Redis Sentinel config. ([@palkan][])
278
54
 
279
- - Handle `Disconnect` requests. ([@palkan][])
55
+ ---
280
56
 
281
- Implement `Disconnect` handler, which invokes `Connection#disconnect` (along with `Channel#unsubscribed` for each subscription).
57
+ See [Changelog](https://github.com/anycable/anycable/blob/0-6-stable/CHANGELOG.md) for versions <1.0.0.
282
58
 
283
59
  [@palkan]: https://github.com/palkan
284
- [@sadovnik]: https://github.com/sadovnik
285
- [@accessd]: https://github.com/accessd
286
- [@DarthSim]: https://github.com/DarthSim
287
60
  [@sponomarev]: https://github.com/sponomarev
288
61
  [@bibendi]: https://github.com/bibendi
@@ -1,4 +1,4 @@
1
- Copyright 2017-2020 Vladimir Dementyev
1
+ Copyright 2017-2021 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
@@ -2,31 +2,35 @@
2
2
  [![Gem Version](https://badge.fury.io/rb/anycable.svg)](https://rubygems.org/gems/anycable)
3
3
  [![Build](https://github.com/anycable/anycable/workflows/Build/badge.svg)](https://github.com/anycable/anycable/actions)
4
4
  [![Gitter](https://img.shields.io/badge/gitter-join%20chat%20%E2%86%92-brightgreen.svg)](https://gitter.im/anycable/Lobby)
5
- [![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)
6
6
 
7
7
  # AnyCable
8
8
 
9
9
  <img align="right" height="150" width="129"
10
10
  title="AnyCable logo" src="https://docs.anycable.io/assets/images/logo.svg">
11
11
 
12
- 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).
13
13
 
14
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.
15
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
+
16
18
  <a href="https://evilmartians.com/">
17
19
  <img src="https://evilmartians.com/badges/sponsored-by-evil-martians.svg" alt="Sponsored by Evil Martians" width="236" height="54"></a>
18
20
 
19
21
  ## Requirements
20
22
 
21
23
  - Ruby >= 2.5
22
- - 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!)
23
25
 
24
26
  ## Usage
25
27
 
26
- Check out our 📑 [Documentation](https://docs.anycable.io).
28
+ Check out our 📑 [Documentation](https://docs.anycable.io/v1).
27
29
 
28
30
  ## Links
29
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
+
30
34
  - [AnyCable: Action Cable on steroids!](https://evilmartians.com/chronicles/anycable-actioncable-on-steroids)
31
35
 
32
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)
@@ -45,12 +49,9 @@ Check out our 📑 [Documentation](https://docs.anycable.io).
45
49
 
46
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)
47
51
 
48
- ## Compatible WebSocket servers
49
-
50
- - [AnyCable Go](https://github.com/anycable/anycable-go)
51
- - [ErlyCable](https://github.com/anycable/erlycable)
52
+ ## Building
52
53
 
53
- ## Build
54
+ ### Generating gRPC files from `.proto`
54
55
 
55
56
  - Install required GRPC gems:
56
57
 
@@ -31,7 +31,7 @@ module AnyCable
31
31
  def logger
32
32
  return @logger if instance_variable_defined?(:@logger)
33
33
 
34
- log_output = AnyCable.config.log_file || STDOUT
34
+ log_output = AnyCable.config.log_file || $stdout
35
35
  @logger = Logger.new(log_output).tap do |logger|
36
36
  logger.level = AnyCable.config.log_level
37
37
  end
@@ -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_method :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_method :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,
@@ -38,7 +47,7 @@ module AnyCable
38
47
  version_check_enabled: true
39
48
  )
40
49
 
41
- alias version_check_enabled? version_check_enabled
50
+ alias_method :version_check_enabled?, :version_check_enabled
42
51
 
43
52
  ignore_options :rpc_server_args
44
53
  flag_options :log_grpc, :debug
@@ -68,7 +77,7 @@ module AnyCable
68
77
  @debug != false
69
78
  end
70
79
 
71
- alias debug? debug
80
+ alias_method :debug?, :debug
72
81
  end
73
82
 
74
83
  def http_health_port_provided?
@@ -89,14 +98,17 @@ module AnyCable
89
98
  # Build Redis parameters
90
99
  def to_redis_params
91
100
  {url: redis_url}.tap do |params|
92
- next if redis_sentinels.nil?
101
+ next if redis_sentinels.nil? || redis_sentinels.empty?
102
+
103
+ sentinels = Array(redis_sentinels)
93
104
 
94
- raise ArgumentError, "redis_sentinels must be an array; got #{redis_sentinels}" unless
95
- redis_sentinels.is_a?(Array)
105
+ next if sentinels.empty?
96
106
 
97
- next if redis_sentinels.empty?
107
+ params[:sentinels] = sentinels.map(&method(:parse_sentinel))
108
+ end.tap do |params|
109
+ next unless redis_url.match?(/rediss:\/\//)
98
110
 
99
- params[:sentinels] = redis_sentinels.map(&method(:parse_sentinel))
111
+ params[:ssl_params] = {verify_mode: OpenSSL::SSL::VERIFY_NONE}
100
112
  end
101
113
  end
102
114
 
@@ -110,16 +122,14 @@ module AnyCable
110
122
 
111
123
  private
112
124
 
113
- SENTINEL_RXP = /^([\w\-_]*)\:(\d+)$/.freeze
114
-
115
125
  def parse_sentinel(sentinel)
116
- return sentinel if sentinel.is_a?(Hash)
117
-
118
- matches = sentinel.match(SENTINEL_RXP)
126
+ return sentinel.transform_keys!(&:to_sym) if sentinel.is_a?(Hash)
119
127
 
120
- raise ArgumentError, "Invalid Sentinel value: #{sentinel}" if matches.nil?
128
+ uri = URI.parse("redis://#{sentinel}")
121
129
 
122
- {"host" => matches[1], "port" => matches[2].to_i}
130
+ {host: uri.host, port: uri.port}.tap do |opts|
131
+ opts[:password] = uri.password if uri.password
132
+ end
123
133
  end
124
134
  end
125
135
  end
@@ -7,7 +7,7 @@ module AnyCable
7
7
  handlers << procify(block)
8
8
  end
9
9
 
10
- alias << add_handler
10
+ alias_method :<<, :add_handler
11
11
 
12
12
  def notify(exp, method_name, message)
13
13
  handlers.each do |handler|
@@ -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
  )
@@ -115,7 +116,8 @@ module AnyCable
115
116
  "REMOTE_ADDR" => request_env.headers.delete("REMOTE_ADDR"),
116
117
  "rack.url_scheme" => uri.scheme&.sub(/^ws/, "http"),
117
118
  # AnyCable specific fields
118
- "anycable.raw_cstate" => request_env.cstate&.to_h
119
+ "anycable.raw_cstate" => request_env.cstate&.to_h,
120
+ "anycable.raw_istate" => request_env.istate&.to_h
119
121
  }.delete_if { |_k, v| v.nil? })
120
122
 
121
123
  env.merge!(build_headers(request_env.headers))
@@ -135,7 +137,7 @@ module AnyCable
135
137
  "SERVER_PORT" => "80",
136
138
  "rack.url_scheme" => "http",
137
139
  "rack.input" => StringIO.new("", "r").tap { |io| io.set_encoding(Encoding::ASCII_8BIT) },
138
- "rack.version" => Rack::VERSION,
140
+ "rack.version" => ::Rack::VERSION,
139
141
  "rack.errors" => StringIO.new("").tap { |io| io.set_encoding(Encoding::ASCII_8BIT) },
140
142
  "rack.multithread" => true,
141
143
  "rack.multiprocess" => false,
@@ -158,7 +160,8 @@ module AnyCable
158
160
 
159
161
  def build_env_response(socket)
160
162
  AnyCable::EnvResponse.new(
161
- cstate: socket.cstate.changed_fields
163
+ cstate: socket.cstate.changed_fields,
164
+ istate: socket.istate.changed_fields
162
165
  )
163
166
  end
164
167
 
@@ -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_method :[], :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_method :[]=, :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.preview2"
4
+ VERSION = "1.0.2"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: anycable
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0.preview2
4
+ version: 1.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - palkan
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-04-09 00:00:00.000000000 Z
11
+ date: 2021-01-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: anyway_config
@@ -108,6 +108,20 @@ dependencies:
108
108
  - - ">="
109
109
  - !ruby/object:Gem::Version
110
110
  version: '3.5'
111
+ - !ruby/object:Gem::Dependency
112
+ name: webmock
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '3.8'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '3.8'
111
125
  description: AnyCable is a polyglot replacement for ActionCable-compatible servers
112
126
  email:
113
127
  - dementiev.vm@gmail.com
@@ -126,6 +140,8 @@ files:
126
140
  - bin/setup
127
141
  - lib/anycable.rb
128
142
  - lib/anycable/broadcast_adapters.rb
143
+ - lib/anycable/broadcast_adapters/base.rb
144
+ - lib/anycable/broadcast_adapters/http.rb
129
145
  - lib/anycable/broadcast_adapters/redis.rb
130
146
  - lib/anycable/cli.rb
131
147
  - lib/anycable/config.rb
@@ -165,9 +181,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
165
181
  version: 2.5.0
166
182
  required_rubygems_version: !ruby/object:Gem::Requirement
167
183
  requirements:
168
- - - ">"
184
+ - - ">="
169
185
  - !ruby/object:Gem::Version
170
- version: 1.3.1
186
+ version: '0'
171
187
  requirements: []
172
188
  rubygems_version: 3.0.6
173
189
  signing_key: