多 Worker 下的定期 Job:以同步 External API 資料為例

多 Worker 下的定期 Job:以同步 External API 資料為例

Problem

FastAPI 有多個 replica / worker,而每個 worker 都會啟動相同的定期 Job。

如果沒有額外協調,同一個 Job 可能會被多個 worker 重複執行。

以下以「定期從 External API 取得資料並寫入 Redis」為例, 目標是: 不論有多少 worker,每個 INTERVAL 最多只執行一次 Job。


Core Method

INTERVAL = 15
POLL = 1
MAX_STALE = 300
  • INTERVAL:最短 refresh 間隔
  • POLL:worker 多久嘗試搶一次 lock
  • MAX_STALE:舊值最多保留多久

核心:

if await r.set(LOCK, owner, nx=True, ex=INTERVAL):
    resp = await client.get(EXT_URL)
    resp.raise_for_status()
    await r.set(KEY, json.dumps(resp.json()["value"]), ex=MAX_STALE)

await asyncio.sleep(POLL)

其中:

SET lock owner NX EX 15
  • NX:lock 不存在時才成功 → 同一時間只有一個 winner
  • EX 15:lock 15 秒後自動過期 → 控制下一次 refresh 最早何時發生

因此這個 lock 比較像 distributed cooldown,不是一般做完就立即 release 的 mutex。

  sequenceDiagram
    participant A as Worker A
    participant B as Worker B
    participant R as Redis
    participant E as External API

    A->>R: SET lock A NX EX INTERVAL
    R-->>A: OK
    A->>E: Refresh
    A->>R: Update cached value

    Note over A,R: INTERVAL<br/>lock remains active

    B->>R: SET lock B NX EX INTERVAL
    R-->>B: nil

    Note over A,B: Every POLL, workers retry

    Note over R: INTERVAL elapsed → lock expires

    B->>R: SET lock B NX EX INTERVAL
    R-->>B: OK
    B->>E: Next refresh

實際 refresh 間隔約為:

INTERVAL ~ INTERVAL + POLL

Stale Value

成功 refresh 後,cache key 設定 MAX_STALE TTL。

API 正常
→ 持續更新 cache

API 暫時失敗
→ 繼續提供舊值

失敗超過 MAX_STALE
→ key 過期
→ 回 503

Limitation

SET NX EX 可以讓共用同一個 Redis / lock key 的 workers 在正常情況下,每個 INTERVAL 最多只有一個取得執行權,但不保證 exactly-once execution。

若 Job 可能超過 lock TTL,或操作具有副作用,應搭配 idempotency 或更嚴格的 coordination 機制。

References

Last updated on