spinoza 0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/COPYING +22 -0
- data/README.md +22 -0
- data/Rakefile +68 -0
- data/lib/spinoza/common.rb +2 -0
- data/lib/spinoza/system/link.rb +0 -0
- data/lib/spinoza/system/lock-manager.rb +154 -0
- data/lib/spinoza/system/node.rb +17 -0
- data/lib/spinoza/system/operation.rb +88 -0
- data/lib/spinoza/system/store.rb +44 -0
- data/lib/spinoza/system/table-spec.rb +31 -0
- data/lib/spinoza/transaction.rb +56 -0
- data/lib/spinoza/version.rb +3 -0
- data/test/test-node.rb +87 -0
- data/test/test-transaction.rb +37 -0
- metadata +97 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 868a9f9ccabcf28a96ace9484c9c0caf0bbacd26
|
4
|
+
data.tar.gz: 42d844a448adc075556b5933731bcb90e4a58356
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4c178a4b1cf1e14cf538dec9602c4a25eca2f57a003a8f8db2b2b92fb9b55feb9014979db38caabc6af9ce2f7fc26dfda73b564da454599fb8b054f80132585b
|
7
|
+
data.tar.gz: ae5bcbdf391a45950d1d3aa4efc0ac52f10d99d413f0ee96e639c179ecb054a311ae14ab3448d0ecbc22976162e02e22d71a0cbb4088e091aa193decb7a1c86a
|
data/COPYING
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013-2014, Joel VanderWerf
|
2
|
+
All rights reserved.
|
3
|
+
|
4
|
+
Redistribution and use in source and binary forms, with or without
|
5
|
+
modification, are permitted provided that the following conditions are met:
|
6
|
+
|
7
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
8
|
+
list of conditions and the following disclaimer.
|
9
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
10
|
+
this list of conditions and the following disclaimer in the documentation
|
11
|
+
and/or other materials provided with the distribution.
|
12
|
+
|
13
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
14
|
+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
15
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
16
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
17
|
+
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
18
|
+
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
19
|
+
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
20
|
+
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
21
|
+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
22
|
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/README.md
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
spinoza
|
2
|
+
=======
|
3
|
+
|
4
|
+
A model of the Calvin distributed database.
|
5
|
+
|
6
|
+
Spinoza, like Calvin, was a philosopher who dealt in determinism.
|
7
|
+
|
8
|
+
The model of the underlying computer and network system is in lib/spinoza/system.
|
9
|
+
|
10
|
+
Calvin is developed by the Yale Databases group; the open-source releases are at https://github.com/yaledb.
|
11
|
+
|
12
|
+
Contact
|
13
|
+
=======
|
14
|
+
|
15
|
+
Joel VanderWerf, vjoel@users.sourceforge.net, [@JoelVanderWerf](https://twitter.com/JoelVanderWerf).
|
16
|
+
|
17
|
+
License and Copyright
|
18
|
+
========
|
19
|
+
|
20
|
+
Copyright (c) 2014, Joel VanderWerf
|
21
|
+
|
22
|
+
License for this project is BSD. See the COPYING file for the standard BSD license.
|
data/Rakefile
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/testtask'
|
3
|
+
|
4
|
+
PRJ = "spinoza"
|
5
|
+
|
6
|
+
def version
|
7
|
+
@version ||= begin
|
8
|
+
require 'spinoza/version'
|
9
|
+
warn "Spinoza::VERSION not a string" unless Spinoza::VERSION.kind_of? String
|
10
|
+
Spinoza::VERSION
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def tag
|
15
|
+
@tag ||= "#{PRJ}-#{version}"
|
16
|
+
end
|
17
|
+
|
18
|
+
desc "Run all tests"
|
19
|
+
task :test => %w{ test:unit }
|
20
|
+
|
21
|
+
namespace :test do
|
22
|
+
desc "Run unit tests"
|
23
|
+
Rake::TestTask.new :unit do |t|
|
24
|
+
t.libs << "lib"
|
25
|
+
t.test_files = FileList["test/*.rb"]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
desc "Commit, tag, and push repo; build and push gem"
|
30
|
+
task :release => "release:is_new_version" do
|
31
|
+
require 'tempfile'
|
32
|
+
|
33
|
+
sh "gem build #{PRJ}.gemspec"
|
34
|
+
|
35
|
+
file = Tempfile.new "template"
|
36
|
+
begin
|
37
|
+
file.puts "release #{version}"
|
38
|
+
file.close
|
39
|
+
sh "git commit --allow-empty -a -v -t #{file.path}"
|
40
|
+
ensure
|
41
|
+
file.close unless file.closed?
|
42
|
+
file.unlink
|
43
|
+
end
|
44
|
+
|
45
|
+
sh "git tag #{tag}"
|
46
|
+
sh "git push"
|
47
|
+
sh "git push --tags"
|
48
|
+
|
49
|
+
sh "gem push #{tag}.gem"
|
50
|
+
end
|
51
|
+
|
52
|
+
namespace :release do
|
53
|
+
desc "Diff to latest release"
|
54
|
+
task :diff do
|
55
|
+
latest = `git describe --abbrev=0 --tags --match '#{PRJ}-*'`.chomp
|
56
|
+
sh "git diff #{latest}"
|
57
|
+
end
|
58
|
+
|
59
|
+
desc "Log to latest release"
|
60
|
+
task :log do
|
61
|
+
latest = `git describe --abbrev=0 --tags --match '#{PRJ}-*'`.chomp
|
62
|
+
sh "git log #{latest}.."
|
63
|
+
end
|
64
|
+
|
65
|
+
task :is_new_version do
|
66
|
+
abort "#{tag} exists; update version!" unless `git tag -l #{tag}`.empty?
|
67
|
+
end
|
68
|
+
end
|
File without changes
|
@@ -0,0 +1,154 @@
|
|
1
|
+
require 'spinoza/common'
|
2
|
+
|
3
|
+
# Manages concurrency in the spinoza system model, which explicitly schedules
|
4
|
+
# all database reads and writes. So all this does is check for concurrency
|
5
|
+
# violations; nothing actually blocks.
|
6
|
+
class Spinoza::LockManager
|
7
|
+
class ConcurrencyError < StandardError; end
|
8
|
+
|
9
|
+
# State of lock on one resource (e.g., a row), which may be held concurrently
|
10
|
+
# by any number of threads. Reentrant.
|
11
|
+
class ReadLock
|
12
|
+
attr_reader :txns
|
13
|
+
|
14
|
+
def initialize txn
|
15
|
+
@txns = Hash.new(0)
|
16
|
+
add txn
|
17
|
+
end
|
18
|
+
|
19
|
+
def unlocked?
|
20
|
+
@txns.all? {|t,c| c == 0}
|
21
|
+
end
|
22
|
+
|
23
|
+
def includes? txn
|
24
|
+
@txns[txn] > 0
|
25
|
+
end
|
26
|
+
|
27
|
+
def add txn
|
28
|
+
@txns[txn] += 1
|
29
|
+
end
|
30
|
+
|
31
|
+
def remove txn
|
32
|
+
if includes? txn
|
33
|
+
@txns[txn] -= 1
|
34
|
+
@txns.delete txn if @txns[txn] == 0
|
35
|
+
else
|
36
|
+
raise ConcurrencyError, "#{self} is not locked by #{txn}"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# State of lock on one resource (e.g., a row), which may be held concurrently
|
42
|
+
# by at most one thread. Reentrant.
|
43
|
+
class WriteLock
|
44
|
+
attr_reader :txn, :count
|
45
|
+
|
46
|
+
def initialize txn
|
47
|
+
@txn = txn
|
48
|
+
@count = 1
|
49
|
+
end
|
50
|
+
|
51
|
+
def unlocked?
|
52
|
+
@txn == nil
|
53
|
+
end
|
54
|
+
|
55
|
+
def includes? txn
|
56
|
+
@txn == txn && @count > 0
|
57
|
+
end
|
58
|
+
|
59
|
+
def add txn
|
60
|
+
if includes? txn
|
61
|
+
@count += 1
|
62
|
+
else
|
63
|
+
raise ConcurrencyError, "#{self} is not locked by #{txn}"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def remove txn
|
68
|
+
if includes? txn
|
69
|
+
@count -= 1
|
70
|
+
@txn = nil if @count == 0
|
71
|
+
else
|
72
|
+
raise ConcurrencyError, "#{self} is not locked by #{txn}"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# { resource => WriteLock | ReadLock | nil, ... }
|
78
|
+
# typically, resource == [table, key]
|
79
|
+
attr_reader :locks
|
80
|
+
|
81
|
+
def initialize
|
82
|
+
@locks = {}
|
83
|
+
end
|
84
|
+
|
85
|
+
def lock_read resource, txn
|
86
|
+
case lock = locks[resource]
|
87
|
+
when nil
|
88
|
+
locks[resource] = ReadLock.new(txn)
|
89
|
+
when ReadLock
|
90
|
+
lock.add txn
|
91
|
+
when WriteLock
|
92
|
+
raise ConcurrencyError, "#{resource} is locked: #{lock}"
|
93
|
+
else raise
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def lock_write resource, txn
|
98
|
+
case lock = locks[resource]
|
99
|
+
when nil
|
100
|
+
locks[resource] = WriteLock.new(txn)
|
101
|
+
when WriteLock
|
102
|
+
lock.add txn
|
103
|
+
when ReadLock
|
104
|
+
raise ConcurrencyError, "#{resource} is locked: #{lock}"
|
105
|
+
else raise
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def unlock_read resource, txn
|
110
|
+
lock = locks[resource]
|
111
|
+
case lock
|
112
|
+
when WriteLock
|
113
|
+
raise ConcurrencyError, "#{resource} is write locked: #{lock}"
|
114
|
+
when nil
|
115
|
+
raise ConcurrencyError, "#{resource} is not locked"
|
116
|
+
when ReadLock
|
117
|
+
begin
|
118
|
+
lock.remove txn
|
119
|
+
locks.delete resource if lock.unlocked?
|
120
|
+
rescue ConcurrencyError => ex
|
121
|
+
raise ConcurrencyError "#{resource}: #{ex.message}"
|
122
|
+
end
|
123
|
+
else raise
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def unlock_write resource, txn
|
128
|
+
lock = locks[resource]
|
129
|
+
case lock
|
130
|
+
when ReadLock
|
131
|
+
raise ConcurrencyError, "#{resource} is read locked: #{lock}"
|
132
|
+
when nil
|
133
|
+
raise ConcurrencyError, "#{resource} is not locked"
|
134
|
+
when WriteLock
|
135
|
+
begin
|
136
|
+
lock.remove txn
|
137
|
+
locks.delete resource if lock.unlocked?
|
138
|
+
rescue ConcurrencyError => ex
|
139
|
+
raise ConcurrencyError "#{resource}: #{ex.message}"
|
140
|
+
end
|
141
|
+
else raise
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def has_read_lock? resource, txn
|
146
|
+
lock = locks[resource]
|
147
|
+
lock.kind_of?(ReadLock) && lock.includes?(txn)
|
148
|
+
end
|
149
|
+
|
150
|
+
def has_write_lock? resource, txn
|
151
|
+
lock = locks[resource]
|
152
|
+
lock.kind_of?(WriteLock) && lock.includes?(txn)
|
153
|
+
end
|
154
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'spinoza/system/store'
|
2
|
+
require 'spinoza/system/table-spec'
|
3
|
+
require 'spinoza/system/lock-manager'
|
4
|
+
|
5
|
+
# A top level entity in the system model, representing on whole node of
|
6
|
+
# the disrtibuted system, typically one per host.
|
7
|
+
class Spinoza::Node
|
8
|
+
attr_reader :store
|
9
|
+
attr_reader :lock_manager
|
10
|
+
|
11
|
+
# Create a node whose store contains the specified tables and which has
|
12
|
+
# its own lock manager.
|
13
|
+
def initialize *table_specs
|
14
|
+
@store = Store.new *table_specs
|
15
|
+
@lock_manager = LockManager.new
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'spinoza/common'
|
2
|
+
|
3
|
+
module Spinoza
|
4
|
+
# Operations are stateless and do not reference any particular store. Hence
|
5
|
+
# they can be serialized and passed around the system.
|
6
|
+
class Operation
|
7
|
+
# Name of table.
|
8
|
+
attr_reader :table
|
9
|
+
|
10
|
+
# Transaction this operation belongs to.
|
11
|
+
|
12
|
+
# ds is Sequel dataset representing the table
|
13
|
+
def execute ds
|
14
|
+
raise
|
15
|
+
end
|
16
|
+
|
17
|
+
# check that this operation is allowed to execute
|
18
|
+
# lm is LockManager
|
19
|
+
def check lm
|
20
|
+
raise
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class InsertOperation < Operation
|
25
|
+
attr_reader :row
|
26
|
+
def initialize txn = nil, table: table, row: row
|
27
|
+
@txn = txn
|
28
|
+
@table, @row = table, row
|
29
|
+
end
|
30
|
+
|
31
|
+
def execute ds
|
32
|
+
ds << row
|
33
|
+
end
|
34
|
+
|
35
|
+
def check lm
|
36
|
+
true
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class UpdateOperation < Operation
|
41
|
+
attr_reader :key, :row
|
42
|
+
def initialize txn = nil, table: table, row: row, key: key
|
43
|
+
@txn = txn
|
44
|
+
@table, @key, @row = table, key, row
|
45
|
+
end
|
46
|
+
|
47
|
+
def execute ds
|
48
|
+
ds.where(key).update(row)
|
49
|
+
end
|
50
|
+
|
51
|
+
def check lm
|
52
|
+
lm.has_write_lock? table, key, txn
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
class DeleteOperation < Operation
|
57
|
+
attr_reader :key
|
58
|
+
def initialize txn = nil, table: table, key: key
|
59
|
+
@txn = txn
|
60
|
+
@table, @key = table, key
|
61
|
+
end
|
62
|
+
|
63
|
+
def execute ds
|
64
|
+
ds.where(key).delete
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
class ReadOperation < Operation
|
69
|
+
attr_reader :key
|
70
|
+
def initialize txn = nil, table: table, key: key
|
71
|
+
@txn = txn
|
72
|
+
@table, @key = table, key
|
73
|
+
end
|
74
|
+
|
75
|
+
def execute ds
|
76
|
+
ReadResult.new(op: self, val: ds.where(key).all)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Result of executing a read operation on a particular node at a
|
81
|
+
# particular time.
|
82
|
+
class ReadResult
|
83
|
+
attr_reader :op, :val
|
84
|
+
def initialize op: op, val: val
|
85
|
+
@op, @val = op, val
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'sequel'
|
2
|
+
require 'spinoza/system/operation'
|
3
|
+
|
4
|
+
# Represents the storage at one node. In our model, this consists of an
|
5
|
+
# in-memory sqlite database with some very simple tables.
|
6
|
+
class Spinoza::Store
|
7
|
+
def initialize *table_specs
|
8
|
+
@db = Sequel.sqlite
|
9
|
+
@table_specs = table_specs
|
10
|
+
|
11
|
+
table_specs.each do |table_spec|
|
12
|
+
@db.create_table table_spec.name do
|
13
|
+
table_spec.column_specs.each do |col|
|
14
|
+
case col.type
|
15
|
+
when "integer", "string", "float"
|
16
|
+
column col.name, col.type, primary_key: col.primary
|
17
|
+
else
|
18
|
+
raise ArgumentError,
|
19
|
+
"Bad col.type: #{col.type} in table #{table_spec.name}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def tables
|
27
|
+
@db.tables
|
28
|
+
end
|
29
|
+
|
30
|
+
def inspect
|
31
|
+
"<#{self.class.name}: #{tables.join(', ')}>"
|
32
|
+
end
|
33
|
+
|
34
|
+
# Execute the operations on this store, skipping any that do not refer to
|
35
|
+
# a table in this store.
|
36
|
+
def execute *operations
|
37
|
+
results = operations.map do |op|
|
38
|
+
if tables.include? op.table
|
39
|
+
op.execute @db[op.table]
|
40
|
+
end
|
41
|
+
end
|
42
|
+
results.grep(ReadResult)
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'spinoza/common'
|
2
|
+
|
3
|
+
# Defines the schema for one table in all replicas.
|
4
|
+
class Spinoza::TableSpec
|
5
|
+
attr_reader :name
|
6
|
+
attr_reader :column_specs
|
7
|
+
|
8
|
+
class ColumnSpec
|
9
|
+
attr_reader :name
|
10
|
+
attr_reader :type
|
11
|
+
attr_reader :primary
|
12
|
+
|
13
|
+
def initialize name, type, primary
|
14
|
+
@name, @type, @primary = name, type, primary
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Usage:
|
19
|
+
#
|
20
|
+
# create_table :Widgets, id: "integer", weight: "float", name: "string"
|
21
|
+
#
|
22
|
+
# The first column is the primary key for the table.
|
23
|
+
#
|
24
|
+
def initialize name, **specs
|
25
|
+
@name = name
|
26
|
+
@column_specs = []
|
27
|
+
specs.each_with_index do |(col_name, col_type), i|
|
28
|
+
@column_specs << ColumnSpec.new(col_name, col_type, (i==0))
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'spinoza/system/operation'
|
2
|
+
|
3
|
+
class Spinoza::Transaction
|
4
|
+
attr_reader :ops
|
5
|
+
|
6
|
+
class RowLocation
|
7
|
+
def initialize txn, table, key
|
8
|
+
@txn, @table, @key = txn, table, key
|
9
|
+
end
|
10
|
+
|
11
|
+
def insert row
|
12
|
+
@txn.ops << InsertOperation.new(@txn, table: @table, row: row)
|
13
|
+
end
|
14
|
+
|
15
|
+
def update row
|
16
|
+
@txn.ops << UpdateOperation.new(@txn, table: @table, row: row, key: @key)
|
17
|
+
end
|
18
|
+
|
19
|
+
def delete
|
20
|
+
@txn.ops << DeleteOperation.new(@txn, table: @table, key: @key)
|
21
|
+
end
|
22
|
+
|
23
|
+
def read
|
24
|
+
@txn.ops << ReadOperation.new(@txn, table: @table, key: @key)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# A txn is just a list of operations, each on a specific table and key.
|
29
|
+
# The ACID guarantees are not provided by this class.
|
30
|
+
#
|
31
|
+
# Example:
|
32
|
+
#
|
33
|
+
# txn = transaction do
|
34
|
+
# at(:persons, name: "Fred").update(age: 41, phrase: "yabba dabba doo")
|
35
|
+
# at(:persons, name: "Wilma").delete
|
36
|
+
# at(:persons, name: "Barney").read
|
37
|
+
# at(:persons).insert(name: "Betty", age: 65)
|
38
|
+
# end
|
39
|
+
#
|
40
|
+
# node.store.execute txn # ==> [ReadResult(...)]
|
41
|
+
#
|
42
|
+
def initialize &block
|
43
|
+
@ops = []
|
44
|
+
if block
|
45
|
+
if block.arity == 0
|
46
|
+
instance_eval &block
|
47
|
+
else
|
48
|
+
yield self
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def at table, **key
|
54
|
+
RowLocation.new(self, table, key)
|
55
|
+
end
|
56
|
+
end
|
data/test/test-node.rb
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'spinoza/system/node'
|
3
|
+
|
4
|
+
include Spinoza
|
5
|
+
|
6
|
+
class TestNode < Minitest::Test
|
7
|
+
def setup
|
8
|
+
@node = Node.new(
|
9
|
+
TableSpec.new :foos, id: "integer", name: "string", len: "float"
|
10
|
+
)
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_store
|
14
|
+
assert_equal [:foos], @node.store.tables
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_ops
|
18
|
+
store = @node.store
|
19
|
+
|
20
|
+
op_i = InsertOperation.new(table: :foos, row: {id: 1, name: "a", len: 1.2})
|
21
|
+
op_r = ReadOperation.new(table: :foos, key: {id: 1})
|
22
|
+
rslt = store.execute op_i, op_r
|
23
|
+
|
24
|
+
assert_equal(1, rslt.size)
|
25
|
+
r_tuples = rslt[0].val
|
26
|
+
assert_equal(1, r_tuples.size)
|
27
|
+
assert_equal({id: 1, name: "a", len: 1.2}, r_tuples[0])
|
28
|
+
|
29
|
+
op_u = UpdateOperation.new(table: :foos, key: {id: 1}, row: {name: "b"})
|
30
|
+
rslt = store.execute op_u, op_r
|
31
|
+
|
32
|
+
assert_equal(1, rslt.size)
|
33
|
+
r_tuples = rslt[0].val
|
34
|
+
assert_equal(1, r_tuples.size)
|
35
|
+
assert_equal({id: 1, name: "b", len: 1.2}, r_tuples[0])
|
36
|
+
|
37
|
+
op_d = DeleteOperation.new(table: :foos, key: {id: 1})
|
38
|
+
rslt = store.execute op_d, op_r
|
39
|
+
assert_equal(1, rslt.size)
|
40
|
+
r_tuples = rslt[0].val
|
41
|
+
assert_equal(0, r_tuples.size)
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_locks
|
45
|
+
lm = @node.lock_manager
|
46
|
+
|
47
|
+
rs1 = [:foos, 1]
|
48
|
+
rs2 = [:foos, 2]
|
49
|
+
|
50
|
+
lm.lock_read rs1, :t1
|
51
|
+
assert lm.has_read_lock?(rs1, :t1)
|
52
|
+
refute lm.has_read_lock?(rs1, :t2)
|
53
|
+
|
54
|
+
lm.lock_read rs1, :t2
|
55
|
+
assert lm.has_read_lock?(rs1, :t1)
|
56
|
+
assert lm.has_read_lock?(rs1, :t2)
|
57
|
+
|
58
|
+
lm.unlock_read rs1, :t1
|
59
|
+
refute lm.has_read_lock?(rs1, :t1)
|
60
|
+
assert lm.has_read_lock?(rs1, :t2)
|
61
|
+
|
62
|
+
assert_raises LockManager::ConcurrencyError do
|
63
|
+
lm.lock_write rs1, :t1
|
64
|
+
end
|
65
|
+
|
66
|
+
lm.lock_read rs1, :t2
|
67
|
+
assert lm.has_read_lock?(rs1, :t2)
|
68
|
+
lm.unlock_read rs1, :t2
|
69
|
+
assert lm.has_read_lock?(rs1, :t2)
|
70
|
+
lm.unlock_read rs1, :t2
|
71
|
+
refute lm.has_read_lock?(rs1, :t2)
|
72
|
+
|
73
|
+
lm.lock_write rs2, :t1
|
74
|
+
assert lm.has_write_lock?(rs2, :t1)
|
75
|
+
assert_raises LockManager::ConcurrencyError do
|
76
|
+
lm.lock_read rs2, :t2
|
77
|
+
end
|
78
|
+
assert lm.has_write_lock?(rs2, :t1)
|
79
|
+
|
80
|
+
lm.lock_write rs2, :t1
|
81
|
+
assert lm.has_write_lock?(rs2, :t1)
|
82
|
+
lm.unlock_write rs2, :t1
|
83
|
+
assert lm.has_write_lock?(rs2, :t1)
|
84
|
+
lm.unlock_write rs2, :t1
|
85
|
+
refute lm.has_write_lock?(rs2, :t1)
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'spinoza/system/node'
|
3
|
+
require 'spinoza/transaction'
|
4
|
+
|
5
|
+
include Spinoza
|
6
|
+
|
7
|
+
class TestTransaction < Minitest::Test
|
8
|
+
def setup
|
9
|
+
@node = Node.new(
|
10
|
+
TableSpec.new :foos, id: "integer", name: "string", len: "float"
|
11
|
+
)
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_txn
|
15
|
+
txn = Transaction.new do
|
16
|
+
at(:foos).insert id: 1, name: "a", len: 1.2
|
17
|
+
at(:foos).insert id: 2, name: "b", len: 3.4
|
18
|
+
at(:foos, id: 2).read
|
19
|
+
at(:foos).insert id: 3, name: "c", len: 5.6
|
20
|
+
at(:foos, id: 2).delete
|
21
|
+
at(:foos, id: 2).read
|
22
|
+
at(:foos, id: 3).update len: 7.8
|
23
|
+
at(:foos, id: 3).read
|
24
|
+
end
|
25
|
+
|
26
|
+
rslt = @node.store.execute(*txn.ops)
|
27
|
+
assert_equal(3, rslt.size)
|
28
|
+
|
29
|
+
assert_equal(1, rslt[0].val.size)
|
30
|
+
assert_equal({id: 2, name: "b", len: 3.4}, rslt[0].val[0])
|
31
|
+
|
32
|
+
assert_equal(0, rslt[1].val.size)
|
33
|
+
|
34
|
+
assert_equal(1, rslt[2].val.size)
|
35
|
+
assert_equal({id: 3, name: "c", len: 7.8}, rslt[2].val[0])
|
36
|
+
end
|
37
|
+
end
|
metadata
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: spinoza
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '0.1'
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Joel VanderWerf
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-05-24 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: sequel
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: sqlite3
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
description: Model of Calvin distributed database.
|
42
|
+
email: vjoel@users.sourceforge.net
|
43
|
+
executables: []
|
44
|
+
extensions: []
|
45
|
+
extra_rdoc_files:
|
46
|
+
- README.md
|
47
|
+
- COPYING
|
48
|
+
files:
|
49
|
+
- COPYING
|
50
|
+
- README.md
|
51
|
+
- Rakefile
|
52
|
+
- lib/spinoza/common.rb
|
53
|
+
- lib/spinoza/system/link.rb
|
54
|
+
- lib/spinoza/system/lock-manager.rb
|
55
|
+
- lib/spinoza/system/node.rb
|
56
|
+
- lib/spinoza/system/operation.rb
|
57
|
+
- lib/spinoza/system/store.rb
|
58
|
+
- lib/spinoza/system/table-spec.rb
|
59
|
+
- lib/spinoza/transaction.rb
|
60
|
+
- lib/spinoza/version.rb
|
61
|
+
- test/test-node.rb
|
62
|
+
- test/test-transaction.rb
|
63
|
+
homepage: https://github.com/vjoel/spinoza
|
64
|
+
licenses:
|
65
|
+
- BSD
|
66
|
+
metadata: {}
|
67
|
+
post_install_message:
|
68
|
+
rdoc_options:
|
69
|
+
- "--quiet"
|
70
|
+
- "--line-numbers"
|
71
|
+
- "--inline-source"
|
72
|
+
- "--title"
|
73
|
+
- spinoza
|
74
|
+
- "--main"
|
75
|
+
- README.md
|
76
|
+
require_paths:
|
77
|
+
- lib
|
78
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '2.0'
|
83
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
84
|
+
requirements:
|
85
|
+
- - ">="
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '0'
|
88
|
+
requirements: []
|
89
|
+
rubyforge_project:
|
90
|
+
rubygems_version: 2.2.2
|
91
|
+
signing_key:
|
92
|
+
specification_version: 4
|
93
|
+
summary: Model of Calvin distributed database.
|
94
|
+
test_files:
|
95
|
+
- test/test-node.rb
|
96
|
+
- test/test-transaction.rb
|
97
|
+
has_rdoc:
|