본문 바로가기
C.W.K.
Stream
Lesson 01 of 04 · published

결과는 빚이야

~14 min · obligations, durability, timers, sqlite, coordination

Level 0메모 쪽지
0 XP0/36 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete

진짜로 자주 나던 사고

Coop 아키텍처 문서는 문제를 두 문장으로 시작해. 외워둘 만해. 워커는 가끔 기록상으로 일을 가져가지도 않고 그냥 시작해. 그리고 더 자주, 긴 작업을 끝내놓고 닫으라는 알림을 잊어버려. 비싼 건 두 번째야. 코드는 나갔고, 푸시도 됐고, 세션은 신나서 한 문단으로 보고까지 했어. 그런데 원장엔 사흘 뒤에도 그 todo가 taken으로 남아 있어. 아빠가 보고 손으로 고칠 때까지.

알림을 더 잘 쓴다고 해결되진 않아. 알림은 컨텍스트 창 속 문장 하나고, 세션이 잊어버린 이유가 바로 그걸 밀어낸 긴 작업이거든. 그래서 아키텍처는 엔진한테 모델 기억과 상관없이 두 가지 사실을 붙들라고 해. 어느 세션이 정확히 어떤 항목을 받았는지. 그리고 받은 항목 중 아직 결과가 안 난 게 뭔지. 앞의 건 Track 3에서 본 바인딩이야. 뒤의 게 이 트랙 전체의 주제, 의무(obligation)야.

빚은 take랑 같이 적혀

워커가 초대를 받아들이면 Engine.accept가 트랜잭션 하나 안에서 여러 일을 해. 도메인에서 항목을 take하고, 바인딩을 넣고, engine_obligations에 행을 하나 넣어. 종류는 outcome이고, due는 지금에 OUTCOME_REVIEW_SECONDS를 더한 시각, 그러니까 두 시간 뒤야. 일은 가져갔는데 갚을 건 없는 순간이 아예 없어. 저장소가 트랜잭션을 거부하면 둘 다 없고, 커밋되면 둘 다 있어.

명령줄 쪽도 똑같이 다뤄. 대신 백스톱이 더 길어. Coop이 알아볼 수 있는 세션 안에서 친 todo take는 바인딩이 되고, 결과 의무가 LEGACY_REVIEW_SECONDS인 세 시간 뒤로 잡혀. 세션 참조가 아예 없는 take도 take야. 엔진은 그걸 거부하지 않아. 거부했더니 세션들이 추적 안 되는 지름길로 몰렸거든. 대신 legacy_binding 의무를 적어. 역시 세 시간 뒤로 잡히고, 사유는 Taken without a session reference; the chief reviews its outcome야.

due는 done이 아냐

due는 이 표에서 제일 많이 오해받는 칸이야. 정확히 해두자. 의무가 리뷰 대상이 되는 시각이야. 그 시각이 지나도 일엔 아무 일도 안 일어나. 아키텍처 문장 그대로, 보고 없이 끝난 평범한 턴은 의무를 리뷰 대상으로 만들 뿐 닫지 않아. idle은 절대 끝났다는 뜻이 아냐.

결과 의무를 갚는 건 처분(disposition)뿐이야. 워커가 직접 done, released, abandoned를 보고하면 _record_outcome을 타. 보고를 적고, 바인딩을 resolved로 만들고, 의무를 resolved로 바꾸면서 보고 id를 처분으로 남겨. 전부 트랜잭션 하나야. CLI에서 닫는 호환 경로도 같은 길을 타고, 치프의 close, release, abandon도 마찬가지야. 배울 게 많은 예외는 blocked 보고야. 엔진은 보고를 저장하고, 의무의 due를 지금으로 당기고, parked를 풀어. 일은 계속 묶여 있어. 막혔다는 건 누가 들여다봐야 한다는 증거지, 끝이 아니거든.

평생 가는 의무, 한 번의 리뷰

이 장치를 정직하게 지키는 구분이 이거야. 의무는 바인딩이 살아 있는 동안 계속 살아. 리뷰는 그걸 한 번 들여다보는 거고. 아키텍처는 일부러 둘을 떼어놨어. 저장되고 버전 확인을 거친 "계속 기다려" 결정은 이번 리뷰를 끝낼 수 있어. 대신 결과 의무는 남겨야 하고, 다음 due를 원자적으로 걸어야 해. 깨움 수신 확인도, 마지막 한 문장도, 놀고 있는 치프도 둘 중 어느 쪽으로도 쳐주지 않아.

리뷰 한 번의 결정의무다음 due
deferpending 유지지금 + 요청한 지연 (30초~30일, 기본 300초)
escalatepending 유지, 아빠한테 알림지금 + 요청한 지연
parkpending 유지, parked = 1, 아빠한테 알림지금 + 30일 (PARK_SECONDS)
close, release, abandonresolved없음

park된 의무는 아빠나 워커만 기다려. 워커 세션을 관찰해도 더는 앞으로 안 당겨져. 워커가 새로 blocked 보고를 올리면 park가 풀리고, 아빠가 직접 처분하면 resolved가 돼. 둘 다 안 오면 30일이 끝나고 다시 갚을 차례가 돌아와. 이 표 안의 어떤 것도 빚을 저절로 잊지 않으니까.

한 경우만 규칙이 더 빡빡해. 아빠가 2026-10-08에 정한 규칙은 "A blocked report is never parked for good"야. 엔진엔 BLOCKED_SWEEP_SECONDS, 여섯 시간으로 들어가 있고. 워커의 마지막 보고가 blocked면 defer나 escalate는 여섯 시간으로 잘려. park도 30일이 아니라 여섯 시간 뒤에 돌아와. 치프가 언급 없이 넘긴 park된 blocked 항목도 여섯 시간 안에 다시 끌려오고. 이유는 현실적이야. 막힌 건 풀리거든. 푸시가 나중에 들어가고, 아빠가 빌드를 깔고, 제안에 답이 와. 그럼 아빠가 따로 묻지 않아도 일이 닫혀야 해.

불변식 5번

이 레슨이 결국 도달하려는 문장이 아키텍처의 다섯 번째 불변식이야. 해결 안 된 결정은 워커, 치프, 엔진이 컨텍스트를 잃어도 살아남아. 워커가 압축되면서 할 일을 잊어도 의무는 행 하나로 남아 있고, 어댑터가 저장소에서 봉투를 복원해. 치프 턴이 죽으면 그 실행은 uncertain으로 남고 의무는 그대로 pending이야. 실패한 실행은 의무를 버리지 않고 due를 15분 뒤로 밀어. 엔진이 재시작해도 저장된 due가 타이머를 몰아. 아키텍처는 반대 설계를 한 줄로 금지해. "an in-memory timer is never the obligation."

빚은 약속이랑 같은 트랜잭션에 적어. 일을 받는 쓰기와 결과를 빚지는 쓰기가 따로면, 그 사이에서 죽는 순간 일은 가져갔는데 빚은 없는 상태가 남아. 쓰기가 한 번이면 빚에서 벗어나는 길은 하나뿐이야. 실제로 처분하는 거.

Code

최소 스케치 (Coop 엔진 아님): 처분으로만 갚아지는 의무, 백스톱, park, 스윕·python
import sqlite3

OUTCOME_REVIEW = 2 * 3600   # backstop after a managed accept
PARK = 30 * 86400           # a parked row waits for Dad or the worker

db = sqlite3.connect(':memory:')
db.execute("""CREATE TABLE obligations (
  item TEXT PRIMARY KEY,
  status TEXT NOT NULL DEFAULT 'pending',  -- pending | resolved
  due REAL NOT NULL,                       -- next eligible review
  parked INTEGER NOT NULL DEFAULT 0,
  reviewed REAL,                           -- last time a review looked
  disposition TEXT)""")

def accept(item, now):
    # In the real engine this shares a transaction with the take.
    db.execute('INSERT INTO obligations(item, due) VALUES (?, ?)', (item, now + OUTCOME_REVIEW))

def evidence(item, now):
    # Fresh worker evidence (a note): review now. Parked rows ignore it.
    db.execute("UPDATE obligations SET due = MIN(due, ?) "
               "WHERE item = ? AND status = 'pending' AND parked = 0", (now, item))

def sweep(now, limit=50):
    # What a review wakes for: pending and due. Nothing else.
    return [r[0] for r in db.execute(
        "SELECT item FROM obligations WHERE status = 'pending' AND due <= ? "
        "ORDER BY due LIMIT ?", (now, limit))]

def defer(item, now, after):
    # Resolves this review, keeps the obligation, arms the next one.
    db.execute("UPDATE obligations SET reviewed = ?, due = ?, parked = 0 WHERE item = ?",
               (now, now + after, item))

def park(item, now):
    db.execute("UPDATE obligations SET reviewed = ?, due = ?, parked = 1 WHERE item = ?",
               (now, now + PARK, item))

def report(item, outcome):
    # Only a disposition discharges the obligation.
    db.execute("UPDATE obligations SET status = 'resolved', disposition = ? "
               "WHERE item = ? AND status = 'pending'", (outcome, item))

H, D = 3600, 86400
accept('todo 1', 0)
accept('todo 2', 0)
print(sweep(10 * 60))        # []          accepted, nothing due yet
evidence('todo 1', 10 * 60)
print(sweep(10 * 60))        # ['todo 1']  a note arrived
defer('todo 1', 10 * 60, 30 * 60)
park('todo 2', 10 * 60)
print(sweep(1 * H))          # ['todo 1']  its deferral came due
report('todo 1', 'done')
evidence('todo 2', 2 * H)    # ignored: parked rows wait
print(sweep(3 * D))          # []
print(sweep(31 * D))         # ['todo 2']  the park ran out; still owed
Coop 엔진 발췌: 백스톱 상수 (주석은 소스 그대로)·python
# An outcome is reviewed when evidence arrives (a worker final, a blocked
# report, a lost session); these timers are only the backstop behind that.
OUTCOME_REVIEW_SECONDS = 2 * 3600
LEGACY_REVIEW_SECONDS = 3 * 3600
PARK_SECONDS = 30 * 86400
# A blocked report is never parked for good: the chief sweeps it again within
# this window, so a blocker that cleared (a push that later went through, a
# build Dad installed) or a proposal Dad already answered is picked up without
# Dad asking (Dad, 2026-10-08, todo #348).
BLOCKED_SWEEP_SECONDS = 6 * 3600

External links

Exercise

네 일에서 뭔가가 "가져가지는" 자리를 하나 골라봐. 너한테 배정된 티켓, 워커가 잡은 잡, 해주겠다고 한 리뷰. 그게 끝날 수 있는 방법을 전부 적어. 그다음 항목, 상태, due 시각, 처분이 들어간 표 하나랑, 갚을 차례가 된 걸 뽑는 쿼리 하나를 써봐. 위 스케치를 돌려보고, defer하는 리뷰가 다음 due를 안 거는 걸로 고쳐봐. 그 뒤로 스윕이 뭘 돌려줘? 안전한 실패야, 조용한 실패야?
Hint
defer가 빚을 다시 잡는 대신 지워버리면 그 항목은 모든 스윕에서 사라져. 아무도 못 듣고, 어느 화면에도 이상이 없어. defer가 due를 안 옮기면 스윕마다 다시 튀어나와. 시끄럽지만 보여. 원하는 실패는 시끄럽고 보이는 쪽이야.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인 — 댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.