skein 0.3.7 → 0.8.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/Gemfile +4 -1
- data/Gemfile.lock +49 -36
- data/LICENSE.md +1 -1
- data/README.md +9 -4
- data/RELEASES.md +2 -0
- data/VERSION +1 -1
- data/bin/skein +1 -1
- data/examples/echo +66 -0
- data/examples/echo-server +77 -0
- data/lib/skein/adapter.rb +3 -0
- data/lib/skein/client/publisher.rb +10 -2
- data/lib/skein/client/rpc.rb +78 -19
- data/lib/skein/client/subscriber.rb +27 -4
- data/lib/skein/client/worker.rb +221 -63
- data/lib/skein/client.rb +20 -11
- data/lib/skein/config.rb +3 -1
- data/lib/skein/connected.rb +88 -17
- data/lib/skein/context.rb +5 -2
- data/lib/skein/handler/async.rb +6 -2
- data/lib/skein/handler.rb +131 -20
- data/lib/skein/rabbitmq.rb +1 -0
- data/lib/skein/timeout_queue.rb +43 -0
- data/lib/skein.rb +18 -2
- data/skein.gemspec +21 -24
- data/test/helper.rb +29 -0
- data/test/unit/test_skein_client.rb +4 -1
- data/test/unit/test_skein_client_publisher.rb +1 -1
- data/test/unit/test_skein_client_rpc.rb +37 -0
- data/test/unit/test_skein_client_subscriber.rb +29 -12
- data/test/unit/test_skein_client_worker.rb +22 -9
- data/test/unit/test_skein_connected.rb +21 -0
- data/test/unit/test_skein_rpc_timeout.rb +19 -0
- data/test/unit/test_skein_worker.rb +4 -0
- metadata +41 -16
- data/lib/skein/rpc/base.rb +0 -23
- data/lib/skein/rpc/error.rb +0 -34
- data/lib/skein/rpc/notification.rb +0 -2
- data/lib/skein/rpc/request.rb +0 -62
- data/lib/skein/rpc/response.rb +0 -38
- data/lib/skein/rpc.rb +0 -24
- data/test/unit/test_skein_rpc_error.rb +0 -10
- data/test/unit/test_skein_rpc_request.rb +0 -93
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 1f9116f29d28ed7c32ceda7ae576d7339b5d10d25232b15d785468823622e2b1
|
4
|
+
data.tar.gz: 186e7299d4e36079bd527dd6829ec5cb882ba26447be7ec126605fde7c6c8de9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2d0229f1b32971be57e3b1164af63c3fdbfbb46cc29bd277ad7a95341e880d32a8056f7a917b5178ddf5319d58c7856f9c599475f5508cd1da27c02b6f6e5744
|
7
|
+
data.tar.gz: c960a2492da91dc3595658a48e86cae0ac9e627c15d156fbbcf615a24c2d49ea14e5b01512748b50bece8c06013686c680234a25bd2d11952c66aa6d528b16cb
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,57 +1,68 @@
|
|
1
1
|
GEM
|
2
2
|
remote: https://rubygems.org/
|
3
3
|
specs:
|
4
|
-
addressable (2.
|
5
|
-
|
6
|
-
birling (0.1
|
7
|
-
builder (3.2.
|
4
|
+
addressable (2.4.0)
|
5
|
+
amq-protocol (2.3.2)
|
6
|
+
birling (0.3.1)
|
7
|
+
builder (3.2.4)
|
8
|
+
bunny (2.19.0)
|
9
|
+
amq-protocol (~> 2.3, >= 2.3.1)
|
10
|
+
sorted_set (~> 1, >= 1.0.2)
|
8
11
|
descendants_tracker (0.0.4)
|
9
12
|
thread_safe (~> 0.3, >= 0.3.1)
|
10
13
|
faraday (0.9.2)
|
11
14
|
multipart-post (>= 1.2, < 3)
|
12
|
-
git (1.
|
13
|
-
|
14
|
-
|
15
|
-
|
15
|
+
git (1.9.1)
|
16
|
+
rchardet (~> 1.8)
|
17
|
+
github_api (0.16.0)
|
18
|
+
addressable (~> 2.4.0)
|
19
|
+
descendants_tracker (~> 0.0.4)
|
16
20
|
faraday (~> 0.8, < 0.10)
|
17
|
-
hashie (>=
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
jeweler (2.3.3)
|
21
|
+
hashie (>= 3.4)
|
22
|
+
mime-types (>= 1.16, < 3.0)
|
23
|
+
oauth2 (~> 1.0)
|
24
|
+
hashie (4.1.0)
|
25
|
+
highline (2.0.3)
|
26
|
+
jeweler (2.3.9)
|
24
27
|
builder
|
25
|
-
bundler
|
28
|
+
bundler
|
26
29
|
git (>= 1.2.5)
|
27
|
-
github_api (~> 0.
|
30
|
+
github_api (~> 0.16.0)
|
28
31
|
highline (>= 1.6.15)
|
29
32
|
nokogiri (>= 1.5.10)
|
30
|
-
psych
|
33
|
+
psych
|
31
34
|
rake
|
32
35
|
rdoc
|
33
36
|
semver2
|
34
|
-
jwt (
|
35
|
-
|
36
|
-
|
37
|
+
jwt (2.2.3)
|
38
|
+
mime-types (2.99.3)
|
39
|
+
mini_portile2 (2.6.1)
|
40
|
+
multi_json (1.15.0)
|
37
41
|
multi_xml (0.6.0)
|
38
|
-
multipart-post (2.
|
39
|
-
nokogiri (1.
|
40
|
-
mini_portile2 (~> 2.1
|
41
|
-
|
42
|
-
|
43
|
-
|
42
|
+
multipart-post (2.1.1)
|
43
|
+
nokogiri (1.12.4)
|
44
|
+
mini_portile2 (~> 2.6.1)
|
45
|
+
racc (~> 1.4)
|
46
|
+
oauth2 (1.4.7)
|
47
|
+
faraday (>= 0.8, < 2.0)
|
48
|
+
jwt (>= 1.0, < 3.0)
|
44
49
|
multi_json (~> 1.3)
|
45
50
|
multi_xml (~> 0.5)
|
46
51
|
rack (>= 1.2, < 3)
|
47
|
-
power_assert (
|
48
|
-
psych (
|
49
|
-
|
50
|
-
rack (2.
|
51
|
-
rake (
|
52
|
-
|
52
|
+
power_assert (2.0.1)
|
53
|
+
psych (4.0.1)
|
54
|
+
racc (1.5.2)
|
55
|
+
rack (2.2.3)
|
56
|
+
rake (13.0.6)
|
57
|
+
rbtree (0.4.4)
|
58
|
+
rchardet (1.8.0)
|
59
|
+
rdoc (6.3.2)
|
53
60
|
semver2 (3.4.2)
|
54
|
-
|
61
|
+
set (1.0.1)
|
62
|
+
sorted_set (1.0.3)
|
63
|
+
rbtree
|
64
|
+
set (~> 1.0)
|
65
|
+
test-unit (3.4.7)
|
55
66
|
power_assert
|
56
67
|
thread_safe (0.3.6)
|
57
68
|
|
@@ -59,10 +70,12 @@ PLATFORMS
|
|
59
70
|
ruby
|
60
71
|
|
61
72
|
DEPENDENCIES
|
62
|
-
birling
|
73
|
+
birling (>= 0.2.0)
|
74
|
+
bunny
|
63
75
|
jeweler
|
76
|
+
march_hare
|
64
77
|
rake
|
65
78
|
test-unit
|
66
79
|
|
67
80
|
BUNDLED WITH
|
68
|
-
|
81
|
+
2.2.27
|
data/LICENSE.md
CHANGED
data/README.md
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
# Skein
|
2
2
|
|
3
|
-
[Skein](https://en.wikipedia.org/wiki/V_formation) is a RabbitMQ-based
|
4
|
-
|
5
|
-
[JSON-RPC](http://json-rpc.org)
|
3
|
+
[Skein](https://en.wikipedia.org/wiki/V_formation) is a RabbitMQ-based method
|
4
|
+
for handling remote procedure calls (RPC) and pub/sub channels over AMQP using
|
5
|
+
[JSON-RPC](http://json-rpc.org) payloads.
|
6
6
|
|
7
7
|
## Dependencies
|
8
8
|
|
@@ -10,7 +10,7 @@ This library requires an active AMQP server like [RabbitMQ](http://rabbitmq.com)
|
|
10
10
|
and a Ruby driver for AMQP like [Bunny](http://rubybunny.info) or
|
11
11
|
[March Hare](http://rubymarchhare.info).
|
12
12
|
|
13
|
-
Both
|
13
|
+
Both JRuby and MRI Ruby are supported with the appropriate driver.
|
14
14
|
|
15
15
|
## Installation
|
16
16
|
|
@@ -55,3 +55,8 @@ a `Skein::Client::Worker` instance:
|
|
55
55
|
end
|
56
56
|
|
57
57
|
Responder.new('test_queue')
|
58
|
+
|
59
|
+
## Debugging
|
60
|
+
|
61
|
+
Setting the environment variable `SKEIN_DEBUG_JSON` will show raw JSON
|
62
|
+
payloads received by both RPC workers and clients.
|
data/RELEASES.md
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
* 0.7.1 - Allow setting of default Skein.config configuration
|
2
|
+
* 0.7.0 - Automatic recovering from closed RabbitMQ channels
|
1
3
|
* 0.3.0 - Adding EventMachine compatibility
|
2
4
|
* 0.2.1 - Cleaning up RPC interface with blocking vs. non-blocking
|
3
5
|
* 0.2.0 - First working version in JRuby/MRI Ruby
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.8.1
|
data/bin/skein
CHANGED
data/examples/echo
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# == Imports ================================================================
|
4
|
+
|
5
|
+
require 'optparse'
|
6
|
+
require 'securerandom'
|
7
|
+
require 'bundler/setup'
|
8
|
+
|
9
|
+
Bundler.require(:default)
|
10
|
+
|
11
|
+
require_relative '../lib/skein'
|
12
|
+
|
13
|
+
# == Support Classes ========================================================
|
14
|
+
|
15
|
+
# == Main ===================================================================
|
16
|
+
|
17
|
+
config = Skein::Config.new
|
18
|
+
|
19
|
+
config.queue = 'skein-echo-test'
|
20
|
+
|
21
|
+
options = {
|
22
|
+
repeat: false
|
23
|
+
}
|
24
|
+
|
25
|
+
program = OptionParser.new do |opts|
|
26
|
+
opts.on('-q', '--queue NAME', 'Queue to listen on') do |s|
|
27
|
+
config.queue = s
|
28
|
+
end
|
29
|
+
opts.on('-e', '--exchange NAME', 'Exchange to attach to') do |s|
|
30
|
+
config.exchange = s
|
31
|
+
end
|
32
|
+
opts.on('-u', '--username NAME', 'Authenticate with username') do |s|
|
33
|
+
config.username = s
|
34
|
+
end
|
35
|
+
opts.on('-p', '--password NAME', 'Authenticate with password') do |s|
|
36
|
+
config.password = s
|
37
|
+
end
|
38
|
+
opts.on('-H', '--host NAME', 'Connect to RabbitMQ host') do |s|
|
39
|
+
config.host = s
|
40
|
+
end
|
41
|
+
opts.on('-P', '--port NAME', 'Connect to RabbitMQ port') do |s|
|
42
|
+
config.port = s.to_i
|
43
|
+
end
|
44
|
+
opts.on('-r', '--repeat', 'Repeat echo') do |s|
|
45
|
+
options[:repeat] = true
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
program.parse!(ARGV)
|
50
|
+
|
51
|
+
client = Skein::Client.new(
|
52
|
+
connection: Skein::RabbitMQ.connect(config)
|
53
|
+
)
|
54
|
+
|
55
|
+
rpc = client.rpc(config.exchange, routing_key: config.queue)
|
56
|
+
threads = Hash.new { |h,k| h[k] = h.length }
|
57
|
+
|
58
|
+
loop do
|
59
|
+
puts rpc.echo(SecureRandom.uuid)
|
60
|
+
|
61
|
+
if (options[:repeat])
|
62
|
+
sleep(1)
|
63
|
+
else
|
64
|
+
break
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# == Imports ================================================================
|
4
|
+
|
5
|
+
require 'optparse'
|
6
|
+
require 'bundler/setup'
|
7
|
+
|
8
|
+
Bundler.require(:default)
|
9
|
+
|
10
|
+
require_relative '../lib/skein'
|
11
|
+
|
12
|
+
# == Support Classes ========================================================
|
13
|
+
|
14
|
+
class EchoWorker < Skein::Client::Worker
|
15
|
+
def echo(*args)
|
16
|
+
puts "Received: #{args.inspect}"
|
17
|
+
|
18
|
+
args
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# == Main ===================================================================
|
23
|
+
|
24
|
+
config = Skein::Config.new
|
25
|
+
|
26
|
+
config.queue = 'skein-echo-test'
|
27
|
+
config.concurrency = 1
|
28
|
+
|
29
|
+
program = OptionParser.new do |opts|
|
30
|
+
opts.on('-q', '--queue NAME', 'Queue to listen on') do |s|
|
31
|
+
config.queue = s
|
32
|
+
end
|
33
|
+
opts.on('-e', '--exchange NAME', 'Exchange to attach to') do |s|
|
34
|
+
config.exchange = s
|
35
|
+
end
|
36
|
+
opts.on('-u', '--username NAME', 'Authenticate with username') do |s|
|
37
|
+
config.username = s
|
38
|
+
end
|
39
|
+
opts.on('-p', '--password NAME', 'Authenticate with password') do |s|
|
40
|
+
config.password = s
|
41
|
+
end
|
42
|
+
opts.on('-H', '--host NAME', 'Connect to RabbitMQ host') do |s|
|
43
|
+
config.host = s
|
44
|
+
end
|
45
|
+
opts.on('-P', '--port NAME', 'Connect to RabbitMQ port') do |s|
|
46
|
+
config.port = s.to_i
|
47
|
+
end
|
48
|
+
opts.on('-w', '--workers COUNT', 'Set workers count') do |s|
|
49
|
+
config.concurrency = s.to_i
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
program.parse!(ARGV)
|
54
|
+
|
55
|
+
worker = EchoWorker.new(
|
56
|
+
config.queue,
|
57
|
+
exchange_name: config.exchange,
|
58
|
+
concurrency: config.concurrency,
|
59
|
+
connection: Skein::RabbitMQ.connect(config)
|
60
|
+
)
|
61
|
+
|
62
|
+
puts "EchoWorker active on #{config.queue}"
|
63
|
+
|
64
|
+
Signal.trap('INT') do |signal|
|
65
|
+
# Force quit immediately, don't wait on threads
|
66
|
+
Process.exit!(0)
|
67
|
+
end
|
68
|
+
|
69
|
+
Signal.trap('QUIT') do |signal|
|
70
|
+
puts "\r Threads: #{Thread.list.length}"
|
71
|
+
Thread.list.each do |thread|
|
72
|
+
puts '#%s %s' % [ thread.object_id, thread.name ]
|
73
|
+
puts thread.backtrace.join("\n")
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
worker.join
|
data/lib/skein/adapter.rb
CHANGED
@@ -1,6 +1,9 @@
|
|
1
1
|
module Skein::Adapter
|
2
2
|
# == Mixin Methods =========================================================
|
3
3
|
|
4
|
+
# REFACTOR: This should be converted into a proper subclass of the
|
5
|
+
# various drivers that does the method re-writing at a lower level.
|
6
|
+
|
4
7
|
def subscribe(queue, block: true, manual_ack: true)
|
5
8
|
case (queue.class.to_s.split(/::/)[0])
|
6
9
|
when 'Bunny'
|
@@ -1,14 +1,22 @@
|
|
1
1
|
class Skein::Client::Publisher < Skein::Connected
|
2
2
|
# == Instance Methods =====================================================
|
3
3
|
|
4
|
-
def initialize(
|
4
|
+
def initialize(exchange_name, type: nil, durable: nil, connection: nil, context: nil)
|
5
5
|
super(connection: connection, context: context)
|
6
6
|
|
7
|
-
@queue = self.channel.topic
|
7
|
+
@queue = self.channel.send(type || :topic, exchange_name, durable: durable)
|
8
8
|
end
|
9
9
|
|
10
10
|
def publish!(message, routing_key = nil)
|
11
11
|
@queue.publish(JSON.dump(message), routing_key: routing_key)
|
12
12
|
end
|
13
13
|
alias_method :<<, :publish!
|
14
|
+
|
15
|
+
def close(delete_queue: false)
|
16
|
+
if (delete_queue)
|
17
|
+
@queue.delete
|
18
|
+
end
|
19
|
+
|
20
|
+
super()
|
21
|
+
end
|
14
22
|
end
|
data/lib/skein/client/rpc.rb
CHANGED
@@ -2,6 +2,11 @@ require 'securerandom'
|
|
2
2
|
require 'fiber'
|
3
3
|
|
4
4
|
class Skein::Client::RPC < Skein::Connected
|
5
|
+
# == Exceptions ===========================================================
|
6
|
+
|
7
|
+
class RPCException < Exception
|
8
|
+
end
|
9
|
+
|
5
10
|
# == Constants ============================================================
|
6
11
|
|
7
12
|
EXCHANGE_NAME_DEFAULT = ''.freeze
|
@@ -10,30 +15,70 @@ class Skein::Client::RPC < Skein::Connected
|
|
10
15
|
|
11
16
|
# == Instance Methods =====================================================
|
12
17
|
|
13
|
-
def initialize(exchange_name = nil, routing_key: nil, connection: nil, context: nil)
|
14
|
-
super(connection: connection, context: context)
|
18
|
+
def initialize(exchange_name = nil, routing_key: nil, connection: nil, context: nil, ident: nil, expiration: nil, persistent: true, durable: true, timeout: nil)
|
19
|
+
super(connection: connection, context: context, ident: ident)
|
15
20
|
|
16
|
-
@rpc_exchange = self.channel.direct(exchange_name || EXCHANGE_NAME_DEFAULT, durable: true)
|
17
21
|
@routing_key = routing_key
|
22
|
+
@timeout = timeout
|
23
|
+
|
24
|
+
@rpc_exchange = self.channel.direct(
|
25
|
+
exchange_name || EXCHANGE_NAME_DEFAULT,
|
26
|
+
durable: durable
|
27
|
+
)
|
28
|
+
|
18
29
|
@response_queue = self.channel.queue(
|
19
30
|
@ident,
|
20
31
|
durable: false,
|
21
32
|
header: true,
|
22
33
|
auto_delete: true
|
23
34
|
)
|
35
|
+
@expiration = expiration
|
36
|
+
@persistent = !!persistent
|
24
37
|
|
25
38
|
@callback = { }
|
26
39
|
|
27
40
|
@consumer = Skein::Adapter.subscribe(@response_queue, block: false) do |payload, delivery_tag, reply_to|
|
28
41
|
self.context.trap do
|
42
|
+
if (ENV['SKEIN_DEBUG_JSON'])
|
43
|
+
$stdout.puts(payload)
|
44
|
+
end
|
45
|
+
|
29
46
|
response = JSON.load(payload)
|
30
47
|
|
31
48
|
if (callback = @callback.delete(response['id']))
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
49
|
+
if (response['error'])
|
50
|
+
exception =
|
51
|
+
case (response['error'] and response['error']['code'])
|
52
|
+
when -32601
|
53
|
+
NoMethodError.new(
|
54
|
+
"%s from `%s' RPC call" % [
|
55
|
+
response.dig('error', 'message'),
|
56
|
+
response.dig('error', 'data', 'method')
|
57
|
+
]
|
58
|
+
)
|
59
|
+
when -32602
|
60
|
+
ArgumentError.new(
|
61
|
+
response.dig('error', 'data', 'message') || 'wrong number of arguments'
|
62
|
+
)
|
63
|
+
else
|
64
|
+
RPCException.new(
|
65
|
+
response.dig('error', 'data', 'message') || response.dig('error', 'message')
|
66
|
+
)
|
67
|
+
end
|
68
|
+
|
69
|
+
case (callback)
|
70
|
+
when Skein::TimeoutQueue
|
71
|
+
callback << exception
|
72
|
+
when Proc
|
73
|
+
callback.call(exception)
|
74
|
+
end
|
75
|
+
else
|
76
|
+
case (callback)
|
77
|
+
when Skein::TimeoutQueue
|
78
|
+
callback << response['result']
|
79
|
+
when Proc
|
80
|
+
callback.call(response['result'])
|
81
|
+
end
|
37
82
|
end
|
38
83
|
end
|
39
84
|
|
@@ -42,20 +87,31 @@ class Skein::Client::RPC < Skein::Connected
|
|
42
87
|
end
|
43
88
|
end
|
44
89
|
|
90
|
+
# Temporarily deliver RPC calls to a different routing key. The supplied
|
91
|
+
# block is executed with this temporary routing in effect.
|
92
|
+
def reroute!(routing_key)
|
93
|
+
routing_key, @routing_key = @routing_key, routing_key
|
94
|
+
|
95
|
+
yield if (block_given?)
|
96
|
+
|
97
|
+
@routing_key = routing_key
|
98
|
+
end
|
99
|
+
|
45
100
|
def close
|
46
|
-
@consumer
|
101
|
+
@consumer&.cancel
|
47
102
|
@consumer = nil
|
48
103
|
|
49
104
|
super
|
50
105
|
end
|
51
106
|
|
52
|
-
def method_missing(name, *args)
|
107
|
+
def method_missing(name, *args, &block)
|
53
108
|
name = name.to_s
|
54
109
|
|
55
110
|
blocking = !name.sub!(/!\z/, '')
|
56
111
|
|
57
112
|
message_id = SecureRandom.uuid
|
58
113
|
request = JSON.dump(
|
114
|
+
jsonrpc: '2.0',
|
59
115
|
method: name,
|
60
116
|
params: args,
|
61
117
|
id: message_id
|
@@ -66,26 +122,29 @@ class Skein::Client::RPC < Skein::Connected
|
|
66
122
|
routing_key: @routing_key,
|
67
123
|
reply_to: blocking ? @ident : nil,
|
68
124
|
content_type: 'application/json',
|
69
|
-
message_id: message_id
|
125
|
+
message_id: message_id,
|
126
|
+
persistent: @persistent,
|
127
|
+
expiration: @expiration
|
70
128
|
)
|
71
129
|
|
72
130
|
if (block_given?)
|
73
131
|
@callback[message_id] =
|
74
132
|
if (defined?(EventMachine))
|
75
|
-
EventMachine.next_tick
|
76
|
-
yield
|
77
|
-
end
|
133
|
+
EventMachine.next_tick(&block)
|
78
134
|
else
|
79
|
-
|
80
|
-
yield
|
81
|
-
end
|
135
|
+
block
|
82
136
|
end
|
83
137
|
elsif (blocking)
|
84
|
-
queue =
|
138
|
+
queue = Skein::TimeoutQueue.new(blocking: true, timeout: @timeout)
|
85
139
|
|
86
140
|
@callback[message_id] = queue
|
87
141
|
|
88
|
-
queue.pop
|
142
|
+
case (result = queue.pop)
|
143
|
+
when Exception
|
144
|
+
raise result
|
145
|
+
else
|
146
|
+
result
|
147
|
+
end
|
89
148
|
end
|
90
149
|
end
|
91
150
|
end
|