anycable-rack-server 0.1.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/LICENSE +1 -1
- data/README.md +63 -26
- data/lib/anycable-rack-server.rb +13 -0
- data/lib/anycable/rack/broadcast_subscribers/base_subscriber.rb +41 -0
- data/lib/anycable/rack/broadcast_subscribers/http_subscriber.rb +44 -0
- data/lib/anycable/rack/broadcast_subscribers/redis_subscriber.rb +34 -19
- data/lib/anycable/rack/coders/json.rb +1 -1
- data/lib/anycable/rack/coders/msgpack.rb +22 -0
- data/lib/anycable/rack/config.rb +22 -0
- data/lib/anycable/rack/connection.rb +36 -21
- data/lib/anycable/rack/hub.rb +32 -1
- data/lib/anycable/rack/middleware.rb +15 -15
- data/lib/anycable/rack/pinger.rb +5 -2
- data/lib/anycable/rack/railtie.rb +6 -30
- data/lib/anycable/rack/rpc/client.rb +41 -14
- data/lib/anycable/rack/rpc/rpc.proto +18 -4
- data/lib/anycable/rack/server.rb +63 -36
- data/lib/anycable/rack/socket.rb +29 -19
- data/lib/anycable/rack/version.rb +1 -1
- metadata +67 -30
- data/lib/anycable/rack/rpc_runner.rb +0 -60
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ac33d07a88c1d94d982a7068f7636fc74013dd13e18a08ba29019c1b6e90bdb1
|
4
|
+
data.tar.gz: c655148349178d792f6609202762845189669053a14f89aa4a5f0c6efae67ff6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4ecbcb8c30cfa5e4b79948bc37c06405068d80bb192f53d98901d7177110da4822a20eff456a12fb7da5e7a3c3f8757ad219acb0befb2d7cd705288b3b1a3b12
|
7
|
+
data.tar.gz: a687c493138b5e0c7429e6a58c1b99ea4388a8b988988f0cc4bcab656db4789494e6b0fb05d59936794dfb6a99dc1b4868b90f82c551c049a2c33cf1c9c78d98
|
data/LICENSE
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
MIT License
|
2
2
|
|
3
|
-
Copyright (c) 2019 Yulia Oletskaya
|
3
|
+
Copyright (c) 2019-2020 Yulia Oletskaya, Vladimir Dementyev
|
4
4
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
data/README.md
CHANGED
@@ -1,4 +1,6 @@
|
|
1
|
-
[![
|
1
|
+
[![Cult Of Martians](http://cultofmartians.com/assets/badges/badge.svg)](https://cultofmartians.com/tasks/anycable-ruby-server.html)
|
2
|
+
[![Gem Version](https://badge.fury.io/rb/anycable-rack-server.svg)](https://rubygems.org/gems/anycable-rack-server)
|
3
|
+
[![Build](https://github.com/anycable/anycable-rack-server/workflows/Build/badge.svg)](https://github.com/anycable/anycable-rack-server/actions)
|
2
4
|
|
3
5
|
# anycable-rack-server
|
4
6
|
|
@@ -8,9 +10,7 @@
|
|
8
10
|
|
9
11
|
```ruby
|
10
12
|
# Initialize server instance first.
|
11
|
-
|
12
|
-
# NOTE: you must run RPC server yourself and provide its host
|
13
|
-
ws_server = AnyCable::Rack::Server.new rpc_host: "localhost:50051"
|
13
|
+
ws_server = AnyCable::Rack::Server.new
|
14
14
|
|
15
15
|
app = Rack::Builder.new do
|
16
16
|
map "/cable" do
|
@@ -18,7 +18,7 @@ app = Rack::Builder.new do
|
|
18
18
|
end
|
19
19
|
end
|
20
20
|
|
21
|
-
# NOTE: don't forget to call `start!` method
|
21
|
+
# NOTE: don't forget to call `start!` method
|
22
22
|
ws_server.start!
|
23
23
|
|
24
24
|
run app
|
@@ -28,59 +28,96 @@ run app
|
|
28
28
|
|
29
29
|
Add `gem "anycable-rack-server"` to you `Gemfile` and make sure your Action Cable adapter is set to `:any_cable`. That's it! We automatically start AnyCable Rack server for your at `/cable` path.
|
30
30
|
|
31
|
-
##
|
31
|
+
## Configuration
|
32
|
+
|
33
|
+
AnyCable Rack Server uses [`anyway_config`](https://github.com/palkan/anyway_config) gem for configuration; thus it is possible to set configuration parameters through environment vars (prefixed with `ANYCABLE_`), `config/anycable.yml` file or `secrets.yml` when using Rails.
|
34
|
+
|
35
|
+
**NOTE:** AnyCable Rack Server uses the same config name (i.e., env prefix, YML file name, etc.) as AnyCable itself.
|
36
|
+
|
37
|
+
You can pass a config object as the option to `AnyCable::Rack::Server.new`:
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
server = AnyCable::Server::Rack.new(config: AnyCable::Rack::Config.new(**params))
|
41
|
+
```
|
42
|
+
|
43
|
+
If no config is passed, a default, global, configuration would be used (`AnyCable::Rack.config`).
|
44
|
+
|
45
|
+
When using Rails, `config.anycable_rack` points to `AnyCable::Rack.config`.
|
46
|
+
|
47
|
+
### Headers
|
32
48
|
|
33
49
|
You can customize the headers being sent with each gRPC request.
|
34
50
|
|
35
51
|
Default headers: `'cookie', 'x-api-token'`.
|
36
52
|
|
37
|
-
Can be specified via
|
53
|
+
Can be specified via configuration:
|
38
54
|
|
39
55
|
```ruby
|
40
|
-
|
41
|
-
rpc_host: "localhost:50051",
|
42
|
-
headers: ["cookie", "x-my-header"]
|
43
|
-
)
|
56
|
+
AnyCable::Rack.config.headers = ["cookie", "x-my-header"]
|
44
57
|
```
|
45
58
|
|
46
|
-
|
59
|
+
Or in Rails:
|
47
60
|
|
48
61
|
```ruby
|
49
62
|
# <environment>.rb
|
50
63
|
config.any_cable_rack.headers = %w[cookie]
|
64
|
+
```
|
65
|
+
|
66
|
+
### Rails-specific options
|
67
|
+
|
68
|
+
```ruby
|
69
|
+
# Mount WebSocket server at the specified path
|
51
70
|
config.any_cable_rack.mount_path = "/cable"
|
52
71
|
# NOTE: here we specify only the port (we assume that a server is running locally)
|
53
72
|
config.any_cable_rack.rpc_port = 50051
|
54
73
|
```
|
55
74
|
|
56
|
-
##
|
75
|
+
## Broadcast adapters
|
76
|
+
|
77
|
+
AnyCable Rack supports Redis (default) and HTTP broadcast adapters (see [the documentation](https://docs.anycable.io/#/ruby/broadcast_adapters)).
|
78
|
+
|
79
|
+
Broadcast adapter is inherited from AnyCable configuration (so, you don't need to configure it twice).
|
57
80
|
|
58
|
-
|
81
|
+
### Using HTTP broadcast adapter
|
59
82
|
|
60
|
-
|
83
|
+
### With Rack
|
61
84
|
|
62
85
|
```ruby
|
63
|
-
|
86
|
+
AnyCable::Rack.config.broadast_adapter = :http
|
64
87
|
|
65
|
-
AnyCable::Rack::
|
66
|
-
root_dir: "<path to your app root directory to run `anycable` from>",
|
67
|
-
rpc_host: "...", # optional host to run RPC server on (defaults to '[::]::50051')
|
68
|
-
command_args: [] # additional CLI arguments
|
69
|
-
)
|
88
|
+
ws_server = AnyCable::Rack::Server
|
70
89
|
|
71
|
-
|
72
|
-
|
73
|
-
|
90
|
+
app = Rack::Builder.new do
|
91
|
+
map "/cable" do
|
92
|
+
run ws_server
|
93
|
+
end
|
94
|
+
|
95
|
+
map "/_anycable_rack_broadcast" do
|
96
|
+
run ws_server.broadcast
|
97
|
+
end
|
98
|
+
end
|
74
99
|
```
|
75
100
|
|
101
|
+
### With Rails
|
102
|
+
|
103
|
+
By default, we mount broadcasts endpoint at `/_anycable_rack_broadcast`.
|
104
|
+
|
105
|
+
You can change this setting:
|
106
|
+
|
107
|
+
```ruby
|
108
|
+
config.any_cable_rack.http_broadcast_path = "/_my_broadcast"
|
109
|
+
```
|
110
|
+
|
111
|
+
**NOTE:** Don't forget to configure `http_broadcast_url` for AnyCable pointing to your web server and the specified broadcast path.
|
112
|
+
|
76
113
|
## Testing
|
77
114
|
|
78
115
|
Run units with `bundle exec rake`.
|
79
116
|
|
80
117
|
## Contributing
|
81
118
|
|
82
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/anycable/anycable-rack-server.
|
119
|
+
Bug reports and pull requests are welcome on GitHub at [https://github.com/anycable/anycable-rack-server](https://github.com/anycable/anycable-rack-server).
|
83
120
|
|
84
121
|
## License
|
85
122
|
|
86
|
-
The gem is available as open source under the terms of the [MIT License](
|
123
|
+
The gem is available as open source under the terms of the [MIT License](./LICENSE).
|
data/lib/anycable-rack-server.rb
CHANGED
@@ -1,4 +1,17 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "anycable/rack/version"
|
4
|
+
require "anycable/rack/config"
|
3
5
|
require "anycable/rack/server"
|
6
|
+
|
7
|
+
module AnyCable
|
8
|
+
module Rack
|
9
|
+
class << self
|
10
|
+
def config
|
11
|
+
@config ||= Config.new
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
4
17
|
require "anycable/rack/railtie" if defined?(Rails)
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
module AnyCable
|
6
|
+
module Rack
|
7
|
+
module BroadcastSubscribers
|
8
|
+
class BaseSubscriber
|
9
|
+
include Logging
|
10
|
+
|
11
|
+
attr_reader :hub, :coder
|
12
|
+
|
13
|
+
def initialize(hub:, coder:, **options)
|
14
|
+
@hub = hub
|
15
|
+
@coder = coder
|
16
|
+
end
|
17
|
+
|
18
|
+
def start
|
19
|
+
# no-op
|
20
|
+
end
|
21
|
+
|
22
|
+
def stop
|
23
|
+
# no-op
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def handle_message(msg)
|
29
|
+
log(:debug) { "Received pub/sub message: #{msg}" }
|
30
|
+
|
31
|
+
data = JSON.parse(msg)
|
32
|
+
if data["stream"]
|
33
|
+
hub.broadcast(data["stream"], data["data"], coder)
|
34
|
+
elsif data["command"] == "disconnect"
|
35
|
+
hub.disconnect(data["payload"]["identifier"], data["payload"]["reconnect"], coder)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
module AnyCable
|
6
|
+
module Rack
|
7
|
+
module BroadcastSubscribers
|
8
|
+
# HTTP Pub/Sub subscriber
|
9
|
+
class HTTPSubscriber < BaseSubscriber
|
10
|
+
attr_reader :token, :path
|
11
|
+
|
12
|
+
def initialize(**options)
|
13
|
+
super
|
14
|
+
@token = options[:token]
|
15
|
+
@path = options[:path]
|
16
|
+
end
|
17
|
+
|
18
|
+
def start
|
19
|
+
log(:info) { "Accepting pub/sub request at #{path}" }
|
20
|
+
end
|
21
|
+
|
22
|
+
def call(env)
|
23
|
+
req = ::Rack::Request.new(env)
|
24
|
+
|
25
|
+
return invalid_request unless req.post?
|
26
|
+
|
27
|
+
if token && req.get_header("HTTP_AUTHORIZATION") != "Bearer #{token}"
|
28
|
+
return invalid_request(401)
|
29
|
+
end
|
30
|
+
|
31
|
+
handle_message req.body.read
|
32
|
+
|
33
|
+
[201, {"Content-Type" => "text/plain"}, ["OK"]]
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def invalid_request(code = 422)
|
39
|
+
[code, {"Content-Type" => "text/plain"}, ["Invalid request"]]
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
gem "redis", "~> 4"
|
4
|
+
|
3
5
|
require "redis"
|
4
6
|
require "json"
|
5
7
|
|
@@ -7,34 +9,47 @@ module AnyCable
|
|
7
9
|
module Rack
|
8
10
|
module BroadcastSubscribers
|
9
11
|
# Redis Pub/Sub subscriber
|
10
|
-
class RedisSubscriber
|
11
|
-
attr_reader :
|
12
|
+
class RedisSubscriber < BaseSubscriber
|
13
|
+
attr_reader :redis_conn, :thread, :channel
|
12
14
|
|
13
|
-
def initialize(hub:, coder:, **options)
|
14
|
-
|
15
|
-
@coder = coder
|
15
|
+
def initialize(hub:, coder:, channel:, **options)
|
16
|
+
super
|
16
17
|
@redis_conn = ::Redis.new(options)
|
17
|
-
@
|
18
|
+
@channel = channel
|
18
19
|
end
|
19
20
|
|
20
|
-
def
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
end
|
25
|
-
end
|
21
|
+
def start
|
22
|
+
subscribe(channel)
|
23
|
+
|
24
|
+
log(:info) { "Subscribed to #{channel}" }
|
26
25
|
end
|
27
26
|
|
28
|
-
def
|
29
|
-
|
30
|
-
@threads.delete(channel)
|
27
|
+
def stop
|
28
|
+
thread&.terminate
|
31
29
|
end
|
32
30
|
|
33
|
-
|
31
|
+
def subscribe(channel)
|
32
|
+
@thread ||= Thread.new do
|
33
|
+
Thread.current.abort_on_exception = true
|
34
|
+
|
35
|
+
redis_conn.without_reconnect do
|
36
|
+
redis_conn.subscribe(channel) do |on|
|
37
|
+
on.subscribe do |chan, count|
|
38
|
+
log(:debug) { "Redis subscriber connected to #{chan} (#{count})" }
|
39
|
+
end
|
40
|
+
|
41
|
+
on.unsubscribe do |chan, count|
|
42
|
+
log(:debug) { "Redis subscribed disconnected from #{chan} (#{count})" }
|
43
|
+
end
|
34
44
|
|
35
|
-
|
36
|
-
|
37
|
-
|
45
|
+
on.message do |_channel, msg|
|
46
|
+
handle_message(msg)
|
47
|
+
rescue
|
48
|
+
log(:error) { "Failed to broadcast message: #{msg}" }
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
38
53
|
end
|
39
54
|
end
|
40
55
|
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
gem "msgpack", "~> 1.4"
|
4
|
+
require "msgpack"
|
5
|
+
|
6
|
+
module AnyCable
|
7
|
+
module Rack
|
8
|
+
module Coders
|
9
|
+
module Msgpack # :nodoc:
|
10
|
+
class << self
|
11
|
+
def decode(bin)
|
12
|
+
MessagePack.unpack(bin)
|
13
|
+
end
|
14
|
+
|
15
|
+
def encode(ruby_obj)
|
16
|
+
BinaryFrame.new(MessagePack.pack(ruby_obj))
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "anyway_config"
|
4
|
+
|
5
|
+
module AnyCable
|
6
|
+
module Rack
|
7
|
+
class Config < Anyway::Config
|
8
|
+
DEFAULT_HEADERS = %w[cookie x-api-token].freeze
|
9
|
+
|
10
|
+
config_name :anycable
|
11
|
+
env_prefix "ANYCABLE"
|
12
|
+
|
13
|
+
attr_config mount_path: "/cable",
|
14
|
+
headers: DEFAULT_HEADERS,
|
15
|
+
coder: :json,
|
16
|
+
rpc_addr: "localhost:50051",
|
17
|
+
rpc_client_pool_size: 5,
|
18
|
+
rpc_client_timeout: 5,
|
19
|
+
http_broadcast_path: "/_anycable_rack_broadcast"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -14,23 +14,24 @@ module AnyCable
|
|
14
14
|
include Logging
|
15
15
|
|
16
16
|
attr_reader :coder,
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
17
|
+
:headers,
|
18
|
+
:hub,
|
19
|
+
:socket,
|
20
|
+
:rpc_client,
|
21
|
+
:sid
|
22
22
|
|
23
|
-
def initialize(socket, hub:, coder:,
|
23
|
+
def initialize(socket, hub:, coder:, rpc_client:, headers:)
|
24
24
|
@socket = socket
|
25
25
|
@coder = coder
|
26
26
|
@headers = headers
|
27
27
|
@hub = hub
|
28
28
|
@sid = SecureRandom.hex(6)
|
29
29
|
|
30
|
-
@rpc_client =
|
30
|
+
@rpc_client = rpc_client
|
31
31
|
|
32
|
-
@_identifiers
|
32
|
+
@_identifiers = "{}"
|
33
33
|
@_subscriptions = Set.new
|
34
|
+
@_istate = {}
|
34
35
|
end
|
35
36
|
|
36
37
|
def handle_open
|
@@ -53,20 +54,21 @@ module AnyCable
|
|
53
54
|
log(:debug) { "Command: #{decoded}" }
|
54
55
|
|
55
56
|
case command
|
56
|
-
when "subscribe"
|
57
|
+
when "subscribe" then subscribe(channel_identifier)
|
57
58
|
when "unsubscribe" then unsubscribe(channel_identifier)
|
58
|
-
when "message"
|
59
|
+
when "message" then send_message(channel_identifier, decoded["data"])
|
59
60
|
else
|
60
61
|
log(:error, "Command not found #{command}")
|
61
62
|
end
|
62
|
-
rescue Exception => e
|
63
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
63
64
|
log(:error, "Failed to execute command #{command}: #{e.message}")
|
64
65
|
end
|
65
66
|
|
66
67
|
private
|
67
68
|
|
68
69
|
def transmit(cable_message)
|
69
|
-
|
70
|
+
encoded = encode(cable_message)
|
71
|
+
socket.transmit(encoded)
|
70
72
|
end
|
71
73
|
|
72
74
|
def close
|
@@ -77,12 +79,8 @@ module AnyCable
|
|
77
79
|
socket.request
|
78
80
|
end
|
79
81
|
|
80
|
-
def request_path
|
81
|
-
request.fullpath
|
82
|
-
end
|
83
|
-
|
84
82
|
def rpc_connect
|
85
|
-
rpc_client.connect(headers: headers,
|
83
|
+
rpc_client.connect(headers: headers, url: request.url)
|
86
84
|
end
|
87
85
|
|
88
86
|
def rpc_disconnect
|
@@ -90,7 +88,9 @@ module AnyCable
|
|
90
88
|
identifiers: @_identifiers,
|
91
89
|
subscriptions: @_subscriptions.to_a,
|
92
90
|
headers: headers,
|
93
|
-
|
91
|
+
url: request.url,
|
92
|
+
state: @_cstate,
|
93
|
+
channels_state: @_istate
|
94
94
|
)
|
95
95
|
end
|
96
96
|
|
@@ -99,7 +99,11 @@ module AnyCable
|
|
99
99
|
command: command,
|
100
100
|
identifier: identifier,
|
101
101
|
connection_identifiers: @_identifiers,
|
102
|
-
data: data
|
102
|
+
data: data,
|
103
|
+
headers: headers,
|
104
|
+
url: request.url,
|
105
|
+
connection_state: @_cstate,
|
106
|
+
state: @_istate[identifier]
|
103
107
|
)
|
104
108
|
end
|
105
109
|
|
@@ -130,16 +134,23 @@ module AnyCable
|
|
130
134
|
end
|
131
135
|
|
132
136
|
def process_command(response, identifier)
|
133
|
-
response.transmissions.each { |transmission| transmit(
|
137
|
+
response.transmissions.each { |transmission| transmit(decode_transmission(transmission)) }
|
134
138
|
hub.remove_channel(socket, identifier) if response.stop_streams
|
135
139
|
response.streams.each { |stream| hub.add_subscriber(stream, socket, identifier) }
|
140
|
+
response.stopped_streams.each { |stream| hub.remove_subscriber(stream, socket, identifier) }
|
141
|
+
|
142
|
+
@_istate[identifier] ||= {}
|
143
|
+
@_istate[identifier].merge!(response.env.istate&.to_h || {})
|
144
|
+
|
136
145
|
close_connection if response.disconnect
|
137
146
|
end
|
138
147
|
|
139
148
|
def process_open(response)
|
149
|
+
response.transmissions&.each { |transmission| transmit(decode_transmission(transmission)) }
|
140
150
|
if response.status == :SUCCESS
|
141
151
|
@_identifiers = response.identifiers
|
142
|
-
response.
|
152
|
+
@_cstate = response.env.cstate&.to_h || {}
|
153
|
+
hub.add_socket(socket, @_identifiers)
|
143
154
|
log(:debug) { "Opened" }
|
144
155
|
else
|
145
156
|
log(:error, "RPC connection command failed: #{response.inspect}")
|
@@ -171,6 +182,10 @@ module AnyCable
|
|
171
182
|
coder.encode(cable_message)
|
172
183
|
end
|
173
184
|
|
185
|
+
def decode_transmission(json_message)
|
186
|
+
JSON.parse(json_message)
|
187
|
+
end
|
188
|
+
|
174
189
|
def decode(websocket_message)
|
175
190
|
coder.decode(websocket_message)
|
176
191
|
end
|