fix(config): read under the commit lock and publish saves in write order

- ConfigStore.read takes the commit lock, so no read handle is open while
  config.json is replaced (Windows refuses that replace).
- os.replace is retried briefly on a Windows PermissionError (antivirus or
  indexer handles) before the save is reported as not written.
- The temp file is written beside realpath(config.json), so a symlinked
  config keeps its link and a bind mount fails truthfully.
- commit() takes a publish callback that runs under the lock after the
  write; settings and LoRA saves publish there, so two concurrent saves
  reach the runtime in write order and the runtime ends on the last one.
- The concurrency tests now wait until the second caller is blocked on the
  lock (tests/lock_probe.py) before releasing the first.
This commit is contained in:
Vito Sansevero
2026-10-22 20:04:09 -07:00
parent 9d1e5a0e22
commit dead4a7aa3
6 changed files with 356 additions and 34 deletions
+15 -7
View File
@@ -1026,11 +1026,19 @@ class AdminRoutesMixin:
if error:
return web.json_response({"success": False, "error": error}, status=400)
published = {}
def publish(_config):
# Under the commit lock: saves reach the runtime in the order
# they were written, so the runtime ends on the last one.
published.update(self._publish_settings(updates, new_roots))
try:
_saved, revision = await self._run_in_executor(
get_config_store().commit,
_settings_mutation(updates, new_roots),
expected_revision=expected_revision,
publish=publish,
)
except (ConfigUnreadable, RevisionConflict, OSError) as exc:
return self._config_commit_failure(exc)
@@ -1039,7 +1047,10 @@ class AdminRoutesMixin:
{
"success": True,
"message": "Settings saved successfully",
**self._publish_settings(updates, new_roots, revision),
"restart_required": bool(published["restart_required_fields"]),
"persisted": True,
"revision": revision,
**published,
}
)
except Exception as e:
@@ -1049,12 +1060,12 @@ class AdminRoutesMixin:
status=500,
)
def _publish_settings(self, updates, new_roots, revision):
"""Install committed settings at runtime; the ConfigCommit fields.
def _publish_settings(self, updates, new_roots):
"""Install committed settings at runtime (inside the config commit).
The values are already durable. A publication that fails is reported
as ``runtime_applied: false`` with the fields that need a restart;
nothing is retried or rolled back.
nothing is retried or rolled back. Never raises.
"""
from ..config import PromptManagerConfig
@@ -1073,9 +1084,6 @@ class AdminRoutesMixin:
runtime_applied = False
restart_fields.append("gallery_root_paths")
return {
"restart_required": bool(restart_fields),
"persisted": True,
"revision": revision,
"runtime_applied": runtime_applied,
"restart_required_fields": restart_fields,
}