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.
- data/.travis.yml +4 -1
- data/CHANGES.md +11 -0
- data/Gemfile +6 -3
- data/LICENSE.txt +1 -1
- data/README.md +27 -243
- data/benchmarks/receiver.rb +2 -2
- data/dcell.gemspec +7 -7
- data/explorer/css/bootstrap-responsive.css +686 -0
- data/explorer/css/bootstrap-responsive.min.css +12 -0
- data/explorer/css/bootstrap.css +3990 -0
- data/explorer/css/bootstrap.min.css +689 -0
- data/explorer/css/explorer.css +28 -0
- data/explorer/ico/favicon.ico +0 -0
- data/explorer/img/glyphicons-halflings-white.png +0 -0
- data/explorer/img/glyphicons-halflings.png +0 -0
- data/explorer/img/logo.png +0 -0
- data/explorer/index.html.erb +94 -0
- data/explorer/js/bootstrap.js +1726 -0
- data/explorer/js/bootstrap.min.js +6 -0
- data/lib/dcell.rb +27 -2
- data/lib/dcell/celluloid_ext.rb +14 -3
- data/lib/dcell/directory.rb +15 -3
- data/lib/dcell/explorer.rb +76 -0
- data/lib/dcell/future_proxy.rb +32 -0
- data/lib/dcell/info_service.rb +117 -0
- data/lib/dcell/mailbox_proxy.rb +6 -7
- data/lib/dcell/messages.rb +5 -6
- data/lib/dcell/node.rb +25 -55
- data/lib/dcell/node_manager.rb +81 -0
- data/lib/dcell/registries/cassandra_adapter.rb +86 -0
- data/lib/dcell/registries/gossip/core.rb +235 -0
- data/lib/dcell/registries/gossip_adapter.rb +26 -0
- data/lib/dcell/registries/moneta_adapter.rb +0 -7
- data/lib/dcell/registries/redis_adapter.rb +0 -31
- data/lib/dcell/registries/zk_adapter.rb +1 -39
- data/lib/dcell/router.rb +37 -30
- data/lib/dcell/rpc.rb +23 -23
- data/lib/dcell/server.rb +5 -2
- data/lib/dcell/version.rb +1 -1
- data/logo.png +0 -0
- data/spec/dcell/actor_proxy_spec.rb +4 -0
- data/spec/dcell/celluloid_ext_spec.rb +11 -0
- data/spec/dcell/directory_spec.rb +1 -1
- data/spec/dcell/explorer_spec.rb +17 -0
- data/spec/dcell/global_spec.rb +4 -0
- data/spec/dcell/registries/gossip_adapter_spec.rb +6 -0
- data/spec/spec_helper.rb +14 -7
- data/spec/support/registry_examples.rb +0 -18
- data/tasks/cassandra.task +84 -0
- metadata +55 -35
- data/celluloid-zmq/.gitignore +0 -17
- data/celluloid-zmq/.rspec +0 -4
- data/celluloid-zmq/CHANGES.md +0 -31
- data/celluloid-zmq/Gemfile +0 -7
- data/celluloid-zmq/README.md +0 -56
- data/celluloid-zmq/Rakefile +0 -7
- data/celluloid-zmq/celluloid-zmq.gemspec +0 -28
- data/celluloid-zmq/lib/celluloid/zmq.rb +0 -36
- data/celluloid-zmq/lib/celluloid/zmq/reactor.rb +0 -90
- data/celluloid-zmq/lib/celluloid/zmq/sockets.rb +0 -130
- data/celluloid-zmq/lib/celluloid/zmq/version.rb +0 -5
- data/celluloid-zmq/lib/celluloid/zmq/waker.rb +0 -55
- data/celluloid-zmq/spec/celluloid/zmq/actor_spec.rb +0 -6
- 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
|