Watch at Ease세상을 읽고, 일상의 답을 찾다.

기술 · 실용 가이드

Python의 “database is locked”: 오류 코드와 실패 단계에 맞춰 재시도하기

시간 제한을 늘려도 오래된 WAL 스냅샷은 갱신되지 않습니다. 오류 코드, 실패 단계, Python 트랜잭션 모드를 기록한 뒤 재시도 범위를 결정하세요.

SQLite 실패 단계별 재시도 범위를 보여 주는 도해
독창적인 개념 일러스트: 쓰기 권한, 오래된 WAL 스냅샷, 읽기 연결에 막힌 커밋을 구분합니다. 보편적 재시도 루프가 아닙니다. Watch at Ease

같은 database is locked 메시지라도 다른 쓰기 연결을 기다리거나, 오래된 읽기 스냅샷을 버리거나, 아직 성공하지 않은 커밋을 끝내야 할 수 있습니다. 마지막 UPDATE만 무작정 반복하면 오래된 판단을 유지하거나 변경을 두 번 적용할 수 있습니다.

예외를 발생시킨 연결부터 확인하세요. Python과 실제 SQLite 버전, 데이터베이스 식별 정보, 저널 모드, 트랜잭션 제어 설정, BEGIN·UPDATE·COMMIT 중 실패한 단계를 기록합니다. 애플리케이션 정책을 바꾸기 전에 임시 데이터베이스로 재현하세요.

확장 코드와 실패 단계를 보존하기

Python 3.11부터 SQLite에서 발생한 예외에는 sqlite_errorcode와 sqlite_errorname이 붙습니다. getattr(exc, "sqlite_errorcode", None), getattr(exc, "sqlite_errorname", None)으로 기록하고, 전체 값을 보존한 뒤 code & 255로 기본 범주를 구하세요. SQLITE_BUSY는 5, SQLITE_LOCKED는 6, SQLITE_BUSY_SNAPSHOT은 517입니다.

BUSY는 대개 다른 연결과의 충돌입니다. LOCKED는 같은 연결의 끝나지 않은 문장이나 공유 캐시와 관련될 수 있습니다. 격리 실험에서 SELECT 결과를 일부만 읽고 커서를 열어 둔 채 DROP TABLE을 실행하자 database table is locked, 코드 6이 나왔습니다. 필요한 작업을 마친 실제 커서를 끝내거나 닫으세요. 오류 문구만으로 무관한 프로세스를 종료하지 마세요.

트랜잭션 경계를 누가 관리하는지 확인하기

Python 3.12에 autocommit이 도입됐습니다. autocommit=True이면 SQL BEGIN으로 트랜잭션을 열었더라도 Connection.commit()과 rollback()은 아무 효과가 없습니다. 해당 트랜잭션은 SQL COMMIT 또는 ROLLBACK으로 끝내고 in_transaction을 확인하세요. autocommit=False에서는 커밋이나 롤백 직후 새 DEFERRED 트랜잭션이 열립니다. isolation_level은 LEGACY_TRANSACTION_CONTROL에서만 작용합니다.

진행 중인 트랜잭션의 복구 지름길로 모드를 바꾸지 마세요. autocommit을 True로 바꾸면 보류 중인 작업이 커밋됩니다. 프레임워크가 경계를 관리할 수도 있습니다. 현재 Python 연결 컨텍스트 관리자는 커밋 실패 시 롤백하지만 autocommit=True에서는 아무것도 하지 않고 연결도 닫지 않습니다. 해결된 2016~2021년 CPython 버그는 역사적 자료이며 현재도 같은 결함이 남았다는 증거가 아닙니다.

대기를 늘리기 전에 재시도 범위를 정하기

작업 시작 전 BEGIN IMMEDIATE가 BUSY로 실패했다면 제한된 예산 안에서 쓰기 권한 획득을 재시도할 수 있습니다. 트랜잭션을 짧게 유지하고 유지 중에는 네트워크 호출을 피하세요. BEGIN IMMEDIATE 성공이 모든 저널 모드의 모든 후속 작업에서 BUSY가 없다는 보편적 보장은 아닙니다.

다른 연결이 커밋한 뒤 WAL 읽기 트랜잭션에서 517이 나오면 그 트랜잭션을 끝내고 새로 시작해 다시 읽고 판단도 다시 계산하세요. 기다리거나 UPDATE만 반복해도 옛 스냅샷은 갱신되지 않습니다. 롤백 저널 모드에서 다른 읽기 연결이 COMMIT을 막아 BUSY가 나오면 트랜잭션은 계속 활성 상태일 수 있습니다. 문서의 해당 조건이 해소되면 COMMIT을 다시 시도하고 이미 적용한 쓰기를 재실행하지 마세요.

임시 파일과 두 연결로 확인한 결과

Python 3.12.14, 실제 SQLite 3.53.1, timeout=0.05초, 임시 데이터베이스 파일, 명시적 SQL 경계로 네 가지 작은 실험을 했습니다. 사용자 데이터베이스는 쓰지 않았습니다. WAL에서 A가 카운터 10을 읽고 B가 11로 바꾼 뒤 A의 쓰기는 517로 실패했습니다. autocommit=True에서 A.rollback() 뒤에도 in_transaction=True였습니다. SQL ROLLBACK으로 해제하고 새 BEGIN IMMEDIATE에서 11을 다시 읽어 한 번 증가시키자 12가 커밋됐습니다.

첫 연결이 쓰기 권한을 보유하는 동안 두 번째 BEGIN IMMEDIATE는 코드 5로 실패했고 실패한 연결에는 활성 트랜잭션이 없었습니다. 별도의 DELETE 저널 실험에서는 열린 읽기 트랜잭션이 다른 연결의 COMMIT을 막았습니다. 코드 5가 나와도 쓰기 트랜잭션은 활성 상태였습니다. 읽기를 끝내고 COMMIT만 반복하자 초기 10은 11이 됐습니다. 재현 가능한 인공 사례이며 알려지지 않은 앱의 진단이나 운영 환경 안전성의 증거는 아닙니다.

상한과 관찰 가능한 완료 검증으로 마무리하기

연결 시간 제한은 대기 예산이며 오래된 스냅샷이나 교착 상태의 치료법이 아닙니다. 대기로 잠금 순환을 풀 수 없으면 SQLite가 busy handler를 호출하지 않을 수 있습니다. 횟수, 경과 시간, 코드, 단계, 트랜잭션 상태를 기록하고 재시도 상한을 정해 소진 시 실패를 알리세요. 모든 OperationalError를 재시도 가능한 경쟁으로 취급하지 마세요.

해결로 인정하기 전에 별도 연결에서 의도한 영속 값을 확인하고 해당 트랜잭션 종료와 관련 경쟁의 재현 결과를 검증하세요. autocommit=False에서는 새 빈 트랜잭션 때문에 in_transaction이 계속 True일 수 있으므로 모드와 함께 해석해야 합니다. 잠금을 풀려고 WAL이나 공유 메모리 파일을 삭제하지 마세요. 손상, I/O 오류, 불명확한 트랜잭션 관리 주체가 있으면 이 재시도 절차를 중단하고 별도 원인을 조사하세요.

Python autocommit 모드와 명시적 SQL 종료 도해
독창적인 개념 일러스트: True 모드에서 Python commit과 rollback은 효과가 없습니다. 수동 트랜잭션은 명시적 SQL로 끝냅니다. Watch at Ease