Hydrodoc_exceptions



Exceptions

O'Caml does not support subtyping for exceptions, i.e. we cannot give a nice exception hierarchy here. Predefined exceptions are:

These exceptions only exist locally and cannot be transmitted from a server to a client.

Remote exceptions

There are only a few types of exceptions that can be transferred over ICE connections. These types are not directly exceptions in the O'Caml sense, and represented differently in client and server code.

User exceptions

As mentioned, user exceptions are defined in the Slice file. Such exceptions are hierarchical, which is not supported by O'Caml exceptions. For this reason, a special mapping is applied, so that all user exceptions appear as User_exception with a parameter that represents the hierarchy (among other things). An example shows how this works. Given a Slice definition

  exception X {
    string text;
  };

  exception Y extends X {
    string detail;
  };

hydrogen maps this to

  type exception_name = [ `X | `Y ]

  and exception_ops =
    < exn_name : exception_name;
      exn_id : string;
      is_X : bool;
      as_X : t_X;
      is_Y : bool;
      as_Y : t_Y;
    >

  and t_X =
    < hydro_ops : exception_ops;
      text : string
    >

  and t_Y =
    < hydro_ops : exception_ops;
      text : string;
      detail : string;
    >

  and user_exception =
    < hydro_ops : exception_ops >

  exception User_exception of user_exception

Note that the <...> notation means O'Caml object types. They are seldom used, but are quite useful in this context. As you can see, every user exception has its own object type that allows access to exception arguments like text and detail, and that also has a method hydro_ops for common properties. The type user_exception is the (artificially added) root of the exception hierarchy, and besides hydro_ops nothing is accessible. The O'Caml exception User_exception is defined and takes only such a root object as argument.

So if you catch a User_exception you have only hydro_ops to analyze it:

Like objects the ICE user exceptions have a runtime type which may deviate from the type statically assigned in the code. When an exception is upcasted to a super exception, the runtime type remains the same, and a later downcast is possible. For instance, given that y is a t_Y:

  let sample (y : t_Y) =
    let x = (y :> t_X) in
    let y' = x # hydro_ops # as_Y in
    y'

For upcasts, one can simply use the O'Caml :> operator. For downcasts, there is the generated as_Y method. Note that the exception object remains really the same, so y = sample y is always true.

Catching user exceptions in client code

Now when you catch a User_exception, how can you distinguish between the several Slice exceptions, and how can you get the arguments? Do it this way:

  try
     let r = response # result in
     ...
  with
  | User_exception ue when ue#hydro_ops#is_Y ->
      let y = ue#hydro_ops#as_Y in
      printf "Exception Y: text = %s detail = %s" x#text x#detail
  | User_exception ue when ue#hydro_ops#is_X ->
      let x = ue#hydro_ops#as_X in
      printf "Exception X: text = %s" x#text

(Assumed, response is what you get by invoking a remote operation using a generated proxy class.) Of course, we test first for Y because all Y exception are also X exceptions because of the exception hierarchy (i.e. is_X is true for all Y exceptions).

Alternatively:

  try
     let r = response # result in
     ...
  with
  | User_exception ue  ->
    ( match ue#hydro_ops#exn_name with
      | `X ->
           let x = ue#hydro_ops#as_X in
           printf "Exception X: text = %s" x#text
      | `Y ->
           let y = ue#hydro_ops#as_Y in
           printf "Exception Y: text = %s detail = %s" x#text x#detail
    )

The latter is advantageous when you want to ensure that all possible exceptions are caught. exn_name always returns the name of the exception that was really thrown, so the order of X and Y does not matter here.

Creating user exceptions

Of course, hydrogen also generates constructors for user exceptions, so it isn't necessary to develop exception classes. In our example, there are these two generated functions:

  val x_X : string -> t_X
  val x_Y : string -> string -> t_Y

The single argument of x_X is the text argument of the X exception, and the two arguments of x_Y are the detail and the text arguments of the Y exception. Arguments are passed in derived-to-base order, i.e. more specific arguments come first.

Throwing user exceptions in server code

Given an interface

  interface F {
    int doSomething(string arg) throws Y;
  }

how do we throw the Y exception in the implementation? Recall that we implement servers by inheriting from skeleton classes:

  class myServant =
  object(self)
    inherit skel_F
    method doSomething arg = ...
  end

The question is now: how does the exception handling work in this context?

Generally, there are two ways of implementing doSomething: as a synchronous method, or as an asynchronous method. The first case is much simpler, as the implementation looks like

  method doSomething arg =
    parachute
      (fun session ->
        ...
      )

Here, parachute is the generated protection function that ensures that the method always returns a result or an exception (in other words that it is synchrounous). Within the part denoted by "..." you can safely

Using a parachute is strongly recommended.

Now to the second, asynchronous case. It is useful if you want to accept the operation invocation, but wait some time until the response is sent. Of cource, the parachute is generally not applicable here, so we have to look for other means of passing exceptions. The implementation looks now like

  method doSomething arg =
    (fun emit_result emit_user_exception session ->
       ...
    )

In this form, there is no handler that would catch O'Caml exceptions thrown in the part denoted by "...". Any O'Caml exception would fall through the whole Hydro runtime, and jump back to the caller of Unixqueue.run! Actually, this is a way of terminating the server immediately (unless the caller of Unixqueue.run deals with exception which is strongly recommended). Life becomes dangerous without parachute.

In the "..." part, or anytime later you can

Any of the "emit" functions or methods triggers that the response message is sent back to the client. If you call several "emit" functions the later calls override the effect of the former, provided that the response is still only queued, but not yet actively being sent.

Here an example that delays the response by 10 seconds:

  method doSomething arg =
    (fun emit_result emit_user_exception session ->
      let g = Unixqueue.new_group session#event_system in
      Unixqueue.once session#event_system g 10.0
        (fun () ->
          let response = ... in
          emit_result response
        )
    )

During the delay the server is still responsive, and can process other operations.

Note that there is not any mechanism in Hydro that takes care of that an operation is responded at all. It is just possible to forget about invocations. If you want to avoid it, we recommend to define a GC finaliser like

  Gc.finalise 
    (fun s -> 
      if not s#is_responded then
         prerr_endline "Forgot to respond!"
    ) 
    session

for the session object. The O'Caml runtime, and that may it make difficult to do more than printing a reminder to stderr, can execute the finaliser at any time in any thread. This is also the reason why Hydro doesn't do it by default.