anycable-core 1.1.0.pre1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +84 -0
- data/MIT-LICENSE +20 -0
- data/README.md +78 -0
- data/bin/anycable +13 -0
- data/bin/anycabled +30 -0
- data/bin/console +7 -0
- data/bin/setup +6 -0
- data/lib/anycable.rb +114 -0
- data/lib/anycable/broadcast_adapters.rb +34 -0
- data/lib/anycable/broadcast_adapters/base.rb +29 -0
- data/lib/anycable/broadcast_adapters/http.rb +131 -0
- data/lib/anycable/broadcast_adapters/redis.rb +46 -0
- data/lib/anycable/cli.rb +319 -0
- data/lib/anycable/config.rb +127 -0
- data/lib/anycable/exceptions_handling.rb +35 -0
- data/lib/anycable/grpc.rb +30 -0
- data/lib/anycable/grpc/check_version.rb +33 -0
- data/lib/anycable/grpc/config.rb +53 -0
- data/lib/anycable/grpc/handler.rb +25 -0
- data/lib/anycable/grpc/rpc_services_pb.rb +24 -0
- data/lib/anycable/grpc/server.rb +103 -0
- data/lib/anycable/health_server.rb +73 -0
- data/lib/anycable/middleware.rb +10 -0
- data/lib/anycable/middleware_chain.rb +74 -0
- data/lib/anycable/middlewares/exceptions.rb +35 -0
- data/lib/anycable/protos/rpc_pb.rb +74 -0
- data/lib/anycable/rpc.rb +91 -0
- data/lib/anycable/rpc/handler.rb +50 -0
- data/lib/anycable/rpc/handlers/command.rb +36 -0
- data/lib/anycable/rpc/handlers/connect.rb +33 -0
- data/lib/anycable/rpc/handlers/disconnect.rb +27 -0
- data/lib/anycable/rspec.rb +4 -0
- data/lib/anycable/rspec/rpc_command_context.rb +21 -0
- data/lib/anycable/socket.rb +169 -0
- data/lib/anycable/version.rb +5 -0
- data/sig/anycable.rbs +37 -0
- data/sig/anycable/broadcast_adapters.rbs +5 -0
- data/sig/anycable/cli.rbs +40 -0
- data/sig/anycable/config.rbs +46 -0
- data/sig/anycable/exceptions_handling.rbs +14 -0
- data/sig/anycable/health_server.rbs +21 -0
- data/sig/anycable/middleware.rbs +5 -0
- data/sig/anycable/middleware_chain.rbs +22 -0
- data/sig/anycable/rpc.rbs +143 -0
- data/sig/anycable/socket.rbs +40 -0
- data/sig/anycable/version.rbs +3 -0
- metadata +237 -0
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AnyCable
|
4
|
+
module ExceptionsHandling # :nodoc:
|
5
|
+
class << self
|
6
|
+
def add_handler(block)
|
7
|
+
handlers << procify(block)
|
8
|
+
end
|
9
|
+
|
10
|
+
alias_method :<<, :add_handler
|
11
|
+
|
12
|
+
def notify(exp, method_name, message)
|
13
|
+
handlers.each do |handler|
|
14
|
+
handler.call(exp, method_name, message)
|
15
|
+
rescue => exp
|
16
|
+
AnyCable.logger.error "!!! EXCEPTION HANDLER THREW AN ERROR !!!"
|
17
|
+
AnyCable.logger.error exp
|
18
|
+
AnyCable.logger.error exp.backtrace.join("\n") unless exp.backtrace.nil?
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def procify(block)
|
25
|
+
return block unless block.lambda?
|
26
|
+
|
27
|
+
proc { |*args| block.call(*args.take(block.arity)) }
|
28
|
+
end
|
29
|
+
|
30
|
+
def handlers
|
31
|
+
@handlers ||= []
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "grpc"
|
4
|
+
|
5
|
+
module AnyCable
|
6
|
+
module GRPC
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
require "anycable/grpc/config"
|
11
|
+
require "anycable/grpc/server"
|
12
|
+
require "anycable/grpc/check_version"
|
13
|
+
|
14
|
+
AnyCable.server_builder = ->(config) {
|
15
|
+
AnyCable.logger.info "gRPC version: #{::GRPC::VERSION}"
|
16
|
+
|
17
|
+
::GRPC.define_singleton_method(:logger) { AnyCable.logger } if config.log_grpc?
|
18
|
+
|
19
|
+
interceptors = []
|
20
|
+
|
21
|
+
if config.version_check_enabled?
|
22
|
+
interceptors << AnyCable::GRPC::CheckVersion.new(AnyCable::PROTO_VERSION)
|
23
|
+
end
|
24
|
+
|
25
|
+
params = config.to_grpc_params
|
26
|
+
params[:host] = config.rpc_host
|
27
|
+
params[:interceptors] = interceptors
|
28
|
+
|
29
|
+
AnyCable::GRPC::Server.new(**params)
|
30
|
+
}
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AnyCable
|
4
|
+
module GRPC
|
5
|
+
# Checks that gRPC client version is compatible with
|
6
|
+
# the current RPC proto version
|
7
|
+
class CheckVersion < ::GRPC::ServerInterceptor
|
8
|
+
attr_reader :version
|
9
|
+
|
10
|
+
def initialize(version)
|
11
|
+
@version = version
|
12
|
+
end
|
13
|
+
|
14
|
+
def request_response(request: nil, call: nil, method: nil)
|
15
|
+
# Call only for AnyCable service
|
16
|
+
return yield unless method.receiver.is_a?(AnyCable::GRPC::Handler)
|
17
|
+
|
18
|
+
check_version(call) do
|
19
|
+
yield
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def check_version(call)
|
24
|
+
supported_versions = call.metadata["protov"]&.split(",")
|
25
|
+
return yield if supported_versions&.include?(version)
|
26
|
+
|
27
|
+
raise ::GRPC::Internal,
|
28
|
+
"Incompatible AnyCable RPC client.\nCurrent server version: #{version}.\n" \
|
29
|
+
"Client supported versions: #{call.metadata["protov"] || "unknown"}."
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
AnyCable::Config.attr_config(
|
4
|
+
### gRPC options
|
5
|
+
rpc_host: "127.0.0.1:50051",
|
6
|
+
# For defaults see https://github.com/grpc/grpc/blob/51f0d35509bcdaba572d422c4f856208162022de/src/ruby/lib/grpc/generic/rpc_server.rb#L186-L216
|
7
|
+
rpc_pool_size: ::GRPC::RpcServer::DEFAULT_POOL_SIZE,
|
8
|
+
rpc_max_waiting_requests: ::GRPC::RpcServer::DEFAULT_MAX_WAITING_REQUESTS,
|
9
|
+
rpc_poll_period: ::GRPC::RpcServer::DEFAULT_POLL_PERIOD,
|
10
|
+
rpc_pool_keep_alive: ::GRPC::Pool::DEFAULT_KEEP_ALIVE,
|
11
|
+
# See https://github.com/grpc/grpc/blob/f526602bff029b8db50a8d57134d72da33d8a752/include/grpc/impl/codegen/grpc_types.h#L292-L315
|
12
|
+
rpc_server_args: {},
|
13
|
+
log_grpc: false
|
14
|
+
)
|
15
|
+
|
16
|
+
AnyCable::Config.ignore_options :rpc_server_args
|
17
|
+
AnyCable::Config.flag_options :log_grpc
|
18
|
+
|
19
|
+
module AnyCable
|
20
|
+
module GRPC
|
21
|
+
module Config
|
22
|
+
def log_grpc
|
23
|
+
debug? || super
|
24
|
+
end
|
25
|
+
|
26
|
+
# Add alias explicitly, 'cause previous alias refers to the original log_grpc method
|
27
|
+
alias_method :log_grpc?, :log_grpc
|
28
|
+
|
29
|
+
# Build gRPC server parameters
|
30
|
+
def to_grpc_params
|
31
|
+
{
|
32
|
+
pool_size: rpc_pool_size,
|
33
|
+
max_waiting_requests: rpc_max_waiting_requests,
|
34
|
+
poll_period: rpc_poll_period,
|
35
|
+
pool_keep_alive: rpc_pool_keep_alive,
|
36
|
+
server_args: rpc_server_args
|
37
|
+
}
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
AnyCable::Config.prepend AnyCable::GRPC::Config
|
44
|
+
|
45
|
+
AnyCable::Config.usage <<~TXT
|
46
|
+
GRPC OPTIONS
|
47
|
+
--rpc-host=host Local address to run gRPC server on, default: "127.0.0.1:50051"
|
48
|
+
--rpc-pool-size=size gRPC workers pool size, default: 30
|
49
|
+
--rpc-max-waiting-requests=num Max waiting requests queue size, default: 20
|
50
|
+
--rpc-poll-period=seconds Poll period (sec), default: 1
|
51
|
+
--rpc-pool-keep-alive=seconds Keep-alive polling interval (sec), default: 1
|
52
|
+
--log-grpc Enable gRPC logging (disabled by default)
|
53
|
+
TXT
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "anycable/socket"
|
4
|
+
require "anycable/rpc"
|
5
|
+
require "anycable/grpc/rpc_services_pb"
|
6
|
+
|
7
|
+
module AnyCable
|
8
|
+
module GRPC
|
9
|
+
# RPC service handler
|
10
|
+
class Handler < AnyCable::GRPC::Service
|
11
|
+
# Handle connection request from WebSocket server
|
12
|
+
def connect(request, _unused_call)
|
13
|
+
AnyCable.rpc_handler.handle(:connect, request)
|
14
|
+
end
|
15
|
+
|
16
|
+
def disconnect(request, _unused_call)
|
17
|
+
AnyCable.rpc_handler.handle(:disconnect, request)
|
18
|
+
end
|
19
|
+
|
20
|
+
def command(request, _unused_call)
|
21
|
+
AnyCable.rpc_handler.handle(:command, request)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,24 @@
|
|
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 GRPC
|
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, ::AnyCable::ConnectionRequest, ::AnyCable::ConnectionResponse
|
18
|
+
rpc :Command, ::AnyCable::CommandMessage, ::AnyCable::CommandResponse
|
19
|
+
rpc :Disconnect, ::AnyCable::DisconnectRequest, ::AnyCable::DisconnectResponse
|
20
|
+
end
|
21
|
+
|
22
|
+
Stub = Service.rpc_stub_class
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,103 @@
|
|
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/grpc/handler"
|
8
|
+
|
9
|
+
module AnyCable
|
10
|
+
module GRPC
|
11
|
+
using(Module.new do
|
12
|
+
refine ::GRPC::RpcServer do
|
13
|
+
attr_reader :pool_size
|
14
|
+
end
|
15
|
+
end)
|
16
|
+
|
17
|
+
# Wrapper over gRPC server.
|
18
|
+
#
|
19
|
+
# Basic example:
|
20
|
+
#
|
21
|
+
# # create new server listening on the loopback interface with 50051 port
|
22
|
+
# server = AnyCable::GRPC::Server.new(host: "127.0.0.1:50051")
|
23
|
+
#
|
24
|
+
# # run gRPC server in bakground
|
25
|
+
# server.start
|
26
|
+
#
|
27
|
+
# # stop server
|
28
|
+
# server.stop
|
29
|
+
class Server
|
30
|
+
attr_reader :grpc_server, :host
|
31
|
+
|
32
|
+
def initialize(host:, logger: nil, **options)
|
33
|
+
@logger = logger
|
34
|
+
@host = host
|
35
|
+
@grpc_server = build_server(**options)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Start gRPC server in background and
|
39
|
+
# wait untill it ready to accept connections
|
40
|
+
def start
|
41
|
+
return if running?
|
42
|
+
|
43
|
+
raise "Cannot re-start stopped server" if stopped?
|
44
|
+
|
45
|
+
logger.info "RPC server is starting..."
|
46
|
+
|
47
|
+
@start_thread = Thread.new { grpc_server.run }
|
48
|
+
|
49
|
+
grpc_server.wait_till_running
|
50
|
+
|
51
|
+
logger.info "RPC server is listening on #{host} (workers_num: #{grpc_server.pool_size})"
|
52
|
+
end
|
53
|
+
|
54
|
+
def wait_till_terminated
|
55
|
+
raise "Server is not running" unless running?
|
56
|
+
|
57
|
+
start_thread.join
|
58
|
+
end
|
59
|
+
|
60
|
+
# Stop gRPC server if it's running
|
61
|
+
def stop
|
62
|
+
return unless running?
|
63
|
+
|
64
|
+
grpc_server.stop
|
65
|
+
|
66
|
+
logger.info "RPC server stopped"
|
67
|
+
end
|
68
|
+
|
69
|
+
def running?
|
70
|
+
grpc_server.running_state == :running
|
71
|
+
end
|
72
|
+
|
73
|
+
def stopped?
|
74
|
+
grpc_server.running_state == :stopped
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
attr_reader :start_thread
|
80
|
+
|
81
|
+
def logger
|
82
|
+
@logger ||= AnyCable.logger
|
83
|
+
end
|
84
|
+
|
85
|
+
def build_server(**options)
|
86
|
+
::GRPC::RpcServer.new(**options).tap do |server|
|
87
|
+
server.add_http2_port(host, :this_port_is_insecure)
|
88
|
+
server.handle(AnyCable::GRPC::Handler)
|
89
|
+
server.handle(build_health_checker)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def build_health_checker
|
94
|
+
health_checker = Grpc::Health::Checker.new
|
95
|
+
health_checker.add_status(
|
96
|
+
"anycable.RPC",
|
97
|
+
Grpc::Health::V1::HealthCheckResponse::ServingStatus::SERVING
|
98
|
+
)
|
99
|
+
health_checker
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AnyCable
|
4
|
+
# Server for HTTP healthchecks.
|
5
|
+
#
|
6
|
+
# Basic usage:
|
7
|
+
#
|
8
|
+
# # create a new healthcheck server for a specified
|
9
|
+
# # server listening on the port
|
10
|
+
# health_server = AnyCable::HealthServer.new(server, port)
|
11
|
+
#
|
12
|
+
# # start health server in background
|
13
|
+
# health_server.start
|
14
|
+
#
|
15
|
+
# # stop health server
|
16
|
+
# health_server.stop
|
17
|
+
class HealthServer
|
18
|
+
SUCCESS_RESPONSE = [200, "Ready"].freeze
|
19
|
+
FAILURE_RESPONSE = [503, "Not Ready"].freeze
|
20
|
+
|
21
|
+
attr_reader :server, :port, :path, :http_server
|
22
|
+
|
23
|
+
def initialize(server, port:, logger: nil, path: "/health")
|
24
|
+
@server = server
|
25
|
+
@port = port
|
26
|
+
@path = path
|
27
|
+
@logger = logger
|
28
|
+
@http_server = build_server
|
29
|
+
end
|
30
|
+
|
31
|
+
def start
|
32
|
+
return if running?
|
33
|
+
|
34
|
+
Thread.new { http_server.start }
|
35
|
+
|
36
|
+
logger.info "HTTP health server is listening on localhost:#{port} and mounted at \"#{path}\""
|
37
|
+
end
|
38
|
+
|
39
|
+
def stop
|
40
|
+
return unless running?
|
41
|
+
|
42
|
+
http_server.shutdown
|
43
|
+
end
|
44
|
+
|
45
|
+
def running?
|
46
|
+
http_server.status == :Running
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def logger
|
52
|
+
@logger ||= AnyCable.logger
|
53
|
+
end
|
54
|
+
|
55
|
+
def build_server
|
56
|
+
begin
|
57
|
+
require "webrick"
|
58
|
+
rescue LoadError
|
59
|
+
raise "Please, install webrick gem to use health server"
|
60
|
+
end
|
61
|
+
|
62
|
+
WEBrick::HTTPServer.new(
|
63
|
+
Port: port,
|
64
|
+
Logger: logger,
|
65
|
+
AccessLog: []
|
66
|
+
).tap do |http_server|
|
67
|
+
http_server.mount_proc path do |_, res|
|
68
|
+
res.status, res.body = server.running? ? SUCCESS_RESPONSE : FAILURE_RESPONSE
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,74 @@
|
|
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
|
+
def call(method_name, request, &block)
|
34
|
+
return yield(method_name, request) if registry.none?
|
35
|
+
|
36
|
+
execute_next_middleware(0, method_name, request, block)
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def execute_next_middleware(ind, method_name, request, block)
|
42
|
+
return block.call(method_name, request) if ind >= registry.size
|
43
|
+
|
44
|
+
registry[ind].call(method_name, request) do
|
45
|
+
execute_next_middleware(ind + 1, method_name, request, block)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
attr_reader :mu, :registry
|
52
|
+
|
53
|
+
def sync
|
54
|
+
mu.synchronize { yield }
|
55
|
+
end
|
56
|
+
|
57
|
+
def check_frozen!
|
58
|
+
raise "Cannot modify AnyCable middlewares after server started" if frozen?
|
59
|
+
end
|
60
|
+
|
61
|
+
def build_middleware(middleware)
|
62
|
+
middleware = middleware.new if
|
63
|
+
middleware.is_a?(Class) && middleware <= AnyCable::Middleware
|
64
|
+
|
65
|
+
unless middleware.is_a?(AnyCable::Middleware)
|
66
|
+
raise ArgumentError,
|
67
|
+
"AnyCable middleware must be a subclass of AnyCable::Middleware, " \
|
68
|
+
"got #{middleware} instead"
|
69
|
+
end
|
70
|
+
|
71
|
+
middleware
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|