Skip to content

wait_c does not gracefully handle EINTR errors (PEP-475) #667

@Jawshua

Description

@Jawshua

Hello there,

The custom wait_c implementation does not implement EINTR handling in the same way that the standard library does as of Python 3.5 / PEP-475 (automatic retries).

This is at the root of a problem we've seen pop up seemingly at random, and rather upsettingly it was obfuscated behind a completely different error when combined with SQLAlchemy sessions and connection pooling.

Eventually we worked out that the signals from our wall profiler were manifesting as an EINTR error, and that psycopg's wait implementation wasn't retrying when they occur. Swapping to the native Python implementation with PSYCOPG_WAIT_FUNC=wait_poll solves the problem.

The result is an Exception that looks something like this

  File "/app/.venv/lib/python3.11/site-packages/sqlalchemy/orm/session.py", line 2232, in execute
    return self._execute_internal(
           ^^^^^^^^^^^^^^^^^^^^^^^
  File "/app/.venv/lib/python3.11/site-packages/sqlalchemy/orm/session.py", line 2127, in _execute_internal
    result: Result[Any] = compile_state_cls.orm_execute_statement(
                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/app/.venv/lib/python3.11/site-packages/sqlalchemy/orm/context.py", line 293, in orm_execute_statement
    result = conn.execute(
             ^^^^^^^^^^^^^
  File "/app/.venv/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1413, in execute
    return meth(
           ^^^^^
  File "/app/.venv/lib/python3.11/site-packages/sqlalchemy/sql/elements.py", line 483, in _execute_on_connection
    return connection._execute_clauseelement(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/app/.venv/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1637, in _execute_clauseelement
    ret = self._execute_context(
          ^^^^^^^^^^^^^^^^^^^^^^
  File "/app/.venv/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1846, in _execute_context
    return self._exec_single_context(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/app/.venv/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1987, in _exec_single_context
    self._handle_dbapi_exception(
  File "/app/.venv/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 2347, in _handle_dbapi_exception
    raise exc_info[1].with_traceback(exc_info[2])
  File "/app/.venv/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1968, in _exec_single_context
    self.dialect.do_execute(
  File "/app/.venv/lib/python3.11/site-packages/sqlalchemy/engine/default.py", line 920, in do_execute
    cursor.execute(statement, parameters)
  File "/app/.venv/lib/python3.11/site-packages/psycopg/cursor.py", line 719, in execute
    self._conn.wait(
  File "/app/.venv/lib/python3.11/site-packages/psycopg/connection.py", line 957, in wait
    return waiting.wait(gen, self.pgconn.socket, timeout=timeout)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "psycopg_binary/_psycopg/waiting.pyx", line 179, in psycopg_binary._psycopg.wait_c
InterruptedError: [Errno 4] Interrupted system call

For posterity, the default FastAPI/SQLAlchemy pattern will result in an Exception that swallows the one above:

    session.close()
  File "/app/.venv/lib/python3.11/site-packages/sqlalchemy/orm/session.py", line 2383, in close
    self._close_impl(invalidate=False)
  File "/app/.venv/lib/python3.11/site-packages/sqlalchemy/orm/session.py", line 2425, in _close_impl
    transaction.close(invalidate)
  File "<string>", line 2, in close
  File "/app/.venv/lib/python3.11/site-packages/sqlalchemy/orm/state_changes.py", line 137, in _go
    ret_value = fn(self, *arg, **kw)
                ^^^^^^^^^^^^^^^^^^^^
  File "/app/.venv/lib/python3.11/site-packages/sqlalchemy/orm/session.py", line 1330, in close
    transaction.close()
  File "/app/.venv/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 2577, in close
    self._do_close()
  File "/app/.venv/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 2715, in _do_close
    self._close_impl()
  File "/app/.venv/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 2701, in _close_impl
    self._connection_rollback_impl()
  File "/app/.venv/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 2693, in _connection_rollback_impl
    self.connection._rollback_impl()
  File "/app/.venv/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1117, in _rollback_impl
    self._handle_dbapi_exception(e, None, None, None, None)
  File "/app/.venv/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 2344, in _handle_dbapi_exception
    raise sqlalchemy_exception.with_traceback(exc_info[2]) from e
  File "/app/.venv/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1115, in _rollback_impl
    self.engine.dialect.do_rollback(self.connection)
  File "/app/.venv/lib/python3.11/site-packages/sqlalchemy/engine/default.py", line 693, in do_rollback
    dbapi_connection.rollback()
  File "/app/.venv/lib/python3.11/site-packages/psycopg/connection.py", line 889, in rollback
    self.wait(self._rollback_gen())
  File "/app/.venv/lib/python3.11/site-packages/psycopg/connection.py", line 957, in wait
    return waiting.wait(gen, self.pgconn.socket, timeout=timeout)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "psycopg_binary/_psycopg/waiting.pyx", line 176, in psycopg_binary._psycopg.wait_c
  File "/app/.venv/lib/python3.11/site-packages/psycopg/connection.py", line 557, in _rollback_gen
    yield from self._exec_command(b"ROLLBACK")
  File "/app/.venv/lib/python3.11/site-packages/psycopg/connection.py", line 461, in _exec_command
    self.pgconn.send_query_params(command, None, result_format=result_format)
  File "psycopg_binary/pq/pgconn.pyx", line 276, in psycopg_binary.pq.PGconn.send_query_params
sqlalchemy.exc.OperationalError: (psycopg.OperationalError) sending query and params failed: another command is already in progress
(Background on this error at: https://sqlalche.me/e/20/e3q8)

Using an SQLAlchemy session contextually produces a similar stack:

    with Session() as session:
  File "/app/.venv/lib/python3.11/site-packages/sqlalchemy/orm/session.py", line 1687, in __exit__
    self.close()
  File "/app/.venv/lib/python3.11/site-packages/sqlalchemy/orm/session.py", line 2383, in close
    self._close_impl(invalidate=False)
  ... <snipped>

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions