zmachine 0.2.1 → 0.3.0
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.
- checksums.yaml +4 -4
- data/Gemfile +2 -0
- data/README.md +1 -0
- data/{echo_client.rb → examples/echo_client.rb} +0 -0
- data/{echo_server.rb → examples/echo_server.rb} +0 -0
- data/lib/zmachine.rb +3 -115
- data/lib/zmachine/acceptor.rb +1 -1
- data/lib/zmachine/channel.rb +38 -5
- data/lib/zmachine/connection.rb +57 -152
- data/lib/zmachine/connection_manager.rb +34 -25
- data/lib/zmachine/hashed_wheel.rb +6 -5
- data/lib/zmachine/jeromq-0.3.2-SNAPSHOT.jar +0 -0
- data/lib/zmachine/reactor.rb +9 -7
- data/lib/zmachine/tcp_channel.rb +13 -40
- data/lib/zmachine/timers.rb +15 -43
- data/lib/zmachine/zmq_channel.rb +52 -60
- data/spec/channel_spec.rb +178 -0
- data/spec/connection_spec.rb +36 -11
- data/spec/hashed_wheel_spec.rb +15 -15
- data/spec/support/echo_mock.rb +32 -0
- data/zmachine.gemspec +1 -1
- metadata +9 -9
- data/lib/zmachine/jeromq-0.3.0-SNAPSHOT.jar +0 -0
- data/spec/tcp_channel_spec.rb +0 -109
- data/spec/zmq_channel_spec.rb +0 -113
@@ -0,0 +1,178 @@
|
|
1
|
+
require 'zmachine/tcp_channel'
|
2
|
+
require 'zmachine/zmq_channel'
|
3
|
+
|
4
|
+
include ZMachine
|
5
|
+
|
6
|
+
shared_examples_for "a Channel" do
|
7
|
+
|
8
|
+
context '#bind' do
|
9
|
+
|
10
|
+
it 'binds a server socket' do
|
11
|
+
expect(@server).to be_bound
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'accepts client connections' do
|
15
|
+
channel = @server.accept
|
16
|
+
expect(channel).to be_a(klass)
|
17
|
+
expect(channel).to be_connected
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
context '#connect' do
|
23
|
+
|
24
|
+
it 'connects to a pending server socket' do
|
25
|
+
expect(@client).to be_connection_pending
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'connects to an accepted server socket' do
|
29
|
+
@server.accept
|
30
|
+
@client.finish_connecting
|
31
|
+
expect(@client).to be_connected
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
context '#send/recv' do
|
37
|
+
|
38
|
+
before(:each) do
|
39
|
+
@channel = @server.accept
|
40
|
+
@channel.raw = false
|
41
|
+
@client.finish_connecting
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'writes outbound buffers to the socket' do
|
45
|
+
@client.send_data(data)
|
46
|
+
expect(@client.write_outbound_data).to eq(true)
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'receives data sent from the client' do
|
50
|
+
@client.send_data(data)
|
51
|
+
@client.write_outbound_data
|
52
|
+
received = @channel.read_inbound_data
|
53
|
+
expect(received.to_s).to eq(data.to_s)
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'receives data sent from the server' do
|
57
|
+
@client.send_data(data)
|
58
|
+
@client.write_outbound_data
|
59
|
+
received = @channel.read_inbound_data
|
60
|
+
@channel.send_data(data)
|
61
|
+
@channel.write_outbound_data
|
62
|
+
received = @client.read_inbound_data
|
63
|
+
expect(received.to_s).to eq(data.to_s)
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
|
68
|
+
context '#close' do
|
69
|
+
|
70
|
+
it 'closes the client connection' do
|
71
|
+
@client.close
|
72
|
+
expect(@client).to be_closed
|
73
|
+
end
|
74
|
+
|
75
|
+
it 'closes the server connection' do
|
76
|
+
@server.close
|
77
|
+
expect(@server).to be_closed
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'closes the accepted connection' do
|
81
|
+
channel = @server.accept
|
82
|
+
channel.close
|
83
|
+
expect(channel).to be_closed
|
84
|
+
end
|
85
|
+
|
86
|
+
it 'closes the connection after writing' do
|
87
|
+
channel = @server.accept
|
88
|
+
@client.finish_connecting
|
89
|
+
@client.send_data(data)
|
90
|
+
@client.close(true)
|
91
|
+
expect(@client).to be_connected
|
92
|
+
@client.write_outbound_data
|
93
|
+
expect(@client).not_to be_connected
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
|
100
|
+
describe TCPChannel do
|
101
|
+
|
102
|
+
it_behaves_like "a Channel"
|
103
|
+
|
104
|
+
let(:klass) { TCPChannel }
|
105
|
+
let(:address) { "0.0.0.0" }
|
106
|
+
let(:port_or_type) { [51635, 51635] }
|
107
|
+
|
108
|
+
before(:each) do
|
109
|
+
@server = klass.new
|
110
|
+
@server.bind(address, port_or_type[0])
|
111
|
+
@client = klass.new
|
112
|
+
@client.connect(address, port_or_type[1])
|
113
|
+
end
|
114
|
+
|
115
|
+
after(:each) do
|
116
|
+
@client.close
|
117
|
+
@server.close
|
118
|
+
ZMachine.context.destroy
|
119
|
+
end
|
120
|
+
|
121
|
+
let(:data) { "foo".to_java_bytes }
|
122
|
+
|
123
|
+
it 'has the correct peer information' do
|
124
|
+
expect(@client.peer).to eq([port_or_type[0], address])
|
125
|
+
channel = @server.accept
|
126
|
+
@client.finish_connecting
|
127
|
+
socket = @client.socket.socket
|
128
|
+
expect(channel.peer).to eq([socket.local_port, socket.local_address.host_address])
|
129
|
+
end
|
130
|
+
|
131
|
+
context '#send/recv' do
|
132
|
+
|
133
|
+
it 'reads data after server has closed connection' do
|
134
|
+
channel = @server.accept
|
135
|
+
@client.finish_connecting
|
136
|
+
channel.send_data(data)
|
137
|
+
channel.close(true)
|
138
|
+
expect(channel).to be_connected
|
139
|
+
channel.write_outbound_data
|
140
|
+
expect(channel).not_to be_connected
|
141
|
+
expect(@client).to be_connected
|
142
|
+
@client.read_inbound_data
|
143
|
+
expect{@client.read_inbound_data}.to raise_error(IOException)
|
144
|
+
end
|
145
|
+
|
146
|
+
end
|
147
|
+
|
148
|
+
end
|
149
|
+
|
150
|
+
describe ZMQChannel do
|
151
|
+
|
152
|
+
it_behaves_like "a Channel"
|
153
|
+
|
154
|
+
let(:klass) { ZMQChannel }
|
155
|
+
let(:address) { "tcp://0.0.0.0:51634" }
|
156
|
+
let(:port_or_type) { [ZMQ::REP, ZMQ::REQ] }
|
157
|
+
|
158
|
+
before(:each) do
|
159
|
+
@server = klass.new
|
160
|
+
@server.bind(address, port_or_type[0])
|
161
|
+
@client = klass.new
|
162
|
+
@client.connect(address, port_or_type[1])
|
163
|
+
@client.raw = false
|
164
|
+
end
|
165
|
+
|
166
|
+
after(:each) do
|
167
|
+
@client.close
|
168
|
+
@server.close
|
169
|
+
ZMachine.context.destroy
|
170
|
+
end
|
171
|
+
|
172
|
+
let(:data) { "foo".to_java_bytes }
|
173
|
+
|
174
|
+
it 'has no concept of a peer' do
|
175
|
+
expect{@client.peer}.to raise_error(RuntimeError)
|
176
|
+
end
|
177
|
+
|
178
|
+
end
|
data/spec/connection_spec.rb
CHANGED
@@ -2,17 +2,19 @@ require 'zmachine/connection'
|
|
2
2
|
|
3
3
|
include ZMachine
|
4
4
|
|
5
|
-
|
5
|
+
shared_examples_for "a Connection" do
|
6
6
|
|
7
7
|
before(:each) do
|
8
|
-
@server = Connection.new.bind(
|
9
|
-
@
|
10
|
-
@client = Connection.new.connect(
|
8
|
+
@server = Connection.new.bind(address, port_or_type[0]) { |c| @accepted = c }
|
9
|
+
@server.channel.raw = false
|
10
|
+
@client = Connection.new.connect(address, port_or_type[1]) { |c| @connection = c }
|
11
|
+
@client.channel.raw = false
|
11
12
|
end
|
12
13
|
|
13
14
|
after(:each) do
|
14
15
|
@client.close
|
15
16
|
@server.close
|
17
|
+
ZMachine.context.destroy
|
16
18
|
end
|
17
19
|
|
18
20
|
let(:data) { "foo" }
|
@@ -20,16 +22,17 @@ describe Connection do
|
|
20
22
|
context 'triggers' do
|
21
23
|
|
22
24
|
it 'triggers acceptable' do
|
23
|
-
@server.channel.should_receive(:accept).
|
24
|
-
@server.should_receive(:connection_accepted).once
|
25
|
+
@server.channel.should_receive(:accept).and_call_original
|
25
26
|
connection = @server.acceptable!
|
26
27
|
expect(connection).to be_connected
|
28
|
+
expect(@accepted).to eq(connection)
|
29
|
+
expect(@connection).to be_a(Connection)
|
27
30
|
end
|
28
31
|
|
29
32
|
it 'triggers connectable' do
|
30
33
|
@server.acceptable!
|
31
|
-
@client.channel.should_receive(:finish_connecting).
|
32
|
-
@client.should_receive(:connection_completed)
|
34
|
+
@client.channel.should_receive(:finish_connecting).and_call_original
|
35
|
+
@client.should_receive(:connection_completed)
|
33
36
|
@client.connectable!
|
34
37
|
expect(@client).to be_connected
|
35
38
|
end
|
@@ -37,7 +40,7 @@ describe Connection do
|
|
37
40
|
it 'triggers writable' do
|
38
41
|
@server.acceptable!
|
39
42
|
@client.connectable!
|
40
|
-
@client.send_data(data
|
43
|
+
@client.send_data(data)
|
41
44
|
@client.channel.should_receive(:write_outbound_data).and_call_original
|
42
45
|
@client.writable!
|
43
46
|
end
|
@@ -45,12 +48,34 @@ describe Connection do
|
|
45
48
|
it 'triggers readable' do
|
46
49
|
connection = @server.acceptable!
|
47
50
|
@client.connectable!
|
48
|
-
@client.send_data(data
|
51
|
+
@client.send_data(data)
|
49
52
|
@client.writable!
|
50
53
|
connection.channel.should_receive(:read_inbound_data).and_call_original
|
51
|
-
connection.should_receive(:receive_data).
|
54
|
+
connection.should_receive(:receive_data).with(data)
|
52
55
|
connection.readable!
|
53
56
|
end
|
54
57
|
|
55
58
|
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
describe Connection do
|
63
|
+
|
64
|
+
context 'TCP' do
|
65
|
+
|
66
|
+
it_behaves_like "a Connection"
|
67
|
+
|
68
|
+
let(:address) { "0.0.0.0" }
|
69
|
+
let(:port_or_type) { [51635, 51635] }
|
70
|
+
|
71
|
+
end
|
72
|
+
|
73
|
+
context 'ZMQ' do
|
74
|
+
|
75
|
+
it_behaves_like "a Connection"
|
76
|
+
|
77
|
+
let(:address) { "tcp://0.0.0.0:51634" }
|
78
|
+
let(:port_or_type) { [ZMQ::REP, ZMQ::REQ] }
|
79
|
+
|
80
|
+
end
|
56
81
|
end
|
data/spec/hashed_wheel_spec.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
require 'zmachine/hashed_wheel'
|
2
2
|
|
3
3
|
describe ZMachine::HashedWheel do
|
4
|
-
let(:wheel) { ZMachine::HashedWheel.new(16,
|
4
|
+
let(:wheel) { ZMachine::HashedWheel.new(16, 0.1) }
|
5
5
|
|
6
6
|
it 'returns a timeout on add' do
|
7
7
|
expect(wheel.add(0)).to be_instance_of(ZMachine::HashedWheelTimeout)
|
@@ -9,11 +9,11 @@ describe ZMachine::HashedWheel do
|
|
9
9
|
|
10
10
|
it 'adds timeouts to the correct slot' do
|
11
11
|
wheel.add 0
|
12
|
-
wheel.add
|
13
|
-
wheel.add 110
|
14
|
-
wheel.add
|
15
|
-
wheel.add
|
16
|
-
wheel.add
|
12
|
+
wheel.add 0.090
|
13
|
+
wheel.add 0.110
|
14
|
+
wheel.add 1.000
|
15
|
+
wheel.add 1.600
|
16
|
+
wheel.add 3.200
|
17
17
|
expect(wheel.slots[0].length).to eq(4)
|
18
18
|
expect(wheel.slots[1].length).to eq(1)
|
19
19
|
expect(wheel.slots[10].length).to eq(1)
|
@@ -21,19 +21,19 @@ describe ZMachine::HashedWheel do
|
|
21
21
|
|
22
22
|
it 'times out same slot timeouts correctly' do
|
23
23
|
now = wheel.reset
|
24
|
-
wheel.add
|
25
|
-
wheel.add
|
24
|
+
wheel.add 0.01
|
25
|
+
wheel.add 0.05
|
26
26
|
timedout = wheel.advance(now + 30 * 1_000_000)
|
27
27
|
expect(timedout.length).to eq(1)
|
28
28
|
end
|
29
29
|
|
30
30
|
it 'calculates the timeout set correctly' do
|
31
31
|
now = wheel.reset
|
32
|
-
wheel.add
|
33
|
-
wheel.add
|
34
|
-
wheel.add
|
35
|
-
wheel.add
|
36
|
-
wheel.add
|
32
|
+
wheel.add 0.010
|
33
|
+
wheel.add 0.040
|
34
|
+
wheel.add 1.900
|
35
|
+
wheel.add 3.300
|
36
|
+
wheel.add 4.000
|
37
37
|
timedout = wheel.advance(now + 3900 * 1_000_000)
|
38
38
|
expect(timedout).to be
|
39
39
|
expect(timedout.length).to eq(4)
|
@@ -41,8 +41,8 @@ describe ZMachine::HashedWheel do
|
|
41
41
|
|
42
42
|
it 'cancels timers correctly' do
|
43
43
|
now = wheel.reset
|
44
|
-
t1 = wheel.add
|
45
|
-
t2 = wheel.add 110
|
44
|
+
t1 = wheel.add 0.090
|
45
|
+
t2 = wheel.add 0.110
|
46
46
|
t1.cancel
|
47
47
|
timedout = wheel.advance(now + 200 * 1_000_000)
|
48
48
|
expect(timedout).to eq([t2])
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require "socket"
|
2
|
+
|
3
|
+
module EchoMock
|
4
|
+
def self.start(port = 6380)
|
5
|
+
server = TCPServer.new("127.0.0.1", port)
|
6
|
+
loop do
|
7
|
+
session = server.accept
|
8
|
+
while line = session.gets
|
9
|
+
session.write(line)
|
10
|
+
session.write("\r\n")
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
module Helper
|
16
|
+
def echo_mock
|
17
|
+
begin
|
18
|
+
pid = fork do
|
19
|
+
trap("TERM") { exit }
|
20
|
+
EchoMock.start
|
21
|
+
end
|
22
|
+
sleep 1 # Give time for the socket to start listening.
|
23
|
+
yield
|
24
|
+
ensure
|
25
|
+
if pid
|
26
|
+
Process.kill("TERM", pid)
|
27
|
+
Process.wait(pid)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
data/zmachine.gemspec
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
Gem::Specification.new do |spec|
|
4
4
|
spec.name = "zmachine"
|
5
|
-
spec.version = "0.
|
5
|
+
spec.version = "0.3.0"
|
6
6
|
spec.authors = ["LiquidM, Inc."]
|
7
7
|
spec.email = ["opensource@liquidm.com"]
|
8
8
|
spec.description = %q{pure JRuby multi-threaded mostly EventMachine compatible event loop}
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: zmachine
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- LiquidM, Inc.
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2013-
|
11
|
+
date: 2013-12-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: liquid-ext
|
@@ -42,8 +42,8 @@ files:
|
|
42
42
|
- benchmarks/benchmark.sh
|
43
43
|
- benchmarks/tcp_channel.rb
|
44
44
|
- benchmarks/zmq_channel.rb
|
45
|
-
- echo_client.rb
|
46
|
-
- echo_server.rb
|
45
|
+
- examples/echo_client.rb
|
46
|
+
- examples/echo_server.rb
|
47
47
|
- lib/zmachine.rb
|
48
48
|
- lib/zmachine/acceptor.rb
|
49
49
|
- lib/zmachine/channel.rb
|
@@ -51,17 +51,17 @@ files:
|
|
51
51
|
- lib/zmachine/connection_manager.rb
|
52
52
|
- lib/zmachine/deferrable.rb
|
53
53
|
- lib/zmachine/hashed_wheel.rb
|
54
|
-
- lib/zmachine/jeromq-0.3.
|
54
|
+
- lib/zmachine/jeromq-0.3.2-SNAPSHOT.jar
|
55
55
|
- lib/zmachine/reactor.rb
|
56
56
|
- lib/zmachine/tcp_channel.rb
|
57
57
|
- lib/zmachine/timers.rb
|
58
58
|
- lib/zmachine/zmq_channel.rb
|
59
|
+
- spec/channel_spec.rb
|
59
60
|
- spec/connection_manager_spec.rb
|
60
61
|
- spec/connection_spec.rb
|
61
62
|
- spec/hashed_wheel_spec.rb
|
62
63
|
- spec/spec_helper.rb
|
63
|
-
- spec/
|
64
|
-
- spec/zmq_channel_spec.rb
|
64
|
+
- spec/support/echo_mock.rb
|
65
65
|
- zmachine.gemspec
|
66
66
|
homepage: https://github.com/liquidm/zmachine
|
67
67
|
licenses:
|
@@ -88,10 +88,10 @@ signing_key:
|
|
88
88
|
specification_version: 4
|
89
89
|
summary: pure JRuby multi-threaded mostly EventMachine compatible event loop
|
90
90
|
test_files:
|
91
|
+
- spec/channel_spec.rb
|
91
92
|
- spec/connection_manager_spec.rb
|
92
93
|
- spec/connection_spec.rb
|
93
94
|
- spec/hashed_wheel_spec.rb
|
94
95
|
- spec/spec_helper.rb
|
95
|
-
- spec/
|
96
|
-
- spec/zmq_channel_spec.rb
|
96
|
+
- spec/support/echo_mock.rb
|
97
97
|
has_rdoc:
|
Binary file
|
data/spec/tcp_channel_spec.rb
DELETED
@@ -1,109 +0,0 @@
|
|
1
|
-
require 'zmachine/tcp_channel'
|
2
|
-
|
3
|
-
include ZMachine
|
4
|
-
|
5
|
-
describe ZMachine::TCPChannel do
|
6
|
-
|
7
|
-
before(:each) do
|
8
|
-
@server = TCPChannel.new
|
9
|
-
@server.bind("0.0.0.0", 0)
|
10
|
-
@port = @server.socket.socket.local_port
|
11
|
-
@client = TCPChannel.new
|
12
|
-
@client.connect("0.0.0.0", @port)
|
13
|
-
end
|
14
|
-
|
15
|
-
after(:each) do
|
16
|
-
@client.close
|
17
|
-
@server.close
|
18
|
-
end
|
19
|
-
|
20
|
-
let(:data) { "foo" }
|
21
|
-
|
22
|
-
context '#bind' do
|
23
|
-
|
24
|
-
it 'binds a server socket' do
|
25
|
-
expect(@server).to be_bound
|
26
|
-
end
|
27
|
-
|
28
|
-
it 'accepts client connections' do
|
29
|
-
channel = @server.accept
|
30
|
-
expect(channel).to be_a(TCPChannel)
|
31
|
-
expect(channel).to be_connected
|
32
|
-
end
|
33
|
-
|
34
|
-
end
|
35
|
-
|
36
|
-
context '#connect' do
|
37
|
-
|
38
|
-
it 'connects to a pending server socket' do
|
39
|
-
expect(@client).to be_connection_pending
|
40
|
-
end
|
41
|
-
|
42
|
-
it 'connects to an accepted server socket' do
|
43
|
-
@server.accept
|
44
|
-
@client.finish_connecting
|
45
|
-
expect(@client).to be_connected
|
46
|
-
end
|
47
|
-
|
48
|
-
end
|
49
|
-
|
50
|
-
context '#send/recv' do
|
51
|
-
|
52
|
-
before(:each) do
|
53
|
-
@channel = @server.accept
|
54
|
-
@client.finish_connecting
|
55
|
-
@client.send_data(data.to_java_bytes)
|
56
|
-
end
|
57
|
-
|
58
|
-
it 'writes outbound buffers to the socket' do
|
59
|
-
expect(@client.write_outbound_data).to eq(true)
|
60
|
-
end
|
61
|
-
|
62
|
-
it 'receives data sent from the client' do
|
63
|
-
@client.write_outbound_data
|
64
|
-
received = @channel.read_inbound_data
|
65
|
-
expect(received).to eq(data)
|
66
|
-
end
|
67
|
-
|
68
|
-
it 'receives data sent from the server' do
|
69
|
-
@client.write_outbound_data
|
70
|
-
received = @channel.read_inbound_data
|
71
|
-
@channel.send_data(data.to_java_bytes)
|
72
|
-
@channel.write_outbound_data
|
73
|
-
received = @client.read_inbound_data
|
74
|
-
expect(received).to eq(data)
|
75
|
-
end
|
76
|
-
|
77
|
-
end
|
78
|
-
|
79
|
-
context '#close' do
|
80
|
-
|
81
|
-
it 'closes the client connection' do
|
82
|
-
@client.close
|
83
|
-
expect(@client).to be_closed
|
84
|
-
end
|
85
|
-
|
86
|
-
it 'closes the server connection' do
|
87
|
-
@server.close
|
88
|
-
expect(@server).to be_closed
|
89
|
-
end
|
90
|
-
|
91
|
-
it 'closes the accepted connection' do
|
92
|
-
channel = @server.accept
|
93
|
-
channel.close
|
94
|
-
expect(channel).to be_closed
|
95
|
-
end
|
96
|
-
|
97
|
-
it 'closes the connection after writing' do
|
98
|
-
channel = @server.accept
|
99
|
-
@client.finish_connecting
|
100
|
-
@client.send_data(data.to_java_bytes)
|
101
|
-
@client.close_after_writing
|
102
|
-
expect(@client).to be_connected
|
103
|
-
@client.write_outbound_data
|
104
|
-
expect(@client).not_to be_connected
|
105
|
-
end
|
106
|
-
|
107
|
-
end
|
108
|
-
|
109
|
-
end
|