Skip to content

Commit 5967b77

Browse files
author
Saravana Kumar Rajendran
committed
Refine runtime migration runner integration
1 parent 1b73ce6 commit 5967b77

4 files changed

Lines changed: 61 additions & 39 deletions

File tree

docs/final-diff.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
11
# Final Diff
22

33
## Summary
4-
- Introduced a reusable runtime migration plan so one function governs self-healing schema fixes for the primary database and every organisation database.【F:src/backend/base/langflow/services/database/service.py†L50-L122】【F:src/backend/base/langflow/services/database/runtime_migrations.py†L1-L44
5-
- Updated `DatabaseService` to execute the shared plan once per engine, ensuring migrations run on first access without per-query overhead.【F:src/backend/base/langflow/services/database/service.py†L50-L154
4+
- Introduced a reusable runtime migration plan so one function governs self-healing schema fixes for the primary database and every organisation database.【F:src/backend/base/langflow/services/database/runtime_migrations.py†L1-L86
5+
- Updated `DatabaseService` to invoke the shared plan once per engine, ensuring migrations run on first access without per-query overhead.【F:src/backend/base/langflow/services/database/service.py†L186-L254
66
- Hooked the runtime plan into bootstrap (`initialize_database`) so the base database aligns before Alembic checks execute.【F:src/backend/base/langflow/services/database/utils.py†L16-L57】
7-
- Refreshed the schema guide to point to the new runtime-migration framework and its tenant coverage.【F:docs/schema-diff.md†L68-L111
7+
- Refreshed the schema guide to point to the new runtime-migration framework and its tenant coverage.【F:docs/schema-diff.md†L41-L49
88

99
## Key Changes
1010

1111
### `src/backend/base/langflow/services/database/runtime_migrations.py`
12-
- Added a dedicated module that lists runtime migrations and exposes `apply_runtime_migrations` for the service layer to call. New migrations now land in one place and run everywhere automatically.【F:src/backend/base/langflow/services/database/runtime_migrations.py†L1-L44
12+
- Added a dedicated module that lists runtime migrations and exposes helpers to apply them and ensure they run exactly once per engine. New migrations now land in one place and run everywhere automatically.【F:src/backend/base/langflow/services/database/runtime_migrations.py†L1-L86
1313

1414
### `src/backend/base/langflow/services/database/service.py`
15-
- Replaced the ad-hoc `drop_legacy_message_context_id` helper with a general `run_runtime_migrations` gate that wraps execution in an async lock and caches completion per service instance.【F:src/backend/base/langflow/services/database/service.py†L50-L154
16-
- Ensured `with_session` blocks on the runtime plan only once per database, covering organisation-specific engines without affecting later queries.【F:src/backend/base/langflow/services/database/service.py†L92-L122
15+
- Replaced inline guards with a thin `run_runtime_migrations` wrapper that delegates to the shared runtime-migration framework.【F:src/backend/base/langflow/services/database/service.py†L250-L254
16+
- Ensured `with_session` blocks on the runtime plan only once per database, covering organisation-specific engines without affecting later queries.【F:src/backend/base/langflow/services/database/service.py†L186-L199
1717

1818
### `src/backend/base/langflow/services/database/utils.py`
1919
- Adjusted bootstrap to call the new `run_runtime_migrations` entrypoint and improved error messaging around runtime fixes.【F:src/backend/base/langflow/services/database/utils.py†L16-L57】
2020

2121
### `docs/schema-diff.md`
22-
- Documented the runtime migration plan so future schema tweaks only require edits to the central function to reach every tenant database.【F:docs/schema-diff.md†L68-L111
22+
- Documented the runtime migration plan so future schema tweaks only require edits to the central function to reach every tenant database.【F:docs/schema-diff.md†L41-L49

docs/schema-diff.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,10 @@ All other tables share the same schema definitions across both branches.
4040

4141
## Deployment guidance for `context_id`
4242

43-
The runtime initialisation path now removes the obsolete `context_id` column automatically before Alembic compares metadata with the live database. During startup `initialize_database` calls `DatabaseService.run_runtime_migrations`, which executes the shared plan in `runtime_migrations.apply_runtime_migrations` to issue `ALTER TABLE "message" DROP COLUMN context_id` whenever the column is still present. In addition, each `DatabaseService` (including organisation-scoped instances) performs the same plan the first time it opens a session, so every tenant database self-heals on first use after deployment.【F:src/backend/base/langflow/services/database/utils.py†L16-L68】【F:src/backend/base/langflow/services/database/service.py†L57-L308】【F:src/backend/base/langflow/services/database/runtime_migrations.py†L1-L44
43+
The runtime initialisation path now removes the obsolete `context_id` column automatically before Alembic compares metadata with the live database. During startup `initialize_database` calls `DatabaseService.run_runtime_migrations`, which delegates to `runtime_migrations.ensure_runtime_migrations` to issue `ALTER TABLE "message" DROP COLUMN context_id` whenever the column is still present. In addition, each `DatabaseService` (including organisation-scoped instances) performs the same plan the first time it opens a session, so every tenant database self-heals on first use after deployment.【F:src/backend/base/langflow/services/database/utils.py†L16-L68】【F:src/backend/base/langflow/services/database/service.py†L186-L254】【F:src/backend/base/langflow/services/database/runtime_migrations.py†L1-L86
4444

4545
- **When deploying the merged branch**: no manual SQL is required; the service will drop the column the first time it sees an upgraded database—both for the primary database at boot and for each organisation database when it is next accessed. This keeps forward compatibility by aligning the schema with the code that no longer references `context_id`.
4646
- **If you must roll back to the old `main` image**: reintroduce the column with `ALTER TABLE "message" ADD COLUMN context_id TEXT NULL;` before starting the old image, otherwise SQLModel will fail to hydrate the `Message` records because the expected column is missing.
4747
- **Fresh installations**: databases created after the merge never create the column, so nothing extra runs.
4848

49-
Because the drop happens ahead of Alembic’s `command.check`, the migration runner no longer sees a schema mismatch, ensuring the new container starts cleanly without needing the destructive `--fix` flag.【F:src/backend/base/langflow/services/database/service.py†L300-L386】【F:src/backend/base/langflow/services/database/utils.py†L18-L60
49+
Because the drop happens ahead of Alembic’s `command.check`, the migration runner no longer sees a schema mismatch, ensuring the new container starts cleanly without needing the destructive `--fix` flag.【F:src/backend/base/langflow/services/database/service.py†L327-L384】【F:src/backend/base/langflow/services/database/utils.py†L16-L68

src/backend/base/langflow/services/database/runtime_migrations.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,38 @@
88

99
from __future__ import annotations
1010

11+
import asyncio
1112
from collections.abc import Callable
13+
from dataclasses import dataclass
1214
from typing import Final
15+
from weakref import WeakKeyDictionary
1316

1417
from lfx.log.logger import logger
1518
from sqlalchemy import inspect
1619
from sqlalchemy.engine import Connection
20+
from sqlalchemy.ext.asyncio import AsyncEngine
1721
from sqlmodel import text
1822

1923
RuntimeMigration = Callable[[Connection], None]
2024

2125

26+
@dataclass
27+
class _RuntimeMigrationState:
28+
lock: asyncio.Lock
29+
done: bool = False
30+
31+
32+
_ENGINE_STATES: "WeakKeyDictionary[AsyncEngine, _RuntimeMigrationState]" = WeakKeyDictionary()
33+
34+
35+
def _state_for_engine(engine: AsyncEngine) -> _RuntimeMigrationState:
36+
state = _ENGINE_STATES.get(engine)
37+
if state is None:
38+
state = _RuntimeMigrationState(lock=asyncio.Lock())
39+
_ENGINE_STATES[engine] = state
40+
return state
41+
42+
2243
def _drop_legacy_message_context_id(connection: Connection) -> None:
2344
inspector = inspect(connection)
2445

@@ -50,3 +71,28 @@ def apply_runtime_migrations(connection: Connection) -> None:
5071

5172
for migration in _RUNTIME_MIGRATIONS:
5273
migration(connection)
74+
75+
76+
async def ensure_runtime_migrations(
77+
engine: AsyncEngine, *, use_noop_database: bool = False
78+
) -> None:
79+
"""Run the runtime migrations exactly once for ``engine``.
80+
81+
``use_noop_database`` short-circuits the plan for in-memory/disabled
82+
database configurations.
83+
"""
84+
85+
state = _state_for_engine(engine)
86+
87+
if use_noop_database:
88+
state.done = True
89+
return
90+
91+
async with state.lock:
92+
if state.done:
93+
return
94+
95+
async with engine.begin() as conn:
96+
await conn.run_sync(apply_runtime_migrations)
97+
98+
state.done = True

src/backend/base/langflow/services/database/service.py

Lines changed: 6 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
from langflow.services.database import models
2929
from langflow.services.database.models.user.crud import get_user_by_username
3030
from langflow.services.database.session import NoopSession
31-
from langflow.services.database.runtime_migrations import apply_runtime_migrations
31+
from langflow.services.database.runtime_migrations import ensure_runtime_migrations
3232
from langflow.services.database.utils import Result, TableResults
3333
from langflow.services.deps import get_settings_service
3434
from langflow.services.utils import teardown_superuser
@@ -49,9 +49,6 @@ def __init__(self, settings_service: SettingsService):
4949
self.database_url: str = settings_service.settings.database_url
5050
self._sanitize_database_url()
5151

52-
self._runtime_migration_lock = asyncio.Lock()
53-
self._runtime_migration_done = False
54-
5552
# This file is in langflow.services.database.manager.py
5653
# the ini is in langflow
5754
langflow_dir = Path(__file__).parent.parent.parent
@@ -190,10 +187,9 @@ def on_connection(self, dbapi_connection, _connection_record) -> None:
190187
@asynccontextmanager
191188
async def with_session(self):
192189
if self.settings_service.settings.use_noop_database:
193-
self._runtime_migration_done = True
194190
yield NoopSession()
195191
else:
196-
await self._ensure_runtime_migrations()
192+
await self.run_runtime_migrations()
197193
async with AsyncSession(self.engine, expire_on_commit=False) as session:
198194
# Start of Selection
199195
try:
@@ -253,30 +249,10 @@ async def assign_orphaned_flows_to_superuser(self) -> None:
253249
await logger.adebug("Successfully assigned orphaned flows to the default superuser")
254250

255251
async def run_runtime_migrations(self) -> None:
256-
"""Execute the runtime migration plan once for this service."""
257-
258-
async with self._runtime_migration_lock:
259-
if self._runtime_migration_done:
260-
return
261-
262-
if self.settings_service.settings.use_noop_database:
263-
self._runtime_migration_done = True
264-
return
265-
266-
async with self.engine.begin() as conn:
267-
await conn.run_sync(self._apply_runtime_migrations)
268-
269-
self._runtime_migration_done = True
270-
271-
@staticmethod
272-
def _apply_runtime_migrations(connection) -> None:
273-
apply_runtime_migrations(connection)
274-
275-
async def _ensure_runtime_migrations(self) -> None:
276-
if self._runtime_migration_done:
277-
return
278-
279-
await self.run_runtime_migrations()
252+
await ensure_runtime_migrations(
253+
self.engine,
254+
use_noop_database=self.settings_service.settings.use_noop_database,
255+
)
280256

281257
@staticmethod
282258
def _generate_unique_flow_name(original_name: str, existing_names: set[str]) -> str:

0 commit comments

Comments
 (0)