Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

LWW set.

  • Loading branch information...
commit 80cf2a70f408f2e7388e3bdfc310d4715b39fb68 1 parent 46f4eda
Kyle Kingsbury authored
13 lib/meangirls.rb
@@ -12,9 +12,22 @@ def parse(s)
12 12 case s['type']
13 13 when '2p-set'
14 14 TwoPhaseSet.new s
  15 + when 'lww-set'
  16 + LWWSet.new s
15 17 else
16 18 raise ArgumentError, "unknown type #{s['type']}"
17 19 end
18 20 end
19 21 module_function :parse
  22 +
  23 + # An ISO8601 time as close to the current time as possible, with the
  24 + # additional constraint that successive calls to this method will return
  25 + # monotonically increasing values.
  26 + #
  27 + # TODO: fold counter inside of time fraction
  28 + def timestamp
  29 + @i ||= 0
  30 + "#{Time.now.utc.iso8601}.#{@i += 1}"
  31 + end
  32 + module_function :timestamp
20 33 end
168 lib/meangirls/lww_set.rb
... ... @@ -0,0 +1,168 @@
  1 +class Meangirls::LWWSet < Meangirls::Set
  2 + require 'time'
  3 +
  4 + class Pair
  5 + attr_accessor :add, :remove
  6 + def initialize(add, remove)
  7 + @add = add
  8 + @remove = remove
  9 + end
  10 +
  11 + def ==(o)
  12 + o.kind_of? Pair and
  13 + add == o.add and
  14 + remove == o.remove
  15 + end
  16 +
  17 + def exists_a?
  18 + return false unless @add
  19 + return true unless @remove
  20 + @add >= @remove
  21 + end
  22 +
  23 + def exists_r?
  24 + return false unless @add
  25 + return true unless @remove
  26 + @add > @remove
  27 + end
  28 +
  29 + def inspect
  30 + "(#{add.inspect}, #{remove.inspect})"
  31 + end
  32 +
  33 + # Merge with another pair, taking the largest add and largest delete stamp.
  34 + def merge(other)
  35 + unless other
  36 + return clone
  37 + end
  38 +
  39 + Pair.new(
  40 + [add, other.add].compact.max,
  41 + [remove, other.remove].compact.max
  42 + )
  43 + end
  44 + alias | merge
  45 + end
  46 +
  47 + def self.biases
  48 + ['a', 'r']
  49 + end
  50 +
  51 + attr_accessor :e
  52 + attr_accessor :bias
  53 + def initialize(hash = nil)
  54 + @e = {}
  55 + @bias = 'a'
  56 +
  57 + if hash
  58 + raise ArgumentError, "hash must contain e" unless hash['e']
  59 + @bias = hash['bias'] if hash['bias']
  60 + hash['e'].each do |list|
  61 + element, add, delete = list
  62 + @e[element] = Pair.new(add, delete).merge(@e[element])
  63 + end
  64 + end
  65 + end
  66 +
  67 + # Inserts e into the set with a default generated timestamp.
  68 + # Your clocks ARE synchronized, right? WRONG!
  69 + def <<(e)
  70 + add(e, timestamp)
  71 + end
  72 +
  73 + # Strict equality: both adds and removes are equal.
  74 + def ==(other)
  75 + other.kind_of? self.class and
  76 + e == other.e
  77 + end
  78 +
  79 + # Add e, with an optional timestamp.
  80 + def add(e, time = timestamp)
  81 + merge_element! e, Pair.new(time, nil)
  82 + self
  83 + end
  84 +
  85 + def as_json
  86 + {
  87 + 'type' => type,
  88 + 'e' => @e.map { |e, pair|
  89 + [e, pair.add, pair.remove]
  90 + }
  91 + }
  92 + end
  93 +
  94 + def clone
  95 + c = super
  96 + c.e = e.clone
  97 + c
  98 + end
  99 +
  100 + # Delete e from self, with optional timestamp.
  101 + def delete(e, time = timestamp)
  102 + merge_element! e, Pair.new(nil, time)
  103 + e
  104 + end
  105 +
  106 + # Is e an element of the set?
  107 + def include?(e)
  108 + return false unless pair = @e[e]
  109 + case bias
  110 + when 'a'
  111 + pair.exists_a?
  112 + when 'r'
  113 + pair.exists_r?
  114 + else
  115 + raise RuntimeError, "unsupported bias #{bias.inspect}"
  116 + end
  117 + end
  118 +
  119 + # Merge with another lww-set
  120 + def merge(other)
  121 + unless other.kind_of? self.class
  122 + raise ArgumentError, "other must be a #{self.class}"
  123 + end
  124 +
  125 + c = clone
  126 + other.e.each do |element, pair|
  127 + c.merge_element!(element, pair)
  128 + end
  129 + c
  130 + end
  131 +
  132 + # Mutates self to update the value for e with the given pair.
  133 + def merge_element!(e, pair)
  134 + if cur = @e[e]
  135 + @e[e] = cur | pair
  136 + else
  137 + @e[e] = pair
  138 + end
  139 + end
  140 +
  141 + def to_set
  142 + case bias
  143 + when 'a'
  144 + @e.inject(Set.new) do |s, l|
  145 + element, pair = l
  146 + s << element if pair.exists_a?
  147 + s
  148 + end
  149 + when 'r'
  150 + @e.inject(Set.new) do |s, l|
  151 + element, pair = l
  152 + s << element if pair.exists_r?
  153 + s
  154 + end
  155 + end
  156 + end
  157 +
  158 + # Default timestamps are the present time in UTC as ISO 8601 strings, WITH a
  159 + # seconds fraction guaranteed to be unique and monotonically increasing for
  160 + # all use of Meangirls.
  161 + def timestamp
  162 + Meangirls.timestamp
  163 + end
  164 +
  165 + def type
  166 + 'lww-set'
  167 + end
  168 +end
1  lib/meangirls/set.rb
... ... @@ -1,5 +1,6 @@
1 1 class Meangirls::Set < Meangirls::CRDT
2 2 require 'meangirls/two_phase_set'
  3 + require 'meangirls/lww_set'
3 4
4 5 include Enumerable
5 6
1  spec/init.rb
... ... @@ -1,4 +1,5 @@
1 1 require 'bacon'
  2 +require 'mocha-on-bacon'
2 3 require "#{File.expand_path(File.dirname(__FILE__))}/crdt"
3 4 require "#{File.expand_path(File.dirname(__FILE__))}/set"
4 5 require "#{File.expand_path(File.dirname(__FILE__))}/../lib/meangirls"
30 spec/lww_set.rb
... ... @@ -0,0 +1,30 @@
  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 +
  18 + should '==' do
  19 + a = @class.new
  20 + b = @class.new
  21 + a.add 1, 0
  22 + b.add 1, 0
  23 + a.should == b
  24 +
  25 + a.delete 1, 1
  26 + a.should.not == b
  27 + b.delete 1, 1
  28 + a.should == b
  29 + end
  30 +end
46 spec/set.rb
@@ -4,19 +4,32 @@
4 4 n.should.be.empty
5 5 end
6 6
7   - should '==' do
8   - @s.should == @s
9   - a = @class.new
10   - b = @class.new
11   - a.should == b
12   - a << 1
13   - a.should.not == b
14   - b << 1
15   - a.should == b
16   - a.delete 1
17   - a.should.not == b
18   - b.delete 1
19   - a.should == b
  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
20 33 end
21 34
22 35 should '===' do
@@ -57,7 +70,7 @@
57 70 a << 2
58 71 (a | []).should == a
59 72 (a | []).should === [1,2]
60   - (a | [2,3]).should == (a << 3)
  73 + (a | [2,3]).should == (a << 3) if @idempotent
61 74 (a | [2,3]).should === [1,2,3]
62 75 end
63 76
@@ -133,22 +146,27 @@
133 146
134 147 ([@s.bias] | (@class.biases rescue [])).each do |bias|
135 148 should "merge biased towards #{bias}" do
  149 + Meangirls.expects(:timestamp).times(0..3).returns(0)
136 150 case bias
137 151 when 'a'
138 152 # Should preserve adds
139 153 a = @class.new
  154 + a.bias = 'a' if a.respond_to? :bias=
140 155 a << 1
141 156 a.delete 1
142 157 b = @class.new
  158 + b.bias = 'a' if b.respond_to? :bias=
143 159 b << 1
144 160 a.merge(b).should === [1]
145 161 b.merge(a).should == a.merge(b)
146 162 when 'r'
147 163 # Should preserve deletes
148 164 a = @class.new
  165 + a.bias = 'r' if a.respond_to? :bias=
149 166 a << 1
150 167 a.delete 1
151 168 b = @class.new
  169 + b.bias = 'r' if b.respond_to? :bias=
152 170 b << 1
153 171 a.merge(b).should === []
154 172 b.merge(a).should == a.merge(b)
0  spec/two_phase_set.rb 100755 → 100644
File mode changed

0 comments on commit 80cf2a7

Please sign in to comment.
Something went wrong with that request. Please try again.