liebre 0.1.21 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rspec +2 -0
- data/Gemfile.lock +9 -7
- data/{LICENSE → LICENSE.txt} +1 -1
- data/README.md +492 -195
- data/Rakefile +2 -0
- data/lib/liebre.rb +27 -16
- data/lib/liebre/actor.rb +11 -0
- data/lib/liebre/actor/consumer.rb +80 -0
- data/lib/liebre/actor/consumer/callback.rb +34 -0
- data/lib/liebre/actor/consumer/core.rb +80 -0
- data/lib/liebre/actor/consumer/reporter.rb +84 -0
- data/lib/liebre/actor/consumer/resources.rb +47 -0
- data/lib/liebre/actor/consumer/resources/config.rb +65 -0
- data/lib/liebre/actor/context.rb +40 -0
- data/lib/liebre/actor/context/declare.rb +44 -0
- data/lib/liebre/actor/context/handler.rb +44 -0
- data/lib/liebre/actor/publisher.rb +58 -0
- data/lib/liebre/actor/publisher/core.rb +42 -0
- data/lib/liebre/actor/publisher/reporter.rb +55 -0
- data/lib/liebre/actor/publisher/resources.rb +33 -0
- data/lib/liebre/actor/rpc/client.rb +88 -0
- data/lib/liebre/actor/rpc/client/core.rb +75 -0
- data/lib/liebre/actor/rpc/client/pending.rb +65 -0
- data/lib/liebre/actor/rpc/client/reporter.rb +71 -0
- data/lib/liebre/actor/rpc/client/resources.rb +62 -0
- data/lib/liebre/actor/rpc/client/task.rb +33 -0
- data/lib/liebre/actor/rpc/server.rb +74 -0
- data/lib/liebre/actor/rpc/server/callback.rb +28 -0
- data/lib/liebre/actor/rpc/server/core.rb +75 -0
- data/lib/liebre/actor/rpc/server/reporter.rb +72 -0
- data/lib/liebre/actor/rpc/server/resources.rb +53 -0
- data/lib/liebre/adapter.rb +8 -0
- data/lib/liebre/adapter/bunny.rb +23 -0
- data/lib/liebre/adapter/bunny/chan.rb +38 -0
- data/lib/liebre/adapter/bunny/conn.rb +32 -0
- data/lib/liebre/adapter/bunny/exchange.rb +20 -0
- data/lib/liebre/adapter/bunny/queue.rb +59 -0
- data/lib/liebre/adapter/interface.rb +26 -0
- data/lib/liebre/adapter/interface/chan.rb +29 -0
- data/lib/liebre/adapter/interface/conn.rb +21 -0
- data/lib/liebre/adapter/interface/exchange.rb +13 -0
- data/lib/liebre/adapter/interface/queue.rb +37 -0
- data/lib/liebre/bridge.rb +72 -0
- data/lib/liebre/bridge/channel_builder.rb +36 -0
- data/lib/liebre/config.rb +8 -38
- data/lib/liebre/engine.rb +61 -0
- data/lib/liebre/engine/builder.rb +48 -0
- data/lib/liebre/engine/repository.rb +56 -0
- data/lib/liebre/engine/state.rb +49 -0
- data/lib/liebre/runner.rb +15 -47
- data/lib/liebre/version.rb +1 -1
- data/liebre.gemspec +9 -7
- data/spec/integration/publish_and_consume_spec.rb +71 -0
- data/spec/integration/rpc_communication_spec.rb +81 -0
- data/spec/integration/start_twice_spec.rb +63 -0
- data/spec/liebre/actor/consumer_spec.rb +169 -0
- data/spec/liebre/actor/context/declare_spec.rb +69 -0
- data/spec/liebre/actor/context/handler_spec.rb +65 -0
- data/spec/liebre/actor/publisher_spec.rb +58 -0
- data/spec/liebre/actor/rpc/client_spec.rb +126 -0
- data/spec/liebre/actor/rpc/server_spec.rb +141 -0
- data/spec/liebre/adapter/bunny_spec.rb +66 -0
- data/spec/liebre/bridge_spec.rb +54 -0
- data/spec/liebre/engine/builder_spec.rb +42 -0
- data/spec/liebre/engine_spec.rb +90 -0
- data/spec/liebre/version_spec.rb +10 -0
- data/spec/spec_helper.rb +2 -9
- metadata +97 -58
- data/lib/liebre/common.rb +0 -7
- data/lib/liebre/common/utils.rb +0 -37
- data/lib/liebre/connection_manager.rb +0 -85
- data/lib/liebre/publisher.rb +0 -113
- data/lib/liebre/runner/consumers.rb +0 -46
- data/lib/liebre/runner/starter.rb +0 -44
- data/lib/liebre/runner/starter/consumer.rb +0 -129
- data/lib/liebre/runner/starter/consumer/handler.rb +0 -35
- data/lib/liebre/runner/starter/resources.rb +0 -45
- data/lib/liebre/runner/starter/resources/queue_builder.rb +0 -63
- data/lib/liebre/runner/starter/rpc.rb +0 -59
- data/lib/liebre/tasks.rb +0 -12
- data/spec/config/liebre.yml +0 -48
- data/spec/config/rabbitmq.yml +0 -35
- data/spec/integration_spec.rb +0 -76
- data/spec/liebre/config_spec.rb +0 -63
- data/spec/liebre/connection_manager_spec.rb +0 -44
- data/spec/liebre/publisher_spec.rb +0 -92
- data/spec/liebre/runner/consumers_spec.rb +0 -59
- data/spec/liebre/runner/starter/consumer_spec.rb +0 -145
- data/spec/liebre/runner/starter/resources/queue_builder_spec.rb +0 -69
- data/spec/liebre/runner/starter/resources_spec.rb +0 -38
- data/spec/liebre/runner/starter/rpc_spec.rb +0 -100
- data/spec/liebre/runner/starter_spec.rb +0 -70
- data/spec/liebre/runner_spec.rb +0 -54
@@ -0,0 +1,81 @@
|
|
1
|
+
RSpec.describe "RPC client-server communication" do
|
2
|
+
|
3
|
+
let(:connections) { {"test_conn" => {}} }
|
4
|
+
|
5
|
+
let :exchange do
|
6
|
+
{:name => "__test__.liebre.rpc_communication_exchange",
|
7
|
+
:type => "fanout",
|
8
|
+
:opts => {:auto_delete => true, :durable => false}}
|
9
|
+
end
|
10
|
+
|
11
|
+
let :client_queue do
|
12
|
+
{:prefix => "__test__.liebre.client_rpc_communication_queue"}
|
13
|
+
end
|
14
|
+
|
15
|
+
let :server_queue do
|
16
|
+
{:name => "__test__.liebre.server_rpc_communication_queue",
|
17
|
+
:opts => {:auto_delete => true, :durable => false}}
|
18
|
+
end
|
19
|
+
|
20
|
+
let(:handler_class) { double 'handler_class' }
|
21
|
+
let(:handler) { double 'handler' }
|
22
|
+
|
23
|
+
let :actors do
|
24
|
+
{
|
25
|
+
:rpc_clients => {
|
26
|
+
:my_client => {
|
27
|
+
:connection => "test_conn",
|
28
|
+
:resources => {:exchange => exchange, :queue => client_queue}
|
29
|
+
}
|
30
|
+
},
|
31
|
+
:rpc_servers => {
|
32
|
+
:my_server => {
|
33
|
+
:connection => "test_conn",
|
34
|
+
:prefetch_count => 5,
|
35
|
+
:pool_size => 1,
|
36
|
+
:handler => handler_class,
|
37
|
+
:resources => {:exchange => exchange, :queue => server_queue}
|
38
|
+
}
|
39
|
+
}
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
43
|
+
let :config do
|
44
|
+
Liebre::Config.new.tap do |config|
|
45
|
+
config.adapter = Liebre::Adapter::Bunny
|
46
|
+
config.connections = connections
|
47
|
+
config.actors = actors
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
let(:payload) { "some_data" }
|
52
|
+
let(:headers) { {"foo" => "bar"} }
|
53
|
+
|
54
|
+
let(:response) { "some_response" }
|
55
|
+
|
56
|
+
it "sends and receives data" do
|
57
|
+
engine = Liebre::Engine.new(config)
|
58
|
+
engine.start
|
59
|
+
|
60
|
+
repo = engine.repo
|
61
|
+
rpc_client = repo.rpc_client(:my_client)
|
62
|
+
|
63
|
+
callback = nil
|
64
|
+
expect(handler_class).to receive :new do |payload, meta, given_callback|
|
65
|
+
expect(payload ).to eq payload
|
66
|
+
expect(meta.headers).to eq headers
|
67
|
+
callback = given_callback
|
68
|
+
|
69
|
+
handler
|
70
|
+
end
|
71
|
+
expect(handler).to receive :call do
|
72
|
+
callback.reply(response)
|
73
|
+
end
|
74
|
+
|
75
|
+
sleep(0.1) # wait for all declarations and bindings to take place
|
76
|
+
|
77
|
+
opts = {:headers => headers}
|
78
|
+
expect(rpc_client.request(payload, opts, 0.2)).to eq response
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
RSpec.describe "Start actors twice" do
|
2
|
+
|
3
|
+
let(:connections) { {"test_conn" => {}} }
|
4
|
+
|
5
|
+
let :exchange do
|
6
|
+
{:name => "__test__.liebre.publish_and_consume_exchange",
|
7
|
+
:type => "fanout",
|
8
|
+
:opts => {:auto_delete => true, :durable => false}}
|
9
|
+
end
|
10
|
+
|
11
|
+
let :queue do
|
12
|
+
{:name => "__test__.liebre.publish_and_consume_queue",
|
13
|
+
:opts => {:auto_delete => true, :durable => false}}
|
14
|
+
end
|
15
|
+
|
16
|
+
let(:handler_class) { double 'handler_class' }
|
17
|
+
let(:handler) { double 'handler' }
|
18
|
+
|
19
|
+
let :actors do
|
20
|
+
{
|
21
|
+
:publishers => {
|
22
|
+
:my_publisher => {
|
23
|
+
:connection => "test_conn",
|
24
|
+
:resources => {:exchange => exchange}
|
25
|
+
}
|
26
|
+
},
|
27
|
+
:consumers => {
|
28
|
+
:my_consumer => {
|
29
|
+
:connection => "test_conn",
|
30
|
+
:prefetch_count => 5,
|
31
|
+
:pool_size => 1,
|
32
|
+
:handler => handler_class,
|
33
|
+
:resources => {:exchange => exchange, :queue => queue}
|
34
|
+
}
|
35
|
+
}
|
36
|
+
}
|
37
|
+
end
|
38
|
+
|
39
|
+
let :config do
|
40
|
+
Liebre::Config.new.tap do |config|
|
41
|
+
config.adapter = Liebre::Adapter::Bunny
|
42
|
+
config.connections = connections
|
43
|
+
config.actors = actors
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
let(:payload) { "some_data" }
|
48
|
+
let(:headers) { {"foo" => "bar"} }
|
49
|
+
|
50
|
+
it "sends and receives data" do
|
51
|
+
engine = Liebre::Engine.new(config)
|
52
|
+
repo = engine.repo
|
53
|
+
|
54
|
+
engine.start(only: [:publishers, :rpc_servers])
|
55
|
+
expect(repo.publisher(:my_publisher)).not_to be nil
|
56
|
+
expect { repo.consumer(:my_consumer) }.to raise_error KeyError
|
57
|
+
|
58
|
+
engine.start()
|
59
|
+
expect(repo.publisher(:my_publisher)).not_to be nil
|
60
|
+
expect(repo.consumer(:my_consumer) ).not_to be nil
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
@@ -0,0 +1,169 @@
|
|
1
|
+
RSpec.describe Liebre::Actor::Consumer do
|
2
|
+
|
3
|
+
let(:chan) { double 'chan' }
|
4
|
+
let(:declare) { double 'declare' }
|
5
|
+
let(:handler) { double 'handler' }
|
6
|
+
|
7
|
+
let :spec do
|
8
|
+
{:exchange => {:name => "foo"},
|
9
|
+
:queue => {:name => "bar"},
|
10
|
+
:bind => {:fake => "bind_config"}}
|
11
|
+
end
|
12
|
+
|
13
|
+
let(:logger) { double 'logger', :info => nil, :error => nil }
|
14
|
+
|
15
|
+
let :context do
|
16
|
+
double 'context', :chan => chan,
|
17
|
+
:declare => declare,
|
18
|
+
:name => "foo",
|
19
|
+
:handler => handler,
|
20
|
+
:spec => spec,
|
21
|
+
:logger => logger
|
22
|
+
end
|
23
|
+
|
24
|
+
subject { described_class.new(context) }
|
25
|
+
|
26
|
+
let(:queue) { double 'queue' }
|
27
|
+
let(:exchange) { double 'exchange' }
|
28
|
+
|
29
|
+
let(:dead_queue) { double 'dead_queue' }
|
30
|
+
let(:dead_exchange) { double 'dead_exchange' }
|
31
|
+
|
32
|
+
let(:dead_letter_opts) do
|
33
|
+
{:arguments => {"x-dead-letter-exchange" => "bar-error"}}
|
34
|
+
end
|
35
|
+
|
36
|
+
before do
|
37
|
+
allow(subject).to receive(:async).and_return(subject)
|
38
|
+
|
39
|
+
allow(declare).to receive(:queue).
|
40
|
+
with(:name => "bar", :opts => dead_letter_opts).and_return(queue)
|
41
|
+
|
42
|
+
allow(declare).to receive(:exchange).
|
43
|
+
with(:name => "foo").and_return(exchange)
|
44
|
+
|
45
|
+
allow(declare).to receive(:queue).
|
46
|
+
with(:name => "bar-error", :opts => {}).and_return(dead_queue)
|
47
|
+
|
48
|
+
allow(declare).to receive(:exchange).
|
49
|
+
with(:name => "bar-error", :type => "fanout", :opts => {}).and_return(dead_exchange)
|
50
|
+
|
51
|
+
allow(declare).to receive(:bind).
|
52
|
+
with(queue, exchange, :fake => "bind_config")
|
53
|
+
|
54
|
+
allow(declare).to receive(:bind).
|
55
|
+
with(dead_queue, dead_exchange)
|
56
|
+
end
|
57
|
+
|
58
|
+
let(:info) { double 'info' }
|
59
|
+
let(:meta) { double 'meta' }
|
60
|
+
let(:payload) { "some_data" }
|
61
|
+
|
62
|
+
describe '#start' do
|
63
|
+
it 'declares and binds queue and exchange, and subscribes to the queue' do
|
64
|
+
expect(declare).to receive(:queue).
|
65
|
+
with(:name => "bar", :opts => dead_letter_opts).and_return(queue)
|
66
|
+
|
67
|
+
expect(declare).to receive(:exchange).
|
68
|
+
with(:name => "foo").and_return(exchange)
|
69
|
+
|
70
|
+
expect(declare).to receive(:bind).
|
71
|
+
with(queue, exchange, :fake => "bind_config")
|
72
|
+
|
73
|
+
expect(declare).to receive(:queue).
|
74
|
+
with(:name => "bar-error", :opts => {}).and_return(dead_queue)
|
75
|
+
|
76
|
+
expect(declare).to receive(:exchange).
|
77
|
+
with(:name => "bar-error", :type => "fanout", :opts => {}).and_return(dead_exchange)
|
78
|
+
|
79
|
+
expect(declare).to receive(:bind).
|
80
|
+
with(dead_queue, dead_exchange)
|
81
|
+
|
82
|
+
subscription_block = nil
|
83
|
+
expect(queue).to receive(:subscribe) do |opts, &block|
|
84
|
+
expect(opts).to eq :block => false, :manual_ack => true
|
85
|
+
subscription_block = block
|
86
|
+
end
|
87
|
+
|
88
|
+
subject.start
|
89
|
+
|
90
|
+
expect(subject).to receive(:consume).
|
91
|
+
with(info, meta, payload)
|
92
|
+
|
93
|
+
subscription_block.(info, meta, payload)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
describe '#clean' do
|
98
|
+
it 'deletes all queues and exchanges' do
|
99
|
+
expect(queue ).to receive(:delete)
|
100
|
+
expect(exchange ).to receive(:delete)
|
101
|
+
expect(dead_queue ).to receive(:delete)
|
102
|
+
expect(dead_exchange).to receive(:delete)
|
103
|
+
|
104
|
+
subject.clean
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
describe '#consume' do
|
109
|
+
context 'on success' do
|
110
|
+
it 'runs the handler with a callbacks object' do
|
111
|
+
callback = nil
|
112
|
+
expect(handler).to receive :call do |given_payload, given_meta, given_callback|
|
113
|
+
expect(given_payload).to eq payload
|
114
|
+
expect(given_meta ).to eq meta
|
115
|
+
|
116
|
+
callback = given_callback
|
117
|
+
end
|
118
|
+
|
119
|
+
subject.consume(info, meta, payload)
|
120
|
+
|
121
|
+
expect(subject).to receive(:ack).with(info, {})
|
122
|
+
callback.ack()
|
123
|
+
|
124
|
+
expect(subject).to receive(:nack).with(info, {})
|
125
|
+
callback.nack()
|
126
|
+
|
127
|
+
expect(subject).to receive(:reject).with(info, :requeue => true)
|
128
|
+
callback.reject(:requeue => true)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
context 'on handler failure' do
|
133
|
+
let(:error) { double 'error' }
|
134
|
+
|
135
|
+
it 'calls failed on the consumer' do
|
136
|
+
error_block = nil
|
137
|
+
expect(handler).to receive :call do |payload, meta, callback, &block|
|
138
|
+
error_block = block
|
139
|
+
end
|
140
|
+
|
141
|
+
subject.consume(info, meta, payload)
|
142
|
+
|
143
|
+
expect(subject).to receive(:failed).
|
144
|
+
with(info, error)
|
145
|
+
|
146
|
+
error_block.(error)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
describe '#ack, #nack, #reject and #failed' do
|
152
|
+
let(:error) { double 'error', :message => "foo", :backtrace => [] }
|
153
|
+
|
154
|
+
it 'delegates to the queue' do
|
155
|
+
expect(queue).to receive(:ack).with(info, {})
|
156
|
+
subject.ack(info)
|
157
|
+
|
158
|
+
expect(queue).to receive(:nack).with(info, {})
|
159
|
+
subject.nack(info)
|
160
|
+
|
161
|
+
expect(queue).to receive(:reject).with(info, :requeue => true)
|
162
|
+
subject.reject(info, :requeue => true)
|
163
|
+
|
164
|
+
expect(queue).to receive(:reject).with(info, {})
|
165
|
+
subject.failed(info, error)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
RSpec.describe Liebre::Actor::Context::Declare do
|
2
|
+
|
3
|
+
let(:chan) { double 'chan' }
|
4
|
+
|
5
|
+
subject { described_class.new(chan) }
|
6
|
+
|
7
|
+
describe '#default_exchange' do
|
8
|
+
let(:default_exchange) { double 'default_exchange' }
|
9
|
+
|
10
|
+
it 'builds the default exchange' do
|
11
|
+
expect(chan).to receive(:default_exchange).
|
12
|
+
and_return(default_exchange)
|
13
|
+
|
14
|
+
expect(subject.default_exchange).to eq default_exchange
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
describe '#exchange' do
|
19
|
+
let(:exchange) { double 'exchange' }
|
20
|
+
|
21
|
+
let :opts do
|
22
|
+
{:name => "foo",
|
23
|
+
:type => "fanout",
|
24
|
+
:opts => {:durable => true}}
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'builds an exchange with the given options' do
|
28
|
+
expect(chan).to receive(:exchange).
|
29
|
+
with("foo", "fanout", :durable => true).
|
30
|
+
and_return(exchange)
|
31
|
+
|
32
|
+
expect(subject.exchange(opts)).to eq exchange
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe '#queue' do
|
37
|
+
let(:queue) { double 'queue' }
|
38
|
+
|
39
|
+
let :opts do
|
40
|
+
{:name => "foo",
|
41
|
+
:opts => {:durable => true}}
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'builds an queue with the given options' do
|
45
|
+
expect(chan).to receive(:queue).
|
46
|
+
with("foo", :durable => true).
|
47
|
+
and_return(queue)
|
48
|
+
|
49
|
+
expect(subject.queue(opts)).to eq queue
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
describe '#bind' do
|
54
|
+
let(:queue) { double 'queue' }
|
55
|
+
let(:exchange) { double 'exchange' }
|
56
|
+
let(:opts) { [{:routing_key => "foo"}, {:routing_key => "bar"}] }
|
57
|
+
|
58
|
+
it 'binds as many option sets as given' do
|
59
|
+
expect(queue).to receive(:bind).
|
60
|
+
with(exchange, :routing_key => "foo")
|
61
|
+
|
62
|
+
expect(queue).to receive(:bind).
|
63
|
+
with(exchange, :routing_key => "bar")
|
64
|
+
|
65
|
+
subject.bind(queue, exchange, opts)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
RSpec.describe Liebre::Actor::Context::Handler do
|
2
|
+
|
3
|
+
let(:pool_size) { 3 }
|
4
|
+
let(:handler_class) { double 'handler_class' }
|
5
|
+
|
6
|
+
let :opts do
|
7
|
+
{:pool_size => pool_size,
|
8
|
+
:handler => handler_class}
|
9
|
+
end
|
10
|
+
|
11
|
+
subject { described_class.new(opts) }
|
12
|
+
|
13
|
+
let(:pool) { double 'pool' }
|
14
|
+
|
15
|
+
before do
|
16
|
+
allow(Concurrent::FixedThreadPool).to receive(:new).
|
17
|
+
with(pool_size).and_return(pool)
|
18
|
+
end
|
19
|
+
|
20
|
+
describe '#call' do
|
21
|
+
def pool_block *args, &block
|
22
|
+
pool_block = nil
|
23
|
+
expect(pool).to receive :post do |&given_block|
|
24
|
+
pool_block = given_block
|
25
|
+
end
|
26
|
+
|
27
|
+
subject.call(*args, &block)
|
28
|
+
pool_block
|
29
|
+
end
|
30
|
+
|
31
|
+
let(:handler) { double 'handler' }
|
32
|
+
|
33
|
+
context 'on success' do
|
34
|
+
it 'runs the handler on the pool' do
|
35
|
+
block = pool_block("foo", "bar") {}
|
36
|
+
|
37
|
+
expect(handler_class).to receive(:new).
|
38
|
+
with("foo", "bar").and_return(handler)
|
39
|
+
|
40
|
+
expect(handler).to receive(:call)
|
41
|
+
|
42
|
+
block.()
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
context 'on handler error' do
|
47
|
+
let(:target) { double 'target' }
|
48
|
+
|
49
|
+
it 'runs the given block with the error as argument' do
|
50
|
+
block = pool_block("foo") { target.boom! }
|
51
|
+
|
52
|
+
expect(target).to receive(:boom!)
|
53
|
+
|
54
|
+
expect(handler_class).to receive(:new).
|
55
|
+
with("foo").and_return(handler)
|
56
|
+
|
57
|
+
expect(handler).to receive(:call).
|
58
|
+
and_raise("boom")
|
59
|
+
|
60
|
+
block.()
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|