anycable-core 1.2.5 → 1.3.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5e9ba8037b7cbd766d021b4e88d49101b6dc3e9a5a6b355929bf9aaa8494f33a
4
- data.tar.gz: 71cd40b2380601f322a335d774cfb41e39f8de6ac29f4b5201a825119bff1031
3
+ metadata.gz: 5eaf888f4c9312a81bbadf980e5279efa1a0403760569b0465a537e6bd2ed291
4
+ data.tar.gz: 85206aa6fb8aac5faa0641c889deb9f4c61d40a27211feffb20ab23590313469
5
5
  SHA512:
6
- metadata.gz: f1a372e6d70eb1db64054aeafcc3baccd674d679652309526fbcfe9329799383b671d532cd16a52dd53b00b4a1658100d000f4564f2cbaac00502e02da00b668
7
- data.tar.gz: 5d9434268c8d7b3903544ab357caadcb69150be82d921c2de2916f4191b1280c0c3b981de285425d969bd1693b02974be4a844447576e3975ed0280c877be3c0
6
+ metadata.gz: 8eb1685bd731c6904126e0ea75a5f4489f268e97f080df60a6eec9ce3ae79cf7c3477ff5cf4add6541a7b09c21455c1d97ababcc1ad818f119e79ac6faf97e56
7
+ data.tar.gz: 9d5c9882b1eb73901e253f64915c3647c38453357560d9ec836a29163aa3b6f24a10b485b1007751e3600f81a4e4a9cb8eb0a37204af35d87cc216934b7530c2
data/CHANGELOG.md CHANGED
@@ -2,6 +2,34 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ## 1.3.1 (2023-05-12)
6
+
7
+ - Fix gRPC health check response for an empty service. ([@palkan][])
8
+
9
+ As per [docs](https://github.com/grpc/grpc/blob/master/doc/health-checking.md), an empty service is used as the key for server's overall health status. So, if we serve any services, we should return `SERVING` status.
10
+
11
+ - Add gRPC health check when using grpc_kit. ([@palkan][])
12
+
13
+ ## 1.3.0 (2023-02-28)
14
+
15
+ - Add configuration presets. ([@palkan][])
16
+
17
+ Provide sensible defaults matching current platform (e.g., Fly.io).
18
+
19
+ - Require Ruby 2.7+ and Anyway Config 2.2+.
20
+
21
+ - Add `rpc_max_connection_age` option (if favour of `rpc_server_args.max_connection_age_ms`) and configured its **default value to be 300 (5 minutes)**. ([@palkan][])
22
+
23
+ - (_Experimental_) Add support for [grpc_kit](https://github.com/cookpad/grpc_kit) as an alternative gRPC implementation. ([@palkan][], [@cylon-v][])
24
+
25
+ Add `grpc_kit` to your Gemfile and specify `ANYCABLE_GRPC_IMPL=grpc_kit` env var to use it.
26
+
27
+ - Setting the redis driver to ruby specified. ([@smasry][])
28
+
29
+ - Add mutual TLS support for connections to Redis. ([@Envek][])
30
+
31
+ `ANYCABLE_REDIS_TLS_CLIENT_CERT_PATH` and `ANYCABLE_REDIS_TLS_CLIENT_KEY_PATH` settings to specify client certificate and key when connecting to Redis server that requires clients to authenticate themselves.
32
+
5
33
  ## 1.2.5 (2022-12-01)
6
34
 
7
35
  - Add `ANYCABLE_REDIS_TLS_VERIFY` setting to disable validation of Redis server TLS certificate. ([@Envek][])
@@ -132,7 +160,6 @@ See [#71](https://github.com/anycable/anycable/pull/71).
132
160
  See [Changelog](https://github.com/anycable/anycable/blob/0-6-stable/CHANGELOG.md) for versions <1.0.0.
133
161
 
134
162
  [@palkan]: https://github.com/palkan
135
- [@sponomarev]: https://github.com/sponomarev
136
- [@bibendi]: https://github.com/bibendi
137
163
  [@smasry]: https://github.com/smasry
138
164
  [@Envek]: https://github.com/Envek
165
+ [@cylon-v]: https://github.com/cylon-v
data/MIT-LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright 2017-2022 Vladimir Dementyev
1
+ Copyright 2017-2023 Vladimir Dementyev
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.md CHANGED
@@ -1,5 +1,6 @@
1
1
  [![Gem Version](https://badge.fury.io/rb/anycable.svg)](https://rubygems.org/gems/anycable)
2
2
  [![Build](https://github.com/anycable/anycable/workflows/Build/badge.svg)](https://github.com/anycable/anycable/actions)
3
+ [![Coverage Status](https://coveralls.io/repos/github/anycable/anycable/badge.svg?branch=master)](https://coveralls.io/github/anycable/anycable?branch=master)
3
4
  [![Documentation](https://img.shields.io/badge/docs-link-brightgreen.svg)](https://docs.anycable.io/v1)
4
5
 
5
6
  # AnyCable
@@ -18,8 +19,8 @@ AnyCable uses the same protocol as ActionCable, so you can use its [JavaScript c
18
19
 
19
20
  ## Requirements
20
21
 
21
- - Ruby >= 2.6
22
- - Redis (for broadcasting **in production**, [discuss other options](https://github.com/anycable/anycable/issues/2) with us!)
22
+ - Ruby >= 2.7
23
+ - Redis or NATS (for broadcasting **in production**, [discuss other options](https://github.com/anycable/anycable/issues/2) with us!)
23
24
 
24
25
  ## Usage
25
26
 
@@ -27,6 +28,8 @@ Check out our 📑 [Documentation](https://docs.anycable.io/v1).
27
28
 
28
29
  ## Links
29
30
 
31
+ - [AnyCable off Rails: connecting Twilio streams with Hanami](https://evilmartians.com/chronicles/anycable-goes-off-rails-connecting-twilio-streams-with-hanami)
32
+
30
33
  - [AnyCable 1.0: Four years of real-time web with Ruby and Go](https://evilmartians.com/chronicles/anycable-1-0-four-years-of-real-time-web-with-ruby-and-go)
31
34
 
32
35
  - [AnyCable: Action Cable on steroids!](https://evilmartians.com/chronicles/anycable-actioncable-on-steroids)
@@ -118,7 +118,7 @@ module AnyCable
118
118
  yield http
119
119
  rescue Timeout::Error, *RECOVERABLE_EXCEPTIONS => e
120
120
  retry_count += 1
121
- if MAX_ATTEMPTS < retry_count
121
+ if retry_count > MAX_ATTEMPTS
122
122
  logger.error("Broadcast request failed: #{e.message}")
123
123
  return
124
124
  end
@@ -31,6 +31,7 @@ module AnyCable
31
31
  )
32
32
  options = AnyCable.config.to_nats_params.merge(options)
33
33
  @nats_conn = ::NATS.connect(nil, options)
34
+ setup_listeners(nats_conn)
34
35
  @channel = channel
35
36
  end
36
37
 
@@ -41,6 +42,19 @@ module AnyCable
41
42
  def announce!
42
43
  logger.info "Broadcasting NATS channel: #{channel}"
43
44
  end
45
+
46
+ private
47
+
48
+ def setup_listeners(nats_client)
49
+ nats_client.on_disconnect { logger.info "NATS client disconnected" }
50
+ nats_client.on_reconnect do
51
+ info = nats_client.server_info
52
+ logger.info "NATS client reconnected: host=#{info[:host]}:#{info[:port]} cluster=#{info[:cluster]}"
53
+ end
54
+ nats_client.on_error do |err|
55
+ logger.warn "NATS client error: #{err.message}"
56
+ end
57
+ end
44
58
  end
45
59
  end
46
60
  end
@@ -21,7 +21,7 @@ module AnyCable
21
21
  #
22
22
  # You can override these params:
23
23
  #
24
- # AnyCable.broadcast_adapter = :redis, url: "redis://my_redis", channel: "_any_cable_"
24
+ # AnyCable.broadcast_adapter = :redis, { url: "redis://my_redis", channel: "_any_cable_" }
25
25
  class Redis < Base
26
26
  attr_reader :redis_conn, :channel
27
27
 
@@ -30,7 +30,7 @@ module AnyCable
30
30
  **options
31
31
  )
32
32
  options = AnyCable.config.to_redis_params.merge(options)
33
- options[:driver] = :ruby
33
+ options[:driver] ||= :ruby
34
34
  @redis_conn = ::Redis.new(**options)
35
35
  @channel = channel
36
36
  end
@@ -3,7 +3,7 @@
3
3
  require "anycable/broadcast_adapters/base"
4
4
 
5
5
  module AnyCable
6
- module BroadcastAdapters # :nodoc:
6
+ module BroadcastAdapters
7
7
  module_function
8
8
 
9
9
  def lookup_adapter(args)
data/lib/anycable/cli.rb CHANGED
@@ -333,7 +333,7 @@ module AnyCable
333
333
  $ anycable [options]
334
334
 
335
335
  CLI
336
- -r, --require=path Location of application file to require, default: "config/environment.rb"
336
+ -r, --require=path Location of application file to require, default candidates: #{APP_CANDIDATES.join(", ")}
337
337
  --server-command=command Command to run WebSocket server
338
338
  -v, --version Print version and exit
339
339
  -h, --help Show this help
@@ -21,6 +21,8 @@ module AnyCable
21
21
  config_name :anycable
22
22
 
23
23
  attr_config(
24
+ presets: "",
25
+
24
26
  ## PubSub
25
27
  broadcast_adapter: :redis,
26
28
 
@@ -29,6 +31,8 @@ module AnyCable
29
31
  redis_sentinels: nil,
30
32
  redis_channel: "__anycable__",
31
33
  redis_tls_verify: false,
34
+ redis_tls_client_cert_path: nil,
35
+ redis_tls_client_key_path: nil,
32
36
 
33
37
  ### NATS options
34
38
  nats_servers: "nats://localhost:4222",
@@ -54,23 +58,21 @@ module AnyCable
54
58
  sid_header_enabled: true
55
59
  )
56
60
 
57
- if respond_to?(:coerce_types)
58
- coerce_types(
59
- redis_sentinels: {type: nil, array: true},
60
- nats_servers: {type: nil, array: true},
61
- redis_tls_verify: :boolean,
62
- nats_dont_randomize_servers: :boolean,
63
- debug: :boolean,
64
- version_check_enabled: :boolean
65
- )
66
- end
61
+ coerce_types(
62
+ presets: {type: nil, array: true},
63
+ redis_sentinels: {type: nil, array: true},
64
+ nats_servers: {type: nil, array: true},
65
+ redis_tls_verify: :boolean,
66
+ nats_dont_randomize_servers: :boolean,
67
+ debug: :boolean,
68
+ version_check_enabled: :boolean
69
+ )
67
70
 
68
71
  flag_options :debug, :nats_dont_randomize_servers
69
72
  ignore_options :nats_options
70
73
 
71
- on_load do
72
- # @type self : AnyCable::Config
73
- self.debug = debug != false
74
+ def load(*)
75
+ super.tap { load_presets }
74
76
  end
75
77
 
76
78
  def log_level
@@ -97,6 +99,8 @@ module AnyCable
97
99
  --redis-channel=name Redis channel for broadcasting, default: "__anycable__"
98
100
  --redis-sentinels=<...hosts> Redis Sentinel followers addresses (as a comma-separated list), default: nil
99
101
  --redis-tls-verify=yes|no Whether to perform server certificate check in case of rediss:// protocol. Default: yes
102
+ --redis-tls-client_cert-path=path Default: nil
103
+ --redis-tls-client_key-path=path Default: nil
100
104
 
101
105
  NATS PUB/SUB
102
106
  --nats-servers=<...addresses> NATS servers for pub/sub, default: "nats://localhost:4222"
@@ -122,9 +126,23 @@ module AnyCable
122
126
 
123
127
  params[:sentinels] = sentinels.map { |sentinel| parse_sentinel(sentinel) }
124
128
  end.tap do |params|
125
- next unless redis_url.match?(/rediss:\/\//) && !redis_tls_verify?
126
-
127
- params[:ssl_params] = {verify_mode: OpenSSL::SSL::VERIFY_NONE}
129
+ next unless redis_url.match?(/rediss:\/\//)
130
+
131
+ if !!redis_tls_client_cert_path ^ !!redis_tls_client_key_path
132
+ raise_validation_error "Both Redis TLS client certificate and private key must be specified (or none of them)"
133
+ end
134
+
135
+ if !redis_tls_verify?
136
+ params[:ssl_params] = {verify_mode: OpenSSL::SSL::VERIFY_NONE}
137
+ else
138
+ cert_path, key_path = redis_tls_client_cert_path, redis_tls_client_key_path
139
+ if cert_path && key_path
140
+ params[:ssl_params] = {
141
+ cert: OpenSSL::X509::Certificate.new(File.read(cert_path)),
142
+ key: OpenSSL::PKey.read(File.read(key_path))
143
+ }
144
+ end
145
+ end
128
146
  end
129
147
  end
130
148
 
@@ -155,5 +173,42 @@ module AnyCable
155
173
  opts[:password] = uri.password if uri.password
156
174
  end
157
175
  end
176
+
177
+ def load_presets
178
+ if presets.nil? || presets.empty?
179
+ self.presets = detect_presets
180
+ __trace__&.record_value(presets, :presets, type: :env)
181
+ end
182
+
183
+ return if presets.empty?
184
+
185
+ presets.each { send(:"load_#{_1}_presets") if respond_to?(:"load_#{_1}_presets", true) }
186
+ end
187
+
188
+ def detect_presets
189
+ [].tap do
190
+ _1 << "fly" if ENV.key?("FLY_APP_NAME") && ENV.key?("FLY_ALLOC_ID") && ENV.key?("FLY_REGION")
191
+ end
192
+ end
193
+
194
+ def load_fly_presets
195
+ write_preset(:rpc_host, "0.0.0.0:50051", preset: "fly")
196
+
197
+ ws_app_name = ENV["ANYCABLE_FLY_WS_APP_NAME"]
198
+ return unless ws_app_name
199
+
200
+ region = ENV.fetch("FLY_REGION")
201
+
202
+ write_preset(:http_broadcast_url, "http://#{region}.#{ws_app_name}.internal:8090/_broadcast", preset: "fly")
203
+ write_preset(:nats_servers, "nats://#{region}.#{ws_app_name}.internal:4222", preset: "fly")
204
+ end
205
+
206
+ def write_preset(key, value, preset:)
207
+ # do not override explicitly provided values
208
+ return unless __trace__&.dig(key.to_s)&.source&.dig(:type) == :defaults
209
+
210
+ write_config_attr(key, value)
211
+ __trace__&.record_value(value, key, type: :preset, preset: preset)
212
+ end
158
213
  end
159
214
  end
@@ -3,13 +3,12 @@
3
3
  AnyCable::Config.attr_config(
4
4
  ### gRPC options
5
5
  rpc_host: "127.0.0.1:50051",
6
- # For defaults see https://github.com/grpc/grpc/blob/51f0d35509bcdaba572d422c4f856208162022de/src/ruby/lib/grpc/generic/rpc_server.rb#L186-L216
7
- rpc_pool_size: ::GRPC::RpcServer::DEFAULT_POOL_SIZE,
8
- rpc_max_waiting_requests: ::GRPC::RpcServer::DEFAULT_MAX_WAITING_REQUESTS,
9
- rpc_poll_period: ::GRPC::RpcServer::DEFAULT_POLL_PERIOD,
10
- rpc_pool_keep_alive: ::GRPC::Pool::DEFAULT_KEEP_ALIVE,
11
- # https://github.com/grpc/grpc/blob/f526602bff029b8db50a8d57134d72da33d8a752/include/grpc/impl/codegen/grpc_types.h#L141-L351
6
+ rpc_pool_size: 30,
7
+ rpc_max_waiting_requests: 20,
8
+ rpc_poll_period: 1,
9
+ rpc_pool_keep_alive: 0.25,
12
10
  rpc_server_args: {},
11
+ rpc_max_connection_age: 300,
13
12
  log_grpc: false
14
13
  )
15
14
 
@@ -33,7 +32,7 @@ module AnyCable
33
32
  max_waiting_requests: rpc_max_waiting_requests,
34
33
  poll_period: rpc_poll_period,
35
34
  pool_keep_alive: rpc_pool_keep_alive,
36
- server_args: normalized_grpc_server_args
35
+ server_args: enhance_grpc_server_args(normalized_grpc_server_args)
37
36
  }
38
37
  end
39
38
 
@@ -46,6 +45,14 @@ module AnyCable
46
45
  skey.start_with?("grpc.") ? skey : "grpc.#{skey}"
47
46
  end
48
47
  end
48
+
49
+ def enhance_grpc_server_args(opts)
50
+ return opts if opts.key?("grpc.max_connection_age_ms")
51
+ return opts unless rpc_max_connection_age.to_i > 0
52
+
53
+ opts["grpc.max_connection_age_ms"] = rpc_max_connection_age.to_i * 1000
54
+ opts
55
+ end
49
56
  end
50
57
  end
51
58
  end
@@ -98,7 +98,7 @@ module AnyCable
98
98
  )
99
99
  health_checker.add_status(
100
100
  "",
101
- Grpc::Health::V1::HealthCheckResponse::ServingStatus::NOT_SERVING
101
+ Grpc::Health::V1::HealthCheckResponse::ServingStatus::SERVING
102
102
  )
103
103
  health_checker
104
104
  end
@@ -0,0 +1,72 @@
1
+ # Copyright 2015 gRPC authors.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ module Grpc
16
+ # Health contains classes and modules that support providing a health check
17
+ # service.
18
+ module Health
19
+ # Checker is implementation of the schema-specified health checking service.
20
+ class Checker < V1::Health::Service
21
+ StatusCodes = GRPC::Core::StatusCodes
22
+ HealthCheckResponse = V1::HealthCheckResponse
23
+
24
+ # Initializes the statuses of participating services
25
+ def initialize
26
+ @statuses = {}
27
+ @status_mutex = Mutex.new # guards access to @statuses
28
+ end
29
+
30
+ # Implements the rpc IDL API method
31
+ def check(req, _call)
32
+ status = nil
33
+ @status_mutex.synchronize do
34
+ status = @statuses["#{req.service}"]
35
+ end
36
+ if status.nil?
37
+ fail GRPC::NotFound.new("Service is not found: #{req.service}")
38
+ end
39
+ HealthCheckResponse.new(status: status)
40
+ end
41
+
42
+ # Adds the health status for a given service.
43
+ def add_status(service, status)
44
+ @status_mutex.synchronize { @statuses["#{service}"] = status }
45
+ end
46
+
47
+ # Adds given health status for all given services
48
+ def set_status_for_services(status, *services)
49
+ @status_mutex.synchronize do
50
+ services.each { |service| @statuses["#{service}"] = status }
51
+ end
52
+ end
53
+
54
+ # Adds health status for each service given within hash
55
+ def add_statuses(service_statuses = {})
56
+ @status_mutex.synchronize do
57
+ service_statuses.each_pair { |service, status| @statuses["#{service}"] = status }
58
+ end
59
+ end
60
+
61
+ # Clears the status for the given service.
62
+ def clear_status(service)
63
+ @status_mutex.synchronize { @statuses.delete("#{service}") }
64
+ end
65
+
66
+ # Clears alls the statuses.
67
+ def clear_all
68
+ @status_mutex.synchronize { @statuses = {} }
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
4
+ # source: grpc/health/v1/health.proto
5
+
6
+ require 'google/protobuf'
7
+
8
+ Google::Protobuf::DescriptorPool.generated_pool.build do
9
+ add_message "grpc.health.v1.HealthCheckRequest" do
10
+ optional :service, :string, 1
11
+ end
12
+ add_message "grpc.health.v1.HealthCheckResponse" do
13
+ optional :status, :enum, 1, "grpc.health.v1.HealthCheckResponse.ServingStatus"
14
+ end
15
+ add_enum "grpc.health.v1.HealthCheckResponse.ServingStatus" do
16
+ value :UNKNOWN, 0
17
+ value :SERVING, 1
18
+ value :NOT_SERVING, 2
19
+ end
20
+ end
21
+
22
+ module Grpc
23
+ module Health
24
+ module V1
25
+ HealthCheckRequest = Google::Protobuf::DescriptorPool.generated_pool.lookup("grpc.health.v1.HealthCheckRequest").msgclass
26
+ HealthCheckResponse = Google::Protobuf::DescriptorPool.generated_pool.lookup("grpc.health.v1.HealthCheckResponse").msgclass
27
+ HealthCheckResponse::ServingStatus = Google::Protobuf::DescriptorPool.generated_pool.lookup("grpc.health.v1.HealthCheckResponse.ServingStatus").enummodule
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,41 @@
1
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
2
+ # Source: grpc/health/v1/health.proto for package 'grpc.health.v1'
3
+ # Original file comments:
4
+ # Copyright 2015 The gRPC Authors
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+ # The canonical version of this proto can be found at
19
+ # https://github.com/grpc/grpc-proto/blob/master/grpc/health/v1/health.proto
20
+ #
21
+
22
+ module Grpc
23
+ module Health
24
+ module V1
25
+ module Health
26
+ class Service
27
+
28
+ include GRPC::GenericService
29
+
30
+ self.marshal_class_method = :encode
31
+ self.unmarshal_class_method = :decode
32
+ self.service_name = 'grpc.health.v1.Health'
33
+
34
+ rpc :Check, HealthCheckRequest, HealthCheckResponse
35
+ end
36
+
37
+ Stub = Service.rpc_stub_class
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "anycable/grpc/handler"
4
+
5
+ require_relative "./health_pb"
6
+ require_relative "./health_services_pb"
7
+ require_relative "./health_checker"
8
+
9
+ module AnyCable
10
+ module GRPC
11
+ raise LoadError, "AnyCable::GRPC::Server has been already loaded!" if defined?(AnyCable::GRPC::Server)
12
+
13
+ using(Module.new do
14
+ refine ::GrpcKit::Server do
15
+ attr_reader :max_pool_size
16
+
17
+ def stopped?
18
+ @stopping
19
+ end
20
+ end
21
+ end)
22
+
23
+ # Wrapper over gRPC kit server.
24
+ #
25
+ # Basic example:
26
+ #
27
+ # # create new server listening on the loopback interface with 50051 port
28
+ # server = AnyCable::GrpcKit::Server.new(host: "127.0.0.1:50051")
29
+ #
30
+ # # run gRPC server in bakground
31
+ # server.start
32
+ #
33
+ # # stop server
34
+ # server.stop
35
+ class Server
36
+ attr_reader :grpc_server, :host, :hostname, :port, :sock
37
+
38
+ def initialize(host:, logger: nil, **options)
39
+ @logger = logger
40
+ @host = host
41
+
42
+ host_parts = host.match(/\A(?<hostname>.+):(?<port>\d{2,5})\z/)
43
+
44
+ @hostname = host_parts[:hostname]
45
+ @port = host_parts[:port].to_i
46
+
47
+ @grpc_server = build_server(**options)
48
+ end
49
+
50
+ # Start gRPC server in background and
51
+ # wait untill it ready to accept connections
52
+ def start
53
+ return if running?
54
+
55
+ raise "Cannot re-start stopped server" if stopped?
56
+
57
+ logger.info "RPC server (grpc_kit) is starting..."
58
+
59
+ @sock = TCPServer.new(hostname, port)
60
+
61
+ server = grpc_server
62
+
63
+ @start_thread = Thread.new do
64
+ loop do
65
+ conn = @sock.accept
66
+ server.run(conn)
67
+ rescue IOError
68
+ # ignore broken connections
69
+ end
70
+ end
71
+
72
+ wait_till_running
73
+
74
+ logger.info "RPC server is listening on #{host} (workers_num: #{grpc_server.max_pool_size})"
75
+ end
76
+
77
+ def wait_till_running
78
+ raise "Server is not running" unless running?
79
+
80
+ timeout = 5
81
+
82
+ loop do
83
+ sock = TCPSocket.new(hostname, port, connect_timeout: 1)
84
+ stub = ::Grpc::Health::V1::Health::Stub.new(sock)
85
+ stub.check(::Grpc::Health::V1::HealthCheckRequest.new)
86
+ sock.close
87
+ break
88
+ rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH, SocketError
89
+ timeout -= 1
90
+ raise "Server is not responding" if timeout.zero?
91
+ end
92
+ end
93
+
94
+ def wait_till_terminated
95
+ raise "Server is not running" unless running?
96
+
97
+ start_thread.join
98
+ end
99
+
100
+ # Stop gRPC server if it's running
101
+ def stop
102
+ return unless running?
103
+
104
+ return if stopped?
105
+
106
+ grpc_server.graceful_shutdown
107
+ sock.close
108
+
109
+ logger.info "RPC server stopped"
110
+ end
111
+
112
+ def running?
113
+ !!sock
114
+ end
115
+
116
+ def stopped?
117
+ grpc_server.stopped?
118
+ end
119
+
120
+ private
121
+
122
+ attr_reader :start_thread
123
+
124
+ def logger
125
+ @logger ||= AnyCable.logger
126
+ end
127
+
128
+ def build_server(**options)
129
+ pool_size = options[:pool_size]
130
+
131
+ ::GrpcKit::Server.new(min_pool_size: pool_size, max_pool_size: pool_size).tap do |server|
132
+ server.handle(AnyCable::GRPC::Handler)
133
+ server.handle(build_health_checker)
134
+ end
135
+ end
136
+
137
+ def build_health_checker
138
+ health_checker = ::Grpc::Health::Checker.new
139
+ health_checker.add_status(
140
+ "anycable.RPC",
141
+ ::Grpc::Health::V1::HealthCheckResponse::ServingStatus::SERVING
142
+ )
143
+ health_checker.add_status(
144
+ "",
145
+ ::Grpc::Health::V1::HealthCheckResponse::ServingStatus::SERVING
146
+ )
147
+ health_checker
148
+ end
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "grpc_kit"
4
+
5
+ module AnyCable
6
+ module GRPC
7
+ end
8
+ end
9
+
10
+ require "anycable/grpc/config"
11
+ require "anycable/grpc_kit/server"
12
+
13
+ AnyCable.server_builder = ->(config) {
14
+ AnyCable.logger.info "gRPC Kit version: #{::GrpcKit::VERSION}"
15
+
16
+ ::GrpcKit.loglevel = :fatal
17
+ ::GrpcKit.logger = AnyCable.logger if config.log_grpc?
18
+
19
+ params = config.to_grpc_params
20
+
21
+ AnyCable::GRPC::Server.new(**params, host: config.rpc_host)
22
+ }
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AnyCable
4
- VERSION = "1.2.5"
4
+ VERSION = "1.3.1"
5
5
  end
data/lib/anycable.rb CHANGED
@@ -109,11 +109,23 @@ module AnyCable
109
109
  end
110
110
  end
111
111
 
112
- # gRPC is the default for now, so, let's try to load it.
113
- begin
114
- require "anycable/grpc"
115
- rescue LoadError => e
116
- # Re-raise an exception if we failed to load grpc .so files
117
- # (e.g., on Alpine Linux)
118
- raise if /(error loading shared library|incompatible architecture)/i.match?(e.message)
112
+ # Try loading a gRPC implementation
113
+ impl = ENV.fetch("ANYCABLE_GRPC_IMPL", "grpc")
114
+
115
+ case impl
116
+ when "grpc"
117
+ begin
118
+ require "grpc/version"
119
+ require "anycable/grpc"
120
+ rescue LoadError => e
121
+ # Re-raise an exception if we failed to load grpc .so files
122
+ # (e.g., on Alpine Linux)
123
+ raise if /(error loading shared library|incompatible architecture)/i.match?(e.message)
124
+ end
125
+ when "grpc_kit"
126
+ begin
127
+ require "grpc_kit/version"
128
+ require "anycable/grpc_kit"
129
+ rescue LoadError
130
+ end
119
131
  end
@@ -5,6 +5,10 @@ module AnyCable
5
5
  attr_reader channel: String
6
6
 
7
7
  def initialize: (?channel: String channel, **untyped options) -> void
8
+
9
+ private
10
+
11
+ def setup_listeners: (untyped nats_conn) -> void
8
12
  end
9
13
  end
10
14
  end
@@ -1,5 +1,7 @@
1
1
  module AnyCable
2
2
  interface _Config
3
+ def presets: () -> Array[String]
4
+ def presets=: (Array[String]) -> void
3
5
  def broadcast_adapter: () -> Symbol
4
6
  def broadcast_adapter=: (Symbol) -> void
5
7
  def redis_url: () -> String
@@ -9,14 +11,19 @@ module AnyCable
9
11
  def redis_channel: () -> String
10
12
  def redis_channel=: (String) -> void
11
13
  def redis_tls_verify: () -> bool
12
- def redis_tls_verify?: () -> bool
13
14
  def redis_tls_verify=: (bool) -> void
15
+ def redis_tls_verify?: () -> bool
16
+ def redis_tls_client_cert_path: () -> String?
17
+ def redis_tls_client_cert_path=: (String) -> void
18
+ def redis_tls_client_key_path: () -> String?
19
+ def redis_tls_client_key_path=: (String) -> void
14
20
  def nats_servers: () -> Array[String]
15
21
  def nats_servers=: (Array[String]) -> void
16
22
  def nats_channel: () -> String
17
23
  def nats_channel=: (String) -> void
18
24
  def nats_dont_randomize_servers: () -> bool
19
25
  def nats_dont_randomize_servers=: (bool) -> void
26
+ def nats_dont_randomize_servers?: () -> bool
20
27
  def nats_options: () -> Hash[untyped, untyped]
21
28
  def nats_options=: (Hash[untyped, untyped]) -> void
22
29
  def http_broadcast_url: () -> String
@@ -29,12 +36,17 @@ module AnyCable
29
36
  def log_level=: (String) -> void
30
37
  def debug: () -> bool
31
38
  def debug=: (bool) -> void
39
+ def debug?: () -> bool
32
40
  def http_health_port: () -> Integer?
33
41
  def http_health_port=: (Integer) -> void
34
42
  def http_health_path: () -> String
35
43
  def http_health_path=: (String) -> void
36
44
  def version_check_enabled: () -> bool
37
45
  def version_check_enabled=: (bool) -> void
46
+ def version_check_enabled?: () -> bool
47
+ def sid_header_enabled: () -> bool
48
+ def sid_header_enabled=: (bool) -> void
49
+ def sid_header_enabled?: () -> bool
38
50
  end
39
51
 
40
52
  class Config < Anyway::Config
@@ -46,6 +58,7 @@ module AnyCable
46
58
  alias debug? debug
47
59
  alias version_check_enabled? version_check_enabled
48
60
 
61
+ def load: (*untyped) -> void
49
62
  def http_health_port_provided?: () -> bool
50
63
  def to_redis_params: () -> { url: String, sentinels: Array[untyped]?, ssl_params: Hash[Symbol, untyped]? }
51
64
  def to_http_health_params: () -> { port: Integer?, path: String }
@@ -54,5 +67,10 @@ module AnyCable
54
67
  private
55
68
 
56
69
  def parse_sentinel: ((String | Hash[untyped, untyped]) sentinel) -> Hash[Symbol, untyped]
70
+ def load_presets: () -> void
71
+ def detect_presets: () -> Array[String]
72
+ def write_preset: (Symbol, untyped, preset: String) -> void
73
+ def write_config_attr: (Symbol, untyped) -> void
74
+ def __trace__: () -> untyped
57
75
  end
58
76
  end
@@ -7,30 +7,38 @@ module AnyCable
7
7
  def rpc_pool_size=: (Integer) -> void
8
8
  def rpc_max_waiting_requests: () -> Integer
9
9
  def rpc_max_waiting_requests=: (Integer) -> void
10
- def rpc_poll_period: () -> Integer
11
- def rpc_poll_period=: (Integer) -> void
12
- def rpc_pool_keep_alive: () -> Integer
13
- def rpc_pool_keep_alive=: (Integer) -> void
10
+ def rpc_poll_period: () -> Numeric
11
+ def rpc_poll_period=: (Numeric) -> void
12
+ def rpc_pool_keep_alive: () -> Numeric
13
+ def rpc_pool_keep_alive=: (Numeric) -> void
14
+ def rpc_max_connection_age: () -> Integer
15
+ def rpc_max_connection_age=: (Integer) -> void
14
16
  def rpc_server_args: () -> Hash[Symbol | String, untyped]?
15
17
  def rpc_server_args=: (Hash[Symbol | String, untyped]) -> void
16
18
  def log_grpc: () -> bool
17
19
  def log_grpc=: (bool) -> void
20
+ def log_grpc?: () -> bool
18
21
  end
19
22
 
20
- module Config : AnyCable::Config
23
+ module Config : AnyCable::_Config
21
24
  include _Config
22
25
 
23
- alias log_grpc? log_grpc
24
-
25
26
  def to_grpc_params: () -> {
26
27
  pool_size: Integer,
27
28
  max_waiting_requests: Integer,
28
- poll_period: Integer,
29
- pool_keep_alive: Integer,
29
+ poll_period: Numeric,
30
+ pool_keep_alive: Numeric,
30
31
  server_args: Hash[String, untyped]
31
32
  }
32
33
 
33
34
  def normalized_grpc_server_args: () -> Hash[String, untyped]
35
+ def enhance_grpc_server_args: (Hash[String, untyped]) -> Hash[String, untyped]
34
36
  end
35
37
  end
36
38
  end
39
+
40
+ module AnyCable
41
+ class Config
42
+ include GRPC::Config
43
+ end
44
+ end
@@ -6,7 +6,7 @@ module AnyCable
6
6
  attr_reader grpc_server: untyped
7
7
  attr_reader host: String
8
8
 
9
- def initialize: (host: String, ?logger: Logger, **untyped options) -> void
9
+ def initialize: (host: String, ?logger: Logger?, **untyped options) -> void
10
10
 
11
11
  private
12
12
 
@@ -12,7 +12,7 @@ module AnyCable
12
12
  attr_reader path: String
13
13
  attr_reader http_server: untyped
14
14
 
15
- def initialize: (_Runnable server, port: Integer port, ?logger: Logger logger, ?path: String path) -> void
15
+ def initialize: (_Runnable server, port: Integer port, ?logger: Logger? logger, ?path: String path) -> void
16
16
  def start: () -> void
17
17
  def stop: () -> void
18
18
  def running?: () -> bool
@@ -1,5 +1,5 @@
1
1
  module AnyCable
2
2
  class Middleware
3
- def call: (Symbol, rpcRequest, rpcMetadata) { (Symbol, rpcRequest, rpcMetadata) -> rpcResponse } -> rpcResponse
3
+ def call: (Symbol, rpcRequest, rpcMetadata) { () -> rpcResponse } -> rpcResponse
4
4
  end
5
5
  end
data/sig/anycable/rpc.rbs CHANGED
@@ -12,6 +12,12 @@ module AnyCable
12
12
  type protoMap = _ProtobufMap
13
13
  type rpcMetadata = Hash[String, String]
14
14
 
15
+ module Status
16
+ SUCCESS: 0
17
+ FAILURE: 1
18
+ ERROR: 2
19
+ end
20
+
15
21
  interface _WithEnvState
16
22
  def cstate: () -> protoMap?
17
23
  def cstate=: (protoMap) -> void
metadata CHANGED
@@ -1,29 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: anycable-core
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.5
4
+ version: 1.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - palkan
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-12-01 00:00:00.000000000 Z
11
+ date: 2023-05-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: anyway_config
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ">="
17
+ - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 2.1.0
19
+ version: '2.2'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - ">="
24
+ - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: 2.1.0
26
+ version: '2.2'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: google-protobuf
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -122,6 +122,34 @@ dependencies:
122
122
  - - ">="
123
123
  - !ruby/object:Gem::Version
124
124
  version: '3.5'
125
+ - !ruby/object:Gem::Dependency
126
+ name: simplecov
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: simplecov-lcov
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
125
153
  - !ruby/object:Gem::Dependency
126
154
  name: webmock
127
155
  requirement: !ruby/object:Gem::Requirement
@@ -181,6 +209,11 @@ files:
181
209
  - lib/anycable/grpc/handler.rb
182
210
  - lib/anycable/grpc/rpc_services_pb.rb
183
211
  - lib/anycable/grpc/server.rb
212
+ - lib/anycable/grpc_kit.rb
213
+ - lib/anycable/grpc_kit/health_checker.rb
214
+ - lib/anycable/grpc_kit/health_pb.rb
215
+ - lib/anycable/grpc_kit/health_services_pb.rb
216
+ - lib/anycable/grpc_kit/server.rb
184
217
  - lib/anycable/health_server.rb
185
218
  - lib/anycable/middleware.rb
186
219
  - lib/anycable/middleware_chain.rb
@@ -241,14 +274,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
241
274
  requirements:
242
275
  - - ">="
243
276
  - !ruby/object:Gem::Version
244
- version: 2.6.0
277
+ version: 2.7.0
245
278
  required_rubygems_version: !ruby/object:Gem::Requirement
246
279
  requirements:
247
280
  - - ">="
248
281
  - !ruby/object:Gem::Version
249
282
  version: '0'
250
283
  requirements: []
251
- rubygems_version: 3.3.11
284
+ rubygems_version: 3.4.8
252
285
  signing_key:
253
286
  specification_version: 4
254
287
  summary: AnyCable core RPC implementation