LabHub
배우기 러닝패스 코스

되돌릴 수 없는 변경 · 되돌리기도 새 변경이다 · 실습

다시 열린 축제와 위험한 되돌리기

LabHub 에서 이어서 보기

목표

취소했던 축제가 다시 열립니다. 취소 직후 상태가 유지된 주문만 복구하고, 기다림을 제한하며, 응답을 잃어도 보상 영수증으로 재개합니다.

왜 중요한가

백업을 통째로 덮으면 취소 이후 다른 담당자의 변경을 지울 수 있습니다. 보상도 새 승인과 기록을 갖는 변경입니다. 앞의 승인 버전 실습과 Python 예외·SQL 트랜잭션을 먼저 학습하세요. 예상 110분입니다. 만료 전에 +시간으로 연장하세요. 세션이 끝나면 파일이 사라집니다. 필요한 코드는 별도로 보관하세요.

환경과 공통 계약

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

검사기는 로컬 labdb의 별도 임시 스키마에서 아래 테이블과 가상 원 변경을 준비하고 자신이 만든 스키마만 정리합니다. 제출 함수는 전달받은 연결의 search_path를 사용합니다. 스키마·주문 ID·고객·DSN을 하드코딩하거나 public의 원 데이터를 변경하지 마세요. 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 changes(change_id text PRIMARY KEY,tenant text NOT NULL,targets jsonb NOT NULL);CREATE TABLE audit(change_id text REFERENCES changes(change_id),id integer NOT NULL, previous_revision integer NOT NULL,new_revision integer NOT NULL,qty integer NOT NULL, PRIMARY KEY(change_id,id));CREATE TABLE undo_records(undo_id text PRIMARY KEY, original_id text NOT NULL UNIQUE REFERENCES changes(change_id),reason text NOT NULL, tenant text NOT NULL,targets jsonb NOT NULL);CREATE TABLE undo_audit(undo_id text REFERENCES undo_records(undo_id),id integer NOT NULL, previous_revision integer NOT NULL,new_revision integer NOT NULL,qty integer NOT NULL, PRIMARY KEY(undo_id,id));

식별자 undo_id·original_id·tenant는 ASCII 영문·숫자·밑줄·하이픈 1–64자의 정확한 str입니다. reason은 정확한 str 1–200자이며 앞뒤 공백, 코드포인트 0–31·127을 허용하지 않습니다. 요청을 자동 수정하지 않습니다. 대상은 id·revision·qty만 가진 정확한 dict이며 id는 int 1–2147483647, 기대 revision은 int 0–2147483646, qty는 int 1–1000입니다. bool은 모두 거부합니다. targets는 중복 없는 ID의 정확한 list 1–16개이며 ID순의 새 list·dict들로 정규화합니다. plan은 original_id·tenant·targets만 가진 정확한 dict입니다. 직접 전달한 입력 오류는 쓰기 전 ValueError입니다.

lock_ms는 정확한 int 50–500, statement_ms는 정확한 int이며 lock_ms보다 크고 2000 이하입니다. 한도는 각 잠금 시도·각 SQL 기준이며 배치 전체 경과 시간 보장이 아닙니다. 보상 영수증 삽입도 기다릴 수 있으므로 compensate는 첫 DB 작업부터 자기 트랜잭션의 예산을 적용합니다.

빌린 con은 autocommit=True·Read Committed이며 외부 호출 시작 시 트랜잭션이 없습니다. 모든 함수는 빌린 연결을 닫지 않고 성공·실패 뒤 열린 트랜잭션과 설정 변경을 남기지 않습니다. restore_one·restore_batch의 내부 중첩 호출은 바깥 트랜잭션을 보존합니다. compensate를 다른 외부 트랜잭션으로 감싸지 마세요. undo_file만 새 연결을 소유합니다. 기록은 확정 뒤 불변이고, 정상 주문 쓰기는 revision을 증가시킨다는 계약입니다. 이 전제를 지키지 않는 운영 프로그램의 쓰기 권한까지 통제하지는 않습니다.

단계

1. 보상 요청을 분명하게 만든다 — Exception 하위 Conflict와 request(undo_id,original_id,reason)를 구현합니다. 아래 식별자·사유 계약을 검증하고 세 키를 가진 새 dict를 반환합니다. 형식 오류는 ValueError이며 이유나 ID를 조용히 고치지 않습니다.
2. 원 영수증과 감사를 대조한다 — load_plan(con,original_id)는 원 changes와 audit를 읽어 original_id·tenant·targets dict를 반환합니다. targets는 ID순이며 원 승인 revision에 1을 더한 취소 직후 기대값입니다. 원 기록 없음·감사 ID/전후 버전/수량 불일치·유효하지 않은 기록·보상 후 integer 상한 초과는 Conflict입니다. 원 승인 revision은 0–2147483645만 허용합니다. 원 기록이나 orders를 수정하지 않으며 반환값 수정도 원 기록에 영향을 주지 않습니다.
3. 취소 직후 상태일 때만 복구한다 — restore_one(con,tenant,target)는 입력을 검증하고 id·tenant·기대 revision·qty·cancelled 상태를 UPDATE 조건에 넣습니다. 일치하면 pending으로 바꾸고 revision을 1 증가시켜 id·revision·qty·state dict를 반환합니다. 없거나 달라졌으면 Conflict입니다. 자체 트랜잭션으로 처리하되 내부 호출에서는 바깥 트랜잭션을 조기 커밋하지 않습니다. 감사나 영수증은 쓰지 않습니다.
4. 기다림을 제한하고 배치 전체를 보호한다 — restore_batch(con,plan,lock_ms=100,statement_ms=800)는 아래 계획·예산 계약을 쓰기 전에 검증합니다. 바깥 트랜잭션 안에 한도를 적용하고 ID순으로 모두 복구해 restore_one 결과 목록을 반환합니다. 어느 오류든 앞 행까지 전체 롤백하고 원 오류를 전달합니다. 성공·실패 뒤 빌린 연결의 lock_timeout·statement_timeout을 원 값으로 보존합니다. 원 기록·대조군은 바꾸지 않습니다.
5. 보상 기록과 업무 변경을 함께 확정한다 — compensate(con,undo_id,original_id,reason,lock_ms=100,statement_ms=800,fault=None)는 요청·예산 검증 뒤 하나의 트랜잭션에서 한도를 적용하고 원 보상 계획을 읽습니다. undo_records에 보상 ID·원 ID·사유·고객·기대 targets를 넣고 배치 복구 후 fault(after-restore), 모든 대상의 취소 직후 revision·새 revision·qty를 undo_audit에 넣은 뒤 fault(after-audit), 실제 COMMIT 뒤 fault(after-commit)를 호출하고 True입니다. 같은 ID·원 ID·사유·계획이면 무변경 False이며 훅도 호출하지 않습니다. 같은 보상 ID의 다른 내용 또는 다른 보상 ID로 같은 원 변경을 다시 보상하면 Conflict입니다. 훅은 있을 때만 호출합니다. 커밋 전 오류는 전부 롤백, 이후 오류는 확정 보존·원 오류 전달입니다. 원 changes·audit는 수정하지 않습니다.
6. 보상 당시와 현재를 나눠 읽는다 — inspect_undo(con,undo_id)는 없으면 None, 있으면 undo_id·original_id·reason·tenant·targets dict를 반환합니다. targets는 보상 당시 기대값의 사본입니다. reconcile_undo(con,undo_id)는 영수증이 없으면 Conflict, 있으면 그 ID들의 현재 orders를 한 SELECT로 조회해 matching·drifted·missing의 ID순 list를 반환합니다. 고객·qty·pending·revision=영수증 기대+1 모두 맞으면 matching, ID가 없으면 missing, 나머지는 drifted입니다. 두 함수 모두 읽기만 합니다.
7. 응답을 잃은 보상을 재개한다 — undo_file(dsn,undo_id,original_id,reason,lock_ms=100,statement_ms=800,fault=None)는 psycopg.connect(dsn,autocommit=True,connect_timeout=2)로 연결을 소유하고 compensate를 호출해 같은 결과를 반환합니다. 성공·실패 모두 연결을 닫고 오류를 숨기지 않습니다. 실제 클라이언트 종료와 동일·서로 다른 보상 ID의 두 프로세스 경쟁을 검사합니다.
8. 잠금 오류만 정해진 횟수 재시도한다 — retry_undo(action,attempts,pause)는 정확한 int attempts 1–3만 허용하며 오류는 첫 호출 전 ValueError입니다. action()을 즉시 호출하고 True·False를 포함한 정상 결과는 그대로 반환합니다. psycopg.errors.LockNotAvailable만 재시도하며 마지막 실패는 같은 오류를 전달합니다. 남은 시도가 있을 때만 pause(방금 실패한 시도 번호)를 호출합니다. 최초 포함 총 attempts회, pause 최대 attempts-1회입니다. 문장 취소·Conflict·입력/연결 오류와 pause 오류는 추가 호출 없이 전달합니다. 실제 검사 action은 같은 ID로 undo_file을 부릅니다.

참고

단계 8개

  1. 보상 요청을 분명하게 만든다
  2. 원 영수증과 감사를 대조한다
  3. 취소 직후 상태일 때만 복구한다
  4. 기다림을 제한하고 배치 전체를 보호한다
  5. 보상 기록과 업무 변경을 함께 확정한다
  6. 보상 당시와 현재를 나눠 읽는다
  7. 응답을 잃은 보상을 재개한다
  8. 잠금 오류만 정해진 횟수 재시도한다