crdt 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|