invenio-db
Advanced tools
+7
-0
@@ -12,2 +12,9 @@ .. | ||
| Version v2.3.0 (released 2026-02-24) | ||
| - fix(config): use UTC for PostgreSQL | ||
| - feat(alembic): set lock_timeout with retry on migration connections | ||
| - docs(sphinx): ignore unresolved Flask-Alembic type refs | ||
| - tests(utc): add unit tests for UTCDateTime | ||
| Version v2.2.1 (released 2026-01-30) | ||
@@ -14,0 +21,0 @@ |
| Metadata-Version: 2.4 | ||
| Name: invenio-db | ||
| Version: 2.2.1 | ||
| Version: 2.3.0 | ||
| Summary: Database management for Invenio. | ||
@@ -73,2 +73,9 @@ Home-page: https://github.com/inveniosoftware/invenio-db | ||
| Version v2.3.0 (released 2026-02-24) | ||
| - fix(config): use UTC for PostgreSQL | ||
| - feat(alembic): set lock_timeout with retry on migration connections | ||
| - docs(sphinx): ignore unresolved Flask-Alembic type refs | ||
| - tests(utc): add unit tests for UTCDateTime | ||
| Version v2.2.1 (released 2026-01-30) | ||
@@ -75,0 +82,0 @@ |
@@ -95,3 +95,3 @@ # -*- coding: utf-8 -*- | ||
| __version__ = "2.2.1" | ||
| __version__ = "2.3.0" | ||
@@ -98,0 +98,0 @@ __all__ = ( |
+78
-1
@@ -13,3 +13,6 @@ # -*- coding: utf-8 -*- | ||
| import logging | ||
| import os | ||
| import random | ||
| import time | ||
| from importlib.metadata import PackageNotFoundError | ||
@@ -20,4 +23,6 @@ from importlib.metadata import version as package_version | ||
| import sqlalchemy as sa | ||
| from flask import current_app | ||
| from flask_alembic import Alembic | ||
| from invenio_base.utils import entry_points | ||
| from sqlalchemy.exc import OperationalError | ||
| from sqlalchemy_utils.functions import get_class_by_table | ||
@@ -29,3 +34,68 @@ | ||
| logger = logging.getLogger(__name__) | ||
| class InvenioAlembic(Alembic): | ||
| """Alembic with PostgreSQL lock_timeout and retry for safe migrations. | ||
| Sets lock_timeout on migration connections so DDL statements fail fast | ||
| instead of blocking reads indefinitely while waiting for exclusive locks. | ||
| On lock timeout, retries with exponential backoff. Already-applied | ||
| migrations are skipped on retry (transaction_per_migration stamps each | ||
| step). | ||
| See https://postgres.ai/blog/20210923-zero-downtime-postgres-schema-migrations-lock-timeout-and-retries | ||
| Configuration: | ||
| - ``DB_MIGRATION_LOCK_TIMEOUT``: PostgreSQL lock_timeout value | ||
| (default ``"1s"``). Set to ``"0"`` to disable. | ||
| - ``DB_MIGRATION_LOCK_TIMEOUT_RETRIES``: number of retries on lock | ||
| timeout (default ``5``). | ||
| """ | ||
| def __init__(self, *args, **kwargs): | ||
| """Initialize InvenioAlembic.""" | ||
| super().__init__(*args, **kwargs) | ||
| def _set_lock_timeout(self): | ||
| """Set lock_timeout on all PostgreSQL migration connections.""" | ||
| for ctx in self.migration_contexts.values(): | ||
| if ( | ||
| ctx.connection is not None | ||
| and ctx.connection.dialect.name == "postgresql" | ||
| ): | ||
| timeout = current_app.config.get("DB_MIGRATION_LOCK_TIMEOUT", "1s") | ||
| ctx.connection.execute( | ||
| sa.text("SELECT set_config('lock_timeout', :val, false)"), | ||
| {"val": timeout}, | ||
| ) | ||
| def run_migrations(self, fn, **kwargs): | ||
| """Run migrations with lock_timeout and retry on lock failure.""" | ||
| max_retries = current_app.config.get("DB_MIGRATION_LOCK_TIMEOUT_RETRIES", 5) | ||
| for attempt in range(max_retries + 1): | ||
| self._set_lock_timeout() | ||
| try: | ||
| super().run_migrations(fn, **kwargs) | ||
| return | ||
| except OperationalError as e: | ||
| is_lock_timeout = hasattr(e.orig, "pgcode") and e.orig.pgcode == "55P03" | ||
| if not is_lock_timeout or attempt >= max_retries: | ||
| raise | ||
| # Exponential backoff with jitter | ||
| delay = min(30, 0.5 * 2**attempt) * (0.5 + random.random() * 0.5) | ||
| logger.warning( | ||
| "Migration lock timeout, retrying in %.1fs (%d/%d)", | ||
| delay, | ||
| attempt + 1, | ||
| max_retries, | ||
| ) | ||
| time.sleep(delay) | ||
| # Clear cached contexts — connection is dead after the error. | ||
| # Next access to migration_contexts creates fresh connections. | ||
| self._get_cache().clear() | ||
| class InvenioDB(object): | ||
@@ -36,3 +106,3 @@ """Invenio database extension.""" | ||
| """Extension initialization.""" | ||
| self.alembic = Alembic(run_mkdir=False, command_name="alembic") | ||
| self.alembic = InvenioAlembic(run_mkdir=False, command_name="alembic") | ||
| if app: | ||
@@ -83,2 +153,9 @@ self.init_app(app, **kwargs) | ||
| app.config.setdefault("SQLALCHEMY_TRACK_MODIFICATIONS", True) | ||
| app.config.setdefault( | ||
| "SQLALCHEMY_ENGINE_OPTIONS", | ||
| # Ensure the database is using the UTC timezone for interpreting timestamps (Postgres only). | ||
| # This overrides any default setting (e.g. in postgresql.conf). Invenio expects the DB to receive | ||
| # and provide UTC timestamps in all cases, so it's important that this doesn't get changed. | ||
| {"connect_args": {"options": "-c timezone=UTC"}}, | ||
| ) | ||
@@ -85,0 +162,0 @@ # Initialize Flask-SQLAlchemy extension. |
+8
-1
| Metadata-Version: 2.4 | ||
| Name: invenio-db | ||
| Version: 2.2.1 | ||
| Version: 2.3.0 | ||
| Summary: Database management for Invenio. | ||
@@ -73,2 +73,9 @@ Home-page: https://github.com/inveniosoftware/invenio-db | ||
| Version v2.3.0 (released 2026-02-24) | ||
| - fix(config): use UTC for PostgreSQL | ||
| - feat(alembic): set lock_timeout with retry on migration connections | ||
| - docs(sphinx): ignore unresolved Flask-Alembic type refs | ||
| - tests(utc): add unit tests for UTCDateTime | ||
| Version v2.2.1 (released 2026-01-30) | ||
@@ -75,0 +82,0 @@ |
Alert delta unavailable
Currently unable to show alert delta for PyPI packages.
75467
5.63%1018
6.93%