anycable 1.0.1 → 1.1.0
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 +31 -0
- data/MIT-LICENSE +1 -1
- data/README.md +1 -4
- metadata +16 -128
- data/bin/anycable +0 -13
- data/bin/anycabled +0 -30
- data/bin/console +0 -7
- data/bin/setup +0 -6
- data/lib/anycable.rb +0 -101
- data/lib/anycable/broadcast_adapters.rb +0 -35
- data/lib/anycable/broadcast_adapters/base.rb +0 -29
- data/lib/anycable/broadcast_adapters/http.rb +0 -130
- data/lib/anycable/broadcast_adapters/redis.rb +0 -43
- data/lib/anycable/cli.rb +0 -353
- data/lib/anycable/config.rb +0 -131
- data/lib/anycable/exceptions_handling.rb +0 -35
- data/lib/anycable/health_server.rb +0 -69
- data/lib/anycable/middleware.rb +0 -27
- data/lib/anycable/middleware_chain.rb +0 -58
- data/lib/anycable/middlewares/check_version.rb +0 -24
- data/lib/anycable/rpc.rb +0 -84
- data/lib/anycable/rpc/rpc_pb.rb +0 -74
- data/lib/anycable/rpc/rpc_services_pb.rb +0 -24
- data/lib/anycable/rpc_handler.rb +0 -183
- data/lib/anycable/rspec.rb +0 -6
- data/lib/anycable/rspec/rpc_command_context.rb +0 -20
- data/lib/anycable/rspec/rpc_stub_context.rb +0 -13
- data/lib/anycable/rspec/with_grpc_server.rb +0 -16
- data/lib/anycable/server.rb +0 -96
- data/lib/anycable/socket.rb +0 -90
- data/lib/anycable/version.rb +0 -5
@@ -1,24 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
4
|
-
# Source: rpc.proto for package 'anycable'
|
5
|
-
|
6
|
-
require "grpc"
|
7
|
-
|
8
|
-
module AnyCable
|
9
|
-
module RPC
|
10
|
-
class Service
|
11
|
-
include GRPC::GenericService
|
12
|
-
|
13
|
-
self.marshal_class_method = :encode
|
14
|
-
self.unmarshal_class_method = :decode
|
15
|
-
self.service_name = "anycable.RPC"
|
16
|
-
|
17
|
-
rpc :Connect, ConnectionRequest, ConnectionResponse
|
18
|
-
rpc :Command, CommandMessage, CommandResponse
|
19
|
-
rpc :Disconnect, DisconnectRequest, DisconnectResponse
|
20
|
-
end
|
21
|
-
|
22
|
-
Stub = Service.rpc_stub_class
|
23
|
-
end
|
24
|
-
end
|
data/lib/anycable/rpc_handler.rb
DELETED
@@ -1,183 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "anycable/socket"
|
4
|
-
require "anycable/rpc"
|
5
|
-
|
6
|
-
# rubocop:disable Metrics/AbcSize
|
7
|
-
# rubocop:disable Metrics/MethodLength
|
8
|
-
# rubocop:disable Metrics/ClassLength
|
9
|
-
module AnyCable
|
10
|
-
# RPC service handler
|
11
|
-
class RPCHandler < AnyCable::RPC::Service
|
12
|
-
# Handle connection request from WebSocket server
|
13
|
-
def connect(request, _unused_call)
|
14
|
-
logger.debug("RPC Connect: #{request.inspect}")
|
15
|
-
|
16
|
-
socket = build_socket(env: rack_env(request.env))
|
17
|
-
|
18
|
-
connection = factory.call(socket)
|
19
|
-
|
20
|
-
connection.handle_open
|
21
|
-
|
22
|
-
if socket.closed?
|
23
|
-
AnyCable::ConnectionResponse.new(
|
24
|
-
status: AnyCable::Status::FAILURE,
|
25
|
-
transmissions: socket.transmissions
|
26
|
-
)
|
27
|
-
else
|
28
|
-
AnyCable::ConnectionResponse.new(
|
29
|
-
status: AnyCable::Status::SUCCESS,
|
30
|
-
identifiers: connection.identifiers_json,
|
31
|
-
transmissions: socket.transmissions,
|
32
|
-
env: build_env_response(socket)
|
33
|
-
)
|
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
|
-
)
|
42
|
-
end
|
43
|
-
|
44
|
-
def disconnect(request, _unused_call)
|
45
|
-
logger.debug("RPC Disconnect: #{request.inspect}")
|
46
|
-
|
47
|
-
socket = build_socket(env: rack_env(request.env))
|
48
|
-
|
49
|
-
connection = factory.call(
|
50
|
-
socket,
|
51
|
-
identifiers: request.identifiers,
|
52
|
-
subscriptions: request.subscriptions
|
53
|
-
)
|
54
|
-
|
55
|
-
if connection.handle_close
|
56
|
-
AnyCable::DisconnectResponse.new(status: AnyCable::Status::SUCCESS)
|
57
|
-
else
|
58
|
-
AnyCable::DisconnectResponse.new(status: AnyCable::Status::FAILURE)
|
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
|
-
)
|
67
|
-
end
|
68
|
-
|
69
|
-
def command(message, _unused_call)
|
70
|
-
logger.debug("RPC Command: #{message.inspect}")
|
71
|
-
|
72
|
-
socket = build_socket(env: rack_env(message.env))
|
73
|
-
|
74
|
-
connection = factory.call(
|
75
|
-
socket,
|
76
|
-
identifiers: message.connection_identifiers
|
77
|
-
)
|
78
|
-
|
79
|
-
result = connection.handle_channel_command(
|
80
|
-
message.identifier,
|
81
|
-
message.command,
|
82
|
-
message.data
|
83
|
-
)
|
84
|
-
|
85
|
-
AnyCable::CommandResponse.new(
|
86
|
-
status: result ? AnyCable::Status::SUCCESS : AnyCable::Status::FAILURE,
|
87
|
-
disconnect: socket.closed?,
|
88
|
-
stop_streams: socket.stop_streams?,
|
89
|
-
streams: socket.streams[:start],
|
90
|
-
stopped_streams: socket.streams[:stop],
|
91
|
-
transmissions: socket.transmissions,
|
92
|
-
env: build_env_response(socket)
|
93
|
-
)
|
94
|
-
rescue => exp
|
95
|
-
notify_exception(exp, :command, message)
|
96
|
-
|
97
|
-
AnyCable::CommandResponse.new(
|
98
|
-
status: AnyCable::Status::ERROR,
|
99
|
-
error_msg: exp.message
|
100
|
-
)
|
101
|
-
end
|
102
|
-
|
103
|
-
private
|
104
|
-
|
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!({
|
111
|
-
"PATH_INFO" => uri.path,
|
112
|
-
"QUERY_STRING" => uri.query,
|
113
|
-
"SERVER_NAME" => uri.host,
|
114
|
-
"SERVER_PORT" => uri.port,
|
115
|
-
"HTTP_HOST" => uri.host,
|
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
|
-
}
|
147
|
-
end
|
148
|
-
|
149
|
-
def build_socket(**options)
|
150
|
-
AnyCable::Socket.new(**options)
|
151
|
-
end
|
152
|
-
|
153
|
-
def build_headers(headers)
|
154
|
-
headers.each_with_object({}) do |(k, v), obj|
|
155
|
-
k = k.upcase
|
156
|
-
k.tr!("-", "_")
|
157
|
-
obj["HTTP_#{k}"] = v
|
158
|
-
end
|
159
|
-
end
|
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
|
-
|
168
|
-
def logger
|
169
|
-
AnyCable.logger
|
170
|
-
end
|
171
|
-
|
172
|
-
def factory
|
173
|
-
AnyCable.connection_factory
|
174
|
-
end
|
175
|
-
|
176
|
-
def notify_exception(exp, method_name, message)
|
177
|
-
AnyCable::ExceptionsHandling.notify(exp, method_name.to_s, message.to_h)
|
178
|
-
end
|
179
|
-
end
|
180
|
-
end
|
181
|
-
# rubocop:enable Metrics/AbcSize
|
182
|
-
# rubocop:enable Metrics/MethodLength
|
183
|
-
# rubocop:enable Metrics/ClassLength
|
data/lib/anycable/rspec.rb
DELETED
@@ -1,20 +0,0 @@
|
|
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
|
@@ -1,13 +0,0 @@
|
|
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
|
@@ -1,16 +0,0 @@
|
|
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
|
-
sleep 0.1
|
13
|
-
end
|
14
|
-
|
15
|
-
after(:all) { @server.stop }
|
16
|
-
end
|
data/lib/anycable/server.rb
DELETED
@@ -1,96 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "grpc"
|
4
|
-
require "grpc/health/checker"
|
5
|
-
require "grpc/health/v1/health_services_pb"
|
6
|
-
|
7
|
-
require "anycable/rpc_handler"
|
8
|
-
require "anycable/health_server"
|
9
|
-
|
10
|
-
module AnyCable
|
11
|
-
# Wrapper over gRPC server.
|
12
|
-
#
|
13
|
-
# Basic example:
|
14
|
-
#
|
15
|
-
# # create new server listening on the loopback interface with 50051 port
|
16
|
-
# server = AnyCable::Server.new(host: "127.0.0.1:50051")
|
17
|
-
#
|
18
|
-
# # run gRPC server in bakground
|
19
|
-
# server.start
|
20
|
-
#
|
21
|
-
# # stop server
|
22
|
-
# server.stop
|
23
|
-
class Server
|
24
|
-
attr_reader :grpc_server, :host
|
25
|
-
|
26
|
-
def initialize(host:, logger: nil, **options)
|
27
|
-
@logger = logger
|
28
|
-
@host = host
|
29
|
-
@grpc_server = build_server(options)
|
30
|
-
end
|
31
|
-
|
32
|
-
# Start gRPC server in background and
|
33
|
-
# wait untill it ready to accept connections
|
34
|
-
def start
|
35
|
-
return if running?
|
36
|
-
|
37
|
-
raise "Cannot re-start stopped server" if stopped?
|
38
|
-
|
39
|
-
logger.info "RPC server is starting..."
|
40
|
-
|
41
|
-
@start_thread = Thread.new { grpc_server.run }
|
42
|
-
|
43
|
-
grpc_server.wait_till_running
|
44
|
-
|
45
|
-
logger.info "RPC server is listening on #{host}"
|
46
|
-
end
|
47
|
-
|
48
|
-
def wait_till_terminated
|
49
|
-
raise "Server is not running" unless running?
|
50
|
-
|
51
|
-
start_thread.join
|
52
|
-
end
|
53
|
-
|
54
|
-
# Stop gRPC server if it's running
|
55
|
-
def stop
|
56
|
-
return unless running?
|
57
|
-
|
58
|
-
grpc_server.stop
|
59
|
-
|
60
|
-
logger.info "RPC server stopped"
|
61
|
-
end
|
62
|
-
|
63
|
-
def running?
|
64
|
-
grpc_server.running_state == :running
|
65
|
-
end
|
66
|
-
|
67
|
-
def stopped?
|
68
|
-
grpc_server.running_state == :stopped
|
69
|
-
end
|
70
|
-
|
71
|
-
private
|
72
|
-
|
73
|
-
attr_reader :start_thread
|
74
|
-
|
75
|
-
def logger
|
76
|
-
@logger ||= AnyCable.logger
|
77
|
-
end
|
78
|
-
|
79
|
-
def build_server(options)
|
80
|
-
GRPC::RpcServer.new(**options).tap do |server|
|
81
|
-
server.add_http2_port(host, :this_port_is_insecure)
|
82
|
-
server.handle(AnyCable::RPCHandler)
|
83
|
-
server.handle(build_health_checker)
|
84
|
-
end
|
85
|
-
end
|
86
|
-
|
87
|
-
def build_health_checker
|
88
|
-
health_checker = Grpc::Health::Checker.new
|
89
|
-
health_checker.add_status(
|
90
|
-
"anycable.RPC",
|
91
|
-
Grpc::Health::V1::HealthCheckResponse::ServingStatus::SERVING
|
92
|
-
)
|
93
|
-
health_checker
|
94
|
-
end
|
95
|
-
end
|
96
|
-
end
|
data/lib/anycable/socket.rb
DELETED
@@ -1,90 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module AnyCable
|
4
|
-
# Socket mock to be used with application connection
|
5
|
-
class Socket
|
6
|
-
# Represents the per-connection store
|
7
|
-
# (for example, used to keep session beetween RPC calls)
|
8
|
-
class State
|
9
|
-
attr_reader :dirty_keys, :source
|
10
|
-
|
11
|
-
def initialize(from)
|
12
|
-
@source = from
|
13
|
-
@dirty_keys = nil
|
14
|
-
end
|
15
|
-
|
16
|
-
def read(key)
|
17
|
-
source&.[](key)
|
18
|
-
end
|
19
|
-
|
20
|
-
alias [] read
|
21
|
-
|
22
|
-
def write(key, val)
|
23
|
-
return if source&.[](key) == val
|
24
|
-
|
25
|
-
@source ||= {}
|
26
|
-
@dirty_keys ||= []
|
27
|
-
dirty_keys << key
|
28
|
-
source[key] = val
|
29
|
-
end
|
30
|
-
|
31
|
-
alias []= write
|
32
|
-
|
33
|
-
def changed_fields
|
34
|
-
return unless source && dirty_keys
|
35
|
-
source.slice(*dirty_keys)
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
attr_reader :transmissions, :env, :cstate, :istate
|
40
|
-
|
41
|
-
def initialize(env: nil)
|
42
|
-
@transmissions = []
|
43
|
-
@env = env
|
44
|
-
@cstate = env["anycable.cstate"] = State.new(env["anycable.raw_cstate"])
|
45
|
-
@istate = env["anycable.istate"] = State.new(env["anycable.raw_istate"])
|
46
|
-
end
|
47
|
-
|
48
|
-
def transmit(websocket_message)
|
49
|
-
transmissions << websocket_message
|
50
|
-
end
|
51
|
-
|
52
|
-
def subscribe(_channel, broadcasting)
|
53
|
-
streams[:start] << broadcasting
|
54
|
-
end
|
55
|
-
|
56
|
-
def unsubscribe(_channel, broadcasting)
|
57
|
-
streams[:stop] << broadcasting
|
58
|
-
end
|
59
|
-
|
60
|
-
def unsubscribe_from_all(_channel)
|
61
|
-
@stop_all_streams = true
|
62
|
-
end
|
63
|
-
|
64
|
-
def streams
|
65
|
-
@streams ||= {start: [], stop: []}
|
66
|
-
end
|
67
|
-
|
68
|
-
def close
|
69
|
-
@closed = true
|
70
|
-
@streams&.clear
|
71
|
-
@stop_all_streams = true
|
72
|
-
end
|
73
|
-
|
74
|
-
def closed?
|
75
|
-
@closed == true
|
76
|
-
end
|
77
|
-
|
78
|
-
def stop_streams?
|
79
|
-
@stop_all_streams == true
|
80
|
-
end
|
81
|
-
|
82
|
-
def session
|
83
|
-
cstate.read(SESSION_KEY)
|
84
|
-
end
|
85
|
-
|
86
|
-
def session=(val)
|
87
|
-
cstate.write(SESSION_KEY, val)
|
88
|
-
end
|
89
|
-
end
|
90
|
-
end
|