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.
@@ -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 = coder.decode(message)
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
- :hub,
15
- :coder,
16
- :rpc_host,
17
- :header_names
18
-
19
- def initialize(pinger:, hub:, coder:, rpc_host:, header_names:)
20
- @pinger = pinger
21
- @hub = hub
22
- @coder = coder
23
- @rpc_host = rpc_host
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, { "Content-Type" => "text/plain" }, ["Not Found"]]
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
- rpc_host: rpc_host,
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('-', '_').upcase}"]
90
- acc[name] = header_val unless header_val.nil? || header_val.empty?
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
@@ -8,7 +8,10 @@ module AnyCable
8
8
  class Pinger
9
9
  INTERVAL = 3
10
10
 
11
- def initialize
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
- { type: :ping, message: time }.to_json
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 = Config.new
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
- # Only if AnyCable adapter is used
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
- if config.run_rpc && !config.running_rpc
41
- AnyCable::Rack::RPCRunner.run(
42
- rpc_host: "[::]:#{config.rpc_port}",
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 "grpc"
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 :stub
11
+ attr_reader :pool, :metadata
11
12
 
12
- def initialize(host)
13
- @stub = AnyCable::RPC::Service.rpc_stub_class.new(host, :this_channel_is_insecure)
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:, path:)
17
- request = ConnectionRequest.new(headers: headers, path: path)
18
- stub.connect(request)
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.command(message)
40
+ pool.with do |stub|
41
+ stub.command(message, metadata)
42
+ end
29
43
  end
30
44
 
31
- def disconnect(identifiers:, subscriptions:, headers:, path:)
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
- headers: headers,
36
- path: path
49
+ env: Env.new(
50
+ headers: headers,
51
+ url: url,
52
+ cstate: state,
53
+ istate: encode_istate(channels_state)
54
+ )
37
55
  )
38
- stub.disconnect(request)
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 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 {
@@ -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/rpc_runner"
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
- DEFAULT_HEADERS = %w[cookie x-api-token].freeze
20
-
21
- attr_reader :broadcast,
22
- :coder,
23
- :hub,
24
- :middleware,
25
- :pinger,
26
- :pubsub_channel,
27
- :rpc_host,
28
- :headers
29
-
30
- def initialize(*args)
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
- @pinger = Pinger.new
35
- @coder = options.fetch(:coder, Coders::JSON)
36
- @pubsub_channel = pubsub_channel
37
-
38
- @headers = options.fetch(:headers, DEFAULT_HEADERS)
39
- @rpc_host = options.fetch(:rpc_host)
40
-
41
- @broadcast = BroadcastSubscribers::RedisSubscriber.new(
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
- rpc_host: rpc_host,
44
+ rpc_client: rpc_client,
52
45
  coder: coder
53
46
  )
54
47
 
55
- log(:info) { "Using RPC server at #{rpc_host}" }
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.subscribe(AnyCable.config.redis_channel)
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.unsubscribe(@_redis_channel)
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(rpc_host: #{rpc_host}, headers: [#{headers.join(', ')}])>"
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