anycable-rack-server 0.1.0 → 0.2.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9e57a096cf575590410db4c25e824d0b754af2b85eb0927e4d9fdf553da4ccea
4
- data.tar.gz: e6c5fce5b74f12bc41ce06955812cff608f50c4c66b063a459fa34ca8ed5e8e1
3
+ metadata.gz: 30ae82e08a26ece6943ba290783ed093045d5dbac93613741321aa9d62486838
4
+ data.tar.gz: a506eaaf81f5982b9a8774b6f6b0beae3e06e00ebd3e5bda1607c58375b15c19
5
5
  SHA512:
6
- metadata.gz: 304f417fc3d477625effcc7134c81708d222df3da565613f4f5878b5c0365fecd5ed2a3e9620cc692976051f9a634a66623f8adfce2729664f34882135eeec2b
7
- data.tar.gz: d5a926617db60ec606f15b73189da24ed80d69012e87b8b4968ea6e0c1e0b7f26b86a3af593a48804f575dfd2c5c7867070555df968c69ba43926d4c97df4501
6
+ metadata.gz: 4ba8c7529041061a05e46c1de89c7f44da1733843026145bd6b4a658221c2f68bd85f05f137c878658dea0ca0a7df74bfc3bbcb6d221b7a2cc013316737549a1
7
+ data.tar.gz: 64031fb35d63ac3d1fa64a490b7b821b9dd3b5070fc888dbb9e51ed1a3b9c4977b5d4ba5268c4f36dbe58d467187945207fc7e5e21ba4b28c2de8c39dd697308
data/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2019 Yulia Oletskaya
3
+ Copyright (c) 2019-2020 Yulia Oletskaya, Vladimir Dementyev
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -1,4 +1,6 @@
1
- [![Gem Version](https://badge.fury.io/rb/anycable-rack-server.svg)](https://rubygems.org/gems/anycable-rack-server) [![Build Status](https://travis-ci.org/anycable/anycable-rack-server.svg?branch=master)](https://travis-ci.org/anycable/anycable-rack-server)
1
+ [![Cult Of Martians](http://cultofmartians.com/assets/badges/badge.svg)](https://cultofmartians.com/tasks/anycable-ruby-server.html)
2
+ [![Gem Version](https://badge.fury.io/rb/anycable-rack-server.svg)](https://rubygems.org/gems/anycable-rack-server)
3
+ [![Build](https://github.com/anycable/anycable-rack-server/workflows/Build/badge.svg)](https://github.com/anycable/anycable-rack-server/actions)
2
4
 
3
5
  # anycable-rack-server
4
6
 
@@ -8,9 +10,7 @@
8
10
 
9
11
  ```ruby
10
12
  # Initialize server instance first.
11
- #
12
- # NOTE: you must run RPC server yourself and provide its host
13
- ws_server = AnyCable::Rack::Server.new rpc_host: "localhost:50051"
13
+ ws_server = AnyCable::Rack::Server.new
14
14
 
15
15
  app = Rack::Builder.new do
16
16
  map "/cable" do
@@ -18,7 +18,7 @@ app = Rack::Builder.new do
18
18
  end
19
19
  end
20
20
 
21
- # NOTE: don't forget to call `start!` method
21
+ # NOTE: don't forget to call `start!` method
22
22
  ws_server.start!
23
23
 
24
24
  run app
@@ -28,48 +28,103 @@ run app
28
28
 
29
29
  Add `gem "anycable-rack-server"` to you `Gemfile` and make sure your Action Cable adapter is set to `:any_cable`. That's it! We automatically start AnyCable Rack server for your at `/cable` path.
30
30
 
31
- ## Settings
31
+ ## Configuration
32
+
33
+ AnyCable Rack Server uses [`anyway_config`](https://github.com/palkan/anyway_config) gem for configuration; thus it is possible to set configuration parameters through environment vars (prefixed with `ANYCABLE_`), `config/anycable.yml` file or `secrets.yml` when using Rails.
34
+
35
+ **NOTE:** AnyCable Rack Server uses the same config name (i.e., env prefix, YML file name, etc.) as AnyCable itself.
36
+
37
+ You can pass a config object as the option to `AnyCable::Rack::Server.new`:
38
+
39
+ ```ruby
40
+ server = AnyCable::Server::Rack.new(config: AnyCable::Rack::Config.new(**params))
41
+ ```
42
+
43
+ If no config is passed, a default, global, configuration would be used (`AnyCable::Rack.config`).
44
+
45
+ When using Rails, `config.anycable_rack` points to `AnyCable::Rack.config`.
46
+
47
+ ### Headers
32
48
 
33
49
  You can customize the headers being sent with each gRPC request.
34
50
 
35
51
  Default headers: `'cookie', 'x-api-token'`.
36
52
 
37
- Can be specified via options:
53
+ Can be specified via configuration:
38
54
 
39
55
  ```ruby
40
- ws_server = AnyCable::Rack::Server.new(
41
- rpc_host: "localhost:50051",
42
- headers: ["cookie", "x-my-header"]
43
- )
56
+ AnyCable::Rack.config.headers = ["cookie", "x-my-header"]
44
57
  ```
45
58
 
46
- In case of Rails you can set server options via `config.any_cable_rack`:
59
+ Or in Rails:
47
60
 
48
61
  ```ruby
49
62
  # <environment>.rb
50
63
  config.any_cable_rack.headers = %w[cookie]
64
+ ```
65
+
66
+ ### Rails-specific options
67
+
68
+ ```ruby
69
+ # Mount WebSocket server at the specified path
51
70
  config.any_cable_rack.mount_path = "/cable"
52
71
  # NOTE: here we specify only the port (we assume that a server is running locally)
53
72
  config.any_cable_rack.rpc_port = 50051
54
73
  ```
55
74
 
75
+ ## Broadcast adapters
76
+
77
+ AnyCable Rack supports Redis (default) and HTTP broadcast adapters (see [the documentation](https://docs.anycable.io/v1/#/ruby/broadcast_adapters)).
78
+
79
+ Broadcast adapter is inherited from AnyCable configuration (so, you don't need to configure it twice).
80
+
81
+ ### Using HTTP broadcast adapter
82
+
83
+ ### With Rack
84
+
85
+ ```ruby
86
+ AnyCable::Rack.config.broadast_adapter = :http
87
+
88
+ ws_server = AnyCable::Rack::Server
89
+
90
+ app = Rack::Builder.new do
91
+ map "/cable" do
92
+ run ws_server
93
+ end
94
+
95
+ map "/_anycable_rack_broadcast" do
96
+ run ws_server.broadcast
97
+ end
98
+ end
99
+ ```
100
+
101
+ ### With Rails
102
+
103
+ By default, we mount broadcasts endpoint at `/_anycable_rack_broadcast`.
104
+
105
+ You can change this setting:
106
+
107
+ ```ruby
108
+ config.any_cable_rack.http_broadcast_path = "/_my_broadcast"
109
+ ```
110
+
111
+ **NOTE:** Don't forget to configure `http_broadcast_url` for AnyCable pointing to your web server and the specified broadcast path.
112
+
56
113
  ## Running RPC from the same process
57
114
 
58
115
  The goal of the Rack server is to simplify the development/testing process. But we still have to run the RPC server.
59
116
 
60
- This gem also provides a way to run RPC server from the same process (spawning a new process):
117
+ This gem also provides a way to run RPC server within the same process.
118
+ All you need to do is set `run_rpc = true` in the configuration:
61
119
 
62
120
  ```ruby
63
121
  # in Rack app
122
+ AnyCable::Rack.config.run_rpc = true
64
123
 
65
- AnyCable::Rack::RPCRunner.run(
66
- root_dir: "<path to your app root directory to run `anycable` from>",
67
- rpc_host: "...", # optional host to run RPC server on (defaults to '[::]::50051')
68
- command_args: [] # additional CLI arguments
69
- )
124
+ # and only after that
125
+ ws_server.start!
70
126
 
71
- # in Rails app you can just specify the configuration parameter (`false` by default)
72
- # and we'll take care of it
127
+ # in Rails
73
128
  config.any_cable_rack.run_rpc = true
74
129
  ```
75
130
 
@@ -79,8 +134,8 @@ Run units with `bundle exec rake`.
79
134
 
80
135
  ## Contributing
81
136
 
82
- Bug reports and pull requests are welcome on GitHub at https://github.com/anycable/anycable-rack-server.
137
+ Bug reports and pull requests are welcome on GitHub at [https://github.com/anycable/anycable-rack-server](https://github.com/anycable/anycable-rack-server).
83
138
 
84
139
  ## License
85
140
 
86
- The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
141
+ The gem is available as open source under the terms of the [MIT License](./LICENSE).
@@ -1,4 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "anycable/rack/config"
3
4
  require "anycable/rack/server"
5
+
6
+ module AnyCable
7
+ module Rack
8
+ class << self
9
+ def config
10
+ @config ||= Config.new
11
+ end
12
+
13
+ def rpc_server
14
+ return @rpc_server if instance_variable_defined?(:@rpc_server)
15
+
16
+ require "anycable/cli"
17
+ @rpc_server = AnyCable::CLI.new(embedded: true)
18
+ end
19
+ end
20
+ end
21
+ end
22
+
4
23
  require "anycable/rack/railtie" if defined?(Rails)
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module AnyCable
6
+ module Rack
7
+ module BroadcastSubscribers
8
+ class BaseSubscriber
9
+ include Logging
10
+
11
+ attr_reader :hub, :coder
12
+
13
+ def initialize(hub:, coder:, **options)
14
+ @hub = hub
15
+ @coder = coder
16
+ end
17
+
18
+ def start
19
+ # no-op
20
+ end
21
+
22
+ def stop
23
+ # no-op
24
+ end
25
+
26
+ private
27
+
28
+ def handle_message(msg)
29
+ log(:debug) { "Received pub/sub message: #{msg}" }
30
+
31
+ data = JSON.parse(msg)
32
+ if data["stream"]
33
+ hub.broadcast(data["stream"], data["data"], coder)
34
+ elsif data["command"] == "disconnect"
35
+ hub.disconnect(data["payload"]["identifier"], data["payload"]["reconnect"])
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module AnyCable
6
+ module Rack
7
+ module BroadcastSubscribers
8
+ # HTTP Pub/Sub subscriber
9
+ class HTTPSubscriber < BaseSubscriber
10
+ attr_reader :token, :path
11
+
12
+ def initialize(**options)
13
+ super
14
+ @token = options[:token]
15
+ @path = options[:path]
16
+ end
17
+
18
+ def start
19
+ log(:info) { "Accepting pub/sub request at #{path}" }
20
+ end
21
+
22
+ def call(env)
23
+ req = ::Rack::Request.new(env)
24
+
25
+ return invalid_request unless req.post?
26
+
27
+ if token && req.get_header("HTTP_AUTHORIZATION") != "Bearer #{token}"
28
+ return invalid_request(401)
29
+ end
30
+
31
+ handle_message req.body.read
32
+
33
+ [201, {"Content-Type" => "text/plain"}, ["OK"]]
34
+ end
35
+
36
+ private
37
+
38
+ def invalid_request(code = 422)
39
+ [code, {"Content-Type" => "text/plain"}, ["Invalid request"]]
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ gem "redis", "~> 4"
4
+
3
5
  require "redis"
4
6
  require "json"
5
7
 
@@ -7,34 +9,47 @@ module AnyCable
7
9
  module Rack
8
10
  module BroadcastSubscribers
9
11
  # Redis Pub/Sub subscriber
10
- class RedisSubscriber
11
- attr_reader :hub, :coder, :redis_conn, :threads
12
+ class RedisSubscriber < BaseSubscriber
13
+ attr_reader :redis_conn, :thread, :channel
12
14
 
13
- def initialize(hub:, coder:, **options)
14
- @hub = hub
15
- @coder = coder
15
+ def initialize(hub:, coder:, channel:, **options)
16
+ super
16
17
  @redis_conn = ::Redis.new(options)
17
- @threads = {}
18
+ @channel = channel
18
19
  end
19
20
 
20
- def subscribe(channel)
21
- @threads[channel] = Thread.new do
22
- redis_conn.subscribe(channel) do |on|
23
- on.message { |_channel, msg| handle_message(msg) }
24
- end
25
- end
21
+ def start
22
+ subscribe(channel)
23
+
24
+ log(:info) { "Subscribed to #{channel}" }
26
25
  end
27
26
 
28
- def unsubscribe(channel)
29
- @threads[channel]&.terminate
30
- @threads.delete(channel)
27
+ def stop
28
+ thread&.terminate
31
29
  end
32
30
 
33
- private
31
+ def subscribe(channel)
32
+ @thread ||= Thread.new do
33
+ Thread.current.abort_on_exception = true
34
+
35
+ redis_conn.without_reconnect do
36
+ redis_conn.subscribe(channel) do |on|
37
+ on.subscribe do |chan, count|
38
+ log(:debug) { "Redis subscriber connected to #{chan} (#{count})" }
39
+ end
40
+
41
+ on.unsubscribe do |chan, count|
42
+ log(:debug) { "Redis subscribed disconnected from #{chan} (#{count})" }
43
+ end
34
44
 
35
- def handle_message(msg)
36
- data = JSON.parse(msg)
37
- hub.broadcast(data["stream"], data["data"], coder)
45
+ on.message do |_channel, msg|
46
+ handle_message(msg)
47
+ rescue
48
+ log(:error) { "Failed to broadcast message: #{msg}" }
49
+ end
50
+ end
51
+ end
52
+ end
38
53
  end
39
54
  end
40
55
  end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "anyway_config"
4
+
5
+ module AnyCable
6
+ module Rack
7
+ class Config < Anyway::Config
8
+ DEFAULT_HEADERS = %w[cookie x-api-token].freeze
9
+
10
+ config_name :anycable
11
+ env_prefix "ANYCABLE"
12
+
13
+ attr_config mount_path: "/cable",
14
+ headers: DEFAULT_HEADERS,
15
+ rpc_addr: "localhost:50051",
16
+ rpc_client_pool_size: 5,
17
+ rpc_client_timeout: 5,
18
+ http_broadcast_path: "/_anycable_rack_broadcast",
19
+ run_rpc: false
20
+ end
21
+ end
22
+ end
@@ -14,23 +14,24 @@ module AnyCable
14
14
  include Logging
15
15
 
16
16
  attr_reader :coder,
17
- :headers,
18
- :hub,
19
- :socket,
20
- :rpc_client,
21
- :sid
17
+ :headers,
18
+ :hub,
19
+ :socket,
20
+ :rpc_client,
21
+ :sid
22
22
 
23
- def initialize(socket, hub:, coder:, rpc_host:, headers:)
23
+ def initialize(socket, hub:, coder:, rpc_client:, headers:)
24
24
  @socket = socket
25
25
  @coder = coder
26
26
  @headers = headers
27
27
  @hub = hub
28
28
  @sid = SecureRandom.hex(6)
29
29
 
30
- @rpc_client = RPC::Client.new(rpc_host)
30
+ @rpc_client = rpc_client
31
31
 
32
- @_identifiers = "{}"
32
+ @_identifiers = "{}"
33
33
  @_subscriptions = Set.new
34
+ @_istate = {}
34
35
  end
35
36
 
36
37
  def handle_open
@@ -53,13 +54,13 @@ module AnyCable
53
54
  log(:debug) { "Command: #{decoded}" }
54
55
 
55
56
  case command
56
- when "subscribe" then subscribe(channel_identifier)
57
+ when "subscribe" then subscribe(channel_identifier)
57
58
  when "unsubscribe" then unsubscribe(channel_identifier)
58
- when "message" then send_message(channel_identifier, decoded["data"])
59
+ when "message" then send_message(channel_identifier, decoded["data"])
59
60
  else
60
61
  log(:error, "Command not found #{command}")
61
62
  end
62
- rescue Exception => e
63
+ rescue Exception => e # rubocop:disable Lint/RescueException
63
64
  log(:error, "Failed to execute command #{command}: #{e.message}")
64
65
  end
65
66
 
@@ -77,12 +78,8 @@ module AnyCable
77
78
  socket.request
78
79
  end
79
80
 
80
- def request_path
81
- request.fullpath
82
- end
83
-
84
81
  def rpc_connect
85
- rpc_client.connect(headers: headers, path: request_path)
82
+ rpc_client.connect(headers: headers, url: request.url)
86
83
  end
87
84
 
88
85
  def rpc_disconnect
@@ -90,7 +87,8 @@ module AnyCable
90
87
  identifiers: @_identifiers,
91
88
  subscriptions: @_subscriptions.to_a,
92
89
  headers: headers,
93
- path: request_path
90
+ url: request.url,
91
+ state: @_cstate
94
92
  )
95
93
  end
96
94
 
@@ -99,7 +97,11 @@ module AnyCable
99
97
  command: command,
100
98
  identifier: identifier,
101
99
  connection_identifiers: @_identifiers,
102
- data: data
100
+ data: data,
101
+ headers: headers,
102
+ url: request.url,
103
+ connection_state: @_cstate,
104
+ state: @_istate[identifier]
103
105
  )
104
106
  end
105
107
 
@@ -133,13 +135,20 @@ module AnyCable
133
135
  response.transmissions.each { |transmission| transmit(decode(transmission)) }
134
136
  hub.remove_channel(socket, identifier) if response.stop_streams
135
137
  response.streams.each { |stream| hub.add_subscriber(stream, socket, identifier) }
138
+ response.stopped_streams.each { |stream| hub.remove_subscriber(stream, socket, identifier) }
139
+
140
+ @_istate[identifier] ||= {}
141
+ @_istate[identifier].merge!(response.env.istate&.to_h || {})
142
+
136
143
  close_connection if response.disconnect
137
144
  end
138
145
 
139
146
  def process_open(response)
147
+ response.transmissions&.each { |transmission| transmit(decode(transmission)) }
140
148
  if response.status == :SUCCESS
141
149
  @_identifiers = response.identifiers
142
- response.transmissions.each { |transmission| transmit(decode(transmission)) }
150
+ @_cstate = response.env.cstate&.to_h || {}
151
+ hub.add_socket(socket, @_identifiers)
143
152
  log(:debug) { "Opened" }
144
153
  else
145
154
  log(:error, "RPC connection command failed: #{response.inspect}")
@@ -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
@@ -69,6 +77,25 @@ module AnyCable
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)
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
+
72
99
  def close_all
73
100
  hub.sockets.dup.each do |socket|
74
101
  hub.remove_socket(socket)
@@ -87,6 +114,11 @@ 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
+ # FIXME: coder support?
119
+ def disconnect_message(reason, reconnect)
120
+ {type: :disconnect, reason: reason, reconnect: reconnect}.to_json
121
+ end
90
122
  end
91
123
  end
92
124
  end
@@ -11,16 +11,16 @@ module AnyCable
11
11
  class Middleware # :nodoc:
12
12
  PROTOCOLS = ["actioncable-v1-json", "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
@@ -45,7 +45,7 @@ module AnyCable
45
45
  private
46
46
 
47
47
  def ping_message(time)
48
- { type: :ping, message: time }.to_json
48
+ {type: :ping, message: time}.to_json
49
49
  end
50
50
  end
51
51
  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,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "connection_pool"
3
4
  require "grpc"
4
5
 
5
6
  module AnyCable
@@ -7,35 +8,53 @@ module AnyCable
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::RPC::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)
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
+ )
37
54
  )
38
- stub.disconnect(request)
55
+ pool.with do |stub|
56
+ stub.disconnect(request, metadata)
57
+ end
39
58
  end
40
59
  end
41
60
  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,8 +7,7 @@ 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"
10
+ require "anycable/rack/broadcast_subscribers/base_subscriber"
12
11
  require "anycable/rack/coders/json"
13
12
 
14
13
  module AnyCable # :nodoc: all
@@ -16,43 +15,39 @@ module AnyCable # :nodoc: all
16
15
  class Server
17
16
  include Logging
18
17
 
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
-
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
33
30
  @hub = Hub.new
34
31
  @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
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
45
40
  )
46
41
 
47
42
  @middleware = Middleware.new(
48
- header_names: headers,
43
+ header_names: config.headers,
49
44
  pinger: pinger,
50
45
  hub: hub,
51
- rpc_host: rpc_host,
46
+ rpc_client: rpc_client,
52
47
  coder: coder
53
48
  )
54
49
 
55
- log(:info) { "Using RPC server at #{rpc_host}" }
50
+ log(:info) { "Connecting to RPC server at #{config.rpc_addr}" }
56
51
  end
57
52
  # rubocop:enable
58
53
 
@@ -61,13 +56,19 @@ module AnyCable # :nodoc: all
61
56
 
62
57
  pinger.run
63
58
 
64
- broadcast.subscribe(AnyCable.config.redis_channel)
59
+ broadcast.start
65
60
 
66
- log(:info) { "Subscribed to #{AnyCable.config.redis_channel}" }
61
+ Rack.rpc_server.run if config.run_rpc
67
62
 
68
63
  @_started = true
69
64
  end
70
65
 
66
+ def shutdown
67
+ log(:info) { "Shutting down..." }
68
+ Rack.rpc_server&.shutdown
69
+ hub.broadcast_all(coder.encode(type: "disconnect", reason: "server_restart", reconnect: true))
70
+ end
71
+
71
72
  def started?
72
73
  @_started == true
73
74
  end
@@ -76,7 +77,7 @@ module AnyCable # :nodoc: all
76
77
  return unless started?
77
78
 
78
79
  @_started = false
79
- broadcast_subscriber.unsubscribe(@_redis_channel)
80
+ broadcast_subscriber.stop
80
81
  pinger.stop
81
82
  hub.close_all
82
83
  end
@@ -86,7 +87,32 @@ module AnyCable # :nodoc: all
86
87
  end
87
88
 
88
89
  def inspect
89
- "#<AnyCable::Rack::Server(rpc_host: #{rpc_host}, headers: [#{headers.join(', ')}])>"
90
+ "#<AnyCable::Rack::Server(rpc_addr: #{config.rpc_addr}, headers: [#{config.headers.join(", ")}])>"
91
+ end
92
+
93
+ private
94
+
95
+ def resolve_broadcast_adapter
96
+ adapter = AnyCable.config.broadcast_adapter.to_s
97
+ require "anycable/rack/broadcast_subscribers/#{adapter}_subscriber"
98
+
99
+ if adapter.to_s == "redis"
100
+ BroadcastSubscribers::RedisSubscriber.new(
101
+ hub: hub,
102
+ coder: coder,
103
+ channel: AnyCable.config.redis_channel,
104
+ **AnyCable.config.to_redis_params
105
+ )
106
+ elsif adapter.to_s == "http"
107
+ BroadcastSubscribers::HTTPSubscriber.new(
108
+ hub: hub,
109
+ coder: coder,
110
+ token: AnyCable.config.http_broadcast_secret,
111
+ path: config.http_broadcast_path
112
+ )
113
+ else
114
+ raise ArgumentError, "Unsupported broadcast adatper: #{adapter}. AnyCable Rack server only supports: redis, http"
115
+ end
90
116
  end
91
117
  end
92
118
  end
@@ -15,11 +15,11 @@ module AnyCable
15
15
  @socket = socket
16
16
  @version = version
17
17
 
18
- @_open_handlers = []
18
+ @_open_handlers = []
19
19
  @_message_handlers = []
20
- @_close_handlers = []
21
- @_error_handlers = []
22
- @_active = true
20
+ @_close_handlers = []
21
+ @_error_handlers = []
22
+ @_active = true
23
23
  end
24
24
 
25
25
  def transmit(data, type: :text)
@@ -29,7 +29,7 @@ module AnyCable
29
29
  type: type
30
30
  )
31
31
  socket.write(frame.to_s)
32
- rescue IOError, Errno::EPIPE, Errno::ETIMEDOUT => e
32
+ rescue Exception => e # rubocop:disable Lint/RescueException
33
33
  log(:error, "Socket send failed: #{e}")
34
34
  close
35
35
  end
@@ -62,13 +62,11 @@ module AnyCable
62
62
  @_open_handlers.each(&:call)
63
63
  each_frame do |data|
64
64
  @_message_handlers.each do |handler|
65
- begin
66
- handler.call(data)
67
- rescue => e # rubocop: disable Style/RescueStandardError
68
- log(:error, "Socket receive failed: #{e}")
69
- @_error_handlers.each { |eh| eh.call(e, data) }
70
- close
71
- end
65
+ handler.call(data)
66
+ rescue => e # rubocop: disable Style/RescueStandardError
67
+ log(:error, "Socket receive failed: #{e}")
68
+ @_error_handlers.each { |eh| eh.call(e, data) }
69
+ close
72
70
  end
73
71
  end
74
72
  ensure
@@ -104,7 +102,7 @@ module AnyCable
104
102
  frame = WebSocket::Frame::Outgoing::Server.new(version: version, type: :close, code: 1000)
105
103
  socket.write(frame.to_s) if frame.supported?
106
104
  socket.close
107
- rescue IOError, Errno::EPIPE, Errno::ETIMEDOUT
105
+ rescue Exception # rubocop:disable Lint/RescueException
108
106
  # already closed
109
107
  end
110
108
 
@@ -113,8 +111,7 @@ module AnyCable
113
111
  Thread.current.abort_on_exception = true
114
112
  loop do
115
113
  sleep 5
116
- time = Time.now.to_i
117
- transmit({ message: time, type: :ping }.to_json)
114
+ transmit nil, type: :ping
118
115
  end
119
116
  end
120
117
 
@@ -137,7 +134,7 @@ module AnyCable
137
134
 
138
135
  framebuffer << data
139
136
 
140
- while frame = framebuffer.next
137
+ while frame = framebuffer.next # rubocop:disable Lint/AssignmentInCondition
141
138
  case frame.type
142
139
  when :close
143
140
  return
@@ -146,7 +143,7 @@ module AnyCable
146
143
  end
147
144
  end
148
145
  end
149
- rescue Errno::EHOSTUNREACH, Errno::ETIMEDOUT, Errno::ECONNRESET, IOError, Errno::EBADF => e
146
+ rescue Exception => e # rubocop:disable Lint/RescueException
150
147
  log(:error, "Socket frame error: #{e}")
151
148
  nil # client disconnected or timed out
152
149
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module AnyCable
4
4
  module Rack
5
- VERSION = "0.1.0"
5
+ VERSION = "0.2.0.rc1"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,71 +1,86 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: anycable-rack-server
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0.rc1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yulia Oletskaya
8
+ - Vladimir Dementyev
8
9
  autorequire:
9
10
  bindir: bin
10
11
  cert_chain: []
11
- date: 2019-01-14 00:00:00.000000000 Z
12
+ date: 2020-06-25 00:00:00.000000000 Z
12
13
  dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: anyway_config
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ">="
19
+ - !ruby/object:Gem::Version
20
+ version: 1.4.2
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ version: 1.4.2
13
28
  - !ruby/object:Gem::Dependency
14
29
  name: anycable
15
30
  requirement: !ruby/object:Gem::Requirement
16
31
  requirements:
17
- - - "~>"
32
+ - - ">="
18
33
  - !ruby/object:Gem::Version
19
- version: '0.6'
34
+ version: '0'
20
35
  type: :runtime
21
36
  prerelease: false
22
37
  version_requirements: !ruby/object:Gem::Requirement
23
38
  requirements:
24
- - - "~>"
39
+ - - ">="
25
40
  - !ruby/object:Gem::Version
26
- version: '0.6'
41
+ version: '0'
27
42
  - !ruby/object:Gem::Dependency
28
- name: websocket
43
+ name: connection_pool
29
44
  requirement: !ruby/object:Gem::Requirement
30
45
  requirements:
31
46
  - - "~>"
32
47
  - !ruby/object:Gem::Version
33
- version: '1.2'
48
+ version: '2.2'
34
49
  type: :runtime
35
50
  prerelease: false
36
51
  version_requirements: !ruby/object:Gem::Requirement
37
52
  requirements:
38
53
  - - "~>"
39
54
  - !ruby/object:Gem::Version
40
- version: '1.2'
55
+ version: '2.2'
41
56
  - !ruby/object:Gem::Dependency
42
- name: redis
57
+ name: websocket
43
58
  requirement: !ruby/object:Gem::Requirement
44
59
  requirements:
45
60
  - - "~>"
46
61
  - !ruby/object:Gem::Version
47
- version: '4'
62
+ version: '1.2'
48
63
  type: :runtime
49
64
  prerelease: false
50
65
  version_requirements: !ruby/object:Gem::Requirement
51
66
  requirements:
52
67
  - - "~>"
53
68
  - !ruby/object:Gem::Version
54
- version: '4'
69
+ version: '1.2'
55
70
  - !ruby/object:Gem::Dependency
56
71
  name: anyt
57
72
  requirement: !ruby/object:Gem::Requirement
58
73
  requirements:
59
- - - "~>"
74
+ - - ">="
60
75
  - !ruby/object:Gem::Version
61
- version: 0.8.4
76
+ version: '0'
62
77
  type: :development
63
78
  prerelease: false
64
79
  version_requirements: !ruby/object:Gem::Requirement
65
80
  requirements:
66
- - - "~>"
81
+ - - ">="
67
82
  - !ruby/object:Gem::Version
68
- version: 0.8.4
83
+ version: '0'
69
84
  - !ruby/object:Gem::Dependency
70
85
  name: minitest
71
86
  requirement: !ruby/object:Gem::Requirement
@@ -96,32 +111,46 @@ dependencies:
96
111
  version: '0'
97
112
  - !ruby/object:Gem::Dependency
98
113
  name: rake
114
+ requirement: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - ">="
117
+ - !ruby/object:Gem::Version
118
+ version: '13.0'
119
+ type: :development
120
+ prerelease: false
121
+ version_requirements: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - ">="
124
+ - !ruby/object:Gem::Version
125
+ version: '13.0'
126
+ - !ruby/object:Gem::Dependency
127
+ name: redis
99
128
  requirement: !ruby/object:Gem::Requirement
100
129
  requirements:
101
130
  - - "~>"
102
131
  - !ruby/object:Gem::Version
103
- version: '12.3'
132
+ version: '4'
104
133
  type: :development
105
134
  prerelease: false
106
135
  version_requirements: !ruby/object:Gem::Requirement
107
136
  requirements:
108
137
  - - "~>"
109
138
  - !ruby/object:Gem::Version
110
- version: '12.3'
139
+ version: '4'
111
140
  - !ruby/object:Gem::Dependency
112
141
  name: rubocop
113
142
  requirement: !ruby/object:Gem::Requirement
114
143
  requirements:
115
- - - "~>"
144
+ - - ">="
116
145
  - !ruby/object:Gem::Version
117
- version: 0.60.0
146
+ version: '0.80'
118
147
  type: :development
119
148
  prerelease: false
120
149
  version_requirements: !ruby/object:Gem::Requirement
121
150
  requirements:
122
- - - "~>"
151
+ - - ">="
123
152
  - !ruby/object:Gem::Version
124
- version: 0.60.0
153
+ version: '0.80'
125
154
  description: AnyCable-compatible Ruby Rack middleware
126
155
  email: yulia.oletskaya@gmail.com
127
156
  executables: []
@@ -131,8 +160,11 @@ files:
131
160
  - LICENSE
132
161
  - README.md
133
162
  - lib/anycable-rack-server.rb
163
+ - lib/anycable/rack/broadcast_subscribers/base_subscriber.rb
164
+ - lib/anycable/rack/broadcast_subscribers/http_subscriber.rb
134
165
  - lib/anycable/rack/broadcast_subscribers/redis_subscriber.rb
135
166
  - lib/anycable/rack/coders/json.rb
167
+ - lib/anycable/rack/config.rb
136
168
  - lib/anycable/rack/connection.rb
137
169
  - lib/anycable/rack/errors.rb
138
170
  - lib/anycable/rack/hub.rb
@@ -142,7 +174,6 @@ files:
142
174
  - lib/anycable/rack/railtie.rb
143
175
  - lib/anycable/rack/rpc/client.rb
144
176
  - lib/anycable/rack/rpc/rpc.proto
145
- - lib/anycable/rack/rpc_runner.rb
146
177
  - lib/anycable/rack/server.rb
147
178
  - lib/anycable/rack/socket.rb
148
179
  - lib/anycable/rack/version.rb
@@ -161,13 +192,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
161
192
  version: '0'
162
193
  required_rubygems_version: !ruby/object:Gem::Requirement
163
194
  requirements:
164
- - - ">="
195
+ - - ">"
165
196
  - !ruby/object:Gem::Version
166
- version: '0'
197
+ version: 1.3.1
167
198
  requirements: []
168
- rubyforge_project:
169
- rubygems_version: 2.7.6
199
+ rubygems_version: 3.0.6
170
200
  signing_key:
171
201
  specification_version: 4
172
- summary: Anycable Rack Server
202
+ summary: AnyCable Rack Server
173
203
  test_files: []
@@ -1,60 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "anycable"
4
- require "anycable/rack/logging"
5
-
6
- $stdout.sync = true
7
-
8
- module AnyCable
9
- module Rack
10
- # Runs AnyCable CLI in a separate process
11
- module RPCRunner
12
- class << self
13
- include Logging
14
-
15
- attr_accessor :running, :pid
16
-
17
- def run(root_dir:, command_args: [], rpc_host: "[::]:50051", env: {})
18
- return if @running
19
-
20
- command_args << "--rpc-host=\"#{rpc_host}\""
21
-
22
- command = "bundle exec anycable #{command_args.join(' ')}"
23
-
24
- log(:info, "Running AnyCable (from #{root_dir}): #{command}")
25
-
26
- out = AnyCable.config.debug? ? STDOUT : IO::NULL
27
-
28
- @pid = Dir.chdir(root_dir) do
29
- Process.spawn(
30
- env,
31
- command,
32
- out: out,
33
- err: out
34
- )
35
- end
36
-
37
- log(:debug) { "AnyCable PID: #{pid}" }
38
-
39
- @running = true
40
-
41
- at_exit { stop }
42
- end
43
-
44
- def stop
45
- return unless running
46
-
47
- log(:debug) { "Terminate PID: #{pid}" }
48
-
49
- Process.kill("SIGKILL", pid)
50
-
51
- @running = false
52
- end
53
-
54
- def running?
55
- running == true
56
- end
57
- end
58
- end
59
- end
60
- end