meangirls 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/Gemfile +3 -0
- data/README.markdown +243 -0
- data/Rakefile +1 -0
- data/lib/meangirls.rb +58 -0
- data/lib/meangirls/counter.rb +33 -0
- data/lib/meangirls/crdt.rb +15 -0
- data/lib/meangirls/g_counter.rb +82 -0
- data/lib/meangirls/lww_set.rb +168 -0
- data/lib/meangirls/or_set.rb +134 -0
- data/lib/meangirls/set.rb +63 -0
- data/lib/meangirls/two_phase_set.rb +92 -0
- data/lib/meangirls/version.rb +3 -0
- data/meangirls.gemspec +24 -0
- data/spec/counter.rb +58 -0
- data/spec/crdt.rb +16 -0
- data/spec/g_counter.rb +14 -0
- data/spec/init.rb +9 -0
- data/spec/lww_set.rb +65 -0
- data/spec/or_set.rb +41 -0
- data/spec/prob.rb +93 -0
- data/spec/run +21 -0
- data/spec/set.rb +186 -0
- data/spec/two_phase_set.rb +37 -0
- metadata +159 -0
data/meangirls.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'meangirls/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "meangirls"
|
8
|
+
spec.version = Meangirls::VERSION
|
9
|
+
spec.authors = ["Kyle Kingsbury"]
|
10
|
+
spec.email = ["aphyr@aphyr.com"]
|
11
|
+
spec.description = %q{Serializable data types for eventually consistent systems.}
|
12
|
+
spec.summary = spec.description
|
13
|
+
spec.homepage = "https://github.com/aphyr/meangirls"
|
14
|
+
|
15
|
+
spec.files = `git ls-files`.split($/)
|
16
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
17
|
+
spec.require_paths = ["lib"]
|
18
|
+
|
19
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
20
|
+
spec.add_development_dependency "rake"
|
21
|
+
spec.add_development_dependency "bacon"
|
22
|
+
spec.add_development_dependency "mocha-on-bacon"
|
23
|
+
spec.add_development_dependency "yajl-ruby"
|
24
|
+
end
|
data/spec/counter.rb
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
shared :counter do
|
2
|
+
should 'create a counter' do
|
3
|
+
@class.new.should.be.kind_of? @class
|
4
|
+
end
|
5
|
+
|
6
|
+
should '==' do
|
7
|
+
a = @class.new
|
8
|
+
b = @class.new
|
9
|
+
a.should == b
|
10
|
+
|
11
|
+
a += 1
|
12
|
+
a.should.not == b
|
13
|
+
b += 2
|
14
|
+
a.should.not == b
|
15
|
+
a += 1
|
16
|
+
a.should == b
|
17
|
+
|
18
|
+
a.increment('foo', 2)
|
19
|
+
a.should.not == b
|
20
|
+
b.increment('foo', 2)
|
21
|
+
a.should == b
|
22
|
+
end
|
23
|
+
|
24
|
+
should '===' do
|
25
|
+
@s.should === @s
|
26
|
+
@s.should === 0
|
27
|
+
@s.should === 0.0
|
28
|
+
end
|
29
|
+
|
30
|
+
should '+' do
|
31
|
+
a = @s + 1
|
32
|
+
a.should.be.kind_of? Meangirls::Counter
|
33
|
+
a.should === 1
|
34
|
+
|
35
|
+
b = @class.new.increment('foo', 2)
|
36
|
+
b += 3
|
37
|
+
b.should === 5
|
38
|
+
end
|
39
|
+
|
40
|
+
should '-' do
|
41
|
+
end
|
42
|
+
|
43
|
+
should 'float?' do
|
44
|
+
@class.new.increment(1)
|
45
|
+
end
|
46
|
+
|
47
|
+
should 'merge' do
|
48
|
+
end
|
49
|
+
|
50
|
+
should 'increment' do
|
51
|
+
end
|
52
|
+
|
53
|
+
should 'to_i' do
|
54
|
+
end
|
55
|
+
|
56
|
+
should 'to_f' do
|
57
|
+
end
|
58
|
+
end
|
data/spec/crdt.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'yajl/json_gem'
|
2
|
+
|
3
|
+
shared :crdt do
|
4
|
+
should 'as_json' do
|
5
|
+
j = @s.as_json
|
6
|
+
j.should.be.kind_of? Hash
|
7
|
+
j['type'].should == @s.type
|
8
|
+
end
|
9
|
+
|
10
|
+
should 'round trip' do
|
11
|
+
@examples.each do |t|
|
12
|
+
Meangirls.parse(t.as_json).should == t
|
13
|
+
Meangirls.parse(JSON.parse(t.as_json.to_json)).should == t
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/spec/g_counter.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
describe 'g-counter' do
|
2
|
+
before do
|
3
|
+
@class = Meangirls::GCounter
|
4
|
+
@s = @class.new
|
5
|
+
@examples = [
|
6
|
+
@class.new,
|
7
|
+
@class.new.increment('a', 1),
|
8
|
+
@class.new.increment('a', 1).increment('b', 0).increment('c', 1500.125)
|
9
|
+
]
|
10
|
+
end
|
11
|
+
|
12
|
+
behaves_like :crdt
|
13
|
+
behaves_like :counter
|
14
|
+
end
|
data/spec/init.rb
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
require 'bacon'
|
2
|
+
require 'mocha-on-bacon'
|
3
|
+
require "#{File.expand_path(File.dirname(__FILE__))}/prob"
|
4
|
+
require "#{File.expand_path(File.dirname(__FILE__))}/crdt"
|
5
|
+
require "#{File.expand_path(File.dirname(__FILE__))}/set"
|
6
|
+
require "#{File.expand_path(File.dirname(__FILE__))}/counter"
|
7
|
+
require "#{File.expand_path(File.dirname(__FILE__))}/../lib/meangirls"
|
8
|
+
|
9
|
+
Bacon.summary_on_exit
|
data/spec/lww_set.rb
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
describe 'lww-set' do
|
2
|
+
before do
|
3
|
+
@class = Meangirls::LWWSet
|
4
|
+
@idempotent = false
|
5
|
+
@s = @class.new
|
6
|
+
@examples = [
|
7
|
+
@class.new,
|
8
|
+
(@class.new << 1),
|
9
|
+
(@class.new - [1,2]),
|
10
|
+
(@class.new - [1,2] + [2,3]),
|
11
|
+
(@class.new + [1,2] - [2,3])
|
12
|
+
]
|
13
|
+
end
|
14
|
+
|
15
|
+
behaves_like :crdt
|
16
|
+
behaves_like :set
|
17
|
+
behaves_like :prob
|
18
|
+
|
19
|
+
should '==' do
|
20
|
+
a = @class.new
|
21
|
+
b = @class.new
|
22
|
+
a.add 1, 0
|
23
|
+
b.add 1, 0
|
24
|
+
a.should == b
|
25
|
+
|
26
|
+
a.delete 1, 1
|
27
|
+
a.should.not == b
|
28
|
+
b.delete 1, 1
|
29
|
+
a.should == b
|
30
|
+
end
|
31
|
+
|
32
|
+
should 'preserve latest writes' do
|
33
|
+
crdt = @class.new
|
34
|
+
test_merge crdt do |siblings, merged|
|
35
|
+
# Reconstruct transaction log.
|
36
|
+
log = []
|
37
|
+
siblings.each do |s|
|
38
|
+
s.e.each do |e, pair|
|
39
|
+
if t = pair.add
|
40
|
+
log << [t, :add, e]
|
41
|
+
end
|
42
|
+
if t = pair.remove
|
43
|
+
log << [t, :remove, e]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
log.sort_by!(&:first)
|
48
|
+
|
49
|
+
# Todo: compact log, removing identical timestamps and choosing operation based on crdt.bias.
|
50
|
+
# Not needed currently as local timestamps are guaranteed unique within process.
|
51
|
+
|
52
|
+
# Replay log on top of original
|
53
|
+
model = log.inject(crdt.to_set) do |set, op|
|
54
|
+
case op[1]
|
55
|
+
when :add
|
56
|
+
set.add op.last
|
57
|
+
when :remove
|
58
|
+
set.delete op.last
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
model == merged.to_set
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
data/spec/or_set.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
describe 'or-set' do
|
2
|
+
before do
|
3
|
+
@class = Meangirls::ORSet
|
4
|
+
@idempotent = false
|
5
|
+
@s = @class.new
|
6
|
+
@examples = [
|
7
|
+
@class.new,
|
8
|
+
(@class.new << 1),
|
9
|
+
(@class.new - [1,2]),
|
10
|
+
(@class.new - [1,2] + [2,3]),
|
11
|
+
(@class.new + [1,2] - [2,3])
|
12
|
+
]
|
13
|
+
end
|
14
|
+
|
15
|
+
behaves_like :crdt
|
16
|
+
behaves_like :set
|
17
|
+
behaves_like :prob
|
18
|
+
|
19
|
+
should '==' do
|
20
|
+
a = @class.new
|
21
|
+
b = @class.new
|
22
|
+
a.add 1, 0
|
23
|
+
b.add 1, 0
|
24
|
+
a.should == b
|
25
|
+
|
26
|
+
a.delete 1
|
27
|
+
a.should.not == b
|
28
|
+
b.delete 1
|
29
|
+
a.should == b
|
30
|
+
end
|
31
|
+
|
32
|
+
should 'preserve independent adds' do
|
33
|
+
test_merge @class.new do |sibs, merged|
|
34
|
+
sibs.inject(Set.new) do |present, sib|
|
35
|
+
present | sib.to_set
|
36
|
+
end.all? do |e|
|
37
|
+
merged.include? e
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/spec/prob.rb
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
shared :prob do
|
2
|
+
COUNT ||= 10000
|
3
|
+
|
4
|
+
require 'pp'
|
5
|
+
def serialize(crdt)
|
6
|
+
crdt.to_json
|
7
|
+
end
|
8
|
+
|
9
|
+
def deserialize(text)
|
10
|
+
Meangirls.parse JSON.parse text
|
11
|
+
end
|
12
|
+
|
13
|
+
# Serialize then deserialize CRDT.
|
14
|
+
def roundtrip(crdt)
|
15
|
+
deserialize serialize crdt
|
16
|
+
end
|
17
|
+
|
18
|
+
# Takes a CRDT and returns n copies.
|
19
|
+
def split(crdt, n = rand(10))
|
20
|
+
n.times.map do
|
21
|
+
roundtrip crdt
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Merge n CRDTs together.
|
26
|
+
def merge(crdts)
|
27
|
+
crdts.inject(&:merge)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Generate a random operation on a CRDT.
|
31
|
+
def operation
|
32
|
+
op = if rand > 0.5
|
33
|
+
:add
|
34
|
+
else
|
35
|
+
:delete
|
36
|
+
end
|
37
|
+
[op, rand(10)]
|
38
|
+
end
|
39
|
+
|
40
|
+
# A sequence of operations on a CRDT.
|
41
|
+
def operations
|
42
|
+
rand(5).times.map do
|
43
|
+
operation
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Apply the given operations to this CRDT.
|
48
|
+
def apply(ops, crdt)
|
49
|
+
ops.inject(crdt) do |c, op|
|
50
|
+
begin
|
51
|
+
c.send *op
|
52
|
+
rescue Meangirls::DeleteNotAllowed
|
53
|
+
rescue Meangirls::ReinsertNotAllowed
|
54
|
+
end
|
55
|
+
c
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Split, apply operations, merge.
|
60
|
+
def splurge(input, operations)
|
61
|
+
splits = split(input, operations.size).map.with_index do |c, i|
|
62
|
+
apply operations[i], c
|
63
|
+
end
|
64
|
+
merged = merge splits
|
65
|
+
[input, splits, merged]
|
66
|
+
end
|
67
|
+
|
68
|
+
# Attempts to generate failing sets of operations given a CRDT and a test
|
69
|
+
# block.
|
70
|
+
def test_merge(crdt, opts = {}, &block)
|
71
|
+
count = opts[:count] || COUNT
|
72
|
+
forks = opts[:forks] || rand(5) + 1
|
73
|
+
|
74
|
+
count.times do
|
75
|
+
operations = forks.times.map { operations() }
|
76
|
+
input, splits, merged = splurge crdt.clone, operations
|
77
|
+
if yield [splits, merged]
|
78
|
+
else
|
79
|
+
puts "Failed!"
|
80
|
+
puts "Input"
|
81
|
+
pp input
|
82
|
+
puts "Operations"
|
83
|
+
pp operations
|
84
|
+
puts "Siblings"
|
85
|
+
pp splits
|
86
|
+
puts "Merged"
|
87
|
+
pp merged
|
88
|
+
false.should.be.true
|
89
|
+
end
|
90
|
+
end
|
91
|
+
true.should.be.true
|
92
|
+
end
|
93
|
+
end
|
data/spec/run
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'open3'
|
4
|
+
require 'find'
|
5
|
+
|
6
|
+
tests = []
|
7
|
+
|
8
|
+
files, args = ARGV.partition do |arg|
|
9
|
+
File.exists? arg
|
10
|
+
end
|
11
|
+
|
12
|
+
dirs = files.empty? ? [File.dirname(__FILE__)] : files
|
13
|
+
dirs.each do |dir|
|
14
|
+
Find.find(dir) do |path|
|
15
|
+
next unless path =~ /\.rb$/
|
16
|
+
next if path =~ /\/init\.rb$/
|
17
|
+
tests << path
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
system *(["bacon", "-r", File.expand_path(File.dirname(__FILE__) + "/init.rb")] + args + tests.sort)
|
data/spec/set.rb
ADDED
@@ -0,0 +1,186 @@
|
|
1
|
+
shared :set do
|
2
|
+
should 'create an empty set' do
|
3
|
+
n = @s
|
4
|
+
n.should.be.empty
|
5
|
+
end
|
6
|
+
|
7
|
+
if @idempotent
|
8
|
+
should '==' do
|
9
|
+
@s.should == @s
|
10
|
+
a = @class.new
|
11
|
+
b = @class.new
|
12
|
+
a.should == b
|
13
|
+
a << 1
|
14
|
+
a.should.not == b
|
15
|
+
b << 1
|
16
|
+
a.should == b
|
17
|
+
a.delete 1
|
18
|
+
a.should.not == b
|
19
|
+
b.delete 1
|
20
|
+
a.should == b
|
21
|
+
end
|
22
|
+
else
|
23
|
+
should '==' do
|
24
|
+
@s.should == @s
|
25
|
+
a = @class.new
|
26
|
+
b = @class.new
|
27
|
+
a.should == b
|
28
|
+
a << 1
|
29
|
+
a.should == a
|
30
|
+
a.delete 1
|
31
|
+
a.should == a
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
should '===' do
|
36
|
+
@s.should === @s
|
37
|
+
@s.should === []
|
38
|
+
@s.should === Set.new
|
39
|
+
|
40
|
+
a = @class.new
|
41
|
+
a << 1
|
42
|
+
a << 2
|
43
|
+
a.should === [2,1]
|
44
|
+
a.should === [1,2].to_set
|
45
|
+
a.should === (1..2)
|
46
|
+
|
47
|
+
a.delete 1
|
48
|
+
a.should === [2]
|
49
|
+
a.should === [2].to_set
|
50
|
+
a.should === (2..2)
|
51
|
+
end
|
52
|
+
|
53
|
+
should '&' do
|
54
|
+
(@s & []).should === []
|
55
|
+
(@s & [1,2,3]).should === []
|
56
|
+
|
57
|
+
a = @class.new
|
58
|
+
a << 1
|
59
|
+
a << 2
|
60
|
+
(a & []).should === []
|
61
|
+
(a & [1,3]).should === [1]
|
62
|
+
end
|
63
|
+
|
64
|
+
should '|' do
|
65
|
+
(@s | []).should === []
|
66
|
+
(@s | [1,2,3]).should === [1,2,3]
|
67
|
+
|
68
|
+
a = @class.new
|
69
|
+
a << 1
|
70
|
+
a << 2
|
71
|
+
(a | []).should == a
|
72
|
+
(a | []).should === [1,2]
|
73
|
+
(a | [2,3]).should == (a << 3) if @idempotent
|
74
|
+
(a | [2,3]).should === [1,2,3]
|
75
|
+
end
|
76
|
+
|
77
|
+
should '-' do
|
78
|
+
(@s - []).should == @s
|
79
|
+
(@s - [1,2,3]).should === [] rescue Meangirls::DeleteNotAllowed
|
80
|
+
|
81
|
+
a = @class.new
|
82
|
+
a << 1
|
83
|
+
a << 2
|
84
|
+
(a - []).should == a
|
85
|
+
(a - [2]).should === [1]
|
86
|
+
(a - [2,3]).should === [1] rescue Meangirls::DeleteNotAllowed
|
87
|
+
end
|
88
|
+
|
89
|
+
should '<<' do
|
90
|
+
(@s << 1).should === [1]
|
91
|
+
(@s << 1 << 2).should === [1,2]
|
92
|
+
end
|
93
|
+
|
94
|
+
should 'delete' do
|
95
|
+
@s.delete(1).should == nil rescue Meangirls::DeleteNotAllowed
|
96
|
+
@s.should === []
|
97
|
+
|
98
|
+
a = @class.new
|
99
|
+
a << 1
|
100
|
+
a << 2
|
101
|
+
a.delete(1).should == 1
|
102
|
+
a.should === [2]
|
103
|
+
end
|
104
|
+
|
105
|
+
should 'empty' do
|
106
|
+
@s.should.be.empty
|
107
|
+
@s << 1
|
108
|
+
@s.should.not.be.empty
|
109
|
+
@s.delete 1
|
110
|
+
@s.should.be.empty
|
111
|
+
end
|
112
|
+
|
113
|
+
should 'merge empty sets' do
|
114
|
+
# Empty set
|
115
|
+
@s.merge(@s).should == @s
|
116
|
+
end
|
117
|
+
|
118
|
+
should 'merge with self' do
|
119
|
+
a = @class.new
|
120
|
+
a << 1
|
121
|
+
a << 2
|
122
|
+
a.merge(a).should == a
|
123
|
+
end
|
124
|
+
|
125
|
+
should 'merge disjoint sets' do
|
126
|
+
a = @class.new
|
127
|
+
a << 2
|
128
|
+
|
129
|
+
b = @class.new
|
130
|
+
b << 1
|
131
|
+
b << 3
|
132
|
+
a.merge(b).should === [1,2,3]
|
133
|
+
b.merge(a).should == a.merge(b)
|
134
|
+
end
|
135
|
+
|
136
|
+
should 'merge overlapping sets' do
|
137
|
+
a = @class.new
|
138
|
+
a << 1
|
139
|
+
a << 2
|
140
|
+
b = @class.new
|
141
|
+
b << 2
|
142
|
+
b << 3
|
143
|
+
a.merge(b).should === [1,2,3]
|
144
|
+
b.merge(a).should == a.merge(b)
|
145
|
+
end
|
146
|
+
|
147
|
+
([@s.bias] | (@class.biases rescue [])).each do |bias|
|
148
|
+
should "merge biased towards #{bias}" do
|
149
|
+
Meangirls.expects(:timestamp).times(0..3).returns(0)
|
150
|
+
case bias
|
151
|
+
when 'a'
|
152
|
+
# Should preserve adds
|
153
|
+
a = @class.new
|
154
|
+
a.bias = 'a' if a.respond_to? :bias=
|
155
|
+
a << 1
|
156
|
+
a.delete 1
|
157
|
+
b = @class.new
|
158
|
+
b.bias = 'a' if b.respond_to? :bias=
|
159
|
+
b << 1
|
160
|
+
a.merge(b).should === [1]
|
161
|
+
b.merge(a).should == a.merge(b)
|
162
|
+
when 'r'
|
163
|
+
# Should preserve deletes
|
164
|
+
a = @class.new
|
165
|
+
a.bias = 'r' if a.respond_to? :bias=
|
166
|
+
a << 1
|
167
|
+
a.delete 1
|
168
|
+
b = @class.new
|
169
|
+
b.bias = 'r' if b.respond_to? :bias=
|
170
|
+
b << 1
|
171
|
+
a.merge(b).should === []
|
172
|
+
b.merge(a).should == a.merge(b)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
should 'size' do
|
178
|
+
@s.size.should == 0
|
179
|
+
@s << 1
|
180
|
+
@s.size.should == 1
|
181
|
+
@s << 1
|
182
|
+
@s.size.should == 1
|
183
|
+
@s.delete 1
|
184
|
+
@s.size.should == 0
|
185
|
+
end
|
186
|
+
end
|