anycable 1.0.3 → 1.1.0.pre1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,29 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module AnyCable
4
- module BroadcastAdapters
5
- class Base
6
- def raw_broadcast(_data)
7
- raise NotImplementedError
8
- end
9
-
10
- def broadcast(stream, payload)
11
- raw_broadcast({stream: stream, data: payload}.to_json)
12
- end
13
-
14
- def broadcast_command(command, **payload)
15
- raw_broadcast({command: command, payload: payload}.to_json)
16
- end
17
-
18
- def announce!
19
- logger.info "Broadcasting via #{self.class.name}"
20
- end
21
-
22
- private
23
-
24
- def logger
25
- AnyCable.logger
26
- end
27
- end
28
- end
29
- end
@@ -1,130 +0,0 @@
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_method :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
@@ -1,43 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- gem "redis", ">= 3"
4
-
5
- require "redis"
6
- require "json"
7
-
8
- module AnyCable
9
- module BroadcastAdapters
10
- # Redis adapter for broadcasting.
11
- #
12
- # Example:
13
- #
14
- # AnyCable.broadast_adapter = :redis
15
- #
16
- # It uses Redis configuration from global AnyCable config
17
- # by default.
18
- #
19
- # You can override these params:
20
- #
21
- # AnyCable.broadcast_adapter = :redis, url: "redis://my_redis", channel: "_any_cable_"
22
- class Redis < Base
23
- attr_reader :redis_conn, :channel
24
-
25
- def initialize(
26
- channel: AnyCable.config.redis_channel,
27
- **options
28
- )
29
- options = AnyCable.config.to_redis_params.merge(options)
30
- @redis_conn = ::Redis.new(options)
31
- @channel = channel
32
- end
33
-
34
- def raw_broadcast(payload)
35
- redis_conn.publish(channel, payload)
36
- end
37
-
38
- def announce!
39
- logger.info "Broadcasting Redis channel: #{channel}"
40
- end
41
- end
42
- end
43
- end
data/lib/anycable/cli.rb DELETED
@@ -1,353 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "optparse"
4
-
5
- require "anycable"
6
-
7
- $stdout.sync = true
8
-
9
- module AnyCable
10
- # Command-line interface for running AnyCable gRPC server
11
- # rubocop:disable Metrics/ClassLength
12
- class CLI
13
- # (not-so-big) List of common boot files for
14
- # different applications
15
- APP_CANDIDATES = %w[
16
- ./config/anycable.rb
17
- ./config/environment.rb
18
- ].freeze
19
-
20
- # Wait for external process termination (s)
21
- WAIT_PROCESS = 2
22
-
23
- attr_reader :server, :health_server, :embedded
24
- alias_method :embedded?, :embedded
25
-
26
- def initialize(embedded: false)
27
- @embedded = embedded
28
- end
29
-
30
- # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
31
- def run(args = [])
32
- @at_stop = []
33
-
34
- extra_options = parse_cli_options!(args)
35
-
36
- # Boot app first, 'cause it might change
37
- # configuration, loggin settings, etc.
38
- boot_app! unless embedded?
39
-
40
- parse_gem_options!(extra_options)
41
-
42
- configure_server!
43
-
44
- logger.info "Starting AnyCable gRPC server (pid: #{Process.pid}, workers_num: #{config.rpc_pool_size})"
45
-
46
- print_versions!
47
-
48
- logger.info "Serving #{defined?(::Rails) ? "Rails " : ""}application from #{boot_file}" unless embedded?
49
-
50
- verify_connection_factory!
51
-
52
- log_grpc! if config.log_grpc
53
-
54
- log_errors!
55
-
56
- use_version_check! if config.version_check_enabled?
57
-
58
- @server = AnyCable::Server.new(
59
- host: config.rpc_host,
60
- **config.to_grpc_params,
61
- interceptors: AnyCable.middleware.to_a
62
- )
63
-
64
- # Make sure middlewares are not adding after server has started
65
- AnyCable.middleware.freeze
66
-
67
- start_health_server! if config.http_health_port_provided?
68
- start_pubsub!
69
-
70
- server.start
71
-
72
- run_custom_server_command! unless server_command.nil?
73
-
74
- return if embedded?
75
-
76
- begin
77
- wait_till_terminated
78
- rescue Interrupt => e
79
- logger.info "Stopping... #{e.message}"
80
-
81
- shutdown
82
-
83
- logger.info "Stopped. Good-bye!"
84
- exit(0)
85
- end
86
- end
87
- # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
88
-
89
- def shutdown
90
- at_stop.each(&:call)
91
- server&.stop
92
- end
93
-
94
- private
95
-
96
- attr_reader :boot_file, :server_command
97
-
98
- def config
99
- AnyCable.config
100
- end
101
-
102
- def logger
103
- AnyCable.logger
104
- end
105
-
106
- def at_stop(&block)
107
- if block
108
- @at_stop << block
109
- else
110
- @at_stop
111
- end
112
- end
113
-
114
- def wait_till_terminated
115
- self_read = setup_signals
116
-
117
- while readable_io = IO.select([self_read]) # rubocop:disable Lint/AssignmentInCondition
118
- signal = readable_io.first[0].gets.strip
119
- raise Interrupt, "SIG#{signal} received"
120
- end
121
- end
122
-
123
- def setup_signals
124
- self_read, self_write = IO.pipe
125
-
126
- %w[INT TERM].each do |signal|
127
- trap signal do
128
- self_write.puts signal
129
- end
130
- end
131
-
132
- self_read
133
- end
134
-
135
- def print_versions!
136
- logger.info "AnyCable version: #{AnyCable::VERSION} (proto_version: #{AnyCable::PROTO_VERSION})"
137
- logger.info "gRPC version: #{GRPC::VERSION}"
138
- end
139
-
140
- # rubocop:disable Metrics/MethodLength
141
- def boot_app!
142
- @boot_file ||= try_detect_app
143
-
144
- if boot_file.nil?
145
- $stdout.puts(
146
- "Couldn't find an application to load. " \
147
- "Please specify the explicit path via -r option, e.g:" \
148
- " anycable -r ./config/boot.rb or anycable -r /app/config/load_me.rb"
149
- )
150
- exit(1)
151
- end
152
-
153
- begin
154
- require boot_file
155
- rescue LoadError => e
156
- $stdout.puts(
157
- "Failed to load application: #{e.message}. " \
158
- "Please specify the explicit path via -r option, e.g:" \
159
- " anycable -r ./config/boot.rb or anycable -r /app/config/load_me.rb"
160
- )
161
- exit(1)
162
- end
163
- end
164
- # rubocop:enable Metrics/MethodLength
165
-
166
- def try_detect_app
167
- APP_CANDIDATES.detect { |path| File.exist?(path) }
168
- end
169
-
170
- def configure_server!
171
- AnyCable.server_callbacks.each(&:call)
172
- end
173
-
174
- def use_version_check!
175
- require "anycable/middlewares/check_version"
176
-
177
- AnyCable.middleware.use(
178
- AnyCable::Middlewares::CheckVersion.new(AnyCable::PROTO_VERSION)
179
- )
180
- end
181
-
182
- def start_health_server!
183
- @health_server = AnyCable::HealthServer.new(
184
- server,
185
- **config.to_http_health_params
186
- )
187
- health_server.start
188
-
189
- at_stop { health_server.stop }
190
- end
191
-
192
- def start_pubsub!
193
- AnyCable.broadcast_adapter.announce!
194
- end
195
-
196
- # rubocop: disable Metrics/MethodLength, Metrics/AbcSize
197
- def run_custom_server_command!
198
- pid = nil
199
- stopped = false
200
- command_thread = Thread.new do
201
- pid = Process.spawn(server_command)
202
- logger.info "Started command: #{server_command} (pid: #{pid})"
203
-
204
- Process.wait pid
205
- pid = nil
206
- raise Interrupt, "Server command exit unexpectedly" unless stopped
207
- end
208
-
209
- command_thread.abort_on_exception = true
210
-
211
- at_stop do
212
- stopped = true
213
- next if pid.nil?
214
-
215
- Process.kill("SIGTERM", pid)
216
-
217
- logger.info "Wait till process #{pid} stop..."
218
-
219
- tick = 0
220
-
221
- loop do
222
- tick += 0.2
223
- break if tick > WAIT_PROCESS
224
-
225
- if pid.nil?
226
- logger.info "Process #{pid} stopped."
227
- break
228
- end
229
- end
230
- end
231
- end
232
- # rubocop: enable Metrics/MethodLength, Metrics/AbcSize
233
-
234
- def log_grpc!
235
- ::GRPC.define_singleton_method(:logger) { AnyCable.logger }
236
- end
237
-
238
- # Add default exceptions handler: print error message to log
239
- def log_errors!
240
- if AnyCable.config.debug?
241
- # Print error with backtrace in debug mode
242
- AnyCable.capture_exception do |e|
243
- AnyCable.logger.error("#{e.message}:\n#{e.backtrace.take(20).join("\n")}")
244
- end
245
- else
246
- AnyCable.capture_exception { |e| AnyCable.logger.error(e.message) }
247
- end
248
- end
249
-
250
- def verify_connection_factory!
251
- return if AnyCable.connection_factory
252
-
253
- logger.error "AnyCable connection factory must be configured. " \
254
- "Make sure you've required a gem (e.g. `anycable-rails`) or " \
255
- "configured `AnyCable.connection_factory` yourself"
256
- exit(1)
257
- end
258
-
259
- def parse_gem_options!(args)
260
- config.parse_options!(args)
261
- rescue OptionParser::InvalidOption => e
262
- $stdout.puts e.message
263
- $stdout.puts "Run anycable -h to see available options"
264
- exit(1)
265
- end
266
-
267
- # rubocop:disable Metrics/MethodLength
268
- def parse_cli_options!(args)
269
- unknown_opts = []
270
-
271
- parser = build_cli_parser
272
-
273
- begin
274
- parser.parse!(args)
275
- rescue OptionParser::InvalidOption => e
276
- unknown_opts << e.args[0]
277
- unless args.size.zero?
278
- unknown_opts << args.shift unless args.first.start_with?("-")
279
- retry
280
- end
281
- end
282
-
283
- unknown_opts
284
- end
285
-
286
- def build_cli_parser
287
- OptionParser.new do |o|
288
- o.on "-v", "--version", "Print version and exit" do |_arg|
289
- $stdout.puts "AnyCable v#{AnyCable::VERSION}"
290
- exit(0)
291
- end
292
-
293
- o.on "-r", "--require [PATH|DIR]", "Location of application file to require" do |arg|
294
- @boot_file = arg
295
- end
296
-
297
- o.on "--server-command VALUE", "Command to run WebSocket server" do |arg|
298
- @server_command = arg
299
- end
300
-
301
- o.on_tail "-h", "--help", "Show help" do
302
- $stdout.puts usage
303
- exit(0)
304
- end
305
- end
306
- end
307
- # rubocop:enable Metrics/MethodLength
308
-
309
- def usage
310
- <<~HELP
311
- anycable: run AnyCable gRPC server (https://anycable.io)
312
-
313
- VERSION
314
- anycable/#{AnyCable::VERSION}
315
-
316
- USAGE
317
- $ anycable [options]
318
-
319
- BASIC OPTIONS
320
- -r, --require=path Location of application file to require, default: "config/environment.rb"
321
- --server-command=command Command to run WebSocket server
322
- --rpc-host=host Local address to run gRPC server on, default: "[::]:50051"
323
- --broadcast-adapter=type Pub/sub adapter type for broadcasts, default: redis
324
- --log-level=level Logging level, default: "info"
325
- --log-file=path Path to log file, default: <none> (log to STDOUT)
326
- --log-grpc Enable gRPC logging (disabled by default)
327
- --debug Turn on verbose logging ("debug" level and gRPC logging on)
328
- -v, --version Print version and exit
329
- -h, --help Show this help
330
-
331
- REDIS PUB/SUB OPTIONS
332
- --redis-url=url Redis URL for pub/sub, default: REDIS_URL or "redis://localhost:6379/5"
333
- --redis-channel=name Redis channel for broadcasting, default: "__anycable__"
334
- --redis-sentinels=<...hosts> Redis Sentinel followers addresses (as a comma-separated list), default: nil
335
-
336
- HTTP PUB/SUB OPTIONS
337
- --http-broadcast-url HTTP pub/sub endpoint URL, default: "http://localhost:8090/_broadcast"
338
- --http-broadcast-secret HTTP pub/sub authorization secret, default: <none> (disabled)
339
-
340
- HTTP HEALTH CHECKER OPTIONS
341
- --http-health-port=port Port to run HTTP health server on, default: <none> (disabled)
342
- --http-health-path=path Endpoint to server health cheks, default: "/health"
343
-
344
- GRPC OPTIONS
345
- --rpc-pool-size=size gRPC workers pool size, default: 30
346
- --rpc-max-waiting-requests=num Max waiting requests queue size, default: 20
347
- --rpc-poll-period=seconds Poll period (sec), default: 1
348
- --rpc-pool-keep-alive=seconds Keep-alive polling interval (sec), default: 1
349
- HELP
350
- end
351
- end
352
- # rubocop:enable Metrics/ClassLength
353
- end