anycable-rack-server 0.1.0 → 0.4.0
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/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
|
-
[](https://cultofmartians.com/tasks/anycable-ruby-server.html)
|
2
|
+
[](https://rubygems.org/gems/anycable-rack-server)
|
3
|
+
[](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
|