Permalink
Browse files

WIP: g-counters

  • Loading branch information...
1 parent 4857da6 commit 16af7aa9e577566bf8bb5fa8acc228d5e775ff9f Kyle Kingsbury committed Apr 8, 2012
Showing with 199 additions and 0 deletions.
  1. +15 −0 lib/meangirls.rb
  2. +33 −0 lib/meangirls/counter.rb
  3. +1 −0 lib/meangirls/crdt.rb
  4. +77 −0 lib/meangirls/g_counter.rb
  5. +58 −0 spec/counter.rb
  6. +14 −0 spec/g_counter.rb
  7. +1 −0 spec/init.rb
View
@@ -7,6 +7,7 @@ class DeleteNotAllowed < RuntimeError; end
require 'set'
require 'base64'
require 'securerandom'
+ require 'socket'
require 'meangirls/crdt'
# Transforms a JSON data structure into a CRDT datatype.
@@ -18,12 +19,26 @@ def parse(s)
LWWSet.new s
when 'or-set'
ORSet.new s
+ when 'g-counter'
+ GCounter.new s
else
raise ArgumentError, "unknown type #{s['type']}"
end
end
module_function :parse
+ # The default node name.
+ def node
+ @node ||= Socket.gethostname
+ end
+ module_function :node
+
+ # Set the default node name.
+ def node=(node)
+ @node = node
+ end
+ module_function :node=
+
# Return a pseudounique tag.
def tag
SecureRandom.urlsafe_base64
View
@@ -0,0 +1,33 @@
+class Meangirls::Counter < Meangirls::CRDT
+ require 'meangirls/g_counter'
+
+ def ===(other)
+ # Can only compare to numerics
+ unless other.kind_of? Meangirls::Counter or
+ other.kind_of? Numeric
+ return false
+ end
+
+ # TODO: This gets awkward when we exceed FP integer precision in aggregate
+ # over actors.
+ if float?
+ self.to_f == other.to_f
+ else
+ self.to_i == other.to_i
+ end
+ end
+
+ # Returns a copy of this counter, with the local Meangirls node's value
+ # incremented by delta.
+ def +(delta)
+ clone.increment(Meangirls.node, delta)
+ end
+
+ def -(delta)
+ clone.increment(Meangirls.node, -1 * delta)
+ end
+
+ def to_i
+ to_f.to_i
+ end
+end
View
@@ -1,5 +1,6 @@
class Meangirls::CRDT
require 'meangirls/set'
+ require 'meangirls/counter'
# Merge a list of CRDTs by folding over merge.
def self.merge(*os)
View
@@ -0,0 +1,77 @@
+class Meangirls::GCounter < Meangirls::Counter
+ attr_accessor :e
+ def initialize(hash = nil)
+ @e = {}
+
+ if hash
+ raise ArgumentError, 'hash must contain e' unless hash['e']
+ @e = hash['e']
+ end
+ end
+
+ # Strict equality: all actor counts match.
+ def ==(other)
+ other.kind_of? self.class and
+ @e == other.e
+ end
+
+ def as_json
+ {
+ 'type' => type,
+ 'e' => @e
+ }
+ end
+
+ # Adds delta to this counter, as tracked by node. Returns self.
+ def increment(node = 1, delta = 1)
+ if delta < 0
+ raise ArgumentError, "Can't decrement a GCounter"
+ end
+
+ if @e[node]
+ @e[node] += delta
+ else
+ @e[node] = delta
+ end
+
+ self
+ end
+
+ def clone
+ c = super
+ c.e = @e.clone
+ end
+
+ # Are any sums floating-point?
+ def float?
+ @e.any? do |k, v|
+ v.float?
+ end
+ end
+
+ # Merge with another GCounter and return the merged copy.
+ def merge(other)
+ unless other.kind_of? self.class
+ raise ArgumentErrornew, "other must be a #{self.class}"
+ end
+
+ copy = clone
+ @e.each do |k, v|
+ if existing = copy.e[k]
+ copy.e[k] = v if v > existing
+ else
+ copy.e[k] = v
+ end
+ end
+
+ copy
+ end
+
+ def to_f
+ @e.values.inject(&:+) || 0.0
+ end
+
+ def type
+ 'g-counter'
+ end
+end
View
@@ -0,0 +1,58 @@
+shared :counter do
+ should 'create a counter' do
+ @class.new.should.be.kind_of? @class
+ end
+
+ should '==' do
+ a = @class.new
+ b = @class.new
+ a.should == b
+
+ a += 1
+ a.should.not == b
+ b += 2
+ a.should.not == b
+ a += 1
+ a.should == b
+
+ a.increment('foo', 2)
+ a.should.not == b
+ b.increment('foo', 2)
+ a.should == b
+ end
+
+ should '===' do
+ @s.should === @s
+ @s.should === 0
+ @s.should === 0.0
+ end
+
+ should '+' do
+ a = @s + 1
+ a.should.be.kind_of? Meangirls::Counter
+ a.should == 1
+
+ b = @class.new.increment('foo', 2)
+ b += 3
+ b.should === 5
+ end
+
+ should '-' do
+ end
+
+ should 'float?' do
+ @class.new.increment(1)
+ end
+
+ should 'merge' do
+ end
+
+ should 'increment' do
+ end
+
+ should 'to_i' do
+ end
+
+ should 'to_f' do
+ end
+end
View
@@ -0,0 +1,14 @@
+describe 'g-counter' do
+ before do
+ @class = Meangirls::GCounter
+ @s = @class.new
+ @examples = [
+ @class.new,
+ @class.new.increment('a', 1),
+ @class.new.increment('a', 1).increment('b', 0).increment('c', 1500.125)
+ ]
+ end
+
+ behaves_like :crdt
+ behaves_like :counter
+end
View
@@ -3,6 +3,7 @@
require "#{File.expand_path(File.dirname(__FILE__))}/prob"
require "#{File.expand_path(File.dirname(__FILE__))}/crdt"
require "#{File.expand_path(File.dirname(__FILE__))}/set"
+require "#{File.expand_path(File.dirname(__FILE__))}/counter"
require "#{File.expand_path(File.dirname(__FILE__))}/../lib/meangirls"
Bacon.summary_on_exit

0 comments on commit 16af7aa

Please sign in to comment.