{"record":{"id":"b9a1e9631577c72f","repo":"juanfont/headscale","slug":"renaming-node-in-database-w","errorCode":null,"errorMessage":"renaming node in database: %w","messagePattern":"renaming node in database: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"hscontrol/db/node.go","lineNumber":205,"sourceCode":") error {\n\terr := dnsname.ValidLabel(newName)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"renaming node: %w\", err)\n\t}\n\n\t// Check if the new name is unique\n\tvar count int64\n\n\tif err := tx.Model(&types.Node{}).Where(\"given_name = ? AND id != ?\", newName, nodeID).Count(&count).Error; err != nil { //nolint:noinlineerr\n\t\treturn fmt.Errorf(\"checking name uniqueness: %w\", err)\n\t}\n\n\tif count > 0 {\n\t\treturn ErrNodeNameNotUnique\n\t}\n\n\tif err := tx.Model(&types.Node{}).Where(\"id = ?\", nodeID).Update(\"given_name\", newName).Error; err != nil { //nolint:noinlineerr\n\t\treturn fmt.Errorf(\"renaming node in database: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (hsdb *HSDatabase) NodeSetExpiry(nodeID types.NodeID, expiry *time.Time) error {\n\treturn hsdb.Write(func(tx *gorm.DB) error {\n\t\treturn NodeSetExpiry(tx, nodeID, expiry)\n\t})\n}\n\n// NodeSetExpiry sets a new expiry time for a node.\n// If expiry is nil, the node's expiry is disabled (node will never expire).\nfunc NodeSetExpiry(tx *gorm.DB, nodeID types.NodeID, expiry *time.Time) error {\n\treturn tx.Model(&types.Node{}).Where(\"id = ?\", nodeID).Update(\"expiry\", expiry).Error\n}\n\nfunc (hsdb *HSDatabase) DeleteNode(node *types.Node) error {","sourceCodeStart":187,"sourceCodeEnd":223,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/db/node.go#L187-L223","documentation":"The final step of RenameNode: an UPDATE of given_name for the node id. Failure is a database-level error — uniqueness race (another node took the name between the COUNT and UPDATE), lock, or the node id no longer existing. The uniqueness pre-check and update are not in a serializable snapshot, so a rare race can surface a constraint violation here.","triggerScenarios":"Two concurrent renames to the same name racing past the COUNT check; node deleted between check and update; DB lock/timeout.","commonSituations":"Automation renaming many nodes in parallel; API retry storms.","solutions":["Unwrap to check for a unique-constraint violation and map it to ErrNodeNameNotUnique semantics for the client","Serialize renames (single writer) or retry the whole rename on constraint races","Verify the node still exists before retrying"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"if err := db.RenameNode(tx, nodeID, name); err != nil {\n\tif errors.Is(err, db.ErrNodeNameNotUnique) || isUniqueViolation(err) {\n\t\treturn ErrNameTaken // surface as 409\n\t}\n\treturn err\n}","preventionTips":["Treat unique-violation here the same as ErrNodeNameNotUnique","Serialize rename operations through one queue","Include the node id in logs to correlate races"],"tags":["go","database","gorm","race-condition","naming"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}