Search code examples
google-cloud-platformgoogle-cloud-functionsgcloudgoogle-cloud-sqlgoogle-cloud-run

google cloud function times out, apparently during database access


I've written a 2nd gen Google Cloud Function in Python that pulls data from an external API and should write it to a PostgreSQL database hosted in Google Cloud SQL. When I try to run the function, it times out without changing anything in the database. I added some print() statements to see where it was failing. The sqlalchemy "engine.create()" seems to go through without issue, but it seems to not get past the first session.execute(stmt). Running the same function locally (while connecting to the cloud db) using function-framework works without issue, however. I'm using the Python cloud.sql.connector library (with the pg8000 driver) to connect to the database.

I'm honestly at a loss to explain why this won't work, or what the issue could even be. I've tried using a service account for the Function that has the global Owner role; the database credentials work when I'm running it locally, as well as in another cloud function; I've tried running the function with curl -H "Authorization: Bearer $(gcloud auth print-identity-token)" https://FUNCTION_URL from the cloud shell, as well as with gcloud function call from my local machine, same result. I'd expect the function to either complete or crash, but instead it just sits there until it times out. The only differences that I perceive between this and another function that connects to the same instance is that this one is a 2nd gen Cloud Function and uses the connector library instead of unix sockets to connect to the database. Same project, same region, same db instance, same GCP service account, same SQL login.

Relevant (I think) bits of code; bit where engine is created:

connector = Connector()

def getconn() -> pg8000.dbapi.Connection:
    conn: pg8000.dbapi.Connection = connector.connect(
        instance_connection_name,
        "pg8000",
        user=username,
        password=password,
        db=database,
        ip_type=IPTypes.PUBLIC,
    )
    return conn
print("creating db connection...")
# create a connection to the database
engine = create_engine("postgresql+pg8000://",
                       creator=getconn,
                       echo=False)
Session = sessionmaker(bind=engine)
session = Session()
print("db con ok")

The method that won't complete (never prints "ok"):

def wipe_table(name):
    print(f"Wiping staging table {name}...")
    stmt = text(f"TRUNCATE {name};")
    session.execute(stmt)
    print("ok")


Solution

  • This is most likely same issue as Cloud Function cannot connect to Cloud SQL with "upstream request timeout" (I recommend updating post with full Cloud Function code for allowing reproduction of error).

    TLDR; the Cloud SQL Python Connector object or SQLAlchemy connection pool should be lazy initialized when used with Cloud Functions because it runs background tasks.

    It turns out global variables that run background tasks can cause issues when run out side of the Cloud Function request context (because Cloud Functions only allocates compute during a request). So Cloud Functions recommends lazy initializing this type of global variable so that the variable is initialized within the request context.

    Example of lazy initializing connection pool is as follows:

    import functions_framework
    import sqlalchemy
    from google.cloud.sql.connector import Connector, IPTypes
    import pg8000
    
    def connect_to_instance() -> sqlalchemy.engine.base.Engine:
        connector = Connector()
    
        def getconn() -> pg8000.dbapi.Connection:
            return connector.connect(
                "...", # the PostgreSQL's instance connection name here
                "pg8000",
                user     = "xyz",
                password = 'supersecret',
                db       = "db_name",
                ip_type  = IPTypes.PUBLIC
            )
        
        return sqlalchemy.create_engine(
            "postgresql+pg8000://",
            creator      = getconn,
        )
    
    # lazy initialization of global db
    db = None
    
    @functions_framework.http
    def hello_http(request):
        # lazy init within request context
        global db
        if not db:
            db = connect_to_instance()
        with db.connect() as conn:
            # ... run queries
    

    More details on this same issue can be found here: https://github.com/GoogleCloudPlatform/cloud-sql-python-connector/issues/830