user_platform_quota_flusher_test.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511
  1. package service
  2. import (
  3. "context"
  4. "errors"
  5. "testing"
  6. "time"
  7. "github.com/Wei-Shaw/sub2api/internal/config"
  8. )
  9. // ---------------------------------------------------------------------------
  10. // Mock: quotaDirtyCache
  11. // ---------------------------------------------------------------------------
  12. type mockQuotaDirtyCache struct {
  13. // popSequence: 第 0 次 Pop 返回 popSequence[0],之后返回 nil(空集)
  14. popSequence [][]UserPlatformQuotaKey
  15. popCallIdx int
  16. // getEntries: BatchGetUserPlatformQuotaCache 返回的 entries(与 keys 对齐)
  17. getEntries []*UserPlatformQuotaCacheEntry
  18. getErr error
  19. // readdCalled: 记录 Readd 收到的 keys(累积所有次调用)
  20. readdCalled [][]UserPlatformQuotaKey
  21. readdErr error
  22. }
  23. func (m *mockQuotaDirtyCache) PopDirtyUserPlatformQuotaKeys(_ context.Context, _ int) ([]UserPlatformQuotaKey, error) {
  24. if m.popCallIdx < len(m.popSequence) {
  25. keys := m.popSequence[m.popCallIdx]
  26. m.popCallIdx++
  27. return keys, nil
  28. }
  29. // 超出序列 → 空集(模拟脏集已清空)
  30. return nil, nil
  31. }
  32. func (m *mockQuotaDirtyCache) ReaddDirtyUserPlatformQuotaKeys(_ context.Context, keys []UserPlatformQuotaKey) error {
  33. m.readdCalled = append(m.readdCalled, keys)
  34. return m.readdErr
  35. }
  36. func (m *mockQuotaDirtyCache) BatchGetUserPlatformQuotaCache(_ context.Context, _ []UserPlatformQuotaKey) ([]*UserPlatformQuotaCacheEntry, error) {
  37. if m.getErr != nil {
  38. return nil, m.getErr
  39. }
  40. return m.getEntries, nil
  41. }
  42. // ---------------------------------------------------------------------------
  43. // Mock: quotaSnapshotWriter
  44. // ---------------------------------------------------------------------------
  45. type mockQuotaSnapshotWriter struct {
  46. receivedSnaps []UserPlatformQuotaSnapshot
  47. returnErr error
  48. }
  49. func (m *mockQuotaSnapshotWriter) BatchSnapshotUsage(_ context.Context, snaps []UserPlatformQuotaSnapshot, _ time.Time) error {
  50. m.receivedSnaps = append(m.receivedSnaps, snaps...)
  51. return m.returnErr
  52. }
  53. // ---------------------------------------------------------------------------
  54. // Helper: 构造窗口起始时间(非 nil)
  55. // ---------------------------------------------------------------------------
  56. func flusherPtrTime(t time.Time) *time.Time { return &t }
  57. func makeEntry(daily, weekly, monthly float64) *UserPlatformQuotaCacheEntry {
  58. now := time.Now().UTC()
  59. return &UserPlatformQuotaCacheEntry{
  60. DailyUsageUSD: daily,
  61. WeeklyUsageUSD: weekly,
  62. MonthlyUsageUSD: monthly,
  63. DailyWindowStart: flusherPtrTime(now),
  64. WeeklyWindowStart: flusherPtrTime(now),
  65. MonthlyWindowStart: flusherPtrTime(now),
  66. }
  67. }
  68. // ---------------------------------------------------------------------------
  69. // newTestFlusher: 直接构造 struct(跳过构造函数,B7 才注入)
  70. // ---------------------------------------------------------------------------
  71. func newTestFlusher(cache quotaDirtyCache, writer quotaSnapshotWriter) *UserPlatformQuotaUsageFlusher {
  72. return &UserPlatformQuotaUsageFlusher{
  73. cache: cache,
  74. quotaRepo: writer,
  75. timingWheel: nil, // 单测不启动 TimingWheel
  76. interval: 5 * time.Second,
  77. batchSize: 100,
  78. flushTimeout: 5 * time.Second,
  79. metrics: &FlusherMetrics{},
  80. }
  81. }
  82. // ---------------------------------------------------------------------------
  83. // 场景 1: PopSnapshotUpsert — 2 key + 2 个含 window 的 entry → writer 收 2 行
  84. // ---------------------------------------------------------------------------
  85. func TestFlusher_PopSnapshotUpsert(t *testing.T) {
  86. keys := []UserPlatformQuotaKey{
  87. {UserID: 1, Platform: "anthropic"},
  88. {UserID: 2, Platform: "openai"},
  89. }
  90. cache := &mockQuotaDirtyCache{
  91. popSequence: [][]UserPlatformQuotaKey{keys}, // 第 1 次返回 keys,之后空
  92. getEntries: []*UserPlatformQuotaCacheEntry{
  93. makeEntry(1.0, 2.0, 3.0),
  94. makeEntry(4.0, 5.0, 6.0),
  95. },
  96. }
  97. writer := &mockQuotaSnapshotWriter{}
  98. f := newTestFlusher(cache, writer)
  99. f.flush()
  100. if len(writer.receivedSnaps) != 2 {
  101. t.Fatalf("expected 2 snaps, got %d", len(writer.receivedSnaps))
  102. }
  103. if f.metrics.FlushBatchSizeTotal.Load() != 2 {
  104. t.Errorf("FlushBatchSizeTotal = %d, want 2", f.metrics.FlushBatchSizeTotal.Load())
  105. }
  106. if f.metrics.FlushSuccessTotal.Load() != 1 {
  107. t.Errorf("FlushSuccessTotal = %d, want 1", f.metrics.FlushSuccessTotal.Load())
  108. }
  109. if f.metrics.FlushErrorTotal.Load() != 0 {
  110. t.Errorf("FlushErrorTotal = %d, want 0", f.metrics.FlushErrorTotal.Load())
  111. }
  112. }
  113. // ---------------------------------------------------------------------------
  114. // 场景 2: MissKeySkipped — 2 key,BatchGet 返回 [entry, nil] → 只刷 1 行,nil 跳过,不 Readd
  115. // ---------------------------------------------------------------------------
  116. func TestFlusher_MissKeySkipped(t *testing.T) {
  117. keys := []UserPlatformQuotaKey{
  118. {UserID: 1, Platform: "anthropic"},
  119. {UserID: 2, Platform: "openai"},
  120. }
  121. cache := &mockQuotaDirtyCache{
  122. popSequence: [][]UserPlatformQuotaKey{keys},
  123. getEntries: []*UserPlatformQuotaCacheEntry{
  124. makeEntry(1.0, 2.0, 3.0),
  125. nil, // MISS
  126. },
  127. }
  128. writer := &mockQuotaSnapshotWriter{}
  129. f := newTestFlusher(cache, writer)
  130. f.flush()
  131. if len(writer.receivedSnaps) != 1 {
  132. t.Fatalf("expected 1 snap, got %d", len(writer.receivedSnaps))
  133. }
  134. if writer.receivedSnaps[0].UserID != 1 {
  135. t.Errorf("expected snap for UserID=1, got %d", writer.receivedSnaps[0].UserID)
  136. }
  137. if len(cache.readdCalled) != 0 {
  138. t.Errorf("Readd should NOT be called on MISS, got %d calls", len(cache.readdCalled))
  139. }
  140. if f.metrics.FlushSuccessTotal.Load() != 1 {
  141. t.Errorf("FlushSuccessTotal = %d, want 1", f.metrics.FlushSuccessTotal.Load())
  142. }
  143. }
  144. // ---------------------------------------------------------------------------
  145. // 场景 3: UpsertFailReadds — writer 返普通 error → keys 被 Readd,FlushErrorTotal=1,DirtyReaddTotal=len
  146. // ---------------------------------------------------------------------------
  147. func TestFlusher_UpsertFailReadds(t *testing.T) {
  148. keys := []UserPlatformQuotaKey{
  149. {UserID: 1, Platform: "anthropic"},
  150. {UserID: 2, Platform: "openai"},
  151. }
  152. cache := &mockQuotaDirtyCache{
  153. popSequence: [][]UserPlatformQuotaKey{keys},
  154. getEntries: []*UserPlatformQuotaCacheEntry{
  155. makeEntry(1.0, 2.0, 3.0),
  156. makeEntry(4.0, 5.0, 6.0),
  157. },
  158. }
  159. writeErr := errors.New("db connection timeout")
  160. writer := &mockQuotaSnapshotWriter{returnErr: writeErr}
  161. f := newTestFlusher(cache, writer)
  162. f.flush()
  163. if f.metrics.FlushErrorTotal.Load() != 1 {
  164. t.Errorf("FlushErrorTotal = %d, want 1", f.metrics.FlushErrorTotal.Load())
  165. }
  166. if len(cache.readdCalled) == 0 {
  167. t.Fatal("Readd should be called after write error")
  168. }
  169. totalReadd := 0
  170. for _, rk := range cache.readdCalled {
  171. totalReadd += len(rk)
  172. }
  173. if totalReadd != len(keys) {
  174. t.Errorf("DirtyReaddTotal (from Readd calls) = %d, want %d", totalReadd, len(keys))
  175. }
  176. if f.metrics.DirtyReaddTotal.Load() != int64(len(keys)) {
  177. t.Errorf("DirtyReaddTotal metric = %d, want %d", f.metrics.DirtyReaddTotal.Load(), len(keys))
  178. }
  179. if f.metrics.FlushSuccessTotal.Load() != 0 {
  180. t.Errorf("FlushSuccessTotal = %d, want 0", f.metrics.FlushSuccessTotal.Load())
  181. }
  182. }
  183. // ---------------------------------------------------------------------------
  184. // 场景 4: FKViolationDropsNoReadd — writer 返 ErrUserPlatformQuotaFKViolation → 不 Readd,FlushFKViolationTotal=1
  185. // ---------------------------------------------------------------------------
  186. func TestFlusher_FKViolationDropsNoReadd(t *testing.T) {
  187. keys := []UserPlatformQuotaKey{
  188. {UserID: 999, Platform: "anthropic"},
  189. }
  190. cache := &mockQuotaDirtyCache{
  191. popSequence: [][]UserPlatformQuotaKey{keys},
  192. getEntries: []*UserPlatformQuotaCacheEntry{
  193. makeEntry(1.0, 2.0, 3.0),
  194. },
  195. }
  196. writer := &mockQuotaSnapshotWriter{returnErr: ErrUserPlatformQuotaFKViolation}
  197. f := newTestFlusher(cache, writer)
  198. f.flush()
  199. if f.metrics.FlushFKViolationTotal.Load() != 1 {
  200. t.Errorf("FlushFKViolationTotal = %d, want 1", f.metrics.FlushFKViolationTotal.Load())
  201. }
  202. if f.metrics.FlushErrorTotal.Load() != 1 {
  203. t.Errorf("FlushErrorTotal = %d, want 1", f.metrics.FlushErrorTotal.Load())
  204. }
  205. if len(cache.readdCalled) != 0 {
  206. t.Errorf("Readd should NOT be called for FK violation (drop), got %d calls", len(cache.readdCalled))
  207. }
  208. if f.metrics.DirtyReaddTotal.Load() != 0 {
  209. t.Errorf("DirtyReaddTotal = %d, want 0 (FK violation drops)", f.metrics.DirtyReaddTotal.Load())
  210. }
  211. }
  212. // ---------------------------------------------------------------------------
  213. // 场景 5: NilSafe — var f *UserPlatformQuotaUsageFlusher; f.flush(); f.Stop() 不 panic
  214. // ---------------------------------------------------------------------------
  215. func TestFlusher_NilSafe(t *testing.T) {
  216. var f *UserPlatformQuotaUsageFlusher
  217. // 下面两行不应 panic
  218. f.flush()
  219. f.Stop()
  220. }
  221. // ---------------------------------------------------------------------------
  222. // 场景 6: StopPreventsFlush — stopped=true 后 tick() 不调 flush(writer 没收到 snaps)
  223. // ---------------------------------------------------------------------------
  224. func TestFlusher_StopPreventsFlush(t *testing.T) {
  225. keys := []UserPlatformQuotaKey{
  226. {UserID: 1, Platform: "anthropic"},
  227. }
  228. cache := &mockQuotaDirtyCache{
  229. popSequence: [][]UserPlatformQuotaKey{keys},
  230. getEntries: []*UserPlatformQuotaCacheEntry{
  231. makeEntry(1.0, 2.0, 3.0),
  232. },
  233. }
  234. writer := &mockQuotaSnapshotWriter{}
  235. f := newTestFlusher(cache, writer)
  236. // 标记为已停止
  237. f.stopped.Store(true)
  238. // tick 应该直接返回,不触发 flush
  239. f.tick()
  240. if len(writer.receivedSnaps) != 0 {
  241. t.Errorf("expected 0 snaps after stop, got %d", len(writer.receivedSnaps))
  242. }
  243. if cache.popCallIdx != 0 {
  244. t.Errorf("Pop should not be called after stop, popCallIdx = %d", cache.popCallIdx)
  245. }
  246. }
  247. // ---------------------------------------------------------------------------
  248. // 场景 B13-1: ZeroPercentCompany — 0% 公司脏集恒空,flusher 空跑无 DB 写
  249. //
  250. // 模拟几乎没有用户配置 quota limit 的公司:脏集始终为空(popSequence 为空切片),
  251. // Pop 每次返回空集。flush() 应早退,不写 DB、不计成功、不 Readd。
  252. // ---------------------------------------------------------------------------
  253. func TestScenario_ZeroPercentCompany(t *testing.T) {
  254. cache := &mockQuotaDirtyCache{
  255. // popSequence 为空 → Pop 超出序列 → 始终返回 nil(空集)
  256. popSequence: [][]UserPlatformQuotaKey{},
  257. }
  258. writer := &mockQuotaSnapshotWriter{}
  259. f := newTestFlusher(cache, writer)
  260. f.flush()
  261. if len(writer.receivedSnaps) != 0 {
  262. t.Errorf("0%% company: expected 0 snaps, got %d", len(writer.receivedSnaps))
  263. }
  264. if f.metrics.FlushBatchSizeTotal.Load() != 0 {
  265. t.Errorf("0%% company: FlushBatchSizeTotal = %d, want 0", f.metrics.FlushBatchSizeTotal.Load())
  266. }
  267. if f.metrics.FlushSuccessTotal.Load() != 0 {
  268. t.Errorf("0%% company: FlushSuccessTotal = %d, want 0 (empty-set early return)", f.metrics.FlushSuccessTotal.Load())
  269. }
  270. if f.metrics.FlushErrorTotal.Load() != 0 {
  271. t.Errorf("0%% company: FlushErrorTotal = %d, want 0", f.metrics.FlushErrorTotal.Load())
  272. }
  273. if len(cache.readdCalled) != 0 {
  274. t.Errorf("0%% company: Readd should never be called, got %d calls", len(cache.readdCalled))
  275. }
  276. }
  277. // ---------------------------------------------------------------------------
  278. // P1: IntervalFallback — flush_interval_ms ≤0 时回退 2s;正常值保留
  279. // ---------------------------------------------------------------------------
  280. func TestNewUserPlatformQuotaUsageFlusher_IntervalFallback(t *testing.T) {
  281. cases := []struct {
  282. name string
  283. inMs int
  284. wantDu time.Duration
  285. }{
  286. {"零值回退 2s", 0, 2 * time.Second},
  287. {"负数回退 2s", -100, 2 * time.Second},
  288. {"正常 2000ms 保留", 2000, 2 * time.Second},
  289. {"正常 500ms 保留", 500, 500 * time.Millisecond},
  290. }
  291. for _, tc := range cases {
  292. t.Run(tc.name, func(t *testing.T) {
  293. cfg := &config.Config{}
  294. cfg.Database.UserPlatformQuotaFlushIntervalMs = tc.inMs
  295. f := NewUserPlatformQuotaUsageFlusher(cfg, nil, nil, nil)
  296. if f.interval != tc.wantDu {
  297. t.Fatalf("interval = %v, want %v", f.interval, tc.wantDu)
  298. }
  299. })
  300. }
  301. }
  302. // ---------------------------------------------------------------------------
  303. // P1: EnabledField — flusher_enabled 配置正确写入 f.enabled
  304. // ---------------------------------------------------------------------------
  305. func TestNewUserPlatformQuotaUsageFlusher_EnabledField(t *testing.T) {
  306. for _, enabled := range []bool{true, false} {
  307. cfg := &config.Config{}
  308. cfg.Database.UserPlatformQuotaFlusherEnabled = enabled
  309. cfg.Database.UserPlatformQuotaFlushIntervalMs = 500
  310. f := NewUserPlatformQuotaUsageFlusher(cfg, nil, nil, nil)
  311. if f.enabled != enabled {
  312. t.Errorf("enabled = %v, want %v", f.enabled, enabled)
  313. }
  314. }
  315. }
  316. // ---------------------------------------------------------------------------
  317. // P2: ReaddFailCounts — BatchGet 失败 + Readd 失败 → DirtyLostTotal 增、DirtyReaddTotal 不变
  318. // BatchGet 失败 + Readd 成功 → DirtyReaddTotal 增、DirtyLostTotal 不变
  319. // ---------------------------------------------------------------------------
  320. func TestFlusher_ReaddFailCounts(t *testing.T) {
  321. keys := []UserPlatformQuotaKey{
  322. {UserID: 10, Platform: "anthropic"},
  323. {UserID: 11, Platform: "openai"},
  324. }
  325. t.Run("Readd 失败计 DirtyLostTotal", func(t *testing.T) {
  326. cache := &mockQuotaDirtyCache{
  327. popSequence: [][]UserPlatformQuotaKey{keys},
  328. getErr: errors.New("redis timeout"), // 触发 BatchGet 失败路径
  329. readdErr: errors.New("redis connection refused"), // Readd 也失败
  330. }
  331. f := newTestFlusher(cache, &mockQuotaSnapshotWriter{})
  332. f.flush()
  333. if f.metrics.DirtyLostTotal.Load() != int64(len(keys)) {
  334. t.Errorf("DirtyLostTotal = %d, want %d", f.metrics.DirtyLostTotal.Load(), len(keys))
  335. }
  336. if f.metrics.DirtyReaddTotal.Load() != 0 {
  337. t.Errorf("DirtyReaddTotal = %d, want 0 (Readd 失败不应计入)", f.metrics.DirtyReaddTotal.Load())
  338. }
  339. })
  340. t.Run("Readd 成功计 DirtyReaddTotal", func(t *testing.T) {
  341. cache := &mockQuotaDirtyCache{
  342. popSequence: [][]UserPlatformQuotaKey{keys},
  343. getErr: errors.New("redis timeout"), // 触发 BatchGet 失败路径
  344. readdErr: nil, // Readd 成功
  345. }
  346. f := newTestFlusher(cache, &mockQuotaSnapshotWriter{})
  347. f.flush()
  348. if f.metrics.DirtyReaddTotal.Load() != int64(len(keys)) {
  349. t.Errorf("DirtyReaddTotal = %d, want %d", f.metrics.DirtyReaddTotal.Load(), len(keys))
  350. }
  351. if f.metrics.DirtyLostTotal.Load() != 0 {
  352. t.Errorf("DirtyLostTotal = %d, want 0 (Readd 成功不应计 lost)", f.metrics.DirtyLostTotal.Load())
  353. }
  354. })
  355. }
  356. // ---------------------------------------------------------------------------
  357. // ClampsBatchSize — NewUserPlatformQuotaUsageFlusher 构造时按
  358. // [defaultFlushBatchSize, maxFlushBatchSize] 区间 clamp batchSize
  359. // ---------------------------------------------------------------------------
  360. func TestNewUserPlatformQuotaUsageFlusher_ClampsBatchSize(t *testing.T) {
  361. cases := []struct {
  362. name string
  363. in int
  364. want int
  365. }{
  366. {"超上限被 clamp", 7000, maxFlushBatchSize},
  367. {"恰好上限保留", maxFlushBatchSize, maxFlushBatchSize},
  368. {"零回退默认", 0, defaultFlushBatchSize},
  369. {"负数回退默认", -5, defaultFlushBatchSize},
  370. {"正常值保留", 500, 500},
  371. }
  372. for _, tc := range cases {
  373. t.Run(tc.name, func(t *testing.T) {
  374. cfg := &config.Config{}
  375. cfg.Database.UserPlatformQuotaFlushBatchSize = tc.in
  376. f := NewUserPlatformQuotaUsageFlusher(cfg, nil, nil, nil)
  377. if f.batchSize != tc.want {
  378. t.Fatalf("batchSize = %d, want %d", f.batchSize, tc.want)
  379. }
  380. })
  381. }
  382. }
  383. // ---------------------------------------------------------------------------
  384. // 场景 B13-2: NinetyPercentCompany — 90% 公司大量用户配 limit,一批 5 key 批量刷库
  385. //
  386. // 模拟大量用户配置了 quota limit 的公司:脏集第一次 Pop 返回 5 个不同用户的 key,
  387. // 之后返回空集(避免 flush 循环)。flush() 应构造 5 条 snapshot 写入 DB,
  388. // 断言绝对值语义(snap 的 DailyUsageUSD 等于 entry 的值)、metrics 正确、不 Readd。
  389. // ---------------------------------------------------------------------------
  390. func TestScenario_NinetyPercentCompany(t *testing.T) {
  391. keys := []UserPlatformQuotaKey{
  392. {UserID: 101, Platform: "anthropic"},
  393. {UserID: 102, Platform: "anthropic"},
  394. {UserID: 103, Platform: "openai"},
  395. {UserID: 104, Platform: "openai"},
  396. {UserID: 105, Platform: "anthropic"},
  397. }
  398. entries := []*UserPlatformQuotaCacheEntry{
  399. makeEntry(1.1, 2.2, 3.3),
  400. makeEntry(4.4, 5.5, 6.6),
  401. makeEntry(7.7, 8.8, 9.9),
  402. makeEntry(0.5, 1.0, 1.5),
  403. makeEntry(10.0, 20.0, 30.0),
  404. }
  405. cache := &mockQuotaDirtyCache{
  406. // 第 1 次 Pop 返回 5 keys,之后返回空集(防止 flush 无限循环)
  407. popSequence: [][]UserPlatformQuotaKey{keys},
  408. getEntries: entries,
  409. }
  410. writer := &mockQuotaSnapshotWriter{}
  411. f := newTestFlusher(cache, writer)
  412. f.flush()
  413. // 应收到 5 条 snapshot
  414. if len(writer.receivedSnaps) != 5 {
  415. t.Fatalf("90%% company: expected 5 snaps, got %d", len(writer.receivedSnaps))
  416. }
  417. // 验证绝对值语义:第 1 条 snap 的各窗口 usage 应等于 entries[0] 的值
  418. snap0 := writer.receivedSnaps[0]
  419. entry0 := entries[0]
  420. if snap0.DailyUsageUSD != entry0.DailyUsageUSD {
  421. t.Errorf("snap[0].DailyUsageUSD = %v, want %v", snap0.DailyUsageUSD, entry0.DailyUsageUSD)
  422. }
  423. if snap0.WeeklyUsageUSD != entry0.WeeklyUsageUSD {
  424. t.Errorf("snap[0].WeeklyUsageUSD = %v, want %v", snap0.WeeklyUsageUSD, entry0.WeeklyUsageUSD)
  425. }
  426. if snap0.MonthlyUsageUSD != entry0.MonthlyUsageUSD {
  427. t.Errorf("snap[0].MonthlyUsageUSD = %v, want %v", snap0.MonthlyUsageUSD, entry0.MonthlyUsageUSD)
  428. }
  429. // FlushBatchSizeTotal 应为 5(本批 keys 数量)
  430. if f.metrics.FlushBatchSizeTotal.Load() != 5 {
  431. t.Errorf("90%% company: FlushBatchSizeTotal = %d, want 5", f.metrics.FlushBatchSizeTotal.Load())
  432. }
  433. // FlushSuccessTotal 应为 1(1 个批次写成功)
  434. if f.metrics.FlushSuccessTotal.Load() != 1 {
  435. t.Errorf("90%% company: FlushSuccessTotal = %d, want 1", f.metrics.FlushSuccessTotal.Load())
  436. }
  437. // 无错误、无 Readd
  438. if f.metrics.FlushErrorTotal.Load() != 0 {
  439. t.Errorf("90%% company: FlushErrorTotal = %d, want 0", f.metrics.FlushErrorTotal.Load())
  440. }
  441. if f.metrics.DirtyReaddTotal.Load() != 0 {
  442. t.Errorf("90%% company: DirtyReaddTotal = %d, want 0", f.metrics.DirtyReaddTotal.Load())
  443. }
  444. if len(cache.readdCalled) != 0 {
  445. t.Errorf("90%% company: Readd should not be called, got %d calls", len(cache.readdCalled))
  446. }
  447. }