anycable 0.5.2 → 0.6.0.rc1

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