본문으로 건너뛰기

analysis_results는 잡 상태 테이블이 아니라 멱등성 캐시다

맥락

server의 워치독이 응답 없는 분석을 재요청한다. 재시도와 워치독은 heymoa-server 소유이며 ai는 요청을 받아 돌기만 한다. 그 재요청이 그대로 중복 분석이 되면 LLM 비용이 배로 나간다. 그렇다고 ai가 잡 상태를 따로 관리하면 server의 analysis_jobs와 같은 것을 두 곳에서 관리하게 된다. 재요청과 원래 요청이 겹쳐 둘 다 끝나는 경우도 있다.

결정

  • analysis_results에는 "이 analysis_id는 전에 끝냈고 결과가 이것이었다"는 사실만 있다. 상태 컬럼이 없다. 끝난 것만 들어오고 진행 중은 저장하지 않는다.
  • PK는 server가 발급한 analysis_id다. 우리는 id를 만들지 않는다. server의 잡 id가 그대로 멱등성 키가 된다.
  • 저장은 upsert다. server 재요청과 원래 요청이 겹쳐 둘 다 끝날 수 있어서다.
  • 같은 id가 다시 오면 분석을 돌리지 않고 보관분으로 callback만 재발송한다. 겹치는 실행은 PgSingleFlight(Postgres advisory lock)가 막는다.

대안

대안 1 — ai가 잡 상태(PENDING/RUNNING/…)를 갖는다. 자기가 무엇을 돌리는지 스스로 알 수 있고, 유실도 스스로 감지한다. 기각한 이유: server의 analysis_jobs와 이중 관리가 된다. 같은 사실을 두 서비스가 각자 들고 있으면 반드시 갈린다. 잡의 현재 단계는 server가 이미 알고 있으므로 ai가 답할 것은 결과 하나뿐이다.

대안 2 — 멱등 처리를 안 한다. 테이블이 단순해진다. 기각한 이유: 워치독 재시도가 곧 중복 과금이다.

결과

  • 워치독 재요청이 중복 과금으로 이어지지 않는다. 브로커 없이 DB row를 큐로 쓰는 at-least-once 재디스패치가 이 성질 위에서 안전하다.
  • "요청의 현재 단계는 server가, 결과는 ai가"라는 소유권 분할이 테이블 모양으로 굳었다.
  • ai는 자기가 지금 무엇을 돌리고 있는지 DB로 알 수 없다. 프로세스가 죽으면 돌던 분석은 그대로 사라진다. 되살리는 것은 전적으로 server의 워치독이고, ai 혼자서는 유실을 감지조차 못 한다.
  • 진행 중을 저장하지 않으므로 "얼마나 걸리고 있나"를 ai 쪽 데이터로 답할 수 없다. 관측은 trace와 로그에만 있다.
  • 실패는 캐시되지 않는다. 같은 입력으로 계속 실패하는 잡은 워치독이 재요청할 때마다 전액 다시 과금된다.
  • 멱등성은 "server가 같은 analysis_id를 재사용한다"는 약속에 걸려 있다. server가 재요청마다 새 id를 발급하면 이 테이블은 아무것도 막지 못한다.