adhd 0.1.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +6 -2
- data/VERSION +1 -1
- data/adhd.gemspec +8 -3
- data/lib/adhd/adhd_rest_server.rb +9 -1
- data/lib/adhd/models/node_db.rb +62 -0
- data/lib/adhd/models/node_doc.rb +26 -88
- data/lib/adhd/node_manager.rb +41 -33
- data/lib/adhd/reactor.rb +30 -36
- data/test/test_adhd.rb +3 -3
- data/test/unit/test_content_doc.rb +0 -0
- data/test/unit/test_node.rb +138 -0
- metadata +6 -1
data/README.rdoc
CHANGED
@@ -1,12 +1,16 @@
|
|
1
1
|
= adhd
|
2
2
|
|
3
|
-
Adhd is an asynchronous, distributed hard drive. Actually we're not sure if it's really asynchronous or even what asynchronicity would mean in the context of a hard drive, but it is definitely distributed. Adhd is essentially a management layer (written using Ruby and eventmachine) which controls clusters of CouchDB databases to replicate files across disparate machines. Unlike most clustering storage solutions, adhd assumes that machines in the cluster may have different
|
3
|
+
Adhd is an asynchronous, distributed hard drive. Actually we're not sure if it's really asynchronous or even what asynchronicity would mean in the context of a hard drive, but it is definitely distributed. Adhd is essentially a management layer (written using Ruby and eventmachine) which controls clusters of CouchDB databases to replicate files across disparate machines. Unlike most clustering storage solutions, adhd assumes that machines in the cluster may have different capabilities and is designed to work both inside and outside the data centre.
|
4
4
|
|
5
5
|
== Installation
|
6
6
|
|
7
7
|
Don't use this software yet. It is experimental and may eat your mother.
|
8
8
|
|
9
|
-
Having said that,
|
9
|
+
Having said that,
|
10
|
+
|
11
|
+
sudo gem install adhd
|
12
|
+
|
13
|
+
may do the job.
|
10
14
|
|
11
15
|
== How it works
|
12
16
|
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.1.
|
1
|
+
0.1.1
|
data/adhd.gemspec
CHANGED
@@ -5,7 +5,7 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{adhd}
|
8
|
-
s.version = "0.1.
|
8
|
+
s.version = "0.1.1"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["dave.hrycyszyn@headlondon.com"]
|
@@ -32,6 +32,7 @@ Gem::Specification.new do |s|
|
|
32
32
|
"lib/adhd/config.yml",
|
33
33
|
"lib/adhd/models/content_doc.rb",
|
34
34
|
"lib/adhd/models/content_shard.rb",
|
35
|
+
"lib/adhd/models/node_db.rb",
|
35
36
|
"lib/adhd/models/node_doc.rb",
|
36
37
|
"lib/adhd/models/shard_range.rb",
|
37
38
|
"lib/adhd/node_manager.rb",
|
@@ -57,7 +58,9 @@ Gem::Specification.new do |s|
|
|
57
58
|
"lib/views/index.erb",
|
58
59
|
"lib/views/layout.erb",
|
59
60
|
"test/helper.rb",
|
60
|
-
"test/test_adhd.rb"
|
61
|
+
"test/test_adhd.rb",
|
62
|
+
"test/unit/test_content_doc.rb",
|
63
|
+
"test/unit/test_node.rb"
|
61
64
|
]
|
62
65
|
s.homepage = %q{http://github.com/futurechimp/adhd}
|
63
66
|
s.rdoc_options = ["--charset=UTF-8"]
|
@@ -65,7 +68,9 @@ Gem::Specification.new do |s|
|
|
65
68
|
s.rubygems_version = %q{1.3.5}
|
66
69
|
s.summary = %q{An experiment in distributed file replication using CouchDB}
|
67
70
|
s.test_files = [
|
68
|
-
"test/
|
71
|
+
"test/unit/test_content_doc.rb",
|
72
|
+
"test/unit/test_node.rb",
|
73
|
+
"test/helper.rb",
|
69
74
|
"test/test_adhd.rb"
|
70
75
|
]
|
71
76
|
|
@@ -3,6 +3,7 @@ require 'uri'
|
|
3
3
|
require 'net/http'
|
4
4
|
require 'webrick'
|
5
5
|
|
6
|
+
|
6
7
|
module ProxyToServer
|
7
8
|
# This implements the connection that proxies an incoming file to to the
|
8
9
|
# respective CouchDB instance, as an attachment.
|
@@ -64,8 +65,15 @@ require 'webrick'
|
|
64
65
|
# Detected end of headers
|
65
66
|
header_data = @buffer[0...($~.begin(0))]
|
66
67
|
|
68
|
+
@web_config = WEBrick::Config::HTTP.clone
|
69
|
+
@web_config[:HTTPVersion] = WEBrick::HTTPVersion.new("1.0")
|
70
|
+
|
67
71
|
# Try the webrick parser
|
68
|
-
@req = WEBrick::HTTPRequest.new(
|
72
|
+
@req = WEBrick::HTTPRequest.new(@web_config)
|
73
|
+
@res = WEBrick::HTTPResponse.new(@web_config)
|
74
|
+
|
75
|
+
|
76
|
+
puts @res.to_s
|
69
77
|
|
70
78
|
StringIO.open(header_data, 'rb') do |socket|
|
71
79
|
@req.parse(socket)
|
@@ -0,0 +1,62 @@
|
|
1
|
+
class NodeDB
|
2
|
+
|
3
|
+
attr_accessor :local_node_db, :our_node
|
4
|
+
|
5
|
+
def initialize(our_nodev)
|
6
|
+
@our_node = our_nodev
|
7
|
+
|
8
|
+
# Get the address of the CDB from the node
|
9
|
+
@local_node_db = our_nodev.get_node_db
|
10
|
+
end
|
11
|
+
|
12
|
+
# Syncs this management node with other existing management nodes by looping
|
13
|
+
# through all known management nodes.
|
14
|
+
#
|
15
|
+
# If replication to or from any management node fails, the method breaks
|
16
|
+
# and continues replicating to other management nodes until all management
|
17
|
+
# nodes have been tried.
|
18
|
+
#
|
19
|
+
# NOTE: randomize the order for load balancing here
|
20
|
+
#
|
21
|
+
# NOTE2: How to build skynet (TODO)
|
22
|
+
# -------------------
|
23
|
+
# If length of management is zero, then chose 3 different random
|
24
|
+
# nodes at each sync, and sync with them in node_name order.
|
25
|
+
# This guarantees that any updates on nodes are communicated in
|
26
|
+
# O(log N) ephocs, at the cost of O(3 * N) connections per epoch.
|
27
|
+
# It also guarantees any new management servers are discovered in
|
28
|
+
# this O(log N) time, creating "jelly fish" or "partition proof"
|
29
|
+
# availability.
|
30
|
+
def sync
|
31
|
+
# We replicate our state to the management node(s)
|
32
|
+
management_nodes = Node.by_is_management.reverse
|
33
|
+
|
34
|
+
management_nodes.each do |mng_node|
|
35
|
+
remote_db = mng_node.get_node_db
|
36
|
+
from_success = @our_node.replicate_from(local_node_db, mng_node, remote_db)
|
37
|
+
to_success = @our_node.replicate_to(local_node_db, mng_node, remote_db)
|
38
|
+
if from_success && to_success && !our_node.is_management
|
39
|
+
break
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Returns all nodes marked as available
|
45
|
+
#
|
46
|
+
def available_node_list
|
47
|
+
all_nodes = Node.by_name
|
48
|
+
return all_nodes.select {|node| node.status == "RUNNING"}
|
49
|
+
end
|
50
|
+
|
51
|
+
# Returns the first RUNNING management node. There is no real dependency on
|
52
|
+
# any specific node, this is just a way for all nodes to agree on the same
|
53
|
+
# node to do the job of the head management node.
|
54
|
+
#
|
55
|
+
def head_management_node
|
56
|
+
management_nodes = Node.by_is_management.reverse
|
57
|
+
hmn = management_nodes.find {|node| node.status == "RUNNING"}
|
58
|
+
return hmn
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
data/lib/adhd/models/node_doc.rb
CHANGED
@@ -1,63 +1,5 @@
|
|
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
1
|
|
55
2
|
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
3
|
unique_id :name
|
62
4
|
|
63
5
|
property :name
|
@@ -75,23 +17,17 @@ class Node < CouchRest::ExtendedDocument
|
|
75
17
|
def get_node_db
|
76
18
|
server = CouchRest.new("#{url}")
|
77
19
|
db = server.database!("#{name}_node_db")
|
78
|
-
# puts "Open db #{db}"
|
79
|
-
db
|
80
20
|
end
|
81
21
|
|
82
22
|
def get_shard_db
|
83
23
|
server = CouchRest.new("#{url}")
|
84
24
|
db = server.database!("#{name}_shard_db")
|
85
|
-
|
86
|
-
|
87
|
-
end
|
88
|
-
|
25
|
+
end
|
26
|
+
|
89
27
|
def get_content_db(shard_db_name)
|
90
28
|
server = CouchRest.new("#{url}")
|
91
29
|
db = server.database!("#{name}_#{shard_db_name}_content_db")
|
92
|
-
|
93
|
-
db
|
94
|
-
end
|
30
|
+
end
|
95
31
|
|
96
32
|
# Replicating databases and marking nodes as unavailable
|
97
33
|
# In the future we should hook these into a "replication manager"
|
@@ -99,32 +35,33 @@ class Node < CouchRest::ExtendedDocument
|
|
99
35
|
# databases, and only do a replication after some time lapses.
|
100
36
|
|
101
37
|
def replicate_to(local_db, other_node, remote_db)
|
102
|
-
|
103
|
-
|
104
|
-
|
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
|
-
|
38
|
+
replicate_to_or_from(local_db, other_node, remote_db, true)
|
39
|
+
end
|
40
|
+
|
119
41
|
def replicate_from(local_db, other_node, remote_db)
|
42
|
+
replicate_to_or_from(local_db, other_node, remote_db, false)
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
# Replicates to or from a management node database. The direction of
|
48
|
+
# replication is controlled by a boolean property.
|
49
|
+
#
|
50
|
+
# Returns true if replication succeeds, false if not.
|
51
|
+
#
|
52
|
+
def replicate_to_or_from(local_db, other_node, remote_db, to = true)
|
120
53
|
# Do not try to contact unavailable nodes
|
121
54
|
return false if other_node.status == "UNAVAILABLE"
|
122
|
-
# No point replicating to ourselves
|
55
|
+
# No point replicating to ourselves
|
123
56
|
return false if (name == other_node.name)
|
124
|
-
|
125
|
-
begin
|
57
|
+
|
58
|
+
begin
|
126
59
|
# Replicate to other node is possible
|
127
|
-
|
60
|
+
if to
|
61
|
+
local_db.replicate_to(remote_db)
|
62
|
+
else
|
63
|
+
local_db.replicate_from(remote_db)
|
64
|
+
end
|
128
65
|
return true
|
129
66
|
rescue Exception => e
|
130
67
|
# Other node turns out to be unavailable
|
@@ -135,5 +72,6 @@ class Node < CouchRest::ExtendedDocument
|
|
135
72
|
|
136
73
|
end
|
137
74
|
|
75
|
+
|
138
76
|
end
|
139
77
|
|
data/lib/adhd/node_manager.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
require 'rubygems'
|
2
2
|
require 'couchrest'
|
3
3
|
require 'ruby-debug'
|
4
|
-
# require File.dirname(__FILE__) + '/models'
|
5
4
|
require File.dirname(__FILE__) + '/models/node_doc'
|
5
|
+
require File.dirname(__FILE__) + '/models/node_db'
|
6
6
|
require File.dirname(__FILE__) + '/models/content_doc'
|
7
7
|
require File.dirname(__FILE__) + '/models/shard_range'
|
8
8
|
require File.dirname(__FILE__) + '/models/content_shard'
|
@@ -79,7 +79,7 @@ module Adhd
|
|
79
79
|
def build_node_admin_databases
|
80
80
|
@conn_manager = ConnectionBank.new
|
81
81
|
|
82
|
-
#
|
82
|
+
# Let's build a nice NodeDB
|
83
83
|
@ndb = NodeDB.new(@our_node)
|
84
84
|
conn_node = UpdateNotifierConnection.new(@config.node_url,
|
85
85
|
@config.couchdb_server_port,
|
@@ -101,12 +101,16 @@ module Adhd
|
|
101
101
|
|
102
102
|
end
|
103
103
|
|
104
|
+
# A node changed status (became available, or became unavailable).
|
105
|
+
#
|
106
|
+
# If we are the admin, when a node joins we should allocate some shards to
|
107
|
+
# it.
|
108
|
+
#
|
109
|
+
# Only the head management node deals with node changes.
|
110
|
+
#
|
111
|
+
# XXXX doc question: what does it mean to be 'the admin'?
|
112
|
+
#
|
104
113
|
def handle_node_update update
|
105
|
-
# Added, removed or changed the status of a node
|
106
|
-
# If we are the admin, when a node joins we should allocate to it
|
107
|
-
# some shards.
|
108
|
-
|
109
|
-
# Only the head management node deals with node changes
|
110
114
|
return if @ndb.head_management_node && ! (@ndb.head_management_node.name == @our_node.name)
|
111
115
|
|
112
116
|
# Given the shard_db and the node_db we should work out a new allocation
|
@@ -118,8 +122,9 @@ module Adhd
|
|
118
122
|
end
|
119
123
|
|
120
124
|
|
125
|
+
# Build content shard databases.
|
126
|
+
#
|
121
127
|
def build_node_content_databases
|
122
|
-
# Get all content shard databases
|
123
128
|
# NOTE: we will have to refresh those then we are re-assigned shards
|
124
129
|
@contentdbs = {} if !@contentdbs
|
125
130
|
current_shards = @srdb.get_content_shards
|
@@ -150,19 +155,21 @@ module Adhd
|
|
150
155
|
end
|
151
156
|
end
|
152
157
|
|
158
|
+
# Kills the connection listening for updates on this shard
|
159
|
+
#
|
160
|
+
# TODO: test if the sync happened
|
161
|
+
# content_shard.this_shard_db.delete!
|
162
|
+
# TODO: run a sync with the current master to ensure that
|
163
|
+
# any changes have been pushed. The DELETE the database
|
164
|
+
# to save space
|
153
165
|
def remove_content_shard content_shard, connection
|
154
|
-
# Kill the connection listening for updates on this shard
|
155
166
|
connection.kill
|
156
167
|
content_shard.sync
|
157
|
-
# TODO: test if the sync happened
|
158
|
-
# content_shard.this_shard_db.delete!
|
159
|
-
# TODO: run a sync with the current master to ensure that
|
160
|
-
# any changes have been pushed. The DELETE the database
|
161
|
-
# to save space
|
162
168
|
end
|
163
169
|
|
170
|
+
# Enters the eventmachine loop
|
171
|
+
#
|
164
172
|
def run
|
165
|
-
# Enters the event machine loop
|
166
173
|
@conn_manager.run_all
|
167
174
|
end
|
168
175
|
|
@@ -201,10 +208,10 @@ module Adhd
|
|
201
208
|
end
|
202
209
|
end
|
203
210
|
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
211
|
+
def sync_admin
|
212
|
+
@ndb.sync # SYNC
|
213
|
+
@srdb.sync # SYNC
|
214
|
+
end
|
208
215
|
|
209
216
|
end
|
210
217
|
end
|
@@ -213,21 +220,22 @@ end
|
|
213
220
|
|
214
221
|
require 'md5'
|
215
222
|
|
223
|
+
# This is an automatic way to allocate shards to nodes that just
|
224
|
+
# arrive in the networks, as well as re-allocate shards if nodes
|
225
|
+
# become unavailable or leave the network.
|
226
|
+
#
|
227
|
+
# NOTE: How to build skynet (Part III)
|
228
|
+
#
|
229
|
+
# The invariant we try to impose on the list of nodes part of a shard
|
230
|
+
# is that there should be at least replication_factor available nodes
|
231
|
+
# in it. At the same time we try to keep the list stable over nodes
|
232
|
+
# joining and leaving. To achieve this we hash in sequence the name of
|
233
|
+
# each node with the name of the shard. We sort this list by hash, and
|
234
|
+
# choose the first n nodes such that at least replication_factor nodes
|
235
|
+
# are available. Then we chose the first available node as the master
|
236
|
+
# for that shard.
|
237
|
+
#
|
216
238
|
def assign_nodes_to_shards(node_list, shard_range_list, replication_factor)
|
217
|
-
# This is an automatic way to allocate shards to nodes that just
|
218
|
-
# arrive in the networks, as well as re-allocate shards if nodes
|
219
|
-
# become unavailable or leave the network.
|
220
|
-
|
221
|
-
# NOTE: How to build skynet (Part III)
|
222
|
-
#
|
223
|
-
# The invarient we try to impost on the list of nodes part of a shard
|
224
|
-
# is that there should be at least replication_factor available nodes
|
225
|
-
# in it. At the same time we try to keep the list stable over nodes
|
226
|
-
# joining and leaving. To achieve this we hash in sequence the name of
|
227
|
-
# each node with the name of the shard. We sort this list by hash, and
|
228
|
-
# choose the first n nodes such that at least replication_factor nodes
|
229
|
-
# are available. Then we chose the first available node as the master
|
230
|
-
# for that shard.
|
231
239
|
|
232
240
|
shard_range_list.each do |shard_range|
|
233
241
|
# Sort all nodes using consistent hashing
|
data/lib/adhd/reactor.rb
CHANGED
@@ -54,10 +54,10 @@ module Adhd
|
|
54
54
|
|
55
55
|
# Shoots update notifications from CouchDB to the @conn.
|
56
56
|
#
|
57
|
-
def receive_data data
|
57
|
+
def receive_data data
|
58
58
|
# puts "received_data: #{data}"
|
59
59
|
# puts "||#{data}||length=#{data.length}||#{data.dump}||"
|
60
|
-
|
60
|
+
|
61
61
|
@buffer += data # Add the data to the current buffer
|
62
62
|
updates = []
|
63
63
|
if @buffer =~ /(\{[^\n]+\}\n)/
|
@@ -65,7 +65,7 @@ module Adhd
|
|
65
65
|
# Trim the buffer to $_.end(0)
|
66
66
|
@buffer = @buffer[$~.end(0)..-1]
|
67
67
|
end
|
68
|
-
|
68
|
+
|
69
69
|
# Regexp for JSON updates is /\{[\n]\}+/
|
70
70
|
updates.each do |json_event|
|
71
71
|
@conn_obj.event_handler(json_event) unless data == "\n"
|
@@ -88,9 +88,9 @@ module Adhd
|
|
88
88
|
# predicate is false we can simply close the connection. For example upon
|
89
89
|
# being given control of a different content shard, or a different master
|
90
90
|
# for the shard.
|
91
|
-
|
91
|
+
#
|
92
92
|
# In practice we will have two types of connections: Replicate and Notify.
|
93
|
-
|
93
|
+
#
|
94
94
|
class UpdateNotifierConnection
|
95
95
|
attr_accessor :db_name, :base_url, :connection_inside, :name
|
96
96
|
|
@@ -102,42 +102,41 @@ module Adhd
|
|
102
102
|
@status = "NOTRUNNING"
|
103
103
|
@base_url = "http://#{@node_url}:#{@couchdb_server_port}"
|
104
104
|
@name = @base_url +"/"+ @db_name
|
105
|
-
@keep_alive = true
|
105
|
+
@keep_alive = true
|
106
106
|
end
|
107
|
-
|
107
|
+
|
108
108
|
def kill
|
109
109
|
@keep_alive = false
|
110
110
|
end
|
111
111
|
|
112
112
|
def start
|
113
|
-
puts "
|
113
|
+
puts "Registering the connection for: #{@db_name}"
|
114
114
|
EM.connect @node_url, @couchdb_server_port, Adhd::DbUpdateNotifier, @db_name, self
|
115
115
|
@status = "RUNNING"
|
116
116
|
end
|
117
117
|
|
118
118
|
def event_handler data
|
119
|
-
|
120
|
-
puts "Run a crazy sync on db #{@db_name}"
|
121
|
-
#@db_obj_for_sync.sync
|
119
|
+
puts "Run a crazy sync on db: #{@db_name}"
|
122
120
|
@sync_block.call(data)
|
123
121
|
end
|
124
122
|
|
125
123
|
def close_handler
|
126
|
-
puts "Closed abnormally #{reason}"
|
124
|
+
puts "Closed abnormally: #{reason}"
|
127
125
|
@status = "NOTRUNNING"
|
128
126
|
end
|
129
127
|
|
130
128
|
def down_for_good(reason)
|
131
129
|
if reason
|
132
|
-
puts "Closed for good #{reason}"
|
130
|
+
puts "Closed for good: #{reason}"
|
133
131
|
end
|
134
132
|
end
|
135
133
|
|
134
|
+
# Returns the truth value of the predicate
|
135
|
+
#
|
136
136
|
def keep_alive?
|
137
|
-
# Returns the truth value of the predicate
|
138
137
|
@keep_alive
|
139
138
|
end
|
140
|
-
|
139
|
+
|
141
140
|
def keep_alive_or_kill!
|
142
141
|
if ! keep_alive?
|
143
142
|
# Schedule this connection for close
|
@@ -154,18 +153,9 @@ module Adhd
|
|
154
153
|
(@status == "NOTRUNNING")
|
155
154
|
end
|
156
155
|
|
157
|
-
|
158
156
|
end
|
159
157
|
|
160
158
|
class Connection
|
161
|
-
#def on_teardown(&block)
|
162
|
-
# # Set the handler to be called then a connection is dead
|
163
|
-
# block(self) # Run the teardown handler
|
164
|
-
#end
|
165
|
-
|
166
|
-
def initialize
|
167
|
-
|
168
|
-
end
|
169
159
|
|
170
160
|
def should_start?
|
171
161
|
!(@status == "RUNNING")
|
@@ -177,21 +167,25 @@ module Adhd
|
|
177
167
|
|
178
168
|
end
|
179
169
|
|
170
|
+
# Manage a bunch of connections for us
|
171
|
+
#
|
180
172
|
class ConnectionBank
|
181
|
-
|
173
|
+
|
182
174
|
def initialize
|
183
175
|
@our_connections = []
|
184
176
|
end
|
185
177
|
|
178
|
+
# Add a connection to the ConnectionBank, making sure we have no duplicates.
|
179
|
+
#
|
186
180
|
def add_connection(conn)
|
187
|
-
# Make sure we have no duplicates
|
188
181
|
@our_connections.each do |c|
|
189
182
|
if conn.name == c.name
|
190
183
|
return
|
191
184
|
end
|
192
|
-
end
|
193
|
-
|
194
|
-
# If it is happy to run, add it to the list and start it!
|
185
|
+
end
|
186
|
+
|
187
|
+
# If it is happy to run, add it to the list and start it!
|
188
|
+
#
|
195
189
|
if conn.keep_alive?
|
196
190
|
@our_connections << conn
|
197
191
|
# Register the teardown handler for when the end comes...
|
@@ -199,9 +193,10 @@ module Adhd
|
|
199
193
|
end
|
200
194
|
end
|
201
195
|
|
196
|
+
# When a connection is down, we check to see if it wants to be kept
|
197
|
+
# alive, and restart it; otherwise we remove it from the list.
|
198
|
+
#
|
202
199
|
def rerun(conn)
|
203
|
-
# When a connection is down, we check to see if it wants to be kept
|
204
|
-
# alive, and restart it otherwise we remove it from the list.
|
205
200
|
if conn.keep_alive?
|
206
201
|
begin
|
207
202
|
conn.start
|
@@ -210,7 +205,7 @@ module Adhd
|
|
210
205
|
end
|
211
206
|
else
|
212
207
|
# It seems we have died of natural causes
|
213
|
-
# XXX: is it true that Ruby does not throw
|
208
|
+
# XXX: is it true that Ruby does not throw an exception for EOF?
|
214
209
|
# Otherwise we will never see this
|
215
210
|
conn.keep_alive_or_kill!
|
216
211
|
@our_connections.delete(conn)
|
@@ -219,14 +214,13 @@ module Adhd
|
|
219
214
|
|
220
215
|
end
|
221
216
|
|
217
|
+
# Go through all connections and run them all
|
218
|
+
# Run within EM.run loop
|
219
|
+
#
|
222
220
|
def run_all
|
223
|
-
# Go through all connections and run them all
|
224
|
-
# Run within EM.run loop
|
225
|
-
# puts "Connection bank runs all... (#{@our_connections.length} connections)"
|
226
221
|
@our_connections.each do |c|
|
227
222
|
if c.is_closed? or !c.keep_alive?
|
228
223
|
puts "Actually rerun #{c.db_name}..."
|
229
|
-
|
230
224
|
rerun(c)
|
231
225
|
end
|
232
226
|
end
|
data/test/test_adhd.rb
CHANGED
@@ -1,8 +1,7 @@
|
|
1
1
|
require 'rubygems'
|
2
2
|
require 'test/unit'
|
3
3
|
require 'shoulda'
|
4
|
-
require '
|
5
|
-
require File.dirname(__FILE__) + '/../models'
|
4
|
+
#require File.dirname(__FILE__) + '/../models'
|
6
5
|
|
7
6
|
class TestAdhd < Test::Unit::TestCase
|
8
7
|
|
@@ -11,7 +10,7 @@ class TestAdhd < Test::Unit::TestCase
|
|
11
10
|
|
12
11
|
setup do
|
13
12
|
assert_nothing_raised do
|
14
|
-
NODESERVER = CouchRest.new("http://192.168.1.
|
13
|
+
NODESERVER = CouchRest.new("http://192.168.1.93:5984")
|
15
14
|
NODESERVER.default_database = "node_db"
|
16
15
|
@node_db = CouchRest::Database.new(NODESERVER, "node_db")
|
17
16
|
end
|
@@ -71,3 +70,4 @@ class TestAdhd < Test::Unit::TestCase
|
|
71
70
|
private
|
72
71
|
|
73
72
|
end
|
73
|
+
|
File without changes
|
@@ -0,0 +1,138 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'test/unit'
|
3
|
+
require 'shoulda'
|
4
|
+
require 'couchrest'
|
5
|
+
require File.dirname(__FILE__) + '/../../lib/adhd/models/node_doc'
|
6
|
+
|
7
|
+
# A db that always pretents to copy a db
|
8
|
+
module FakeDb
|
9
|
+
def get_target
|
10
|
+
@target
|
11
|
+
end
|
12
|
+
|
13
|
+
def replicate_to t
|
14
|
+
@target = t
|
15
|
+
end
|
16
|
+
|
17
|
+
def replicate_from target
|
18
|
+
@target = t
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# A db that always throws a replication exception
|
23
|
+
module BlowDb
|
24
|
+
def get_target
|
25
|
+
@target
|
26
|
+
end
|
27
|
+
|
28
|
+
def replicate_to t
|
29
|
+
throw Exception.new
|
30
|
+
end
|
31
|
+
|
32
|
+
def replicate_from target
|
33
|
+
@target = t
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
class TestAdhd < Test::Unit::TestCase
|
39
|
+
|
40
|
+
context "The Node model" do
|
41
|
+
setup do
|
42
|
+
@node = Node.new
|
43
|
+
end
|
44
|
+
|
45
|
+
should "have a name property" do
|
46
|
+
assert @node.respond_to? "name"
|
47
|
+
end
|
48
|
+
|
49
|
+
should "have a url property" do
|
50
|
+
assert @node.respond_to? "url"
|
51
|
+
end
|
52
|
+
|
53
|
+
should "have have an is_store property" do
|
54
|
+
assert @node.respond_to? "is_store"
|
55
|
+
end
|
56
|
+
|
57
|
+
should "have an is_management property" do
|
58
|
+
assert @node.respond_to? "is_management"
|
59
|
+
end
|
60
|
+
|
61
|
+
should "have an is_directory property" do
|
62
|
+
assert @node.respond_to? "is_directory"
|
63
|
+
end
|
64
|
+
|
65
|
+
should "have a status property" do
|
66
|
+
assert @node.respond_to? "status"
|
67
|
+
end
|
68
|
+
|
69
|
+
should "have timestamp properties" do
|
70
|
+
assert @node.respond_to? "created_at"
|
71
|
+
assert @node.respond_to? "updated_at"
|
72
|
+
end
|
73
|
+
|
74
|
+
should "have methods to replicate to and from other DBs" do
|
75
|
+
assert @node.respond_to? "replicate_to"
|
76
|
+
assert @node.respond_to? "replicate_from"
|
77
|
+
end
|
78
|
+
|
79
|
+
context "replication" do
|
80
|
+
|
81
|
+
setup do
|
82
|
+
@other_node = Node.new
|
83
|
+
|
84
|
+
# Simulate a DB class
|
85
|
+
local_db_klass = Class.new do
|
86
|
+
include FakeDb
|
87
|
+
end
|
88
|
+
local_blow_db_klass = Class.new do
|
89
|
+
include BlowDb
|
90
|
+
end
|
91
|
+
|
92
|
+
@node.name = "Node1"
|
93
|
+
@other_node.name = "Node2"
|
94
|
+
@local_db = local_db_klass.new
|
95
|
+
@local_blow_db = local_blow_db_klass.new
|
96
|
+
end
|
97
|
+
|
98
|
+
should "copy databases across" do
|
99
|
+
@node.replicate_to(@local_db, @other_node, "TARGET")
|
100
|
+
assert @local_db.get_target && @local_db.get_target == "TARGET"
|
101
|
+
end
|
102
|
+
|
103
|
+
should "not copy to same node" do
|
104
|
+
assert !@node.replicate_to(@local_db, @node, "TARGET")
|
105
|
+
assert !@local_db.get_target # Has not copied anything indeed
|
106
|
+
end
|
107
|
+
|
108
|
+
should "not copy to same node" do
|
109
|
+
assert !@node.replicate_to(@local_db, @node, "TARGET")
|
110
|
+
assert !@local_db.get_target # Has not copied anything indeed
|
111
|
+
end
|
112
|
+
|
113
|
+
should "not copy to unavailable nodes" do
|
114
|
+
@other_node.status = "UNAVAILABLE"
|
115
|
+
assert !@node.replicate_to(@local_db, @other_node, "TARGET")
|
116
|
+
assert !@local_db.get_target # Has not copied anything indeed
|
117
|
+
end
|
118
|
+
|
119
|
+
should "tag unavailable nodes" do
|
120
|
+
fake_node_klass = Class.new do
|
121
|
+
attr_accessor :status, :saved, :name
|
122
|
+
def initialize
|
123
|
+
@name = "Node2"
|
124
|
+
end
|
125
|
+
def save
|
126
|
+
@saved = true
|
127
|
+
end
|
128
|
+
end
|
129
|
+
fake_node = fake_node_klass.new
|
130
|
+
|
131
|
+
assert !@node.replicate_to(@local_blow_db, fake_node, "TARGET")
|
132
|
+
assert fake_node.status == "UNAVAILABLE"
|
133
|
+
assert fake_node.saved
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: adhd
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- dave.hrycyszyn@headlondon.com
|
@@ -87,6 +87,7 @@ files:
|
|
87
87
|
- lib/adhd/config.yml
|
88
88
|
- lib/adhd/models/content_doc.rb
|
89
89
|
- lib/adhd/models/content_shard.rb
|
90
|
+
- lib/adhd/models/node_db.rb
|
90
91
|
- lib/adhd/models/node_doc.rb
|
91
92
|
- lib/adhd/models/shard_range.rb
|
92
93
|
- lib/adhd/node_manager.rb
|
@@ -113,6 +114,8 @@ files:
|
|
113
114
|
- lib/views/layout.erb
|
114
115
|
- test/helper.rb
|
115
116
|
- test/test_adhd.rb
|
117
|
+
- test/unit/test_content_doc.rb
|
118
|
+
- test/unit/test_node.rb
|
116
119
|
has_rdoc: true
|
117
120
|
homepage: http://github.com/futurechimp/adhd
|
118
121
|
licenses: []
|
@@ -142,5 +145,7 @@ signing_key:
|
|
142
145
|
specification_version: 3
|
143
146
|
summary: An experiment in distributed file replication using CouchDB
|
144
147
|
test_files:
|
148
|
+
- test/unit/test_content_doc.rb
|
149
|
+
- test/unit/test_node.rb
|
145
150
|
- test/helper.rb
|
146
151
|
- test/test_adhd.rb
|