Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Merge branch 'master' into or-sets

Conflicts:
	lib/meangirls.rb
  • Loading branch information...
commit 4caf7b8112f639cd3e0e4c864662138a2bb2da69 2 parents 6d14281 + 80cf2a7
Kyle Kingsbury authored
View
13 lib/meangirls.rb
@@ -14,6 +14,8 @@ def parse(s)
case s['type']
when '2p-set'
TwoPhaseSet.new s
+ when 'lww-set'
+ LWWSet.new s
else
raise ArgumentError, "unknown type #{s['type']}"
end
@@ -25,4 +27,15 @@ def tag
SecureRandom.urlsafe_base64
end
module_function :tag
+
+ # An ISO8601 time as close to the current time as possible, with the
+ # additional constraint that successive calls to this method will return
+ # monotonically increasing values.
+ #
+ # TODO: fold counter inside of time fraction
+ def timestamp
+ @i ||= 0
+ "#{Time.now.utc.iso8601}.#{@i += 1}"
+ end
+ module_function :timestamp
end
View
168 lib/meangirls/lww_set.rb
@@ -0,0 +1,168 @@
+class Meangirls::LWWSet < Meangirls::Set
+ require 'time'
+
+ class Pair
+ attr_accessor :add, :remove
+ def initialize(add, remove)
+ @add = add
+ @remove = remove
+ end
+
+ def ==(o)
+ o.kind_of? Pair and
+ add == o.add and
+ remove == o.remove
+ end
+
+ def exists_a?
+ return false unless @add
+ return true unless @remove
+ @add >= @remove
+ end
+
+ def exists_r?
+ return false unless @add
+ return true unless @remove
+ @add > @remove
+ end
+
+ def inspect
+ "(#{add.inspect}, #{remove.inspect})"
+ end
+
+ # Merge with another pair, taking the largest add and largest delete stamp.
+ def merge(other)
+ unless other
+ return clone
+ end
+
+ Pair.new(
+ [add, other.add].compact.max,
+ [remove, other.remove].compact.max
+ )
+ end
+ alias | merge
+ end
+
+ def self.biases
+ ['a', 'r']
+ end
+
+ attr_accessor :e
+ attr_accessor :bias
+ def initialize(hash = nil)
+ @e = {}
+ @bias = 'a'
+
+ if hash
+ raise ArgumentError, "hash must contain e" unless hash['e']
+ @bias = hash['bias'] if hash['bias']
+ hash['e'].each do |list|
+ element, add, delete = list
+ @e[element] = Pair.new(add, delete).merge(@e[element])
+ end
+ end
+ end
+
+ # Inserts e into the set with a default generated timestamp.
+ # Your clocks ARE synchronized, right? WRONG!
+ def <<(e)
+ add(e, timestamp)
+ end
+
+ # Strict equality: both adds and removes are equal.
+ def ==(other)
+ other.kind_of? self.class and
+ e == other.e
+ end
+
+ # Add e, with an optional timestamp.
+ def add(e, time = timestamp)
+ merge_element! e, Pair.new(time, nil)
+ self
+ end
+
+ def as_json
+ {
+ 'type' => type,
+ 'e' => @e.map { |e, pair|
+ [e, pair.add, pair.remove]
+ }
+ }
+ end
+
+ def clone
+ c = super
+ c.e = e.clone
+ c
+ end
+
+ # Delete e from self, with optional timestamp.
+ def delete(e, time = timestamp)
+ merge_element! e, Pair.new(nil, time)
+ e
+ end
+
+ # Is e an element of the set?
+ def include?(e)
+ return false unless pair = @e[e]
+ case bias
+ when 'a'
+ pair.exists_a?
+ when 'r'
+ pair.exists_r?
+ else
+ raise RuntimeError, "unsupported bias #{bias.inspect}"
+ end
+ end
+
+ # Merge with another lww-set
+ def merge(other)
+ unless other.kind_of? self.class
+ raise ArgumentError, "other must be a #{self.class}"
+ end
+
+ c = clone
+ other.e.each do |element, pair|
+ c.merge_element!(element, pair)
+ end
+ c
+ end
+
+ # Mutates self to update the value for e with the given pair.
+ def merge_element!(e, pair)
+ if cur = @e[e]
+ @e[e] = cur | pair
+ else
+ @e[e] = pair
+ end
+ end
+
+ def to_set
+ case bias
+ when 'a'
+ @e.inject(Set.new) do |s, l|
+ element, pair = l
+ s << element if pair.exists_a?
+ s
+ end
+ when 'r'
+ @e.inject(Set.new) do |s, l|
+ element, pair = l
+ s << element if pair.exists_r?
+ s
+ end
+ end
+ end
+
+ # Default timestamps are the present time in UTC as ISO 8601 strings, WITH a
+ # seconds fraction guaranteed to be unique and monotonically increasing for
+ # all use of Meangirls.
+ def timestamp
+ Meangirls.timestamp
+ end
+
+ def type
+ 'lww-set'
+ end
+end
View
1  lib/meangirls/set.rb
@@ -1,5 +1,6 @@
class Meangirls::Set < Meangirls::CRDT
require 'meangirls/two_phase_set'
+ require 'meangirls/lww_set'
include Enumerable
View
4 lib/meangirls/two_phase_set.rb
@@ -81,10 +81,6 @@ def include?(e)
@a.include? e and not @r.include? e
end
- def size
- to_set.size
- end
-
def to_set
@a - @r
end
View
1  spec/init.rb
@@ -1,4 +1,5 @@
require 'bacon'
+require 'mocha-on-bacon'
require "#{File.expand_path(File.dirname(__FILE__))}/crdt"
require "#{File.expand_path(File.dirname(__FILE__))}/set"
require "#{File.expand_path(File.dirname(__FILE__))}/../lib/meangirls"
View
30 spec/lww_set.rb
@@ -0,0 +1,30 @@
+describe 'lww-set' do
+ before do
+ @class = Meangirls::LWWSet
+ @idempotent = false
+ @s = @class.new
+ @examples = [
+ @class.new,
+ (@class.new << 1),
+ (@class.new - [1,2]),
+ (@class.new - [1,2] + [2,3]),
+ (@class.new + [1,2] - [2,3])
+ ]
+ end
+
+ behaves_like :crdt
+ behaves_like :set
+
+ should '==' do
+ a = @class.new
+ b = @class.new
+ a.add 1, 0
+ b.add 1, 0
+ a.should == b
+
+ a.delete 1, 1
+ a.should.not == b
+ b.delete 1, 1
+ a.should == b
+ end
+end
View
46 spec/set.rb
@@ -4,19 +4,32 @@
n.should.be.empty
end
- should '==' do
- @s.should == @s
- a = @class.new
- b = @class.new
- a.should == b
- a << 1
- a.should.not == b
- b << 1
- a.should == b
- a.delete 1
- a.should.not == b
- b.delete 1
- a.should == b
+ if @idempotent
+ should '==' do
+ @s.should == @s
+ a = @class.new
+ b = @class.new
+ a.should == b
+ a << 1
+ a.should.not == b
+ b << 1
+ a.should == b
+ a.delete 1
+ a.should.not == b
+ b.delete 1
+ a.should == b
+ end
+ else
+ should '==' do
+ @s.should == @s
+ a = @class.new
+ b = @class.new
+ a.should == b
+ a << 1
+ a.should == a
+ a.delete 1
+ a.should == a
+ end
end
should '===' do
@@ -57,7 +70,7 @@
a << 2
(a | []).should == a
(a | []).should === [1,2]
- (a | [2,3]).should == (a << 3)
+ (a | [2,3]).should == (a << 3) if @idempotent
(a | [2,3]).should === [1,2,3]
end
@@ -133,22 +146,27 @@
([@s.bias] | (@class.biases rescue [])).each do |bias|
should "merge biased towards #{bias}" do
+ Meangirls.expects(:timestamp).times(0..3).returns(0)
case bias
when 'a'
# Should preserve adds
a = @class.new
+ a.bias = 'a' if a.respond_to? :bias=
a << 1
a.delete 1
b = @class.new
+ b.bias = 'a' if b.respond_to? :bias=
b << 1
a.merge(b).should === [1]
b.merge(a).should == a.merge(b)
when 'r'
# Should preserve deletes
a = @class.new
+ a.bias = 'r' if a.respond_to? :bias=
a << 1
a.delete 1
b = @class.new
+ b.bias = 'r' if b.respond_to? :bias=
b << 1
a.merge(b).should === []
b.merge(a).should == a.merge(b)
View
0  spec/two_phase_set.rb 100755 → 100644
File mode changed
Please sign in to comment.
Something went wrong with that request. Please try again.