adhd 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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 amounts of bandwidth available and is designed to work both inside and outside the data centre.
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, <pre>sudo gem install adhd</pre> may do the job.
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.0
1
+ 0.1.1
@@ -5,7 +5,7 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{adhd}
8
- s.version = "0.1.0"
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/helper.rb",
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(WEBrick::Config::HTTP)
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
+
@@ -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
- # puts "Open db #{db}"
86
- db
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
- # puts "Open db #{db}"
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
- # 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
-
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
- local_db.replicate_from(remote_db)
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
 
@@ -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
- # Lets build a nice NodeDB
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
- def sync_admin
205
- @ndb.sync # SYNC
206
- @srdb.sync # SYNC
207
- end
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
@@ -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 "Register the connection for #{@db_name}"
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
- # puts "||#{data}||nn"
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
- # Manage a bunch of connections for us
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 and exception for EOF?
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
@@ -1,8 +1,7 @@
1
1
  require 'rubygems'
2
2
  require 'test/unit'
3
3
  require 'shoulda'
4
- require 'adhd'
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.104:5984")
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.0
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