anycable 1.0.0.preview2 → 1.0.0.rc1

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: 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