Skip to content

Commit 9f25216

Browse files
fix: [RBAC Migration]Hydrate workspace config at startup (#1575)
* Hydrate workspace config at startup Add WorkspaceConfigService.hydrate_on_startup() which calls load_config() to eagerly populate the config manager from the DB during application startup. Call this method from the app lifespan (run_startup) and log errors if hydration fails. This prevents a DB-mode restart from leaving the in-memory config unset and causing the frontend onboarding wizard to reappear; load_config() is mode-aware, so invoking it unconditionally is safe. * lint fix * style: ruff format (auto) * fix mypy --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 31b746b commit 9f25216

2 files changed

Lines changed: 30 additions & 12 deletions

File tree

src/app/lifespan.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,13 @@ async def run_startup(app: FastAPI):
176176
logger.error("Runtime DB migration failed", error=str(e))
177177
raise
178178

179+
try:
180+
wcs = services.get("workspace_config_service")
181+
if wcs is not None:
182+
await wcs.hydrate_on_startup()
183+
except Exception as e:
184+
logger.error("Workspace config hydration failed at startup", error=str(e))
185+
179186
await TelemetryClient.send_event(Category.APPLICATION_STARTUP, MessageId.ORB_APP_STARTED)
180187

181188
# FastMCP requires its own lifespan to be entered before requests
@@ -217,9 +224,7 @@ async def run_shutdown(app: FastAPI):
217224
task.cancel()
218225
if background_tasks:
219226
await asyncio.gather(*background_tasks, return_exceptions=True)
220-
logger.info(
221-
"Background tasks cancelled at shutdown", count=len(background_tasks)
222-
)
227+
logger.info("Background tasks cancelled at shutdown", count=len(background_tasks))
223228

224229
# Drain any pending workspace_config DB-mirror tasks before we
225230
# tear down the engine. Without this, a save_config triggered

src/services/workspace_config_service.py

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from __future__ import annotations
2626

2727
import asyncio
28-
from typing import Any, Optional
28+
from typing import Any
2929

3030
from sqlalchemy.ext.asyncio import async_sessionmaker
3131

@@ -137,6 +137,19 @@ async def reload_config(self) -> OpenRAGConfig:
137137
self._cm._config = None
138138
return await self.load_config()
139139

140+
async def hydrate_on_startup(self) -> None:
141+
"""Eagerly populate ``config_manager._config`` from the DB at
142+
lifespan startup.
143+
144+
Without this, in `db` mode a restart leaves ``_config = None``
145+
and the synchronous ``get_openrag_config()`` falls back to
146+
defaults (no yaml exists) — so ``/api/settings`` reports
147+
``onboarding.current_step=0`` and the frontend flashes the
148+
wizard on every restart. ``load_config()`` is itself mode-aware,
149+
so this is safe to call unconditionally.
150+
"""
151+
await self.load_config()
152+
140153
async def is_onboarded(self) -> bool:
141154
mode = get_storage_mode()
142155
if mode == "files":
@@ -155,7 +168,7 @@ async def is_onboarded(self) -> bool:
155168
return False # no yaml fallback in pure-db mode
156169
return self._cm.load_config().edited
157170

158-
async def get_onboarding_step(self) -> Optional[Any]:
171+
async def get_onboarding_step(self) -> Any | None:
159172
"""Returns the legacy step indicator — usually an int index from
160173
the OnboardingState dataclass, sometimes None. Treat as opaque."""
161174
mode = get_storage_mode()
@@ -181,10 +194,10 @@ async def get_onboarding_step(self) -> Optional[Any]:
181194

182195
async def save_config(
183196
self,
184-
config: Optional[OpenRAGConfig] = None,
197+
config: OpenRAGConfig | None = None,
185198
*,
186199
preserve_edited: bool = False,
187-
actor_user_id: Optional[str] = None,
200+
actor_user_id: str | None = None,
188201
) -> bool:
189202
mode = get_storage_mode()
190203

@@ -217,7 +230,7 @@ async def save_config(
217230

218231
async def update_onboarding_state(
219232
self,
220-
actor_user_id: Optional[str] = None,
233+
actor_user_id: str | None = None,
221234
**kwargs: Any,
222235
) -> bool:
223236
mode = get_storage_mode()
@@ -278,8 +291,8 @@ def _install_yaml_write_hooks(self) -> None:
278291
cm._db_mirror_original_save = cm.save_config_file # type: ignore[attr-defined]
279292
cm._db_mirror_original_update_ob = cm.update_onboarding_state # type: ignore[attr-defined]
280293

281-
original_save = cm._db_mirror_original_save
282-
original_update_ob = cm._db_mirror_original_update_ob
294+
original_save = cm._db_mirror_original_save # type: ignore[attr-defined]
295+
original_update_ob = cm._db_mirror_original_update_ob # type: ignore[attr-defined]
283296

284297
def patched_save(config=None, preserve_edited: bool = False) -> bool:
285298
mode = get_storage_mode()
@@ -321,7 +334,7 @@ def patched_update_ob(**kwargs) -> bool:
321334

322335
def _apply_in_memory(
323336
self,
324-
config: Optional[OpenRAGConfig],
337+
config: OpenRAGConfig | None,
325338
preserve_edited: bool,
326339
) -> None:
327340
"""Mirror the in-process effects of ``ConfigManager.save_config_file``
@@ -373,7 +386,7 @@ async def _mirror_to_db(
373386
self,
374387
config: OpenRAGConfig,
375388
*,
376-
actor_user_id: Optional[str] = None,
389+
actor_user_id: str | None = None,
377390
) -> None:
378391
config_dict = config.to_dict()
379392

0 commit comments

Comments
 (0)