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 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
@@ -0,0 +1,8 @@
1
+ module NATS
2
+ module IO
3
+ # NOTE: These are all announced to the server on CONNECT
4
+ VERSION = "0.1.0"
5
+ LANG = "#{RUBY_ENGINE}2".freeze
6
+ PROTOCOL = 1
7
+ end
8
+ 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: []