공급자 호출이 끝나도 실행 슬롯이 반환되지 않을 수 있다

AI SDK가 응답을 돌려준 뒤에도 사용량 기록은 소유자·라이선스 조회와 DB 삽입을 기다린다. 이 기록까지 공급자 호출 슬롯(permit)을 점유한 함수 안에서 처리하면, 공급자 호출이 끝났는데도 다음 요청에 슬롯을 돌려주지 못한다. 공급자 데드라인이 같은 함수를 감싸고 있다면 기록 지연이 AI 요청의 시간 초과 판정에도 섞인다.

AI Commons의 사용량 기록 구현은 SDK 결과를 확정하는 위치와 기록 완료를 기다리는 위치를 나눴다. 제품 통합 전의 기능 구현이다. observe()는 SDK의 반환값이나 예외로 사용량 기록의 결과를 정하고 같은 객체를 즉시 전달한다. 바깥의 run()이 슬롯·데드라인을 관리하는 본문 종료 후 기록 Promise를 기다린다.

다음은 OpenAI 연결부와 트래커의 순서를 줄인 코드다. 실제 요청 인자와 오류 변환은 생략했다. Gemini도 같은 트래커로 관찰 위치와 대기 위치를 나눈다.

const response = await usage.run(() =>
  this.guarded(() => usage.observe(() => this.client.responses.create(input))),
);

// run(): guarded 본문이 끝난 다음
if (settled) await done;
return result;
  1. 실제 SDK 호출 직전에 기록 시작슬롯을 얻지 못해 전송 전에 실패하면 begin·observe를 호출하지 않아 공급자 기록도 만들지 않는다.
  2. SDK 종료 시 관찰 결과 확정성공·실패를 정하고 슬롯을 점유한 본문은 기록 저장을 기다리지 않고 끝난다.
  3. 슬롯 반환 후 정상 경로의 기록 대기호출자의 완료 시점에는 여전히 기록 지연이 더해질 수 있다. 동시성 슬롯을 돌려주는 시점과 호출자 응답 시점이 다르다.

데드라인이 먼저 끝나 SDK가 아직 진행 중이면 run()은 기록을 기다리지 않고 시간 초과를 전달한다. 이후 SDK가 성공하면 사용량 기록에는 SUCCEEDED가 남을 수 있다. 호출자가 받은 결과와 실제로 관찰한 공급자 결과를 같은 상태로 해석하면 이 늦은 응답을 설명할 수 없다.

대기 상한은 두 단계에 따로 걸린다

슬롯 반환 뒤로 옮겼다고 저장을 무조건 오래 기다릴 필요는 없다. 기록이 비활성화돼 있으면 귀속 조회부터 생략한다. 저장 회로가 열려 삽입을 건너뛸 상태라면 귀속도 조회하지 않는다. 이 확인은 회로 상태를 읽는 isCircuitOpen()으로 한다. 실제 요청 가능 여부를 판정하는 canRequest()를 미리 호출하면 복구 시험 요청의 허용 횟수를 소비할 수 있기 때문이다.

서로 다른 두 기록 대기
단계상한을 넘었을 때계속 남는 일
소유자·라이선스 귀속귀속을 null로 두고 기록 진행원래 조회 Promise는 남아 후속 호출이 재사용할 수 있다.
사용량 INSERT기다림을 끝내고 실패 상태를 회로에 반영이미 시작한 INSERT는 취소하지 않는다.

두 단계는 각각 recordTimeoutMs를 쓰며 순서대로 수행된다. 하나의 총 기록 데드라인이 아니다. 특히 Promise.race에서 타임아웃이 이겨도 DB 쿼리 실행이나 연결 점유가 끝났다는 뜻은 아니다. 대기 시간을 제한하는 정책과 DB 부하를 제한하는 정책은 별도로 필요하다.

정상 경로에서 기다리는 대신 기록을 완전히 분리하면 호출 지연을 더 줄일 수 있지만, 프로세스 종료 때 아직 저장하지 못한 기록을 잃을 수 있다. 이 구현에도 영속적인 전송 대기열이나 자동 재처리 계약은 없다. 기록 실패를 로그로 남기는 것만으로 사용량의 완전한 복원이 보장되지는 않는다.

대상이 삭제됐다는 오류보다 진단 DB 오류가 먼저 보이면 안 된다

LMS 내보내기에는 같은 경계의 다른 형태가 있다. 대상 삭제를 감지해 JobTargetGoneError를 받은 뒤, 내보내기 행을 FAILED로 기록하는 DB 갱신이 실패하면 그 두 번째 예외가 원래 예외를 가릴 수 있다. 상위 처리기는 대상 삭제와 일반 DB 장애에 서로 다른 종료·재시도 정책을 적용하므로 오류 종류를 유지해야 한다.

슬라이드·영상 내보내기 수정은 삭제 상태를 기록하는 갱신만 try/catch로 감싸고 경고를 남긴다. 호출부는 그 뒤 원래 오류를 다시 던진다. 대신 DB에 남은 내보내기 상태가 실제 중단 상태를 반영하지 못할 수 있다. 원래 오류를 유지하는 일과 진단 상태를 빠짐없이 저장하는 일에는 서로 다른 보장이 필요하다.

여기에도 범위가 있다. 갱신 실패를 처리하는 logger.warn() 자체는 추가 보호 없이 호출된다. 경고 로거까지 예외를 던지면 원래 오류가 다시 가려질 수 있다. 사용량 트래커는 recorder의 거절을 흡수하지만, recorder를 직접 부르는 모든 경로의 모든 로깅 실패까지 보호한다고 일반화할 수는 없다.

검증 대상은 속도보다 종료 순서다

트래커와 OpenAI 클라이언트 테스트는 기록기에 가짜 지연을 넣고, 슬롯 반환이 기록 완료보다 먼저인지 확인한다. 테스트 시간에서는 슬롯이 0ms에 반환되고 호출자는 3000ms의 기록 대기 후 완료된다. 이는 가짜 타이머로 만든 순서 검증이며 실제 응답 시간 측정이 아니다. SDK의 같은 결과·예외 전달, 중복 begin의 한 기록, 전송 전 실패의 무기록, 데드라인 이후 늦은 성공도 별도 사례로 다룬다.

recorder·sink 테스트는 비활성화와 열린 회로에서 귀속 조회를 생략하는지, 귀속 시간 초과와 저장 실패를 처리하는지 확인한다. 슬라이드 내보내기 테스트는 상태 갱신에 db down을 주입해 원래 오류의 이름과 삭제 사유가 유지되는지 확인한다. 로거와 DB가 동시에 실패하는 경우나 실제 DB 연결 점유 시간은 이 테스트 범위 밖이다.

공유 실행 허가가 얼마나 많은 공급자 호출을 허용할지 정한다면, 이 변경은 슬롯을 어느 작업까지 점유할지 정한다. AI Commons의 관찰 기록을 해석할 때도 호출 결과, 기록 대기, 진단 저장 성공을 구분해야 한다.