nats-pure 0.1.0

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