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.
@@ -1,10 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AnyCable
4
- module RackServer
4
+ module Rack
5
5
  module Errors
6
6
  class HijackNotAvailable < RuntimeError; end
7
- class UnknownCommand < StandardError; end
8
7
  class MiddlewareSetup < StandardError; end
9
8
  end
10
9
  end
@@ -1,9 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "set"
4
+
3
5
  module AnyCable
4
- module RackServer
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
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AnyCable
4
- module RackServer
4
+ module Rack
5
5
  module Logging # :nodoc:
6
- PREFIX = 'AnycableRackServer'
6
+ PREFIX = "AnyCableRackServer"
7
7
 
8
8
  private
9
9
 
@@ -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 RackServer
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
- { type: :ping, message: time }.to_json
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 ConnectionRequest {
18
- string path = 1;
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
- string path = 3;
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