nats 0.3.12 → 0.4.2

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.
data/COPYING CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2010 Derek Collison <derek.collison@gmail.com>. All rights reserved.
1
+ Copyright (c) 2010, 2011 Derek Collison <derek.collison@gmail.com>. All rights reserved.
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining a copy
4
4
  of this software and associated documentation files (the "Software"), to
data/ChangeLog ADDED
@@ -0,0 +1,11 @@
1
+ 2011-02-21, version 0.4.2
2
+ * queue group support
3
+ * auto-unsubscribe support
4
+ * time expiration on subscriptions
5
+ * jruby initial support (1.5.6, 1.6.0-RC1)
6
+ * performance enhancements
7
+ * complete config file support
8
+
9
+ 2010-11-20, version 0.3.12
10
+ * original upload to RubyGems
11
+
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # NATS
2
2
 
3
- EventMachine based Publish-Subscribe Messaging that just works.
3
+ A lightweight EventMachine based publish-subscribe messaging system.
4
4
 
5
5
  ## Supported Platforms
6
6
 
@@ -8,15 +8,12 @@ This gem currently works on the following Ruby platforms:
8
8
 
9
9
  - MRI 1.8 and 1.9 (Performance is best on 1.9.2)
10
10
  - Rubinius
11
- - JRuby (not quite yet)
11
+ - JRuby
12
12
 
13
13
  ## Getting Started
14
14
 
15
15
  [sudo] gem install nats
16
-
17
- or
18
-
19
- git clone
16
+ == or ==
20
17
  [sudo] rake geminstall
21
18
 
22
19
  nats-sub foo &
@@ -34,46 +31,72 @@ This gem currently works on the following Ruby platforms:
34
31
  # Simple Publisher
35
32
  NATS.publish('foo.bar.baz', 'Hello World!')
36
33
 
37
- # Publish with closure, callback fires when server has processed the message
38
- NATS.publish('foo', 'You done?') { puts 'msg processed!' }
39
-
40
34
  # Unsubscribing
41
- s = NATS.subscribe('bar') { |msg| puts "Msg received : '#{msg}'" }
42
- NATS.unsubscribe(s)
35
+ sid = NATS.subscribe('bar') { |msg| puts "Msg received : '#{msg}'" }
36
+ NATS.unsubscribe(sid)
43
37
 
44
- # Request/Response
38
+ # Requests
39
+ NATS.request('help') { |response| puts "Got a response: '#{response}'" }
45
40
 
46
- # The helper
47
- NATS.subscribe('help') do |msg, reply|
48
- NATS.publish(reply, "I'll help!")
49
- end
41
+ # Replies
42
+ NATS.subscribe('help') { |msg, reply| NATS.publish(reply, "I'll help!") }
50
43
 
51
- # Help request
52
- NATS.request('help') { |response|
53
- puts "Got a response: '#{response}'"
54
- }
44
+ # Stop using NATS.stop, exits EM loop if NATS.start started the loop
45
+ NATS.stop
55
46
 
56
- # Wildcard Subscriptions
47
+ end
57
48
 
58
- # '*" matches any token
59
- NATS.subscribe('foo.*.baz') { |msg, _, sub| puts "Msg received on [#{sub}] : '#{msg}'" }
49
+ ## Wildcard Subscriptions
60
50
 
61
- # '>" can only be last token, and matches to any depth
62
- NATS.subscribe('foo.>') { |msg, _, sub| puts "Msg received on [#{sub}] : '#{msg}'" }
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}'" }
63
55
 
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}'" }
64
59
 
65
- # Stop using NATS.stop, exits EM loop if NATS.start started it
66
- NATS.stop
60
+ ## Queues Groups
61
+
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}'" }
67
+
68
+ ## Advanced Usage
69
+
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 }
79
+
80
+ # Auto-unsubscribe after MAX_WANTED messages received
81
+ NATS.unsubscribe(sid, MAX_WANTED)
82
+
83
+ # Multiple connections
84
+ NATS.subscribe('test') do |msg|
85
+ puts "received msg"
86
+ NATS.stop
87
+ end
88
+
89
+ # Form second connection to send message on
90
+ NATS.connect { NATS.publish('test', 'Hello World!') }
67
91
 
68
- end
69
92
 
70
- See examples and benchmark for more..
93
+ See examples and benchmark for more information..
71
94
 
72
95
  ## License
73
96
 
74
97
  (The MIT License)
75
98
 
76
- Copyright (c) 2010 Derek Collison
99
+ Copyright (c) 2010, 2011 Derek Collison
77
100
 
78
101
  Permission is hereby granted, free of charge, to any person obtaining a copy
79
102
  of this software and associated documentation files (the "Software"), to
data/Rakefile CHANGED
@@ -1,8 +1,8 @@
1
1
 
2
- desc "Run rspec"
2
+ desc "Run rspec"
3
3
  task :spec do
4
+ sh('bundle install')
4
5
  require "rspec/core/rake_task"
5
-
6
6
  RSpec::Core::RakeTask.new do |t|
7
7
  t.rspec_opts = %w(-fs -c)
8
8
  end
data/bin/nats-queue ADDED
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'nats/client'
5
+
6
+ trap("TERM") { NATS.stop }
7
+ trap("INT") { NATS.stop }
8
+
9
+ def usage
10
+ puts "Usage: nats-queue <subject> <queue name>"; exit
11
+ end
12
+
13
+ subject, queue_group = ARGV
14
+ usage unless subject and queue_group
15
+
16
+ NATS.on_error { |err| puts "Server Error: #{err}"; exit! }
17
+
18
+ NATS.start do
19
+ puts "Listening on [#{subject}], queue group [#{queue_group}]"
20
+ NATS.subscribe(subject, queue_group) { |msg| puts "Received '#{msg}'" }
21
+ end
data/lib/nats/client.rb CHANGED
@@ -1,84 +1,33 @@
1
-
2
1
  require 'uri'
3
2
 
4
- require File.dirname(__FILE__) + '/ext/em'
5
- require File.dirname(__FILE__) + '/ext/bytesize'
6
- require File.dirname(__FILE__) + '/ext/json'
7
-
8
- # NATS is a simple publish-subscribe messaging system.
9
- #
10
- # == Usage
11
- # <tt>
12
- # require "nats/client"
13
- #
14
- # NATS.start do
15
- #
16
- # # Simple Subscriber
17
- # NATS.subscribe('foo') { |msg| puts "Msg received : '#{msg}'" }
18
- #
19
- # # Simple Publisher
20
- # NATS.publish('foo.bar.baz', 'Hello World!')
21
- #
22
- # # Publish with closure, callback fires when server has processed the message
23
- # NATS.publish('foo', 'You done?') { puts 'msg processed!' }
24
- #
25
- # # Unsubscribing
26
- # s = NATS.subscribe('bar') { |msg| puts "Msg received : '#{msg}'" }
27
- # NATS.unsubscribe(s)
28
- #
29
- # # Request/Response
30
- #
31
- # # The helper
32
- # NATS.subscribe('help') do |msg, reply|
33
- # NATS.publish(reply, "I'll help!")
34
- # end
35
- #
36
- # # Help request
37
- # NATS.request('help') { |response|
38
- # puts "Got a response: '#{response}'"
39
- # }
40
- #
41
- # # Wildcard Subscriptions
42
- #
43
- # # '*" matches any token
44
- # NATS.subscribe('foo.*.baz') { |msg, _, sub| puts "Msg received on [#{sub}] : '#{msg}'" }
45
- #
46
- # # '>" can only be last token, and matches to any depth
47
- # NATS.subscribe('foo.>') { |msg, _, sub| puts "Msg received on [#{sub}] : '#{msg}'" }
48
- #
49
- #
50
- # # Stop using NATS.stop, exits EM loop if NATS.start started it
51
- # NATS.stop
52
- #
53
- # end
54
- #
55
- # </tt>
3
+ ep = File.expand_path(File.dirname(__FILE__))
56
4
 
5
+ require "#{ep}/ext/em"
6
+ require "#{ep}/ext/bytesize"
7
+ require "#{ep}/ext/json"
57
8
 
58
9
  module NATS
59
10
 
60
- # Version <b>0.3.12</b>
61
- VERSION = "0.3.12".freeze
11
+ VERSION = "0.4.2".freeze
62
12
 
63
- # Default port: <b>4222</b>
64
13
  DEFAULT_PORT = 4222
65
-
66
- # Default URI to connect to the server, <b>nats://localhost:4222</b>
67
14
  DEFAULT_URI = "nats://localhost:#{DEFAULT_PORT}".freeze
68
15
 
69
- # Max attempts at a reconnect: <b>10</b>
70
16
  MAX_RECONNECT_ATTEMPTS = 10
71
-
72
- # Maximum time to wait for a reconnect: <b>2 seconds</b>
73
17
  RECONNECT_TIME_WAIT = 2
74
18
 
19
+ AUTOSTART_PID_FILE = '/tmp/nats-server.pid'
20
+ AUTOSTART_LOG_FILE = '/tmp/nats-server.log'
21
+
75
22
  # Protocol
76
- MSG = /^MSG\s+(\S+)\s+(\S+)\s+((\S+)\s+)?(\d+)$/i #:nodoc:
77
- OK = /^\+OK/i #:nodoc:
78
- ERR = /^-ERR\s+('.+')?/i #:nodoc:
79
- PING = /^PING/i #:nodoc:
80
- PONG = /^PONG/i #:nodoc:
81
- INFO = /^INFO\s+(.+)/i #:nodoc:
23
+ # @private
24
+ MSG = /\AMSG\s+([^\s\r\n]+)\s+([^\s\r\n]+)\s+(([^\s\r\n]+)[^\S\r\n]+)?(\d+)\r\n/i #:nodoc:
25
+ OK = /\A\+OK\s*\r\n/i #:nodoc:
26
+ ERR = /\A-ERR\s+('.+')?\r\n/i #:nodoc:
27
+ PING = /\APING\r\n/i #:nodoc:
28
+ PONG = /\APONG\r\n/i #:nodoc:
29
+ INFO = /\AINFO\s+([^\r\n]+)\r\n/i #:nodoc:
30
+ UNKNOWN = /\A(.*)\r\n/ #:nodoc:
82
31
 
83
32
  # Responses
84
33
  CR_LF = ("\r\n".freeze) #:nodoc:
@@ -93,6 +42,10 @@ module NATS
93
42
  SUB = /^([^\.\*>\s]+|>$|\*)(\.([^\.\*>\s]+|>$|\*))*$/ #:nodoc:
94
43
  SUB_NO_WC = /^([^\.\*>\s]+)(\.([^\.\*>\s]+))*$/ #:nodoc:
95
44
 
45
+ # Parser
46
+ AWAITING_CONTROL_LINE = 1 #:nodoc:
47
+ AWAITING_MSG_PAYLOAD = 2 #:nodoc:
48
+
96
49
  # Duplicate autostart protection
97
50
  @@tried_autostart = {}
98
51
 
@@ -100,72 +53,142 @@ module NATS
100
53
  end
101
54
 
102
55
  class << self
103
- attr_reader :client, :reactor_was_running, :err_cb, :err_cb_overridden #:nodoc:
56
+ attr_reader :client, :reactor_was_running, :err_cb, :err_cb_overridden #:nodoc:
57
+ attr_accessor :timeout_cb #:nodoc
58
+
104
59
  alias :reactor_was_running? :reactor_was_running
105
60
 
106
- # Create and return a connection to the server with the given options. The server will be autostarted if needed if
107
- # the <b>uri</b> is determined to be local. The optional block will be called when the connection has been completed.
61
+ # Create and return a connection to the server with the given options.
62
+ # The server will be autostarted if needed if the <b>uri</b> is determined to be local.
63
+ # The optional block will be called when the connection has been completed.
108
64
  #
109
- def connect(options = {}, &blk)
110
- options[:uri] ||= ENV['NATS_URI'] || DEFAULT_URI
111
- options[:debug] ||= ENV['NATS_DEBUG']
112
- options[:autostart] = (ENV['NATS_AUTO'] || true) unless options[:autostart] != nil
113
- uri = options[:uri] = URI.parse(options[:uri])
114
- @err_cb = proc { raise Error, "Could not connect to server on #{uri}."} unless err_cb
115
- check_autostart(uri) if options[:autostart]
116
- client = EM.connect(uri.host, uri.port, self, options)
65
+ # @param [Hash] opts
66
+ # @option opts [String] :uri The URI to connect to, example nats://localhost:4222
67
+ # @option opts [Boolean] :autostart Boolean that can be used to suppress autostart functionality.
68
+ # @option opts [Boolean] :reconnect Boolean that can be used to suppress reconnect functionality.
69
+ # @option opts [Boolean] :debug Boolean that can be used to output additional debug information.
70
+ # @option opts [Boolean] :verbose Boolean that is sent to server for setting verbose protocol mode.
71
+ # @option opts [Boolean] :pedantic Boolean that is sent to server for setting pedantic mode.
72
+ # @param [Block] &blk called when the connection is completed. Connection will be passed to the block.
73
+ # @return [NATS] connection to the server.
74
+ def connect(opts={}, &blk)
75
+ # Defaults
76
+ opts[:verbose] = false if opts[:verbose].nil?
77
+ opts[:pedantic] = false if opts[:pedantic].nil?
78
+ opts[:reconnect] = true if opts[:reconnect].nil?
79
+
80
+ # Override with ENV
81
+ opts[:uri] ||= ENV['NATS_URI'] || DEFAULT_URI
82
+ opts[:verbose] = ENV['NATS_VERBOSE'] unless ENV['NATS_VERBOSE'].nil?
83
+ opts[:pedantic] = ENV['NATS_PEDANTIC'] unless ENV['NATS_PEDANTIC'].nil?
84
+ opts[:debug] = ENV['NATS_DEBUG'] if !ENV['NATS_DEBUG'].nil?
85
+ opts[:autostart] = (ENV['NATS_AUTO'] || true) if opts[:autostart].nil?
86
+
87
+ @uri = opts[:uri] = URI.parse(opts[:uri])
88
+ @err_cb = proc { raise Error, "Could not connect to server on #{@uri}."} unless err_cb
89
+ check_autostart(@uri) if opts[:autostart]
90
+ client = EM.connect(@uri.host, @uri.port, self, opts)
117
91
  client.on_connect(&blk) if blk
118
92
  return client
119
93
  end
120
94
 
121
- # Create a default client connection to the server. See connect for more information.
95
+ # Create a default client connection to the server.
96
+ # @see NATS::connect
122
97
  def start(*args, &blk)
123
98
  @reactor_was_running = EM.reactor_running?
124
99
  unless (@reactor_was_running || blk)
125
100
  raise(Error, "EM needs to be running when NATS.start called without a run block")
126
101
  end
102
+ # Setup optimized select versions
103
+ EM.epoll; EM.kqueue
127
104
  EM.run { @client = connect(*args, &blk) }
128
105
  end
129
106
 
130
107
  # Close the default client connection and optionally call the associated block.
108
+ # @param [Block] &blk called when the connection is closed.
131
109
  def stop(&blk)
132
110
  client.close if (client and client.connected?)
133
111
  blk.call if blk
112
+ @@tried_autostart = {}
113
+ @err_cb = nil
114
+ end
115
+
116
+ # @return [Boolean] Connected state
117
+ def connected?
118
+ return false unless client
119
+ client.connected?
120
+ end
121
+
122
+ # @return [Hash] Options
123
+ def options
124
+ return {} unless client
125
+ client.options
134
126
  end
135
127
 
136
128
  # Set the default on_error callback.
129
+ # @param [Block] &callback called when an error has been detected.
137
130
  def on_error(&callback)
138
131
  @err_cb, @err_cb_overridden = callback, true
139
132
  end
140
133
 
141
- # Publish a message using the default client connection. See NATS#publish for more information.
134
+ # Publish a message using the default client connection.
135
+ # @see NATS#publish
142
136
  def publish(*args, &blk)
143
137
  (@client ||= connect).publish(*args, &blk)
144
138
  end
145
139
 
146
- # Subscribe using the default client connection. See NATS#subscribe for more information.
140
+ # Subscribe using the default client connection.
141
+ # @see NATS#subscribe
147
142
  def subscribe(*args, &blk)
148
143
  (@client ||= connect).subscribe(*args, &blk)
149
144
  end
150
145
 
151
146
  # Cancel a subscription on the default client connection.
147
+ # @see NATS#unsubscribe
152
148
  def unsubscribe(*args)
153
149
  (@client ||= connect).unsubscribe(*args)
154
150
  end
155
151
 
156
- # Publish a message and wait for a response on the default client connection. See NATS#request for more information.
152
+ # Set a timeout for receiving messages for the subscription.
153
+ # @see NATS#timeout
154
+ def timeout(*args, &blk)
155
+ (@client ||= connect).timeout(*args, &blk)
156
+ end
157
+
158
+ # Publish a message and wait for a response on the default client connection.
159
+ # @see NATS#request
157
160
  def request(*args, &blk)
158
161
  (@client ||= connect).request(*args, &blk)
159
162
  end
160
163
 
161
- # Returns a subject that can be used for "directed" communications, utilized in #request.
164
+ # Returns a subject that can be used for "directed" communications.
165
+ # @return [String]
162
166
  def create_inbox
163
167
  v = [rand(0x0010000),rand(0x0010000),rand(0x0010000),
164
168
  rand(0x0010000),rand(0x0010000),rand(0x1000000)]
165
169
  "_INBOX.%04x%04x%04x%04x%04x%06x" % v
166
170
  end
167
171
 
168
- def check_autostart(uri) #:nodoc:
172
+ def wait_for_server(uri, max_wait = 5) # :nodoc:
173
+ start = Time.now
174
+ while (Time.now - start < max_wait) # Wait max_wait seconds max
175
+ break if server_running?(uri)
176
+ sleep(0.1)
177
+ end
178
+ end
179
+
180
+ def server_running?(uri) # :nodoc:
181
+ require 'socket'
182
+ s = TCPSocket.new(uri.host, uri.port)
183
+ s.close
184
+ return true
185
+ rescue
186
+ return false
187
+ end
188
+
189
+ private
190
+
191
+ def check_autostart(uri)
169
192
  return if uri_is_remote?(uri) || @@tried_autostart[uri]
170
193
  @@tried_autostart[uri] = true
171
194
  return if server_running?(uri)
@@ -173,41 +196,25 @@ module NATS
173
196
  wait_for_server(uri)
174
197
  end
175
198
 
176
- def uri_is_remote?(uri) #:nodoc:
199
+ def uri_is_remote?(uri)
177
200
  uri.host != 'localhost' && uri.host != '127.0.0.1'
178
201
  end
179
202
 
180
- def try_autostart_succeeded?(uri) #:nodoc:
203
+ def try_autostart_succeeded?(uri)
181
204
  port_arg = "-p #{uri.port}"
182
205
  user_arg = "--user #{uri.user}" if uri.user
183
206
  pass_arg = "--pass #{uri.password}" if uri.password
184
- log_arg = '-l /tmp/nats-server.log'
185
- pid_arg = '-P /tmp/nats-server.pid'
207
+ log_arg = "-l #{AUTOSTART_LOG_FILE}"
208
+ pid_arg = "-P #{AUTOSTART_PID_FILE}"
186
209
  # daemon mode to release client
187
210
  system("nats-server #{port_arg} #{user_arg} #{pass_arg} #{log_arg} #{pid_arg} -d 2> /dev/null")
188
211
  $? == 0
189
212
  end
190
213
 
191
- def wait_for_server(uri) #:nodoc:
192
- start = Time.now
193
- while (Time.now - start < 5) # Wait 5 seconds max
194
- break if server_running?(uri)
195
- sleep(0.1)
196
- end
197
- end
198
-
199
- def server_running?(uri) #:nodoc:
200
- require 'socket'
201
- s = TCPSocket.new(uri.host, uri.port)
202
- s.close
203
- return true
204
- rescue
205
- return false
206
- end
207
-
208
214
  end
209
215
 
210
- attr_reader :connect_cb, :err_cb, :err_cb_overridden, :connected, :closing, :reconnecting #:nodoc:
216
+ attr_reader :connected, :connect_cb, :err_cb, :err_cb_overridden #:nodoc:
217
+ attr_reader :closing, :reconnecting, :options #:nodoc
211
218
 
212
219
  alias :connected? :connected
213
220
  alias :closing? :closing
@@ -215,7 +222,9 @@ module NATS
215
222
 
216
223
  def initialize(options)
217
224
  @uri = options[:uri]
218
- @debug = options[:debug]
225
+ @uri.user = options[:user] if options[:user]
226
+ @uri.password = options[:pass] if options[:pass]
227
+ @options = options
219
228
  @ssid, @subs = 1, {}
220
229
  @err_cb = NATS.err_cb
221
230
  @reconnect_timer, @needed = nil, nil
@@ -224,36 +233,78 @@ module NATS
224
233
  end
225
234
 
226
235
  # Publish a message to a given subject, with optional reply subject and completion block
227
- def publish(subject, data=EMPTY_MSG, opt_reply=nil, &blk)
236
+ # @param [String] subject
237
+ # @param [Object, #to_s] msg
238
+ # @param [String] opt_reply
239
+ # @param [Block] blk, closure called when publish has been processed by the server.
240
+ def publish(subject, msg=EMPTY_MSG, opt_reply=nil, &blk)
228
241
  return unless subject
229
- data = data.to_s
230
- send_command("PUB #{subject} #{opt_reply} #{data.bytesize}#{CR_LF}#{data}#{CR_LF}")
242
+ msg = msg.to_s
243
+ send_command("PUB #{subject} #{opt_reply} #{msg.bytesize}#{CR_LF}#{msg}#{CR_LF}")
231
244
  queue_server_rt(&blk) if blk
232
245
  end
233
246
 
234
- # Subscribe to a subject with optional wildcards. Messages will be delivered to the supplied callback.
247
+ # Subscribe to a subject with optional wildcards.
248
+ # Messages will be delivered to the supplied callback.
235
249
  # Callback can take any number of the supplied arguments as defined by the list: msg, reply, sub.
236
- # Returns subscription id which can be passed to NATS#unsubscribe.
237
- def subscribe(subject, &callback)
250
+ # Returns subscription id which can be passed to #unsubscribe.
251
+ # @param [String] subject, optionally with wilcards.
252
+ # @param [Hash] opts, optional options hash, e.g. :queue, :max.
253
+ # @param [Block] callback, called when a message is delivered.
254
+ # @return [Object] sid, Subject Identifier
255
+ def subscribe(subject, opts={}, &callback)
238
256
  return unless subject
239
- @ssid += 1
240
- @subs[@ssid] = { :subject => subject, :callback => callback }
241
- send_command("SUB #{subject} #{@ssid}#{CR_LF}")
242
- @ssid
257
+ sid = (@ssid += 1)
258
+ sub = @subs[sid] = { :subject => subject, :callback => callback, :received => 0 }
259
+ sub[:queue] = opts[:queue] if opts[:queue]
260
+ sub[:max] = opts[:max] if opts[:max]
261
+ send_command("SUB #{subject} #{opts[:queue]} #{sid}#{CR_LF}")
262
+ # Setup server support for auto-unsubscribe
263
+ unsubscribe(sid, opts[:max]) if opts[:max]
264
+ sid
243
265
  end
244
266
 
245
267
  # Cancel a subscription.
246
- def unsubscribe(sid)
247
- @subs.delete(sid)
248
- send_command("UNSUB #{sid}#{CR_LF}")
268
+ # @param [Object] sid
269
+ # @param [Number] opt_max, optional number of responses to receive before auto-unsubscribing
270
+ def unsubscribe(sid, opt_max=nil)
271
+ opt_max_str = " #{opt_max}" unless opt_max.nil?
272
+ send_command("UNSUB #{sid}#{opt_max_str}#{CR_LF}")
273
+ return unless sub = @subs[sid]
274
+ sub[:max] = opt_max
275
+ @subs.delete(sid) unless (sub[:max] && (sub[:received] < sub[:max]))
276
+ end
277
+
278
+ # Setup a timeout for receiving messages for the subscription.
279
+ # @param [Object] sid
280
+ # @param [Number] timeout, float in seconds
281
+ # @param [Hash] opts, options, :auto_unsubscribe(true), :expected(1)
282
+ def timeout(sid, timeout, opts={}, &callback)
283
+ # Setup a timeout if requested
284
+ return unless sub = @subs[sid]
285
+
286
+ auto_unsubscribe, expected = true, 1
287
+ auto_unsubscribe = opts[:auto_unsubscribe] if opts.key?(:auto_unsubscribe)
288
+ expected = opts[:expected] if opts.key?(:expected)
289
+
290
+ EM.cancel_timer(sub[:timeout]) if sub[:timeout]
291
+
292
+ sub[:timeout] = EM.add_timer(timeout) do
293
+ unsubscribe(sid) if auto_unsubscribe
294
+ callback.call(sid) if callback
295
+ end
296
+ sub[:expected] = expected
249
297
  end
250
298
 
251
299
  # Send a request and have the response delivered to the supplied callback.
252
- # Returns subscription id which can be passed to NATS#unsubscribe.
253
- def request(subject, data=nil, &cb)
300
+ # @param [String] subject
301
+ # @param [Object] msg
302
+ # @param [Block] callback
303
+ # @return [Object] sid
304
+ def request(subject, data=nil, opts={}, &cb)
254
305
  return unless subject
255
306
  inbox = NATS.create_inbox
256
- s = subscribe(inbox) { |msg, reply|
307
+ s = subscribe(inbox, opts) { |msg, reply|
257
308
  case cb.arity
258
309
  when 0 then cb.call
259
310
  when 1 then cb.call(msg)
@@ -265,16 +316,19 @@ module NATS
265
316
  end
266
317
 
267
318
  # Define a callback to be called when the client connection has been established.
319
+ # @param [Block] callback
268
320
  def on_connect(&callback)
269
321
  @connect_cb = callback
270
322
  end
271
323
 
272
324
  # Define a callback to be called when errors occur on the client connection.
325
+ # @param [Block] &blk called when the connection is closed.
273
326
  def on_error(&callback)
274
327
  @err_cb, @err_cb_overridden = callback, true
275
328
  end
276
329
 
277
330
  # Define a callback to be called when a reconnect attempt is being made.
331
+ # @param [Block] &blk called when the connection is closed.
278
332
  def on_reconnect(&callback)
279
333
  @reconnect_cb = callback
280
334
  end
@@ -285,12 +339,12 @@ module NATS
285
339
  close_connection_after_writing
286
340
  end
287
341
 
288
- def user_err_cb? #:nodoc:
342
+ def user_err_cb? # :nodoc:
289
343
  err_cb_overridden || NATS.err_cb_overridden
290
344
  end
291
345
 
292
346
  def send_connect_command #:nodoc:
293
- cs = { :verbose => false, :pedantic => false }
347
+ cs = { :verbose => @options[:verbose], :pedantic => @options[:pedantic] }
294
348
  if @uri.user
295
349
  cs[:user] = @uri.user
296
350
  cs[:pass] = @uri.password
@@ -305,8 +359,13 @@ module NATS
305
359
  end
306
360
 
307
361
  def on_msg(subject, sid, reply, msg) #:nodoc:
308
- return unless subscriber = @subs[sid]
309
- if cb = subscriber[:callback]
362
+ return unless sub = @subs[sid]
363
+
364
+ # Check for auto_unsubscribe
365
+ sub[:received] += 1
366
+ return unsubscribe(sid) if (sub[:max] && (sub[:received] > sub[:max]))
367
+
368
+ if cb = sub[:callback]
310
369
  case cb.arity
311
370
  when 0 then cb.call
312
371
  when 1 then cb.call(msg)
@@ -314,6 +373,12 @@ module NATS
314
373
  else cb.call(msg, reply, subject)
315
374
  end
316
375
  end
376
+
377
+ # Check for a timeout, and cancel if received >= expected
378
+ if (sub[:timeout] && sub[:received] >= sub[:expected])
379
+ EM.cancel_timer(sub[:timeout])
380
+ sub[:timeout] = nil
381
+ end
317
382
  end
318
383
 
319
384
  def flush_pending #:nodoc:
@@ -323,39 +388,55 @@ module NATS
323
388
  end
324
389
 
325
390
  def receive_data(data) #:nodoc:
326
- (@buf ||= '') << data
327
- while (@buf && !@buf.empty?)
328
- if (@needed && @buf.bytesize >= @needed + CR_LF_SIZE)
329
- payload = @buf.slice(0, @needed)
330
- on_msg(@sub, @sid, @reply, payload)
331
- @buf = @buf.slice((@needed + CR_LF_SIZE), @buf.bytesize)
332
- @sub = @sid = @reply = @needed = nil
333
- elsif @buf =~ /^(.*)\r\n/ # Process a control line
334
- @buf = $'
335
- op = $1
336
- case op
337
- when MSG
338
- @sub, @sid, @reply, @needed = $1, $2.to_i, $4, $5.to_i
339
- when OK # No-op right now
340
- when ERR
341
- @err_cb = proc { raise Error, "Error received from server :#{$1}."} unless user_err_cb?
342
- err_cb.call($1)
343
- when PING
344
- send_command(PONG_RESPONSE)
345
- when PONG
346
- cb = @pongs.shift
347
- cb.call if cb
348
- when INFO
349
- process_info($1)
391
+ @buf = @buf ? @buf << data : data
392
+ while (@buf)
393
+ case @parse_state
394
+
395
+ when AWAITING_CONTROL_LINE
396
+ case @buf
397
+ when MSG
398
+ @buf = $'
399
+ @sub, @sid, @reply, @needed = $1, $2.to_i, $4, $5.to_i
400
+ @parse_state = AWAITING_MSG_PAYLOAD
401
+ when OK # No-op right now
402
+ @buf = $'
403
+ when ERR
404
+ @buf = $'
405
+ @err_cb = proc { raise Error, "Error received from server :#{$1}."} unless user_err_cb?
406
+ err_cb.call($1)
407
+ when PING
408
+ @buf = $'
409
+ send_command(PONG_RESPONSE)
410
+ when PONG
411
+ @buf = $'
412
+ cb = @pongs.shift
413
+ cb.call if cb
414
+ when INFO
415
+ @buf = $'
416
+ process_info($1)
417
+ when UNKNOWN
418
+ @buf = $'
419
+ @err_cb = proc { raise Error, "Error: Ukknown Protocol."} unless user_err_cb?
420
+ err_cb.call($1)
421
+ else
422
+ # If we are here we do not have a complete line yet that we understand.
423
+ return
424
+ end
425
+ @buf = nil if (@buf && @buf.empty?)
426
+
427
+ when AWAITING_MSG_PAYLOAD
428
+ return unless (@needed && @buf.bytesize >= (@needed + CR_LF_SIZE))
429
+ on_msg(@sub, @sid, @reply, @buf.slice(0, @needed))
430
+ @buf = @buf.slice((@needed + CR_LF_SIZE), @buf.bytesize)
431
+ @sub = @sid = @reply = @needed = nil
432
+ @parse_state = AWAITING_CONTROL_LINE
433
+ @buf = nil if (@buf && @buf.empty?)
350
434
  end
351
- else # Waiting for additional data
352
- return
353
- end
354
435
  end
355
436
  end
356
437
 
357
438
  def process_info(info) #:nodoc:
358
- @server_info = JSON.parse(info, :symbolize_keys => true)
439
+ @server_info = JSON.parse(info, :symbolize_keys => true, :symbolize_names => true)
359
440
  end
360
441
 
361
442
  def connection_completed #:nodoc:
@@ -366,13 +447,16 @@ module NATS
366
447
  @subs.each_pair { |k, v| send_command("SUB #{v[:subject]} #{k}#{CR_LF}") }
367
448
  end
368
449
  flush_pending if @pending
369
- @err_cb = proc { raise Error, "Client disconnected from server on #{@uri}."} unless user_err_cb? or reconnecting?
450
+ unless user_err_cb? or reconnecting?
451
+ @err_cb = proc { raise Error, "Client disconnected from server on #{@uri}."}
452
+ end
370
453
  if (connect_cb and not reconnecting?)
371
454
  # We will round trip the server here to make sure all state from any pending commands
372
455
  # has been processed before calling the connect callback.
373
456
  queue_server_rt { connect_cb.call(self) }
374
457
  end
375
458
  @reconnecting = false
459
+ @parse_state = AWAITING_CONTROL_LINE
376
460
  end
377
461
 
378
462
  def schedule_reconnect(wait=RECONNECT_TIME_WAIT) #:nodoc:
@@ -382,7 +466,7 @@ module NATS
382
466
  end
383
467
 
384
468
  def unbind #:nodoc:
385
- if connected? and not closing? and not reconnecting?
469
+ if connected? and not closing? and not reconnecting? and @options[:reconnect]
386
470
  schedule_reconnect
387
471
  else
388
472
  process_disconnect unless reconnecting?
@@ -396,7 +480,9 @@ module NATS
396
480
  end
397
481
  ensure
398
482
  EM.cancel_timer(@reconnect_timer) if @reconnect_timer
399
- EM.stop if (NATS.client == self and connected? and closing? and not NATS.reactor_was_running?)
483
+ if (NATS.client == self and connected? and closing? and not NATS.reactor_was_running?)
484
+ EM.stop
485
+ end
400
486
  @connected = @reconnecting = false
401
487
  true # Chaining
402
488
  end