estimateWaitSeconds needs to take a data type due to provider diffs
This commit is contained in:
parent
60435b645f
commit
4d65cc45f4
2 changed files with 94 additions and 10 deletions
|
|
@ -142,8 +142,14 @@ pub const LoadProgress = struct {
|
|||
const display_idx = self.index_offset + index + 1;
|
||||
switch (status) {
|
||||
.fetching => {
|
||||
// Show rate-limit wait before the fetch
|
||||
if (self.svc.estimateWaitSeconds()) |w| {
|
||||
// Show rate-limit wait before the fetch.
|
||||
// Prices come from the candle pipeline; ask about
|
||||
// candles_daily so the wait reflects whichever
|
||||
// provider serves candles (currently Tiingo, no
|
||||
// rate limiter -- so this is effectively a no-op,
|
||||
// but stays correct if the provider gains a limiter
|
||||
// or the type's primary changes).
|
||||
if (self.svc.estimateWaitSeconds(.candles_daily)) |w| {
|
||||
if (w > 0) stderrRateLimitWait(self.io, w, self.color);
|
||||
}
|
||||
stderrProgress(self.io, symbol, " (fetching)", display_idx, self.grand_total, self.color);
|
||||
|
|
|
|||
|
|
@ -1687,14 +1687,35 @@ pub const DataService = struct {
|
|||
return mr.meta.last_date;
|
||||
}
|
||||
|
||||
/// Estimate wait time (in seconds) before the next TwelveData API call can proceed.
|
||||
/// Returns 0 if a request can be made immediately. Returns null if no API key.
|
||||
pub fn estimateWaitSeconds(self: *DataService) ?u64 {
|
||||
if (self.td) |*td| {
|
||||
const ns = td.rate_limiter.estimateWaitNs();
|
||||
return if (ns == 0) 0 else @max(1, ns / std.time.ns_per_s);
|
||||
}
|
||||
return null;
|
||||
/// Estimate wait time (in seconds) before a fetch for `data_type`
|
||||
/// can proceed without blocking on its provider's rate limiter.
|
||||
/// Returns 0 if a request can be made immediately, or if the
|
||||
/// provider for this data type has no rate limiter. Returns null
|
||||
/// if the relevant provider isn't instantiated yet (e.g., no API
|
||||
/// key, or first call hasn't happened to lazy-init it).
|
||||
///
|
||||
/// The caller asks "how long until getX can proceed?" -- the
|
||||
/// service maps data type to provider internally so the caller
|
||||
/// doesn't have to know which provider serves which data.
|
||||
pub fn estimateWaitSeconds(self: *DataService, data_type: cache.DataType) ?u64 {
|
||||
const ns: u64 = switch (data_type) {
|
||||
// Polygon-served: dividends and splits.
|
||||
.dividends, .splits => if (self.pg) |*pg| pg.rate_limiter.estimateWaitNs() else return null,
|
||||
// FMP-served: earnings.
|
||||
.earnings => if (self.fmp) |*fmp| fmp.rate_limiter.estimateWaitNs() else return null,
|
||||
// Cboe-served: options chains.
|
||||
.options => if (self.cboe) |*cboe| cboe.rate_limiter.estimateWaitNs() else return null,
|
||||
// EDGAR-served: ETF metrics, entity facts, ticker maps.
|
||||
.etf_metrics, .entity_facts, .tickers_funds, .tickers_companies => if (self.edgar) |*e| e.rate_limiter.estimateWaitNs() else return null,
|
||||
// No proactive token-bucket limiter for these. Tiingo
|
||||
// (candles) has a 1000/day quota enforced reactively
|
||||
// via 429-then-backoff in `getCandles`; Wikidata
|
||||
// (classification) has no published quota; the legacy
|
||||
// `etf_profile` and `meta` types aren't fetched. Nothing
|
||||
// useful to wait for at the call site, so report 0.
|
||||
.candles_daily, .candles_meta, .classification, .etf_profile, .meta => 0,
|
||||
};
|
||||
return if (ns == 0) 0 else @max(1, ns / std.time.ns_per_s);
|
||||
}
|
||||
|
||||
/// Read candles from cache only (no network fetch). Used by TUI for display.
|
||||
|
|
@ -3087,6 +3108,63 @@ test "DataService getProvider returns NoApiKey for Wikidata without user_email"
|
|||
try std.testing.expectError(DataError.NoApiKey, ed_result);
|
||||
}
|
||||
|
||||
test "estimateWaitSeconds returns null when relevant provider not instantiated" {
|
||||
const allocator = std.testing.allocator;
|
||||
const config = Config{ .cache_dir = "/tmp/zfin-test-cache" };
|
||||
var svc = DataService.init(std.testing.io, allocator, config);
|
||||
defer svc.deinit();
|
||||
|
||||
// No providers initialized yet (lazy). Each rate-limited data
|
||||
// type returns null because its provider is missing.
|
||||
try std.testing.expectEqual(@as(?u64, null), svc.estimateWaitSeconds(.dividends));
|
||||
try std.testing.expectEqual(@as(?u64, null), svc.estimateWaitSeconds(.splits));
|
||||
try std.testing.expectEqual(@as(?u64, null), svc.estimateWaitSeconds(.earnings));
|
||||
try std.testing.expectEqual(@as(?u64, null), svc.estimateWaitSeconds(.options));
|
||||
try std.testing.expectEqual(@as(?u64, null), svc.estimateWaitSeconds(.etf_metrics));
|
||||
try std.testing.expectEqual(@as(?u64, null), svc.estimateWaitSeconds(.entity_facts));
|
||||
}
|
||||
|
||||
test "estimateWaitSeconds returns 0 for types without rate limiters" {
|
||||
// candles_daily, classification, etc. are served by providers
|
||||
// that don't have a rate limiter (Tiingo, Wikidata). The
|
||||
// function returns 0 for these regardless of provider state --
|
||||
// there's nothing to wait for.
|
||||
const allocator = std.testing.allocator;
|
||||
const config = Config{ .cache_dir = "/tmp/zfin-test-cache" };
|
||||
var svc = DataService.init(std.testing.io, allocator, config);
|
||||
defer svc.deinit();
|
||||
|
||||
try std.testing.expectEqual(@as(?u64, 0), svc.estimateWaitSeconds(.candles_daily));
|
||||
try std.testing.expectEqual(@as(?u64, 0), svc.estimateWaitSeconds(.candles_meta));
|
||||
try std.testing.expectEqual(@as(?u64, 0), svc.estimateWaitSeconds(.classification));
|
||||
try std.testing.expectEqual(@as(?u64, 0), svc.estimateWaitSeconds(.etf_profile));
|
||||
try std.testing.expectEqual(@as(?u64, 0), svc.estimateWaitSeconds(.meta));
|
||||
}
|
||||
|
||||
test "estimateWaitSeconds returns 0 for fresh rate-limited providers" {
|
||||
// Once the provider is instantiated, an unused rate limiter
|
||||
// returns 0 (no wait). This is the steady-state happy path
|
||||
// for the call at the top of each refresh iteration.
|
||||
const allocator = std.testing.allocator;
|
||||
const config = Config{
|
||||
.cache_dir = "/tmp/zfin-test-cache",
|
||||
.polygon_key = "test-polygon-key",
|
||||
.fmp_key = "test-fmp-key",
|
||||
};
|
||||
var svc = DataService.init(std.testing.io, allocator, config);
|
||||
defer svc.deinit();
|
||||
|
||||
// Touch each provider to lazy-init it. We don't care about the
|
||||
// returned pointer; just need svc.pg / svc.fmp to be non-null.
|
||||
_ = try svc.getProvider(Polygon);
|
||||
_ = try svc.getProvider(Fmp);
|
||||
|
||||
// Fresh limiters have full token bucket -> 0 wait.
|
||||
try std.testing.expectEqual(@as(?u64, 0), svc.estimateWaitSeconds(.dividends));
|
||||
try std.testing.expectEqual(@as(?u64, 0), svc.estimateWaitSeconds(.splits));
|
||||
try std.testing.expectEqual(@as(?u64, 0), svc.estimateWaitSeconds(.earnings));
|
||||
}
|
||||
|
||||
// ── lookupInTickerMaps ────────────────────────────────────────
|
||||
//
|
||||
// Pure function — no I/O. Consumed by `lookupEdgarFallback`,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue