meangirls 0.1.0
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.
- 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
|