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.
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