Transaction handling in PsqlODBC does not work - Mailing list pgsql-odbc

From Zoltan Boszormenyi
Subject Transaction handling in PsqlODBC does not work
Date
Msg-id 428D210B.7090508@dunaweb.hu
Whole thread Raw
List pgsql-odbc
Hi,

I am experimenting with transactions using SELECT FOR UPDATE.

I created and populated a table:

CREATE mytable1 (id serial primary key, t varchar(10));
INSERT INTO mytable1 (t) values ('A');
INSERT INTO mytable1 (t) values ('B');
INSERT INTO mytable1 (t) values ('C');
INSERT INTO mytable1 (t) values ('D');

 From two psql sessions, I can do

BEGIN WORK;
SELECT t FROM mytable1 WHERE id=2 FOR UPDATE;
ROLLBACK;

The second session correctly block on the SELECT FROM
statement until I issue a ROLLBACK or COMMIT in the
first session but I can also abort the statement
by pressing Ctrl-C in the second psql session.

I thought I try to do the same in ODBC using SQLCancel().

It would be useful e.g. to display a "please wait" dialog
before executing a SELECT FOR UPDATE and immediately closing it
when the statement executed normally but allow the user
to abort the operation when it blocks, e.g. when another user
is just modifying the same record and be able to return to
the previous state.

My test program doesn't work the way I expect although
it behaves the same with both built-in PostgreSQL drivers in
unixODBC and with the official 8.00.0101 psqlodbc.so.
At least it's consistent. :-)

The server is PostgreSQL-8.0.3 on FC3/AMD64.

I attached my program, someone may enlighten my why it doesn't work.
It may also be a bug in my program.

The program has three mandatory options: DSN, userid and password
and an optional fourth, a string that may contain these letters:
B = issue BEGIN WORK before the SELECT FOR UPDATE
R = issue ROLLBACK after the SELECT FOR UPDATE
O = issue ROLLBACK using SQLEndTran(), overrides R
C = try to cancel the second (blocked) connection
A = execute SELECTs asynchronously

The program works like this:

Allocate two ODBC environment handles and two connection handles
for both environments. Both connection is set to use ODBC 3.0+
functions and autocommit disabled. The three threads are created,
their start time is separated by 2 seconds. The first two thread
try to do the same sequence as above:

BEGIN WORK; (optional, option B)
SELECT t FROM mytable1 WHERE id=2 FOR UPDATE;
ROLLBACK; (optional, option R or O)

The third thread tries to abort the SELECT FOR UPDATE in the
second (blocked) thread using SQLCancel(). The long running time
(the blocked state) of the second thread ensures that global
variable hStmt2 is valid.

When the threads finish, the program cleans up. Problem is,
it finishes only when there is no transaction.

Bug #1:

Despite I set SQL_AUTOCOMMIT_OFF on both connection,
the SELECT FOR UPDATE doesn't block on my second connection,
e.g. the ODBC driver doesn't issue BEGIN WORK before executing
a statement outside of a transaction. I had the impression
it's implicitely included. It isn't the case. And it seems
neither SQLTransact() nor SQLEndTran() issues "BEGIN WORK",
only the COMMIT or ROLLBACK they were instructed.
So the letter B is required. With letter B given on the commandline,
my second connection blocks correctly on SELECT FOR UPDATE.

Bug #2:

With letters B and C given, SQLCancel() blocks in another thread
for the second connection, regardless of the presence or lack of
option 'A', e.g. both in sync or async mode.

Bug #3:

With letters B and R given, the first connection blocks in
SQLAllocHandle() that should allocate the statement handle
for the manual ROLLBACK.

Bug #4:

With letters B and O given, the first connection blocks in
SQLEndTran().

Bug #3 and #4 happens with either given or omitted 'C' option,
i.e. with or without SQLCancel() and with or without 'A', sync
or async mode.

It must be a problem in my program, I don't believe noone
uses transactions from ODBC on PostgreSQL servers.

BTW anyone can lookup page 303 in the ODBC 3.5 Developer's
Guide, where is says that ODBC API funtions should be executed
asynchronously only on single-threaded OSs, on multi-threaded
OSs ODBC API functions should be called in separate threads,
which I just did. For me, it means I don't have to set
statement attribute SQL_ASYNC_ENABLE to SQL_ASYNC_ENABLE_ON.

Best regards,
Zoltán Böszörményi
/*
 * Test whether SQLCancel() really works
 * from another thread on a blocked transaction.
 *
 * Create and populate a test table in advance:
 *
 * CREATE mytable1 (id serial primary key, t varchar(10));
 * INSERT INTO mytable1 (t) values ('A');
 * INSERT INTO mytable1 (t) values ('B');
 * INSERT INTO mytable1 (t) values ('C');
 * INSERT INTO mytable1 (t) values ('D');
 *
 * and create an ODBC DSN four your database.
 *
 * Compilation:
 *
 * gcc4 -Wall -o test-sqlcancel test-sqlcancel.c -lpthread -lodbc
 *
 * Usage:
 *
 * ./test-sqlcancel DSN USERNAME PASSWORD
 *
 */

#define _REENTRANT

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sql.h>
#include <sqlext.h>
#include <pthread.h>

#define ODBC2    1


SQLHENV        hEnv1, hEnv2;
SQLHDBC        hCon1, hCon2;
SQLHSTMT    hStmt1, hStmt2;
pthread_t    thr1, thr2, thr3;
int        begintran, endtran, canceltran, asynctran;
char        *sqlstmt = "select t from mytable1 where id=2 for update;";
char        *dsn, *uid, *pwd;

void *upd_thread1(void *dummy) {
  SQLRETURN     ret;
  SQLHSTMT    hStmt_tr;
  char        t[64];

  memset(t, 0, sizeof(t));
#if 0 /* This doesn't work here. */
#if ODBC2
  ret = SQLTransact(SQL_NULL_HDBC, hCon1, SQL_ROLLBACK);
#else
  ret = SQLEndTran(SQL_HANDLE_DBC, hCon1, SQL_ROLLBACK);
#endif
  printf("%s():%d after BEGIN WORK; ret = 0x%02x\n", __FUNCTION__, __LINE__, ret);
#endif
  if (begintran) {
    ret = SQLAllocHandle(SQL_HANDLE_STMT, hCon1, &hStmt_tr);
    ret = SQLExecDirect(hStmt_tr, (SQLPOINTER)"begin work;", SQL_NTS);
    SQLFreeHandle(SQL_HANDLE_STMT, hStmt_tr);
    printf("%s():%d after BEGIN WORK; ret = 0x%02x\n", __FUNCTION__, __LINE__, ret);
  }

  ret = SQLAllocHandle(SQL_HANDLE_STMT, hCon1, &hStmt1);

  if (asynctran) {
    ret = SQLSetStmtOption(hStmt1, SQL_ASYNC_ENABLE, SQL_ASYNC_ENABLE_ON);
  printf("%s():%d after SQLSetStmtOption() ret=0x%02x\n", __FUNCTION__, __LINE__, ret);
  }

  ret = SQLExecDirect(hStmt1, (SQLPOINTER)sqlstmt, SQL_NTS);
  printf("%s():%d before SQLFetch()\n", __FUNCTION__, __LINE__);

  ret = SQLBindCol(hStmt1, 1, SQL_C_CHAR, (SQLPOINTER)t, sizeof(t), NULL);

  ret = SQLFetch(hStmt1);
  printf("%s():%d after SQLFetch(), ret = 0x%02x, t='%s'\n", __FUNCTION__, __LINE__, ret, t);

  usleep(10000000);
  printf("%s():%d after usleep(10000000)\n", __FUNCTION__, __LINE__);

  if (endtran == 2) {
#if ODBC2
    ret = SQLTransact(SQL_NULL_HDBC, hCon1, SQL_ROLLBACK);
#else
    ret = SQLEndTran(SQL_HANDLE_DBC, hCon1, SQL_ROLLBACK);
#endif
  } else if (endtran == 1) {
    ret = SQLAllocHandle(SQL_HANDLE_STMT, hCon1, &hStmt_tr);
    printf("%s():%d after AllocHandle for ROLLBACK; ret = 0x%02x\n", __FUNCTION__, __LINE__, ret);
    ret = SQLExecDirect(hStmt_tr, (SQLPOINTER)"rollback;", SQL_NTS);
    printf("%s():%d after ROLLBACK; ret = 0x%02x\n", __FUNCTION__, __LINE__, ret);
    SQLFreeHandle(SQL_HANDLE_STMT, hStmt_tr);
  }

  SQLFreeHandle(SQL_HANDLE_STMT, hStmt1);
  printf("%s():%d after SQLFreeHandle()\n", __FUNCTION__, __LINE__);

  pthread_exit(0);
  return NULL;
}

void *upd_thread2(void *dummy) {
  SQLRETURN     ret;
  SQLHSTMT    hStmt_tr;
  char        t[64];

  memset(t, 0, sizeof(t));

#if 0 /* This doesn't work here. */
#if ODBC2
  ret = SQLTransact(SQL_NULL_HDBC, hCon2, SQL_ROLLBACK);
#else
  ret = SQLEndTran(SQL_HANDLE_DBC, hCon2, SQL_ROLLBACK);
#endif
  printf("%s():%d after BEGIN WORK; ret = 0x%02x\n", __FUNCTION__, __LINE__, ret);
#endif
  if (begintran) {
    ret = SQLAllocHandle(SQL_HANDLE_STMT, hCon2, &hStmt_tr);
    ret = SQLExecDirect(hStmt_tr, (SQLPOINTER)"begin work;", SQL_NTS);
    SQLFreeHandle(SQL_HANDLE_STMT, hStmt_tr);
    printf("%s():%d after BEGIN WORK; ret = 0x%02x\n", __FUNCTION__, __LINE__, ret);
  }

  ret = SQLAllocHandle(SQL_HANDLE_STMT, hCon2, &hStmt2);

  if (asynctran) {
    ret = SQLSetStmtOption(hStmt2, SQL_ASYNC_ENABLE, SQL_ASYNC_ENABLE_ON);
  printf("%s():%d after SQLSetStmtOption() ret=0x%02x\n", __FUNCTION__, __LINE__, ret);
  }

  ret = SQLExecDirect(hStmt2, (SQLPOINTER)sqlstmt, SQL_NTS);
  printf("%s():%d before SQLFetch()\n", __FUNCTION__, __LINE__);

  ret = SQLBindCol(hStmt2, 1, SQL_C_CHAR, (SQLPOINTER)t, sizeof(t), NULL);

  ret = SQLFetch(hStmt2);
  printf("%s():%d after SQLFetch(), ret = 0x%02x, t='%s'\n", __FUNCTION__, __LINE__, ret, t);

  if (endtran == 2) {
#if ODBC2
    ret = SQLTransact(SQL_NULL_HDBC, hCon2, SQL_ROLLBACK);
#else
    ret = SQLEndTran(SQL_HANDLE_DBC, hCon2, SQL_ROLLBACK);
#endif
    printf("%s():%d after ROLLBACK, ret = 0x%02x\n", __FUNCTION__, __LINE__, ret);
  } else if (endtran == 1) {
    ret = SQLAllocHandle(SQL_HANDLE_STMT, hCon2, &hStmt_tr);
    printf("%s():%d after AllocHandle for ROLLBACK; ret = 0x%02x\n", __FUNCTION__, __LINE__, ret);
    ret = SQLExecDirect(hStmt_tr, (SQLPOINTER)"rollback;", SQL_NTS);
    printf("%s():%d after ROLLBACK; ret = 0x%02x\n", __FUNCTION__, __LINE__, ret);
    SQLFreeHandle(SQL_HANDLE_STMT, hStmt_tr);
  }

  SQLFreeHandle(SQL_HANDLE_STMT, hStmt2);
  printf("%s():%d after SQLFreeHandle()\n", __FUNCTION__, __LINE__);

  pthread_exit(0);
  return NULL;
}

void *cancel_thread(void *dummy) {
  SQLRETURN    ret;

  printf("%s():%d before SQLCancel()\n", __FUNCTION__, __LINE__);
  ret = SQLCancel(hStmt2);
  printf("%s():%d after SQLCancel() ret = 0x%02x\n", __FUNCTION__, __LINE__, ret);

  pthread_exit(0);
  return NULL;
}

int main(int argc, char **argv) {
  SQLRETURN    ret;
  int        err, i;

  if (argc != 4 && argc != 5) {
    printf("Usage: test-sqlcancel dsn uid pwd [B[R[O]]]\n");
    printf("B = issue BEGIN WORK before the SELECT FOR UPDATE\n");
    printf("R = issue ROLLBACK after the SELECT FOR UPDATE\n");
    printf("O = issue ROLLBACK using SQLEndTran(), implicitely activates R\n");
    printf("C = try to cancel the second (blocked) connection\n");
    printf("A = execute SELECTs asynchronously\n");
    return 0;
  }
  dsn = argv[1];
  uid = argv[2];
  pwd = argv[3];
  begintran = endtran = canceltran = asynctran = 0;
  if (argc == 5) {
    for (i = 0; i < strlen(argv[4]); i++) {
      switch (argv[4][i]) {
      case 'B':
        begintran = 1;
        break;
      case 'R':
        if (!endtran)
          endtran = 1;
        break;
      case 'O':
        if (endtran != 2)
          endtran = 2;
        break;
      case 'C':
        canceltran = 1;
        break;
      case 'A':
        asynctran = 1;
        break;
      default:
        break;
      }
    }
  }

  ret = SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &hEnv1);
  if (ret == SQL_ERROR)
    return 0;

  ret = SQLSetEnvAttr(hEnv1, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0);
  if (ret == SQL_ERROR)
    goto out_henv1;

  ret = SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &hEnv2);
  if (ret == SQL_ERROR)
    goto out_henv1;

  ret = SQLSetEnvAttr(hEnv2, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0);
  if (ret == SQL_ERROR)
    goto out_henv2;

  ret = SQLAllocHandle(SQL_HANDLE_DBC, hEnv1, &hCon1);
  if (ret == SQL_ERROR)
    goto out_henv2;

  ret = SQLConnect(hCon1,
          (SQLPOINTER)dsn, SQL_NTS,
          (SQLPOINTER)uid, SQL_NTS,
          (SQLPOINTER)pwd, SQL_NTS);
  if (ret == SQL_ERROR)
    goto out_hcon1;

  ret = SQLSetConnectAttr(hCon1, SQL_ATTR_AUTOCOMMIT,
          (SQLPOINTER)SQL_AUTOCOMMIT_OFF, SQL_IS_UINTEGER);
  if (ret == SQL_ERROR)
    goto out_discon1;

  ret = SQLAllocHandle(SQL_HANDLE_DBC, hEnv2, &hCon2);
  if (ret == SQL_ERROR)
    goto out_discon1;

  ret = SQLConnect(hCon2,
          (SQLPOINTER)dsn, SQL_NTS,
          (SQLPOINTER)uid, SQL_NTS,
          (SQLPOINTER)pwd, SQL_NTS);
  if (ret == SQL_ERROR)
    goto out_hcon2;

  ret = SQLSetConnectAttr(hCon2, SQL_ATTR_AUTOCOMMIT,
          (SQLPOINTER)SQL_AUTOCOMMIT_OFF, SQL_IS_UINTEGER);
  if (ret == SQL_ERROR)
    goto out_discon2;

  err = pthread_create(&thr1, NULL, upd_thread1, NULL);
  usleep(2000000);
  err = pthread_create(&thr2, NULL, upd_thread2, NULL);

  if (canceltran) {
    usleep(2000000);
    err = pthread_create(&thr3, NULL, cancel_thread, NULL);
    pthread_join(thr3, NULL);
  }

  pthread_join(thr2, NULL);
  pthread_join(thr1, NULL);

  printf("threads exited\n");

out_discon2:
  SQLDisconnect(hCon2);
out_hcon2:
  SQLFreeHandle(SQL_HANDLE_DBC, hCon2);
out_discon1:
  SQLDisconnect(hCon1);
out_hcon1:
  SQLFreeHandle(SQL_HANDLE_DBC, hCon1);
out_henv2:
  SQLFreeHandle(SQL_HANDLE_ENV, hEnv2);
out_henv1:
  SQLFreeHandle(SQL_HANDLE_ENV, hEnv1);
  return 0;
}

pgsql-odbc by date:

Previous
From: Jeff Eckermann
Date:
Subject: Re: My MS-Access problem keeps getting weirder and
Next
From: Raul Carolus
Date:
Subject: Note about ODBC drivers on a Windows Terminal Server Environment