Loading ...
Sorry, an error occurred while loading the content.

[newbie] Exception and resource cleanup

Expand Messages
  • Yauheni Akhotnikau
    Hi! There is another thing in Eiffel which I don t understand yet. What is best way of resoure cleanup in the case of exception? For example, let I need to
    Message 1 of 10 , Aug 3, 2007
    View Source
    • 0 Attachment
      Hi!

      There is another thing in Eiffel which I don't understand yet. What is
      best way of resoure cleanup in the case of exception?

      For example, let I need to connect to some device (COM port, PC/SC reader,
      some external controller, etc), initialize it, write some data to it and
      disconnect. After successful connect I need to close connection regardless
      of the reason of operation termination (e.g. normal return or exception).

      As I can see now in Eiffel I can do that such way:

      connect_and_write_to_device (
      device_params: DEVICE_PARAMS;
      data_to_send: DATA_TO_SEND) is
      local
      device_connector: DEVICE_CONNECTOR
      device: DEVICE
      do
      create device_connector.make (device_params)
      device_connector.connect
      if device_connector.is_connected then
      create device.make (device_connector.connection)
      device.initialize
      device.write_data (data_to_send)
      device_connector.close_connection
      end
      rescue
      if device_connector /= Void and then device_connector.is_connected then
      device_connector.close_connection
      end
      end

      But there are two calls of 'close_connection'. So it's easy to forget to
      write one.

      Such approach seems to be error prone. So several languages have special
      constructs to simplify resource cleanup in the case of exception.

      Ruby, for example, allows to use 'ensure' clause which is executed before
      returning from the current method regardless of reason. So in Ruby it's
      necessary to write only one close_connection:

      device_connector = DeviceConnector.new(device_params)
      device_connector.connect
      begin
      ...
      ensure
      device_connector.close_connection
      end

      C++ and D support RAII idiom where resource cleanup is performed in
      descructors during stack rewinding. So, if DeviceConnector's destructor
      calls 'close_connection' code in C++/D looks less verbose:

      {
      DeviceConnector connector(device_params);
      connector.connect();
      Device device(connector.connection());
      device.initialize();
      device.write_data(data_to_send);
      }

      The D language allows yet another way of resource cleanup -- special
      scope() construct:

      {
      auto connector = new DeviceConnector(device_params);
      connector.connect;
      scope(exit) connector.close_connection;
      auto device(connector.connection);
      device.initialize();
      device.write_data(data_to_send);
      }

      AFAIK, C# support Disposable idiom. So, if DeviceConnector implements
      IDisposable interface its allow to write something like:

      {
      DeviceConnector connector = new DeviceConnector(deviceParams);
      connector.connect();
      using(connector) {
      Device device = new Device(connector.connection);
      device.Initialize();
      device.WriteData(dataToSend);
      }
      }

      Has Eiffel some way to simplify resource cleanup in the case of exception?

      --
      Regards,
      Yauheni Akhotnikau
      Senior Programmer
      Intervale
      e-mail:eao197@... <mailto:eao197@...>
    • Peter Horan
      ... It depends, I guess. ... How about: connect_and_write_to_device ( device_params: DEVICE_PARAMS; data_to_send: DATA_TO_SEND) is local device_connector:
      Message 2 of 10 , Aug 3, 2007
      View Source
      • 0 Attachment
        Yauheni Akhotnikau wrote:

        > There is another thing in Eiffel which I don't understand yet. What is
        > best way of resoure cleanup in the case of exception?

        It depends, I guess.

        > As I can see now in Eiffel I can do that such way:

        How about:
        connect_and_write_to_device (
        device_params: DEVICE_PARAMS;
        data_to_send: DATA_TO_SEND) is
        local
        device_connector: DEVICE_CONNECTOR
        device: DEVICE
        failed: BOOLEAN
        do
        if not failed then
        create device_connector.make (device_params)
        device_connector.connect
        if device_connector.is_connected then
        create device.make (device_connector.connection)
        device.initialize
        device.write_data (data_to_send)
        else
        raise -- In class EXCEPTIONS & has arguments
        -- We failed to connect
        end
        end
        if device_connector /= Void and then
        device_connector.is_connected then
        device_connector.close_connection
        end
        rescue
        failed := True
        retry
        end

        But now, you cannot tell if the write was done successfully, as it
        stands. You could promote failed to a publicly available attribute.

        I would have separated the routine into `connect' and `write', I think,
        and built this on top.
        --
        Peter Horan School of Engineering and Information Technology
        peter@... Deakin University
        +61-3-5227 1234 (Voice) Geelong, Victoria 3217, AUSTRALIA
        +61-3-5227 2028 (FAX) http://www.eit.deakin.edu.au/~peter

        -- The Eiffel guarantee: From specification to implementation
        -- (http://www.cetus-links.org/oo_eiffel.html)
      • Yauheni Akhotnikau
        On Fri, 03 Aug 2007 12:05:43 +0400, Peter Horan ... I think it is possible to rewrite your sample without calling raise : do if not
        Message 3 of 10 , Aug 3, 2007
        View Source
        • 0 Attachment
          On Fri, 03 Aug 2007 12:05:43 +0400, Peter Horan <peter@...>
          wrote:

          >> As I can see now in Eiffel I can do that such way:
          >
          > How about:
          > connect_and_write_to_device (
          > device_params: DEVICE_PARAMS;
          > data_to_send: DATA_TO_SEND) is
          > local
          > device_connector: DEVICE_CONNECTOR
          > device: DEVICE
          > failed: BOOLEAN
          > do
          > if not failed then
          > create device_connector.make (device_params)
          > device_connector.connect
          > if device_connector.is_connected then
          > create device.make (device_connector.connection)
          > device.initialize
          > device.write_data (data_to_send)
          > else
          > raise -- In class EXCEPTIONS & has arguments
          > -- We failed to connect
          > end
          > end
          > if device_connector /= Void and then
          > device_connector.is_connected then
          > device_connector.close_connection
          > end
          > rescue
          > failed := True
          > retry
          > end
          >
          > But now, you cannot tell if the write was done successfully, as it
          > stands. You could promote failed to a publicly available attribute.
          >

          I think it is possible to rewrite your sample without calling 'raise':

          do
          if not failed then
          create device_connector.make (device_params)
          device_connector.connect
          if device_connector.is_connected then
          create device.make (device_connector.connection)
          device.initialize
          device.write_data (data_to_send)
          end
          end

          -- We are here if:
          -- a) everything is fine, no any exception.
          -- b) we retried execution in rescue clause.
          if device_connector /= Void and then device_connector.is_connected then
          device_connector.close_connection
          end
          rescue
          failed := true
          retry
          end

          But, I think this simple example shows how easy to write wrong or
          redundant or too complex resource cleanup code.

          There is an another side of problem. If connect_and_write_to_device catchs
          all exceptions then clients of connect_and_write_to_device would have to
          write 'staircase' code:

          obj1.do_something
          if obj1.is_successful then
          obj2.do_something
          if obj2.is_successful then
          obj3.do_something
          if obj3.do_something then
          ...
          end
          end
          end

          its reminds me "good" old days with C:

          int error;
          if( -1 != (error = do_something1()))
          if( -1 != (error = do_something2()))
          if( -1 != (error = do_something3()))
          ...
          else
          goto cleanup;
          else
          goto cleanup;
          else
          goto cleanup;

          But more 'modern' exception-handling appoaches allow to write simply:

          obj1.do_something
          obj2.do_something
          obj3.do_something

          --
          Regards,
          Yauheni Akhotnikau
          Senior Programmer
          Intervale
          e-mail:eao197@... <mailto:eao197@...>
        • Emil Mocan
          Hello, I am reading OOSC in these weeks and I m not really familiarized with Eiffel. I think that the clean-up must be done at the appropriate level of
          Message 4 of 10 , Aug 3, 2007
          View Source
          • 0 Attachment
            Hello,

            I am reading OOSC in these weeks and I'm not really familiarized with Eiffel.
            I think that the clean-up must be done at the appropriate level of abstraction.

            For example, I would resolve the proposed problem by moving the rescue clause text in the rescue clause of the two procedures of the DEVICE class, initialize and write_data. So if you have trouble during initialization or writing, you will ensure from there that the connection is closed.
            To complete the picture, I think the DEVICE_CONNECTOR would have to be a member of the DEVICE class and all the DEVICE_CONNECTOR objects operations would be launched from within DEVICE objects procedures. In few words, the DEVICE will be a client of DEVICE_CONNECTOR.

            Regards,
            Emil Mocan.

            Yauheni Akhotnikau wrote:
            	Hi!
            
            There is another thing in Eiffel which I don't understand yet. What is  
            best way of resoure cleanup in the case of exception?
            
            For example, let I need to connect to some device (COM port, PC/SC reader,  
            some external controller, etc), initialize it, write some data to it and  
            disconnect. After successful connect I need to close connection regardless  
            of the reason of operation termination (e.g. normal return or exception).
            
            As I can see now in Eiffel I can do that such way:
            
            connect_and_write_to_device (
               device_params: DEVICE_PARAMS;
               data_to_send: DATA_TO_SEND) is
               local
                 device_connector: DEVICE_CONNECTOR
                 device: DEVICE
               do
                 create device_connector.make (device_params)
                 device_connector.connect
                 if device_connector.is_connected then
                   create device.make (device_connector.connection)
                   device.initialize
                   device.write_data (data_to_send)
                   device_connector.close_connection
                 end
               rescue
                 if device_connector /= Void and then device_connector.is_connected then
                   device_connector.close_connection
                 end
               end
            
            But there are two calls of 'close_connection'. So it's easy to forget to  
            write one.
            
            Such approach seems to be error prone. So several languages have special  
            constructs to simplify resource cleanup in the case of exception.
            
            Ruby, for example, allows to use 'ensure' clause which is executed before  
            returning from the current method regardless of reason. So in Ruby it's  
            necessary to write only one close_connection:
            
            device_connector = DeviceConnector.new(device_params)
            device_connector.connect
            begin
               ...
            ensure
               device_connector.close_connection
            end
            
            C++ and D support RAII idiom where resource cleanup is performed in  
            descructors during stack rewinding. So, if DeviceConnector's destructor  
            calls 'close_connection' code in C++/D looks less verbose:
            
            {
               DeviceConnector connector(device_params);
               connector.connect();
               Device device(connector.connection());
               device.initialize();
               device.write_data(data_to_send);
            }
            
            The D language allows yet another way of resource cleanup -- special  
            scope() construct:
            
            {
               auto connector = new DeviceConnector(device_params);
               connector.connect;
               scope(exit) connector.close_connection;
               auto device(connector.connection);
               device.initialize();
               device.write_data(data_to_send);
            }
            
            AFAIK, C# support Disposable idiom. So, if DeviceConnector implements  
            IDisposable interface its allow to write something like:
            
            {
               DeviceConnector connector = new DeviceConnector(deviceParams);
               connector.connect();
               using(connector) {
                 Device device = new Device(connector.connection);
                 device.Initialize();
                 device.WriteData(dataToSend);
               }
            }
            
            Has Eiffel some way to simplify resource cleanup in the case of exception?
            
              
          • Yauheni Akhotnikau
            ... So you propose something like that: class DEVICE create make feature -- Initialization make (a_connection: DEVICE_CONNECTION) is do connection :=
            Message 5 of 10 , Aug 3, 2007
            View Source
            • 0 Attachment
              On Fri, 03 Aug 2007 15:20:28 +0400, Emil Mocan <emil@...> wrote:

              > Hello,
              >
              > I am reading OOSC in these weeks and I'm not really familiarized with
              > Eiffel.
              > I think that the clean-up must be done at the appropriate level of
              > abstraction.
              >
              > For example, I would resolve the proposed problem by moving the rescue
              > clause text in the rescue clause of the two procedures of the DEVICE
              > class, /initialize/ and /write_data/. So if you have trouble during
              > initialization or writing, you will ensure from there that the
              > connection is closed.

              So you propose something like that:

              class
              DEVICE
              create
              make
              feature -- Initialization
              make (a_connection: DEVICE_CONNECTION) is
              do
              connection := a_connection
              end
              feature -- Status
              is_ok: BOOLEAN
              feature -- Commands
              initialize is
              local
              is_exception: BOOLEAN
              do
              if not is_exception then
              ... -- some actions
              end
              is_ok := not is_exception
              rescue
              is_exception := True
              retry
              end
              write_data (data_to_send: DATA_TO_SEND) is
              local
              is_exception: BOOLEAN
              do
              if not is_exception then
              ... -- some actions
              end
              is_ok := not is_exception
              rescue
              is_exception := True
              retry
              end
              feature {NONE}
              connection: DEVICE_CONNECTION
              end

              ... -- somewhere in another class
              connect_and_write_to_device (
              device_params: DEVICE_PARAMS;
              data_to_send: DATA_TO_SEND) is
              local
              device_connector: DEVICE_CONNECTOR
              device: DEVICE_CONNECTOR
              do
              create device_connector.make (device_params)
              device_connector.connect
              is_ok := device_connector.is_connected
              if is_ok then
              create device.make (device_connector.connection)
              device.initialize
              is_ok := device.is_ok
              if is_ok then
              device.write_data (data_to_send)
              is_ok := device.is_ok
              end
              end
              end
              is_ok: BOOLEAN


              Is I've understood you correctly?

              --
              Regards,
              Yauheni Akhotnikau
              Senior Programmer
              Intervale
              e-mail:eao197@... <mailto:eao197@...>
            • Brian Heilig
              I have written a library in Eiffel called secom (serial communication) that implements what you are asking (including clean up). Here are the relevant
              Message 6 of 10 , Aug 3, 2007
              View Source
              • 0 Attachment
                I have written a library in Eiffel called secom (serial
                communication) that implements what you are asking (including clean
                up). Here are the relevant sections:

                In the examples the `connect_and_write_to_device' looks like this. It
                is the root creation procedure of my root class:

                make is
                -- Execute 'read_port'
                local
                to: COM_ABSTRACT_TIMEOUTS
                do
                Arguments.set_program_name ("read_port")
                create error_handler.make_standard
                set_default_parameters
                read_command_line

                create {COM_DEVICE} device.make (device_name)
                if not device.is_open then
                report_error (Failed_open_error)
                end

                set_control_settings

                -- Set the device to blocking mode
                to := device.timeouts
                to.set_overall_timer (5000)
                device.set_timeouts (to)

                -- Read from the device and output the results to stdout
                from
                until False
                loop
                device.read_line
                if device.timed_out then
                report_timeout_error
                end
                std.output.put_string (device.last_string)
                end
                rescue
                if Exceptions.is_developer_exception then
                std.error.put_string (Exceptions.developer_exception_name)
                std.error.put_new_line
                else
                std.error.put_string ("An unknown exception was received")
                std.error.put_new_line
                end
                Exceptions.die (1)
                end

                And now for some more preachy Eiffel-isms. Errors and exceptions have
                well defined meanings (although I may not be able to define them
                well). Errors are bad things that the program anticipates throughout
                the execution of that program, but do not violate any contracts.
                Exceptions are bad things that the program does not anticipate
                (except in usually one exception handler) and are the result of a
                potential or real contract violation.

                You'll notice that there is no `close' call in the above code. The
                close is taken care of on the 'destructor' of the COM_DEVICE object.
                I guess this is the part you were looking for:

                class
                COM_DEVICE

                inherit

                ...

                MEMORY
                redefine dispose end

                ...

                create
                make

                ...

                feature -- Removal

                dispose is
                -- If the device is still open then close it
                do
                if is_open then
                close
                end
                ensure then
                is_closed: not is_open
                end

                feature {NONE} -- Errors

                ...

                end -- class COM_DEVICE

                `dispose' is a feature that every class has and is called when the
                object is collected. The default definition is to do nothing.

                Hope that helps,
                Brian
              • Yauheni Akhotnikau
                On Fri, 03 Aug 2007 16:25:20 +0400, Brian Heilig ... Does report_error throw an exception? ...
                Message 7 of 10 , Aug 3, 2007
                View Source
                • 0 Attachment
                  On Fri, 03 Aug 2007 16:25:20 +0400, Brian Heilig <Brian.Heilig@...>
                  wrote:

                  > I have written a library in Eiffel called secom (serial
                  > communication) that implements what you are asking (including clean
                  > up). Here are the relevant sections:
                  >
                  > In the examples the `connect_and_write_to_device' looks like this. It
                  > is the root creation procedure of my root class:
                  >

                  <...fragment skipped...>

                  Does 'report_error' throw an exception?

                  > You'll notice that there is no `close' call in the above code. The
                  > close is taken care of on the 'destructor' of the COM_DEVICE object.
                  > I guess this is the part you were looking for:
                  >

                  <...fragment skipped...>

                  > `dispose' is a feature that every class has and is called when the
                  > object is collected. The default definition is to do nothing.

                  I worked for a Java project where someone freed connections to database in
                  'finalize' method. But as 'dispose' in Eiffel, 'finalize' in Java is
                  called only when object is collected. Sometimes objects had been alive for
                  long time and DB connection pool had never got connection back.

                  --
                  Regards,
                  Yauheni Akhotnikau
                  Senior Programmer
                  Intervale
                  e-mail:eao197@... <mailto:eao197@...>
                • Heilig, Brian - SSD
                  Message 8 of 10 , Aug 3, 2007
                  View Source
                  • 0 Attachment
                    > Does 'report_error' throw an exception?

                    No, but it does quit the application.

                    > I worked for a Java project where someone freed connections to
                    > database in 'finalize' method. But as 'dispose' in Eiffel, 'finalize'
                    > in Java is called only when object is collected. Sometimes objects
                    > had been alive for long time and DB connection pool had never got
                    > connection back.

                    That you say "it never got the connection back" seems like a problem of
                    either the garbage collector, or the application had a dangling
                    reference. But you are correct that a garbage collector does not allow
                    the developer to ignore resource management in all cases. But I've
                    personally never had a situation where it made a difference.

                    Brian
                    *****************************************************************
                    This e-mail and any files transmitted with it may be proprietary
                    and are intended solely for the use of the individual or entity to
                    whom they are addressed. If you have received this e-mail in
                    error please notify the sender. Please note that any views or
                    opinions presented in this e-mail are solely those of the author
                    and do not necessarily represent those of ITT Corporation. The
                    recipient should check this e-mail and any attachments for the
                    presence of viruses. ITT accepts no liability for any damage
                    caused by any virus transmitted by this e-mail.
                    *******************************************************************
                  • Yauheni Akhotnikau
                    On Fri, 03 Aug 2007 17:12:32 +0400, Heilig, Brian - SSD ... No there wes no problem with garbage collector, there was a problem with program design: in case of
                    Message 9 of 10 , Aug 3, 2007
                    View Source
                    • 0 Attachment
                      On Fri, 03 Aug 2007 17:12:32 +0400, Heilig, Brian - SSD
                      <Brian.Heilig@...> wrote:

                      >> I worked for a Java project where someone freed connections to
                      >> database in 'finalize' method. But as 'dispose' in Eiffel, 'finalize'
                      >> in Java is called only when object is collected. Sometimes objects
                      >> had been alive for long time and DB connection pool had never got
                      >> connection back.
                      >
                      > That you say "it never got the connection back" seems like a problem of
                      > either the garbage collector, or the application had a dangling
                      > reference.

                      No there wes no problem with garbage collector, there was a problem with
                      program design: in case of error object who own DB connection do not
                      return it to connection pool. So next time pool returns next connection
                      and in the case of error that connection didn't return to pool. After
                      several error there was no available connection in the pool. Because of
                      that program just refused all new clients and do nothing. Because nothing
                      was doing no memory operation had been envolved. And garbage collector
                      didn't work. It was almost 7 years ago, may be now GC started on
                      periodical base.

                      --
                      Regards,
                      Yauheni Akhotnikau
                      Senior Programmer
                      Intervale
                      e-mail:eao197@... <mailto:eao197@...>
                    • Emil Mocan
                      ... Hello, No, I proposed something different but after a little thought I realized I did not resolved the problem. From OOSC (page 309-310) I see that the
                      Message 10 of 10 , Aug 3, 2007
                      View Source
                      • 0 Attachment
                        Yauheni Akhotnikau wrote:
                        > So you propose something like that:
                        >
                        > class
                        > DEVICE
                        > create
                        > make
                        > feature -- Initialization
                        > make (a_connection: DEVICE_CONNECTION) is
                        > do
                        > connection := a_connection
                        > end
                        > feature -- Status
                        > is_ok: BOOLEAN
                        > feature -- Commands
                        > initialize is
                        > local
                        > is_exception: BOOLEAN
                        > do
                        > if not is_exception then
                        > ... -- some actions
                        > end
                        > is_ok := not is_exception
                        > rescue
                        > is_exception := True
                        > retry
                        > end
                        > write_data (data_to_send: DATA_TO_SEND) is
                        > local
                        > is_exception: BOOLEAN
                        > do
                        > if not is_exception then
                        > ... -- some actions
                        > end
                        > is_ok := not is_exception
                        > rescue
                        > is_exception := True
                        > retry
                        > end
                        > feature {NONE}
                        > connection: DEVICE_CONNECTION
                        > end
                        >
                        > ... -- somewhere in another class
                        > connect_and_write_to_device (
                        > device_params: DEVICE_PARAMS;
                        > data_to_send: DATA_TO_SEND) is
                        > local
                        > device_connector: DEVICE_CONNECTOR
                        > device: DEVICE_CONNECTOR
                        > do
                        > create device_connector.make (device_params)
                        > device_connector.connect
                        > is_ok := device_connector.is_connected
                        > if is_ok then
                        > create device.make (device_connector.connection)
                        > device.initialize
                        > is_ok := device.is_ok
                        > if is_ok then
                        > device.write_data (data_to_send)
                        > is_ok := device.is_ok
                        > end
                        > end
                        > end
                        > is_ok: BOOLEAN
                        >
                        >
                        > Is I've understood you correctly?
                        >
                        >
                        Hello,

                        No, I proposed something different but after a little thought I realized
                        I did not resolved the problem.
                        From OOSC (page 309-310) I see that the easy way is to inherit the
                        DEVICE_CONNECTOR class from MEMORY. We can go back to the original code
                        you proposed.
                        What we have to do is to redefine the /dispose/ procedure in
                        DEVICE_CONNECTOR class in order to cleanup the resources used - in this
                        case, to close the connection. This works when the instance of
                        DEVICE_CONNECTOR is reclaimed by garbage collector.
                        One thing I don't know and don't have time to experiment: _In the case
                        of an exception the instance is reclaimed and the /dispose/ procedure
                        gets executed?_

                        Regards,
                        Emil.

                        class
                        DEVICE_CONNECTOR
                        inherit
                        MEMORY
                        redefine
                        dispose
                        end
                        create
                        make
                        feature -- Removal
                        dispose is
                        -- Action to be executed just before garbage collection
                        -- reclaims an object.
                        -- Default version does nothing; redefine in descendants
                        -- to perform specific dispose actions. Those actions
                        -- should only take care of freeing external resources;
                        -- they should not perform remote calls on other objects
                        -- since these may also be dead and reclaimed.
                        do
                        if opened then
                        close_connection
                        end
                        end
                        feature -- Status
                        opened: BOOLEAN

                        -- Other procedures and queries
                        end

                        ----------------------------------------

                        Yauheni Akhotnikau wrote:

                        As I can see now in Eiffel I can do that such way:

                        connect_and_write_to_device (
                        device_params: DEVICE_PARAMS;
                        data_to_send: DATA_TO_SEND) is
                        local
                        device_connector: DEVICE_CONNECTOR
                        device: DEVICE
                        do
                        create device_connector.make (device_params)
                        device_connector.connect
                        if device_connector.is_connected then
                        create device.make (device_connector.connection)
                        device.initialize
                        device.write_data (data_to_send)
                        device_connector.close_connection
                        end
                        rescue
                        if device_connector /= Void and then device_connector.is_connected then
                        device_connector.close_connection
                        end
                        end

                        But there are two calls of 'close_connection'. So it's easy to forget to
                        write one.




                        [Non-text portions of this message have been removed]
                      Your message has been successfully submitted and would be delivered to recipients shortly.