|
22 | 22 | * opens a file, so a late-joining task (the normal case under autoscaling) loads the current shared |
23 | 23 | * state before its first client syncs. Catch-up + tail are seamless: the tailer resumes from the |
24 | 24 | * exact id catch-up stopped at. |
25 | | - * - {@link shouldSeed} coordinates the one-time seed with a Redis lock AND an empty-stream check, so |
26 | | - * exactly one task ever writes the seed cluster-wide (the fix for split-brain). |
| 25 | + * - The one-time seed is written via the atomic {@link seedIfEmpty} (append-iff-empty in one Redis |
| 26 | + * step), so exactly one task ever writes the seed cluster-wide (the fix for split-brain) — even if two |
| 27 | + * tasks race. {@link shouldSeed} is a Redis lock + empty-stream check layered on top ONLY as an |
| 28 | + * efficiency gate (so tasks don't all run the seed fetch); correctness does not depend on it. |
27 | 29 | * |
28 | 30 | * When `REDIS_URL` is unset (single-pod dev) the store is DISABLED and every method degrades to the |
29 | 31 | * relay's original single-replica behavior: seed locally, no stream, no tailer. |
@@ -364,17 +366,20 @@ export class FileDocStore { |
364 | 366 | } |
365 | 367 |
|
366 | 368 | /** |
367 | | - * Decide whether THIS task should build and write the file's one-time seed. Returns a lock TOKEN only |
368 | | - * when the shared stream is genuinely empty AND this task wins the seed lock — so exactly one task |
369 | | - * across the cluster ever seeds a file, even if several open it at once (the fix for split-brain |
370 | | - * seeding). `null` otherwise. Release the token with {@link releaseSeedLock}. Disabled → always a |
371 | | - * token (single-replica: seed locally). |
| 369 | + * Decide whether THIS task should run the (expensive) seed fetch + write for a file. Returns a lock |
| 370 | + * TOKEN when this task wins the seed lock and the stream still looks empty; `null` otherwise. This is an |
| 371 | + * EFFICIENCY gate — it stops every task that opens the file at once from each fetching the seed. It does |
| 372 | + * NOT by itself guarantee a single seed: exactly-once is enforced by the atomic {@link seedIfEmpty} the |
| 373 | + * token-holder then calls (the split-brain guard), so a lock that expires mid-seed cannot cause a |
| 374 | + * double-seed. Release the token with {@link releaseSeedLock}. Disabled → always a token (single-replica: |
| 375 | + * seed locally). |
372 | 376 | */ |
373 | 377 | async shouldSeed(name: string): Promise<string | null> { |
374 | 378 | const token = await this.acquireLock(`${SEED_LOCK_PREFIX}${name}`, SEED_LOCK_TTL_MS) |
375 | 379 | if (!token || token === DISABLED_LOCK_TOKEN) return token |
376 | 380 | // The lock could be free yet the stream already seeded (a prior holder seeded then its lock |
377 | | - // expired). Re-check under the lock so we never write a SECOND seed on top of an existing one. |
| 381 | + // expired). Re-check so we skip the redundant seed fetch — the atomic seedIfEmpty would no-op anyway, |
| 382 | + // but this avoids the wasted app round-trip. |
378 | 383 | if (await this.streamHasContent(name)) { |
379 | 384 | await this.releaseSeedLock(name, token) |
380 | 385 | return null |
|
0 commit comments