anycable-core 1.1.0.pre1
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 +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
|