activemessaging 0.6.1 → 0.7.1
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/README +22 -0
- data/Rakefile +48 -26
- data/VERSION +1 -0
- data/activemessaging.gemspec +113 -0
- data/generators/filter/templates/filter_test.rb +1 -1
- data/generators/processor/templates/broker.yml +18 -7
- data/init.rb +3 -0
- data/lib/activemessaging.rb +0 -3
- data/lib/activemessaging/adapters/asqs.rb +69 -61
- data/lib/activemessaging/adapters/base.rb +51 -63
- data/lib/activemessaging/adapters/beanstalk.rb +88 -0
- data/lib/activemessaging/adapters/jms.rb +7 -7
- data/lib/activemessaging/adapters/reliable_msg.rb +136 -140
- data/lib/activemessaging/adapters/stomp.rb +90 -22
- data/lib/activemessaging/adapters/test.rb +7 -25
- data/lib/activemessaging/adapters/wmq.rb +7 -16
- data/lib/activemessaging/base_message.rb +19 -0
- data/lib/activemessaging/gateway.rb +38 -41
- data/lib/activemessaging/test_helper.rb +4 -9
- data/poller.rb +14 -0
- data/test/all_tests.rb +10 -0
- data/test/app/config/broker.yml +4 -0
- data/test/asqs_test.rb +102 -0
- data/test/config_test.rb +42 -0
- data/test/filter_test.rb +131 -0
- data/test/gateway_test.rb +195 -0
- data/test/jms_test.rb +61 -0
- data/test/reliable_msg_test.rb +83 -0
- data/test/stomp_test.rb +131 -0
- data/test/test_helper.rb +24 -0
- data/test/tracer_test.rb +57 -0
- metadata +69 -53
- data/messaging.rb.example +0 -5
@@ -1,82 +1,70 @@
|
|
1
|
+
require 'activemessaging/adapter'
|
2
|
+
require 'activemessaging/base_message'
|
3
|
+
|
1
4
|
module ActiveMessaging
|
2
5
|
module Adapters
|
3
|
-
module Base
|
4
|
-
|
5
|
-
|
6
|
-
# use this as a base for implementing new connections
|
7
|
-
class Connection
|
8
|
-
include ActiveMessaging::Adapter
|
9
|
-
|
10
|
-
#use the register method to add the adapter to the configurable list of supported adapters
|
11
|
-
# register :generic
|
12
6
|
|
13
|
-
|
14
|
-
|
7
|
+
# use this as a base for implementing new connections
|
8
|
+
class BaseConnection
|
9
|
+
include ActiveMessaging::Adapter
|
15
10
|
|
16
|
-
|
17
|
-
|
18
|
-
end
|
11
|
+
#use the register method to add the adapter to the configurable list of supported adapters
|
12
|
+
# register :base
|
19
13
|
|
20
|
-
|
21
|
-
|
22
|
-
end
|
14
|
+
#configurable params
|
15
|
+
attr_accessor :reliable
|
23
16
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
end
|
28
|
-
|
29
|
-
# destination_name string, headers hash
|
30
|
-
# unsubscribe to listen on a destination
|
31
|
-
def unsubscribe destination_name, message_headers={}
|
32
|
-
end
|
17
|
+
#generic init method needed by a13g
|
18
|
+
def initialize cfg
|
19
|
+
end
|
33
20
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
end
|
21
|
+
# called to cleanly get rid of connection
|
22
|
+
def disconnect
|
23
|
+
end
|
38
24
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
25
|
+
# destination_name string, headers hash
|
26
|
+
# subscribe to listen on a destination
|
27
|
+
def subscribe destination_name, message_headers={}
|
28
|
+
end
|
43
29
|
|
44
|
-
|
45
|
-
|
46
|
-
|
30
|
+
# destination_name string, headers hash
|
31
|
+
# unsubscribe to listen on a destination
|
32
|
+
def unsubscribe destination_name, message_headers={}
|
33
|
+
end
|
47
34
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
end
|
52
|
-
|
35
|
+
# destination_name string, body string, headers hash
|
36
|
+
# send a single message to a destination
|
37
|
+
def send destination_name, message_body, message_headers={}
|
53
38
|
end
|
54
39
|
|
55
|
-
#
|
56
|
-
|
57
|
-
|
40
|
+
# receive a single message from any of the subscribed destinations
|
41
|
+
# check each destination once, then sleep for poll_interval
|
42
|
+
def receive
|
43
|
+
end
|
58
44
|
|
59
|
-
|
60
|
-
|
61
|
-
end
|
45
|
+
# called after a message is successfully received and processed
|
46
|
+
def received message, headers={}
|
62
47
|
end
|
63
48
|
|
64
|
-
#
|
65
|
-
#
|
66
|
-
|
67
|
-
class Message
|
68
|
-
attr_accessor :headers, :body, :command
|
69
|
-
|
70
|
-
def initialize headers, id, body, response, destination, command='MESSAGE'
|
71
|
-
@headers, @body, @command = headers, body, command
|
72
|
-
headers['destination'] = destination.name
|
73
|
-
end
|
74
|
-
|
75
|
-
def to_s
|
76
|
-
"<Base::Message body='#{body}' headers='#{headers.inspect}' command='#{command}' >"
|
77
|
-
end
|
49
|
+
# called after a message is successfully received but unsuccessfully processed
|
50
|
+
# purpose is to return the message to the destination so receiving and processing and be attempted again
|
51
|
+
def unreceive message, headers={}
|
78
52
|
end
|
79
|
-
|
53
|
+
|
80
54
|
end
|
55
|
+
|
56
|
+
## I recommend having a destination object to represent each subscribed destination
|
57
|
+
# class Destination
|
58
|
+
# attr_accessor :name
|
59
|
+
#
|
60
|
+
# def to_s
|
61
|
+
# "<Base::Destination name='#{name}'>"
|
62
|
+
# end
|
63
|
+
# end
|
64
|
+
|
65
|
+
## You should also define your own message based on the BaseMessage class
|
66
|
+
# class Message < ActiveMessaging::BaseMessage
|
67
|
+
# end
|
68
|
+
|
81
69
|
end
|
82
70
|
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
#
|
2
|
+
# contributed by Al-Faisal El-Dajani on 11/04/2009
|
3
|
+
#
|
4
|
+
# One caveat: beanstalk does not accept the underscore '_' as a legal character in queue names.
|
5
|
+
# So in messaging.rb you'll need to modify your queue names to use something other than the underscore, a dash perhaps.
|
6
|
+
# Accepting the underscore as a valid char in queue names is an open issue in beanstalk, and should be fixed in a future version.
|
7
|
+
#
|
8
|
+
|
9
|
+
require 'beanstalk-client'
|
10
|
+
require 'activemessaging/adapters/base'
|
11
|
+
|
12
|
+
module ActiveMessaging
|
13
|
+
module Adapters
|
14
|
+
module Beanstalk
|
15
|
+
|
16
|
+
class Connection < ActiveMessaging::Adapters::BaseConnection
|
17
|
+
register :beanstalk
|
18
|
+
|
19
|
+
attr_accessor :connection, :host, :port
|
20
|
+
|
21
|
+
def initialize cfg
|
22
|
+
@host = cfg[:host] || 'localhost'
|
23
|
+
@port = cfg[:port] || 11300
|
24
|
+
|
25
|
+
@connection = ::Beanstalk::Pool.new("#{@host}:#{@port}")
|
26
|
+
end
|
27
|
+
|
28
|
+
def disconnect
|
29
|
+
@connection.close
|
30
|
+
end
|
31
|
+
|
32
|
+
def subscribe tube, message_headers={}
|
33
|
+
@connection.watch(tube)
|
34
|
+
end
|
35
|
+
|
36
|
+
def unsubscribe tube, message_headers={}
|
37
|
+
@connection.ignore(tube)
|
38
|
+
end
|
39
|
+
|
40
|
+
def send tube, message, message_headers={}
|
41
|
+
priority = message_headers[:priority] || 65536
|
42
|
+
delay = message_headers[:delay] || 0
|
43
|
+
ttr = message_headers[:ttr] || 120
|
44
|
+
|
45
|
+
@connection.use(tube)
|
46
|
+
@connection.put(message, priority, delay, ttr)
|
47
|
+
end
|
48
|
+
|
49
|
+
def receive
|
50
|
+
message = @connection.reserve
|
51
|
+
Beanstalk::Message.new message
|
52
|
+
end
|
53
|
+
|
54
|
+
def received message, message_headers={}
|
55
|
+
message.delete
|
56
|
+
end
|
57
|
+
|
58
|
+
def unreceive message, message_headers={}
|
59
|
+
message.release
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
|
64
|
+
class Message < ActiveMessaging::BaseMessage
|
65
|
+
attr_accessor :beanstalk_job
|
66
|
+
|
67
|
+
def initialize beanstalk_job
|
68
|
+
bsh = {
|
69
|
+
'destination' => beanstalk_job.stats['tube'],
|
70
|
+
'priority' => beanstalk_job.pri,
|
71
|
+
'delay' => beanstalk_job.delay,
|
72
|
+
'ttr' => beanstalk_job.ttr
|
73
|
+
}
|
74
|
+
super(beanstalk_job.body, beanstalk_job.id, bsh, beanstalk_job.stats['tube'])
|
75
|
+
@beanstalk_job = beanstalk_job
|
76
|
+
end
|
77
|
+
|
78
|
+
def delete
|
79
|
+
@beanstalk_job.delete
|
80
|
+
end
|
81
|
+
|
82
|
+
def release
|
83
|
+
@beanstalk_job.release
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -25,7 +25,7 @@ module ActiveMessaging
|
|
25
25
|
#this initialize is probably activemq specific. There might be a more generic
|
26
26
|
#way of getting this without resorting to jndi lookup.
|
27
27
|
eval <<-end_eval
|
28
|
-
@connection_factory = Java::#{cfg[:connection_factory]}.new(@login, @
|
28
|
+
@connection_factory = Java::#{cfg[:connection_factory]}.new(@login, @passcode, @url)
|
29
29
|
end_eval
|
30
30
|
elsif cfg.has_key? :jndi
|
31
31
|
@connection_factory = javax.naming.InitialContext.new().lookup(cfg[:jndi])
|
@@ -164,18 +164,18 @@ module ActiveMessaging
|
|
164
164
|
def condition_message message
|
165
165
|
message.class.class_eval {
|
166
166
|
alias_method :body, :text unless method_defined? :body
|
167
|
-
|
168
|
-
def command
|
169
|
-
"MESSAGE"
|
170
|
-
end
|
171
|
-
|
167
|
+
|
172
168
|
def headers
|
173
169
|
destination.to_s =~ %r{(queue|topic)://(.*)}
|
174
170
|
puts "/#{$1}/#{$2}"
|
175
171
|
{'destination' => "/#{$1}/#{$2}"}
|
176
172
|
end
|
177
173
|
|
178
|
-
|
174
|
+
def matches_subscription?(subscription)
|
175
|
+
self.headers['destination'].to_s == subscription.value.to_s
|
176
|
+
end
|
177
|
+
|
178
|
+
} unless message.nil? || message.respond_to?(:headers)
|
179
179
|
message
|
180
180
|
end
|
181
181
|
|
@@ -1,5 +1,8 @@
|
|
1
1
|
require 'reliable-msg'
|
2
2
|
|
3
|
+
require 'activemessaging/adapters/base'
|
4
|
+
|
5
|
+
# add access to the queue_manager to use for adding transactions
|
3
6
|
module ReliableMsg
|
4
7
|
|
5
8
|
class Client
|
@@ -14,177 +17,170 @@ end
|
|
14
17
|
|
15
18
|
module ActiveMessaging
|
16
19
|
module Adapters
|
17
|
-
|
20
|
+
class ReliableMsgConnection < ActiveMessaging::Adapters::BaseConnection
|
18
21
|
|
19
22
|
THREAD_OLD_TXS = :a13g_reliable_msg_old_txs
|
20
23
|
|
21
24
|
QUEUE_PARAMS = [:expires,:delivery,:priority,:max_deliveries,:drb_uri,:tx_timeout,:connect_count]
|
22
25
|
TOPIC_PARAMS = [:expires,:drb_uri,:tx_timeout,:connect_count]
|
23
26
|
|
24
|
-
|
25
|
-
include ActiveMessaging::Adapter
|
27
|
+
register :reliable_msg
|
26
28
|
|
27
|
-
|
29
|
+
#configurable params
|
30
|
+
attr_accessor :subscriptions, :destinations, :poll_interval, :current_subscription, :tx_timeout
|
28
31
|
|
29
|
-
|
30
|
-
|
32
|
+
#generic init method needed by a13g
|
33
|
+
def initialize cfg
|
34
|
+
@poll_interval = cfg[:poll_interval] || 1
|
35
|
+
@reliable = cfg[:reliable] || true
|
36
|
+
@tx_timeout = cfg[:tx_timeout] || ::ReliableMsg::Client::DEFAULT_TX_TIMEOUT
|
31
37
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
@tx_timeout = cfg[:tx_timeout] || ::ReliableMsg::Client::DEFAULT_TX_TIMEOUT
|
38
|
+
@subscriptions = {}
|
39
|
+
@destinations = {}
|
40
|
+
@current_subscription = 0
|
41
|
+
end
|
37
42
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
43
|
+
# called to cleanly get rid of connection
|
44
|
+
def disconnect
|
45
|
+
nil
|
46
|
+
end
|
42
47
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
get_or_create_destination(destination_name, message_headers)
|
53
|
-
if subscriptions.has_key? destination_name
|
54
|
-
subscriptions[destination_name].add
|
55
|
-
else
|
56
|
-
subscriptions[destination_name] = Subscription.new(destination_name, message_headers)
|
57
|
-
end
|
48
|
+
# destination_name string, headers hash
|
49
|
+
# subscribe to listen on a destination
|
50
|
+
# use '/destination-type/name' convetion, like stomp
|
51
|
+
def subscribe destination_name, message_headers={}
|
52
|
+
get_or_create_destination(destination_name, message_headers)
|
53
|
+
if subscriptions.has_key? destination_name
|
54
|
+
subscriptions[destination_name].add
|
55
|
+
else
|
56
|
+
subscriptions[destination_name] = Subscription.new(destination_name, message_headers)
|
58
57
|
end
|
58
|
+
end
|
59
59
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
60
|
+
# destination_name string, headers hash
|
61
|
+
# unsubscribe to listen on a destination
|
62
|
+
def unsubscribe destination_name, message_headers={}
|
63
|
+
subscriptions[destination_name].remove
|
64
|
+
subscriptions.delete(destination_name) if subscriptions[destination_name].count <= 0
|
65
|
+
end
|
66
66
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
end
|
78
|
-
end
|
79
|
-
|
80
|
-
def get_or_create_destination destination_name, message_headers={}
|
81
|
-
return destinations[destination_name] if destinations.has_key? destination_name
|
82
|
-
dd = /^\/(queue|topic)\/(.*)$/.match(destination_name)
|
83
|
-
rm_class = dd[1].titleize
|
84
|
-
message_headers.delete("id")
|
85
|
-
dest_headers = message_headers.reject {|k,v| rm_class == 'Queue' ? !QUEUE_PARAMS.include?(k) : !TOPIC_PARAMS.include?(k)}
|
86
|
-
rm_dest = "ReliableMsg::#{rm_class}".constantize.new(dd[2], dest_headers)
|
87
|
-
destinations[destination_name] = rm_dest
|
67
|
+
# destination_name string, body string, headers hash
|
68
|
+
# send a single message to a destination
|
69
|
+
def send destination_name, message_body, message_headers={}
|
70
|
+
dest = get_or_create_destination(destination_name)
|
71
|
+
begin
|
72
|
+
dest.put message_body, message_headers
|
73
|
+
rescue Object=>err
|
74
|
+
raise err unless reliable
|
75
|
+
puts "send failed, will retry in #{@poll_interval} seconds"
|
76
|
+
sleep @poll_interval
|
88
77
|
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def get_or_create_destination destination_name, message_headers={}
|
81
|
+
return destinations[destination_name] if destinations.has_key? destination_name
|
82
|
+
dd = /^\/(queue|topic)\/(.*)$/.match(destination_name)
|
83
|
+
rm_class = dd[1].titleize
|
84
|
+
message_headers.delete("id")
|
85
|
+
dest_headers = message_headers.reject {|k,v| rm_class == 'Queue' ? !QUEUE_PARAMS.include?(k) : !TOPIC_PARAMS.include?(k)}
|
86
|
+
rm_dest = "ReliableMsg::#{rm_class}".constantize.new(dd[2], dest_headers)
|
87
|
+
destinations[destination_name] = rm_dest
|
88
|
+
end
|
89
89
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
end
|
126
|
-
return Message.new(reliable_msg.id, reliable_msg.object, reliable_msg.headers, destination_name, 'MESSAGE', @tx) if reliable_msg
|
127
|
-
|
128
|
-
Thread.current[::ReliableMsg::Client::THREAD_CURRENT_TX] = nil
|
90
|
+
# receive a single message from any of the subscribed destinations
|
91
|
+
# check each destination once, then sleep for poll_interval
|
92
|
+
def receive
|
93
|
+
|
94
|
+
raise "No subscriptions to receive messages from." if (subscriptions.nil? || subscriptions.empty?)
|
95
|
+
start = current_subscription
|
96
|
+
while true
|
97
|
+
self.current_subscription = ((current_subscription < subscriptions.length-1) ? current_subscription + 1 : 0)
|
98
|
+
sleep poll_interval if (current_subscription == start)
|
99
|
+
destination_name = subscriptions.keys.sort[current_subscription]
|
100
|
+
destination = destinations[destination_name]
|
101
|
+
unless destination.nil?
|
102
|
+
# from the way we use this, assume this is the start of a transaction,
|
103
|
+
# there should be no current transaction
|
104
|
+
ctx = Thread.current[::ReliableMsg::Client::THREAD_CURRENT_TX]
|
105
|
+
raise "There should not be an existing reliable-msg transaction. #{ctx.inspect}" if ctx
|
106
|
+
|
107
|
+
# start a new transaction
|
108
|
+
@tx = {:qm=>destination.queue_manager}
|
109
|
+
@tx[:tid] = @tx[:qm].begin @tx_timeout
|
110
|
+
Thread.current[::ReliableMsg::Client::THREAD_CURRENT_TX] = @tx
|
111
|
+
begin
|
112
|
+
|
113
|
+
# now call a get on the destination - it will use the transaction
|
114
|
+
#the commit or the abort will occur in the received or unreceive methods
|
115
|
+
reliable_msg = destination.get subscriptions[destination_name].headers[:selector]
|
116
|
+
@tx[:qm].commit(@tx[:tid]) if reliable_msg.nil?
|
117
|
+
|
118
|
+
rescue Object=>err
|
119
|
+
#abort the transaction on error
|
120
|
+
@tx[:qm].abort(@tx[:tid])
|
121
|
+
|
122
|
+
raise err unless reliable
|
123
|
+
puts "receive failed, will retry in #{@poll_interval} seconds"
|
124
|
+
sleep poll_interval
|
129
125
|
end
|
126
|
+
return Message.new(reliable_msg.object, reliable_msg.id, reliable_msg.headers, destination_name, @tx) if reliable_msg
|
127
|
+
|
128
|
+
Thread.current[::ReliableMsg::Client::THREAD_CURRENT_TX] = nil
|
130
129
|
end
|
131
130
|
end
|
131
|
+
end
|
132
132
|
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
end
|
142
|
-
|
133
|
+
# called after a message is successfully received and processed
|
134
|
+
def received message, headers={}
|
135
|
+
begin
|
136
|
+
message.transaction[:qm].commit(message.transaction[:tid])
|
137
|
+
rescue Object=>ex
|
138
|
+
puts "received failed: #{ex.message}"
|
139
|
+
ensure
|
140
|
+
Thread.current[::ReliableMsg::Client::THREAD_CURRENT_TX] = nil
|
143
141
|
end
|
144
142
|
|
145
|
-
# called after a message is successfully received and processed
|
146
|
-
def unreceive message, headers={}
|
147
|
-
begin
|
148
|
-
message.transaction[:qm].abort(message.transaction[:tid])
|
149
|
-
rescue Object=>ex
|
150
|
-
puts "unreceive failed: #{ex.message}"
|
151
|
-
ensure
|
152
|
-
Thread.current[::ReliableMsg::Client::THREAD_CURRENT_TX] = nil
|
153
|
-
end
|
154
|
-
end
|
155
|
-
|
156
143
|
end
|
157
144
|
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
@count += 1
|
145
|
+
# called after a message is successfully received and processed
|
146
|
+
def unreceive message, headers={}
|
147
|
+
begin
|
148
|
+
message.transaction[:qm].abort(message.transaction[:tid])
|
149
|
+
rescue Object=>ex
|
150
|
+
puts "unreceive failed: #{ex.message}"
|
151
|
+
ensure
|
152
|
+
Thread.current[::ReliableMsg::Client::THREAD_CURRENT_TX] = nil
|
167
153
|
end
|
154
|
+
end
|
168
155
|
|
169
|
-
|
170
|
-
|
171
|
-
|
156
|
+
end
|
157
|
+
|
158
|
+
class Subscription
|
159
|
+
attr_accessor :name, :headers, :count
|
160
|
+
|
161
|
+
def initialize(destination, headers={}, count=1)
|
162
|
+
@destination, @headers, @count = destination, headers, count
|
163
|
+
end
|
164
|
+
|
165
|
+
def add
|
166
|
+
@count += 1
|
167
|
+
end
|
172
168
|
|
169
|
+
def remove
|
170
|
+
@count -= 1
|
173
171
|
end
|
174
172
|
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
@id, @body, @headers, @command, @transaction = id, body, headers, command, transaction
|
180
|
-
headers['destination'] = destination_name
|
181
|
-
end
|
173
|
+
end
|
174
|
+
|
175
|
+
class Message < ActiveMessaging::BaseMessage
|
176
|
+
attr_accessor :transaction
|
182
177
|
|
183
|
-
|
184
|
-
|
185
|
-
|
178
|
+
def initialize body, id, headers, destination, transaction=nil
|
179
|
+
super(body, id, headers, destination)
|
180
|
+
@transaction = transaction
|
186
181
|
end
|
187
|
-
|
182
|
+
|
188
183
|
end
|
184
|
+
|
189
185
|
end
|
190
186
|
end
|