anycable 0.5.2 → 0.6.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/ISSUE_TEMPLATE.md +25 -0
- data/.github/PULL_REQUEST_TEMPLATE.md +31 -0
- data/.rubocop.yml +22 -22
- data/.travis.yml +1 -2
- data/CHANGELOG.md +92 -0
- data/README.md +10 -58
- data/anycable.gemspec +10 -7
- data/benchmarks/.gitignore +1 -0
- data/benchmarks/2018-10-27.md +181 -0
- data/benchmarks/assets/2018-10-27-action-cable-rss.png +0 -0
- data/benchmarks/assets/2018-10-27-action-cable-rtt.png +0 -0
- data/benchmarks/assets/2018-10-27-anycable-rss.png +0 -0
- data/benchmarks/assets/2018-10-27-anycable-rtt.png +0 -0
- data/benchmarks/assets/2018-10-27-async-rss.png +0 -0
- data/benchmarks/assets/2018-10-27-async-rtt.png +0 -0
- data/benchmarks/assets/2018-10-27-falcon-cable-rss.png +0 -0
- data/benchmarks/assets/2018-10-27-falcon-cable-rtt.png +0 -0
- data/benchmarks/assets/2018-10-27-iodine-cable-rss.png +0 -0
- data/benchmarks/assets/2018-10-27-iodine-cable-rtt.png +0 -0
- data/benchmarks/assets/2018-10-27-plezi-rss.png +0 -0
- data/benchmarks/assets/2018-10-27-plezi-rtt.png +0 -0
- data/benchmarks/bench.png +0 -0
- data/benchmarks/benchmark.yml +12 -10
- data/benchmarks/hosts +2 -2
- data/benchmarks/rtt_plot.py +74 -0
- data/benchmarks/rtt_plot_test.py +16 -0
- data/benchmarks/servers.yml +25 -3
- data/bin/anycable +13 -0
- data/etc/bug_report_template.rb +1 -1
- data/lib/anycable.rb +53 -16
- data/lib/anycable/broadcast_adapters.rb +33 -0
- data/lib/anycable/broadcast_adapters/redis.rb +42 -0
- data/lib/anycable/cli.rb +323 -0
- data/lib/anycable/config.rb +91 -17
- data/lib/anycable/exceptions_handling.rb +31 -0
- data/lib/anycable/handler/capture_exceptions.rb +39 -0
- data/lib/anycable/health_server.rb +53 -31
- data/lib/anycable/middleware.rb +19 -0
- data/lib/anycable/middleware_chain.rb +58 -0
- data/lib/anycable/rpc/rpc_pb.rb +1 -1
- data/lib/anycable/rpc/rpc_services_pb.rb +1 -1
- data/lib/anycable/rpc_handler.rb +28 -26
- data/lib/anycable/server.rb +114 -39
- data/lib/anycable/socket.rb +1 -1
- data/lib/anycable/version.rb +2 -2
- metadata +45 -26
- data/lib/anycable/handler/exceptions_handling.rb +0 -43
- data/lib/anycable/pubsub.rb +0 -26
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
data/benchmarks/benchmark.yml
CHANGED
@@ -5,16 +5,18 @@
|
|
5
5
|
remote_user: ubuntu
|
6
6
|
gather_facts: False
|
7
7
|
vars:
|
8
|
-
server_host: '172.31.
|
8
|
+
server_host: '172.31.21.207'
|
9
9
|
hostname: ws-bench-client
|
10
|
-
local_ips: ['172.31.
|
11
|
-
local_ips_str: '-l 172.31.
|
12
|
-
|
10
|
+
local_ips: ['172.31.19.119', '172.31.19.120', '172.31.17.121', '172.31.17.122']
|
11
|
+
# local_ips_str: '-l 172.31.19.119 -l 172.31.19.120 -l 172.31.17.121 -l 172.31.17.122'
|
12
|
+
local_ips_str: ''
|
13
|
+
steps: 10
|
13
14
|
step_size: 1000
|
14
15
|
sample_size: 100
|
15
16
|
concurrency: 8
|
16
17
|
payload_size: 200
|
17
18
|
prepare: False
|
19
|
+
log_file: "./last_bench.log"
|
18
20
|
tasks:
|
19
21
|
- name: Prepare the machine
|
20
22
|
tags: prepare
|
@@ -41,15 +43,15 @@
|
|
41
43
|
chdir: /webapps/anycable_bench
|
42
44
|
ignore_errors: yes
|
43
45
|
|
44
|
-
- name: Print
|
46
|
+
- name: Print base command
|
45
47
|
debug: msg="bin/websocket-bench broadcast {{ local_ips_str }} --concurrent {{ concurrency }} --sample-size {{ sample_size }} --step-size {{ step_size }} --payload-padding {{ payload_size }} --total-steps {{ steps }} ws://{{ server_host }}:3334/cable"
|
46
|
-
tags:
|
48
|
+
tags: base
|
47
49
|
|
48
|
-
- name:
|
50
|
+
- name: Base benchmark
|
49
51
|
become_user: deplo
|
50
52
|
shell: bin/websocket-bench broadcast {{ local_ips_str }} --concurrent {{ concurrency }} --sample-size {{ sample_size }} --step-size {{ step_size }} --payload-padding {{ payload_size }} --total-steps {{ steps }} ws://{{ server_host }}:3334/cable
|
51
53
|
register: bench
|
52
|
-
tags:
|
54
|
+
tags: base
|
53
55
|
args:
|
54
56
|
chdir: /webapps/anycable_bench
|
55
57
|
ignore_errors: yes
|
@@ -58,10 +60,10 @@
|
|
58
60
|
debug: var=bench.stdout_lines
|
59
61
|
tags:
|
60
62
|
- action_cable
|
61
|
-
-
|
63
|
+
- base
|
62
64
|
|
63
65
|
- name: Benchmark results (stderr)
|
64
66
|
debug: var=bench.stderr_lines
|
65
67
|
tags:
|
66
68
|
- action_cable
|
67
|
-
-
|
69
|
+
- base
|
data/benchmarks/hosts
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
[benchmark]
|
2
|
-
ec2-
|
2
|
+
ec2-18-202-197-244.eu-west-1.compute.amazonaws.com ansible_ssh_private_key_file=/Users/palkan/.ssh/macos-dev
|
3
3
|
|
4
4
|
[servers]
|
5
|
-
ec2-34-
|
5
|
+
ec2-34-255-216-103.eu-west-1.compute.amazonaws.com ansible_ssh_private_key_file=/Users/palkan/.ssh/macos-dev
|
@@ -0,0 +1,74 @@
|
|
1
|
+
#coding: utf-8
|
2
|
+
|
3
|
+
import sys
|
4
|
+
import os
|
5
|
+
import re
|
6
|
+
import argparse
|
7
|
+
|
8
|
+
def process_file(input_file):
|
9
|
+
log = {}
|
10
|
+
log['clients'] = []
|
11
|
+
log['95per'] = []
|
12
|
+
log['min'] = []
|
13
|
+
log['med'] = []
|
14
|
+
log['max'] = []
|
15
|
+
|
16
|
+
for line in input_file:
|
17
|
+
point = parse_line(line)
|
18
|
+
if point:
|
19
|
+
log['clients'].append(point['clients'])
|
20
|
+
log['95per'].append(point['95per'])
|
21
|
+
log['min'].append(point['min'])
|
22
|
+
log['med'].append(point['med'])
|
23
|
+
log['max'].append(point['max'])
|
24
|
+
return log
|
25
|
+
|
26
|
+
def parse_line(line):
|
27
|
+
# clients: 1000 95per-rtt: 1328ms min-rtt: 2ms median-rtt: 457ms max-rtt: 1577ms
|
28
|
+
matches = re.search('clients:\s+(\d+)\s+95per\-rtt:\s+(\d+)ms\s+min\-rtt:\s+(\d+)ms\s+median\-rtt:\s+(\d+)ms\s+max\-rtt:\s+(\d+)ms', line)
|
29
|
+
if matches:
|
30
|
+
return {
|
31
|
+
'clients': int(matches.group(1)),
|
32
|
+
'95per': int(matches.group(2)),
|
33
|
+
'min': int(matches.group(3)),
|
34
|
+
'med': int(matches.group(4)),
|
35
|
+
'max': int(matches.group(5))
|
36
|
+
}
|
37
|
+
return False
|
38
|
+
|
39
|
+
def generate_plot(log, output):
|
40
|
+
import matplotlib.patches as mpatches
|
41
|
+
import matplotlib.pyplot as plt
|
42
|
+
|
43
|
+
with plt.rc_context({'backend': 'Agg'}):
|
44
|
+
|
45
|
+
fig = plt.figure()
|
46
|
+
ax = fig.add_subplot(1, 1, 1)
|
47
|
+
|
48
|
+
ax.plot(log['clients'], log['95per'], '-', lw=1, color='r', label='95 percentile')
|
49
|
+
ax.plot(log['clients'], log['med'], '-', lw=1, color='green', dashes=[10, 5], label='Median')
|
50
|
+
ax.plot(log['clients'], log['max'], '-', lw=1, color='grey', label='Max')
|
51
|
+
|
52
|
+
ax.set_ylabel('RTT ms', color='r')
|
53
|
+
ax.set_xlabel('clients num')
|
54
|
+
ax.set_ylim(0., max(log['max']) * 1.1)
|
55
|
+
|
56
|
+
handles, labels = ax.get_legend_handles_labels()
|
57
|
+
ax.legend(handles, labels, bbox_to_anchor=(0.4, 1))
|
58
|
+
|
59
|
+
ax.grid()
|
60
|
+
|
61
|
+
fig.savefig(output)
|
62
|
+
|
63
|
+
if __name__ == "__main__":
|
64
|
+
parser = argparse.ArgumentParser(description='Generate RTT chart')
|
65
|
+
parser.add_argument('-i', dest='inputfile', type=argparse.FileType('r'), help='input file containing benchmark results', required=True)
|
66
|
+
parser.add_argument('-o', dest='outputfile', type=argparse.FileType('w'), help='output file to write resulted chart PNG', required=True)
|
67
|
+
|
68
|
+
args = parser.parse_args()
|
69
|
+
|
70
|
+
data = process_file(args.inputfile)
|
71
|
+
|
72
|
+
generate_plot(data, args.outputfile)
|
73
|
+
|
74
|
+
print('Done')
|
@@ -0,0 +1,16 @@
|
|
1
|
+
import unittest
|
2
|
+
import os
|
3
|
+
import rtt_plot
|
4
|
+
|
5
|
+
class TestRttPlot(unittest.TestCase):
|
6
|
+
def test_parse_line(self):
|
7
|
+
self.assertEqual(
|
8
|
+
{ 'clients': 1000, '95per': 1328, 'min': 2, 'med': 457, 'max': 1577 },
|
9
|
+
rtt_plot.parse_line(' "clients: 1000 95per-rtt: 1328ms min-rtt: 2ms median-rtt: 457ms max-rtt: 1577ms"')
|
10
|
+
)
|
11
|
+
self.assertFalse(
|
12
|
+
rtt_plot.parse_line('2018/10/28 02:24:13 Missing received broadcasts: expected 23100000, got 23005351')
|
13
|
+
)
|
14
|
+
|
15
|
+
if __name__ == '__main__':
|
16
|
+
unittest.main()
|
data/benchmarks/servers.yml
CHANGED
@@ -13,24 +13,46 @@
|
|
13
13
|
- action_cable
|
14
14
|
- anycable
|
15
15
|
- plezi
|
16
|
+
- iodine_cable
|
17
|
+
- falcon_cable
|
18
|
+
- falcon_async
|
19
|
+
- kill
|
16
20
|
with_items:
|
17
21
|
- "3334"
|
18
22
|
ignore_errors: true
|
19
23
|
- name: Run Action Cable
|
20
24
|
become_user: deplo
|
21
25
|
tags: action_cable
|
22
|
-
|
26
|
+
command: bash -lc "WEB_CONCURRENCY={{ web_concurrency }} bundle exec rails s -p 3334 -e production"
|
23
27
|
args:
|
24
28
|
chdir: /webapps/anycable_bench/ruby/action-cable-server
|
25
29
|
- name: Run Anycable Go
|
26
30
|
become_user: deplo
|
27
31
|
tags: anycable
|
28
|
-
shell: ANYCABLE_GO_BIN="anycable-go-0.6.0" ANYCABLE_PORT="3334" bundle exec bin/anycable
|
32
|
+
shell: bash -lc "ANYCABLE_GO_BIN="anycable-go-0.6.0-alpha" ANYCABLE_PORT="3334" bundle exec bin/anycable"
|
29
33
|
args:
|
30
34
|
chdir: /webapps/anycable_bench/ruby/action-cable-server
|
31
35
|
- name: Run Iodine/Plezi
|
32
36
|
become_user: deplo
|
33
37
|
tags: plezi
|
34
|
-
shell: iodine -p 3334
|
38
|
+
shell: bash -lc "bundle exec iodine -p 3334 -w {{ web_concurrency }} -t 16"
|
35
39
|
args:
|
36
40
|
chdir: /webapps/anycable_bench/ruby/plezi-iodine
|
41
|
+
- name: Run Iodine/ActionCable
|
42
|
+
become_user: deplo
|
43
|
+
tags: iodine_cable
|
44
|
+
shell: bash -lc "RAILS_ENV=production bundle exec iodine -p 3334 -w {{ web_concurrency }} -t 16"
|
45
|
+
args:
|
46
|
+
chdir: /webapps/anycable_bench/ruby/action-cable-server
|
47
|
+
- name: Run Falcon/ActionCable
|
48
|
+
become_user: deplo
|
49
|
+
tags: falcon_cable
|
50
|
+
shell: bash -lc "RAILS_ENV=production bundle exec falcon serve -b http://0.0.0.0:3334"
|
51
|
+
args:
|
52
|
+
chdir: /webapps/anycable_bench/ruby/action-cable-server
|
53
|
+
- name: Run Falcon/Async
|
54
|
+
become_user: deplo
|
55
|
+
tags: falcon_async
|
56
|
+
shell: bash -lc "bundle exec falcon serve -b http://0.0.0.0:3334"
|
57
|
+
args:
|
58
|
+
chdir: /webapps/anycable_bench/ruby/falcon
|
data/bin/anycable
ADDED
data/etc/bug_report_template.rb
CHANGED
@@ -25,7 +25,7 @@ ENV['ANYT_TARGET_URL'] ||= "ws://localhost:8080/cable"
|
|
25
25
|
# Comment this line if you want to run WebSocket server manually
|
26
26
|
ENV['ANYT_COMMAND'] ||= "anycable-go"
|
27
27
|
|
28
|
-
ActionCable.server.config.logger = Rails.logger =
|
28
|
+
ActionCable.server.config.logger = Rails.logger = AnyCable.logger
|
29
29
|
|
30
30
|
# Test scenario
|
31
31
|
feature "issue_xyz" do
|
data/lib/anycable.rb
CHANGED
@@ -4,29 +4,37 @@ require "anycable/version"
|
|
4
4
|
require "anycable/config"
|
5
5
|
require "logger"
|
6
6
|
|
7
|
-
|
7
|
+
require "anycable/exceptions_handling"
|
8
|
+
require "anycable/broadcast_adapters"
|
9
|
+
|
10
|
+
require "anycable/middleware_chain"
|
11
|
+
|
12
|
+
require "anycable/server"
|
13
|
+
|
14
|
+
# AnyCable allows to use any websocket service (written in any language) as a replacement
|
8
15
|
# for ActionCable server.
|
9
16
|
#
|
10
|
-
#
|
17
|
+
# AnyCable includes a gRPC server, which is used by external WS server to execute commands
|
11
18
|
# (authentication, subscription authorization, client-to-server messages).
|
12
19
|
#
|
13
|
-
# Broadcasting messages to WS is done through Redis Pub/Sub.
|
14
|
-
module
|
20
|
+
# Broadcasting messages to WS is done through _broadcast adapter_ (Redis Pub/Sub by default).
|
21
|
+
module AnyCable
|
15
22
|
class << self
|
16
23
|
# Provide connection factory which
|
17
24
|
# is a callable object with build
|
18
25
|
# a Connection object
|
19
26
|
attr_accessor :connection_factory
|
20
27
|
|
21
|
-
|
22
|
-
|
23
|
-
|
28
|
+
attr_writer :logger
|
29
|
+
|
30
|
+
attr_reader :middleware
|
24
31
|
|
25
32
|
def logger
|
26
33
|
return @logger if instance_variable_defined?(:@logger)
|
27
|
-
|
34
|
+
|
35
|
+
log_output = AnyCable.config.log_file || STDOUT
|
28
36
|
@logger = Logger.new(log_output).tap do |logger|
|
29
|
-
logger.level =
|
37
|
+
logger.level = AnyCable.config.log_level
|
30
38
|
end
|
31
39
|
end
|
32
40
|
|
@@ -38,22 +46,51 @@ module Anycable
|
|
38
46
|
yield(config) if block_given?
|
39
47
|
end
|
40
48
|
|
49
|
+
# Register a custom block that will be called
|
50
|
+
# when an exception is raised during gRPC call
|
51
|
+
def capture_exception(&block)
|
52
|
+
ExceptionsHandling << block
|
53
|
+
end
|
54
|
+
|
41
55
|
def error_handlers
|
42
|
-
|
43
|
-
|
56
|
+
warn <<~DEPRECATION
|
57
|
+
Using `AnyCable.error_handlers` is deprecated!
|
58
|
+
Please, use `AnyCable.capture_exception` instead.
|
59
|
+
DEPRECATION
|
60
|
+
ExceptionsHandling
|
44
61
|
end
|
45
62
|
|
46
|
-
def
|
47
|
-
|
63
|
+
def broadcast_adapter
|
64
|
+
self.broadcast_adapter = :redis unless instance_variable_defined?(:@broadcast_adapter)
|
65
|
+
@broadcast_adapter
|
66
|
+
end
|
67
|
+
|
68
|
+
def broadcast_adapter=(adapter)
|
69
|
+
if adapter.is_a?(Symbol) || adapter.is_a?(Array)
|
70
|
+
adapter = BroadcastAdapters.lookup_adapter(adapter)
|
71
|
+
end
|
72
|
+
|
73
|
+
unless adapter.respond_to?(:broadcast)
|
74
|
+
raise ArgumentError, "BroadcastAdapter must implement #broadcast method. " \
|
75
|
+
"#{adapter.class} doesn't implement it."
|
76
|
+
end
|
77
|
+
|
78
|
+
@broadcast_adapter = adapter
|
48
79
|
end
|
49
80
|
|
50
81
|
# Raw broadcast message to the channel, sends only string!
|
51
82
|
# To send hash or object use ActionCable.server.broadcast instead!
|
52
83
|
def broadcast(channel, payload)
|
53
|
-
|
84
|
+
broadcast_adapter.broadcast(channel, payload)
|
54
85
|
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
attr_writer :middleware
|
55
90
|
end
|
91
|
+
|
92
|
+
self.middleware = MiddlewareChain.new
|
56
93
|
end
|
57
94
|
|
58
|
-
|
59
|
-
|
95
|
+
# Backward compatibility
|
96
|
+
Anycable = AnyCable
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AnyCable
|
4
|
+
module BroadcastAdapters # :nodoc:
|
5
|
+
module_function
|
6
|
+
|
7
|
+
# rubocop: disable Metrics/AbcSize, Metrics/MethodLength
|
8
|
+
def lookup_adapter(args)
|
9
|
+
adapter, options = Array(args)
|
10
|
+
path_to_adapter = "anycable/broadcast_adapters/#{adapter}"
|
11
|
+
adapter_class_name = adapter.to_s.split("_").map(&:capitalize).join
|
12
|
+
|
13
|
+
unless BroadcastAdapters.const_defined?(adapter_class_name, false)
|
14
|
+
begin
|
15
|
+
require path_to_adapter
|
16
|
+
rescue LoadError => e
|
17
|
+
# We couldn't require the adapter itself.
|
18
|
+
if e.path == path_to_adapter
|
19
|
+
raise e.class, "Couldn't load the '#{adapter}' broadcast adapter for AnyCable",
|
20
|
+
e.backtrace
|
21
|
+
# Bubbled up from the adapter require.
|
22
|
+
else
|
23
|
+
raise e.class, "Error loading the '#{adapter}' broadcast adapter for AnyCable",
|
24
|
+
e.backtrace
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
BroadcastAdapters.const_get(adapter_class_name, false).new(options || {})
|
30
|
+
end
|
31
|
+
# rubocop: enable Metrics/AbcSize, Metrics/MethodLength
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
gem "redis", ">= 4.0"
|
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
|
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 broadcast(stream, payload)
|
35
|
+
redis_conn.publish(
|
36
|
+
channel,
|
37
|
+
{ stream: stream, data: payload }.to_json
|
38
|
+
)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|