crdt 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/crdt.rb +1 -1
- data/lib/crdt/or_graph.rb +167 -0
- data/lib/crdt/or_set.rb +14 -2
- data/lib/crdt/pn_counter.rb +25 -11
- data/lib/crdt/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 144854095bee0b768400839d8c599d2edb3274ab
|
4
|
+
data.tar.gz: 93a47b03011a29a6ea63475437b202c1068ed309
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e02b8ad58ad150e670782320963c626afe7202b7d74a054051af47dd497a3f38e8bbb2a3ba527a426753c8ced005c3974cd40e3a9aab27c96455e5579249950a
|
7
|
+
data.tar.gz: b68ef708f83d7c06a6d21cac54f482784d63c015d6b31eb733bd1ec3fa653c397a4ef9c9de382fee0354fa222240fc4dfa5286ff96d9c460d60972f4ca8c0b01
|
data/lib/crdt.rb
CHANGED
@@ -0,0 +1,167 @@
|
|
1
|
+
module CRDT
|
2
|
+
# Observe Remove Graph (variant of 2P2P Graph)
|
3
|
+
#
|
4
|
+
# This is a general purpose graph data type. It works by keeping a 2P set for vertices and an OR set for edges
|
5
|
+
#
|
6
|
+
# Vertices are created uniquely on a node, and are represented with a token. It is left to the user to tie this token to their internal data.
|
7
|
+
# When merging changes, removes take precedence over adds, which can cause some surprising behavior when removing vertices
|
8
|
+
class ORGraph
|
9
|
+
# Create a new graph
|
10
|
+
def initialize(node_identity = Thread.current.object_id, token_counter = 0)
|
11
|
+
@node_identity = node_identity
|
12
|
+
@token_counter = token_counter
|
13
|
+
@vertices = {}
|
14
|
+
@edges = {}
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_accessor :vertices, :edges
|
18
|
+
|
19
|
+
# Test if a given vertex token is in this graph
|
20
|
+
def has_vertex?(token)
|
21
|
+
vertex = @vertices[token]
|
22
|
+
return false unless vertex
|
23
|
+
return ! vertex[:removed]
|
24
|
+
end
|
25
|
+
|
26
|
+
# Test if an edge exists between the given vertices
|
27
|
+
def has_edge?(from, to)
|
28
|
+
edge = @edges[edge_token(from, to)]
|
29
|
+
return false unless edge
|
30
|
+
return ! edge[:observed].empty?
|
31
|
+
end
|
32
|
+
|
33
|
+
# Get a list of all the edges that originate at the given vertex
|
34
|
+
def outgoing_edges(from)
|
35
|
+
@vertices[from][:outgoing_edges].map { |to| [from, to] }
|
36
|
+
end
|
37
|
+
|
38
|
+
# Get a list of all the edges that terminate at the given vertex
|
39
|
+
def incoming_edges(to)
|
40
|
+
@vertices[to][:incoming_edges].map { |from| [from, to] }
|
41
|
+
end
|
42
|
+
|
43
|
+
# Add a new vertex to the graph
|
44
|
+
#
|
45
|
+
# @return token representing the newly created vertex
|
46
|
+
def create_vertex
|
47
|
+
token = issue_token
|
48
|
+
# the edge arrays are a performance optimization to provide O(1) lookup for edges by vertex
|
49
|
+
@vertices[token] = { incoming_edges: [], outgoing_edges: [], removed: false }
|
50
|
+
return token
|
51
|
+
end
|
52
|
+
|
53
|
+
# add an edge leading from the given vertex to the given vertex
|
54
|
+
#
|
55
|
+
# @return token representing the created edge
|
56
|
+
def add_edge(from, to)
|
57
|
+
@vertices[from][:outgoing_edges] << to
|
58
|
+
@vertices[to][:incoming_edges] << from
|
59
|
+
token = edge_token(from, to)
|
60
|
+
@edges[token] ||= { observed: [], removed: [] }
|
61
|
+
@edges[token][:observed] << issue_token
|
62
|
+
|
63
|
+
return token
|
64
|
+
end
|
65
|
+
|
66
|
+
# remove a vertex from this graph, and any edges that involve it
|
67
|
+
def remove_vertex(vertex)
|
68
|
+
@vertices[vertex][:removed] = true
|
69
|
+
(incoming_edges(vertex) + outgoing_edges(vertex)).each do |from, to|
|
70
|
+
remove_edge(from, to)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# remove an edge from this graph
|
75
|
+
def remove_edge(from, to)
|
76
|
+
edge = @edges[edge_token(from, to)]
|
77
|
+
edge[:removed] += edge[:observed]
|
78
|
+
edge[:observed] = []
|
79
|
+
@vertices[from][:outgoing_edges] -= [to]
|
80
|
+
@vertices[to][:outgoing_edges] -= [from]
|
81
|
+
end
|
82
|
+
|
83
|
+
# Get a hash representation of this graph, suitable for serialization to JSON
|
84
|
+
def to_h
|
85
|
+
return {
|
86
|
+
node_identity: @node_identity,
|
87
|
+
token_counter: @token_counter,
|
88
|
+
vertices: @vertices,
|
89
|
+
edges: @edges,
|
90
|
+
}
|
91
|
+
end
|
92
|
+
|
93
|
+
# Create a new Graph from a hash, such as that deserialized from JSON
|
94
|
+
def self.from_h(hash)
|
95
|
+
graph = ORGraph.new(hash["node_identity"], hash["token_counter"])
|
96
|
+
|
97
|
+
hash["vertices"].each do |token, vertex|
|
98
|
+
graph.vertices[token] ||= {
|
99
|
+
incoming_edges: vertex[:incoming_edges].dup,
|
100
|
+
outgoing_edges: vertex[:outgoing_edges].dup,
|
101
|
+
removed: vertex[:removed],
|
102
|
+
}
|
103
|
+
end
|
104
|
+
hash["edges"].each do |token, edge|
|
105
|
+
graph.edges[token] = {
|
106
|
+
observed: edge[:observed].dup,
|
107
|
+
removed: edge[:removed].dup,
|
108
|
+
}
|
109
|
+
end
|
110
|
+
|
111
|
+
return graph
|
112
|
+
end
|
113
|
+
|
114
|
+
# Perform a one-way merge, bringing in changes from another graph
|
115
|
+
def merge(other)
|
116
|
+
other.vertices.each do |token, vertex|
|
117
|
+
@vertices[token] ||= {
|
118
|
+
incoming_edges: [],
|
119
|
+
outgoing_edges: [],
|
120
|
+
removed: false,
|
121
|
+
}
|
122
|
+
|
123
|
+
# cleaning out removed edges is taken care of while merging edges
|
124
|
+
@vertices[token][:incoming_edges] |= vertex[:incoming_edges]
|
125
|
+
@vertices[token][:outgoing_edges] |= vertex[:outgoing_edges]
|
126
|
+
@vertices[token][:removed] |= vertex[:removed]
|
127
|
+
end
|
128
|
+
other.edges.each do |edge_token, edge|
|
129
|
+
from, to = from_edge_token(edge_token)
|
130
|
+
@edges[edge_token] ||= {
|
131
|
+
observed: [],
|
132
|
+
removed: [],
|
133
|
+
}
|
134
|
+
|
135
|
+
@edges[edge_token][:observed] |= edge[:observed]
|
136
|
+
@edges[edge_token][:removed] |= edge[:removed]
|
137
|
+
@edges[edge_token][:observed] -= @edges[edge_token][:removed]
|
138
|
+
|
139
|
+
# vertex removal takes precedence over edge creation
|
140
|
+
if @vertices[from][:removed] || @vertices[to][:removed]
|
141
|
+
@edges[edge_token][:removed] += @edges[edge_token][:observed]
|
142
|
+
@edges[edge_token][:observed] = []
|
143
|
+
end
|
144
|
+
|
145
|
+
if @edges[edge_token][:observed].empty?
|
146
|
+
@vertices[to][:incoming_edges].delete(from)
|
147
|
+
@vertices[from][:outgoing_edges].delete(to)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
private
|
153
|
+
# issue a token unique to this node
|
154
|
+
def issue_token
|
155
|
+
@token_counter += 1
|
156
|
+
token = "#{@node_identity}:#{@token_counter}"
|
157
|
+
end
|
158
|
+
|
159
|
+
def edge_token(from, to)
|
160
|
+
"#{from}->#{to}"
|
161
|
+
end
|
162
|
+
|
163
|
+
def from_edge_token(token)
|
164
|
+
token.split("->")
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
data/lib/crdt/or_set.rb
CHANGED
@@ -28,6 +28,18 @@ module CRDT
|
|
28
28
|
return ! tokens[:observed].empty?
|
29
29
|
end
|
30
30
|
|
31
|
+
def each
|
32
|
+
if block_given?
|
33
|
+
@items.each do |item, record|
|
34
|
+
next if record[:observed].empty?
|
35
|
+
yield item
|
36
|
+
end
|
37
|
+
else
|
38
|
+
return to_enum
|
39
|
+
end
|
40
|
+
end
|
41
|
+
include Enumerable
|
42
|
+
|
31
43
|
# Add an item to this set
|
32
44
|
def add(item)
|
33
45
|
# the token in this implementation is "better", since it's easier for us to parse/garbage collect
|
@@ -72,8 +84,8 @@ module CRDT
|
|
72
84
|
def merge(other)
|
73
85
|
other.items.each do |item, record|
|
74
86
|
@items[item] ||= {observed: [], removed: []}
|
75
|
-
@items[item][:observed]
|
76
|
-
@items[item][:removed]
|
87
|
+
@items[item][:observed] |= record[:observed]
|
88
|
+
@items[item][:removed] |= record[:removed]
|
77
89
|
@items[item][:observed] -= @items[item][:removed]
|
78
90
|
end
|
79
91
|
end
|
data/lib/crdt/pn_counter.rb
CHANGED
@@ -12,8 +12,7 @@ module CRDT
|
|
12
12
|
# The space cost of synchronization is O(m)
|
13
13
|
#
|
14
14
|
# # Implementation notes:
|
15
|
-
# This implementation is a CvRDT. That means it
|
16
|
-
# This implementation doesn't support garbage collection, although you could add it by removing a node's records, and folding it into a base value.
|
15
|
+
# This implementation is a CvRDT. That means it sends a full copy of the entire structure, rather than messages
|
17
16
|
class PNCounter
|
18
17
|
# @param hash [Hash] a serialized PNCounter, conforming to the format here
|
19
18
|
#
|
@@ -27,7 +26,7 @@ module CRDT
|
|
27
26
|
# }
|
28
27
|
# }
|
29
28
|
def self.from_h(hash)
|
30
|
-
counter = PNCounter.new
|
29
|
+
counter = PNCounter.new(hash["node_identity"], hash["base_value"])
|
31
30
|
|
32
31
|
hash["positive"].each do |source, amount|
|
33
32
|
counter.increase(amount, source)
|
@@ -42,6 +41,8 @@ module CRDT
|
|
42
41
|
# Get a hash representation of this object, which is suitable for serialization to JSON
|
43
42
|
def to_h
|
44
43
|
return {
|
44
|
+
node_identity: @node_identity,
|
45
|
+
base_value: @base_value,
|
45
46
|
cached_value: @cached_value,
|
46
47
|
positive: @positive_counters,
|
47
48
|
negative: @negative_counters,
|
@@ -50,12 +51,13 @@ module CRDT
|
|
50
51
|
|
51
52
|
# Create a new counter
|
52
53
|
#
|
53
|
-
# @param
|
54
|
-
def initialize(
|
55
|
-
@
|
54
|
+
# @param node_identity Identifier for this node, used for tracking changes to the counter. Defaults to the current Thread's object ID
|
55
|
+
def initialize(node_identity = Thread.current.object_id, base_value = 0)
|
56
|
+
@base_value = base_value
|
57
|
+
@cached_value = base_value
|
56
58
|
@positive_counters = {}
|
57
59
|
@negative_counters = {}
|
58
|
-
@
|
60
|
+
@node_identity = node_identity
|
59
61
|
end
|
60
62
|
|
61
63
|
attr_accessor :positive_counters, :negative_counters
|
@@ -64,7 +66,7 @@ module CRDT
|
|
64
66
|
#
|
65
67
|
# @param amount [Number] a non-negative amount to decrease this counter by
|
66
68
|
def increase(amount, source = nil)
|
67
|
-
source ||= @
|
69
|
+
source ||= @node_identity
|
68
70
|
positive_counters[source] ||= 0
|
69
71
|
positive_counters[source] += amount
|
70
72
|
@cached_value += amount
|
@@ -76,7 +78,7 @@ module CRDT
|
|
76
78
|
#
|
77
79
|
# @param amount [Number] a non-negative amount to decrease this counter by
|
78
80
|
def decrease(amount, source = nil)
|
79
|
-
source ||= @
|
81
|
+
source ||= @node_identity
|
80
82
|
negative_counters[source] ||= 0
|
81
83
|
negative_counters[source] += amount
|
82
84
|
@cached_value -= amount
|
@@ -87,23 +89,25 @@ module CRDT
|
|
87
89
|
# Add something to this counter
|
88
90
|
#
|
89
91
|
# @param other [Number] the amount to add to this counter
|
90
|
-
def
|
92
|
+
def +(other)
|
91
93
|
if other > 0
|
92
94
|
increase(other)
|
93
95
|
else
|
94
96
|
decrease(- other)
|
95
97
|
end
|
98
|
+
self
|
96
99
|
end
|
97
100
|
|
98
101
|
# Subtract something from this counter
|
99
102
|
#
|
100
103
|
# @param other [Number] the amount to subtract from this counter
|
101
|
-
def
|
104
|
+
def -(other)
|
102
105
|
if other > 0
|
103
106
|
decrease(other)
|
104
107
|
else
|
105
108
|
increase(- other)
|
106
109
|
end
|
110
|
+
self
|
107
111
|
end
|
108
112
|
|
109
113
|
def value
|
@@ -139,5 +143,15 @@ module CRDT
|
|
139
143
|
|
140
144
|
return self
|
141
145
|
end
|
146
|
+
|
147
|
+
# Garbage collect a node, removing its counters and folding them into the new base value.
|
148
|
+
#
|
149
|
+
# This should only be called if your cluster management has indicated that a node has left the cluster permanently.
|
150
|
+
def gc(node)
|
151
|
+
@base_value += @positive_counters[node]
|
152
|
+
@base_value -= @negative_counters[node]
|
153
|
+
@positive_counters.delete(node)
|
154
|
+
@negative_counters.delete(node)
|
155
|
+
end
|
142
156
|
end
|
143
157
|
end
|
data/lib/crdt/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: crdt
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Steven Karas
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-
|
11
|
+
date: 2015-02-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -52,6 +52,7 @@ files:
|
|
52
52
|
- crdt.gemspec
|
53
53
|
- lib/crdt.rb
|
54
54
|
- lib/crdt/lww_register.rb
|
55
|
+
- lib/crdt/or_graph.rb
|
55
56
|
- lib/crdt/or_set.rb
|
56
57
|
- lib/crdt/pn_counter.rb
|
57
58
|
- lib/crdt/vector_clock.rb
|