anycable 0.6.0.rc1 → 1.0.0.preview1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +97 -5
  3. data/MIT-LICENSE +1 -1
  4. data/README.md +13 -6
  5. data/bin/anycable +1 -1
  6. data/bin/anycabled +30 -0
  7. data/lib/anycable.rb +16 -11
  8. data/lib/anycable/broadcast_adapters.rb +3 -3
  9. data/lib/anycable/broadcast_adapters/redis.rb +2 -2
  10. data/lib/anycable/cli.rb +20 -4
  11. data/lib/anycable/config.rb +10 -5
  12. data/lib/anycable/exceptions_handling.rb +13 -9
  13. data/lib/anycable/health_server.rb +2 -3
  14. data/lib/anycable/middleware.rb +3 -0
  15. data/lib/anycable/middleware_chain.rb +2 -2
  16. data/lib/anycable/middlewares/check_version.rb +24 -0
  17. data/lib/anycable/rpc.rb +76 -0
  18. data/lib/anycable/rpc/rpc_pb.rb +54 -39
  19. data/lib/anycable/rpc/rpc_services_pb.rb +4 -3
  20. data/lib/anycable/rpc_handler.rb +77 -24
  21. data/lib/anycable/rspec.rb +6 -0
  22. data/lib/anycable/rspec/rpc_command_context.rb +20 -0
  23. data/lib/anycable/rspec/rpc_stub_context.rb +13 -0
  24. data/lib/anycable/rspec/with_grpc_server.rb +15 -0
  25. data/lib/anycable/server.rb +4 -46
  26. data/lib/anycable/socket.rb +39 -2
  27. data/lib/anycable/version.rb +1 -1
  28. metadata +32 -70
  29. data/.github/ISSUE_TEMPLATE.md +0 -25
  30. data/.github/PULL_REQUEST_TEMPLATE.md +0 -31
  31. data/.gitignore +0 -40
  32. data/.hound.yml +0 -3
  33. data/.rubocop.yml +0 -71
  34. data/.travis.yml +0 -12
  35. data/Gemfile +0 -8
  36. data/Makefile +0 -5
  37. data/PITCHME.md +0 -139
  38. data/PITCHME.yaml +0 -1
  39. data/Rakefile +0 -8
  40. data/anycable.gemspec +0 -35
  41. data/assets/Memory3.png +0 -0
  42. data/assets/Memory5.png +0 -0
  43. data/assets/RTT3.png +0 -0
  44. data/assets/RTT5.png +0 -0
  45. data/assets/Scheme1.png +0 -0
  46. data/assets/Scheme2.png +0 -0
  47. data/assets/cpu_chart.gif +0 -0
  48. data/assets/cpu_chart2.gif +0 -0
  49. data/assets/evlms.png +0 -0
  50. data/benchmarks/.gitignore +0 -2
  51. data/benchmarks/2017-02-12.md +0 -308
  52. data/benchmarks/2018-03-04.md +0 -192
  53. data/benchmarks/2018-05-27-rpc-bench.md +0 -57
  54. data/benchmarks/2018-10-27.md +0 -181
  55. data/benchmarks/HowTo.md +0 -23
  56. data/benchmarks/ansible.cfg +0 -9
  57. data/benchmarks/assets/2018-10-27-action-cable-rss.png +0 -0
  58. data/benchmarks/assets/2018-10-27-action-cable-rtt.png +0 -0
  59. data/benchmarks/assets/2018-10-27-anycable-rss.png +0 -0
  60. data/benchmarks/assets/2018-10-27-anycable-rtt.png +0 -0
  61. data/benchmarks/assets/2018-10-27-async-rss.png +0 -0
  62. data/benchmarks/assets/2018-10-27-async-rtt.png +0 -0
  63. data/benchmarks/assets/2018-10-27-falcon-cable-rss.png +0 -0
  64. data/benchmarks/assets/2018-10-27-falcon-cable-rtt.png +0 -0
  65. data/benchmarks/assets/2018-10-27-iodine-cable-rss.png +0 -0
  66. data/benchmarks/assets/2018-10-27-iodine-cable-rtt.png +0 -0
  67. data/benchmarks/assets/2018-10-27-plezi-rss.png +0 -0
  68. data/benchmarks/assets/2018-10-27-plezi-rtt.png +0 -0
  69. data/benchmarks/bench.png +0 -0
  70. data/benchmarks/benchmark.yml +0 -69
  71. data/benchmarks/hosts +0 -5
  72. data/benchmarks/rtt_plot.py +0 -74
  73. data/benchmarks/rtt_plot_test.py +0 -16
  74. data/benchmarks/servers.yml +0 -58
  75. data/circle.yml +0 -8
  76. data/etc/bug_report_template.rb +0 -76
  77. data/lib/anycable/handler/capture_exceptions.rb +0 -39
  78. data/protos/rpc.proto +0 -55
@@ -4,25 +4,29 @@ module AnyCable
4
4
  module ExceptionsHandling # :nodoc:
5
5
  class << self
6
6
  def add_handler(block)
7
- handlers << block
7
+ handlers << procify(block)
8
8
  end
9
9
 
10
10
  alias << add_handler
11
11
 
12
- def notify(exp)
12
+ def notify(exp, method_name, message)
13
13
  handlers.each do |handler|
14
- begin
15
- handler.call(exp)
16
- rescue StandardError => exp
17
- AnyCable.logger.error "!!! EXCEPTION HANDLER THREW AN ERROR !!!"
18
- AnyCable.logger.error exp
19
- AnyCable.logger.error exp.backtrace.join("\n") unless exp.backtrace.nil?
20
- end
14
+ handler.call(exp, method_name, message)
15
+ rescue => exp
16
+ AnyCable.logger.error "!!! EXCEPTION HANDLER THREW AN ERROR !!!"
17
+ AnyCable.logger.error exp
18
+ AnyCable.logger.error exp.backtrace.join("\n") unless exp.backtrace.nil?
21
19
  end
22
20
  end
23
21
 
24
22
  private
25
23
 
24
+ def procify(block)
25
+ return block unless block.lambda?
26
+
27
+ proc { |*args| block.call(*args.take(block.arity)) }
28
+ end
29
+
26
30
  def handlers
27
31
  @handlers ||= []
28
32
  end
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "webrick"
4
- require "anycable/server"
5
-
6
3
  module AnyCable
7
4
  # Server for HTTP healthchecks.
8
5
  #
@@ -54,6 +51,8 @@ module AnyCable
54
51
  attr_reader :logger
55
52
 
56
53
  def build_server
54
+ require "webrick"
55
+
57
56
  WEBrick::HTTPServer.new(
58
57
  Port: port,
59
58
  Logger: logger,
@@ -7,6 +7,9 @@ module AnyCable
7
7
  # for request/response calls
8
8
  class Middleware < GRPC::Interceptor
9
9
  def request_response(request: nil, call: nil, method: nil)
10
+ # Call middlewares only for AnyCable service
11
+ return yield unless method.receiver.is_a?(AnyCable::RPCHandler)
12
+
10
13
  call(request, call, method) do
11
14
  yield
12
15
  end
@@ -48,8 +48,8 @@ module AnyCable
48
48
 
49
49
  unless middleware.is_a?(AnyCable::Middleware)
50
50
  raise ArgumentError,
51
- "AnyCable middleware must be a subclass of AnyCable::Middleware, " \
52
- "got #{middleware} instead"
51
+ "AnyCable middleware must be a subclass of AnyCable::Middleware, " \
52
+ "got #{middleware} instead"
53
53
  end
54
54
 
55
55
  middleware
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnyCable
4
+ module Middlewares
5
+ # Checks that RPC client version is compatibly with
6
+ # the current RPC proto version
7
+ class CheckVersion < Middleware
8
+ attr_reader :version
9
+
10
+ def initialize(version)
11
+ @version = version
12
+ end
13
+
14
+ def call(_request, call, _method)
15
+ supported_versions = call.metadata["protov"]&.split(",")
16
+ return yield if supported_versions&.include?(version)
17
+
18
+ raise GRPC::Internal,
19
+ "Incompatible AnyCable RPC client.\nCurrent server version: #{version}.\n" \
20
+ "Client supported versions: #{call.metadata["protov"] || "unknown"}."
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "anycable/rpc/rpc_pb"
4
+ require "anycable/rpc/rpc_services_pb"
5
+
6
+ # Extend some PB auto-generated classes
7
+ module AnyCable
8
+ # Current RPC proto version (used for compatibility checks)
9
+ PROTO_VERSION = "v1"
10
+ SESSION_KEY = "_s_"
11
+
12
+ # Add setters/getter for cstate field
13
+ module WithConnectionState
14
+ def initialize(session: nil, **other)
15
+ if session
16
+ other[:cstate] ||= {}
17
+ other[:cstate][SESSION_KEY] = session
18
+ end
19
+ super(**other)
20
+ end
21
+
22
+ def session=(val)
23
+ self.cstate = {} unless cstate
24
+ cstate[SESSION_KEY] = val
25
+ end
26
+
27
+ def session
28
+ cstate[SESSION_KEY]
29
+ end
30
+
31
+ def cstate
32
+ env.cstate
33
+ end
34
+
35
+ def cstate=(val)
36
+ env.cstate = val
37
+ end
38
+ end
39
+
40
+ # Status predicates
41
+ module StatusPredicates
42
+ def success?
43
+ status == :SUCCESS
44
+ end
45
+
46
+ def failure?
47
+ status == :FAILURE
48
+ end
49
+
50
+ def error?
51
+ status == :ERROR
52
+ end
53
+ end
54
+
55
+ class ConnectionResponse
56
+ prepend WithConnectionState
57
+ include StatusPredicates
58
+ end
59
+
60
+ class CommandMessage
61
+ prepend WithConnectionState
62
+ end
63
+
64
+ class CommandResponse
65
+ prepend WithConnectionState
66
+ include StatusPredicates
67
+ end
68
+
69
+ class DisconnectRequest
70
+ prepend WithConnectionState
71
+ end
72
+
73
+ class DisconnectResponse
74
+ include StatusPredicates
75
+ end
76
+ end
@@ -1,51 +1,66 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Generated by the protocol buffer compiler. DO NOT EDIT!
2
4
  # source: rpc.proto
3
5
 
4
- require 'google/protobuf'
6
+ require "google/protobuf"
5
7
 
6
8
  Google::Protobuf::DescriptorPool.generated_pool.build do
7
- add_message "anycable.ConnectionRequest" do
8
- optional :path, :string, 1
9
- map :headers, :string, :string, 2
10
- end
11
- add_message "anycable.ConnectionResponse" do
12
- optional :status, :enum, 1, "anycable.Status"
13
- optional :identifiers, :string, 2
14
- repeated :transmissions, :string, 3
15
- optional :error_msg, :string, 4
16
- end
17
- add_message "anycable.CommandMessage" do
18
- optional :command, :string, 1
19
- optional :identifier, :string, 2
20
- optional :connection_identifiers, :string, 3
21
- optional :data, :string, 4
22
- end
23
- add_message "anycable.CommandResponse" do
24
- optional :status, :enum, 1, "anycable.Status"
25
- optional :disconnect, :bool, 2
26
- optional :stop_streams, :bool, 3
27
- repeated :streams, :string, 4
28
- repeated :transmissions, :string, 5
29
- optional :error_msg, :string, 6
30
- end
31
- add_message "anycable.DisconnectRequest" do
32
- optional :identifiers, :string, 1
33
- repeated :subscriptions, :string, 2
34
- optional :path, :string, 3
35
- map :headers, :string, :string, 4
36
- end
37
- add_message "anycable.DisconnectResponse" do
38
- optional :status, :enum, 1, "anycable.Status"
39
- optional :error_msg, :string, 2
40
- end
41
- add_enum "anycable.Status" do
42
- value :ERROR, 0
43
- value :SUCCESS, 1
44
- value :FAILURE, 2
9
+ add_file("rpc.proto", syntax: :proto3) do
10
+ add_message "anycable.Env" do
11
+ optional :url, :string, 1
12
+ map :headers, :string, :string, 2
13
+ map :cstate, :string, :string, 3
14
+ end
15
+ add_message "anycable.EnvResponse" do
16
+ map :cstate, :string, :string, 1
17
+ end
18
+ add_message "anycable.ConnectionRequest" do
19
+ optional :env, :message, 3, "anycable.Env"
20
+ end
21
+ add_message "anycable.ConnectionResponse" do
22
+ optional :status, :enum, 1, "anycable.Status"
23
+ optional :identifiers, :string, 2
24
+ repeated :transmissions, :string, 3
25
+ optional :error_msg, :string, 4
26
+ optional :env, :message, 5, "anycable.EnvResponse"
27
+ end
28
+ add_message "anycable.CommandMessage" do
29
+ optional :command, :string, 1
30
+ optional :identifier, :string, 2
31
+ optional :connection_identifiers, :string, 3
32
+ optional :data, :string, 4
33
+ optional :env, :message, 5, "anycable.Env"
34
+ end
35
+ add_message "anycable.CommandResponse" do
36
+ optional :status, :enum, 1, "anycable.Status"
37
+ optional :disconnect, :bool, 2
38
+ optional :stop_streams, :bool, 3
39
+ repeated :streams, :string, 4
40
+ repeated :transmissions, :string, 5
41
+ optional :error_msg, :string, 6
42
+ optional :env, :message, 7, "anycable.EnvResponse"
43
+ end
44
+ add_message "anycable.DisconnectRequest" do
45
+ optional :identifiers, :string, 1
46
+ repeated :subscriptions, :string, 2
47
+ optional :env, :message, 5, "anycable.Env"
48
+ end
49
+ add_message "anycable.DisconnectResponse" do
50
+ optional :status, :enum, 1, "anycable.Status"
51
+ optional :error_msg, :string, 2
52
+ end
53
+ add_enum "anycable.Status" do
54
+ value :ERROR, 0
55
+ value :SUCCESS, 1
56
+ value :FAILURE, 2
57
+ end
45
58
  end
46
59
  end
47
60
 
48
61
  module AnyCable
62
+ Env = Google::Protobuf::DescriptorPool.generated_pool.lookup("anycable.Env").msgclass
63
+ EnvResponse = Google::Protobuf::DescriptorPool.generated_pool.lookup("anycable.EnvResponse").msgclass
49
64
  ConnectionRequest = Google::Protobuf::DescriptorPool.generated_pool.lookup("anycable.ConnectionRequest").msgclass
50
65
  ConnectionResponse = Google::Protobuf::DescriptorPool.generated_pool.lookup("anycable.ConnectionResponse").msgclass
51
66
  CommandMessage = Google::Protobuf::DescriptorPool.generated_pool.lookup("anycable.CommandMessage").msgclass
@@ -1,17 +1,18 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Generated by the protocol buffer compiler. DO NOT EDIT!
2
4
  # Source: rpc.proto for package 'anycable'
3
5
 
4
- require 'grpc'
6
+ require "grpc"
5
7
 
6
8
  module AnyCable
7
9
  module RPC
8
10
  class Service
9
-
10
11
  include GRPC::GenericService
11
12
 
12
13
  self.marshal_class_method = :encode
13
14
  self.unmarshal_class_method = :decode
14
- self.service_name = 'anycable.RPC'
15
+ self.service_name = "anycable.RPC"
15
16
 
16
17
  rpc :Connect, ConnectionRequest, ConnectionResponse
17
18
  rpc :Command, CommandMessage, CommandResponse
@@ -1,43 +1,50 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "anycable/socket"
4
- require "anycable/rpc/rpc_pb"
5
- require "anycable/rpc/rpc_services_pb"
6
-
7
- require "anycable/handler/capture_exceptions"
4
+ require "anycable/rpc"
8
5
 
9
6
  # rubocop:disable Metrics/AbcSize
10
7
  # rubocop:disable Metrics/MethodLength
8
+ # rubocop:disable Metrics/ClassLength
11
9
  module AnyCable
12
10
  # RPC service handler
13
11
  class RPCHandler < AnyCable::RPC::Service
14
- prepend AnyCable::Handler::CaptureExceptions
15
-
16
12
  # Handle connection request from WebSocket server
17
13
  def connect(request, _unused_call)
18
14
  logger.debug("RPC Connect: #{request.inspect}")
19
15
 
20
- socket = build_socket(env: rack_env(request))
16
+ socket = build_socket(env: rack_env(request.env))
21
17
 
22
18
  connection = factory.call(socket)
23
19
 
24
20
  connection.handle_open
25
21
 
26
22
  if socket.closed?
27
- AnyCable::ConnectionResponse.new(status: AnyCable::Status::FAILURE)
23
+ AnyCable::ConnectionResponse.new(
24
+ status: AnyCable::Status::FAILURE,
25
+ transmissions: socket.transmissions
26
+ )
28
27
  else
29
28
  AnyCable::ConnectionResponse.new(
30
29
  status: AnyCable::Status::SUCCESS,
31
30
  identifiers: connection.identifiers_json,
32
- transmissions: socket.transmissions
31
+ transmissions: socket.transmissions,
32
+ env: build_env_response(socket)
33
33
  )
34
34
  end
35
+ rescue => exp
36
+ notify_exception(exp, :connect, request)
37
+
38
+ AnyCable::ConnectionResponse.new(
39
+ status: AnyCable::Status::ERROR,
40
+ error_msg: exp.message
41
+ )
35
42
  end
36
43
 
37
44
  def disconnect(request, _unused_call)
38
45
  logger.debug("RPC Disconnect: #{request.inspect}")
39
46
 
40
- socket = build_socket(env: rack_env(request))
47
+ socket = build_socket(env: rack_env(request.env))
41
48
 
42
49
  connection = factory.call(
43
50
  socket,
@@ -50,12 +57,19 @@ module AnyCable
50
57
  else
51
58
  AnyCable::DisconnectResponse.new(status: AnyCable::Status::FAILURE)
52
59
  end
60
+ rescue => exp
61
+ notify_exception(exp, :disconnect, request)
62
+
63
+ AnyCable::DisconnectResponse.new(
64
+ status: AnyCable::Status::ERROR,
65
+ error_msg: exp.message
66
+ )
53
67
  end
54
68
 
55
69
  def command(message, _unused_call)
56
70
  logger.debug("RPC Command: #{message.inspect}")
57
71
 
58
- socket = build_socket
72
+ socket = build_socket(env: rack_env(message.env))
59
73
 
60
74
  connection = factory.call(
61
75
  socket,
@@ -73,30 +87,58 @@ module AnyCable
73
87
  disconnect: socket.closed?,
74
88
  stop_streams: socket.stop_streams?,
75
89
  streams: socket.streams,
76
- transmissions: socket.transmissions
90
+ transmissions: socket.transmissions,
91
+ env: build_env_response(socket)
92
+ )
93
+ rescue => exp
94
+ notify_exception(exp, :command, message)
95
+
96
+ AnyCable::CommandResponse.new(
97
+ status: AnyCable::Status::ERROR,
98
+ error_msg: exp.message
77
99
  )
78
100
  end
79
101
 
80
102
  private
81
103
 
82
- # Build env from path
83
- def rack_env(request)
84
- uri = URI.parse(request.path)
85
- {
86
- "QUERY_STRING" => uri.query,
87
- "SCRIPT_NAME" => "",
104
+ # Build Rack env from request
105
+ def rack_env(request_env)
106
+ uri = URI.parse(request_env.url)
107
+
108
+ env = base_rack_env
109
+ env.merge!(
88
110
  "PATH_INFO" => uri.path,
111
+ "QUERY_STRING" => uri.query,
112
+ "SERVER_NAME" => uri.host,
89
113
  "SERVER_PORT" => uri.port.to_s,
90
114
  "HTTP_HOST" => uri.host,
91
- # Hack to avoid Missing rack.input error
92
- "rack.request.form_input" => "",
93
- "rack.input" => "",
94
- "rack.request.form_hash" => {}
95
- }.merge(build_headers(request.headers))
115
+ "REMOTE_ADDR" => request_env.headers.delete("REMOTE_ADDR"),
116
+ "rack.url_scheme" => uri.scheme,
117
+ # AnyCable specific fields
118
+ "anycable.raw_cstate" => request_env.cstate&.to_h
119
+ )
120
+
121
+ env.merge!(build_headers(request_env.headers))
122
+ end
123
+
124
+ def base_rack_env
125
+ # Minimum required variables according to Rack Spec
126
+ # (not all of them though, just those enough for Action Cable to work)
127
+ # See https://rubydoc.info/github/rack/rack/master/file/SPEC
128
+ {
129
+ "REQUEST_METHOD" => "GET",
130
+ "SCRIPT_NAME" => "",
131
+ "PATH_INFO" => "/",
132
+ "QUERY_STRING" => "",
133
+ "SERVER_NAME" => "",
134
+ "SERVER_PORT" => "80",
135
+ "rack.url_scheme" => "http",
136
+ "rack.input" => ""
137
+ }
96
138
  end
97
139
 
98
140
  def build_socket(**options)
99
- AnyCable::Socket.new(options)
141
+ AnyCable::Socket.new(**options)
100
142
  end
101
143
 
102
144
  def build_headers(headers)
@@ -107,6 +149,12 @@ module AnyCable
107
149
  end
108
150
  end
109
151
 
152
+ def build_env_response(socket)
153
+ AnyCable::EnvResponse.new(
154
+ cstate: socket.cstate.changed_fields
155
+ )
156
+ end
157
+
110
158
  def logger
111
159
  AnyCable.logger
112
160
  end
@@ -114,7 +162,12 @@ module AnyCable
114
162
  def factory
115
163
  AnyCable.connection_factory
116
164
  end
165
+
166
+ def notify_exception(exp, method_name, message)
167
+ AnyCable::ExceptionsHandling.notify(exp, method_name.to_s, message.to_h)
168
+ end
117
169
  end
118
170
  end
119
171
  # rubocop:enable Metrics/AbcSize
120
172
  # rubocop:enable Metrics/MethodLength
173
+ # rubocop:enable Metrics/ClassLength