{"record":{"id":"d89bff4949d5def0","repo":"fatedier/frp","slug":"control-manager-is-closed","errorCode":null,"errorMessage":"control manager is closed","messagePattern":"control manager is closed","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"server/control.go","lineNumber":125,"sourceCode":"// Add makes ctl the pending current generation and records the predecessor\n// finalization barrier it must wait for before activation.\nfunc (cm *ControlManager) Add(ctl *Control) error {\n\tfor {\n\t\t// Never wait for a run gate while holding cm.mu.\n\t\tcm.mu.RLock()\n\t\told := cm.ctlsByRunID[ctl.runID]\n\t\tcm.mu.RUnlock()\n\t\tif old != nil {\n\t\t\told.runMu.Lock()\n\t\t}\n\n\t\tcm.mu.Lock()\n\t\tif cm.closed {\n\t\t\tcm.mu.Unlock()\n\t\t\tif old != nil {\n\t\t\t\told.runMu.Unlock()\n\t\t\t}\n\t\t\treturn fmt.Errorf(\"control manager is closed\")\n\t\t}\n\t\tif cm.ctlsByRunID[ctl.runID] != old {\n\t\t\tcm.mu.Unlock()\n\t\t\tif old != nil {\n\t\t\t\told.runMu.Unlock()\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tid := ControlID(nextControlID.Add(1))\n\t\tif err := ctl.admit(cm, id); err != nil {\n\t\t\tcm.mu.Unlock()\n\t\t\tif old != nil {\n\t\t\t\told.runMu.Unlock()\n\t\t\t}\n\t\t\treturn err\n\t\t}\n","sourceCodeStart":107,"sourceCodeEnd":143,"githubUrl":"https://github.com/fatedier/frp/blob/6c8a8d0a97d03b44e9528d30b30c70cb9d61b405/server/control.go#L107-L143","documentation":"Thrown by ControlManager.Add when the manager has been closed (cm.closed == true) while a new Control is being admitted. The control manager is the frps-side registry of client control connections keyed by run ID; once Close() runs (server shutdown), no new controls can be registered and Add fails immediately.","triggerScenarios":"Calling ControlManager.Add(ctl) after ControlManager.Close() has already set cm.closed = true. This happens when a frpc client logs in concurrently with frps shutdown: the login goroutine builds a Control and calls Add, but the shutdown path won the race and marked the manager closed.","commonSituations":"Rolling restarts or SIGTERM of frps while clients are reconnecting; aggressive reconnect loops from frpc hitting a server that is draining; tests that close the manager then feed it leftover login connections.","solutions":["If you operate the deployment: let frps finish shutting down; frpc will reconnect to the new instance automatically via its reconnect loop.","If you embed frps as a library: stop accepting new login connections (close the listener) before calling ControlManager.Close(), so Add cannot race with shutdown.","Add a shutdown health gate in your login handler that rejects new sessions before constructing a Control.","In tests, synchronize Close() with in-flight Add() calls via a WaitGroup before asserting state."],"exampleFix":"// before\nlistener.Accept() // -> spawns login -> cm.Add(ctl) races with cm.Close()\n\n// after\n// stop accepting before closing the manager\nlistener.Close()\nloginWG.Wait()\ncm.Close()","handlingStrategy":"validation","validationCode":"// before spawning a login goroutine\nif cm.IsClosed() { // expose or track shutdown state\n    return errors.New(\"server shutting down, reject login\")\n}","typeGuard":null,"tryCatchPattern":"// Go: treat as terminal for this login attempt\nif err := cm.Add(ctl); err != nil {\n    if strings.Contains(err.Error(), \"control manager is closed\") {\n        // server draining: close conn, do not retry against this instance\n        conn.Close()\n        return\n    }\n    return err\n}","preventionTips":["Close the login listener before closing the ControlManager during shutdown.","Track in-flight logins with a WaitGroup and drain them before Close().","In clients, rely on the built-in reconnect loop instead of immediate retries during server restarts."],"tags":["go","frp","lifecycle","shutdown","concurrency"],"backgroundTag":null,"analyzedSha":"6c8a8d0a97d03b44e9528d30b30c70cb9d61b405","analyzedAt":"2026-08-15T06:53:27.215Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}