anycable-core 1.1.0.pre1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +84 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +78 -0
  5. data/bin/anycable +13 -0
  6. data/bin/anycabled +30 -0
  7. data/bin/console +7 -0
  8. data/bin/setup +6 -0
  9. data/lib/anycable.rb +114 -0
  10. data/lib/anycable/broadcast_adapters.rb +34 -0
  11. data/lib/anycable/broadcast_adapters/base.rb +29 -0
  12. data/lib/anycable/broadcast_adapters/http.rb +131 -0
  13. data/lib/anycable/broadcast_adapters/redis.rb +46 -0
  14. data/lib/anycable/cli.rb +319 -0
  15. data/lib/anycable/config.rb +127 -0
  16. data/lib/anycable/exceptions_handling.rb +35 -0
  17. data/lib/anycable/grpc.rb +30 -0
  18. data/lib/anycable/grpc/check_version.rb +33 -0
  19. data/lib/anycable/grpc/config.rb +53 -0
  20. data/lib/anycable/grpc/handler.rb +25 -0
  21. data/lib/anycable/grpc/rpc_services_pb.rb +24 -0
  22. data/lib/anycable/grpc/server.rb +103 -0
  23. data/lib/anycable/health_server.rb +73 -0
  24. data/lib/anycable/middleware.rb +10 -0
  25. data/lib/anycable/middleware_chain.rb +74 -0
  26. data/lib/anycable/middlewares/exceptions.rb +35 -0
  27. data/lib/anycable/protos/rpc_pb.rb +74 -0
  28. data/lib/anycable/rpc.rb +91 -0
  29. data/lib/anycable/rpc/handler.rb +50 -0
  30. data/lib/anycable/rpc/handlers/command.rb +36 -0
  31. data/lib/anycable/rpc/handlers/connect.rb +33 -0
  32. data/lib/anycable/rpc/handlers/disconnect.rb +27 -0
  33. data/lib/anycable/rspec.rb +4 -0
  34. data/lib/anycable/rspec/rpc_command_context.rb +21 -0
  35. data/lib/anycable/socket.rb +169 -0
  36. data/lib/anycable/version.rb +5 -0
  37. data/sig/anycable.rbs +37 -0
  38. data/sig/anycable/broadcast_adapters.rbs +5 -0
  39. data/sig/anycable/cli.rbs +40 -0
  40. data/sig/anycable/config.rbs +46 -0
  41. data/sig/anycable/exceptions_handling.rbs +14 -0
  42. data/sig/anycable/health_server.rbs +21 -0
  43. data/sig/anycable/middleware.rbs +5 -0
  44. data/sig/anycable/middleware_chain.rbs +22 -0
  45. data/sig/anycable/rpc.rbs +143 -0
  46. data/sig/anycable/socket.rbs +40 -0
  47. data/sig/anycable/version.rbs +3 -0
  48. 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,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnyCable
4
+ # Middleware is an analague of Rack middlewares but for AnyCable RPC calls
5
+ class Middleware
6
+ def call(_method_name, _request)
7
+ raise NotImplementedError
8
+ end
9
+ end
10
+ 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