be more clear what negative cache means
This commit is contained in:
parent
51aaee9966
commit
2a89125977
1 changed files with 74 additions and 3 deletions
|
|
@ -62,6 +62,25 @@ pub const DataError = error{
|
|||
AuthError,
|
||||
};
|
||||
|
||||
/// Decide whether a provider failure is permanent enough to merit a
|
||||
/// negative-cache entry. Negative entries suppress retries until the
|
||||
/// next manual `--refresh` / `cache clear`, so writing one is only
|
||||
/// safe when we're confident more attempts won't succeed.
|
||||
///
|
||||
/// Today the only certain-permanent failure is `NotFound`: the symbol
|
||||
/// just doesn't have data of this type at this provider. Everything
|
||||
/// else (rate limit, network blip, server 5xx, auth, parse error) is
|
||||
/// either transient or fixable; recording a negative entry would
|
||||
/// silently suppress retries for hours/days.
|
||||
///
|
||||
/// Rate-limit (`error.RateLimited`) is excluded here because callers
|
||||
/// handle it specially (single retry after backoff). Anything that
|
||||
/// reaches this classifier and isn't `NotFound` returns false →
|
||||
/// caller returns `FetchFailed` without poisoning the cache.
|
||||
pub fn isPermanentProviderFailure(err: anyerror) bool {
|
||||
return err == error.NotFound;
|
||||
}
|
||||
|
||||
/// Re-exported provider types needed by commands via DataService.
|
||||
pub const CompanyOverview = alphavantage.CompanyOverview;
|
||||
|
||||
|
|
@ -314,7 +333,14 @@ pub const DataService = struct {
|
|||
s.write(T, symbol, retried, data_type.ttl());
|
||||
return .{ .data = retried, .source = .fetched, .timestamp = std.Io.Timestamp.now(self.io, .real).toSeconds(), .allocator = self.allocator };
|
||||
}
|
||||
s.writeNegative(symbol, data_type);
|
||||
// Only NotFound (provider says "this symbol genuinely has
|
||||
// no data of this type") gets a negative-cache entry.
|
||||
// Transient failures (network, 5xx, auth misconfig, parse
|
||||
// error) propagate as FetchFailed without poisoning the
|
||||
// cache, so the next call retries naturally.
|
||||
if (isPermanentProviderFailure(err)) {
|
||||
s.writeNegative(symbol, data_type);
|
||||
}
|
||||
return DataError.FetchFailed;
|
||||
};
|
||||
|
||||
|
|
@ -561,6 +587,11 @@ pub const DataService = struct {
|
|||
}
|
||||
return DataError.TransientError;
|
||||
}
|
||||
// FetchFailed at this point means BOTH Tiingo and Yahoo
|
||||
// returned NotFound (or Yahoo was unavailable on top of
|
||||
// Tiingo NotFound) — symbol genuinely has no candle data
|
||||
// anywhere we look. Negative-cache the result so we don't
|
||||
// keep retrying nonexistent symbols.
|
||||
s.writeNegative(symbol, .candles_daily);
|
||||
return DataError.FetchFailed;
|
||||
};
|
||||
|
|
@ -634,7 +665,9 @@ pub const DataService = struct {
|
|||
return DataError.FetchFailed;
|
||||
};
|
||||
}
|
||||
s.writeNegative(symbol, .earnings);
|
||||
if (isPermanentProviderFailure(err)) {
|
||||
s.writeNegative(symbol, .earnings);
|
||||
}
|
||||
return DataError.FetchFailed;
|
||||
};
|
||||
|
||||
|
|
@ -659,7 +692,9 @@ pub const DataService = struct {
|
|||
return DataError.FetchFailed;
|
||||
};
|
||||
}
|
||||
s.writeNegative(symbol, .etf_profile);
|
||||
if (isPermanentProviderFailure(err)) {
|
||||
s.writeNegative(symbol, .etf_profile);
|
||||
}
|
||||
return DataError.FetchFailed;
|
||||
};
|
||||
|
||||
|
|
@ -1608,6 +1643,42 @@ pub const DataService = struct {
|
|||
|
||||
// ── Tests ─────────────────────────────────────────────────────────
|
||||
|
||||
test "isPermanentProviderFailure: NotFound is permanent" {
|
||||
try std.testing.expect(isPermanentProviderFailure(error.NotFound));
|
||||
}
|
||||
|
||||
test "isPermanentProviderFailure: RequestFailed is transient" {
|
||||
try std.testing.expect(!isPermanentProviderFailure(error.RequestFailed));
|
||||
}
|
||||
|
||||
test "isPermanentProviderFailure: ServerError is transient" {
|
||||
try std.testing.expect(!isPermanentProviderFailure(error.ServerError));
|
||||
}
|
||||
|
||||
test "isPermanentProviderFailure: Unauthorized is transient" {
|
||||
// Auth misconfigs are user-fixable (set the API key); not a reason
|
||||
// to permanently suppress retries.
|
||||
try std.testing.expect(!isPermanentProviderFailure(error.Unauthorized));
|
||||
}
|
||||
|
||||
test "isPermanentProviderFailure: InvalidResponse is transient" {
|
||||
// Parse errors are usually a provider format change or one-off
|
||||
// garbage response — retrying later is fine.
|
||||
try std.testing.expect(!isPermanentProviderFailure(error.InvalidResponse));
|
||||
}
|
||||
|
||||
test "isPermanentProviderFailure: PaymentRequired is transient" {
|
||||
// FMP marks plan-locked symbols with HTTP 402; user can upgrade
|
||||
// their plan or rotate providers, so don't poison the cache.
|
||||
try std.testing.expect(!isPermanentProviderFailure(error.PaymentRequired));
|
||||
}
|
||||
|
||||
test "isPermanentProviderFailure: RateLimited is transient" {
|
||||
// Rate-limit is the textbook transient case; the caller already
|
||||
// handles it specially with backoff + retry.
|
||||
try std.testing.expect(!isPermanentProviderFailure(error.RateLimited));
|
||||
}
|
||||
|
||||
test "isMutualFund identifies mutual funds" {
|
||||
// Standard mutual fund tickers (5 letters ending in X)
|
||||
try std.testing.expect(DataService.isMutualFund("FDSCX"));
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue