anycable-core 1.2.5 → 1.3.1

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