anycable 0.6.0 → 1.0.0.preview1

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.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +93 -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 +8 -13
  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 +16 -6
  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 -48
  26. data/lib/anycable/socket.rb +39 -2
  27. data/lib/anycable/version.rb +1 -1
  28. metadata +34 -72
  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
@@ -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