Skip to content

Commit 8ef8dba

Browse files
ralyodioclaude
andcommitted
fix: seed rings without timing out on the biggest topic, and retry a failed seed in minutes
The first production seed died at the request deadline on the largest topic: the candidate query range-scanned every keyword row of the topic, joined, grouped and sorted them, and only then took a hundred; the size query counted the same join in full. Now both walk feeds in admission order with a primary-key probe into feed_keywords and stop at the rows they need (the size count stops at the minimum the caller asks about). A topic that still fails is counted and reported and the pass moves on, and the poller retries a failed seed ten minutes later instead of at the next six-hour mark, which is what the first deploy would have waited. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XYae2mH3khdwiXUVzcVMDw
1 parent accc9de commit 8ef8dba

4 files changed

Lines changed: 58 additions & 30 deletions

File tree

‎apps/poller/src/index.js‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -795,11 +795,18 @@ async function ringTick() {
795795

796796
try {
797797
if (Date.now() - lastRingSeed >= ringSeedMs) {
798+
// Stamped only once the pass has run: a pass that throws is retried
799+
// ten minutes on, not six hours on, which is what the first deploy
800+
// would have waited after the largest topic timed out.
801+
const seeded = await seedTopRings(db, {
802+
topics: ringTopics,
803+
minMembers: 5,
804+
onError: (topic, err) => log('ring-seed-error', { topic, message: String(err?.message ?? err) }),
805+
});
798806
lastRingSeed = Date.now();
799-
const seeded = await seedTopRings(db, { topics: ringTopics, minMembers: 5 });
800807
// Logged only when something changed: once the rings exist, a line
801808
// every six hours saying "0 added" is not a log.
802-
if (seeded.created || seeded.added) log('rings-seeded', seeded);
809+
if (seeded.created || seeded.added || seeded.failed) log('rings-seeded', seeded);
803810
}
804811

805812
const result = await verifyRingMembers(db, {
@@ -814,6 +821,8 @@ async function ringTick() {
814821
if (result.checked) log('rings', result);
815822
} catch (err) {
816823
log('rings-error', { message: String(err?.message ?? err) });
824+
// Try the seed again soon rather than at the next six-hour mark.
825+
lastRingSeed = Math.min(lastRingSeed, Date.now() - ringSeedMs + 10 * 60 * 1000);
817826
} finally {
818827
ringing = false;
819828
}

‎packages/db/src/webrings.js‎

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -224,18 +224,19 @@ export async function memberBySlug(db, ringSlug, memberSlug) {
224224
* @returns {Promise<Array<{ id: string, slug: string, site_url: string, created_at: string }>>}
225225
*/
226226
export async function topicRingCandidates(db, topicSlug, limit) {
227-
// Driven from feed_keywords, whose (slug, count) index makes this a range
228-
// scan of one topic's rows, rather than from feeds, where the same filter
229-
// is a walk of the whole directory with a subquery per row. Grouped on the
230-
// feed because a feed can carry several spellings of one slug.
227+
// Walked from feeds in admission order (feeds_created_idx) with a primary
228+
// key probe into feed_keywords per row, so the statement stops the moment
229+
// `limit` members are found. The other way round, a range scan of the
230+
// topic's keyword rows joined, grouped and sorted before the limit applies,
231+
// is a full pass over a big topic: the first seed in production timed out
232+
// on the largest topic before anything was written. A small topic walks
233+
// the directory with a point lookup per feed, which is the cheap case.
231234
const { rows } = await db.execute({
232235
sql: `select f.id, f.slug, f.site_url, f.created_at
233-
from feed_keywords k
234-
join feeds f on f.id = k.feed_id
235-
where k.slug = ?
236-
and f.status = 'active'
236+
from feeds f
237+
where f.status = 'active'
237238
and f.site_url is not null and f.site_url <> ''
238-
group by f.id
239+
and exists (select 1 from feed_keywords k where k.feed_id = f.id and k.slug = ?)
239240
order by f.created_at asc, f.id asc
240241
limit ?`,
241242
args: [topicSlug, limit],
@@ -352,15 +353,21 @@ export async function topRingTopics(db, opts = {}) {
352353
* @param {string} topicSlug
353354
* @returns {Promise<number>}
354355
*/
355-
export async function topicRingSize(db, topicSlug) {
356+
export async function topicRingSize(db, topicSlug, opts = {}) {
357+
// `cap` stops the count once it is high enough to answer the caller's
358+
// question ("at least five?"), so the biggest topics cost the same as the
359+
// smallest. Without it the count is exact.
360+
const cap = Number(opts.cap) > 0 ? Number(opts.cap) : null;
356361
const { rows } = await db.execute({
357-
sql: `select count(distinct f.id) as n
358-
from feed_keywords k
359-
join feeds f on f.id = k.feed_id
360-
where k.slug = ?
361-
and f.status = 'active'
362-
and f.site_url is not null and f.site_url <> ''`,
363-
args: [topicSlug],
362+
sql: `select count(*) as n from (
363+
select f.id
364+
from feeds f
365+
where f.status = 'active'
366+
and f.site_url is not null and f.site_url <> ''
367+
and exists (select 1 from feed_keywords k where k.feed_id = f.id and k.slug = ?)
368+
${cap ? 'limit ?' : ''}
369+
)`,
370+
args: cap ? [topicSlug, cap] : [topicSlug],
364371
});
365372
return Number(rows[0]?.n ?? 0);
366373
}

‎packages/db/test/webrings.test.js‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,8 @@ test('the top topics are the most covered ones with enough feeds to ring', async
128128
assert.equal(await webrings.topicRingSize(db, 'physics'), 5, 'the feeds that can link, distinct');
129129
assert.equal(await webrings.topicRingSize(db, 'chemistry'), 1);
130130
assert.equal(await webrings.topicRingSize(db, 'nothing'), 0);
131+
assert.equal(await webrings.topicRingSize(db, 'physics', { cap: 3 }), 3, 'a capped count stops at the cap');
132+
assert.equal(await webrings.topicRingSize(db, 'chemistry', { cap: 3 }), 1, 'and is exact under it');
131133
});
132134

133135
test('a check records status and stamp, and a descriptor sets made_by', async () => {

‎packages/ingest/src/webring.js‎

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -337,9 +337,14 @@ export async function verifyRingMembers(db, opts) {
337337
* eligible feeds fall under `minMembers` is skipped rather than made into
338338
* a ring of two, since a ring needs somewhere to hop to.
339339
*
340+
* A topic whose queries fail (the first production seed hit the request
341+
* deadline on the largest topic) is counted as failed and reported through
342+
* `onError`, and the pass goes on to the next one; one slow topic must not
343+
* cost every other ring its seed.
344+
*
340345
* @param {import('@libsql/client').Client} db
341-
* @param {{ topics?: number, minMembers?: number, limit?: number }} [opts]
342-
* @returns {Promise<{ rings: number, created: number, added: number, skipped: number }>}
346+
* @param {{ topics?: number, minMembers?: number, limit?: number, onError?: ((topic: string, err: unknown) => void)|null }} [opts]
347+
* @returns {Promise<{ rings: number, created: number, added: number, skipped: number, failed: number }>}
343348
*/
344349
export async function seedTopRings(db, opts = {}) {
345350
const topics = Math.max(1, Number(opts.topics ?? 20) || 20);
@@ -350,19 +355,24 @@ export async function seedTopRings(db, opts = {}) {
350355
// on a topic, and a topic can be well covered by feeds that have no site
351356
// to link from or that the crawler has given up on.
352357
const candidates = await webrings.topRingTopics(db, { count: topics * 2, minFeeds: minMembers });
353-
const tally = { rings: 0, created: 0, added: 0, skipped: 0 };
358+
const tally = { rings: 0, created: 0, added: 0, skipped: 0, failed: 0 };
354359

355360
for (const topic of candidates) {
356361
if (tally.rings >= topics) break;
357-
const size = await webrings.topicRingSize(db, topic.slug);
358-
if (size < minMembers) {
359-
tally.skipped += 1;
360-
continue;
362+
try {
363+
const size = await webrings.topicRingSize(db, topic.slug, { cap: minMembers });
364+
if (size < minMembers) {
365+
tally.skipped += 1;
366+
continue;
367+
}
368+
const result = await webrings.seedTopicRing(db, topic.slug, { limit });
369+
tally.rings += 1;
370+
if (result.created) tally.created += 1;
371+
tally.added += result.added;
372+
} catch (err) {
373+
tally.failed += 1;
374+
opts.onError?.(topic.slug, err);
361375
}
362-
const result = await webrings.seedTopicRing(db, topic.slug, { limit });
363-
tally.rings += 1;
364-
if (result.created) tally.created += 1;
365-
tally.added += result.added;
366376
}
367377

368378
return tally;

0 commit comments

Comments
 (0)