{"record":{"id":"f8cc63b5def4d5fc","repo":"hashicorp/nomad","slug":"node-doesn-t-have-a-drain-strategy-set","errorCode":null,"errorMessage":"node doesn't have a drain strategy set","messagePattern":"node doesn't have a drain strategy set","errorType":"console","errorClass":null,"httpStatus":null,"severity":"error","filePath":"nomad/drainer/draining_node.go","lineNumber":62,"sourceCode":"\n\t// Should never happen\n\tif n.node == nil || n.node.DrainStrategy == nil {\n\t\treturn false, time.Time{}\n\t}\n\n\treturn n.node.DrainStrategy.DeadlineTime()\n}\n\n// IsDone returns if the node is done draining batch and service allocs. System\n// allocs must be stopped before marking drain complete unless they're being\n// ignored.\nfunc (n *drainingNode) IsDone() (bool, error) {\n\tn.l.RLock()\n\tdefer n.l.RUnlock()\n\n\t// Should never happen\n\tif n.node == nil || n.node.DrainStrategy == nil {\n\t\treturn false, fmt.Errorf(\"node doesn't have a drain strategy set\")\n\t}\n\n\t// Retrieve the allocs on the node\n\tallocs, err := n.state.AllocsByNode(nil, n.node.ID)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tfor _, alloc := range allocs {\n\t\t// System and plugin jobs are only stopped after a node is\n\t\t// done draining everything else, so ignore them here.\n\t\tif alloc.Job.Type == structs.JobTypeSystem || alloc.Job.IsPlugin() {\n\t\t\tcontinue\n\t\t}\n\n\t\t// If there is a non-terminal we aren't done\n\t\tif !alloc.ClientTerminalStatus() {\n\t\t\treturn false, nil","sourceCodeStart":44,"sourceCodeEnd":80,"githubUrl":"https://github.com/hashicorp/nomad/blob/482b49bf1aec006f089bcfc7e632d8f6ac303e5e/nomad/drainer/draining_node.go#L44-L80","documentation":"drainingNode.IsDone checks whether a node's drain has completed, but the node must have a DrainStrategy set to make that determination. Nomad returns this error (marked 'should never happen') when the cached node has lost its drain strategy, meaning the drain bookkeeping state is inconsistent.","triggerScenarios":"assertDrainingNode calls IsDone on a drainingNode whose cached n.node is nil or n.node.DrainStrategy is nil — e.g. the drain was removed concurrently while the drainer still held the node object.","commonSituations":"Drain cancelled (nomad node drain -disable) at nearly the same moment the drainer evaluates completion; leader failover with stale cached node data; bugs in NodeDrain Raft updates clearing DrainStrategy while the node remains in the drainer's set.","solutions":["Verify the node's drain state with `nomad node status <id>` and `nomad node drain -status <id>`","If the drain was intentionally stopped, this error is transient — the drainer will drop the node on next refresh","If a drain is desired, re-issue `nomad node drain -enable <id>`","Restart the leader to rebuild drainer state if it persists"],"exampleFix":"// before\ndrain, _ := node.DrainStrategy == nil, node\n// guard in tooling before asserting drain done\nif node.DrainStrategy != nil {\n    done, err := drainTracker.IsDone(node)\n}\n","handlingStrategy":"validation","validationCode":"node, _ := state.NodeByID(nil, nodeID)\nif node == nil || node.DrainStrategy == nil {\n    // not draining; skip IsDone check\n}","typeGuard":"func isDraining(n *structs.Node) bool { return n != nil && n.DrainStrategy != nil }","tryCatchPattern":"try {\n    done, err := dn.IsDone()\n} catch (e) {\n    if (e.message.includes(\"drain strategy set\")) {\n        // drain was cancelled/removed; drop node from tracking\n        return\n    }\n    throw e\n}","preventionTips":["Avoid toggling node drains concurrently with drain-completion automation","After leader failover, re-read node state before asserting drain status","Treat this error as 'drain no longer active' rather than retrying"],"tags":["nomad","node-drain","state-inconsistency"],"backgroundTag":"drain-strategy-missing","analyzedSha":"482b49bf1aec006f089bcfc7e632d8f6ac303e5e","analyzedAt":"2026-09-04T07:54:14.808Z","contentChangedAt":"2026-09-04T07:54:14.808Z","schemaVersion":2},"datasetVersion":"2026-09-08T15:18:49.778Z"}