Skip to content

Commit 618cae4

Browse files
committed
feat: improve pom extractor performances
Signed-off-by: Umberto Sgueglia <usgueglia@contractor.linuxfoundation.org>
1 parent 61e299f commit 618cae4

13 files changed

Lines changed: 749 additions & 216 deletions

File tree

backend/.env.dist.local

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,3 +206,27 @@ POM_FETCHER_NON_CRITICAL_CONCURRENCY=20
206206
# pom-fetcher critical (HTTP)
207207
POM_FETCHER_BATCH_SIZE=100
208208
POM_FETCHER_CONCURRENCY=5
209+
# ── non-critical DB-only (usato quando DIRECT_POM_FOR_ALL=false) ──────────────
210+
# POM_FETCHER_NON_CRITICAL_BATCH_SIZE=500
211+
POM_FETCHER_NON_CRITICAL_CONCURRENCY=20 # solo DB writes, nessun HTTP
212+
POM_FETCHER_NON_CRITICAL_REFRESH_DAYS=1
213+
214+
# ── non-critical HTTP / direct-pom (usato quando DIRECT_POM_FOR_ALL=true) ─────
215+
# POM_FETCHER_NON_CRITICAL_POM_CONCURRENCY=5
216+
# POM_FETCHER_NON_CRITICAL_POM_REFRESH_DAYS=1
217+
218+
# ── critical HTTP / full-pom (sempre attivo) ──────────────────────────────────
219+
# POM_FETCHER_BATCH_SIZE=100
220+
# POM_FETCHER_CONCURRENCY=5
221+
# POM_FETCHER_FULL_REFRESH_DAYS=1
222+
223+
# ── modalità e rate limiting ──────────────────────────────────────────────────
224+
# POM_FETCHER_DIRECT_POM_FOR_ALL=true
225+
# POM_FETCHER_GROUP_DELAY_MS=200
226+
227+
228+
POM_FETCHER_BATCH_SIZE=50
229+
POM_FETCHER_CONCURRENCY=3
230+
POM_FETCHER_REFRESH_DAYS=1
231+
POM_FETCHER_GROUP_DELAY_MS=500
232+
POM_FETCHER_IDLE_SLEEP_SEC=3600

scripts/services/pom-fetcher.yaml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ x-env-args: &env-args
88
SUPPRESS_NO_CONFIG_WARNING: 'true'
99
LOG_LEVEL: 'info'
1010
POM_FETCHER_BATCH_SIZE: '50'
11-
POM_FETCHER_CONCURRENCY: '3'
12-
POM_FETCHER_STALE_DAYS: '7'
11+
POM_FETCHER_CONCURRENCY: '5'
12+
POM_FETCHER_REFRESH_DAYS: '1'
13+
POM_FETCHER_GROUP_DELAY_MS: '200'
1314
POM_FETCHER_IDLE_SLEEP_SEC: '3600'
1415

1516
services:

services/apps/packages_worker/src/bin/pom-fetcher.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,7 @@ const main = async () => {
2121
log.info('pom-fetcher starting...')
2222

2323
const config = getPomFetcherConfig()
24-
log.info(
25-
{ batchSize: config.batchSize, concurrency: config.concurrency, fullRefreshDays: config.fullRefreshDays },
26-
'Config loaded',
27-
)
24+
log.info(config, 'Config loaded')
2825

2926
const qx = await getPackagesDb()
3027
await qx.selectOne('SELECT 1')

services/apps/packages_worker/src/config.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,10 @@ export function getEnricherConfig() {
3535

3636
export function getPomFetcherConfig() {
3737
return {
38-
// critical packages — HTTP-bound, keep low
3938
batchSize: parseInt(process.env.POM_FETCHER_BATCH_SIZE ?? '50', 10),
40-
concurrency: parseInt(process.env.POM_FETCHER_CONCURRENCY ?? '3', 10),
41-
fullRefreshDays: parseInt(process.env.POM_FETCHER_FULL_REFRESH_DAYS ?? '90', 10),
42-
// non-critical packages — DB-only, can go much higher
43-
nonCriticalBatchSize: parseInt(process.env.POM_FETCHER_NON_CRITICAL_BATCH_SIZE ?? '500', 10),
44-
nonCriticalConcurrency: parseInt(process.env.POM_FETCHER_NON_CRITICAL_CONCURRENCY ?? '20', 10),
45-
nonCriticalRefreshDays: parseInt(process.env.POM_FETCHER_NON_CRITICAL_REFRESH_DAYS ?? '180', 10),
39+
concurrency: parseInt(process.env.POM_FETCHER_CONCURRENCY ?? '5', 10),
40+
refreshDays: parseInt(process.env.POM_FETCHER_REFRESH_DAYS ?? '1', 10),
41+
groupDelayMs: parseInt(process.env.POM_FETCHER_GROUP_DELAY_MS ?? '200', 10),
4642
idleSleepSec: parseInt(process.env.POM_FETCHER_IDLE_SLEEP_SEC ?? '3600', 10),
4743
}
4844
}

services/apps/packages_worker/src/pom-fetcher/README.md

Lines changed: 294 additions & 0 deletions
Large diffs are not rendered by default.

services/apps/packages_worker/src/pom-fetcher/extract.ts

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import { XMLParser } from 'fast-xml-parser'
1111
export interface PomMaintainer {
1212
username: string | null
1313
displayName: string | null
14-
/** Raw email from POM — hash with SHA-256 before storing (GDPR) */
1514
email: string | null
1615
url: string | null
1716
role: 'author' | 'maintainer'
@@ -55,7 +54,7 @@ interface PomPerson {
5554
// ─── Config ───────────────────────────────────────────────────────────────────
5655

5756
const MAVEN_REPO = 'https://repo1.maven.org/maven2'
58-
const MAX_PARENT_HOPS = 5
57+
export const MAX_PARENT_HOPS = 7
5958
const REQUEST_TIMEOUT_MS = 15_000
6059

6160
const parser = new XMLParser({
@@ -80,8 +79,10 @@ async function getWithRetry(url: string): Promise<string> {
8079
const res = await axios.get<string>(url, { responseType: 'text', timeout: REQUEST_TIMEOUT_MS })
8180
return res.data
8281
} catch (err) {
83-
if (axios.isAxiosError(err) && err.response?.status === 429) {
84-
if (attempt < MAX_RETRIES) {
82+
if (axios.isAxiosError(err)) {
83+
const status = err.response?.status
84+
// 429 = explicit rate limit, 403 = CDN throttle (Maven Central uses both)
85+
if ((status === 429 || status === 403) && attempt < MAX_RETRIES) {
8586
const delay = RETRY_BASE_MS * 2 ** attempt + Math.random() * 500
8687
await sleep(delay)
8788
continue
@@ -195,10 +196,65 @@ async function resolveWithInheritance(
195196
}
196197
}
197198

198-
// ─── Public entry point ───────────────────────────────────────────────────────
199+
// ─── Public entry points ──────────────────────────────────────────────────────
199200

200201
/**
201-
* Fetches and resolves POM metadata for the given Maven artifact.
202+
* Fetches only the root POM without following the parent chain.
203+
* Faster than extractArtifact — use for non-critical packages where inherited
204+
* fields (licenses, SCM) may be missing but throughput matters more.
205+
*/
206+
export async function extractArtifactDirect(
207+
groupId: string,
208+
artifactId: string,
209+
version: string,
210+
log: (msg: string) => void = () => undefined,
211+
): Promise<PomExtractionResult> {
212+
const purl = `pkg:maven/${groupId}/${artifactId}@${version}`
213+
const pom = await fetchPom(groupId, artifactId, version, log)
214+
215+
if (!pom) {
216+
return {
217+
groupId,
218+
artifactId,
219+
version,
220+
purl,
221+
description: null,
222+
licenses: [],
223+
licensesRaw: null,
224+
scmUrl: null,
225+
homepageUrl: null,
226+
developers: [],
227+
contributors: [],
228+
parentHops: 0,
229+
error: `POM not found: ${buildPomUrl(groupId, artifactId, version)}`,
230+
}
231+
}
232+
233+
const licenses = extractLicenses(pom)
234+
const scmUrl = extractStr(pom.scm?.url ?? pom.scm?.connection)
235+
const developers = extractPersons(pom.developers?.developer, 'author')
236+
const contributors = extractPersons(pom.contributors?.contributor, 'maintainer')
237+
238+
return {
239+
groupId,
240+
artifactId,
241+
version,
242+
purl,
243+
description: extractStr(pom.description),
244+
licenses,
245+
licensesRaw: licenses.length > 0 ? licenses.join(', ') : null,
246+
scmUrl,
247+
homepageUrl: extractStr(pom.url),
248+
developers,
249+
contributors,
250+
parentHops: 0,
251+
error: null,
252+
}
253+
}
254+
255+
/**
256+
* Fetches and resolves POM metadata for the given Maven artifact, following
257+
* the parent chain to inherit licenses and SCM when not in the direct POM.
202258
* Always returns a result object; errors are captured in `result.error`.
203259
*/
204260
export async function extractArtifact(

services/apps/packages_worker/src/pom-fetcher/metadata.ts

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
2-
* Resolves the latest release version of a Maven artifact using the
3-
* maven-metadata.xml endpoint on Maven Central.
2+
* Fetches maven-metadata.xml for a Maven artifact and returns the full version
3+
* list plus the current release version.
44
*
55
* URL format:
66
* https://repo1.maven.org/maven2/{groupPath}/{artifactId}/maven-metadata.xml
@@ -24,14 +24,19 @@ const parser = new XMLParser({
2424
parseAttributeValue: false,
2525
})
2626

27+
export interface MavenVersionsMetadata {
28+
versions: string[]
29+
releaseVersion: string | null
30+
}
31+
2732
async function sleep(ms: number): Promise<void> {
2833
return new Promise((r) => setTimeout(r, ms))
2934
}
3035

31-
export async function resolveLatestVersion(
36+
export async function resolveVersionsList(
3237
groupId: string,
3338
artifactId: string,
34-
): Promise<string | null> {
39+
): Promise<MavenVersionsMetadata | null> {
3540
const groupPath = groupId.replace(/\./g, '/')
3641
const url = `${MAVEN_REPO}/${groupPath}/${artifactId}/maven-metadata.xml`
3742

@@ -45,11 +50,20 @@ export async function resolveLatestVersion(
4550
const release = typeof versioning?.release === 'string' ? versioning.release.trim() : null
4651
const latest = typeof versioning?.latest === 'string' ? versioning.latest.trim() : null
4752

48-
return release || latest || null
53+
const rawVersions = versioning?.versions?.version
54+
let versions: string[] = []
55+
if (Array.isArray(rawVersions)) {
56+
versions = rawVersions.map((v: unknown) => String(v).trim()).filter(Boolean)
57+
} else if (typeof rawVersions === 'string' && rawVersions.trim()) {
58+
versions = [rawVersions.trim()]
59+
}
60+
61+
return { versions, releaseVersion: release || latest || null }
4962
} catch (err) {
5063
if (axios.isAxiosError(err)) {
5164
if (err.response?.status === 404) return null
52-
if (err.response?.status === 429 && attempt < MAX_RETRIES) {
65+
// 429 = explicit rate limit, 403 = CDN throttle (Maven Central uses both)
66+
if ((err.response?.status === 429 || err.response?.status === 403) && attempt < MAX_RETRIES) {
5367
const delay = RETRY_BASE_MS * 2 ** attempt + Math.random() * 500
5468
await sleep(delay)
5569
continue
@@ -61,3 +75,11 @@ export async function resolveLatestVersion(
6175

6276
return null
6377
}
78+
79+
export async function resolveLatestVersion(
80+
groupId: string,
81+
artifactId: string,
82+
): Promise<string | null> {
83+
const meta = await resolveVersionsList(groupId, artifactId)
84+
return meta?.releaseVersion ?? null
85+
}

0 commit comments

Comments
 (0)