- EodPriceQueryService.TryGetPrice: FR007 快照 miss 直查库命中后 Invalidate, 下次访问整表重载回 O(1),否则该键 TTL 窗口内每次读取都退化为点查 - 移除点查多余的 OrderByDescending(主键等值查询取首行即可) - Fr007FixingCache.EnsureFresh: 重载失败(DB抖动)保留旧快照不抛并重置TTL时钟 ——历史定盘不可变旧值依旧正确,同时防重试风暴;版本失效路径不受TTL掩盖 - 补两测试: 重载失败保快照窗口内不重试 / 首次装载失败miss不抛窗口内不重试
138 lines
6.3 KiB
C#
138 lines
6.3 KiB
C#
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窗口内不重试");
|
||
}
|
||
}
|
||
}
|