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 +4 -4
- data/CHANGELOG.md +18 -259
- data/README.md +8 -9
- data/lib/anycable.rb +1 -1
- data/lib/anycable/broadcast_adapters.rb +2 -0
- data/lib/anycable/broadcast_adapters/base.rb +29 -0
- data/lib/anycable/broadcast_adapters/http.rb +130 -0
- data/lib/anycable/broadcast_adapters/redis.rb +7 -6
- data/lib/anycable/cli.rb +11 -4
- data/lib/anycable/config.rb +12 -6
- data/lib/anycable/health_server.rb +4 -2
- data/lib/anycable/middleware.rb +6 -1
- data/lib/anycable/middlewares/check_version.rb +1 -1
- data/lib/anycable/rpc.rb +8 -0
- data/lib/anycable/rpc/rpc_pb.rb +3 -0
- data/lib/anycable/rpc_handler.rb +6 -3
- data/lib/anycable/server.rb +6 -2
- data/lib/anycable/socket.rb +10 -5
- data/lib/anycable/version.rb +1 -1
- metadata +18 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: deebe94f2e541a9d14ccf4a56df910cdb5a2fb17ae6815ffaaa2f761fdcd07ad
|
4
|
+
data.tar.gz: e96c5b0d28dd7617c2cfa1249fde5bd357465be7596915720c27718f486c4f16
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b585f2797184d7bc78cc761707b741dcfa8a6b796df91cccd7ac1e44c9210a84852daba838adaaa6b2f1548d2d0f82cf57713b32a4a86d0ffa671f64321b9df1
|
7
|
+
data.tar.gz: dccd27127eaafc5b7821ba18404546269e9aaf3163faccffaba864e166cb1c19cdc914ccf9de87da963cac447541058e0d7ba906674298f9dda8573ca4e917c8
|
data/CHANGELOG.md
CHANGED
@@ -1,6 +1,20 @@
|
|
1
1
|
# Change log
|
2
2
|
|
3
|
-
##
|
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
|
-
|
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
|
-
|
41
|
+
---
|
280
42
|
|
281
|
-
|
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
|
[](https://rubygems.org/gems/anycable)
|
3
3
|
[](https://github.com/anycable/anycable/actions)
|
4
4
|
[](https://gitter.im/anycable/Lobby)
|
5
|
-
[](https://docs.anycable.io)
|
5
|
+
[](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,
|
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
|
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
|
-
##
|
49
|
-
|
50
|
-
- [AnyCable Go](https://github.com/anycable/anycable-go)
|
51
|
-
- [ErlyCable](https://github.com/anycable/erlycable)
|
50
|
+
## Building
|
52
51
|
|
53
|
-
|
52
|
+
### Generating gRPC files from `.proto`
|
54
53
|
|
55
54
|
- Install required GRPC gems:
|
56
55
|
|
data/lib/anycable.rb
CHANGED
@@ -67,7 +67,7 @@ module AnyCable
|
|
67
67
|
end
|
68
68
|
|
69
69
|
def broadcast_adapter
|
70
|
-
self.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
|
|
@@ -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
|
35
|
-
redis_conn.publish(
|
36
|
-
|
37
|
-
|
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
|
data/lib/anycable/cli.rb
CHANGED
@@ -183,7 +183,7 @@ module AnyCable
|
|
183
183
|
end
|
184
184
|
|
185
185
|
def start_pubsub!
|
186
|
-
|
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
|
-
--
|
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"
|
data/lib/anycable/config.rb
CHANGED
@@ -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
|
-
|
95
|
-
redis_sentinels.is_a?(Array)
|
101
|
+
sentinels = Array(redis_sentinels)
|
96
102
|
|
97
|
-
next if
|
103
|
+
next if sentinels.empty?
|
98
104
|
|
99
|
-
params[:sentinels] =
|
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
|
-
{
|
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"
|
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
|
-
|
51
|
+
def logger
|
52
|
+
@logger ||= AnyCable.logger
|
53
|
+
end
|
52
54
|
|
53
55
|
def build_server
|
54
56
|
require "webrick"
|
data/lib/anycable/middleware.rb
CHANGED
@@ -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::
|
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
|
data/lib/anycable/rpc.rb
CHANGED
data/lib/anycable/rpc/rpc_pb.rb
CHANGED
@@ -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
|
data/lib/anycable/rpc_handler.rb
CHANGED
@@ -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
|
|
data/lib/anycable/server.rb
CHANGED
@@ -23,7 +23,7 @@ module AnyCable
|
|
23
23
|
class Server
|
24
24
|
attr_reader :grpc_server, :host
|
25
25
|
|
26
|
-
def initialize(host:, logger:
|
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 :
|
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|
|
data/lib/anycable/socket.rb
CHANGED
@@ -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,
|
52
|
-
|
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
|
data/lib/anycable/version.rb
CHANGED
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.
|
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-
|
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
|