From 9699bed4803e3c4f5b86a65698ba52c6097a1af6 Mon Sep 17 00:00:00 2001 From: Emil Lerch Date: Tue, 30 Jun 2026 16:44:15 -0700 Subject: [PATCH] short circuit getCandles if a negative cache exists --- src/service.zig | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/service.zig b/src/service.zig index bef84c2..0dbf7d4 100644 --- a/src/service.zig +++ b/src/service.zig @@ -826,6 +826,17 @@ pub const DataService = struct { /// `opts.force_refresh = true` -> treats cache as stale and fetches. pub fn getCandles(self: *DataService, symbol: []const u8, opts: FetchOptions) DataError!FetchResult(Candle) { var s = self.store(); + + // Negative cache: this symbol is known to have no candle data on + // any provider (e.g. crypto like DOGE-USD that Tiingo can't + // serve). The marker lives in candles_daily.srf - written by the + // permanent-NotFound path below, or synced verbatim from a + // ZFIN_SERVER. Bail before any server sync or provider fetch so + // repeated calls don't re-hit the network every invocation. + // --refresh-data=force (force_refresh) bypasses this to retry. + if (!opts.force_refresh and s.isNegative(symbol, .candles_daily)) + return DataError.FetchFailed; + const today = fmt.todayDate(self.io); // wall-clock required: candle-meta freshness uses a market-aware @@ -3340,6 +3351,32 @@ test "loadLiveQuotes: empty symbol list returns empty without touching the netwo try std.testing.expectEqual(@as(usize, 0), prices.count()); } +test "getCandles: negative candle cache short-circuits without touching the network" { + const allocator = std.testing.allocator; + const io = std.testing.io; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + const dir_path = try tmp.dir.realPathFileAlloc(io, ".", allocator); + defer allocator.free(dir_path); + + var svc = DataService.init(io, allocator, .{ .cache_dir = dir_path }); + defer svc.deinit(); + + // Seed a negative candle entry: this symbol has no candle data on + // any provider (the crypto / defunct-ticker case). The marker lives + // in candles_daily.srf; candles_meta.srf is never created. + var s = svc.store(); + try s.ensureSymbolDir("DOGE-USD"); + s.writeNegative("DOGE-USD", .candles_daily); + + // Any server sync or provider fetch would trip this panic guard. + svc.panic_on_network_attempt = true; + + // getCandles must recognize the negative entry and fail fast, with + // no network round-trip (no panic). + try std.testing.expectError(DataError.FetchFailed, svc.getCandles("DOGE-USD", .{})); +} + test "DataService store helper creates valid store" { const allocator = std.testing.allocator; const config = Config{