anycable 0.6.4 → 1.0.0.rc2
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +24 -236
- data/MIT-LICENSE +1 -1
- data/README.md +17 -16
- data/lib/anycable.rb +3 -12
- data/lib/anycable/broadcast_adapters.rb +4 -2
- data/lib/anycable/broadcast_adapters/base.rb +29 -0
- data/lib/anycable/broadcast_adapters/http.rb +130 -0
- data/lib/anycable/broadcast_adapters/redis.rb +7 -6
- data/lib/anycable/cli.rb +35 -11
- data/lib/anycable/config.rb +44 -21
- data/lib/anycable/exceptions_handling.rb +5 -7
- data/lib/anycable/health_server.rb +4 -2
- data/lib/anycable/middleware.rb +9 -1
- data/lib/anycable/middleware_chain.rb +2 -2
- data/lib/anycable/middlewares/check_version.rb +24 -0
- data/lib/anycable/rpc.rb +84 -0
- data/lib/anycable/rpc/rpc_pb.rb +57 -39
- data/lib/anycable/rpc/rpc_services_pb.rb +4 -3
- data/lib/anycable/rpc_handler.rb +44 -24
- data/lib/anycable/rspec.rb +6 -0
- data/lib/anycable/rspec/rpc_command_context.rb +20 -0
- data/lib/anycable/rspec/rpc_stub_context.rb +13 -0
- data/lib/anycable/rspec/with_grpc_server.rb +15 -0
- data/lib/anycable/server.rb +6 -62
- data/lib/anycable/socket.rb +48 -6
- data/lib/anycable/version.rb +1 -1
- metadata +21 -41
@@ -48,8 +48,8 @@ module AnyCable
|
|
48
48
|
|
49
49
|
unless middleware.is_a?(AnyCable::Middleware)
|
50
50
|
raise ArgumentError,
|
51
|
-
|
52
|
-
|
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 compatible 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
|
data/lib/anycable/rpc.rb
ADDED
@@ -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
|
data/lib/anycable/rpc/rpc_pb.rb
CHANGED
@@ -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
|
6
|
+
require "google/protobuf"
|
5
7
|
|
6
8
|
Google::Protobuf::DescriptorPool.generated_pool.build do
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
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
|
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 =
|
15
|
+
self.service_name = "anycable.RPC"
|
15
16
|
|
16
17
|
rpc :Connect, ConnectionRequest, ConnectionResponse
|
17
18
|
rpc :Command, CommandMessage, CommandResponse
|
data/lib/anycable/rpc_handler.rb
CHANGED
@@ -1,8 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "anycable/socket"
|
4
|
-
require "anycable/rpc
|
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(
|
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
|
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
|
60
|
+
rescue => exp
|
58
61
|
notify_exception(exp, :disconnect, request)
|
59
62
|
|
60
63
|
AnyCable::DisconnectResponse.new(
|
@@ -66,9 +69,7 @@ module AnyCable
|
|
66
69
|
def command(message, _unused_call)
|
67
70
|
logger.debug("RPC Command: #{message.inspect}")
|
68
71
|
|
69
|
-
|
70
|
-
# but we still want `connection.env` to work
|
71
|
-
socket = build_socket(env: base_rack_env)
|
72
|
+
socket = build_socket(env: rack_env(message.env))
|
72
73
|
|
73
74
|
connection = factory.call(
|
74
75
|
socket,
|
@@ -85,10 +86,12 @@ module AnyCable
|
|
85
86
|
status: result ? AnyCable::Status::SUCCESS : AnyCable::Status::FAILURE,
|
86
87
|
disconnect: socket.closed?,
|
87
88
|
stop_streams: socket.stop_streams?,
|
88
|
-
streams: socket.streams,
|
89
|
-
|
89
|
+
streams: socket.streams[:start],
|
90
|
+
stopped_streams: socket.streams[:stop],
|
91
|
+
transmissions: socket.transmissions,
|
92
|
+
env: build_env_response(socket)
|
90
93
|
)
|
91
|
-
rescue
|
94
|
+
rescue => exp
|
92
95
|
notify_exception(exp, :command, message)
|
93
96
|
|
94
97
|
AnyCable::CommandResponse.new(
|
@@ -100,27 +103,31 @@ module AnyCable
|
|
100
103
|
private
|
101
104
|
|
102
105
|
# Build Rack env from request
|
103
|
-
def rack_env(
|
104
|
-
uri = URI.parse(
|
106
|
+
def rack_env(request_env)
|
107
|
+
uri = URI.parse(request_env.url)
|
105
108
|
|
106
109
|
env = base_rack_env
|
107
|
-
env.merge!(
|
110
|
+
env.merge!({
|
108
111
|
"PATH_INFO" => uri.path,
|
109
112
|
"QUERY_STRING" => uri.query,
|
110
113
|
"SERVER_NAME" => uri.host,
|
111
|
-
"SERVER_PORT" => uri.port
|
114
|
+
"SERVER_PORT" => uri.port,
|
112
115
|
"HTTP_HOST" => uri.host,
|
113
|
-
"REMOTE_ADDR" =>
|
114
|
-
"rack.url_scheme" => uri.scheme
|
115
|
-
|
116
|
-
|
117
|
-
|
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))
|
118
124
|
end
|
119
125
|
|
120
126
|
def base_rack_env
|
121
127
|
# Minimum required variables according to Rack Spec
|
122
128
|
# (not all of them though, just those enough for Action Cable to work)
|
123
129
|
# See https://rubydoc.info/github/rack/rack/master/file/SPEC
|
130
|
+
# and https://github.com/rack/rack/blob/master/lib/rack/lint.rb
|
124
131
|
{
|
125
132
|
"REQUEST_METHOD" => "GET",
|
126
133
|
"SCRIPT_NAME" => "",
|
@@ -129,7 +136,13 @@ module AnyCable
|
|
129
136
|
"SERVER_NAME" => "",
|
130
137
|
"SERVER_PORT" => "80",
|
131
138
|
"rack.url_scheme" => "http",
|
132
|
-
"rack.input" => ""
|
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
|
133
146
|
}
|
134
147
|
end
|
135
148
|
|
@@ -145,6 +158,13 @@ module AnyCable
|
|
145
158
|
end
|
146
159
|
end
|
147
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
|
+
|
148
168
|
def logger
|
149
169
|
AnyCable.logger
|
150
170
|
end
|
@@ -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
|