message_bus 2.1.6 → 2.2.0.pre
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of message_bus might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/.rubocop.yml +13 -92
- data/.rubocop_todo.yml +659 -0
- data/.travis.yml +1 -1
- data/CHANGELOG +61 -0
- data/Dockerfile +18 -0
- data/Gemfile +3 -1
- data/Guardfile +0 -1
- data/README.md +188 -101
- data/Rakefile +12 -1
- data/assets/message-bus.js +1 -1
- data/docker-compose.yml +46 -0
- data/examples/bench/config.ru +8 -9
- data/examples/bench/unicorn.conf.rb +1 -1
- data/examples/chat/chat.rb +150 -153
- data/examples/minimal/config.ru +2 -3
- data/lib/message_bus.rb +224 -36
- data/lib/message_bus/backends.rb +7 -0
- data/lib/message_bus/backends/base.rb +184 -0
- data/lib/message_bus/backends/memory.rb +304 -226
- data/lib/message_bus/backends/postgres.rb +359 -318
- data/lib/message_bus/backends/redis.rb +380 -337
- data/lib/message_bus/client.rb +99 -41
- data/lib/message_bus/connection_manager.rb +29 -21
- data/lib/message_bus/diagnostics.rb +50 -41
- data/lib/message_bus/distributed_cache.rb +5 -7
- data/lib/message_bus/message.rb +2 -2
- data/lib/message_bus/rack/diagnostics.rb +65 -55
- data/lib/message_bus/rack/middleware.rb +64 -44
- data/lib/message_bus/rack/thin_ext.rb +13 -9
- data/lib/message_bus/rails/railtie.rb +2 -0
- data/lib/message_bus/timer_thread.rb +2 -2
- data/lib/message_bus/version.rb +2 -1
- data/message_bus.gemspec +3 -2
- data/spec/assets/support/jasmine_helper.rb +1 -1
- data/spec/lib/fake_async_middleware.rb +1 -6
- data/spec/lib/message_bus/assets/asset_encoding_spec.rb +3 -3
- data/spec/lib/message_bus/backend_spec.rb +409 -0
- data/spec/lib/message_bus/client_spec.rb +8 -11
- data/spec/lib/message_bus/connection_manager_spec.rb +8 -14
- data/spec/lib/message_bus/distributed_cache_spec.rb +0 -4
- data/spec/lib/message_bus/multi_process_spec.rb +6 -7
- data/spec/lib/message_bus/rack/middleware_spec.rb +47 -43
- data/spec/lib/message_bus/timer_thread_spec.rb +0 -2
- data/spec/lib/message_bus_spec.rb +59 -43
- data/spec/spec_helper.rb +16 -4
- metadata +12 -9
- data/spec/lib/message_bus/backends/postgres_spec.rb +0 -221
- data/spec/lib/message_bus/backends/redis_spec.rb +0 -271
data/lib/message_bus/client.rb
CHANGED
@@ -1,9 +1,40 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
-
class MessageBus::Client
|
3
|
-
attr_accessor :client_id, :user_id, :group_ids, :connect_time,
|
4
|
-
:subscribed_sets, :site_id, :cleanup_timer,
|
5
|
-
:async_response, :io, :headers, :seq, :use_chunked
|
6
2
|
|
3
|
+
# Represents a connected subscriber and delivers published messages over its
|
4
|
+
# connected socket.
|
5
|
+
class MessageBus::Client
|
6
|
+
# @return [String] the unique ID provided by the client
|
7
|
+
attr_accessor :client_id
|
8
|
+
# @return [String,Integer] the user ID the client was authenticated for
|
9
|
+
attr_accessor :user_id
|
10
|
+
# @return [Array<String,Integer>] the group IDs the authenticated client is a member of
|
11
|
+
attr_accessor :group_ids
|
12
|
+
# @return [Time] the time at which the client connected
|
13
|
+
attr_accessor :connect_time
|
14
|
+
# @return [String] the site ID the client was authenticated for; used for hosting multiple
|
15
|
+
attr_accessor :site_id
|
16
|
+
# @return [MessageBus::TimerThread::Cancelable] a timer job that is used to
|
17
|
+
# auto-disconnect the client at the configured long-polling interval
|
18
|
+
attr_accessor :cleanup_timer
|
19
|
+
# @return [Thin::AsyncResponse, nil]
|
20
|
+
attr_accessor :async_response
|
21
|
+
# @return [IO] the HTTP socket the client is connected on
|
22
|
+
attr_accessor :io
|
23
|
+
# @return [Hash<String => String>] custom headers to include in HTTP responses
|
24
|
+
attr_accessor :headers
|
25
|
+
# @return [Integer] the connection sequence number the client provided when connecting
|
26
|
+
attr_accessor :seq
|
27
|
+
# @return [Boolean] whether or not the client should use chunked encoding
|
28
|
+
attr_accessor :use_chunked
|
29
|
+
|
30
|
+
# @param [Hash] opts
|
31
|
+
# @option opts [String] :client_id the unique ID provided by the client
|
32
|
+
# @option opts [String,Integer] :user_id (`nil`) the user ID the client was authenticated for
|
33
|
+
# @option opts [Array<String,Integer>] :group_ids (`[]`) the group IDs the authenticated client is a member of
|
34
|
+
# @option opts [String] :site_id (`nil`) the site ID the client was authenticated for; used for hosting multiple
|
35
|
+
# applications or instances of an application against a single message_bus
|
36
|
+
# @option opts [#to_i] :seq (`0`) the connection sequence number the client provided when connecting
|
37
|
+
# @option opts [MessageBus::Instance] :message_bus (`MessageBus`) a specific instance of message_bus
|
7
38
|
def initialize(opts)
|
8
39
|
self.client_id = opts[:client_id]
|
9
40
|
self.user_id = opts[:user_id]
|
@@ -17,11 +48,14 @@ class MessageBus::Client
|
|
17
48
|
@chunks_sent = 0
|
18
49
|
end
|
19
50
|
|
51
|
+
# @yield executed with a lock on the Client instance
|
52
|
+
# @return [void]
|
20
53
|
def synchronize
|
21
54
|
@lock.synchronize { yield }
|
22
55
|
end
|
23
56
|
|
24
|
-
|
57
|
+
# Closes the client connection
|
58
|
+
def close
|
25
59
|
if cleanup_timer
|
26
60
|
# concurrency may nil cleanup timer
|
27
61
|
cleanup_timer.cancel rescue nil
|
@@ -30,10 +64,12 @@ class MessageBus::Client
|
|
30
64
|
ensure_closed!
|
31
65
|
end
|
32
66
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
67
|
+
# Delivers a backlog of messages to the client, if there is anything in it.
|
68
|
+
# If chunked encoding/streaming is in use, will keep the connection open;
|
69
|
+
# if not, will close it.
|
70
|
+
#
|
71
|
+
# @param [Array<MessageBus::Message>] backlog the set of messages to deliver
|
72
|
+
# @return [void]
|
37
73
|
def deliver_backlog(backlog)
|
38
74
|
if backlog.length > 0
|
39
75
|
if use_chunked
|
@@ -44,53 +80,41 @@ class MessageBus::Client
|
|
44
80
|
end
|
45
81
|
end
|
46
82
|
|
83
|
+
# If no data has yet been sent to the client, sends an empty chunk; prevents
|
84
|
+
# clients from entering a timeout state if nothing is delivered initially.
|
47
85
|
def ensure_first_chunk_sent
|
48
86
|
if use_chunked && @chunks_sent == 0
|
49
87
|
write_chunk("[]")
|
50
88
|
end
|
51
89
|
end
|
52
90
|
|
53
|
-
|
54
|
-
return unless in_async?
|
55
|
-
if use_chunked
|
56
|
-
write_chunk("[]")
|
57
|
-
if @io
|
58
|
-
@io.write("0\r\n\r\n")
|
59
|
-
@io.close
|
60
|
-
@io = nil
|
61
|
-
end
|
62
|
-
if @async_response
|
63
|
-
@async_response << ("0\r\n\r\n")
|
64
|
-
@async_response.done
|
65
|
-
@async_response = nil
|
66
|
-
end
|
67
|
-
else
|
68
|
-
write_and_close "[]"
|
69
|
-
end
|
70
|
-
rescue
|
71
|
-
# we may have a dead socket, just nil the @io
|
72
|
-
@io = nil
|
73
|
-
@async_response = nil
|
74
|
-
end
|
75
|
-
|
76
|
-
def close
|
77
|
-
ensure_closed!
|
78
|
-
end
|
79
|
-
|
91
|
+
# @return [Boolean] whether the connection is closed or not
|
80
92
|
def closed?
|
81
93
|
!@async_response && !@io
|
82
94
|
end
|
83
95
|
|
96
|
+
# Subscribes the client to messages on a channel, optionally from a
|
97
|
+
# defined starting point.
|
98
|
+
#
|
99
|
+
# @param [String] channel the channel to subscribe to
|
100
|
+
# @param [Integer, nil] last_seen_id the ID of the last message the client
|
101
|
+
# received. If nil, will be subscribed from the head of the backlog.
|
102
|
+
# @return [void]
|
84
103
|
def subscribe(channel, last_seen_id)
|
85
104
|
last_seen_id = nil if last_seen_id == ""
|
86
105
|
last_seen_id ||= @bus.last_id(channel)
|
87
106
|
@subscriptions[channel] = last_seen_id.to_i
|
88
107
|
end
|
89
108
|
|
109
|
+
# @return [Hash<String => Integer>] the active subscriptions, mapping channel
|
110
|
+
# names to last seen message IDs
|
90
111
|
def subscriptions
|
91
112
|
@subscriptions
|
92
113
|
end
|
93
114
|
|
115
|
+
# Delivers a message to the client, even if it's empty
|
116
|
+
# @param [MessageBus::Message, nil] msg the message to deliver
|
117
|
+
# @return [void]
|
94
118
|
def <<(msg)
|
95
119
|
json = messages_to_json([msg])
|
96
120
|
if use_chunked
|
@@ -100,10 +124,9 @@ class MessageBus::Client
|
|
100
124
|
end
|
101
125
|
end
|
102
126
|
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
127
|
+
# @param [MessageBus::Message] msg the message in question
|
128
|
+
# @return [Boolean] whether or not the client has permission to receive the
|
129
|
+
# passed message
|
107
130
|
def allowed?(msg)
|
108
131
|
allowed = !msg.user_ids || msg.user_ids.include?(self.user_id)
|
109
132
|
allowed &&= !msg.client_ids || msg.client_ids.include?(self.client_id)
|
@@ -116,6 +139,11 @@ class MessageBus::Client
|
|
116
139
|
)
|
117
140
|
end
|
118
141
|
|
142
|
+
# @return [Array<MessageBus::Message>] the set of messages the client is due
|
143
|
+
# to receive, based on its subscriptions and permissions. Includes status
|
144
|
+
# message if any channels have no messages available and the client
|
145
|
+
# requested a message newer than the newest on the channel, or when there
|
146
|
+
# are messages available that the client doesn't have permission for.
|
119
147
|
def backlog
|
120
148
|
r = []
|
121
149
|
new_message_ids = nil
|
@@ -130,6 +158,7 @@ class MessageBus::Client
|
|
130
158
|
end
|
131
159
|
|
132
160
|
next if id < 0
|
161
|
+
|
133
162
|
messages = @bus.backlog(k, id, site_id)
|
134
163
|
|
135
164
|
if messages.length == 0
|
@@ -162,7 +191,7 @@ class MessageBus::Client
|
|
162
191
|
r || []
|
163
192
|
end
|
164
193
|
|
165
|
-
|
194
|
+
private
|
166
195
|
|
167
196
|
# heavily optimised to avoid all uneeded allocations
|
168
197
|
NEWLINE = "\r\n".freeze
|
@@ -180,6 +209,7 @@ class MessageBus::Client
|
|
180
209
|
@io.write(HTTP_11)
|
181
210
|
@headers.each do |k, v|
|
182
211
|
next if k == "Content-Type"
|
212
|
+
|
183
213
|
@io.write(k)
|
184
214
|
@io.write(COLON_SPACE)
|
185
215
|
@io.write(v)
|
@@ -244,7 +274,35 @@ class MessageBus::Client
|
|
244
274
|
end
|
245
275
|
end
|
246
276
|
|
277
|
+
def ensure_closed!
|
278
|
+
return unless in_async?
|
279
|
+
|
280
|
+
if use_chunked
|
281
|
+
write_chunk("[]")
|
282
|
+
if @io
|
283
|
+
@io.write("0\r\n\r\n")
|
284
|
+
@io.close
|
285
|
+
@io = nil
|
286
|
+
end
|
287
|
+
if @async_response
|
288
|
+
@async_response << ("0\r\n\r\n")
|
289
|
+
@async_response.done
|
290
|
+
@async_response = nil
|
291
|
+
end
|
292
|
+
else
|
293
|
+
write_and_close "[]"
|
294
|
+
end
|
295
|
+
rescue
|
296
|
+
# we may have a dead socket, just nil the @io
|
297
|
+
@io = nil
|
298
|
+
@async_response = nil
|
299
|
+
end
|
300
|
+
|
247
301
|
def messages_to_json(msgs)
|
248
302
|
MessageBus::Rack::Middleware.backlog_to_json(msgs)
|
249
303
|
end
|
304
|
+
|
305
|
+
def in_async?
|
306
|
+
@async_response || @io
|
307
|
+
end
|
250
308
|
end
|
@@ -1,10 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require 'json' unless defined? ::JSON
|
3
4
|
|
5
|
+
# Manages a set of subscribers with active connections to the server, such that
|
6
|
+
# messages which are published during the connection may be dispatched.
|
4
7
|
class MessageBus::ConnectionManager
|
5
8
|
require 'monitor'
|
6
9
|
include MonitorMixin
|
7
10
|
|
11
|
+
# @param [MessageBus::Instance] bus the message bus for which to manage connections
|
8
12
|
def initialize(bus = nil)
|
9
13
|
@clients = {}
|
10
14
|
@subscriptions = {}
|
@@ -12,6 +16,9 @@ class MessageBus::ConnectionManager
|
|
12
16
|
mon_initialize
|
13
17
|
end
|
14
18
|
|
19
|
+
# Dispatches a message to any connected clients which are permitted to receive it
|
20
|
+
# @param [MessageBus::Message] msg the message to dispatch
|
21
|
+
# @return [void]
|
15
22
|
def notify_clients(msg)
|
16
23
|
synchronize do
|
17
24
|
begin
|
@@ -34,37 +41,42 @@ class MessageBus::ConnectionManager
|
|
34
41
|
remove_client(client) if client.closed?
|
35
42
|
end
|
36
43
|
end
|
37
|
-
|
38
44
|
rescue => e
|
39
45
|
@bus.logger.error "notify clients crash #{e} : #{e.backtrace}"
|
40
46
|
end
|
41
47
|
end
|
42
48
|
end
|
43
49
|
|
50
|
+
# Keeps track of a client with an active connection
|
51
|
+
# @param [MessageBus::Client] client the client to track
|
52
|
+
# @return [void]
|
44
53
|
def add_client(client)
|
45
54
|
synchronize do
|
46
55
|
existing = @clients[client.client_id]
|
47
56
|
if existing && existing.seq > client.seq
|
48
|
-
client.
|
57
|
+
client.close
|
49
58
|
else
|
50
59
|
if existing
|
51
60
|
remove_client(existing)
|
52
|
-
existing.
|
61
|
+
existing.close
|
53
62
|
end
|
54
63
|
|
55
64
|
@clients[client.client_id] = client
|
56
65
|
@subscriptions[client.site_id] ||= {}
|
57
|
-
client.subscriptions.each do |k,
|
66
|
+
client.subscriptions.each do |k, _v|
|
58
67
|
subscribe_client(client, k)
|
59
68
|
end
|
60
69
|
end
|
61
70
|
end
|
62
71
|
end
|
63
72
|
|
73
|
+
# Removes a client
|
74
|
+
# @param [MessageBus::Client] c the client to remove
|
75
|
+
# @return [void]
|
64
76
|
def remove_client(c)
|
65
77
|
synchronize do
|
66
78
|
@clients.delete c.client_id
|
67
|
-
@subscriptions[c.site_id].each do |
|
79
|
+
@subscriptions[c.site_id].each do |_k, set|
|
68
80
|
set.delete c.client_id
|
69
81
|
end
|
70
82
|
if c.cleanup_timer
|
@@ -74,12 +86,24 @@ class MessageBus::ConnectionManager
|
|
74
86
|
end
|
75
87
|
end
|
76
88
|
|
89
|
+
# Finds a client by ID
|
90
|
+
# @param [String] client_id the client ID to search by
|
91
|
+
# @return [MessageBus::Client] the client with the specified ID
|
77
92
|
def lookup_client(client_id)
|
78
93
|
synchronize do
|
79
94
|
@clients[client_id]
|
80
95
|
end
|
81
96
|
end
|
82
97
|
|
98
|
+
# @return [Integer] the number of tracked clients
|
99
|
+
def client_count
|
100
|
+
synchronize do
|
101
|
+
@clients.length
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
83
107
|
def subscribe_client(client, channel)
|
84
108
|
synchronize do
|
85
109
|
set = @subscriptions[client.site_id][channel]
|
@@ -90,20 +114,4 @@ class MessageBus::ConnectionManager
|
|
90
114
|
set << client.client_id
|
91
115
|
end
|
92
116
|
end
|
93
|
-
|
94
|
-
def client_count
|
95
|
-
synchronize do
|
96
|
-
@clients.length
|
97
|
-
end
|
98
|
-
end
|
99
|
-
|
100
|
-
def stats
|
101
|
-
synchronize do
|
102
|
-
{
|
103
|
-
client_count: @clients.length,
|
104
|
-
subscriptions: @subscriptions
|
105
|
-
}
|
106
|
-
end
|
107
|
-
end
|
108
|
-
|
109
117
|
end
|
@@ -1,53 +1,62 @@
|
|
1
|
+
# MessageBus diagnostics are used for troubleshooting the bus and optimising its configuration
|
2
|
+
# @see MessageBus::Rack::Diagnostics
|
1
3
|
class MessageBus::Diagnostics
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
4
|
+
class << self
|
5
|
+
# Enables diagnostics functionality
|
6
|
+
# @param [MessageBus::Instance] bus a specific instance of message_bus
|
7
|
+
# @return [void]
|
8
|
+
def enable(bus = MessageBus)
|
9
|
+
full_path = full_process_path
|
10
|
+
start_time = Time.now.to_f
|
11
|
+
hostname = self.hostname
|
12
|
+
|
13
|
+
# it may make sense to add a channel per machine/host to streamline
|
14
|
+
# process to process comms
|
15
|
+
bus.subscribe('/_diagnostics/hup') do |msg|
|
16
|
+
if Process.pid == msg.data["pid"] && hostname == msg.data["hostname"]
|
17
|
+
$shutdown = true
|
18
|
+
sleep 4
|
19
|
+
Process.kill("HUP", $$)
|
20
|
+
end
|
12
21
|
end
|
13
|
-
rescue
|
14
|
-
# skip it ... not linux or something weird
|
15
|
-
end
|
16
|
-
end
|
17
22
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
+
bus.subscribe('/_diagnostics/discover') do |msg|
|
24
|
+
bus.on_connect.call msg.site_id if bus.on_connect
|
25
|
+
bus.publish '/_diagnostics/process-discovery', {
|
26
|
+
pid: Process.pid,
|
27
|
+
process_name: $0,
|
28
|
+
full_path: full_path,
|
29
|
+
uptime: (Time.now.to_f - start_time).to_i,
|
30
|
+
hostname: hostname
|
31
|
+
}, user_ids: [msg.data["user_id"]]
|
32
|
+
bus.on_disconnect.call msg.site_id if bus.on_disconnect
|
33
|
+
end
|
23
34
|
end
|
24
|
-
end
|
25
35
|
|
26
|
-
|
27
|
-
full_path = full_process_path
|
28
|
-
start_time = Time.now.to_f
|
29
|
-
hostname = self.hostname
|
36
|
+
private
|
30
37
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
+
def full_process_path
|
39
|
+
begin
|
40
|
+
system = `uname`.strip
|
41
|
+
if system == "Darwin"
|
42
|
+
`ps -o "comm=" -p #{Process.pid}`
|
43
|
+
elsif system == "FreeBSD"
|
44
|
+
`ps -o command -p #{Process.pid}`.split("\n", 2)[1].strip
|
45
|
+
else
|
46
|
+
info = `ps -eo "%p|$|%a" | grep '^\\s*#{Process.pid}'`
|
47
|
+
info.strip.split('|$|')[1]
|
48
|
+
end
|
49
|
+
rescue
|
50
|
+
# skip it ... not linux or something weird
|
38
51
|
end
|
39
52
|
end
|
40
53
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
uptime: (Time.now.to_f - start_time).to_i,
|
48
|
-
hostname: hostname
|
49
|
-
}, user_ids: [msg.data["user_id"]]
|
50
|
-
bus.on_disconnect.call msg.site_id if bus.on_disconnect
|
54
|
+
def hostname
|
55
|
+
begin
|
56
|
+
`hostname`.strip
|
57
|
+
rescue
|
58
|
+
# skip it
|
59
|
+
end
|
51
60
|
end
|
52
61
|
end
|
53
62
|
end
|
@@ -1,16 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# Like a hash, just does its best to stay in sync across the farm.
|
4
|
-
# On boot all instances are blank, but they populate as various processes
|
5
|
-
# fill it up.
|
6
|
-
|
7
3
|
require 'weakref'
|
8
4
|
require 'base64'
|
9
5
|
require 'securerandom'
|
10
6
|
|
11
7
|
module MessageBus
|
8
|
+
# Like a hash, just does its best to stay in sync across the farm.
|
9
|
+
# On boot all instances are blank, but they populate as various processes
|
10
|
+
# fill it up.
|
12
11
|
class DistributedCache
|
13
|
-
|
14
12
|
DEFAULT_SITE_ID = 'default'
|
15
13
|
|
16
14
|
class Manager
|
@@ -50,7 +48,6 @@ module MessageBus
|
|
50
48
|
when "delete" then hash.delete(payload["key"])
|
51
49
|
when "clear" then hash.clear
|
52
50
|
end
|
53
|
-
|
54
51
|
rescue WeakRef::RefError
|
55
52
|
@subscribers.delete_at(i)
|
56
53
|
ensure
|
@@ -61,8 +58,10 @@ module MessageBus
|
|
61
58
|
|
62
59
|
def ensure_subscribe!
|
63
60
|
return if @subscribed
|
61
|
+
|
64
62
|
@lock.synchronize do
|
65
63
|
return if @subscribed
|
64
|
+
|
66
65
|
@message_bus.subscribe(CHANNEL_NAME) do |message|
|
67
66
|
@lock.synchronize do
|
68
67
|
process_message(message)
|
@@ -160,6 +159,5 @@ module MessageBus
|
|
160
159
|
|
161
160
|
@data[site_id] ||= {}
|
162
161
|
end
|
163
|
-
|
164
162
|
end
|
165
163
|
end
|