August 2008
Introduction
In the first
article we began exploring how application software could made to be as flexible as
the World Wide Web by following a REST or RESTful design approach [1]. To make
it clear that we are not talking about REST using the HTTP protocol
and its methods (GET, POST, etc.) we will adopt the term "Resource Oriented
Computing" or ROC to refer to our approach. This name comes from a fundamental
point about the approach: resources are identified by URI addresses and are
requested by clients. You might think of ROC as a refinement or evolution of
RESTful design thinking, applied to application software.
To recap the first article, we described a software architecture in which
clients, service endpoints and an Intermediary work together to provide a logical
computing environment. We grounded our discussion at the physical level by showing prototypical code
for a client and endpoint. In the following Java code, a request is created
and issued to the Intermediary. The request comprises a logical URI address,
a request verb and (optionally) a physical class specifying the form of the
returned resource representation. The Intermediary is accessed through the
variable "context":
Request req;
Representation rep;
req = context.createRequest("resource:/customers/");
req.setVerb(Request.SOURCE);
req.setType(DOM.class);
rep = context.issueRequest(req);
It is the job of the Intermediary to then:
-
Resolve the logical URI address to a physical endpoint,
- Send the request to the endpoint,
- Accept the representation returned by the endpoint
and send it back to the requesting client,
- Drop any connection it established between the client and the endpoint.
The responsibility of the endpoint is to return a copy of the requested information
in the form of an immutable representation of the information (called a "resource
representation" or, more simply, a "representation").
In the first article, we argued that this architecture provides an environment
for application software construction that mirrors the Web. By exclusively
using logical coupling between client code and service endpoints, application
software becomes more flexible. Just as web sites can be added, changed and
dropped without disrupting the Web as a whole, analogous changes can be made
to logically bound application software - even in a running system.
In this article we look at services that can be provided by the Intermediary.
As we do, it will become clear that referring to the Intermediary as a "microkernel" may
be more appropriate.
Microkernel Services
Logical Address Resolution
The most important service the microkernel can provide is logical address
resolution. When client software needs information (or an information processing
service), it issues a request to the microkernel containing a logical URI address.
The microkernel searches for a mapping from the requested logical URI address
to physical code (an endpoint) within a logical address space (also known as a context).
Note: programming at the logical level using URI addresses, logical address
spaces, and SOA-like microservices is the topic of a future article in this
series.
In the Web there is only a single, global logical address space. In the ROC
system we are designing there is no reason to have such a limitation. Instead,
our architecture can support multiple address spaces, each with its own static
resources and endpoints. Each logical address space is defined, implemented,
and managed by an entity called a module. A module can:
-
Map logical addresses to physical endpoints: A module contains physical code (endpoints) and physical resource representations
(such as static HTML pages or configuration control files). Declarations
within a module define the mapping from the logical address space of the
module to these physical entities.
-
Export a portion of the logical address space: To facilitate information hiding, each module locates its resources within
a private address space. To facilitate information sharing, each module
contains an export declaration which defines the portion of the private
address space that other modules can import.
-
Import other logical address spaces: Each module can import and utilize the logical address space of zero or
more other modules. This allows a variety of different architectures to
be constructed from the same basic building blocks.
-
Load Java classes to function as endpoints: Since each module can
contain endpoints implemented physically as Java code, each module
will need a classloader. To keep modules truly separate from each other,
each module implements its own custom classloader that loads code and
static resources into a private area and that respects export and import
statements.
Modules have a well-defined way of resolving logical addresses to physical
endpoints within a private address space and an import/export mechanism for
logical addresses. Application software architects can use these capabilities
when linking modules together to compose the layers of a software system design.
Looking at our prototype client code again:
req = context.createRequest("resource:/customers/");
req.setVerb(Request.SOURCE);
req.setType(DOM.class);
rep = context.issueRequest(req);
We know that the microkernel searches for a mapping from the address "resource:/customers/" to
an endpoint. The endpoint might exist in the current address space, an imported
address space, or even in an address space further up the request stack. The
developer who writes the client code does not need to know anything about the
large scale structure of the application. The developer only needs to make
a request for the information required. It is up to the application composer
or architect to determine the layering and large scale structure of the application.
In fact, the application structure can changed after both the client
and endpoint code is written with no impact whatsoever to the existing
Java code.
Transrepresentation
The advantages of logical address mapping can be further augmented with the
idea of transrepresentation, as introduced
in the first article. With support for transrepresentation (also known as transreption),
client and endpoint code is decoupled even with respect to type.
If a client requests that information be returned in a particular representation
type, then the microkernel will forward that type information to the endpoint.
The endpoint may or may not be able to deliver the information in the requested
form. If it cannot, the endpoint will return whatever representation type it
knows how to return. (Many endpoints may, in fact, be written to return only
one form of information).
If the endpoint does not return the information in the requested form, the
microkernel will search for a transreption service (transreptor) that can transform the returned
representation type automatically. This transreption will occur transparently
to both the client and the service. In our example, if the endpoint mapped
to "resource:/customers/" returns information as a JSON object, then
the microkernel will search for a transreption service that can change the
form of the information from JSON to XML DOM.
If the microkernel cannot locate an appropriate transreptor or if the transreptor
fails, an error is returned to the client. This signals to the client that
the system could not provide the information in the form requested. It is important
to note that a transreption service does not change the information
itself, it only changes the physical form (representation) of the information.
Thus, there is never a chance that information will be returned to the client
that the client did not request.
There is no equivalent to the idea of transreption in the World Wide Web -
this is an innovation that currently applies only to application software development.
From the broader perspective of general computing, however, transreption is
an interesting idea that goes beyond just providing additional decoupling between
client and endpoint code. Many computing tasks which transform information
from one representation into another can be considered to be transreption tasks.
This includes processes which have, traditionally, been implemented as standalone
tools such as parsers, compilers, etc. By treating these information transformers
as transreptors, they can be smoothly and transparently incorporated into our
ROC architecture as just another logically addressed service.
Caching
According to the REST design approach, returned resource representations are
copies of the information located at a resource address. And, importantly,
they are immutable copies of the information
which was current at the time of the request. Because the representation is
immutable, there is no difference between recomputing the representation
and reading the immutable copy - as long as the information managed by the
endpoint has not changed.
This leads to the idea of using a cache to save computed resource representations
and somehow keep track of the validity of the information. Our microkernel
can cache returned representations by simply associating the URI address of
the request with the representation. The URI becomes the cache key and the representation is stored
as the cache value. With a cache in
place, client requests may be satisfied by retrieving a cached representation
without ever resending the request to an endpoint. The cache can also maintain
a dependency hierarchy which will atomically and transparently invalidate all
cache entries that are dependent on a resource that has become invalid.
Scheduling
Our ROC architecture also has several important ramifications for process
scheduling. When a client sends a resource request to the microkernel, the
microkernel is free to use the provided thread or to schedule processing on
one or more other threads. It is also free to determine when to schedule processing
at the endpoint, which endpoint to use, and even the priority of the request
relative to other requests.
With full decoupling of clients and endpoints, the microkernel is able to
schedule work across CPU cores analogous to the way a load balancer distributes
work across servers in a large web site's server farm. Just as a browser has
no clue whether a web site has a single server or ten thousand servers, client
code in our architecture is unaware of whether the machine executing its request
has only a single core or sixty-four cores. And that means that application
software can scale up with the addition of CPU cores without the software developer writing any thread
aware code. This is a very significant result because, as many know
by now, writing code to perform well on multiple CPU cores is a challenging
task.
Implications
In the first and second articles we have described an architecture that brings
the flexibility of the Web to application software. Realizing an implementation
of this architecture leads to a combination of technologies and ideas which
has several interesting implications, including:
Decoupling - a long sought characteristic that leads to flexibility
- is provided for both physical coupling and type coupling.
Multi-core scaling. Writing software to take advantage of emerging
multi-core computers is very challenging. Our approach mitigates this issue,
both by allowing application software to scale with cores and by not requiring
developers to write multi-threaded code.
Caching and Dependency Tracking eliminates mindless redundant computation
across whole applications and systems. Since all requests are based
on logical addresses they are all candidates for caching. This optimization
is transparent across entire systems and can greatly reduce server overhead.
In addition, caching facilitates the automatic rebalancing of a server as the
workload changes.
Container Management is Simplified. As physical containers, modules
organize code (endpoints) and static resources and provide a private logical
address space. Because resources in modules are logically coupled to
other parts of the system, they can be replaced (updated, rolled back, etc.)
as the system runs. (All that is required is to have the microkernel temporarily
queue requests to a module for the time it takes to do the physical swap).
Modules can also have version numbers, allowing different versions to be run
simultaneously. The microkernel will always resolve a versioned request to
the correct version of a module.
Conclusion
This article rounds-out the discussion of a RESTful approach to application
software design at the physical code level. The Resource Oriented Computing
approach is not only simple, powerful, and elegant but it also leads to more
flexible software applications and systems. Upcoming articles will move towards
the logical level to examine how to integrate different programming languages
as services and how to compose layered applications using ROC. We will also
explore the new set of architectural and design patterns that become available
to us at the logical level.
References
[1] A
RESTful Core for Web-like Application Flexibility - Part 1
PRINTER FRIENDLY VERSION
|