Skip to content

feat: 내부 단절·stale 실행과 deadline의 bounded 결과 회수 [1.85] - #238

Merged
chowon442 merged 2 commits into
feat/s07-run-coordinationfrom
worktree-task-1.85
Oct 18, 2026
Merged

chowon442 merged 2 commits into
feat/s07-run-coordinationfrom
worktree-task-1.85

Conversation

@chowon442

Copy link
Copy Markdown
Member

📌 관련 이슈

🏷️ PR 타입

  • ✨ 기능 추가 (Feature)
  • 🐛 버그 수정 (Bug Fix)
  • ♻️ 리팩토링 (Refactoring)
  • 📝 문서 수정 (Documentation)
  • 🎨 스타일 변경 (Style)
  • ✅ 테스트 추가 (Test)

📝 작업 내용

Task 1.85 — 내부 단절·Server 재시작·stale heartbeat·deadline에서 저장 결과를 한 번만 회수하거나 실행 소실을 bounded 실패로 수습한다. 대상 브랜치 feat/s07-run-coordination.

  • stale 회수 (RunCoordinator.recoverStale, recovery.interval): leader만, RUNNING|RETRYING + attempt >= 1 + lease 만료 + deadline 전 + isExecuting=false인 run을 같은 attempt·fence로 조건부 인수(RunClaimStore.takeOver, 동시 인수는 하나만 이김). 인수 worker도 로컬 permit을 잡는다. CANCELING은 고르지 않는다(1.84 소유).
  • status 우선 수습 (RunExecution RECOVER): terminal → 1.82 settle(StoredTerminal)(새 stream·provider 실행 없음, snapshot 복원 없음, 대조값 ai_runs.request_fingerprint) / 같은 epoch executing → 같은 요청으로 OBSERVE / canceling·run.canceled·다른 epoch → OBSOLETE / fingerprint 불일치 → INTERNAL_ERROR.
  • 미시작 vs 실행 소실: Brain record는 Postgres 영속이라 absent = 등록된 적 없음. 단 이 worker가 frame을 받았거나 이 attempt의 Brain source event가 저장돼 있으면 실행 소실 → 재전송 없이 failByServer(PROVIDER_ERROR)(1.82가 한 번 보상). 1.83 worker status 수습에도 같은 규칙을 적용했다(frame을 받은 뒤의 absent에 같은 요청을 보내면 새 graph가 START됐다).
  • 누적 한도: 재전달 한도는 run 행 retry_count부터 이어서 센다. 인수 횟수는 V5 ai_runs.recovery_count(단조 trigger)로 누적, max-recoveries(3) 초과 인수는 FINAL_CHECK(status 한 번으로 저장 terminal만 회수, 아니면 실패). deadline·max-status-checks는 기존대로.
  • 관측: recoveries{result}·recoveries.finished{result}(permit 반환)·execution_lost, 로그는 runId·attempt·mode·state·reason만.
  • 문서: docs/contracts/run-coordinator.md "stale 회수" 절(결정표·한도·경합·1.87/S13 인계).

📸 스크린샷

로컬(격리 Postgres postgres-t185:25432 tmpfs, eclipse-temurin:21-jdk 컨테이너, flock):

  • ./gradlew test --tests 'com.proovy.domain.run.coordinator.*' — RunRecoveryPostgresTest 11, RunCoordinatorPostgresTest 15, Properties 6, WorkerPool 1: 33 통과, skip 0
  • ./gradlew test --tests 'com.proovy.db.*' --tests 'com.proovy.domain.run.terminal.*' — 통과(skip 0, V5 migration 포함)
  • 전체 ./gradlew test는 CI 결과로 확인

✅ 체크리스트

  • 코드 리뷰를 받을 준비가 완료되었습니다
  • 테스트를 작성하고 모두 통과했습니다
  • 문서를 업데이트했습니다 (필요한 경우)
  • 코드 스타일 가이드를 준수했습니다
  • 셀프 리뷰를 완료했습니다

📎 기타 참고사항

fault injection (실제 Postgres + 실제 BrainRunClient + loopback 가짜 Brain): Server 재시작(stop()→Brain 상태 주입→start()) 뒤 저장 terminal 회수(delivery 1, 완료·보상 1회) / Brain 실행 중 재부착(같은 attempt·fence·deadline) / terminal 저장 전 소실(absent+저장 activity → 재전송 없이 실패·보상 1회) / 보내기 전 사라진 이전 프로세스(absent·증거 없음 → 첫 START 1회) / status 500 유실 뒤 회수 / 조회 한도 소진(실패·보상 1회, 재전송 없음) / 인수 한도 초과(FINAL_CHECK, OBSERVE 없음) / 동시 인수 6개 중 1개 승리·CANCELING·살아 있는 lease·비leader 인수 거부 / deadline 지난 stale은 sweep timeout(조회 0) / heartbeat만 늦은 로컬 worker는 인수 안 함 / recovery_count 단조.

후속 계약 메모

  • migration V5 점유(V5__run_recovery_count.sql, 추가형). 1.84가 V5를 쓰면 뒤 merge가 재번호.
  • 1.83 테스트 조정: 만료 lease 고아 run은 이제 회수되므로 슬롯 테스트는 다른 인스턴스의 살아 있는 lease로 바꿨고, "absent → 재전송" 테스트는 frame 없는 단절(Step.DROP)로 바꿨다(frame 뒤 absent는 실행 소실).
  • RunTerminalService·BrowserRunController·cancel 경로는 바꾸지 않았다. coordinator 공개 hook 시그니처 불변.
  • FINAL_CHECK가 executing 상태에서 실패시킨 run은 Brain record가 executing으로 남을 수 있다 → S13 cleanup·Brain lease sweep과 함께.
  • 미검증(S14): 실제 Brain crash 뒤 Brain status 응답, Cloud Run SIGTERM·다중 인스턴스 인수 실측, 회수 주기·한도 수치.
  • 로컬 환경 메모: 호스트 디스크가 가득 차(여유 ~6MB, 공용 이미지·볼륨) Postgres를 tmpfs로 띄웠다. 다른 세션 자원은 지우지 않았다.

🤖 Generated with Claude Code

… 소실 실패 (#225)

- RunCoordinator.recoverStale: leader가 lease 만료 RUNNING|RETRYING run(CANCELING 제외, deadline 전, 로컬 worker 없음)을
  같은 attempt·fence로 인수하고 로컬 permit을 잡아 RECOVER worker를 띄운다
- RunExecution RECOVER/FINAL_CHECK: status 먼저 — 저장 terminal은 1.82 settle(StoredTerminal)로 새 실행 없이 확정,
  같은 epoch executing은 같은 요청으로 OBSERVE, absent는 Brain 시작 증거가 없을 때만 첫 START
- absent + Brain 시작 증거(받은 frame·저장된 attempt source event) = 실행 소실 → 재전송 없이 failByServer(PROVIDER_ERROR)
  (1.83 worker status 수습에도 적용)
- 재전달 예산은 retry_count부터 이어서 세고, 인수 횟수는 V5 recovery_count(단조)로 누적 — max-recoveries 초과 시
  status 한 번만 확인 후 실패 수습
- 관측: recoveries{result}·recoveries.finished{result}·execution_lost, payload 없는 로그
- 테스트: RunRecoveryPostgresTest(재시작 전후 terminal·실행 중·실행 소실·미시작·status 유실·조회 한도·인수 한도·
  동시 인수·CANCELING·deadline·살아 있는 로컬 worker), 1.83 테스트 조정
- docs/contracts/run-coordinator.md stale 회수 절·1.87/S13 인계

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… 실행 run 후보 제외, 거부 시 회수 종료 기록, 일시 DB 재전달 횟수 저장 (#225)

- FINAL_CHECK: status 조회 1회, 다른 epoch executing도 실패, canceling·run.canceled는 1.84 소유로 두고 FINAL_CANCEL_PENDING 관측
- takeOver 뒤 coordinator가 멈추는 중이면 lease 즉시 만료·permit 반환(ABANDONED_STOPPING)
- findStale에서 이 인스턴스의 실행 중 run을 SQL로 제외
- RECOVER launch 거부 시 recoveries.finished{STOPPED}
- 호출자 없는 3인자 RunExecution 생성자 삭제
- 일시 DB 오류 재전달도 markRetrying으로 retry_count에 남김(DB 불가 시 메모리만)
- 문서: 재부착의 재전달 예산 소모 결정, 반복 인수 규칙, 슬롯 over-commit 인계, 일시 DB 예외

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chowon442
chowon442 merged commit 0cc79e3 into feat/s07-run-coordination Oct 18, 2026
1 check passed
@chowon442
chowon442 deleted the worktree-task-1.85 branch October 19, 2026 16:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant