anycable 0.5.2 → 0.6.0.rc1
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 +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
|