nats-pure 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|