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 +1 -1
- data/ChangeLog +11 -0
- data/README.md +53 -30
- data/Rakefile +2 -2
- data/bin/nats-queue +21 -0
- data/lib/nats/client.rb +244 -158
- data/lib/nats/ext/em.rb +7 -0
- data/lib/nats/server.rb +9 -291
- data/lib/nats/server/const.rb +32 -22
- data/lib/nats/server/options.rb +63 -31
- data/lib/nats/server/server.rb +307 -0
- data/lib/nats/server/sublist.rb +14 -14
- data/lib/nats/server/util.rb +34 -0
- data/nats.gemspec +12 -8
- metadata +13 -28
@@ -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
|
data/lib/nats/server/sublist.rb
CHANGED
@@ -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 = '
|
12
|
-
s.summary = '
|
13
|
-
s.homepage =
|
14
|
-
s.description =
|
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 = [
|
18
|
-
s.email = [
|
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('
|
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
|