Chapter 5 Distribunaut

In early 2008, I was working for a company that was using Ruby on Rails as the framework behind the application we were building. For the most part we were happy with Rails, but there were things we wanted to do that Rails was just not a good fit for. First we realized that what had started as a Web 2.0 application was anything but that. Instead, we came to the conclusion that we were building a rather large portal application.

For all of its pros, Rails has a few cons as well. I won’t go into all of them now, but the biggest disadvantage we found was that Rails doesn’t want to help you write complex portal applications. It wants you to build smaller, simpler applications—at least, at the time it did. With Rails 3.0 on the horizon, that may change.

In addition to building this large portal, we decided we wanted to split our application into many applications. The advantages we saw were smaller code bases that were easier to maintain and separate applications that were easier to scale. We also could push updates and new features sooner, because we didn’t have a gigantic code base to worry about.

We identified three main problems. First, we wanted to let each application maintain its own set of routing, but we wanted the other applications to be able to use the dynamic routing we had become accustomed to in Rails. We didn’t want to hardcode URLs in the other applications; we wanted them generated by the application they would be linking to. Second, we wanted to share views and layouts among these applications. We didn’t want to have to deal with SVN externals, GIT submodules, or symlinks. We wanted to be able to quickly say, “Here is a URL for a layout. Render it like you would a local layout.” Finally, we wanted to share models and libraries throughout all these applications without having to worry about packaging them and redeploying all these applications each time we made a bug fix to a model.

With these goals in mind, I set out to find a Ruby web framework that would help us achieve these goals. After downloading and testing nearly 20 frameworks, I was at a loss for the solution we needed. Then I found Rack.1 Rack bills itself as a framework for frameworks. It is a middleware abstraction layer that lets framework developers get on with developing their framework without worrying about things like parsing requests and talking with application servers. Within a few hours I had a simple MVC-based framework up and running, and the Mack Framework was born.

I then spent the next year building and developing a large feature set for Mack, including all the libraries to handle distributed routes, views, and models. During that time I was asked time and again to make these components available outside the Mack framework for others to use. In April 2009, I announced an early version of a library I dubbed Distribunaut.

Distribunaut2 is a port of one-third of the distributed features that are found in Mack. In particular, it focuses on making it incredibly easy to distribute models and other Ruby classes. You will not find distributed views/layouts and routes in Distribunaut. The reason is that they are too specific to each of the web frameworks out there, and coding for each one would be a lot of work.

So with that brief history of Distribunaut, let’s look at what it can do for us.

Installation

Installing the Distribunaut library is simple. It can be installed using RubyGems:

$ gem install markbates-distribunaut -s http://gems.github.com

You should then see something similar to the following, telling you that you have successfully installed the gem:

Successfully installed markbates-distribunaut-0.2.1

Blastoff: Hello, World!

Distribunaut uses DRb and Rinda to do most of its heavy lifting. The good news is that because you have already learned all about DRb and Rinda, you can easily jump into experimenting with Distribunaut.

As you’ll remember from our look at Rinda, we need to start a RingServer before we can run any code. Distribunaut ships with a convenient binary to help make starting, stopping, and restarting a RingServer easy:

$ distribunaut_ring_server start

If you wanted to stop the RingServer, you would do so with the following command:

$ distribunaut_ring_server stop

You can probably guess how to restart the server. You should restart the RingServer between all these examples, just so things don’t go a bit funny on you:

$ distribunaut_ring_server restart

So, with a RingServer running nicely as a daemon in the background, let’s kick off things with a simple “Hello World” application. Let’s start with a server. Keep in mind that, as we talked about earlier in the book, when we are using DRb and Rinda, applications can act as both a server and a client. So when we use the term “server” here, we are merely using it to describe a bit of code that serves up some content. So what does our HelloWorld class look like with Distribunaut? Let’s see:

image

First we require rubygems and then the Distribunaut library itself. After that we hit the first of two lines that make Distribunaut special.

Each Distribunaut “application” needs a unique name. When we talk about applications within Distribunaut, we are actually talking about a Ruby VM/process that contains one or more Distribunaut classes. The name of that application should be unique to avoid confusion. We will look at what can happen with redundant application, and class, names a bit later in this chapter.

To manage its configurations, Distribunaut uses the Configatron3 library. We set the application as follows:

configatron.distribunaut.app_name = :hello_world_app

This needs to happen only once per Ruby VM. If you set it multiple times, strange things can happen, so be careful. In our sample code we are setting the application name to :hello_world_app. We could just as easily set it to something like :server1 if we wanted to make it more generic for other Distribunaut classes we were planning on running in the same Ruby VM.

After we have set up our application name, the only other thing we have to do is include the Distribunaut::Distributable module in our HelloWorld class. Then we are ready to try to get a “Hello, World!” remotely.

Before we get to our client code, let’s take a quick look at what the preceding HelloWorld class would’ve looked like had we used raw DRb and Rinda:

image

Although the HelloWorld class part of it is relatively the same, much more noise is required at the end to get our HelloWorld instance into the RingServer. At this point it is also worth pointing out that in the Rinda version of HelloWorld we had to create a new instance of the class. This means that we can’t call any class methods that HelloWorld may have. This includes the ability to call the new method and get a new instance of the HelloWorld class. We are stuck with that instance only. We did not do anything of the sort with the Distribunaut version of the class. In fact, you probably have noticed that we didn’t make any calls to get it into the RingServer. We’ll talk about why that is shortly. First, let’s look at our client code:

image

If we were to run this code, we should see the following output:

Hello, World!

What just happened there? Where did the Distribunaut::Distributed::HelloWorld class come from? How did it know to print “Hello, World!” when we called the say_hi method? All great questions.

The Distribunaut::Distributed module is “special.” When you preface a constant such as HelloWorld in that module, it queries the RingServer and attempts to find a service that matches that constant. So, in our case it searched the RingServer for a service called HelloWorld. It found the HelloWorld class we created earlier and returned a reference to it. With that reference we could call the new method on that class, which returned a new instance of the HelloWorld class. And then we could call the say_hi method.

So if we didn’t explicitly place our HelloWorld class in the RingServer, how did we access it? And how were we able to call a class method on it, when we know that you have to put instances only into a RingServer? The same answer applies to both questions. When we included the Distribunaut::Distributable module into the HelloWorld class, it created a Singleton wrapper class on-the-fly that then proxies all methods called on that proxy class onto the original HelloWorld class. With that we can put the Singleton instance into the RingServer. Then we can call class methods, which allows us to do things like call the new and get back a new instance of the class.

Having all of this happen automatically also helps clean up the usual supporting code you need to write to both set an instance into the RingServer and retrieve that instance later. Just look at what a plain-vanilla DRb and Rinda implementation of the client would look like:

image

This is not only more code, but also uglier code.

Building a Distributed Logger with Distribunaut

So now that you have a good understanding of how Distribunaut works, and what it does under the covers, let’s try to create a distributed logger and see how it goes. To create our distributed logger, we want to create a RemoteLogger class. Here’s what that would look like:

image

Although this looks a lot more intimidating than our HelloWorld class, it really isn’t. The extra code comes from making it a bit easier to access the underlying Ruby Logger class we want to wrap. We could have just harnessed the incredible power of Ruby and opened up the Logger class and included the Distribunaut::Distributable module directly into it, but that is generally not considered good practice. Besides, this way lets us talk about a few things we couldn’t talk about otherwise. Let’s look at it in a bit more depth; you’ll see it isn’t that complex.

After we require the correct classes and define our application name (this time we are calling it :remote_logger), we create a constant called LOGGER to act as a holder for our Logger instance. We want only one instance of the Logger class. That is why we assign it to the global constant—so that we can access it throughout the rest of our code.

After we have included the Distribunaut::Distributable module into our RemoteLogger class, we then add a few methods for convenience. The first of these methods is a class-level override of the new method. We do this so that when our clients try to create a new instance of the RemoteLogger class, they are actually getting the wrapped Logger class instead. Next we generate the five standard logging methods on Logger, putting them at the class level of the RemoteLogger class. These methods simply proxy the methods onto the single instance of our Logger class that we have stored in our LOGGER constant. We do this so that our clients can call these methods at the class level of RemoteLogger without having to create a new instance of it. This is easier to demonstrate in the client code.

With all of that out of the way, let’s see what our client code would look like:

image

In this client we first create a new “instance” of the RemoteLogger class. I put “instance” in quotes for a reason. Remember that we don’t actually get a new instance of the RemoteLogger class. Instead, we simply get back a reference to the global instance of the Logger class we set up earlier.

As soon as we have the RemoteLogger, we can call the standard logging methods, such as debug. We should see our message printed to the server’s screen, not the client’s. After we call the debug method, we call the class method error on the RemoteLogger class and pass it the message “oops!”.

If we were to run all of this, we would get the following:

Hello, World!
oops!

As you can see, creating a new distributed logger with Distribunaut is actually quite easy. We could have simplified the code by not giving class-level convenience methods for the common logging methods. But it was only a few more lines of code, and it could make the end user’s life a little easier.

Avoiding Confusion of Services

Earlier, when speaking about application names, I mentioned that names need to be unique to avoid confusion, but I didn’t explain what I meant.

You know from Chapter 2, “Rinda,” that when we create a Tuple to put into the RingServer, we give it some unique characteristics that allow us to retrieve it easily. The combination of these characteristics becomes sort of like an ID for that particular Tuple. So imagine if we were to put two Tuples into the RingServer that had the same characteristics. How would we retrieve the specific one we want? If we use the same application name, we not only run the risk of overwriting another Tuple, but we also make it difficult to find later.

As you have seen, Distribunaut performs a lot of magic that keeps us from having to write as much code. It also makes the code we write cleaner and easier to use and maintain. One thing Distribunaut does for you is build the search characteristics for you when you make a call to the special Distribunaut::Distributed module. When Distribunaut goes to build the search parameters for that request, it takes into account only the class, or service, name you provide. Because of this, if you have two applications serving up a class with the same name, you are unsure which one you will receive from the query. In some cases this might be fine, but in other cases, it might be a problem.

Let’s look at a simple example. Let’s create a service that serves up a User class. We want to launch at least two instances of this service for this example. To do that we need to run the following code twice to start two instances of it:

image

A large majority of the preceding code simply finds out what the last service, if there is one, was called. Then it names the service that is currently launching so that it has a unique name. Although most of this is straightforward Ruby code, it is worth pointing out the call to the available_services method on the Distribunaut::Utils::Rinda module. The available_services method, as its name implies, returns an Array of the services that are currently registered with Distribunaut. The elements of this Array are Distribunaut::Tuple classes, which are simply a convenience class to make it easier to deal with Tuples that are in the Distribunaut format.

After we have decided on an appropriate application name and registered it, we create a User class, include the Distribunaut::Distributable module and give it an instance method that returns the application name that is running this service.

Now, with a couple of instances of our service running, let’s look at the style of client we have been using so far in this chapter:

image

So which instance of the user service do we get when we run this? Well, on my system I see the following printed:

user_server_1

On your system you might see this:

user_server_2

or another variation, depending on how many instances you have running. There is no guarantee which instance you will receive when accessing services this way. Again, this might be acceptable in your environment, or it might not.

So what do you do when this is unacceptable, or you want to get a specific instance of a service? Distribunaut provides you with a method called lookup. This method is found on the Distribunaut::Distributed module. The lookup method takes a URL to find the specific instance you are looking for.

Right about now you should be wondering how you are supposed to know the URL of the service you want to look up. Don’t worry. Distribunaut has you covered by making it easy to look up these services. Let’s look at a client that wants to find specific instances of the user services we have running:

image

Building the URL for the service we want is quite simple. The format is distributed://<application_name>/<service_name>. Because of this format, it is important that we have unique application names for each Ruby VM so that we can easily seek out the one we are looking for.

With the URLs in hand for the two services we are looking for, we can call the lookup method and find these two services. When we have them, we can create new instances of the User class and print the return value of the app_server_name method. You should see something similar to the following printed:

user_server_1
user_server_2

With the lookup method now in our arsenal, we can code with confidence, knowing that we will always get the specific instance of a service we are looking for. And we can do it without having to deal with IP addresses, ports, and other such nonsense.

Borrowing a Service with Distribunaut

As you probably remember from Chapter 2, when we retrieve Tuples from the RingServer, we have two choices. We can either read the Tuple or take the Tuple. The former leaves the Tuple in the RingServer for others to access simultaneously. The latter removes the Tuple from the RingServer; as a consequence, no one else can gain access to that Tuple.

So what happens when we access a service using Distribunaut? Are we doing a read or a take from the RingServer? Distribunaut does a read from the RingServer, thereby allowing others to access the same service at the same time.

Most of the time this is the exact behavior you want. You usually want to be a good citizen and let others access the service you are accessing as well. Sometimes, however, you might need to grab hold of a service exclusively, do a few things with that service, and then return it to the RingServer for others to have access to.

So how do we take a service from the RingServer, use that service, and then return it for wider use? We could use raw Rinda and DRb code, but that would be ugly, and prone to error should any of the underpinnings of Distribunaut change. Instead, Distribunaut offers the concept of borrowing a service.

To demonstrate how to borrow a service, let’s use our simple HelloWorld class as the service we want to borrow:

image

Here is what our client code would look like to borrow the HelloWorld service:

image

If we were to run this code, we should see the following printed:

image

So exactly what did we do, and how did it work? The borrow method takes a block and yields a reference to our proxy service, as we discussed earlier in this chapter. This works the same way as if we had called Distribunaut::Distributed::HelloWorld directly. The difference is, before the block gets executed, the service is located and removed from the RingServer. It is then “locked” and placed back into the RingServer in such a way that others can’t retrieve it. After the block finishes executing, the service is unlocked in the RingServer and is available again for public consumption.

If we look at what is happening in the block, we see that we call the new method on the hw_class variable, which is the reference to the HelloWorld service. The new method creates a new instance of the HelloWorld class, and we can call the say_hi method on it.

To demonstrate that we can’t access the HelloWorld service directly, we attempt to call it, but, as we see, it raises a Rinda::RequestExpiredError because the service cannot be found.

After the block has finished executing, we again try to access the HelloWorld service as we would normally. This time, instead of an exception, we are greeted with a pleasant “Hello, World!”.

As you can see, the concept of borrowing a service allows us to easily take control of the service we want, do the work we need to do on that service, and then have it automatically returned to the RingServer for others to use. It also has the added benefit of being quite easy to code. We don’t have to write fragile code that takes the service from the RingServer, handles exceptions that may arise, and ensures that the service gets placed back into the RingServer correctly.

Conclusion

Obviously I’m slightly biased in my feelings about Distribunaut, seeing as how I am the developer of the library. With that said, I feel strongly that Distribunaut makes distributed objects incredibly easy to code, use, and maintain.

The library continues to grow and develop. Its fundamentals were pulled from the mack-distributed gem for the Mack Framework, but the library has grown and evolved much since its origins. Even during the course of writing this chapter, I found several bugs, enhancements, and performance improvements that could be made, so I made changes. The underpinnings of this library have been working hard in several production environments and have proven themselves to be reliable, fast, and easy to use.

Overall I feel that the simple interface, basically just including a module, makes an already easy system for building distributed applications, DRb and Rinda, even easier. Instead of having to write code to look up services, read them, parse them, manage them, and so on, you can use something you are already familiar with—simple Ruby objects.

What does the future hold for Distribunaut? As far as the feature set is concerned, that is hard to say. I try to develop features that will actually be used, not features that I think are cool. What I can tell you for sure is that Distribunaut will continue to be maintained and grown to keep up with the challenges of developing distributed applications.

Endnotes

1. http://rack.rubyforge.org/

2. http://github.com/markbates/distribunaut/tree/master

3. http://github.com/markbates/configatron/tree/master

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.15.237.164