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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +84 -0
- data/MIT-LICENSE +20 -0
- data/README.md +78 -0
- data/bin/anycable +13 -0
- data/bin/anycabled +30 -0
- data/bin/console +7 -0
- data/bin/setup +6 -0
- data/lib/anycable.rb +114 -0
- data/lib/anycable/broadcast_adapters.rb +34 -0
- data/lib/anycable/broadcast_adapters/base.rb +29 -0
- data/lib/anycable/broadcast_adapters/http.rb +131 -0
- data/lib/anycable/broadcast_adapters/redis.rb +46 -0
- data/lib/anycable/cli.rb +319 -0
- data/lib/anycable/config.rb +127 -0
- data/lib/anycable/exceptions_handling.rb +35 -0
- data/lib/anycable/grpc.rb +30 -0
- data/lib/anycable/grpc/check_version.rb +33 -0
- data/lib/anycable/grpc/config.rb +53 -0
- data/lib/anycable/grpc/handler.rb +25 -0
- data/lib/anycable/grpc/rpc_services_pb.rb +24 -0
- data/lib/anycable/grpc/server.rb +103 -0
- data/lib/anycable/health_server.rb +73 -0
- data/lib/anycable/middleware.rb +10 -0
- data/lib/anycable/middleware_chain.rb +74 -0
- data/lib/anycable/middlewares/exceptions.rb +35 -0
- data/lib/anycable/protos/rpc_pb.rb +74 -0
- data/lib/anycable/rpc.rb +91 -0
- data/lib/anycable/rpc/handler.rb +50 -0
- data/lib/anycable/rpc/handlers/command.rb +36 -0
- data/lib/anycable/rpc/handlers/connect.rb +33 -0
- data/lib/anycable/rpc/handlers/disconnect.rb +27 -0
- data/lib/anycable/rspec.rb +4 -0
- data/lib/anycable/rspec/rpc_command_context.rb +21 -0
- data/lib/anycable/socket.rb +169 -0
- data/lib/anycable/version.rb +5 -0
- data/sig/anycable.rbs +37 -0
- data/sig/anycable/broadcast_adapters.rbs +5 -0
- data/sig/anycable/cli.rbs +40 -0
- data/sig/anycable/config.rbs +46 -0
- data/sig/anycable/exceptions_handling.rbs +14 -0
- data/sig/anycable/health_server.rbs +21 -0
- data/sig/anycable/middleware.rbs +5 -0
- data/sig/anycable/middleware_chain.rbs +22 -0
- data/sig/anycable/rpc.rbs +143 -0
- data/sig/anycable/socket.rbs +40 -0
- data/sig/anycable/version.rbs +3 -0
- 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
|
+
[](https://rubygems.org/gems/anycable)
|
2
|
+
[](https://github.com/anycable/anycable/actions)
|
3
|
+
[](https://gitter.im/anycable/Lobby)
|
4
|
+
[](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
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
data/bin/setup
ADDED
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
|