{"id":"64e5b38217ee42a1","repo":"pytest-dev/pytest","slug":"lock-path-got-renamed-after-successful-creation","errorCode":null,"errorMessage":"lock path got renamed after successful creation","messagePattern":"lock path got renamed after successful creation","errorType":"exception","errorClass":"OSError","httpStatus":null,"severity":"error","filePath":"src/_pytest/pathlib.py","lineNumber":302,"sourceCode":"            \"could not create numbered dir with prefix \"\n            f\"{prefix} in {root} after 10 tries\"\n        )\n\n\ndef create_cleanup_lock(p: Path) -> Path:\n    \"\"\"Create a lock to prevent premature directory cleanup.\"\"\"\n    lock_path = get_lock_path(p)\n    try:\n        fd = os.open(str(lock_path), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o644)\n    except FileExistsError as e:\n        raise OSError(f\"cannot create lockfile in {p}\") from e\n    else:\n        pid = os.getpid()\n        spid = str(pid).encode()\n        os.write(fd, spid)\n        os.close(fd)\n        if not lock_path.is_file():\n            raise OSError(\"lock path got renamed after successful creation\")\n        return lock_path\n\n\ndef register_cleanup_lock_removal(lock_path: Path, register: Any) -> Any:\n    \"\"\"Register a cleanup function for removing a lock.\"\"\"\n    pid = os.getpid()\n\n    def cleanup_on_exit(lock_path: Path = lock_path, original_pid: int = pid) -> None:\n        current_pid = os.getpid()\n        if current_pid != original_pid:\n            # fork\n            return\n        try:\n            lock_path.unlink()\n        except OSError:\n            pass\n\n    return register(cleanup_on_exit)","sourceCodeStart":284,"sourceCodeEnd":320,"githubUrl":"https://github.com/pytest-dev/pytest/blob/98b357f69e380da908740a212288d73b2ee06687/src/_pytest/pathlib.py#L284-L320","documentation":"After successfully creating the cleanup lock file, pytest re-checks it still exists. If another process renamed or removed it in the window between creation and verification, it raises OSError as a race-condition guard, signalling the lock is no longer authoritative.","triggerScenarios":"Another concurrent process deletes/renames the freshly-created .lock file before the is_file() re-check at pathlib.py:301 completes. Essentially a TOCTOU race on the lock path.","commonSituations":"Multiple cache-cleaner or pytest processes contending over the same directory; external cleanup (cron, janitor) touching the cache dir mid-run.","solutions":["Run only one cache-cleaner / pytest per cache directory at a time","Give each worker its own --cachedir / TMPDIR to remove contention","Retry the pytest invocation once the concurrent cleanup has stopped"],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"import time\nfor attempt in range(3):\n    try:\n        create_cleanup_lock(p)\n        break\n    except OSError:\n        time.sleep(0.1)\nelse:\n    raise","preventionTips":["Run a single cache cleaner per cache directory to remove the race","Give concurrent workers distinct cache dirs"],"tags":["pathlib","cache","lock","race-condition","pytest"],"analyzedSha":"98b357f69e380da908740a212288d73b2ee06687","analyzedAt":"2026-08-04T20:26:34.442Z","schemaVersion":2}