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
- A session begins with a single B-Action. A
B-action normally returns a
handle (reference, id, pointer, etc) argument,
h.
- Any number of M- actions may occur after
the single B-action. Each M-
action somehow uses the handle h obtained from
the B-action.
- A session ends with a single E-action that
somehow invalidates or consumes the handle h.
An E-action is triggered only when
no additional M-actions are desired or possible.
This simple protocol can be described as the regular expression:
(B M* E).
Examples
- C++ Objects:
-
- B = {new}
- M = { [messages to created object] }
- E = {delete}
- Transactions:
-
- B = {beginTransaction}
- M = { [messages to participating objects] }
- E = {commit, abort}
- Locking:
-
- B = {acquire}
- M = { [messages to locked objects] }
- E = {release}
- Unix Files:
-
- B = {open, dup, ... }
- M = {read, write, ...}
- E = {close}
- Network Channels and Sockets
-
- B = {connect, ... }
- M = {send, accept, ...}
- E = {disconnect, ...}
- CORBA Services:
-
- B = {create, duplicate}
- M = { [messages to service] }
- E = {release}
Problem
How do you make sure that this protocol is followed? That is:
- Exactly one B-action occurs before all M-actions
- Exactly one E-action occurs after all M-actions
- No M-Action or E-Action occurs otherwise.
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)