rjr 0.9.0 → 0.11.7

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/lib/rjr/ws_node.rb CHANGED
@@ -5,11 +5,25 @@
5
5
  # Copyright (C) 2012 Mohammed Morsi <mo@morsi.org>
6
6
  # Licensed under the Apache License, Version 2.0
7
7
 
8
+ skip_module = false
9
+ begin
8
10
  require 'em-websocket'
9
- require 'rjr/web_socket'
11
+ require 'em-websocket-client'
12
+ rescue LoadError
13
+ skip_module = true
14
+ end
15
+
16
+ if skip_module
17
+ # TODO output: "ws dependencies could not be loaded, skipping ws node definition"
18
+ require 'rjr/missing_node'
19
+ RJR::WSNode = RJR::MissingNode
10
20
 
21
+ else
22
+ require 'socket'
11
23
  require 'rjr/node'
12
24
  require 'rjr/message'
25
+ require 'rjr/dispatcher'
26
+ require 'rjr/thread_pool2'
13
27
 
14
28
  module RJR
15
29
 
@@ -31,8 +45,7 @@ class WSNodeCallback
31
45
 
32
46
  # Implementation of {RJR::NodeCallback#invoke}
33
47
  def invoke(callback_method, *data)
34
- #msg = CallbackMessage.new(:data => data)
35
- msg = RequestMessage.new :method => callback_method, :args => data, :headers => @message_headers
48
+ msg = NotificationMessage.new :method => callback_method, :args => data, :headers => @message_headers
36
49
  raise RJR::Errors::ConnectionError.new("websocket closed") if @socket.state == :closed
37
50
  # TODO surround w/ begin/rescue block incase of other socket errors?
38
51
  @socket.send(msg.to_s)
@@ -62,17 +75,57 @@ end
62
75
  # puts client.invoke_request('ws://localhost:7777', 'hello', 'mo')
63
76
  #
64
77
  class WSNode < RJR::Node
65
- RJR_NODE_TYPE = :websockets
78
+ RJR_NODE_TYPE = :ws
66
79
 
67
80
  private
68
- # Initialize the ws subsystem
69
- def init_node
81
+
82
+ # Internal helper initialize new connection
83
+ def init_node(uri, &on_init)
84
+ connection = nil
85
+ @connections_lock.synchronize {
86
+ connection = @connections.find { |c|
87
+ c.url == uri
88
+ }
89
+ if connection.nil?
90
+ connection = EventMachine::WebSocketClient.connect(uri)
91
+ connection.callback do
92
+ on_init.call(connection)
93
+ end
94
+ @connections << connection
95
+ # TODO sleep until connected?
96
+ else
97
+ on_init.call(connection)
98
+ end
99
+ }
100
+ connection
101
+ end
102
+
103
+ # Internal helper handle messages
104
+ def handle_msg(endpoint, msg)
105
+ # TODO use messageutil incase of large messages?
106
+ if RequestMessage.is_request_message?(msg)
107
+ ThreadPool2Manager << ThreadPool2Job.new { handle_request(endpoint, msg, false) }
108
+
109
+ elsif NotificationMessage.is_notification_message?(msg)
110
+ ThreadPool2Manager << ThreadPool2Job.new { handle_request(endpoint, msg, true) }
111
+
112
+ elsif ResponseMessage.is_response_message?(msg)
113
+ handle_response(msg)
114
+
115
+ end
70
116
  end
71
117
 
72
118
  # Internal helper, handle request message received
73
- def handle_request(socket, message)
74
- client_port, client_ip = Socket.unpack_sockaddr_in(socket.get_peername)
75
- msg = RequestMessage.new(:message => message, :headers => @message_headers)
119
+ def handle_request(endpoint, message, notification=false)
120
+ # XXX hack to handle client disconnection (should grap port/ip immediately on connection and use that)
121
+ client_port,client_ip = nil,nil
122
+ begin
123
+ client_port, client_ip = Socket.unpack_sockaddr_in(endpoint.get_peername)
124
+ rescue Exception=>e
125
+ end
126
+
127
+ msg = notification ? NotificationMessage.new(:message => message, :headers => @message_headers) :
128
+ RequestMessage.new(:message => message, :headers => @message_headers)
76
129
  headers = @message_headers.merge(msg.headers)
77
130
  result = Dispatcher.dispatch_request(msg.jr_method,
78
131
  :method_args => msg.jr_args,
@@ -83,12 +136,53 @@ class WSNode < RJR::Node
83
136
  :rjr_node_id => @node_id,
84
137
  :rjr_node_type => RJR_NODE_TYPE,
85
138
  :rjr_callback =>
86
- WSNodeCallback.new(:socket => socket,
139
+ WSNodeCallback.new(:socket => endpoint,
87
140
  :headers => headers))
88
- response = ResponseMessage.new(:id => msg.msg_id, :result => result, :headers => headers)
89
- socket.send(response.to_s)
141
+ unless notification
142
+ response = ResponseMessage.new(:id => msg.msg_id, :result => result, :headers => headers)
143
+ endpoint.send(response.to_s)
144
+ end
145
+ end
146
+
147
+ # Internal helper, handle response message received
148
+ def handle_response(data)
149
+ msg = ResponseMessage.new(:message => data, :headers => @message_headers)
150
+ res = err = nil
151
+ begin
152
+ res = Dispatcher.handle_response(msg.result)
153
+ rescue Exception => e
154
+ err = e
155
+ end
156
+
157
+ @response_lock.synchronize {
158
+ result = [msg.msg_id, res]
159
+ result << err if !err.nil?
160
+ @responses << result
161
+ @response_cv.signal
162
+ }
163
+ end
164
+
165
+ # Internal helper, block until response matching message id is received
166
+ def wait_for_result(message)
167
+ res = nil
168
+ while res.nil?
169
+ @response_lock.synchronize{
170
+ # FIXME throw err if more than 1 match found
171
+ res = @responses.select { |response| message.msg_id == response.first }.first
172
+ if !res.nil?
173
+ @responses.delete(res)
174
+
175
+ else
176
+ @response_cv.signal
177
+ @response_cv.wait @response_lock
178
+
179
+ end
180
+ }
181
+ end
182
+ return res
90
183
  end
91
184
 
185
+
92
186
  public
93
187
  # WSNode initializer
94
188
  # @param [Hash] args the options to create the web socket node with
@@ -99,6 +193,13 @@ class WSNode < RJR::Node
99
193
  @host = args[:host]
100
194
  @port = args[:port]
101
195
 
196
+ @connections = []
197
+ @connections_lock = Mutex.new
198
+
199
+ @response_lock = Mutex.new
200
+ @response_cv = ConditionVariable.new
201
+ @responses = []
202
+
102
203
  @connection_event_handlers = {:closed => [], :error => []}
103
204
  end
104
205
 
@@ -117,22 +218,11 @@ class WSNode < RJR::Node
117
218
  # Implementation of {RJR::Node#listen}
118
219
  def listen
119
220
  em_run do
120
- init_node
121
221
  EventMachine::WebSocket.start(:host => @host, :port => @port) do |ws|
122
- ws.onopen {}
123
- ws.onclose {
124
- @connection_event_handlers[:closed].each { |h|
125
- h.call self
126
- }
127
- }
128
- ws.onerror {|e|
129
- @connection_event_handlers[:error].each { |h|
130
- h.call self
131
- }
132
- }
133
- ws.onmessage { |msg|
134
- @thread_pool << ThreadPoolJob.new { handle_request(ws, msg) }
135
- }
222
+ ws.onopen { }
223
+ ws.onclose { @connection_event_handlers[:closed].each { |h| h.call self } }
224
+ ws.onerror { |e| @connection_event_handlers[:error].each { |h| h.call self } }
225
+ ws.onmessage { |msg| handle_msg(ws, msg) }
136
226
  end
137
227
  end
138
228
  end
@@ -143,17 +233,55 @@ class WSNode < RJR::Node
143
233
  # @param [String] rpc_method json-rpc method to invoke on destination
144
234
  # @param [Array] args array of arguments to convert to json and invoke remote method wtih
145
235
  def invoke_request(uri, rpc_method, *args)
146
- init_node
147
236
  message = RequestMessage.new :method => rpc_method,
148
237
  :args => args,
149
238
  :headers => @message_headers
150
- socket = WebSocket.new(uri)
151
- socket.send(message.to_s)
152
- res = socket.receive()
153
- msg = ResponseMessage.new(:message => res, :headers => @message_headers)
154
- headers = @message_headers.merge(msg.headers)
155
- return Dispatcher.handle_response(msg.result)
239
+
240
+ em_run{
241
+ init_node(uri) do |c|
242
+ c.stream { |msg| handle_msg(c, msg) }
243
+
244
+ c.send_msg message.to_s
245
+ end
246
+ }
247
+
248
+ # TODO optional timeout for response ?
249
+ result = wait_for_result(message)
250
+
251
+ if result.size > 2
252
+ raise Exception, result[2]
253
+ end
254
+ return result[1]
255
+ end
256
+
257
+ # Instructs node to send rpc notification (immadiately returns / no response is generated)
258
+ #
259
+ # @param [String] uri location of node to send notification to, should be
260
+ # in format of ws://hostname:port
261
+ # @param [String] rpc_method json-rpc method to invoke on destination
262
+ # @param [Array] args array of arguments to convert to json and invoke remote method wtih
263
+ def send_notification(uri, rpc_method, *args)
264
+ # will block until message is published
265
+ published_l = Mutex.new
266
+ published_c = ConditionVariable.new
267
+
268
+ message = NotificationMessage.new :method => rpc_method,
269
+ :args => args,
270
+ :headers => @message_headers
271
+ em_run{
272
+ init_node(uri) do |c|
273
+ c.send_msg message.to_s
274
+
275
+ # XXX same bug w/ tcp node, due to nature of event machine
276
+ # we aren't guaranteed that message is actually written to socket
277
+ # here, process must be kept alive until data is sent or will be lost
278
+ published_l.synchronize { published_c.signal }
279
+ end
280
+ }
281
+ published_l.synchronize { published_c.wait published_l }
282
+ nil
156
283
  end
157
284
  end
158
285
 
159
286
  end # module RJR
287
+ end
data/lib/rjr.rb CHANGED
@@ -6,16 +6,11 @@
6
6
  # rjr - Ruby Json Rpc
7
7
  module RJR ; end
8
8
 
9
- require 'rjr/common'
10
- require 'rjr/errors'
11
- require 'rjr/thread_pool'
12
- require 'rjr/semaphore'
13
- require 'rjr/node'
14
- require 'rjr/dispatcher'
15
- require 'rjr/message'
16
- require 'rjr/local_node'
17
9
  require 'rjr/amqp_node'
18
- require 'rjr/ws_node'
19
- require 'rjr/web_node'
20
10
  require 'rjr/tcp_node'
11
+ require 'rjr/web_node'
12
+ require 'rjr/ws_node'
13
+ require 'rjr/local_node'
21
14
  require 'rjr/multi_node'
15
+
16
+ require 'rjr/util'
@@ -49,6 +49,10 @@ end
49
49
 
50
50
 
51
51
  describe RJR::Handler do
52
+ before(:each) do
53
+ RJR::DispatcherStat.reset
54
+ end
55
+
52
56
  it "should return method not found result if method name is not specified" do
53
57
  handler = RJR::Handler.new :method => nil
54
58
  result = handler.handle
@@ -65,6 +69,15 @@ describe RJR::Handler do
65
69
  invoked.should == true
66
70
  end
67
71
 
72
+ it "should create dispatcher stat when invoking handler" do
73
+ handler = RJR::Handler.new :method => 'foobar',
74
+ :handler => lambda { 42 }
75
+ handler.handle({:method_args => [] })
76
+ RJR::DispatcherStat.stats.size.should == 1
77
+ RJR::DispatcherStat.stats.first.request.method.should == 'foobar'
78
+ RJR::DispatcherStat.stats.first.result.result.should == 42
79
+ end
80
+
68
81
  it "should return handler's return value in successful result" do
69
82
  retval = Object.new
70
83
  handler = RJR::Handler.new :method => 'foobar',
@@ -90,6 +103,59 @@ describe RJR::Handler do
90
103
  end
91
104
  end
92
105
 
106
+ describe RJR::DispatcherStat do
107
+ before(:each) do
108
+ RJR::DispatcherStat.reset
109
+ end
110
+
111
+ it "should store request and result" do
112
+ req = RJR::Request.new
113
+ res = RJR::Result.new
114
+ stat = RJR::DispatcherStat.new req, res
115
+ (stat.request == req).should be_true
116
+ stat.result.should == res
117
+ end
118
+
119
+ it "should track global stats" do
120
+ req = RJR::Request.new
121
+ res = RJR::Result.new
122
+ stat = RJR::DispatcherStat.new req, res
123
+
124
+ RJR::DispatcherStat << stat
125
+ RJR::DispatcherStat.stats.should include(stat)
126
+ end
127
+
128
+ it "should be convertable to json" do
129
+ req = RJR::Request.new :method => 'foobar', :method_args => [:a, :b],
130
+ :headers => { :foo => :bar }, :rjr_node_type => :local,
131
+ :rjr_node_id => :loc1
132
+ res = RJR::Result.new :result => 42
133
+
134
+ stat = RJR::DispatcherStat.new req, res
135
+ j = stat.to_json()
136
+ j.should include('"json_class":"RJR::DispatcherStat"')
137
+ j.should include('"method":"foobar"')
138
+ j.should include('"method_args":["a","b"]')
139
+ j.should include('"headers":{"foo":"bar"}')
140
+ j.should include('"rjr_node_type":"local"')
141
+ j.should include('"rjr_node_id":"loc1"')
142
+ j.should include('"result":42')
143
+ end
144
+
145
+ it "should be convertable from json" do
146
+ j = '{"json_class":"RJR::DispatcherStat","data":{"request":{"method":"foobar","method_args":["a","b"],"headers":{"foo":"bar"},"rjr_node_type":"local","rjr_node_id":"loc1"},"result":{"result":42,"error_code":null,"error_msg":null,"error_class":null}}}'
147
+ s = JSON.parse(j)
148
+
149
+ s.class.should == RJR::DispatcherStat
150
+ s.request.method.should == 'foobar'
151
+ s.request.method_args.should == ['a', 'b']
152
+ s.request.headers.should == { 'foo' => 'bar' }
153
+ s.request.rjr_node_type.should == 'local'
154
+ s.request.rjr_node_id.should == 'loc1'
155
+ s.result.result.should == 42
156
+ end
157
+ end
158
+
93
159
  describe RJR::Dispatcher do
94
160
  it "should dispatch request to registered handler" do
95
161
  invoked_foobar = false
@@ -111,6 +177,21 @@ describe RJR::Dispatcher do
111
177
  invoked_barfoo.should == false
112
178
  end
113
179
 
180
+ it "should allow user to determine registered handlers" do
181
+ foobar = lambda {}
182
+ barfoo = lambda {}
183
+ RJR::Dispatcher.add_handler('foobar', &foobar)
184
+ RJR::Dispatcher.add_handler('barfoo', &barfoo)
185
+
186
+ RJR::Dispatcher.has_handler_for?('foobar').should be_true
187
+ RJR::Dispatcher.has_handler_for?('barfoo').should be_true
188
+ RJR::Dispatcher.has_handler_for?('money').should be_false
189
+
190
+ RJR::Dispatcher.handler_for('foobar').handler_proc.should == foobar
191
+ RJR::Dispatcher.handler_for('barfoo').handler_proc.should == barfoo
192
+ RJR::Dispatcher.handler_for('money').should be_nil
193
+ end
194
+
114
195
  it "should allow a single handler to be subscribed to multiple methods" do
115
196
  invoked_handler = 0
116
197
  RJR::Dispatcher.init_handlers
@@ -0,0 +1,85 @@
1
+ require 'rjr/dispatcher'
2
+ require 'rjr/em_adapter'
3
+
4
+ describe EMManager do
5
+ it "should start and halt the reactor thread" do
6
+ manager = EMManager.new
7
+ manager.start
8
+ EventMachine.reactor_running?.should be_true
9
+ manager.running?.should be_true
10
+ manager.instance_variable_get(:@reactor_thread).should_not be_nil
11
+ rt = manager.instance_variable_get(:@reactor_thread)
12
+ ['sleep', 'run'].should include(manager.instance_variable_get(:@reactor_thread).status)
13
+
14
+ manager.start
15
+ rt2 = manager.instance_variable_get(:@reactor_thread)
16
+ rt.should == rt2
17
+
18
+ manager.halt
19
+ manager.join
20
+ EventMachine.reactor_running?.should be_false
21
+ manager.running?.should be_false
22
+ manager.instance_variable_get(:@reactor_thread).should be_nil
23
+ end
24
+
25
+ it "should allow the user to schedule jobs" do
26
+ manager = EMManager.new
27
+ manager.start
28
+ manager.running?.should be_true
29
+ block_ran = false
30
+ manager.schedule {
31
+ block_ran = true
32
+ }
33
+ sleep 0.5
34
+ block_ran.should == true
35
+
36
+ manager.halt
37
+ manager.join
38
+ manager.running?.should be_false
39
+ end
40
+
41
+ it "should allow the user to keep the reactor alive until forcibly stopped" do
42
+ manager = EMManager.new
43
+ manager.start
44
+ manager.running?.should be_true
45
+ manager.schedule { "foo" }
46
+
47
+ manager.running?.should be_true
48
+
49
+ # forcibly stop the reactor
50
+ manager.halt
51
+ manager.join
52
+ manager.running?.should be_false
53
+ end
54
+
55
+ it "should allow the user to schedule at job after a specified interval" do
56
+ manager = EMManager.new
57
+ manager.start
58
+ manager.running?.should be_true
59
+ block_called = false
60
+ manager.add_timer(1) { block_called = true }
61
+ sleep 0.5
62
+ block_called.should == false
63
+ sleep 1
64
+ block_called.should == true
65
+ manager.halt
66
+ manager.join
67
+ end
68
+
69
+ it "should allow the user to schedule at job repeatidly with a specified interval" do
70
+ manager = EMManager.new
71
+ manager.start
72
+ manager.running?.should be_true
73
+ times_block_called = 0
74
+ manager.add_periodic_timer(1) { times_block_called += 1 }
75
+ sleep 0.6
76
+ times_block_called.should == 0
77
+ sleep 0.6
78
+ times_block_called.should == 1
79
+ sleep 1.2
80
+ times_block_called.should == 2
81
+
82
+ manager.halt
83
+ manager.join
84
+ end
85
+ end
@@ -0,0 +1,60 @@
1
+ require 'rjr/inspect'
2
+
3
+ describe 'rjr/inspect.rb' do
4
+ describe "select_stats" do
5
+ before(:each) do
6
+ RJR::DispatcherStat.reset
7
+
8
+ req1 = RJR::Request.new :rjr_node_type => :local, :method => 'foobar'
9
+ req2 = RJR::Request.new :rjr_node_type => :tcp, :method => 'barfoo'
10
+ req3 = RJR::Request.new :rjr_node_type => :amqp, :method => 'foobar'
11
+ res1 = RJR::Result.new :result => true
12
+ res2 = RJR::Result.new :error_code => 123
13
+ res3 = RJR::Result.new :error_code => 123
14
+
15
+ @stat1 = RJR::DispatcherStat.new req1, res1
16
+ @stat2 = RJR::DispatcherStat.new req2, res2
17
+ @stat3 = RJR::DispatcherStat.new req3, res3
18
+ RJR::DispatcherStat << @stat1 << @stat2 << @stat3
19
+ end
20
+
21
+ it "should get all dispatcherstats" do
22
+ stats = select_stats
23
+ stats.size.should == 3
24
+ stats.first.should == @stat1
25
+ stats[1].should == @stat2
26
+ stats.last.should == @stat3
27
+ end
28
+
29
+ it "should get all dispatcherstats for a node" do
30
+ stats = select_stats 'on_node', 'local'
31
+ stats.size.should == 1
32
+ stats.first.should == @stat1
33
+ end
34
+
35
+ it "should get all dispatcherstats for a method" do
36
+ stats = select_stats 'for_method', 'foobar'
37
+ stats.size.should == 2
38
+ stats.first.should == @stat1
39
+ stats.last.should == @stat3
40
+ end
41
+
42
+ it "should get all successfull/failed dispatcherstats" do
43
+ stats = select_stats 'successful'
44
+ stats.size.should == 1
45
+ stats.first.should == @stat1
46
+
47
+ stats = select_stats 'failed'
48
+ stats.size.should == 2
49
+ stats.first.should == @stat2
50
+ stats.last.should == @stat3
51
+ end
52
+
53
+ it "should get dispatcher stats meeting multiple criteria" do
54
+ stats = select_stats 'for_method', 'foobar', 'successful'
55
+ stats.size.should == 1
56
+ stats.first.should == @stat1
57
+ end
58
+ end
59
+
60
+ end
@@ -143,3 +143,61 @@ describe RJR::ResponseMessage do
143
143
  }.should raise_error JSON::ParserError
144
144
  end
145
145
  end
146
+
147
+ describe RJR::NotificationMessage do
148
+ it "should accept notification parameters" do
149
+ msg = RJR::NotificationMessage.new :method => 'test',
150
+ :args => ['a', 1],
151
+ :headers => {:h => 2}
152
+ msg.jr_method.should == "test"
153
+ msg.jr_args.should =~ ['a', 1]
154
+ msg.headers.should have_key(:h)
155
+ msg.headers[:h].should == 2
156
+ end
157
+
158
+ it "should produce valid json" do
159
+ msg = RJR::NotificationMessage.new :method => 'test',
160
+ :args => ['a', 1],
161
+ :headers => {:h => 2}
162
+
163
+ msg_string = msg.to_s
164
+ msg_string.should include('"h":2')
165
+ msg_string.should include('"method":"test"')
166
+ msg_string.should include('"params":["a",1]')
167
+ msg_string.should include('"jsonrpc":"2.0"')
168
+ msg_string.should_not include('"id":"')
169
+ end
170
+
171
+ it "should parse notification message string" do
172
+ msg_string = '{"jsonrpc": "2.0", ' +
173
+ '"method": "test", "params": ["a", 1]}'
174
+ msg = RJR::NotificationMessage.new :message => msg_string
175
+ msg.json_message.should == msg_string
176
+ msg.jr_method.should == 'test'
177
+ msg.jr_args.should =~ ['a', 1]
178
+ end
179
+
180
+ it "should extract optional headers from message string" do
181
+ msg_string = '{"jsonrpc": "2.0", ' +
182
+ '"method": "test", "params": ["a", 1], ' +
183
+ '"h": 2}'
184
+ msg = RJR::NotificationMessage.new :message => msg_string, :headers => {'f' => 'g'}
185
+ msg.json_message.should == msg_string
186
+ msg.headers.should have_key 'h'
187
+ msg.headers.should have_key 'f'
188
+ msg.headers['h'].should == 2
189
+ msg.headers['f'].should == 'g'
190
+ end
191
+
192
+ it "should fail if parsing invalid message string" do
193
+ lambda {
194
+ msg = RJR::NotificationMessage.new :message => 'foobar'
195
+ }.should raise_error JSON::ParserError
196
+ end
197
+ end
198
+
199
+ describe RJR::MessageUtil do
200
+ it "should extract json messages out of a message stream" do
201
+ # TODO!
202
+ end
203
+ end
@@ -3,8 +3,8 @@ require 'rjr/amqp_node'
3
3
  require 'rjr/web_node'
4
4
  require 'rjr/dispatcher'
5
5
 
6
- describe RJR::AMQPNode do
7
- it "should invoke and satisfy amqp requests" do
6
+ describe RJR::MultiNode do
7
+ it "should invoke and satisfy requests over multiple protocols" do
8
8
  foolbar_invoked = false
9
9
  barfoo_invoked = false
10
10
  RJR::Dispatcher.init_handlers
@@ -25,10 +25,11 @@ describe RJR::AMQPNode do
25
25
 
26
26
  amqp = RJR::AMQPNode.new :node_id => 'amqp', :broker => 'localhost'
27
27
  web = RJR::WebNode.new :node_id => 'web', :host => 'localhost', :port => 9876
28
- multi = RJR::MultiNode.new :node_id => 'multi', :nodes => [amqp, web]
28
+ multi = RJR::MultiNode.new :node_id => 'multi', :nodes => [amqp, web], :keep_alive => true
29
29
 
30
30
  multi.listen
31
- amqp_client = RJR::AMQPNode.new :node_id => 'client', :broker => 'localhost'
31
+
32
+ amqp_client = RJR::AMQPNode.new :node_id => 'client', :broker => 'localhost', :keep_alive => true # see comment about keepalive in amqp_node_spec
32
33
  res = amqp_client.invoke_request 'amqp-queue', 'foolbar', 'myparam1'
33
34
  res.should == 'retval1'
34
35