anycable 0.5.2 → 0.6.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +166 -2
  3. data/MIT-LICENSE +1 -1
  4. data/README.md +19 -60
  5. data/bin/anycable +13 -0
  6. data/bin/anycabled +30 -0
  7. data/lib/anycable.rb +69 -18
  8. data/lib/anycable/broadcast_adapters.rb +33 -0
  9. data/lib/anycable/broadcast_adapters/redis.rb +42 -0
  10. data/lib/anycable/cli.rb +329 -0
  11. data/lib/anycable/config.rb +93 -17
  12. data/lib/anycable/exceptions_handling.rb +37 -0
  13. data/lib/anycable/health_server.rb +52 -31
  14. data/lib/anycable/middleware.rb +19 -0
  15. data/lib/anycable/middleware_chain.rb +58 -0
  16. data/lib/anycable/rpc/rpc_pb.rb +1 -1
  17. data/lib/anycable/rpc/rpc_services_pb.rb +1 -1
  18. data/lib/anycable/rpc_handler.rb +77 -32
  19. data/lib/anycable/server.rb +132 -39
  20. data/lib/anycable/socket.rb +1 -1
  21. data/lib/anycable/version.rb +2 -2
  22. metadata +34 -59
  23. data/.gitignore +0 -40
  24. data/.hound.yml +0 -3
  25. data/.rubocop.yml +0 -71
  26. data/.travis.yml +0 -13
  27. data/Gemfile +0 -8
  28. data/Makefile +0 -5
  29. data/PITCHME.md +0 -139
  30. data/PITCHME.yaml +0 -1
  31. data/Rakefile +0 -8
  32. data/anycable.gemspec +0 -32
  33. data/assets/Memory3.png +0 -0
  34. data/assets/Memory5.png +0 -0
  35. data/assets/RTT3.png +0 -0
  36. data/assets/RTT5.png +0 -0
  37. data/assets/Scheme1.png +0 -0
  38. data/assets/Scheme2.png +0 -0
  39. data/assets/cpu_chart.gif +0 -0
  40. data/assets/cpu_chart2.gif +0 -0
  41. data/assets/evlms.png +0 -0
  42. data/benchmarks/.gitignore +0 -1
  43. data/benchmarks/2017-02-12.md +0 -308
  44. data/benchmarks/2018-03-04.md +0 -192
  45. data/benchmarks/2018-05-27-rpc-bench.md +0 -57
  46. data/benchmarks/HowTo.md +0 -23
  47. data/benchmarks/ansible.cfg +0 -9
  48. data/benchmarks/benchmark.yml +0 -67
  49. data/benchmarks/hosts +0 -5
  50. data/benchmarks/servers.yml +0 -36
  51. data/circle.yml +0 -8
  52. data/etc/bug_report_template.rb +0 -76
  53. data/lib/anycable/handler/exceptions_handling.rb +0 -43
  54. data/lib/anycable/pubsub.rb +0 -26
  55. data/protos/rpc.proto +0 -55
@@ -0,0 +1,37 @@
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 << add_handler
11
+
12
+ def notify(exp, method_name, message)
13
+ handlers.each do |handler|
14
+ begin
15
+ handler.call(exp, method_name, message)
16
+ rescue StandardError => exp
17
+ AnyCable.logger.error "!!! EXCEPTION HANDLER THREW AN ERROR !!!"
18
+ AnyCable.logger.error exp
19
+ AnyCable.logger.error exp.backtrace.join("\n") unless exp.backtrace.nil?
20
+ end
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def procify(block)
27
+ return block unless block.lambda?
28
+
29
+ proc { |*args| block.call(*args.take(block.arity)) }
30
+ end
31
+
32
+ def handlers
33
+ @handlers ||= []
34
+ end
35
+ end
36
+ end
37
+ end
@@ -1,44 +1,65 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'webrick'
4
- require 'anycable/server'
3
+ module AnyCable
4
+ # Server for HTTP healthchecks.
5
+ #
6
+ # Basic usage:
7
+ #
8
+ # # create a new healthcheck server for a specified
9
+ # # gRPC server lisening on the port
10
+ # health_server = AnyCable::HealthServer.new(grpc_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
5
20
 
6
- module Anycable
7
- # Server for HTTP healthchecks
8
- module HealthServer
9
- class << self
10
- def start(port)
11
- return if running?
21
+ attr_reader :grpc_server, :port, :path, :server
12
22
 
13
- @health_server ||= build_server(port)
14
- Thread.new { @health_server.start }
23
+ def initialize(grpc_server, port:, path: "/health", logger: AnyCable.logger)
24
+ @grpc_server = grpc_server
25
+ @port = port
26
+ @path = path
27
+ @logger = logger
28
+ @server = build_server
29
+ end
15
30
 
16
- Anycable.logger.info "HTTP health server is listening on #{port}"
17
- end
31
+ def start
32
+ return if running?
18
33
 
19
- def stop
20
- return unless running?
21
- @health_server.shutdown
22
- end
34
+ Thread.new { server.start }
23
35
 
24
- def running?
25
- @health_server&.status == :Running
26
- end
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
+ server.shutdown
43
+ end
44
+
45
+ def running?
46
+ server.status == :Running
47
+ end
48
+
49
+ private
27
50
 
28
- private
51
+ attr_reader :logger
29
52
 
30
- SUCCESS_RESPONSE = [200, "Ready"].freeze
31
- FAILURE_RESPONSE = [503, "Not Ready"].freeze
53
+ def build_server
54
+ require "webrick"
32
55
 
33
- def build_server(port)
34
- WEBrick::HTTPServer.new(
35
- Port: port,
36
- Logger: Anycable.logger,
37
- AccessLog: []
38
- ).tap do |server|
39
- server.mount_proc '/health' do |_, res|
40
- res.status, res.body = Anycable::Server.running? ? SUCCESS_RESPONSE : FAILURE_RESPONSE
41
- end
56
+ WEBrick::HTTPServer.new(
57
+ Port: port,
58
+ Logger: logger,
59
+ AccessLog: []
60
+ ).tap do |server|
61
+ server.mount_proc path do |_, res|
62
+ res.status, res.body = grpc_server.running? ? SUCCESS_RESPONSE : FAILURE_RESPONSE
42
63
  end
43
64
  end
44
65
  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
@@ -45,7 +45,7 @@ Google::Protobuf::DescriptorPool.generated_pool.build do
45
45
  end
46
46
  end
47
47
 
48
- module Anycable
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
@@ -3,7 +3,7 @@
3
3
 
4
4
  require 'grpc'
5
5
 
6
- module Anycable
6
+ module AnyCable
7
7
  module RPC
8
8
  class Service
9
9
 
@@ -1,18 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'anycable/socket'
4
- require 'anycable/rpc/rpc_pb'
5
- require 'anycable/rpc/rpc_services_pb'
6
-
7
- require 'anycable/handler/exceptions_handling'
3
+ require "anycable/socket"
4
+ require "anycable/rpc/rpc_pb"
5
+ require "anycable/rpc/rpc_services_pb"
8
6
 
9
7
  # rubocop:disable Metrics/AbcSize
10
8
  # rubocop:disable Metrics/MethodLength
11
- module Anycable
9
+ # rubocop:disable Metrics/ClassLength
10
+ module AnyCable
12
11
  # RPC service handler
13
- class RPCHandler < Anycable::RPC::Service
14
- prepend Handler::ExceptionsHandling
15
-
12
+ class RPCHandler < AnyCable::RPC::Service
16
13
  # Handle connection request from WebSocket server
17
14
  def connect(request, _unused_call)
18
15
  logger.debug("RPC Connect: #{request.inspect}")
@@ -24,14 +21,21 @@ module Anycable
24
21
  connection.handle_open
25
22
 
26
23
  if socket.closed?
27
- Anycable::ConnectionResponse.new(status: Anycable::Status::FAILURE)
24
+ AnyCable::ConnectionResponse.new(status: AnyCable::Status::FAILURE)
28
25
  else
29
- Anycable::ConnectionResponse.new(
30
- status: Anycable::Status::SUCCESS,
26
+ AnyCable::ConnectionResponse.new(
27
+ status: AnyCable::Status::SUCCESS,
31
28
  identifiers: connection.identifiers_json,
32
29
  transmissions: socket.transmissions
33
30
  )
34
31
  end
32
+ rescue StandardError => exp
33
+ notify_exception(exp, :connect, request)
34
+
35
+ AnyCable::ConnectionResponse.new(
36
+ status: AnyCable::Status::ERROR,
37
+ error_msg: exp.message
38
+ )
35
39
  end
36
40
 
37
41
  def disconnect(request, _unused_call)
@@ -46,16 +50,25 @@ module Anycable
46
50
  )
47
51
 
48
52
  if connection.handle_close
49
- Anycable::DisconnectResponse.new(status: Anycable::Status::SUCCESS)
53
+ AnyCable::DisconnectResponse.new(status: AnyCable::Status::SUCCESS)
50
54
  else
51
- Anycable::DisconnectResponse.new(status: Anycable::Status::FAILURE)
55
+ AnyCable::DisconnectResponse.new(status: AnyCable::Status::FAILURE)
52
56
  end
57
+ rescue StandardError => exp
58
+ notify_exception(exp, :disconnect, request)
59
+
60
+ AnyCable::DisconnectResponse.new(
61
+ status: AnyCable::Status::ERROR,
62
+ error_msg: exp.message
63
+ )
53
64
  end
54
65
 
55
66
  def command(message, _unused_call)
56
67
  logger.debug("RPC Command: #{message.inspect}")
57
68
 
58
- socket = build_socket
69
+ # We don't have path/headers information here,
70
+ # but we still want `connection.env` to work
71
+ socket = build_socket(env: base_rack_env)
59
72
 
60
73
  connection = factory.call(
61
74
  socket,
@@ -68,51 +81,83 @@ module Anycable
68
81
  message.data
69
82
  )
70
83
 
71
- Anycable::CommandResponse.new(
72
- status: result ? Anycable::Status::SUCCESS : Anycable::Status::FAILURE,
84
+ AnyCable::CommandResponse.new(
85
+ status: result ? AnyCable::Status::SUCCESS : AnyCable::Status::FAILURE,
73
86
  disconnect: socket.closed?,
74
87
  stop_streams: socket.stop_streams?,
75
88
  streams: socket.streams,
76
89
  transmissions: socket.transmissions
77
90
  )
91
+ rescue StandardError => exp
92
+ notify_exception(exp, :command, message)
93
+
94
+ AnyCable::CommandResponse.new(
95
+ status: AnyCable::Status::ERROR,
96
+ error_msg: exp.message
97
+ )
78
98
  end
79
99
 
80
100
  private
81
101
 
82
- # Build env from path
102
+ # Build Rack env from request
83
103
  def rack_env(request)
84
104
  uri = URI.parse(request.path)
105
+
106
+ env = base_rack_env
107
+ env.merge!(
108
+ "PATH_INFO" => uri.path,
109
+ "QUERY_STRING" => uri.query,
110
+ "SERVER_NAME" => uri.host,
111
+ "SERVER_PORT" => uri.port.to_s,
112
+ "HTTP_HOST" => uri.host,
113
+ "REMOTE_ADDR" => request.headers.delete("REMOTE_ADDR"),
114
+ "rack.url_scheme" => uri.scheme
115
+ )
116
+
117
+ env.merge!(build_headers(request.headers))
118
+ end
119
+
120
+ def base_rack_env
121
+ # Minimum required variables according to Rack Spec
122
+ # (not all of them though, just those enough for Action Cable to work)
123
+ # See https://rubydoc.info/github/rack/rack/master/file/SPEC
85
124
  {
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
- # Hack to avoid Missing rack.input error
92
- 'rack.request.form_input' => '',
93
- 'rack.input' => '',
94
- 'rack.request.form_hash' => {}
95
- }.merge(build_headers(request.headers))
125
+ "REQUEST_METHOD" => "GET",
126
+ "SCRIPT_NAME" => "",
127
+ "PATH_INFO" => "/",
128
+ "QUERY_STRING" => "",
129
+ "SERVER_NAME" => "",
130
+ "SERVER_PORT" => "80",
131
+ "rack.url_scheme" => "http",
132
+ "rack.input" => ""
133
+ }
96
134
  end
97
135
 
98
136
  def build_socket(**options)
99
- Anycable::Socket.new(options)
137
+ AnyCable::Socket.new(**options)
100
138
  end
101
139
 
102
140
  def build_headers(headers)
103
141
  headers.each_with_object({}) do |(k, v), obj|
104
142
  k = k.upcase
105
- k.tr!('-', '_')
143
+ k.tr!("-", "_")
106
144
  obj["HTTP_#{k}"] = v
107
145
  end
108
146
  end
109
147
 
110
148
  def logger
111
- Anycable.logger
149
+ AnyCable.logger
112
150
  end
113
151
 
114
152
  def factory
115
- Anycable.connection_factory
153
+ AnyCable.connection_factory
154
+ end
155
+
156
+ def notify_exception(exp, method_name, message)
157
+ AnyCable::ExceptionsHandling.notify(exp, method_name.to_s, message.to_h)
116
158
  end
117
159
  end
118
160
  end
161
+ # rubocop:enable Metrics/AbcSize
162
+ # rubocop:enable Metrics/MethodLength
163
+ # rubocop:enable Metrics/ClassLength
@@ -1,59 +1,152 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'grpc'
4
- require 'anycable/rpc_handler'
5
- require 'anycable/health_server'
3
+ require "grpc"
4
+ require "grpc/health/checker"
5
+ require "grpc/health/v1/health_services_pb"
6
6
 
7
- module Anycable
8
- # Wrapper over GRPC server
9
- module Server
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 the loopback interface with 50051 port
16
+ # server = AnyCable::Server.new(host: "127.0.0.1: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
- attr_reader :grpc_server
25
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
26
+ def start(**options)
27
+ warn <<~DEPRECATION
28
+ DEPRECATION WARNING: Using AnyCable::Server.start is deprecated!
29
+ Please, use anycable CLI instead.
12
30
 
13
- def start
14
- log_grpc! if Anycable.config.log_grpc
31
+ See https://docs.anycable.io/#upgrade_to_0_6_0
32
+ DEPRECATION
15
33
 
16
- start_http_health_server
17
- start_grpc_server
18
- end
34
+ AnyCable.server_callbacks.each(&:call)
19
35
 
20
- def stop
21
- return unless running?
22
- @grpc_server.stop
23
- end
36
+ server = new(
37
+ host: AnyCable.config.rpc_host,
38
+ **AnyCable.config.to_grpc_params,
39
+ interceptors: AnyCable.middleware.to_a,
40
+ **options
41
+ )
24
42
 
25
- def running?
26
- grpc_server&.running_state == :running
27
- end
43
+ AnyCable.middleware.freeze
44
+
45
+ if AnyCable.config.http_health_port_provided?
46
+ health_server = AnyCable::HealthServer.new(
47
+ server,
48
+ **AnyCable.config.to_http_health_params
49
+ )
50
+ health_server.start
51
+ end
52
+
53
+ at_exit do
54
+ server.stop
55
+ health_server&.stop
56
+ end
28
57
 
29
- # Enable GRPC logging
30
- def log_grpc!
31
- GRPC.define_singleton_method(:logger) { Anycable.logger }
58
+ AnyCable.logger.info "Broadcasting Redis channel: #{AnyCable.config.redis_channel}"
59
+
60
+ server.start
61
+ server.wait_till_terminated
32
62
  end
63
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
64
+ end
33
65
 
34
- private
66
+ attr_reader :grpc_server, :host
35
67
 
36
- def start_grpc_server
37
- @grpc_server ||= build_server
68
+ def initialize(host:, logger: AnyCable.logger, **options)
69
+ @logger = logger
70
+ @host = host
71
+ @grpc_server = build_server(options)
72
+ end
38
73
 
39
- Anycable.logger.info "RPC server is listening on #{Anycable.config.rpc_host}"
40
- Anycable.logger.info "Broadcasting Redis channel: #{Anycable.config.redis_channel}"
74
+ # Start gRPC server in background and
75
+ # wait untill it ready to accept connections
76
+ def start
77
+ return if running?
41
78
 
42
- grpc_server.run_till_terminated
43
- end
79
+ raise "Cannot re-start stopped server" if stopped?
44
80
 
45
- def build_server
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
81
+ check_default_host
82
+
83
+ logger.info "RPC server is starting..."
84
+
85
+ @start_thread = Thread.new { grpc_server.run }
86
+
87
+ grpc_server.wait_till_running
88
+
89
+ logger.info "RPC server is listening on #{host}"
90
+ end
91
+
92
+ def wait_till_terminated
93
+ raise "Server is not running" unless running?
51
94
 
52
- def start_http_health_server
53
- return unless Anycable.config.http_health_port_provided?
54
- Anycable::HealthServer.start(Anycable.config.http_health_port)
55
- at_exit { Anycable::HealthServer.stop }
95
+ start_thread.join
96
+ end
97
+
98
+ # Stop gRPC server if it's running
99
+ def stop
100
+ return unless running?
101
+
102
+ grpc_server.stop
103
+
104
+ logger.info "RPC server stopped"
105
+ end
106
+
107
+ def running?
108
+ grpc_server.running_state == :running
109
+ end
110
+
111
+ def stopped?
112
+ grpc_server.running_state == :stopped
113
+ end
114
+
115
+ private
116
+
117
+ attr_reader :logger, :start_thread
118
+
119
+ def build_server(options)
120
+ GRPC::RpcServer.new(**options).tap do |server|
121
+ server.add_http2_port(host, :this_port_is_insecure)
122
+ server.handle(AnyCable::RPCHandler)
123
+ server.handle(build_health_checker)
56
124
  end
57
125
  end
126
+
127
+ def build_health_checker
128
+ health_checker = Grpc::Health::Checker.new
129
+ health_checker.add_status(
130
+ "anycable.RPC",
131
+ Grpc::Health::V1::HealthCheckResponse::ServingStatus::SERVING
132
+ )
133
+ health_checker
134
+ end
135
+
136
+ def check_default_host
137
+ return unless host.is_a?(Anycable::Config::DefaultHostWrapper)
138
+
139
+ warn <<~DEPRECATION
140
+ DEPRECATION WARNING: You're using default rpc_host configuration which starts AnyCable RPC
141
+ server on all available interfaces including external IPv4 and IPv6.
142
+ This is about to be changed to loopback interface only in future versions.
143
+
144
+ Please, consider switching to the loopback interface or set "[::]:50051"
145
+ explicitly in your configuration, if you want to continue with the current
146
+ behavior and supress this message.
147
+
148
+ See https://docs.anycable.io/#/configuration
149
+ DEPRECATION
150
+ end
58
151
  end
59
152
  end