{"record":{"id":"c1c3f71835eae047","repo":"hashicorp/nomad","slug":"can-not-set-node-s-scheduling-eligibility-to-eligi-c1c3f7","errorCode":null,"errorMessage":"can not set node's scheduling eligibility to eligible while it is draining","messagePattern":"can not set node's scheduling eligibility to eligible while it is draining","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"nomad/state/state_store.go","lineNumber":1330,"sourceCode":"\t\treturn fmt.Errorf(\"node lookup failed: %v\", err)\n\t}\n\tif existing == nil {\n\t\treturn fmt.Errorf(\"node not found\")\n\t}\n\n\t// Copy the existing node\n\texistingNode := existing.(*structs.Node)\n\tcopyNode := existingNode.Copy()\n\tcopyNode.StatusUpdatedAt = updatedAt\n\n\t// Add the event if given\n\tif event != nil {\n\t\tappendNodeEvents(index, copyNode, []*structs.NodeEvent{event})\n\t}\n\n\t// Check if this is a valid action\n\tif copyNode.DrainStrategy != nil && eligibility == structs.NodeSchedulingEligible {\n\t\treturn fmt.Errorf(\"can not set node's scheduling eligibility to eligible while it is draining\")\n\t}\n\n\t// Update the eligibility in the copy\n\tcopyNode.SchedulingEligibility = eligibility\n\tcopyNode.ModifyIndex = index\n\n\t// Insert the node\n\tif err := txn.Insert(\"nodes\", copyNode); err != nil {\n\t\treturn fmt.Errorf(\"node update failed: %v\", err)\n\t}\n\tif err := txn.Insert(\"index\", &IndexEntry{\"nodes\", index}); err != nil {\n\t\treturn fmt.Errorf(\"index update failed: %v\", err)\n\t}\n\n\treturn nil\n}\n\n// UpsertNodeEvents adds the node events to the nodes, rotating events as","sourceCodeStart":1312,"sourceCodeEnd":1348,"githubUrl":"https://github.com/hashicorp/nomad/blob/482b49bf1aec006f089bcfc7e632d8f6ac303e5e/nomad/state/state_store.go#L1312-L1348","documentation":"updateNodeEligibilityImpl rejects setting a node's SchedulingEligibility to 'eligible' while the node still has a DrainStrategy set — a draining node must not accept new allocations. This is a domain invariant check, not an infrastructure failure.","triggerScenarios":"Calling UpdateNodeEligibility(nodeID, NodeSchedulingEligible) on a node whose DrainStrategy is non-nil; a drain-stop request that only marks eligible without clearing the drain strategy (drain==nil but strategy already set); UpsertPlanResults attempting to flip eligibility on a draining node.","commonSituations":"Operators clicking 'set eligible' on a node mid-drain in the Nomad UI/CLI; automation that sets eligible=true before the drain completes; API users unaware drain must complete or be cancelled first.","solutions":["Complete or cancel the drain first (UpdateNodeDrain with drain==nil to remove DrainStrategy), then set eligibility.","Wait for the drain to finish — poll the node until DrainStrategy is cleared — before setting eligible.","Send a combined drain-stop request with markEligible=true instead of separate calls.","Check node.DrainStrategy != nil in the client before issuing the eligibility change."],"exampleFix":"// before\nstore.UpdateNodeEligibility(idx, nodeID, structs.NodeSchedulingEligible) // fails: still draining\n// after\n// stop the drain first; markEligible=true re-enables scheduling in one transaction\nerr := store.UpdateNodeDrain(idx, nodeID, nil, true)","handlingStrategy":"validation","validationCode":"node, err := store.NodeByID(nil, nodeID)\nif err != nil {\n    return err\n}\nif node.DrainStrategy != nil && eligibility == structs.NodeSchedulingEligible {\n    return fmt.Errorf(\"node %s is draining; complete or cancel the drain before setting eligible\", nodeID)\n}","typeGuard":"func canSetEligible(n *structs.Node) bool {\n    return n != nil && n.DrainStrategy == nil\n}","tryCatchPattern":"if err := store.UpdateNodeEligibility(idx, nodeID, structs.NodeSchedulingEligible); err != nil {\n    if strings.Contains(err.Error(), \"while it is draining\") {\n        // cancel the drain first, then retry\n        return store.UpdateNodeDrain(idx, nodeID, nil, true)\n    }\n    return err\n}","preventionTips":["Use UpdateNodeDrain with drain=nil and markEligible=true to stop draining and enable scheduling atomically.","In UIs/CLIs, disable the 'set eligible' action while the node shows an active drain strategy.","Poll node status until DrainStrategy is cleared before enabling scheduling in scripts."],"tags":["nomad","state-store","node-drain","scheduling-eligibility","invalid-state"],"backgroundTag":"invalid-state-transition","analyzedSha":"482b49bf1aec006f089bcfc7e632d8f6ac303e5e","analyzedAt":"2026-09-04T07:54:14.808Z","contentChangedAt":"2026-09-04T07:54:14.808Z","schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}