anycable 0.5.2 → 0.6.0.rc1
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/.github/ISSUE_TEMPLATE.md +25 -0
- data/.github/PULL_REQUEST_TEMPLATE.md +31 -0
- data/.rubocop.yml +22 -22
- data/.travis.yml +1 -2
- data/CHANGELOG.md +92 -0
- data/README.md +10 -58
- data/anycable.gemspec +10 -7
- data/benchmarks/.gitignore +1 -0
- data/benchmarks/2018-10-27.md +181 -0
- data/benchmarks/assets/2018-10-27-action-cable-rss.png +0 -0
- data/benchmarks/assets/2018-10-27-action-cable-rtt.png +0 -0
- data/benchmarks/assets/2018-10-27-anycable-rss.png +0 -0
- data/benchmarks/assets/2018-10-27-anycable-rtt.png +0 -0
- data/benchmarks/assets/2018-10-27-async-rss.png +0 -0
- data/benchmarks/assets/2018-10-27-async-rtt.png +0 -0
- data/benchmarks/assets/2018-10-27-falcon-cable-rss.png +0 -0
- data/benchmarks/assets/2018-10-27-falcon-cable-rtt.png +0 -0
- data/benchmarks/assets/2018-10-27-iodine-cable-rss.png +0 -0
- data/benchmarks/assets/2018-10-27-iodine-cable-rtt.png +0 -0
- data/benchmarks/assets/2018-10-27-plezi-rss.png +0 -0
- data/benchmarks/assets/2018-10-27-plezi-rtt.png +0 -0
- data/benchmarks/bench.png +0 -0
- data/benchmarks/benchmark.yml +12 -10
- data/benchmarks/hosts +2 -2
- data/benchmarks/rtt_plot.py +74 -0
- data/benchmarks/rtt_plot_test.py +16 -0
- data/benchmarks/servers.yml +25 -3
- data/bin/anycable +13 -0
- data/etc/bug_report_template.rb +1 -1
- data/lib/anycable.rb +53 -16
- data/lib/anycable/broadcast_adapters.rb +33 -0
- data/lib/anycable/broadcast_adapters/redis.rb +42 -0
- data/lib/anycable/cli.rb +323 -0
- data/lib/anycable/config.rb +91 -17
- data/lib/anycable/exceptions_handling.rb +31 -0
- data/lib/anycable/handler/capture_exceptions.rb +39 -0
- data/lib/anycable/health_server.rb +53 -31
- data/lib/anycable/middleware.rb +19 -0
- data/lib/anycable/middleware_chain.rb +58 -0
- data/lib/anycable/rpc/rpc_pb.rb +1 -1
- data/lib/anycable/rpc/rpc_services_pb.rb +1 -1
- data/lib/anycable/rpc_handler.rb +28 -26
- data/lib/anycable/server.rb +114 -39
- data/lib/anycable/socket.rb +1 -1
- data/lib/anycable/version.rb +2 -2
- metadata +45 -26
- data/lib/anycable/handler/exceptions_handling.rb +0 -43
- data/lib/anycable/pubsub.rb +0 -26
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "anycable/rpc/rpc_pb"
|
4
|
+
|
5
|
+
module AnyCable
|
6
|
+
module Handler # :nodoc:
|
7
|
+
# Handle app-level errors.
|
8
|
+
#
|
9
|
+
# NOTE: this functionality couldn't be implemeted
|
10
|
+
# as middleware, 'cause interceptors do not support
|
11
|
+
# aborting the call and returning a data
|
12
|
+
module CaptureExceptions
|
13
|
+
RESPONSE_CLASS = {
|
14
|
+
command: AnyCable::CommandResponse,
|
15
|
+
connect: AnyCable::ConnectionResponse,
|
16
|
+
disconnect: AnyCable::DisconnectResponse
|
17
|
+
}.freeze
|
18
|
+
|
19
|
+
RESPONSE_CLASS.keys.each do |mid|
|
20
|
+
module_eval <<~CODE, __FILE__, __LINE__ + 1
|
21
|
+
def #{mid}(*)
|
22
|
+
capture_exceptions(:#{mid}) { super }
|
23
|
+
end
|
24
|
+
CODE
|
25
|
+
end
|
26
|
+
|
27
|
+
def capture_exceptions(method_name)
|
28
|
+
yield
|
29
|
+
rescue StandardError => exp
|
30
|
+
AnyCable::ExceptionsHandling.notify(exp)
|
31
|
+
|
32
|
+
RESPONSE_CLASS.fetch(method_name).new(
|
33
|
+
status: AnyCable::Status::ERROR,
|
34
|
+
error_msg: exp.message
|
35
|
+
)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -1,44 +1,66 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
3
|
+
require "webrick"
|
4
|
+
require "anycable/server"
|
5
5
|
|
6
|
-
module
|
7
|
-
# Server for HTTP healthchecks
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
6
|
+
module AnyCable
|
7
|
+
# Server for HTTP healthchecks.
|
8
|
+
#
|
9
|
+
# Basic usage:
|
10
|
+
#
|
11
|
+
# # create a new healthcheck server for a specified
|
12
|
+
# # gRPC server lisening on the port
|
13
|
+
# health_server = AnyCable::HealthServer.new(grpc_server, port)
|
14
|
+
#
|
15
|
+
# # start health server in background
|
16
|
+
# health_server.start
|
17
|
+
#
|
18
|
+
# # stop health server
|
19
|
+
# health_server.stop
|
20
|
+
class HealthServer
|
21
|
+
SUCCESS_RESPONSE = [200, "Ready"].freeze
|
22
|
+
FAILURE_RESPONSE = [503, "Not Ready"].freeze
|
12
23
|
|
13
|
-
|
14
|
-
Thread.new { @health_server.start }
|
24
|
+
attr_reader :grpc_server, :port, :path, :server
|
15
25
|
|
16
|
-
|
17
|
-
|
26
|
+
def initialize(grpc_server, port:, path: "/health", logger: AnyCable.logger)
|
27
|
+
@grpc_server = grpc_server
|
28
|
+
@port = port
|
29
|
+
@path = path
|
30
|
+
@logger = logger
|
31
|
+
@server = build_server
|
32
|
+
end
|
18
33
|
|
19
|
-
|
20
|
-
|
21
|
-
@health_server.shutdown
|
22
|
-
end
|
34
|
+
def start
|
35
|
+
return if running?
|
23
36
|
|
24
|
-
|
25
|
-
|
26
|
-
|
37
|
+
Thread.new { server.start }
|
38
|
+
|
39
|
+
logger.info "HTTP health server is listening on localhost:#{port} and mounted at \"#{path}\""
|
40
|
+
end
|
41
|
+
|
42
|
+
def stop
|
43
|
+
return unless running?
|
44
|
+
|
45
|
+
server.shutdown
|
46
|
+
end
|
47
|
+
|
48
|
+
def running?
|
49
|
+
server.status == :Running
|
50
|
+
end
|
27
51
|
|
28
|
-
|
52
|
+
private
|
29
53
|
|
30
|
-
|
31
|
-
FAILURE_RESPONSE = [503, "Not Ready"].freeze
|
54
|
+
attr_reader :logger
|
32
55
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
end
|
56
|
+
def build_server
|
57
|
+
WEBrick::HTTPServer.new(
|
58
|
+
Port: port,
|
59
|
+
Logger: logger,
|
60
|
+
AccessLog: []
|
61
|
+
).tap do |server|
|
62
|
+
server.mount_proc path do |_, res|
|
63
|
+
res.status, res.body = grpc_server.running? ? SUCCESS_RESPONSE : FAILURE_RESPONSE
|
42
64
|
end
|
43
65
|
end
|
44
66
|
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "grpc"
|
4
|
+
|
5
|
+
module AnyCable
|
6
|
+
# Middleware is a wrapper over gRPC interceptors
|
7
|
+
# for request/response calls
|
8
|
+
class Middleware < GRPC::Interceptor
|
9
|
+
def request_response(request: nil, call: nil, method: nil)
|
10
|
+
call(request, call, method) do
|
11
|
+
yield
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def call(*)
|
16
|
+
raise NotImplementedError
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "anycable/middleware"
|
4
|
+
require "monitor"
|
5
|
+
|
6
|
+
module AnyCable
|
7
|
+
# Middleware chain is used to build the list of
|
8
|
+
# gRPC server interceptors.
|
9
|
+
#
|
10
|
+
# Each interceptor should be a subsclass of
|
11
|
+
# AnyCable::Middleware and implement `#call` method.
|
12
|
+
class MiddlewareChain
|
13
|
+
def initialize
|
14
|
+
@registry = []
|
15
|
+
@mu = Monitor.new
|
16
|
+
end
|
17
|
+
|
18
|
+
def use(middleware)
|
19
|
+
check_frozen!
|
20
|
+
middleware = build_middleware(middleware)
|
21
|
+
sync { registry << middleware }
|
22
|
+
end
|
23
|
+
|
24
|
+
def freeze
|
25
|
+
registry.freeze
|
26
|
+
super
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_a
|
30
|
+
registry
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
attr_reader :mu, :registry
|
36
|
+
|
37
|
+
def sync
|
38
|
+
mu.synchronize { yield }
|
39
|
+
end
|
40
|
+
|
41
|
+
def check_frozen!
|
42
|
+
raise "Cannot modify AnyCable middlewares after server started" if frozen?
|
43
|
+
end
|
44
|
+
|
45
|
+
def build_middleware(middleware)
|
46
|
+
middleware = middleware.new if
|
47
|
+
middleware.is_a?(Class) && middleware <= AnyCable::Middleware
|
48
|
+
|
49
|
+
unless middleware.is_a?(AnyCable::Middleware)
|
50
|
+
raise ArgumentError,
|
51
|
+
"AnyCable middleware must be a subclass of AnyCable::Middleware, " \
|
52
|
+
"got #{middleware} instead"
|
53
|
+
end
|
54
|
+
|
55
|
+
middleware
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
data/lib/anycable/rpc/rpc_pb.rb
CHANGED
@@ -45,7 +45,7 @@ Google::Protobuf::DescriptorPool.generated_pool.build do
|
|
45
45
|
end
|
46
46
|
end
|
47
47
|
|
48
|
-
module
|
48
|
+
module AnyCable
|
49
49
|
ConnectionRequest = Google::Protobuf::DescriptorPool.generated_pool.lookup("anycable.ConnectionRequest").msgclass
|
50
50
|
ConnectionResponse = Google::Protobuf::DescriptorPool.generated_pool.lookup("anycable.ConnectionResponse").msgclass
|
51
51
|
CommandMessage = Google::Protobuf::DescriptorPool.generated_pool.lookup("anycable.CommandMessage").msgclass
|
data/lib/anycable/rpc_handler.rb
CHANGED
@@ -1,17 +1,17 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
3
|
+
require "anycable/socket"
|
4
|
+
require "anycable/rpc/rpc_pb"
|
5
|
+
require "anycable/rpc/rpc_services_pb"
|
6
6
|
|
7
|
-
require
|
7
|
+
require "anycable/handler/capture_exceptions"
|
8
8
|
|
9
9
|
# rubocop:disable Metrics/AbcSize
|
10
10
|
# rubocop:disable Metrics/MethodLength
|
11
|
-
module
|
11
|
+
module AnyCable
|
12
12
|
# RPC service handler
|
13
|
-
class RPCHandler <
|
14
|
-
prepend Handler::
|
13
|
+
class RPCHandler < AnyCable::RPC::Service
|
14
|
+
prepend AnyCable::Handler::CaptureExceptions
|
15
15
|
|
16
16
|
# Handle connection request from WebSocket server
|
17
17
|
def connect(request, _unused_call)
|
@@ -24,10 +24,10 @@ module Anycable
|
|
24
24
|
connection.handle_open
|
25
25
|
|
26
26
|
if socket.closed?
|
27
|
-
|
27
|
+
AnyCable::ConnectionResponse.new(status: AnyCable::Status::FAILURE)
|
28
28
|
else
|
29
|
-
|
30
|
-
status:
|
29
|
+
AnyCable::ConnectionResponse.new(
|
30
|
+
status: AnyCable::Status::SUCCESS,
|
31
31
|
identifiers: connection.identifiers_json,
|
32
32
|
transmissions: socket.transmissions
|
33
33
|
)
|
@@ -46,9 +46,9 @@ module Anycable
|
|
46
46
|
)
|
47
47
|
|
48
48
|
if connection.handle_close
|
49
|
-
|
49
|
+
AnyCable::DisconnectResponse.new(status: AnyCable::Status::SUCCESS)
|
50
50
|
else
|
51
|
-
|
51
|
+
AnyCable::DisconnectResponse.new(status: AnyCable::Status::FAILURE)
|
52
52
|
end
|
53
53
|
end
|
54
54
|
|
@@ -68,8 +68,8 @@ module Anycable
|
|
68
68
|
message.data
|
69
69
|
)
|
70
70
|
|
71
|
-
|
72
|
-
status: result ?
|
71
|
+
AnyCable::CommandResponse.new(
|
72
|
+
status: result ? AnyCable::Status::SUCCESS : AnyCable::Status::FAILURE,
|
73
73
|
disconnect: socket.closed?,
|
74
74
|
stop_streams: socket.stop_streams?,
|
75
75
|
streams: socket.streams,
|
@@ -83,36 +83,38 @@ module Anycable
|
|
83
83
|
def rack_env(request)
|
84
84
|
uri = URI.parse(request.path)
|
85
85
|
{
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
86
|
+
"QUERY_STRING" => uri.query,
|
87
|
+
"SCRIPT_NAME" => "",
|
88
|
+
"PATH_INFO" => uri.path,
|
89
|
+
"SERVER_PORT" => uri.port.to_s,
|
90
|
+
"HTTP_HOST" => uri.host,
|
91
91
|
# Hack to avoid Missing rack.input error
|
92
|
-
|
93
|
-
|
94
|
-
|
92
|
+
"rack.request.form_input" => "",
|
93
|
+
"rack.input" => "",
|
94
|
+
"rack.request.form_hash" => {}
|
95
95
|
}.merge(build_headers(request.headers))
|
96
96
|
end
|
97
97
|
|
98
98
|
def build_socket(**options)
|
99
|
-
|
99
|
+
AnyCable::Socket.new(options)
|
100
100
|
end
|
101
101
|
|
102
102
|
def build_headers(headers)
|
103
103
|
headers.each_with_object({}) do |(k, v), obj|
|
104
104
|
k = k.upcase
|
105
|
-
k.tr!(
|
105
|
+
k.tr!("-", "_")
|
106
106
|
obj["HTTP_#{k}"] = v
|
107
107
|
end
|
108
108
|
end
|
109
109
|
|
110
110
|
def logger
|
111
|
-
|
111
|
+
AnyCable.logger
|
112
112
|
end
|
113
113
|
|
114
114
|
def factory
|
115
|
-
|
115
|
+
AnyCable.connection_factory
|
116
116
|
end
|
117
117
|
end
|
118
118
|
end
|
119
|
+
# rubocop:enable Metrics/AbcSize
|
120
|
+
# rubocop:enable Metrics/MethodLength
|
data/lib/anycable/server.rb
CHANGED
@@ -1,59 +1,134 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
3
|
+
require "grpc"
|
4
|
+
require "grpc/health/checker"
|
5
|
+
require "grpc/health/v1/health_services_pb"
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
|
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 [::]:50051 (default host)
|
16
|
+
# server = AnyCable::Server.new(host: "[::]:50051")
|
17
|
+
#
|
18
|
+
# # run gRPC server in bakground
|
19
|
+
# server.start
|
20
|
+
#
|
21
|
+
# # stop server
|
22
|
+
# server.stop
|
23
|
+
class Server
|
10
24
|
class << self
|
11
|
-
|
25
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
26
|
+
def start(**options)
|
27
|
+
warn <<~DEPRECATION
|
28
|
+
Using AnyCable::Server.start is deprecated!
|
29
|
+
Please, use anycable CLI instead.
|
12
30
|
|
13
|
-
|
14
|
-
|
31
|
+
See https://docs.anycable.io/#upgrade_to_0_6_0
|
32
|
+
DEPRECATION
|
15
33
|
|
16
|
-
|
17
|
-
|
18
|
-
|
34
|
+
server = new(
|
35
|
+
host: AnyCable.config.rpc_host,
|
36
|
+
**AnyCable.config.to_grpc_params,
|
37
|
+
interceptors: AnyCable.middleware.to_a,
|
38
|
+
**options
|
39
|
+
)
|
19
40
|
|
20
|
-
|
21
|
-
return unless running?
|
22
|
-
@grpc_server.stop
|
23
|
-
end
|
41
|
+
AnyCable.middleware.freeze
|
24
42
|
|
25
|
-
|
26
|
-
|
27
|
-
|
43
|
+
if AnyCable.config.http_health_port_provided?
|
44
|
+
health_server = AnyCable::HealthServer.new(
|
45
|
+
server,
|
46
|
+
**AnyCable.config.to_http_health_params
|
47
|
+
)
|
48
|
+
health_server.start
|
49
|
+
end
|
28
50
|
|
29
|
-
|
30
|
-
|
31
|
-
|
51
|
+
at_exit do
|
52
|
+
server.stop
|
53
|
+
health_server&.stop
|
54
|
+
end
|
55
|
+
|
56
|
+
AnyCable.logger.info "Broadcasting Redis channel: #{AnyCable.config.redis_channel}"
|
57
|
+
|
58
|
+
server.start
|
59
|
+
server.wait_till_terminated
|
32
60
|
end
|
61
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
62
|
+
end
|
33
63
|
|
34
|
-
|
64
|
+
DEFAULT_HOST = "0.0.0.0:50051"
|
35
65
|
|
36
|
-
|
37
|
-
@grpc_server ||= build_server
|
66
|
+
attr_reader :grpc_server, :host
|
38
67
|
|
39
|
-
|
40
|
-
|
68
|
+
def initialize(host: DEFAULT_HOST, logger: AnyCable.logger, **options)
|
69
|
+
@logger = logger
|
70
|
+
@host = host
|
71
|
+
@grpc_server = build_server(options)
|
72
|
+
end
|
41
73
|
|
42
|
-
|
43
|
-
|
74
|
+
# Start gRPC server in background and
|
75
|
+
# wait untill it ready to accept connections
|
76
|
+
def start
|
77
|
+
return if running?
|
44
78
|
|
45
|
-
|
46
|
-
GRPC::RpcServer.new.tap do |server|
|
47
|
-
server.add_http2_port(Anycable.config.rpc_host, :this_port_is_insecure)
|
48
|
-
server.handle(Anycable::RPCHandler)
|
49
|
-
end
|
50
|
-
end
|
79
|
+
raise "Cannot re-start stopped server" if stopped?
|
51
80
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
81
|
+
logger.info "RPC server is starting..."
|
82
|
+
|
83
|
+
@start_thread = Thread.new { grpc_server.run }
|
84
|
+
|
85
|
+
grpc_server.wait_till_running
|
86
|
+
|
87
|
+
logger.info "RPC server is listening on #{host}"
|
88
|
+
end
|
89
|
+
|
90
|
+
def wait_till_terminated
|
91
|
+
raise "Server is not running" unless running?
|
92
|
+
|
93
|
+
start_thread.join
|
94
|
+
end
|
95
|
+
|
96
|
+
# Stop gRPC server if it's running
|
97
|
+
def stop
|
98
|
+
return unless running?
|
99
|
+
|
100
|
+
grpc_server.stop
|
101
|
+
|
102
|
+
logger.info "RPC server stopped"
|
103
|
+
end
|
104
|
+
|
105
|
+
def running?
|
106
|
+
grpc_server.running_state == :running
|
107
|
+
end
|
108
|
+
|
109
|
+
def stopped?
|
110
|
+
grpc_server.running_state == :stopped
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
|
115
|
+
attr_reader :logger, :start_thread
|
116
|
+
|
117
|
+
def build_server(options)
|
118
|
+
GRPC::RpcServer.new(options).tap do |server|
|
119
|
+
server.add_http2_port(host, :this_port_is_insecure)
|
120
|
+
server.handle(AnyCable::RPCHandler)
|
121
|
+
server.handle(build_health_checker)
|
56
122
|
end
|
57
123
|
end
|
124
|
+
|
125
|
+
def build_health_checker
|
126
|
+
health_checker = Grpc::Health::Checker.new
|
127
|
+
health_checker.add_status(
|
128
|
+
"anycable.RPC",
|
129
|
+
Grpc::Health::V1::HealthCheckResponse::ServingStatus::SERVING
|
130
|
+
)
|
131
|
+
health_checker
|
132
|
+
end
|
58
133
|
end
|
59
134
|
end
|