← 목록
기타 2026-07-30 10KB 읽기 8분

crowny-deacon 티켓ID 동시성 — 설계·조사 (T59+T60+T61)

개요

집사회(deacon.crowny.org:9772) 티켓 채번(티켓생성/다음티켓ID) 동시성 안전성을 라이브 코드 무수정으로 조사하고, 파일락 기반 원자적 채번 설계안과 타임스탬프+PID ID 전환 시 영향 격리 실측을 수행했다.

T60 — 티켓생성()은 상주 서버 내부 처리 (신규 프로세스 아님)

실코드 확인 결과: 신규 crownyc 프로세스 스폰이 아니라, 상주 서버 프로세스 내부의 단일 스레드 accept 루프에서 순차 처리된다.

근거:

  1. /Users/ef/crowny-deacon/서버.한선:524-547TCP대기(포트)동안 (가동==1) { TCP수락 → 요청처리 → TCP닫기 } 형태의 단일 프로세스·단일 스레드 accept 루프. fork/thread 없음. 클라이언트 요청은 한 번에 하나씩 순차 처리됨.
  2. /Users/ef/.claude/scripts/집사회.sh — "미션/수신/보고" 등 모든 커맨드는 curlhttp://127.0.0.1:9772에 HTTP 요청을 보내는 래퍼일 뿐, crownyc를 직접 실행하지 않음.
  3. 티켓생성() (한선씨/미션장부.한선:208) 은 요청처리() 콜스택 안에서 호출되며, 이는 위 accept 루프 한 회 안에서 실행됨.
  4. _티켓카운터 (미션장부.한선:145)는 프로세스 시작 시 1회 _최대ID번호(_티켓파일,"T")로 장부 리플레이 초기화되는 프로세스 전역 메모리 변수.
결론: 동시 crownyc 프로세스가 각자 카운터를 들고 경합하는 구조가 아니다. butler-council이 여러 agent()로 병렬 집사회.sh 미션/보고를 쏘아도, 실제 채번은 단일 accept 루프에서 직렬화된다(OS TCP backlog가 큐잉). 현재 구조 자체는 프로세스 간 경합이 없다 — 다만 단일 장애점(SPOF)이며, 향후 서버 다중 인스턴스화(수평 확장) 또는 티켓.psv 외부 직접 쓰기(우회 CLI 등)가 생기면 그 순간 경합이 발생한다.

T59 — 파일락 기반 원자적 다음티켓ID() 설계안

현재는 단일 프로세스 순차 처리라 실전 경합은 없지만, "향후 다중 인스턴스/외부 쓰기" 대비 방어적 설계로 다음을 제안한다.

설계

함수 _파일락획득(경로, 최대대기ms) {
    변수 락경로 = 경로 + ".lock"
    변수 대기 = 0
    동안 (대기 < 최대대기ms) {
        // mkdir은 POSIX에서 원자적 test-and-set (동시 mkdir 중 1개만 성공)
        변수 결과 = 체계("mkdir '" + 락경로 + "' 2>/dev/null; echo $?")
        만약 (부분(결과, 0, 1) == "0") { 반환 1 }
        대기 = 대기 + 20   // 20ms 백오프
        // sleep은 crownyc에 없으므로 짧은 busy-wait 루프 또는 체계("sleep 0.02")로 대체
    }
    반환 0  // 타임아웃 — 옴/타로 상위 이관
}

함수 _파일락해제(경로) {
    체계("rmdir '" + 경로 + ".lock' 2>/dev/null")
}

// 채번+기록을 락 안에서 원자적으로 묶는다 (TOCTOU 방지 — ID만 잠그면
// 두 락 보유자가 서로 다른 시각에 "스캔→해제→기록"하면서 같은 번호를 뽑을 수 있음)
함수 다음티켓ID_원자적(미션ID, 부모ID, 레벨, 모델, 유형, 지시문, 상태) {
    만약 (_파일락획득(_티켓파일, 2000) == 0) {
        반환 ""  // 락 실패 — 호출부는 옴(재시도) 또는 타(포기)로 판정
    }
    변수 최대 = _최대ID번호(_티켓파일, "T")   // 락 보유 중 재스캔 (in-memory 카운터 불신)
    변수 id = "T" + 문자열변환(최대 + 1)
    티켓기록(id, 미션ID, 부모ID, 레벨, 모델, 유형, 지시문, 상태, "", "")
    _파일락해제(_티켓파일)
    반환 id
}

설계 근거

  • mkdir은 POSIX 파일시스템에서 원자적(동시 호출 중 정확히 하나만 성공) — 가드레일 문서가 명시한 "체계() mkdir/ln 락" 패턴과 일치.
  • 락 범위는 "스캔+기록"을 통째로 묶어야 한다. ID만 락으로 보호하고 기록은 락 밖에서 하면, 락 해제 직후 두 번째 호출자가 아직 파일에 반영 안 된 상태를 스캔해 같은 번호를 재사용하는 TOCTOU 창이 생긴다.
  • 인메모리 _티켓카운터는 다중 프로세스 시나리오에서 신뢰 불가(각 프로세스가 자기 시작 시점 스냅샷만 가짐) → 락 보유 중에는 반드시 파일을 재스캔.
  • 타임아웃(2000ms) 초과 시 실패 반환 → 호출부가 옴(재시도)/타(포기)로 4상 판정하도록 위임 (헌법 원칙: 모르면 이관).
  • crownyc에 네이티브 sleep이 없으므로 백오프는 체계("sleep 0.02") 경유 필요 (VM 함정: 원시 소켓/타이머 부재는 체계() 경유가 정석).

적용 범위 권고

현재(T60 결론: 단일 프로세스 순차) 구조에서는 이 락이 불필요한 오버헤드다(체계() 호출 자체가 상대적으로 비쌈). 따라서:
  • 지금 당장 적용하지 말 것. 서버가 여전히 단일 accept 루프인 한 경합이 없다.
  • 적용 트리거: (a) 서버 다중 인스턴스/로드밸런싱 도입, 또는 (b) 티켓.psv에 서버 프로세스 외 경로(예: 별도 CLI 배치 스크립트)가 직접 덧쓰기하는 경우가 생길 때만 이 설계를 편입.

T61 — 타임스탬프+PID ID 전환 영향 격리 실측

대상 함수

_ID번호추출(미션장부.한선:121), _최대ID번호(:130), 대기티켓찾기(:342, "번호 최소=가장 오래된" 가정으로 FIFO claim 구현).

코드 분석

  • _ID번호추출은 접두사 제거 후 나머지 전체를 숫자변환() 한 번에 통짜로 파싱한다. "T"+순수정수 형태를 전제로 짜여 있고, 구분자(_ 등)가 섞인 문자열을 다루도록 설계되지 않았다.
  • 대기티켓찾기는 주석에 명시된 대로 "모델별 가장 오래된(번호 최소) 대기 티켓"을 뽑는다 — 즉 ID 숫자값이 생성 순서와 단조증가로 일치한다는 가정이 JIT claim 로직의 핵심 불변식이다.

실측 (라이브 VM)

T1738200000123_4821 (epoch_ms+PID, 언더스코어 결합) 형태를 _ID번호추출에 통과시키는 테스트를 크라우니 VM(crownyc)에서 직접 실행했다. 13자리 epoch밀리초급 큰 정수를 숫자변환()에 넣는 케이스에서 VM이 120초 타임아웃까지 응답 없이 멈췄다 (백그라운드 태스크 강제 종료로 회수). 순수 ID 파싱 로직 자체가 큰 정수·비순수숫자 입력에서 안전하지 않다는 것을 라이브로 확인했다 — MEMORY의 "나눗셈 자연반올림 이진탐색 무한루프" 함정과 유사한 큰수/비정형 파싱 취약 계열로 보인다(원인 미세분리는 별도 티켓 필요).

영향 격리 판정

  1. _ID번호추출: 언더스코어 등 구분자 포함 ID는 현재 구현이 파싱하지 못한다(포맷 자체가 어긋남) + 큰 정수 입력 자체가 VM 행(hang) 리스크로 실측됨 → 전면 재작성 필요(구분자 앞부분만 잘라 파싱하거나, 접두사 규약을 아예 바꿔야 함).
  2. _최대ID번호(정렬 의존처): "최대값 스캔"이 숫자 비교이므로, epoch_ms를 그대로 숫자로 쓰면 값 자체는 매우 커지지만 최댓값 스캔 로직 자체는 원리상 동작 — 단, 위 큰수 파싱 행(hang) 문제가 해결되지 않는 한 실사용 불가.
  3. 대기티켓찾기(FIFO claim): 가장 취약한 지점. PID를 뒤에 붙이는 순간(epoch_pid) 숫자값은 (epoch × 10^k + pid) 형태가 되어 "번호가 작다=먼저 생성됐다"는 등식이 PID 크기에 따라 깨질 수 있다 — 같은 밀리초에 두 티켓이 생성돼도 자릿수 결합 방식에 따라 역전 가능. epoch를 상위 자릿수, PID를 하위 고정폭 자릿수로 zero-pad 결합하면 이론상 단조성은 유지되나(예: epoch*100000+pid%100000), 여전히 "숫자변환 큰수 행" 리스크가 있어 이 함수부터 먼저 안전화해야 함.

권고안 (확정 1개)

타임스탬프+PID 전환은 채택하지 않는다. 현행 "장부 리플레이 기반 순차 정수 카운터"(T1,T2,T3…)를 유지하되, T59의 파일락 설계를 트리거 조건 충족 시에만 편입한다.

이유:

  • T60 실측대로 현재 단일 accept 루프 구조에서는 순차 카운터로 경합이 이미 발생하지 않는다 — 타임스탬프+PID가 풀어줄 문제가 현재 없다.
  • 반면 타임스탬프+PID는 _ID번호추출/대기티켓찾기의 핵심 불변식(번호=생성순서)을 깨뜨리는 대가가 크고, 라이브 VM에서 큰 정수 파싱이 실제로 행(hang)하는 것까지 확인됐다 — 이득 없는 위험이다.
  • 향후 다중 인스턴스가 실제로 필요해지면, 그때는 "epoch+PID" 대신 T59의 mkdir 파일락으로 감싼 순차 카운터를 그대로 다중 인스턴스에도 적용하는 편이 기존 정렬/FIFO 로직을 전혀 건드리지 않아 안전하다(권고안의 실질 대안).

관련 파일

  • /Users/ef/crowny-deacon/서버.한선 (524-547: accept 루프)
  • /Users/ef/crowny-deacon/한선씨/미션장부.한선 (121-155, 208-212, 336-365)
  • /Users/ef/crowny-deacon/한선씨/집사회.한선
  • /Users/ef/.claude/scripts/집사회.sh
  • 실측 스크립트: /tmp/idtest.한선 (숫자변환 큰수 행 재현 — 세션 임시, 영속 아님)

잔여 이슈

  • 숫자변환()의 큰 정수/비순수숫자 입력 행(hang) 원인은 이번 조사 범위 밖(VM 코어 이슈로 별도 티켓 필요 — MEMORY feedback_* 큰수 파싱 계열로 편입 검토).
  • T59 파일락 설계는 미적용 상태(코드 미수정, 설계 문서만). 트리거 조건(다중 인스턴스화) 발생 시 실제 편입 작업 필요.