{"record":{"id":"e079c79e33101334","repo":"affaan-m/ECC","slug":"another-nasiko-lifecycle-operation-won-stale-lock-recovery","errorCode":null,"errorMessage":"Another Nasiko lifecycle operation won stale-lock recovery: ${lockPath}.","messagePattern":"Another Nasiko lifecycle operation won stale-lock recovery: (.+?)\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"scripts/lib/nasiko-release.js","lineNumber":373,"sourceCode":"          throw new Error(`Another Nasiko lifecycle operation won lock acquisition: ${lockPath}.`);\n        }\n        throw retryError;\n      }\n    }\n    throw error;\n  }\n  const isProcessAlive = options.isProcessAlive || processIsAlive;\n  if (!existing || isProcessAlive(existing.metadata.pid)) {\n    throw new Error(`Another Nasiko lifecycle operation is already in progress; inspect ${lockPath} before recovering a stale lock.`);\n  }\n  if (!removeLockIfOwned(lockPath, existing.stats, fileSystem)) {\n    throw new Error(`Nasiko lifecycle lock changed during stale-owner recovery: ${lockPath}.`);\n  }\n  try {\n    return createLifecycleLock(lockPath, fileSystem);\n  } catch (error) {\n    if (error.code === 'EEXIST') {\n      throw new Error(`Another Nasiko lifecycle operation won stale-lock recovery: ${lockPath}.`);\n    }\n    throw error;\n  }\n}\n\nasync function installNasiko(options = {}, dependencies = {}) {\n  const version = options.version || 'v0.1.0';\n  const base = getQualifiedRelease(version, dependencies.platform || process.platform, dependencies.arch || process.arch);\n  const release = dependencies.releaseOverride ? { ...base, ...dependencies.releaseOverride } : base;\n  const installDirectory = validateInstallDirectory(options.installDir || defaultInstallDirectory(release, dependencies.environment || process.env, dependencies.homeDirectory || os.homedir()));\n  const destination = path.join(installDirectory, release.binaryName);\n  const plan = { dryRun: Boolean(options.dryRun), version, platform: release.os, architecture: release.arch, manifestDigest: release.manifestDigest, binaryDigest: release.binaryDigest, registryOrigin: REGISTRY_ORIGIN, destination, license: release.license, sourceUrl: release.sourceUrl };\n  if (options.dryRun) return plan;\n  if (!options.yes) throw new Error('Nasiko installation requires explicit --yes consent.');\n  fs.mkdirSync(installDirectory, { recursive: true, mode: 0o755 });\n  assertPrivateInstallDirectory(installDirectory);\n  const releaseLock = acquireLifecycleLock(installDirectory);\n  const metadataPath = metadataPathFor(destination);","sourceCodeStart":355,"sourceCodeEnd":391,"githubUrl":"https://github.com/affaan-m/ECC/blob/8321021c54d670126ce3b2969d5deb880b4b0c2a/scripts/lib/nasiko-release.js#L355-L391","documentation":"After successfully removing a stale lock, the library re-attempts createLifecycleLock. If that create fails with EEXIST, another process created a new lock in the window between stale-lock removal and re-creation; it throws this error instead of looping. This is a benign lost-race indication.","triggerScenarios":"installNasiko/uninstallNasiko stale-lock recovery where a competing process acquires the lock immediately after the stale owner is removed — e.g. two recovery attempts firing simultaneously on a dead-owner lock.","commonSituations":"Two CI jobs both detecting the same stale lock and racing to recover it; a scheduled install colliding with a manual recovery; shared network filesystems with delayed metadata propagation amplifying the race window.","solutions":["Retry the operation after a short delay — the winner will finish and release the lock.","Add mutual exclusion above the library (per-host flock or a CI concurrency group) to avoid recovery races.","Confirm the winner is progressing (check the new lock's pid) and wait rather than fighting for the lock."],"exampleFix":"// before\ninstallNasiko({ yes: true }) // both recovery racers start\n// after (CI)\nconcurrency: { group: 'nasiko-install', cancel-in-progress: false }  # YAML: serialize jobs\ninstallNasiko({ yes: true })","handlingStrategy":"retry","validationCode":"// Check lock liveness before recovery to avoid racing other recoverers:\nconst lock = path.join(installDir, '.ecc-nasiko-lifecycle.lock')\nconst meta = JSON.parse(fs.readFileSync(lock, 'utf8'))\nlet alive = false\ntry { process.kill(meta.pid, 0); alive = true } catch (e) { alive = e.code !== 'ESRCH' }\nif (alive) throw new Error('owner still running; no recovery needed')","typeGuard":null,"tryCatchPattern":"try {\n  await installNasiko({ yes: true })\n} catch (e) {\n  if (e.message.includes('won stale-lock recovery')) {\n    await new Promise(r => setTimeout(r, 2000))\n    return installNasiko({ yes: true }) // let the winner finish first\n  }\n  throw e\n}","preventionTips":["Use a single recovery coordinator per host/installDir.","Serialize installs with CI concurrency groups.","Back off and retry; the winner holds a valid lock that will be released shortly."],"tags":["concurrency","locking","race-condition"],"backgroundTag":"address-already-in-use","analyzedSha":"8321021c54d670126ce3b2969d5deb880b4b0c2a","analyzedAt":"2026-09-16T10:08:13.343Z","contentChangedAt":"2026-09-16T10:08:13.343Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}