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.

Rule -- Semantics of Dirty Regions:
1) All calls logged to a dirty region, whether synchronous or asynchronous, are ignored.
2) A synchronous separate feature call to a dirty region immediately triggers an exception in the client. Afterwards, the region is clean again.

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.

Rule -- Unlocking Dirty Regions: After releasing a lock on a dirty region, the region is clean again.

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.

Info: Upon lock passing, the dirtyness of a region is preserved.

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