relaxo 0.4.7 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.rspec +3 -0
- data/.travis.yml +6 -3
- data/Gemfile +9 -2
- data/README.md +67 -60
- data/Rakefile +12 -0
- data/lib/relaxo.rb +6 -15
- data/lib/relaxo/{connection.rb → changeset.rb} +59 -27
- data/lib/relaxo/database.rb +67 -83
- data/lib/relaxo/{json.rb → dataset.rb} +36 -9
- data/lib/relaxo/directory.rb +97 -0
- data/lib/relaxo/version.rb +1 -1
- data/relaxo.gemspec +1 -2
- data/spec/relaxo/changeset_spec.rb +39 -0
- data/spec/relaxo/concurrency_spec.rb +39 -0
- data/spec/relaxo/database_spec.rb +68 -33
- data/spec/relaxo/enumeration_spec.rb +30 -0
- data/spec/relaxo/performance_spec.rb +78 -0
- data/spec/relaxo/test_records.rb +25 -0
- metadata +18 -29
- data/lib/relaxo/attachments.rb +0 -65
- data/lib/relaxo/client.rb +0 -168
- data/lib/relaxo/transaction.rb +0 -116
- data/spec/relaxo/connection_spec.rb +0 -29
- data/spec/relaxo/spec_helper.rb +0 -4
- data/spec/relaxo/transaction_spec.rb +0 -63
@@ -18,16 +18,43 @@
|
|
18
18
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
19
|
# THE SOFTWARE.
|
20
20
|
|
21
|
-
|
21
|
+
require 'rugged'
|
22
22
|
|
23
|
-
|
24
|
-
end
|
23
|
+
require_relative 'directory'
|
25
24
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
25
|
+
module Relaxo
|
26
|
+
class Dataset
|
27
|
+
def initialize(repository, tree)
|
28
|
+
@repository = repository
|
29
|
+
@tree = tree
|
30
|
+
|
31
|
+
@directories = {}
|
32
|
+
end
|
33
|
+
|
34
|
+
def read(path)
|
35
|
+
if entry = @tree.path(path) and entry[:type] == :blob and oid = entry[:oid]
|
36
|
+
@repository.read(oid)
|
37
|
+
end
|
38
|
+
rescue Rugged::TreeError
|
39
|
+
return nil
|
40
|
+
end
|
41
|
+
|
42
|
+
alias [] read
|
43
|
+
|
44
|
+
def exist?(path)
|
45
|
+
read(path) != nil
|
46
|
+
end
|
47
|
+
|
48
|
+
def each(path = nil, &block)
|
49
|
+
return to_enum(:each, path) unless block_given?
|
50
|
+
|
51
|
+
directory(path).each(&block)
|
52
|
+
end
|
53
|
+
|
54
|
+
protected
|
55
|
+
|
56
|
+
def directory(path = nil)
|
57
|
+
@directories[path] ||= Directory.new(@repository, @tree, path)
|
58
|
+
end
|
30
59
|
end
|
31
|
-
|
32
|
-
Relaxo::JSON = JSON
|
33
60
|
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
# Copyright, 2017, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
require 'rugged'
|
22
|
+
|
23
|
+
module Relaxo
|
24
|
+
class Directory
|
25
|
+
def initialize(repository, tree, path)
|
26
|
+
@repository = repository
|
27
|
+
@tree = tree
|
28
|
+
@path = path
|
29
|
+
|
30
|
+
@entries = nil
|
31
|
+
@changes = {}
|
32
|
+
end
|
33
|
+
|
34
|
+
def freeze
|
35
|
+
@changes.freeze
|
36
|
+
|
37
|
+
super
|
38
|
+
end
|
39
|
+
|
40
|
+
def entries
|
41
|
+
@entries ||= load_entries!
|
42
|
+
end
|
43
|
+
|
44
|
+
def each(&block)
|
45
|
+
return to_enum(:each) unless block_given?
|
46
|
+
|
47
|
+
entries.each do |entry|
|
48
|
+
entry[:object] ||= @repository.read(entry[:oid])
|
49
|
+
|
50
|
+
yield entry[:name], entry[:object]
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def insert(entry)
|
55
|
+
_, _, name = entry[:name].rpartition('/')
|
56
|
+
|
57
|
+
@changes[name] = entry
|
58
|
+
|
59
|
+
# Blow away the cache:
|
60
|
+
@entries = nil
|
61
|
+
end
|
62
|
+
|
63
|
+
def delete(entry)
|
64
|
+
_, _, name = entry[:name].rpartition('/')
|
65
|
+
|
66
|
+
@changes[name] = nil
|
67
|
+
|
68
|
+
# Blow away the cache:
|
69
|
+
@entries = nil
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def fetch_tree(path = @path)
|
75
|
+
entry = @tree.path(path)
|
76
|
+
|
77
|
+
Rugged::Tree.new(@repository, entry[:oid])
|
78
|
+
rescue Rugged::TreeError
|
79
|
+
return nil
|
80
|
+
end
|
81
|
+
|
82
|
+
# Load the entries from the tree, applying any changes.
|
83
|
+
def load_entries!
|
84
|
+
entries = @changes.dup
|
85
|
+
|
86
|
+
if tree = fetch_tree
|
87
|
+
tree.each_blob do |entry|
|
88
|
+
unless entries.key? entry[:name]
|
89
|
+
entries[entry[:name]] = entry
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
return entries.values.compact.sort_by{|entry| entry[:name]}
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
data/lib/relaxo/version.rb
CHANGED
data/relaxo.gemspec
CHANGED
@@ -22,8 +22,7 @@ Gem::Specification.new do |spec|
|
|
22
22
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
23
23
|
spec.require_paths = ["lib"]
|
24
24
|
|
25
|
-
spec.add_dependency "
|
26
|
-
spec.add_dependency "rest-client"
|
25
|
+
spec.add_dependency "rugged"
|
27
26
|
|
28
27
|
spec.add_development_dependency "rspec", "~> 3.4.0"
|
29
28
|
spec.add_development_dependency "bundler", "~> 1.3"
|
@@ -0,0 +1,39 @@
|
|
1
|
+
|
2
|
+
require_relative 'test_records'
|
3
|
+
|
4
|
+
RSpec.describe Relaxo::Changeset do
|
5
|
+
include_context "test records"
|
6
|
+
|
7
|
+
it "should enumerate all documents including writes" do
|
8
|
+
records = []
|
9
|
+
|
10
|
+
database.commit(message: "Testing Enumeration") do |dataset|
|
11
|
+
5.times do |i|
|
12
|
+
object = dataset.append("extra-#{i}")
|
13
|
+
dataset.write("#{prefix}/extra-#{i}", object)
|
14
|
+
end
|
15
|
+
|
16
|
+
expect(dataset.exist?("#{prefix}/extra-0")).to be_truthy
|
17
|
+
|
18
|
+
records = dataset.each(prefix).to_a
|
19
|
+
end
|
20
|
+
|
21
|
+
expect(records.count).to be 25
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should enumerate all documents excluding deletes" do
|
25
|
+
records = []
|
26
|
+
|
27
|
+
database.commit(message: "Testing Enumeration") do |dataset|
|
28
|
+
5.times do |i|
|
29
|
+
dataset.delete("#{prefix}/#{i}")
|
30
|
+
end
|
31
|
+
|
32
|
+
expect(dataset.exist?("#{prefix}/0")).to be_falsey
|
33
|
+
|
34
|
+
records = dataset.each(prefix).to_a
|
35
|
+
end
|
36
|
+
|
37
|
+
expect(records.count).to be 15
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
|
2
|
+
require_relative 'test_records'
|
3
|
+
|
4
|
+
RSpec.describe Relaxo::Changeset do
|
5
|
+
include_context "test records"
|
6
|
+
|
7
|
+
it "should detect conflicts" do
|
8
|
+
events = []
|
9
|
+
|
10
|
+
alice = Fiber.new do
|
11
|
+
database.commit(message: "Alice Data") do |changeset|
|
12
|
+
events << :alice
|
13
|
+
|
14
|
+
object = changeset.append("sample-data-1")
|
15
|
+
changeset.write("conflict-path", object)
|
16
|
+
|
17
|
+
Fiber.yield
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
bob = Fiber.new do
|
22
|
+
database.commit(message: "Bob Data") do |changeset|
|
23
|
+
events << :bob
|
24
|
+
|
25
|
+
object = changeset.append("sample-data-1")
|
26
|
+
changeset.write("conflict-path", object)
|
27
|
+
|
28
|
+
Fiber.yield
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
alice.resume
|
33
|
+
bob.resume
|
34
|
+
alice.resume
|
35
|
+
bob.resume
|
36
|
+
|
37
|
+
expect(events).to be == [:alice, :bob, :bob]
|
38
|
+
end
|
39
|
+
end
|
@@ -1,52 +1,87 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
1
|
|
3
2
|
require 'relaxo'
|
4
|
-
require 'relaxo/attachments'
|
5
|
-
|
6
|
-
require_relative 'spec_helper'
|
7
3
|
|
8
4
|
RSpec.describe Relaxo::Database do
|
9
|
-
|
10
|
-
|
11
|
-
|
5
|
+
let(:database_path) {File.join(__dir__, 'test')}
|
6
|
+
|
7
|
+
let(:database) {Relaxo.connect(database_path, test_key: "test_value")}
|
8
|
+
|
9
|
+
let(:document_path) {'test/document.json'}
|
10
|
+
let(:sample_json) {'[1, 2, 3]'}
|
11
|
+
|
12
|
+
before(:each) {FileUtils.rm_rf(database_path)}
|
13
|
+
|
14
|
+
it "should be initially empty" do
|
15
|
+
expect(database).to be_empty
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should not be empty with one document" do
|
19
|
+
database.commit(message: "Create test document") do |dataset|
|
20
|
+
oid = dataset.append(sample_json)
|
21
|
+
dataset.write(document_path, oid)
|
22
|
+
end
|
12
23
|
|
13
|
-
|
14
|
-
|
24
|
+
expect(database).to_not be_empty
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should have metadata" do
|
28
|
+
expect(database[:test_key]).to be == "test_value"
|
29
|
+
end
|
30
|
+
|
31
|
+
it "should create a document" do
|
32
|
+
database.commit(message: "Create test document") do |dataset|
|
33
|
+
oid = dataset.append(sample_json)
|
34
|
+
dataset.write(document_path, oid)
|
15
35
|
end
|
16
36
|
|
17
|
-
|
37
|
+
database.current do |dataset|
|
38
|
+
expect(dataset[document_path].data).to be == sample_json
|
39
|
+
end
|
18
40
|
end
|
19
41
|
|
20
|
-
it "should
|
21
|
-
|
42
|
+
it "should erase a document" do
|
43
|
+
database.commit(message: "Create test document") do |dataset|
|
44
|
+
oid = dataset.append(sample_json)
|
45
|
+
dataset.write(document_path, oid)
|
46
|
+
end
|
22
47
|
|
23
|
-
|
48
|
+
database.commit(message: "Delete test document") do |dataset|
|
49
|
+
dataset.delete(document_path)
|
50
|
+
end
|
24
51
|
|
25
|
-
|
52
|
+
database.current do |dataset|
|
53
|
+
expect(dataset[document_path]).to be nil
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should create multiple documents" do
|
58
|
+
database.commit(message: "Create first document") do |dataset|
|
59
|
+
oid = dataset.append(sample_json)
|
60
|
+
dataset.write(document_path, oid)
|
61
|
+
end
|
26
62
|
|
27
|
-
|
28
|
-
|
63
|
+
database.commit(message: "Create second document") do |dataset|
|
64
|
+
oid = dataset.append(sample_json)
|
65
|
+
dataset.write(document_path + '2', oid)
|
66
|
+
end
|
29
67
|
|
30
|
-
|
31
|
-
|
32
|
-
expect(
|
68
|
+
database.current do |dataset|
|
69
|
+
expect(dataset[document_path].data).to be == sample_json
|
70
|
+
expect(dataset[document_path + '2'].data).to be == sample_json
|
33
71
|
end
|
34
72
|
end
|
35
73
|
|
36
|
-
it "
|
37
|
-
document
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
}
|
45
|
-
|
46
|
-
result = @database.save(document)
|
47
|
-
expect(result['ok']).to be true
|
74
|
+
it "can enumerate documents" do
|
75
|
+
database.commit(message: "Create first document") do |dataset|
|
76
|
+
oid = dataset.append(sample_json)
|
77
|
+
|
78
|
+
10.times do |id|
|
79
|
+
dataset.write(document_path + "-#{id}", oid)
|
80
|
+
end
|
81
|
+
end
|
48
82
|
|
49
|
-
|
50
|
-
|
83
|
+
database.current do |dataset|
|
84
|
+
expect(dataset.each('test').count).to be == 10
|
85
|
+
end
|
51
86
|
end
|
52
87
|
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
|
2
|
+
require_relative 'test_records'
|
3
|
+
|
4
|
+
RSpec.describe Relaxo::Dataset do
|
5
|
+
include_context "test records"
|
6
|
+
|
7
|
+
it "should enumerate all documents" do
|
8
|
+
records = []
|
9
|
+
|
10
|
+
database.current do |dataset|
|
11
|
+
records = dataset.each(prefix).to_a
|
12
|
+
end
|
13
|
+
|
14
|
+
expect(records.count).to be 20
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
RSpec.describe Relaxo::Changeset do
|
19
|
+
include_context "test records"
|
20
|
+
|
21
|
+
it "should enumerate all documents" do
|
22
|
+
records = []
|
23
|
+
|
24
|
+
database.commit(message: "Testing Enumeration") do |dataset|
|
25
|
+
records = dataset.each(prefix).to_a
|
26
|
+
end
|
27
|
+
|
28
|
+
expect(records.count).to be 20
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
|
2
|
+
require 'benchmark/ips' if ENV['BENCHMARK']
|
3
|
+
require 'ruby-prof' if ENV['PROFILE']
|
4
|
+
require 'flamegraph' if ENV['FLAMEGRAPH']
|
5
|
+
|
6
|
+
RSpec.describe "Relaxo Performance" do
|
7
|
+
let(:database_path) {File.join(__dir__, 'test')}
|
8
|
+
let(:database) {Relaxo.connect(database_path)}
|
9
|
+
|
10
|
+
if defined? Benchmark
|
11
|
+
def benchmark(name = nil)
|
12
|
+
Benchmark.ips do |benchmark|
|
13
|
+
# Collect more data for benchmark:
|
14
|
+
benchmark.time = 20
|
15
|
+
benchmark.warmup = 10
|
16
|
+
|
17
|
+
benchmark.report(name) do |i|
|
18
|
+
yield i
|
19
|
+
end
|
20
|
+
|
21
|
+
benchmark.compare!
|
22
|
+
end
|
23
|
+
end
|
24
|
+
elsif defined? RubyProf
|
25
|
+
def benchmark(name)
|
26
|
+
result = RubyProf.profile do
|
27
|
+
yield 1000
|
28
|
+
end
|
29
|
+
|
30
|
+
#result.eliminate_methods!([/^((?!Utopia).)*$/])
|
31
|
+
printer = RubyProf::FlatPrinter.new(result)
|
32
|
+
printer.print($stderr, min_percent: 1.0)
|
33
|
+
|
34
|
+
printer = RubyProf::GraphHtmlPrinter.new(result)
|
35
|
+
filename = name.gsub('/', '_') + '.html'
|
36
|
+
File.open(filename, "w") do |file|
|
37
|
+
printer.print(file)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
elsif defined? Flamegraph
|
41
|
+
def benchmark(name)
|
42
|
+
filename = name.gsub('/', '_') + '.html'
|
43
|
+
Flamegraph.generate(filename) do
|
44
|
+
yield 1
|
45
|
+
end
|
46
|
+
end
|
47
|
+
else
|
48
|
+
def benchmark(name)
|
49
|
+
yield 1
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
before(:each) do
|
54
|
+
FileUtils.rm_rf(database_path)
|
55
|
+
end
|
56
|
+
|
57
|
+
it "single transaction should be fast" do
|
58
|
+
benchmark("single") do |iterations|
|
59
|
+
database.commit(message: "Some Documents") do |dataset|
|
60
|
+
iterations.times do |i|
|
61
|
+
object = dataset.append("good-#{i}")
|
62
|
+
dataset.write("#{i%100}/#{i}", object)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
it "multiple transactions should be fast" do
|
69
|
+
benchmark("multiple") do |iterations|
|
70
|
+
iterations.times do |i|
|
71
|
+
database.commit(message: "Some Documents") do |dataset|
|
72
|
+
object = dataset.append("good-#{i}")
|
73
|
+
dataset.write("#{i%100}/#{i}", object)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|