Skip to content

Commit c50d5e9

Browse files
committed
fix: isolate per-repo failures in security contacts batch
Signed-off-by: Mouad BANI <mouad-mb@outlook.com>
1 parent 95dcf42 commit c50d5e9

2 files changed

Lines changed: 25 additions & 14 deletions

File tree

services/apps/packages_worker/src/security-contacts/processBatch.ts

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -107,23 +107,25 @@ async function processRepo(
107107
deps: ExtractorDeps,
108108
qx: QueryExecutor,
109109
): Promise<void> {
110-
// One failing extractor must not sink the repo.
111110
const results = await Promise.allSettled(EXTRACTORS.map((extract) => extract(target, deps)))
112111

113-
// Every extractor failed (transient outage) — preserve existing data, just record the attempt.
114-
if (results.every((r) => r.status === 'rejected')) {
115-
log.warn({ repoId: target.repoId }, 'All extractors failed — preserving existing data')
112+
// Replace the repo's contacts only when every extractor succeeded. If any failed (transient
113+
// error — non-200s throw), a destructive rewrite would drop contacts a failed tier-A/B extractor
114+
// still has, so preserve existing data and just record the attempt; retried next cadence.
115+
const failed = results.find((r) => r.status === 'rejected') as PromiseRejectedResult | undefined
116+
if (failed) {
117+
log.warn(
118+
{ repoId: target.repoId, errMsg: failed.reason?.message },
119+
'Extractor failed — preserving existing data',
120+
)
116121
await markRepoAttempted(qx, target.repoId)
117122
return
118123
}
119124

120125
let contacts: RawContact[] = []
121126
const policies: Partial<RepoPolicies> = {}
122127
for (const r of results) {
123-
if (r.status !== 'fulfilled') {
124-
log.warn({ repoId: target.repoId, errMsg: r.reason?.message }, 'Extractor failed')
125-
continue
126-
}
128+
if (r.status !== 'fulfilled') continue
127129
contacts.push(...r.value.contacts)
128130
for (const [key, value] of Object.entries(r.value.policies)) {
129131
if (!(policies as Record<string, unknown>)[key] && value != null) {
@@ -153,7 +155,16 @@ export async function processBatch(qx: QueryExecutor, config: Config): Promise<B
153155
}
154156

155157
const targets = batch.map(toTarget)
156-
await mapWithConcurrency(targets, CONCURRENCY, (target) => processRepo(target, deps, qx))
158+
// Isolate per-repo failures: mapWithConcurrency is fail-fast, so a single repo's DB error must
159+
// not abort the batch (and, across Temporal retries, halt the whole sweep). Unmarked repos on
160+
// error are simply retried next sweep.
161+
await mapWithConcurrency(targets, CONCURRENCY, async (target) => {
162+
try {
163+
await processRepo(target, deps, qx)
164+
} catch (err) {
165+
log.error({ repoId: target.repoId, errMsg: (err as Error).message }, 'Repo processing failed')
166+
}
167+
})
157168

158169
log.info({ processed: targets.length }, 'Security contacts batch complete')
159170
return { processed: targets.length }

services/apps/packages_worker/src/security-contacts/writeContacts.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,6 @@ import { QueryExecutor } from '@crowd/data-access-layer/src/queryExecutor'
22

33
import { RepoPolicies, ScoredContact } from './types'
44

5-
/**
6-
* Idempotent per-repo recompute: replace the repo's security_contacts rows and refresh
7-
* the policy columns in one transaction. Policy columns use COALESCE so a run that doesn't
8-
* rediscover a field (partial/failed extractor pass) never clears a previously known value.
9-
*/
105
// Records the attempt without touching contacts/policies — preserves existing data on total
116
// failure while advancing contacts_last_refreshed so the repo isn't reprocessed this sweep.
127
export async function markRepoAttempted(qx: QueryExecutor, repoId: string): Promise<void> {
@@ -15,6 +10,11 @@ export async function markRepoAttempted(qx: QueryExecutor, repoId: string): Prom
1510
})
1611
}
1712

13+
/**
14+
* Idempotent per-repo recompute: replace the repo's security_contacts rows and refresh
15+
* the policy columns in one transaction. Policy columns use COALESCE so a run that doesn't
16+
* rediscover a field (partial/failed extractor pass) never clears a previously known value.
17+
*/
1818
export async function writeContacts(
1919
qx: QueryExecutor,
2020
repoId: string,

0 commit comments

Comments
 (0)