anycable 1.0.0.preview2 → 1.0.0.rc1

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: 61d002d899e5ef0c6c8e17b131a569dee4e93d948a9a2d401e20c764ee573ae2
4
- data.tar.gz: 20743c021aa6c79177cd727d31468b1823ed492c11da1967513fa7cd5bc18f0a
3
+ metadata.gz: deebe94f2e541a9d14ccf4a56df910cdb5a2fb17ae6815ffaaa2f761fdcd07ad
4
+ data.tar.gz: e96c5b0d28dd7617c2cfa1249fde5bd357465be7596915720c27718f486c4f16
5
5
  SHA512:
6
- metadata.gz: a5d8ce6d031bfa1b65e12cbe49a1bb2813267997d090126a09fe55d30dc7e44a62c5315bf3e1f430bb6c8a67909245db2a9e03d3654ac7922cc08d6d09a7c14a
7
- data.tar.gz: eb9ecd34150e474fbb8c914d3f0b95e3c23752fe141fc6acf5819fefbaface12d27103bc0a62487d42ec3d76e2228c43f0c3e9b3c34423fc71ce422ad0200131
6
+ metadata.gz: b585f2797184d7bc78cc761707b741dcfa8a6b796df91cccd7ac1e44c9210a84852daba838adaaa6b2f1548d2d0f82cf57713b32a4a86d0ffa671f64321b9df1
7
+ data.tar.gz: dccd27127eaafc5b7821ba18404546269e9aaf3163faccffaba864e166cb1c19cdc914ccf9de87da963cac447541058e0d7ba906674298f9dda8573ca4e917c8
@@ -1,6 +1,20 @@
1
1
  # Change log
2
2
 
3
- ## 🚧 1.0.0 (_coming soon_)
3
+ ## 1.0.0.rc1 (2020-06-10)
4
+
5
+ - Add `Env#istate` and `EnvResponse#istate` to store channel state. ([@palkan][])
6
+
7
+ That would allow to mimic instance variables usage in Action Cable channels.
8
+
9
+ - Add `CommandResponse#stopped_streams` to support unsubscribing from particular broadcastings. ([@palkan])
10
+
11
+ `Socket#unsubscribe` is now implemented as well.
12
+
13
+ - Add `AnyCable.broadcast_adapter#broadcast_command` method. ([@palkan][])
14
+
15
+ It could be used to send commands to WS server (e.g., remote disconnect).
16
+
17
+ - Add `:http` broadcasting adapter. ([@palkan][])
4
18
 
5
19
  - **RPC schema has changed**. ([@palkan][])
6
20
 
@@ -22,267 +36,12 @@ Now you can access `request` object in channels, too (e.g., to read headers/cook
22
36
 
23
37
  See [#71](https://github.com/anycable/anycable/pull/71).
24
38
 
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`.
117
-
118
- Use it to run a gRPC server:
119
-
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
- ```
126
-
127
- All configuration options are also supported as CLI options (see `anycable -h` for more information).
128
-
129
- The only required options is the application file to load (`-r/--require`).
130
-
131
- You can omit it if you want to load an app form `./config/environment.rb` (e.g. with Rails) or `./config/anycable.rb`.
132
-
133
- AnyCable CLI also allows you to run a separate command (process) from within a RPC server:
134
-
135
- ```sh
136
- bundle exec anycable --server-command "anycable-go -p 3334"
137
- ```
138
-
139
- #### Configuration
140
-
141
- - Default server host is changed from `localhost:50051` to `0.0.0.0:50051`
142
- - Expose gRPC server parameters via `rpc_*` config params:
143
-
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
- ```
151
-
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.
155
-
156
- Now it's possible to pass Sentinel configuration via env vars:
157
-
158
- ```sh
159
- ANYCABLE_REDIS_SENTINELS=127.0.0.1:26380,127.0.0.1:26381 bundle exec anycable
160
- ```
161
-
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][])
256
-
257
- - Make commands handling more abstract. ([@palkan][])
258
-
259
- We now do not explicitly call channels action but use the only one entrypoint for all commands:
260
-
261
- ```ruby
262
- connection.handle_channel_command(identifier, command, data)
263
- ```
264
-
265
- This method should return `true` if command was successful and `false` otherwise.
266
-
267
- ## 0.4.0 (2017-01-22)
268
-
269
- - Refactor RPC API. ([@palkan][])
270
-
271
- Replace `Subscribe`, `Unsubscribe` and `Perform` methods with `Command` method.
272
-
273
- - Extract Rails functionality to separate gem. ([@palkan][])
274
-
275
- All Rails specifics now live here [https://github.com/anycable/anycable-rails](https://github.com/anycable/anycable-rails).
276
-
277
- ## 0.3.0 (2016-12-28)
39
+ - Fix building Redis Sentinel config. ([@palkan][])
278
40
 
279
- - Handle `Disconnect` requests. ([@palkan][])
41
+ ---
280
42
 
281
- Implement `Disconnect` handler, which invokes `Connection#disconnect` (along with `Channel#unsubscribed` for each subscription).
43
+ See [Changelog](https://github.com/anycable/anycable/blob/0-6-stable/CHANGELOG.md) for versions <1.0.0.
282
44
 
283
45
  [@palkan]: https://github.com/palkan
284
- [@sadovnik]: https://github.com/sadovnik
285
- [@accessd]: https://github.com/accessd
286
- [@DarthSim]: https://github.com/DarthSim
287
46
  [@sponomarev]: https://github.com/sponomarev
288
47
  [@bibendi]: https://github.com/bibendi
data/README.md CHANGED
@@ -2,28 +2,30 @@
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
 
@@ -45,12 +47,9 @@ Check out our 📑 [Documentation](https://docs.anycable.io).
45
47
 
46
48
  - 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
49
 
48
- ## Compatible WebSocket servers
49
-
50
- - [AnyCable Go](https://github.com/anycable/anycable-go)
51
- - [ErlyCable](https://github.com/anycable/erlycable)
50
+ ## Building
52
51
 
53
- ## Build
52
+ ### Generating gRPC files from `.proto`
54
53
 
55
54
  - Install required GRPC gems:
56
55
 
@@ -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
@@ -183,7 +183,7 @@ module AnyCable
183
183
  end
184
184
 
185
185
  def start_pubsub!
186
- logger.info "Broadcasting Redis channel: #{config.redis_channel}"
186
+ AnyCable.broadcast_adapter.announce!
187
187
  end
188
188
 
189
189
  # rubocop: disable Metrics/MethodLength, Metrics/AbcSize
@@ -313,9 +313,7 @@ module AnyCable
313
313
  -r, --require=path Location of application file to require, default: "config/environment.rb"
314
314
  --server-command=command Command to run WebSocket server
315
315
  --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
316
+ --broadcast-adapter=type Pub/sub adapter type for broadcasts, default: redis
319
317
  --log-level=level Logging level, default: "info"
320
318
  --log-file=path Path to log file, default: <none> (log to STDOUT)
321
319
  --log-grpc Enable gRPC logging (disabled by default)
@@ -323,6 +321,15 @@ module AnyCable
323
321
  -v, --version Print version and exit
324
322
  -h, --help Show this help
325
323
 
324
+ REDIS PUB/SUB OPTIONS
325
+ --redis-url=url Redis URL for pub/sub, default: REDIS_URL or "redis://localhost:6379/5"
326
+ --redis-channel=name Redis channel for broadcasting, default: "__anycable__"
327
+ --redis-sentinels=<...hosts> Redis Sentinel followers addresses (as a comma-separated list), default: nil
328
+
329
+ HTTP PUB/SUB OPTIONS
330
+ --http-broadcast-url HTTP pub/sub endpoint URL, default: "http://localhost:8090/_broadcast"
331
+ --http-broadcast-secret HTTP pub/sub authorization secret, default: <none> (disabled)
332
+
326
333
  HTTP HEALTH CHECKER OPTIONS
327
334
  --http-health-port=port Port to run HTTP health server on, default: <none> (disabled)
328
335
  --http-health-path=path Endpoint to server health cheks, default: "/health"
@@ -19,11 +19,18 @@ module AnyCable
19
19
  # See https://github.com/grpc/grpc/blob/f526602bff029b8db50a8d57134d72da33d8a752/include/grpc/impl/codegen/grpc_types.h#L292-L315
20
20
  rpc_server_args: {},
21
21
 
22
+ ## PubSub
23
+ broadcast_adapter: :redis,
24
+
22
25
  ### Redis options
23
26
  redis_url: ENV.fetch("REDIS_URL", "redis://localhost:6379/5"),
24
27
  redis_sentinels: nil,
25
28
  redis_channel: "__anycable__",
26
29
 
30
+ ### HTTP broadcasting options
31
+ http_broadcast_url: "http://localhost:8090/_broadcast",
32
+ http_broadcast_secret: nil,
33
+
27
34
  ### Logging options
28
35
  log_file: nil,
29
36
  log_level: :info,
@@ -91,12 +98,11 @@ module AnyCable
91
98
  {url: redis_url}.tap do |params|
92
99
  next if redis_sentinels.nil?
93
100
 
94
- raise ArgumentError, "redis_sentinels must be an array; got #{redis_sentinels}" unless
95
- redis_sentinels.is_a?(Array)
101
+ sentinels = Array(redis_sentinels)
96
102
 
97
- next if redis_sentinels.empty?
103
+ next if sentinels.empty?
98
104
 
99
- params[:sentinels] = redis_sentinels.map(&method(:parse_sentinel))
105
+ params[:sentinels] = sentinels.map(&method(:parse_sentinel))
100
106
  end
101
107
  end
102
108
 
@@ -113,13 +119,13 @@ module AnyCable
113
119
  SENTINEL_RXP = /^([\w\-_]*)\:(\d+)$/.freeze
114
120
 
115
121
  def parse_sentinel(sentinel)
116
- return sentinel if sentinel.is_a?(Hash)
122
+ return sentinel.transform_keys!(&:to_sym) if sentinel.is_a?(Hash)
117
123
 
118
124
  matches = sentinel.match(SENTINEL_RXP)
119
125
 
120
126
  raise ArgumentError, "Invalid Sentinel value: #{sentinel}" if matches.nil?
121
127
 
122
- {"host" => matches[1], "port" => matches[2].to_i}
128
+ {host: matches[1], port: matches[2].to_i}
123
129
  end
124
130
  end
125
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
  )
@@ -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))
@@ -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
 
@@ -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.preview2"
4
+ VERSION = "1.0.0.rc1"
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.0.rc1
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: 2020-06-10 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