Get hands-on experience with 20+ free Google Cloud products and $300 in free credit for new customers.

StaleDataError with Flask-SQLAlchemy using Cloud SQL Connector for MySQL on Google Cloud Run

Hi,

I'm migrating a local Flask app that uses Flask-SQLAlchemy to Google Cloud Run, but getting StaleDataErrors on db.session.commit() related to the Cloud SQL Connector. 

These errors don't happen locally or when I'm in Cloud Run if using the standard Flask-SQLAlchemy connector method which is just setting the SQLALCHEMY_DATABASE_URI variable.

Here's the error message in the log:

 

 

sqlalchemy.orm.exc.StaleDataError: UPDATE statement on table 'mytable' expected to update 1 row(s); 0 were matched.

 

 

Which is caught on the db.session.commit(), see:

 

 

File "/usr/local/lib/python3.10/site-packages/gunicorn/workers/gthread.py", line 271, in handle
    keepalive = self.handle_request(req, conn)
  File "/usr/local/lib/python3.10/site-packages/gunicorn/workers/gthread.py", line 323, in handle_request
    respiter = self.wsgi(environ, resp.start_response)
  File "/usr/local/lib/python3.10/site-packages/flask/app.py", line 2551, in __call__
    return self.wsgi_app(environ, start_response)
  File "/usr/local/lib/python3.10/site-packages/flask/app.py", line 2531, in wsgi_app
    response = self.handle_exception(e)
  File "/usr/local/lib/python3.10/site-packages/flask/app.py", line 2528, in wsgi_app
    response = self.full_dispatch_request()
  File "/usr/local/lib/python3.10/site-packages/flask/app.py", line 1825, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "/usr/local/lib/python3.10/site-packages/flask/app.py", line 1823, in full_dispatch_request
    rv = self.dispatch_request()
  File "/usr/local/lib/python3.10/site-packages/flask/app.py", line 1799, in dispatch_request
    return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)
  File "/usr/local/lib/python3.10/site-packages/flask_security/decorators.py", line 394, in decorated_view
    return fn(*args, **kwargs)
  File "/usr/local/lib/python3.10/site-packages/flask_security/decorators.py", line 488, in decorated_view
    return fn(*args, **kwargs)
  File "/app/app/admin/routes.py", line 248, in edit_mytable
    db.session.commit()
  File "/usr/local/lib/python3.10/site-packages/sqlalchemy/orm/scoping.py", line 553, in commit
    return self._proxied.commit()
  File "/usr/local/lib/python3.10/site-packages/sqlalchemy/orm/session.py", line 1903, in commit
    trans.commit(_to_root=True)
  File "<string>", line 2, in commit
  File "/usr/local/lib/python3.10/site-packages/sqlalchemy/orm/state_changes.py", line 137, in _go

 

 

Here's how I'm using the Cloud SQL Connector:

 

 

from google.cloud.sql.connector import Connector, IPTypes

basedir = os.path.abspath(os.path.dirname(__file__))
load_dotenv(path.join(basedir, '.env'))


def getconn():
    with Connector() as connector:
        conn = connector.connect(
            environ.get('INSTANCE_CONNECTION_NAME'), # Cloud SQL Instance Connection Name
            "pymysql",
            user=environ.get('DATABASE_USER'),
            password=environ.get('DATABASE_PASSWORD'),
            db=environ.get('DATABASE_NAME'),
            ip_type= IPTypes.PRIVATE  # IPTypes.PRIVATE for private IP
        )
        return conn


class Config(object):
    SECRET_KEY = environ.get("SECRET_KEY")
    FLASK_APP = "app.py"

    # MySQL database
     if os.getenv('K_SERVICE', ''):
         # Using google cloud connector
         SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://'
         SQLALCHEMY_ENGINE_OPTIONS = {"creator": getconn}
    # else:
         # Running locally
         SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://{}:{}@{}:{}/{}'.format(
             environ.get('DATABASE_USER'),
             environ.get('DATABASE_PASSWORD'),
             environ.get('DATABASE_HOST'),
             environ.get('DATABASE_PORT'),
             environ.get('DATABASE_NAME')
         ) + '?charset=utf8mb4'

 

 

And here's an example route that is getting the error, but it happens for all routes that edit an object.  Note that the error happens consistently when the user doesn't change any data and just clicks "Submit" on the form.

 

 

 

@blueprint.route('/mytable/edit/<int:id>', methods=['GET', 'POST'])
@auth_required()
@roles_required('admin')
def edit_mytable(id):
    add_mytable = False

    mytable = MyTables.query.get_or_404(id)
    form = MyTableForm(obj=mytable)
    if form.validate_on_submit():
        mytable.id = form.id.data
        mytable.term = form.term.data.lower()
        db.session.commit()
        flash('You have successfully edited mytable.', 'info')

        # redirect to the next page
        return redirect(url_for('admin_blueprint.list_mytable'))

    form.id.data = mytable.id
    form.term.data = mytable.term
    return render_template('mytable.html', action="Edit",
                           add_mytable=add_mytable, form=form,
                           mytable=mytable, title="Edit My Table")

 

 

I suspect this has something to do with the connection pooling when using the Cloud SQL Connector, but I'm not able to troubleshoot it as it runs fine locally.  I've tried various pool settings with no luck.  Is there a setting for Cloud SQL Connector that would not cause StaleDataErrors?

If I add a db.session.refresh(object) to preceed all of my db.session.commit() statements, there is no error, but that isn't typical and I'd prefer to just keep the standard pattern of calling db.session.commit() in all routes and database updates.

Thank you!

 

 

0 2 1,634
2 REPLIES 2

Hi @stillerz,

Welcome to Google Cloud Community!

With regards the error that you receive, according to SQLAlchemy ORM Exceptions documentation:

An operation encountered database state that is unaccounted for.

Most likely:

A flush may have attempted to update or delete rows and an unexpected number of rows were matched during the UPDATE or DELETE statement. Note that when version_id_col is used, rows in UPDATE or DELETE statements are also matched against the current known version identifier.


You may use session.close() as suggested in the Stack Overflow links below:

Hope this helps. 

Hi @robertcarlos,

No, that doesn't help actually.

I would like to use the Google Cloud SQL Connector method of connecting to my Cloud SQL database, but it is not working.  Is it possible there is a bug, when working with Flask-SQLAlchemy and this connector?

What I would like to know is why my database connection --

1.  Works fine when I use a SQLALCHEMY_DATABASE_URI connection string, and

2. Doesn't work, when I use the Google Cloud SQL Connector for the same database

What you sent me was documentation about closing database sessions, which is what Flask-SQLAlchemy already does (check the comments in the SO post), so there should be no need for that.  In fact, that's probably why option 1 above works fine.