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
data/lib/anycable/rack/hub.rb
CHANGED
@@ -6,6 +6,8 @@ module AnyCable
|
|
6
6
|
module Rack
|
7
7
|
# From https://github.com/rails/rails/blob/v5.0.1/actioncable/lib/action_cable/subscription_adapter/subscriber_map.rb
|
8
8
|
class Hub
|
9
|
+
INTERNAL_STREAM = :__internal__
|
10
|
+
|
9
11
|
attr_reader :streams, :sockets
|
10
12
|
|
11
13
|
def initialize
|
@@ -16,6 +18,12 @@ module AnyCable
|
|
16
18
|
@sync = Mutex.new
|
17
19
|
end
|
18
20
|
|
21
|
+
def add_socket(socket, identifier)
|
22
|
+
@sync.synchronize do
|
23
|
+
@streams[INTERNAL_STREAM][identifier] << socket
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
19
27
|
def add_subscriber(stream, socket, channel)
|
20
28
|
@sync.synchronize do
|
21
29
|
@streams[stream][channel] << socket
|
@@ -63,12 +71,31 @@ module AnyCable
|
|
63
71
|
end
|
64
72
|
|
65
73
|
list.each do |(channel_id, sockets)|
|
66
|
-
decoded =
|
74
|
+
decoded = JSON.parse(message)
|
67
75
|
cmessage = channel_message(channel_id, decoded, coder)
|
68
76
|
sockets.each { |socket| socket.transmit(cmessage) }
|
69
77
|
end
|
70
78
|
end
|
71
79
|
|
80
|
+
def broadcast_all(message)
|
81
|
+
sockets.each_key { |socket| socket.transmit(message) }
|
82
|
+
end
|
83
|
+
|
84
|
+
def disconnect(identifier, reconnect, coder)
|
85
|
+
sockets = @sync.synchronize do
|
86
|
+
return unless @streams[INTERNAL_STREAM].key?(identifier)
|
87
|
+
|
88
|
+
@streams[INTERNAL_STREAM][identifier].to_a
|
89
|
+
end
|
90
|
+
|
91
|
+
msg = disconnect_message("remote", reconnect, coder)
|
92
|
+
|
93
|
+
sockets.each do |socket|
|
94
|
+
socket.transmit(msg)
|
95
|
+
socket.close
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
72
99
|
def close_all
|
73
100
|
hub.sockets.dup.each do |socket|
|
74
101
|
hub.remove_socket(socket)
|
@@ -87,6 +114,10 @@ module AnyCable
|
|
87
114
|
def channel_message(channel_id, message, coder)
|
88
115
|
coder.encode(identifier: channel_id, message: message)
|
89
116
|
end
|
117
|
+
|
118
|
+
def disconnect_message(reason, reconnect, coder)
|
119
|
+
coder.encode({type: :disconnect, reason: reason, reconnect: reconnect})
|
120
|
+
end
|
90
121
|
end
|
91
122
|
end
|
92
123
|
end
|
@@ -9,18 +9,18 @@ require "anycable/rack/socket"
|
|
9
9
|
module AnyCable
|
10
10
|
module Rack
|
11
11
|
class Middleware # :nodoc:
|
12
|
-
PROTOCOLS = ["actioncable-v1-json", "actioncable-unsupported"].freeze
|
12
|
+
PROTOCOLS = ["actioncable-v1-json", "actioncable-v1-msgpack", "actioncable-unsupported"].freeze
|
13
13
|
attr_reader :pinger,
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
def initialize(pinger:, hub:, coder:,
|
20
|
-
@pinger
|
21
|
-
@hub
|
22
|
-
@coder
|
23
|
-
@
|
14
|
+
:hub,
|
15
|
+
:coder,
|
16
|
+
:rpc_client,
|
17
|
+
:header_names
|
18
|
+
|
19
|
+
def initialize(pinger:, hub:, coder:, rpc_client:, header_names:)
|
20
|
+
@pinger = pinger
|
21
|
+
@hub = hub
|
22
|
+
@coder = coder
|
23
|
+
@rpc_client = rpc_client
|
24
24
|
@header_names = header_names
|
25
25
|
end
|
26
26
|
|
@@ -59,7 +59,7 @@ module AnyCable
|
|
59
59
|
end
|
60
60
|
|
61
61
|
def not_found
|
62
|
-
[404, {
|
62
|
+
[404, {"Content-Type" => "text/plain"}, ["Not Found"]]
|
63
63
|
end
|
64
64
|
|
65
65
|
def websocket?(env)
|
@@ -71,7 +71,7 @@ module AnyCable
|
|
71
71
|
socket,
|
72
72
|
hub: hub,
|
73
73
|
coder: coder,
|
74
|
-
|
74
|
+
rpc_client: rpc_client,
|
75
75
|
headers: fetch_headers(socket.request)
|
76
76
|
)
|
77
77
|
socket.onopen { connection.handle_open }
|
@@ -86,8 +86,8 @@ module AnyCable
|
|
86
86
|
|
87
87
|
def fetch_headers(request)
|
88
88
|
header_names.each_with_object({}) do |name, acc|
|
89
|
-
header_val = request.env["HTTP_#{name.tr(
|
90
|
-
acc[name]
|
89
|
+
header_val = request.env["HTTP_#{name.tr("-", "_").upcase}"]
|
90
|
+
acc[name] = header_val unless header_val.nil? || header_val.empty?
|
91
91
|
end
|
92
92
|
end
|
93
93
|
end
|
data/lib/anycable/rack/pinger.rb
CHANGED
@@ -8,7 +8,10 @@ module AnyCable
|
|
8
8
|
class Pinger
|
9
9
|
INTERVAL = 3
|
10
10
|
|
11
|
-
|
11
|
+
attr_reader :coder
|
12
|
+
|
13
|
+
def initialize(coder)
|
14
|
+
@coder = coder
|
12
15
|
@_sockets = []
|
13
16
|
@_stopped = false
|
14
17
|
end
|
@@ -45,7 +48,7 @@ module AnyCable
|
|
45
48
|
private
|
46
49
|
|
47
50
|
def ping_message(time)
|
48
|
-
{
|
51
|
+
coder.encode({type: :ping, message: time})
|
49
52
|
end
|
50
53
|
end
|
51
54
|
end
|
@@ -3,48 +3,24 @@
|
|
3
3
|
module AnyCable
|
4
4
|
module Rack
|
5
5
|
class Railtie < ::Rails::Railtie # :nodoc: all
|
6
|
-
class Config < Anyway::Config
|
7
|
-
config_name :anycable_rack
|
8
|
-
env_prefix "ANYCABLE_RACK"
|
9
|
-
|
10
|
-
attr_config mount_path: "/cable",
|
11
|
-
headers: AnyCable::Rack::Server::DEFAULT_HEADERS,
|
12
|
-
rpc_port: 50_051,
|
13
|
-
rpc_host: "localhost",
|
14
|
-
run_rpc: false,
|
15
|
-
running_rpc: false
|
16
|
-
|
17
|
-
private :running_rpc=
|
18
|
-
end
|
19
|
-
|
20
6
|
config.before_configuration do
|
21
|
-
config.any_cable_rack =
|
7
|
+
config.any_cable_rack = AnyCable::Rack.config
|
22
8
|
end
|
23
9
|
|
24
10
|
initializer "anycable.rack.mount", after: "action_cable.routes" do
|
25
11
|
config.after_initialize do |app|
|
26
12
|
config = app.config.any_cable_rack
|
27
13
|
|
28
|
-
|
29
|
-
next unless ::ActionCable.server.config.cable&.fetch("adapter", nil) == "any_cable"
|
14
|
+
next unless config.mount_path
|
30
15
|
|
31
|
-
server = AnyCable::Rack::Server.new
|
32
|
-
headers: config.headers,
|
33
|
-
rpc_host: "#{config.rpc_host}:#{config.rpc_port}"
|
34
|
-
)
|
16
|
+
server = AnyCable::Rack::Server.new
|
35
17
|
|
36
18
|
app.routes.prepend do
|
37
19
|
mount server => config.mount_path
|
38
|
-
end
|
39
20
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
root_dir: ::Rails.root.to_s,
|
44
|
-
env: {
|
45
|
-
"ANYCABLE_RACK_RUNNING_RPC" => "true"
|
46
|
-
}
|
47
|
-
)
|
21
|
+
if AnyCable.config.broadcast_adapter.to_s == "http"
|
22
|
+
mount server.broadcast => config.http_broadcast_path
|
23
|
+
end
|
48
24
|
end
|
49
25
|
|
50
26
|
server.start!
|
@@ -1,41 +1,68 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "
|
3
|
+
require "connection_pool"
|
4
|
+
require "anycable/grpc"
|
4
5
|
|
5
6
|
module AnyCable
|
6
7
|
module Rack
|
7
8
|
module RPC
|
8
9
|
# AnyCable RPC client
|
9
10
|
class Client
|
10
|
-
attr_reader :
|
11
|
+
attr_reader :pool, :metadata
|
11
12
|
|
12
|
-
def initialize(host)
|
13
|
-
@
|
13
|
+
def initialize(host:, size:, timeout:)
|
14
|
+
@pool = ConnectionPool.new(size: size, timeout: timeout) do
|
15
|
+
AnyCable::GRPC::Service.rpc_stub_class.new(host, :this_channel_is_insecure)
|
16
|
+
end
|
17
|
+
@metadata = {metadata: {"protov" => "v1"}}.freeze
|
14
18
|
end
|
15
19
|
|
16
|
-
def connect(headers:,
|
17
|
-
request = ConnectionRequest.new(headers: headers,
|
18
|
-
stub
|
20
|
+
def connect(headers:, url:)
|
21
|
+
request = ConnectionRequest.new(env: Env.new(headers: headers, url: url))
|
22
|
+
pool.with do |stub|
|
23
|
+
stub.connect(request, metadata)
|
24
|
+
end
|
19
25
|
end
|
20
26
|
|
21
|
-
def command(command:, identifier:, connection_identifiers:, data:)
|
27
|
+
def command(command:, identifier:, connection_identifiers:, data:, headers:, url:, connection_state: nil, state: nil)
|
22
28
|
message = CommandMessage.new(
|
23
29
|
command: command,
|
24
30
|
identifier: identifier,
|
25
31
|
connection_identifiers: connection_identifiers,
|
26
|
-
data: data
|
32
|
+
data: data,
|
33
|
+
env: Env.new(
|
34
|
+
headers: headers,
|
35
|
+
url: url,
|
36
|
+
cstate: connection_state,
|
37
|
+
istate: state
|
38
|
+
)
|
27
39
|
)
|
28
|
-
stub
|
40
|
+
pool.with do |stub|
|
41
|
+
stub.command(message, metadata)
|
42
|
+
end
|
29
43
|
end
|
30
44
|
|
31
|
-
def disconnect(identifiers:, subscriptions:, headers:,
|
45
|
+
def disconnect(identifiers:, subscriptions:, headers:, url:, state: nil, channels_state: nil)
|
32
46
|
request = DisconnectRequest.new(
|
33
47
|
identifiers: identifiers,
|
34
48
|
subscriptions: subscriptions,
|
35
|
-
|
36
|
-
|
49
|
+
env: Env.new(
|
50
|
+
headers: headers,
|
51
|
+
url: url,
|
52
|
+
cstate: state,
|
53
|
+
istate: encode_istate(channels_state)
|
54
|
+
)
|
37
55
|
)
|
38
|
-
stub
|
56
|
+
pool.with do |stub|
|
57
|
+
stub.disconnect(request, metadata)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
# We need a string -> string Hash here
|
64
|
+
def encode_istate(state)
|
65
|
+
state.transform_values(&:to_json)
|
39
66
|
end
|
40
67
|
end
|
41
68
|
end
|
@@ -14,9 +14,20 @@ enum Status {
|
|
14
14
|
FAILURE = 2;
|
15
15
|
}
|
16
16
|
|
17
|
-
message
|
18
|
-
string
|
17
|
+
message Env {
|
18
|
+
string url = 1;
|
19
19
|
map<string,string> headers = 2;
|
20
|
+
map<string,string> cstate = 3;
|
21
|
+
map<string,string> istate = 4;
|
22
|
+
}
|
23
|
+
|
24
|
+
message EnvResponse {
|
25
|
+
map<string,string> cstate = 1;
|
26
|
+
map<string,string> istate = 2;
|
27
|
+
}
|
28
|
+
|
29
|
+
message ConnectionRequest {
|
30
|
+
Env env = 3;
|
20
31
|
}
|
21
32
|
|
22
33
|
message ConnectionResponse {
|
@@ -24,6 +35,7 @@ message ConnectionResponse {
|
|
24
35
|
string identifiers = 2;
|
25
36
|
repeated string transmissions = 3;
|
26
37
|
string error_msg = 4;
|
38
|
+
EnvResponse env = 5;
|
27
39
|
}
|
28
40
|
|
29
41
|
message CommandMessage {
|
@@ -31,6 +43,7 @@ message CommandMessage {
|
|
31
43
|
string identifier = 2;
|
32
44
|
string connection_identifiers = 3;
|
33
45
|
string data = 4;
|
46
|
+
Env env = 5;
|
34
47
|
}
|
35
48
|
|
36
49
|
message CommandResponse {
|
@@ -40,13 +53,14 @@ message CommandResponse {
|
|
40
53
|
repeated string streams = 4;
|
41
54
|
repeated string transmissions = 5;
|
42
55
|
string error_msg = 6;
|
56
|
+
EnvResponse env = 7;
|
57
|
+
repeated string stopped_streams = 8;
|
43
58
|
}
|
44
59
|
|
45
60
|
message DisconnectRequest {
|
46
61
|
string identifiers = 1;
|
47
62
|
repeated string subscriptions = 2;
|
48
|
-
|
49
|
-
map<string,string> headers = 4;
|
63
|
+
Env env = 5;
|
50
64
|
}
|
51
65
|
|
52
66
|
message DisconnectResponse {
|
data/lib/anycable/rack/server.rb
CHANGED
@@ -7,52 +7,45 @@ require "anycable/rack/pinger"
|
|
7
7
|
require "anycable/rack/errors"
|
8
8
|
require "anycable/rack/middleware"
|
9
9
|
require "anycable/rack/logging"
|
10
|
-
require "anycable/rack/
|
11
|
-
require "anycable/rack/broadcast_subscribers/redis_subscriber"
|
12
|
-
require "anycable/rack/coders/json"
|
10
|
+
require "anycable/rack/broadcast_subscribers/base_subscriber"
|
13
11
|
|
14
12
|
module AnyCable # :nodoc: all
|
15
13
|
module Rack
|
16
14
|
class Server
|
17
15
|
include Logging
|
18
16
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
options = args.last.is_a?(Hash) ? args.last : {}
|
32
|
-
|
17
|
+
attr_reader :config,
|
18
|
+
:broadcast,
|
19
|
+
:coder,
|
20
|
+
:hub,
|
21
|
+
:middleware,
|
22
|
+
:pinger,
|
23
|
+
:rpc_client,
|
24
|
+
:headers,
|
25
|
+
:rpc_cli
|
26
|
+
|
27
|
+
def initialize(config: AnyCable::Rack.config)
|
28
|
+
@config = config
|
33
29
|
@hub = Hub.new
|
34
|
-
@
|
35
|
-
@
|
36
|
-
|
37
|
-
|
38
|
-
@
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
hub: hub,
|
43
|
-
coder: coder,
|
44
|
-
**AnyCable.config.to_redis_params
|
30
|
+
@coder = resolve_coder(config.coder)
|
31
|
+
@pinger = Pinger.new(coder)
|
32
|
+
|
33
|
+
@broadcast = resolve_broadcast_adapter
|
34
|
+
@rpc_client = RPC::Client.new(
|
35
|
+
host: config.rpc_addr,
|
36
|
+
size: config.rpc_client_pool_size,
|
37
|
+
timeout: config.rpc_client_timeout
|
45
38
|
)
|
46
39
|
|
47
40
|
@middleware = Middleware.new(
|
48
|
-
header_names: headers,
|
41
|
+
header_names: config.headers,
|
49
42
|
pinger: pinger,
|
50
43
|
hub: hub,
|
51
|
-
|
44
|
+
rpc_client: rpc_client,
|
52
45
|
coder: coder
|
53
46
|
)
|
54
47
|
|
55
|
-
log(:info) { "
|
48
|
+
log(:info) { "Connecting to RPC server at #{config.rpc_addr}" }
|
56
49
|
end
|
57
50
|
# rubocop:enable
|
58
51
|
|
@@ -61,13 +54,17 @@ module AnyCable # :nodoc: all
|
|
61
54
|
|
62
55
|
pinger.run
|
63
56
|
|
64
|
-
broadcast.
|
65
|
-
|
66
|
-
log(:info) { "Subscribed to #{AnyCable.config.redis_channel}" }
|
57
|
+
broadcast.start
|
67
58
|
|
68
59
|
@_started = true
|
69
60
|
end
|
70
61
|
|
62
|
+
def shutdown
|
63
|
+
log(:info) { "Shutting down..." }
|
64
|
+
Rack.rpc_server&.shutdown
|
65
|
+
hub.broadcast_all(coder.encode(type: "disconnect", reason: "server_restart", reconnect: true))
|
66
|
+
end
|
67
|
+
|
71
68
|
def started?
|
72
69
|
@_started == true
|
73
70
|
end
|
@@ -76,7 +73,7 @@ module AnyCable # :nodoc: all
|
|
76
73
|
return unless started?
|
77
74
|
|
78
75
|
@_started = false
|
79
|
-
broadcast_subscriber.
|
76
|
+
broadcast_subscriber.stop
|
80
77
|
pinger.stop
|
81
78
|
hub.close_all
|
82
79
|
end
|
@@ -86,7 +83,37 @@ module AnyCable # :nodoc: all
|
|
86
83
|
end
|
87
84
|
|
88
85
|
def inspect
|
89
|
-
"#<AnyCable::Rack::Server(
|
86
|
+
"#<AnyCable::Rack::Server(rpc_addr: #{config.rpc_addr}, headers: [#{config.headers.join(", ")}])>"
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
def resolve_broadcast_adapter
|
92
|
+
adapter = AnyCable.config.broadcast_adapter.to_s
|
93
|
+
require "anycable/rack/broadcast_subscribers/#{adapter}_subscriber"
|
94
|
+
|
95
|
+
if adapter.to_s == "redis"
|
96
|
+
BroadcastSubscribers::RedisSubscriber.new(
|
97
|
+
hub: hub,
|
98
|
+
coder: coder,
|
99
|
+
channel: AnyCable.config.redis_channel,
|
100
|
+
**AnyCable.config.to_redis_params
|
101
|
+
)
|
102
|
+
elsif adapter.to_s == "http"
|
103
|
+
BroadcastSubscribers::HTTPSubscriber.new(
|
104
|
+
hub: hub,
|
105
|
+
coder: coder,
|
106
|
+
token: AnyCable.config.http_broadcast_secret,
|
107
|
+
path: config.http_broadcast_path
|
108
|
+
)
|
109
|
+
else
|
110
|
+
raise ArgumentError, "Unsupported broadcast adatper: #{adapter}. AnyCable Rack server only supports: redis, http"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def resolve_coder(name)
|
115
|
+
require "anycable/rack/coders/#{name}"
|
116
|
+
AnyCable::Rack::Coders.const_get(name.capitalize)
|
90
117
|
end
|
91
118
|
end
|
92
119
|
end
|