1+ import asyncio
12import json
23import logging
34import os
3536 WEBUI_NAME ,
3637 log ,
3738)
38- from open_webui .internal .db import Base , get_db
39+ from open_webui .internal .db import Base , get_db , get_async_db
3940from open_webui .utils .redis import get_redis_connection
4041
4142
@@ -90,6 +91,7 @@ def load_json_config():
9091
9192
9293def save_to_db (data ):
94+ """Sync save — used ONLY at startup/import time."""
9395 with get_db () as db :
9496 existing_config = db .query (Config ).first ()
9597 if not existing_config :
@@ -102,12 +104,39 @@ def save_to_db(data):
102104 db .commit ()
103105
104106
107+ async def async_save_to_db (data ):
108+ """Async save — used for ALL runtime config persistence."""
109+ from sqlalchemy import select
110+
111+ async with get_async_db () as db :
112+ result = await db .execute (select (Config ).limit (1 ))
113+ existing_config = result .scalars ().first ()
114+ if not existing_config :
115+ new_config = Config (data = data , version = 0 )
116+ db .add (new_config )
117+ else :
118+ existing_config .data = data
119+ existing_config .updated_at = datetime .now ()
120+ db .add (existing_config )
121+ await db .commit ()
122+
123+
105124def reset_config ():
125+ """Sync reset — used ONLY at startup."""
106126 with get_db () as db :
107127 db .query (Config ).delete ()
108128 db .commit ()
109129
110130
131+ async def async_reset_config ():
132+ """Async reset — used at runtime."""
133+ from sqlalchemy import delete as sa_delete
134+
135+ async with get_async_db () as db :
136+ await db .execute (sa_delete (Config ))
137+ await db .commit ()
138+
139+
111140# When initializing, check if config.json exists and migrate it to the database
112141if os .path .exists (f'{ DATA_DIR } /config.json' ):
113142 data = load_json_config ()
@@ -144,6 +173,7 @@ def get_config_value(config_path: str):
144173
145174
146175def save_config (config ):
176+ """Sync save — used ONLY at startup/import time."""
147177 global CONFIG_DATA
148178 global PERSISTENT_CONFIG_REGISTRY
149179 try :
@@ -159,6 +189,23 @@ def save_config(config):
159189 return True
160190
161191
192+ async def async_save_config (config ):
193+ """Async save — used for ALL runtime config persistence."""
194+ global CONFIG_DATA
195+ global PERSISTENT_CONFIG_REGISTRY
196+ try :
197+ await async_save_to_db (config )
198+ CONFIG_DATA = config
199+
200+ # Trigger updates on all registered PersistentConfig entries
201+ for config_item in PERSISTENT_CONFIG_REGISTRY :
202+ config_item .update ()
203+ except Exception as e :
204+ log .exception (e )
205+ return False
206+ return True
207+
208+
162209T = TypeVar ('T' )
163210
164211ENABLE_PERSISTENT_CONFIG = os .environ .get ('ENABLE_PERSISTENT_CONFIG' , 'True' ).lower () == 'true'
@@ -202,6 +249,7 @@ def update(self):
202249 log .info (f'Updated { self .env_name } to new value { self .value } ' )
203250
204251 def save (self ):
252+ """Sync save — used ONLY at startup/import time."""
205253 log .info (f"Saving '{ self .env_name } ' to the database" )
206254 path_parts = self .config_path .split ('.' )
207255 sub_config = CONFIG_DATA
@@ -213,6 +261,19 @@ def save(self):
213261 save_to_db (CONFIG_DATA )
214262 self .config_value = self .value
215263
264+ async def async_save (self ):
265+ """Async save — used for ALL runtime config persistence."""
266+ log .info (f"Saving '{ self .env_name } ' to the database" )
267+ path_parts = self .config_path .split ('.' )
268+ sub_config = CONFIG_DATA
269+ for key in path_parts [:- 1 ]:
270+ if key not in sub_config :
271+ sub_config [key ] = {}
272+ sub_config = sub_config [key ]
273+ sub_config [path_parts [- 1 ]] = self .value
274+ await async_save_to_db (CONFIG_DATA )
275+ self .config_value = self .value
276+
216277
217278class AppConfig :
218279 _redis : Union [redis .Redis , redis .cluster .RedisCluster ] = None
@@ -246,12 +307,27 @@ def __setattr__(self, key, value):
246307 self ._state [key ] = value
247308 else :
248309 self ._state [key ].value = value
249- self ._state [key ].save ()
310+
311+ # At runtime (inside the event loop) persist via the async engine
312+ # to avoid blocking the loop and contending with the async DB pool.
313+ # At startup/import time, fall back to sync.
314+ try :
315+ loop = asyncio .get_running_loop ()
316+ loop .create_task (self ._async_persist (key ))
317+ except RuntimeError :
318+ self ._state [key ].save ()
250319
251320 if self ._redis and ENABLE_PERSISTENT_CONFIG :
252321 redis_key = f'{ self ._redis_key_prefix } :config:{ key } '
253322 self ._redis .set (redis_key , json .dumps (self ._state [key ].value ))
254323
324+ async def _async_persist (self , key ):
325+ """Persist a single config key via the async engine."""
326+ try :
327+ await self ._state [key ].async_save ()
328+ except Exception as e :
329+ log .error (f'Failed to async-persist config key { key } : { e } ' )
330+
255331 def __getattr__ (self , key ):
256332 if key not in self ._state :
257333 raise AttributeError (f"Config key '{ key } ' not found" )
0 commit comments