fix(retry): review round 1 — minor/nit 4건 일괄 (#2 v0.2.3)
m1 — NoteRepository.test.ts 에 retryAllFailed OR IGNORE race-safe 회귀 가드 1 case 추가. failed 노트인데 pending_jobs row 가 이미 존재하는 비정상 race 상태 시뮬레이션 → INSERT OR IGNORE 라 duplicate 안 됨, 기존 attempts/next_run_at 보존. m2 — store.retryAllFailed 의 r.count 무시 의도 주석 1줄. 단일 process (Electron) 환경 + 모든 ai_status='failed' 가 retry 대상이라 사용자 시점 카운트는 0 reset 가 정확. n1 — AiWorker unreachableBackoffStep increment 명료화. Math.min(..., length-1) → 명시적 if 가드 (step < length-1) 로 cap 도달 시 no-op 의도 가시화. 동작 동일. n2 — AiWorker.processJob 의 max 의미 주석 1줄. unreachable/timeout 분기는 attempt -= 1 로 인덱스 stay 라 max 무관 — future maintainer 위해 명시. n3 (FailedBanner inline style) 은 v0.2.4 backlog (banner theme cleanup). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -120,6 +120,8 @@ export class AiWorker {
|
||||
}
|
||||
|
||||
private async processJob(job: Job): Promise<void> {
|
||||
// `max` 는 schema/other 분기 (attempts 증가) 의 cap 이다.
|
||||
// unreachable/timeout 분기는 `attempt -= 1; continue` 로 인덱스 stay — max 와 무관 무한 retry.
|
||||
const max = this.backoffsMs.length;
|
||||
for (let attempt = job.attempts; attempt < max; attempt++) {
|
||||
const startMs = this.now().getTime();
|
||||
@@ -170,7 +172,11 @@ export class AiWorker {
|
||||
// 무한 retry: attempts 증가 안 함, in-place loop + sleep.
|
||||
// markAiFailed / ai_failed emit 안 함 — ratio 통계는 schema/other 만 누적.
|
||||
const sleepMs = this.nextBackoffMs(this.unreachableBackoffStep);
|
||||
this.unreachableBackoffStep = Math.min(this.unreachableBackoffStep + 1, this.unreachableBackoffsMs.length - 1);
|
||||
// step 이 cap 도달 후엔 인덱스 stay — increment 는 무의미하지만 안전한 no-op.
|
||||
// (Math.min 가드: cap 넘어가도 length-1 로 묶임.)
|
||||
if (this.unreachableBackoffStep < this.unreachableBackoffsMs.length - 1) {
|
||||
this.unreachableBackoffStep += 1;
|
||||
}
|
||||
const nextRunAt = new Date(Date.now() + sleepMs).toISOString();
|
||||
this.repo.setNextRunAt(job.noteId, nextRunAt, msg);
|
||||
await this.sleep(sleepMs);
|
||||
|
||||
@@ -182,6 +182,8 @@ export const useInbox = create<InboxState>((set, get) => ({
|
||||
await inboxApi.retryAllFailed();
|
||||
// 낙관적 갱신: failedCount = 0. AiWorker 처리 진행 중에 PendingBanner 가 N건 노출.
|
||||
// refreshMeta 가 트리거되면 자연 동기 (worker.onUpdate → main → renderer).
|
||||
// 반환된 r.count 는 의도적으로 무시 — 단일 process 환경 (Electron) 이라 race 무관,
|
||||
// 모든 ai_status='failed' 가 retry 대상이므로 사용자 시점 카운트는 0 으로 reset 가 정확.
|
||||
set({ failedCount: 0 });
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -628,6 +628,21 @@ describe('NoteRepository — failed retry helpers', () => {
|
||||
expect(repo.retryAllFailed('2026-05-01T12:00:00.000Z')).toEqual({ ids: [] });
|
||||
});
|
||||
|
||||
it('retryAllFailed — pending_jobs 이미 존재 시 OR IGNORE (race 안전)', () => {
|
||||
// failed 노트인데 pending_jobs row 가 이미 존재하는 비정상 race 상태 시뮬레이션.
|
||||
// attempts=2, next_run_at=과거 — retryAllFailed 가 INSERT OR IGNORE 라 그대로 보존되어야.
|
||||
const id = makeFailed('a');
|
||||
db.prepare(`INSERT INTO pending_jobs (note_id, attempts, next_run_at) VALUES (?, 2, ?)`)
|
||||
.run(id, '2026-04-30T00:00:00.000Z');
|
||||
const r = repo.retryAllFailed('2026-05-01T12:00:00.000Z');
|
||||
expect(r.ids).toEqual([id]);
|
||||
const jobs = repo.getAllPendingJobs().filter((j) => j.noteId === id);
|
||||
expect(jobs).toHaveLength(1); // duplicate 안 됨
|
||||
// OR IGNORE 라 기존 row 보존 — attempts=2, nextRunAt 그대로
|
||||
expect(jobs[0]!.attempts).toBe(2);
|
||||
expect(jobs[0]!.nextRunAt).toBe('2026-04-30T00:00:00.000Z');
|
||||
});
|
||||
|
||||
it('setNextRunAt — attempts 변경 없이 next_run_at + last_error 갱신', () => {
|
||||
const { id } = repo.create({ rawText: 'x' });
|
||||
repo.incrementJobAttempt(id, '2026-05-01T11:00:00.000Z', 'first error');
|
||||
|
||||
Reference in New Issue
Block a user