spinoza 0.1 → 0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|