Improve database backend retry handling for stale SQLAlchemy connections#10122
Improve database backend retry handling for stale SQLAlchemy connections#10122harshang03 wants to merge 5 commits into
Conversation
Enable pool health defaults for the SQLAlchemy result backend and invalidate cached engines/sessions whenever retryable database errors occur so retries reconnect cleanly instead of reusing dead connections.
There was a problem hiding this comment.
Pull request overview
This PR hardens Celery’s SQLAlchemy database result backend against stale/invalid connections by setting safer engine defaults, adding a retry hook to clean up backend state before retry sleeps, and adding unit tests to cover the new behavior.
Changes:
- Enable SQLAlchemy pool health options by default (
pool_pre_ping=True,pool_recycle=3600) for the database backend configuration. - Add a generic
on_backend_retryable_error()hook inBaseBackendand invoke it from retry loops to allow backends to clean up state before sleeping/retrying. - Extend the SQLAlchemy database backend to classify common SQLAlchemy connection/state errors as retryable and invalidate cached engine/session state on retry, with unit tests for these behaviors.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
celery/app/defaults.py |
Changes default database_engine_options to include pool health settings. |
celery/backends/base.py |
Adds and invokes on_backend_retryable_error() from backend retry loops. |
celery/backends/database/__init__.py |
Defines retryable SQLAlchemy errors, applies engine-option defaults/merging, and invalidates session state on retry. |
celery/backends/database/session.py |
Adds SessionManager.invalidate() to drop cached engine/session state and dispose the engine. |
t/unit/backends/test_base.py |
Adds assertions that the new retry hook is invoked during retry loops. |
t/unit/backends/test_database.py |
Adds tests for engine-option defaults/overrides, retry classification, and session invalidation. |
| engine_options=Option( | ||
| { | ||
| 'pool_pre_ping': True, | ||
| 'pool_recycle': 3600, | ||
| }, | ||
| type='dict', old={'celery_result_engine_options'}, | ||
| ), |
There was a problem hiding this comment.
database_engine_options default is now non-empty, but the configuration docs still state the default is {} (e.g. docs/userguide/configuration.rst under database_engine_options). Can we update the docs (and add a versionchanged:: note) so the documented default matches the new behavior?
| DEFAULT_DATABASE_ENGINE_OPTIONS = { | ||
| 'pool_pre_ping': True, | ||
| 'pool_recycle': 3600, | ||
| } |
There was a problem hiding this comment.
DEFAULT_DATABASE_ENGINE_OPTIONS duplicates the default set in app/defaults.py for database_engine_options. Keeping two sources of truth increases the risk they drift; can we centralize this default (e.g., derive it from config defaults or reuse a shared constant) so it only needs to be updated in one place?
There was a problem hiding this comment.
can you please cross check this?
There was a problem hiding this comment.
I've removed the default value and extend and fix this in 25f4697
| if self.always_retry and self.exception_safe_to_retry(exc): | ||
| if retries < self.max_retries: | ||
| retries += 1 | ||
| self.on_backend_retryable_error(exc) | ||
|
|
There was a problem hiding this comment.
Same as above: an exception raised by on_backend_retryable_error() here would replace the original backend error and prevent the intended retry. Making this hook best-effort (catch + log) would keep retry behavior reliable.
| # Start with the default engine options, but allow explicit empty dicts | ||
| # from configuration or constructor arguments to disable all defaults. | ||
| engine_options_base = dict(DEFAULT_DATABASE_ENGINE_OPTIONS) | ||
|
|
||
| conf_engine_options = conf.database_engine_options | ||
| if conf_engine_options is not None: | ||
| if not conf_engine_options: | ||
| engine_options_base = {} | ||
| else: |
There was a problem hiding this comment.
The comment says an explicit empty mapping disables the defaults, but the current logic resets engine_options_base = {} when engine_options == {}, which also discards any non-default options coming from conf.database_engine_options. If the intent is “empty disables everything”, the comment should reflect that; if the intent is “empty disables only Celery’s defaults”, the merge logic likely needs adjusting.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #10122 +/- ##
==========================================
- Coverage 87.53% 87.50% -0.04%
==========================================
Files 153 153
Lines 19385 19417 +32
Branches 2229 2235 +6
==========================================
+ Hits 16968 16990 +22
- Misses 2121 2126 +5
- Partials 296 301 +5
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. |
✅ All Copilot Review Comments AddressedI've made some modifications to address all the Copilot review comments. Since I don't have write access to this branch, I've opened a new PR that references this one: New PR: #10124 Summary of Fixes:
Changes Made:
Codecov Comparison:
@auvipy @harshang03 Please review PR #10124 which contains all the fixes. Thanks to @harshang03 for the initial implementation! |
|
thanks. continuing with new pr |
Summary
pool_pre_pingand a boundedpool_recyclewindow fordatabase_engine_optionsDatabaseBackendtreat SQLAlchemy connection/state errors as retryable and invalidate cached engine/session state on each retry pathFiles changed
celery/app/defaults.py: set defaultdatabase_engine_optionspool health optionscelery/backends/base.py: addon_backend_retryable_error()hook and call it in retry loopscelery/backends/database/__init__.py: classify retry-safe DB exceptions, invalidate backend sessions on retry, and merge engine options with safe defaultscelery/backends/database/session.py: addSessionManager.invalidate()to dispose cached engine/session entriest/unit/backends/test_base.py: assert retry hook invocation for backend retry loopst/unit/backends/test_database.py: add tests for default/override engine options, retry classification, and session invalidationTest plan
.venv/bin/python -m pytest t/unit/backends/test_database.py t/unit/backends/test_base.pyissue: #10109