anycable 0.5.2 → 0.6.4

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