anycable 1.0.0.preview1 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
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: