dcell 0.9.0 → 0.10.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.
Files changed (64) hide show
  1. data/.travis.yml +4 -1
  2. data/CHANGES.md +11 -0
  3. data/Gemfile +6 -3
  4. data/LICENSE.txt +1 -1
  5. data/README.md +27 -243
  6. data/benchmarks/receiver.rb +2 -2
  7. data/dcell.gemspec +7 -7
  8. data/explorer/css/bootstrap-responsive.css +686 -0
  9. data/explorer/css/bootstrap-responsive.min.css +12 -0
  10. data/explorer/css/bootstrap.css +3990 -0
  11. data/explorer/css/bootstrap.min.css +689 -0
  12. data/explorer/css/explorer.css +28 -0
  13. data/explorer/ico/favicon.ico +0 -0
  14. data/explorer/img/glyphicons-halflings-white.png +0 -0
  15. data/explorer/img/glyphicons-halflings.png +0 -0
  16. data/explorer/img/logo.png +0 -0
  17. data/explorer/index.html.erb +94 -0
  18. data/explorer/js/bootstrap.js +1726 -0
  19. data/explorer/js/bootstrap.min.js +6 -0
  20. data/lib/dcell.rb +27 -2
  21. data/lib/dcell/celluloid_ext.rb +14 -3
  22. data/lib/dcell/directory.rb +15 -3
  23. data/lib/dcell/explorer.rb +76 -0
  24. data/lib/dcell/future_proxy.rb +32 -0
  25. data/lib/dcell/info_service.rb +117 -0
  26. data/lib/dcell/mailbox_proxy.rb +6 -7
  27. data/lib/dcell/messages.rb +5 -6
  28. data/lib/dcell/node.rb +25 -55
  29. data/lib/dcell/node_manager.rb +81 -0
  30. data/lib/dcell/registries/cassandra_adapter.rb +86 -0
  31. data/lib/dcell/registries/gossip/core.rb +235 -0
  32. data/lib/dcell/registries/gossip_adapter.rb +26 -0
  33. data/lib/dcell/registries/moneta_adapter.rb +0 -7
  34. data/lib/dcell/registries/redis_adapter.rb +0 -31
  35. data/lib/dcell/registries/zk_adapter.rb +1 -39
  36. data/lib/dcell/router.rb +37 -30
  37. data/lib/dcell/rpc.rb +23 -23
  38. data/lib/dcell/server.rb +5 -2
  39. data/lib/dcell/version.rb +1 -1
  40. data/logo.png +0 -0
  41. data/spec/dcell/actor_proxy_spec.rb +4 -0
  42. data/spec/dcell/celluloid_ext_spec.rb +11 -0
  43. data/spec/dcell/directory_spec.rb +1 -1
  44. data/spec/dcell/explorer_spec.rb +17 -0
  45. data/spec/dcell/global_spec.rb +4 -0
  46. data/spec/dcell/registries/gossip_adapter_spec.rb +6 -0
  47. data/spec/spec_helper.rb +14 -7
  48. data/spec/support/registry_examples.rb +0 -18
  49. data/tasks/cassandra.task +84 -0
  50. metadata +55 -35
  51. data/celluloid-zmq/.gitignore +0 -17
  52. data/celluloid-zmq/.rspec +0 -4
  53. data/celluloid-zmq/CHANGES.md +0 -31
  54. data/celluloid-zmq/Gemfile +0 -7
  55. data/celluloid-zmq/README.md +0 -56
  56. data/celluloid-zmq/Rakefile +0 -7
  57. data/celluloid-zmq/celluloid-zmq.gemspec +0 -28
  58. data/celluloid-zmq/lib/celluloid/zmq.rb +0 -36
  59. data/celluloid-zmq/lib/celluloid/zmq/reactor.rb +0 -90
  60. data/celluloid-zmq/lib/celluloid/zmq/sockets.rb +0 -130
  61. data/celluloid-zmq/lib/celluloid/zmq/version.rb +0 -5
  62. data/celluloid-zmq/lib/celluloid/zmq/waker.rb +0 -55
  63. data/celluloid-zmq/spec/celluloid/zmq/actor_spec.rb +0 -6
  64. data/celluloid-zmq/spec/spec_helper.rb +0 -2
@@ -0,0 +1,81 @@
1
+ module DCell
2
+ # Manage nodes we're connected to
3
+ class NodeManager
4
+ include Celluloid::ZMQ
5
+ include Enumerable
6
+
7
+ attr_reader :gossip_rate, :heartbeat_timeout
8
+
9
+ def initialize
10
+ @nodes = {}
11
+
12
+ @gossip_rate = 5 # How often to send gossip in seconds
13
+ @heartbeat_timeout = 10 # How soon until a lost heartbeat triggers a node partition
14
+ each { |node| node.socket if node } # Connect all so we can gossip
15
+ @gossip = after(gossip_rate) { gossip_timeout }
16
+ end
17
+
18
+ # Return all available nodes in the cluster
19
+ def all
20
+ Directory.all.map do |node_id|
21
+ find node_id
22
+ end
23
+ end
24
+
25
+ # Iterate across all available nodes
26
+ def each
27
+ Directory.all.each do |node_id|
28
+ yield find node_id
29
+ end
30
+ end
31
+
32
+ # Find a node by its node ID
33
+ def find(id)
34
+ node = @nodes[id]
35
+ return node if node
36
+
37
+ addr = Directory[id]
38
+ return unless addr
39
+
40
+ if id == DCell.id
41
+ node = DCell.me
42
+ else
43
+ node = Node.new(id, addr)
44
+ end
45
+
46
+ @nodes[id] ||= node
47
+ @nodes[id]
48
+ end
49
+ alias_method :[], :find
50
+
51
+ # Send gossip to a random node (except ourself) after the given interval
52
+ def gossip_timeout
53
+ nodes = select { |node| node.state == :connected }
54
+ peer = nodes.select { |node| node.id != DCell.id }.sample(1)[0]
55
+ if peer
56
+ nodes = nodes.inject([]) { |a,n| a << [n.id, n.addr, n.timestamp]; a }
57
+ data = nil
58
+ if DCell.registry.is_a? Registry::GossipAdapter
59
+ data = peer.fresh? ? DCell.registry.values : DCell.registry.changed
60
+ end
61
+ DCell.me.tick
62
+ peer.send_message DCell::Message::Gossip.new nodes, data
63
+ end
64
+ @gossip = after(gossip_rate) { gossip_timeout }
65
+ end
66
+
67
+ def handle_gossip(peers, data)
68
+ peers.each do |id, addr, timestamp|
69
+ if (node = find(id))
70
+ node.handle_timestamp! timestamp
71
+ else
72
+ Directory[id] = addr
73
+ Celluloid::Logger.info "Found node #{id}"
74
+ end
75
+ end
76
+ if DCell.registry.is_a? Registry::GossipAdapter
77
+ data.map { |data| DCell.registry.observe data } if data
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,86 @@
1
+ require 'cassandra'
2
+
3
+ # create the keyspace / columnfamily with cqlsh
4
+ #
5
+ # create keyspace dcell
6
+ # with strategy_class='SimpleStrategy'
7
+ # and strategy_options:replication_factor=3;
8
+ #
9
+ # create columnfamily dcell (dcell_type ascii primary key);
10
+
11
+ # not sure this is right yet ...
12
+ # Keyspace "whatever" [
13
+ # ColumnFamily "dcell" {
14
+ # RowKey "nodes": {
15
+ # <nodeid>: <address>,
16
+ # <nodeid>: <address>,
17
+ # ...
18
+ # }
19
+ # RowKey "globals": {
20
+ # <key>: <marshal blob>,
21
+ # <key>: <marshal blob>,
22
+ # ...
23
+ # }
24
+ # }
25
+ # ]
26
+ #
27
+
28
+ module DCell
29
+ module Registry
30
+ class CassandraAdapter
31
+ DEFAULT_KEYSPACE = "dcell"
32
+ DEFAULT_CF = "dcell"
33
+
34
+ def initialize(options)
35
+ # Convert all options to symbols :/
36
+ options = options.inject({}) { |h,(k,v)| h[k.to_sym] = v; h }
37
+
38
+ keyspace = options[:keyspace] || DEFAULT_KEYSPACE
39
+ columnfamily = options[:columnfamily] || DEFAULT_CF
40
+
41
+ options[:servers] ||= []
42
+ options[:servers] << options[:server] if options[:server]
43
+ options[:servers] << "localhost:9160" unless options[:servers].any?
44
+
45
+ cass = Cassandra.new(keyspace, options[:servers])
46
+
47
+ @global_registry = GlobalRegistry.new(cass, columnfamily)
48
+ end
49
+
50
+ def clear_globals
51
+ @global_registry.clear
52
+ end
53
+
54
+ class GlobalRegistry
55
+ def initialize(cass, cf)
56
+ @cass = cass
57
+ @cf = cf
58
+ end
59
+
60
+ def get(key)
61
+ string = @cass.get @cf, "globals", key.to_s
62
+ Marshal.load string if string
63
+ end
64
+
65
+ # Set a global value
66
+ def set(key, value)
67
+ string = Marshal.dump value
68
+ @cass.insert @cf, "globals", { key.to_s => string }
69
+ end
70
+
71
+ # The keys to all globals in the system
72
+ def global_keys
73
+ @cass.get(@cf, "globals").keys
74
+ end
75
+
76
+ def clear
77
+ @cass.del @cf, "globals"
78
+ end
79
+ end
80
+
81
+ def get_global(key); @global_registry.get(key) end
82
+ def set_global(key, value); @global_registry.set(key, value) end
83
+ def global_keys; @global_registry.global_keys end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,235 @@
1
+ require 'forwardable'
2
+ require 'uri'
3
+
4
+ module DCell
5
+ module Gossip
6
+ class VersionVector
7
+ extend Forwardable
8
+
9
+ class Status
10
+ STATES = [:precedes, :equal, :concurrent, :succeeds]
11
+
12
+ STATES.each do |state|
13
+ class_eval %Q{
14
+ def #{state}?
15
+ @state == '#{state}'.to_sym
16
+ end
17
+ def #{state}!
18
+ @state = '#{state}'.to_sym
19
+ end
20
+ }
21
+ end
22
+ end
23
+
24
+ def_delegators :@versions, :[]
25
+ attr_reader :versions
26
+
27
+ def initialize(id)
28
+ @versions = {}
29
+ update_at id
30
+ end
31
+
32
+ def update_at(id)
33
+ observe id
34
+ @versions[id] += 1
35
+ end
36
+
37
+ def observe(id)
38
+ @versions[id] ||= 0
39
+ end
40
+
41
+ def covers?(nodes, vector)
42
+ nodes.each do |node|
43
+ # We must have an entry for the node
44
+ return false unless @versions.keys.include? node
45
+
46
+ # And someone else must have seen the entry, too.
47
+ version = vector.versions[node] || 0
48
+ return false if @versions[node] != version
49
+ end
50
+ true
51
+ end
52
+
53
+ def compare(other)
54
+ v1_bigger = false
55
+ v2_bigger = false
56
+
57
+ @versions.each do |id, version|
58
+ if not other.versions.include? id
59
+ # Version vectors behave like vector clocks, with slightly
60
+ # different update rules. A vector clock assumes that all
61
+ # processes initially observe version 0. Since we don't
62
+ # know the topology ahead of time, we assume that a missing
63
+ # entry corresponds to a node that has not yet been discovered,
64
+ # and thus the version is implicitly 0.
65
+ v1_bigger = true if version > 0
66
+ else
67
+ v1_bigger = true if version > other.versions[id]
68
+ v2_bigger = true if version < other.versions[id]
69
+ end
70
+ end
71
+
72
+ other.versions.each do |id, version|
73
+ if not @versions.include? id
74
+ # See the comment above for the similar v1_bigger calculation.
75
+ v2_bigger = true if version > 0
76
+ else
77
+ v2_bigger = true if version > @versions[id]
78
+ v1_bigger = true if version < @versions[id]
79
+ end
80
+ end
81
+
82
+ status = Status.new
83
+ if !v1_bigger
84
+ if !v2_bigger
85
+ status.equal!
86
+ else
87
+ status.precedes!
88
+ end
89
+ elsif !v2_bigger
90
+ status.succeeds!
91
+ else
92
+ status.concurrent!
93
+ end
94
+ return status
95
+ end
96
+
97
+ # Take the entrywise maximum of the versions
98
+ def merge!(other)
99
+ @versions.each do |id, version|
100
+ if other.versions.include? id
101
+ @versions[id] = other.versions[id] if other.versions[id] > version
102
+ end
103
+ end
104
+
105
+ other.versions.each do |id, version|
106
+ if @versions.include? id
107
+ @versions[id] = @versions[id] if @versions[id] > version
108
+ end
109
+ end
110
+ @versions.merge!(other.versions.reject { |k,v| @versions.include? k })
111
+ end
112
+
113
+ def to_s
114
+ @versions.to_s
115
+ end
116
+ end
117
+
118
+ class Store
119
+ class Data
120
+ attr_reader :key, :value, :vector
121
+ def initialize(key, value, id)
122
+ @key = key
123
+ @value = value
124
+ @vector = VersionVector.new(id)
125
+ @changed = true
126
+ end
127
+
128
+ def clear
129
+ @vector.update_at DCell.id
130
+ @deleted = true
131
+ @value = nil
132
+ @changed = true
133
+ end
134
+
135
+ def deleted?
136
+ @deleted
137
+ end
138
+
139
+ def changed?
140
+ @changed
141
+ end
142
+
143
+ def observe
144
+ @vector.observe DCell.id
145
+ end
146
+
147
+ def value=(value)
148
+ if @value != value
149
+ @vector.update_at DCell.id
150
+ @value = value
151
+ @deleted = value.nil?
152
+ @changed = true
153
+ Celluloid::Logger.debug "Updated key #{key} to #{value}"
154
+ end
155
+ end
156
+
157
+ def merge!(other)
158
+ # We'll take other if we preceded it, or if we are
159
+ # concurrent with it (though issue a warning that data
160
+ # has been lost).
161
+ status = @vector.compare(other.vector)
162
+ if status.precedes? or status.concurrent?
163
+ if other.value != @value
164
+ if status.concurrent?
165
+ Celluloid::Logger.debug "Dropping local copy of concurrent data for #{@key}"
166
+ else
167
+ Celluloid::Logger.debug "Observed updated data #{key} => #{other.value}"
168
+ end
169
+ @value = other.value
170
+ @deleted = @value.nil?
171
+ end
172
+ elsif status.succeeds?
173
+ Celluloid::Logger.debug "Local data succeeds for #{@key}"
174
+ end
175
+ @vector.merge!(other.vector) unless status.equal?
176
+
177
+ # Stop gossiping if this has been seen by every known, healthy node
178
+ nodes = DCell::Node.all.map { |node| node.state == :connected }
179
+ @changed = false if @vector.covers?(nodes, other.vector)
180
+ end
181
+ end
182
+
183
+ def initialize(base_path)
184
+ @base_path = base_path
185
+ @data = {}
186
+ end
187
+
188
+ def path_for(key)
189
+ "#{@base_path}/#{key}"
190
+ end
191
+
192
+ def get(key)
193
+ data = @data[path_for(key)]
194
+ return data.value if data and not data.deleted?
195
+ nil
196
+ end
197
+
198
+ def set(key, value)
199
+ key = path_for(key)
200
+ if not @data[key]
201
+ @data[key] = Data.new(key, value, DCell.id)
202
+ else
203
+ @data[key].value = value
204
+ end
205
+ end
206
+
207
+ def observe(other)
208
+ key = other.key
209
+ if not @data[key]
210
+ @data[key] = other
211
+ @data[key].observe
212
+ Celluloid::Logger.debug "Observed new data #{key} => #{other.value}"
213
+ else
214
+ @data[key].merge!(other)
215
+ end
216
+ end
217
+
218
+ def keys
219
+ @data.keys.map { |k| k =~ /#{@base_path}\/(.+)$/; $1 }
220
+ end
221
+
222
+ def clear
223
+ @data.map(&:clear)
224
+ end
225
+
226
+ def changed
227
+ @data.each_value.select(&:changed?)
228
+ end
229
+
230
+ def values
231
+ @data.values
232
+ end
233
+ end
234
+ end
235
+ end
@@ -0,0 +1,26 @@
1
+ require 'forwardable'
2
+
3
+ module DCell
4
+ module Registry
5
+ class GossipAdapter
6
+ extend Forwardable
7
+ PREFIX = "/dcell"
8
+
9
+ def_delegator :@global_registry, :get, :get_global
10
+ def_delegator :@global_registry, :set, :set_global
11
+ def_delegator :@global_registry, :clear, :clear_globals
12
+ def_delegator :@global_registry, :keys, :global_keys
13
+ def_delegators :@global_registry, :changed, :observe, :values
14
+
15
+ def initialize(options)
16
+ # Convert all options to symbols :/
17
+ options = options.inject({}) { |h,(k,v)| h[k.to_sym] = v; h }
18
+
19
+ @env = options[:env] || 'production'
20
+ @base_path = options[:namespace] || "#{PREFIX}/#{@env}"
21
+
22
+ @global_registry = Gossip::Store.new("#{@base_path}/globals")
23
+ end
24
+ end
25
+ end
26
+ end
@@ -16,7 +16,6 @@ module DCell
16
16
  # @moneta = Moneta::TieredCache.new options
17
17
  @moneta = Moneta::Memory.new options
18
18
 
19
- @node_registry = Registry.new(@moneta, :nodes)
20
19
  @global_registry = Registry.new(@moneta, :globals)
21
20
  end
22
21
 
@@ -39,7 +38,6 @@ module DCell
39
38
  end
40
39
 
41
40
  # DCell registry behaviors
42
- alias_method :nodes, :all
43
41
  alias_method :global_keys, :all
44
42
 
45
43
  def clear
@@ -47,11 +45,6 @@ module DCell
47
45
  end
48
46
  end
49
47
 
50
- def get_node(node_id); @node_registry.get(node_id) end
51
- def set_node(node_id, addr); @node_registry.set(node_id, addr) end
52
- def nodes; @node_registry.nodes end
53
- def clear_nodes; @node_registry.clear end
54
-
55
48
  def get_global(key); @global_registry.get(key) end
56
49
  def set_global(key, value); @global_registry.set(key, value) end
57
50
  def global_keys; @global_registry.global_keys end