AbstractWe describe the problem of asynchronous exceptions in Eiffel’s Simple Concurrent Object-Oriented Programming (SCOOP). We discuss a range of possible solutions to further enable dependable computing in concurrent Eiffel. We propose a mechanism to handle aynchronous exceptions via a limited developer choice, including the notion of a failed or dead object, and necessarily introduce a small number of new exceptions. We additionally describe a number of mechanisms that were discarded as unsuitable.
The Simple Concurrent Object-Oriented Programming (SCOOP) mechanism is proposed as a means to introduce inter-object concurrency into the Eiffel programming language [Mey97, EcI05]. SCOOP extends the Eiffel language by adding one keyword, separate, which can be applied to classes, entities and formal routine arguments. Application of separate to a class indicates that objects of that class execute in their own conceptual thread of control; application of separate to entities (variables) or arguments of routines indicate that these constructs are points of synchronisation. Thus, SCOOP aims at a minimal (syntactic) extension to the Eiffel language, through combining the notions of thread and synchronisation in separate classes and entities.
Aim, scope and limitations
We discuss the SCOOP mechanism as it relates to sequential Eiffel’s existing exception handling. As we shall see, the existing exception handling mechanism is insufficient to deal with failures in SCOOP. We thus desire an extension to the exception handling mechanism that 1. permits the creation of reliable, predictable systems; 2. allows the use of existing (sequential) libraries; and 3. is not unduly burdensome to either the run-time system or the developer. The aim of this paper is to propose an extension to Eiffel’s sequential exception handling in order to satisfy these requirements and fully support SCOOP. As the focus of this work is on exception handling in SCOOP (as SCOOP is currently documented [Mey97]), we do not aim to compare SCOOP itself with other approaches to concurrency such as those found in Java, active objects or POSIX threads, although we do examine some related work that directly deals with exceptions. Some discussion on these issues can be found in [PB06].
We start with brief overviews of Eiffel in Section 2 and SCOOP in Section 3. From this, we state the problem with exceptions in SCOOP in Section 4. Next, we describe the related work in this area in Section 5. We then further discuss exceptions and describe our proposed solutions in Section 6. Sections 7 and 8 describe the mechanisms we rejected, and additional sources of exceptions in the concurrent environment. The paper ends with a discussion and our conclusions in Sections 9 and 10.
Eiffel is a pure object-oriented (OO) programming language [Mey97, EcI05] that provides constructs typical of the OO paradigm, including classes, objects, inheritance, associations, composite (“expanded”) types, polymorphism and dynamic binding, and automatic memory management. Novelties with Eiffel include its support for full multiple inheritance, generic types (including constrained generics), agents (closures and iterators over structures), and strong support for assertions, via preconditions and postconditions of routines, and invariants of classes.
Routines may have preconditions (require clauses) and postconditions (ensure clauses). The former must be true when a routine is called (i.e., it is established by the caller) while the latter must be true when the routine’s execution terminates. Classes may have invariants specifying properties that must be true of all objects of the class at stable points in time, i.e., after any valid client call on the object. An exception is raised if an assertion (precondition, postcondition or invariant) evaluates to false.
For more details on the language, see [Mey97] or [EcI05].
Exceptions in sequential Eiffel can occur when an assertion is violated (e.g., a routine is called with precondition false), a called routine fails, an interrupt is sent by the operating system, or an operation fails (e.g., creation of a new object fails, arithmetic overflow occurs).
Exceptions are handled by so-called rescue clauses. A rescue clause is attached to a routine (after the postcondition) and describes a sequence of instructions that should be executed when the routine is to recover from an undesirable run-time event. One new instruction that can be included in rescue clauses is retry, which is a directive to re-start execution of the routine body from the beginning.
SCOOP introduces concurrency to Eiffel by the addition of the keyword separate. The separate keyword may be applied to the definition of a class or the declaration of an entity (a variable) or formal routine argument.
Access to a separate object, whether via an entity or formal argument indicates different semantics to the usual sequential Eiffel model. In the sequential model, a call to a routine causes execution to switch to the called object whereupon the routine executes; on completion, execution continues at the next instruction of the original object.
In SCOOP, procedure calls are asynchronous. The called object can queue multiple calls, allowing callers to continue concurrent execution. Function calls (e.g., a := x.f) and reference access to attributes are synchronous — but may be subject to lazy evaluation (also known as wait-by-necessity).
Races are prevented by the enforced convention that a separate formal argument causes the object to be exclusively locked (‘reserved’) during that routine call. However, there are complications with locking, in that deadlocks may arise, or concurrency may not be maximised, unless some form of lock passing [Bro06a, Bro06c] is used.
SCOOP introduces the notion of a processor. When a separate object is created, a new processor is also created to handle its processing. This processor is called the object’s handler. (Objects created as non-separate are handled by the creator’s handler.) Thus, a processor is an autonomous thread of control capable of supporting sequential instruction execution [Mey97]. A system in general may have many processors associated with it.
Compton [Com02a] introduces the notion of a subsystem: a model of a processor and the set of objects it operates on. In his terminology, a separate object is any object that is in a different subsystem. In this paper, we will refer to subsystems rather than processors (to avoid possible confusion with real CPUs).
We additionally introduce the notion of a partition to describe any real processing resource, e.g., a CPU, a POSIX thread or process [Bro06a]. Subsystems are assigned to partitions.
Preconditions and waiting
As described in Section 2, Eiffel uses require and ensure clauses for specifying the pre- and postconditions of routines. If a precondition or postcondition evaluates to false, an exception is raised.
There are two possible views on exceptions here:
In the first case, it is reasonable, at least during development, for the exception to cause an immediate system halt with suitable diagnostic information to allow the bug to be fixed. In the second case, catastrophic responses are likely problematic, particularly in systems required to be fault-tolerant.
In SCOOP, a require clause on a routine belonging to a separate object specifies a wait condition: if the routine’s require clause evaluates to false, the processor associated with that object waits until the precondition is true before proceeding with routine execution.
There is then a third interpretation to be placed on exceptions that would otherwise arise from preconditions: that they are guards restricting when a feature may execute. Clearly, these exceptions are not bugs (although an unintended deadlock would clearly be a bug). However, we must handle exceptions that are raised while evaluating the precondition.
4 THE PROBLEM: EXCEPTIONS AND SCOOP
A major advantage of SCOOP is that it allows procedure calls on separate objects to progress asynchronously: that is, a caller can enqueue a procedure call on a callee and then continue processing. Similarly, function calls can, by lazy evaluation, issue a request and then continue until that return value is actually needed in the caller.
This brings with it a problem: suppose that the called routine fails. A routine fails when an exception is raised, and there is either no rescue clause, or the rescue clause does not lead to a retry. This failure will cause an exception in the caller in the sequential programming model, where the exception will be propagated through the list of callers until either a rescue clause succeeds with retry, or the root class’s creation routine fails and the run-time system halts the whole program.
However, in SCOOP, if the caller and callee are separate, then the caller might already have completed when the callee fails. In this case, there is no obvious target to send the exception to. We cannot send the exception to another arbitrary object in the same subsystem or partition since there is not necessarily a call relationship between these objects.
So we ask: How should we handle asynchronous exceptions in SCOOP?
This is an instance of a more general problem: in distributed or concurrent setting, exception handling is difficult. The problem is particularly challenging in SCOOP with its complex underlying run-time structure, and because SCOOP does not disentangle locking and synchronisation. Note that this problem does not arise in synchronous languages such as Esterel [Est99], where the system can guarantee that a caller will still exist whenever a callee may raise an exception.
5 RELATED WORK
Implementations of SCOOP
An incomplete prototype of the SCOOP mechanism was implemented by Compton [Com02a] by building upon the GNU SmartEiffel compiler and run-time system. A prototype preprocessor implementation was constructed by Fuks et al. for ISE Eiffel [Fuk04].
More recently, Nienaltowski et al. [Nie05] have produced the most complete implementation of SCOOP to date. This (in common with other prototypes) is a preprocessor that rewrites SCOOP-using classes. None of these prototypes can be considered a full implementation of the SCOOP specification in [Mey97].
Exceptions in SCOOP
Compton and Walker [Com02b] note that
This clearly matches the problem as described earlier. They continued by discussing this with Meyer, and say
However, we view both options as being overly aggressive to be the only solutions: they will result in trivial errors bringing down large, (hopefully) long-lived systems. (A highly concurrent system will surely have many cooperating subsystems.)
Adrian discusses an extension of SmallEiffel (itself now known as SmartEiffel) to include SCOOP [Adr02]. In that work, he quotes from the SmallEiffel mailing list, one of which notes that asynchronous exceptions would require all routines to be able to handle all possible exceptions (an undesirable outcome). Instead, Adrian says that SmallEiffel will adopt the following approach:
However, this approach does not make clear how an exception would reach the “root call of a subsystem” in the first place: there is not necessarily a causal relationship between a caller and the first object to be created in a SCOOP subsystem.
Recently, Arslan and Meyer proposed a notion of ‘busy processors’ [Ars06]. In this work, an unhandled asynchronous exception causes the processor to be marked‘busy’ until the processor that enqueued the failed call takes action to reestablish the invariant on the called object. However, we do not see that there is necessarily a causal relationship between particular processors and the object-call relationships.
We are unaware of any Eiffel/SCOOP implementation that handles asynchronous exceptions. We know of no other serious proposals other than Arslan and Meyer’s busy processors.
Exceptions in other concurrent languages
Ada 95 [Taf97] introduces concurrency via tasks. Rendezvous is possible between tasks, and passive objects (protected types) allow sharing of data. An unhandled exception results in the termination of the enclosing task. Subsequent attempts to rendezvous with a terminated task cause an exception in the new caller.
Java’s concurrency support is essentially thread-based, with block-based locking (synchronization) on a per-thread basis. There are few cases for asynchronous exceptions, so the problems that SCOOP encounters do not arise in general for Java.
Exceptions in object-oriented programming
Other more general work on exceptions in object-oriented programming has been undertaken, notably in the Exception Handling in Object Oriented Systems series [Rom05]. However, little of the work directly addresses the issue of asynchronous exceptions as the problem relates to SCOOP. Some from other areas is applicable as we discuss shortly.
Dony et al. [Don91] discuss a range of issues, and many contributions emphasise providing choice and developer-chosen links between exceptions and handlers. Issarny [Iss01]’s concurrent exception handling is another example of synchronous exception handling. Parallel procedures are grouped in multiprocedure blocks. It is not obvious how this approach can be extended to asynchronous concurrent systems. Romanovsky and Kienzle [Rom01] survey a number of approaches to exception handling in concurrency, and illustrate that exception handling is typically associated with the textual context of a program, i.e., exceptions propogate outwards through the associated nested contexts. Again, there is no obvious solution offered to the problem of asynchronous, i.e., out of context, exceptions. The discussions on transactions, however, suggest that changing Eiffel’s concurrency model to be based around transactions may be compatible with exception handling approaches proven in other application domains.
Asynchronous exceptions have received attention. Caromel and Chazarain [Car05] describe a Java approach. However, although the calls are asynchronous, the end of each try/catch block waits for incomplete asynchronous calls, thus the problem that we enounter of the caller expiring before the callee responds with an exception does not arise.
An interesting point that has not arisen directly so far relates to callbacks. This issue arises in sequential Eiffel as well as SCOOP. Suppose object a passes a reference to feature f of object c to object b, with the intention that object b subsequently executes c.f , a callback. If c.f throws an exception, then arguably a should receive the report, not b; in other cases, it may make more sense in terms of the system for b to receive the exception, as it would at this time. This particular programming idiom is popular in event-based systems such as GUIs. Ploski and Hasselbring [Plo05] suggest firstly handling such exceptions locally; otherwise propogating the exception to “the invoked callback’s clients, that is, modules affected by the callback’s failure”. However, it is not trivial to identify the affected modules.
Finally, the most relevant related work is due to Rintala, and deals with multiple concurrent and asynchronous exceptions in C++ [Rin05]. Results from asynchronous calls are initially substituted by futures; these act as placeholders for the eventual result from the call. Exceptions that arise during the asynchronous call are routed through the future; essentially, they are the return value of the future. Subsequent references then obtain the exception rather than the normal result. Finally, multiple exceptions can be grouped into compound exceptions.
6 FIXING SCOOP EXCEPTIONS
The main problem with exceptions in SCOOP is that the sequential model of a chain of callers does not hold. The client call that should be told of an exception in the called object may no longer exist; the lack of synchrony that makes SCOOP so appealing means that the model of exceptions needs updating.
While fixing this, we require that existing sequential code continues to work without modification. This section applies to exceptions arising from unhandled exceptions in objects that were called by another separate object. There are three options, that could be chosen for groups of objects and/or classes by the developer:
We suggest that the first should be the default response. Additionally, a compiletime configuration mechanism or run-time system class should allow objects to be designated for a specific policy (via, say, ‘set exception policy’).
Halting as a response to unhandled asynchronous exceptions
An unhandled exception in the sequential model results in the entire system halting. We justify this as halting an entire SCOOP system in response to an unhandled asynchronous exception is analogous to the sequential case.
Thus we make halt the entire system the default response to an unhandled (asynchronous or synchronous) exception. This is a simple, low-overhead solution. Moreover, it is consistent with the Eiffel design-by-contract philosophy: exceptions are usually viewed as being a fault in the implementation that needs repair.
We choose this as the default response to be conservative: our next option as a default may allow objects to silently fail. By forcing the developer to explicitly choose an alternative response, they should then design their system to respond appropriately.
As we remark in Section 5, Compton and Walker in discussion with Meyer offer this as one of two possible solutions. We offer other options to the system designer as we view halting the entire system as too aggressive to be the only solution: it prevents any form of fault tolerance.
Unhandled asynchronous exceptions cause the object to fail and die
As an alternative to halting the system when a routine of an object fails and there is no sequential caller, we may instead mark the object as failed. It will never interact
with any other object again nor will it process any further calls: this means that any calls already enqueued will not be processed. If a caller considers it important that a procedure call completes, then this caller must ensure that it checks a return value or status.
Future attempts to enqueue work for that object or to access it in any way result in the caller receiving a ‘separate object failure’ exception. An implication of this proposal is that enqueueing a call to a subsystem’s FIFO is synchronous.
The only information that needs to be retained is the full name (i.e., sufficient information to unambiguously identify it); the type of the object; and the exception object. All other data, including its part of the call queue, can be garbage collected. The exception object can be returned as part of the separate object failure exception allowing other objects to possibly determine an appropriate response.
When it is determined that an object will die due to an unhandled asynchronous exception, the system will call the procedure identified by ‘set asynchronous exception handler’ (or else ‘default asynchronous exception handler’, a feature that can be overridden in descendent classes). This is treated similarly to default rescue and offers the failed object a ‘last gasp’ to carry out some work. The intention is that an object can use this call to set up a handler that makes an appropriate response, e.g., calling a different object to report its failure, or to create a replacement object and indicate to other clients that they should fail-over to the replacement.
There may be some complications resulting from this part of the mechanism: consider the case where the called handler makes a series of subsequent calls, including calls to the dying object. Should they be processed? Similarly, how should subsequent exceptions during this handler be managed?
This part of our solution also handles the case where objects may not be contacted again by another object (we call these independent objects below). This can arise because some objects may have been created with the express purpose of carrying out a task independently, so they may never need to be contacted by any other separate object again, e.g., some Internet service daemons where a client asks for service and a new process is started. In SCOOP, a new separate object could be used for each client. Our proposal allows such objects to report to another object that they have failed.
This solution is similar but not identical to the second option suggested by Compton and Walker in discussion with Meyer (Section 5). In particular,
This solution is also related to Rintala’s approach [Rin05], in the sense that Rintala’s futures would be replaced by the exception.
Ignoring asynchronous objects
Some objects may not encapsulate any state, or for other reasons, an unhandled
The recent ECMA standard for Eiffel [EcI05] alludes to (but does not describe)
7 DISCARDED MECHANISMS
We considered a wide range of possible mechanisms and discarded all but those described above.
Halting the subsystem or partition We might further suggest that affected subsystem
or partition is halted, instead of just the failed object.
Allowing clearing of an asynchronous exception A failed object may, or may
not, have a valid invariant. Alternatively, the object may not encapsulate any
state and might be able to continue without any remedial action. Instead of
destroying a failed object, we might allow it to be ‘resurrected’ by another
object taking action that achieves both: 1. the invariant is restored (unless
the invariant is already valid); and 2. the exception is ‘cleared’ on the failed
object (perhaps by a specific routine of the CONCURRENCY class). The
first condition requires that a creation procedure is executed on the object —
this must be done if the invariant is false, and may be if the invariant is true.
The second condition is an indication to the system that the problem has been
Queues of exceptions for polling or waiting We could arrange that failed objects place their exception into a queue. This queue could be periodically polled by other objects, or could be waited on (i.e., blocking behaviour). But what should happen then? We must arrange that the exception is handled, and also deal with the case when the exception is not handled for a long time (or ever). This option has all the problems of allowing resurrection of a failed object: the lack of a causal connection and the issues of clearing exceptions result in us rejecting this option.
ADDITIONAL SOURCES OF EXCEPTIONS
There are three special sources of exception we should consider:
Deadlock It is well known that non-trivial concurrent systems can easily admit deadlock. Nothing in SCOOP directly prevents that, although the philosophy of locking the objects that are needed by listing them as formal arguments may help structure systems such that deadlock is less likely. So other approaches are needed:
Problems in the infrastructure Our system comprises one or more partitions.
Suppose that the CPU executing one of these partitions fails or is disconnected
from its communications network. In this case, no further calls can be made to
or from that partition. What should be done in this case? Clearly, this is the
type of problem that exceptions should manage. An attempt to communicate
Duels Duels, or ‘requesting special service’, as described in Meyer’s text [Mey97, section 30.8], are a way for one object to (attempt to) demand access to a reserved object. The result of this can sometimes be the exception ‘is concurrency interrupt’. This itself may propogate causing a routine failure. If exception handling can be managed for separate objects as proposed in this work, then the use of duels becomes possible, although other ongoing work may provide a better solution for asynchronous transfer of control1.
The difficulties caused by asynchronous exceptions are a necessary evil: we cannot remove them except by losing the appeal of SCOOP’s asynchronous procedures. We can, however, manage them. We compare our work with our aims from Section 1:
3. Is not unduly burdensome to either the run-time system or the developer: The run-time system must include some means of configuring the desired behaviour, but this is a very limited overhead compared to the rest of the work associated with managing concurrent objects. Developers must necessarily consider the behaviour of their system when it fails: the proposal here gives them a small, expressive but not excessive toolkit to manage this behaviour.
Our work is strongly influenced by the Ada approach to tasks, although all objects in SCOOP have a thread of control. In particular, we do not allow propogation of exceptions outside of an asynchronous ‘frame’, and the resulting failure (termination) of an object (task) results in further exceptions to callers.
The impact of this proposal on developers is minimal compared to the complexity of the intrinsic task. Developing concurrent systems is known to be subtle; the developer must necessarily decide how to handle exceptions. The default is a safe case, but now the developer can choose that particular classes (or individual objects) may fail without halting the entire system while still offering a mechanism for recovery. Developers must be aware that if a separate call must execute successfully, then they have to take action to ensure that it does occur: either by checking that a result is returned, or otherwise obtaining some form of acknowledgement from the separate object. Otherwise, a failed object could discard its job queue and the calls are never executed without the caller being aware of this failure.
If radical changes are considered, then the concept of compensation2 may be appropriate: this provides ‘forward recovery’ in the case of aborted transactions where the effects of the aborted transaction cannot be undone. An approach that would cause substantial changes to Eiffel in general is to treat success as the exception. Thus the main flow of the code makes different or repeated attempts to succeed, while branches or exceptions are invoked when these attempts succeed.
An alternative radical change is to use a more synchronous form, such as cobegincoend or a construct that mimics Esterel’s [Est99] parallel construct. This then removes the entire problem of asynchronous exceptions by ensuring that a synchronous rendezvous is still available whenever an exception may arise.
These approaches are interesting and may be very profitable in the long term: we have explicitly restricted ourselves to making minimal changes to Eiffel itself, so do not consider them further in this work.
Our contributions in this paper are
Our mechanism can be summarised thus: unhandled exceptions propagate as far as possible using the normal sequential mechanism. We have a number of options for handling asynchronous exceptions: halt the entire system (the default response); the object is marked as failed; or ignore the exception. As no single option is satisfactory in all cases, the developer can specify the appropriate response. Exceptions (for deadlocks, infrastructure problems and failed objects) and routines (for setting policies and ‘last gasp’ handlers) are introduced.
The next steps in our work are:
Bertrand Meyer, Volkan Arslan and others at the CORDIE’06 workshop for their comments on the purpose of exceptions in Eiffel.
2 We first heard of compensation in the context of Eiffel from Volkan Arslan and Sebastien Vaucouleur in February 2005. Prof. Sir Tony Hoare suggested other possible mechanisms, notably that of treating success as the exceptional case at the 2006 UTP Symposium.
[Adr02] C. Adrian, SCOOP for SmallEiffel, draft, http://www.chez.com/cadrian/eiffel/scoop.html, 2002, last accessed 10th November 2005.
[Ars06] V. Arslan and B. Meyer. Asynchronous exceptions in concurrent objectoriented programming. In [PB06].
[Bro06a] P.J. Brooke and R.F. Paige. A critique of SCOOP. In [PB06].
[Bro06b] P.J. Brooke and R.F. Paige. An alternative model of concurrency for Eiffel. In [PB06].
[Bro06c] P.J. Brooke, R.F. Paige, and J.L. Jacob. A CSP model of Eiffel’s SCOOP. Submitted to Formal Aspects of Computing, November 2006.
[Car05] D. Caromel and G. Chazarain. Robust exception handling in an asynchronous environment. In [Rom05], 2005.
[Com02a] M. Compton. SCOOP: an Investigation of Concurrency in Eiffel, MSc Thesis, Australian National University, 2000.
[Com02b] M. Compton and R. Walker. A Run-time System for SCOOP. Journal of Object Technology 1(3), special issue: TOOLS USA 2002 proceedings, pp. 119-157, 2002. http://www.jot.fm/issues/issue_2002_08/article8/
[Don91] C. Dony, J. Purchase and R. Winder. Exception handling in objectoriented systems. ECOOP’91, 1991.
[EcI05] ECMA-367: Eiffel Analysis, Design and Programming Language, Ecma International, June 2005.
[Est99] The Esterel v5 Language Primer. ftp://ftpsop.inria.fr/meije/esterel/papers/primer.pdf,
[FSE95] Formal Systems (Europe) Ltd. Failures-Divergence Refinement: FDR 2. http://www.formal.demon.co.uk/, December 1995.
[Fuk04] O. Fuks, J.S. Ostroff, and R.F. Paige. SECG: The SCOOP-to-Eiffel Code Generator. Journal of Object Technology 3(10), November/December 2004. http://www.jot.fm/issues/issue_2004_11/article3/
[Iss01] V. Issarny. Concurrent exception handling. In Advances in Exception Handling Techniques, LNCS 2022, 2001.
[Mey97] B. Meyer, Object-Oriented Software Construction, 2nd Edition, Prentice Hall, 1997.
[Nie05] P. Nienaltowski and B. Meyer, SCOOPLI implementation, http://se.inf.ethz.ch/research/scoop.html, 2005.
[PB06] Richard F. Paige and Phillip J. Brooke, editors. Proc. First International Symposium on Concurrency, Real-Time, and Distribution in Eiffel-like Languages (CORDIE), number YCS-TR-405. University of York, July 2006.
[Plo05] J. Ploski and W. Hasselbring. The callback problem in exception handling. In [Rom05], 2005.
[Rin05] M. Rintala. Handling multiple concurrent exceptions in C++ using futures. In [Rom05], 2005.
[Rom01] A. Romanovsky and J. Kienzle. Action-oriented exception handling in cooperative and competitive concurrent object-oriented systems. In Advances in Exception Handling Techniques, LNCS 2022, 2001.
[Rom05] A. Romanovsky, C. Dony, J.L. Knudsen, and A. Tripathi (editors). Developing Systems that Handle Exceptions, Proceedings of ECOOP 2005 Workshop on Exception Handling in Object Oriented Systems. Technical Report No 05-050. Department of Computer Science. LIRMM. Montpellier-II University. 2005.
[Taf97] T. Taft and R. A. Duff, editors. Ada 95 Reference Manual. Number 1246 in Lectures Notes in Computer Science. Springer-Verlag, 1997.
About the authors
Cite this column as follows: Phillip J. Brooke and Richard F. Paige: Exceptions in Concurrent Eiffel, in Journal of Object Technology, vol. 6 no. 10 November-December 2007, pp. 111-126 http://www.jot.fm/issues/issue_2007_11/article4