A New Project, Part 12


sticky, part 12



Finally some code. I'd like to introduce you to Essex, a lightweight component manager for Perl. While it is only partly done, I think there is still enough here to be worth discussing. Essex is not intended to be an application framework or content management system or anything like that. It is just a simple set of classes designed to help you manage other classes. It is a programmer's toolkit -- people only curious about the new project as a whole may want to skip this piece, as it will be focused on the technical details that the end user will never see. Then again, posting will be light during the coding phase, so this is a good way to follow along even if you aren't downloading nightly builds. And really, who is?

Essex was influenced by the design of Avalon, an Apache Jakarta project from several years back. I first saw Avalon a few years ago and felt that it was an example of intelligent component architecture design. In fact, the Avalon patterns, and some of their code, went on to serve as a foundation for a number of major projects at other companies in following years. Unfortunately, the Avalon team seems to have been mired in internal politics, and while the project has been resurrected in bits and pieces (most notably Excalibur), I worry that as a whole it may have lost it's stride.

Part of the problem they faced is that there are just too many ways to build a component architecture. It means different things to different people, and no two application have the exact same requirements. To that end, I decided to make Essex a fraction of what Avalon was. All I wanted was to make a lightweight component manager that could help control the lifecycle of small services. Component based architectures really do have advantages -- but their biggest disadvantage is that they can get pretty heavy and be a pain to work with. While you can write better code using one, it often takes longer to do the simple things.

So here's what Essex has so far:



See, there isn't much there. But that's okay -- it probably doesn't need much more.

So how would you use Essex? Well, you first need to get yourself a service manager. That's as simple as:

use Unto::Essex::ServiceManager;

my $manager = new Unto::Essex::ServiceManager( 'conf/services.yml', 'conf/configuration.yml' );


And once you have a service manager, you can request a component. You request the component by a symbolic name (called a "role"), and the service manager decides how best to retrieve that service for you. The manager takes care of instantiating, initializing, configuring, etc., the service -- all you have to do is remember to return it when you're done with it. For example:

my $logger = $manager->lookup( 'logger' );
$logger->log_info( 'Got a logger.' );
$manager->release( $logger );


Pretty simple. So how does the manager know what class corresponds to which role? In the example above, the 'conf/services.yml' parameter to the manager's constructor points to a file that contains the mappings. For example, this file might contain:

services:
  - role: logger
    class: Unto::Essex::StderrLogService
    singleton: true
  - role: hello
    class: Unto::Essex::HelloService
    pool-max: 10


Note that I picked YAML over all the other formats only after hours of research and deliberation. I had originally picked XML, but I was distressed by the amount of work it took to both parse and author the configuration files. Worse, the configuration files didn't even look good -- and they were going to be edited by hand by developers. Other options, such as Apache-style and INI-style conf files all had their plusses and minuses (I even looked at JSON for configuration), but in the end YAML won hands down for it's impressive breadth of functionality, it's wide-ranging support, it's cleanliness of syntax, and for having a world class specification. I liked YAML so much that it is definitely going to be an option for the REST data serialization -- it may even be the default format.

So what happens to a service once it is being handled by the manager? Well, every service contains stages called "lifecycle methods", and those lifecycle methods are invoked at certain points in time by the service manager. In the case of Essex the lifecycle methods are more limited than Avalon, but they may prove to be enough. The Essex service lifecycle consists of:



If a service is looked up via the service manager and released via that same manager, then each of those methods will be called in that order. However, the methods will only be invoked if they exist on the object -- this is a simplification of the interface-based model that a C++ or Java class would use.

Since it is reasonable to simply let all services implement every one of the lifecycle methods, an abstract base class has been provided to make the job even easier. You don't have to, but if you want you can extend this abstract base class in your own services with:

package MyService;
 
use base qw( Unto::Essex::AbstractService );


That's all there is to it. Once a mapping has been made in the services.yml file, MyService is a fully functional part of the component architecture. Of course, it is more interesting if it implements some logic or at least overrides the lifecycle methods. (But keep in mind, if you override initialize or configure, for example, then you need to have them call the version in the superclass.)

A few other things to know. If the services.yml file contains a parameter pool-max: n for any given service mapping then that role will return at most n instances at any one time. Currently this is non-blocking, but I may make it block with a timeout in the future. This is good for database connection pools and similar limited resources, but most services can just ignore it. Likewise, a service marked singleton: true will hand out the same physical instance to any caller. Just be sure to call $service_manager->dispose( ) before the application dies so that any open resource can be closed and all pools drained.

You can also override the default factory that instantiates each service (i.e., you can write the code that calls new for each instance). This is done by specifying the factory parameter in the services mapping and it can point to any class that provides a new_instance method. This was missing in the original Avalon project, but comes in useful when managing certain patterns such as singletons.

Lastly, I added a little bit of funtionality in the property.pm module. I wouldn't normally put a module at the root namespace, but this one acts so similarly to the "constant" pragma that it felt right to do so. Using the property module is as simple as:

use property first_name => 'SCALAR';
use property friends => 'ARRAY';
use property _secrets => 'HASH';


This module will automatically modify the module's code to include methods like "get_first_name" and "set_first_name" and store the data in a instance variable. Even better, these setters will be typechecked so that inappropriate values are caught at runtime. (I think that Perl 6 may include something like this. I hope so -- it's a killer feature that saves a lot of time. Who knows, maybe someone will add this to Perl 5.8.7.)

If you want to look at any of the code you can browse the SVN tree at: http://svn.unto.net/svn/public/essex/trunk/ (the modules live under lib). The latest release was tagged 0.1 and can be found at http://svn.unto.net/svn/public/essex/tags/0.1/. Or you can download a nightly tgz file at http://www.unto.net/nightlies/essex-latest.tgz. Or even better, use a subversion client to download the trunk with:

$ svn co http://svn.unto.net/svn/public/essex/trunk essex


(The svn roots may move -- I'll post here and keep the project homepage updated if they do.)

Installing Essex should be as simple as:

$ sudo cpan Error YAML Module::Build
$ perl Build.PL
$ ./Build
$ ./Build test
$ ./Build install


This was just a solid day's worth of work (and a few years of practice), but I think that it is a pretty good start. I still need to add a generic dispatcher service (though I'll probably customize the dispatcher more inside Orchard) and a simple command-line and mod_perl base application class. I'll also add a few more simple services, such as a DBI/DBD service, a cache service, and maybe a template service, but for the most part, I think Essex is already doing most of what I need it to do. I could easily make it thread-safe and add support for threadable services, but since I won't be using it in Orchard I will probably wait a while on that.

I don't expect this to get a lot of traffic, but it was a necessary step in building the new project, and in the spirit of transparent development the work on Essex probably deserved a mention. As always, feedback and comments are welcome. Please feel free to download the tree and send patches -- the only philosophy I have here is keeping things simple. If your patch is interesting, but it is not simple, I may just suggest that you start your own fork of Essex. That's not a bad thing -- projects do better when they have a direction. But no matter what, I'd love to know what you think.