nats-pure 0.1.0
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.
- checksums.yaml +7 -0
- data/lib/nats/io/client.rb +1119 -0
- data/lib/nats/io/parser.rb +85 -0
- data/lib/nats/io/version.rb +8 -0
- metadata +48 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 3e87e20f03f5ef479937bce67c2133957a1e2d40
|
4
|
+
data.tar.gz: d8b27723a8a054be7d5a6c20d9e7c42d8ee0932e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ba54415e67b435e27007500d070245a281dd7ba517472adbbce3e7de87822263f1518814fa0c51ab247979139902ff8faa938f15783498f22a07787a830c732b
|
7
|
+
data.tar.gz: af88d16e6d1b9840681fb87a16854233ea45189403daf48be5c975408b4864f8d4893042e14868b59c772d1b992f4308d2773f6df118267b32ec2b8e25cec4a0
|
@@ -0,0 +1,1119 @@
|
|
1
|
+
require 'nats/io/parser'
|
2
|
+
require 'nats/io/version'
|
3
|
+
require 'thread'
|
4
|
+
require 'socket'
|
5
|
+
require 'json'
|
6
|
+
require 'monitor'
|
7
|
+
require 'uri'
|
8
|
+
require 'securerandom'
|
9
|
+
|
10
|
+
begin
|
11
|
+
require "openssl"
|
12
|
+
rescue LoadError
|
13
|
+
end
|
14
|
+
|
15
|
+
module NATS
|
16
|
+
module IO
|
17
|
+
|
18
|
+
DEFAULT_PORT = 4222
|
19
|
+
DEFAULT_URI = "nats://localhost:#{DEFAULT_PORT}".freeze
|
20
|
+
|
21
|
+
MAX_RECONNECT_ATTEMPTS = 10
|
22
|
+
RECONNECT_TIME_WAIT = 2
|
23
|
+
|
24
|
+
# Maximum accumulated pending commands bytesize before forcing a flush.
|
25
|
+
MAX_PENDING_SIZE = 32768
|
26
|
+
|
27
|
+
# Maximum number of flush kicks that can be queued up before we block.
|
28
|
+
MAX_FLUSH_KICK_SIZE = 1024
|
29
|
+
|
30
|
+
# Maximum number of bytes which we will be gathering on a single read.
|
31
|
+
# TODO: Make dynamic?
|
32
|
+
MAX_SOCKET_READ_BYTES = 32768
|
33
|
+
|
34
|
+
# Ping intervals
|
35
|
+
DEFAULT_PING_INTERVAL = 120
|
36
|
+
DEFAULT_PING_MAX = 2
|
37
|
+
|
38
|
+
# Default IO timeouts
|
39
|
+
DEFAULT_CONNECT_TIMEOUT = 2
|
40
|
+
DEFAULT_READ_WRITE_TIMEOUT = 2
|
41
|
+
|
42
|
+
CR_LF = ("\r\n".freeze)
|
43
|
+
CR_LF_SIZE = (CR_LF.bytesize)
|
44
|
+
|
45
|
+
PING_REQUEST = ("PING#{CR_LF}".freeze)
|
46
|
+
PONG_RESPONSE = ("PONG#{CR_LF}".freeze)
|
47
|
+
|
48
|
+
SUB_OP = ('SUB'.freeze)
|
49
|
+
EMPTY_MSG = (''.freeze)
|
50
|
+
|
51
|
+
# Connection States
|
52
|
+
DISCONNECTED = 0
|
53
|
+
CONNECTED = 1
|
54
|
+
CLOSED = 2
|
55
|
+
RECONNECTING = 3
|
56
|
+
CONNECTING = 4
|
57
|
+
|
58
|
+
class Error < StandardError; end
|
59
|
+
|
60
|
+
# When the NATS server sends us an 'ERR' message.
|
61
|
+
class ServerError < Error; end
|
62
|
+
|
63
|
+
# When we detect error on the client side.
|
64
|
+
class ClientError < Error; end
|
65
|
+
|
66
|
+
# When we cannot connect to the server (either initially or after a reconnect).
|
67
|
+
class ConnectError < Error; end
|
68
|
+
|
69
|
+
# When we cannot connect to the server because authorization failed.
|
70
|
+
class AuthError < ConnectError; end
|
71
|
+
|
72
|
+
# When we cannot connect since there are no servers available.
|
73
|
+
class NoServersError < ConnectError; end
|
74
|
+
|
75
|
+
# When we do not get a result within a specified time.
|
76
|
+
class Timeout < Error; end
|
77
|
+
|
78
|
+
# When we use an invalid subject.
|
79
|
+
class BadSubject < Error; end
|
80
|
+
|
81
|
+
class Client
|
82
|
+
include MonitorMixin
|
83
|
+
|
84
|
+
attr_reader :status, :server_info, :server_pool, :options, :connected_server, :stats, :uri
|
85
|
+
|
86
|
+
def initialize
|
87
|
+
super # required to initialize monitor
|
88
|
+
@options = nil
|
89
|
+
|
90
|
+
# Read/Write IO
|
91
|
+
@io = nil
|
92
|
+
|
93
|
+
# Queues for coalescing writes of commands we need to send to server.
|
94
|
+
@flush_queue = nil
|
95
|
+
@pending_queue = nil
|
96
|
+
|
97
|
+
# Parser with state
|
98
|
+
@parser = NATS::Protocol::Parser.new(self)
|
99
|
+
|
100
|
+
# Threads for both reading and flushing command
|
101
|
+
@flusher_thread = nil
|
102
|
+
@read_loop_thread = nil
|
103
|
+
@ping_interval_thread = nil
|
104
|
+
|
105
|
+
# Info that we get from the server
|
106
|
+
@server_info = { }
|
107
|
+
|
108
|
+
# URI from server to which we are currently connected
|
109
|
+
@uri = nil
|
110
|
+
@server_pool = []
|
111
|
+
|
112
|
+
@status = DISCONNECTED
|
113
|
+
|
114
|
+
# Subscriptions
|
115
|
+
@subs = { }
|
116
|
+
@ssid = 0
|
117
|
+
|
118
|
+
# Ping interval
|
119
|
+
@pings_outstanding = 0
|
120
|
+
@pongs_received = 0
|
121
|
+
@pongs = []
|
122
|
+
@pongs.extend(MonitorMixin)
|
123
|
+
|
124
|
+
# Accounting
|
125
|
+
@pending_size = 0
|
126
|
+
@stats = {
|
127
|
+
in_msgs: 0,
|
128
|
+
out_msgs: 0,
|
129
|
+
in_bytes: 0,
|
130
|
+
out_bytes: 0,
|
131
|
+
reconnects: 0
|
132
|
+
}
|
133
|
+
|
134
|
+
# Sticky error
|
135
|
+
@last_err = nil
|
136
|
+
|
137
|
+
# Async callbacks
|
138
|
+
@err_cb = proc { |e| raise e }
|
139
|
+
@close_cb = proc { }
|
140
|
+
@disconnect_cb = proc { }
|
141
|
+
@reconnect_cb = proc { }
|
142
|
+
|
143
|
+
# Secure TLS options
|
144
|
+
@tls = nil
|
145
|
+
end
|
146
|
+
|
147
|
+
# Establishes connection to NATS
|
148
|
+
def connect(opts={})
|
149
|
+
opts[:verbose] = false if opts[:verbose].nil?
|
150
|
+
opts[:pedantic] = false if opts[:pedantic].nil?
|
151
|
+
opts[:reconnect] = true if opts[:reconnect].nil?
|
152
|
+
opts[:reconnect_time_wait] = RECONNECT_TIME_WAIT if opts[:reconnect_time_wait].nil?
|
153
|
+
opts[:max_reconnect_attempts] = MAX_RECONNECT_ATTEMPTS if opts[:max_reconnect_attempts].nil?
|
154
|
+
opts[:ping_interval] = DEFAULT_PING_INTERVAL if opts[:ping_interval].nil?
|
155
|
+
opts[:max_outstanding_pings] = DEFAULT_PING_MAX if opts[:max_outstanding_pings].nil?
|
156
|
+
|
157
|
+
# Override with ENV
|
158
|
+
opts[:verbose] = ENV['NATS_VERBOSE'].downcase == 'true' unless ENV['NATS_VERBOSE'].nil?
|
159
|
+
opts[:pedantic] = ENV['NATS_PEDANTIC'].downcase == 'true' unless ENV['NATS_PEDANTIC'].nil?
|
160
|
+
opts[:reconnect] = ENV['NATS_RECONNECT'].downcase == 'true' unless ENV['NATS_RECONNECT'].nil?
|
161
|
+
opts[:reconnect_time_wait] = ENV['NATS_RECONNECT_TIME_WAIT'].to_i unless ENV['NATS_RECONNECT_TIME_WAIT'].nil?
|
162
|
+
opts[:max_reconnect_attempts] = ENV['NATS_MAX_RECONNECT_ATTEMPTS'].to_i unless ENV['NATS_MAX_RECONNECT_ATTEMPTS'].nil?
|
163
|
+
opts[:ping_interval] = ENV['NATS_PING_INTERVAL'].to_i unless ENV['NATS_PING_INTERVAL'].nil?
|
164
|
+
opts[:max_outstanding_pings] = ENV['NATS_MAX_OUTSTANDING_PINGS'].to_i unless ENV['NATS_MAX_OUTSTANDING_PINGS'].nil?
|
165
|
+
@options = opts
|
166
|
+
|
167
|
+
# Process servers in the NATS cluster and pick one to connect
|
168
|
+
uris = opts[:servers] || [DEFAULT_URI]
|
169
|
+
uris.shuffle! unless @options[:dont_randomize_servers]
|
170
|
+
uris.each do |u|
|
171
|
+
@server_pool << { :uri => u.is_a?(URI) ? u.dup : URI.parse(u) }
|
172
|
+
end
|
173
|
+
|
174
|
+
# Check for TLS usage
|
175
|
+
@tls = @options[:tls]
|
176
|
+
|
177
|
+
begin
|
178
|
+
current = select_next_server
|
179
|
+
|
180
|
+
# Create TCP socket connection to NATS
|
181
|
+
@io = create_socket
|
182
|
+
@io.connect
|
183
|
+
|
184
|
+
# Capture state that we have had a TCP connection established against
|
185
|
+
# this server and could potentially be used for reconnecting.
|
186
|
+
current[:was_connected] = true
|
187
|
+
|
188
|
+
# Connection established and now in process of sending CONNECT to NATS
|
189
|
+
@status = CONNECTING
|
190
|
+
|
191
|
+
# Established TCP connection successfully so can start connect
|
192
|
+
process_connect_init
|
193
|
+
|
194
|
+
# Reset reconnection attempts if connection is valid
|
195
|
+
current[:reconnect_attempts] = 0
|
196
|
+
rescue NoServersError => e
|
197
|
+
@disconnect_cb.call(e) if @disconnect_cb
|
198
|
+
raise e
|
199
|
+
rescue => e
|
200
|
+
# Capture sticky error
|
201
|
+
synchronize { @last_err = e }
|
202
|
+
|
203
|
+
if should_not_reconnect?
|
204
|
+
@disconnect_cb.call(e) if @disconnect_cb
|
205
|
+
raise e
|
206
|
+
end
|
207
|
+
|
208
|
+
# Continue retrying until there are no options left in the server pool
|
209
|
+
retry
|
210
|
+
end
|
211
|
+
|
212
|
+
# Initialize queues and loops for message dispatching and processing engine
|
213
|
+
@flush_queue = SizedQueue.new(MAX_FLUSH_KICK_SIZE)
|
214
|
+
@pending_queue = SizedQueue.new(MAX_PENDING_SIZE)
|
215
|
+
@pings_outstanding = 0
|
216
|
+
@pongs_received = 0
|
217
|
+
@pending_size = 0
|
218
|
+
|
219
|
+
# Server roundtrip went ok so consider to be connected at this point
|
220
|
+
@status = CONNECTED
|
221
|
+
|
222
|
+
# Connected to NATS so Ready to start parser loop, flusher and ping interval
|
223
|
+
start_threads!
|
224
|
+
end
|
225
|
+
|
226
|
+
def publish(subject, msg=EMPTY_MSG, opt_reply=nil, &blk)
|
227
|
+
raise BadSubject if !subject or subject.empty?
|
228
|
+
msg_size = msg.bytesize
|
229
|
+
|
230
|
+
# Accounting
|
231
|
+
@stats[:out_msgs] += 1
|
232
|
+
@stats[:out_bytes] += msg_size
|
233
|
+
|
234
|
+
send_command("PUB #{subject} #{opt_reply} #{msg_size}\r\n#{msg}\r\n")
|
235
|
+
@flush_queue << :pub if @flush_queue.empty?
|
236
|
+
end
|
237
|
+
|
238
|
+
# Create subscription which is dispatched asynchronously
|
239
|
+
# messages to a callback.
|
240
|
+
def subscribe(subject, opts={}, &callback)
|
241
|
+
sid = (@ssid += 1)
|
242
|
+
sub = @subs[sid] = Subscription.new
|
243
|
+
sub.subject = subject
|
244
|
+
sub.callback = callback
|
245
|
+
sub.received = 0
|
246
|
+
sub.queue = opts[:queue] if opts[:queue]
|
247
|
+
sub.max = opts[:max] if opts[:max]
|
248
|
+
|
249
|
+
send_command("SUB #{subject} #{opts[:queue]} #{sid}#{CR_LF}")
|
250
|
+
@flush_queue << :sub
|
251
|
+
|
252
|
+
# Setup server support for auto-unsubscribe when receiving enough messages
|
253
|
+
unsubscribe(sid, opts[:max]) if opts[:max]
|
254
|
+
|
255
|
+
sid
|
256
|
+
end
|
257
|
+
|
258
|
+
# Sends a request expecting a single response or raises a timeout
|
259
|
+
# in case the request is not retrieved within the specified deadline.
|
260
|
+
# If given a callback, then the request happens asynchronously.
|
261
|
+
def request(subject, payload, opts={}, &blk)
|
262
|
+
return unless subject
|
263
|
+
inbox = new_inbox
|
264
|
+
|
265
|
+
# If a callback was passed, then have it process
|
266
|
+
# the messages asynchronously and return the sid.
|
267
|
+
if blk
|
268
|
+
opts[:max] ||= 1
|
269
|
+
s = subscribe(inbox, opts) do |msg, reply|
|
270
|
+
case blk.arity
|
271
|
+
when 0 then blk.call
|
272
|
+
when 1 then blk.call(msg)
|
273
|
+
else blk.call(msg, reply)
|
274
|
+
end
|
275
|
+
end
|
276
|
+
publish(subject, payload, inbox)
|
277
|
+
|
278
|
+
return s
|
279
|
+
end
|
280
|
+
|
281
|
+
# In case block was not given, handle synchronously
|
282
|
+
# with a timeout and only allow a single response.
|
283
|
+
timeout = opts[:timeout] ||= 0.5
|
284
|
+
opts[:max] = 1
|
285
|
+
|
286
|
+
sub = Subscription.new
|
287
|
+
sub.subject = inbox
|
288
|
+
sub.received = 0
|
289
|
+
future = sub.new_cond
|
290
|
+
sub.future = future
|
291
|
+
|
292
|
+
sid = nil
|
293
|
+
synchronize do
|
294
|
+
sid = (@ssid += 1)
|
295
|
+
@subs[sid] = sub
|
296
|
+
end
|
297
|
+
|
298
|
+
send_command("SUB #{inbox} #{sid}#{CR_LF}")
|
299
|
+
@flush_queue << :sub
|
300
|
+
unsubscribe(sid, 1)
|
301
|
+
|
302
|
+
sub.synchronize do
|
303
|
+
# Publish the request and then wait for the response...
|
304
|
+
publish(subject, payload, inbox)
|
305
|
+
|
306
|
+
with_nats_timeout(timeout) do
|
307
|
+
future.wait(timeout)
|
308
|
+
end
|
309
|
+
end
|
310
|
+
response = sub.response
|
311
|
+
|
312
|
+
response
|
313
|
+
end
|
314
|
+
|
315
|
+
# Auto unsubscribes the server by sending UNSUB command and throws away
|
316
|
+
# subscription in case already present and has received enough messages.
|
317
|
+
def unsubscribe(sid, opt_max=nil)
|
318
|
+
opt_max_str = " #{opt_max}" unless opt_max.nil?
|
319
|
+
send_command("UNSUB #{sid}#{opt_max_str}#{CR_LF}")
|
320
|
+
@flush_queue << :unsub
|
321
|
+
|
322
|
+
return unless sub = @subs[sid]
|
323
|
+
synchronize do
|
324
|
+
sub.max = opt_max
|
325
|
+
@subs.delete(sid) unless (sub.max && (sub.received < sub.max))
|
326
|
+
end
|
327
|
+
end
|
328
|
+
|
329
|
+
# Send a ping and wait for a pong back within a timeout.
|
330
|
+
def flush(timeout=60)
|
331
|
+
# Schedule sending a PING, and block until we receive PONG back,
|
332
|
+
# or raise a timeout in case the response is past the deadline.
|
333
|
+
pong = @pongs.new_cond
|
334
|
+
@pongs.synchronize do
|
335
|
+
@pongs << pong
|
336
|
+
|
337
|
+
# Flush once pong future has been prepared
|
338
|
+
@pending_queue << PING_REQUEST
|
339
|
+
@flush_queue << :ping
|
340
|
+
with_nats_timeout(timeout) do
|
341
|
+
pong.wait(timeout)
|
342
|
+
end
|
343
|
+
end
|
344
|
+
end
|
345
|
+
|
346
|
+
alias :servers :server_pool
|
347
|
+
|
348
|
+
def discovered_servers
|
349
|
+
servers.select {|s| s[:discovered] }
|
350
|
+
end
|
351
|
+
|
352
|
+
# Methods only used by the parser
|
353
|
+
|
354
|
+
def process_pong
|
355
|
+
# Take first pong wait and signal any flush in case there was one
|
356
|
+
@pongs.synchronize do
|
357
|
+
pong = @pongs.pop
|
358
|
+
pong.signal unless pong.nil?
|
359
|
+
end
|
360
|
+
@pings_outstanding -= 1
|
361
|
+
@pongs_received += 1
|
362
|
+
end
|
363
|
+
|
364
|
+
# Received a ping so respond back with a pong
|
365
|
+
def process_ping
|
366
|
+
@pending_queue << PONG_RESPONSE
|
367
|
+
@flush_queue << :ping
|
368
|
+
pong = @pongs.new_cond
|
369
|
+
@pongs.synchronize { @pongs << pong }
|
370
|
+
end
|
371
|
+
|
372
|
+
# Handles protocol errors being sent by the server.
|
373
|
+
def process_err(err)
|
374
|
+
# FIXME: In case of a stale connection, then handle as process_op_error
|
375
|
+
|
376
|
+
# In case of permissions violation then dispatch the error callback
|
377
|
+
# while holding the lock.
|
378
|
+
current = server_pool.first
|
379
|
+
current[:error_received] = true
|
380
|
+
if current[:auth_required]
|
381
|
+
@err_cb.call(NATS::IO::AuthError.new(err))
|
382
|
+
else
|
383
|
+
@err_cb.call(NATS::IO::ServerError.new(err))
|
384
|
+
end
|
385
|
+
|
386
|
+
# Otherwise, capture the error under a lock and close
|
387
|
+
# the connection gracefully.
|
388
|
+
synchronize do
|
389
|
+
@last_err = NATS::IO::ServerError.new(err)
|
390
|
+
end
|
391
|
+
|
392
|
+
close
|
393
|
+
end
|
394
|
+
|
395
|
+
def process_msg(subject, sid, reply, data)
|
396
|
+
# Accounting
|
397
|
+
@stats[:in_msgs] += 1
|
398
|
+
@stats[:in_bytes] += data.size
|
399
|
+
|
400
|
+
# Throw away in case we no longer manage the subscription
|
401
|
+
sub = nil
|
402
|
+
synchronize { sub = @subs[sid] }
|
403
|
+
return unless sub
|
404
|
+
|
405
|
+
# Check for auto_unsubscribe
|
406
|
+
sub.synchronize do
|
407
|
+
sub.received += 1
|
408
|
+
if sub.max
|
409
|
+
case
|
410
|
+
when sub.received > sub.max
|
411
|
+
# Client side support in case server did not receive unsubscribe
|
412
|
+
unsubscribe(sid)
|
413
|
+
return
|
414
|
+
when sub.received == sub.max
|
415
|
+
# Cleanup here if we have hit the max..
|
416
|
+
@subs.delete(sid)
|
417
|
+
end
|
418
|
+
end
|
419
|
+
|
420
|
+
# In case of a request which requires a future
|
421
|
+
# do so here already while holding the lock and return
|
422
|
+
if sub.future
|
423
|
+
future = sub.future
|
424
|
+
sub.response = Msg.new(subject, reply, data)
|
425
|
+
future.signal
|
426
|
+
|
427
|
+
return
|
428
|
+
end
|
429
|
+
end
|
430
|
+
|
431
|
+
# Distinguish between async subscriptions with callbacks
|
432
|
+
# and request subscriptions which expect a single response.
|
433
|
+
if sub.callback
|
434
|
+
cb = sub.callback
|
435
|
+
case cb.arity
|
436
|
+
when 0 then cb.call
|
437
|
+
when 1 then cb.call(data)
|
438
|
+
when 2 then cb.call(data, reply)
|
439
|
+
else cb.call(data, reply, subject)
|
440
|
+
end
|
441
|
+
end
|
442
|
+
end
|
443
|
+
|
444
|
+
# Close connection to NATS, flushing in case connection is alive
|
445
|
+
# and there are any pending messages, should not be used while
|
446
|
+
# holding the lock.
|
447
|
+
def close
|
448
|
+
synchronize do
|
449
|
+
return if @status == CLOSED
|
450
|
+
@status = CLOSED
|
451
|
+
end
|
452
|
+
|
453
|
+
# Kick the flusher so it bails due to closed state
|
454
|
+
@flush_queue << :fallout
|
455
|
+
Thread.pass
|
456
|
+
|
457
|
+
# FIXME: More graceful way of handling the following?
|
458
|
+
# Ensure ping interval and flusher are not running anymore
|
459
|
+
@ping_interval_thread.exit if @ping_interval_thread.alive?
|
460
|
+
@flusher_thread.exit if @flusher_thread.alive?
|
461
|
+
@read_loop_thread.exit if @read_loop_thread.alive?
|
462
|
+
|
463
|
+
# TODO: Delete any other state which we are not using here too.
|
464
|
+
synchronize do
|
465
|
+
@pongs.synchronize do
|
466
|
+
@pongs.each do |pong|
|
467
|
+
pong.signal
|
468
|
+
end
|
469
|
+
@pongs.clear
|
470
|
+
end
|
471
|
+
|
472
|
+
# Try to write any pending flushes in case
|
473
|
+
# we have a connection then close it.
|
474
|
+
begin
|
475
|
+
cmds = []
|
476
|
+
cmds << @pending_queue.pop until @pending_queue.empty?
|
477
|
+
|
478
|
+
# FIXME: Fails when empty on TLS connection?
|
479
|
+
@io.write(cmds.join) unless cmds.empty?
|
480
|
+
rescue => e
|
481
|
+
@last_err = e
|
482
|
+
@err_cb.call(e) if @err_cb
|
483
|
+
end if @io and not @io.closed?
|
484
|
+
|
485
|
+
# TODO: Destroy any remaining subscriptions
|
486
|
+
@disconnect_cb.call if @disconnect_cb
|
487
|
+
@close_cb.call if @close_cb
|
488
|
+
|
489
|
+
# Close the established connection in case
|
490
|
+
# we still have it.
|
491
|
+
if @io
|
492
|
+
@io.close
|
493
|
+
@io = nil
|
494
|
+
end
|
495
|
+
end
|
496
|
+
end
|
497
|
+
|
498
|
+
def new_inbox
|
499
|
+
"_INBOX.#{SecureRandom.hex(13)}"
|
500
|
+
end
|
501
|
+
|
502
|
+
def connected_server
|
503
|
+
connected? ? @uri : nil
|
504
|
+
end
|
505
|
+
|
506
|
+
def connected?
|
507
|
+
@status == CONNECTED
|
508
|
+
end
|
509
|
+
|
510
|
+
def connecting?
|
511
|
+
@status == CONNECTING
|
512
|
+
end
|
513
|
+
|
514
|
+
def reconnecting?
|
515
|
+
@status == RECONNECTING
|
516
|
+
end
|
517
|
+
|
518
|
+
def closed?
|
519
|
+
@status == CLOSED
|
520
|
+
end
|
521
|
+
|
522
|
+
def on_error(&callback)
|
523
|
+
@err_cb = callback
|
524
|
+
end
|
525
|
+
|
526
|
+
def on_disconnect(&callback)
|
527
|
+
@disconnect_cb = callback
|
528
|
+
end
|
529
|
+
|
530
|
+
def on_reconnect(&callback)
|
531
|
+
@reconnect_cb = callback
|
532
|
+
end
|
533
|
+
|
534
|
+
def on_close(&callback)
|
535
|
+
@close_cb = callback
|
536
|
+
end
|
537
|
+
|
538
|
+
def last_error
|
539
|
+
synchronize do
|
540
|
+
@last_err
|
541
|
+
end
|
542
|
+
end
|
543
|
+
|
544
|
+
private
|
545
|
+
|
546
|
+
def select_next_server
|
547
|
+
raise NoServersError.new("nats: No servers available") if server_pool.empty?
|
548
|
+
|
549
|
+
# Pick next from head of the list
|
550
|
+
srv = server_pool.shift
|
551
|
+
|
552
|
+
# Track connection attempts to this server
|
553
|
+
srv[:reconnect_attempts] ||= 0
|
554
|
+
srv[:reconnect_attempts] += 1
|
555
|
+
|
556
|
+
# In case there was an error from the server we will
|
557
|
+
# take it out from rotation unless we specify infinite
|
558
|
+
# reconnects via setting :max_reconnect_attempts to -1
|
559
|
+
if options[:max_reconnect_attempts] < 0 || can_reuse_server?(srv)
|
560
|
+
server_pool << srv
|
561
|
+
end
|
562
|
+
|
563
|
+
# Back off in case we are reconnecting to it and have been connected
|
564
|
+
sleep @options[:reconnect_time_wait] if should_delay_connect?(srv)
|
565
|
+
|
566
|
+
# Set url of the server to which we would be connected
|
567
|
+
@uri = srv[:uri]
|
568
|
+
@uri.user = @options[:user] if @options[:user]
|
569
|
+
@uri.password = @options[:pass] if @options[:pass]
|
570
|
+
|
571
|
+
srv
|
572
|
+
end
|
573
|
+
|
574
|
+
def process_info(line)
|
575
|
+
parsed_info = JSON.parse(line)
|
576
|
+
|
577
|
+
# INFO can be received asynchronously too,
|
578
|
+
# so has to be done under the lock.
|
579
|
+
synchronize do
|
580
|
+
# Symbolize keys from parsed info line
|
581
|
+
@server_info = parsed_info.reduce({}) do |info, (k,v)|
|
582
|
+
info[k.to_sym] = v
|
583
|
+
|
584
|
+
info
|
585
|
+
end
|
586
|
+
|
587
|
+
# Detect any announced server that we might not be aware of...
|
588
|
+
connect_urls = @server_info[:connect_urls]
|
589
|
+
if connect_urls
|
590
|
+
srvs = []
|
591
|
+
connect_urls.each do |url|
|
592
|
+
u = URI.parse("nats://#{url}")
|
593
|
+
present = server_pool.detect do |srv|
|
594
|
+
srv[:uri].host == u.host && srv[:uri].port == u.port
|
595
|
+
end
|
596
|
+
|
597
|
+
if not present
|
598
|
+
# Let explicit user and pass options set the credentials.
|
599
|
+
u.user = options[:user] if options[:user]
|
600
|
+
u.password = options[:pass] if options[:pass]
|
601
|
+
|
602
|
+
# Use creds from the current server if not set explicitly.
|
603
|
+
if @uri
|
604
|
+
u.user ||= @uri.user if @uri.user
|
605
|
+
u.password ||= @uri.password if @uri.password
|
606
|
+
end
|
607
|
+
|
608
|
+
srvs << { :uri => u, :reconnect_attempts => 0, :discovered => true }
|
609
|
+
end
|
610
|
+
end
|
611
|
+
srvs.shuffle! unless @options[:dont_randomize_servers]
|
612
|
+
|
613
|
+
# Include in server pool but keep current one as the first one.
|
614
|
+
server_pool.push(*srvs)
|
615
|
+
end
|
616
|
+
end
|
617
|
+
|
618
|
+
@server_info
|
619
|
+
end
|
620
|
+
|
621
|
+
def server_using_secure_connection?
|
622
|
+
@server_info[:ssl_required] || @server_info[:tls_required]
|
623
|
+
end
|
624
|
+
|
625
|
+
def client_using_secure_connection?
|
626
|
+
@uri.scheme == "tls" || @tls
|
627
|
+
end
|
628
|
+
|
629
|
+
def send_command(command)
|
630
|
+
@pending_size += command.bytesize
|
631
|
+
@pending_queue << command
|
632
|
+
|
633
|
+
# TODO: kick flusher here in case pending_size growing large
|
634
|
+
end
|
635
|
+
|
636
|
+
def auth_connection?
|
637
|
+
!@uri.user.nil?
|
638
|
+
end
|
639
|
+
|
640
|
+
def connect_command
|
641
|
+
cs = {
|
642
|
+
:verbose => @options[:verbose],
|
643
|
+
:pedantic => @options[:pedantic],
|
644
|
+
:lang => NATS::IO::LANG,
|
645
|
+
:version => NATS::IO::VERSION,
|
646
|
+
:protocol => NATS::IO::PROTOCOL
|
647
|
+
}
|
648
|
+
cs[:name] = @options[:name] if @options[:name]
|
649
|
+
|
650
|
+
if auth_connection?
|
651
|
+
cs[:user] = @uri.user
|
652
|
+
cs[:pass] = @uri.password
|
653
|
+
end
|
654
|
+
|
655
|
+
"CONNECT #{cs.to_json}#{CR_LF}"
|
656
|
+
end
|
657
|
+
|
658
|
+
def with_nats_timeout(timeout)
|
659
|
+
start_time = MonotonicTime.now
|
660
|
+
yield
|
661
|
+
end_time = MonotonicTime.now
|
662
|
+
duration = end_time - start_time
|
663
|
+
raise NATS::IO::Timeout.new("nats: timeout") if duration > timeout
|
664
|
+
end
|
665
|
+
|
666
|
+
# Handles errors from reading, parsing the protocol or stale connection.
|
667
|
+
# the lock should not be held entering this function.
|
668
|
+
def process_op_error(e)
|
669
|
+
should_bail = synchronize do
|
670
|
+
connecting? || closed? || reconnecting?
|
671
|
+
end
|
672
|
+
return if should_bail
|
673
|
+
|
674
|
+
synchronize do
|
675
|
+
# If we were connected and configured to reconnect,
|
676
|
+
# then trigger disconnect and start reconnection logic
|
677
|
+
if connected? and should_reconnect?
|
678
|
+
@status = RECONNECTING
|
679
|
+
@io.close if @io
|
680
|
+
@io = nil
|
681
|
+
|
682
|
+
# TODO: Reconnecting pending buffer?
|
683
|
+
|
684
|
+
# Reconnect under a different thread than the one
|
685
|
+
# which got the error.
|
686
|
+
Thread.new do
|
687
|
+
begin
|
688
|
+
# Abort currently running reads in case they're around
|
689
|
+
# FIXME: There might be more graceful way here...
|
690
|
+
@read_loop_thread.exit if @read_loop_thread.alive?
|
691
|
+
@flusher_thread.exit if @flusher_thread.alive?
|
692
|
+
@ping_interval_thread.exit if @ping_interval_thread.alive?
|
693
|
+
|
694
|
+
attempt_reconnect
|
695
|
+
rescue NoServersError => e
|
696
|
+
@last_err = e
|
697
|
+
close
|
698
|
+
end
|
699
|
+
end
|
700
|
+
|
701
|
+
Thread.exit
|
702
|
+
return
|
703
|
+
end
|
704
|
+
|
705
|
+
# Otherwise, stop trying to reconnect and close the connection
|
706
|
+
@status = DISCONNECTED
|
707
|
+
@last_err = e
|
708
|
+
end
|
709
|
+
|
710
|
+
# Otherwise close the connection to NATS
|
711
|
+
close
|
712
|
+
end
|
713
|
+
|
714
|
+
# Gathers data from the socket and sends it to the parser.
|
715
|
+
def read_loop
|
716
|
+
loop do
|
717
|
+
begin
|
718
|
+
should_bail = synchronize do
|
719
|
+
# FIXME: In case of reconnect as well?
|
720
|
+
@status == CLOSED or @status == RECONNECTING
|
721
|
+
end
|
722
|
+
if !@io or @io.closed? or should_bail
|
723
|
+
return
|
724
|
+
end
|
725
|
+
|
726
|
+
# TODO: Remove timeout and just wait to be ready
|
727
|
+
data = @io.read(MAX_SOCKET_READ_BYTES)
|
728
|
+
@parser.parse(data) if data
|
729
|
+
rescue Errno::ETIMEDOUT
|
730
|
+
# FIXME: We do not really need a timeout here...
|
731
|
+
retry
|
732
|
+
rescue => e
|
733
|
+
# In case of reading/parser errors, trigger
|
734
|
+
# reconnection logic in case desired.
|
735
|
+
process_op_error(e)
|
736
|
+
end
|
737
|
+
end
|
738
|
+
end
|
739
|
+
|
740
|
+
# Waits for client to notify the flusher that it will be
|
741
|
+
# it is sending a command.
|
742
|
+
def flusher_loop
|
743
|
+
loop do
|
744
|
+
# Blocks waiting for the flusher to be kicked...
|
745
|
+
@flush_queue.pop
|
746
|
+
|
747
|
+
should_bail = synchronize do
|
748
|
+
@status != CONNECTED || @status == CONNECTING
|
749
|
+
end
|
750
|
+
return if should_bail
|
751
|
+
|
752
|
+
# Skip in case nothing remains pending already.
|
753
|
+
next if @pending_queue.empty?
|
754
|
+
|
755
|
+
# FIXME: should limit how many commands to take at once
|
756
|
+
# since producers could be adding as many as possible
|
757
|
+
# until reaching the max pending queue size.
|
758
|
+
cmds = []
|
759
|
+
cmds << @pending_queue.pop until @pending_queue.empty?
|
760
|
+
begin
|
761
|
+
@io.write(cmds.join) unless cmds.empty?
|
762
|
+
rescue => e
|
763
|
+
synchronize do
|
764
|
+
@last_err = e
|
765
|
+
@err_cb.call(e) if @err_cb
|
766
|
+
end
|
767
|
+
|
768
|
+
# TODO: Thread.exit?
|
769
|
+
process_op_error(e)
|
770
|
+
return
|
771
|
+
end if @io
|
772
|
+
|
773
|
+
synchronize do
|
774
|
+
@pending_size = 0
|
775
|
+
end
|
776
|
+
end
|
777
|
+
end
|
778
|
+
|
779
|
+
def ping_interval_loop
|
780
|
+
loop do
|
781
|
+
sleep @options[:ping_interval]
|
782
|
+
if @pings_outstanding > @options[:max_outstanding_pings]
|
783
|
+
# FIXME: Check if we have to dispatch callbacks.
|
784
|
+
close
|
785
|
+
end
|
786
|
+
@pings_outstanding += 1
|
787
|
+
|
788
|
+
send_command(PING_REQUEST)
|
789
|
+
@flush_queue << :ping
|
790
|
+
end
|
791
|
+
rescue => e
|
792
|
+
process_op_error(e)
|
793
|
+
end
|
794
|
+
|
795
|
+
def process_connect_init
|
796
|
+
line = @io.read_line
|
797
|
+
_, info_json = line.split(' ')
|
798
|
+
process_info(info_json)
|
799
|
+
|
800
|
+
case
|
801
|
+
when (server_using_secure_connection? and client_using_secure_connection?)
|
802
|
+
tls_context = nil
|
803
|
+
|
804
|
+
if @tls
|
805
|
+
# Allow prepared context and customizations via :tls opts
|
806
|
+
tls_context = @tls[:context] if @tls[:context]
|
807
|
+
else
|
808
|
+
# Defaults
|
809
|
+
tls_context = OpenSSL::SSL::SSLContext.new
|
810
|
+
tls_context.ssl_version = :TLSv1_2
|
811
|
+
end
|
812
|
+
|
813
|
+
# Setup TLS connection by rewrapping the socket
|
814
|
+
tls_socket = OpenSSL::SSL::SSLSocket.new(@io.socket, tls_context)
|
815
|
+
tls_socket.connect
|
816
|
+
@io.socket = tls_socket
|
817
|
+
when (server_using_secure_connection? and !client_using_secure_connection?)
|
818
|
+
raise NATS::IO::ConnectError.new('TLS/SSL required by server')
|
819
|
+
when (client_using_secure_connection? and !server_using_secure_connection?)
|
820
|
+
raise NATS::IO::ConnectError.new('TLS/SSL not supported by server')
|
821
|
+
else
|
822
|
+
# Otherwise, use a regular connection.
|
823
|
+
end
|
824
|
+
|
825
|
+
if @server_info[:auth_required]
|
826
|
+
current = server_pool.first
|
827
|
+
current[:auth_required] = true
|
828
|
+
end
|
829
|
+
|
830
|
+
# Send connect and process synchronously. If using TLS,
|
831
|
+
# it should have handled upgrading at this point.
|
832
|
+
@io.write(connect_command)
|
833
|
+
|
834
|
+
# Send ping/pong after connect
|
835
|
+
@io.write(PING_REQUEST)
|
836
|
+
|
837
|
+
next_op = @io.read_line
|
838
|
+
if @options[:verbose]
|
839
|
+
# Need to get another command here if verbose
|
840
|
+
raise NATS::IO::ConnectError.new("expected to receive +OK") unless next_op =~ NATS::Protocol::OK
|
841
|
+
next_op = @io.read_line
|
842
|
+
end
|
843
|
+
|
844
|
+
case next_op
|
845
|
+
when NATS::Protocol::PONG
|
846
|
+
when NATS::Protocol::ERR
|
847
|
+
if @server_info[:auth_required]
|
848
|
+
raise NATS::IO::AuthError.new($1)
|
849
|
+
else
|
850
|
+
raise NATS::IO::ServerError.new($1)
|
851
|
+
end
|
852
|
+
else
|
853
|
+
raise NATS::IO::ConnectError.new("expected PONG, got #{next_op}")
|
854
|
+
end
|
855
|
+
end
|
856
|
+
|
857
|
+
# Reconnect logic, this is done while holding the lock.
|
858
|
+
def attempt_reconnect
|
859
|
+
@disconnect_cb.call(@last_err) if @disconnect_cb
|
860
|
+
|
861
|
+
# Clear sticky error
|
862
|
+
@last_err = nil
|
863
|
+
|
864
|
+
# Do reconnect
|
865
|
+
begin
|
866
|
+
current = select_next_server
|
867
|
+
|
868
|
+
# Establish TCP connection with new server
|
869
|
+
@io = create_socket
|
870
|
+
@io.connect
|
871
|
+
@stats[:reconnects] += 1
|
872
|
+
|
873
|
+
# Established TCP connection successfully so can start connect
|
874
|
+
process_connect_init
|
875
|
+
|
876
|
+
# Reset reconnection attempts if connection is valid
|
877
|
+
current[:reconnect_attempts] = 0
|
878
|
+
rescue NoServersError => e
|
879
|
+
raise e
|
880
|
+
rescue => e
|
881
|
+
@last_err = e
|
882
|
+
|
883
|
+
# Continue retrying until there are no options left in the server pool
|
884
|
+
retry
|
885
|
+
end
|
886
|
+
|
887
|
+
# Clear pending flush calls and reset state before restarting loops
|
888
|
+
@flush_queue.clear
|
889
|
+
@pings_outstanding = 0
|
890
|
+
@pongs_received = 0
|
891
|
+
|
892
|
+
# Replay all subscriptions
|
893
|
+
@subs.each_pair do |sid, sub|
|
894
|
+
@io.write("SUB #{sub.subject} #{sub.queue} #{sid}#{CR_LF}")
|
895
|
+
end
|
896
|
+
|
897
|
+
# Flush anything which was left pending, in case of errors during flush
|
898
|
+
# then we should raise error then retry the reconnect logic
|
899
|
+
cmds = []
|
900
|
+
cmds << @pending_queue.pop until @pending_queue.empty?
|
901
|
+
@io.write(cmds.join) unless cmds.empty?
|
902
|
+
@status = CONNECTED
|
903
|
+
@pending_size = 0
|
904
|
+
|
905
|
+
# Now connected to NATS, and we can restart parser loop, flusher
|
906
|
+
# and ping interval
|
907
|
+
start_threads!
|
908
|
+
|
909
|
+
# Dispatch the reconnected callback while holding lock
|
910
|
+
# which we should have already
|
911
|
+
@reconnect_cb.call if @reconnect_cb
|
912
|
+
end
|
913
|
+
|
914
|
+
def start_threads!
|
915
|
+
# Reading loop for gathering data
|
916
|
+
@read_loop_thread = Thread.new { read_loop }
|
917
|
+
@read_loop_thread.abort_on_exception = true
|
918
|
+
|
919
|
+
# Flusher loop for sending commands
|
920
|
+
@flusher_thread = Thread.new { flusher_loop }
|
921
|
+
@flusher_thread.abort_on_exception = true
|
922
|
+
|
923
|
+
# Ping interval handling for keeping alive the connection
|
924
|
+
@ping_interval_thread = Thread.new { ping_interval_loop }
|
925
|
+
@ping_interval_thread.abort_on_exception = true
|
926
|
+
end
|
927
|
+
|
928
|
+
def can_reuse_server?(server)
|
929
|
+
# We will retry a number of times to reconnect to a server
|
930
|
+
# unless we got a hard error from it already.
|
931
|
+
server[:reconnect_attempts] <= @options[:max_reconnect_attempts] && !server[:error_received]
|
932
|
+
end
|
933
|
+
|
934
|
+
def should_delay_connect?(server)
|
935
|
+
server[:was_connected] && server[:reconnect_attempts] >= 0
|
936
|
+
end
|
937
|
+
|
938
|
+
def should_not_reconnect?
|
939
|
+
!@options[:reconnect]
|
940
|
+
end
|
941
|
+
|
942
|
+
def should_reconnect?
|
943
|
+
@options[:reconnect]
|
944
|
+
end
|
945
|
+
|
946
|
+
def create_socket
|
947
|
+
NATS::IO::Socket.new({
|
948
|
+
uri: @uri,
|
949
|
+
connect_timeout: DEFAULT_CONNECT_TIMEOUT
|
950
|
+
})
|
951
|
+
end
|
952
|
+
end
|
953
|
+
|
954
|
+
# Implementation adapted from https://github.com/redis/redis-rb
|
955
|
+
class Socket
|
956
|
+
attr_accessor :socket
|
957
|
+
|
958
|
+
# Exceptions raised during non-blocking I/O ops that require retrying the op
|
959
|
+
NBIO_READ_EXCEPTIONS = [Errno::EWOULDBLOCK, Errno::EAGAIN, ::IO::WaitReadable]
|
960
|
+
NBIO_WRITE_EXCEPTIONS = [Errno::EWOULDBLOCK, Errno::EAGAIN, ::IO::WaitWritable]
|
961
|
+
|
962
|
+
def initialize(options={})
|
963
|
+
@uri = options[:uri]
|
964
|
+
@connect_timeout = options[:connect_timeout]
|
965
|
+
@write_timeout = options[:write_timeout]
|
966
|
+
@read_timeout = options[:read_timeout]
|
967
|
+
@socket = nil
|
968
|
+
end
|
969
|
+
|
970
|
+
def connect
|
971
|
+
addrinfo = ::Socket.getaddrinfo(@uri.host, nil, ::Socket::AF_UNSPEC, ::Socket::SOCK_STREAM)
|
972
|
+
addrinfo.each_with_index do |ai, i|
|
973
|
+
begin
|
974
|
+
@socket = connect_addrinfo(ai, @uri.port, @connect_timeout)
|
975
|
+
rescue SystemCallError
|
976
|
+
# Give up if no more available
|
977
|
+
raise if addrinfo.length == i+1
|
978
|
+
end
|
979
|
+
end
|
980
|
+
|
981
|
+
# Set TCP no delay by default
|
982
|
+
@socket.setsockopt(::Socket::IPPROTO_TCP, ::Socket::TCP_NODELAY, 1)
|
983
|
+
end
|
984
|
+
|
985
|
+
def read_line(deadline=nil)
|
986
|
+
# FIXME: Should accumulate and read in a non blocking way instead
|
987
|
+
raise Errno::ETIMEDOUT unless ::IO.select([@socket], nil, nil, deadline)
|
988
|
+
@socket.gets
|
989
|
+
end
|
990
|
+
|
991
|
+
def read(max_bytes, deadline=nil)
|
992
|
+
begin
|
993
|
+
return @socket.read_nonblock(max_bytes)
|
994
|
+
rescue *NBIO_READ_EXCEPTIONS
|
995
|
+
if ::IO.select([@socket], nil, nil, deadline)
|
996
|
+
retry
|
997
|
+
else
|
998
|
+
raise Errno::ETIMEDOUT
|
999
|
+
end
|
1000
|
+
rescue *NBIO_WRITE_EXCEPTIONS
|
1001
|
+
if ::IO.select(nil, [@socket], nil, deadline)
|
1002
|
+
retry
|
1003
|
+
else
|
1004
|
+
raise Errno::ETIMEDOUT
|
1005
|
+
end
|
1006
|
+
end
|
1007
|
+
rescue EOFError => e
|
1008
|
+
if RUBY_ENGINE == 'jruby' and e.message == 'No message available'
|
1009
|
+
# FIXME: <EOFError: No message available> can happen in jruby
|
1010
|
+
# even though seems it is temporary and eventually possible
|
1011
|
+
# to read from socket.
|
1012
|
+
return nil
|
1013
|
+
end
|
1014
|
+
raise Errno::ECONNRESET
|
1015
|
+
end
|
1016
|
+
|
1017
|
+
def write(data, deadline=nil)
|
1018
|
+
length = data.bytesize
|
1019
|
+
total_written = 0
|
1020
|
+
|
1021
|
+
loop do
|
1022
|
+
begin
|
1023
|
+
written = @socket.write_nonblock(data)
|
1024
|
+
|
1025
|
+
total_written += written
|
1026
|
+
break total_written if total_written >= length
|
1027
|
+
data = data.byteslice(written..-1)
|
1028
|
+
rescue *NBIO_WRITE_EXCEPTIONS
|
1029
|
+
if ::IO.select(nil, [@socket], nil, deadline)
|
1030
|
+
retry
|
1031
|
+
else
|
1032
|
+
raise Errno::ETIMEDOUT
|
1033
|
+
end
|
1034
|
+
rescue *NBIO_READ_EXCEPTIONS => e
|
1035
|
+
if ::IO.select([@socket], nil, nil, deadline)
|
1036
|
+
retry
|
1037
|
+
else
|
1038
|
+
raise Errno::ETIMEDOUT
|
1039
|
+
end
|
1040
|
+
end
|
1041
|
+
end
|
1042
|
+
|
1043
|
+
rescue EOFError
|
1044
|
+
raise Errno::ECONNRESET
|
1045
|
+
end
|
1046
|
+
|
1047
|
+
def close
|
1048
|
+
@socket.close
|
1049
|
+
end
|
1050
|
+
|
1051
|
+
def closed?
|
1052
|
+
@socket.closed?
|
1053
|
+
end
|
1054
|
+
|
1055
|
+
private
|
1056
|
+
|
1057
|
+
def connect_addrinfo(ai, port, timeout)
|
1058
|
+
sock = ::Socket.new(::Socket.const_get(ai[0]), ::Socket::SOCK_STREAM, 0)
|
1059
|
+
sockaddr = ::Socket.pack_sockaddr_in(port, ai[3])
|
1060
|
+
|
1061
|
+
begin
|
1062
|
+
sock.connect_nonblock(sockaddr)
|
1063
|
+
rescue Errno::EINPROGRESS
|
1064
|
+
raise Errno::ETIMEDOUT unless ::IO.select(nil, [sock], nil, @connect_timeout)
|
1065
|
+
|
1066
|
+
# Confirm that connection was established
|
1067
|
+
begin
|
1068
|
+
sock.connect_nonblock(sockaddr)
|
1069
|
+
rescue Errno::EISCONN
|
1070
|
+
# Connection was established without issues.
|
1071
|
+
end
|
1072
|
+
end
|
1073
|
+
|
1074
|
+
sock
|
1075
|
+
end
|
1076
|
+
end
|
1077
|
+
end
|
1078
|
+
|
1079
|
+
Msg = Struct.new(:subject, :reply, :data)
|
1080
|
+
|
1081
|
+
class Subscription
|
1082
|
+
include MonitorMixin
|
1083
|
+
|
1084
|
+
attr_accessor :subject, :queue, :future, :callback, :response, :received, :max
|
1085
|
+
|
1086
|
+
def initialize
|
1087
|
+
super # required to initialize monitor
|
1088
|
+
@subject = ''
|
1089
|
+
@queue = nil
|
1090
|
+
@future = nil
|
1091
|
+
@callback = nil
|
1092
|
+
@response = nil
|
1093
|
+
@received = 0
|
1094
|
+
@max = nil
|
1095
|
+
end
|
1096
|
+
end
|
1097
|
+
|
1098
|
+
# Implementation of MonotonicTime adapted from
|
1099
|
+
# https://github.com/ruby-concurrency/concurrent-ruby/
|
1100
|
+
class MonotonicTime
|
1101
|
+
class << self
|
1102
|
+
case
|
1103
|
+
when defined?(Process::CLOCK_MONOTONIC)
|
1104
|
+
def now
|
1105
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
1106
|
+
end
|
1107
|
+
when RUBY_ENGINE == 'jruby'
|
1108
|
+
def now
|
1109
|
+
java.lang.System.nanoTime() / 1_000_000_000.0
|
1110
|
+
end
|
1111
|
+
else
|
1112
|
+
def now
|
1113
|
+
# Fallback to regular time behavior
|
1114
|
+
::Time.now.to_f
|
1115
|
+
end
|
1116
|
+
end
|
1117
|
+
end
|
1118
|
+
end
|
1119
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module NATS
|
2
|
+
module Protocol
|
3
|
+
|
4
|
+
MSG = /\AMSG\s+([^\s]+)\s+([^\s]+)\s+(([^\s]+)[^\S\r\n]+)?(\d+)\r\n/i
|
5
|
+
OK = /\A\+OK\s*\r\n/i
|
6
|
+
ERR = /\A-ERR\s+('.+')?\r\n/i
|
7
|
+
PING = /\APING\s*\r\n/i
|
8
|
+
PONG = /\APONG\s*\r\n/i
|
9
|
+
INFO = /\AINFO\s+([^\r\n]+)\r\n/i
|
10
|
+
UNKNOWN = /\A(.*)\r\n/
|
11
|
+
|
12
|
+
AWAITING_CONTROL_LINE = 1
|
13
|
+
AWAITING_MSG_PAYLOAD = 2
|
14
|
+
|
15
|
+
CR_LF = ("\r\n".freeze)
|
16
|
+
CR_LF_SIZE = (CR_LF.bytesize)
|
17
|
+
|
18
|
+
PING_REQUEST = ("PING#{CR_LF}".freeze)
|
19
|
+
PONG_RESPONSE = ("PONG#{CR_LF}".freeze)
|
20
|
+
|
21
|
+
SUB_OP = ('SUB'.freeze)
|
22
|
+
EMPTY_MSG = (''.freeze)
|
23
|
+
|
24
|
+
class Parser
|
25
|
+
def initialize(nc)
|
26
|
+
@nc = nc
|
27
|
+
@buf = nil
|
28
|
+
@needed = nil
|
29
|
+
@parse_state = AWAITING_CONTROL_LINE
|
30
|
+
|
31
|
+
@sub = nil
|
32
|
+
@sid = nil
|
33
|
+
@reply = nil
|
34
|
+
@needed = nil
|
35
|
+
end
|
36
|
+
|
37
|
+
def parse(data)
|
38
|
+
@buf = @buf ? @buf << data : data
|
39
|
+
while (@buf)
|
40
|
+
case @parse_state
|
41
|
+
when AWAITING_CONTROL_LINE
|
42
|
+
case @buf
|
43
|
+
when MSG
|
44
|
+
@buf = $'
|
45
|
+
@sub, @sid, @reply, @needed = $1, $2.to_i, $4, $5.to_i
|
46
|
+
@parse_state = AWAITING_MSG_PAYLOAD
|
47
|
+
when OK # No-op right now
|
48
|
+
@buf = $'
|
49
|
+
when ERR
|
50
|
+
@buf = $'
|
51
|
+
@nc.process_err($1)
|
52
|
+
when PING
|
53
|
+
@buf = $'
|
54
|
+
@nc.process_ping
|
55
|
+
when PONG
|
56
|
+
@buf = $'
|
57
|
+
@nc.process_pong
|
58
|
+
when INFO
|
59
|
+
@buf = $'
|
60
|
+
# First INFO message is processed synchronously on connect,
|
61
|
+
# and onwards we would be receiving asynchronously INFO commands
|
62
|
+
# signaling possible changes in the topology of the NATS cluster.
|
63
|
+
@nc.process_info($1)
|
64
|
+
when UNKNOWN
|
65
|
+
@buf = $'
|
66
|
+
@nc.process_err("Unknown protocol: #{$1}")
|
67
|
+
else
|
68
|
+
# If we are here we do not have a complete line yet that we understand.
|
69
|
+
return
|
70
|
+
end
|
71
|
+
@buf = nil if (@buf && @buf.empty?)
|
72
|
+
|
73
|
+
when AWAITING_MSG_PAYLOAD
|
74
|
+
return unless (@needed && @buf.bytesize >= (@needed + CR_LF_SIZE))
|
75
|
+
@nc.process_msg(@sub, @sid, @reply, @buf.slice(0, @needed))
|
76
|
+
@buf = @buf.slice((@needed + CR_LF_SIZE), @buf.bytesize)
|
77
|
+
@sub = @sid = @reply = @needed = nil
|
78
|
+
@parse_state = AWAITING_CONTROL_LINE
|
79
|
+
@buf = nil if (@buf && @buf.empty?)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
metadata
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: nats-pure
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Waldemar Quevedo
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-12-07 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: NATS is an open-source, high-performance, lightweight cloud messaging
|
14
|
+
system.
|
15
|
+
email:
|
16
|
+
- wally@apcera.com
|
17
|
+
executables: []
|
18
|
+
extensions: []
|
19
|
+
extra_rdoc_files: []
|
20
|
+
files:
|
21
|
+
- lib/nats/io/client.rb
|
22
|
+
- lib/nats/io/parser.rb
|
23
|
+
- lib/nats/io/version.rb
|
24
|
+
homepage: https://nats.io
|
25
|
+
licenses:
|
26
|
+
- MIT
|
27
|
+
metadata: {}
|
28
|
+
post_install_message:
|
29
|
+
rdoc_options: []
|
30
|
+
require_paths:
|
31
|
+
- lib
|
32
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
33
|
+
requirements:
|
34
|
+
- - ">="
|
35
|
+
- !ruby/object:Gem::Version
|
36
|
+
version: '0'
|
37
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - ">="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '0'
|
42
|
+
requirements: []
|
43
|
+
rubyforge_project:
|
44
|
+
rubygems_version: 2.5.1
|
45
|
+
signing_key:
|
46
|
+
specification_version: 4
|
47
|
+
summary: NATS is an open-source, high-performance, lightweight cloud messaging system.
|
48
|
+
test_files: []
|