等待非同步 Log Flush 完成的穩定偵測法

面對 async log writer(如 OTEL BatchProcessor),固定 sleep 要嘛太短導致漏讀,要嘛太長浪費時間。正確做法是 poll 直到讀到的筆數連續穩定,代表 flush 已完成。


問題情境

OTEL BatchProcessor 不立即 flush,而是分批寫入:

1
2
3
4
5
6
Agent 執行完畢
↓ 50ms
BatchProcessor flush 第一批(3 筆)
↓ 200ms(burst gap)
BatchProcessor flush 第二批(2 筆)
↓ 完成

如果只 sleep 固定時間:

  • Sleep 100ms → 只讀到 3 筆,以為完成,漏掉 2 筆
  • Sleep 600ms → 總是等太久,即使 100ms 就完成了

穩定偵測邏輯

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func waitForLogStable(logPath string, maxWait time.Duration) error {
deadline := time.Now().Add(maxWait)
lastCount := -1
stableRounds := 0

for time.Now().Before(deadline) {
entries := readLogEntries(logPath)
count := len(entries)

if count > 0 && count == lastCount {
stableRounds++
if stableRounds >= 2 {
return nil // 連續 3 次讀到相同筆數,視為 flush 完成
}
} else {
stableRounds = 0
lastCount = count
}

time.Sleep(200 * time.Millisecond)
}

return fmt.Errorf("log did not stabilize within %s (last count: %d)", maxWait, lastCount)
}

時序示意:

1
2
3
4
5
6
t=0ms    count=0  → stableRounds=0
t=200ms count=3 → stableRounds=0(新增)
t=400ms count=3 → stableRounds=1
t=600ms count=5 → stableRounds=0(burst gap 後又有新增)
t=800ms count=5 → stableRounds=1
t=1000ms count=5 → stableRounds=2 → return nil ✓

為何需要連續 3 次(stableRounds >= 2)

只要求 2 次(stableRounds >= 1)可能在 burst-gap 中間提早返回:

1
2
3
t=200ms  count=3  → stableRounds=0
t=400ms count=3 → stableRounds=1 → 若只要求 1 次,此時 return!
↑ 但 t=600ms 還有第二批 5 筆到來,已漏讀

連續 3 次相同表示已等過至少 2 個 poll interval(400ms),足以覆蓋一般 burst-gap。若 BatchProcessor 間隔更長,可調整 stableRounds 閾值或 time.Sleep 間距。


應用場景

此模式適用於任何非同步 log consumer:

場景 說明
OTEL BatchSpanProcessor 本文情境,等待 trace flush
Kafka consumer lag poll 直到 consumer group offset 穩定
File tail(tail -f 等待寫入程序完成後再解析
DB async replication 等待 replica 追上 primary
非同步 job queue poll 直到 queue depth 不再變化