Thread: Avoiding Statement.cancel() races

Avoiding Statement.cancel() races

From
Oliver Jowett
Date:
I'm trying to work out how to avoid race conditions when using cancel() to
do graceful shutdown of a DB access thread, but it seems like there's a race
condition in the JDBC API itself.

I have a worker thread doing DB work. Another thread decides that it wants
to stop this worker thread (in our case, due to cluster partition). So it
does the usual set-flag-and-notify trick to inform the DB thread.

However that worker thread might be currently running a query -- and in turn
that query may block for an extended period of time (for example, if it's
waiting on a backend lock that's held by a client connection from the other
side of the partition, then it'll only continue when that other client
connection finally dies which could take up to the TCP abort interval).

I'd like to write code that looks something like this:

  private volatile boolean stopping = false;
  private Statement currentStatement = null;

  public void run() {
     // Worker thread
     while (!stopping) {
       // ... wait() for more work ...

       synchronized (this) {
         if (stopping) throw new SQLException("db thread cancelled");
         currentStatement = /* ... */;
       }
       results = currentStatement.executeUpdate();
       synchronized (this) {
         currentStatement = null;
       }
       // ...
    }
  }

  public void stopWorkerThread() {
     synchronized (this) {
       stopping = true;
       if (currentStatement != null) currentStatement.cancel();
       this.notify();
     }
  }

However there's an obvious race condition here between starting query
execution and cancelling the statement -- it's possible for
stopWorkerThread() to run at just the wrong time and "cancel" the statement
before it starts execution, having no effect and leaving the worker thread
still running a query (at least as I understand cancel() .. that's how it's
implemented in the postgresql driver anyway).

Extending the synchronized block to enclose 'someStatement.execute()'
doesn't help since then stopWorkerThread() can't enter that monitor while
the query is executing, which defeats the purpose of doing this in the first
place.

This seems like a design flaw in the JDBC API. Is there a way to solve this
problem that I'm missing?

Perhaps cancel() should be "permanent" i.e. once cancelled, a Statement
cannot be reused: all subsequent attempts at query execution are immediately
cancelled. This seems simpler, but I'd have expected any behaviour like this
to be explicitly spelled out in the cancel() javadoc if it was intended to
work that way.

Alternatively, we could add a cancelPermanently() method to PGstatement that
has this behaviour as a postgresql extension.

Any thoughts?

-O

Re: Avoiding Statement.cancel() races

From
Kris Jurka
Date:

On Fri, 21 Nov 2003, Oliver Jowett wrote:

> However there's an obvious race condition here between starting query
> execution and cancelling the statement -- it's possible for
> stopWorkerThread() to run at just the wrong time and "cancel" the statement
> before it starts execution, having no effect and leaving the worker thread
> still running a query (at least as I understand cancel() .. that's how it's
> implemented in the postgresql driver anyway).

What if calling cancel on a non running statement threw an SQLException?
Then your stopWorkerThread could retry the cancel.

Kris Jurka


Re: Avoiding Statement.cancel() races

From
Oliver Jowett
Date:
On Fri, Nov 21, 2003 at 06:18:07AM -0500, Kris Jurka wrote:
>
>
> On Fri, 21 Nov 2003, Oliver Jowett wrote:
>
> > However there's an obvious race condition here between starting query
> > execution and cancelling the statement -- it's possible for
> > stopWorkerThread() to run at just the wrong time and "cancel" the statement
> > before it starts execution, having no effect and leaving the worker thread
> > still running a query (at least as I understand cancel() .. that's how it's
> > implemented in the postgresql driver anyway).
>
> What if calling cancel on a non running statement threw an SQLException?
> Then your stopWorkerThread could retry the cancel.

That might work. You'd need a separate SQLException subclass (or SQL state
code) to distinguish "no statement running" from "cancel failed".

I'm not sure if other applications would expect this behaviour, though.
Also, I don't really like spinning on a retry -- you become quite dependent
on the fairness of the JVMs threading/locking model.

-O