This book is pre-release and is an evolving work-in-progress. It is published here for the purposes of gaining feedback and providing early value to those who have an interesting resource oriented computing.
Please send any comments or feedback to: firstname.lastname@example.org
© 2018 Tony Butterfield.
All rights reserved.
There is no part of this exposition that I broach more cautiously than the question, "What is a resource?" The definition obviously lies at the heart of resource-oriented computing (ROC). A glib answer might be to say everything is resource, but equally, you can say everything is an object, or everything is bytes. It is worth remembering that "resource" is just a name - a noun. The noun resource has heritage from the web architecture, and we borrow from that. In the web architecture, a resource is anything that can be identified, named, addressed, or handled. Though that description doesn't seem on face value that useful, that is the essence of it. So let's pick that apart a little. By looking at how this thing we call a resource relates to some other concepts, we get a much clearer definition of the term.
Figure: The core concepts of resource oriented computing
Every resource must have an identifier. For a resource to be useful, it must be identifiable. An identifier is usually a string of characters whose structure is unimportant for the moment. Essentially it is a name. A resource may have more that one identifier, but it must have at least one. If a resource where to have no identifier then it could never be referenced.
An address space is a container for resources. Address spaces provide modularity for resources by collecting sets of them together. In the web, there is just one global address space, and that contains all the resources from all the websites that exist at any particular point in time. However, a browser operating on a LAN essentially see's two address spaces, one on the local network, and then the global one. Address spaces are used to resolve requests to resources.
Requests are used to interact with resources. The only way for anything to interact with a resource is by issuing a request. At a minimum, a request specifies the resource identifier and a verb. The verb specifies a mode of interaction. There is a limited set of verbs which include source and sink. The SOURCE verb retrieves the current state of a resource; SINK updates the state of a resource.
When a request is executed, a response is returned. The response indicates the success status of execution as well as other meta-data and possibly some state from the resource.
A request also specifies a request scope which is a simple, ordered list of address spaces. These address spaces are used to resolve a request to a specific resource residing in a specific address space. Address spaces are tested in turn to see if they can resolve the resource. When a resolution is found, the request is delegated to that space to process. Request scope can be considered analogous to the concept of scope in programming languages. Scope in ROC is dynamic, not lexical.1
Endpoints embody resources in software. For resource interaction to become concrete, a resolving address space must also define an endpoint to handle the request. An endpoint is a software component which can receive a resolved request and perform the requested verb. When an address space resolves a request, it delegates it to a specific endpoint. Pattern matching of the resource identifier usually determines how the space performs resolution to an endpoint. Usually, endpoints handle requests to multiple resources, but the mapping is configurable.
Requests pass state into resources and responses pass state from resources as representations. Representations are immutable snapshots of state. Resources do not necessarily need to be low-level streams of bytes but may be higher level data structures or object models. The only constraint is that they must be immutable. Immutability is vital to stop out-of-band communication with resources and to ensure consistency. This becomes very important when we look in more detail at representation expiry and caching.
Endpoints may make requests to other resources. This definition means that resources can be recursive decomposed. An endpoint acts as a client requesting sub-resources, and higher-level resources compose low-level resources. One endpoint, usually a transport endpoint, initiates a root request and this results in the recursive tree of sub-requests. A root request is thus defined as a request with no parent request. Typically transports issue a root request in response to some external stimuli.
It is this set of interrelated definitions that define the core of resource-oriented computing and hence what a resource is.
From this, we can see that a resource is merely an entity which we model within a resource-oriented system. A resource may or may not contain state. It may be mutable or immutable depending upon which verbs it supports. A resource may model an external system, wrap a technology, provide a service, implement a programming language or provide a representation of some state stored in a file or database.
To be clear a resource is not:
an identifier. A resource identifier gives a resource a name.
a representation. A representation is just a snapshot of state to pass into or out of a resource.
an endpoint. An endpoint is software that provides an implementation of a resource.
Endpoints fall, roughly, into different categories based upon their functionality and the resources they provide.
Figure: Categorisation of Endpoints
Overlays wrap an entire address space; as such they manage access to the wrapped space from a host space. Overlays have many uses including implementing a simple import of one space into another. Other more sophisticated uses are interface adaption and layering non-functional requirements like rate limiting or access control.
Transports are endpoints which don’t embody any addressable resources, hence no requests can resolve to them. What they do is perceive external stimuli and convert them into requests that are issued into their host space. The requests issued by transports are called root requests as they have no parent.
Accessors, in a sense, are the most obvious of endpoint categories. They directly provide an embodiment of one or more resources. For example, a File accessor maps the files within a filesystem to resources within the host space with identifiers starting with “file:” The file accessor resolves SOURCE, SINK, EXISTS and DELETE requests which have a direct effect on the file on the filesystem.
The file accessor is an example of a data accessor because it models a resource which has state. Service accessors typically model transformations or language runtimes. These embody derivative resources which depend upon the state of other resources rather than any state which they directly contain. For example, consider the Groovy runtime accessor which has no state but which provides the response from executing a given groovy program. Usually, these stateless or service accessors only implement the SOURCE verb.
Transreptors are again endpoints that don’t embody any resources. They convert representations from one form to another. Their purpose is to adapt representations from what an implementing endpoint provides to what a requesting endpoint wants. A simple example is a compiler that takes a sequence of characters and transrepts to an abstract syntax tree. Transreptors resolve requests with the TRANSREPT verb according to the scoping rules of any other request.
Two concepts need to be discussed to cover additional modularity techniques over and above the concept of spaces as the container of resources. These concepts are modules and prototypes. Modules provide a physical deployment mechanism for spaces with their ancillary components such as endpoints and any code libraries.
Prototypes provide a mechanism for instantiating endpoint instances based on pre-defined types. Figure: Modularity Concepts
In the abstract, modules are not necessary to ROC - in practice they are critical. Modules provide a physical mechanism for deploying functionality into a resource-oriented system in a modular way. Within NetKernel they provide a language called the Standard Module Definition which instantiates and configures spaces and their endpoints. Also, being based on the Java programming language provides modular class loaders for accessing endpoint code and their class libraries.
Prototypes are abstract endpoint definitions ready to be instantiated and configured when and where needed. Typically they are defined in a library module and then instantiated within applications. It is possible to specify configuration parameters when a prototype is instantiated. Often prototypes are used for Overlays as standard types of overlays are used often in different spaces.
So far we have talked about the concepts the resource-oriented abstraction without considering how it might be implemented. In this section, I want to cover some of the fundamental concepts behind the implementation of a ROC system and in particular NetKernel. While some of these details could be implemented differently, it is useful to gain a high-level understanding of their operation to ground the abstraction in a practical implementation.
Figure: Resource Oriented Infrastructure
The kernel is the core of NetKernel. It provides the middleware with which resource requests are issued, resolved and executed. When is a request is issued by an endpoint it is issued to the kernel to process, though this process could be hidden from the code within an endpoint by an Application Programming Interface (API).
Request processing is a multi-stage operation; we will cover it with more detail in later sections, but for the moment, we can consider the three key stages as resolve, execute, and transrept. This process can be cut short by the use of caches. Firstly, resolution can be bypassed if the same request has been resolved previously and the resolution cache holds the outcome. Then secondly, the execution can be bypassed if the same request has been executed previously and its response remains valid and is held by the representation cache.
Endpoints can configure how the response representations they generate remain valid by adding meta-data to the response. A simple example of this is to add a time-to-live (TTL) period. Transrept is the final stage, in this stage, the representation return by the endpoint is compared to the representation specified in the request. If the types don't match a transreptor is resolved and invoked.
An additional role of the kernel is to manage a registry of public spaces so that other spaces can locate and delegate to them. Typically a module registers a module with kernel if it provides some public and generally useful services such as a resource library that applications might want to incorporate. An import endpoint might then instantiated in a client space to delegate requests through to a library space.
Requests are issued either synchronously or asynchronously. For the processing asynchronous requests, the kernel maintains a thread pool. Requests are queued when the thread pool is exhausted.