{"record":{"id":"c738591828f30e0b","repo":"router-for-me/CLIProxyAPI","slug":"node-heartbeat-timeout-plus-reclaim-grace-must-exc","errorCode":null,"errorMessage":"node heartbeat timeout plus reclaim grace must exceed CPA heartbeat timeout plus cancel bound","messagePattern":"node heartbeat timeout plus reclaim grace must exceed CPA heartbeat timeout plus cancel bound","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/config/credential_concurrency.go","lineNumber":184,"sourceCode":"\t}\n\treturn nil\n}\n\n// ValidateCredentialConcurrencyLifecycle verifies the Home lifecycle timing safety invariant.\nfunc ValidateCredentialConcurrencyLifecycle(nodeHeartbeatTimeout time.Duration, cfg CredentialConcurrencyConfig) error {\n\tif nodeHeartbeatTimeout <= 0 {\n\t\treturn fmt.Errorf(\"credential concurrency lifecycle durations must be positive\")\n\t}\n\tif errValidate := ValidateCredentialConcurrency(cfg); errValidate != nil {\n\t\treturn errValidate\n\t}\n\tleft, leftOverflow := addCredentialConcurrencyDuration(nodeHeartbeatTimeout, cfg.ReclaimGrace)\n\tright, rightOverflow := addCredentialConcurrencyDuration(cfg.CPAHeartbeatTimeout, cfg.CPACancelBound)\n\tif leftOverflow || rightOverflow {\n\t\treturn fmt.Errorf(\"credential concurrency lifecycle timing safety invariant overflows\")\n\t}\n\tif left <= right {\n\t\treturn fmt.Errorf(\"node heartbeat timeout plus reclaim grace must exceed CPA heartbeat timeout plus cancel bound\")\n\t}\n\treturn nil\n}\n\nfunc addCredentialConcurrencyDuration(left time.Duration, right time.Duration) (time.Duration, bool) {\n\tif right > 0 && left > time.Duration(1<<63-1)-right {\n\t\treturn 0, true\n\t}\n\treturn left + right, false\n}\n","sourceCodeStart":166,"sourceCodeEnd":195,"githubUrl":"https://github.com/router-for-me/CLIProxyAPI/blob/78f0c4079e3e6273d65d03b5549cffc898703264/internal/config/credential_concurrency.go#L166-L195","documentation":"The core timing-safety invariant of the credential concurrency lifecycle: nodeHeartbeatTimeout + reclaim-grace must be strictly greater than cpa-heartbeat-timeout + cpa-cancel-bound. It guarantees a node's lease cannot be reclaimed (and its credentials reused) while a CPA-side cancellation is still possibly in flight, preventing double-use of a credential. Violating the inequality rejects the config.","triggerScenarios":"config.yaml where nodeHeartbeatTimeout + reclaim-grace <= cpa-heartbeat-timeout + cpa-cancel-bound, e.g. node timeout 10s + reclaim-grace 5s = 15s vs cpa-heartbeat-timeout 20ms + cpa-cancel-bound 5s = 5.02s passes; but node timeout 2s + grace 1s = 3s vs cpa timeout 2s + cancel bound 5s = 7s fails. Typical failing case: large cpa-cancel-bound with a small node heartbeat timeout.","commonSituations":"Raising cpa-heartbeat-timeout or cpa-cancel-bound for slow networks without raising the node heartbeat timeout; shrinking reclaim-grace to reclaim credentials faster; embedding the SDK and choosing a small nodeHeartbeatTimeout.","solutions":["Increase the node heartbeat timeout and/or reclaim-grace so their sum exceeds cpa-heartbeat-timeout + cpa-cancel-bound with margin.","Or reduce cpa-heartbeat-timeout / cpa-cancel-bound.","A safe starting point: node timeout 30s, reclaim-grace 5s, cpa-heartbeat-timeout 3s, cpa-cancel-bound 5s.","Re-run validation after each change until the inequality holds."],"exampleFix":"# before (config.yaml)\ncredential-concurrency:\n  cpa-heartbeat-timeout: 10s\n  cpa-cancel-bound: 5s\n  reclaim-grace: 1s\n# nodeHeartbeatTimeout=5s -> 5+1=6 <= 10+5=15 (fails)\n\n# after\ncredential-concurrency:\n  cpa-heartbeat-timeout: 3s\n  cpa-cancel-bound: 5s\n  reclaim-grace: 5s\n# nodeHeartbeatTimeout=30s -> 30+5=35 > 3+5=8 (passes)","handlingStrategy":"validation","validationCode":"// Go: check the invariant before applying config.\nfunc timingInvariantOK(nodeHeartbeatTimeout time.Duration, c config.CredentialConcurrencyConfig) bool {\n    return nodeHeartbeatTimeout+c.ReclaimGrace > c.CPAHeartbeatTimeout+c.CPACancelBound\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Whenever you change any of the four timings, recompute both sums and keep left > right with margin.","Start from defaults (node 30s, grace 5s vs CPA 3s + 5s) and change one value at a time.","Add the invariant to your config-change checklist."],"tags":["config","validation","credential-concurrency","lifecycle","timing-invariant"],"backgroundTag":null,"analyzedSha":"78f0c4079e3e6273d65d03b5549cffc898703264","analyzedAt":"2026-08-15T12:26:37.444Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}