anycable 0.6.3 → 1.0.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +23 -222
- data/MIT-LICENSE +1 -1
- data/README.md +21 -15
- data/lib/anycable.rb +3 -12
- data/lib/anycable/broadcast_adapters.rb +5 -3
- data/lib/anycable/broadcast_adapters/base.rb +29 -0
- data/lib/anycable/broadcast_adapters/http.rb +130 -0
- data/lib/anycable/broadcast_adapters/redis.rb +7 -6
- data/lib/anycable/cli.rb +24 -7
- data/lib/anycable/config.rb +44 -21
- data/lib/anycable/exceptions_handling.rb +5 -7
- data/lib/anycable/health_server.rb +6 -4
- data/lib/anycable/middleware.rb +9 -1
- data/lib/anycable/middleware_chain.rb +2 -2
- data/lib/anycable/middlewares/check_version.rb +24 -0
- data/lib/anycable/rpc.rb +84 -0
- data/lib/anycable/rpc/rpc_pb.rb +57 -39
- data/lib/anycable/rpc/rpc_services_pb.rb +4 -3
- data/lib/anycable/rpc_handler.rb +65 -25
- data/lib/anycable/rspec.rb +6 -0
- data/lib/anycable/rspec/rpc_command_context.rb +20 -0
- data/lib/anycable/rspec/rpc_stub_context.rb +13 -0
- data/lib/anycable/rspec/with_grpc_server.rb +15 -0
- data/lib/anycable/server.rb +7 -63
- data/lib/anycable/socket.rb +48 -6
- data/lib/anycable/version.rb +1 -1
- metadata +27 -42
@@ -0,0 +1,130 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
require "uri"
|
5
|
+
require "net/http"
|
6
|
+
|
7
|
+
module AnyCable
|
8
|
+
module BroadcastAdapters
|
9
|
+
# HTTP adapter for broadcasting.
|
10
|
+
#
|
11
|
+
# Example:
|
12
|
+
#
|
13
|
+
# AnyCable.broadast_adapter = :http
|
14
|
+
#
|
15
|
+
# It uses configuration from global AnyCable config
|
16
|
+
# by default.
|
17
|
+
#
|
18
|
+
# You can override these params:
|
19
|
+
#
|
20
|
+
# AnyCable.broadcast_adapter = :http, url: "http://ws.example.com/_any_cable_"
|
21
|
+
class Http < Base
|
22
|
+
# Taken from: https://github.com/influxdata/influxdb-ruby/blob/886058079c66d4fd019ad74ca11342fddb0b753d/lib/influxdb/errors.rb#L18
|
23
|
+
RECOVERABLE_EXCEPTIONS = [
|
24
|
+
Errno::ECONNABORTED,
|
25
|
+
Errno::ECONNREFUSED,
|
26
|
+
Errno::ECONNRESET,
|
27
|
+
Errno::EHOSTUNREACH,
|
28
|
+
Errno::EINVAL,
|
29
|
+
Errno::ENETUNREACH,
|
30
|
+
Net::HTTPBadResponse,
|
31
|
+
Net::HTTPHeaderSyntaxError,
|
32
|
+
Net::ProtocolError,
|
33
|
+
SocketError,
|
34
|
+
(OpenSSL::SSL::SSLError if defined?(OpenSSL))
|
35
|
+
].compact.freeze
|
36
|
+
|
37
|
+
OPEN_TIMEOUT = 5
|
38
|
+
READ_TIMEOUT = 10
|
39
|
+
|
40
|
+
MAX_ATTEMPTS = 3
|
41
|
+
DELAY = 2
|
42
|
+
|
43
|
+
attr_reader :url, :headers, :authorized
|
44
|
+
alias authorized? authorized
|
45
|
+
|
46
|
+
def initialize(url: AnyCable.config.http_broadcast_url, secret: AnyCable.config.http_broadcast_secret)
|
47
|
+
@url = url
|
48
|
+
@headers = {}
|
49
|
+
if secret
|
50
|
+
headers["Authorization"] = "Bearer #{secret}"
|
51
|
+
@authorized = true
|
52
|
+
end
|
53
|
+
|
54
|
+
@uri = URI.parse(url)
|
55
|
+
@queue = Queue.new
|
56
|
+
end
|
57
|
+
|
58
|
+
def raw_broadcast(payload)
|
59
|
+
ensure_thread_is_alive
|
60
|
+
queue << payload
|
61
|
+
end
|
62
|
+
|
63
|
+
# Wait for background thread to process all the messages
|
64
|
+
# and stop it
|
65
|
+
def shutdown
|
66
|
+
queue << :stop
|
67
|
+
thread.join if thread&.alive?
|
68
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
69
|
+
logger.error "Broadcasting thread exited with exception: #{e.message}"
|
70
|
+
end
|
71
|
+
|
72
|
+
def announce!
|
73
|
+
logger.info "Broadcasting HTTP url: #{url}#{authorized? ? " (with authorization)" : ""}"
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
attr_reader :uri, :queue, :thread
|
79
|
+
|
80
|
+
def ensure_thread_is_alive
|
81
|
+
return if thread&.alive?
|
82
|
+
|
83
|
+
@thread = Thread.new do
|
84
|
+
loop do
|
85
|
+
msg = queue.pop
|
86
|
+
break if msg == :stop
|
87
|
+
|
88
|
+
handle_response perform_request(msg)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def perform_request(payload)
|
94
|
+
build_http do |http|
|
95
|
+
req = Net::HTTP::Post.new(url, {"Content-Type" => "application/json"}.merge(headers))
|
96
|
+
req.body = payload
|
97
|
+
http.request(req)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def handle_response(response)
|
102
|
+
return unless response
|
103
|
+
return if Net::HTTPCreated === response
|
104
|
+
|
105
|
+
logger.error "Broadcast request responded with unexpected status: #{response.code}"
|
106
|
+
end
|
107
|
+
|
108
|
+
def build_http
|
109
|
+
retry_count = 0
|
110
|
+
|
111
|
+
begin
|
112
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
113
|
+
http.open_timeout = OPEN_TIMEOUT
|
114
|
+
http.read_timeout = READ_TIMEOUT
|
115
|
+
http.use_ssl = url.match?(/^https/)
|
116
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
117
|
+
yield http
|
118
|
+
rescue Timeout::Error, *RECOVERABLE_EXCEPTIONS => e
|
119
|
+
retry_count += 1
|
120
|
+
return logger.error("Broadcast request failed: #{e.message}") if MAX_ATTEMPTS < retry_count
|
121
|
+
|
122
|
+
sleep((DELAY**retry_count) * retry_count)
|
123
|
+
retry
|
124
|
+
ensure
|
125
|
+
http.finish if http.started?
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -19,7 +19,7 @@ module AnyCable
|
|
19
19
|
# You can override these params:
|
20
20
|
#
|
21
21
|
# AnyCable.broadcast_adapter = :redis, url: "redis://my_redis", channel: "_any_cable_"
|
22
|
-
class Redis
|
22
|
+
class Redis < Base
|
23
23
|
attr_reader :redis_conn, :channel
|
24
24
|
|
25
25
|
def initialize(
|
@@ -31,11 +31,12 @@ module AnyCable
|
|
31
31
|
@channel = channel
|
32
32
|
end
|
33
33
|
|
34
|
-
def
|
35
|
-
redis_conn.publish(
|
36
|
-
|
37
|
-
|
38
|
-
|
34
|
+
def raw_broadcast(payload)
|
35
|
+
redis_conn.publish(channel, payload)
|
36
|
+
end
|
37
|
+
|
38
|
+
def announce!
|
39
|
+
logger.info "Broadcasting Redis channel: #{channel}"
|
39
40
|
end
|
40
41
|
end
|
41
42
|
end
|
data/lib/anycable/cli.rb
CHANGED
@@ -36,11 +36,11 @@ module AnyCable
|
|
36
36
|
|
37
37
|
configure_server!
|
38
38
|
|
39
|
-
logger.info "Starting AnyCable gRPC server (pid: #{Process.pid})"
|
39
|
+
logger.info "Starting AnyCable gRPC server (pid: #{Process.pid}, workers_num: #{config.rpc_pool_size})"
|
40
40
|
|
41
41
|
print_versions!
|
42
42
|
|
43
|
-
logger.info "Serving #{defined?(::Rails) ?
|
43
|
+
logger.info "Serving #{defined?(::Rails) ? "Rails " : ""}application from #{boot_file}"
|
44
44
|
|
45
45
|
verify_connection_factory!
|
46
46
|
|
@@ -48,6 +48,8 @@ module AnyCable
|
|
48
48
|
|
49
49
|
log_errors!
|
50
50
|
|
51
|
+
use_version_check! if config.version_check_enabled?
|
52
|
+
|
51
53
|
@server = AnyCable::Server.new(
|
52
54
|
host: config.rpc_host,
|
53
55
|
**config.to_grpc_params,
|
@@ -124,7 +126,7 @@ module AnyCable
|
|
124
126
|
end
|
125
127
|
|
126
128
|
def print_versions!
|
127
|
-
logger.info "AnyCable version: #{AnyCable::VERSION}"
|
129
|
+
logger.info "AnyCable version: #{AnyCable::VERSION} (proto_version: #{AnyCable::PROTO_VERSION})"
|
128
130
|
logger.info "gRPC version: #{GRPC::VERSION}"
|
129
131
|
end
|
130
132
|
|
@@ -162,6 +164,14 @@ module AnyCable
|
|
162
164
|
AnyCable.server_callbacks.each(&:call)
|
163
165
|
end
|
164
166
|
|
167
|
+
def use_version_check!
|
168
|
+
require "anycable/middlewares/check_version"
|
169
|
+
|
170
|
+
AnyCable.middleware.use(
|
171
|
+
AnyCable::Middlewares::CheckVersion.new(AnyCable::PROTO_VERSION)
|
172
|
+
)
|
173
|
+
end
|
174
|
+
|
165
175
|
def start_health_server!
|
166
176
|
@health_server = AnyCable::HealthServer.new(
|
167
177
|
server,
|
@@ -173,7 +183,7 @@ module AnyCable
|
|
173
183
|
end
|
174
184
|
|
175
185
|
def start_pubsub!
|
176
|
-
|
186
|
+
AnyCable.broadcast_adapter.announce!
|
177
187
|
end
|
178
188
|
|
179
189
|
# rubocop: disable Metrics/MethodLength, Metrics/AbcSize
|
@@ -303,9 +313,7 @@ module AnyCable
|
|
303
313
|
-r, --require=path Location of application file to require, default: "config/environment.rb"
|
304
314
|
--server-command=command Command to run WebSocket server
|
305
315
|
--rpc-host=host Local address to run gRPC server on, default: "[::]:50051"
|
306
|
-
--
|
307
|
-
--redis-channel=name Redis channel for broadcasting, default: "__anycable__"
|
308
|
-
--redis-sentinels=<...hosts> Redis Sentinel followers addresses (as a comma-separated list), default: nil
|
316
|
+
--broadcast-adapter=type Pub/sub adapter type for broadcasts, default: redis
|
309
317
|
--log-level=level Logging level, default: "info"
|
310
318
|
--log-file=path Path to log file, default: <none> (log to STDOUT)
|
311
319
|
--log-grpc Enable gRPC logging (disabled by default)
|
@@ -313,6 +321,15 @@ module AnyCable
|
|
313
321
|
-v, --version Print version and exit
|
314
322
|
-h, --help Show this help
|
315
323
|
|
324
|
+
REDIS PUB/SUB OPTIONS
|
325
|
+
--redis-url=url Redis URL for pub/sub, default: REDIS_URL or "redis://localhost:6379/5"
|
326
|
+
--redis-channel=name Redis channel for broadcasting, default: "__anycable__"
|
327
|
+
--redis-sentinels=<...hosts> Redis Sentinel followers addresses (as a comma-separated list), default: nil
|
328
|
+
|
329
|
+
HTTP PUB/SUB OPTIONS
|
330
|
+
--http-broadcast-url HTTP pub/sub endpoint URL, default: "http://localhost:8090/_broadcast"
|
331
|
+
--http-broadcast-secret HTTP pub/sub authorization secret, default: <none> (disabled)
|
332
|
+
|
316
333
|
HTTP HEALTH CHECKER OPTIONS
|
317
334
|
--http-health-port=port Port to run HTTP health server on, default: <none> (disabled)
|
318
335
|
--http-health-path=path Endpoint to server health cheks, default: "/health"
|
data/lib/anycable/config.rb
CHANGED
@@ -8,11 +8,9 @@ module AnyCable
|
|
8
8
|
class Config < Anyway::Config
|
9
9
|
config_name :anycable
|
10
10
|
|
11
|
-
DefaultHostWrapper = Class.new(String)
|
12
|
-
|
13
11
|
attr_config(
|
14
12
|
### gRPC options
|
15
|
-
rpc_host:
|
13
|
+
rpc_host: "127.0.0.1:50051",
|
16
14
|
# For defaults see https://github.com/grpc/grpc/blob/51f0d35509bcdaba572d422c4f856208162022de/src/ruby/lib/grpc/generic/rpc_server.rb#L186-L216
|
17
15
|
rpc_pool_size: GRPC::RpcServer::DEFAULT_POOL_SIZE,
|
18
16
|
rpc_max_waiting_requests: GRPC::RpcServer::DEFAULT_MAX_WAITING_REQUESTS,
|
@@ -21,11 +19,18 @@ module AnyCable
|
|
21
19
|
# See https://github.com/grpc/grpc/blob/f526602bff029b8db50a8d57134d72da33d8a752/include/grpc/impl/codegen/grpc_types.h#L292-L315
|
22
20
|
rpc_server_args: {},
|
23
21
|
|
22
|
+
## PubSub
|
23
|
+
broadcast_adapter: :redis,
|
24
|
+
|
24
25
|
### Redis options
|
25
26
|
redis_url: ENV.fetch("REDIS_URL", "redis://localhost:6379/5"),
|
26
27
|
redis_sentinels: nil,
|
27
28
|
redis_channel: "__anycable__",
|
28
29
|
|
30
|
+
### HTTP broadcasting options
|
31
|
+
http_broadcast_url: "http://localhost:8090/_broadcast",
|
32
|
+
http_broadcast_secret: nil,
|
33
|
+
|
29
34
|
### Logging options
|
30
35
|
log_file: nil,
|
31
36
|
log_level: :info,
|
@@ -34,25 +39,44 @@ module AnyCable
|
|
34
39
|
|
35
40
|
### Health check options
|
36
41
|
http_health_port: nil,
|
37
|
-
http_health_path: "/health"
|
42
|
+
http_health_path: "/health",
|
43
|
+
|
44
|
+
### Misc options
|
45
|
+
version_check_enabled: true
|
38
46
|
)
|
39
47
|
|
48
|
+
alias version_check_enabled? version_check_enabled
|
49
|
+
|
40
50
|
ignore_options :rpc_server_args
|
41
51
|
flag_options :log_grpc, :debug
|
42
52
|
|
43
|
-
|
44
|
-
|
45
|
-
|
53
|
+
# Support both anyway_config 1.4 and 2.0.
|
54
|
+
# DEPRECATE: Drop <2.0 support in 1.1
|
55
|
+
if respond_to?(:on_load)
|
56
|
+
on_load { self.debug = debug != false }
|
46
57
|
|
47
|
-
|
48
|
-
|
49
|
-
|
58
|
+
def log_level
|
59
|
+
debug? ? :debug : super
|
60
|
+
end
|
50
61
|
|
51
|
-
|
52
|
-
|
53
|
-
|
62
|
+
def log_grpc
|
63
|
+
debug? || super
|
64
|
+
end
|
65
|
+
else
|
66
|
+
def log_level
|
67
|
+
debug ? :debug : @log_level
|
68
|
+
end
|
69
|
+
|
70
|
+
def log_grpc
|
71
|
+
debug || @log_grpc
|
72
|
+
end
|
73
|
+
|
74
|
+
def debug
|
75
|
+
@debug != false
|
76
|
+
end
|
54
77
|
|
55
|
-
|
78
|
+
alias debug? debug
|
79
|
+
end
|
56
80
|
|
57
81
|
def http_health_port_provided?
|
58
82
|
!http_health_port.nil? && http_health_port != ""
|
@@ -71,15 +95,14 @@ module AnyCable
|
|
71
95
|
|
72
96
|
# Build Redis parameters
|
73
97
|
def to_redis_params
|
74
|
-
{
|
98
|
+
{url: redis_url}.tap do |params|
|
75
99
|
next if redis_sentinels.nil?
|
76
100
|
|
77
|
-
|
78
|
-
redis_sentinels.is_a?(Array)
|
101
|
+
sentinels = Array(redis_sentinels)
|
79
102
|
|
80
|
-
next if
|
103
|
+
next if sentinels.empty?
|
81
104
|
|
82
|
-
params[:sentinels] =
|
105
|
+
params[:sentinels] = sentinels.map(&method(:parse_sentinel))
|
83
106
|
end
|
84
107
|
end
|
85
108
|
|
@@ -96,13 +119,13 @@ module AnyCable
|
|
96
119
|
SENTINEL_RXP = /^([\w\-_]*)\:(\d+)$/.freeze
|
97
120
|
|
98
121
|
def parse_sentinel(sentinel)
|
99
|
-
return sentinel if sentinel.is_a?(Hash)
|
122
|
+
return sentinel.transform_keys!(&:to_sym) if sentinel.is_a?(Hash)
|
100
123
|
|
101
124
|
matches = sentinel.match(SENTINEL_RXP)
|
102
125
|
|
103
126
|
raise ArgumentError, "Invalid Sentinel value: #{sentinel}" if matches.nil?
|
104
127
|
|
105
|
-
{
|
128
|
+
{host: matches[1], port: matches[2].to_i}
|
106
129
|
end
|
107
130
|
end
|
108
131
|
end
|
@@ -11,13 +11,11 @@ module AnyCable
|
|
11
11
|
|
12
12
|
def notify(exp, method_name, message)
|
13
13
|
handlers.each do |handler|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
AnyCable.logger.error exp.backtrace.join("\n") unless exp.backtrace.nil?
|
20
|
-
end
|
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?
|
21
19
|
end
|
22
20
|
end
|
23
21
|
|
@@ -1,7 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "webrick"
|
4
|
-
|
5
3
|
module AnyCable
|
6
4
|
# Server for HTTP healthchecks.
|
7
5
|
#
|
@@ -22,7 +20,7 @@ module AnyCable
|
|
22
20
|
|
23
21
|
attr_reader :grpc_server, :port, :path, :server
|
24
22
|
|
25
|
-
def initialize(grpc_server, port:, path: "/health"
|
23
|
+
def initialize(grpc_server, port:, logger: nil, path: "/health")
|
26
24
|
@grpc_server = grpc_server
|
27
25
|
@port = port
|
28
26
|
@path = path
|
@@ -50,9 +48,13 @@ module AnyCable
|
|
50
48
|
|
51
49
|
private
|
52
50
|
|
53
|
-
|
51
|
+
def logger
|
52
|
+
@logger ||= AnyCable.logger
|
53
|
+
end
|
54
54
|
|
55
55
|
def build_server
|
56
|
+
require "webrick"
|
57
|
+
|
56
58
|
WEBrick::HTTPServer.new(
|
57
59
|
Port: port,
|
58
60
|
Logger: logger,
|
data/lib/anycable/middleware.rb
CHANGED
@@ -5,13 +5,21 @@ require "grpc"
|
|
5
5
|
module AnyCable
|
6
6
|
# Middleware is a wrapper over gRPC interceptors
|
7
7
|
# for request/response calls
|
8
|
-
class Middleware < GRPC::
|
8
|
+
class Middleware < GRPC::ServerInterceptor
|
9
9
|
def request_response(request: nil, call: nil, method: nil)
|
10
|
+
# Call middlewares only for AnyCable service
|
11
|
+
return yield unless method.receiver.is_a?(AnyCable::RPCHandler)
|
12
|
+
|
10
13
|
call(request, call, method) do
|
11
14
|
yield
|
12
15
|
end
|
13
16
|
end
|
14
17
|
|
18
|
+
def server_streamer(**kwargs)
|
19
|
+
p kwargs
|
20
|
+
yield
|
21
|
+
end
|
22
|
+
|
15
23
|
def call(*)
|
16
24
|
raise NotImplementedError
|
17
25
|
end
|
@@ -48,8 +48,8 @@ module AnyCable
|
|
48
48
|
|
49
49
|
unless middleware.is_a?(AnyCable::Middleware)
|
50
50
|
raise ArgumentError,
|
51
|
-
|
52
|
-
|
51
|
+
"AnyCable middleware must be a subclass of AnyCable::Middleware, " \
|
52
|
+
"got #{middleware} instead"
|
53
53
|
end
|
54
54
|
|
55
55
|
middleware
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AnyCable
|
4
|
+
module Middlewares
|
5
|
+
# Checks that RPC client version is compatible with
|
6
|
+
# the current RPC proto version
|
7
|
+
class CheckVersion < Middleware
|
8
|
+
attr_reader :version
|
9
|
+
|
10
|
+
def initialize(version)
|
11
|
+
@version = version
|
12
|
+
end
|
13
|
+
|
14
|
+
def call(_request, call, _method)
|
15
|
+
supported_versions = call.metadata["protov"]&.split(",")
|
16
|
+
return yield if supported_versions&.include?(version)
|
17
|
+
|
18
|
+
raise GRPC::Internal,
|
19
|
+
"Incompatible AnyCable RPC client.\nCurrent server version: #{version}.\n" \
|
20
|
+
"Client supported versions: #{call.metadata["protov"] || "unknown"}."
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|