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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: dd48ff44957feb80db35dde367d7738a00017ba4
4
- data.tar.gz: 14f4d3d491163faaad568b5dd827e058671a3640
3
+ metadata.gz: 144854095bee0b768400839d8c599d2edb3274ab
4
+ data.tar.gz: 93a47b03011a29a6ea63475437b202c1068ed309
5
5
  SHA512:
6
- metadata.gz: f93a162bb0765597bb6519a41691bcca73bcd5c7ee6f6d67b91f02434a514d5088b11980716fa0a9297eabadb71c9a5d7be370d06255f1a4195562699715d5b0
7
- data.tar.gz: 512bd46616582d6d910302677d3f9956e261965db82391f5cccaf1413aae37aa813545b2daf081137519c11b7909038b4984beb8c68fb4e9d6c73d98997a0f67
6
+ metadata.gz: e02b8ad58ad150e670782320963c626afe7202b7d74a054051af47dd497a3f38e8bbb2a3ba527a426753c8ced005c3974cd40e3a9aab27c96455e5579249950a
7
+ data.tar.gz: b68ef708f83d7c06a6d21cac54f482784d63c015d6b31eb733bd1ec3fa653c397a4ef9c9de382fee0354fa222240fc4dfa5286ff96d9c460d60972f4ca8c0b01
@@ -11,5 +11,5 @@ end
11
11
  or_set
12
12
  lww_register
13
13
  }.each do |lib|
14
- require File.expand_path("crdt/#{lib}", __DIR__)
14
+ require File.expand_path("crdt/#{lib}", __dir__)
15
15
  end
@@ -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
@@ -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] += record[:observed]
76
- @items[item][:removed] += record[: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
@@ -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 takes
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 this_source Identifier for this node, used for tracking changes to the counter. Defaults to the current Thread's object ID
54
- def initialize(this_source = Thread.current.object_id)
55
- @cached_value = 0
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
- @this_source = this_source
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 ||= @this_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 ||= @this_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 +=(other)
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 -=(other)
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
@@ -1,3 +1,3 @@
1
1
  module CRDT
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
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.1.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-01-24 00:00:00.000000000 Z
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