@@ -30,6 +30,20 @@ export interface BatchResult {
3030 processed : number
3131}
3232
33+ // Two-tier refresh cadence (the worker runs on a daily cron). Just under 24h/168h so a repo
34+ // processed at ~06:00 is eligible again at the next daily/weekly tick rather than slipping a day.
35+ const DAILY_INTERVAL_HOURS = 20 // repos never evaluated or with no contacts yet
36+ const WEEKLY_INTERVAL_HOURS = 156 // already-enriched repos (have contacts)
37+
38+ // Tuned for throughput within the platform ceilings:
39+ // - CONCURRENCY: parallel repos; safe against GitHub REST secondary limits given the token pool.
40+ // - FETCH_TIMEOUT_MS: generous enough for slow registries (Maven metadata/POM) without hanging slots.
41+ // - BATCH_SIZE: bounded by the 30-min activity timeout (worst case: an all-cargo batch throttled
42+ // to crates.io's 1 req/s finishes ~8 min).
43+ const CONCURRENCY = 100
44+ const FETCH_TIMEOUT_MS = 15000
45+ const BATCH_SIZE = 500
46+
3347const EXTRACTORS : Extractor [ ] = [
3448 extractSecurityInsights , // A1
3549 extractPvr , // A2
@@ -46,7 +60,7 @@ interface SweepRow {
4660 packages : RepoPackage [ ] | null
4761}
4862
49- async function fetchBatch ( qx : QueryExecutor , config : Config ) : Promise < SweepRow [ ] > {
63+ async function fetchBatch ( qx : QueryExecutor ) : Promise < SweepRow [ ] > {
5064 return qx . select (
5165 `
5266 SELECT r.id::text AS id,
@@ -58,14 +72,28 @@ async function fetchBatch(qx: QueryExecutor, config: Config): Promise<SweepRow[]
5872 JOIN packages p ON p.id = pr.package_id AND p.is_critical
5973 WHERE r.host = 'github'
6074 AND (
75+ -- never evaluated → always eligible
6176 r.contacts_last_refreshed IS NULL
62- OR r.contacts_last_refreshed < NOW() - INTERVAL '$(updateIntervalHours) hours'
77+ -- evaluated but no contacts found yet → retry on the daily cadence
78+ OR (
79+ NOT EXISTS (SELECT 1 FROM security_contacts sc WHERE sc.repo_id = r.id)
80+ AND r.contacts_last_refreshed < NOW() - INTERVAL '$(dailyIntervalHours) hours'
81+ )
82+ -- already enriched (has contacts) → refresh on the weekly cadence
83+ OR (
84+ EXISTS (SELECT 1 FROM security_contacts sc WHERE sc.repo_id = r.id)
85+ AND r.contacts_last_refreshed < NOW() - INTERVAL '$(weeklyIntervalHours) hours'
86+ )
6387 )
6488 GROUP BY r.id
6589 ORDER BY r.id
6690 LIMIT $(batchSize)
6791 ` ,
68- { batchSize : config . batchSize , updateIntervalHours : config . updateIntervalHours } ,
92+ {
93+ batchSize : BATCH_SIZE ,
94+ dailyIntervalHours : DAILY_INTERVAL_HOURS ,
95+ weeklyIntervalHours : WEEKLY_INTERVAL_HOURS ,
96+ } ,
6997 )
7098}
7199
@@ -130,17 +158,17 @@ async function processRepo(
130158}
131159
132160export async function processBatch ( qx : QueryExecutor , config : Config ) : Promise < BatchResult > {
133- const batch = await fetchBatch ( qx , config )
161+ const batch = await fetchBatch ( qx )
134162 if ( batch . length === 0 ) return { processed : 0 }
135163
136164 const deps : ExtractorDeps = {
137- fetchTimeoutMs : config . fetchTimeoutMs ,
165+ fetchTimeoutMs : FETCH_TIMEOUT_MS ,
138166 userAgent : config . userAgent ,
139167 getToken : getSecurityContactsToken ,
140168 }
141169
142170 const targets = batch . map ( toTarget )
143- await runWithConcurrency ( targets , config . concurrency , ( target ) => processRepo ( target , deps , qx ) )
171+ await runWithConcurrency ( targets , CONCURRENCY , ( target ) => processRepo ( target , deps , qx ) )
144172
145173 log . info ( { processed : targets . length } , 'Security contacts batch complete' )
146174 return { processed : targets . length }
0 commit comments