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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE.md +25 -0
  3. data/.github/PULL_REQUEST_TEMPLATE.md +31 -0
  4. data/.rubocop.yml +22 -22
  5. data/.travis.yml +1 -2
  6. data/CHANGELOG.md +92 -0
  7. data/README.md +10 -58
  8. data/anycable.gemspec +10 -7
  9. data/benchmarks/.gitignore +1 -0
  10. data/benchmarks/2018-10-27.md +181 -0
  11. data/benchmarks/assets/2018-10-27-action-cable-rss.png +0 -0
  12. data/benchmarks/assets/2018-10-27-action-cable-rtt.png +0 -0
  13. data/benchmarks/assets/2018-10-27-anycable-rss.png +0 -0
  14. data/benchmarks/assets/2018-10-27-anycable-rtt.png +0 -0
  15. data/benchmarks/assets/2018-10-27-async-rss.png +0 -0
  16. data/benchmarks/assets/2018-10-27-async-rtt.png +0 -0
  17. data/benchmarks/assets/2018-10-27-falcon-cable-rss.png +0 -0
  18. data/benchmarks/assets/2018-10-27-falcon-cable-rtt.png +0 -0
  19. data/benchmarks/assets/2018-10-27-iodine-cable-rss.png +0 -0
  20. data/benchmarks/assets/2018-10-27-iodine-cable-rtt.png +0 -0
  21. data/benchmarks/assets/2018-10-27-plezi-rss.png +0 -0
  22. data/benchmarks/assets/2018-10-27-plezi-rtt.png +0 -0
  23. data/benchmarks/bench.png +0 -0
  24. data/benchmarks/benchmark.yml +12 -10
  25. data/benchmarks/hosts +2 -2
  26. data/benchmarks/rtt_plot.py +74 -0
  27. data/benchmarks/rtt_plot_test.py +16 -0
  28. data/benchmarks/servers.yml +25 -3
  29. data/bin/anycable +13 -0
  30. data/etc/bug_report_template.rb +1 -1
  31. data/lib/anycable.rb +53 -16
  32. data/lib/anycable/broadcast_adapters.rb +33 -0
  33. data/lib/anycable/broadcast_adapters/redis.rb +42 -0
  34. data/lib/anycable/cli.rb +323 -0
  35. data/lib/anycable/config.rb +91 -17
  36. data/lib/anycable/exceptions_handling.rb +31 -0
  37. data/lib/anycable/handler/capture_exceptions.rb +39 -0
  38. data/lib/anycable/health_server.rb +53 -31
  39. data/lib/anycable/middleware.rb +19 -0
  40. data/lib/anycable/middleware_chain.rb +58 -0
  41. data/lib/anycable/rpc/rpc_pb.rb +1 -1
  42. data/lib/anycable/rpc/rpc_services_pb.rb +1 -1
  43. data/lib/anycable/rpc_handler.rb +28 -26
  44. data/lib/anycable/server.rb +114 -39
  45. data/lib/anycable/socket.rb +1 -1
  46. data/lib/anycable/version.rb +2 -2
  47. metadata +45 -26
  48. data/lib/anycable/handler/exceptions_handling.rb +0 -43
  49. data/lib/anycable/pubsub.rb +0 -26
Binary file
@@ -5,16 +5,18 @@
5
5
  remote_user: ubuntu
6
6
  gather_facts: False
7
7
  vars:
8
- server_host: '172.31.17.82'
8
+ server_host: '172.31.21.207'
9
9
  hostname: ws-bench-client
10
- local_ips: ['172.31.17.191', '172.31.17.192', '172.31.17.193', '172.31.17.194']
11
- local_ips_str: '-l 172.31.17.191 -l 172.31.17.192 -l 172.31.17.193 -l 172.31.17.194'
12
- steps: 20
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 Plezi command
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: plezi
48
+ tags: base
47
49
 
48
- - name: Plezi benchmark
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: plezi
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
- - plezi
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
- - plezi
69
+ - base
data/benchmarks/hosts CHANGED
@@ -1,5 +1,5 @@
1
1
  [benchmark]
2
- ec2-52-19-57-142.eu-west-1.compute.amazonaws.com ansible_ssh_private_key_file=/Users/palkan/.ssh/macos-dev
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-252-33-102.eu-west-1.compute.amazonaws.com ansible_ssh_private_key_file=/Users/palkan/.ssh/macos-dev
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()
@@ -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
- shell: WEB_CONCURRENCY={{ web_concurrency }} bundle exec rails s -p 3334 -e production
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
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative "../lib/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
@@ -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 = Anycable.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
- # Anycable allows to use any websocket service (written in any language) as a replacement
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
- # Anycable includes a gRPC server, which is used by external WS server to execute commands
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 Anycable
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
- def logger=(logger)
22
- @logger = logger
23
- end
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
- log_output = Anycable.config.log_file || STDOUT
34
+
35
+ log_output = AnyCable.config.log_file || STDOUT
28
36
  @logger = Logger.new(log_output).tap do |logger|
29
- logger.level = Anycable.config.log_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
- return @error_handlers if instance_variable_defined?(:@error_handlers)
43
- @error_handlers = []
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 pubsub
47
- @pubsub ||= PubSub.new
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
- pubsub.broadcast(channel, payload)
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
- require "anycable/server"
59
- require "anycable/pubsub"
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