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 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