From 0ab07066ba9bdb3683255a3e8dfd0acc2cbcb3cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A2=A8=E9=9B=AA?= Date: Tue, 15 Sep 2026 10:43:55 +0800 Subject: [PATCH] fix(listener): do not hold mutation lock during Mihomo ApplyConfig RegenerateConfig and async flush generated YAML under s.mu then released the lock before ApplyConfig. Holding the lock across ApplyConfig deadlocked Create when a concurrent regenerate was blocked in the applier (CI 10m timeout). fix(listener): do not hold mutation lock during Mihomo ApplyConfig RegenerateConfig and async flush generated YAML under s.mu then released the lock before ApplyConfig. Holding the lock across ApplyConfig deadlocked Create when a concurrent regenerate was blocked in the applier (CI 10m timeout). fix(listener): do not hold mutation lock during Mihomo ApplyConfig RegenerateConfig and async flush generated YAML under s.mu then released the lock before ApplyConfig. Holding the lock across ApplyConfig deadlocked Create when a concurrent regenerate was blocked in the applier (CI 10m timeout). fix(listener): do not hold mutation lock during Mihomo ApplyConfig RegenerateConfig and async flush generated YAML under s.mu then released the lock before ApplyConfig. Holding the lock across ApplyConfig deadlocked Create when a concurrent regenerate was blocked in the applier (CI 10m timeout). fix(listener): do not hold mutation lock during Mihomo ApplyConfig RegenerateConfig and async flush generated YAML under s.mu then released the lock before ApplyConfig. Holding the lock across ApplyConfig deadlocked Create when a concurrent regenerate was blocked in the applier (CI 10m timeout). fix(listener): do not hold mutation lock during Mihomo ApplyConfig RegenerateConfig and async flush generated YAML under s.mu then released the lock before ApplyConfig. Holding the lock across ApplyConfig deadlocked Create when a concurrent regenerate was blocked in the applier (CI 10m timeout). fix(listener): do not hold mutation lock during Mihomo ApplyConfig RegenerateConfig and async flush generated YAML under s.mu then released the lock before ApplyConfig. Holding the lock across ApplyConfig deadlocked Create when a concurrent regenerate was blocked in the applier (CI 10m timeout). fix(listener): do not hold mutation lock during Mihomo ApplyConfig RegenerateConfig and async flush generated YAML under s.mu then released the lock before ApplyConfig. Holding the lock across ApplyConfig deadlocked Create when a concurrent regenerate was blocked in the applier (CI 10m timeout). fix(listener): do not hold mutation lock during Mihomo ApplyConfig RegenerateConfig and async flush generated YAML under s.mu then released the lock before ApplyConfig. Holding the lock across ApplyConfig deadlocked Create when a concurrent regenerate was blocked in the applier (CI 10m timeout). fix(listener): do not hold mutation lock during Mihomo ApplyConfig RegenerateConfig and async flush generated YAML under s.mu then released the lock before ApplyConfig. Holding the lock across ApplyConfig deadlocked Create when a concurrent regenerate was blocked in the applier (CI 10m timeout). fix(listener): do not hold mutation lock during Mihomo ApplyConfig RegenerateConfig and async flush generated YAML under s.mu then released the lock before ApplyConfig. Holding the lock across ApplyConfig deadlocked Create when a concurrent regenerate was blocked in the applier (CI 10m timeout). fix(listener): do not hold mutation lock during Mihomo ApplyConfig RegenerateConfig and async flush generated YAML under s.mu then released the lock before ApplyConfig. Holding the lock across ApplyConfig deadlocked Create when a concurrent regenerate was blocked in the applier (CI 10m timeout). --- backend/internal/listener/service.go | 32 ++++++++++++++++++---------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/backend/internal/listener/service.go b/backend/internal/listener/service.go index 56c8072..9b8bc5d 100644 --- a/backend/internal/listener/service.go +++ b/backend/internal/listener/service.go @@ -64,10 +64,17 @@ func (s *Service) flushConfigApply() { if !run { return } + // Generate under the mutation lock, ApplyConfig outside — ApplyConfig may + // restart Mihomo and block for a long time; holding s.mu would deadlock + // concurrent Create/Update/Delete (and the concurrency test). s.mu.Lock() - defer s.mu.Unlock() - if err := s.regenerateConfigLocked(); err != nil { - //nolint:keep — panel stays source of truth; operator can Reload from UI. + yamlContent, err := s.generateConfigYAMLLocked() + s.mu.Unlock() + if err != nil { + log.Printf("warning: Mihomo config generate after listener change failed: %v", err) + return + } + if err := s.applyGeneratedYAML(yamlContent); err != nil { log.Printf("warning: Mihomo config reload after listener change failed: %v", err) } } @@ -290,18 +297,21 @@ func (s *Service) ensureEndpointAvailable(candidate *models.Listener) error { } func (s *Service) RegenerateConfig() error { - // Keep generation and application ordered with listener mutations so an old - // snapshot cannot overwrite the configuration from a newer Create/Update/Delete. + // Snapshot YAML under the mutation lock, then ApplyConfig without holding + // s.mu so listener CRUD is not blocked for the duration of Mihomo restart. s.mu.Lock() - defer s.mu.Unlock() - return s.regenerateConfigLocked() + yamlContent, err := s.generateConfigYAMLLocked() + s.mu.Unlock() + if err != nil { + return err + } + return s.applyGeneratedYAML(yamlContent) } +// regenerateConfigLocked generates and applies while the caller holds s.mu. +// Prefer RegenerateConfig / flushConfigApply for production paths so ApplyConfig +// does not keep the mutation lock. Kept for any remaining call sites that already hold mu. func (s *Service) regenerateConfigLocked() error { - // Caller must hold s.mu. Keep the lock for the whole generate+apply so - // concurrent Create/Update/Delete cannot interleave DB mutations with a - // half-applied config. Long ApplyConfig is mitigated by a higher frontend - // mutation timeout and async user-credential reloads. yamlContent, err := s.generateConfigYAMLLocked() if err != nil { return err