Exceptions in SCOOP
Introduction
Exceptions are a rather nasty issue in concurrency. In a shared memory system, an exception can leave a system in an inconsistent state, for example because they jump over an unlock operation. In message passing systems on the other hand they can introduce unnecessary synchronization just to make sure that no exception happened, or they can create havoc because the recipient of an exception message is no longer in a state where it can handle it.
For SCOOP, the exception mechanism was carefully designed with the following goals in mind:
- Comprehensibility: It should be easy to understand
- Compatibility with exceptions in sequential programs
- Consistency: An exception should not leave objects in a tainted state.
- Asynchrony: Exceptions should not restrict the level of concurrency.
Exception Propagation
Within a single processor, exceptions propagate just like in a sequential program.When a routine encounters an exception, the rescue
clause is entered, and if no retry
statement is present, the exception is propagated to the caller.This ensures backwards compatibility with sequential programs, because when there's only the root processor, the semantics are exactly the same.Furthermore, this mechanism has proven itself useful for restoring invariants after an exception in order to bring objects to a consistent state.
The interesting case is when an exception propagates between regions, which happens during a separate call.In that case there are two possibilities:
- The call is synchronous: The exception is propagated to the client region.
- The call is asynchronous: The exception is not propagated, because the client is busy executing something else. Instead, the supplier region is marked as dirty.
This decision was mostly made to ensure comprehensibility.Propagating an exception to the client in an asynchonous call would be really hard to handle.The client would have to be ready to handle an exception at any point in time, and there would have been a need for an additional language mechanism to protect critical sections.Because of these reasons SCOOP restricts exception propagation to synchronous calls only.
Dirty Regions
A region marked as dirty has suffered an exception in an asynchronous call, which could not be propagated to its client.The dirty mark has a big impact for future separate calls.
The reason for these rules is that a series of commands and a subsequent query often depend on each other.For example, a first call may instruct the target region to open a file, the next call to append a string to it, followed by a query to get the new size of the file.If the first call already fails, there's no point in executing subsequent calls.Even worse, it can make recovery from exceptions very hard to do in the client if it has no idea which calls have been successfully executed after the first exception.
The dirty mark will also vanish when an region is unlocked.
This is probably the most controversial design decision, because it allows for exceptions to be lost.During the design of the exception mechanism, there was a choice of two other solutions.One would have been to add an automatic "safeguard" synchronization whenever an unlock operation happens, during which exceptions could be propagated. The obvious downside is that it severely limits the uses of asynchrony.Another solution would have been to preserve the exception, and raise it in the client that next logs a call.The last solution only partially solves the problem (there might be no next client logging a query at all), but introduces a new problem that processors can get an exception completely out of context.However, the main reason to choose the "forget-upon-unlock" solution over the other two is that it's easy to simulate the behaviour manually (as you'll see in the next section), while it's impossible to have a "forget-upon-unlock" semantics if one of the other models is used.
Preventing Exception Loss
One way to prevent exceptions from being lost is to add a synchronous query at the end of a routine:
put_character (c: CHARACTER; a_file: separate MY_FILE)
local
l_sync: POINTER
do
a_file.open
a_file.put_character (c)
a_file.close
-- This ensures that exceptions are propagated:
l_sync := a_file.default_pointer
end
Another possibility is to store the failure in the separate object:
class MY_FILE feature
is_tainted: BOOLEAN
open
do
-- Open a file.
rescue
is_tainted := True
end
-- other features
end
class CLIENT feature
put_character (c: CHARACTER; a_file: separate MY_FILE)
do
if a_file.is_tainted then
-- Handle exception in `a_file'.
end
a_file.open
a_file.put_character (c)
a_file.close
end
end