LabHub
배우기 러닝패스 코스

되돌릴 수 없는 변경 · 중단된 배치를 이어서 끝내기 · 실습

외계인 축제 취소 작업이 25%에서 끊겼다

LabHub 에서 이어서 보기

목표

외계인 디저트 축제의 주문 40건을 10건씩 취소합니다. 승인 목록은 고정하고, 중간에 프로세스가 사라져도 확정한 청크 다음부터 이어갑니다.

왜 중요한가

전체 롤백을 청크별 확정으로 바꾸면 업무 계약도 달라집니다. 부분 완료를 명시적으로 승인받고, 체크포인트가 실제 업무 변경·감사와 맞는지 검사해야 합니다. Python 함수·예외, SQL 트랜잭션, 앞의 승인 버전·보상 실습을 먼저 학습하세요. 예상 120분이므로 만료 전에 +시간으로 연장하세요. 세션이 끝나면 파일이 사라집니다. 필요한 코드는 별도로 보관하세요.

환경과 공통 계약

산출물은 /root/chunks/worker.py입니다. PostgreSQL 16·psycopg 3.2.3·Python 3이 이미지에 있으며 런타임 설치는 없습니다. postgres 사용자로 /root에 쓸 수 있고 추가 capability나 사용자 전환은 필요 없습니다.

채점기는 로컬 labdb의 별도 임시 스키마에서 아래 테이블과 가상 주문을 준비하고 자신이 만든 스키마만 정리합니다. 학생 함수는 전달받은 연결의 search_path와 DSN을 사용합니다. public 테이블을 변경하거나 스키마·고객·ID·DSN을 하드코딩하지 마세요. SQL 값은 매개변수로 전달합니다.

CREATE TABLE orders(id integer PRIMARY KEY,tenant text NOT NULL, qty integer NOT NULL CHECK(qty BETWEEN 1 AND 1000), state text NOT NULL CHECK(state IN ('pending','paid','cancelled')), revision integer NOT NULL CHECK(revision>=0));CREATE TABLE jobs(job_id text PRIMARY KEY,tenant text NOT NULL,targets jsonb NOT NULL, chunk_size integer NOT NULL CHECK(chunk_size BETWEEN 1 AND 10), next_index integer NOT NULL CHECK(next_index>=0));CREATE TABLE job_audit(job_id text REFERENCES jobs(job_id),ordinal integer NOT NULL, id integer NOT NULL,previous_revision integer NOT NULL,new_revision integer NOT NULL, qty integer NOT NULL,PRIMARY KEY(job_id,ordinal),UNIQUE(job_id,id));

식별자 job_id·tenant는 정확한 str이며 ASCII 영문·숫자·밑줄·하이픈 1–64자입니다. items/targets는 정확한 list 1–64개이고 각 항목은 id·revision·qty만 가진 정확한 dict입니다. id는 int 1–2147483647, revision은 int 0–2147483646, qty는 int 1–1000입니다. bool은 정수로 허용하지 않습니다. 중복 ID는 거부하고 ID순의 깊은 사본으로 정규화합니다. chunk_size는 정확한 int 1–10, allow_partial은 정확히 True여야 합니다. 잘못된 직접 입력은 쓰기 전 ValueError이며 자동 수정하지 않습니다.

등록 이후 jobs의 tenant·targets·chunk_size와 job_audit는 불변이라는 계약입니다. next_index는 0 이상 대상 수 이하이며, 완료 위치가 아니라면 chunk_size의 배수여야 합니다. 저장된 targets는 정규화 입력과 같아야 합니다. 감사는 ordinal순으로 원 승인 배열의 [0:next_index]와 ID·이전 revision·새 revision=이전+1·qty가 정확히 같아야 합니다. 남는 감사도 손상입니다. 원 주문의 후속 변경은 과거 감사 손상이 아니므로 앞 청크의 현재 값으로 과거 승인을 덮지 않습니다.

run_chunk가 처리할 구간은 [next_index:min(next_index+chunk_size,대상 수)]입니다. fault는 있을 때만 각 문자열 인자로 호출합니다. 훅 오류를 숨기지 않습니다. 잠금·문장 한도는 각 SQL 기준이며 청크 전체 경과 시간 제한이 아닙니다. 성공·실패 후 원 연결의 설정을 복원하세요.

빌린 con은 autocommit=True·Read Committed이며 외부 호출 시작 시 열린 트랜잭션이 없습니다. 함수는 연결을 닫지 않고 성공·실패 후 트랜잭션을 남기지 않습니다. cancel_chunk의 내부 중첩 호출은 바깥 트랜잭션을 보존해야 합니다. run_chunk는 실제 커밋 뒤 훅을 호출하므로 다른 외부 트랜잭션으로 감싸지 마세요. chunk_file만 연결을 소유합니다.

단계

1. 부분 완료 승인을 고정한다 — Exception 하위 Conflict와 manifest(job_id,tenant,items,chunk_size,allow_partial)를 구현합니다. 아래 입력 계약을 검증하고 job_id·tenant·targets·chunk_size·allow_partial의 새 dict를 반환합니다. targets는 ID순의 새 list와 새 dict들입니다. 명시적인 True 승인만 허용하고 잘못된 입력은 ValueError입니다.
2. 재등록해도 승인을 덮지 않는다 — register(con,plan)은 manifest의 다섯 키만 가진 정확한 dict를 검증·정규화한 뒤 jobs에 next_index=0으로 등록하고 True입니다. 같은 작업 ID의 같은 고객·정규화 targets·chunk_size는 아무것도 바꾸지 않고 False, 다른 내용은 Conflict입니다. 입력 오류는 쓰기 전 ValueError이며 주문·감사는 변경하지 않습니다.
3. 승인과 진행률을 따로 읽는다 — inspect_job(con,job_id)는 ID를 검증하고 없으면 None, 있으면 job_id·tenant·targets·chunk_size·next_index dict를 반환합니다. 읽기 전용이고 반환값 수정이 원 기록에 영향을 주지 않습니다.
4. 청크 안의 한 행 실패도 모두 롤백한다 — cancel_chunk(con,tenant,items)는 대상 입력을 먼저 검증하고 ID순으로 현재 고객·ID·revision·qty·pending을 조건부 UPDATE합니다. cancelled로 바꾸고 revision을 1 증가시켜 id·previous_revision·new_revision·qty dict 목록을 반환합니다. 한 행이라도 불일치하면 Conflict로 청크 전체 롤백합니다. jobs·job_audit는 쓰지 않으며 중첩 호출은 바깥 트랜잭션을 조기 확정하지 않습니다.
5. 변경·감사·위치를 함께 확정한다 — run_chunk(con,job_id,fault=None)는 자기 트랜잭션의 잠금 한도 500ms·문장 한도 2000ms를 먼저 설정하고 jobs 행을 잠가 읽습니다. 없거나 저장된 승인·next_index·감사가 아래 계약과 다르면 Conflict입니다. 다음 구간을 cancel_chunk로 변경한 뒤 fault(after-orders), 구간의 순번·ID·전후 버전·수량을 job_audit에 모두 저장한 뒤 fault(after-audit), next_index를 구간 끝으로 바꾼 뒤 fault(after-checkpoint), 실제 COMMIT 뒤 fault(after-commit)를 호출합니다. 반환값은 processed ID순 list·next_index·done bool입니다. 이미 완료면 processed=[]·done=True이고 훅은 호출하지 않습니다. 커밋 전 오류는 이번 청크만 롤백하고 앞 청크는 보존합니다. 커밋 후 오류는 확정한 상태를 보존하고 원 오류를 전달합니다.
6. 과거 완료와 현재 차이를 보고한다 — report(con,job_id)는 없으면 Conflict, 있으면 approved·committed·remaining·matching·drifted·missing의 ID순 list를 가진 dict를 반환합니다. approved는 원 승인, committed는 next_index 앞부분, remaining은 나머지입니다. committed의 현재 행이 고객·qty·cancelled·원 revision+1과 모두 같으면 matching, ID가 없으면 missing, 나머지는 drifted입니다. 진행률과 현재 행은 하나의 SELECT로 조회하며 데이터를 고치지 않습니다.
7. 클라이언트 종료 뒤 남은 구간을 잇는다 — chunk_file(dsn,job_id,fault=None)는 psycopg.connect(dsn,autocommit=True,connect_timeout=2)로 소유 연결을 열어 run_chunk를 호출하고 같은 결과를 반환합니다. 성공·실패 모두 연결을 닫으며 오류를 숨기지 않습니다. 네 훅 지점의 실제 클라이언트 종료 뒤 재개 결과를 검사합니다.
8. 두 작업자로 제한된 횟수만 진행한다 — drain(dsn,job_id,max_chunks)는 ID와 정확한 int 1–64의 max_chunks를 연결 생성 전에 검증합니다. chunk_file을 최대 max_chunks번 호출하되 done=True면 즉시 끝냅니다. 호출 횟수 calls·이번 호출이 실제 처리한 processed의 연결 목록·마지막 next_index·마지막 done을 반환합니다. 어느 오류든 즉시 전달하고 숨은 재시도는 하지 않습니다. 두 프로세스가 같은 작업을 끝까지 처리할 때 전체 승인 ID가 정확히 한 번씩만 변경돼야 합니다.

참고

단계 8개

  1. 부분 완료 승인을 고정한다
  2. 재등록해도 승인을 덮지 않는다
  3. 승인과 진행률을 따로 읽는다
  4. 청크 안의 한 행 실패도 모두 롤백한다
  5. 변경·감사·위치를 함께 확정한다
  6. 과거 완료와 현재 차이를 보고한다
  7. 클라이언트 종료 뒤 남은 구간을 잇는다
  8. 두 작업자로 제한된 횟수만 진행한다