Acteur
This is a lightweight web framework that sits on top of
Netty, the asynchronous Java NIO
server framework.
Its goal is to solve a specific problem that must be addressed to
make asynchronous frameworks usable for non-trivial tasks: That
most back-end APIs in Java are synchronous. It's wonderful
that you can write a non-blocking, asynchronous web server that
can handle thousands of connections at a time with a single thread.
But as soon as that server needs to talk to something else - say
a database, or even just a file, all of that scalability goes
away and you have a single-threaded server which is offline until
its done having a conversation with something else.
In other words, asynchronous is only useful if the entire
stack is asynchronous. One of the wonderful things about
Java is the amount of libraries available, and most of them
expect to do blocking I/O. So you need, at the least, some sort
of threading model that lets you pretend your blocking back-end
calls are actually non-blocking.
The standard way to do asynchronous programming is callbacks.
That is how you do it in popular frameworks
such as Node.js. But that creates its own problem: Such programs
usually have deeply nested callback structures which are hard to read
and harder to reuse;
and in a language like javascript, it is easy to have bugs by doing
things like referencing a loop variable from a callback that runs long
after the loop completed. The Java equivalent would have you writing
scads of deeply nested inner classes - which you can do, but that
tends not to result in readable or maintainable code.
So we have a few problems to solve:
- People like to think about programming as linear sequences
of events, not callbacks
- The closest Java comes to function objects are anonymous
inner classes, and they are verbose
- Deeply nested inline callbacks, even if they do
something useful, are painful to extract into actually
reusable logic
These can be solved fairly cleanly with a simple observation:
All of those callbacks consist of synchronous code - you're just carving
up the handling of a request into small chunks of synchronous code
which don't necessarily get run in linear fashion.
So, if you can provide a list of the chunks of logic to run,
that feels sequential - as in, you can reason about it the way
you would about sequential code - whether it actually runs that way or not.
So, we actually don't want to standardize on anonymous inner classes as
the programming model; and we want to provide a way to stick together
a list of chunks of logic to run.
Those chunks of logic are called:
Acteurs
The name comes from Jaroslav Tulach's
observation that this library is more-or-less the
Actor pattern, but looks a little strange. So, an Acteur is
an Actor...but slightly foreign :-)
An Acteur does all of its work either in its constructor, culminating in a
call to setState()
, or in its override of getState()
.
You bundle together a list of Acteurs in a Page - a Page is really a list
of Acteurs to run to validate a request and set up a response, and an aggregation
point for things like response headers and whatever will write response data.
Acteurs are instantiated by Google Guice - in fact, you assemble a page largely
by giving it a list of Acteur subclasses; they will be instantiated as
needed.
In particular, each Acteur only plays one role. Say that the logic involved
in loading a page involves:
- Check that the URL requested is legal
- Check the request cache header IF_MODIFIED_SINCE, and if present and its condition holds, respond with NOT_MODIFIED
- Authenticate the user
- Find the object expressed by the URL
- Write it into the response
Each of these is handled by a separate Acteur.
Now, a trick is needed so each one of these can use the results of the earlier
one's work. This is handled transparently by the framework, using Guice scopes.
The output of an Acteur's work is a State. A State can include an array
of context objects. Under the hood, before the next Acteur is created,
we re-enter the Scope, adding into it all of the context objects included
in the previous Acteur's exit state.
Take, for example, authentication. This is what AuthenticateBasicActeur
actually does:
public class AuthenticateBasicActeur extends Acteur {
@Inject
AuthenticateBasicActeur(Event event, Authenticator authenticator, @Named("realm") String realm) throws IOException {
Realm r = Realm.createSimple(realm);
BasicCredentials credentials = event.getHeader(Headers.AUTHORIZATION);
if (credentials == null) {
unauthorized(r);
} else {
LoginInfo info = authenticator.authenticate(realm, credentials);
if (info == null) {
unauthorized(r);
} else {
setState(new ConsumedLockedState(info, r, info.getRole(), info.getUser()));
}
}
}
private void unauthorized(Realm realm) {
add(Headers.WWW_AUTHENTICATE, realm);
setState(new RespondWith(HttpResponseStatus.UNAUTHORIZED));
}
}
The next Acteur in the chain can then ask for the User
object
found in the above one's state in its constructor:
public class FileFinder extends Acteur {
@Inject
FileFinder (Event event, User user) throws IOException {
File f = new File (user.getFolder(), event.getPath().toString());
if (!f.exists) {
setState (new RespondWith(HttpResponseStatus.NOT_FOUND));
} else {
setState (new ConsumedLockedState(f));
}
}
}
and the next one can start sending chunks of the file back to respond
to the request - by simply having a constructor that takes File
as one of its arguments.