Skip to content

A framework for writing lightweight, scalable servers with Guice and Netty

Notifications You must be signed in to change notification settings

timboudreau/acteur

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Repository files navigation

Acteur

Acteur is a framework for writing web server applications with Nettyby composing together reusable chunks of logic called Acteurs (think of the Actor pattern, but a little bit foreign:-)).

A further description of the framework's aims can be found in this blog.This project uses Netty's4.x (with its revised API) for HTTP.

Read theacteur tutorialfor a step-by-step description of building a web API.

Or read theFAQ.

It's goal is to make it easy to write small, fast, scalable HTTP servers while ending up with reusable code as a natural side effect of using the framework.

If you think this project is worthwhile, or there are features you'd like to see, considerdonating.

It uses a few best-of-breed libraries in addition to Netty to work its magic (such as Joda Time for dates and times, Jackson for JSON processing and Guava or MIME types and a few other things).
Most importantly, it uses Google's Guice dependency injection framework to decouple those reusable chunks of logic.

An Acteur application is a subclass ofApplication.In a pattern we will see again, most of the code and work of anApplicationhappens in its constructor, and most of it consists of addingPagesubtypes (Classobjects) to the application.

Essentially we are treating constructors as function objects. Each Acteur construtor outputs aStatewhich can contain additional objects that the next Acteur in the chain can request by mentioning them in their constructor arguments.

An Application is a list of Pages, and each Page is a list of Acteurs. Typically these are all specified by passing Class objects, and instantiated on-demand, once per request.

Similarly, most of the work of aPagealso happens in its constructor, in the form of adding Acteur subclasses (instantiated, or just the types) to the Page. At runtime, thePageinstance can be used to set the headers of the response. Actual Page and Acteur instances are constructed on a per-request basis.

There are a few things you can override if you want to, but the constructor is where the action is.

This breaking of logic into small classes instantiated by Guice accomplishes several things:

  • The natural pattern is logic reuse, not copy/paste programming
  • By composing the act of responding to requests into small wads of reusable logic, the system can take care of threading issues and interleave responses to many requests, making it easy to write asynchronous, highly scalable code from chunks of single-threaded, imperative code - so fewer threads can handle more requests
  • Individual pieces of logic form natural units for tests
  • Encouraging the use of constructors naturally leads to using final fields. Many bugs in all types of applications come from having mutable state, or having state diffused throughout a lot of objects. So, unlike a lot of JavaBean-based frameworks, the natural way to do things in Acteur also happens to be a way that maximizes the help you can get from the compiler to ensure your code is correct.

This ends up adding up to something resembling recursive callbacks, without the mess. Whereas in Javascript one might write

blogs.find(id,function(err,blog) {
blog.withContent(function(err,content) {
response.write(content);
});
});

with the caveat that in fact, not of the nested functions run sequentially, in Acteur, one would write one Acteur to locate the blog and inject it into the next, and so forth - small, readable, reusable chunks of logic run just as asynchronously. Incidentally, since the objects which need to be in-scope are (literally) contained in the scope, the required memory footprint can be smaller (only variables that will actually be used are included, rather than every variable visible from the current call).

Status

Acteurisin use in a few production web applications in-house - which is to say itissomebody'sjobto work on and improve it. Recently Netty has undergone major incompatible refactoring. This project will keep up with such things, but occasionally changes in Netty may make it uncompilable until Acteur can be updated. Hopefully the Netty folks will finish their refactoring soon.

Inspiration

This framework emerged from a bunch of heterogenous things:

  • A desire to use Netty, and finding it was very low-level
  • Experience with asynchronous programming usingNode.jsand wanting to get some of the goodness of its programming model for Java
  • The knowledge that Servlet-based frameworks are not going to play nicely with async servers any time soon because to be useful, the whole stack (file and database I/O, you name it) needs to be non-blocking or a single-threaded server is worse than useless
  • A suspicion that (unlikely as it sounds) NetBeans Visual Library's model of action response chains would actually be a fertile model for the switching required for responding to events from an evented server
  • Apache Wicket's lack of fear of constructors and doing work in constructors, as compared to the factory fetish of most Java frameworks
  • The observation that if you want Guice to run some code for you, the best bet is to put it in a constructor (setter injection is evil anyway)
  • Lots of experience with Guice, and the need for a way to create objects that can supply objects to inject into other objects

What Acteur Is

Acteur takes a page from Apache Wicket in terms of being built around Page and Application classes which you actually subclass (as opposed to typical Java web applications with scads of XML and factories for factories). It is explicitly not trying to be a declarative framework - someonecould trivially create single Page or Application classes that read declarative data and populate themselves from that. Rather Acteur is the bones you would build such a thing on top of.

Netty supports all sorts of protocols, not just HTTP. Acteur is specifically solving the problem of HTTP, at least for now (SPDY is a glimmer in its author's eye).

Acteur is not in the business of dictating to you how you model data - in fact, it is focused more on the steps that happenbeforeyou get around to producing a response body (Netty'sChannelFutureListeners work quite well for that) - i.e. doing handling cache-related headers simply, well and up-front rather than as an afterthought. And making sure that any logic this will be shared by multiple HTTP calls can be implemented cleanly as separate pieces.

So, Acteur does not hide Netty's API for actually writing data. Very simple HTTP responses can be composed by passing a string to the constructor of the RespondWithstate; for doing more complicated output processing, you probably want to implement Netty'sChannelFutureListenerand write directly to the output channel.

There are a lot of well-done solutions for generating HTML or JSON, doing templating and that sort of thing.

A Basic Application

As mentioned above, an application is composed from Pages, and a Page is composed from Acteurs. Here is what that looks like:

publicclassAppextendsApplication{

App() {
add(HelloPage.class);
}

staticclassHelloPageextendsPage{

@Inject
HelloPage(ActeurFactoryfactory) {
add(factory.matchMethods(Method.GET));
add(factory.matchPath("^hello$"));// A regular expression
add(SayHelloActeur.class);
}
}

staticclassSayHelloActeurextendsActeur{

@Inject
SayHelloActeur(Pagepage,Eventevt) {
page.getReponseHeaders().setContentType("text/html; charset=utf8");
setState(newRespondWith(HttpResponseStatus.OK,
"<html><head><title>Hello</title></head><body><h1>Hello World</h1>"
+"Hello from Acteur</body></html>"));
}
}

publicstaticvoidmain(String[]args)throwsIOException,InterruptedException{
ServerModule<App>module=newServerModule<>(App.class);
module.start(8192);
}
}

Now, say we would like to have this application look for a url parametername and generate some customized output. That lets us show off what you can do with injection.

We'll add one line to the the application (outermost) class itself, to tell Guice thatjava.lang.Stringis a type which may be injected from one Acteur into another (in practice, String is an odd choice, but it works for a demo):

@ImplicitBindings(String.class)

The framework lets Acteurs dynamically create objects for injection into subsequent Acteurs. Guice demands that all type-bindings be configured at application start-time. So in order to have a raw String be allowed by Guice, we need to tell Guice that String is one of the classes it should bind.

Then we modify our Acteurs and page slightly.

staticclassHelloPageextendsPage{
@Inject
HelloPage(ActeurFactoryfactory) {
add(factory.matchMethods(Method.GET));
add(factory.matchPath("^hello$"));
add(factory.requireParameters("name"));
add(FindNameActeur.class);
add(SayHelloActeur.class);
}
}

staticclassFindNameActeurextendsActeur{
@Inject
FindNameActeur(Eventevt) {
// name will always be non-null - requireParameters() will have
// aborted the request with a 400 BAD REQUEST response before we
// get here if it is missing
Stringname=evt.getParameter("name");
// name will be injected into SayHelloActeur's constructor
setState(newConsumedLockedState(name));
}
}

staticclassSayHelloActeurextendsActeur{
@Inject
SayHelloActeur(Pagepage,Stringname) {
page.getReponseHeaders().setContentType("text/html; charset=utf8");
setState(newRespondWith(HttpResponseStatus.OK,
"<html><head><title>Hello</title></head><body><h1>Hello"+name+"</h1>"
+"Hello from Acteur to"+name+"</body></html>"));
}
}

So if you run this application and run, say

curl -i http://localhost:8192/hello?name=Tim

you will get a nice personalized hello page. Admittedly this example is a bit contrived; for more real-world uses, see the twoActeurFactorymethods:

  • injectRequestBodyAsJSON(Class<T> type)- this uses Jackson to parse the request body into the type requested (if an error occurs, you can handle it by overridingApplication.onError)
  • injectRequestParametersAs- allows you to provide an Java interface which should be used to represent the request parameters;literallythe method names should match parameter names; supported types are Java primitive types, DateTime and Duration. So you can simply add the Acteur produced by callingacteurFactory.injectRequestParametersAs(MyType.class)and then write an Acteur that requests aMyTypein its constructor.

More Complex Output

The above example simply uses a JavaStringto send all of its output at once. If you want to do something more complex, you will simply use Netty's clean and simple API for writing output to a channel. Instead of passing the string to theRespondWithconstructor, leave it out. Say you want to pipeline a bunch of output which may take some time to compute:

staticclassMyOutputterimplementsChannelFutureListener{
@Override
publicvoidoperationComplete(ChannelFuturefuture)throwsException{
future=future.channel().write(Unpooled.wrappedBuffer("The output".getBytes()));
// add this as a listener to write more output, or add
// ChannelFutureListener.CLOSE
}
}

Configuration

Acteur uses theGiuliuslibrary to facilitate binding values in properties files to@Namedvalues which Guice can inject. While not going into exhaustive detail here, what this does is it makes it easy to configure an application using a hierarchy of properties files. The default file name isdefaults.properties (but you can use something different by specifying it at startup time). In a nutshell, how it works is this:

  • At startup time, the system will look for and merge together properties files from the following locations, in this order (so values in subsequent files override earlier ones):
    • All files in JARs (classpath order) named META-INF/settings/generated-defaults.properties- these are generated using the@Defaultsannotation
    • All files in JARs (classpath order) namedMETA-INF/settings/defaults.properties
    • /etc/defaults.properties
    • ~/defaults.properties(a file in the process` user's home dir)
    • ./defaults.properties(a file in the process working dir)

To set base values for things, the easy (and reliable) way is to use the @Defaultsannotation - this guarantees that configuration files are optional and there are always sane default values.@Defaultssimply takes an array of strings, and uses properties-file syntax, e.g.

@Defaults({"port=8192","dbserver=localhost"})

This could also be written

@Defaults("port=8192\ndbserver=localhost ")

but the former is more readable.

Technical Details

A lot of the heavy lifting in creating Acteurs which are injected by objects from other Acteurs is handled by the utility classReentrantScope.Each Acteur is instantiated within this scope (available from a getter on the Application). The scope is entered multiple times, each time contributing any objects provided by the previous Acteur in the chain. It is also possible to inject an ExecutorServicewhich will wrap any Runnables or Callables posted to it in the current scope contents, so it is possible to run code on a background thread with the same scope contents as when it was posted (and in fact, this is how Acteurs are run).

An Acteurmustset its state, either by callingsetState()within its constructor, or overridinggetState().Three State subclasses are provided as inner classes of the superclass (so they can only be instantiated inside an Acteur subclass):

  • RejectedState- the Acteur says it doesn't know what to do about the event
  • ConsumedState- the Acteur does recognize the request, and perhaps is providing some objects for the next Acteur to use
  • ConsumedLockedState- the Acteur recognizes the request to the degree that it accepts responsibility for responding - no other Pages will be tried if subsequent Acteurs reject the request
  • RespondWith- processing of the event is complete, and the response should be sent

About

A framework for writing lightweight, scalable servers with Guice and Netty

Topics

Resources

Stars

Watchers

Forks

Packages

No packages published

Languages