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