- 1 The journal ASDF System
- 2 Links
- 3 Portability
- 4 Background
- 5 Distinguishing features
- 6 Basics
- 7 Logging
- 8 Tracing
- 9 Replay
- 10 Testing
- 11 Persistence
- 12 Safety
- 13 Events reference
- 14 Journals reference
- 15 Bundles reference
- 16 Streamlets reference
- 17 Glossary
- Version: 0.1.0
- Description: A library built around explicit execution traces for logging, tracing, testing and persistence.
- Licence: MIT, see COPYING.
- Author: Gábor Melis mega@retes.hu
- Homepage: http://github.com/melisgl/journal
- Bug tracker: http://github.com/melisgl/journal/issues
- Source control: GIT
Here is the official repository and the HTML documentation for the latest version.
Tested on ABCL, CCL, CLISP, CMUCL, ECL, and SBCL. AllegroCL Express
edition runs out of heap while running the tests. On Lisps that seem
to lack support for disabling and enabling of interrupts, such as
ABCL and CLISP, durability is compromised, and any attempt to
SYNC-JOURNAL
(see Synchronization strategies and Safety) will be a
runtime error.
Logging, tracing, testing, and persistence are about what happened during code execution. Recording machine-readable logs and traces can be repurposed for white-box testing. More, when the code is rerun, selected frames may return their recorded values without executing the code, which could serve as a mock object framework for writing tests. This ability to isolate external interactions and to reexecute traces is sufficient to reconstruct the state of a program, achieving simple persistence not unlike a journaling filesystem or event sourcing.
Journal is the library to log, trace, test and persist. It has a
single macro at its heart: JOURNALED
, which does pretty much what
was described. It can be thought of as generating two events around
its body: one that records the name and an argument list (as in a
function call), and another that records the return values. In
Lisp-like pseudocode:
(defmacro journaled (name args &body body)
`(progn
(record-event `(:in ,name :args ,args))
(let ((,return-values (multiple-value-list (progn ,@body))))
(record-event `(:out ,name :values ,return-values))
(values-list ,return-values))))
This is basically how recording works. When replaying events from a
previous run, the return values of BODY
can be checked against the
recorded ones, or we may return the recorded values without even
running BODY
.
In summary, we can produce selective execution traces by wrapping
code in JOURNALED
and use those traces for various purposes. The
Journal library is this idea taken to its logical conclusion.
-
Nested contexts and single messages
-
Customizable content and format
-
Human- or machine-readable output
#68200.234: ("some-context")
#68200.234: Informative log message
#68200.250: => NIL
See Logging for a complete example.
Compared to CL:TRACE
-
Ability to handle non-local exits
-
Customizable content and format
-
Optional timestamps, internal real- and run-time
(FOO 2.1)
(1+ 2.1)
=> 3.1
=E "SIMPLE-ERROR" "The assertion (INTEGERP 3.1) failed."
See Tracing for a complete example.
-
White-box testing based on execution traces
-
Isolation of external dependencies
-
Record-and-replay testing
(define-file-bundle-test (test-user-registration :directory "registration")
(let ((username (replayed ("ask-username")
(format t "Please type your username: ")
(read-line))))
(add-user username)
(assert (user-exists-p username))))
See Testing for a complete example.
-
Event Sourcing: replay interactions with the external world
-
Unchanged control flow
-
Easy to implement history, undo
(defun my-resumable-autosaving-game-with-history ()
(with-bundle (bundle)
(play-guess-my-number)))
See Persistence for a complete example.
The JOURNALED
macro does both recording and replaying of events,
possibly at the same time. Recording is easy: events generated by
JOURNALED
are simply written to a journal, which is a sequence of
events much like a file. What events are generated is described in
JOURNALED
. Replay is much more involved, thus it gets its own
section. The journals used for recording and replaying are specified
by WITH-JOURNALING
or by WITH-BUNDLE
.
The Journals reference is presented later, but for most purposes,
creating them (e.g. with MAKE-IN-MEMORY-JOURNAL
, MAKE-FILE-JOURNAL
)
and maybe querying their contents with LIST-EVENTS
will suffice.
Some common cases of journal creation are handled by the convenience
function TO-JOURNAL
.
Built on top of journals, Bundles juggle repeated replay-and-record cycles focussing on persistence.
-
[generic-function] TO-JOURNAL DESIGNATOR
Return the journal designated by
DESIGNATOR
or signal an error. The default implementation:-
returns
DESIGNATOR
itself if it is of typeJOURNAL
, -
returns a new
IN-MEMORY-JOURNAL
ifDESIGNATOR
isT
, -
returns a new
FILE-JOURNAL
ifDESIGNATOR
is aPATHNAME
(0
1
).
-
-
[macro] WITH-JOURNALING (&KEY RECORD REPLAY REPLAY-EOJ-ERROR-P) &BODY BODY
Turn recording and/or replaying of events on or off for the duration of
BODY
. BothRECORD
andREPLAY
should be aJOURNAL
designator (in the sense ofTO-JOURNAL
) orNIL
.If
RECORD
designates aJOURNAL
, then events generated by enclosedJOURNALED
blocks are written to that journal (with exceptions, see theLOG-RECORD
argument ofJOURNALED
). IfREPLAY
designates aJOURNAL
, then the generated events are matched against events from that journal according to the rules of Replay.A
JOURNAL-ERROR
is signalled ifRECORD
is aJOURNAL
that has been previously recorded to by anotherWITH-JOURNALING
(that is, if itsJOURNAL-STATE
is not:NEW
) or ifREPLAY
is aJOURNAL
that is not a complete recording of successful replay (i.e. itsJOURNAL-STATE
is not:COMPLETED
). These checks are intended to catch mistakes that would render the new or existing records unusable for replay. WhenWITH-JOURNALING
finishes, theRECORD
journal is marked:COMPLETED
or:FAILED
in itsJOURNAL-STATE
.REPLAY-EOJ-ERROR-P
controls whetherEND-OF-JOURNAL
is signalled when a new event is being matched to the replay journal from which there are no more events to read. If there was aJOURNALING-FAILURE
or aREPLAY-FAILURE
during execution, thenEND-OF-JOURNAL
is not signalled.If
BODY
completes successfully, butREPLAY
has unprocessed events, thenREPLAY-INCOMPLETE
is signalled.WITH-JOURNALING
for differentRECORD
journals can be nested and run independently.
-
[glossary-term] block
A journaled block, or simply block, is a number of forms wrapped in
JOURNALED
. When a block is executed, a frame is created.
-
[glossary-term] frame
A frame is an
IN-EVENT
,OUT-EVENT
pair, which are created when a block is entered and left, respectively.
-
[function] RECORD-JOURNAL
Return the
JOURNAL
in which events are currently being recorded (seeWITH-JOURNALING
andWITH-BUNDLE
) orNIL
.
-
[function] REPLAY-JOURNAL
Return the
JOURNAL
from which events are currently being replayed (seeWITH-JOURNALING
andWITH-BUNDLE
) orNIL
.
-
[macro] JOURNALED (NAME &KEY (LOG-RECORD :RECORD) VERSION ARGS VALUES CONDITION INSERTABLE REPLAY-VALUES REPLAY-CONDITION) &BODY BODY
JOURNALED
generates events upon entering and leaving the dynamic extent ofBODY
(also known as the journaled block), which we call the In-events and Out-events. Between generating the two events,BODY
is typically executed normally (except for Replaying the outcome).Where the generated events are written is determined by the
:RECORD
argument of the enclosingWITH-JOURNALING
. If there is no enclosingWITH-JOURNALING
andLOG-RECORD
isNIL
, then event recording is turned off andJOURNALED
imposes minimal overhead.-
NAME
can be of any type exceptNULL
, not evaluated. For names, and for anything that gets written to a journal, a non-keyword symbol is a reasonable choice as it can be easily made unique. However, it also exposes the package structure, which might make reading stuff back more difficult. Keywords and strings do not have this problem. -
ARGS
can be of any type, but is typically a list.
Also see
:LOG-RECORD
in the Logging section. For a description ofVERSION
,INSERTABLE
,REPLAY-VALUES
andREPLAY-CONDITION
, see Journaled for replay. -
Upon entering a block, JOURNALED
generates an IN-EVENT
,
which conceptually opens a new frame. These in-events are created
from the NAME
, VERSION
and ARGS
arguments of JOURNALED
. For example,
(journaled (name :version version :args args) ...)
creates an event like this:
`(:in ,name :version ,version :args ,args)
where :VERSION
and :ARGS
may be omitted if they are NIL
. Versions
are used for Replay.
Upon leaving a block, JOURNALED
generates an OUT-EVENT
, closing
the frame opened by the corresponding IN-EVENT
. These out-events
are property lists like this:
(:out foo :version 1 :values (42))
Their NAME
and VERSION
(FOO
and 1
in the example) are the same
as in the in-event: they come from the corresponding arguments of
JOURNALED
. EXIT
and OUTCOME
are filled in differently depending on
how the block finished its execution.
-
[type] EVENT-EXIT
One of
:VALUES
,:CONDITION
,:ERROR
and:NLX
. Indicates whether a journaled block-
returned normally (
:VALUES
, see values outcome), -
unwound on an expected condition (
:CONDITION
, see condition outcome), -
unwound on an unexpected condition (
:ERROR
, see error outcome), -
unwound by performing a non-local exit of some other kind such as a throw (
:NLX
, see nlx outcome).
The first two are expected outcomes, while the latter two are unexpected outcomes.
-
-
[glossary-term] values outcome
If the
JOURNALED
block returns normally,EVENT-EXIT
is:VALUES
, and the outcome is the list of values returned:(journaled (foo) (values 7 t)) ;; generates the out-event (:out foo :values (7 t))
The list of return values of the block is transformed by the
VALUES
argument ofJOURNALED
, whose default is#'IDENTITY
. Also see Working with unreadable values).
-
[glossary-term] condition outcome
If the block unwound due to a condition, and
JOURNALED
'sCONDITION
argument (a function whose default is(CONSTANTLY NIL)
) returns non-NIL
when invoked on it, thenEVENT-EXIT
is:CONDITION
, and the outcome is this return value:(journaled (foo :condition (lambda (c) (prin1-to-string c))) (error "xxx")) ;; generates the out-event (:out foo :condition "xxx")
Conditions thus recognized are those that can be considered part of normal execution. Just like return values, these expected conditions may be required to match what's in the replay journal. Furthermore, given a suitable
REPLAY-CONDITION
inJOURNALED
, they may be replayed without running the block.
-
[glossary-term] error outcome
If the
JOURNALED
block unwound due to a condition, butJOURNALED
'sCONDITION
argument returnsNIL
when invoked on it, thenEVENT-EXIT
is:ERROR
and the outcome the string representations of the type of the condition and the condition itself.(journaled (foo) (error "xxx")) ;; generates this out-event: ;; (:out foo :error ("simple-error" "xxx"))
The conversion to string is performed with
PRINC
inWITH-STANDARD-IO-SYNTAX
. This scheme is intended to avoid leaking random implementation details into the journal, which would makeREAD
ing it back difficult.In contrast with condition outcomes, error outcomes are what the code is not prepared to handle or replay in a meaningful way.
-
[glossary-term] nlx outcome
If the
JOURNALED
block performed a non-local exit that was not due to a condition, thenEVENT-EXIT
is:NLX
and the outcome isNIL
.(catch 'xxx (journaled (foo) (throw 'xxx nil))) ;; generates the out-event (:out foo :nlx nil)
Note that condition outcomes and error outcomes are also due to non-local exits but are distinct from nlx outcomes.
Currently, nlx outcomes are detected rather heuristically as there is no portable way to detect what really caused the unwinding of the stack.
There is a further grouping of outcomes into expected and unexpected.
-
[glossary-term] expected outcome
An
OUT-EVENT
is said to have an expected outcome if it had a values outcome or a condition outcome, or equivalently, when itsEVENT-EXIT
is:VALUES
or:CONDITION
.
-
[glossary-term] unexpected outcome
An
OUT-EVENT
is said to have an unexpected outcome if it had an error outcome or an nlx outcome, or equivalently, when itsEVENT-EXIT
is:ERROR
or:NLX
.
The events recorded often need to be readable. This is always
required with FILE-JOURNAL
s, often with IN-MEMORY-JOURNAL
s, but
never with PPRINT-JOURNAL
s. By choosing an appropriate identifier or
string representation of the unreadable object to journal, this is
not a problem in practice. JOURNALED
provides the VALUES
hook for this purpose.
With EXTERNAL-EVENT
s, whose outcome is replayed (see
Replaying the outcome), we also need to be able to reverse the
transformation of VALUES
, and this is what the
REPLAY-VALUES
argument of JOURNALED
is for.
Let's see a complete example.
(defclass user ()
((id :initarg :id :reader user-id)))
(defmethod print-object ((user user) stream)
(print-unreadable-object (user stream :type t)
(format stream "~S" (slot-value user 'id))))
(defvar *users* (make-hash-table))
(defun find-user (id)
(gethash id *users*))
(defun add-user (id)
(setf (gethash id *users*) (make-instance 'user :id id)))
(defvar *user7* (add-user 7))
(defun get-message ()
(replayed (listen :values (values-> #'user-id)
:replay-values (values<- #'find-user))
(values *user7* "hello")))
(jtrace user-id find-user get-message)
(let ((bundle (make-file-bundle "/tmp/user-example/")))
(format t "Recording")
(with-bundle (bundle)
(get-message))
(format t "~%Replaying")
(with-bundle (bundle)
(get-message)))
.. Recording
.. (GET-MESSAGE)
.. (USER-ID #<USER 7>)
.. => 7
.. => #<USER 7>, "hello"
.. Replaying
.. (GET-MESSAGE)
.. (FIND-USER 7)
.. => #<USER 7>, T
.. => #<USER 7>, "hello"
==> #<USER 7>
=> "hello"
To be able to journal the return values of GET-MESSAGE
, the USER
object must be transformed to something readable. On the
Recording
run, (VALUES-> #'USER-ID)
replaces the user object
with its id in the EVENT-OUTCOME
recorded, but the original user
object is returned.
When Replaying
, the journaled OUT-EVENT
is replayed (see
Replaying the outcome):
(:OUT GET-MESSAGE :VERSION :INFINITY :VALUES (7 "hello"))
The user object is looked up according to :REPLAY-VALUES
and is
returned along with "hello"
.
-
[function] VALUES-> &REST FNS
A utility to create a function suitable as the
VALUES
argument ofJOURNALED
. TheVALUES
function is called with the list of values returned by the block and returns a transformed set of values that may be recorded in a journal. While arbitrary transformations are allowed,VALUES->
handles the common case of transforming individual elements of the list independently by calling the functions in FN with the values of the list of the same position.(funcall (values-> #'1+) '(7 :something)) => (8 :SOMETHING)
Note how
#'1+
is applied only to the first element of the values list. The list of functions is shorter than the values list, so:SOMETHING
is not transformed. A value can be left explicitly untransformed by specifying #'IDENTITY
orNIL
as the function:(funcall (values-> #'1+ nil #'symbol-name) '(7 :something :another)) => (8 :SOMETHING "ANOTHER")
-
[function] VALUES<- &REST FNS
The inverse of
VALUES->
, this returns a function suitable as theREPLAY-VALUES
argument ofJOURNALED
. It does pretty much whatVALUES->
does, but the function returned returns the transformed list as multiple values instead of as a list.(funcall (values<- #'1-) '(8 :something)) => 7 => :SOMETHING
-
[function] LIST-EVENTS &OPTIONAL (JOURNAL (RECORD-JOURNAL))
Return a list of all the events in the journal designated by
JOURNAL
. CallsSYNC-JOURNAL
first to make sure that all writes are taken into account.
-
[function] EVENTS-TO-FRAMES EVENTS
Convert a flat list of events, such as those returned by
LIST-EVENTS
, to a nested list representing the frames. Each frame is a list of the form(<in-event> <nested-frames>* <out-event>?)
. Like inPRINT-EVENTS
,EVENTS
may be aJOURNAL
.(events-to-frames '((:in foo :args (1 2)) (:in bar :args (7)) (:leaf "leaf") (:out bar :values (8)) (:out foo :values (2)) (:in foo :args (3 4)) (:in bar :args (8)))) => (((:IN FOO :ARGS (1 2)) ((:IN BAR :ARGS (7)) (:LEAF "leaf") (:OUT BAR :VALUES (8))) (:OUT FOO :VALUES (2))) ((:IN FOO :ARGS (3 4)) ((:IN BAR :ARGS (8)))))
Note that, as in the above example, incomplete frames (those without an
OUT-EVENT
) are included in the output.
-
[function] EXPECTED-TYPE TYPE
Return a function suitable as the
CONDITION
argument ofJOURNALED
, which returns the type of its single argument as a string if it is ofTYPE
, elseNIL
.
-
[function] PRINT-EVENTS EVENTS &KEY STREAM
Print
EVENTS
toSTREAM
as lists, starting a new line for each event and indenting them according to their nesting structure.EVENTS
may be a sequence or aJOURNAL
, in which caseLIST-EVENTS
is called on it first.(print-events '((:in log :args ("first arg" 2)) (:in versioned :version 1 :args (3)) (:out versioned :version 1 :values (42 t)) (:out log :condition "a :CONDITION outcome") (:in log-2) (:out log-2 :nlx nil) (:in external :version :infinity) (:out external :version :infinity :error ("ERROR" "an :ERROR outcome")))) .. .. (:IN LOG :ARGS ("first arg" 2)) .. (:IN VERSIONED :VERSION 1 :ARGS (3)) .. (:OUT VERSIONED :VERSION 1 :VALUES (42 T)) .. (:OUT LOG :CONDITION "a :CONDITION outcome") .. (:IN LOG-2) .. (:OUT LOG-2 :NLX NIL) .. (:IN EXTERNAL :VERSION :INFINITY) .. (:OUT EXTERNAL :VERSION :INFINITY :ERROR ("ERROR" "an :ERROR outcome")) => ; No value
-
[function] PPRINT-EVENTS EVENTS &KEY STREAM (PRETTIFIER 'PRETTIFY-EVENT)
Like
PRINT-EVENTS
, but produces terser, more human readable output.(pprint-events '((:in log :args ("first arg" 2)) (:in versioned :version 1 :args (3)) (:leaf "This is a leaf, not a frame.") (:out versioned :version 1 :values (42 t)) (:out log :condition "a :CONDITION outcome") (:in log-2) (:out log-2 :nlx nil) (:in external :version :infinity) (:out external :version :infinity :error ("ERROR" "an :ERROR outcome")))) .. .. (LOG "first arg" 2) .. (VERSIONED 3) v1 .. This is a leaf, not a frame. .. => 42, T .. =C "a :CONDITION outcome" .. (LOG-2) .. =X .. (EXTERNAL) ext .. =E "ERROR" "an :ERROR outcome" => ; No value
The function given as the
PRETTIFIER
argument formats individual events. The above output was produced withPRETTIFY-EVENT
. For a description ofPRETTIFIER
's arguments seePRETTIFY-EVENT
.
-
[function] PRETTIFY-EVENT EVENT DEPTH STREAM
Write
EVENT
toSTREAM
in a somewhat human-friendly format. This is the functionPPRINT-JOURNAL
,PPRINT-EVENTS
, and Tracing use by default. In addition to the basic example inPPRINT-EVENTS
, decoration on events is printed before normal, indented output like this:(pprint-events '((:leaf "About to sleep" :time "19:57:00" :function "FOO"))) .. .. 19:57:00 FOO: About to sleep
DEPTH
is the nesting level of theEVENT
. Top-level events have depth 0.PRETTIFY-EVENT
prints indents the output after printing the decorations by 2 spaces per depth.
Instead of collecting events and then printing them, events can be pretty-printed to a stream as they generated. This is accomplished with Pretty-printing journals, discussed in detail later, in the following way:
(let ((journal (make-pprint-journal)))
(with-journaling (:record journal)
(journaled (foo) "Hello")))
..
.. (FOO)
.. => "Hello"
Note that Pretty-printing journals are not tied to WITH-JOURNALING
and are
most often used for Logging and Tracing.
-
[condition] JOURNALING-FAILURE SERIOUS-CONDITION
Signalled during the dynamic extent of
WITH-JOURNALING
when an error threatens to leave the journaling mechanism in an inconsistent state. These include I/O errors encountered reading or writing journals byWITH-JOURNALING
,JOURNALED
,LOGGED
,WITH-REPLAY-FILTER
,SYNC-JOURNAL
, and alsoSTORAGE-CONDITION
s, assertion failures, errors callingJOURNALED
'sVALUES
andCONDITION
function arguments. Crucially, this does not apply to non-local exits from other code, such asJOURNALED
blocks, whose error handling is largely unaltered (see Out-events and Replay failures).In general, any non-local exit from critical parts of the code is turned into a
JOURNALING-FAILURE
to protect the integrity of theRECORD-JOURNAL
. The condition that caused the unwinding is inJOURNALING-FAILURE-EMBEDDED-CONDITION
, orNIL
if it was a pure non-local exit likeTHROW
. This is aSERIOUS-CONDITION
, not to be handled withinWITH-JOURNALING
.After a
JOURNALING-FAILURE
, the journaling mechanism cannot be trusted anymore. TheREPLAY-JOURNAL
might have failed a read and be out-of-sync. TheRECORD-JOURNAL
may have missing events (or even half-written events withFILE-JOURNAL
s withoutSYNC
, see Synchronization strategies), and further writes to it would risk replayability, which is equivalent to database corruption. Thus, upon signallingJOURNALING-FAILURE
,JOURNAL-STATE
is set to-
:COMPLETED
if the journal is in state:RECORDING
or:LOGGING
and the transition to:RECORDING
was reflected in storage, -
else it is set to
:FAILED
.
After a
JOURNALING-FAILURE
, any further attempt within the affectedWITH-JOURNALING
to use the critical machinery mentioned above (JOURNALED
,LOGGED
, etc) resignals the same journal failure condition. As a consequence, the record journal cannot be changed, and the only way to recover is to leaveWITH-JOURNALING
. This does not affect processing in other threads, which by design cannot write to the record journal.Note that in contrast with
JOURNALING-FAILURE
andREPLAY-FAILURE
, which necessitate leavingWITH-JOURNALING
to recover from, the other conditions –JOURNAL-ERROR
, andSTREAMLET-ERROR
– are subclasses ofERROR
as the their handling need not be so heavy-handed. -
- [reader] JOURNALING-FAILURE-EMBEDDED-CONDITION JOURNALING-FAILURE (:EMBEDDED-CONDITION)
-
[condition] RECORD-UNEXPECTED-OUTCOME
Signalled (with
SIGNAL
: this is not anERROR
) byJOURNALED
when aVERSIONED-EVENT
or anEXTERNAL-EVENT
had an UNEXPECTED-OUTCOME while inJOURNAL-STATE
:RECORDING
. Upon signalling this condition,JOURNAL-STATE
is set to:LOGGING
, thus no more events can be recorded that will affect replay of the journal being recorded. The event that triggered this condition is recorded in state:LOGGING
, with its version downgraded. Since Replay (except invoked) is built on the assumption that control flow is deterministic, an unexpected outcome is significant because it makes this assumption to hold unlikely.Also see
REPLAY-UNEXPECTED-OUTCOME
.
-
[condition] DATA-EVENT-LOSSAGE JOURNALING-FAILURE
Signalled when a data event is about to be recorded in
JOURNAL-STATE
:MISMATCHED
or:LOGGING
. Since the data event will not be replayed that constitutes data loss.
-
[condition] JOURNAL-ERROR ERROR
Signalled by
WITH-JOURNALING
,WITH-BUNDLE
and by:LOG-RECORD
. It is also signalled by the low-level streamlet interface (see Streamlets reference).
-
[condition] END-OF-JOURNAL JOURNAL-ERROR
This might be signalled by the replay mechanism if
WITH-JOURNALING
'sREPLAY-EOJ-ERROR-P
is true. UnlikeREPLAY-FAILURE
s, this does not affectJOURNAL-STATE
ofRECORD-JOURNAL
. At a lower level, it is signalled byREAD-EVENT
upon reading past the end of theJOURNAL
ifEOJ-ERROR-P
.
Before we get into the details, here is a self-contained example that demonstrates typical use.
(defvar *communication-log* nil)
(defvar *logic-log* nil)
(defvar *logic-log-level* 0)
(defun call-with-connection (port fn)
(framed (call-with-connection :log-record *communication-log*
:args `(,port))
(funcall fn)))
(defun fetch-data (key)
(let ((value 42))
(logged ((and (<= 1 *logic-log-level*) *logic-log*))
"The value of ~S is ~S." key value)
value))
(defun init-logging (&key (logic-log-level 1))
(let* ((stream (open "/tmp/xxx.log"
:direction :output
:if-does-not-exist :create
:if-exists :append))
(journal (make-pprint-journal
:stream (make-broadcast-stream
(make-synonym-stream '*standard-output*)
stream))))
(setq *communication-log* journal)
(setq *logic-log* journal)
(setq *logic-log-level* logic-log-level)))
(init-logging)
(call-with-connection 8080 (lambda () (fetch-data :foo)))
..
.. (CALL-WITH-CONNECTION 8080)
.. The value of :FOO is 42.
.. => 42
=> 42
(setq *logic-log-level* 0)
(call-with-connection 8080 (lambda () (fetch-data :foo)))
..
.. (CALL-WITH-CONNECTION 8080)
.. => 42
=> 42
(ignore-errors
(call-with-connection 8080 (lambda () (error "Something unexpected."))))
..
.. (CALL-WITH-CONNECTION 8080)
.. =E "SIMPLE-ERROR" "Something unexpected."
Imagine a utility library called glib.
(defvar *glib-log* nil)
(defvar *patience* 1)
(defun sl33p (seconds)
(logged (*glib-log*) "Sleeping for ~As." seconds)
(sleep (* *patience* seconds)))
Glib follows the recommendation to have a special variable globally
bound to NIL
by default. The value of *GLIB-LOG*
is the journal to
which glib log messages will be routed. Since it's NIL
, the log
messages are muffled, and to record any log message, we need to
change its value.
Let's send the logs to a PPRINT-JOURNAL
:
(setq *glib-log* (make-pprint-journal
:log-decorator (make-log-decorator :time t)))
(sl33p 0.01)
..
.. 2020-08-31T12:45:23.827172+02:00: Sleeping for 0.01s.
That's a bit too wordy. For this tutorial, let's stick to less verbose output:
(setq *glib-log* (make-pprint-journal))
(sl33p 0.01)
..
.. Sleeping for 0.01s.
To log to a file:
(setq *glib-log* (make-pprint-journal
:stream (open "/tmp/glib.log"
:direction :output
:if-does-not-exist :create
:if-exists :append)))
Capturing logs in WITH-JOURNALING
's RECORD-JOURNAL
If we were recording a journal for replay and wanted to include glib logs in the journal, we would do something like this:
(with-journaling (:record t)
(let ((*glib-log* :record))
(sl33p 0.01)
(journaled (non-glib-stuff :version 1)))
(list-events))
=> ((:LEAF "Sleeping for 0.01s.")
(:IN NON-GLIB-STUFF :VERSION 1)
(:OUT NON-GLIB-STUFF :VERSION 1 :VALUES (NIL)))
We could even (SETQ *GLIB-LOG* :RECORD)
to make it so that glib
messages are included by default in the RECORD-JOURNAL
. In this
example, the special *GLIB-LOG*
acts like a log category for all
the log messages of the glib library (currently one).
Next, we route *GLIB-LOG*
to wherever *APP-LOG*
is pointing by
binding *GLIB-LOG*
to the symbol *APP-LOG*
(see :LOG-RECORD
).
(defvar *app-log* nil)
(let ((*glib-log* '*app-log*))
(setq *app-log* nil)
(logged (*glib-log*) "This is not written anywhere.")
(setq *app-log* (make-pprint-journal :pretty nil))
(sl33p 0.01))
..
.. (:LEAF "Sleeping for 0.01s.")
Note how pretty-printing was turned off, and we see the LEAF-EVENT
generated by LOGGED
in its raw plist form.
Finally, to make routing decisions conditional we need to change
SL33P
:
(defvar *glib-log-level* 1)
(defun sl33p (seconds)
(logged ((and (<= 2 *glib-log-level*) *glib-log*))
"Sleeping for ~As." (* *patience* seconds))
(sleep seconds))
;;; Check that it all works:
(let ((*glib-log-level* 1)
(*glib-log* (make-pprint-journal)))
(format t "~%With log-level ~A" *glib-log-level*)
(sl33p 0.01)
(setq *glib-log-level* 2)
(format t "~%With log-level ~A" *glib-log-level*)
(sl33p 0.01))
..
.. With log-level 1
.. With log-level 2
.. Sleeping for 0.01s.
LOGGED
is for single messages. JOURNALED
, or in this example FRAMED
,
can provide nested context:
(defun callv (var value symbol &rest args)
"Call SYMBOL-FUNCTION of SYMBOL with VAR dynamically bound to VALUE."
(framed ("glib:callv" :log-record *glib-log*
:args `(,var ,value ,symbol ,@args))
(progv (list var) (list value)
(apply (symbol-function symbol) args))))
(callv '*print-base* 2 'print 10)
..
.. ("glib:callv" *PRINT-BASE* 2 PRINT 10)
.. 1010
.. => 10
=> 10
(let ((*glib-log-level* 2))
(callv '*patience* 7 'sl33p 0.01))
..
.. ("glib:callv" *PATIENCE* 7 SL33P 0.01)
.. Sleeping for 0.07s.
.. => NIL
Customizing the output format is possible if we don't necessarily expect to be able to read the logs back programmatically. There is an example in Tracing, which is built on Pretty-printing journals.
Here, we discuss how to make logs more informative.
-
[glossary-term] decoration
JOURNAL-LOG-DECORATOR
adds additional data toLOG-EVENT
s as they are written to the journal. This data is called decoration, and it is to capture the context in which the event was triggered. SeeMAKE-LOG-DECORATOR
for a typical example. Decorations, since they can be onLOG-EVENT
s only, do not affect Replay. Decorations are most often used with Pretty-printing.
-
[accessor] JOURNAL-LOG-DECORATOR JOURNAL (:LOG-DECORATOR = NIL)
If non-
NIL
, this is a function to add decoration toLOG-EVENT
s before they are written to a journal. The only allowed transformation is to append a plist to the event, which is a plist itself. The keys can be anything.
-
[function] MAKE-LOG-DECORATOR &KEY TIME REAL-TIME RUN-TIME THREAD DEPTH OUT-NAME
Return a function suitable as
JOURNAL-LOG-DECORATOR
that may add a string timestamp, the internal real-time or run-time (both in seconds), the name of the thread, to events, which will be handled byPRETTIFY-EVENT
. IfDEPTH
, thenPRETTIFY-EVENT
will the nesting level of the event being printed. IfOUT-NAME
, thePRETTIFY-EVENT
will print the name of Out-events.All arguments are boolean-valued symbols.
(funcall (make-log-decorator :depth t :out-name t :thread t :time t :real-time t :run-time t) (make-leaf-event :foo)) => (:LEAF :FOO :DEPTH T :OUT-NAME T :THREAD "worker" :TIME "2023-05-26T12:27:44.172614+01:00" :REAL-TIME 2531.3254 :RUN-TIME 28.972797)
WITH-JOURNALING
and WITH-BUNDLE
control replaying and recording
within their dynamic extent, which is rather a necessity because
Replay needs to read the events in the same order as the JOURNALED
blocks are being executed. However, LOG-EVENT
s do not affect
replay, so we can allow more flexibility in routing them.
The LOG-RECORD
argument of JOURNALED
and LOGGED
controls where
LOG-EVENT
s are written both within WITH-JOURNALING
and without. The
algorithm to determine the target journal is this:
-
If
LOG-RECORD
is:RECORD
, then theRECORD-JOURNAL
is returned. -
If
LOG-RECORD
isNIL
, then it is returned. -
If
LOG-RECORD
is aJOURNAL
, then it is returned. -
If
LOG-RECORD
is a symbol (other thanNIL
), then theSYMBOL-VALUE
of that symbol is assigned toLOG-RECORD
, and we go to step 1.
If the return value is NIL
, then the event will not be written
anywhere, else it is written to the journal returned.
This is reminiscent of SYNONYM-STREAM
s, also in that it is possible
end up in cycles in the resolution. For this reason, the algorithm
stop with a JOURNAL-ERROR
after 100 iterations.
Events may be written to LOG-RECORD
even without an enclosing
WITH-JOURNALING
, and it does not affect the JOURNAL-STATE
. However,
it is a JOURNAL-ERROR
to write to a :COMPLETED
journal (see
JOURNAL-STATE
).
When multiple threads log to the same journal, it is guaranteed that individual events are written atomically, but frames from different threads do not necessarily nest. To keep the log informative, the name of thread may be added to the events as decoration.
Also, see notes on thread Safety.
-
[macro] LOGGED (&OPTIONAL (LOG-RECORD :RECORD)) FORMAT-CONTROL &REST FORMAT-ARGS
LOGGED
creates a singleLEAF-EVENT
, whose name is the string constructed byFORMAT
. For example:(with-journaling (:record t) (logged () "Hello, ~A." "world") (list-events)) => ((:LEAF "Hello, world."))
LEAF-EVENT
s areLOG-EVENT
s with no separate in- and out-events. They have anEVENT-NAME
and no other properties. UseLOGGED
for point-in-time textual log messages, andJOURNALED
withVERSION
NIL
(i.e.FRAMED
) to provide context.Also, see
:LOG-RECORD
.
JTRACE
behaves similarly to CL:TRACE
but deals with
non-local exits gracefully.
(defun foo (x)
(sleep 0.12)
(1+ x))
(defun bar (x)
(foo (+ x 2))
(error "xxx"))
(jtrace foo bar)
(ignore-errors (bar 1))
..
.. 0: (BAR 1)
.. 1: (FOO 3)
.. 1: FOO => 4
.. 0: BAR =E "SIMPLE-ERROR" "xxx"
It can also include the name of the originating thread and timestamps in the output:
(let ((*trace-thread* t)
(*trace-time* t)
(*trace-depth* nil)
(*trace-out-name* nil))
(ignore-errors (bar 1)))
..
.. 2020-09-02T19:58:19.415204+02:00 worker: (BAR 1)
.. 2020-09-02T19:58:19.415547+02:00 worker: (FOO 3)
.. 2020-09-02T19:58:19.535766+02:00 worker: => 4
.. 2020-09-02T19:58:19.535908+02:00 worker: =E "SIMPLE-ERROR" "xxx"
(let ((*trace-real-time* t)
(*trace-run-time* t)
(*trace-depth* nil)
(*trace-out-name* nil))
(ignore-errors (bar 1)))
..
.. #16735.736 !68.368: (BAR 1)
.. #16735.736 !68.369: (FOO 3)
.. #16735.857 !68.369: => 4
.. #16735.857 !68.369: =E "SIMPLE-ERROR" "xxx"
If these options are insufficient, the content and the format of the trace can be customized:
(let ((*trace-journal*
(make-pprint-journal :pretty '*trace-pretty*
:prettifier (lambda (event depth stream)
(format stream "~%Depth: ~A, event: ~S"
depth event))
:stream (make-synonym-stream '*error-output*)
:log-decorator (lambda (event)
(append event '(:custom 7))))))
(ignore-errors (bar 1)))
..
.. Depth: 0, event: (:IN BAR :ARGS (1) :CUSTOM 7)
.. Depth: 1, event: (:IN FOO :ARGS (3) :CUSTOM 7)
.. Depth: 1, event: (:OUT FOO :VALUES (4) :CUSTOM 7)
.. Depth: 0, event: (:OUT BAR :ERROR ("SIMPLE-ERROR" "xxx") :CUSTOM 7)
In the above, *TRACE-JOURNAL*
was bound locally to keep the example
from wrecking the global default, but the same effect could be
achieved by SETF
ing PPRINT-JOURNAL-PRETTIFIER
,
PPRINT-JOURNAL-STREAM
and JOURNAL-LOG-DECORATOR
.
-
[macro] JTRACE &REST NAMES
Like
CL:TRACE
,JTRACE
takes a list of symbols. When functions denoted by thoseNAMES
are invoked, their names, arguments and outcomes are printed in human readable form to*TRACE-OUTPUT*
. These values may not be readable,JTRACE
does not care.The format of the output is the same as that of
PPRINT-EVENTS
. Behind the scenes,JTRACE
encapsulates the global functions withNAMES
in wrapper that behaves as ifFOO
in the example above was defined like this:(defun foo (x) (framed (foo :args `(,x) :log-record *trace-journal*) (1+ x)))
If
JTRACE
is invoked with no arguments, it returns the list of symbols currently traced.On Lisps other than SBCL, where a function encapsulation facility is not available or it is not used by Journal,
JTRACE
simply setsSYMBOL-FUNCTION
. This solution loses the tracing encapsulation when the function is recompiled. On these platforms,(JTRACE)
also retraces all functions that should be traced but aren't.The main advantage of
JTRACE
overCL:TRACE
is the ability to trace errors, not just normal return values. As it is built onJOURNALED
, it can also detect – somewhat heuristically –THROW
s and similar.
-
[macro] JUNTRACE &REST NAMES
Like
CL:UNTRACE
,JUNTRACE
makes it so that the global functions denoted by the symbolsNAMES
are no longer traced byJTRACE
. When invoked with no arguments, it untraces all traced functions.
-
[variable] *TRACE-PRETTY* T
If
*TRACE-PRETTY*
is true, thenJTRACE
produces output likePPRINT-EVENTS
, else it's likePRINT-EVENTS
.
-
[variable] *TRACE-DEPTH* T
Controls whether to decorate the trace with the depth of event. See
MAKE-LOG-DECORATOR
.
-
[variable] *TRACE-OUT-NAME* T
Controls whether trace should print the
EVENT-NAME
of Out-events, which is redundant with theEVENT-NAME
of the corresponding In-events. SeeMAKE-LOG-DECORATOR
.
-
[variable] *TRACE-THREAD* NIL
Controls whether to decorate the trace with the name of the originating thread. See
MAKE-LOG-DECORATOR
.
-
[variable] *TRACE-TIME* NIL
Controls whether to decorate the trace with a timestamp. See
MAKE-LOG-DECORATOR
.
-
[variable] *TRACE-REAL-TIME* NIL
Controls whether to decorate the trace with the internal real-time. See
MAKE-LOG-DECORATOR
.
-
[variable] *TRACE-RUN-TIME* NIL
Controls whether to decorate the trace with the internal run-time. See
MAKE-LOG-DECORATOR
.
-
[variable] *TRACE-JOURNAL* #<PPRINT-JOURNAL :NEW 1>
The
JOURNAL
whereJTRACE
writesLOG-EVENT
s. By default, it is aPPRINT-JOURNAL
that sets up aSYNONYM-STREAM
to*TRACE-OUTPUT*
and sends its output there. It pays attention to*TRACE-PRETTY*
, and its log decorator is affected by*TRACE-TIME*
and*TRACE-THREAD*
. However, by changingJOURNAL-LOG-DECORATOR
andPPRINT-JOURNAL-PRETTIFIER
, content and output can be customized.
Slime, by default,
binds C-c C-t
to toggling CL:TRACE
. To integrate JTRACE
into
Slime, load src/mgl-jrn.el
into Emacs.
-
If you installed Journal with Quicklisp, the location of
mgl-jrn.el
may change with updates, and you may want to copy the current version to a stable location:(journal:install-journal-elisp "~/quicklisp/")
Then, assuming the Elisp file is in the quicklisp directory, add
this to your .emacs
:
(load "~/quicklisp/mgl-jrn.el")
Since JTRACE
lacks some features of CL:TRACE
, most notably that of
tracing non-global functions, it is assigned a separate binding,
C-c C-j
.
-
[function] INSTALL-JOURNAL-ELISP TARGET-DIR
Copy
mgl-jrn.el
distributed with this package toTARGET-DIR
.
During replay, code is executed normally with special rules for
blocks. There are two modes for dealing with blocks: replaying the
code and replaying the outcome. When code is replayed, upon entering
and leaving a block, the events generated are matched to events read
from the journal being replayed. If the events don't match,
REPLAY-FAILURE
is signalled, which marks the record journal as having
failed the replay. This is intended to make sure that the state of
the program during the replay matches the state at the time of
recording. In the other mode, when the outcome is replayed, a block
may not be executed at all, but its recorded outcome is
reproduced (i.e. the recorded return values are simply returned).
Replay can be only be initiated with WITH-JOURNALING
(or its close
kin WITH-BUNDLE
). After the per-event processing described below,
when WITH-JOURNALING
finishes, it might signal REPLAY-INCOMPLETE
if
there are unprocessed non-log events left in the replay journal.
Replay is deemed successful or failed depending on whether all
events are replayed from the replay journal without a
REPLAY-FAILURE
. A journal that records events from a successful
replay can be used in place of the journal that was replayed, and so
on. The logic of replacing journals with their successful replays is
automated by Bundles. WITH-JOURNALING
does not allow replay from
journals that were failed replays themselves. The mechanism, in
terms of which tracking success and failure of replays is
implemented, revolves around JOURNAL-STATE
and
EVENT-VERSION
s, which we discuss next.
-
[type] JOURNAL-STATE
JOURNAL
's state with respect to replay is updated duringWITH-JOURNALING
. The possible states are:-
:NEW
: This journal was just created but never recorded to. -
:REPLAYING
: Replaying events has started, some events may have been replayed successfully, but there are more non-log events to replay. -
:MISMATCHED
: There was aREPLAY-FAILURE
. In this state,VERSIONED-EVENT
s generated are downgraded toLOG-EVENT
s,EXTERNAL-EVENT
s and invoked triggerDATA-EVENT-LOSSAGE
. -
:RECORDING
: All events from the replay journal were successfully replayed, and now new events are being recorded without being matched to the replay journal. -
:LOGGING
: There was aRECORD-UNEXPECTED-OUTCOME
. In this state,VERSIONED-EVENT
s generated are downgraded toLOG-EVENT
s,EXTERNAL-EVENT
s and invoked triggerDATA-EVENT-LOSSAGE
. -
:FAILED
: The journal is to be discarded. It encountered aJOURNALING-FAILURE
or aREPLAY-FAILURE
without completing the replay and reaching:RECORDING
. -
:COMPLETED
: All events were successfully replayed andWITH-JOURNALING
finished or aJOURNALING-FAILURE
occurred while:RECORDING
or:LOGGING
.
The state transitions are:
:NEW -> :REPLAYING (on entering WITH-JOURNALING) :REPLAYING -> :MISMATCHED (on REPLAY-FAILURE) :REPLAYING -> :FAILED (on REPLAY-INCOMPLETE) :REPLAYING -> :FAILED (on JOURNALING-FAILURE) :REPLAYING -> :RECORDING (on successfully replaying all events) :MISMATCHED -> :FAILED (on leaving WITH-JOURNALING) :RECORDING -> :LOGGING (on RECORD-UNEXPECTED-OUTCOME) :RECORDING/:LOGGING -> :COMPLETED (on leaving WITH-JOURNALING) :RECORDING/:LOGGING -> :COMPLETED (on JOURNALING-FAILURE)
:NEW
is the starting state. It is aJOURNAL-ERROR
to attempt to write to journals in:COMPLETED
. Note that once in:RECORDING
, the only possible terminal state is:COMPLETED
. -
The following arguments of JOURNALED
control behaviour under replay.
-
VERSION
: seeEVENT-VERSION
below. -
INSERTABLE
controls whetherVERSIONED-EVENT
s andEXTERNAL-EVENT
s may be replayed with the insert replay strategy (see The replay strategy). Does not affectLOG-EVENT
s, which are always _insert_ed. Note that insertingEXTERNAL-EVENT
s while:REPLAYING
is often not meaningful (e.g. asking the user for input may lead to aREPLAY-FAILURE
). SeePEEK-REPLAY-EVENT
for an example on how to properly insert these kinds ofEXTERNAL-EVENT
s. -
REPLAY-VALUES
, a function orNIL
, may be called withEVENT-OUTCOME
when replaying and:VERSION
:INFINITY
.NIL
is equivalent toVALUES-LIST
. SeeVALUES<-
for an example. -
REPLAY-CONDITION
, a function orNIL
, may be called withEVENT-OUTCOME
(the return value of the function provided as:CONDITION
) when replaying and:VERSION
is:INFINITY
.NIL
is equivalent to theERROR
function. Replaying conditions is cumbersome and best avoided.
-
[variable] *FORCE-INSERTABLE* NIL
The default value of the
INSERTABLE
argument ofJOURNALED
forVERSIONED-EVENT
s. Binding this toT
allows en-masse structural upgrades in combination withWITH-REPLAY-FILTER
. Does not affectEXTERNAL-EVENT
s. See Upgrades and replay.
-
[type] EVENT-VERSION
An event's version is either
NIL
, a positiveFIXNUM
, or:INFINITY
, which correspond toLOG-EVENT
s,VERSIONED-EVENT
s, andEXTERNAL-EVENT
s, respectively, and have an increasingly strict behaviour with regards to Replay. AllEVENT
s have versions. The versions of the in- and out-events belonging to the same frame are the same.
-
[type] LOG-EVENT
Events with
EVENT-VERSION
NIL
called log events. During Replay, they are never matched to events from the replay journal, and log events in the replay do not affect events being recorded either. These properties allow log events to be recorded in arbitrary journals withJOURNALED
'sLOG-RECORD
argument. The convenience macroFRAMED
is creating frames of log-events, while theLOGGED
generates a log-event that's aLEAF-EVENT
.
-
[type] VERSIONED-EVENT
Events with a positive integer
EVENT-VERSION
are called versioned events. In Replay, they undergo consistency checks unlikeLOG-EVENT
s, but the rules for them are less strict than forEXTERNAL-EVENT
s. In particular, higher versions are always considered compatible with lower versions, they become an upgrade in terms of the The replay strategy, and versioned events can be inserted into the record without a corresponding replay event withJOURNALED
'sINSERTABLE
.If a
VERSIONED-EVENT
has an unexpected outcome,RECORD-UNEXPECTED-OUTCOME
is signalled.
-
[type] EXTERNAL-EVENT
Events with
EVENT-VERSION
:INFINITY
are called external events. They are likeVERSIONED-EVENT
s whose version was bumped all the way to infinity, which rules out easy, non-matching upgrades. Also, they are never inserted to the record without a matching replay event (see The replay strategy).In return for these restrictions, external events can be replayed without running the corresponding block (see Replaying the outcome). This allows their out-event variety, called data events, to be non-deterministic. Data events play a crucial role in Persistence.
If an
EXTERNAL-EVENT
has an unexpected outcome,RECORD-UNEXPECTED-OUTCOME
is signalled.
Built on top of JOURNALED
, the macros below record a pair of
In-events and Out-events but differ in how they are replayed and
the requirements on their blocks. The following table names the
type of EVENT
produced (Event
), how In-events are
replayed (In-e.
), whether the block is always run (Run
), how
Out-events are replayed (Out-e.
), whether the block must be
deterministic (Det
) or side-effect free (SEF
).
| | Event | In-e. | Run | Out-e. | Det | SEF |
|----------+-----------+--------+-----+--------+-----+-----|
| FRAMED | log | skip | y | skip | n | n |
| CHECKED | versioned | match | y | match | y | n |
| REPLAYED | external | match | n | replay | n | y |
| INVOKED | versioned | replay | y | match | y | n |
Note that the replay-replay combination is not implemented because there is nowhere to return values from replay-triggered functions.
-
[macro] FRAMED (NAME &KEY LOG-RECORD ARGS VALUES CONDITION) &BODY BODY
A wrapper around
JOURNALED
to produce frames ofLOG-EVENT
s. That is,VERSION
is alwaysNIL
, and some irrelevant arguments are omitted. The relatedLOGGED
creates a singleLEAF-EVENT
.With
FRAMED
,BODY
is always run and noREPLAY-FAILURE
s are triggered.BODY
is not required to be deterministic, and it may have side-effects.
-
[macro] CHECKED (NAME &KEY (VERSION 1) ARGS VALUES CONDITION INSERTABLE) &BODY BODY
A wrapper around
JOURNALED
to produce frames ofVERSIONED-EVENT
s.VERSION
defaults to 1.CHECKED
is for ensuring that supposedly deterministic processing does not veer off the replay.With
CHECKED
,BODY
– which must be deterministic – is always run andREPLAY-FAILURE
s are triggered when the events generated do not match the events in the replay journal.BODY
may have side-effects.For further discussion of determinism, see
REPLAYED
.
-
[macro] REPLAYED (NAME &KEY ARGS VALUES CONDITION INSERTABLE REPLAY-VALUES REPLAY-CONDITION) &BODY BODY
A wrapper around
JOURNALED
to produce frames ofEXTERNAL-EVENT
s.VERSION
is:INFINITY
.REPLAYED
is for primarily for marking and isolating non-deterministic processing.With
REPLAYED
, theIN-EVENT
is checked for consistency with the replay (as withCHECKED
), butBODY
is not run (assuming it has a recorded expected outcome), and the outcome in theOUT-EVENT
is reproduced (see Replaying the outcome). For this scheme to work,REPLAYED
requires itsBODY
to be side-effect free, but it may be non-deterministic.
-
[glossary-term] invoked
Invoked refers to functions and blocks defined by
DEFINE-INVOKED
orFLET-INVOKED
. Invoked frames may be recorded in response to asynchronous events, and at replay the presence of its in-event triggers the execution of the function associated with the name of the event.On the one hand,
FRAMED
,CHECKED
,REPLAYED
or plainJOURNALED
have In-events that are always predictable from the code and the preceding events. The control flow – on the level of recorded frames – is deterministic in this sense. On the other hand, Invoked encodes in itsIN-EVENT
what function to call next, introducing non-deterministic control flow.By letting events choose the code to run, Invoked resembles typical event sourcing frameworks. When Invoked is used exclusively, the journal becomes a sequence of events. In contrast,
JOURNALED
and its wrappers put code first, and the journal will be a projection of the call tree.
-
[macro] DEFINE-INVOKED FUNCTION-NAME ARGS (NAME &KEY (VERSION 1) INSERTABLE) &BODY BODY
DEFINE-INVOKED
is intended for recording asynchronous function invocations like event or signal handlers. It defines a function that recordsVERSIONED-EVENT
s withARGS
set to the actual arguments. At replay, it is invoked whenever the recordedIN-EVENT
becomes the replay event.DEFUN
andCHECKED
rolled into one,DEFINE-INVOKED
defines a top-level function withFUNCTION-NAME
andARGS
(only simple positional arguments are allowed) and wrapsCHECKED
withNAME
, the sameARGS
andINSERTABLE
aroundBODY
. Whenever anIN-EVENT
becomes the replay event, and it has aDEFINE-INVOKED
defined with the name of the event,FUNCTION-NAME
is invoked withEVENT-ARGS
.While
BODY
's return values are recorded as usual, the defined function returns no values to make it less likely to affect control flow in a way that's not possible to reproduce when the function is called by the replay mechanism.(defvar *state*) (define-invoked foo (x) ("foo") (setq *state* (1+ x))) (define-invoked bar (x) ("bar") (setq *state* (+ 2 x))) (if (zerop (random 2)) (foo 0) (bar 1))
The above can be alternatively implemented with
REPLAYED
explicitly encapsulating the non-determinism:(let ((x (replayed (choose) (random 2)))) (if (zerop x) (checked (foo :args `(,x)) (setq *state* (1+ x))) (checked (bar :args `(,x)) (setq *state* (+ 2 x)))))
-
[macro] FLET-INVOKED DEFINITIONS &BODY BODY
Like
DEFINE-INVOKED
, but withFLET
instead ofDEFUN
. The event name and the function are associated in the dynamic extent ofBODY
.WITH-JOURNALING
does not change the bindings. The example inDEFINE-INVOKED
can be rewritten as:(let ((state nil)) (flet-invoked ((foo (x) ("foo") (setq state (1+ x))) (bar (x) ("bar") (setq state (+ 2 x)))) (if (zerop (random 2)) (foo 0) (bar 1))))
Consider replaying the same code repeatedly, hoping to make progress in the processing. Maybe based on the availability of external input, the code may error out. After each run, one has to decide whether to keep the journal just recorded or stick with the replay journal. A typical solution to this would look like this:
(let ((record nil))
(loop
(setq record (make-in-memory-journal))
(with-journaling (:record record :replay replay)
...)
(when (and
;; RECORD is a valid replay of REPLAY ...
(eq (journal-state record) :completed)
;; ... and is also significantly different from it ...
(journal-diverged-p record))
;; so use it for future replays.
(setq replay record))))
This is pretty much what bundles automate. The above becomes:
(let ((bundle (make-in-memory-bundle)))
(loop
(with-bundle (bundle)
...)))
With FILE-JOURNAL
s, the motivating example above would be even more
complicated, but FILE-BUNDLE
s work the same way as
IN-MEMORY-BUNDLE
s.
-
[macro] WITH-BUNDLE (BUNDLE) &BODY BODY
This is like
WITH-JOURNALING
where theREPLAY-JOURNAL
is the last successfully completed one inBUNDLE
, and theRECORD-JOURNAL
is a new one created inBUNDLE
. WhenWITH-BUNDLE
finishes, the record journal is inJOURNAL-STATE
:FAILED
or:COMPLETED
.To avoid accumulating useless data, the new record is immediately deleted when
WITH-BUNDLE
finishes if it has not diverged from the replay journal (seeJOURNAL-DIVERGENT-P
). Because:FAILED
journals are always divergent in this sense, they are deleted instead based on whether there is already a previous failed journal in the bundle and the new record is identical to that journal (seeIDENTICAL-JOURNALS-P
).It is a
JOURNAL-ERROR
to have concurrent or nestedWITH-BUNDLE
s on the same bundle.
The replay process for both In-events and Out-events starts by
determining how the generated event (the new event from now on)
shall be replayed. Roughly, the decision is based on the NAME
and
VERSION
of the new event and the replay event (the next event to be
read from the replay). There are four possible strategies:
-
match: A new in-event must match the replay event in its
ARGS
. See Matching in-events for details. A new out-event must match the replay event'sEXIT
andOUTCOME
, see Matching out-events. -
upgrade: The new event is not matched to any replay event, but an event is consumed from the replay journal. This happens if the next new event has the same name as the replay event, but its version is higher.
-
insert: The new event is not matched to any replay event, and no events are consumed from the replay journal, which may be empty. This is always the case for new
LOG-EVENT
s and when there are no more events to read from the replay journal (unlessREPLAY-EOJ-ERROR-P
). ForVERSIONED-EVENT
s, it is affected by settingJOURNALED
'sINSERTABLE
to true (see Journaled for replay).The out-event's strategy is always insert if the strategy for the corresponding in-event was insert.
-
Also,
END-OF-JOURNAL
,REPLAY-NAME-MISMATCH
andREPLAY-VERSION-DOWNGRADE
may be signalled. See the algorithm below details.
The strategy is determined by the following algorithm, invoked whenever an event is generated by a journaled block:
-
Log events are not matched to the replay. If the new event is a log event or a
REPLAY-FAILURE
has been signalled before (i.e. the record journal'sJOURNAL-STATE
is:MISMATCHED
), then insert is returned. -
Else, log events to be read in the replay journal are skipped, and the next unread, non-log event is peeked at (without advancing the replay journal).
-
end of replay: If there are no replay events left, then:
-
If
REPLAY-EOJ-ERROR-P
isNIL
inWITH-JOURNALING
(the default), insert is returned. -
If
REPLAY-EOJ-ERROR-P
is true, thenEND-OF-JOURNAL
is signalled.
-
-
mismatched name: Else, if the next unread replay event's name is not
EQUAL
to the name of the new event, then:-
For
VERSIONED-EVENT
s,REPLAY-NAME-MISMATCH
is signalled ifINSERTABLE
isNIL
, else insert is returned. -
For
EXTERNAL-EVENT
s,REPLAY-NAME-MISMATCH
is signalled.
-
-
matching name: Else, if the name of the next unread event in the replay journal is
EQUAL
to the name of new event, then it is chosen as the replay event.-
If the replay event's version is higher than the new event's version, then
REPLAY-VERSION-DOWNGRADE
is signalled. -
If the two versions are equal, then match is returned.
-
If the new event's version is higher, then upgrade is returned.
Where
:INFINITY
is considered higher than any integer and equal to itself. -
-
In summary:
| new event | end-of-replay | mismatched name | matching name |
|-----------+-------------------+-------------------+---------------|
| Log | insert | insert | insert |
| Versioned | insert/eoj-error | insert/name-error | match-version |
| External | insert/eoj-error | insert/name-error | match-version |
Version matching (match-version
above) is based on which event has
a higher version:
| replay event | = | new event |
|-----------------+-------+-----------|
| downgrade-error | match | upgrade |
-
[glossary-term] replay event
The replay event is the next event to be read from
REPLAY-JOURNAL
which is not to be skipped. There may be no replay event if there are no more unread events in the replay journal.An event in the replay journal is skipped if it is a
LOG-EVENT
or there is aWITH-REPLAY-FILTER
with a matching:SKIP
. If:SKIP
is in effect, the replay event may be indeterminate.Events from the replay journal are read when they are
:MATCH
ed or:UPGRADE
d (see The replay strategy), when nested events are echoed while Replaying the outcome, or when there is an invoked defined with the same name as the replay event.The replay event is available via
PEEK-REPLAY-EVENT
.
If the replay strategy is match, then, for in-events, the matching process continues like this:
-
If the
EVENT-ARGS
are notEQUAL
, thenREPLAY-ARGS-MISMATCH
signalled. -
At this point, two things might happen:
-
For
VERSIONED-EVENT
s, the block will be executed as normal and its outcome will be matched to the replay event (see Matching out-events). -
For
EXTERNAL-EVENT
s, the corresponding replayOUT-EVENT
is looked at. If there is one, meaning that the frame finished with an expected outcome, then its outcome will be replayed (see Replaying the outcome). If theOUT-EVENT
is missing, thenEXTERNAL-EVENT
s behave likeVERSIONED-EVENT
s, and the block is executed.
-
So, if an in-event is triggered that matches the replay,
EVENT-VERSION
(0
1
) is :INFINITY
, then normal execution is altered in the
following manner:
-
The journaled block is not executed.
-
To keep execution and the replay journal in sync, events of frames nested in the current one are skipped over in the replay journal.
-
All events (including
LOG-EVENT
s) skipped over are echoed to the record journal. This serves to keep a trail of what happened during the original recording. Note that functions corresponding to invoked frames are called when theirIN-EVENT
is skipped over. -
The out-event corresponding to the in-event being processed is then read from the replay journal and is recorded again (to allow recording to function properly).
To be able to reproduce the outcome in the replay journal, some
assistance may be required from REPLAY-VALUES
and REPLAY-CONDITION
:
-
If the replay event has a normal return (i.e.
EVENT-EXIT
(0
1
):VALUES
), then the recorded return values (inEVENT-OUTCOME
) are returned immediately as in(VALUES-LIST (EVENT-OUTCOME REPLAY-EVENT))
. IfREPLAY-VALUES
is specified, it is called instead ofVALUES-LIST
. See Working with unreadable values for an example. -
Similarly, if the replay event has unwound with an expected condition (has
EVENT-EXIT
:CONDITION
), then the recorded condition (inEVENT-OUTCOME
) is signalled as IN(ERROR (EVENT-OUTCOME REPLAY-EVENT))
. IfREPLAY-CONDITION
is specified, it is called instead ofERROR
(0
1
).REPLAY-CONDITION
must not return normally, and it's aJOURNAL-ERROR
if it does.
WITH-REPLAY-FILTER
's NO-REPLAY-OUTCOME
can selectively turn off
replaying the outcome. See Testing on multiple levels, for an
example.
If there were no Replay failures during the matching of the
IN-EVENT
, and the conditions for Replaying the outcome were not
met, then the block is executed. When the outcome of the block is
determined, an OUT-EVENT
is triggered and is matched to the replay
journal. The matching of out-events starts out as in
The replay strategy with checks for EVENT-NAME
and
EVENT-VERSION
.
If the replay strategy is insert or upgrade, then the out-event
is written to RECORD-JOURNAL
, consuming an event with a matching
name from the REPLAY-JOURNAL
in the latter case. If the strategy is
match, then:
-
If the new event has an unexpected outcome, then
REPLAY-UNEXPECTED-OUTCOME
is signalled. Note that the replay event always has an expected outcome due to the handling ofRECORD-UNEXPECTED-OUTCOME
. -
If the new event has an expected outcome, then unless the new and replay event's
EVENT-EXIT
(0
1
)s areEQ
and theirEVENT-OUTCOME
s areEQUAL
,REPLAY-OUTCOME-MISMATCH
is signalled. -
Else, the replay event is consumed and the new event is written the
RECORD-JOURNAL
.
Note that The replay strategy for the in-event and the out-event of
the same frame may differ if the corresponding out-event is not
present in REPLAY-JOURNAL
, which may be the case when the recording
process failed hard without unwinding properly, or when an
unexpected outcome triggered the transition to JOURNAL-STATE
:LOGGING
.
-
[condition] REPLAY-FAILURE SERIOUS-CONDITION
A abstract superclass (never itself signalled) for all kinds of mismatches between the events produced and the replay journal. Signalled only in
JOURNAL-STATE
:REPLAYING
and only once perWITH-JOURNALING
. If aREPLAY-FAILURE
is signalled for anEVENT
, then the event will be recorded, butRECORD-JOURNAL
will transition toJOURNAL-STATE
:MISMATCHED
. LikeJOURNALING-FAILURE
, this is a serious condition because it is to be handled outside the enclosingWITH-JOURNALING
. If aREPLAY-FAILURE
were to be handled inside theWITH-JOURNALING
, keep in mind that in:MISMATCHED
, replay always uses the insert replay strategy (see The replay strategy).
- [reader] REPLAY-FAILURE-NEW-EVENT REPLAY-FAILURE (:NEW-EVENT)
- [reader] REPLAY-FAILURE-REPLAY-EVENT REPLAY-FAILURE (:REPLAY-EVENT)
- [reader] REPLAY-FAILURE-REPLAY-JOURNAL REPLAY-FAILURE (= '(REPLAY-JOURNAL))
-
[condition] REPLAY-NAME-MISMATCH REPLAY-FAILURE
Signalled when the new event's and replay event's
EVENT-NAME
are notEQUAL
. TheREPLAY-FORCE-INSERT
,REPLAY-FORCE-UPGRADE
restarts are provided.
-
[condition] REPLAY-VERSION-DOWNGRADE REPLAY-FAILURE
Signalled when the new event and the replay event have the same
EVENT-NAME
, but the new event has a lower version. TheREPLAY-FORCE-UPGRADE
restart is provided.
-
[condition] REPLAY-ARGS-MISMATCH REPLAY-FAILURE
Signalled when the new event's and replay event's
EVENT-ARGS
are notEQUAL
. TheREPLAY-FORCE-UPGRADE
restart is provided.
-
[condition] REPLAY-OUTCOME-MISMATCH REPLAY-FAILURE
Signalled when the new event's and replay event's
EVENT-EXIT
(0
1
) and/orEVENT-OUTCOME
are notEQUAL
. TheREPLAY-FORCE-UPGRADE
restart is provided.
-
[condition] REPLAY-UNEXPECTED-OUTCOME REPLAY-FAILURE
Signalled when the new event has an unexpected outcome. Note that the replay event always has an expected outcome due to the logic of
RECORD-UNEXPECTED-OUTCOME
. No restarts are provided.
-
[condition] REPLAY-INCOMPLETE REPLAY-FAILURE
Signalled if there are unprocessed non-log events in
REPLAY-JOURNAL
whenWITH-JOURNALING
finishes and the body ofWITH-JOURNALING
returned normally, which is to prevent this condition to cancel an ongoing unwinding. No restarts are provided.
-
[restart] REPLAY-FORCE-INSERT
This restart forces The replay strategy to be
:INSERT
, overridingREPLAY-NAME-MISMATCH
. This is intended for upgrades, and extreme care must be taken not to lose data.
-
[restart] REPLAY-FORCE-UPGRADE
This restart forces The replay strategy to be
:UPGRADE
, overridingREPLAY-NAME-MISMATCH
,REPLAY-VERSION-DOWNGRADE
,REPLAY-ARGS-MISMATCH
,REPLAY-OUTCOME-MISMATCH
. This is intended for upgrades, and extreme care must be taken not to lose data.
The replay mechanism is built on the assumption that the tree of
frames is the same when the code is replayed as it was when the
replay journal was originally recorded. Thus, non-deterministic
control flow poses a challenge, but non-determinism can be isolated
with EXTERNAL-EVENT
s. However, when the code changes, we might find
the structure of frames in previous recordings hard to accommodate.
In this case, we might decide to alter the structure, giving up some
of the safety provided by the replay mechanism. There are various
tools at our disposal to control this tradeoff between safety and
flexibility:
-
We can insert individual frames with
JOURNALED
'sINSERTABLE
, upgrade frames by bumpingJOURNALED
'sVERSION
, and filter frames withWITH-REPLAY-FILTER
. This option allows for the most consistency checks. -
The
REPLAY-FORCE-UPGRADE
andREPLAY-FORCE-INSERT
restarts allow overriding The replay strategy, but their use requires great care to be taken. -
Or we may decide to keep the bare minimum of the replay journal around and discard everything except for
EXTERNAL-EVENT
s. This option is equivalent to(let ((*force-insertable* t)) (with-replay-filter (:skip '((:name nil))) 42))
-
Rerecording the journal without replay might be another option if there are no
EXTERNAL-EVENT
s to worry about. -
Finally, we can rewrite the replay journal using the low-level interface (see Streamlets reference). In this case, extreme care must be taken not to corrupt the journal (and lose data) as there are no consistency checks to save us.
With that, let's see how WITH-REPLAY-FILTER
works.
-
[macro] WITH-REPLAY-STREAMLET (VAR) &BODY BODY
Open
REPLAY-JOURNAL
for reading withWITH-OPEN-JOURNAL
, set theREAD-POSITION
on it to the event next read by the Replay mechanism (which is never aLOG-EVENT
). The low-level Reading from streamlets api is then available to inspect the contents of the replay. It is an error ifREPLAY-JOURNAL
isNIL
.
-
[function] PEEK-REPLAY-EVENT
Return the replay event to be read from
REPLAY-JOURNAL
. This is roughly equivalent to(when (replay-journal) (with-replay-streamlet (streamlet) (peek-event streamlet))
except
PEEK-REPLAY-EVENT
takes into accountWITH-REPLAY-FILTER
:MAP
, and it may return(:INDETERMINATE)
ifWITH-REPLAY-FILTER
:SKIP
is in effect and what events are to be skipped cannot be decided until the next in-event generated by the code.Imagine a business process for paying an invoice. In the first version of this process, we just pay the invoice:
(replayed (pay))
We have left the implementation of PAY blank. In the second version, we need to get an approval first:
(when (replayed (get-approval) (= (random 2) 0)) (replayed (pay)))
Replaying a journal produced by the first version of the code with the second version would run into difficulties because inserting
EXTERNAL-EVENT
s is tricky.We have to first decide how to handle the lack of approval in the first version. Here, we just assume the processes started by the first version get approval automatically. The implementation is based on a dummy
PROCESS
block whose version is bumped when the payment process changes and is inspected at the start of journaling.When v1 is replayed with v2, we introduce an
INSERTABLE
, versionedGET-APPROVAL
block that just returnsT
. When replaying the code again, still with v2, theGET-APPROVAL
block will be upgraded to:INFINITY
.(let ((bundle (make-in-memory-bundle))) ;; First version of the payment process. Just pay. (with-bundle (bundle) (checked (process :version 1)) (replayed (pay))) ;; Second version of the payment process. Only pay if approved. (loop repeat 2 do (with-bundle (bundle) (let ((replay-process-event (peek-replay-event))) (checked (process :version 2)) (when (if (and replay-process-event (< (event-version replay-process-event) 2)) ;; This will be upgraded to :INFINITY the second ;; time around the LOOP. (checked (get-approval :insertable t) t) (replayed (get-approval) (= (random 2) 0))) (replayed (pay)))))))
-
[macro] WITH-REPLAY-FILTER (&KEY MAP SKIP NO-REPLAY-OUTCOME) &BODY BODY
WITH-REPLAY-FILTER
performs journal upgrade during replay by allowing events to be transformed as they are read from the replay journal or skipped if they match some patterns. For how to add new blocks in a code upgrade, seeJOURNALED
's:INSERTABLE
argument. In addition, it also allows some control over Replaying the outcome.-
MAP
: A function called with an event read from the replay journal which returns a transformed event. See Events reference.MAP
takes effect before beforeSKIP
. -
SKIP
: In addition to filtering outLOG-EVENT
s (which always happens during replay), filter out all events that belong to frames that match any of itsSKIP
patterns. Filtered out events are never seen byJOURNALED
as it replays events.SKIP
patterns are of the format(&KEY NAME VERSION<)
, whereVERSION<
is a validEVENT-VERSION
, andNAME
may beNIL
, which acts as a wildcard.SKIP
is for whenJOURNALED
blocks are removed from the code, which would render replaying previously recorded journals impossible. Note that, for reasons of safety, it is not possible to filterEXTERNAL-EVENT
s. -
NO-REPLAY-OUTCOME
is a list ofEVENT-NAME
s. Replaying the outcome is prevented for frames withEQUAL
names. See Testing on multiple levels for an example.
WITH-REPLAY-FILTER
affects only the immediately enclosingWITH-JOURNALING
. AWITH-REPLAY-FILTER
nested within another in the sameWITH-JOURNALING
inherits theSKIP
patterns of its parent, to which it adds its own. TheMAP
function is applied to before the parent'sMAP
.Examples of
SKIP
patterns:;; Match events with name FOO and version 1, 2, 3 or 4 (:name foo :version< 5) ;; Match events with name BAR and any version (:name bar :version< :infinity) ;; Same as the previous (:name bar) ;; Match all names (:name nil) ;; Same as the previous ()
Skipping can be thought of as removing nodes of the tree of frames, connecting its children to its parent. The following example removes frames
J1
andJ2
from aroundJ3
, theJ1
frame from withinJ3
, and the thirdJ1
frame.(let ((journal (make-in-memory-journal))) ;; Record trees J1 -> J2 -> J3 -> J1, and J1. (with-journaling (:record journal) (checked (j1) (checked (j2) (checked (j3) (checked (j1) 42)))) (checked (j1) 7)) ;; Filter out all occurrences of VERSIONED-EVENTs named J1 and ;; J2 from the replay, leaving only J3 to match. (with-journaling (:replay journal :record t :replay-eoj-error-p t) (with-replay-filter (:skip '((:name j1) (:name j2))) (checked (j3) 42))))
-
Having discussed the Replay mechanism, next are Testing and Persistence, which rely heavily on replay. Suppose we want to unit test user registration. Unfortunately, the code communicates with a database service and also takes input from the user. A natural solution is to create mock objects for these external systems to unshackle the test from the cumbersome database dependency and to allow it to run without user interaction.
We do this below by wrapping external interaction in JOURNALED
with
:VERSION
:INFINITY
(see Replaying the outcome).
(defparameter *db* (make-hash-table))
(defun set-key (key value)
(replayed ("set-key" :args `(,key ,value))
(format t "Updating db~%")
(setf (gethash key *db*) value)
nil))
(defun get-key (key)
(replayed ("get-key" :args `(,key))
(format t "Query db~%")
(gethash key *db*)))
(defun ask-username ()
(replayed ("ask-username")
(format t "Please type your username: ")
(read-line)))
(defun maybe-win-the-grand-prize ()
(checked ("maybe-win-the-grand-prize")
(when (= 1000000 (hash-table-count *db*))
(format t "You are the lucky one!"))))
(defun register-user (username)
(unless (get-key username)
(set-key username `(:user-object :username ,username))
(maybe-win-the-grand-prize)))
Now, we write a test that records these interactions in a file when it's run for the first time.
(define-file-bundle-test (test-user-registration
:directory (asdf:system-relative-pathname
:journal "test/registration/"))
(let ((username (ask-username)))
(register-user username)
(assert (get-key username))
(register-user username)
(assert (get-key username))))
;; Original recording: everything is executed
JRN> (test-user-registration)
Please type your username: joe
Query db
Updating db
Query db
Query db
Query db
=> NIL
On reruns, none of the external stuff is executed. The return values
of the external JOURNALED
blocks are replayed from the journal:
;; Replay: all external interactions are mocked.
JRN> (test-user-registration)
=> NIL
Should the code change, we might want to upgrade carefully (see Upgrades and replay) or just rerecord from scratch:
JRN> (test-user-registration :rerecord t)
Please type your username: joe
Query db
Updating db
Query db
Query db
Query db
=> NIL
Thus satisfied that our test runs, we can commit the journal file in the bundle into version control. Its contents are:
(:IN "ask-username" :VERSION :INFINITY)
(:OUT "ask-username" :VERSION :INFINITY :VALUES ("joe" NIL))
(:IN "get-key" :VERSION :INFINITY :ARGS ("joe"))
(:OUT "get-key" :VERSION :INFINITY :VALUES (NIL NIL))
(:IN "set-key" :VERSION :INFINITY :ARGS ("joe" (:USER-OBJECT :USERNAME "joe")))
(:OUT "set-key" :VERSION :INFINITY :VALUES (NIL))
(:IN "maybe-win-the-grand-prize" :VERSION 1)
(:OUT "maybe-win-the-grand-prize" :VERSION 1 :VALUES (NIL))
(:IN "get-key" :VERSION :INFINITY :ARGS ("joe"))
(:OUT "get-key" :VERSION :INFINITY :VALUES ((:USER-OBJECT :USERNAME "joe") T))
(:IN "get-key" :VERSION :INFINITY :ARGS ("joe"))
(:OUT "get-key" :VERSION :INFINITY :VALUES ((:USER-OBJECT :USERNAME "joe") T))
(:IN "get-key" :VERSION :INFINITY :ARGS ("joe"))
(:OUT "get-key" :VERSION :INFINITY :VALUES ((:USER-OBJECT :USERNAME "joe") T))
Note that when this journal is replayed, new VERSIONED-EVENT
s are
required to match the replay. So, after the original recording, we
can check by eyeballing that the record represents a correct
execution. Then on subsequent replays, even though
MAYBE-WIN-THE-GRAND-PRIZE
sits behind REGISTER-USER
and is hard
to test with ASSERT
s, the replay mechanism verifies that it is
called only for new users.
This record-and-replay style of testing is not the only possibility: direct inspection of a journal with the low-level events api (see Events reference) can facilitate checking non-local invariants.
-
[macro] DEFINE-FILE-BUNDLE-TEST (NAME &KEY DIRECTORY (EQUIVALENTP T)) &BODY BODY
Define a function with
NAME
for record-and-replay testing. The function'sBODY
is executed in aWITH-BUNDLE
to guarantee replayability. The bundle in question is aFILE-BUNDLE
created inDIRECTORY
. The function has a single keyword argument,RERECORD
. IfRERECORD
is true, the bundle is deleted withDELETE-FILE-BUNDLE
to start afresh.Furthermore, if
BODY
returns normally, and it is a replay of a previous run, andEQUIVALENTP
, then it isASSERT
ed that the record and replay journals areEQUIVALENT-REPLAY-JOURNALS-P
. If this check fails,RECORD-JOURNAL
is discarded when the function returns. In addition to the replay consistency, this checks that no inserts or upgrades were performed (see The replay strategy).
Nesting REPLAYED
s (that is, frames of EXTERNAL-EVENT
s) is not
obviously useful since the outer REPLAYED
will be replayed by
outcome, and the inner one will be just echoed to the record
journal. However, if we turn off Replaying the outcome for the
outer, the inner will be replayed.
This is useful for testing layered communication. For example, we
might have written code that takes input from an external
system (READ-LINE
) and does some complicated
processing (READ-FROM-STRING
) before returning the input in a form
suitable for further processing. Suppose we wrap REPLAYED
around
READ-FROM-STRING
for Persistence because putting it around
READ-LINE
would expose low-level protocol details in the journal,
making protocol changes difficult.
However, upon realizing that READ-FROM-STRING
was not the best tool
for the job and switching to PARSE-INTEGER
, we want to test by
replaying all previously recorded journals. For this, we prevent the
outer REPLAYED
from being replayed by outcome with
WITH-REPLAY-FILTER
:
(let ((bundle (make-in-memory-bundle)))
;; Original with READ-FROM-STRING
(with-bundle (bundle)
(replayed ("accept-number")
(values (read-from-string (replayed ("input-number")
(read-line))))))
;; Switch to PARSE-INTEGER and test by replay.
(with-bundle (bundle)
(with-replay-filter (:no-replay-outcome '("accept-number"))
(replayed ("accept-number")
;; 1+ is our bug.
(values (1+ (parse-integer (replayed ("input-number")
(read-line)))))))))
The inner input-number
block is replayed by outcome, and
PARSE-INTEGER
is called with the string READ-LINE
returned in the
original invocation. The outcome of the outer accept-number
block
checked as if it was a VERSIONED-EVENT
and we get a
REPLAY-OUTCOME-MISMATCH
due to the bug.
Let's write a simple game.
(defun play-guess-my-number ()
(let ((my-number (replayed (think-of-a-number)
(random 10))))
(format t "~%I thought of a number.~%")
(loop for i upfrom 0 do
(write-line "Guess my number:")
(let ((guess (replayed (read-guess)
(values (parse-integer (read-line))))))
(format t "You guessed ~D.~%" guess)
(when (= guess my-number)
(checked (game-won :args `(,(1+ i))))
(format t "You guessed it in ~D tries!" (1+ i))
(return))))))
(defparameter *the-evergreen-game* (make-in-memory-bundle))
Unfortunately, the implementation is lacking in the input validation
department. In the transcript below, PARSE-INTEGER
fails with junk in string
when the user enters not a number
:
CL-USER> (handler-case
(with-bundle (*the-evergreen-game*)
(play-guess-my-number))
(error (e)
(format t "Oops. ~A~%" e)))
I thought of a number.
Guess my number:
7 ; real user input
You guessed 7.
Guess my number:
not a number ; real user input
Oops. junk in string "not a number"
Instead of fixing this bug, we just restart the game from the
beginning, Replaying the outcome of external interactions marked
with REPLAYED
:
CL-USER> (with-bundle (*the-evergreen-game*)
(play-guess-my-number))
I thought of a number.
Guess my number:
You guessed 7.
Guess my number: ; New recording starts here
5 ; real user input
You guessed 5.
Guess my number:
4 ; real user input
You guessed 4.
Guess my number:
2 ; real user input
You guessed 2.
You guessed it in 4 tries!
We can now replay this game many times without any user interaction:
CL-USER> (with-bundle (*the-evergreen-game*)
(play-guess-my-number))
I thought of a number.
Guess my number:
You guessed 7.
Guess my number:
You guessed 5.
Guess my number:
You guessed 4.
Guess my number:
You guessed 2.
You guessed it in 4 tries!
This simple mechanism allows us to isolate external interactions and write tests in record-and-replay style based on the events produced:
CL-USER> (list-events *the-evergreen-game*)
((:IN THINK-OF-A-NUMBER :VERSION :INFINITY)
(:OUT THINK-OF-A-NUMBER :VERSION :INFINITY :VALUES (2))
(:IN READ-GUESS :VERSION :INFINITY)
(:OUT READ-GUESS :VERSION :INFINITY :VALUES (7))
(:IN READ-GUESS :VERSION :INFINITY :ARGS NIL)
(:OUT READ-GUESS :VERSION :INFINITY :VALUES (5))
(:IN READ-GUESS :VERSION :INFINITY :ARGS NIL)
(:OUT READ-GUESS :VERSION :INFINITY :VALUES (4))
(:IN READ-GUESS :VERSION :INFINITY :ARGS NIL)
(:OUT READ-GUESS :VERSION :INFINITY :VALUES (2))
(:IN GAME-WON :VERSION 1 :ARGS (4))
(:OUT GAME-WON :VERSION 1 :VALUES (NIL)))
In fact, being able to replay this game at all already checks it
through the GAME-WON
event that the number of tries calculation is
correct.
In addition, thus being able to reconstruct the internal state of
the program gives us persistence by replay. If instead of a
IN-MEMORY-BUNDLE
, we used a FILE-BUNDLE
, the game would have been
saved on disk without having to write any code for saving and
loading the game state.
Persistence by replay, also known as event sourcing, is appropriate when the external interactions are well-defined and stable. Storing events shines in comparison to persisting state when the control flow is too complicated to be interrupted and resumed easily. Resuming execution in deeply nested function calls is fraught with such peril that it is often easier to flatten the program into a state machine, which is as pleasant as manually managing continuations.
In contrast, the Journal library does not favour certain styles of
control flow and only requires that non-determinism is packaged up
in REPLAYED
, which allows it to reconstruct the state of the program
from the recorded events at any point during its execution and
resume from there.
In the following, we explore how journals can serve as a
persistence mechanism and the guarantees they offer. The high-level
summary is that journals with SYNC
can serve as a durable and
consistent storage medium. The other two
ACID properties, atomicity and
isolation, do not apply because Journal is single-client and does
not need transactions.
-
[glossary-term] aborted execution
Aborted execution is when the operating system or the application crashes, calls
abort()
, is killed by aSIGKILL
signal or there is a power outage. Synchronization guarantees are defined in the face of aborted execution and do not apply to hardware errors, Lisp or OS bugs.
-
[glossary-term] data event
Data events are the only events that may be non-deterministic. They record information that could change if the same code were run multiple times. Data events typically correspond to interactions with the user, servers or even the random number generator. Due to their non-determinism, they are the only parts of the journal not reproducible by rerunning the code. In this sense, only the data events are not redundant with the code, and whether other events are persisted does not affect durability. There are two kinds of data events:
-
An
EXTERNAL-EVENT
that is also anOUT-EVENT
. -
The
IN-EVENT
of an invoked function, which lies outside the normal, deterministic control flow.
-
When a journal or bundle is created (see MAKE-IN-MEMORY-JOURNAL
,
MAKE-FILE-JOURNAL
, MAKE-IN-MEMORY-BUNDLE
, MAKE-FILE-BUNDLE
), the
SYNC
option determines when – as a RECORD-JOURNAL
– the recorded
events and JOURNAL-STATE
changes are persisted durably. For
FILE-JOURNAL
s, persisting means calling something like fsync
,
while for IN-MEMORY-JOURNAL
s, a user defined function is called to
persist the data.
-
NIL
: Never synchronize. AFILE-JOURNAL
's file may be corrupted on aborted execution. InIN-MEMORY-JOURNAL
s,SYNC-FN
is never called. -
T
: This is the no data loss setting with minimal synchronization. It guarantees consistency (i.e. no corruption) and durability up to the most recent data event written inJOURNAL-STATE
:RECORDING
or for the entire record journal in states:FAILED
and:COMPLETED
.:FAILED
or:COMPLETED
is guaranteed when leavingWITH-JOURNALING
at the latest. -
Values other than
NIL
andT
are reserved for future extensions. Using them triggers aJOURNAL-ERROR
.
Unlike FILE-JOURNAL
s, IN-MEMORY-JOURNAL
s do not have any built-in
persistent storage backing them, but with SYNC-FN
, persistence can
be tacked on. If non-NIL
, SYNC-FN
must be a function of a single
argument, an IN-MEMORY-JOURNAL
. SYNC-FN
is called according to
Synchronization strategies, and upon normal return the journal must
be stored durably.
The following example saves the entire journal history when a new
data event is recorded. Note how SYNC-TO-DB
is careful to
overwrite *DB*
only if it is called with a journal that has not
failed the replay (as in Replay failures) and is sufficiently
different from the replay journal as determined by
JOURNAL-DIVERGENT-P
.
(defparameter *db* ())
(defun sync-to-db (journal)
(when (and (member (journal-state journal)
'(:recording :logging :completed))
(journal-divergent-p journal))
(setq *db* (journal-events journal))
(format t "Saved ~S~%New events from position ~S~%" *db*
(journal-previous-sync-position journal))))
(defun make-db-backed-record-journal ()
(make-in-memory-journal :sync-fn 'sync-to-db))
(defun make-db-backed-replay-journal ()
(make-in-memory-journal :events *db*))
(with-journaling (:record (make-db-backed-record-journal)
:replay (make-db-backed-replay-journal))
(replayed (a)
2)
(ignore-errors
(replayed (b)
(error "Whoops"))))
.. Saved #((:IN A :VERSION :INFINITY)
.. (:OUT A :VERSION :INFINITY :VALUES (2)))
.. New events from position 0
.. Saved #((:IN A :VERSION :INFINITY)
.. (:OUT A :VERSION :INFINITY :VALUES (2))
.. (:IN B :VERSION :INFINITY)
.. (:OUT B :ERROR ("SIMPLE-ERROR" "Whoops")))
.. New events from position 2
..
In a real application, external events often involve unreliable or
high-latency communication. In the above example, block B
signals
an error, say, to simulate some kind of network condition. Now, a
new journal for replay is created and initialized with the saved
events, and the whole process is restarted.
(defun run-with-db ()
(with-journaling (:record (make-db-backed-record-journal)
:replay (make-db-backed-replay-journal))
(replayed (a)
(format t "A~%")
2)
(replayed (b)
(format t "B~%")
3)))
(run-with-db)
.. B
.. Saved #((:IN A :VERSION :INFINITY)
.. (:OUT A :VERSION :INFINITY :VALUES (2))
.. (:IN B :VERSION :INFINITY)
.. (:OUT B :VERSION :INFINITY :VALUES (3)))
.. New events from position 0
..
=> 3
Note that on the rerun, block A
is not executed because external
events are replayed simply by reproducing their outcome, in this
case returning 2. See Replaying the outcome. Block B
, on the
other hand, was rerun because it had an unexpected outcome the
first time around. This time it ran without error, a data event was
triggered, and SYNC-FN
was invoked.
If we were to invoke the now completed RUN-WITH-DB
again, it would
simply return 3 without ever invoking SYNC-FN
:
(run-with-db)
=> 3
With JOURNAL-REPLAY-MISMATCH
, SYNC-FN
can be optimized to to reuse
the sequence of events in the replay journal up until the point of
divergence.
For FILE-JOURNAL
s, SYNC
determines when the events written to the
RECORD-JOURNAL
and its JOURNAL-STATE
will be persisted durably in
the file. Syncing to the file involves two calls to fsync
and is
not cheap.
Syncing events to files is implemented as follows.
-
When the journal file is created, its parent directory is immediately fsynced to make sure that the file will not be lost on aborted execution.
-
When an event is about to be written the first time after file creation or after a sync, a transaction start marker is written to the file.
-
Any number of events may be subsequently written until syncing is deemed necessary (see Synchronization strategies).
-
At this point,
fsync
is called to flush all event data and state changes to the file, and the transaction start marker is overwritten with a transaction completed marker and anotherfsync
is performed. -
When reading back this file (e.g. for replay), an open transaction marker is treated as the end of file.
Note that this implementation assumes that after writing the start
transaction marker, a crash cannot leave any kind of garbage bytes
around: it must leave zeros. This is not true for all filesytems.
For example, ext3/ext4 with data=writeback
can leave garbage
around.
Changes to journals come in two varieties: adding an event and
changing the JOURNAL-STATE
. Both are performed by JOURNALED
only
unless the low-level streamlet interface is used (see
Streamlets reference). Using JOURNALED
wrapped in a
WITH-JOURNALING
, WITH-BUNDLE
, or :LOG-RECORD
without WITH-JOURNALING
is thread-safe.
-
Every journal is guaranteed to have at most a single writer active at any time. Writers are mainly
WITH-JOURNALING
andWITH-BUNDLE
, but any journals directly logged to have a log writer stored in the journal object. See Logging. -
WITH-JOURNALING
andWITH-BUNDLE
have dynamic extent as writers, but log writers of journals have indefinite extent: once a journal is used as aLOG-RECORD
, there remains a writer. -
Attempting to create a second writer triggers a
JOURNAL-ERROR
. -
Writing to the same journal via
:LOG-RECORD
from multiple threads concurrently is possible since this doesn't create multiple writers. It is ensured with locking that events are written atomically. Frames can be interleaved, but these areLOG-EVENT
s, so this does not affect replay. -
The juggling of replay and record journals performed by
WITH-BUNDLE
is also thread-safe. -
It is ensured that there is at most one
FILE-JOURNAL
object in the same Lisp image is backed by the same file. -
Similarly, there is at most
FILE-BUNDLE
object for a directory.
Currently, there is no protection against multiple OS processes
writing the same FILE-JOURNAL
or FILE-BUNDLE
.
Journal is designed to be async-unwind safe but not reentrant.
Interrupts are disabled only for the most critical cleanup forms. If
a thread is killed without unwinding, that constitutes
aborted execution, so guarantees about Synchronization to storage apply, but
JOURNAL
objects written by the thread are not safe to access, and
the Lisp should probably be restarted.
Events are normally triggered upon entering and leaving the
dynamic extent of a JOURNALED
block (see In-events and
Out-events) and also by LOGGED
. Apart from being part of the
low-level substrate of the Journal library, working with events
directly is sometimes useful when writing tests that inspect
recorded events. Otherwise, skip this entire section.
All EVENT
s have EVENT-NAME
and EVENT-VERSION
(0
1
), which feature
prominently in The replay strategy. After the examples in
In-events and Out-events, the following example is a reminder of
how events look in the simplest case.
(with-journaling (:record t)
(journaled (foo :version 1 :args '(1 2))
(+ 1 2))
(logged () "Oops")
(list-events))
=> ((:IN FOO :VERSION 1 :ARGS (1 2))
(:OUT FOO :VERSION 1 :VALUES (3))
(:LEAF "Oops"))
So, a JOURNALED
block generates an IN-EVENT
and an OUT-EVENT
, which
are simple property lists. The following reference lists these
properties, their semantics and the functions to read them.
-
[type] EVENT
An event is either an
IN-EVENT
, anOUT-EVENT
or aLEAF-EVENT
.
-
[function] EVENT= EVENT-1 EVENT-2
Return whether
EVENT-1
andEVENT-2
represent the same event. In- and out-events belonging to the same frame are not the same event.EVENT-OUTCOME
s are not compared whenEVENT-EXIT
(0
1
) is:ERROR
to avoid undue dependence on implementation specific string representations. This function is useful in conjunction withMAKE-IN-EVENT
andMAKE-OUT-EVENT
to write tests.
-
[function] EVENT-NAME EVENT
The name of an event can be of any type. It is often a symbol or a string. When replaying, names are compared with
EQUAL
. AllEVENT
s have names. The names of the in- and out-events belonging to the same frame are the same.
-
[function] EVENT-VERSION EVENT
Return the version of
EVENT
of typeEVENT-VERSION
.
-
[function] LOG-EVENT-P EVENT
See if
EVENT
is aLOG-EVENT
.
-
[function] VERSIONED-EVENT-P EVENT
See if
EVENT
is aVERSIONED-EVENT
.
-
[function] EXTERNAL-EVENT-P EVENT
See if
EVENT
is anEXTERNAL-EVENT
.
-
[type] IN-EVENT
IN-EVENT
s are triggered upon entering the dynamic extent of aJOURNALED
block.IN-EVENT
s haveEVENT-NAME
,EVENT-VERSION
, andEVENT-ARGS
. See In-events for a more introductory treatment.
-
[function] IN-EVENT-P EVENT
See if
EVENT
is aIN-EVENT
.
-
[function] MAKE-IN-EVENT &KEY NAME VERSION ARGS
Create an
IN-EVENT
withNAME
,VERSION
(of typeEVENT-VERSION
) andARGS
as itsEVENT-NAME
,EVENT-VERSION
andEVENT-ARGS
.
-
[function] EVENT-ARGS IN-EVENT
Return the arguments of
IN-EVENT
, normally populated using theARGS
form inJOURNALED
.
-
[type] OUT-EVENT
OUT-EVENT
s are triggered upon leaving the dynamic extent of theJOURNALED
block.OUT-EVENT
s haveEVENT-NAME
,EVENT-VERSION
,EVENT-EXIT
andEVENT-OUTCOME
. See Out-events for a more introductory treatment.
-
[function] OUT-EVENT-P EVENT
See if
EVENT
is anOUT-EVENT
.
-
[function] MAKE-OUT-EVENT &KEY NAME VERSION EXIT OUTCOME
Create an
OUT-EVENT
withNAME
,VERSION
(of typeEVENT-VERSION
),EXIT
(of typeEVENT-EXIT
), andOUTCOME
as itsEVENT-NAME
,EVENT-VERSION
,EVENT-EXIT
andEVENT-OUTCOME
.
-
[function] EVENT-EXIT OUT-EVENT
Return how the journaled block finished. See
EVENT-EXIT
for the possible types.
-
[function] EXPECTED-OUTCOME-P OUT-EVENT
See if
OUT-EVENT
has an expected outcome.
-
[function] UNEXPECTED-OUTCOME-P OUT-EVENT
See if
OUT-EVENT
has an unexpected outcome.
-
[function] EVENT-OUTCOME OUT-EVENT
Return the outcome of the frame (or loosely speaking of a block) to which
OUT-EVENT
belongs.
-
[type] LEAF-EVENT
Leaf events are triggered by
LOGGED
. UnlikeIN-EVENT
s andOUT-EVENT
s, which represent a frame, leaf events represent a point in execution thus cannot have children. They are also the poorest of their kind: they only have anEVENT-NAME
. TheirVERSION
is alwaysNIL
, which makes themLOG-EVENT
s.
-
[function] LEAF-EVENT-P EVENT
See if
EVENT
is aLEAF-EVENT
.
-
[function] MAKE-LEAF-EVENT NAME
Create a
LEAF-EVENT
withNAME
.
In Basics, we covered the bare minimum needed to work with journals. Here, we go into the details.
-
[class] JOURNAL
JOURNAL
is an abstract base class for a sequence of events. In case ofFILE-JOURNAL
s, the events are stored in a file, while forIN-MEMORY-JOURNAL
s, they are in a Lisp array. When a journal is opened, it is possible to perform I/O on it (see Streamlets reference), which is normally taken care of byWITH-JOURNALING
. For this reason, the user's involvement with journals normally only consists of creating and using them inWITH-JOURNALING
.
-
[reader] JOURNAL-STATE JOURNAL (:STATE)
Return the state of
JOURNAL
, which is of typeJOURNAL-STATE
.
-
[reader] JOURNAL-SYNC JOURNAL (:SYNC = NIL)
The
SYNC
argument specified at instantiation. See Synchronization strategies.
-
[function] SYNC-JOURNAL &OPTIONAL (JOURNAL (RECORD-JOURNAL))
Durably persist changes made to
JOURNAL
ifJOURNAL-SYNC
isT
. The changes that are persisted are-
WRITE-EVENT
s andJOURNAL-STATE
changes made in an enclosingWITH-JOURNALING
; and -
LOG-RECORD
s from any thread.
In particular, writes made in a
WITH-JOURNALING
in another thread are not persisted.SYNC-JOURNAL
is a noop ifJOURNAL-SYNC
isNIL
. It is safe to call from any thread. -
-
[reader] JOURNAL-REPLAY-MISMATCH JOURNAL (= NIL)
If
JOURNAL-DIVERGENT-P
, then this is a list of two elements: theREAD-POSITION
s in theRECORD-JOURNAL
andREPLAY-JOURNAL
of the first events that were different (ignoringLOG-EVENT
s). It isNIL
, otherwise.
-
[function] JOURNAL-DIVERGENT-P JOURNAL
See if
WITH-JOURNALING
recorded any event so far in this journal that was notEQUAL
to its replay event or it had no corresponding replay event. This completely ignoresLOG-EVENT
s in both journals being compared and can be called any time during Replay. It plays a role inWITH-BUNDLE
deciding when a journal is important enough to keep and also in Synchronization with in-memory journals.The position of the first mismatch is available via
JOURNAL-REPLAY-MISMATCH
.
After replay finished (i.e. WITH-JOURNALING
completed), we can ask
whether there were any changes produced. This is answered in the
strictest sense by IDENTICAL-JOURNALS-P
and somewhat more
functionally by EQUIVALENT-REPLAY-JOURNALS-P
.
Also see JOURNAL-DIVERGENT-P
.
-
[generic-function] IDENTICAL-JOURNALS-P JOURNAL-1 JOURNAL-2
Compare two journals in a strict sense: whether they have the same
JOURNAL-STATE
and the lists of their events (as inLIST-EVENTS
) areEQUAL
.
-
[generic-function] EQUIVALENT-REPLAY-JOURNALS-P JOURNAL-1 JOURNAL-2
See if two journals are equivalent when used the for
REPLAY
inWITH-JOURNALING
.EQUIVALENT-REPLAY-JOURNALS-P
is likeIDENTICAL-JOURNALS-P
, but it ignoresLOG-EVENT
s and allows events withEVENT-EXIT
(0
1
):ERROR
to differ in their outcomes, which may very well be implementation specific, anyway. Also, it considers two groups of states as different:NEW
,:REPLAYING
,:MISMATCHED
,:FAILED
vs:RECORDING
,:LOGGING
, COMPLETED.
The rest of section is about concrete subclasses of JOURNAL
.
-
[class] IN-MEMORY-JOURNAL JOURNAL
IN-MEMORY-JOURNAL
s are backed by a non-persistent Lisp array of events. Much quicker thanFILE-JOURNAL
s, they are ideal for smallish journals persisted manually (see Synchronization with in-memory journals for an example).They are also useful for writing tests based on what events were generated. They differ from
FILE-JOURNAL
s in that events written toIN-MEMORY-JOURNAL
s are not serialized (and deserialized on replay) with the following consequences for the objects recorded byJOURNALED
(i.e. itsNAME
,ARGS
arguments, and also the returnVALUES
(0
1
) of the block, or the value returned byCONDITION
):
-
[function] MAKE-IN-MEMORY-JOURNAL &KEY (EVENTS NIL EVENTSP) STATE (SYNC NIL SYNCP) SYNC-FN
Create an
IN-MEMORY-JOURNAL
.The returned journal's
JOURNAL-STATE
will be set toSTATE
. IfSTATE
isNIL
, then it is replaced by a default value, which is:COMPLETED
if theEVENTS
argument is provided, else it is:NEW
.Thus,
(make-in-memory-journal)
creates a journal suitable for recording, and to make a replay journal, use:STATE
:COMPLETED
with some sequence ofEVENTS
:(make-in-memory-journal :events '((:in foo :version 1)) :state :completed)
SYNC
determines whenSYNC-FN
will be invoked on theRECORD-JOURNAL
.SYNC
defaults toT
ifSYNC-FN
, else toNIL
. For a description of possible values, see Synchronization strategies. For more discussion, see Synchronization with in-memory journals.
-
[reader] JOURNAL-EVENTS IN-MEMORY-JOURNAL (:EVENTS)
A sequence of events in the journal. Not to be mutated by client code.
-
[reader] JOURNAL-PREVIOUS-SYNC-POSITION IN-MEMORY-JOURNAL (= 0)
The length of
JOURNAL-EVENTS
at the time of the most recent invocation ofSYNC-FN
.
-
[class] FILE-JOURNAL JOURNAL
A
FILE-JOURNAL
is a journal whose contents andJOURNAL-STATE
are persisted in a file. This is theJOURNAL
subclass with out-of-the-box persistence, but see File bundles for a more full-featured solution for repeated Replays.Since serialization in
FILE-JOURNAL
s is built on top of LispREAD
andWRITE
, everything thatJOURNALED
records in events (i.e. itsNAME
,ARGS
arguments, and also the returnVALUES
(0
1
) of the block, or the value returned byCONDITION
) must be readable.File journals are human-readable and editable by hand with some care. When editing, the following needs to be remembered:
-
The first character of the file represents its
JOURNAL-STATE
. It is a#\Space
(for state:NEW
,:REPLAYING
,:MISMATCHED
and:FAILED
), or a#\Newline
(for state:RECORDING
,:LOGGING
and:COMPLETED
). -
If the journal has
SYNC
(see Synchronization strategies), then between two events, there may be#\Del
(also called#\Rubout
) or#\Ack
characters (CHAR-CODE
127 and 6).#\Del
marks the end of the journal contents that may be read back: it's kind of an uncommitted-transaction marker for the events that follow it.#\Ack
characters, of which there may be many in the file, mark the sequence of events until the next marker of either kind as valid (or committed).#\Ack
characters are ignored when reading the journal.
Thus, when editing a file, don't change the first character and leave the
#\Del
character, if any, where it is. Also see Synchronization with file journals. -
-
[function] MAKE-FILE-JOURNAL PATHNAME &KEY SYNC
Return a
FILE-JOURNAL
backed by the file withPATHNAME
. The file is created when the journal is opened for writing. For a description ofSYNC
, see Synchronization strategies.If there is already an existing
FILE-JOURNAL
backed by the same file, then that object is returned. If the existing object has different options (e.g. it hasSYNC
T
while theSYNC
argument isNIL
here), then aJOURNAL-ERROR
is signalled.If there is already an existing
FILE-JOURNAL
backed by the same file, theJOURNAL-STATE
is not:NEW
, but the file doesn't exist, then the existing object is invalidated: attempts to write will fail withJOURNAL-ERROR
. If the existing journal object is being written, then invalidation fails with aJOURNAL-ERROR
. After invalidation, a newFILE-JOURNAL
object is created.
-
[reader] PATHNAME-OF FILE-JOURNAL (:PATHNAME)
The pathname of the file backing the journal.
-
[class] PPRINT-JOURNAL JOURNAL
Events written to a
PPRINT-JOURNAL
have a customizable output format.PPRINT-JOURNAL
s are intended for producing prettier output for Logging and Tracing, but they do not support reads, so they cannot be used as aREPLAY-JOURNAL
or inLIST-EVENTS
, for example. On the other hand, events written toPPRINT-JOURNAL
s need not be readable.
-
[function] MAKE-PPRINT-JOURNAL &KEY (STREAM (MAKE-SYNONYM-STREAM '*STANDARD-OUTPUT*)) (PRETTY T) (PRETTIFIER 'PRETTIFY-EVENT) LOG-DECORATOR
Creates a
PPRINT-JOURNAL
.
-
[accessor] PPRINT-JOURNAL-STREAM PPRINT-JOURNAL (:STREAM = *STANDARD-OUTPUT*)
The stream where events are dumped. May be set any time to another
STREAM
.
-
[accessor] PPRINT-JOURNAL-PRETTY PPRINT-JOURNAL (:PRETTY = T)
Whether to use
PPRINT-JOURNAL-PRETTIFIER
or write events in as the property lists they are. A boolean-valued symbol.
-
[accessor] PPRINT-JOURNAL-PRETTIFIER PPRINT-JOURNAL (:PRETTIFIER = 'PRETTIFY-EVENT)
A function like
PRETTIFY-EVENT
that writes an event to a stream. Only used whenPPRINT-JOURNAL-PRETTY
, this is the output format customization knob. Also see decorations.
In Bundles, we covered the repeated replay problem that
WITH-BUNDLE
automates. Here, we provide a reference for the bundle
classes.
-
[class] BUNDLE
A
BUNDLE
consists of a sequence of journals which are all reruns of the same code, hopefully making more and more progress towards completion. These journals are Replays of the previous successful one, extending it with new events. Upon replay (seeWITH-BUNDLE
), the latest journal in the bundle inJOURNAL-STATE
:COMPLETED
plays the role of the replay journal, and a new journal is added to the bundle for recording. If the replay succeeds, this new journal eventually becomes:COMPLETED
and takes over the role of the replay journal for future replays until another replay succeeds. When the bundle is created and it has no journals yet, the replay journal is an empty, completed one.This is an abstract base class. Direct subclasses are
IN-MEMORY-BUNDLE
andFILE-BUNDLE
.
-
[accessor] MAX-N-FAILED BUNDLE (:MAX-N-FAILED = 1)
If
MAX-N-FAILED
is non-NIL
, and the number of journals ofJOURNAL-STATE
:FAILED
in the bundle exceeds its value, then some journals (starting with the oldest) are deleted.
-
[accessor] MAX-N-COMPLETED BUNDLE (:MAX-N-COMPLETED = 1)
If
MAX-N-COMPLETED
is non-NIL
, and the number of journals ofJOURNAL-STATE
:COMPLETED
in the bundle exceeds its value, then some journals (starting with the oldest) are deleted.
-
[class] IN-MEMORY-BUNDLE BUNDLE
An
IN-MEMORY-BUNDLE
is aBUNDLE
that is built onIN-MEMORY-JOURNAL
s.IN-MEMORY-BUNDLE
s have limited utility as a persistence mechanism and are provided mainly for reasons of symmetry and for testing. See Synchronization with in-memory journals for an example of how to achieve persistence without bundles.
-
[function] MAKE-IN-MEMORY-BUNDLE &KEY (MAX-N-FAILED 1) (MAX-N-COMPLETED 1) SYNC SYNC-FN
Create a new
IN-MEMORY-BUNDLE
withMAX-N-FAILED
andMAX-N-COMPLETED
.SYNC
andSYNC-FN
are passed on toMAKE-IN-MEMORY-JOURNAL
.
-
[class] FILE-BUNDLE BUNDLE
A
FILE-BUNDLE
is aBUNDLE
that is built onFILE-JOURNAL
s. It provides easy replay-based persistence.
-
[reader] DIRECTORY-OF FILE-BUNDLE (:DIRECTORY)
The directory where the files backing the
FILE-JOURNAL
s in theFILE-BUNDLE
are kept.
-
[function] MAKE-FILE-BUNDLE DIRECTORY &KEY (MAX-N-FAILED 1) (MAX-N-COMPLETED 1) SYNC
Return a
FILE-BUNDLE
object backed byFILE-JOURNAL
s inDIRECTORY
. SeeMAX-N-FAILED
andMAX-N-COMPLETED
. For a description ofSYNC
, see Synchronization strategies.If there is already a
FILE-BUNDLE
with the same directory (according toTRUENAME
), return that object is returned if it has the sameMAX-N-FAILED
,MAX-N-COMPLETED
andSYNC
options, elseJOURNAL-ERROR
is signalled.
-
[function] DELETE-FILE-BUNDLE DIRECTORY
Delete all journal files (
*.jrn
) fromDIRECTORY
. Delete the directory if empty after the journal files were deleted, else signal an error. ExistingFILE-BUNDLE
objects are not updated, soMAKE-FILE-JOURNAL
with FORCE-RELOAD may be required.
This section is relevant mostly for implementing new kinds of
JOURNAL
s in addition to FILE-JOURNAL
s and IN-MEMORY-JOURNAL
s. In
normal operation, STREAMLET
s are not worked with directly.
-
[class] STREAMLET
A
STREAMLET
is a handle to perform I/O on aJOURNAL
. The high-level stuff (WITH-JOURNALING
,JOURNALED
, etc) is built on top of streamlets.
-
[reader] JOURNAL STREAMLET (:JOURNAL)
The
JOURNAL
that was passed toOPEN-STREAMLET
. This is the journalSTREAMLET
operates on.
-
[generic-function] OPEN-STREAMLET JOURNAL &KEY DIRECTION
Return a
STREAMLET
suitable for performing I/O onJOURNAL
.DIRECTION
(defaults to:INPUT
) is one of:INPUT
,:OUTPUT
,:IO
, and it has the same purpose as the similarly named argument ofCL:OPEN
.
-
[generic-function] CLOSE-STREAMLET STREAMLET
Close
STREAMLET
, which was returned byOPEN-STREAMLET
. After closing,STREAMLET
may not longer be used for IO.
-
[generic-function] MAKE-STREAMLET-FINALIZER STREAMLET
Return
NIL
or a function of no arguments suitable as a finalizer forSTREAMLET
. That is, a function that closesSTREAMLET
but holds no reference to it. This is intended for streamlets that are not dynamic-extent, so usingWITH-OPEN-JOURNAL
is not appropriate.
-
[generic-function] OPEN-STREAMLET-P STREAMLET
Return true if
STREAMLET
is open.STREAMLET
s are open until they have been explicitly closed withCLOSE-STREAMLET
.
-
[function] INPUT-STREAMLET-P STREAMLET
See if
STREAMLET
was opened for input (theDIRECTION
argument ofOPEN-STREAMLET
was:INPUT
or:IO
).
-
[function] OUTPUT-STREAMLET-P STREAMLET
See if
STREAMLET
was opened for input (theDIRECTION
argument ofOPEN-STREAMLET
was:OUTPUT
or:IO
).
-
[macro] WITH-OPEN-JOURNAL (VAR JOURNAL &KEY (DIRECTION :INPUT)) &BODY BODY
This is like
WITH-OPEN-FILE
but forJOURNAL
s. Open the journal designated byJOURNAL
(seeTO-JOURNAL
) withOPEN-STREAMLET
, passingDIRECTION
along, and bindVAR
to the resultingSTREAMLET
. CallCLOSE-STREAMLET
afterBODY
finishes. IfJOURNAL
isNIL
, thenVAR
is bound toNIL
and no streamlet is created.
-
[condition] STREAMLET-ERROR ERROR
Like
CL:STREAM-ERROR:
failures pertaining to I/O on a closedSTREAMLET
or of the wrongDIRECTION
. Actual I/O errors are not encapsulated inSTREAMLET-ERROR
.
-
[generic-function] READ-EVENT STREAMLET &OPTIONAL EOJ-ERROR-P
Read the event at the current read position from
STREAMLET
, and move the read position to the event after. If there are no more events, signalEND-OF-JOURNAL
or returnNIL
depending onEOJ-ERROR-P
. SignalsSTREAMLET-ERROR
ifSTREAMLET
is notINPUT-STREAMLET-P
or notOPEN-STREAMLET-P
.
-
[generic-function] READ-POSITION STREAMLET
Return an integer that identifies the position of the next event to be read from
STREAMLET
.SETF
able, seeSET-READ-POSITION
.
-
[generic-function] SET-READ-POSITION STREAMLET POSITION
Set the read position of
STREAMLET
toPOSITION
, which must have been acquired fromREAD-POSITION
.
-
[macro] SAVE-EXCURSION (STREAMLET) &BODY BODY
Save
READ-POSITION
ofSTREAMLET
, executeBODY
, and make sure to restore the saved read position.
-
[generic-function] PEEK-EVENT STREAMLET
Read the next event from
STREAMLET
without changing the read position, or returnNIL
if there is no event to be read.
-
[method] PEEK-EVENT (STREAMLET STREAMLET)
This is a slow default implementation, which relies on
SAVE-EXCURSION
andREAD-EVENT
.
-
[generic-function] WRITE-EVENT EVENT STREAMLET
Write
EVENT
toSTREAMLET
. Writing always happens at the end ofSTREAMLET
's journal regardless of theREAD-POSITION
, and the read position is not changed. SignalsSTREAMLET-ERROR
ifSTREAMLET
is notOUTPUT-STREAMLET-P
or notOPEN-STREAMLET-P
.
-
[method] WRITE-EVENT EVENT (JOURNAL JOURNAL)
For convenience, it is possible to write directly to a
JOURNAL
, in which case the journal's internal output streamlet is used. This internal streamlet is opened for:OUTPUT
and may be used by:LOG-RECORD
.
-
[generic-function] WRITE-POSITION STREAMLET
Return an integer that identifies the position of the next event to be written to
STREAMLET
.
-
[generic-function] REQUEST-COMPLETED-ON-ABORT STREAMLET
Make it so that upon aborted execution,
STREAMLET
'sJOURNAL
will be inJOURNAL-STATE
:COMPLETED
when loaded fresh (e.g. when creating aFILE-JOURNAL
with an existing file). Any previously written events must be persisted before making this change. BeforeREQUEST-COMPLETED-ON-ABORT
is called, a journal must be reloaded in state:FAILED
.It is permissible to defer carrying out this request until the next
SYNC-STREAMLET
call. If the request was carried out, return true. If it was deferred, returnNIL
.
-
[generic-function] SYNC-STREAMLET STREAMLET
Durably persist the effects of all preceding
WRITE-EVENT
calls made viaSTREAMLET
to its journal and any deferredREQUEST-COMPLETED-ON-ABORT
in this order.
-
[glossary-term] async-unwind
If an asynchronous event, say a
SIGINT
triggered byC-c
, is delivered to a thread running Lisp or foreign code called from Lisp, a Lisp condition is typically signalled. If the handler for this condition unwinds the stack, then we have an asynchronous unwind. Another example isBT:INTERRUPT-THREAD
, which, as it can execute arbitrary code, may unwind the stack in the target thread.
-
[glossary-term] boolean-valued symbol
Imagine writing two
STREAM
s with a spaghetti of functions and wanting to have pretty-printed output on one of them. Unfortunately, binding*PRINT-PRETTY*
toT
will affect writes to both streams.One solution would be to have streams look up their own print-pretty flag with
(SYMBOL-VALUE (STREAM-PRETTY-PRINT STREAM))
and have the caller specify the dynamic variable they want:(defvar *print-pretty-1* nil) (setf (stream-print-pretty stream-1) '*print-pretty-1*) (let ((*print-pretty-1* t)) (spaghetti stream-1 stream-2))
Note that if the default
STREAM-PRINT-PRETTY
is'*PRINT-PRETTY*
, then we have the normal Common Lisp behaviour. SettingSTREAM-PRINT-PRETTY
toNIL
orT
also works, because they are self-evaluating.The above hypothetical example demonstrates the concept of boolean-valued symbols on
CL:STREAM
s. In Journal, they are used byMAKE-LOG-DECORATOR
andPPRINT-JOURNAL
s.
-
[glossary-term] readable
In Common Lisp, readable objects are those that can be printed readably. Anything written to stream-based journals needs to be readable.