anycable 0.6.3 → 1.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,84 @@
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
+
39
+ def istate
40
+ env.istate
41
+ end
42
+
43
+ def istate=(val)
44
+ env.istate = val
45
+ end
46
+ end
47
+
48
+ # Status predicates
49
+ module StatusPredicates
50
+ def success?
51
+ status == :SUCCESS
52
+ end
53
+
54
+ def failure?
55
+ status == :FAILURE
56
+ end
57
+
58
+ def error?
59
+ status == :ERROR
60
+ end
61
+ end
62
+
63
+ class ConnectionResponse
64
+ prepend WithConnectionState
65
+ include StatusPredicates
66
+ end
67
+
68
+ class CommandMessage
69
+ prepend WithConnectionState
70
+ end
71
+
72
+ class CommandResponse
73
+ prepend WithConnectionState
74
+ include StatusPredicates
75
+ end
76
+
77
+ class DisconnectRequest
78
+ prepend WithConnectionState
79
+ end
80
+
81
+ class DisconnectResponse
82
+ include StatusPredicates
83
+ end
84
+ end
@@ -1,51 +1,69 @@
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
+ map :istate, :string, :string, 4
15
+ end
16
+ add_message "anycable.EnvResponse" do
17
+ map :cstate, :string, :string, 1
18
+ map :istate, :string, :string, 2
19
+ end
20
+ add_message "anycable.ConnectionRequest" do
21
+ optional :env, :message, 3, "anycable.Env"
22
+ end
23
+ add_message "anycable.ConnectionResponse" do
24
+ optional :status, :enum, 1, "anycable.Status"
25
+ optional :identifiers, :string, 2
26
+ repeated :transmissions, :string, 3
27
+ optional :error_msg, :string, 4
28
+ optional :env, :message, 5, "anycable.EnvResponse"
29
+ end
30
+ add_message "anycable.CommandMessage" do
31
+ optional :command, :string, 1
32
+ optional :identifier, :string, 2
33
+ optional :connection_identifiers, :string, 3
34
+ optional :data, :string, 4
35
+ optional :env, :message, 5, "anycable.Env"
36
+ end
37
+ add_message "anycable.CommandResponse" do
38
+ optional :status, :enum, 1, "anycable.Status"
39
+ optional :disconnect, :bool, 2
40
+ optional :stop_streams, :bool, 3
41
+ repeated :streams, :string, 4
42
+ repeated :transmissions, :string, 5
43
+ optional :error_msg, :string, 6
44
+ optional :env, :message, 7, "anycable.EnvResponse"
45
+ repeated :stopped_streams, :string, 8
46
+ end
47
+ add_message "anycable.DisconnectRequest" do
48
+ optional :identifiers, :string, 1
49
+ repeated :subscriptions, :string, 2
50
+ optional :env, :message, 5, "anycable.Env"
51
+ end
52
+ add_message "anycable.DisconnectResponse" do
53
+ optional :status, :enum, 1, "anycable.Status"
54
+ optional :error_msg, :string, 2
55
+ end
56
+ add_enum "anycable.Status" do
57
+ value :ERROR, 0
58
+ value :SUCCESS, 1
59
+ value :FAILURE, 2
60
+ end
45
61
  end
46
62
  end
47
63
 
48
64
  module AnyCable
65
+ Env = Google::Protobuf::DescriptorPool.generated_pool.lookup("anycable.Env").msgclass
66
+ EnvResponse = Google::Protobuf::DescriptorPool.generated_pool.lookup("anycable.EnvResponse").msgclass
49
67
  ConnectionRequest = Google::Protobuf::DescriptorPool.generated_pool.lookup("anycable.ConnectionRequest").msgclass
50
68
  ConnectionResponse = Google::Protobuf::DescriptorPool.generated_pool.lookup("anycable.ConnectionResponse").msgclass
51
69
  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,8 +1,7 @@
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"
4
+ require "anycable/rpc"
6
5
 
7
6
  # rubocop:disable Metrics/AbcSize
8
7
  # rubocop:disable Metrics/MethodLength
@@ -14,22 +13,26 @@ module AnyCable
14
13
  def connect(request, _unused_call)
15
14
  logger.debug("RPC Connect: #{request.inspect}")
16
15
 
17
- socket = build_socket(env: rack_env(request))
16
+ socket = build_socket(env: rack_env(request.env))
18
17
 
19
18
  connection = factory.call(socket)
20
19
 
21
20
  connection.handle_open
22
21
 
23
22
  if socket.closed?
24
- AnyCable::ConnectionResponse.new(status: AnyCable::Status::FAILURE)
23
+ AnyCable::ConnectionResponse.new(
24
+ status: AnyCable::Status::FAILURE,
25
+ transmissions: socket.transmissions
26
+ )
25
27
  else
26
28
  AnyCable::ConnectionResponse.new(
27
29
  status: AnyCable::Status::SUCCESS,
28
30
  identifiers: connection.identifiers_json,
29
- transmissions: socket.transmissions
31
+ transmissions: socket.transmissions,
32
+ env: build_env_response(socket)
30
33
  )
31
34
  end
32
- rescue StandardError => exp
35
+ rescue => exp
33
36
  notify_exception(exp, :connect, request)
34
37
 
35
38
  AnyCable::ConnectionResponse.new(
@@ -41,7 +44,7 @@ module AnyCable
41
44
  def disconnect(request, _unused_call)
42
45
  logger.debug("RPC Disconnect: #{request.inspect}")
43
46
 
44
- socket = build_socket(env: rack_env(request))
47
+ socket = build_socket(env: rack_env(request.env))
45
48
 
46
49
  connection = factory.call(
47
50
  socket,
@@ -54,7 +57,7 @@ module AnyCable
54
57
  else
55
58
  AnyCable::DisconnectResponse.new(status: AnyCable::Status::FAILURE)
56
59
  end
57
- rescue StandardError => exp
60
+ rescue => exp
58
61
  notify_exception(exp, :disconnect, request)
59
62
 
60
63
  AnyCable::DisconnectResponse.new(
@@ -66,7 +69,7 @@ module AnyCable
66
69
  def command(message, _unused_call)
67
70
  logger.debug("RPC Command: #{message.inspect}")
68
71
 
69
- socket = build_socket
72
+ socket = build_socket(env: rack_env(message.env))
70
73
 
71
74
  connection = factory.call(
72
75
  socket,
@@ -83,10 +86,12 @@ module AnyCable
83
86
  status: result ? AnyCable::Status::SUCCESS : AnyCable::Status::FAILURE,
84
87
  disconnect: socket.closed?,
85
88
  stop_streams: socket.stop_streams?,
86
- streams: socket.streams,
87
- transmissions: socket.transmissions
89
+ streams: socket.streams[:start],
90
+ stopped_streams: socket.streams[:stop],
91
+ transmissions: socket.transmissions,
92
+ env: build_env_response(socket)
88
93
  )
89
- rescue StandardError => exp
94
+ rescue => exp
90
95
  notify_exception(exp, :command, message)
91
96
 
92
97
  AnyCable::CommandResponse.new(
@@ -97,24 +102,52 @@ module AnyCable
97
102
 
98
103
  private
99
104
 
100
- # Build env from path
101
- def rack_env(request)
102
- uri = URI.parse(request.path)
103
- {
104
- "QUERY_STRING" => uri.query,
105
- "SCRIPT_NAME" => "",
105
+ # Build Rack env from request
106
+ def rack_env(request_env)
107
+ uri = URI.parse(request_env.url)
108
+
109
+ env = base_rack_env
110
+ env.merge!({
106
111
  "PATH_INFO" => uri.path,
107
- "SERVER_PORT" => uri.port.to_s,
112
+ "QUERY_STRING" => uri.query,
113
+ "SERVER_NAME" => uri.host,
114
+ "SERVER_PORT" => uri.port,
108
115
  "HTTP_HOST" => uri.host,
109
- # Hack to avoid Missing rack.input error
110
- "rack.request.form_input" => "",
111
- "rack.input" => "",
112
- "rack.request.form_hash" => {}
113
- }.merge(build_headers(request.headers))
116
+ "REMOTE_ADDR" => request_env.headers.delete("REMOTE_ADDR"),
117
+ "rack.url_scheme" => uri.scheme&.sub(/^ws/, "http"),
118
+ # AnyCable specific fields
119
+ "anycable.raw_cstate" => request_env.cstate&.to_h,
120
+ "anycable.raw_istate" => request_env.istate&.to_h
121
+ }.delete_if { |_k, v| v.nil? })
122
+
123
+ env.merge!(build_headers(request_env.headers))
124
+ end
125
+
126
+ def base_rack_env
127
+ # Minimum required variables according to Rack Spec
128
+ # (not all of them though, just those enough for Action Cable to work)
129
+ # See https://rubydoc.info/github/rack/rack/master/file/SPEC
130
+ # and https://github.com/rack/rack/blob/master/lib/rack/lint.rb
131
+ {
132
+ "REQUEST_METHOD" => "GET",
133
+ "SCRIPT_NAME" => "",
134
+ "PATH_INFO" => "/",
135
+ "QUERY_STRING" => "",
136
+ "SERVER_NAME" => "",
137
+ "SERVER_PORT" => "80",
138
+ "rack.url_scheme" => "http",
139
+ "rack.input" => StringIO.new("", "r").tap { |io| io.set_encoding(Encoding::ASCII_8BIT) },
140
+ "rack.version" => Rack::VERSION,
141
+ "rack.errors" => StringIO.new("").tap { |io| io.set_encoding(Encoding::ASCII_8BIT) },
142
+ "rack.multithread" => true,
143
+ "rack.multiprocess" => false,
144
+ "rack.run_once" => false,
145
+ "rack.hijack?" => false
146
+ }
114
147
  end
115
148
 
116
149
  def build_socket(**options)
117
- AnyCable::Socket.new(options)
150
+ AnyCable::Socket.new(**options)
118
151
  end
119
152
 
120
153
  def build_headers(headers)
@@ -125,6 +158,13 @@ module AnyCable
125
158
  end
126
159
  end
127
160
 
161
+ def build_env_response(socket)
162
+ AnyCable::EnvResponse.new(
163
+ cstate: socket.cstate.changed_fields,
164
+ istate: socket.istate.changed_fields
165
+ )
166
+ end
167
+
128
168
  def logger
129
169
  AnyCable.logger
130
170
  end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Utils for testing AnyCable and its plugins
4
+ require "anycable/rspec/rpc_stub_context"
5
+ require "anycable/rspec/rpc_command_context"
6
+ require "anycable/rspec/with_grpc_server"
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.shared_context "anycable:rpc:command" do
4
+ include_context "anycable:rpc:stub"
5
+
6
+ let(:command) { "" }
7
+ let(:channel_id) { "" }
8
+ let(:identifiers) { {} }
9
+ let(:data) { {} }
10
+
11
+ let(:request) do
12
+ AnyCable::CommandMessage.new(
13
+ command: command,
14
+ identifier: channel_id,
15
+ connection_identifiers: identifiers.to_json,
16
+ data: data.to_json,
17
+ env: env
18
+ )
19
+ end
20
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.shared_context "anycable:rpc:stub" do
4
+ before(:all) do
5
+ @service = AnyCable::RPC::Stub.new(AnyCable.config.rpc_host, :this_channel_is_insecure)
6
+ end
7
+
8
+ let(:service) { @service }
9
+
10
+ let(:url) { "ws://example.anycable.com/cable" }
11
+ let(:headers) { {} }
12
+ let(:env) { AnyCable::Env.new(url: url, headers: headers) }
13
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.shared_context "anycable:rpc:server" do
4
+ before(:all) do
5
+ @server = AnyCable::Server.new(
6
+ host: AnyCable.config.rpc_host,
7
+ **AnyCable.config.to_grpc_params,
8
+ interceptors: AnyCable.middleware.to_a
9
+ )
10
+
11
+ @server.start
12
+ end
13
+
14
+ after(:all) { @server.stop }
15
+ end
@@ -21,51 +21,9 @@ module AnyCable
21
21
  # # stop server
22
22
  # server.stop
23
23
  class Server
24
- class << self
25
- # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
26
- def start(**options)
27
- warn <<~DEPRECATION
28
- DEPRECATION WARNING: Using AnyCable::Server.start is deprecated!
29
- Please, use anycable CLI instead.
30
-
31
- See https://docs.anycable.io/#upgrade_to_0_6_0
32
- DEPRECATION
33
-
34
- AnyCable.server_callbacks.each(&:call)
35
-
36
- server = new(
37
- host: AnyCable.config.rpc_host,
38
- **AnyCable.config.to_grpc_params,
39
- interceptors: AnyCable.middleware.to_a,
40
- **options
41
- )
42
-
43
- AnyCable.middleware.freeze
44
-
45
- if AnyCable.config.http_health_port_provided?
46
- health_server = AnyCable::HealthServer.new(
47
- server,
48
- **AnyCable.config.to_http_health_params
49
- )
50
- health_server.start
51
- end
52
-
53
- at_exit do
54
- server.stop
55
- health_server&.stop
56
- end
57
-
58
- AnyCable.logger.info "Broadcasting Redis channel: #{AnyCable.config.redis_channel}"
59
-
60
- server.start
61
- server.wait_till_terminated
62
- end
63
- # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
64
- end
65
-
66
24
  attr_reader :grpc_server, :host
67
25
 
68
- def initialize(host:, logger: AnyCable.logger, **options)
26
+ def initialize(host:, logger: nil, **options)
69
27
  @logger = logger
70
28
  @host = host
71
29
  @grpc_server = build_server(options)
@@ -78,8 +36,6 @@ module AnyCable
78
36
 
79
37
  raise "Cannot re-start stopped server" if stopped?
80
38
 
81
- check_default_host
82
-
83
39
  logger.info "RPC server is starting..."
84
40
 
85
41
  @start_thread = Thread.new { grpc_server.run }
@@ -114,10 +70,14 @@ module AnyCable
114
70
 
115
71
  private
116
72
 
117
- attr_reader :logger, :start_thread
73
+ attr_reader :start_thread
74
+
75
+ def logger
76
+ @logger ||= AnyCable.logger
77
+ end
118
78
 
119
79
  def build_server(options)
120
- GRPC::RpcServer.new(options).tap do |server|
80
+ GRPC::RpcServer.new(**options).tap do |server|
121
81
  server.add_http2_port(host, :this_port_is_insecure)
122
82
  server.handle(AnyCable::RPCHandler)
123
83
  server.handle(build_health_checker)
@@ -132,21 +92,5 @@ module AnyCable
132
92
  )
133
93
  health_checker
134
94
  end
135
-
136
- def check_default_host
137
- return unless host.is_a?(Anycable::Config::DefaultHostWrapper)
138
-
139
- warn <<~DEPRECATION
140
- DEPRECATION WARNING: You're using default rpc_host configuration which starts AnyCable RPC
141
- server on all available interfaces including external IPv4 and IPv6.
142
- This is about to be changed to loopback interface only in future versions.
143
-
144
- Please, consider switching to the loopback interface or set "[::]:50051"
145
- explicitly in your configuration, if you want to continue with the current
146
- behavior and supress this message.
147
-
148
- See https://docs.anycable.io/#/configuration
149
- DEPRECATION
150
- end
151
95
  end
152
96
  end