anycable-core 1.1.0.pre1

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