Home > Software engineering >  Postgres docker "server closed the connection unexpectedly"
Postgres docker "server closed the connection unexpectedly"

Time:01-29

I want to run and connect to the postgresql Docker image in Python using SQLModel. Here's my attempt

from contextlib import contextmanager

import docker
from sqlmodel import create_engine, SQLModel, Field

DEFAULT_POSTGRES_PORT = 5432


class Foo(SQLModel, table=True):
    id_: int = Field(primary_key=True)


@contextmanager
def postgres_engine():
    db_pass = "foo"
    host_port = 1234

    client = docker.from_env()

    container = client.containers.run(
        "postgres",
        ports={DEFAULT_POSTGRES_PORT: host_port},
        environment={"POSTGRES_PASSWORD": db_pass},
        detach=True,
    )

    try:
        engine = create_engine(
            f"postgresql://postgres:{db_pass}@localhost:{host_port}/postgres"
        )

        SQLModel.metadata.create_all(engine)

        yield engine
    finally:
        container.kill()
        container.remove()


with postgres_engine():
    pass

I'm seeing

sqlalchemy.exc.OperationalError: (psycopg2.OperationalError) connection to server at "localhost" (127.0.0.1), port 1234 failed: server closed the connection unexpectedly
    This probably means the server terminated abnormally
    before or while processing the request.

If instead of using the Python Docker SDK I use the CLI with

docker run -it -e POSTGRES_PASSWORD=foo -p 1234:5432 postgres

I don't see an error.

CodePudding user response:

You have a race condition in your code. Take a look at the logs when you start up a Postgres container:

$ docker run --rm -e POSTGRES_PASSWORD=secret postgres:14
The files belonging to this database system will be owned by user "postgres".
This user must also own the server process.

The database cluster will be initialized with locale "en_US.utf8".
The default database encoding has accordingly been set to "UTF8".
The default text search configuration will be set to "english".
.
.
.

2023-01-28 02:02:54.575 UTC [1] LOG:  database system is ready to accept connections

All of that initialization takes a second or so to run. On the other hand, your code immediately tries to interact with the database after creating the container:

        engine = create_engine(
            f"postgresql://postgres:{db_pass}@localhost:{host_port}/postgres"
        )

        SQLModel.metadata.create_all(engine)

In other words, it's trying to connect to the database before the database is ready to handle connections. The best solution is to block until you are able to communicate successfully with the Postgres server. Something like this:

        engine = create_engine(
            f"postgresql://postgres:{db_pass}@localhost:{host_port}/postgres"
        )

        while True:
            try:
                with engine.connect() as conn:
                   conn.execute(sqlalchemy.select(1))
            except sqlalchemy.exc.OperationalError:
                print('retrying connection')
                time.sleep(1)
                continue
            else:
                break

        SQLModel.metadata.create_all(engine)

With this code in place, running the script produces this output:

retrying connection
retrying connection

...and then exits without error. The complete script, with the above modifications, looks like:

from contextlib import contextmanager
import time

import docker
import sqlalchemy
from sqlmodel import create_engine, SQLModel, Field

DEFAULT_POSTGRES_PORT = 5432


class Foo(SQLModel, table=True):
    id_: int = Field(primary_key=True)


@contextmanager
def postgres_engine():
    db_pass = "secret"
    host_port = 1234

    client = docker.from_env()

    container = client.containers.run(
        "postgres",
        ports={DEFAULT_POSTGRES_PORT: host_port},
        environment={"POSTGRES_PASSWORD": db_pass},
        detach=True,
    )

    try:
        engine = create_engine(
            f"postgresql://postgres:{db_pass}@localhost:{host_port}/postgres"
        )

        while True:
            try:
                with engine.connect() as conn:
                   conn.execute(sqlalchemy.select(1))
            except sqlalchemy.exc.OperationalError:
                print('retrying connection')
                time.sleep(1)
                continue
            else:
                break

        SQLModel.metadata.create_all(engine)

        yield engine
    finally:
        container.kill()
        container.remove()


with postgres_engine() as engine:
    pass
  • Related