Permalink
Browse files

Add Risky::Resolver.

This provides dead-simple conflict resolution for orthogonal values.
  • Loading branch information...
1 parent bb159c2 commit 065d95a76f32940e51e0e5c32d6ec1ad265c69e3 Kyle Kingsbury committed Jul 12, 2011
Showing with 167 additions and 0 deletions.
  1. +4 −0 lib/risky/all.rb
  2. +74 −0 lib/risky/resolver.rb
  3. +89 −0 spec/resolver.rb
View
4 lib/risky/all.rb
@@ -0,0 +1,4 @@
+require 'risky/cron_list'
+require 'risky/indexes'
+require 'risky/resolver'
+require 'risky/timestamps'
View
74 lib/risky/resolver.rb
@@ -0,0 +1,74 @@
+module Risky::Resolver
+ # Makes it easy to resolve conflicts in an object in different ways.
+ #
+ # class User
+ # include Risky::Resolver
+ # value :union, :resolve => :union
+ # value :union, :resolve => lambda do |xs| xs.first end
+
+ module ClassMethods
+ def merge(versions)
+ p = super(versions).clone
+
+ # For each field, use the given resolver to merge all the conflicting
+ # values together.
+ values.each do |value, opts|
+ next unless resolver = opts[:resolve]
+
+ # Convert symbols and such to callables.
+ unless resolver.respond_to? :call
+ resolver = begin
+ # Try our resolvers
+ Resolvers.method resolver
+ rescue
+ # Try a class method
+ method resolver
+ end
+ end
+
+ # Resolve and set
+ p.send("#{value}=", resolver.call(
+ versions.map do |version|
+ version.send value
+ end
+ ))
+ end
+
+ p
+ end
+ end
+
+ module Resolvers
+ extend self
+
+ def intersection(xs)
+ xs.compact.inject do |i, x|
+ i & x
+ end
+ end
+
+ def max(xs)
+ xs.compact.max
+ end
+
+ def merge(xs)
+ xs.compact.inject do |m, x|
+ m.merge x
+ end
+ end
+
+ def min(xs)
+ xs.compact.min
+ end
+
+ def union(xs)
+ xs.compact.inject do |u, x|
+ u | x
+ end
+ end
+ end
+
+ def self.included(base)
+ base.extend ClassMethods
+ end
+end
View
89 spec/resolver.rb
@@ -0,0 +1,89 @@
+#!/usr/bin/env ruby
+
+require 'rubygems'
+require 'bacon'
+require "#{File.expand_path(File.dirname(__FILE__))}/../lib/risky"
+require 'risky/resolver'
+require 'pp'
+
+Bacon.summary_on_exit
+
+Risky.riak = proc { Riak::Client.new(:host => '127.0.0.1') }
+Thread.abort_on_exception = true
+
+class Multi < Risky
+ bucket :mult
+ allow_mult
+
+ include Risky::Resolver
+
+ value :users, :default => []
+ value :union, :resolve => :union
+ value :intersection, :resolve => Risky::Resolver::Resolvers.method(:intersection)
+ value :max, :resolve => :max
+ value :min, :resolve => :min
+ value :merge, :resolve => :merge
+ value :custom, :resolve => lambda { |xs|
+ :custom
+ }
+
+ def self.merge(v)
+ p = super v
+
+ p.users = v.map(&:users).min
+
+ p
+ end
+end
+
+describe Risky::Resolver do
+ def conflict(field, values)
+ key = rand(100000).to_s
+ object = Multi.new(key).save
+
+ # Create conflicting versions
+ values.map.with_index do |value, i|
+ Multi.riak.client_id = i + 1
+ [Multi[key], value]
+ end.each.with_index do |pair, i|
+ o, value = pair
+ Multi.riak.client_id = i + 1
+ o[field] = value
+ o.save
+ end
+
+ ro = Multi.bucket[key]
+ ro.should.conflict
+ begin
+ ro.siblings.map { |s| s.data[field] }.sort.should == values.sort
+ rescue ArgumentError
+ ro.siblings.map { |s| s.data[field] }.to_set.should == values.to_set
+ end
+
+ # Get all conflicts
+ Multi[key, :r => :all]
+ end
+
+ def test(property, ins, out)
+ it property do
+ conflict(property, ins)[property].should == out
+ end
+ end
+
+ def set_test(property, ins, out)
+ it property do
+ conflict(property, ins)[property].to_set.should == out.to_set
+ end
+ end
+
+ set_test 'union', [[1], [2]], [1,2]
+ set_test 'union', [[1], nil], [1]
+ set_test 'union', [[1,4,1], [2,3], [4,4]], [1,2,3,4]
+ set_test 'intersection', [[1,2],[]], []
+ set_test 'intersection', [[1,2,3,4], [1,2,3], [2,3,4]], [2,3]
+ test 'min', [0,1,2,3], 0
+ test 'max', [0,2,4,2], 4
+ test 'max', [nil, nil], nil
+ test 'max', [nil, 4], 4
+ test 'custom', ['a', 'b', 'c'], :custom
+end

0 comments on commit 065d95a

Please sign in to comment.