anycable 0.5.2 → 0.6.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE.md +25 -0
  3. data/.github/PULL_REQUEST_TEMPLATE.md +31 -0
  4. data/.rubocop.yml +22 -22
  5. data/.travis.yml +1 -2
  6. data/CHANGELOG.md +92 -0
  7. data/README.md +10 -58
  8. data/anycable.gemspec +10 -7
  9. data/benchmarks/.gitignore +1 -0
  10. data/benchmarks/2018-10-27.md +181 -0
  11. data/benchmarks/assets/2018-10-27-action-cable-rss.png +0 -0
  12. data/benchmarks/assets/2018-10-27-action-cable-rtt.png +0 -0
  13. data/benchmarks/assets/2018-10-27-anycable-rss.png +0 -0
  14. data/benchmarks/assets/2018-10-27-anycable-rtt.png +0 -0
  15. data/benchmarks/assets/2018-10-27-async-rss.png +0 -0
  16. data/benchmarks/assets/2018-10-27-async-rtt.png +0 -0
  17. data/benchmarks/assets/2018-10-27-falcon-cable-rss.png +0 -0
  18. data/benchmarks/assets/2018-10-27-falcon-cable-rtt.png +0 -0
  19. data/benchmarks/assets/2018-10-27-iodine-cable-rss.png +0 -0
  20. data/benchmarks/assets/2018-10-27-iodine-cable-rtt.png +0 -0
  21. data/benchmarks/assets/2018-10-27-plezi-rss.png +0 -0
  22. data/benchmarks/assets/2018-10-27-plezi-rtt.png +0 -0
  23. data/benchmarks/bench.png +0 -0
  24. data/benchmarks/benchmark.yml +12 -10
  25. data/benchmarks/hosts +2 -2
  26. data/benchmarks/rtt_plot.py +74 -0
  27. data/benchmarks/rtt_plot_test.py +16 -0
  28. data/benchmarks/servers.yml +25 -3
  29. data/bin/anycable +13 -0
  30. data/etc/bug_report_template.rb +1 -1
  31. data/lib/anycable.rb +53 -16
  32. data/lib/anycable/broadcast_adapters.rb +33 -0
  33. data/lib/anycable/broadcast_adapters/redis.rb +42 -0
  34. data/lib/anycable/cli.rb +323 -0
  35. data/lib/anycable/config.rb +91 -17
  36. data/lib/anycable/exceptions_handling.rb +31 -0
  37. data/lib/anycable/handler/capture_exceptions.rb +39 -0
  38. data/lib/anycable/health_server.rb +53 -31
  39. data/lib/anycable/middleware.rb +19 -0
  40. data/lib/anycable/middleware_chain.rb +58 -0
  41. data/lib/anycable/rpc/rpc_pb.rb +1 -1
  42. data/lib/anycable/rpc/rpc_services_pb.rb +1 -1
  43. data/lib/anycable/rpc_handler.rb +28 -26
  44. data/lib/anycable/server.rb +114 -39
  45. data/lib/anycable/socket.rb +1 -1
  46. data/lib/anycable/version.rb +2 -2
  47. metadata +45 -26
  48. data/lib/anycable/handler/exceptions_handling.rb +0 -43
  49. data/lib/anycable/pubsub.rb +0 -26
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "anycable/rpc/rpc_pb"
4
+
5
+ module AnyCable
6
+ module Handler # :nodoc:
7
+ # Handle app-level errors.
8
+ #
9
+ # NOTE: this functionality couldn't be implemeted
10
+ # as middleware, 'cause interceptors do not support
11
+ # aborting the call and returning a data
12
+ module CaptureExceptions
13
+ RESPONSE_CLASS = {
14
+ command: AnyCable::CommandResponse,
15
+ connect: AnyCable::ConnectionResponse,
16
+ disconnect: AnyCable::DisconnectResponse
17
+ }.freeze
18
+
19
+ RESPONSE_CLASS.keys.each do |mid|
20
+ module_eval <<~CODE, __FILE__, __LINE__ + 1
21
+ def #{mid}(*)
22
+ capture_exceptions(:#{mid}) { super }
23
+ end
24
+ CODE
25
+ end
26
+
27
+ def capture_exceptions(method_name)
28
+ yield
29
+ rescue StandardError => exp
30
+ AnyCable::ExceptionsHandling.notify(exp)
31
+
32
+ RESPONSE_CLASS.fetch(method_name).new(
33
+ status: AnyCable::Status::ERROR,
34
+ error_msg: exp.message
35
+ )
36
+ end
37
+ end
38
+ end
39
+ end
@@ -1,44 +1,66 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'webrick'
4
- require 'anycable/server'
3
+ require "webrick"
4
+ require "anycable/server"
5
5
 
6
- module Anycable
7
- # Server for HTTP healthchecks
8
- module HealthServer
9
- class << self
10
- def start(port)
11
- return if running?
6
+ module AnyCable
7
+ # Server for HTTP healthchecks.
8
+ #
9
+ # Basic usage:
10
+ #
11
+ # # create a new healthcheck server for a specified
12
+ # # gRPC server lisening on the port
13
+ # health_server = AnyCable::HealthServer.new(grpc_server, port)
14
+ #
15
+ # # start health server in background
16
+ # health_server.start
17
+ #
18
+ # # stop health server
19
+ # health_server.stop
20
+ class HealthServer
21
+ SUCCESS_RESPONSE = [200, "Ready"].freeze
22
+ FAILURE_RESPONSE = [503, "Not Ready"].freeze
12
23
 
13
- @health_server ||= build_server(port)
14
- Thread.new { @health_server.start }
24
+ attr_reader :grpc_server, :port, :path, :server
15
25
 
16
- Anycable.logger.info "HTTP health server is listening on #{port}"
17
- end
26
+ def initialize(grpc_server, port:, path: "/health", logger: AnyCable.logger)
27
+ @grpc_server = grpc_server
28
+ @port = port
29
+ @path = path
30
+ @logger = logger
31
+ @server = build_server
32
+ end
18
33
 
19
- def stop
20
- return unless running?
21
- @health_server.shutdown
22
- end
34
+ def start
35
+ return if running?
23
36
 
24
- def running?
25
- @health_server&.status == :Running
26
- end
37
+ Thread.new { server.start }
38
+
39
+ logger.info "HTTP health server is listening on localhost:#{port} and mounted at \"#{path}\""
40
+ end
41
+
42
+ def stop
43
+ return unless running?
44
+
45
+ server.shutdown
46
+ end
47
+
48
+ def running?
49
+ server.status == :Running
50
+ end
27
51
 
28
- private
52
+ private
29
53
 
30
- SUCCESS_RESPONSE = [200, "Ready"].freeze
31
- FAILURE_RESPONSE = [503, "Not Ready"].freeze
54
+ attr_reader :logger
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
+ def build_server
57
+ WEBrick::HTTPServer.new(
58
+ Port: port,
59
+ Logger: logger,
60
+ AccessLog: []
61
+ ).tap do |server|
62
+ server.mount_proc path do |_, res|
63
+ res.status, res.body = grpc_server.running? ? SUCCESS_RESPONSE : FAILURE_RESPONSE
42
64
  end
43
65
  end
44
66
  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,17 +1,17 @@
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'
3
+ require "anycable/socket"
4
+ require "anycable/rpc/rpc_pb"
5
+ require "anycable/rpc/rpc_services_pb"
6
6
 
7
- require 'anycable/handler/exceptions_handling'
7
+ require "anycable/handler/capture_exceptions"
8
8
 
9
9
  # rubocop:disable Metrics/AbcSize
10
10
  # rubocop:disable Metrics/MethodLength
11
- module Anycable
11
+ module AnyCable
12
12
  # RPC service handler
13
- class RPCHandler < Anycable::RPC::Service
14
- prepend Handler::ExceptionsHandling
13
+ class RPCHandler < AnyCable::RPC::Service
14
+ prepend AnyCable::Handler::CaptureExceptions
15
15
 
16
16
  # Handle connection request from WebSocket server
17
17
  def connect(request, _unused_call)
@@ -24,10 +24,10 @@ module Anycable
24
24
  connection.handle_open
25
25
 
26
26
  if socket.closed?
27
- Anycable::ConnectionResponse.new(status: Anycable::Status::FAILURE)
27
+ AnyCable::ConnectionResponse.new(status: AnyCable::Status::FAILURE)
28
28
  else
29
- Anycable::ConnectionResponse.new(
30
- status: Anycable::Status::SUCCESS,
29
+ AnyCable::ConnectionResponse.new(
30
+ status: AnyCable::Status::SUCCESS,
31
31
  identifiers: connection.identifiers_json,
32
32
  transmissions: socket.transmissions
33
33
  )
@@ -46,9 +46,9 @@ module Anycable
46
46
  )
47
47
 
48
48
  if connection.handle_close
49
- Anycable::DisconnectResponse.new(status: Anycable::Status::SUCCESS)
49
+ AnyCable::DisconnectResponse.new(status: AnyCable::Status::SUCCESS)
50
50
  else
51
- Anycable::DisconnectResponse.new(status: Anycable::Status::FAILURE)
51
+ AnyCable::DisconnectResponse.new(status: AnyCable::Status::FAILURE)
52
52
  end
53
53
  end
54
54
 
@@ -68,8 +68,8 @@ module Anycable
68
68
  message.data
69
69
  )
70
70
 
71
- Anycable::CommandResponse.new(
72
- status: result ? Anycable::Status::SUCCESS : Anycable::Status::FAILURE,
71
+ AnyCable::CommandResponse.new(
72
+ status: result ? AnyCable::Status::SUCCESS : AnyCable::Status::FAILURE,
73
73
  disconnect: socket.closed?,
74
74
  stop_streams: socket.stop_streams?,
75
75
  streams: socket.streams,
@@ -83,36 +83,38 @@ module Anycable
83
83
  def rack_env(request)
84
84
  uri = URI.parse(request.path)
85
85
  {
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,
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
91
  # Hack to avoid Missing rack.input error
92
- 'rack.request.form_input' => '',
93
- 'rack.input' => '',
94
- 'rack.request.form_hash' => {}
92
+ "rack.request.form_input" => "",
93
+ "rack.input" => "",
94
+ "rack.request.form_hash" => {}
95
95
  }.merge(build_headers(request.headers))
96
96
  end
97
97
 
98
98
  def build_socket(**options)
99
- Anycable::Socket.new(options)
99
+ AnyCable::Socket.new(options)
100
100
  end
101
101
 
102
102
  def build_headers(headers)
103
103
  headers.each_with_object({}) do |(k, v), obj|
104
104
  k = k.upcase
105
- k.tr!('-', '_')
105
+ k.tr!("-", "_")
106
106
  obj["HTTP_#{k}"] = v
107
107
  end
108
108
  end
109
109
 
110
110
  def logger
111
- Anycable.logger
111
+ AnyCable.logger
112
112
  end
113
113
 
114
114
  def factory
115
- Anycable.connection_factory
115
+ AnyCable.connection_factory
116
116
  end
117
117
  end
118
118
  end
119
+ # rubocop:enable Metrics/AbcSize
120
+ # rubocop:enable Metrics/MethodLength
@@ -1,59 +1,134 @@
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 [::]:50051 (default host)
16
+ # server = AnyCable::Server.new(host: "[::]: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
+ 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
+ server = new(
35
+ host: AnyCable.config.rpc_host,
36
+ **AnyCable.config.to_grpc_params,
37
+ interceptors: AnyCable.middleware.to_a,
38
+ **options
39
+ )
19
40
 
20
- def stop
21
- return unless running?
22
- @grpc_server.stop
23
- end
41
+ AnyCable.middleware.freeze
24
42
 
25
- def running?
26
- grpc_server&.running_state == :running
27
- end
43
+ if AnyCable.config.http_health_port_provided?
44
+ health_server = AnyCable::HealthServer.new(
45
+ server,
46
+ **AnyCable.config.to_http_health_params
47
+ )
48
+ health_server.start
49
+ end
28
50
 
29
- # Enable GRPC logging
30
- def log_grpc!
31
- GRPC.define_singleton_method(:logger) { Anycable.logger }
51
+ at_exit do
52
+ server.stop
53
+ health_server&.stop
54
+ end
55
+
56
+ AnyCable.logger.info "Broadcasting Redis channel: #{AnyCable.config.redis_channel}"
57
+
58
+ server.start
59
+ server.wait_till_terminated
32
60
  end
61
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
62
+ end
33
63
 
34
- private
64
+ DEFAULT_HOST = "0.0.0.0:50051"
35
65
 
36
- def start_grpc_server
37
- @grpc_server ||= build_server
66
+ attr_reader :grpc_server, :host
38
67
 
39
- Anycable.logger.info "RPC server is listening on #{Anycable.config.rpc_host}"
40
- Anycable.logger.info "Broadcasting Redis channel: #{Anycable.config.redis_channel}"
68
+ def initialize(host: DEFAULT_HOST, logger: AnyCable.logger, **options)
69
+ @logger = logger
70
+ @host = host
71
+ @grpc_server = build_server(options)
72
+ end
41
73
 
42
- grpc_server.run_till_terminated
43
- end
74
+ # Start gRPC server in background and
75
+ # wait untill it ready to accept connections
76
+ def start
77
+ return if running?
44
78
 
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
79
+ raise "Cannot re-start stopped server" if stopped?
51
80
 
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 }
81
+ logger.info "RPC server is starting..."
82
+
83
+ @start_thread = Thread.new { grpc_server.run }
84
+
85
+ grpc_server.wait_till_running
86
+
87
+ logger.info "RPC server is listening on #{host}"
88
+ end
89
+
90
+ def wait_till_terminated
91
+ raise "Server is not running" unless running?
92
+
93
+ start_thread.join
94
+ end
95
+
96
+ # Stop gRPC server if it's running
97
+ def stop
98
+ return unless running?
99
+
100
+ grpc_server.stop
101
+
102
+ logger.info "RPC server stopped"
103
+ end
104
+
105
+ def running?
106
+ grpc_server.running_state == :running
107
+ end
108
+
109
+ def stopped?
110
+ grpc_server.running_state == :stopped
111
+ end
112
+
113
+ private
114
+
115
+ attr_reader :logger, :start_thread
116
+
117
+ def build_server(options)
118
+ GRPC::RpcServer.new(options).tap do |server|
119
+ server.add_http2_port(host, :this_port_is_insecure)
120
+ server.handle(AnyCable::RPCHandler)
121
+ server.handle(build_health_checker)
56
122
  end
57
123
  end
124
+
125
+ def build_health_checker
126
+ health_checker = Grpc::Health::Checker.new
127
+ health_checker.add_status(
128
+ "anycable.RPC",
129
+ Grpc::Health::V1::HealthCheckResponse::ServingStatus::SERVING
130
+ )
131
+ health_checker
132
+ end
58
133
  end
59
134
  end