adhd 0.0.1 → 0.1.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/README.rdoc +62 -2
- data/VERSION +1 -1
- data/adhd.gemspec +10 -8
- data/bin/adhd +23 -8
- data/bin/adhd_cleanup +57 -0
- data/lib/adhd/adhd_rest_server.rb +229 -0
- data/lib/adhd/config.yml +1 -1
- data/lib/adhd/models/content_doc.rb +17 -0
- data/lib/adhd/models/content_shard.rb +97 -0
- data/lib/adhd/models/node_doc.rb +139 -0
- data/lib/adhd/models/shard_range.rb +202 -0
- data/lib/adhd/node_manager.rb +260 -0
- data/lib/adhd/reactor.rb +194 -12
- data/test/test_adhd.rb +0 -11
- metadata +11 -7
- data/lib/adhd.rb +0 -120
- data/lib/adhd/models.rb +0 -388
- data/lib/adhd/node.rb +0 -13
- data/models.rb +0 -19
data/lib/adhd/config.yml
CHANGED
@@ -0,0 +1,17 @@
|
|
1
|
+
class ContentDoc < CouchRest::ExtendedDocument
|
2
|
+
# NOTE: NO DEFAULT DATABASE IN THE OBJECT -- WE WILL BE STORING A LOT OF
|
3
|
+
# DATABASES OF THIS TYPE.
|
4
|
+
|
5
|
+
|
6
|
+
property :_id
|
7
|
+
property :internal_id
|
8
|
+
property :size_bytes
|
9
|
+
property :filename
|
10
|
+
property :mime_type
|
11
|
+
|
12
|
+
view_by :internal_id
|
13
|
+
|
14
|
+
# A special attachment "File" is expected to exist
|
15
|
+
|
16
|
+
end
|
17
|
+
|
@@ -0,0 +1,97 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
class ContentShard
|
4
|
+
attr_accessor :nodes, :this_shard, :our_node, :this_shard_db
|
5
|
+
|
6
|
+
def initialize(nodesv, this_shardv)
|
7
|
+
@nodes = nodesv
|
8
|
+
@this_shard = this_shardv
|
9
|
+
|
10
|
+
# Work out the rest
|
11
|
+
@our_node = nodesv.our_node
|
12
|
+
@this_shard_db = nodesv.our_node.get_content_db(this_shardv.shard_db_name)
|
13
|
+
|
14
|
+
@last_sync_seq = 0 # @this_shard_db.info['update_seq']
|
15
|
+
end
|
16
|
+
|
17
|
+
def in_shard?(internal_id)
|
18
|
+
internal_id >= this_shard.range_start && internal_id < this_shard.range_end
|
19
|
+
end
|
20
|
+
|
21
|
+
def write_doc(content_doc)
|
22
|
+
# Write a content document to this shard
|
23
|
+
# Make sure it is in this shard
|
24
|
+
if in_shard? content_doc.internal_id
|
25
|
+
this_shard_db.save_doc(content_doc)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def sync
|
30
|
+
# A Shard only pushes with the master of the shard
|
31
|
+
# or the node with the highest is_storage value alive
|
32
|
+
# Shard masters ensure changes are pushed to all
|
33
|
+
|
34
|
+
# NOTE: This method needs serious refactoring
|
35
|
+
# No need to update
|
36
|
+
return false if @this_shard_db.info['update_seq'] == @last_sync_seq
|
37
|
+
|
38
|
+
# Are we the shard master?
|
39
|
+
am_master = (our_node.name == this_shard.master_node)
|
40
|
+
|
41
|
+
if !am_master
|
42
|
+
master_node = Node.by_name(:key => this_shard.master_node).first
|
43
|
+
remote_db = master_node.get_content_db(this_shard.shard_db_name)
|
44
|
+
bool_to = @our_node.replicate_to(this_shard_db, master_node, remote_db)
|
45
|
+
if bool_to
|
46
|
+
@last_sync_seq = @this_shard_db.info['update_seq']
|
47
|
+
return true
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Either we are the master or the master has failed -- we replicate with
|
52
|
+
# all nodes or the first available aside us and master
|
53
|
+
all_good = true
|
54
|
+
this_shard.node_list.each do |node_name|
|
55
|
+
# Push all changes to the other nodes
|
56
|
+
remote_node = Node.by_name(:key => node_name).first
|
57
|
+
remote_db = remote_node.get_content_db(this_shard.shard_db_name)
|
58
|
+
all_good &= @our_node.replicate_to(this_shard_db, remote_node, remote_db)
|
59
|
+
|
60
|
+
if !am_master && bool_to
|
61
|
+
# NOTE: How to build skynet, Note 2
|
62
|
+
# We are doing some "gonzo" replication, here. Our master is
|
63
|
+
# clearly down so we find the second best node; we push our
|
64
|
+
# changes to this node, and now also *replicate from*
|
65
|
+
# that node.
|
66
|
+
@our_node.replicate_from(this_shard_db, remote_node, remote_db)
|
67
|
+
@last_sync_seq = @this_shard_db.info['update_seq']
|
68
|
+
break
|
69
|
+
end
|
70
|
+
end
|
71
|
+
if all_good
|
72
|
+
@last_sync_seq = @this_shard_db.info['update_seq']
|
73
|
+
return true
|
74
|
+
else
|
75
|
+
return false
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
|
81
|
+
class ContentDoc < CouchRest::ExtendedDocument
|
82
|
+
# NOTE: NO DEFAULT DATABASE IN THE OBJECT -- WE WILL BE STORING A LOT OF
|
83
|
+
# DATABASES OF THIS TYPE.
|
84
|
+
|
85
|
+
|
86
|
+
property :_id
|
87
|
+
property :internal_id
|
88
|
+
property :size_bytes
|
89
|
+
property :filename
|
90
|
+
property :mime_type
|
91
|
+
|
92
|
+
view_by :internal_id
|
93
|
+
|
94
|
+
# A special attachment "File" is expected to exist
|
95
|
+
|
96
|
+
end
|
97
|
+
|
@@ -0,0 +1,139 @@
|
|
1
|
+
## Key Restrictions ok internal_IDs: must only contain [a-z0-9-]
|
2
|
+
|
3
|
+
class NodeDB
|
4
|
+
|
5
|
+
attr_accessor :local_node_db, :our_node
|
6
|
+
|
7
|
+
def initialize(our_nodev)
|
8
|
+
@our_node = our_nodev
|
9
|
+
|
10
|
+
# Get the address of the CDB from the node
|
11
|
+
@local_node_db = our_nodev.get_node_db
|
12
|
+
end
|
13
|
+
|
14
|
+
def sync
|
15
|
+
# We replicate our state to the management node(s)
|
16
|
+
management_nodes = Node.by_is_management.reverse
|
17
|
+
# NOTE: randomize the order for load balancing here
|
18
|
+
|
19
|
+
# NOTE2: How to build skynet (TODO)
|
20
|
+
# -------------------
|
21
|
+
# If length of management is zero, then chose 3 different random
|
22
|
+
# nodes at each sync, and sync with them in node_name order.
|
23
|
+
# This guarantees that any updates on nodes are communicated in
|
24
|
+
# O(log N) ephocs, at the cost of O(3 * N) connections per epoch.
|
25
|
+
# It also guarantees any new management servers are discovered in
|
26
|
+
# this O(log N) time, creating "jelly fish" or "partition proof"
|
27
|
+
# availability.
|
28
|
+
|
29
|
+
management_nodes.each do |mng_node|
|
30
|
+
remote_db = mng_node.get_node_db
|
31
|
+
bool_from = @our_node.replicate_from(local_node_db, mng_node, remote_db)
|
32
|
+
bool_to = @our_node.replicate_to(local_node_db, mng_node, remote_db)
|
33
|
+
if bool_from && bool_to && !our_node.is_management
|
34
|
+
#puts "Pushed to management"
|
35
|
+
break
|
36
|
+
end
|
37
|
+
#puts "Did not push to management"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def available_node_list
|
42
|
+
# Returns all nodes marked as available
|
43
|
+
all_nodes = Node.by_name
|
44
|
+
return all_nodes.select {|node| node.status == "RUNNING"}
|
45
|
+
end
|
46
|
+
|
47
|
+
def head_management_node
|
48
|
+
management_nodes = Node.by_is_management.reverse
|
49
|
+
hmn = management_nodes.find {|node| node.status == "RUNNING"}
|
50
|
+
return hmn
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
class Node < CouchRest::ExtendedDocument
|
56
|
+
#NODESERVER = CouchRest.new("#{ARGV[1]}")
|
57
|
+
#NODESERVER.default_database = "#{ARGV[0]}_node_db"
|
58
|
+
|
59
|
+
#use_database NODESERVER.default_database
|
60
|
+
|
61
|
+
unique_id :name
|
62
|
+
|
63
|
+
property :name
|
64
|
+
property :url
|
65
|
+
property :is_store
|
66
|
+
property :is_management
|
67
|
+
property :is_directory
|
68
|
+
property :status
|
69
|
+
|
70
|
+
timestamps!
|
71
|
+
|
72
|
+
view_by :name
|
73
|
+
view_by :is_management
|
74
|
+
|
75
|
+
def get_node_db
|
76
|
+
server = CouchRest.new("#{url}")
|
77
|
+
db = server.database!("#{name}_node_db")
|
78
|
+
# puts "Open db #{db}"
|
79
|
+
db
|
80
|
+
end
|
81
|
+
|
82
|
+
def get_shard_db
|
83
|
+
server = CouchRest.new("#{url}")
|
84
|
+
db = server.database!("#{name}_shard_db")
|
85
|
+
# puts "Open db #{db}"
|
86
|
+
db
|
87
|
+
end
|
88
|
+
|
89
|
+
def get_content_db(shard_db_name)
|
90
|
+
server = CouchRest.new("#{url}")
|
91
|
+
db = server.database!("#{name}_#{shard_db_name}_content_db")
|
92
|
+
# puts "Open db #{db}"
|
93
|
+
db
|
94
|
+
end
|
95
|
+
|
96
|
+
# Replicating databases and marking nodes as unavailable
|
97
|
+
# In the future we should hook these into a "replication manager"
|
98
|
+
# for databases. The manager should set up continuous replication across
|
99
|
+
# databases, and only do a replication after some time lapses.
|
100
|
+
|
101
|
+
def replicate_to(local_db, other_node, remote_db)
|
102
|
+
# Do not try to contact unavailable nodes
|
103
|
+
return false if other_node.status == "UNAVAILABLE"
|
104
|
+
# No point replicating to ourselves
|
105
|
+
return false if (name == other_node.name)
|
106
|
+
|
107
|
+
begin
|
108
|
+
# Replicate to other node is possible
|
109
|
+
local_db.replicate_to(remote_db)
|
110
|
+
return true
|
111
|
+
rescue Exception => e
|
112
|
+
# Other node turns out to be unavailable
|
113
|
+
other_node.status = "UNAVAILABLE"
|
114
|
+
other_node.save
|
115
|
+
return false
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def replicate_from(local_db, other_node, remote_db)
|
120
|
+
# Do not try to contact unavailable nodes
|
121
|
+
return false if other_node.status == "UNAVAILABLE"
|
122
|
+
# No point replicating to ourselves
|
123
|
+
return false if (name == other_node.name)
|
124
|
+
|
125
|
+
begin
|
126
|
+
# Replicate to other node is possible
|
127
|
+
local_db.replicate_from(remote_db)
|
128
|
+
return true
|
129
|
+
rescue Exception => e
|
130
|
+
# Other node turns out to be unavailable
|
131
|
+
other_node.status = "UNAVAILABLE"
|
132
|
+
other_node.save
|
133
|
+
return false
|
134
|
+
end
|
135
|
+
|
136
|
+
end
|
137
|
+
|
138
|
+
end
|
139
|
+
|
@@ -0,0 +1,202 @@
|
|
1
|
+
class ShardRangeDB
|
2
|
+
|
3
|
+
attr_accessor :nodes, :local_shard_db, :our_node
|
4
|
+
|
5
|
+
def initialize(nodesv)
|
6
|
+
@nodes = nodesv
|
7
|
+
|
8
|
+
# Automatically get our shard_db address from our own node name
|
9
|
+
@our_node = nodesv.our_node
|
10
|
+
@local_shard_db = nodesv.our_node.get_shard_db
|
11
|
+
|
12
|
+
puts "Assign default database for shard ranges (#{@local_shard_db})"
|
13
|
+
ShardRange.use_database @local_shard_db
|
14
|
+
end
|
15
|
+
|
16
|
+
def sync
|
17
|
+
# We replicate our state from the management node(s)
|
18
|
+
# We never push content if we are only storage
|
19
|
+
management_nodes = Node.by_is_management.reverse
|
20
|
+
|
21
|
+
# NOTE: randomize the order for load balancing here
|
22
|
+
|
23
|
+
management_nodes.each do |mng_node|
|
24
|
+
remote_db = mng_node.get_shard_db
|
25
|
+
bool_from = @our_node.replicate_from(local_shard_db, mng_node, remote_db)
|
26
|
+
if our_node.is_management
|
27
|
+
# Push any changes to other management nodes
|
28
|
+
bool_to = @our_node.replicate_to(local_shard_db, mng_node, remote_db)
|
29
|
+
end
|
30
|
+
break if bool_from && !our_node.is_management
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def build_shards(number)
|
35
|
+
# Make a large list of possible id boundaries
|
36
|
+
characters = []
|
37
|
+
("0".."9").each do |c|
|
38
|
+
characters << c
|
39
|
+
end
|
40
|
+
("a".."f").each do |c|
|
41
|
+
characters << c
|
42
|
+
end
|
43
|
+
|
44
|
+
# Generate 36 x 36 keys to choose boundaries from
|
45
|
+
all_keys = []
|
46
|
+
characters.each do |c1|
|
47
|
+
characters.each do |c2|
|
48
|
+
characters.each do |c3|
|
49
|
+
all_keys << (c1+c2+c3)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Now chose our boundaries
|
55
|
+
num_range_keys = all_keys.length
|
56
|
+
approx_shard_size = (num_range_keys * 1.0) / number
|
57
|
+
|
58
|
+
shard_starts = []
|
59
|
+
(0...number).each do |n|
|
60
|
+
shard_starts << (all_keys[(n * approx_shard_size).floor])
|
61
|
+
end
|
62
|
+
|
63
|
+
shard_ends = shard_starts.clone
|
64
|
+
shard_ends << ("z" * 3)
|
65
|
+
shard_ends.delete_at(0)
|
66
|
+
|
67
|
+
# Finally build them!
|
68
|
+
puts "Build Shards"
|
69
|
+
(0...number).each do |n|
|
70
|
+
puts "Shard #{n}: from #{shard_starts[n]} to #{shard_ends[n]}"
|
71
|
+
shard_name = "sh_#{shard_starts[n]}_to_#{shard_ends[n]}"
|
72
|
+
sr = ShardRange.new
|
73
|
+
sr.range_start = shard_starts[n]
|
74
|
+
sr.range_end = shard_ends[n]
|
75
|
+
sr.shard_db_name = shard_name
|
76
|
+
sr.save
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def get_shard(internal_id)
|
81
|
+
# Finds the list of shards within which this ID lives
|
82
|
+
all_shards = ShardRange.by_range_start
|
83
|
+
selected_shards = []
|
84
|
+
all_shards.each do |a_shard| # TODO: linear search is inefficient -- create a view
|
85
|
+
if a_shard.range_start <= internal_id && a_shard.range_end > internal_id
|
86
|
+
selected_shards << a_shard
|
87
|
+
end
|
88
|
+
end
|
89
|
+
selected_shards
|
90
|
+
end
|
91
|
+
|
92
|
+
def get_content_shards
|
93
|
+
# Return the content_shards of our node
|
94
|
+
content_shards = {}
|
95
|
+
ShardRange.by_node(:key => our_node.name).each do |s|
|
96
|
+
|
97
|
+
# Build a content shard object
|
98
|
+
content_shards[s.shard_db_name] = ContentShard.new(nodes, s)
|
99
|
+
end
|
100
|
+
puts "Content shards #{content_shards.length}"
|
101
|
+
content_shards
|
102
|
+
end
|
103
|
+
|
104
|
+
def write_doc_directly(content_doc)
|
105
|
+
# Write a document directly to a nodes content repository
|
106
|
+
success = {:ok => false , :reason => "No available node found"}
|
107
|
+
doc_shard = get_shard(content_doc.internal_id).first
|
108
|
+
doc_shard.get_nodes.each do |node|
|
109
|
+
# Try to write the doc to this node
|
110
|
+
begin
|
111
|
+
remote_node = Node.by_name(:key => node).first
|
112
|
+
remote_ndb = NodeDB.new(remote_node)
|
113
|
+
remote_content_shard = ContentShard.new(remote_ndb, doc_shard)
|
114
|
+
remote_content_shard.this_shard_db.save_doc(content_doc)
|
115
|
+
success = {:ok => true, :doc => content_doc, :db => remote_content_shard.this_shard_db}
|
116
|
+
break
|
117
|
+
rescue RestClient::RequestFailed => rf
|
118
|
+
if rf.http_code == 409
|
119
|
+
puts "Document already there"
|
120
|
+
return {:ok => false , :reason => "Document already in database"}
|
121
|
+
end
|
122
|
+
rescue Exception =>e
|
123
|
+
puts "Could not put doc in node #{node} because of #{rf}"
|
124
|
+
# TODO: change status or chose another management server
|
125
|
+
remote_node.status = "UNAVAILABLE"
|
126
|
+
remote_node.save
|
127
|
+
end
|
128
|
+
end
|
129
|
+
return success
|
130
|
+
end
|
131
|
+
|
132
|
+
def get_doc_directly(internal_id)
|
133
|
+
# Write a document directly to a nodes content repository
|
134
|
+
doc_shard = get_shard(internal_id).first
|
135
|
+
|
136
|
+
# TODO: Randomize the order of nodes for load balancing in retrieval!
|
137
|
+
docx = []
|
138
|
+
doc_shard.get_nodes.each do |node|
|
139
|
+
# Try to write the doc to this node
|
140
|
+
begin
|
141
|
+
remote_node = Node.by_name(:key => node).first
|
142
|
+
remote_ndb = NodeDB.new(remote_node)
|
143
|
+
remote_content_shard = ContentShard.new(remote_ndb, doc_shard)
|
144
|
+
|
145
|
+
docx = ContentDoc.by_internal_id(:key => internal_id, :database => remote_content_shard.this_shard_db)
|
146
|
+
if docx.length > 0
|
147
|
+
return {:ok => true, :doc => docx.first, :db => remote_content_shard.this_shard_db }
|
148
|
+
end
|
149
|
+
rescue
|
150
|
+
puts "Could not put doc in node #{node.name}"
|
151
|
+
# TODO: change status or chose another management server
|
152
|
+
remote_node.status = "UNAVAILABLE"
|
153
|
+
remote_node.save
|
154
|
+
end
|
155
|
+
end
|
156
|
+
return {:ok => false }
|
157
|
+
end
|
158
|
+
|
159
|
+
end
|
160
|
+
|
161
|
+
class ShardRange < CouchRest::ExtendedDocument
|
162
|
+
# SHARDSERVER = CouchRest.new("#{ARGV[1]}")
|
163
|
+
# SHARDSERVER.default_database = "#{ARGV[0]}_shard_db"
|
164
|
+
# use_database SHARDSERVER.default_database
|
165
|
+
|
166
|
+
property :range_start
|
167
|
+
property :range_end
|
168
|
+
property :node_list
|
169
|
+
property :master_node
|
170
|
+
property :shard_db_name
|
171
|
+
|
172
|
+
view_by :range_start
|
173
|
+
|
174
|
+
# View "node" - given a node returns the shards watched
|
175
|
+
# How to use this new
|
176
|
+
#
|
177
|
+
# puts "Which Shards does 'node1' watch?"
|
178
|
+
# ShardRange.by_node(:key => "node1").each do |s|
|
179
|
+
# puts "Shard: #{s.shard_db_name}"
|
180
|
+
# end
|
181
|
+
|
182
|
+
|
183
|
+
view_by :node,
|
184
|
+
:map =>
|
185
|
+
"function(doc) {
|
186
|
+
if (doc['couchrest-type'] == 'ShardRange' && doc.node_list) {
|
187
|
+
doc.node_list.forEach(function(node){
|
188
|
+
emit(node, 1);
|
189
|
+
});
|
190
|
+
}
|
191
|
+
}"
|
192
|
+
|
193
|
+
def get_nodes
|
194
|
+
# Return all nodes, with the master being first
|
195
|
+
all_nodes = node_list.clone
|
196
|
+
all_nodes.delete(master_node)
|
197
|
+
all_nodes = [master_node] + all_nodes
|
198
|
+
all_nodes
|
199
|
+
end
|
200
|
+
|
201
|
+
end
|
202
|
+
|