KrxBackfill.cs 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869
  1. namespace Application.Helpers;
  2. /// <summary>
  3. /// KRX 일별 수집 배치의 공용 백필 오케스트레이션 (순수 로직, DB 비의존 — 테스트 가능).
  4. /// endDate 에서 startDate 방향(과거)으로 영업일을 훑으며, 아직 적재되지 않은 날만 수집한다:
  5. /// • 주말/휴장일(holidays) 은 건너뛴다.
  6. /// • existsForDate(day) == true (이미 적재됨) 이면 건너뛴다 → resumable.
  7. /// • 그 외에는 fetchAndUpsertForDate(day) 를 호출하고, quota 보호를 위해 호출 사이에 delayMs 만큼 쉰다.
  8. /// • fetch 횟수가 maxPerRun 에 도달하면 즉시 멈춘다 → 하루 배치가 quota 상한 안에서 조금씩 채운다.
  9. /// 최신일부터 채우므로(recent-first) 여러 번의 일일 실행에 걸쳐 3년치를 메우고 이후엔 최신만 유지한다.
  10. /// 반환값은 이번 실행에서 실제 fetch 한 날 수.
  11. /// </summary>
  12. public static class KrxBackfill
  13. {
  14. /// <param name="existsForDate">해당 날짜가 이미 적재되어 있는지 (true = skip)</param>
  15. /// <param name="fetchAndUpsertForDate">해당 날짜를 수집·upsert (부작용 담당)</param>
  16. /// <param name="startDate">백필 하한(포함) — 보통 today.AddYears(-BackfillYears)</param>
  17. /// <param name="endDate">백필 상한(포함) — 보통 직전 영업일. 여기서부터 과거로 훑는다</param>
  18. /// <param name="holidays">휴장일 집합 (주말은 자동 제외)</param>
  19. /// <param name="maxPerRun">이번 실행 최대 fetch 수 (quota 상한). 0 이하면 fetch 안 함</param>
  20. /// <param name="delayMs">fetch 호출 사이 지연(ms). 0 이하면 지연 없음</param>
  21. public static async Task<int> RunAsync(
  22. Func<DateOnly, CancellationToken, Task<bool>> existsForDate,
  23. Func<DateOnly, CancellationToken, Task> fetchAndUpsertForDate,
  24. DateOnly startDate,
  25. DateOnly endDate,
  26. IReadOnlySet<DateOnly> holidays,
  27. int maxPerRun,
  28. int delayMs,
  29. CancellationToken ct)
  30. {
  31. if (maxPerRun <= 0)
  32. {
  33. return 0;
  34. }
  35. var fetched = 0;
  36. for (var day = endDate; day >= startDate; day = day.AddDays(-1))
  37. {
  38. ct.ThrowIfCancellationRequested();
  39. if (day.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday || holidays.Contains(day))
  40. {
  41. continue;
  42. }
  43. if (await existsForDate(day, ct))
  44. {
  45. continue;
  46. }
  47. if (fetched > 0 && delayMs > 0)
  48. {
  49. await Task.Delay(delayMs, ct);
  50. }
  51. await fetchAndUpsertForDate(day, ct);
  52. fetched++;
  53. if (fetched >= maxPerRun)
  54. {
  55. break;
  56. }
  57. }
  58. return fetched;
  59. }
  60. }