Skip to content

Commit f6a9eba

Browse files
committed
fix(news): avoid false digest failure after build
1 parent bb1f985 commit f6a9eba

2 files changed

Lines changed: 45 additions & 10 deletions

File tree

server/worldmonitor/news/v1/list-feed-digest.ts

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,19 @@ async function settleBeforeDeadline<T>(
161161
}
162162
}
163163

164+
function finishSuccessfulDigestAttempt(
165+
variant: string,
166+
lang: string,
167+
slot: ReturnType<typeof beginDigestAttempt>,
168+
): null {
169+
completeDigestAttempt(variant, lang, slot);
170+
// Publication remains inside the shared in-flight promise. Clear the build
171+
// identity before that phase starts so an outer response timeout cannot
172+
// relabel a completed build as `build-error` and overwrite its canonical
173+
// publication with a negative sentinel.
174+
return null;
175+
}
176+
164177
type DigestFeedEntry = { attemptId: string; category: string; feed: ServerFeed };
165178

166179
/**
@@ -2042,7 +2055,7 @@ export async function listFeedDigest(
20422055
const result = await buildDigest(variant, lang, (await revokedPromise).urls);
20432056
const totalItems = Object.values(result.categories).reduce((sum, b) => sum + b.items.length, 0);
20442057
if (totalItems > 0) {
2045-
completeDigestAttempt(variant, lang, leaderSlot);
2058+
leaderSlot = finishSuccessfulDigestAttempt(variant, lang, leaderSlot);
20462059
return result;
20472060
}
20482061
leaderFailure = publishFailedAttempt(
@@ -2056,15 +2069,17 @@ export async function listFeedDigest(
20562069
completeDigestAttempt(variant, lang, leaderSlot);
20572070
return null;
20582071
} catch (err) {
2059-
leaderFailure = publishFailedAttempt(
2060-
variant,
2061-
lang,
2062-
digestCacheKey,
2063-
leaderSlot,
2064-
'build-error',
2065-
30,
2066-
);
2067-
completeDigestAttempt(variant, lang, leaderSlot);
2072+
if (leaderSlot) {
2073+
leaderFailure = publishFailedAttempt(
2074+
variant,
2075+
lang,
2076+
digestCacheKey,
2077+
leaderSlot,
2078+
'build-error',
2079+
30,
2080+
);
2081+
completeDigestAttempt(variant, lang, leaderSlot);
2082+
}
20682083
throw err;
20692084
}
20702085
},
@@ -3170,6 +3185,7 @@ export const __testing__ = {
31703185
fallbackDigestCache,
31713186
markFallbackCoverageStale,
31723187
settleBeforeDeadline,
3188+
finishSuccessfulDigestAttempt,
31733189
lastGoodStoreTesting,
31743190
beginDigestAttempt,
31753191
completeDigestAttempt,

tests/digest-lastgood.test.mts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1017,6 +1017,25 @@ describe('durable last-good wiring (#7084)', () => {
10171017
assert.ok(Date.now() - started < 2_000, 'the absolute deadline must bound every unresolved tail');
10181018
});
10191019

1020+
it('a completed build is not relabeled as failed when publication crosses the response deadline', async () => {
1021+
reset();
1022+
let slot = mod.__testing__.beginDigestAttempt('full', 'en', new Date(NOW).toISOString());
1023+
slot = mod.__testing__.finishSuccessfulDigestAttempt('full', 'en', slot);
1024+
1025+
const publication = await mod.__testing__.settleBeforeDeadline(
1026+
new Promise(() => {}),
1027+
Date.now() + 20,
1028+
'unavailable',
1029+
);
1030+
assert.equal(publication, 'unavailable');
1031+
assert.equal(slot, null, 'successful build completion must clear the pending leader identity');
1032+
assert.equal(
1033+
stub.transactionCalls.length,
1034+
0,
1035+
'a publication timeout must not write a build-error attempt or canonical sentinel',
1036+
);
1037+
});
1038+
10201039
it('fresh and cached serving fail CLOSED when revocations are unreadable', async () => {
10211040
reset();
10221041
stub.pipeline = async () => [{ error: 'ERR' }];

0 commit comments

Comments
 (0)