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