数据形态:每工作日一行、发布后不变、全历史仅数千行 → 整表快照优于逐键缓存。
① Fr007FixingCache:首次访问批量SELECT全历史进字典,此后读取O(1)零查询;
快照以引用替换发布(Volatile),读侧无撕裂,重载加锁去抖;
② 负缓存防线:miss(当日~11:15未发布/补录历史)永不进快照,TryGetPrice 回退直查库——
否则发布后TTL窗口内仍查不到,上午平仓会一直用回退利率;
③ 写侧版本失效:SwapFlowService 新增/修改/删除 FR007 后 Invalidate,下次访问立即
整表重载不等TTL;其余通用写入口(导入/复制/合成/直改库)靠TTL兜底,可见性≤1分钟;
④ TryGetPrice 仅 FR007 走缓存路由(全仓生产调用方仅 SwapDealService.TryGetFloatRate
与 Fr007IndexFixer 两处,均定盘链),其他标的照旧直查,零影响;
⑤ YLErpDAL 增加 InternalsVisibleTo("UnitTestProject"),时钟/装载器测试接缝保持 internal。
验证:Fr007FixingCacheTest 5/5(预载命中/miss不缓存/写侧立即生效/TTL过期重载/稳态零重载);
GLMS20260817Fr007UnwindMorningTest 等内存套件 55/55 全绿。
106 lines
4.6 KiB
C#
106 lines
4.6 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, "稳态窗口内多次读取零重载(零查询)");
|
||
}
|
||
}
|
||
}
|