Skip to content

Commit 2153813

Browse files
committed
Merge branch 'develop' of github.com:topcoder-platform/review-api-v6 into PM-5738_deterministic-ai-workflow
2 parents 00b42e5 + 507e307 commit 2153813

12 files changed

Lines changed: 819 additions & 60 deletions

docs/KAFKA_SETUP.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,12 @@ The application includes a robust Dead Letter Queue implementation for handling
152152
- **Kafka UI**: Monitor topics, partitions, and consumer groups at http://localhost:8080
153153
- **Health Checks**: Kafka connection status is included in application health checks
154154

155+
### Kafka Client and Recovery
156+
157+
- The service uses `@platformatic/kafka` 2.8.0 for broker failover and consumer group recovery fixes.
158+
- Platformatic Kafka 2.x raises the aggregate consumer Fetch limit to 50 MiB. The service deliberately retains the previous 10 MiB `maxBytes` limit to avoid increasing its per-consumer memory envelope.
159+
- Terminal consumer or producer client errors and offset commit timeouts mark Kafka health as `reconnecting` and start the shared reconnect lifecycle. A successful reconnect returns health to `ready`; exhausted attempts mark it as `failed` with the last failure reason.
160+
155161
### Environment Variables
156162

157163
All Kafka-related environment variables are documented in `.env.sample`:

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
"@nestjs/platform-express": "^11.1.28",
3838
"@nestjs/schedule": "^6.1.3",
3939
"@nestjs/swagger": "^11.4.6",
40-
"@platformatic/kafka": "^1.34.0",
40+
"@platformatic/kafka": "2.8.0",
4141
"@prisma/client": "6.19.3",
4242
"archiver": "^6.0.2",
4343
"axios": "^1.18.1",

pnpm-lock.yaml

Lines changed: 19 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/api/my-review/myReview.controller.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,12 @@ export class MyReviewController {
6868
description: 'Filter by challenge status',
6969
enum: ChallengeStatus,
7070
})
71+
@ApiQuery({
72+
name: 'resourceRoleIds',
73+
required: false,
74+
description:
75+
'Comma-separated resource role identifiers assigned to the requesting member',
76+
})
7177
@ApiQuery({
7278
name: 'past',
7379
required: false,

src/api/my-review/myReview.service.spec.ts

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,126 @@ describe('MyReviewService', () => {
231231
expect(queryDetails.values).toContain(ChallengeStatus.COMPLETED);
232232
});
233233

234+
it('pages role-filtered past reviews by distinct challenge before restoring resource rows', async () => {
235+
challengePrismaMock.$queryRaw
236+
.mockResolvedValueOnce([{ total: 1n }])
237+
.mockResolvedValueOnce([]);
238+
239+
await service.getMyReviews(
240+
{ isMachine: false, userId: '151743' },
241+
{
242+
past: 'true',
243+
resourceRoleIds:
244+
' reviewer-role-id, ,screener-role-id,reviewer-role-id, ',
245+
},
246+
{ page: 1, perPage: 50 },
247+
);
248+
249+
const [countQuery, rowQuery] = challengePrismaMock.$queryRaw.mock.calls.map(
250+
(call) => call[0],
251+
);
252+
253+
[countQuery, rowQuery].forEach((query) => {
254+
const queryDetails = query.inspect();
255+
const sql = queryDetails.sql.replace(/\s+/g, ' ');
256+
const requestedRoleValues = queryDetails.values.filter(
257+
(value) => value === 'reviewer-role-id' || value === 'screener-role-id',
258+
);
259+
260+
expect(sql).toContain('r."roleId" IN (?, ?)');
261+
expect(requestedRoleValues).toEqual([
262+
'reviewer-role-id',
263+
'screener-role-id',
264+
]);
265+
expect(queryDetails.values).not.toContain('');
266+
});
267+
268+
const countSql = countQuery.inspect().sql.replace(/\s+/g, ' ');
269+
const rowSql = rowQuery.inspect().sql.replace(/\s+/g, ' ');
270+
const limitPosition = rowSql.indexOf('LIMIT ?');
271+
const finalResourceJoinPosition = rowSql.indexOf('JOIN base_matches bm');
272+
273+
expect(countSql).toContain('SELECT COUNT(DISTINCT c.id) AS "total"');
274+
expect(rowSql).toContain('challenge_page AS MATERIALIZED');
275+
expect(rowSql).toContain('SELECT DISTINCT "challengeId"');
276+
expect(limitPosition).toBeGreaterThan(-1);
277+
expect(finalResourceJoinPosition).toBeGreaterThan(limitPosition);
278+
});
279+
280+
it('filters member-ordered past reviews by the requested resource roles', async () => {
281+
challengePrismaMock.$queryRaw
282+
.mockResolvedValueOnce([{ total: 1n }])
283+
.mockResolvedValueOnce([]);
284+
285+
await service.getMyReviews(
286+
{ isMachine: false, userId: '151743' },
287+
{
288+
past: 'true',
289+
resourceRoleIds: 'reviewer-role-id,screener-role-id',
290+
sortBy: 'challengeName',
291+
},
292+
{ page: 1, perPage: 50 },
293+
);
294+
295+
const rowQuery = challengePrismaMock.$queryRaw.mock.calls[1][0];
296+
const queryDetails = rowQuery.inspect();
297+
const sql = queryDetails.sql.replace(/\s+/g, ' ');
298+
299+
expect(sql).toContain('base_matches AS MATERIALIZED');
300+
expect(sql).toContain('challenge_page AS MATERIALIZED');
301+
expect(sql.indexOf('LIMIT ?')).toBeLessThan(
302+
sql.indexOf('JOIN base_matches bm'),
303+
);
304+
expect(sql).toContain('r."roleId" IN (?, ?)');
305+
expect(queryDetails.values).toEqual(
306+
expect.arrayContaining(['reviewer-role-id', 'screener-role-id']),
307+
);
308+
});
309+
310+
it('joins member resources in the admin count query when filtering by role', async () => {
311+
challengePrismaMock.$queryRaw.mockResolvedValueOnce([{ total: 0n }]);
312+
313+
await service.getMyReviews(
314+
{ isMachine: true, userId: 'admin-member-id' },
315+
{ resourceRoleIds: 'reviewer-role-id' },
316+
);
317+
318+
const countQuery = challengePrismaMock.$queryRaw.mock.calls[0][0];
319+
const queryDetails = countQuery.inspect();
320+
const sql = queryDetails.sql.replace(/\s+/g, ' ');
321+
322+
expect(sql).toContain('LEFT JOIN resources."Resource" r');
323+
expect(sql).toContain('r."memberId" = ?');
324+
expect(sql).toContain('r."roleId" IN (?)');
325+
expect(queryDetails.values).toEqual(
326+
expect.arrayContaining(['admin-member-id', 'reviewer-role-id']),
327+
);
328+
});
329+
330+
it('uses challenge-level pagination for an admin past-role filter', async () => {
331+
challengePrismaMock.$queryRaw
332+
.mockResolvedValueOnce([{ total: 1n }])
333+
.mockResolvedValueOnce([]);
334+
335+
await service.getMyReviews(
336+
{ isMachine: true, userId: 'admin-member-id' },
337+
{ past: 'true', resourceRoleIds: 'reviewer-role-id' },
338+
{ page: 1, perPage: 50 },
339+
);
340+
341+
const [countQuery, rowQuery] = challengePrismaMock.$queryRaw.mock.calls.map(
342+
(call) => call[0],
343+
);
344+
const countSql = countQuery.inspect().sql.replace(/\s+/g, ' ');
345+
const rowSql = rowQuery.inspect().sql.replace(/\s+/g, ' ');
346+
347+
expect(countSql).toContain('SELECT COUNT(DISTINCT c.id) AS "total"');
348+
expect(rowSql).toContain('challenge_page AS MATERIALIZED');
349+
expect(rowSql.indexOf('LIMIT ?')).toBeLessThan(
350+
rowSql.indexOf('JOIN base_matches bm'),
351+
);
352+
});
353+
234354
it('uses a paged base query for past member challenge end sorting', async () => {
235355
challengePrismaMock.$queryRaw
236356
.mockResolvedValueOnce([{ total: 1n }])
@@ -245,9 +365,14 @@ describe('MyReviewService', () => {
245365
const query = challengePrismaMock.$queryRaw.mock.calls[1][0];
246366
const queryDetails = query.inspect();
247367
const sql = queryDetails.sql.replace(/\s+/g, ' ');
368+
const countSql = challengePrismaMock.$queryRaw.mock.calls[0][0]
369+
.inspect()
370+
.sql.replace(/\s+/g, ' ');
248371

372+
expect(countSql).toContain('SELECT COUNT(*) AS "total"');
249373
expect(sql).toContain('base_page AS MATERIALIZED');
250374
expect(sql).toContain('JOIN LATERAL');
375+
expect(sql).not.toContain('challenge_page AS MATERIALIZED');
251376
expect(sql).not.toContain('COUNT(*) OVER() AS "totalCount"');
252377
expect(sql).toContain('ORDER BY c."endDate" DESC NULLS LAST');
253378
expect(sql).toContain('ORDER BY bp."challengeEndDate" DESC NULLS LAST');

0 commit comments

Comments
 (0)