anycable-rack-server 0.0.1 → 0.3.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 +101 -15
- data/lib/anycable-rack-server.rb +15 -1
- 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 +57 -0
- data/lib/anycable/{rack-server → rack}/coders/json.rb +4 -2
- data/lib/anycable/rack/config.rb +21 -0
- data/lib/anycable/rack/connection.rb +197 -0
- data/lib/anycable/{rack-server → rack}/errors.rb +1 -2
- data/lib/anycable/{rack-server → rack}/hub.rb +42 -1
- data/lib/anycable/{rack-server → rack}/logging.rb +2 -2
- data/lib/anycable/rack/middleware.rb +95 -0
- data/lib/anycable/{rack-server → rack}/pinger.rb +4 -5
- data/lib/anycable/rack/railtie.rb +31 -0
- data/lib/anycable/rack/rpc/client.rb +70 -0
- data/lib/anycable/{rack-server → rack}/rpc/rpc.proto +18 -4
- data/lib/anycable/rack/server.rb +117 -0
- data/lib/anycable/{rack-server → rack}/socket.rb +21 -28
- data/lib/anycable/{rack-server → rack}/version.rb +2 -2
- metadata +106 -40
- data/lib/anycable/rack-server.rb +0 -107
- data/lib/anycable/rack-server/broadcast_subscribers/redis_subscriber.rb +0 -40
- data/lib/anycable/rack-server/connection.rb +0 -189
- data/lib/anycable/rack-server/middleware.rb +0 -82
- data/lib/anycable/rack-server/rpc/client.rb +0 -42
@@ -1,9 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "set"
|
4
|
+
|
3
5
|
module AnyCable
|
4
|
-
module
|
6
|
+
module Rack
|
5
7
|
# From https://github.com/rails/rails/blob/v5.0.1/actioncable/lib/action_cable/subscription_adapter/subscriber_map.rb
|
6
8
|
class Hub
|
9
|
+
INTERNAL_STREAM = :__internal__
|
10
|
+
|
7
11
|
attr_reader :streams, :sockets
|
8
12
|
|
9
13
|
def initialize
|
@@ -14,6 +18,12 @@ module AnyCable
|
|
14
18
|
@sync = Mutex.new
|
15
19
|
end
|
16
20
|
|
21
|
+
def add_socket(socket, identifier)
|
22
|
+
@sync.synchronize do
|
23
|
+
@streams[INTERNAL_STREAM][identifier] << socket
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
17
27
|
def add_subscriber(stream, socket, channel)
|
18
28
|
@sync.synchronize do
|
19
29
|
@streams[stream][channel] << socket
|
@@ -67,6 +77,32 @@ module AnyCable
|
|
67
77
|
end
|
68
78
|
end
|
69
79
|
|
80
|
+
def broadcast_all(message)
|
81
|
+
sockets.each_key { |socket| socket.transmit(message) }
|
82
|
+
end
|
83
|
+
|
84
|
+
def disconnect(identifier, reconnect)
|
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)
|
92
|
+
|
93
|
+
sockets.each do |socket|
|
94
|
+
socket.transmit(msg)
|
95
|
+
socket.close
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def close_all
|
100
|
+
hub.sockets.dup.each do |socket|
|
101
|
+
hub.remove_socket(socket)
|
102
|
+
socket.close
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
70
106
|
private
|
71
107
|
|
72
108
|
def cleanup(stream, socket, channel)
|
@@ -78,6 +114,11 @@ module AnyCable
|
|
78
114
|
def channel_message(channel_id, message, coder)
|
79
115
|
coder.encode(identifier: channel_id, message: message)
|
80
116
|
end
|
117
|
+
|
118
|
+
# FIXME: coder support?
|
119
|
+
def disconnect_message(reason, reconnect)
|
120
|
+
{type: :disconnect, reason: reason, reconnect: reconnect}.to_json
|
121
|
+
end
|
81
122
|
end
|
82
123
|
end
|
83
124
|
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "websocket"
|
4
|
+
|
5
|
+
require "anycable/rack/connection"
|
6
|
+
require "anycable/rack/errors"
|
7
|
+
require "anycable/rack/socket"
|
8
|
+
|
9
|
+
module AnyCable
|
10
|
+
module Rack
|
11
|
+
class Middleware # :nodoc:
|
12
|
+
PROTOCOLS = ["actioncable-v1-json", "actioncable-unsupported"].freeze
|
13
|
+
attr_reader :pinger,
|
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
|
+
@header_names = header_names
|
25
|
+
end
|
26
|
+
|
27
|
+
def call(env)
|
28
|
+
return not_found unless websocket?(env)
|
29
|
+
|
30
|
+
rack_hijack(env)
|
31
|
+
listen_socket(env)
|
32
|
+
|
33
|
+
[-1, {}, []]
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def handshake
|
39
|
+
@handshake ||= WebSocket::Handshake::Server.new(protocols: PROTOCOLS)
|
40
|
+
end
|
41
|
+
|
42
|
+
def rack_hijack(env)
|
43
|
+
raise Errors::HijackNotAvailable unless env["rack.hijack"]
|
44
|
+
|
45
|
+
env["rack.hijack"].call
|
46
|
+
send_handshake(env)
|
47
|
+
end
|
48
|
+
|
49
|
+
def send_handshake(env)
|
50
|
+
handshake.from_rack(env)
|
51
|
+
env["rack.hijack_io"].write(handshake.to_s)
|
52
|
+
end
|
53
|
+
|
54
|
+
def listen_socket(env)
|
55
|
+
socket = Socket.new(env, env["rack.hijack_io"], handshake.version)
|
56
|
+
init_connection(socket)
|
57
|
+
init_pinger(socket)
|
58
|
+
socket.listen
|
59
|
+
end
|
60
|
+
|
61
|
+
def not_found
|
62
|
+
[404, {"Content-Type" => "text/plain"}, ["Not Found"]]
|
63
|
+
end
|
64
|
+
|
65
|
+
def websocket?(env)
|
66
|
+
env["HTTP_UPGRADE"] == "websocket"
|
67
|
+
end
|
68
|
+
|
69
|
+
def init_connection(socket)
|
70
|
+
connection = Connection.new(
|
71
|
+
socket,
|
72
|
+
hub: hub,
|
73
|
+
coder: coder,
|
74
|
+
rpc_client: rpc_client,
|
75
|
+
headers: fetch_headers(socket.request)
|
76
|
+
)
|
77
|
+
socket.onopen { connection.handle_open }
|
78
|
+
socket.onclose { connection.handle_close }
|
79
|
+
socket.onmessage { |data| connection.handle_command(data) }
|
80
|
+
end
|
81
|
+
|
82
|
+
def init_pinger(socket)
|
83
|
+
pinger.add(socket)
|
84
|
+
socket.onclose { pinger.remove(socket) }
|
85
|
+
end
|
86
|
+
|
87
|
+
def fetch_headers(request)
|
88
|
+
header_names.each_with_object({}) do |name, acc|
|
89
|
+
header_val = request.env["HTTP_#{name.tr("-", "_").upcase}"]
|
90
|
+
acc[name] = header_val unless header_val.nil? || header_val.empty?
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -1,7 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "json"
|
4
|
+
|
3
5
|
module AnyCable
|
4
|
-
module
|
6
|
+
module Rack
|
5
7
|
# Sends pings to sockets
|
6
8
|
class Pinger
|
7
9
|
INTERVAL = 3
|
@@ -9,7 +11,6 @@ module AnyCable
|
|
9
11
|
def initialize
|
10
12
|
@_sockets = []
|
11
13
|
@_stopped = false
|
12
|
-
run
|
13
14
|
end
|
14
15
|
|
15
16
|
def add(socket)
|
@@ -24,7 +25,6 @@ module AnyCable
|
|
24
25
|
@_stopped = true
|
25
26
|
end
|
26
27
|
|
27
|
-
# rubocop: disable Metrics/MethodLength
|
28
28
|
def run
|
29
29
|
Thread.new do
|
30
30
|
loop do
|
@@ -41,12 +41,11 @@ module AnyCable
|
|
41
41
|
end
|
42
42
|
end
|
43
43
|
end
|
44
|
-
# rubocop: enable Metrics/MethodLength
|
45
44
|
|
46
45
|
private
|
47
46
|
|
48
47
|
def ping_message(time)
|
49
|
-
{
|
48
|
+
{type: :ping, message: time}.to_json
|
50
49
|
end
|
51
50
|
end
|
52
51
|
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AnyCable
|
4
|
+
module Rack
|
5
|
+
class Railtie < ::Rails::Railtie # :nodoc: all
|
6
|
+
config.before_configuration do
|
7
|
+
config.any_cable_rack = AnyCable::Rack.config
|
8
|
+
end
|
9
|
+
|
10
|
+
initializer "anycable.rack.mount", after: "action_cable.routes" do
|
11
|
+
config.after_initialize do |app|
|
12
|
+
config = app.config.any_cable_rack
|
13
|
+
|
14
|
+
next unless config.mount_path
|
15
|
+
|
16
|
+
server = AnyCable::Rack::Server.new
|
17
|
+
|
18
|
+
app.routes.prepend do
|
19
|
+
mount server => config.mount_path
|
20
|
+
|
21
|
+
if AnyCable.config.broadcast_adapter.to_s == "http"
|
22
|
+
mount server.broadcast => config.http_broadcast_path
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
server.start!
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "connection_pool"
|
4
|
+
require "anycable/grpc"
|
5
|
+
|
6
|
+
module AnyCable
|
7
|
+
module Rack
|
8
|
+
module RPC
|
9
|
+
# AnyCable RPC client
|
10
|
+
class Client
|
11
|
+
attr_reader :pool, :metadata
|
12
|
+
|
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
|
18
|
+
end
|
19
|
+
|
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
|
25
|
+
end
|
26
|
+
|
27
|
+
def command(command:, identifier:, connection_identifiers:, data:, headers:, url:, connection_state: nil, state: nil)
|
28
|
+
message = CommandMessage.new(
|
29
|
+
command: command,
|
30
|
+
identifier: identifier,
|
31
|
+
connection_identifiers: connection_identifiers,
|
32
|
+
data: data,
|
33
|
+
env: Env.new(
|
34
|
+
headers: headers,
|
35
|
+
url: url,
|
36
|
+
cstate: connection_state,
|
37
|
+
istate: state
|
38
|
+
)
|
39
|
+
)
|
40
|
+
pool.with do |stub|
|
41
|
+
stub.command(message, metadata)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def disconnect(identifiers:, subscriptions:, headers:, url:, state: nil, channels_state: nil)
|
46
|
+
request = DisconnectRequest.new(
|
47
|
+
identifiers: identifiers,
|
48
|
+
subscriptions: subscriptions,
|
49
|
+
env: Env.new(
|
50
|
+
headers: headers,
|
51
|
+
url: url,
|
52
|
+
cstate: state,
|
53
|
+
istate: encode_istate(channels_state)
|
54
|
+
)
|
55
|
+
)
|
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)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
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 {
|
@@ -0,0 +1,117 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "anycable"
|
4
|
+
|
5
|
+
require "anycable/rack/hub"
|
6
|
+
require "anycable/rack/pinger"
|
7
|
+
require "anycable/rack/errors"
|
8
|
+
require "anycable/rack/middleware"
|
9
|
+
require "anycable/rack/logging"
|
10
|
+
require "anycable/rack/broadcast_subscribers/base_subscriber"
|
11
|
+
require "anycable/rack/coders/json"
|
12
|
+
|
13
|
+
module AnyCable # :nodoc: all
|
14
|
+
module Rack
|
15
|
+
class Server
|
16
|
+
include Logging
|
17
|
+
|
18
|
+
attr_reader :config,
|
19
|
+
:broadcast,
|
20
|
+
:coder,
|
21
|
+
:hub,
|
22
|
+
:middleware,
|
23
|
+
:pinger,
|
24
|
+
:rpc_client,
|
25
|
+
:headers,
|
26
|
+
:rpc_cli
|
27
|
+
|
28
|
+
def initialize(config: AnyCable::Rack.config)
|
29
|
+
@config = config
|
30
|
+
@hub = Hub.new
|
31
|
+
@pinger = Pinger.new
|
32
|
+
# TODO: Support other coders
|
33
|
+
@coder = Coders::JSON
|
34
|
+
|
35
|
+
@broadcast = resolve_broadcast_adapter
|
36
|
+
@rpc_client = RPC::Client.new(
|
37
|
+
host: config.rpc_addr,
|
38
|
+
size: config.rpc_client_pool_size,
|
39
|
+
timeout: config.rpc_client_timeout
|
40
|
+
)
|
41
|
+
|
42
|
+
@middleware = Middleware.new(
|
43
|
+
header_names: config.headers,
|
44
|
+
pinger: pinger,
|
45
|
+
hub: hub,
|
46
|
+
rpc_client: rpc_client,
|
47
|
+
coder: coder
|
48
|
+
)
|
49
|
+
|
50
|
+
log(:info) { "Connecting to RPC server at #{config.rpc_addr}" }
|
51
|
+
end
|
52
|
+
# rubocop:enable
|
53
|
+
|
54
|
+
def start!
|
55
|
+
log(:info) { "Starting..." }
|
56
|
+
|
57
|
+
pinger.run
|
58
|
+
|
59
|
+
broadcast.start
|
60
|
+
|
61
|
+
@_started = true
|
62
|
+
end
|
63
|
+
|
64
|
+
def shutdown
|
65
|
+
log(:info) { "Shutting down..." }
|
66
|
+
Rack.rpc_server&.shutdown
|
67
|
+
hub.broadcast_all(coder.encode(type: "disconnect", reason: "server_restart", reconnect: true))
|
68
|
+
end
|
69
|
+
|
70
|
+
def started?
|
71
|
+
@_started == true
|
72
|
+
end
|
73
|
+
|
74
|
+
def stop
|
75
|
+
return unless started?
|
76
|
+
|
77
|
+
@_started = false
|
78
|
+
broadcast_subscriber.stop
|
79
|
+
pinger.stop
|
80
|
+
hub.close_all
|
81
|
+
end
|
82
|
+
|
83
|
+
def call(env)
|
84
|
+
middleware.call(env)
|
85
|
+
end
|
86
|
+
|
87
|
+
def inspect
|
88
|
+
"#<AnyCable::Rack::Server(rpc_addr: #{config.rpc_addr}, headers: [#{config.headers.join(", ")}])>"
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
def resolve_broadcast_adapter
|
94
|
+
adapter = AnyCable.config.broadcast_adapter.to_s
|
95
|
+
require "anycable/rack/broadcast_subscribers/#{adapter}_subscriber"
|
96
|
+
|
97
|
+
if adapter.to_s == "redis"
|
98
|
+
BroadcastSubscribers::RedisSubscriber.new(
|
99
|
+
hub: hub,
|
100
|
+
coder: coder,
|
101
|
+
channel: AnyCable.config.redis_channel,
|
102
|
+
**AnyCable.config.to_redis_params
|
103
|
+
)
|
104
|
+
elsif adapter.to_s == "http"
|
105
|
+
BroadcastSubscribers::HTTPSubscriber.new(
|
106
|
+
hub: hub,
|
107
|
+
coder: coder,
|
108
|
+
token: AnyCable.config.http_broadcast_secret,
|
109
|
+
path: config.http_broadcast_path
|
110
|
+
)
|
111
|
+
else
|
112
|
+
raise ArgumentError, "Unsupported broadcast adatper: #{adapter}. AnyCable Rack server only supports: redis, http"
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|