• Re: psycopg2: proper positioning of .commit() within try: except: bloc

    From Rob Cliffe@3:633/280.2 to All on Sun Sep 8 02:11:57 2024
    Subject: Re: psycopg2: proper positioning of .commit() within try: except:
    blocks

    On 07/09/2024 16:48, Karsten Hilbert via Python-list wrote:
    Dear all,

    unto now I had been thinking this is a wise idiom (in code
    that needs not care whether it fails to do what it tries to
    do^1):

    conn = psycopg2.connection(...)
    curs = conn.cursor()
    try:
    curs.execute(SOME_SQL)
    except PSYCOPG2-Exception:
    some logging being done, and, yes, I
    can safely inhibit propagation^1
    finally:
    conn.commit() # will rollback, if SOME_SQL failed
    conn.close()

    So today I head to learn that conn.commit() may very well
    raise a DB related exception, too:

    psycopg2.errors.SerializationFailure: could not serialize access due to read/write dependencies among transactions
    DETAIL: Reason code: Canceled on identification as a pivot, during commit attempt.
    TIP: The transaction might succeed if retried.

    Now, what is the proper placement of the .commit() ?

    (doing "with ... as conn:" does not free me of committing appropriately)

    Should I

    try:
    curs.execute(SOME_SQL)
    conn.commit()
    except PSYCOPG2-Exception:
    some logging being done, and, yes, I
    can safely inhibit propagation^1
    finally:
    conn.close() # which should .rollback() automagically in case we had not reached to .commit()

    ?

    Thanks for insights,
    Karsten
    I would put the curs.execute and the conn.commit in separate
    try...except blocks.ÿ That way you know which one failed, and can put appropriate info in the log, which may help trouble-shooting.
    (The general rule is to keep try...except blocks small.ÿ And of course
    only catch the exceptions you are interested in, which you seem to be
    already doing.)
    Best wishes
    Rob Cliffe

    --- MBSE BBS v1.0.8.4 (Linux-x86_64)
    * Origin: ---:- FTN<->UseNet Gate -:--- (3:633/280.2@fidonet)
  • From Karsten Hilbert@3:633/280.2 to All on Sun Sep 8 05:44:36 2024
    Subject: Re: psycopg2: proper positioning of .commit() within try: except:
    blocks

    Am Sat, Sep 07, 2024 at 09:46:03AM -0700 schrieb Adrian Klaver:

    unto now I had been thinking this is a wise idiom (in code
    that needs not care whether it fails to do what it tries to
    do^1):

    conn =3D psycopg2.connection(...)

    In the above do you have:

    https://www.psycopg.org/docs/extensions.html#psycopg2.extensions.ISOLATI=
    ON_LEVEL_SERIALIZABLE

    psycopg2.extensions.ISOLATION_LEVEL_SERIALIZABLE

    I do indeed.

    Or is that in some other concurrent transaction?

    In fact in that codebase all transactions -- running
    concurrently or not -- are set to SERIALIZABLE.

    They are not psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT,
    for that matter.

    curs =3D conn.cursor()
    try:
    curs.execute(SOME_SQL)
    except PSYCOPG2-Exception:
    some logging being done, and, yes, I
    can safely inhibit propagation^1
    finally:
    conn.commit() # will rollback, if SOME_SQL failed

    It will if you use with conn:, otherwise it up to you to do the rollback=
    ()

    Are you are doing a rollback() in except PSYCOPG2-Exception: ?

    No I don't but - to my understanding - an ongoing transaction
    is being closed upon termination of the hosting connection.
    Unless .commit() is explicitely being issued somewhere in the
    code that closing of a transaction will amount to a ROLLBACK.

    In case of SQL having failed within a given transaction a
    COMMIT will fail-but-rollback, too (explicit ROLLBACK would
    succeed while a COMMIT would fail and, in-effect, roll back).

    IOW, when SOME_SQL has failed it won't matter that I close
    the connection with conn.commit() and it won't matter that
    conn.commit() runs a COMMIT on the database -- an open
    transaction having run that failed SQL will still roll back
    as if ROLLBACK had been issued. Or else my mental model is
    wrong.

    https://www.psycopg.org/docs/connection.html#connection.close

    In the particular case I was writing about the SQL itself
    succeeded but then the COMMIT failed due to serialization. I
    was wondering about where to best place any needed
    conn.commit(). My knee-jerk reaction was to then put it last
    in the try: block...

    All this is probably more related to Python than to PostgreSQL.

    Thanks,
    Karsten
    =2D-
    GPG 40BE 5B0E C98E 1713 AFA6 5BC0 3BEA AC80 7D4F C89B

    --- MBSE BBS v1.0.8.4 (Linux-x86_64)
    * Origin: ---:- FTN<->UseNet Gate -:--- (3:633/280.2@fidonet)
  • From Karsten Hilbert@3:633/280.2 to All on Sun Sep 8 06:45:24 2024
    Subject: Re: psycopg2: proper positioning of .commit() within try: except:
    blocks

    Am Sat, Sep 07, 2024 at 01:03:34PM -0700 schrieb Adrian Klaver:

    In the case you show you are doing commit() before the close() so any er=
    rors in the
    transactions will show up then. My first thought would be to wrap the co=
    mmit() in a
    try/except and deal with error there.

    Right, and this was suggested elsewhere ;)

    And, yeah, the actual code is much more involved :-D

    #------------------------------------------------------------------------
    def __safely_close_cursor_and_rollback_close_conn(close_cursor=3DNone, rol= lback_tx=3DNone, close_conn=3DNone):
    if close_cursor:
    try:
    close_cursor()
    except PG_ERROR_EXCEPTION as pg_exc:
    _log.exception('cannot close cursor')
    gmConnectionPool.log_pg_exception_details(pg_exc)
    if rollback_tx:
    try:
    # need to rollback so ABORT state isn't retained in pooled connections
    rollback_tx()
    except PG_ERROR_EXCEPTION as pg_exc:
    _log.exception('cannot rollback transaction')
    gmConnectionPool.log_pg_exception_details(pg_exc)
    if close_conn:
    try:
    close_conn()
    except PG_ERROR_EXCEPTION as pg_exc:
    _log.exception('cannot close connection')
    gmConnectionPool.log_pg_exception_details(pg_exc)

    #------------------------------------------------------------------------
    def run_rw_queries (
    link_obj:_TLnkObj=3DNone,
    queries:_TQueries=3DNone,
    end_tx:bool=3DFalse,
    return_data:bool=3DNone,
    get_col_idx:bool=3DFalse,
    verbose:bool=3DFalse
    ) -> tuple[list[dbapi.extras.DictRow], dict[str, int] | None]:
    """Convenience function for running read-write queries.

    Typically (part of) a transaction.

    Args:
    link_obj: None, cursor, connection
    queries:

    * a list of dicts [{'cmd': <string>, 'args': <dict> or <tuple>)
    * to be executed as a single transaction
    * the last query may usefully return rows, such as:

    SELECT currval('some_sequence');
    or
    INSERT/UPDATE ... RETURNING some_value;

    end_tx:

    * controls whether the transaction is finalized (eg.
    COMMITted/ROLLed BACK) or not, this allows the
    call to run_rw_queries() to be part of a framing
    transaction
    * if link_obj is a *connection* then "end_tx" will
    default to False unless it is explicitly set to
    True which is taken to mean "yes, you do have full
    control over the transaction" in which case the
    transaction is properly finalized
    * if link_obj is a *cursor* we CANNOT finalize the
    transaction because we would need the connection for that
    * if link_obj is *None* "end_tx" will, of course, always
    be True, because we always have full control over the
    connection, not ending the transaction would be pointless

    return_data:

    * if true, the returned data will include the rows
    the last query selected
    * if false, it returns None instead

    get_col_idx:

    * True: the returned tuple will include a dictionary
    mapping field names to column positions
    * False: the returned tuple includes None instead of a field mapping dic=
    tionary

    Returns:

    * (None, None) if last query did not return rows
    * ("fetchall() result", <index>) if last query returned any rows and "re=
    turn_data" was True

    * for *index* see "get_col_idx"
    """
    assert queries is not None, '<queries> must not be None'

    if link_obj is None:
    conn =3D get_connection(readonly =3D False)
    curs =3D conn.cursor()
    conn_close =3D conn.close
    tx_commit =3D conn.commit
    tx_rollback =3D conn.rollback
    curs_close =3D curs.close
    notices_accessor =3D conn
    else:
    conn_close =3D lambda *x: None
    tx_commit =3D lambda *x: None
    tx_rollback =3D lambda *x: None
    curs_close =3D lambda *x: None
    if isinstance(link_obj, dbapi._psycopg.cursor):
    curs =3D link_obj
    notices_accessor =3D curs.connection
    elif isinstance(link_obj, dbapi._psycopg.connection):
    if end_tx:
    tx_commit =3D link_obj.commit
    tx_rollback =3D link_obj.rollback
    curs =3D link_obj.cursor()
    curs_close =3D curs.close
    notices_accessor =3D link_obj
    else:
    raise ValueError('link_obj must be cursor, connection or None but not [=
    %s]' % link_obj)

    for query in queries:
    try:
    args =3D query['args']
    except KeyError:
    args =3D None
    try:
    curs.execute(query['cmd'], args)
    if verbose:
    gmConnectionPool.log_cursor_state(curs)
    for notice in notices_accessor.notices:
    _log.debug(notice.replace('\n', '/').replace('\n', '/'))
    del notices_accessor.notices[:]
    # DB related exceptions
    except dbapi.Error as pg_exc:
    _log.error('query failed in RW connection')
    gmConnectionPool.log_pg_exception_details(pg_exc)
    for notice in notices_accessor.notices:
    _log.debug(notice.replace('\n', '/').replace('\n', '/'))
    del notices_accessor.notices[:]
    __safely_close_cursor_and_rollback_close_conn (
    curs_close,
    tx_rollback,
    conn_close
    )
    # privilege problem ?
    if pg_exc.pgcode =3D=3D PG_error_codes.INSUFFICIENT_PRIVILEGE:
    details =3D 'Query: [%s]' % curs.query.decode(errors =3D 'replace').st=
    rip().strip('\n').strip().strip('\n')
    if curs.statusmessage !=3D '':
    details =3D 'Status: %s\n%s' % (
    curs.statusmessage.strip().strip('\n').strip().strip('\n'),
    details
    )
    if pg_exc.pgerror is None:
    msg =3D '[%s]' % pg_exc.pgcode
    else:
    msg =3D '[%s]: %s' % (pg_exc.pgcode, pg_exc.pgerror)
    raise gmExceptions.AccessDenied (
    msg,
    source =3D 'PostgreSQL',
    code =3D pg_exc.pgcode,
    details =3D details
    )

    # other DB problem
    gmLog2.log_stack_trace()
    raise

    # other exception
    except Exception:
    _log.exception('error running query in RW connection')
    gmConnectionPool.log_cursor_state(curs)
    for notice in notices_accessor.notices:
    _log.debug(notice.replace('\n', '/').replace('\n', '/'))
    del notices_accessor.notices[:]
    gmLog2.log_stack_trace()
    __safely_close_cursor_and_rollback_close_conn (
    curs_close,
    tx_rollback,
    conn_close
    )
    raise

    data =3D None
    col_idx =3D None
    if return_data:
    try:
    data =3D curs.fetchall()
    except Exception:
    _log.exception('error fetching data from RW query')
    gmLog2.log_stack_trace()
    __safely_close_cursor_and_rollback_close_conn (
    curs_close,
    tx_rollback,
    conn_close
    )
    raise

    if get_col_idx:
    col_idx =3D get_col_indices(curs)
    curs_close()
    tx_commit()
    conn_close()
    return (data, col_idx)

    #------------------------------------------------------------------------


    Best,
    Karsten
    =2D-
    GPG 40BE 5B0E C98E 1713 AFA6 5BC0 3BEA AC80 7D4F C89B

    --- MBSE BBS v1.0.8.4 (Linux-x86_64)
    * Origin: ---:- FTN<->UseNet Gate -:--- (3:633/280.2@fidonet)
  • From Karsten Hilbert@3:633/280.2 to All on Sun Sep 8 07:20:57 2024
    Subject: Re: psycopg2: proper positioning of .commit() within try: except:
    blocks

    Am Sat, Sep 07, 2024 at 02:09:28PM -0700 schrieb Adrian Klaver:

    Right, and this was suggested elsewhere ;)

    And, yeah, the actual code is much more involved :-D


    I see that.

    The question is does the full code you show fail?

    The code sample you show in your original post is doing something very d=
    ifferent then
    what you show in your latest post. At this point I do not understand the=
    exact problem
    we are dealing with.

    We are not dealing with an unsolved problem. I had been
    asking for advice where to best place that .commit() call in
    case I am overlooking benefits and drawbacks of choices.

    The

    try:
    do something
    except:
    log something
    finally:
    .commit()

    cadence is fairly Pythonic and elegant in that it ensures the
    the .commit() will always be reached regardless of exceptions
    being thrown or not and them being handled or not.

    It is also insufficient because the .commit() itself may
    elicit exceptions (from the database).

    So there's choices:

    Ugly:

    try:
    do something
    except:
    log something
    finally:
    try:
    .commit()
    except:
    log some more

    Fair but not feeling quite safe:

    try:
    do something
    .commit()
    except:
    log something

    Boring and repetitive and safe(r):

    try:
    do something
    except:
    log something
    try:
    .commit()
    except:
    log something

    I eventually opted for the last version, except for factoring
    out the second try: except: block.

    Best,
    Karsten
    =2D-
    GPG 40BE 5B0E C98E 1713 AFA6 5BC0 3BEA AC80 7D4F C89B

    --- MBSE BBS v1.0.8.4 (Linux-x86_64)
    * Origin: ---:- FTN<->UseNet Gate -:--- (3:633/280.2@fidonet)
  • From Greg Ewing@3:633/280.2 to All on Sun Sep 8 10:48:50 2024
    Subject: Re: psycopg2: proper positioning of .commit() within try: except:
    blocks

    On 8/09/24 9:20 am, Karsten Hilbert wrote:
    try:
    do something
    except:
    log something
    finally:
    .commit()

    cadence is fairly Pythonic and elegant in that it ensures the
    the .commit() will always be reached regardless of exceptions
    being thrown or not and them being handled or not.

    That seems wrong to me. I would have thought the commit should only
    be attempted if everything went right.

    What if there's a problem in your code that causes a non-SQL-related
    exception when some but not all of the SQL statements in the
    transaction bave been issued? The database doesn't know something
    has gone wrong, so it will happily commit a partially-completed
    transaction and possibly corrupt your data.

    This is how I normally do things like this:

    try:
    do something
    .commit()
    except:
    log something
    .rollback()

    Doing an explicit rollback ensures that the transaction is always
    rolled back if it is interrupted for any reason.

    --
    Greg

    --- MBSE BBS v1.0.8.4 (Linux-x86_64)
    * Origin: ---:- FTN<->UseNet Gate -:--- (3:633/280.2@fidonet)
  • From Jon Ribbens@3:633/280.2 to All on Sun Sep 8 21:03:21 2024
    Subject: Re: psycopg2: proper positioning of .commit() within try: except:
    blocks

    On 2024-09-08, Greg Ewing <greg.ewing@canterbury.ac.nz> wrote:
    On 8/09/24 9:20 am, Karsten Hilbert wrote:
    try:
    do something
    except:
    log something
    finally:
    .commit()

    cadence is fairly Pythonic and elegant in that it ensures the
    the .commit() will always be reached regardless of exceptions
    being thrown or not and them being handled or not.

    That seems wrong to me. I would have thought the commit should only
    be attempted if everything went right.

    What if there's a problem in your code that causes a non-SQL-related exception when some but not all of the SQL statements in the
    transaction bave been issued? The database doesn't know something
    has gone wrong, so it will happily commit a partially-completed
    transaction and possibly corrupt your data.

    This is how I normally do things like this:

    try:
    do something
    .commit()
    except:
    log something
    .rollback()

    Doing an explicit rollback ensures that the transaction is always
    rolled back if it is interrupted for any reason.

    What if there's an exception in your exception handler? I'd put the
    rollback in the 'finally' handler, so it's always called. If you've
    already called 'commit' then the rollback does nothing of course.

    --- MBSE BBS v1.0.8.4 (Linux-x86_64)
    * Origin: A noiseless patient Spider (3:633/280.2@fidonet)
  • From Karsten Hilbert@3:633/280.2 to All on Sun Sep 8 21:06:19 2024
    Subject: Re: psycopg2: proper positioning of .commit() within try: except:
    blocks

    Am Sun, Sep 08, 2024 at 12:48:50PM +1200 schrieb Greg Ewing via Python-lis=
    t:

    On 8/09/24 9:20 am, Karsten Hilbert wrote:
    try:
    do something
    except:
    log something
    finally:
    .commit()

    cadence is fairly Pythonic and elegant in that it ensures the
    the .commit() will always be reached regardless of exceptions
    being thrown or not and them being handled or not.

    That seems wrong to me. I would have thought the commit should only
    be attempted if everything went right.

    What if there's a problem in your code that causes a non-SQL-related exception when some but not all of the SQL statements in the
    transaction bave been issued? The database doesn't know something
    has gone wrong, so it will happily commit a partially-completed
    transaction and possibly corrupt your data.

    A-ha !

    try:
    run_some_SQL_that_succeeds()
    print(no_such_name) # tongue-in-cheek
    1 / 0 # for good measure
    except SOME_DB_ERROR:
    print('some DB error, can be ignored for now')
    finally:
    commit()

    which is wrong, given that the failing *Python* statements
    may very well belong into the *business level* "transaction"
    which a/the database transaction is part of.

    See, that's why I was asking in the first place :-)

    I was overlooking implications.

    Karsten
    =2D-
    GPG 40BE 5B0E C98E 1713 AFA6 5BC0 3BEA AC80 7D4F C89B

    --- MBSE BBS v1.0.8.4 (Linux-x86_64)
    * Origin: ---:- FTN<->UseNet Gate -:--- (3:633/280.2@fidonet)
  • From Karsten Hilbert@3:633/280.2 to All on Sun Sep 8 21:13:37 2024
    Subject: Re: psycopg2: proper positioning of .commit() within try: except:
    blocks

    Am Sun, Sep 08, 2024 at 12:48:50PM +1200 schrieb Greg Ewing via Python-lis=
    t:

    On 8/09/24 9:20 am, Karsten Hilbert wrote:
    try:
    do something
    except:
    log something
    finally:
    .commit()

    cadence is fairly Pythonic and elegant in that it ensures the
    the .commit() will always be reached regardless of exceptions
    being thrown or not and them being handled or not.

    That seems wrong to me. I would have thought the commit should only
    be attempted if everything went right.

    It is only attempted when "everything" went right. The fault
    in my thinking was what the "everything" might encompass.
    When some SQL fails it won't matter whether I say
    conn.commit() or conn.rollback() or, in fact, nothing at all
    =2D- the (DB !) transaction will be rolled back in any case.

    However, that reasoning missed this:

    What if there's a problem in your code that causes a non-SQL-related exception when some but not all of the SQL statements in the
    transaction bave been [-- even successfully --] issued?

    Still, in this code pattern:

    try:
    do something
    .commit()
    except:
    log something
    it doesn't technically matter whether I say .commit or .rollback here:
    .rollback()

    .... but ...

    Doing an explicit rollback ensures that the transaction is always
    rolled back if it is interrupted for any reason.

    explicit is better than implicit ;-)

    Karsten
    =2D-
    GPG 40BE 5B0E C98E 1713 AFA6 5BC0 3BEA AC80 7D4F C89B

    --- MBSE BBS v1.0.8.4 (Linux-x86_64)
    * Origin: ---:- FTN<->UseNet Gate -:--- (3:633/280.2@fidonet)
  • From Rob Cliffe@3:633/280.2 to All on Sun Sep 8 23:58:03 2024
    Subject: Re: psycopg2: proper positioning of .commit() within try: except:
    blocks



    On 07/09/2024 22:20, Karsten Hilbert via Python-list wrote:
    Am Sat, Sep 07, 2024 at 02:09:28PM -0700 schrieb Adrian Klaver:

    Right, and this was suggested elsewhere ;)

    And, yeah, the actual code is much more involved :-D

    I see that.

    The question is does the full code you show fail?

    The code sample you show in your original post is doing something very different then
    what you show in your latest post. At this point I do not understand the exact problem
    we are dealing with.
    We are not dealing with an unsolved problem. I had been
    asking for advice where to best place that .commit() call in
    case I am overlooking benefits and drawbacks of choices.

    The

    try:
    do something
    except:
    log something
    finally:
    .commit()

    cadence is fairly Pythonic and elegant in that it ensures the
    the .commit() will always be reached regardless of exceptions
    being thrown or not and them being handled or not.

    It is also insufficient because the .commit() itself may
    elicit exceptions (from the database).

    So there's choices:

    Ugly:

    try:
    do something
    except:
    log something
    finally:
    try:
    .commit()
    except:
    log some more

    Fair but not feeling quite safe:

    try:
    do something
    .commit()
    except:
    log something

    Boring and repetitive and safe(r):

    try:
    do something
    except:
    log something
    try:
    .commit()
    except:
    log something

    I eventually opted for the last version, except for factoring
    out the second try: except: block.

    Best,
    Karsten
    --
    GPG 40BE 5B0E C98E 1713 AFA6 5BC0 3BEA AC80 7D4F C89B
    Unless I'm missing something, the 1st & 3rd versions always do the
    commit() even after the first bit fails, which seems wrong.
    I suggest the 1st version but replacing "finally" by "else".ÿ Then the try-commit-except will not be executed if the "something" fails.
    Perhaps the extra indentation of the second try block is a bit ugly, but
    it is more important that it does the right thing.
    If it is convenient (it may not be) to put the whole thing in a
    function, you may feel that the follwing is less ugly:

    try:
    do something
    except:
    log something
    return
    try:
    .commit()
    except:
    log some more
    return

    Best wishes
    Rob Cliffe


    --- MBSE BBS v1.0.8.4 (Linux-x86_64)
    * Origin: ---:- FTN<->UseNet Gate -:--- (3:633/280.2@fidonet)
  • From Karsten Hilbert@3:633/280.2 to All on Mon Sep 9 00:13:49 2024
    Subject: Re: psycopg2: proper positioning of .commit() within try: except:
    blocks

    Am Sun, Sep 08, 2024 at 02:58:03PM +0100 schrieb Rob Cliffe via Python-lis=
    t:

    Ugly:

    try:
    do something
    except:
    log something
    finally:
    try:
    .commit()
    except:
    log some more

    Fair but not feeling quite safe:

    try:
    do something
    .commit()
    except:
    log something

    Boring and repetitive and safe(r):

    try:
    do something
    except:
    log something
    try:
    .commit()
    except:
    log something

    I eventually opted for the last version, except for factoring
    out the second try: except: block.

    Unless I'm missing something, the 1st & 3rd versions always do the commi=
    t() even after
    the first bit fails, which seems wrong.

    Well, it does run a Python function called "commit". That
    function will call "COMMIT" on the database. The end result
    will be correct (at the SQL level) because the COMMIT will
    not effect a durable commit of data when the SQL in "do
    something" had failed.

    We have, however, elicited that there may well be other
    things belonging into the running business level transaction
    which may fail and which should lead to data not being
    committed despite being technically correct at the SQL level.

    I suggest the 1st version but replacing "finally" by "else".=A0 Then the=
    try-commit-except
    will not be executed if the "something" fails.

    The whole point was to consolidate the commit into one place.
    It is unfortunately named, though. It should be called
    "close_transaction".

    Perhaps the extra indentation of the second try block is a bit ugly, but=
    it is more
    important that it does the right thing.
    If it is convenient (it may not be) to put the whole thing in a function=
    , you may feel
    that the follwing is less ugly:

    The whole thing does reside inside a function but the exit-early pattern

    try:
    do something
    except:
    log something
    return
    try:
    .commit()
    except:
    log some more
    return

    won't help as there's more stuff to be done inside that function.

    Thanks,
    Karsten


    For what it's worth here's the current state of code:

    #------------------------------------------------------------------------
    def __safely_close_cursor_and_rollback_close_conn(close_cursor=3DNone, rol= lback_tx=3DNone, close_conn=3DNone):
    if close_cursor:
    try:
    close_cursor()
    except PG_ERROR_EXCEPTION as pg_exc:
    _log.exception('cannot close cursor')
    gmConnectionPool.log_pg_exception_details(pg_exc)
    if rollback_tx:
    try:
    # need to rollback so ABORT state isn't retained in pooled connections
    rollback_tx()
    except PG_ERROR_EXCEPTION as pg_exc:
    _log.exception('cannot rollback transaction')
    gmConnectionPool.log_pg_exception_details(pg_exc)
    if close_conn:
    try:
    close_conn()
    except PG_ERROR_EXCEPTION as pg_exc:
    _log.exception('cannot close connection')
    gmConnectionPool.log_pg_exception_details(pg_exc)

    #------------------------------------------------------------------------
    def __log_notices(notices_accessor=3DNone):
    for notice in notices_accessor.notices:
    _log.debug(notice.replace('\n', '/').replace('\n', '/'))
    del notices_accessor.notices[:]

    #------------------------------------------------------------------------
    def __perhaps_reraise_as_permissions_error(pg_exc, curs):
    if pg_exc.pgcode !=3D PG_error_codes.INSUFFICIENT_PRIVILEGE:
    return

    # privilege problem -- normalize as GNUmed exception
    details =3D 'Query: [%s]' % curs.query.decode(errors =3D 'replace').strip=
    ().strip('\n').strip().strip('\n')
    if curs.statusmessage !=3D '':
    details =3D 'Status: %s\n%s' % (
    curs.statusmessage.strip().strip('\n').strip().strip('\n'),
    details
    )
    if pg_exc.pgerror is None:
    msg =3D '[%s]' % pg_exc.pgcode
    else:
    msg =3D '[%s]: %s' % (pg_exc.pgcode, pg_exc.pgerror)
    raise gmExceptions.AccessDenied (
    msg,
    source =3D 'PostgreSQL',
    code =3D pg_exc.pgcode,
    details =3D details
    )

    #------------------------------------------------------------------------
    def run_rw_queries (
    link_obj:_TLnkObj=3DNone,
    queries:_TQueries=3DNone,
    end_tx:bool=3DFalse,
    return_data:bool=3DNone,
    get_col_idx:bool=3DFalse,
    verbose:bool=3DFalse
    ) -> tuple[list[dbapi.extras.DictRow], dict[str, int] | None]:
    """Convenience function for running read-write queries.

    Typically (part of) a transaction.

    Args:
    link_obj: None, cursor, connection
    queries:

    * a list of dicts [{'cmd': <string>, 'args': <dict> or <tuple>)
    * to be executed as a single transaction
    * the last query may usefully return rows, such as:

    SELECT currval('some_sequence');
    or
    INSERT/UPDATE ... RETURNING some_value;

    end_tx:

    * controls whether the transaction is finalized (eg.
    COMMITted/ROLLed BACK) or not, this allows the
    call to run_rw_queries() to be part of a framing
    transaction
    * if link_obj is a *connection* then "end_tx" will
    default to False unless it is explicitly set to
    True which is taken to mean "yes, you do have full
    control over the transaction" in which case the
    transaction is properly finalized
    * if link_obj is a *cursor* we CANNOT finalize the
    transaction because we would need the connection for that
    * if link_obj is *None* "end_tx" will, of course, always
    be True, because we always have full control over the
    connection, not ending the transaction would be pointless

    return_data:

    * if true, the returned data will include the rows
    the last query selected
    * if false, it returns None instead

    get_col_idx:

    * True: the returned tuple will include a dictionary
    mapping field names to column positions
    * False: the returned tuple includes None instead of a field mapping dic=
    tionary

    Returns:

    * (None, None) if last query did not return rows
    * ("fetchall() result", <index>) if last query returned any rows and "re=
    turn_data" was True

    * for *index* see "get_col_idx"
    """
    assert queries is not None, '<queries> must not be None'
    assert isinstance(link_obj, (dbapi._psycopg.connection, dbapi._psycopg.cu=
    rsor, type(None))), '<link_obj> must be None, a cursor, or a connection, b=
    ut [%s] is of type (%s)' % (link_obj, type(link_obj))

    if link_obj is None:
    conn =3D get_connection(readonly =3D False)
    curs =3D conn.cursor()
    conn_close =3D conn.close
    tx_commit =3D conn.commit
    tx_rollback =3D conn.rollback
    curs_close =3D curs.close
    notices_accessor =3D conn
    else:
    conn_close =3D lambda *x: None
    tx_commit =3D lambda *x: None
    tx_rollback =3D lambda *x: None
    curs_close =3D lambda *x: None
    if isinstance(link_obj, dbapi._psycopg.cursor):
    curs =3D link_obj
    notices_accessor =3D curs.connection
    elif isinstance(link_obj, dbapi._psycopg.connection):
    curs =3D link_obj.cursor()
    curs_close =3D curs.close
    notices_accessor =3D link_obj
    if end_tx:
    tx_commit =3D link_obj.commit
    tx_rollback =3D link_obj.rollback
    for query in queries:
    try:
    args =3D query['args']
    except KeyError:
    args =3D None
    try:
    curs.execute(query['cmd'], args)
    if verbose:
    gmConnectionPool.log_cursor_state(curs)
    __log_notices(notices_accessor)
    # DB related exceptions
    except dbapi.Error as pg_exc:
    _log.error('query failed in RW connection')
    gmConnectionPool.log_pg_exception_details(pg_exc)
    __log_notices(notices_accessor)
    __safely_close_cursor_and_rollback_close_conn (
    curs_close,
    tx_rollback,
    conn_close
    )
    __perhaps_reraise_as_permissions_error(pg_exc, curs)
    # not a permissions problem
    gmLog2.log_stack_trace()
    raise

    # other exceptions
    except Exception:
    _log.exception('error running query in RW connection')
    gmConnectionPool.log_cursor_state(curs)
    __log_notices(notices_accessor)
    gmLog2.log_stack_trace()
    __safely_close_cursor_and_rollback_close_conn (
    curs_close,
    tx_rollback,
    conn_close
    )
    raise

    if not return_data:
    curs_close()
    tx_commit()
    conn_close()
    return (None, None)

    data =3D None
    try:
    data =3D curs.fetchall()
    except Exception:
    _log.exception('error fetching data from RW query')
    gmLog2.log_stack_trace()
    __safely_close_cursor_and_rollback_close_conn (
    curs_close,
    tx_rollback,
    conn_close
    )
    raise

    col_idx =3D None
    if get_col_idx:
    col_idx =3D get_col_indices(curs)
    curs_close()
    tx_commit()
    conn_close()
    return (data, col_idx)

    #------------------------------------------------------------------------

    =2D-
    GPG 40BE 5B0E C98E 1713 AFA6 5BC0 3BEA AC80 7D4F C89B

    --- MBSE BBS v1.0.8.4 (Linux-x86_64)
    * Origin: ---:- FTN<->UseNet Gate -:--- (3:633/280.2@fidonet)
  • From Greg Ewing@3:633/280.2 to All on Mon Sep 9 11:33:24 2024
    Subject: Re: psycopg2: proper positioning of .commit() within try: except:
    blocks

    On 8/09/24 11:03 pm, Jon Ribbens wrote:
    On 2024-09-08, Greg Ewing <greg.ewing@canterbury.ac.nz> wrote:
    try:
    do something
    .commit()
    except:
    log something
    .rollback()

    What if there's an exception in your exception handler? I'd put the
    rollback in the 'finally' handler, so it's always called.

    Good point. Putting the rollback first would be safer/

    --
    Greg

    --- MBSE BBS v1.0.8.4 (Linux-x86_64)
    * Origin: ---:- FTN<->UseNet Gate -:--- (3:633/280.2@fidonet)
  • From Greg Ewing@3:633/280.2 to All on Mon Sep 9 11:48:32 2024
    Subject: Re: psycopg2: proper positioning of .commit() within try: except:
    blocks

    On 9/09/24 2:13 am, Karsten Hilbert wrote:
    For what it's worth here's the current state of code:

    That code doesn't inspire much confidence in me. It's far too
    convoluted with too much micro-management of exceptions.

    I would much prefer to have just *one* place where exceptions are
    caught and logged.

    --
    Greg

    --- MBSE BBS v1.0.8.4 (Linux-x86_64)
    * Origin: ---:- FTN<->UseNet Gate -:--- (3:633/280.2@fidonet)
  • From Karsten Hilbert@3:633/280.2 to All on Mon Sep 9 18:40:14 2024
    Subject: Re: psycopg2: proper positioning of .commit() within try: except:
    blocks

    Am Mon, Sep 09, 2024 at 01:48:32PM +1200 schrieb Greg Ewing via Python-lis=
    t:

    That code doesn't inspire much confidence in me. It's far too
    convoluted with too much micro-management of exceptions.

    I would much prefer to have just *one* place where exceptions are
    caught and logged.

    I am open to suggestions.

    Karsten
    =2D-
    GPG 40BE 5B0E C98E 1713 AFA6 5BC0 3BEA AC80 7D4F C89B

    --- MBSE BBS v1.0.8.4 (Linux-x86_64)
    * Origin: ---:- FTN<->UseNet Gate -:--- (3:633/280.2@fidonet)
  • From Karsten Hilbert@3:633/280.2 to All on Mon Sep 9 18:56:47 2024
    Subject: Re: psycopg2: proper positioning of .commit() within try: except:
    blocks

    Am Mon, Sep 09, 2024 at 01:48:32PM +1200 schrieb Greg Ewing via Python-lis=
    t:

    That code doesn't inspire much confidence in me. It's far too
    convoluted with too much micro-management of exceptions.

    It is catching two exceptions, re-raising both of them,
    except for re-raising one of them as another kind of
    exception. What would you doing differently and how ?

    I would much prefer to have just *one* place where exceptions are
    caught and logged.

    There's, of course, a top level handler which logs and user-displays-as-appropriate any exceptions. This is code
    from a much larger codebase.

    Karsten
    =2D-
    GPG 40BE 5B0E C98E 1713 AFA6 5BC0 3BEA AC80 7D4F C89B

    --- MBSE BBS v1.0.8.4 (Linux-x86_64)
    * Origin: ---:- FTN<->UseNet Gate -:--- (3:633/280.2@fidonet)