Skip to content

Commit 5c0e982

Browse files
committed
Merge remote-tracking branch 'origin/main' into feat/org-page-tinybird-pipes
Signed-off-by: Joana Maia <jmaia@contractor.linuxfoundation.org>
2 parents 546d061 + 14bd1a6 commit 5c0e982

31 files changed

Lines changed: 936 additions & 154 deletions

File tree

pnpm-lock.yaml

Lines changed: 37 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
DOCKERFILE="./services/docker/Dockerfile.projects_evaluation_worker"
2+
CONTEXT="../"
3+
REPO="sjc.ocir.io/axbydjxa5zuh/projects-evaluation-worker"
4+
SERVICES="projects-evaluation-worker"
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
FROM node:20-alpine as builder
2+
3+
RUN apk add --no-cache python3 make g++
4+
5+
WORKDIR /usr/crowd/app
6+
RUN npm install -g corepack@latest && corepack enable pnpm && corepack prepare pnpm@9.15.0 --activate
7+
8+
COPY ./pnpm-workspace.yaml ./pnpm-lock.yaml ./
9+
RUN pnpm fetch
10+
11+
COPY ./services ./services
12+
RUN pnpm i --frozen-lockfile
13+
14+
FROM node:20-bookworm-slim as runner
15+
16+
WORKDIR /usr/crowd/app
17+
RUN npm install -g corepack@latest && corepack enable pnpm && corepack prepare pnpm@9.15.0 --activate && apt update && apt install -y ca-certificates --no-install-recommends && rm -rf /var/lib/apt/lists/*
18+
19+
COPY --from=builder /usr/crowd/app/node_modules ./node_modules
20+
COPY --from=builder /usr/crowd/app/services/base.tsconfig.json ./services/base.tsconfig.json
21+
COPY --from=builder /usr/crowd/app/services/libs ./services/libs
22+
COPY --from=builder /usr/crowd/app/services/archetypes/ ./services/archetypes
23+
COPY --from=builder /usr/crowd/app/services/apps/projects_evaluation_worker/ ./services/apps/projects_evaluation_worker
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
version: '3.1'
2+
3+
x-env-args: &env-args
4+
DOCKER_BUILDKIT: 1
5+
NODE_ENV: docker
6+
SERVICE: projects-evaluation-worker
7+
CROWD_TEMPORAL_TASKQUEUE: projects-evaluation
8+
SHELL: /bin/sh
9+
10+
services:
11+
projects-evaluation-worker:
12+
build:
13+
context: ../../
14+
dockerfile: ./scripts/services/docker/Dockerfile.projects_evaluation_worker
15+
command: 'pnpm run start'
16+
working_dir: /usr/crowd/app/services/apps/projects_evaluation_worker
17+
env_file:
18+
- ../../backend/.env.dist.local
19+
- ../../backend/.env.dist.composed
20+
- ../../backend/.env.override.local
21+
- ../../backend/.env.override.composed
22+
environment:
23+
<<: *env-args
24+
restart: always
25+
networks:
26+
- crowd-bridge
27+
28+
projects-evaluation-worker-dev:
29+
build:
30+
context: ../../
31+
dockerfile: ./scripts/services/docker/Dockerfile.projects_evaluation_worker
32+
command: 'pnpm run dev'
33+
working_dir: /usr/crowd/app/services/apps/projects_evaluation_worker
34+
env_file:
35+
- ../../backend/.env.dist.local
36+
- ../../backend/.env.dist.composed
37+
- ../../backend/.env.override.local
38+
- ../../backend/.env.override.composed
39+
environment:
40+
<<: *env-args
41+
hostname: projects-evaluation-worker
42+
networks:
43+
- crowd-bridge
44+
volumes:
45+
- ../../services/libs/audit-logs/src:/usr/crowd/app/services/libs/audit-logs/src
46+
- ../../services/libs/common/src:/usr/crowd/app/services/libs/common/src
47+
- ../../services/libs/common_services/src:/usr/crowd/app/services/libs/common_services/src
48+
- ../../services/libs/data-access-layer/src:/usr/crowd/app/services/libs/data-access-layer/src
49+
- ../../services/libs/database/src:/usr/crowd/app/services/libs/database/src
50+
- ../../services/libs/integrations/src:/usr/crowd/app/services/libs/integrations/src
51+
- ../../services/libs/logging/src:/usr/crowd/app/services/libs/logging/src
52+
- ../../services/libs/nango/src:/usr/crowd/app/services/libs/nango/src
53+
- ../../services/libs/opensearch/src:/usr/crowd/app/services/libs/opensearch/src
54+
- ../../services/libs/queue/src:/usr/crowd/app/services/libs/queue/src
55+
- ../../services/libs/redis/src:/usr/crowd/app/services/libs/redis/src
56+
- ../../services/libs/snowflake/src:/usr/crowd/app/services/libs/snowflake/src
57+
- ../../services/libs/telemetry/src:/usr/crowd/app/services/libs/telemetry/src
58+
- ../../services/libs/temporal/src:/usr/crowd/app/services/libs/temporal/src
59+
- ../../services/libs/types/src:/usr/crowd/app/services/libs/types/src
60+
- ../../services/apps/projects_evaluation_worker/src:/usr/crowd/app/services/apps/projects_evaluation_worker/src
61+
62+
networks:
63+
crowd-bridge:
64+
external: true

services/apps/automatic_projects_discovery_worker/src/activities/activities.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Context } from '@temporalio/activity'
12
import { parse } from 'csv-parse'
23

34
import { bulkUpsertProjectCatalog } from '@crowd/data-access-layer'
@@ -102,6 +103,7 @@ export async function processDataset(
102103
totalProcessed += batch.length
103104
batch = []
104105

106+
Context.current().heartbeat({ totalProcessed, batchNumber })
105107
log.info({ totalProcessed, batchNumber, datasetId: dataset.id }, 'Batch upserted.')
106108
}
107109
}

services/apps/automatic_projects_discovery_worker/src/sources/insights-discussions/source.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,6 @@ export class InsightsDiscussionsSource implements IDiscoverySource {
261261
projectSlug,
262262
repoName,
263263
repoUrl,
264-
action: 'evaluate',
265264
}
266265
}
267266
}

services/apps/automatic_projects_discovery_worker/src/sources/lf-criticality-score/source.ts

Lines changed: 114 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import http from 'http'
22
import https from 'https'
33
import { Readable } from 'stream'
44

5+
import { timeout } from '@crowd/common'
56
import { getServiceLogger } from '@crowd/logging'
67

78
import { IDatasetDescriptor, IDiscoverySource, IDiscoverySourceRow } from '../types'
@@ -12,6 +13,31 @@ const DEFAULT_API_HOST = 'lf-criticality-score-api.example.com'
1213
const DEFAULT_API_PORT = 443
1314
const PAGE_SIZE = 100
1415

16+
function parseEnvInt(
17+
value: string | undefined,
18+
defaultValue: number,
19+
min: number,
20+
max: number,
21+
): number {
22+
const parsed = parseInt(value ?? '', 10)
23+
return Number.isFinite(parsed) && parsed >= min && parsed <= max ? parsed : defaultValue
24+
}
25+
26+
// Requests per second sent to the LF Criticality Score API (throttle between pages).
27+
const REQUESTS_PER_SECOND = parseEnvInt(
28+
process.env.LF_CRITICALITY_SCORE_REQUESTS_PER_SECOND,
29+
5,
30+
1,
31+
100,
32+
)
33+
// Max per-page attempts (initial + retries) on 429 or 5xx before giving up.
34+
const MAX_ATTEMPTS = parseEnvInt(
35+
process.env.LF_CRITICALITY_SCORE_MAX_ATTEMPTS ?? process.env.LF_CRITICALITY_SCORE_MAX_RETRIES,
36+
7,
37+
1,
38+
20,
39+
)
40+
1541
interface LfApiResponse {
1642
page: number
1743
pageSize: number
@@ -46,6 +72,37 @@ function getApiBaseUrl(): string {
4672
return `${scheme}://${host}:${port}`
4773
}
4874

75+
interface HttpGetResult {
76+
statusCode: number
77+
retryAfterMs: number | null
78+
body: string
79+
}
80+
81+
function parseRetryAfterMs(header: string | string[] | undefined): number | null {
82+
const raw = Array.isArray(header) ? header[0] : header
83+
if (!raw) return null
84+
const secs = parseFloat(raw.trim())
85+
return Number.isFinite(secs) && secs > 0 ? secs * 1000 : null
86+
}
87+
88+
function httpGet(url: string): Promise<HttpGetResult> {
89+
return new Promise((resolve, reject) => {
90+
const client = url.startsWith('https://') ? https : http
91+
const req = client.get(url, (res) => {
92+
const statusCode = res.statusCode ?? 0
93+
const retryAfterMs = parseRetryAfterMs(res.headers['retry-after'])
94+
const chunks: Uint8Array[] = []
95+
res.on('data', (chunk: Uint8Array) => chunks.push(chunk))
96+
res.on('end', () =>
97+
resolve({ statusCode, retryAfterMs, body: Buffer.concat(chunks).toString('utf8') }),
98+
)
99+
res.on('error', reject)
100+
})
101+
req.on('error', reject)
102+
req.end()
103+
})
104+
}
105+
49106
async function fetchPage(
50107
baseUrl: string,
51108
page: number,
@@ -55,31 +112,50 @@ async function fetchPage(
55112
if (scoredAfter) params.set('scoredAfter', scoredAfter)
56113
const url = `${baseUrl}/projects?${params.toString()}`
57114

58-
return new Promise((resolve, reject) => {
59-
const client = url.startsWith('https://') ? https : http
115+
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
116+
let result: HttpGetResult | null = null
60117

61-
const req = client.get(url, (res) => {
62-
if (res.statusCode !== 200) {
63-
reject(new Error(`LF Criticality Score API returned status ${res.statusCode} for ${url}`))
64-
res.resume()
65-
return
118+
try {
119+
result = await httpGet(url)
120+
} catch (networkErr) {
121+
if (attempt === MAX_ATTEMPTS - 1) {
122+
throw new Error(`LF Criticality Score API network error for ${url}: ${networkErr}`)
66123
}
124+
const delayMs = Math.min(Math.pow(2, attempt) * 1000, 60_000)
125+
log.warn(
126+
{ page, attempt: attempt + 1, maxAttempts: MAX_ATTEMPTS, delayMs, err: String(networkErr) },
127+
'LF Criticality Score: network error, retrying...',
128+
)
129+
await timeout(delayMs)
130+
continue
131+
}
67132

68-
const chunks: Uint8Array[] = []
69-
res.on('data', (chunk: Uint8Array) => chunks.push(chunk))
70-
res.on('end', () => {
71-
try {
72-
resolve(JSON.parse(Buffer.concat(chunks).toString('utf8')) as LfApiResponse)
73-
} catch (err) {
74-
reject(new Error(`Failed to parse LF Criticality Score API response: ${err}`))
75-
}
76-
})
77-
res.on('error', reject)
78-
})
133+
const { statusCode, retryAfterMs, body } = result
79134

80-
req.on('error', reject)
81-
req.end()
82-
})
135+
if (statusCode === 200) {
136+
try {
137+
return JSON.parse(body) as LfApiResponse
138+
} catch (err) {
139+
throw new Error(`Failed to parse LF Criticality Score API response: ${err}`)
140+
}
141+
}
142+
143+
const isRetryable = statusCode === 429 || statusCode >= 500
144+
if (!isRetryable || attempt === MAX_ATTEMPTS - 1) {
145+
throw new Error(`LF Criticality Score API returned status ${statusCode} for ${url}`)
146+
}
147+
148+
const delayMs = retryAfterMs ?? Math.min(Math.pow(2, attempt) * 1000, 60_000)
149+
150+
log.warn(
151+
{ page, attempt: attempt + 1, maxAttempts: MAX_ATTEMPTS, statusCode, delayMs },
152+
'LF Criticality Score: rate limited or server error, retrying...',
153+
)
154+
await timeout(delayMs)
155+
}
156+
157+
// Unreachable, but satisfies TypeScript.
158+
throw new Error(`LF Criticality Score API failed for ${url} after ${MAX_ATTEMPTS} attempts`)
83159
}
84160

85161
export class LfCriticalityScoreSource implements IDiscoverySource {
@@ -113,29 +189,29 @@ export class LfCriticalityScoreSource implements IDiscoverySource {
113189
'LF Criticality Score: starting stream fetch.',
114190
)
115191

192+
const throttleIntervalMs = Math.round(1000 / REQUESTS_PER_SECOND)
193+
116194
async function* pages() {
117-
let page = 1
118-
let totalPages = 1
195+
const firstPage = await fetchPage(baseUrl, 1, scoredAfter)
196+
const { totalPages } = firstPage
197+
198+
log.info(
199+
{ datasetId: dataset.id, total: firstPage.total, totalPages, pageSize: firstPage.pageSize },
200+
'LF Criticality Score: first page received — total records available.',
201+
)
202+
203+
for (const row of firstPage.data) {
204+
yield row
205+
}
206+
207+
for (let page = 2; page <= totalPages; page++) {
208+
await timeout(throttleIntervalMs)
119209

120-
do {
121210
log.info(
122211
{ datasetId: dataset.id, page, totalPages },
123212
'LF Criticality Score: fetching page...',
124213
)
125214
const response = await fetchPage(baseUrl, page, scoredAfter)
126-
totalPages = response.totalPages
127-
128-
if (page === 1) {
129-
log.info(
130-
{
131-
datasetId: dataset.id,
132-
total: response.total,
133-
totalPages,
134-
pageSize: response.pageSize,
135-
},
136-
'LF Criticality Score: first page received — total records available.',
137-
)
138-
}
139215

140216
for (const row of response.data) {
141217
yield row
@@ -145,9 +221,7 @@ export class LfCriticalityScoreSource implements IDiscoverySource {
145221
{ datasetId: dataset.id, page, totalPages, rowsInPage: response.data.length },
146222
'LF Criticality Score: page fetched.',
147223
)
148-
149-
page++
150-
} while (page <= totalPages)
224+
}
151225

152226
log.info({ datasetId: dataset.id, totalPages }, 'LF Criticality Score: all pages fetched.')
153227
}

services/apps/automatic_projects_discovery_worker/src/workflows/discoverProjects.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ const listActivities = proxyActivities<typeof activities>({
77
retry: { maximumAttempts: 3 },
88
})
99

10-
// processDataset is long-running (10-20 min for ~119MB / ~750K rows).
10+
// processDataset is long-running: ~119MB / ~750K rows + inter-page throttle can exceed 60 min.
1111
const processActivities = proxyActivities<typeof activities>({
12-
startToCloseTimeout: '30 minutes',
12+
startToCloseTimeout: '90 minutes',
13+
heartbeatTimeout: '5 minutes',
1314
retry: { maximumAttempts: 3 },
1415
})
1516

services/apps/cron_service/src/jobs/queueMonitoring.job.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -189,9 +189,9 @@ async function getTopicMessageCount(
189189
try {
190190
const topicOffsets = await admin.fetchTopicOffsets(topic)
191191

192-
// Sum up all partition offsets to get total messages
192+
// Sum (high - low) per partition — actual messages currently in the topic.
193193
const totalMessages = topicOffsets.reduce((sum, partition) => {
194-
return sum + Number(partition.offset)
194+
return sum + Math.max(0, Number(partition.offset) - Number(partition.low))
195195
}, 0)
196196

197197
return totalMessages

0 commit comments

Comments
 (0)