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.
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