anycable-rack-server 0.1.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9e57a096cf575590410db4c25e824d0b754af2b85eb0927e4d9fdf553da4ccea
4
- data.tar.gz: e6c5fce5b74f12bc41ce06955812cff608f50c4c66b063a459fa34ca8ed5e8e1
3
+ metadata.gz: ac33d07a88c1d94d982a7068f7636fc74013dd13e18a08ba29019c1b6e90bdb1
4
+ data.tar.gz: c655148349178d792f6609202762845189669053a14f89aa4a5f0c6efae67ff6
5
5
  SHA512:
6
- metadata.gz: 304f417fc3d477625effcc7134c81708d222df3da565613f4f5878b5c0365fecd5ed2a3e9620cc692976051f9a634a66623f8adfce2729664f34882135eeec2b
7
- data.tar.gz: d5a926617db60ec606f15b73189da24ed80d69012e87b8b4968ea6e0c1e0b7f26b86a3af593a48804f575dfd2c5c7867070555df968c69ba43926d4c97df4501
6
+ metadata.gz: 4ecbcb8c30cfa5e4b79948bc37c06405068d80bb192f53d98901d7177110da4822a20eff456a12fb7da5e7a3c3f8757ad219acb0befb2d7cd705288b3b1a3b12
7
+ data.tar.gz: a687c493138b5e0c7429e6a58c1b99ea4388a8b988988f0cc4bcab656db4789494e6b0fb05d59936794dfb6a99dc1b4868b90f82c551c049a2c33cf1c9c78d98
data/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2019 Yulia Oletskaya
3
+ Copyright (c) 2019-2020 Yulia Oletskaya, Vladimir Dementyev
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -1,4 +1,6 @@
1
- [![Gem Version](https://badge.fury.io/rb/anycable-rack-server.svg)](https://rubygems.org/gems/anycable-rack-server) [![Build Status](https://travis-ci.org/anycable/anycable-rack-server.svg?branch=master)](https://travis-ci.org/anycable/anycable-rack-server)
1
+ [![Cult Of Martians](http://cultofmartians.com/assets/badges/badge.svg)](https://cultofmartians.com/tasks/anycable-ruby-server.html)
2
+ [![Gem Version](https://badge.fury.io/rb/anycable-rack-server.svg)](https://rubygems.org/gems/anycable-rack-server)
3
+ [![Build](https://github.com/anycable/anycable-rack-server/workflows/Build/badge.svg)](https://github.com/anycable/anycable-rack-server/actions)
2
4
 
3
5
  # anycable-rack-server
4
6
 
@@ -8,9 +10,7 @@
8
10
 
9
11
  ```ruby
10
12
  # Initialize server instance first.
11
- #
12
- # NOTE: you must run RPC server yourself and provide its host
13
- ws_server = AnyCable::Rack::Server.new rpc_host: "localhost:50051"
13
+ ws_server = AnyCable::Rack::Server.new
14
14
 
15
15
  app = Rack::Builder.new do
16
16
  map "/cable" do
@@ -18,7 +18,7 @@ app = Rack::Builder.new do
18
18
  end
19
19
  end
20
20
 
21
- # NOTE: don't forget to call `start!` method
21
+ # NOTE: don't forget to call `start!` method
22
22
  ws_server.start!
23
23
 
24
24
  run app
@@ -28,59 +28,96 @@ run app
28
28
 
29
29
  Add `gem "anycable-rack-server"` to you `Gemfile` and make sure your Action Cable adapter is set to `:any_cable`. That's it! We automatically start AnyCable Rack server for your at `/cable` path.
30
30
 
31
- ## Settings
31
+ ## Configuration
32
+
33
+ AnyCable Rack Server uses [`anyway_config`](https://github.com/palkan/anyway_config) gem for configuration; thus it is possible to set configuration parameters through environment vars (prefixed with `ANYCABLE_`), `config/anycable.yml` file or `secrets.yml` when using Rails.
34
+
35
+ **NOTE:** AnyCable Rack Server uses the same config name (i.e., env prefix, YML file name, etc.) as AnyCable itself.
36
+
37
+ You can pass a config object as the option to `AnyCable::Rack::Server.new`:
38
+
39
+ ```ruby
40
+ server = AnyCable::Server::Rack.new(config: AnyCable::Rack::Config.new(**params))
41
+ ```
42
+
43
+ If no config is passed, a default, global, configuration would be used (`AnyCable::Rack.config`).
44
+
45
+ When using Rails, `config.anycable_rack` points to `AnyCable::Rack.config`.
46
+
47
+ ### Headers
32
48
 
33
49
  You can customize the headers being sent with each gRPC request.
34
50
 
35
51
  Default headers: `'cookie', 'x-api-token'`.
36
52
 
37
- Can be specified via options:
53
+ Can be specified via configuration:
38
54
 
39
55
  ```ruby
40
- ws_server = AnyCable::Rack::Server.new(
41
- rpc_host: "localhost:50051",
42
- headers: ["cookie", "x-my-header"]
43
- )
56
+ AnyCable::Rack.config.headers = ["cookie", "x-my-header"]
44
57
  ```
45
58
 
46
- In case of Rails you can set server options via `config.any_cable_rack`:
59
+ Or in Rails:
47
60
 
48
61
  ```ruby
49
62
  # <environment>.rb
50
63
  config.any_cable_rack.headers = %w[cookie]
64
+ ```
65
+
66
+ ### Rails-specific options
67
+
68
+ ```ruby
69
+ # Mount WebSocket server at the specified path
51
70
  config.any_cable_rack.mount_path = "/cable"
52
71
  # NOTE: here we specify only the port (we assume that a server is running locally)
53
72
  config.any_cable_rack.rpc_port = 50051
54
73
  ```
55
74
 
56
- ## Running RPC from the same process
75
+ ## Broadcast adapters
76
+
77
+ AnyCable Rack supports Redis (default) and HTTP broadcast adapters (see [the documentation](https://docs.anycable.io/#/ruby/broadcast_adapters)).
78
+
79
+ Broadcast adapter is inherited from AnyCable configuration (so, you don't need to configure it twice).
57
80
 
58
- The goal of the Rack server is to simplify the development/testing process. But we still have to run the RPC server.
81
+ ### Using HTTP broadcast adapter
59
82
 
60
- This gem also provides a way to run RPC server from the same process (spawning a new process):
83
+ ### With Rack
61
84
 
62
85
  ```ruby
63
- # in Rack app
86
+ AnyCable::Rack.config.broadast_adapter = :http
64
87
 
65
- AnyCable::Rack::RPCRunner.run(
66
- root_dir: "<path to your app root directory to run `anycable` from>",
67
- rpc_host: "...", # optional host to run RPC server on (defaults to '[::]::50051')
68
- command_args: [] # additional CLI arguments
69
- )
88
+ ws_server = AnyCable::Rack::Server
70
89
 
71
- # in Rails app you can just specify the configuration parameter (`false` by default)
72
- # and we'll take care of it
73
- config.any_cable_rack.run_rpc = true
90
+ app = Rack::Builder.new do
91
+ map "/cable" do
92
+ run ws_server
93
+ end
94
+
95
+ map "/_anycable_rack_broadcast" do
96
+ run ws_server.broadcast
97
+ end
98
+ end
74
99
  ```
75
100
 
101
+ ### With Rails
102
+
103
+ By default, we mount broadcasts endpoint at `/_anycable_rack_broadcast`.
104
+
105
+ You can change this setting:
106
+
107
+ ```ruby
108
+ config.any_cable_rack.http_broadcast_path = "/_my_broadcast"
109
+ ```
110
+
111
+ **NOTE:** Don't forget to configure `http_broadcast_url` for AnyCable pointing to your web server and the specified broadcast path.
112
+
76
113
  ## Testing
77
114
 
78
115
  Run units with `bundle exec rake`.
79
116
 
80
117
  ## Contributing
81
118
 
82
- Bug reports and pull requests are welcome on GitHub at https://github.com/anycable/anycable-rack-server.
119
+ Bug reports and pull requests are welcome on GitHub at [https://github.com/anycable/anycable-rack-server](https://github.com/anycable/anycable-rack-server).
83
120
 
84
121
  ## License
85
122
 
86
- The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
123
+ The gem is available as open source under the terms of the [MIT License](./LICENSE).
@@ -1,4 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "anycable/rack/version"
4
+ require "anycable/rack/config"
3
5
  require "anycable/rack/server"
6
+
7
+ module AnyCable
8
+ module Rack
9
+ class << self
10
+ def config
11
+ @config ||= Config.new
12
+ end
13
+ end
14
+ end
15
+ end
16
+
4
17
  require "anycable/rack/railtie" if defined?(Rails)
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module AnyCable
6
+ module Rack
7
+ module BroadcastSubscribers
8
+ class BaseSubscriber
9
+ include Logging
10
+
11
+ attr_reader :hub, :coder
12
+
13
+ def initialize(hub:, coder:, **options)
14
+ @hub = hub
15
+ @coder = coder
16
+ end
17
+
18
+ def start
19
+ # no-op
20
+ end
21
+
22
+ def stop
23
+ # no-op
24
+ end
25
+
26
+ private
27
+
28
+ def handle_message(msg)
29
+ log(:debug) { "Received pub/sub message: #{msg}" }
30
+
31
+ data = JSON.parse(msg)
32
+ if data["stream"]
33
+ hub.broadcast(data["stream"], data["data"], coder)
34
+ elsif data["command"] == "disconnect"
35
+ hub.disconnect(data["payload"]["identifier"], data["payload"]["reconnect"], coder)
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module AnyCable
6
+ module Rack
7
+ module BroadcastSubscribers
8
+ # HTTP Pub/Sub subscriber
9
+ class HTTPSubscriber < BaseSubscriber
10
+ attr_reader :token, :path
11
+
12
+ def initialize(**options)
13
+ super
14
+ @token = options[:token]
15
+ @path = options[:path]
16
+ end
17
+
18
+ def start
19
+ log(:info) { "Accepting pub/sub request at #{path}" }
20
+ end
21
+
22
+ def call(env)
23
+ req = ::Rack::Request.new(env)
24
+
25
+ return invalid_request unless req.post?
26
+
27
+ if token && req.get_header("HTTP_AUTHORIZATION") != "Bearer #{token}"
28
+ return invalid_request(401)
29
+ end
30
+
31
+ handle_message req.body.read
32
+
33
+ [201, {"Content-Type" => "text/plain"}, ["OK"]]
34
+ end
35
+
36
+ private
37
+
38
+ def invalid_request(code = 422)
39
+ [code, {"Content-Type" => "text/plain"}, ["Invalid request"]]
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ gem "redis", "~> 4"
4
+
3
5
  require "redis"
4
6
  require "json"
5
7
 
@@ -7,34 +9,47 @@ module AnyCable
7
9
  module Rack
8
10
  module BroadcastSubscribers
9
11
  # Redis Pub/Sub subscriber
10
- class RedisSubscriber
11
- attr_reader :hub, :coder, :redis_conn, :threads
12
+ class RedisSubscriber < BaseSubscriber
13
+ attr_reader :redis_conn, :thread, :channel
12
14
 
13
- def initialize(hub:, coder:, **options)
14
- @hub = hub
15
- @coder = coder
15
+ def initialize(hub:, coder:, channel:, **options)
16
+ super
16
17
  @redis_conn = ::Redis.new(options)
17
- @threads = {}
18
+ @channel = channel
18
19
  end
19
20
 
20
- def subscribe(channel)
21
- @threads[channel] = Thread.new do
22
- redis_conn.subscribe(channel) do |on|
23
- on.message { |_channel, msg| handle_message(msg) }
24
- end
25
- end
21
+ def start
22
+ subscribe(channel)
23
+
24
+ log(:info) { "Subscribed to #{channel}" }
26
25
  end
27
26
 
28
- def unsubscribe(channel)
29
- @threads[channel]&.terminate
30
- @threads.delete(channel)
27
+ def stop
28
+ thread&.terminate
31
29
  end
32
30
 
33
- private
31
+ def subscribe(channel)
32
+ @thread ||= Thread.new do
33
+ Thread.current.abort_on_exception = true
34
+
35
+ redis_conn.without_reconnect do
36
+ redis_conn.subscribe(channel) do |on|
37
+ on.subscribe do |chan, count|
38
+ log(:debug) { "Redis subscriber connected to #{chan} (#{count})" }
39
+ end
40
+
41
+ on.unsubscribe do |chan, count|
42
+ log(:debug) { "Redis subscribed disconnected from #{chan} (#{count})" }
43
+ end
34
44
 
35
- def handle_message(msg)
36
- data = JSON.parse(msg)
37
- hub.broadcast(data["stream"], data["data"], coder)
45
+ on.message do |_channel, msg|
46
+ handle_message(msg)
47
+ rescue
48
+ log(:error) { "Failed to broadcast message: #{msg}" }
49
+ end
50
+ end
51
+ end
52
+ end
38
53
  end
39
54
  end
40
55
  end
@@ -5,7 +5,7 @@ require "json"
5
5
  module AnyCable
6
6
  module Rack
7
7
  module Coders
8
- module JSON # :nodoc:
8
+ module Json # :nodoc:
9
9
  class << self
10
10
  def decode(json_str)
11
11
  ::JSON.parse(json_str)
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ gem "msgpack", "~> 1.4"
4
+ require "msgpack"
5
+
6
+ module AnyCable
7
+ module Rack
8
+ module Coders
9
+ module Msgpack # :nodoc:
10
+ class << self
11
+ def decode(bin)
12
+ MessagePack.unpack(bin)
13
+ end
14
+
15
+ def encode(ruby_obj)
16
+ BinaryFrame.new(MessagePack.pack(ruby_obj))
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "anyway_config"
4
+
5
+ module AnyCable
6
+ module Rack
7
+ class Config < Anyway::Config
8
+ DEFAULT_HEADERS = %w[cookie x-api-token].freeze
9
+
10
+ config_name :anycable
11
+ env_prefix "ANYCABLE"
12
+
13
+ attr_config mount_path: "/cable",
14
+ headers: DEFAULT_HEADERS,
15
+ coder: :json,
16
+ rpc_addr: "localhost:50051",
17
+ rpc_client_pool_size: 5,
18
+ rpc_client_timeout: 5,
19
+ http_broadcast_path: "/_anycable_rack_broadcast"
20
+ end
21
+ end
22
+ end
@@ -14,23 +14,24 @@ module AnyCable
14
14
  include Logging
15
15
 
16
16
  attr_reader :coder,
17
- :headers,
18
- :hub,
19
- :socket,
20
- :rpc_client,
21
- :sid
17
+ :headers,
18
+ :hub,
19
+ :socket,
20
+ :rpc_client,
21
+ :sid
22
22
 
23
- def initialize(socket, hub:, coder:, rpc_host:, headers:)
23
+ def initialize(socket, hub:, coder:, rpc_client:, headers:)
24
24
  @socket = socket
25
25
  @coder = coder
26
26
  @headers = headers
27
27
  @hub = hub
28
28
  @sid = SecureRandom.hex(6)
29
29
 
30
- @rpc_client = RPC::Client.new(rpc_host)
30
+ @rpc_client = rpc_client
31
31
 
32
- @_identifiers = "{}"
32
+ @_identifiers = "{}"
33
33
  @_subscriptions = Set.new
34
+ @_istate = {}
34
35
  end
35
36
 
36
37
  def handle_open
@@ -53,20 +54,21 @@ module AnyCable
53
54
  log(:debug) { "Command: #{decoded}" }
54
55
 
55
56
  case command
56
- when "subscribe" then subscribe(channel_identifier)
57
+ when "subscribe" then subscribe(channel_identifier)
57
58
  when "unsubscribe" then unsubscribe(channel_identifier)
58
- when "message" then send_message(channel_identifier, decoded["data"])
59
+ when "message" then send_message(channel_identifier, decoded["data"])
59
60
  else
60
61
  log(:error, "Command not found #{command}")
61
62
  end
62
- rescue Exception => e
63
+ rescue Exception => e # rubocop:disable Lint/RescueException
63
64
  log(:error, "Failed to execute command #{command}: #{e.message}")
64
65
  end
65
66
 
66
67
  private
67
68
 
68
69
  def transmit(cable_message)
69
- socket.transmit(encode(cable_message))
70
+ encoded = encode(cable_message)
71
+ socket.transmit(encoded)
70
72
  end
71
73
 
72
74
  def close
@@ -77,12 +79,8 @@ module AnyCable
77
79
  socket.request
78
80
  end
79
81
 
80
- def request_path
81
- request.fullpath
82
- end
83
-
84
82
  def rpc_connect
85
- rpc_client.connect(headers: headers, path: request_path)
83
+ rpc_client.connect(headers: headers, url: request.url)
86
84
  end
87
85
 
88
86
  def rpc_disconnect
@@ -90,7 +88,9 @@ module AnyCable
90
88
  identifiers: @_identifiers,
91
89
  subscriptions: @_subscriptions.to_a,
92
90
  headers: headers,
93
- path: request_path
91
+ url: request.url,
92
+ state: @_cstate,
93
+ channels_state: @_istate
94
94
  )
95
95
  end
96
96
 
@@ -99,7 +99,11 @@ module AnyCable
99
99
  command: command,
100
100
  identifier: identifier,
101
101
  connection_identifiers: @_identifiers,
102
- data: data
102
+ data: data,
103
+ headers: headers,
104
+ url: request.url,
105
+ connection_state: @_cstate,
106
+ state: @_istate[identifier]
103
107
  )
104
108
  end
105
109
 
@@ -130,16 +134,23 @@ module AnyCable
130
134
  end
131
135
 
132
136
  def process_command(response, identifier)
133
- response.transmissions.each { |transmission| transmit(decode(transmission)) }
137
+ response.transmissions.each { |transmission| transmit(decode_transmission(transmission)) }
134
138
  hub.remove_channel(socket, identifier) if response.stop_streams
135
139
  response.streams.each { |stream| hub.add_subscriber(stream, socket, identifier) }
140
+ response.stopped_streams.each { |stream| hub.remove_subscriber(stream, socket, identifier) }
141
+
142
+ @_istate[identifier] ||= {}
143
+ @_istate[identifier].merge!(response.env.istate&.to_h || {})
144
+
136
145
  close_connection if response.disconnect
137
146
  end
138
147
 
139
148
  def process_open(response)
149
+ response.transmissions&.each { |transmission| transmit(decode_transmission(transmission)) }
140
150
  if response.status == :SUCCESS
141
151
  @_identifiers = response.identifiers
142
- response.transmissions.each { |transmission| transmit(decode(transmission)) }
152
+ @_cstate = response.env.cstate&.to_h || {}
153
+ hub.add_socket(socket, @_identifiers)
143
154
  log(:debug) { "Opened" }
144
155
  else
145
156
  log(:error, "RPC connection command failed: #{response.inspect}")
@@ -171,6 +182,10 @@ module AnyCable
171
182
  coder.encode(cable_message)
172
183
  end
173
184
 
185
+ def decode_transmission(json_message)
186
+ JSON.parse(json_message)
187
+ end
188
+
174
189
  def decode(websocket_message)
175
190
  coder.decode(websocket_message)
176
191
  end