[python-sybase] Connection unusable after an exception has been raised

Dave Cole djc at object-craft.com.au
Tue, 16 Nov 2004 10:37:52 +1100


Skip Montanaro wrote:
> If a cursor's execute() method results in an error (say, from a trigger
> failing for some reason) it appears there is no recourse to continue other
> than closing the connection and starting over.  Here's the simple test case:
> 
>     c = Sybase.Connection(...)
>     c1 = c.cursor()
>     c1.execute("select * from blah")
>     c2 = c.cursor()
>     c2.execute("select 1")
> 
> The first execute() call results in a DatabaseError exception because there
> is no column "blah".  The second execute() call also fails with a
> DatabaseError, this time in ct_send.  The message is:
> 
>     ct_send(): user api layer: external error: This routine cannot be called
>     because another command structure has results pending.
> 
> How do I toss those pending results?  I've tried wrapping the c1 calls in
> c.begin()/c.rollback() calls.  I've tried closing and/or deleting c1.  I've
> tried fetching the nonexistent results of the failed execute() call.
> Nothing seems to make Sybase happy again until a new connection is created.

I think I know what is causing this.  If someone could do an experiment 
for me I will make the change and cut a new release (with the output 
hook patch as well).

The cursor class uses a _FetchLazy object to perform lazy fetching of 
rows from the server.  The _FetchLazy object has an internal state which 
tracks the current state of the connection being used to fetch results. 
  It is constructed in the IDLE state.  Looking at the Cursor.execute() 
method:

     # Discard any previous results
     self._fetcher = None

     # Prepare to retrieve new results.
     fetcher = self._fetcher = _FetchLazy(self._owner)
     cmd = fetcher._cmd
     cmd.ct_command(CS_LANG_CMD, sql)
     for name, value in params.items():
         buf = DataBuf(value)
         buf.name = name
         status = cmd.ct_param(buf)
         if status != CS_SUCCEED:
             fetcher._raise_error(Error, 'ct_param')
     self.description = fetcher.start(self.arraysize)

If an exception is raised during either the ct_command() function or 
ct_send() the fetcher is still in the IDLE state.  This means that when 
the next execute() is performed and the previous fetcher is discarded, 
no cancel action is performed.

There are two possible ways to attempt a fix.

1) Construct the Fetcher in the FETCHING state.  This will mean that any 
exception raised in the ct_command() or the ct_send() function will 
result in ct_cancel(CS_CANCEL_ALL) when the Fetcher is deleted.

     In _FetchLazy.__init__() change:
         self._state = _LAZY_IDLE
     to
         self._state = _LAZY_FETCHING

2) Set the Fetcher state to FETCHING immediately before calling 
ct_send() in _FetchLazy.start().

     In _FetchLazy.start() change:
         status = self._cmd.ct_send()
     to
         self._set_state(_LAZY_FETCHING)
         status = self._cmd.ct_send()

For various reasons I do not currently have a working Sybase server at 
the moment so would appreciate it if someone else could make the change 
and test for me.

- Dave

-- 
http://www.object-craft.com.au