"""Chrome 프로필 슬롯 배정 — 프로세스가 여러 개여도 같은 프로필을 잡지 않게. Chrome 은 user_data_dir 당 인스턴스 하나만 허용한다(ProcessSingleton). 워커 프로세스를 여러 개 띄우면 두 번째가 "기존 브라우저 세션에서 여는 중입니다" 로 죽고, 그 소스의 검색이 전부 TargetClosedError 로 실패한다(2026-08-05 실측: 워커 2프로세스 → 잡 2건 DEAD). PID 를 경로에 넣는 방식은 쓰지 않는다 — 재시작마다 새 디렉터리가 생겨 **웜 쿠키 (cf_clearance·Akamai 세션)를 매번 버리고** 디렉터리가 무한히 쌓인다. 대신 슬롯 번호를 파일 락으로 선점한다: 살아있는 프로세스가 없으면 같은 슬롯을 다시 쓰므로 쿠키가 유지되고, 프로세스가 죽으면 OS 가 락을 자동 해제해 슬롯이 곧바로 회수된다(정리 코드 불필요). """ import fcntl import os from common.logger import LOG # 잡은 락 핸들을 프로세스 수명 동안 살려 둔다 — GC 되면 락이 풀려 다른 프로세스가 같은 슬롯을 집는다. _HELD: list = [] def claim_profile_slot(base_dir: str, source: str, worker_index: int = 0, max_slots: int = 32) -> str: """이 프로세스가 배타적으로 쓸 프로필 경로를 돌려준다. 같은 (source, worker_index) 라도 프로세스가 다르면 다른 슬롯이 배정된다. 슬롯을 모두 뺏겼으면 마지막 후보를 그대로 쓴다 — 검색을 못 하는 것보다 낫고, 그 경우엔 로그로 드러난다(동시 프로세스 수가 max_slots 를 넘었다는 뜻). """ os.makedirs(base_dir, exist_ok=True) for slot in range(max_slots): path = os.path.join(base_dir, f"lps_{source}_w{worker_index}_s{slot}") os.makedirs(path, exist_ok=True) lock_path = path + ".lock" try: fh = open(lock_path, "w") fcntl.flock(fh, fcntl.LOCK_EX | fcntl.LOCK_NB) # 비블로킹 — 남이 쥐고 있으면 즉시 실패 except OSError: continue fh.write(f"{os.getpid()}\n") fh.flush() _HELD.append(fh) return path fallback = os.path.join(base_dir, f"lps_{source}_w{worker_index}_s{max_slots - 1}") LOG.w(f"[profile] {source} 슬롯 {max_slots}개 모두 사용 중 — {fallback} 공유(프로세스 과다)") return fallback