nats 0.4.10 → 0.4.22

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.
@@ -0,0 +1,42 @@
1
+ # HISTORY
2
+
3
+ ## v0.4.22 (Mar 5, 2012)
4
+
5
+ - HTTP based server monitoring (/varz, /connz, /healthz)
6
+ - Perfomance and Stability improvements
7
+ - Client monitoring
8
+ - Server to Client pings
9
+ - Multiple Auth users
10
+ - SSL/TSL support
11
+ - nats-top utility
12
+ - Connection state dump on SIGUSR2
13
+ - Client Server information support
14
+ - Client Fast Producer support
15
+ - Client reconenct callbacks
16
+ - Server Max Connections support
17
+ - See full list @ https://github.com/derekcollison/nats/compare/v0.4.10...v0.4.22
18
+
19
+ ## v0.4.10 (Apr 21, 2011)
20
+
21
+ - Minor bug fixes
22
+ - See full list @ https://github.com/derekcollison/nats/compare/v0.4.8...v0.4.10
23
+
24
+ ## v0.4.8 (Apr 2, 2011)
25
+
26
+ - Minor bug fixes
27
+ - See full list @ https://github.com/derekcollison/nats/compare/v0.4.2...v0.4.8
28
+
29
+ ## v0.4.2 (Feb 21, 2011)
30
+
31
+ - Queue group support
32
+ - Auto-unsubscribe support
33
+ - Time expiration on subscriptions
34
+ - Jruby initial support
35
+ - Performance enhancements
36
+ - Complete config file support
37
+ - See full list @ https://github.com/derekcollison/nats/compare/v0.3.12...v0.4.2
38
+
39
+ ## v0.3.12 (Nov 21, 2010)
40
+
41
+ - Initial Release
42
+
data/README.md CHANGED
@@ -1,102 +1,114 @@
1
1
  # NATS
2
2
 
3
- A lightweight EventMachine based publish-subscribe messaging system.
3
+ A lightweight publish-subscribe and distributed queueing messaging system.
4
+
5
+ [![Build Status](https://secure.travis-ci.org/derekcollison/nats.png)](http://travis-ci.org/derekcollison/nats)
4
6
 
5
7
  ## Supported Platforms
6
8
 
7
9
  This gem currently works on the following Ruby platforms:
8
10
 
9
- - MRI 1.8 and 1.9 (Performance is best on 1.9.2)
11
+ - MRI 1.8 and 1.9 (Performance is best on 1.9.3)
10
12
  - Rubinius
11
13
  - JRuby
12
14
 
15
+ There is a [Node.js](https://github.com/derekcollison/node_nats) client available as well.
16
+
13
17
  ## Getting Started
14
18
 
15
- [sudo] gem install nats
16
- == or ==
17
- [sudo] rake geminstall
19
+ ```bash
20
+ [sudo] gem install nats
21
+ == or ==
22
+ [sudo] rake geminstall
18
23
 
19
- nats-sub foo &
20
- nats-pub foo "Hello World!'
24
+ nats-sub foo &
25
+ nats-pub foo 'Hello World!'
26
+ ```
21
27
 
22
- ## Usage
28
+ ## Basic Usage
23
29
 
24
- require "nats/client"
30
+ ```ruby
31
+ require "nats/client"
25
32
 
26
- NATS.start do
33
+ NATS.start do
27
34
 
28
- # Simple Subscriber
29
- NATS.subscribe('foo') { |msg| puts "Msg received : '#{msg}'" }
35
+ # Simple Subscriber
36
+ NATS.subscribe('foo') { |msg| puts "Msg received : '#{msg}'" }
30
37
 
31
- # Simple Publisher
32
- NATS.publish('foo.bar.baz', 'Hello World!')
38
+ # Simple Publisher
39
+ NATS.publish('foo.bar.baz', 'Hello World!')
33
40
 
34
- # Unsubscribing
35
- sid = NATS.subscribe('bar') { |msg| puts "Msg received : '#{msg}'" }
36
- NATS.unsubscribe(sid)
41
+ # Unsubscribing
42
+ sid = NATS.subscribe('bar') { |msg| puts "Msg received : '#{msg}'" }
43
+ NATS.unsubscribe(sid)
37
44
 
38
- # Requests
39
- NATS.request('help') { |response| puts "Got a response: '#{response}'" }
45
+ # Requests
46
+ NATS.request('help') { |response| puts "Got a response: '#{response}'" }
40
47
 
41
- # Replies
42
- NATS.subscribe('help') { |msg, reply| NATS.publish(reply, "I'll help!") }
48
+ # Replies
49
+ NATS.subscribe('help') { |msg, reply| NATS.publish(reply, "I'll help!") }
43
50
 
44
- # Stop using NATS.stop, exits EM loop if NATS.start started the loop
45
- NATS.stop
51
+ # Stop using NATS.stop, exits EM loop if NATS.start started the loop
52
+ NATS.stop
46
53
 
47
- end
54
+ end
55
+ ```
48
56
 
49
57
  ## Wildcard Subscriptions
50
58
 
51
- # '*" matches any token, at any level of the subject.
52
- NATS.subscribe('foo.*.baz') { |msg, reply, sub| puts "Msg received on [#{sub}] : '#{msg}'" }
53
- NATS.subscribe('foo.bar.*') { |msg, reply, sub| puts "Msg received on [#{sub}] : '#{msg}'" }
54
- NATS.subscribe('*.bar.*') { |msg, reply, sub| puts "Msg received on [#{sub}] : '#{msg}'" }
59
+ ```ruby
60
+ # "*" matches any token, at any level of the subject.
61
+ NATS.subscribe('foo.*.baz') { |msg, reply, sub| puts "Msg received on [#{sub}] : '#{msg}'" }
62
+ NATS.subscribe('foo.bar.*') { |msg, reply, sub| puts "Msg received on [#{sub}] : '#{msg}'" }
63
+ NATS.subscribe('*.bar.*') { |msg, reply, sub| puts "Msg received on [#{sub}] : '#{msg}'" }
55
64
 
56
- # '>" matches any length of the tail of a subject and can only be last token
57
- # E.g. 'foo.>' will match 'foo.bar', 'foo.bar.baz', 'foo.foo.bar.bax.22'
58
- NATS.subscribe('foo.>') { |msg, reply, sub| puts "Msg received on [#{sub}] : '#{msg}'" }
65
+ # ">" matches any length of the tail of a subject and can only be the last token
66
+ # E.g. 'foo.>' will match 'foo.bar', 'foo.bar.baz', 'foo.foo.bar.bax.22'
67
+ NATS.subscribe('foo.>') { |msg, reply, sub| puts "Msg received on [#{sub}] : '#{msg}'" }
68
+ ```
59
69
 
60
70
  ## Queues Groups
61
71
 
62
- # All subscriptions with the same queue name will form a queue group
63
- # Each message will be delivered to only one subscriber per queue group, queuing semantics
64
- # You can have as many queue groups as you wish
65
- # Normal subscribers will continue to work as expected.
66
- NATS.subscribe(subject, :queue => 'job.workers') { |msg| puts "Received '#{msg}'" }
72
+ ```ruby
73
+ # All subscriptions with the same queue name will form a queue group
74
+ # Each message will be delivered to only one subscriber per queue group, queuing semantics
75
+ # You can have as many queue groups as you wish
76
+ # Normal subscribers will continue to work as expected.
77
+ NATS.subscribe(subject, :queue => 'job.workers') { |msg| puts "Received '#{msg}'" }
78
+ ```
67
79
 
68
80
  ## Advanced Usage
81
+ ```ruby
82
+ # Publish with closure, callback fires when server has processed the message
83
+ NATS.publish('foo', 'You done?') { puts 'msg processed!' }
69
84
 
70
- # Publish with closure, callback fires when server has processed the message
71
- NATS.publish('foo', 'You done?') { puts 'msg processed!' }
72
-
73
- # Timeouts for subscriptions
74
- sid = NATS.subscribe('foo') { received += 1 }
75
- NATS.timeout(sid, TIMEOUT_IN_SECS) { timeout_recvd = true }
76
-
77
- # Timeout unless a certain number of messages have been received
78
- NATS.timeout(sid, TIMEOUT_IN_SECS, :expected => 2) { timeout_recvd = true }
85
+ # Timeouts for subscriptions
86
+ sid = NATS.subscribe('foo') { received += 1 }
87
+ NATS.timeout(sid, TIMEOUT_IN_SECS) { timeout_recvd = true }
79
88
 
80
- # Auto-unsubscribe after MAX_WANTED messages received
81
- NATS.unsubscribe(sid, MAX_WANTED)
89
+ # Timeout unless a certain number of messages have been received
90
+ NATS.timeout(sid, TIMEOUT_IN_SECS, :expected => 2) { timeout_recvd = true }
82
91
 
83
- # Multiple connections
84
- NATS.subscribe('test') do |msg|
85
- puts "received msg"
86
- NATS.stop
87
- end
92
+ # Auto-unsubscribe after MAX_WANTED messages received
93
+ NATS.unsubscribe(sid, MAX_WANTED)
88
94
 
89
- # Form second connection to send message on
90
- NATS.connect { NATS.publish('test', 'Hello World!') }
95
+ # Multiple connections
96
+ NATS.subscribe('test') do |msg|
97
+ puts "received msg"
98
+ NATS.stop
99
+ end
91
100
 
101
+ # Form second connection to send message on
102
+ NATS.connect { NATS.publish('test', 'Hello World!') }
103
+ ```
92
104
 
93
- See examples and benchmark for more information..
105
+ See examples and benchmarks for more information..
94
106
 
95
107
  ## License
96
108
 
97
109
  (The MIT License)
98
110
 
99
- Copyright (c) 2010, 2011 Derek Collison
111
+ Copyright (c) 2010-2012 Derek Collison
100
112
 
101
113
  Permission is hereby granted, free of charge, to any person obtaining a copy
102
114
  of this software and associated documentation files (the "Software"), to
data/Rakefile CHANGED
@@ -1,4 +1,3 @@
1
-
2
1
  desc "Run rspec"
3
2
  task :spec do
4
3
  sh('bundle install')
@@ -7,6 +6,7 @@ task :spec do
7
6
  t.rspec_opts = %w(-fs -c)
8
7
  end
9
8
  end
9
+ task :default => :spec
10
10
 
11
11
  desc "Build the gem"
12
12
  task :gem do
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'optparse'
4
+ require 'rubygems'
5
+ require 'nats/client'
6
+
7
+ ['TERM', 'INT'].each { |s| trap(s) { puts; exit! } }
8
+
9
+ def usage
10
+ puts "Usage: nats-request <subject> <msg> [-s server] [-t] [-n responses]"; exit
11
+ end
12
+
13
+ args = ARGV.dup
14
+ opts_parser = OptionParser.new do |opts|
15
+ opts.on('-s SERVER') { |server| $nats_server = server }
16
+ opts.on('-t') { $show_time = true }
17
+ opts.on('-n RESPONSES') { |responses| $responses = Integer(responses) if Integer(responses) > 0 }
18
+ end
19
+ args = opts_parser.parse!(args)
20
+
21
+ subject, msg = args
22
+ usage unless subject
23
+ msg ||= 'Hello World'
24
+
25
+ def time_prefix
26
+ "[#{Time.now}] " if $show_time
27
+ end
28
+
29
+ def header
30
+ $i=0 unless $i
31
+ "#{time_prefix}[\##{$i+=1}]"
32
+ end
33
+
34
+ NATS.on_error { |err| puts "Server Error: #{err}"; exit! }
35
+
36
+ NATS.start(:uri => $nats_server, :autostart => true) do
37
+ NATS.request(subject, msg) { |(msg, reply)|
38
+ puts "#{header} Replied with : '#{msg}'"
39
+ exit! if $responses && ($responses-=1) < 1
40
+ }
41
+ end
@@ -0,0 +1,146 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'optparse'
4
+ require 'net/http'
5
+ require 'uri'
6
+ require 'io/wait'
7
+
8
+ require 'rubygems'
9
+ require 'json'
10
+
11
+ def usage
12
+ puts "Usage: nats-top [-s server_uri] [-m local monitor port] [-n num_connections] [-d delay_secs] [--sort sort_by]"
13
+ puts "--sort_options for more help"
14
+ exit
15
+ end
16
+
17
+ $valid_sort_options = ['pending_size', 'msgs_to', 'msgs_from', 'bytes_to', 'bytes_from', 'subs']
18
+
19
+ def sort_options_help
20
+ puts "Available sort_by options: #{$valid_sort_options.join(', ')}."
21
+ puts "E.g. #{$0} -s bytes_to"
22
+ exit
23
+ end
24
+
25
+ args = ARGV.dup
26
+ opts_parser = OptionParser.new do |opts|
27
+ opts.on('-s server_uri') { |server| $nats_server = server }
28
+ opts.on('-m local_port') { |port| $nats_port = port.to_i }
29
+ opts.on('-n num_connections') { |num| $num_connections = num.to_i }
30
+ opts.on('-d delay') { |delay| $delay = delay.to_f }
31
+ opts.on('--sort sort_by') { |sort_key| $sort_key = sort_key }
32
+ opts.on('--sort_options') { sort_options_help }
33
+ opts.on('-h') { usage }
34
+ opts.on('--help') { usage }
35
+ end
36
+ args = opts_parser.parse!(args)
37
+
38
+ DEFAULT_MONITOR_PORT = 9222
39
+ DEFAULT_NUM_CONNECTIONS = 10
40
+ DEFAULT_DELAY = 1 #sec
41
+ DEFAULT_SORT = 'pending_size'
42
+
43
+ $nats_port = DEFAULT_MONITOR_PORT if $nats_port.nil?
44
+ $num_connections = DEFAULT_NUM_CONNECTIONS if $num_connections.nil?
45
+ $nats_server = "http://localhost:#{$nats_port}" if $nats_server.nil?
46
+
47
+ $nats_server = "http://#{$nats_server}" unless $nats_server.start_with?('http')
48
+
49
+ $delay = DEFAULT_DELAY if $delay.nil?
50
+ $sort_key = DEFAULT_SORT if $sort_key.nil?
51
+ $sort_key.downcase!
52
+
53
+ unless $valid_sort_options.include?($sort_key)
54
+ puts "Invalid sort_by argument: #{$sort_key}"
55
+ sort_options_help
56
+ end
57
+
58
+ varz_uri = URI.parse("#{$nats_server}/varz")
59
+ connz_uri = URI.parse("#{$nats_server}/connz?n=#{$num_connections}&s=#{$sort_key}")
60
+
61
+ def psize(size, prec=1)
62
+ return 'NA' unless size
63
+ return sprintf("%.#{prec}f", size) if size < 1024
64
+ return sprintf("%.#{prec}fK", size/1024.0) if size < (1024*1024)
65
+ return sprintf("%.#{prec}fM", size/(1024.0*1024.0)) if size < (1024*1024*1024)
66
+ return sprintf("%.#{prec}fG", size/(1024.0*1024.0*1024.0))
67
+ end
68
+
69
+ def clear_screen
70
+ print "\e[H\e[2J"
71
+ end
72
+
73
+ ['TERM', 'INT'].each { |s| trap(s) { clear_screen; exit! } }
74
+
75
+ in_last_msgs = in_last_bytes = 0
76
+ out_last_msgs = out_last_bytes = 0
77
+
78
+ poll = Time.now
79
+ first = true
80
+
81
+ while true
82
+ begin
83
+
84
+ varz_response = Net::HTTP::get_response(varz_uri)
85
+ varz = JSON.parse(varz_response.body, :symbolize_keys => true, :symbolize_names => true)
86
+
87
+ # Simple rates
88
+ delta_in_msgs, in_last_msgs = varz[:in_msgs] - in_last_msgs, varz[:in_msgs]
89
+ delta_in_bytes, in_last_bytes = varz[:in_bytes] - in_last_bytes, varz[:in_bytes]
90
+ delta_out_msgs, out_last_msgs = varz[:out_msgs] - out_last_msgs, varz[:out_msgs]
91
+ delta_out_bytes, out_last_bytes = varz[:out_bytes] - out_last_bytes, varz[:out_bytes]
92
+
93
+ now = Time.now
94
+ tdelta, poll = now - poll, now
95
+
96
+ unless first
97
+ rate_in_msgs = delta_in_msgs / tdelta
98
+ rate_in_bytes = delta_in_bytes / tdelta
99
+ rate_out_msgs = delta_out_msgs / tdelta
100
+ rate_out_bytes = delta_out_bytes / tdelta
101
+ end
102
+
103
+ connz_response = Net::HTTP::get_response(connz_uri)
104
+ connz = JSON.parse(connz_response.body, :symbolize_keys => true, :symbolize_names => true)
105
+
106
+ clear_screen
107
+
108
+ puts "\nServer:"
109
+ puts " Load: CPU: #{varz[:cpu]}% Memory: #{psize(varz[:mem])}"
110
+ print " In: Msgs: #{psize(varz[:in_msgs])} Bytes: #{psize(varz[:in_bytes])}"
111
+ puts " Msgs/Sec: #{psize(rate_in_msgs)} Bytes/Sec: #{psize(rate_in_bytes)}"
112
+
113
+ print " Out: Msgs: #{psize(varz[:out_msgs])} Bytes: #{psize(varz[:out_bytes])}"
114
+ puts " Msgs/Sec: #{psize(rate_out_msgs)} Bytes/Sec: #{psize(rate_out_bytes)}"
115
+
116
+ puts "\nConnections: #{psize(connz[:num_connections], 0)}"
117
+
118
+ conn_t = " %-20s %-8s %-6s %-10s %-10s %-10s %-10s %-10s\n"
119
+ printf(conn_t, 'HOST', 'CID', 'SUBS', 'PENDING', 'MSGS_TO', 'MSGS_FROM', 'BYTES_TO', 'BYTES_FROM')
120
+
121
+ connz[:connections].each do |conn|
122
+ printf(conn_t, "#{conn[:ip]}:#{conn[:port]}",
123
+ conn[:cid],
124
+ psize(conn[:subscriptions]),
125
+ psize(conn[:pending_size]),
126
+ psize(conn[:out_msgs]),
127
+ psize(conn[:in_msgs]),
128
+ psize(conn[:out_bytes]),
129
+ psize(conn[:in_bytes])
130
+ )
131
+ end
132
+ puts
133
+
134
+ first = false
135
+
136
+ sleep($delay)
137
+
138
+ rescue => e
139
+ puts "Error: #{e}"
140
+ exit(1)
141
+ end
142
+
143
+ end
144
+
145
+
146
+
@@ -8,7 +8,7 @@ require "#{ep}/ext/json"
8
8
 
9
9
  module NATS
10
10
 
11
- VERSION = "0.4.10".freeze
11
+ VERSION = '0.4.22'.freeze
12
12
 
13
13
  DEFAULT_PORT = 4222
14
14
  DEFAULT_URI = "nats://localhost:#{DEFAULT_PORT}".freeze
@@ -16,6 +16,11 @@ module NATS
16
16
  MAX_RECONNECT_ATTEMPTS = 10
17
17
  RECONNECT_TIME_WAIT = 2
18
18
 
19
+ MAX_PENDING_SIZE = 32768
20
+
21
+ # Maximum outbound size per client to trigger FP, 20MB
22
+ FAST_PRODUCER_THRESHOLD = (10*1024*1024)
23
+
19
24
  # Protocol
20
25
  # @private
21
26
  MSG = /\AMSG\s+([^\s]+)\s+([^\s]+)\s+(([^\s]+)[^\S\r\n]+)?(\d+)\r\n/i #:nodoc:
@@ -50,19 +55,20 @@ module NATS
50
55
  # Duplicate autostart protection
51
56
  @@tried_autostart = {}
52
57
 
53
- class Error < StandardError #:nodoc:
54
- end
58
+ class Error < StandardError; end #:nodoc:
55
59
 
56
60
  # When the NATS server sends us an ERROR message, this is raised/passed by default
57
- class ServerError < Error #:nodoc:
58
- end
61
+ class ServerError < Error; end #:nodoc:
62
+
63
+ # When we detect error on the client side (e.g. Fast Producer)
64
+ class ClientError < Error; end #:nodoc:
59
65
 
60
66
  # When we cannot connect to the server (either initially or after a reconnect), this is raised/passed
61
- class ConnectError < Error #:nodoc:
62
- end
67
+ class ConnectError < Error; end #:nodoc:
63
68
 
64
69
  class << self
65
70
  attr_reader :client, :reactor_was_running, :err_cb, :err_cb_overridden #:nodoc:
71
+ attr_reader :reconnect_cb #:nodoc
66
72
  attr_accessor :timeout_cb #:nodoc
67
73
 
68
74
  alias :reactor_was_running? :reactor_was_running
@@ -72,12 +78,15 @@ module NATS
72
78
  # The optional block will be called when the connection has been completed.
73
79
  #
74
80
  # @param [Hash] opts
75
- # @option opts [String] :uri The URI to connect to, example nats://localhost:4222
81
+ # @option opts [String|URI] :uri The URI to connect to, example nats://localhost:4222
76
82
  # @option opts [Boolean] :autostart Boolean that can be used to engage server autostart functionality.
77
83
  # @option opts [Boolean] :reconnect Boolean that can be used to suppress reconnect functionality.
78
84
  # @option opts [Boolean] :debug Boolean that can be used to output additional debug information.
79
85
  # @option opts [Boolean] :verbose Boolean that is sent to server for setting verbose protocol mode.
80
86
  # @option opts [Boolean] :pedantic Boolean that is sent to server for setting pedantic mode.
87
+ # @option opts [Boolean] :ssl Boolean that is sent to server for setting TLS/SSL mode.
88
+ # @option opts [Integer] :max_reconnect_attempts Integer that can be used to set the max number of reconnect tries
89
+ # @option opts [Integer] :reconnect_time_wait Integer that can be used to set the number of seconds to wait between reconnect tries
81
90
  # @param [Block] &blk called when the connection is completed. Connection will be passed to the block.
82
91
  # @return [NATS] connection to the server.
83
92
  def connect(opts={}, &blk)
@@ -85,14 +94,22 @@ module NATS
85
94
  opts[:verbose] = false if opts[:verbose].nil?
86
95
  opts[:pedantic] = false if opts[:pedantic].nil?
87
96
  opts[:reconnect] = true if opts[:reconnect].nil?
97
+ opts[:ssl] = false if opts[:ssl].nil?
98
+ opts[:max_reconnect_attempts] = MAX_RECONNECT_ATTEMPTS if opts[:max_reconnect_attempts].nil?
99
+ opts[:reconnect_time_wait] = RECONNECT_TIME_WAIT if opts[:reconnect_time_wait].nil?
88
100
 
89
101
  # Override with ENV
90
102
  opts[:uri] ||= ENV['NATS_URI'] || DEFAULT_URI
91
- opts[:verbose] = ENV['NATS_VERBOSE'] unless ENV['NATS_VERBOSE'].nil?
92
- opts[:pedantic] = ENV['NATS_PEDANTIC'] unless ENV['NATS_PEDANTIC'].nil?
93
- opts[:debug] = ENV['NATS_DEBUG'] if !ENV['NATS_DEBUG'].nil?
94
- @uri = opts[:uri] = URI.parse(opts[:uri])
95
- @err_cb = proc {|e| raise e } unless err_cb
103
+ opts[:verbose] = ENV['NATS_VERBOSE'].downcase == 'true' unless ENV['NATS_VERBOSE'].nil?
104
+ opts[:pedantic] = ENV['NATS_PEDANTIC'].downcase == 'true' unless ENV['NATS_PEDANTIC'].nil?
105
+ opts[:debug] = ENV['NATS_DEBUG'].downcase == 'true' unless ENV['NATS_DEBUG'].nil?
106
+ opts[:reconnect] = ENV['NATS_RECONNECT'].downcase == 'true' unless ENV['NATS_RECONNECT'].nil?
107
+ opts[:fast_producer_error] = ENV['NATS_FAST_PRODUCER'].downcase == 'true' unless ENV['NATS_FAST_PRODUCER'].nil?
108
+ opts[:ssl] = ENV['NATS_SSL'].downcase == 'true' unless ENV['NATS_SSL'].nil?
109
+ opts[:max_reconnect_attempts] = ENV['NATS_MAX_RECONNECT_ATTEMPTS'].to_i unless ENV['NATS_MAX_RECONNECT_ATTEMPTS'].nil?
110
+ opts[:reconnect_time_wait] = ENV['NATS_RECONNECT_TIME_WAIT'].to_i unless ENV['NATS_RECONNECT_TIME_WAIT'].nil?
111
+ @uri = opts[:uri] = opts[:uri].is_a?(URI) ? opts[:uri] : URI.parse(opts[:uri])
112
+ @err_cb = proc { |e| raise e } unless err_cb
96
113
  check_autostart(@uri) if opts[:autostart] == true
97
114
 
98
115
  client = EM.connect(@uri.host, @uri.port, self, opts)
@@ -115,7 +132,7 @@ module NATS
115
132
  # Close the default client connection and optionally call the associated block.
116
133
  # @param [Block] &blk called when the connection is closed.
117
134
  def stop(&blk)
118
- client.close if (client and client.connected?)
135
+ client.close if (client and (client.connected? || client.reconnecting?))
119
136
  blk.call if blk
120
137
  @@tried_autostart = {}
121
138
  @err_cb = nil
@@ -127,18 +144,36 @@ module NATS
127
144
  client.connected?
128
145
  end
129
146
 
147
+ # @return [Boolean] Reconnecting state
148
+ def reconnecting?
149
+ return false unless client
150
+ client.reconnecting?
151
+ end
152
+
130
153
  # @return [Hash] Options
131
154
  def options
132
155
  return {} unless client
133
156
  client.options
134
157
  end
135
158
 
159
+ # @return [Hash] Server information
160
+ def server_info
161
+ return nil unless client
162
+ client.server_info
163
+ end
164
+
136
165
  # Set the default on_error callback.
137
166
  # @param [Block] &callback called when an error has been detected.
138
167
  def on_error(&callback)
139
168
  @err_cb, @err_cb_overridden = callback, true
140
169
  end
141
170
 
171
+ # Set the default on_reconnect callback.
172
+ # @param [Block] &callback called when a reconnect attempt is made.
173
+ def on_reconnect(&callback)
174
+ @reconnect_cb = callback
175
+ end
176
+
142
177
  # Publish a message using the default client connection.
143
178
  # @see NATS#publish
144
179
  def publish(*args, &blk)
@@ -177,6 +212,18 @@ module NATS
177
212
  "_INBOX.%04x%04x%04x%04x%04x%06x" % v
178
213
  end
179
214
 
215
+ # Flushes all messages and subscriptions in the default connection
216
+ # @see NATS#flush
217
+ def flush(*args, &blk)
218
+ (@client ||= connect).flush(*args, &blk)
219
+ end
220
+
221
+ # Return bytes outstanding for the default client connection.
222
+ # @see NATS#pending_data_size
223
+ def pending_data_size(*args)
224
+ (@client ||= connect).pending_data_size(*args)
225
+ end
226
+
180
227
  def wait_for_server(uri, max_wait = 5) # :nodoc:
181
228
  start = Time.now
182
229
  while (Time.now - start < max_wait) # Wait max_wait seconds max
@@ -194,6 +241,10 @@ module NATS
194
241
  return false
195
242
  end
196
243
 
244
+ def clear_client # :nodoc:
245
+ @client = nil
246
+ end
247
+
197
248
  private
198
249
 
199
250
  def check_autostart(uri)
@@ -222,7 +273,8 @@ module NATS
222
273
  end
223
274
 
224
275
  attr_reader :connected, :connect_cb, :err_cb, :err_cb_overridden #:nodoc:
225
- attr_reader :closing, :reconnecting, :options #:nodoc
276
+ attr_reader :closing, :reconnecting, :options, :server_info #:nodoc
277
+ attr_reader :msgs_received, :msgs_sent, :bytes_received, :bytes_sent, :pings
226
278
 
227
279
  alias :connected? :connected
228
280
  alias :closing? :closing
@@ -232,11 +284,15 @@ module NATS
232
284
  @uri = options[:uri]
233
285
  @uri.user = options[:user] if options[:user]
234
286
  @uri.password = options[:pass] if options[:pass]
287
+ @ssl = options[:ssl] if options[:ssl]
235
288
  @options = options
236
289
  @ssid, @subs = 1, {}
237
290
  @err_cb = NATS.err_cb
238
291
  @reconnect_timer, @needed = nil, nil
292
+ @reconnect_cb = NATS.reconnect_cb
239
293
  @connected, @closing, @reconnecting = false, false, false
294
+ @msgs_received = @msgs_sent = @bytes_received = @bytes_sent = @pings = 0
295
+ @pending_size = 0
240
296
  send_connect_command
241
297
  end
242
298
 
@@ -248,6 +304,11 @@ module NATS
248
304
  def publish(subject, msg=EMPTY_MSG, opt_reply=nil, &blk)
249
305
  return unless subject
250
306
  msg = msg.to_s
307
+
308
+ # Accounting
309
+ @msgs_sent += 1
310
+ @bytes_sent += msg.bytesize if msg
311
+
251
312
  send_command("PUB #{subject} #{opt_reply} #{msg.bytesize}#{CR_LF}#{msg}#{CR_LF}")
252
313
  queue_server_rt(&blk) if blk
253
314
  end
@@ -283,6 +344,12 @@ module NATS
283
344
  @subs.delete(sid) unless (sub[:max] && (sub[:received] < sub[:max]))
284
345
  end
285
346
 
347
+ # Return the active subscription count.
348
+ # @return [Number]
349
+ def subscription_count
350
+ @subs.size
351
+ end
352
+
286
353
  # Setup a timeout for receiving messages for the subscription.
287
354
  # @param [Object] sid
288
355
  # @param [Number] timeout, float in seconds
@@ -323,6 +390,13 @@ module NATS
323
390
  return s
324
391
  end
325
392
 
393
+ # Flushes all messages and subscriptions for the connection.
394
+ # All messages and subscriptions have been processed by the server
395
+ # when the optional callback is called.
396
+ def flush(&blk)
397
+ queue_server_rt(&blk) if blk
398
+ end
399
+
326
400
  # Define a callback to be called when the client connection has been established.
327
401
  # @param [Block] callback
328
402
  def on_connect(&callback)
@@ -335,7 +409,7 @@ module NATS
335
409
  @err_cb, @err_cb_overridden = callback, true
336
410
  end
337
411
 
338
- # Define a callback to be called when a reconnect attempt is being made.
412
+ # Define a callback to be called when a reconnect attempt is made.
339
413
  # @param [Block] &blk called when the connection is closed.
340
414
  def on_reconnect(&callback)
341
415
  @reconnect_cb = callback
@@ -344,7 +418,14 @@ module NATS
344
418
  # Close the connection to the server.
345
419
  def close
346
420
  @closing = true
347
- close_connection_after_writing
421
+ EM.cancel_timer(@reconnect_timer) if @reconnect_timer
422
+ close_connection_after_writing if connected?
423
+ process_disconnect if reconnecting?
424
+ end
425
+
426
+ # Return bytes outstanding waiting to be sent to server.
427
+ def pending_data_size
428
+ get_outbound_data_size + @pending_size
348
429
  end
349
430
 
350
431
  def user_err_cb? # :nodoc:
@@ -357,6 +438,7 @@ module NATS
357
438
  cs[:user] = @uri.user
358
439
  cs[:pass] = @uri.password
359
440
  end
441
+ cs[:ssl_required] = @ssl if @ssl
360
442
  send_command("CONNECT #{cs.to_json}#{CR_LF}")
361
443
  end
362
444
 
@@ -367,10 +449,22 @@ module NATS
367
449
  end
368
450
 
369
451
  def on_msg(subject, sid, reply, msg) #:nodoc:
452
+
453
+ # Accounting - We should account for inbound even if they are not processed.
454
+ @msgs_received += 1
455
+ @bytes_received += msg.bytesize if msg
456
+
370
457
  return unless sub = @subs[sid]
371
458
 
372
459
  # Check for auto_unsubscribe
373
460
  sub[:received] += 1
461
+ if sub[:max]
462
+ # Client side support in case server did not receive unsubscribe
463
+ return unsubscribe(sid) if (sub[:received] > sub[:max])
464
+ # cleanup here if we have hit the max..
465
+ @subs.delete(sid) if (sub[:received] == sub[:max])
466
+ end
467
+
374
468
  return unsubscribe(sid) if (sub[:max] && (sub[:received] > sub[:max]))
375
469
 
376
470
  if cb = sub[:callback]
@@ -391,70 +485,86 @@ module NATS
391
485
 
392
486
  def flush_pending #:nodoc:
393
487
  return unless @pending
394
- @pending.each { |p| send_data(p) }
395
- @pending = nil
488
+ send_data(@pending.join)
489
+ @pending, @pending_size = nil, 0
396
490
  end
397
491
 
398
492
  def receive_data(data) #:nodoc:
399
493
  @buf = @buf ? @buf << data : data
400
494
  while (@buf)
401
- case @parse_state
402
-
403
- when AWAITING_CONTROL_LINE
404
- case @buf
405
- when MSG
406
- @buf = $'
407
- @sub, @sid, @reply, @needed = $1, $2.to_i, $4, $5.to_i
408
- @parse_state = AWAITING_MSG_PAYLOAD
409
- when OK # No-op right now
410
- @buf = $'
411
- when ERR
412
- @buf = $'
413
- err_cb.call(NATS::ServerError.new($1))
414
- when PING
415
- @buf = $'
416
- send_command(PONG_RESPONSE)
417
- when PONG
418
- @buf = $'
419
- cb = @pongs.shift
420
- cb.call if cb
421
- when INFO
422
- @buf = $'
423
- process_info($1)
424
- when UNKNOWN
425
- @buf = $'
426
- err_cb.call(NATS::Error.new("Unknown protocol: $1"))
427
- else
428
- # If we are here we do not have a complete line yet that we understand.
429
- return
430
- end
431
- @buf = nil if (@buf && @buf.empty?)
432
-
433
- when AWAITING_MSG_PAYLOAD
434
- return unless (@needed && @buf.bytesize >= (@needed + CR_LF_SIZE))
435
- on_msg(@sub, @sid, @reply, @buf.slice(0, @needed))
436
- @buf = @buf.slice((@needed + CR_LF_SIZE), @buf.bytesize)
437
- @sub = @sid = @reply = @needed = nil
438
- @parse_state = AWAITING_CONTROL_LINE
439
- @buf = nil if (@buf && @buf.empty?)
495
+ case @parse_state
496
+ when AWAITING_CONTROL_LINE
497
+ case @buf
498
+ when MSG
499
+ @buf = $'
500
+ @sub, @sid, @reply, @needed = $1, $2.to_i, $4, $5.to_i
501
+ @parse_state = AWAITING_MSG_PAYLOAD
502
+ when OK # No-op right now
503
+ @buf = $'
504
+ when ERR
505
+ @buf = $'
506
+ err_cb.call(NATS::ServerError.new($1))
507
+ when PING
508
+ @pings += 1
509
+ @buf = $'
510
+ send_command(PONG_RESPONSE)
511
+ when PONG
512
+ @buf = $'
513
+ cb = @pongs.shift
514
+ cb.call if cb
515
+ when INFO
516
+ @buf = $'
517
+ process_info($1)
518
+ when UNKNOWN
519
+ @buf = $'
520
+ err_cb.call(NATS::ServerError.new("Unknown protocol: $1"))
521
+ else
522
+ # If we are here we do not have a complete line yet that we understand.
523
+ return
440
524
  end
525
+ @buf = nil if (@buf && @buf.empty?)
526
+
527
+ when AWAITING_MSG_PAYLOAD
528
+ return unless (@needed && @buf.bytesize >= (@needed + CR_LF_SIZE))
529
+ on_msg(@sub, @sid, @reply, @buf.slice(0, @needed))
530
+ @buf = @buf.slice((@needed + CR_LF_SIZE), @buf.bytesize)
531
+ @sub = @sid = @reply = @needed = nil
532
+ @parse_state = AWAITING_CONTROL_LINE
533
+ @buf = nil if (@buf && @buf.empty?)
534
+ end
535
+
441
536
  end
442
537
  end
443
538
 
444
539
  def process_info(info) #:nodoc:
445
540
  @server_info = JSON.parse(info, :symbolize_keys => true, :symbolize_names => true)
541
+ if @server_info[:ssl_required] && @ssl
542
+ start_tls
543
+ else
544
+ if @server_info[:ssl_required]
545
+ err_cb.call(NATS::ClientError.new('TLS/SSL required by server'))
546
+ elsif @ssl
547
+ err_cb.call(NATS::ClientError.new('TLS/SSL not supported by server'))
548
+ end
549
+ end
550
+ @server_info
446
551
  end
447
552
 
448
- def connection_completed #:nodoc:
553
+ def ssl_handshake_completed
449
554
  @connected = true
555
+ flush_pending
556
+ end
557
+
558
+ def connection_completed #:nodoc:
559
+ @connected = true unless @ssl
450
560
  if reconnecting?
451
561
  EM.cancel_timer(@reconnect_timer)
452
562
  send_connect_command
453
563
  @subs.each_pair { |k, v| send_command("SUB #{v[:subject]} #{k}#{CR_LF}") }
454
564
  end
455
- flush_pending if @pending
565
+ flush_pending unless @ssl
456
566
  unless user_err_cb? or reconnecting?
457
- @err_cb = proc {|e| raise e }
567
+ @err_cb = proc { |e| raise e }
458
568
  end
459
569
  if (connect_cb and not reconnecting?)
460
570
  # We will round trip the server here to make sure all state from any pending commands
@@ -468,43 +578,49 @@ module NATS
468
578
  def schedule_reconnect(wait=RECONNECT_TIME_WAIT) #:nodoc:
469
579
  @reconnecting = true
470
580
  @reconnect_attempts = 0
581
+ @connected = false
471
582
  @reconnect_timer = EM.add_periodic_timer(wait) { attempt_reconnect }
472
583
  end
473
584
 
474
585
  def unbind #:nodoc:
475
586
  if connected? and not closing? and not reconnecting? and @options[:reconnect]
476
- schedule_reconnect
587
+ schedule_reconnect(@options[:reconnect_time_wait])
477
588
  else
478
589
  process_disconnect unless reconnecting?
479
590
  end
480
591
  end
481
592
 
593
+ def disconnect_error_string
594
+ return "Client disconnected from server on #{@uri}." if @connected
595
+ return "Could not connect to server on #{@uri}"
596
+ end
597
+
482
598
  def process_disconnect #:nodoc:
483
- if not closing? and @err_cb
484
- err_string = @connected ? "Client disconnected from server on #{@uri}." : "Could not connect to server on #{@uri}"
485
- err_cb.call(NATS::ConnectError.new(err_string))
486
- end
599
+ err_cb.call(NATS::ConnectError.new(disconnect_error_string)) if not closing? and @err_cb
487
600
  ensure
488
601
  EM.cancel_timer(@reconnect_timer) if @reconnect_timer
489
- if (NATS.client == self and connected? and closing? and not NATS.reactor_was_running?)
490
- EM.stop
602
+ if (NATS.client == self)
603
+ NATS.clear_client
604
+ EM.stop if ((connected? || reconnecting?) and closing? and not NATS.reactor_was_running?)
491
605
  end
492
606
  @connected = @reconnecting = false
493
607
  true # Chaining
494
608
  end
495
609
 
496
610
  def attempt_reconnect #:nodoc:
497
- process_disconnect and return if (@reconnect_attempts += 1) > MAX_RECONNECT_ATTEMPTS
611
+ process_disconnect and return if (@reconnect_attempts += 1) > @options[:max_reconnect_attempts]
498
612
  EM.reconnect(@uri.host, @uri.port, self)
613
+ @reconnect_cb.call unless @reconnect_cb.nil?
499
614
  end
500
615
 
501
616
  def send_command(command) #:nodoc:
502
- queue_command(command) and return unless connected?
503
- send_data(command)
504
- end
505
-
506
- def queue_command(command) #:nodoc:
617
+ EM.next_tick { flush_pending } if (connected? && @pending.nil?)
507
618
  (@pending ||= []) << command
619
+ @pending_size += command.bytesize
620
+ flush_pending if (connected? && @pending_size > MAX_PENDING_SIZE)
621
+ if (@options[:fast_producer_error] && pending_data_size > FAST_PRODUCER_THRESHOLD)
622
+ err_cb.call(NATS::ClientError.new("Fast Producer: #{pending_data_size} bytes outstanding"))
623
+ end
508
624
  true
509
625
  end
510
626