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.
@@ -0,0 +1,307 @@
1
+ module NATSD #:nodoc: all
2
+
3
+ # Subscriber
4
+ Subscriber = Struct.new(:conn, :subject, :sid, :qgroup, :num_responses, :max_responses)
5
+
6
+ class Server
7
+
8
+ class << self
9
+ attr_reader :id, :info, :log_time, :auth_required, :debug_flag, :trace_flag, :options
10
+ attr_reader :max_payload, :max_pending, :max_control_line, :auth_timeout
11
+
12
+ alias auth_required? :auth_required
13
+ alias debug_flag? :debug_flag
14
+ alias trace_flag? :trace_flag
15
+
16
+ def version; "nats server version #{NATSD::VERSION}" end
17
+
18
+ def host; @options[:addr] end
19
+ def port; @options[:port] end
20
+ def pid_file; @options[:pid_file] end
21
+
22
+ def process_options(argv=[])
23
+ @options = {}
24
+
25
+ # Allow command line to override config file, so do them first.
26
+ parser.parse!(argv)
27
+ read_config_file
28
+ finalize_options
29
+ end
30
+
31
+ def setup(argv)
32
+ process_options(argv)
33
+
34
+ @id, @cid = fast_uuid, 1
35
+ @sublist = Sublist.new
36
+ @info = {
37
+ :server_id => Server.id,
38
+ :version => VERSION,
39
+ :auth_required => auth_required?,
40
+ :max_payload => @max_payload
41
+ }
42
+
43
+ # Check for daemon flag
44
+ if @options[:daemonize]
45
+ require 'rubygems'
46
+ require 'daemons'
47
+ unless @options[:log_file]
48
+ # These log messages visible to controlling TTY
49
+ log "Starting #{NATSD::APP_NAME} version #{NATSD::VERSION} on port #{NATSD::Server.port}"
50
+ log "Switching to daemon mode"
51
+ end
52
+ Daemons.daemonize(:app_name => APP_NAME, :mode => :exec)
53
+ end
54
+
55
+ setup_logs
56
+
57
+ # Setup optimized select versions
58
+ EM.epoll unless @options[:noepoll]
59
+ EM.kqueue unless @options[:nokqueue]
60
+
61
+ # Write pid file if need be.
62
+ File.open(@options[:pid_file], 'w') { |f| f.puts "#{Process.pid}" } if @options[:pid_file]
63
+ end
64
+
65
+ def subscribe(sub)
66
+ @sublist.insert(sub.subject, sub)
67
+ end
68
+
69
+ def unsubscribe(sub)
70
+ @sublist.remove(sub.subject, sub)
71
+ end
72
+
73
+ def deliver_to_subscriber(sub, subject, reply, msg)
74
+
75
+ # Allows nil reply to not have extra space
76
+ reply = reply + ' ' if reply
77
+
78
+ conn = sub.conn
79
+
80
+ conn.send_data("MSG #{subject} #{sub.sid} #{reply}#{msg.bytesize}#{CR_LF}")
81
+ conn.send_data(msg)
82
+ conn.send_data(CR_LF)
83
+
84
+ # Account for these response and check for auto-unsubscribe (pruning interest graph)
85
+ sub.num_responses += 1
86
+ conn.delete_subscriber(sub) if (sub.max_responses && sub.num_responses >= sub.max_responses)
87
+
88
+ # Check the outbound queue here and react if need be..
89
+ if conn.get_outbound_data_size > NATSD::Server.max_pending
90
+ conn.error_close SLOW_CONSUMER
91
+ log "Slow consumer dropped, exceeded #{NATSD::Server.max_pending} bytes pending", conn.client_info
92
+ end
93
+ end
94
+
95
+ def route_to_subscribers(subject, reply, msg)
96
+ qsubs = nil
97
+
98
+ @sublist.match(subject).each do |sub|
99
+ # Skip anyone in the closing state
100
+ next if sub.conn.closing
101
+
102
+ unless sub[:qgroup]
103
+ deliver_to_subscriber(sub, subject, reply, msg)
104
+ else
105
+ if NATSD::Server.trace_flag?
106
+ trace("Matched queue subscriber", sub[:subject], sub[:qgroup], sub[:sid], sub.conn.client_info)
107
+ end
108
+ # Queue this for post processing
109
+ qsubs ||= Hash.new([])
110
+ qsubs[sub[:qgroup]] = qsubs[sub[:qgroup]] << sub
111
+ end
112
+ end
113
+
114
+ return unless qsubs
115
+
116
+ qsubs.each_value do |subs|
117
+ # Randomly pick a subscriber from the group
118
+ sub = subs[rand*subs.size]
119
+ if NATSD::Server.trace_flag?
120
+ trace("Selected queue subscriber", sub[:subject], sub[:qgroup], sub[:sid], sub.conn.client_info)
121
+ end
122
+ deliver_to_subscriber(sub, subject, reply, msg)
123
+ end
124
+ end
125
+
126
+ def auth_ok?(user, pass)
127
+ user == @options[:user] && pass == @options[:pass]
128
+ end
129
+
130
+ def cid
131
+ @cid += 1
132
+ end
133
+
134
+ def info_string
135
+ @info.to_json
136
+ end
137
+
138
+ end
139
+ end
140
+
141
+ module Connection #:nodoc:
142
+
143
+ attr_reader :cid, :closing
144
+
145
+ def client_info
146
+ @client_info ||= Socket.unpack_sockaddr_in(get_peername)
147
+ end
148
+
149
+ def post_init
150
+ @cid = Server.cid
151
+ @subscriptions = {}
152
+ @verbose = @pedantic = true # suppressed by most clients, but allows friendly telnet
153
+ @receive_data_calls = 0
154
+ @parse_state = AWAITING_CONTROL_LINE
155
+ send_info
156
+ @auth_pending = EM.add_timer(NATSD::Server.auth_timeout) { connect_auth_timeout } if Server.auth_required?
157
+ debug "Client connection created", client_info, cid
158
+ end
159
+
160
+ def connect_auth_timeout
161
+ error_close AUTH_REQUIRED
162
+ debug "Connection timeout due to lack of auth credentials", cid
163
+ end
164
+
165
+ def receive_data(data)
166
+ @receive_data_calls += 1
167
+ @buf = @buf ? @buf << data : data
168
+ return close_connection if @buf =~ /(\006|\004)/ # ctrl+c or ctrl+d for telnet friendly
169
+
170
+ # while (@buf && !@buf.empty? && !@closing)
171
+ while (@buf && !@closing)
172
+ case @parse_state
173
+
174
+ when AWAITING_CONTROL_LINE
175
+ case @buf
176
+ when PUB_OP
177
+ ctrace('PUB OP', strip_op($&)) if NATSD::Server.trace_flag?
178
+ return connect_auth_timeout if @auth_pending
179
+ @buf = $'
180
+ @parse_state = AWAITING_MSG_PAYLOAD
181
+ @msg_sub, @msg_reply, @msg_size = $1, $3, $4.to_i
182
+ if (@msg_size > NATSD::Server.max_payload)
183
+ debug "Message payload size exceeded (#{@msg_size}/#{NATSD::Server.max_payload}), closing connection"
184
+ error_close PAYLOAD_TOO_BIG
185
+ end
186
+ send_data(INVALID_SUBJECT) if (@pedantic && !(@msg_sub =~ SUB_NO_WC))
187
+ when SUB_OP
188
+ ctrace('SUB OP', strip_op($&)) if NATSD::Server.trace_flag?
189
+ return connect_auth_timeout if @auth_pending
190
+ @buf = $'
191
+ sub, qgroup, sid = $1, $3, $4
192
+ return send_data(INVALID_SUBJECT) if !($1 =~ SUB)
193
+ return send_data(INVALID_SID_TAKEN) if @subscriptions[sid]
194
+ sub = Subscriber.new(self, sub, sid, qgroup, 0)
195
+ @subscriptions[sid] = sub
196
+ Server.subscribe(sub)
197
+ send_data(OK) if @verbose
198
+ when UNSUB_OP
199
+ ctrace('UNSUB OP', strip_op($&)) if NATSD::Server.trace_flag?
200
+ return connect_auth_timeout if @auth_pending
201
+ @buf = $'
202
+ sid, sub = $1, @subscriptions[$1]
203
+ return send_data(INVALID_SID_NOEXIST) unless sub
204
+ # If we have set max_responses, we will unsubscribe once we have received the appropriate
205
+ # amount of responses
206
+ sub.max_responses = ($2 && $3) ? $3.to_i : nil
207
+ delete_subscriber(sub) unless (sub.max_responses && (sub.num_responses < sub.max_responses))
208
+ send_data(OK) if @verbose
209
+ when PING
210
+ ctrace('PING OP', strip_op($&)) if NATSD::Server.trace_flag?
211
+ @buf = $'
212
+ send_data(PONG_RESPONSE)
213
+ when CONNECT
214
+ ctrace('CONNECT OP', strip_op($&)) if NATSD::Server.trace_flag?
215
+ @buf = $'
216
+ begin
217
+ config = JSON.parse($1, :symbolize_keys => true, :symbolize_names => true)
218
+ process_connect_config(config)
219
+ rescue => e
220
+ send_data(INVALID_CONFIG)
221
+ log_error
222
+ end
223
+ when INFO
224
+ ctrace('INFO OP', strip_op($&)) if NATSD::Server.trace_flag?
225
+ return connect_auth_timeout if @auth_pending
226
+ @buf = $'
227
+ send_info
228
+ when UNKNOWN
229
+ ctrace('Unknown Op', strip_op($&)) if NATSD::Server.trace_flag?
230
+ return connect_auth_timeout if @auth_pending
231
+ @buf = $'
232
+ send_data(UNKNOWN_OP)
233
+ else
234
+ # If we are here we do not have a complete line yet that we understand.
235
+ # If too big, cut the connection off.
236
+ if @buf.bytesize > NATSD::Server.max_control_line
237
+ debug "Control line size exceeded (#{@buf.bytesize}/#{NATSD::Server.max_control_line}), closing connection.."
238
+ error_close PROTOCOL_OP_TOO_BIG
239
+ end
240
+ return
241
+ end
242
+ @buf = nil if (@buf && @buf.empty?)
243
+
244
+ when AWAITING_MSG_PAYLOAD
245
+ return unless (@buf.bytesize >= (@msg_size + CR_LF_SIZE))
246
+ msg = @buf.slice(0, @msg_size)
247
+ ctrace('Processing msg', @msg_sub, @msg_reply, msg) if NATSD::Server.trace_flag?
248
+ send_data(OK) if @verbose
249
+ Server.route_to_subscribers(@msg_sub, @msg_reply, msg)
250
+ @buf = @buf.slice((@msg_size + CR_LF_SIZE), @buf.bytesize)
251
+ @msg_sub = @msg_size = @reply = nil
252
+ @parse_state = AWAITING_CONTROL_LINE
253
+ @buf = nil if (@buf && @buf.empty?)
254
+ end
255
+ end
256
+
257
+ end
258
+
259
+ def send_info
260
+ send_data("INFO #{Server.info_string}#{CR_LF}")
261
+ end
262
+
263
+ def process_connect_config(config)
264
+ @verbose = config[:verbose] unless config[:verbose].nil?
265
+ @pedantic = config[:pedantic] unless config[:pedantic].nil?
266
+ return send_data(OK) unless Server.auth_required?
267
+
268
+ EM.cancel_timer(@auth_pending)
269
+ if Server.auth_ok?(config[:user], config[:pass])
270
+ send_data(OK) if @verbose
271
+ @auth_pending = nil
272
+ else
273
+ error_close AUTH_FAILED
274
+ debug "Authorization failed for connection", cid
275
+ end
276
+ end
277
+
278
+ def delete_subscriber(sub)
279
+ ctrace('DELSUB OP', sub.subject, sub.qgroup, sub.sid) if NATSD::Server.trace_flag?
280
+ Server.unsubscribe(sub)
281
+ @subscriptions.delete(sub.sid)
282
+ end
283
+
284
+ def error_close(msg)
285
+ send_data(msg)
286
+ close_connection_after_writing
287
+ @closing = true
288
+ end
289
+
290
+ def unbind
291
+ debug "Client connection closed", client_info, cid
292
+ # ctrace "Receive_Data called #{@receive_data_calls} times." if @receive_data_calls > 0
293
+ @subscriptions.each_value { |sub| Server.unsubscribe(sub) }
294
+ EM.cancel_timer(@auth_pending) if @auth_pending
295
+ @auth_pending = nil
296
+ end
297
+
298
+ def ctrace(*args)
299
+ trace(args, "c: #{cid}")
300
+ end
301
+
302
+ def strip_op(op='')
303
+ op.dup.sub(CR_LF, EMPTY)
304
+ end
305
+ end
306
+
307
+ end
@@ -1,9 +1,9 @@
1
1
  #--
2
2
  #
3
3
  # Sublist implementation for a publish-subscribe system.
4
- # This container class holds subscriptions and matches
4
+ # This container class holds subscriptions and matches
5
5
  # candidate subjects to those subscriptions.
6
- # Certain wildcards are supported for subscriptions.
6
+ # Certain wildcards are supported for subscriptions.
7
7
  # '*' will match any given token at any level.
8
8
  # '>' will match all subsequent tokens.
9
9
  #--
@@ -14,12 +14,12 @@ class Sublist #:nodoc:
14
14
  PWC = '*'.freeze
15
15
  FWC = '>'.freeze
16
16
  CACHE_SIZE = 4096
17
-
17
+
18
18
  attr_reader :count
19
19
 
20
20
  SublistNode = Struct.new(:leaf_nodes, :next_level)
21
21
  SublistLevel = Struct.new(:nodes, :pwc, :fwc)
22
-
22
+
23
23
  def initialize(options = {})
24
24
  @count = 0
25
25
  @results = []
@@ -36,7 +36,7 @@ class Sublist #:nodoc:
36
36
  # does not need to completely go away when a remove happens..
37
37
  #
38
38
  # front end caching is on by default, but we can turn it off here if needed
39
-
39
+
40
40
  def disable_cache; @cache = nil; end
41
41
  def enable_cache; @cache ||= {}; end
42
42
  def clear_cache; @cache = {} if @cache; end
@@ -71,16 +71,16 @@ class Sublist #:nodoc:
71
71
  when PWC then node = level.pwc
72
72
  else node = level.nodes[token]
73
73
  end
74
- level = node.next_level
74
+ level = node.next_level
75
75
  end
76
76
  # This could be expensize if a large number of subscribers exist.
77
- node.leaf_nodes.delete(subscriber) if (node && node.leaf_nodes)
77
+ node.leaf_nodes.delete(subscriber) if (node && node.leaf_nodes)
78
78
  clear_cache # Clear the cache
79
79
  end
80
-
80
+
81
81
  # Match a subject to all subscribers, return the array of matches.
82
82
  def match(subject)
83
- return @cache[subject] if (@cache && @cache[subject])
83
+ return @cache[subject] if (@cache && @cache[subject])
84
84
  tokens = subject.split('.')
85
85
  @results.clear
86
86
  matchAll(@root, tokens)
@@ -93,24 +93,24 @@ class Sublist #:nodoc:
93
93
  end
94
94
 
95
95
  private
96
-
96
+
97
97
  def matchAll(level, tokens)
98
98
  node, pwc = nil, nil # Define for scope
99
99
  i, ts = 0, tokens.size
100
100
  while (i < ts) do
101
101
  return if level == nil
102
102
  # Handle a full wildcard here by adding all of the subscribers.
103
- @results.concat(level.fwc.leaf_nodes) if level.fwc
103
+ @results.concat(level.fwc.leaf_nodes) if level.fwc
104
104
  # Handle an internal partial wildcard by branching recursively
105
105
  lpwc = level.pwc
106
106
  matchAll(lpwc.next_level, tokens[i+1, ts]) if lpwc
107
107
  node, pwc = level.nodes[tokens[i]], lpwc
108
108
  #level = node.next_level if node
109
- level = node ? node.next_level : nil
109
+ level = node ? node.next_level : nil
110
110
  i += 1
111
111
  end
112
- @results.concat(pwc.leaf_nodes) if pwc
112
+ @results.concat(pwc.leaf_nodes) if pwc
113
113
  @results.concat(node.leaf_nodes) if node
114
- end
114
+ end
115
115
 
116
116
  end
@@ -0,0 +1,34 @@
1
+
2
+ def fast_uuid #:nodoc:
3
+ v = [rand(0x0010000),rand(0x0010000),rand(0x0010000),
4
+ rand(0x0010000),rand(0x0010000),rand(0x1000000)]
5
+ "%04x%04x%04x%04x%04x%06x" % v
6
+ end
7
+
8
+ def log(*args) #:nodoc:
9
+ args.unshift(Time.now) if NATSD::Server.log_time
10
+ pp args.compact
11
+ end
12
+
13
+ def debug(*args) #:nodoc:
14
+ log *args if NATSD::Server.debug_flag?
15
+ end
16
+
17
+ def trace(*args) #:nodoc:
18
+ log *args if NATSD::Server.trace_flag?
19
+ end
20
+
21
+ def log_error(e=$!) #:nodoc:
22
+ debug e, e.backtrace
23
+ end
24
+
25
+ def shutdown #:nodoc:
26
+ puts
27
+ log 'Server exiting..'
28
+ EM.stop
29
+ FileUtils.rm(NATSD::Server.pid_file) if NATSD::Server.pid_file
30
+ exit
31
+ end
32
+
33
+ ['TERM','INT'].each { |s| trap(s) { shutdown } }
34
+
data/nats.gemspec CHANGED
@@ -8,39 +8,43 @@ require 'nats/server/const.rb'
8
8
  spec = Gem::Specification.new do |s|
9
9
  s.name = 'nats'
10
10
  s.version = NATSD::VERSION
11
- s.date = '2010-11-20'
12
- s.summary = 'Simple Publish-Subscribe Messaging System'
13
- s.homepage = "http://github.com/derekcollison/nats"
14
- s.description = "A lightweight, fast, publish-subscribe messaging system."
11
+ s.date = '2011-02-09'
12
+ s.summary = 'A lightweight publish-subscribe messaging system.'
13
+ s.homepage = 'http://github.com/derekcollison/nats'
14
+ s.description = 'A lightweight publish-subscribe messaging system.'
15
15
  s.has_rdoc = true
16
16
 
17
- s.authors = ["Derek Collison"]
18
- s.email = ["derek.collison@gmail.com"]
17
+ s.authors = ['Derek Collison']
18
+ s.email = ['derek.collison@gmail.com']
19
19
 
20
20
  s.add_dependency('eventmachine', '>= 0.12.10')
21
- s.add_dependency('yajl-ruby', '>= 0.7.8')
21
+ s.add_dependency('json_pure', '>= 1.5.1')
22
22
  s.add_dependency('daemons', '>= 1.1.0')
23
23
 
24
24
  s.require_paths = ['lib']
25
25
  s.bindir = 'bin'
26
- s.executables = [NATSD::APP_NAME, 'nats-pub', 'nats-sub']
26
+ s.executables = [NATSD::APP_NAME, 'nats-pub', 'nats-sub', 'nats-queue']
27
27
 
28
28
  s.files = %w[
29
29
  COPYING
30
30
  README.md
31
+ ChangeLog
31
32
  nats.gemspec
32
33
  Rakefile
33
34
  bin/nats-server
34
35
  bin/nats-sub
35
36
  bin/nats-pub
37
+ bin/nats-queue
36
38
  lib/nats/client.rb
37
39
  lib/nats/ext/bytesize.rb
38
40
  lib/nats/ext/em.rb
39
41
  lib/nats/ext/json.rb
40
42
  lib/nats/server.rb
43
+ lib/nats/server/server.rb
41
44
  lib/nats/server/options.rb
42
45
  lib/nats/server/sublist.rb
43
46
  lib/nats/server/const.rb
47
+ lib/nats/server/util.rb
44
48
  ]
45
49
 
46
50
  end