I had a change of mind recently. It began with the realization that
WITH-TIMEOUT as it is implemented in sbcl is inherently racy when
;; the race is here
(timeout () nil)))
(timeout () nil))
Furthermore the solution described in my previous mails has nesting
semantics that give me headaches. Maybe handling asynchronous
conditions by the normal handlers is not a great idea?
Allegro's WITH-TIMEOUT unwinds to the WITH-TIMEOUT form and runs
the timeout forms.
with-timeout (seconds &body timeout-body) &body body
To implement it a catch can be set up that's thrown to by the
interruption that's run by the timer. If that catch tag is 'private'
(has gensym nature) then nesting WITH-TIMEOUTs is safe.
The sketch of a solution I have in mind now is this: interruptions are
run with a different set of condition handlers manipulated by
ASYNCHRONOUS-HANDLER-BIND and put behind an unwind barrier. When a NLX
tries to go through the barrier it is cancelled and the barrier
returns normally (possibly warns or do other things).
To get through the barrier an interruption needs a ticket. This ticket
is established by CATCH/OOP (out-of-phase). The interruption throws to
it by THROW/OOP and the barrier lets it through. Roughly.
In more detail, when throwing THROW/OOP signals ABOUT-TO-THROW/OOP and
the default handler looks up block reasons for async unwinding
(established by WITH-ASYNCHRONOUS-UNWIND-BLOCK-REASON, cleanup forms
or ungoing unwinds). If there is a block reason it signals
BLOCKED-ASYNCHRONOUS-THROW for which the default handler invokes the
RETRY-LATER restart and the throw (not the interruption!) is stored
for the corresponding CATCH/OOP. Else it is let through the barrier.
WITH-TIMEOUT is implemented by something like:
(let ((tag (gensym "timeout-tag-")))
(make-timer (lambda () (throw/oop ,tag))))
The timer function is called as an interruption (the IN-INTERRUPTION
macro) meaning that:
* it's wrapped in a barrier that blocks normal NLXs
* lets oop throws through
* binds *handler-clusters* & co to what ASYNCHRONOUS-HANDLER-BIND
* sets up a handler for ABOUT-TO-THROW/OOP that checks the block
reasons and signals BLOCKED-ASYNCHRONOUS-THROW
Another important thing is block reasons only count if they are
established after the CATCH/OOP tag.
This approach is a whole lot nicer than the previous one. The main
difference is that semantics don't change when a block reason is in
effect (e.g. in cleanup forms). In other words nesting is
sane. WITH-TIMEOUT works everywhere, including cleanup forms and
Interruptions are out of phase wrt the normal control flow and the
reality of the CL standard. Sharing condition handlers and allowing
NLXs leads to trouble, but some kind of controlled tunnel has to be
provided to travel between the two worlds. This is accomplished by
CATCH/OOP and THROW/OOP with the help of IN-INTERRUPTION and
* what if more than one throw is stored for a CATCH/OOP? which value(s)
* better names
* what does a barrier do when a NLX is denied?
Safe unwinding from foreign callbacks is likely to be implementable on
top of catch/oop, throw/oop and unwind barriers with an in-callback
macro thrown in that has a slightly different task from in-interruption
but that's another story.