| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113 |
- namespace Application.Helpers;
- /// <summary>SEIBro 일일 호출 한도의 관리 단위 — API 카테고리 (docs/SEIBro/readme.md §1, getXrcStk* 는 Stock 소속)</summary>
- public enum SeibroCategory
- {
- Corp = 1,
- Stock = 2,
- Bond = 3,
- Deriv = 4,
- Foreign = 5
- }
- /// <summary>
- /// SEIBro 카테고리별 일일 호출 예산 트래커 (DI 싱글턴) — 같은 카테고리에 속한 여러 수집 배치가 하나의 카운터를 공유한다.
- /// KST 날짜가 바뀌면 카운터가 자동 리셋된다 (SEIBro 한도가 일 단위이므로).
- /// 예산 상한(dailyBudget)은 호출자가 설정(Seibro:*Budget)에서 주입한다 — 트래커는 소비량만 기억한다.
- /// ⚠️ in-memory 카운터라 프로세스 재시작 시 리셋되는 한계가 있다 — 예산이 실한도의 80% 라 재시작 후 재소비분은
- /// 20% 헤드룸이 흡수한다 (허용된 설계 트레이드오프, DB 카운터로의 승격은 운영 관찰 후 결정).
- /// </summary>
- public sealed class SeibroQuota
- {
- private readonly Lock _lock = new();
- private readonly Func<DateOnly> _todayKst;
- private readonly Dictionary<SeibroCategory, int> _used = [];
- private DateOnly _date;
- public SeibroQuota() : this(TodayKst) { }
- /// <summary>테스트용 — KST 오늘 날짜 공급자 주입 (날짜 롤오버 검증)</summary>
- public SeibroQuota(Func<DateOnly> todayKstProvider)
- {
- _todayKst = todayKstProvider;
- _date = _todayKst();
- }
- /// <summary>count 만큼 소비 시도 — 예산 내면 소비 후 true, 초과면 소비 없이 false. count 가 0 이하면 항상 true.</summary>
- public bool TryConsume(SeibroCategory category, int count, int dailyBudget)
- {
- if (count <= 0)
- {
- return true;
- }
- lock (_lock)
- {
- RolloverIfNeeded();
- var used = _used.GetValueOrDefault(category);
- if (used + count > dailyBudget)
- {
- return false;
- }
- _used[category] = used + count;
- return true;
- }
- }
- /// <summary>오늘 남은 예산 (음수 없음) — rolling sweep 의 maxPerRun 산정용</summary>
- public int Remaining(SeibroCategory category, int dailyBudget)
- {
- lock (_lock)
- {
- RolloverIfNeeded();
- return Math.Max(0, dailyBudget - _used.GetValueOrDefault(category));
- }
- }
- /// <summary>오늘 소비량 (로그·모니터링용)</summary>
- public int Used(SeibroCategory category)
- {
- lock (_lock)
- {
- RolloverIfNeeded();
- return _used.GetValueOrDefault(category);
- }
- }
- private void RolloverIfNeeded()
- {
- var today = _todayKst();
- if (today == _date)
- {
- return;
- }
- _date = today;
- _used.Clear();
- }
- private static DateOnly TodayKst()
- {
- return DateOnly.FromDateTime(TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, Kst));
- }
- // KST 는 DST 없음 — 시스템에 타임존 데이터가 없으면 고정 +9 fallback (DailyScheduledService 와 동일 규칙)
- private static readonly TimeZoneInfo Kst = ResolveKst();
- private static TimeZoneInfo ResolveKst()
- {
- if (TimeZoneInfo.TryFindSystemTimeZoneById("Asia/Seoul", out var tz))
- {
- return tz;
- }
- if (TimeZoneInfo.TryFindSystemTimeZoneById("Korea Standard Time", out tz))
- {
- return tz;
- }
- return TimeZoneInfo.CreateCustomTimeZone("KST", TimeSpan.FromHours(9), "KST", "KST");
- }
- }
|