A {\it stable object} is an object whose state is stored on the disk or other medium whence its state can be recovered if a program crashes.
The generic interface Stable
defines a subtype of a given object
type that is just like that object type, but stable. The generic
argument to Stable
is an interface named Data
, which is
assumed to contain an object type named Data.T
. Thus the type
Stable(Data).T
is like a Data.T
, but stable. In case of a
failure (of either the program or the system) such objects can
recover by resuming their last recorded state.
The state of a stable object is stored as a checkpoint together with a redo log; the log contains entries for all updates performed since the last checkpoint. These updates are recorded by logging a number identifying an update method, together with the arguments of the method. The typical cost for an update is therefore on the order of the cost of a single disk write.
In order to keep the redo log from growing without bound (which
would cause the recovery time to grow without bound), it is necessary
to periodically write a new checkpoint. While writing a new
checkpoint the data structure is unavailable for updating. As
a client of the Stable
package, you can control how often checkpoints
are made.
The strategy used by the Stable
package is described more fully
by Andrew D. Birrell, Michael B. Jones, and Edward P. Wobber
in ``A simple and efficient implementation for small databases'',
{\it Proceedings of the 11th ACM Symposium on Operating System
Principles}. This paper is also available as SRC Research Report
24, January 1988.
The stable stub generator stablegen
will automatically produce
an implementation of any generic instance Stable(Data)
. The
default implementation produced by stablegen
reads and writes
checkpoints using the pickles package, and reads and writes
the redo log using the same marshaling mechanism as the network
objects stub generator (stubgen
). This is suitable for many
applications, but not for all. The comments at the end of the
interface explain how to customize the behavior of the stable
package in several different ways.
The initialization method for a stable object will reset it to the stable state, if the stable state is present; otherwise the method creates and initializes the stable state.
By default, the stable state (checkpoint and log) for an object
is stored in a directory in the ordinary file system. It is
possible to override this default. For example, in a system
that contains some form of stable random access memeory, it may
be preferable for reasons of speed to keep the stable data in
the stable RAM instead of on the disk. This flexibility is provided
by an abstraction called a LogManager
, which is basically an
object that provides streams for logs and checkpoints. A LogManager
may be provided when a stable object is initialized; if omitted,
the default log manager is used, which stores the log and checkpoint
in the ordinary file system. See LogManager.i3
for details.
The stub generator produces overrides for all update methods
of the stable object. The override method writes its parameters
together with a method number to the log, and then calls the
corresponding method of its supertype. Finally they write an
additional commit
record to the log to record that the method
terminated. (If a crash occurs while the method is running, the
commit record will not be present and the call that crashed will
not be repeated during recovery.)
The recovery process is started by init
if a stable
backup is found for the object being initialized. To recover,
the log manager is used to obtain a reader on the checkpoint,
the readCheckpoint
method of the object is used to reconstruct
an object from the checkpoint. The log manager is also used
to obtain a reader on the redo log. The entries in the redo
log are read, and the recorded updates are replayed. Any returned
values or exceptions from these methods are ignored. If update
methods use VAR
parameters, the recovery procedure will pass
dummies to them that contain the same values the originally passed
variables had. Notice that if an update method reads or writes
any state other that the fields of the stable object itself,
the recovery process may not work as expected.
A Stable(Data).T
is unmonitored. The client is responsible
for ensuring that it is not updated concurrently nor updated
while a checkpoint is written. Clients are also responsible to
write checkpoints periodically by calling the Checkpoint()
procedure.
GENERIC INTERFACEWhereStable (Data);
Data.T
is an object type. Also, the Data
interface should
contain a pragma indicating which methods of Data.T
are update methods,
as explained in the man page for the stub generator stablegen
.
IMPORT LogManager, StableError, Pathname, Wr, Rd; CONST Brand = "(Stable" & Data.Brand & ")"; DefaultBrand = "(Default " & Brand & ")";
This branding is required for all generic interfaces. The Data interface must contain:
CONST Brand = <text-constant>;
TYPE T <: Public; Public = Data.T OBJECT METHODS init(nm: Pathname.T; VAR recovered: BOOLEAN; forceToDisk := TRUE; lm: LogManager.T := NIL): T RAISES {StableError.E}; dispose() RAISES {StableError.E}; flushLog() RAISES {StableError.E}; freeLog() RAISES {StableError.E}; writeCheckpoint (wr: Wr.T) RAISES {StableError.E}; readCheckpoint (rd: Rd.T): T RAISES {StableError.E}; END;The call
st.init(nm, recovered, forceToDisk, lm)
makes the
object st
stable, with stable state stored by the log manager
lm
under the name nm
. If this stable state is present
when init
is called, then recovery is initiated,
recovered
is set to TRUE
, and the recovered object is
returned. Otherwise, the state of st
is recorded stably
and st
itself is returned. It is a runtime error to call
st.init
twice without an intervening st.dispose
(see below).
If forceToDisk
is TRUE
, the log will be flushed after
every update method. If forceToDisk
is false, the
log will be flushed only when the client calls the flushLog
method. Leaving forceToDisk
false is more efficient, but
if forceToDisk
is false and flushLog
is not called
frequently, recovery may fail to replay some update
methods. The number of lost updates depends on how much
buffering is performed by the log manager.
If lm
is NIL
, then the file system log manager is used.
This means that nm
is interpreted as the name of a file
system directory, in which the stable state is stored as
a checkpoint and redo log file.
The method call st.freeLog()
closes the log writer for st
.
The log will be reopened by the next call to an update method.
Freeing the log might be necessary, for example, to prevent running
out of file descriptors if many stable objects are allocated but
few are active at once.
The readcheckpoint
method must reconstruct a data structure
equivalent to the one written by writecheckpoint
. By default,
both of these methods use pickles, but you can override them if
you want to.
The call st.dispose()
deletes the stable state of st
. It
should be called when stability for st
is no longer desired.
The stability of an object and modifications to its state are
orthogonal. That is, update methods may be called after
a call to st.dispose.
PROCEDURE Checkpoint(t: T) RAISES {StableError.E};
Write a new checkpoint for t
and empty its redo log.
You are responsible for calling
Checkpoint
periodically, to
prevent the redo log from growing without bound.
It is a runtime error to call Checkpoint
after calling
st.dispose,
unless the object has subsequently been restabilized
with st.init.
END Stable.\subsection{Exceptions} The
StableError.E
exception is raised in various circumstances. The first
element of the AtomList.T
argument identifies the nature of the
error. Subsequent elements may identify further details of the
error, especially in the case when lower-level exceptions are
propagated by the Stable
package.
\subsection{Customizations}
\begin{itemize}
\item If you don't want every update to be flushed
to disk, set forceToDisk
to FALSE
when initializing the
stable object, and flush the log
manually with the flushLog
method.
\item If you don't want to use Pickle
for checkpointing, override
the writeCheckpoint()
and readCheckpoint()
methods of the stable
object to read and write checkpoints in your preferred format.
(If you are aiming at long-lived persistent state you should
avoid pickles, since new versions of the Modula-3 system can't always
read pickles written by earlier versions.)
\item If you don't want to store the stable state in the ordinary file system,
implement your own log manager (as explained in the LogManager
interface)
and pass your own manager to the init
method instead of the default.
\item If you don't want to use network-object style marshaling for recording
the update method calls in the redo log, you will have to write
you own implementation of of Stable(Data).m3
instead of using the
stub generator stablegen
. (You may still find it
helpful to use the generated code as starting point.)
\end{itemize}