anycable-core 1.1.0.pre1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![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
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
|