message_bus 2.1.6 → 2.2.0.pre
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.
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
|