Files
zszq-trs/UnitTestProject/Modules/DataProviderModule/Fr007FixingCacheTest.cs
hjhan 7a5089555d fix(fr007-cache): 直查命中失效快照+去冗余排序+重载失败保旧快照防重试风暴
- EodPriceQueryService.TryGetPrice: FR007 快照 miss 直查库命中后 Invalidate,
  下次访问整表重载回 O(1),否则该键 TTL 窗口内每次读取都退化为点查
- 移除点查多余的 OrderByDescending(主键等值查询取首行即可)
- Fr007FixingCache.EnsureFresh: 重载失败(DB抖动)保留旧快照不抛并重置TTL时钟
  ——历史定盘不可变旧值依旧正确,同时防重试风暴;版本失效路径不受TTL掩盖
- 补两测试: 重载失败保快照窗口内不重试 / 首次装载失败miss不抛窗口内不重试
2026-08-18 09:33:28 +08:00

138 lines
6.3 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using YLErp.Modules.DataProviderModule;
namespace YLErp.Modules.DataProviderModule
{
/// <summary>
/// Fr007FixingCache 快照缓存行为契约(纯内存,不连库;Loader/时钟均为注入接缝):
/// ① 首次访问批量预载、命中 O(1);② miss 不进快照(负缓存防线——当日发布前 miss、发布后须能查到);
/// ③ 写侧版本失效:Invalidate 后立即重载取到新值(不等 TTL);④ TTL 过期自动重载(直改库兜底);
/// ⑤ TTL 窗口内无写入不重载(零查询稳态)。
/// </summary>
[TestClass]
public class Fr007FixingCacheTest
{
private static readonly DateTime D1 = new(2026, 7, 6);
private static readonly DateTime D2 = new(2026, 7, 13);
private int _loadCount;
private Dictionary<DateTime, double> _market;
[TestInitialize]
public void Init()
{
_loadCount = 0;
_market = new Dictionary<DateTime, double> { [D1] = 0.0142, [D2] = 0.01425 };
Fr007FixingCache.ResetForTest();
Fr007FixingCache.LoadSnapshot = () => { _loadCount++; return new Dictionary<DateTime, double>(_market); };
}
[TestCleanup]
public void Cleanup() => Fr007FixingCache.ResetForTest();
private static void AdvanceClock(long ticks) => Fr007FixingCache.NowTicks = () => ticks;
[TestMethod]
public void 首次访问预载并命中()
{
Fr007FixingCache.NowTicks = () => 1_000_000L;
Assert.IsTrue(Fr007FixingCache.TryGet(D1, out var p1), "预载后历史定盘应命中");
Assert.AreEqual(0.0142, p1, 1e-12);
Assert.AreEqual(1, _loadCount, "首次访问恰好装载一次");
Assert.IsTrue(Fr007FixingCache.TryGet(D2, out var p2), "同快照内多次命中");
Assert.AreEqual(0.01425, p2, 1e-12);
Assert.AreEqual(1, _loadCount, "命中不应重复装载");
}
[TestMethod]
public void miss不进快照_当日新发布经直查兜底后TTL内可见()
{
Fr007FixingCache.NowTicks = () => 1_000_000L;
var today = new DateTime(2026, 8, 18);
Assert.IsFalse(Fr007FixingCache.TryGet(today, out _), "快照无该行应 miss(调用方直查库兜底)");
Assert.AreEqual(1, _loadCount);
// 直查库发现了新发布的当日行 → 写侧失效(SwapFlowService 场景)→ 重载后可见
_market[today] = 0.0143;
Fr007FixingCache.Invalidate();
Assert.IsTrue(Fr007FixingCache.TryGet(today, out var p), "写侧失效重载后当日行应可见");
Assert.AreEqual(0.0143, p, 1e-12);
Assert.AreEqual(2, _loadCount);
}
[TestMethod]
public void 写侧失效立即重载取到修正值_不等TTL()
{
Fr007FixingCache.NowTicks = () => 1_000_000L;
Fr007FixingCache.TryGet(D1, out _);
Assert.AreEqual(1, _loadCount);
_market[D1] = 0.0150; // 界面修正错价
Fr007FixingCache.Invalidate();
// 时钟只走了 1 tick(远小于 TTL),仍必须重载
Fr007FixingCache.NowTicks = () => 1_000_001L;
Assert.IsTrue(Fr007FixingCache.TryGet(D1, out var p), "修正后仍应命中");
Assert.AreEqual(0.0150, p, 1e-12, "TTL 未到也必须看到写侧修正值");
Assert.AreEqual(2, _loadCount, "版本失效应立即触发重载");
}
[TestMethod]
public void TTL过期自动重载_直改库兜底()
{
Fr007FixingCache.NowTicks = () => 1_000_000L;
Fr007FixingCache.TryGet(D1, out _);
Assert.AreEqual(1, _loadCount);
_market[D1] = 0.0160; // 直改库(无 Invalidate
Fr007FixingCache.NowTicks = () => 1_000_000L + TimeSpan.FromMinutes(1).Ticks + 1; // TTL+1 tick
Assert.IsTrue(Fr007FixingCache.TryGet(D1, out var p));
Assert.AreEqual(0.0160, p, 1e-12, "TTL 过期后应重载并看到直改库的新值");
Assert.AreEqual(2, _loadCount);
}
[TestMethod]
public void TTL窗口内无写入零重载()
{
Fr007FixingCache.NowTicks = () => 1_000_000L;
Fr007FixingCache.TryGet(D1, out _);
Fr007FixingCache.NowTicks = () => 1_000_000L + TimeSpan.FromMinutes(1).Ticks - 1; // TTL-1 tick:稳态窗口
for (int i = 0; i < 10; i++) Fr007FixingCache.TryGet(D1, out _);
Assert.AreEqual(1, _loadCount, "稳态窗口内多次读取零重载(零查询)");
}
[TestMethod]
public void TTL过期重载失败_保留旧快照不抛_且窗口内不重试()
{
Fr007FixingCache.NowTicks = () => 1_000_000L;
Assert.IsTrue(Fr007FixingCache.TryGet(D1, out _), "先成功装载一次");
Assert.AreEqual(1, _loadCount);
Fr007FixingCache.LoadSnapshot = () => { _loadCount++; throw new InvalidOperationException("db down"); };
Fr007FixingCache.NowTicks = () => 1_000_000L + TimeSpan.FromMinutes(1).Ticks + 1; // TTL 过期 → 触发重载 → 失败
// 不抛:历史定盘不可变,命中继续走旧快照(异常会直接 fail 本用例)
Assert.IsTrue(Fr007FixingCache.TryGet(D1, out var p), "重载失败应保留旧快照继续命中");
Assert.AreEqual(0.0142, p, 1e-12, "旧快照值不变");
Assert.AreEqual(2, _loadCount, "失败的重载尝试恰好一次");
Fr007FixingCache.TryGet(D2, out _); // TTL 时钟已被重置:窗口内不再重试(防重试风暴)
Assert.AreEqual(2, _loadCount, "失败后TTL窗口内不得反复重试全表SELECT");
}
[TestMethod]
public void 首次装载失败_miss不抛_窗口内不重试()
{
Fr007FixingCache.LoadSnapshot = () => { _loadCount++; throw new InvalidOperationException("db down"); };
Fr007FixingCache.NowTicks = () => 1_000_000L;
Assert.IsFalse(Fr007FixingCache.TryGet(D1, out _), "装载失败=空快照miss(调用方直查库兜底,新数据该响仍响)");
Assert.AreEqual(1, _loadCount);
Fr007FixingCache.TryGet(D1, out _);
Assert.AreEqual(1, _loadCount, "失败后TTL窗口内不重试");
}
}
}