Sessions

A design pattern by Doug Lea, Computer Science Department, State University of New York at Oswego, Oswego, NY 13126. Voice: 315-341-2688, Fax: 315-341-5424. E-Mail: dl@g.oswego.edu.

Context

Abstractly, sessions consist of three sets of actions on resources. Call the kinds of actions B, M, and E (for Beginning, Middle, and End). The rules are that

This simple protocol can be described as the regular expression: (B M* E).

Examples

C++ Objects:
Transactions:
Locking:
Unix Files:
Network Channels and Sockets
CORBA Services:

Problem

How do you make sure that this protocol is followed? That is:

Forces

Because of the structure of the problem, enforcing sequencing of M-Actions after B-Actions is usually simple. In most cases, M-actions require handles that can only have been constructed via B-actions, so must follow them. (An exception to this is that if B-actions can fail, you may need a way of preventing other actions from occuring.)

The most central concerns revolve around the conditions under which E-Actions occur. Arranging that an E-Action occurs in the right way almost always leads to corresponding policies for B-Actions and M-Actions. In particular, the following forces may apply:

Resource Consumption
B-actions typically represent resource allocation, and E-actions resource reclamation. An E-action for handle h may be required before some other B-action for some other handle h'. For example, in the case where actions in E release resources (memory, files, etc) they must be performed soon enough to allow new allocations. You'd like to minimize the number of resources in use at any time.
Safety
Often, an attempt to perform an M-action after an E-action is a catastrophic failure, for which there is no sensible recovery. Similarly for multiple E-Actions. Ideally, design policies should provably prevent such failures. In turn these policies should be enforceable by an implementation language or other tools.
Locality
When multiple objects must invoke the actions in B, M, E, and the actions in set M are unknown in advance, ensuring that only legal B-M-E traces occur is a non-local, dynamic problem requiring some kind of multi-object protocols, policies, or conventions. Designs should minimize the number of objects involved in such policies.
Simplicity
To avoid error, you'd like to minimize the total number of associated policies and mechanisms adopted in a system. Ideally, policies should consist of simple conventions that do not depend on special actions being performed by a large number of objects.
Efficiency
You'd like to minimize the time and space overhead associated with the protocols.

Design Steps

Discover or devise the enabling conditions for E-Actions, along with policies about how these conditions will be tested or otherwise dealt with. Structure everything else accordingly.

There are two aspects to design: (1) Choosing or determining Policies surrounding enabling conditions and responsibility for detecting them and carrying out the corresponding E-Actions. (2) Possibly grouping resources into Containers managed as a whole rather than individually.

POLICIES

Policies can be grouped into three categories:
Fixed
Fixed rules are those that isolate E-Actions to one, or perhaps a small fixed number of spots of code in a program. This can often be done in a way that can be statically determined to to preclude all possible safety violations.
Local
Local policies are rules that apply to each participating object in a system in isolation. They are available when the enabling condition for an E-Action is localizable to a single object or method.
Cooperative
Cooperative designs are necessary when no single (or small fixed number of) objects or methods can determine using strictly local information whether an E-Action needs to be applied. Thus, each participating object must engage in a common cooperative protocol with others so that together they can ensure that E-Actions are taken.
The associated structures and designs tend to lead to common strengths and weaknesses with respect to the above criteria. However, because choices are so closely tied to the forms of enabling conditions, you can't always optimize over all forces simultanously. For example, the main weakness of local and cooperative approaches is that all entities involved with the resource must obey a common and consistent set of policies and/or protocols, thus adding a burden for all programmers. While it is normally possible to isolate this burden to a few conventions, it cannot be eliminated.

Some common forms of each of these categories are described below. While many mixtures of these are possible (and even common) the fewer policies and mechanisms used, the better.

Fixed Algorithms

When the entire sequence of actions in B, M, E is fixed and known in advance (even when occurring across multiple objects) then the entire sequence can be hard-coded. For example, if a certain algorithm demands that a set of objects open a file, read exactly three inputs, and then close it, this sequence may simply be coded as such. An advantage of this approach is that E-Actions are taken as soon as they are logically possible, thus minimizing resource consumption. It is sometimes possible to do this even in conjunction with reachability tracking (see below) in order to guarantee early recovery in special cases.

Sometimes, even when not immediately apparent in a design, finite sequences can be discovered (at great effort) via static analysis of the system, determining all usage dependencies, and associating the E-action with the final M-action.

The main weakness of this approach is of course its fragility. If the algorithm changes, the resource control must change as well.

Fixed Special Resource States

If a resource can enter states under which no M-Action can succeed anyway, then detection of these states can serve as the enabling condition for an E-Action so long as M-Action failure and resource liveness failure can be handled equally well (or poorly!) by all clients. For example, if a ReadOnlyFile becomes unusable upon encountering EOF, you might be able to associate detection of EOF with closing the file. Similarly, if any one part of a transaction fails, you might be able to associate this with an abort of the full transaction.

The most heavily localized version of this is for the resource itself to perform an E-Action on itself upon detecting a special state. However, this is not normally possible when the E-Action consists of actual resource destruction. If detection and destruction is placed within a normal method, then the resource will cease to exist sometime in the course of executing the method. In some languages the resulting behavior is undefined, and in all cases is dangerous.

However, similar localization can still be obtained by creating wrappers of various sorts (classes, free-standing procedures) for each M-action that perform the action, check for special states, and perform E-Actions if necessary. (But note that in C++ in particular, wrapper classes that act like handles (i.e., ``fake pointers'' or ``smart pointers'') are themselves hard-to-impossible to make perfectly transparent to all other clients, so require additional design policies and coding rules.)

And when M-Actions themselves are localized to a few places, you can just code in the checks upon each M-action. For example, each read action on a file could check for EOF and if so close the file.

Fixed Special System States

Sometimes, it is possible for a system or application to arrive in a state in which an E-Action is required regardless of any other normal enabling conditions. For example, a system may be out of memory, yet not be allowed to shut down. Or a set of lockable resources may be deadlocked. The only way to accomodate such requirements this is via system-level pre-emptive reclamation and recovery mechanisms that fall out of the scope of this pattern.

Localized Consumption Policies

You can require that any object receiving a handle perform an E-action on it (i.e., ``consume'' it) after using it for local M-actions, and further require that it not pass the handle to any other object. This forces all management to be local (in which case a fixed strategy applies locally).

If the object does need to send a handle to another object in a message, it must not send the one it has, but instead send another resource with equivalent utility (e.g., via some kind of copy or clone operation) or a set of instructions allowing the receiver to perform a B-action to construct such a resource. The simplest form of this boils down to ``by-copy'' (by-value) argument passing in traditional programming languages.

This option does not apply when resources must intrinsically be shared, which is true more often than not. It can also incur substantial performance overhead.

Cooperative Consumption Policies

An object receiving a handle can also receive some kind of indication about whether it should perform an E-Action upon it when done. This indication may consist of an additional argument to the receiving method, a special form of the argument (which can sometimes be implemented as a special parameter mode in the programming language), or as tag within the resource itself, in which case it is equivalent to one-bit reference counting (see below).

For example, a collection class may support a method indicating whether an inserted object should be deleted when removed from the collection. (In particular, in a C++ collection, inserting a static object into a collection that normally does an element deletion cascade upon its own destruction should be specially tagged.)

Any given participating object may take over responsibility for E-Actions by ``lending'' the resource to the first of a series of activities, telling them not to take E-Actions, and then finally performing the E-Action upon return from this nested activity. This usage amounts to a simple form of ownership tranfer protocols (see below).

Either consumption or non-consumption can be made the default case. Typically, non-consumption is easier and safer to arrange. For example, in C++, it is possible to construct (via templates) special wrapper subclasses of any resource that force deletion of the inner resource when it goes out of scope in the receiver. Thus, the caller of each method controls whether the resource should be deleted, and the receiver may assume non-consumption.

Fixed Reachability Tracking Mechanisms

The simplest enabling conditions arrange E-Actions when no further M-actions are possible because no live object possesses a handle to the resource that would be necessary to perform an M-Action (i.e., the resource is unreachable). Unreachability is among the safest possible enabling conditions, since safety violations stemming from an M-action occurring after an E-action cannot happen.

Detection of this condition can be localized by creating a privileged infrastructure object, a garbage collector that can perform dynamic reachability analysis asynchronously with respect to the application program, and perform E-actions for all unreachable resources. No further application-level policies are required to manage sessions.

This option is available only if reachability analysis can in fact be localized into an infrastructure process, if this process is efficient and timely enough to release resources early and often enough, and, when necessary, if this process can be programmed to perform arbitrary E-actions (sometimes called finalization) in addition to simply deleting the resource. These requirements rule out using garbage collection for resource management in several languages and environments.

Cooperative Reachability Tracking

With the cooperation of all clients, the resource itself (or a helper or wrapper) can keep track of its own reachability. If the resource maintains a registry of its clients, and each client registers before engaging in any M-Actions, and unregisters when done, then the resource can perform the E-Action itself when there are no clients. When the E-Action consists of deletion, this must normally be done via wrappers (see above).

There is rarely any point to maintaining a full list of clients; a simple count normally suffices. When the count is zero, an E-Action can be performed.

This strategy can fail if a resource can also be its own (perhaps indirect) client. In this case a cycle of references can exist even though the resource is unreachable from any other live object.

In C++, it is possible to partially automate this policy by merging it with block structuring and copy-based rules. If the wrapper objects maintaining the handles themselves are passed by copy, then reference counting may be performed in the course of copy-construction and deletion. This leads to the classic form of reference counting used in C++.

CONTAINMENT

When enabling conditions and E-Actions of a group of resources are somehow related, it is usually easier to manage them as a group rather than each one individually. Variants of the above policies can be adapted to deal with containers of resources rather than single resources.

Containment-based strategies are those that somehow structure the B-Actions and E-Actions for several resources at once into a matched pair that encloses all corresponding M-Actions. Thus all of the enclosed resources have co-extensive lifetimes. Normally, the container itself must be tracked via some defined policy, but this is usually simpler than tracking all of the parts individually.

All variants bear the weakness that, without additional design and coding policies, it is normally impossible to statically guarantee that all possible M-Actions are indeed enclosed within the designated scope. In other words, policies are required that prevent containers from leaking handles via exports (method arguments or return values) to clients that do not share coextensive or subextensive lifetimes, and/or policies that prevent such clients from using them after they are no longer valid. For example, all ``foreign'' clients may adopt the policy that they will never perform M-Actions on a received handle after returning procedural contol back to their callers (i.e., they never store them for later use).

Object Containment

If the enabling conditions for E-Actions for several resources are all the same, then this can be centralized under a single enclosing object that logially ``owns'' all of these resources.

Examples include C++ composite objects (i.e., objects holding subobjects), C++-style Repositories holding by-copy collections of other objects, many Transaction Coordinator designs, and Unix processes that free all process-local resources upon process deletion. Any such container C performs or initialtes the B-Action for each resource under its control, and ultimately performs all of their E-Action when its own E-Action is triggered. Containment may be recursive, so that a single E-Action cascades into nested E-Actions for all of its parts. The Container may also support other methods that perform E-Actions on all of the enclosed resources without destroying the container itself.

Transferable Containment

Each resource may maintain a reference to an object that is designated to be ultimately responsible for performing E-Actions upon it, and conversely, each such object can maintain a list of resources it manages or have some other way of telling which ones it is responsible for. Resources should ignore (or issue exceptions surrounding) requests for E-Actions from objects that are not their owners. Rather than references, any kind of value may serve as a tag; for example an encryption protocol can be used.

If such ownership is transferable, then objects may dynamically designate and redesignate management responsibility to ensure that an object that is able to detect enabling conditions for E-Actions is given the opportunity to do so. For example, when a resource is transferred from one collection to another, it's ownership may be transfered as well.

Block Containment

If the enabling conditions for E-Actions for several resources are all bound by the same static program scope, then all of them can be managed within this scope. This is the scope = lifetime rule of strictly block-structured langauges in which all resources are allocated via stack-based mechanics. When B-Actions are simply allocations and E-Actions are deallocations, then this amounts to standard stack-allocation. When they consist of arbitrary actions, then this is equivalent to C++ rules for block-locals.

Thread Containment

If the enabling conditions for E-Actions for several resources participating in the same thread of activity are all triggered by the same event(s), then all of them can be managed together via exception handlers. A handler that performs all E-Actions may be placed at the base of a call sequence (via, for example, a catch in C++). Code that detects the enabling condition may then throw an exception caught by this handler.
Last update Tue Jun 13 05:51:41 1995 Doug Lea (dl at gee)