anycable-core 1.1.0.pre1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +84 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +78 -0
  5. data/bin/anycable +13 -0
  6. data/bin/anycabled +30 -0
  7. data/bin/console +7 -0
  8. data/bin/setup +6 -0
  9. data/lib/anycable.rb +114 -0
  10. data/lib/anycable/broadcast_adapters.rb +34 -0
  11. data/lib/anycable/broadcast_adapters/base.rb +29 -0
  12. data/lib/anycable/broadcast_adapters/http.rb +131 -0
  13. data/lib/anycable/broadcast_adapters/redis.rb +46 -0
  14. data/lib/anycable/cli.rb +319 -0
  15. data/lib/anycable/config.rb +127 -0
  16. data/lib/anycable/exceptions_handling.rb +35 -0
  17. data/lib/anycable/grpc.rb +30 -0
  18. data/lib/anycable/grpc/check_version.rb +33 -0
  19. data/lib/anycable/grpc/config.rb +53 -0
  20. data/lib/anycable/grpc/handler.rb +25 -0
  21. data/lib/anycable/grpc/rpc_services_pb.rb +24 -0
  22. data/lib/anycable/grpc/server.rb +103 -0
  23. data/lib/anycable/health_server.rb +73 -0
  24. data/lib/anycable/middleware.rb +10 -0
  25. data/lib/anycable/middleware_chain.rb +74 -0
  26. data/lib/anycable/middlewares/exceptions.rb +35 -0
  27. data/lib/anycable/protos/rpc_pb.rb +74 -0
  28. data/lib/anycable/rpc.rb +91 -0
  29. data/lib/anycable/rpc/handler.rb +50 -0
  30. data/lib/anycable/rpc/handlers/command.rb +36 -0
  31. data/lib/anycable/rpc/handlers/connect.rb +33 -0
  32. data/lib/anycable/rpc/handlers/disconnect.rb +27 -0
  33. data/lib/anycable/rspec.rb +4 -0
  34. data/lib/anycable/rspec/rpc_command_context.rb +21 -0
  35. data/lib/anycable/socket.rb +169 -0
  36. data/lib/anycable/version.rb +5 -0
  37. data/sig/anycable.rbs +37 -0
  38. data/sig/anycable/broadcast_adapters.rbs +5 -0
  39. data/sig/anycable/cli.rbs +40 -0
  40. data/sig/anycable/config.rbs +46 -0
  41. data/sig/anycable/exceptions_handling.rbs +14 -0
  42. data/sig/anycable/health_server.rbs +21 -0
  43. data/sig/anycable/middleware.rbs +5 -0
  44. data/sig/anycable/middleware_chain.rbs +22 -0
  45. data/sig/anycable/rpc.rbs +143 -0
  46. data/sig/anycable/socket.rbs +40 -0
  47. data/sig/anycable/version.rbs +3 -0
  48. metadata +237 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 0f3906d5bd75c90a70af60b0ce10ab49c5940bc7818aa90f4e2af61831b9d5f2
4
+ data.tar.gz: 3a5b9eea11ea0769d5dcfebd8721c3cf0d6925b470d636f895c37e8da82818a4
5
+ SHA512:
6
+ metadata.gz: 116b8c9a943c5785773f4286115cb01955e188bcc45e9583986db08cbbc7c945817280217df07956d2ca3bc739ad95a1120b7f1bf7a19655b19c1cccd49d290a
7
+ data.tar.gz: aacd6b3e0dda79471150f6305dde49ec1efafdc05fae3c4e9da7ccaf1fa9dfb9c1d7554ac3154e74104b99807f2400f0da465f53da2dcd283870715609384e3f
data/CHANGELOG.md ADDED
@@ -0,0 +1,84 @@
1
+ # Change log
2
+
3
+ ## master
4
+
5
+ ## 1.1.0-dev
6
+
7
+ - **BREAKING** Move middlewares from gRPC interceptors to custom implementation. ([@palkan][])
8
+
9
+ That allowed us to have _real_ middlewares with ability to modify responses, intercept exceptions, etc.
10
+ The API changed a bit:
11
+
12
+ ```diff
13
+ class SomeMiddleware < AnyCable::Middleware
14
+ - def call(request, rpc_call, rpc_handler)
15
+ + def call(rpc_method_name, request)
16
+ yield
17
+ end
18
+ end
19
+ ```
20
+
21
+ - **Ruby >= 2.6** is required.
22
+ - **Anyway Config >= 2.1** is required.
23
+
24
+ ## 1.0.3 (2021-03-05)
25
+
26
+ - Ruby 3.0 compatibility. ([@palkan][])
27
+
28
+ ## 1.0.2 (2021-01-05)
29
+
30
+ - Handle TLS Redis connections by using VERIFY_NONE mode. ([@palkan][])
31
+
32
+ ## 1.0.1 (2020-07-07)
33
+
34
+ - Support providing passwords for Redis Sentinels. ([@palkan][])
35
+
36
+ Use the following format: `ANYCABLE_REDIS_SENTINELS=:password1@my.redis.sentinel.first:26380,:password2@my.redis.sentinel.second:26380`.
37
+
38
+ ## 1.0.0 (2020-07-01)
39
+
40
+ - Add `embedded` option to CLI runner. ([@palkan][])
41
+
42
+ - Add `Env#istate` and `EnvResponse#istate` to store channel state. ([@palkan][])
43
+
44
+ That would allow to mimic instance variables usage in Action Cable channels.
45
+
46
+ - Add `CommandResponse#stopped_streams` to support unsubscribing from particular broadcastings. ([@palkan])
47
+
48
+ `Socket#unsubscribe` is now implemented as well.
49
+
50
+ - Add `AnyCable.broadcast_adapter#broadcast_command` method. ([@palkan][])
51
+
52
+ It could be used to send commands to WS server (e.g., remote disconnect).
53
+
54
+ - Add `:http` broadcasting adapter. ([@palkan][])
55
+
56
+ - **RPC schema has changed**. ([@palkan][])
57
+
58
+ Using `anycable-go` v1.x is required.
59
+
60
+ - **Ruby 2.5+ is required**. ([@palkan][])
61
+
62
+ - Added RPC proto version check. ([@palkan][])
63
+
64
+ Server must sent `protov` metadata with the supported versions (comma-separated list). If there is no matching version an exception is raised.
65
+
66
+ Current RPC proto version is **v1**.
67
+
68
+ - Added `request` support to channels. ([@palkan][])
69
+
70
+ Now you can access `request` object in channels, too (e.g., to read headers/cookies/URL/etc).
71
+
72
+ - Change default server address from `[::]:50051` to `127.0.0.1:50051`. ([@palkan][])
73
+
74
+ See [#71](https://github.com/anycable/anycable/pull/71).
75
+
76
+ - Fix building Redis Sentinel config. ([@palkan][])
77
+
78
+ ---
79
+
80
+ See [Changelog](https://github.com/anycable/anycable/blob/0-6-stable/CHANGELOG.md) for versions <1.0.0.
81
+
82
+ [@palkan]: https://github.com/palkan
83
+ [@sponomarev]: https://github.com/sponomarev
84
+ [@bibendi]: https://github.com/bibendi
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2017-2021 Vladimir Dementyev
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,78 @@
1
+ [![Gem Version](https://badge.fury.io/rb/anycable.svg)](https://rubygems.org/gems/anycable)
2
+ [![Build](https://github.com/anycable/anycable/workflows/Build/badge.svg)](https://github.com/anycable/anycable/actions)
3
+ [![Gitter](https://img.shields.io/badge/gitter-join%20chat%20%E2%86%92-brightgreen.svg)](https://gitter.im/anycable/Lobby)
4
+ [![Documentation](https://img.shields.io/badge/docs-link-brightgreen.svg)](https://docs.anycable.io/v1)
5
+
6
+ # AnyCable
7
+
8
+ <img align="right" height="150" width="129"
9
+ title="AnyCable logo" src="https://docs.anycable.io/assets/images/logo.svg">
10
+
11
+ AnyCable allows you to use any WebSocket server (written in any language) as a replacement for your Ruby server (such as Faye, Action Cable, etc).
12
+
13
+ AnyCable uses the same protocol as ActionCable, so you can use its [JavaScript client](https://www.npmjs.com/package/actioncable) without any monkey-patching.
14
+
15
+ <a href="https://evilmartians.com/">
16
+ <img src="https://evilmartians.com/badges/sponsored-by-evil-martians.svg" alt="Sponsored by Evil Martians" width="236" height="54"></a>
17
+
18
+ ## Requirements
19
+
20
+ - Ruby >= 2.6
21
+ - Redis (for broadcasting **in production**, [discuss other options](https://github.com/anycable/anycable/issues/2) with us!)
22
+
23
+ ## Usage
24
+
25
+ Check out our 📑 [Documentation](https://docs.anycable.io/v1).
26
+
27
+ ## Links
28
+
29
+ - [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)
30
+
31
+ - [AnyCable: Action Cable on steroids!](https://evilmartians.com/chronicles/anycable-actioncable-on-steroids)
32
+
33
+ - [Connecting LiteCable to Hanami](http://gabrielmalakias.com.br/ruby/hanami/iot/2017/05/26/websockets-connecting-litecable-to-hanami.html) by [@GabrielMalakias](https://github.com/GabrielMalakias)
34
+
35
+ - [From Action to Any](https://medium.com/@leshchuk/from-action-to-any-1e8d863dd4cf) by [@alekseyl](https://github.com/alekseyl)
36
+
37
+ ## Talks
38
+
39
+ - High-speed cables for Ruby, RubyConf 2018, [slides](https://speakerdeck.com/palkan/rubyconf-2018-high-speed-cables-for-ruby) and [video](https://www.youtube.com/watch?v=8XRcOZXOzV4) (EN)
40
+
41
+ - One cable to rule them all, RubyKaigi 2018, [slides](https://speakerdeck.com/palkan/rubykaigi-2018-anycable-one-cable-to-rule-them-all) and [video](https://www.youtube.com/watch?v=jXCPuNICT8s) (EN)
42
+
43
+ - Wroc_Love.rb 2018 [slides](https://speakerdeck.com/palkan/wroc-love-dot-rb-2018-cables-cables-cables) and [video](https://www.youtube.com/watch?v=AUxFFOehiy0) (EN)
44
+
45
+ - RubyConfMY 2017 [slides](https://speakerdeck.com/palkan/rubyconf-malaysia-2017-anycable) and [video](https://www.youtube.com/watch?v=j5oFx525zNw) (EN)
46
+
47
+ - RailsClub Moscow 2016 [slides](https://speakerdeck.com/palkan/railsclub-moscow-2016-anycable) and [video](https://www.youtube.com/watch?v=-k7GQKuBevY&list=PLiWUIs1hSNeOXZhotgDX7Y7qBsr24cu7o&index=4) (RU)
48
+
49
+ ## Building
50
+
51
+ ### Generating gRPC files from `.proto`
52
+
53
+ - Install required GRPC gems:
54
+
55
+ ```sh
56
+ gem install grpc
57
+ gem install grpc-tools
58
+ ```
59
+
60
+ - Re-generate GRPC files (if necessary):
61
+
62
+ ```sh
63
+ make
64
+ ```
65
+
66
+ ## Contributing
67
+
68
+ Bug reports and pull requests are welcome on GitHub at [https://github.com/anycable/anycable](https://github.com/anycable/anycable).
69
+
70
+ Please, provide reproduction script (using [this template](https://github.com/anycable/anycable/blob/master/etc/bug_report_template.rb)) when submitting bugs if possible.
71
+
72
+ ## License
73
+
74
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
75
+
76
+ ## Security Contact
77
+
78
+ To report a security vulnerability, please contact us at `anycable@evilmartians.com`. We will coordinate the fix and disclosure.
data/bin/anycable ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "anycable/cli"
4
+
5
+ begin
6
+ cli = AnyCable::CLI.new
7
+ cli.run(ARGV)
8
+ rescue => e
9
+ raise e if $DEBUG
10
+ STDERR.puts e.message
11
+ STDERR.puts e.backtrace.join("\n")
12
+ exit 1
13
+ end
data/bin/anycabled ADDED
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "anycable/cli"
4
+
5
+ begin
6
+ require "daemons"
7
+ rescue LoadError
8
+ raise <<~MSG
9
+ You need to add gem 'daemons' to your Gemfile if you want to use `anycabled`:
10
+
11
+ # Gemfile
12
+ gem "daemons", "~> 1.3", require: false
13
+ MSG
14
+ end
15
+
16
+ options = {
17
+ dir: "tmp/pids",
18
+ log_output: false
19
+ }
20
+
21
+ # Preserve current directory. We need it inside the server.
22
+ current_dir = Dir.pwd
23
+
24
+ # Clean ARGV from daemon command and args
25
+ _, _, args = Daemons::Controller.split_argv(ARGV)
26
+
27
+ Daemons.run_proc("anycable", options) do
28
+ Dir.chdir current_dir
29
+ AnyCable::CLI.new.run(args)
30
+ end
data/bin/console ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "anycable"
5
+
6
+ require "pry"
7
+ Pry.start
data/bin/setup ADDED
@@ -0,0 +1,6 @@
1
+ #!/bin/sh
2
+
3
+ set -e
4
+
5
+ gem install bundler --conservative
6
+ bundle check || bundle install
data/lib/anycable.rb ADDED
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "anycable/version"
4
+ require "anycable/config"
5
+ require "logger"
6
+
7
+ require "anycable/exceptions_handling"
8
+ require "anycable/broadcast_adapters"
9
+
10
+ require "anycable/middleware_chain"
11
+ require "anycable/middlewares/exceptions"
12
+
13
+ require "anycable/socket"
14
+ require "anycable/rpc"
15
+ require "anycable/health_server"
16
+
17
+ # AnyCable allows to use any websocket service (written in any language) as a replacement
18
+ # for ActionCable server.
19
+ #
20
+ # AnyCable includes an RPC server (gRPC by default), which is used by external WS server to execute commands
21
+ # (authentication, subscription authorization, client-to-server messages).
22
+ #
23
+ # Broadcasting messages to WS is done through _broadcast adapter_ (Redis Pub/Sub by default).
24
+ module AnyCable
25
+ class << self
26
+ # Provide connection factory which
27
+ # is a callable object with build
28
+ # a Connection object
29
+ attr_accessor :connection_factory
30
+
31
+ # Provide a method to build a server to serve RPC
32
+ attr_accessor :server_builder
33
+
34
+ attr_writer :logger, :rpc_handler
35
+
36
+ attr_reader :middleware
37
+
38
+ def logger
39
+ return @logger if instance_variable_defined?(:@logger)
40
+
41
+ log_output = AnyCable.config.log_file || $stdout
42
+ @logger = Logger.new(log_output).tap do |logger|
43
+ logger.level = AnyCable.config.log_level
44
+ end
45
+ end
46
+
47
+ def config
48
+ @config ||= Config.new
49
+ end
50
+
51
+ def configure
52
+ yield(config) if block_given?
53
+ end
54
+
55
+ # Register a custom block that will be called
56
+ # when an exception is raised during RPC call
57
+ def capture_exception(&block)
58
+ ExceptionsHandling << block
59
+ end
60
+
61
+ # Register a callback to be invoked before
62
+ # the server starts
63
+ def configure_server(&block)
64
+ server_callbacks << block
65
+ end
66
+
67
+ def server_callbacks
68
+ @server_callbacks ||= []
69
+ end
70
+
71
+ def broadcast_adapter
72
+ self.broadcast_adapter = AnyCable.config.broadcast_adapter.to_sym unless instance_variable_defined?(:@broadcast_adapter)
73
+ @broadcast_adapter
74
+ end
75
+
76
+ def broadcast_adapter=(adapter)
77
+ if adapter.is_a?(Symbol) || adapter.is_a?(Array)
78
+ adapter = BroadcastAdapters.lookup_adapter(adapter)
79
+ end
80
+
81
+ unless adapter.respond_to?(:broadcast)
82
+ raise ArgumentError, "BroadcastAdapter must implement #broadcast method. " \
83
+ "#{adapter.class} doesn't implement it."
84
+ end
85
+
86
+ @broadcast_adapter = adapter
87
+ end
88
+
89
+ # Raw broadcast message to the channel, sends only string!
90
+ # To send hash or object use ActionCable.server.broadcast instead!
91
+ def broadcast(channel, payload)
92
+ broadcast_adapter.broadcast(channel, payload)
93
+ end
94
+
95
+ def rpc_handler
96
+ @rpc_handler ||= AnyCable::RPC::Handler.new
97
+ end
98
+
99
+ private
100
+
101
+ attr_writer :middleware
102
+ end
103
+
104
+ self.middleware = MiddlewareChain.new.tap do |chain|
105
+ # Include exceptions handling middleware by default
106
+ chain.use(Middlewares::Exceptions)
107
+ end
108
+ end
109
+
110
+ # gRPC is the default for now, so, let's try to load it.
111
+ begin
112
+ require "anycable/grpc"
113
+ rescue LoadError
114
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "anycable/broadcast_adapters/base"
4
+
5
+ module AnyCable
6
+ module BroadcastAdapters # :nodoc:
7
+ module_function
8
+
9
+ def lookup_adapter(args)
10
+ adapter, options = Array(args)
11
+ path_to_adapter = "anycable/broadcast_adapters/#{adapter}"
12
+ adapter_class_name = adapter.to_s.split("_").map(&:capitalize).join
13
+
14
+ unless BroadcastAdapters.const_defined?(adapter_class_name, false)
15
+ begin
16
+ require path_to_adapter
17
+ rescue LoadError => e
18
+ # We couldn't require the adapter itself.
19
+ if e.path == path_to_adapter
20
+ raise e.class, "Couldn't load the '#{adapter}' broadcast adapter for AnyCable",
21
+ e.backtrace || []
22
+ # Bubbled up from the adapter require.
23
+ else
24
+ raise e.class, "Error loading the '#{adapter}' broadcast adapter for AnyCable",
25
+ e.backtrace || []
26
+ end
27
+ end
28
+ end
29
+
30
+ options ||= {}
31
+ BroadcastAdapters.const_get(adapter_class_name, false).new(**options)
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,29 @@
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
@@ -0,0 +1,131 @@
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
+ # @type break: nil
87
+ break if msg == :stop
88
+
89
+ handle_response perform_request(msg)
90
+ end
91
+ end
92
+ end
93
+
94
+ def perform_request(payload)
95
+ build_http do |http|
96
+ req = Net::HTTP::Post.new(url, {"Content-Type" => "application/json"}.merge(headers))
97
+ req.body = payload
98
+ http.request(req)
99
+ end
100
+ end
101
+
102
+ def handle_response(response)
103
+ return unless response
104
+ return if Net::HTTPCreated === response
105
+
106
+ logger.error "Broadcast request responded with unexpected status: #{response.code}"
107
+ end
108
+
109
+ def build_http
110
+ retry_count = 0
111
+
112
+ begin
113
+ http = Net::HTTP.new(uri.host, uri.port)
114
+ http.open_timeout = OPEN_TIMEOUT
115
+ http.read_timeout = READ_TIMEOUT
116
+ http.use_ssl = url.match?(/^https/)
117
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
118
+ yield http
119
+ rescue Timeout::Error, *RECOVERABLE_EXCEPTIONS => e
120
+ retry_count += 1
121
+ return logger.error("Broadcast request failed: #{e.message}") if MAX_ATTEMPTS < retry_count
122
+
123
+ sleep((DELAY**retry_count).to_int * retry_count)
124
+ retry
125
+ ensure
126
+ http.finish if http.started?
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end