nats 0.3.12 → 0.4.2

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