| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970 |
- namespace Application.Helpers;
- /// <summary>
- /// SEIBro per-키 rolling 수집의 공용 오케스트레이션 (순수 로직, DB 비의존 — 테스트 가능). KrxBackfill 의 자매 헬퍼 —
- /// KrxBackfill 이 "날짜 스윕"(기간 파라미터, 전시장 반환)이라면 이쪽은 "키 스윕"(ISIN/발행회사번호별 개별 호출)이다:
- /// • 대상 = (키, 마지막 갱신시각) 목록. 미처리(LastUpdatedAt == null) 를 먼저, 그다음 오래된(stale) 순으로 처리한다
- /// → 여러 날에 걸친 실행으로 전체 유니버스가 자동 보강·순환 갱신된다 (resumable).
- /// • fetch 횟수가 maxPerRun 에 도달하면 즉시 멈춘다 → 하루 배치가 카테고리 quota(SeibroQuota) 상한 안에서 조금씩 채운다.
- /// • quota 보호를 위해 호출 사이에 delayMs 만큼 쉰다 (첫 호출 전에는 쉬지 않는다 — KrxBackfill 동일).
- /// • 동률(같은 LastUpdatedAt·둘 다 null)은 입력 순서를 보존한다 (stable sort).
- /// 반환값은 이번 실행에서 실제 fetch 한 키 수.
- /// </summary>
- public static class SeibroRollingSweep
- {
- /// <param name="targets">대상 키 + 마지막 갱신시각 (null = 미처리 → 최우선)</param>
- /// <param name="fetchAndUpsert">해당 키를 수집·upsert (부작용 담당)</param>
- /// <param name="maxPerRun">이번 실행 최대 fetch 수 (quota 상한). 0 이하면 fetch 안 함</param>
- /// <param name="delayMs">fetch 호출 사이 지연(ms). 0 이하면 지연 없음</param>
- /// <param name="onError">개별 키 수집 실패 시 호출(로깅용). null 이면 조용히 skip. 실패 키는 마커 미기록 → 다음 실행 재시도(무손실)</param>
- public static async Task<int> RunAsync<TKey>(
- IReadOnlyList<(TKey Key, DateTime? LastUpdatedAt)> targets,
- Func<TKey, CancellationToken, Task> fetchAndUpsert,
- int maxPerRun,
- int delayMs,
- CancellationToken ct,
- Func<TKey, Exception, Task>? onError = null)
- {
- if (maxPerRun <= 0)
- {
- return 0;
- }
- // 미처리(null) 우선, 그다음 오래된 순 — OrderBy 는 stable 이라 동률은 입력 순서 유지
- var ordered = targets.OrderBy(c => c.LastUpdatedAt.HasValue ? 1 : 0).ThenBy(c => c.LastUpdatedAt ?? DateTime.MinValue);
- var fetched = 0;
- foreach (var (key, _) in ordered)
- {
- ct.ThrowIfCancellationRequested();
- if (fetched > 0 && delayMs > 0)
- {
- await Task.Delay(delayMs, ct);
- }
- try
- {
- await fetchAndUpsert(key, ct);
- }
- catch (Exception ex) when (ex is not OperationCanceledException)
- {
- // 개별 키 수집/파싱 실패(예: SEIBro <SeibroAPI> soft-error 응답)는 run 전체를 중단시키지 않고 skip.
- // 마커는 fetchAndUpsert 내부(성공 시)에만 기록되므로 실패 키는 다음 실행에 재시도된다(무손실).
- if (onError is not null)
- {
- await onError(key, ex);
- }
- }
- fetched++;
- if (fetched >= maxPerRun)
- {
- break;
- }
- }
- return fetched;
- }
- }
|