spinoza 0.1 → 0.2
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/README.md +66 -3
- data/lib/spinoza/calvin/executor.rb +107 -0
- data/lib/spinoza/calvin/node.rb +44 -0
- data/lib/spinoza/calvin/readcaster.rb +50 -0
- data/lib/spinoza/calvin/scheduler.rb +134 -0
- data/lib/spinoza/calvin/sequencer.rb +74 -0
- data/lib/spinoza/common.rb +3 -0
- data/lib/spinoza/system/link.rb +33 -0
- data/lib/spinoza/system/lock-manager.rb +22 -8
- data/lib/spinoza/system/log.rb +95 -0
- data/lib/spinoza/system/meta-log.rb +103 -0
- data/lib/spinoza/system/model.rb +14 -0
- data/lib/spinoza/system/node.rb +56 -7
- data/lib/spinoza/system/operation.rb +22 -6
- data/lib/spinoza/system/store.rb +15 -14
- data/lib/spinoza/system/{table-spec.rb → table.rb} +10 -6
- data/lib/spinoza/system/timeline.rb +81 -0
- data/lib/spinoza/transaction.rb +170 -39
- data/lib/spinoza/version.rb +1 -1
- data/test/test-executor.rb +110 -0
- data/test/test-link.rb +43 -0
- data/test/test-log.rb +47 -0
- data/test/test-meta-log.rb +63 -0
- data/test/test-node.rb +35 -14
- data/test/test-readcaster.rb +87 -0
- data/test/test-scheduler.rb +163 -0
- data/test/test-sequencer.rb +78 -0
- data/test/test-timeline.rb +58 -0
- data/test/test-transaction.rb +75 -18
- metadata +42 -3
data/test/test-link.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'spinoza/system/link'
|
3
|
+
|
4
|
+
class TestLink < Minitest::Test
|
5
|
+
include Spinoza
|
6
|
+
|
7
|
+
class MockNode
|
8
|
+
attr_reader :msgs
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@msgs = []
|
12
|
+
end
|
13
|
+
|
14
|
+
def recv msg: nil
|
15
|
+
@msgs << msg
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def setup
|
20
|
+
@timeline = Timeline.new
|
21
|
+
@node1 = MockNode.new
|
22
|
+
@node2 = MockNode.new
|
23
|
+
@link = Link[timeline: @timeline,
|
24
|
+
src: @node1, dst: @node2, latency: 1.0
|
25
|
+
]
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_link
|
29
|
+
@link.send_message "hello"
|
30
|
+
assert_equal [], @node2.msgs
|
31
|
+
@timeline.evolve 0.9
|
32
|
+
assert_equal [], @node2.msgs
|
33
|
+
@timeline.evolve 0.2
|
34
|
+
assert_equal ["hello"], @node2.msgs
|
35
|
+
end
|
36
|
+
|
37
|
+
def test_fifo
|
38
|
+
@link.send_message "foo"
|
39
|
+
@link.send_message "bar"
|
40
|
+
@timeline.evolve 1.0
|
41
|
+
assert_equal ["foo", "bar"], @node2.msgs
|
42
|
+
end
|
43
|
+
end
|
data/test/test-log.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'spinoza/system/log'
|
3
|
+
|
4
|
+
class TestLog < Minitest::Test
|
5
|
+
include Spinoza
|
6
|
+
|
7
|
+
class MockNode
|
8
|
+
attr_reader :time_now
|
9
|
+
|
10
|
+
def initialize time_now
|
11
|
+
@time_now = time_now
|
12
|
+
end
|
13
|
+
|
14
|
+
def evolve dt
|
15
|
+
@time_now += dt
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def setup
|
20
|
+
@log = Log.new dt_durable: 0.300, dt_replicated: 0.500
|
21
|
+
@sender = MockNode.new 0.0
|
22
|
+
@recver = MockNode.new 0.0
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_log
|
26
|
+
@log.write "a", 1, node: @sender
|
27
|
+
assert_raises Log::KeyConflictError do
|
28
|
+
@log.write "a", 1, node: @sender
|
29
|
+
end
|
30
|
+
|
31
|
+
assert_equal 0.300, @log.time_durable("a")
|
32
|
+
assert_equal 0.500, @log.time_replicated("a")
|
33
|
+
|
34
|
+
assert_equal 1, @log.read("a", node: @sender)
|
35
|
+
assert_equal nil, @log.read("a", node: @recver)
|
36
|
+
|
37
|
+
@sender.evolve 0.200
|
38
|
+
refute @log.durable? "a"
|
39
|
+
@sender.evolve 0.200
|
40
|
+
assert @log.durable? "a"
|
41
|
+
|
42
|
+
@recver.evolve 0.400
|
43
|
+
assert_equal nil, @log.read("a", node: @recver)
|
44
|
+
@recver.evolve 0.200
|
45
|
+
assert_equal 1, @log.read("a", node: @recver)
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'spinoza/system/meta-log'
|
3
|
+
|
4
|
+
class TestMetaLog < Minitest::Test
|
5
|
+
include Spinoza
|
6
|
+
|
7
|
+
class MockNode
|
8
|
+
attr_reader :time_now, :timeline
|
9
|
+
|
10
|
+
def initialize time_now
|
11
|
+
@time_now = time_now
|
12
|
+
@timeline = [] # note: history and future
|
13
|
+
end
|
14
|
+
|
15
|
+
def evolve dt
|
16
|
+
@time_now += dt
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def setup
|
21
|
+
@meta_log = MetaLog.new dt_quorum: 0.300, dt_replicated: 0.500
|
22
|
+
@sender = MockNode.new 0.0
|
23
|
+
@recver = MockNode.new 0.0
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_meta_log
|
27
|
+
id = @meta_log.append "a", node: @sender
|
28
|
+
|
29
|
+
assert_equal 0.300, @meta_log.time_quorum(id)
|
30
|
+
assert_equal 0.500, @meta_log.time_replicated(id)
|
31
|
+
|
32
|
+
assert_equal "a", @meta_log.get(id, node: @sender)
|
33
|
+
assert_equal nil, @meta_log.get(id, node: @recver)
|
34
|
+
|
35
|
+
@sender.evolve 0.200
|
36
|
+
refute @meta_log.quorum? id
|
37
|
+
@sender.evolve 0.200
|
38
|
+
assert @meta_log.quorum? id
|
39
|
+
|
40
|
+
@recver.evolve 0.400
|
41
|
+
assert_equal nil, @meta_log.get(id, node: @recver)
|
42
|
+
@recver.evolve 0.200
|
43
|
+
assert_equal "a", @meta_log.get(id, node: @recver)
|
44
|
+
end
|
45
|
+
|
46
|
+
def test_listeners
|
47
|
+
listener = []
|
48
|
+
@meta_log.on_entry_available listener, :push
|
49
|
+
|
50
|
+
@meta_log.append "a", node: @sender
|
51
|
+
@meta_log.append "b", node: @sender
|
52
|
+
|
53
|
+
assert_equal 2, @sender.timeline.size
|
54
|
+
|
55
|
+
e = @sender.timeline[0]
|
56
|
+
assert_equal @sender.time_now + 0.500, e.time
|
57
|
+
assert_equal "a", e.data[:value]
|
58
|
+
|
59
|
+
e = @sender.timeline[1]
|
60
|
+
assert_equal @sender.time_now + 0.500, e.time
|
61
|
+
assert_equal "b", e.data[:value]
|
62
|
+
end
|
63
|
+
end
|
data/test/test-node.rb
CHANGED
@@ -1,17 +1,18 @@
|
|
1
1
|
require 'minitest/autorun'
|
2
2
|
require 'spinoza/system/node'
|
3
3
|
|
4
|
-
include Spinoza
|
5
|
-
|
6
4
|
class TestNode < Minitest::Test
|
5
|
+
include Spinoza
|
6
|
+
|
7
7
|
def setup
|
8
|
-
@node = Node
|
9
|
-
|
10
|
-
|
8
|
+
@node = Node[
|
9
|
+
Table[:foos, id: "integer", name: "string", len: "float"],
|
10
|
+
timeline: nil
|
11
|
+
]
|
11
12
|
end
|
12
13
|
|
13
14
|
def test_store
|
14
|
-
assert_equal [:foos], @node.store.tables
|
15
|
+
assert_equal Set[:foos], @node.store.tables
|
15
16
|
end
|
16
17
|
|
17
18
|
def test_ops
|
@@ -22,23 +23,18 @@ class TestNode < Minitest::Test
|
|
22
23
|
rslt = store.execute op_i, op_r
|
23
24
|
|
24
25
|
assert_equal(1, rslt.size)
|
25
|
-
|
26
|
-
assert_equal(1, r_tuples.size)
|
27
|
-
assert_equal({id: 1, name: "a", len: 1.2}, r_tuples[0])
|
26
|
+
assert_equal({id: 1, name: "a", len: 1.2}, rslt[0].val)
|
28
27
|
|
29
28
|
op_u = UpdateOperation.new(table: :foos, key: {id: 1}, row: {name: "b"})
|
30
29
|
rslt = store.execute op_u, op_r
|
31
30
|
|
32
31
|
assert_equal(1, rslt.size)
|
33
|
-
|
34
|
-
assert_equal(1, r_tuples.size)
|
35
|
-
assert_equal({id: 1, name: "b", len: 1.2}, r_tuples[0])
|
32
|
+
assert_equal({id: 1, name: "b", len: 1.2}, rslt[0].val)
|
36
33
|
|
37
34
|
op_d = DeleteOperation.new(table: :foos, key: {id: 1})
|
38
35
|
rslt = store.execute op_d, op_r
|
39
36
|
assert_equal(1, rslt.size)
|
40
|
-
|
41
|
-
assert_equal(0, r_tuples.size)
|
37
|
+
refute rslt[0].val
|
42
38
|
end
|
43
39
|
|
44
40
|
def test_locks
|
@@ -50,6 +46,7 @@ class TestNode < Minitest::Test
|
|
50
46
|
lm.lock_read rs1, :t1
|
51
47
|
assert lm.has_read_lock?(rs1, :t1)
|
52
48
|
refute lm.has_read_lock?(rs1, :t2)
|
49
|
+
refute lm.has_read_lock?([:bars, 3], :t1)
|
53
50
|
|
54
51
|
lm.lock_read rs1, :t2
|
55
52
|
assert lm.has_read_lock?(rs1, :t1)
|
@@ -76,6 +73,8 @@ class TestNode < Minitest::Test
|
|
76
73
|
lm.lock_read rs2, :t2
|
77
74
|
end
|
78
75
|
assert lm.has_write_lock?(rs2, :t1)
|
76
|
+
refute lm.has_write_lock?(rs2, :t3)
|
77
|
+
refute lm.has_write_lock?([:bars, 3], :t1)
|
79
78
|
|
80
79
|
lm.lock_write rs2, :t1
|
81
80
|
assert lm.has_write_lock?(rs2, :t1)
|
@@ -84,4 +83,26 @@ class TestNode < Minitest::Test
|
|
84
83
|
lm.unlock_write rs2, :t1
|
85
84
|
refute lm.has_write_lock?(rs2, :t1)
|
86
85
|
end
|
86
|
+
|
87
|
+
def test_unlock_all
|
88
|
+
lm = @node.lock_manager
|
89
|
+
|
90
|
+
rs1 = [:foos, 1]
|
91
|
+
rs2 = [:foos, 2]
|
92
|
+
|
93
|
+
lm.lock_read rs1, :t1
|
94
|
+
lm.lock_write rs2, :t2
|
95
|
+
|
96
|
+
lm.unlock_all :t1
|
97
|
+
refute lm.has_write_lock?(rs1, :t1)
|
98
|
+
refute lm.has_write_lock?(rs2, :t1)
|
99
|
+
refute lm.has_write_lock?(rs1, :t2)
|
100
|
+
assert lm.has_write_lock?(rs2, :t2)
|
101
|
+
|
102
|
+
lm.unlock_all :t2
|
103
|
+
refute lm.has_write_lock?(rs1, :t1)
|
104
|
+
refute lm.has_write_lock?(rs2, :t1)
|
105
|
+
refute lm.has_write_lock?(rs1, :t2)
|
106
|
+
refute lm.has_write_lock?(rs2, :t2)
|
107
|
+
end
|
87
108
|
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'spinoza/calvin/readcaster'
|
3
|
+
require 'spinoza/system/node'
|
4
|
+
require 'spinoza/system/link'
|
5
|
+
require 'spinoza/system/timeline'
|
6
|
+
require 'spinoza/transaction'
|
7
|
+
|
8
|
+
class TestReadcaster < Minitest::Test
|
9
|
+
include Spinoza
|
10
|
+
|
11
|
+
def setup
|
12
|
+
@timeline = Spinoza::Timeline.new
|
13
|
+
|
14
|
+
@node = Node[
|
15
|
+
Table[:as, id: "integer", name: "string"],
|
16
|
+
Table[:bs, id: "integer", name: "string"],
|
17
|
+
timeline: @timeline
|
18
|
+
]
|
19
|
+
|
20
|
+
@na = Node[
|
21
|
+
Table[:as, id: "integer", name: "string"],
|
22
|
+
timeline: @timeline
|
23
|
+
]
|
24
|
+
|
25
|
+
@nb = Node[
|
26
|
+
Table[:bs, id: "integer", name: "string"],
|
27
|
+
timeline: @timeline
|
28
|
+
]
|
29
|
+
|
30
|
+
@node.link @na, latency: 1.0
|
31
|
+
@node.link @nb, latency: 1.0
|
32
|
+
|
33
|
+
store = @node.store
|
34
|
+
|
35
|
+
op_ia = InsertOperation.new(table: :as, row: {id: 2, name: "a2"})
|
36
|
+
op_ib = InsertOperation.new(table: :bs, row: {id: 2, name: "b2"})
|
37
|
+
store.execute op_ia, op_ib
|
38
|
+
|
39
|
+
# normally this is part of a Calvin::Node, but here we have a System::Node
|
40
|
+
@readcaster = Calvin::Readcaster.new(node: @node)
|
41
|
+
|
42
|
+
@txn = transaction do
|
43
|
+
at(:as).insert id: 1, name: "a1"
|
44
|
+
at(:as, id: 2).read
|
45
|
+
at(:bs, id: 2).read
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def test_readcaster
|
50
|
+
local_read_results = @readcaster.execute_local_reads @txn
|
51
|
+
assert_equal 2, local_read_results.size
|
52
|
+
assert_equal "a2", local_read_results[0].val[:name]
|
53
|
+
assert_equal "b2", local_read_results[1].val[:name]
|
54
|
+
end
|
55
|
+
|
56
|
+
def test_serve_reads
|
57
|
+
results = []
|
58
|
+
class << @readcaster; self; end.send :define_method,
|
59
|
+
:send_read do |link, **opts|
|
60
|
+
results << [link, opts]
|
61
|
+
end
|
62
|
+
|
63
|
+
local_read_results = @readcaster.execute_local_reads @txn
|
64
|
+
@readcaster.serve_reads @txn, local_read_results
|
65
|
+
|
66
|
+
assert_equal 1, results.size
|
67
|
+
link, opts = results.first
|
68
|
+
|
69
|
+
assert_equal @node.links[@na], link
|
70
|
+
table, read_results = opts.values_at(:table, :read_results)
|
71
|
+
assert_equal :bs, table
|
72
|
+
assert_equal 1, read_results.size
|
73
|
+
assert_equal "b2", read_results[0].val[:name]
|
74
|
+
end
|
75
|
+
|
76
|
+
def test_all_reads_local
|
77
|
+
assert @txn.all_reads_are_local? @node
|
78
|
+
refute @txn.all_reads_are_local? @na
|
79
|
+
refute @txn.all_reads_are_local? @nb
|
80
|
+
end
|
81
|
+
|
82
|
+
def test_remote_read_tables
|
83
|
+
assert_equal Set[], @txn.remote_read_tables(@node)
|
84
|
+
assert_equal Set[:bs], @txn.remote_read_tables(@na)
|
85
|
+
assert_equal Set[:as], @txn.remote_read_tables(@nb)
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,163 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'spinoza/transaction'
|
3
|
+
require 'spinoza/system/timeline'
|
4
|
+
require 'spinoza/system/log'
|
5
|
+
require 'spinoza/system/meta-log'
|
6
|
+
require 'spinoza/calvin/node'
|
7
|
+
|
8
|
+
#===========#
|
9
|
+
# DEBUGGING #
|
10
|
+
#===========#
|
11
|
+
#
|
12
|
+
# require 'pp'
|
13
|
+
# pp @results
|
14
|
+
# pp @timeline.history.select {|time, event| event.action != :step_epoch}
|
15
|
+
|
16
|
+
class TestScheduler < Minitest::Test
|
17
|
+
include Spinoza
|
18
|
+
|
19
|
+
def mkresults node, txn, rslt
|
20
|
+
{
|
21
|
+
transaction: txn,
|
22
|
+
time: node.timeline.now,
|
23
|
+
values: rslt.map {|rr| rr.val}
|
24
|
+
}
|
25
|
+
end
|
26
|
+
|
27
|
+
FOOS = Table[:foos, id: "integer", name: "string"]
|
28
|
+
BARS = Table[:bars, id: "integer", name: "string"]
|
29
|
+
|
30
|
+
# spec is an array of arrays of Tables, such as [ [FOOS], [FOOS, BARS] ]
|
31
|
+
def mknodes spec
|
32
|
+
@nodes = spec.map.with_index do |tables, i|
|
33
|
+
Calvin::Node[
|
34
|
+
*tables,
|
35
|
+
name: i,
|
36
|
+
timeline: @timeline,
|
37
|
+
log: @log,
|
38
|
+
meta_log: @meta_log,
|
39
|
+
sequencer: nil, # so the default will get created
|
40
|
+
scheduler: nil ## TODO dependency injection
|
41
|
+
]
|
42
|
+
end
|
43
|
+
|
44
|
+
@results = {}
|
45
|
+
@nodes.each do |node|
|
46
|
+
rs = @results[node] = []
|
47
|
+
node.on_transaction_finish do |txn, rslt|
|
48
|
+
rs << mkresults(node, txn, rslt)
|
49
|
+
#@node.default_output txn, rslt
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def setup
|
55
|
+
@timeline = Timeline.new
|
56
|
+
@log = Log.new dt_durable: 0.300, dt_replicated: 0.500
|
57
|
+
@meta_log = MetaLog.new dt_quorum: 0.300, dt_replicated: 0.500
|
58
|
+
end
|
59
|
+
|
60
|
+
def test_one_node
|
61
|
+
mknodes [ [FOOS] ]
|
62
|
+
|
63
|
+
txn1 = transaction do
|
64
|
+
at(:foos).insert id: 1, name: "a"
|
65
|
+
at(:foos).insert id: 2, name: "b"
|
66
|
+
at(:foos).insert id: 3, name: "c"
|
67
|
+
end
|
68
|
+
|
69
|
+
txn2 = transaction do
|
70
|
+
at(:foos, id: 1).read
|
71
|
+
at(:foos, id: 2).delete
|
72
|
+
at(:foos, id: 3).update name: "cc"
|
73
|
+
end
|
74
|
+
|
75
|
+
txn3 = transaction do
|
76
|
+
at(:foos, id: 1).read
|
77
|
+
at(:foos, id: 2).read
|
78
|
+
at(:foos, id: 3).read
|
79
|
+
end
|
80
|
+
|
81
|
+
@nodes[0].sequencer.accept_transaction txn1
|
82
|
+
@nodes[0].sequencer.accept_transaction txn2
|
83
|
+
@nodes[0].sequencer.accept_transaction txn3
|
84
|
+
|
85
|
+
@timeline.evolve 1.0
|
86
|
+
|
87
|
+
rs = @results[@nodes[0]]
|
88
|
+
assert_equal [0.81, 0.81, 0.81], rs.map{|r| r[:time]}
|
89
|
+
|
90
|
+
assert_empty rs[0][:values]
|
91
|
+
assert_equal [{id: 1, name: "a"}], rs[1][:values]
|
92
|
+
assert_equal [{id: 1, name: "a"}, nil, {id: 3, name: "cc"}],
|
93
|
+
rs[2][:values]
|
94
|
+
end
|
95
|
+
|
96
|
+
def test_two_nodes_same_tables
|
97
|
+
mknodes [ [FOOS], [FOOS] ]
|
98
|
+
|
99
|
+
txn1 = transaction do
|
100
|
+
at(:foos).insert id: 1, name: "a"
|
101
|
+
at(:foos).insert id: 2, name: "b"
|
102
|
+
at(:foos).insert id: 3, name: "c"
|
103
|
+
end
|
104
|
+
|
105
|
+
txn2 = transaction do
|
106
|
+
at(:foos, id: 1).read
|
107
|
+
at(:foos, id: 2).delete
|
108
|
+
at(:foos, id: 3).update name: "cc"
|
109
|
+
end
|
110
|
+
|
111
|
+
txn3 = transaction do
|
112
|
+
at(:foos, id: 1).read
|
113
|
+
at(:foos, id: 2).read
|
114
|
+
at(:foos, id: 3).read
|
115
|
+
end
|
116
|
+
|
117
|
+
@nodes[0].sequencer.accept_transaction txn1
|
118
|
+
@nodes[0].sequencer.accept_transaction txn2
|
119
|
+
@nodes[0].sequencer.accept_transaction txn3
|
120
|
+
|
121
|
+
@timeline.evolve 1.0
|
122
|
+
|
123
|
+
@results.each do |node, rs|
|
124
|
+
desc = node.inspect
|
125
|
+
assert_equal [0.81, 0.81, 0.81], rs.map{|r| r[:time]}, desc
|
126
|
+
|
127
|
+
assert_empty rs[0][:values], desc
|
128
|
+
assert_equal [{id: 1, name: "a"}], rs[1][:values], desc
|
129
|
+
assert_equal [{id: 1, name: "a"}, nil, {id: 3, name: "cc"}],
|
130
|
+
rs[2][:values], desc
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def test_remote_reads
|
135
|
+
mknodes [ [FOOS], [BARS] ]
|
136
|
+
|
137
|
+
@nodes[0].link @nodes[1], latency: 0.010
|
138
|
+
|
139
|
+
txn1 = transaction do
|
140
|
+
at(:foos).insert id: 1, name: "a"
|
141
|
+
at(:bars).insert id: 2, name: "b"
|
142
|
+
end
|
143
|
+
|
144
|
+
txn2 = transaction do
|
145
|
+
at(:foos, id: 1).read
|
146
|
+
at(:bars, id: 2).update name: "bb"
|
147
|
+
# write must be present or else msg to inactive node is optimized away
|
148
|
+
end
|
149
|
+
|
150
|
+
@nodes[0].sequencer.accept_transaction txn1
|
151
|
+
@nodes[0].sequencer.accept_transaction txn2
|
152
|
+
|
153
|
+
@timeline.evolve 2.0
|
154
|
+
|
155
|
+
rs = @results[@nodes[1]]
|
156
|
+
|
157
|
+
assert_equal 0.81, rs[0][:time]
|
158
|
+
assert_empty rs[0][:values]
|
159
|
+
|
160
|
+
assert_equal 0.81 + 0.010, rs[1][:time]
|
161
|
+
assert_equal [{id: 1, name: "a"}], rs[1][:values]
|
162
|
+
end
|
163
|
+
end
|