BlogClawdbot Gateway 크래시 해결

설정 변경이 시스템을 무너뜨릴 때

시스템의 강함은 큰 장애에 어떻게 대응하는가 아니라, 작은 변화를 얼마나 우아하게 처리하는가로 정해진다.


문제

어느 날 오후, Clawdbot 게이트웨이가 크래시됐다. 원인은 TTS(Text-To-Speech) 음성 설정을 변경했을 때.

이게 왜 문제인가?

  • 게이트웨이가 예상치 못하게 재시작됨
  • 재시작 중 AbortError 발생
  • 프로세스 종료 → 통신 불가

겉보기에는 간단한 설정 변경이 시스템을 중단시켰다.

내가 해야 할 일은 “빨리 재시작하기”가 아니라 “왜 이런 일이 일어났는가”를 이해하는 것이었다.


기존 접근의 한계

만약 단순하게 생각했다면:

1️⃣ “재시작하면 되지 않을까?”

pkill -f clawdbot-gateway
clawdbot gateway run
  • 문제: 근본 원인 불명
  • 위험: 같은 상황에서 다시 크래시

2️⃣ “설정 변경을 피하자”

clawdbot config set gateway.reload.mode hot
  • 문제: 재시작이 필요한 다른 설정은?
  • 위험: 일부 설정이 반영되지 않음

3️⃣ “다음 버전 업데이트를 기다리자”

  • 문제: 지금 당장 필요함
  • 위험: 조직이 커지면 이런 대기는 불가능

내가 내린 판단

판단 기준: “이 문제의 본질은 무엇인가”

에러 메시지: AbortError: This operation was aborted

         무엇이 abort 됐나?

설정 변경 후 크래시

         왜 설정 변경만 해도?

나는 로그를 추적하기로 결정했다. 이는:

  • 빠른 응급 처치보다 느림
  • 하지만 구근본 원인을 이해할 수 있음
  • 향후 같은 문제 방지 가능

진단: 연쇄 실패의 구조

증상 분석

tail -50 ~/.clawdbot/logs/gateway.err.log

핵심 로그:

[reload] config change requires gateway restart (meta.lastTouchedAt)
[clawdbot] Unhandled promise rejection: AbortError: This operation was aborted

첫 번째 발견: meta.lastTouchedAt 때문에 재시작이 트리거됨

근본 원인 추적

설정 파일을 변경할 때마다:

1. 사용자: "TTS 음성을 변경"

2. 설정 저장됨

3. meta.lastTouchedAt 타임스탬프 자동 업데이트 (시스템이 자동)

4. config watcher가 파일 변경 감지

5. buildGatewayReloadPlan()에서 변경 경로 분석
   - "messages.tts.elevenlabs.voiceId" → reload rule 있음 ✓
   - "meta.lastTouchedAt" → reload rule 없음 ❌

6. 결론: "재시작이 필요하다"

7. SIGUSR1 신호로 graceful restart 시도

8. 진행 중인 HTTP 요청들이 abort됨

9. AbortError → unhandled rejection handler에 도달

10. process.exit(1) → 크래시

2개의 버그가 동시에 일어났다:

  1. Reload Rule 누락: meta prefix에 rule이 없음
  2. Error Handling 부족: AbortError가 graceful shutdown 중에 발생해도 처리되지 않음

왜 이전엔 발견 안 됐을까?

이 버그는:

  • 평상시에는 나타나지 않음 (설정 변경 시에만)
  • 설정을 자주 변경하지 않으면 드물게 발생
  • 문서에 명시되지 않음 (metadata 변경의 side effect)

이것이 “숨겨진 버그”의 특징이다.


의사결정: 두 가지 수정

나는 두 가지 레벨에서 수정을 결정했다:

1단계: 재시작 불필요 설정 추가

파일: /usr/local/lib/node_modules/clawdbot/dist/gateway/config-reload.js

const BASE_RELOAD_RULES_TAIL = [
+   { prefix: "meta", kind: "none" },  // ← 추가
    { prefix: "identity", kind: "none" },
    { prefix: "wizard", kind: "none" },
];

의미: “메타데이터는 변경되어도 게이트웨이를 재시작하지 마”

2단계: 우아한 에러 처리

파일: /usr/local/lib/node_modules/clawdbot/dist/infra/unhandled-rejections.js

export function installUnhandledRejectionHandler() {
    process.on("unhandledRejection", (reason, _promise) => {
        if (isUnhandledRejectionHandled(reason))
            return;
 
+       // Graceful shutdown 중 발생하는 AbortError는 정상
+       if (reason instanceof Error && reason.name === "AbortError") {
+           console.error("[clawdbot] AbortError during shutdown (ignored):", reason.message);
+           return;  // ← 프로세스 종료 안 함
+       }
 
        console.error("[clawdbot] Unhandled promise rejection:", formatUncaughtError(reason));
        process.exit(1);
    });
}

의미: “AbortError는 shutdown 중 정상 발생하는 에러니까 무시해”


기술적 배경: 왜 이 두 가지인가?

Reload System의 구조

Clawdbot은:

설정 파일 변경 감지

어떤 경로가 변경됐는가? (diffConfigPaths)

그 경로의 reload rule은?
    ├─ kind: "none" → 재시작 불필요
    ├─ kind: "hot" → hot reload
    └─ (rule 없음) → 재시작 필수

필요한 경우 SIGUSR1 신호 전송

meta.lastTouchedAt에 rule이 없었다 = “이건 재시작이 필요한 설정”으로 간주됨

Graceful Restart의 동작

1. SIGUSR1 신호 수신
2. 새로운 요청은 받지 않음
3. 진행 중인 요청이 끝날 때까지 대기
4. (또는 timeout이면 abort)
5. 모든 연결 종료 (abort)
6. 프로세스 재시작

문제: Step 5에서 abort되면서 AbortError 발생 → 처리되지 않으면 프로세스 강제 종료


시스템 설계의 교훈

이 문제를 통해 배운 것들:

1️⃣ Configuration의 분류

// ❌ 나쁜 구조
config = {
    settings: {...},
    metadata: {...},  // 섞여있음
}
 
// ✅ 좋은 구조
config = {
    settings: {...},  // 비즈니스 설정
    metadata: {...},  // 시스템 메타데이터
    // 둘의 변경이 다르게 처리됨
}

시스템 메타데이터(like timestamp)는 설정 변경의 사이드 이펙트여야지, 의도적 변경이 아니어야 한다.

2️⃣ Error Handler의 규칙

// ❌ 너무 일반적
process.on("unhandledRejection", () => process.exit(1));
 
// ✅ 맥락을 고려
process.on("unhandledRejection", (reason) => {
    if (isExpectedDuringShutdown(reason))
        return;  // 로그만 남기고 진행
 
    if (isCritical(reason))
        process.exit(1);  // 치명적이면 종료
});

모든 unhandledRejection이 재앙은 아니다. 특히 shutdown 중에는 예상되는 에러들이 있다.

3️⃣ 설정 시스템의 자동화

// ❌ 수동으로 rule 작성
const RELOAD_RULES = [
    { prefix: "a", kind: "none" },
    { prefix: "b", kind: "hot" },
    // ... 언젠가 누군가 새로운 항목 추가하는데 rule 빼먹을 수 있음
];
 
// ✅ 검증 로직 추가
function validateReloadRules(config: Config, rules: ReloadRule[]) {
    const configPrefixes = Object.keys(config);
    const rulePrefixes = rules.map(r => r.prefix);
 
    const missing = configPrefixes.filter(p => !rulePrefixes.includes(p));
    if (missing.length > 0) {
        throw new Error(`Missing reload rules for: ${missing.join(', ')}`);
    }
}

새로운 설정 경로를 추가하면 자동으로 감지되도록.


실제 적용

수정 후 테스트

# 설정 변경
clawdbot gateway config patch {
  "messages": {
    "tts": {
      "elevenlabs": {
        "voiceSettings": { "stability": 0.7 }
      }
    }
  }
}

결과:

✅ 게이트웨이 재시작 안 됨
✅ 설정 변경 반영됨
✅ 진행 중이던 작업 계속됨

배포

# 게이트웨이 재시작 (수정된 파일 로드)
pkill -f clawdbot-gateway
clawdbot gateway run --bind loopback --port 18789 &

이제 같은 상황에서 안정적으로 작동한다.


중요한 주의사항: 업데이트 위험

문제

우리가 수정한 파일들은:

/usr/local/lib/node_modules/clawdbot/dist/
├── gateway/config-reload.js      ← 우리가 수정
└── infra/unhandled-rejections.js ← 우리가 수정

npm 패키지 내부 파일이다.

업데이트하면?

clawdbot update
# 또는
curl -fsSL https://molt.bot/install.sh | bash

실행하면 수정사항이 덮어써짐 💀

대응 방안

방법비용이점
업데이트 후 재수정낮음, 반복최신 버전 유지
공식 릴리즈 대기높음, 시간영구 해결
PR 기여중간, 정중오픈소스 기여

선택 - 공식 수정 대기

나는 다음 Clawdbot 버전에 이 수정이 포함될 때까지 대기하기로 결정했다. 왜냐하면:

  1. This is a bug, not a workaround: 임시 방편이 아니라 버그 수정
  2. Permanent fix preferred: 매번 수동으로 수정하는 건 unsustainable
  3. Open source contribution: 다른 사용자도 이 문제를 겪을 수 있음

생각해볼 질문

당신의 시스템에서:

  1. 설정 변경이 언제 시스템 재시작을 트리거하는가?

    • 모든 설정 변경인가?
    • 일부만 재시작이 필요한가?
    • 이게 문서화되어 있는가?
  2. Graceful shutdown은 정말 graceful한가?

    • 예상되는 에러들을 구분하는가?
    • 아니면 모든 에러를 같게 취급하는가?
  3. 새로운 설정을 추가할 때마다 어디서 수정해야 하는가?

    • 1개 파일?
    • 5개 파일?
    • 10개 파일?
    • 이게 문제가 아닌가?
  4. 숨겨진 버그는 어떻게 찾을 것인가?

    • 로그는 충분히 명확한가?
    • 재현 방법이 있는가?
  5. 팀이 이 버그를 다시 만들지 않으려면 어떻게 할 것인가?

    • 문서화?
    • 자동 검증?
    • 코드 리뷰 체크리스트?

마지막 통찰

이 문제의 본질은 **버그가 아니라 “시스템 설계”**였다.

  • 메타데이터가 설정과 섞여있었다
  • Reload rule이 완벽하지 않았다
  • Error handler가 맥락을 고려하지 않았다

개발자의 일은 단순히 버그를 고치는 게 아니라:

  1. 왜 이런 버그가 생겼는가
  2. 어떻게 다시 생기지 않게 할 것인가
  3. 조직이 커져도 이 실수를 반복하지 않으려면

이 세 가지를 고민하는 것이다.

만약 다음에 또 같은 문제가 생긴다면, 그건 버그가 아니라 배운 교훈을 활용하지 않은 것이다.


참고

공식 이슈 리포트:

디버깅에 도움을 준 로디에게 감사합니다. 🦊