summaryrefslogtreecommitdiff
path: root/src/services/api
diff options
context:
space:
mode:
authorNavneetBarwal-RA <navneet51.barwal@routerarchitects.com>2026-02-09 16:52:22 +0530
committerNavneetBarwal-RA <navneet51.barwal@routerarchitects.com>2026-02-09 17:50:16 +0530
commit5b47c96a6d1f9ccaf46180599f44d7f9b188ad69 (patch)
tree04caeb5978aacdaed9edb92360bc6c4eaefc623b /src/services/api
parent407b4e2486387051cf7d4bb83bbe310a87404bb7 (diff)
downloadvyos-1x-5b47c96a6d1f9ccaf46180599f44d7f9b188ad69.tar.gz
vyos-1x-5b47c96a6d1f9ccaf46180599f44d7f9b188ad69.zip
http-api: T8235: Use asyncio lock to prevent deadlock in config-file handler
The HTTPS API config-file handler previously used a blocking threading.Lock in an async FastAPI endpoint, which can block the event loop and make the API unresponsive under concurrent load. This commit replaces the blocking lock with an asyncio.Lock to serialize config operations without blocking async execution, resolving the reported deadlock when concurrent /config-file load operations occur. The behavior of run_in_threadpool and background task scheduling remains consistent with prior logic, so the handler preserves existing commit and commit-confirm semantics.
Diffstat (limited to 'src/services/api')
-rw-r--r--src/services/api/rest/routers.py103
1 files changed, 50 insertions, 53 deletions
diff --git a/src/services/api/rest/routers.py b/src/services/api/rest/routers.py
index c8102c980..25c29b4c3 100644
--- a/src/services/api/rest/routers.py
+++ b/src/services/api/rest/routers.py
@@ -18,6 +18,7 @@
# pylint: disable=wildcard-import,unused-wildcard-import
# pylint: disable=broad-exception-caught
+import asyncio
import json
import copy
import logging
@@ -85,6 +86,7 @@ LOG = logging.getLogger('http_api.routers')
lock = Lock()
+asynclock = asyncio.Lock()
def check_auth(key_list, key):
key_id = None
@@ -664,68 +666,63 @@ async def config_file_op(data: ConfigFileModel, background_tasks: BackgroundTask
# A non-zero confirm_time will start commit-confirm timer on commit
confirm_time = data.confirm_time
- lock.acquire()
+ # Serialize config operations without blocking the event loop
+ async with asynclock:
+ try:
+ if op == 'save':
+ path = data.file or '/config/config.boot'
+ msg = session.save_config(path)
+
+ elif op in ('load', 'merge'):
+ if data.file:
+ path = data.file
+ elif data.string:
+ path = '/tmp/config.file'
+ with open(path, 'w') as f:
+ f.write(data.string)
+ else:
+ return error(400, 'Missing required field "file | string"')
- try:
- if op == 'save':
- if data.file:
- path = data.file
- else:
- path = '/config/config.boot'
- msg = session.save_config(path)
- elif op in ('load', 'merge'):
- if data.file:
- path = data.file
- elif data.string:
- path = '/tmp/config.file'
- with open(path, 'w') as f:
- f.write(data.string)
- else:
- return error(400, 'Missing required field "file | string"')
+ match op:
+ case 'load':
+ session.migrate_and_load_config(path)
+ case 'merge':
+ session.merge_config(path, destructive=data.destructive)
- match op:
- case 'load':
- session.migrate_and_load_config(path)
- case 'merge':
- session.merge_config(path, destructive=data.destructive)
+ config = Config(session_env=env)
+ d = get_config_diff(config)
- config = Config(session_env=env)
- d = get_config_diff(config)
+ state.confirm_time = confirm_time if confirm_time else 0
- state.confirm_time = confirm_time if confirm_time else 0
+ if not d.is_node_changed(['service', 'https']):
+ if confirm_time:
+ out, err = await run_in_threadpool(run_commit_confirm, state)
+ else:
+ out, err = await run_in_threadpool(run_commit, state)
- if not d.is_node_changed(['service', 'https']):
- if confirm_time:
- out, err = await run_in_threadpool(run_commit_confirm, state)
if err:
raise err
- msg = msg + out if msg else out
+ msg = (msg or '') + (out or '')
else:
- out, err = await run_in_threadpool(run_commit, state)
- if err:
- raise err
- msg = msg + out if msg else out
+ if confirm_time:
+ background_tasks.add_task(call_commit_confirm, state)
+ else:
+ background_tasks.add_task(call_commit, state)
+ out = self_ref_msg
+ msg = (msg or '') + (out or '')
+ elif op == 'confirm':
+ msg = session.confirm()
else:
- if confirm_time:
- background_tasks.add_task(call_commit_confirm, state)
- else:
- background_tasks.add_task(call_commit, state)
- out = self_ref_msg
- msg = msg + out if msg else out
-
- elif op == 'confirm':
- msg = session.confirm()
- else:
- return error(400, f"'{op}' is not a valid operation")
- except ConfigSessionError as e:
- return error(400, str(e))
- except Exception:
- LOG.critical(traceback.format_exc())
- return error(500, 'An internal error occured. Check the logs for details.')
- finally:
- if 'IN_COMMIT_CONFIRM' in env:
- del env['IN_COMMIT_CONFIRM']
- lock.release()
+ return error(400, f"'{op}' is not a valid operation")
+
+ except ConfigSessionError as e:
+ return error(400, str(e))
+ except Exception:
+ LOG.critical(traceback.format_exc())
+ return error(500, 'An internal error occured. Check the logs for details.')
+ finally:
+ if 'IN_COMMIT_CONFIRM' in env:
+ del env['IN_COMMIT_CONFIRM']
return success(msg)