Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Ensure SQLAlchemy sessions are closed #25031

Conversation

john-bodley
Copy link
Member

SUMMARY

At Airbnb we've been running into the infamous SQLAlchemy QueuePool issue which typically occurs when connections aren't being closed. Thankfully Flask-SQLAlchemy normally handles this for us, per here, when the app context is torn down.

Grokking the code it seems like there are instances (for right or wrong) where we create our own sessions which aren't then explicitly closed. This PR ensures that either i) these sessions are now closed, or ii) the Flask-SQLAlchemy session is used.

BEFORE/AFTER SCREENSHOTS OR ANIMATED GIF

TESTING INSTRUCTIONS

CI.

ADDITIONAL INFORMATION

  • Has associated issue:
  • Required feature flags:
  • Changes UI
  • Includes DB Migration (follow approval process in SIP-59)
    • Migration is atomic, supports rollback & is backwards-compatible
    • Confirm DB migration upgrade and downgrade tested
    • Runtime estimates and downtime expectations provided
  • Introduces new feature or API
  • Removes existing feature or API

@john-bodley john-bodley force-pushed the john-bodley--fix-reshare-flask-sqlalchemy-session branch from ad2cf09 to d3ca60a Compare August 19, 2023 05:18
@pull-request-size pull-request-size bot added size/S and removed size/M labels Aug 19, 2023
@john-bodley john-bodley force-pushed the john-bodley--fix-reshare-flask-sqlalchemy-session branch from d3ca60a to a9427f0 Compare August 19, 2023 05:19
@@ -96,6 +96,7 @@ class DummyStrategy(Strategy): # pylint: disable=too-few-public-methods
def get_payloads(self) -> list[dict[str, int]]:
session = db.create_scoped_session()
charts = session.query(Slice).all()
session.close()
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I gather these standalone scoped sessions exist because tasks fall outside the jurisdiction of Flask. If this isn't the case then—to adhere to the KISS priniciple—we should simply reuse the Flask-SQLAlchemy session.

Copy link
Member

@villebro villebro left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. However, at some point we might want to discuss if we may want to make it possible to not commit in the DAOs or similar methods. I've been writing some custom functionality in our security manager that wants to perform transaction-like operations, and I'd prefer to either have the whole chain of events succeed, or rollback everything. An option could be to add a commit=True parameter to these methods, or then just remove the explicit commit and assume callers do it (e.g. in the api.py) after performing the subtasks.

@john-bodley
Copy link
Member Author

john-bodley commented Aug 19, 2023

@villebro I was talking with @michael-s-molina earlier this week and plan to write a SIP about DAOs. The plan is—as you pointed out—that one shouldn't commit within a DAO but rather on a unit of work (typically a Flask request, though in our case it's likely a command). I hope to have the SIP drafted earlier next week which will also mention SQLAlchemy nested transactions, which as the name suggests, allows for one to nest operations (leveraging SAVEPOINTS) which ensures one of two states, the transaction is committed or rolled back.

This removes the need to pass around commit = True | False to the various methods. Here's a taste of what it looks like.

@villebro
Copy link
Member

@john-bodley this sounds amazing - I really look forward to this reform! ❤️

@john-bodley john-bodley marked this pull request as ready for review August 19, 2023 15:16
@john-bodley john-bodley force-pushed the john-bodley--fix-reshare-flask-sqlalchemy-session branch from a9427f0 to 8ce364c Compare August 21, 2023 04:48
@pull-request-size pull-request-size bot added size/M and removed size/S labels Aug 21, 2023
@john-bodley john-bodley force-pushed the john-bodley--fix-reshare-flask-sqlalchemy-session branch 2 times, most recently from 92d241c to 5de7ef8 Compare August 21, 2023 16:17
@pull-request-size pull-request-size bot added size/L and removed size/M labels Aug 21, 2023

# copy template dashboard to user
template = session.query(Dashboard).filter_by(id=int(dashboard_id)).first()
template = db.session.query(Dashboard).filter_by(id=int(dashboard_id)).first()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we use the connection parameter here? According to the event definition:

connection – the Connection being used to emit INSERT statements for this instance. This provides a handle into the current transaction on the target database specific to this instance.

Copy link
Member Author

@john-bodley john-bodley Aug 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@michael-s-molina I've reverted the PR back to my initial implementation which leverages the connection parameter.

It states that it provides a handle into the current transaction, yet we're likely over committing, i.e., there should only be one commit per unit of work. I'm not sure what happens if you close the session without committing. Maybe we using the connection wrongly.

Copy link
Member Author

@john-bodley john-bodley Aug 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh it seems we should be using SQLAlchemy's object_session rather than creating a new one. This should remove the need to commit and explicitly close the session as this is handled by Flask-SQLAlchemy.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regrettably I was running into this issue and had to revert my last commit. I think we likely will have to revisit how tagging is working at a later date.

@john-bodley john-bodley force-pushed the john-bodley--fix-reshare-flask-sqlalchemy-session branch from 5de7ef8 to cec52c6 Compare August 21, 2023 19:13
@pull-request-size pull-request-size bot added size/M and removed size/L labels Aug 21, 2023
@john-bodley john-bodley force-pushed the john-bodley--fix-reshare-flask-sqlalchemy-session branch 2 times, most recently from e2e37fe to cfc8e46 Compare August 21, 2023 19:16
@john-bodley john-bodley force-pushed the john-bodley--fix-reshare-flask-sqlalchemy-session branch 2 times, most recently from f4443a2 to 73d4ed1 Compare August 21, 2023 20:14
@john-bodley john-bodley force-pushed the john-bodley--fix-reshare-flask-sqlalchemy-session branch from 73d4ed1 to cfc8e46 Compare August 21, 2023 21:14
@pull-request-size pull-request-size bot added size/S and removed size/M labels Aug 21, 2023
@@ -414,13 +415,12 @@ def export_dashboards( # pylint: disable=too-many-locals
"native_filter_configuration", []
)
for native_filter in native_filter_configuration:
session = db.session()
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's unclear why a new session was instantiated (and never closed) here. On line #433 the Flask-SQLAlchemy session is used and thus it seems prudent (and hopefully) safe to use db.session.

@@ -96,6 +96,7 @@ def copy_dashboard(_mapper: Mapper, connection: Connection, target: Dashboard) -
)
session.add(extra_attributes)
session.commit()
session.close()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we can just reuse the db.session everywhere so we don't create this arbitrary session

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we really need to use this session, better to wrap into try/catch and close at final otherwise exceptions thrown in commit will skip session.close

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zephyring that is the ideal solution, using the same session (which is obtainable via inspect(target).session) however per #25031 (comment) this is currently problematic given the way our SQLAlchemy event listener callbacks are configured.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@john-bodley Could you apply the try/finally pattern to the places where you're closing the session? I also think we can remove the session.commit() on line 91.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW @michael-s-molina per our offline conversation you can see here that the session is rolled back before closing.

@eschutho
Copy link
Member

This is great. We were just investigating this issue that we originally thought was a problem with ssh connections, but later found that that the root cause was data dbs hitting the max connections. cc @hughhhh

@john-bodley john-bodley force-pushed the john-bodley--fix-reshare-flask-sqlalchemy-session branch from 07e6f80 to 5a11302 Compare August 23, 2023 18:16
@john-bodley john-bodley merged commit adaab35 into apache:master Aug 23, 2023
29 checks passed
@michael-s-molina michael-s-molina added the v3.0 Label added by the release manager to track PRs to be included in the 3.0 branch label Aug 23, 2023
michael-s-molina pushed a commit that referenced this pull request Aug 24, 2023
john-bodley added a commit to airbnb/superset-fork that referenced this pull request Aug 24, 2023
(cherry picked from commit adaab35)
(cherry picked from commit 0f390e401555da4cc8639eb7b3bfa462f23edef7)
@mistercrunch mistercrunch added 🍒 3.0.0 🍒 3.0.1 🍒 3.0.2 🍒 3.0.3 🍒 3.0.4 🏷️ bot A label used by `supersetbot` to keep track of which PR where auto-tagged with release labels 🚢 3.1.0 labels Mar 8, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🏷️ bot A label used by `supersetbot` to keep track of which PR where auto-tagged with release labels size/L v3.0 Label added by the release manager to track PRs to be included in the 3.0 branch 🍒 3.0.0 🍒 3.0.1 🍒 3.0.2 🍒 3.0.3 🍒 3.0.4 🚢 3.1.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants