Before continuing with my exploration of replication in CouchDB, I think I need a quick Ruby gem to help me out. I shut down all of my VMs after last night, meaning that I'll have to re-establish replication when I bring them back online.
I have already used Bones to generate gems, but have yet to try Jeweler. Many people seem to like it as a gem generator / manager, so this seems like a good time to give it a try.
After choosing yet another gem name with a complete lack of imagination, I am ready to begin. To create a scaffold gem with Jeweler, you just need to to call the jeweler command along with the name of your gem:
cstrom@whitefall:~/repos$ jeweler couch-replicate --rspecI also pass in the
create .gitignore
create Rakefile
create LICENSE
create README.rdoc
create .document
create lib
create lib/couch-replicate.rb
create spec
create spec/spec_helper.rb
create spec/couch-replicate_spec.rb
create spec/spec.opts
Jeweler has prepared your gem in couch-replicate
--rspec
option to create RSpec specs and helpers. I thought about sticking with Jeweler's default testing framework, Shoulda because I do not have much experience with it. Although I know some Shoulda and do not expect much trouble with Jeweler, I think it best to limit the number of new things when learning.Getting started, I need to write a spec or two. Right now the Jeweler generated scaffold is egging me on:
cstrom@whitefall:~/repos/couch-replicate$ spec ./spec/couch-replicate_spec.rbHaha. Nice.
F
1)
RuntimeError in 'CouchReplicate fails'
hey buddy, you should probably rename this file and start specing for real
./spec/couch-replicate_spec.rb:5:
Finished in 0.039491 seconds
1 example, 1 failure
I keep the file name and add my first spec. When I established replication last night with
curl
, the commands looked something like this:curl -X POST http://couch-011a.local:5984/_replicate \When using
-d '{"source":"test", "target":"http://couch-011b.local:5984/test", "continuous":true}'
RestClient
, the overall structure of the POST will be identical. The POST will go against the _replicate
resource on the replication source. The JSON payload will include the name of the database being replicated on the source CouchDB server as well as the full URL of the target database (host + DB name). For now, I will assume that the database name on the source and the target will be the same.So, in RSpec, an example of the behavior I expect is something like:
it "should be able to tell a node to replicate itself" doExecuting this example, I find:
RestClient.
should_receive(:post).
with("#{@src_host}/_replicate",
%q|{"source":"#{@db}", "target":"#{@target_host}/#{@db}", "continuous":true}|)
CouchReplicate.replicate(@src_host, @target_host, @db)
end
cstrom@whitefall:~/repos/couch-replicate$ spec ./spec/couch-replicate_spec.rbWell, that's not unexpected. I need to require rest-client, but also list it as a runtime dependency. I add the runtime dependency first:
F
1)
NameError in 'CouchReplicate should be able to tell a node to replicate itself'
uninitialized constant RestClient
./spec/couch-replicate_spec.rb:11:
Finished in 0.010073 seconds
1 example, 1 failure
Jeweler::Tasks.new do |gem|I use the greater than, but on same patch level comparator, "~>", because I got burned recently by an API change between minor releases.
#...
gem.add_development_dependency "rspec", "~> 1.2.0"
gem.add_dependency "rest-client", "~> 1.4.0"
#...
end
When I run rake now, however, I get:
cstrom@whitefall:~/repos/couch-replicate$ rakeBah! I am pretty sure that is the right form for the comparator. What happens when I run that
(in /home/cstrom/repos/couch-replicate)
Missing some dependencies. Install them with the following commands:
gem install rspec --version "~> 1.2.0"
gem install
command line?cstrom@whitefall:~/repos/couch-replicate$ gem install rspec --version "~> 1.2.0"Dang. That was the right comparator. The
WARNING: Installing to ~/.gem since /var/lib/gems/1.8 and
/var/lib/gems/1.8/bin aren't both writable.
**************************************************
Thank you for installing rspec-1.2.9
Please be sure to read History.rdoc and Upgrade.rdoc
for useful information about this release.
**************************************************
Successfully installed rspec-1.2.9
1 gem installed
gem
command recognized it, but Jeweler does not. Shame.Ah well, that may be something for an open source hack night. For now, I will stick with the less restrictive (and less useful) ">=" form:
gem.add_development_dependency "rspec", ">= 1.2.9"That gets me back to my original failure:
gem.add_dependency "rest-client", ">= 1.4.2"
cstrom@whitefall:~/repos/couch-replicate$ rakeNow I am in the normal BDD cycle of change-the-message or make-it-pass. To change the message, I
(in /home/cstrom/repos/couch-replicate)
All dependencies seem to be installed.
F
1)
NameError in 'CouchReplicate should be able to tell a node to replicate itself'
uninitialized constant RestClient
./spec/couch-replicate_spec.rb:11:
Finished in 0.041447 seconds
1 example, 1 failure
require 'rubygems'
in spec_helper.rb
, then require 'restclient'
in couch-replicate.rb. The message from the example has now changed:cstrom@whitefall:~/repos/couch-replicate$ spec ./spec/couch-replicate_spec.rbI can change this message by actually defining the
F
1)
NameError in 'CouchReplicate should be able to tell a node to replicate itself'
uninitialized constant CouchReplicate
./spec/couch-replicate_spec.rb:16:
Finished in 0.011301 seconds
1 example, 1 failure
CouchReplicate
class in the couch-replicate.rb
file:class CouchReplicateNow, I get the message the
end
replicate
method has not been defined:cstrom@whitefall:~/repos/couch-replicate$ spec ./spec/couch-replicate_spec.rbAfter defining the
F
1)
NoMethodError in 'CouchReplicate should be able to tell a node to replicate itself'
undefined method `replicate' for CouchReplicate:Class
./spec/couch-replicate_spec.rb:16:
Finished in 0.011109 seconds
1 example, 1 failure
replicate
method and working the change-the-message cycle a bit more, I finally make-it-pass with:def self.replicate(source_host, target_host, db)I could have pulled in json/json-pure, but I do not expect to work with complex data structures in this gem. For now, I will leave JSON in a string as the "simplest thing that could possibly work". If need be, I will make use of json/json-pure later.
RestClient.post("#{source_host}/_replicate",
%Q|{"source":"#{db}", "target":"#{target_host}/#{db}", "continuous":true}|)
end
Before moving on, I try this method out in
irb
to make sure that I have not overlooked anything. I boot up two of my CouchDB 0.11 VMs and make sure that replication is not enabled:Yup, no replication going on here. In
irb
, I tell this couch-011a server to replicate the "test" database to couch-011b:irb(main):002:0> require 'rubygems'That looks promising. Sure enough, there is now a replication process in place:
=> true
irb(main):003:0> require 'couch-replicate'
=> true
irb(main):004:0> CouchReplicate.replicate('http://couch-011a.local:5984', 'http://couch-011b.local:5984', 'test')
=> 202 Accepted | text/plain 59 bytes
That's all well and good, but of limited use beyond the
curl
command from the other night. What I would like is to pass in a database name and a list of servers that should replicate that database. An RSpec example of how I want this to work:it "should replicate in a circle" doThe easiest way to make that example pass is to replicate the last host to the first host:
another_host = 'http://couch03.example.org:5984'
CouchReplicate.
should_receive(:replicate).
with(another_host, @src_host, @db)
CouchReplicate.link(@db, [@src_host, @target_host, another_host])
end
def self.link(db, hosts)The example is passing, but not exactly what I want. I also want the first host to replicate to the second, the second to replicate to the third, and so on. I think this example ought to suffice to describe what I want:
self.replicate(hosts.last, hosts.first, db)
end
it "should replicate in pairs" do(the
CouchReplicate.
should_receive(:replicate).
with(@target_host, @another_host, @db)
CouchReplicate.link(@db, [@src_host, @target_host, @another_host])
end
@target_host
and @another_host
are the second and third hosts in the list supplied to link
)To implement that, I use a
Enumerable
method that I never thought I would ever legitimately need, each_cons
. I have no idea what the mnemonic for cons
is (Lisp?), but each_cons(2)
will iterate through the first and second element of the array, the second and third element of the array, the third and fourth element of the array and so on. This is perfect for linking hosts in an array:def self.link(db, hosts)Amazingly, that make the example pass. I cannot believe I actually used
Array(hosts).each_cons(2) do |src, target|
self.replicate(src, target, db)
end
self.replicate(hosts.last, hosts.first, db)
end
each_cons
. I have seen that method dozens of times when reading the Enumerable
documentation and each time thought, "why on earth would I ever do something like that?" Now I know.In addition to linking forward, I would also like to link in the opposite direction:
it "should be able to reverse link" doA simple call to
CouchReplicate.
should_receive(:link).
with(@db, [@host03, @host02, @host01])
CouchReplicate.reverse_link(@db, [@host01, @host02, @host03])
end
reverse
will suffice:def self.reverse_link(db, hosts)Lastly, I would like to support replicating to every nth host. If there are ten hosts, and I replicate to every 3rd host, then host 1 should replicate to host 4, host 2 to host 5, ..., host 10 to host 3. In RSpec:
self.link(db, hosts.reverse)
end
it "should replicate nth host" doI can make this pass with:
CouchReplicate.
should_receive(:replicate).
with(@host02, @host05, @db)
CouchReplicate.nth(3, @db, [@host01, @host02, @host03, @host04, @host05])
end
def self.nth(n, db, hosts)That works fine until the end is reached, but
Array(hosts).each_cons(n+1) do |src, *n_hosts|
self.replicate(src, n_hosts.last, db)
end
end
@host05
is not told to replicate to @host03
. To make it do so, I create an RSpec example:it "should replicate nth host in a circle" doThat fails until I pad the end of the
CouchReplicate.
should_receive(:replicate).
with(@host05, @host03, @db)
CouchReplicate.nth(3, @db, [@host01, @host02, @host03, @host04, @host05])
end
each_cons
array with n
extra entries from the beginning of the hosts array:def self.nth(n, db, hosts)The functional programmer in me is shuddering and I cannot believe that I have now used
(Array(hosts) + Array(hosts)[0..n]).each_cons(n+1) do |src, *n_hosts|
self.replicate(src, n_hosts.last, db)
end
end
each_cons
twice, but hey, it works.After building a minimal command line interface, I add extremely minimal documentation and am ready to publish to github:
cstrom@whitefall:~/repos/couch-replicate$ rake github:release # Release Gem to GitHubAw nuts! Looking back, it seems that I missed the
(in /home/cstrom/repos/couch-replicate)
Pushing master to origin
rake aborted!
git push "origin" "master" 2>&1:ERROR: eee-c/couch-replicate doesn't exist yet. Did you enter it correctly?
fatal: The remote end hung up unexpectedly
(See full trace by running task with --trace)
--create-repo
when I first ran jeweler. Ah well. I create the repository on github, then I can release:cstrom@whitefall:~/repos/couch-replicate$ rake github:release # Release Gem to GitHubThen I can release to gemcutter:
(in /home/cstrom/repos/couch-replicate)
Pushing master to origin
cstrom@whitefall:~/repos/couch-replicate$ rake gemcutter:release # Release gem to GemcutterWith that, I can install my new gem:
(in /home/cstrom/repos/couch-replicate)
Generated: couch-replicate.gemspec
couch-replicate.gemspec is valid.
WARNING: no rubyforge_project specified
Successfully built RubyGem
Name: couch-replicate
Version: 0.0.1
File: couch-replicate-0.0.1.gem
Executing "gem push ./pkg/couch-replicate-0.0.1.gem":
gem push ./pkg/couch-replicate-0.0.1.gem
Pushing gem to Gemcutter...
Successfully registered gem: couch-replicate (0.0.1)
cstrom@whitefall:~/repos/couch-replicate$ gem install couch-replicateAnd run it:
WARNING: Installing to ~/.gem since /var/lib/gems/1.8 and
/var/lib/gems/1.8/bin aren't both writable.
Successfully installed couch-replicate-0.0.1
1 gem installed
cstrom@whitefall:~$ couch-replicate test http://couch-011a.local:5984 http://couch-011b.local:5984 --reverseNot bad. From conception to a released gem in a single day. I am not quite sure that I am 100% on everything that Jeweler took care of behind the scenes, but that is probably more a function of my pushing to release in a single day. This Jeweler thing seems pretty nice.
Reverse linking replication hosts...
Day #49
No comments:
Post a Comment