LabHub
배우기 러닝패스 코스

Idempotency — Two Clicks, One Charge

Stop an old worker from overwriting a new result

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

목표

SQLite에서 소유권 임대와 증가하는 fencing token을 구현합니다.

왜 중요한가

워커 A가 일을 가져간 뒤 멈췄다. 임대가 만료되자 B가 같은 일을 가져가 완료했지만 뒤늦게 살아난 A도 완료 결과를 썼다. 임대 시간을 정하는 것만으로 오래된 작업자의 쓰기를 막을 수 없다. 저장 시점에도 소유자와 세대 번호를 확인해야 한다.

단계

  1. /root/work/idem-fencing-lab/service.py에서 init_db(path)는 jobs(id TEXT PRIMARY KEY, owner TEXT, until REAL NOT NULL DEFAULT 0, fence INTEGER NOT NULL DEFAULT 0, result TEXT, done INTEGER NOT NULL DEFAULT 0)를 멱등 생성합니다.

처음 한 번 준비하세요. 기존 파일은 덮어쓰지 않습니다.

mkdir -p /root/work/idem-fencing-lab
test -e /root/work/idem-fencing-lab/service.py || cp /opt/fixtures/ten_labs/idem-fencing-lab/service.py /root/work/idem-fencing-lab/service.py
cd /root/work/idem-fencing-lab
  1. /root/work/idem-fencing-lab/service.py에서 enqueue(path, job_id)는 없는 id를 기본 상태로 넣고 True, 이미 있으면 상태를 바꾸지 않고 False입니다.

  2. /root/work/idem-fencing-lab/service.py에서 state(path, job_id)는 행을 id, owner, until, fence, result, done 키의 dict로 반환하고 없으면 None입니다.

  3. /root/work/idem-fencing-lab/service.py에서 claim(path, job_id, owner, now, ttl)는 ttl<=0이면 ValueError입니다. 없는 작업·완료된 작업·until>now인 작업은 None입니다. 나머지는 owner와 until=now+ttl을 저장하고 fence를 1 올려 새 fence를 반환합니다.

  4. /root/work/idem-fencing-lab/service.py에서 renew(path, job_id, owner, fence, now, ttl)는 ttl<=0이면 ValueError입니다. owner·fence가 일치하고 done=0, until>now일 때만 until=now+ttl로 바꾸어 True, 아니면 False입니다.

  5. /root/work/idem-fencing-lab/service.py에서 complete(path, job_id, owner, fence, now, result)는 현재 임대(owner·fence 일치, done=0, until>now)일 때만 result와 done=1을 저장해 True입니다. 나머지는 False이며 기존 결과를 보존합니다.

  6. /root/work/idem-fencing-lab/service.py에서 release(path, job_id, owner, fence)는 owner·fence가 같고 done=0인 행의 owner=NULL, until=0으로 바꾸고 True입니다. fence는 보존합니다. 나머지는 False입니다.

  7. /root/work/idem-fencing-lab/service.py에서 claim_many(path, job_id, owners, now, ttl)는 ThreadPoolExecutor로 각 owner의 claim을 동시에 호출해 입력 순서의 반환값 리스트를 돌려줍니다. 새 작업은 딱 하나만 fence를 얻고 나머지는 None이어야 합니다.

참고

임대 상태 테이블을 만든다

/root/work/idem-fencing-lab/service.py에서 init_db(path)는 jobs(id TEXT PRIMARY KEY, owner TEXT, until REAL NOT NULL DEFAULT 0, fence INTEGER NOT NULL DEFAULT 0, result TEXT, done INTEGER NOT NULL DEFAULT 0)를 멱등 생성합니다.

처음 한 번 준비하세요. 기존 파일은 덮어쓰지 않습니다.

mkdir -p /root/work/idem-fencing-lab
test -e /root/work/idem-fencing-lab/service.py || cp /opt/fixtures/ten_labs/idem-fencing-lab/service.py /root/work/idem-fencing-lab/service.py
cd /root/work/idem-fencing-lab

워커 재시작 시 세대 번호를 초기화하면 오래된 토큰이 다시 유효해집니다.

저장 후 bash /opt/lab/checks/idem-fencing-lab/01-contract.sh로 확인하세요.

최초 작업만 등록한다

/root/work/idem-fencing-lab/service.py에서 enqueue(path, job_id)는 없는 id를 기본 상태로 넣고 True, 이미 있으면 상태를 바꾸지 않고 False입니다.

재등록이 진행 중인 임대를 초기화하지 않게 INSERT OR IGNORE를 사용합니다.

저장 후 bash /opt/lab/checks/idem-fencing-lab/02-contract.sh로 확인하세요.

현재 상태를 읽는다

/root/work/idem-fencing-lab/service.py에서 state(path, job_id)는 행을 id, owner, until, fence, result, done 키의 dict로 반환하고 없으면 None입니다.

별도 DB 연결에서 저장 상태를 읽어 프로세스 안의 캐시와 구별합니다.

저장 후 bash /opt/lab/checks/idem-fencing-lab/03-contract.sh로 확인하세요.

만료된 임대를 원자적으로 가져간다

/root/work/idem-fencing-lab/service.py에서 claim(path, job_id, owner, now, ttl)는 ttl<=0이면 ValueError입니다. 없는 작업·완료된 작업·until>now인 작업은 None입니다. 나머지는 owner와 until=now+ttl을 저장하고 fence를 1 올려 새 fence를 반환합니다.

읽기와 갱신은 하나의 BEGIN IMMEDIATE 안에서 수행합니다. 경계 now==until은 재할당 가능합니다.

저장 후 bash /opt/lab/checks/idem-fencing-lab/04-contract.sh로 확인하세요.

현재 세대만 임대를 갱신한다

/root/work/idem-fencing-lab/service.py에서 renew(path, job_id, owner, fence, now, ttl)는 ttl<=0이면 ValueError입니다. owner·fence가 일치하고 done=0, until>now일 때만 until=now+ttl로 바꾸어 True, 아니면 False입니다.

이미 만료된 소유권을 renew로 되살리면 새 워커와 충돌합니다.

저장 후 bash /opt/lab/checks/idem-fencing-lab/05-contract.sh로 확인하세요.

오래된 완료를 거절한다

/root/work/idem-fencing-lab/service.py에서 complete(path, job_id, owner, fence, now, result)는 현재 임대(owner·fence 일치, done=0, until>now)일 때만 result와 done=1을 저장해 True입니다. 나머지는 False이며 기존 결과를 보존합니다.

완료 쓰기에도 임대 검사가 있어야 늦게 돌아온 워커를 막습니다.

저장 후 bash /opt/lab/checks/idem-fencing-lab/06-contract.sh로 확인하세요.

현재 워커만 임대를 반납한다

/root/work/idem-fencing-lab/service.py에서 release(path, job_id, owner, fence)는 owner·fence가 같고 done=0인 행의 owner=NULL, until=0으로 바꾸고 True입니다. fence는 보존합니다. 나머지는 False입니다.

반납 때 fence까지 초기화하면 과거 토큰 번호를 재사용하게 됩니다.

저장 후 bash /opt/lab/checks/idem-fencing-lab/07-contract.sh로 확인하세요.

동시 claim에 승자는 하나다

/root/work/idem-fencing-lab/service.py에서 claim_many(path, job_id, owners, now, ttl)는 ThreadPoolExecutor로 각 owner의 claim을 동시에 호출해 입력 순서의 반환값 리스트를 돌려줍니다. 새 작업은 딱 하나만 fence를 얻고 나머지는 None이어야 합니다.

사전 조회 후 연결을 닫고 갱신하지 마세요. 잠금이 두 연산을 함께 보호해야 합니다.

저장 후 bash /opt/lab/checks/idem-fencing-lab/08-contract.sh로 확인하세요.