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).
This commit is contained in:
風雪 committed 2026-09-15 10:43:55 +08:00
1 parent 534eacb0c3
commit 0ab07066ba
1 file changed
+21 -11
+21 -11
View File
@@ -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