From c49b3f06503f63ac370005e78d89800a11448c31 Mon Sep 17 00:00:00 2001 From: Lee Haesung Date: Tue, 1 Sep 2026 10:16:06 +0900 Subject: [PATCH 1/4] feat(judge): configure request routing keys --- apps/backend/.env.example | 9 ++ .../libs/amqp/src/amqp.service.spec.ts | 96 +++++++++++++++++++ apps/backend/libs/amqp/src/amqp.service.ts | 48 ++++++++-- .../libs/constants/src/rabbitmq.constants.ts | 2 +- 4 files changed, 145 insertions(+), 10 deletions(-) create mode 100644 apps/backend/libs/amqp/src/amqp.service.spec.ts diff --git a/apps/backend/.env.example b/apps/backend/.env.example index d5b25d8373..057127d879 100644 --- a/apps/backend/.env.example +++ b/apps/backend/.env.example @@ -14,6 +14,15 @@ REDIS_PASSWORD="skku1234" AWS_ACCESS_KEY_ID="skku" AWS_SECRET_ACCESS_KEY="skku1234" +### RabbitMQ judge routing ### +# TEST_KEY and REJUDGE_KEY fall back to JUDGE_SUBMISSION_ROUTING_KEY when unset, +# preserving the former single judge request queue behavior. +SUBMISSION_KEY="judge.submission" +TEST_KEY="judge.test" +REJUDGE_KEY="judge.rejudge" +# Legacy fallback used by deployments that have not configured the keys above. +JUDGE_SUBMISSION_ROUTING_KEY="judge.submission" + ################################################################################ # NOTE: The following variables are sensitive. # Please copy the values from Notion 'Secrets' page, or fill in your own values. diff --git a/apps/backend/libs/amqp/src/amqp.service.spec.ts b/apps/backend/libs/amqp/src/amqp.service.spec.ts new file mode 100644 index 0000000000..67fd72ae01 --- /dev/null +++ b/apps/backend/libs/amqp/src/amqp.service.spec.ts @@ -0,0 +1,96 @@ +import { type ConfigService } from '@nestjs/config' +import { type AmqpConnection } from '@golevelup/nestjs-rabbitmq' +import { expect } from 'chai' +import { type TraceService } from 'nestjs-otel' +import * as sinon from 'sinon' +import { DEFAULT_SUBMISSION_KEY, EXCHANGE } from '@libs/constants' +import { JudgeAMQPService } from './amqp.service' + +type RoutingKeyConfig = Partial< + Record< + | 'SUBMISSION_KEY' + | 'TEST_KEY' + | 'REJUDGE_KEY' + | 'JUDGE_SUBMISSION_ROUTING_KEY', + string + > +> + +describe('JudgeAMQPService', () => { + const sandbox = sinon.createSandbox() + const publish = sandbox.stub().resolves() + const traceService = { + startSpan: sandbox.stub().returns({ + setAttributes: sandbox.stub(), + end: sandbox.stub() + }) + } as unknown as TraceService + + afterEach(() => { + sandbox.resetHistory() + }) + + function createService(config: RoutingKeyConfig) { + const configService = { + get: (key: keyof RoutingKeyConfig) => config[key] + } as ConfigService + + return new JudgeAMQPService( + { publish } as unknown as AmqpConnection, + traceService, + configService + ) + } + + async function expectRoutingKey( + service: JudgeAMQPService, + routingKey: string, + isTest = false, + isUserTest = false, + isRejudge = false + ) { + await service.publishJudgeRequestMessage( + { request: routingKey }, + 42, + isTest, + isUserTest, + isRejudge + ) + + expect(publish.calledWith(EXCHANGE, routingKey)).to.be.true + sandbox.resetHistory() + } + + it('uses workload-specific routing keys when configured', async () => { + const service = createService({ + SUBMISSION_KEY: 'submission.key', + TEST_KEY: 'test.key', + REJUDGE_KEY: 'rejudge.key' + }) + + await expectRoutingKey(service, 'submission.key') + await expectRoutingKey(service, 'test.key', true) + await expectRoutingKey(service, 'test.key', false, true) + await expectRoutingKey(service, 'rejudge.key', false, false, true) + }) + + it('falls back to the legacy submission routing key for every workload', async () => { + const service = createService({ + JUDGE_SUBMISSION_ROUTING_KEY: 'legacy.submission.key' + }) + + await expectRoutingKey(service, 'legacy.submission.key') + await expectRoutingKey(service, 'legacy.submission.key', true) + await expectRoutingKey(service, 'legacy.submission.key', false, true) + await expectRoutingKey(service, 'legacy.submission.key', false, false, true) + }) + + it('falls back to the built-in submission key when no routing key is configured', async () => { + const service = createService({}) + + await expectRoutingKey(service, DEFAULT_SUBMISSION_KEY) + await expectRoutingKey(service, DEFAULT_SUBMISSION_KEY, true) + await expectRoutingKey(service, DEFAULT_SUBMISSION_KEY, false, true) + await expectRoutingKey(service, DEFAULT_SUBMISSION_KEY, false, false, true) + }) +}) diff --git a/apps/backend/libs/amqp/src/amqp.service.ts b/apps/backend/libs/amqp/src/amqp.service.ts index a557150321..a6528b48b3 100644 --- a/apps/backend/libs/amqp/src/amqp.service.ts +++ b/apps/backend/libs/amqp/src/amqp.service.ts @@ -1,4 +1,5 @@ import { Injectable, Logger } from '@nestjs/common' +import { ConfigService } from '@nestjs/config' import { AmqpConnection, Nack, @@ -22,9 +23,9 @@ import { MESSAGE_PRIORITY_HIGH, MESSAGE_PRIORITY_MIDDLE, MESSAGE_PRIORITY_LOW, - SUBMISSION_KEY, SUBMISSION_MESSAGE_TYPE, - RUN_SUBMISSION_MESSAGE_TYPE + RUN_SUBMISSION_MESSAGE_TYPE, + DEFAULT_SUBMISSION_KEY } from '@libs/constants' @Injectable() @@ -33,7 +34,8 @@ export class JudgeAMQPService { constructor( private readonly amqpConnection: AmqpConnection, - private readonly traceService: TraceService + private readonly traceService: TraceService, + private readonly configService: ConfigService ) {} startSubscription() { @@ -95,12 +97,17 @@ export class JudgeAMQPService { ) span.setAttributes({ submissionId }) - await this.amqpConnection.publish(EXCHANGE, SUBMISSION_KEY, judgeRequest, { - messageId: String(submissionId), - persistent: true, - type: this.calculateMessageType(isTest, isUserTest), - priority: this.calculateMessagePriority(isTest, isUserTest, isRejudge) - }) + await this.amqpConnection.publish( + EXCHANGE, + this.calculateRoutingKey(isTest, isUserTest, isRejudge), + judgeRequest, + { + messageId: String(submissionId), + persistent: true, + type: this.calculateMessageType(isTest, isUserTest), + priority: this.calculateMessagePriority(isTest, isUserTest, isRejudge) + } + ) span.end() } @@ -113,6 +120,29 @@ export class JudgeAMQPService { return JUDGE_MESSAGE_TYPE } + /** + * 채점 workload에 맞는 request queue routing key를 선택합니다. + * 새 workload key가 없는 기존 배포에서는 모든 요청을 submission routing + * key로 보내 단일 queue 동작을 유지합니다. + */ + private calculateRoutingKey( + isTest: boolean, + isUserTest: boolean, + isRejudge: boolean + ) { + if (isRejudge) return this.getRoutingKey('REJUDGE_KEY') + if (isTest || isUserTest) return this.getRoutingKey('TEST_KEY') + return this.getRoutingKey('SUBMISSION_KEY') + } + + private getRoutingKey(key: 'SUBMISSION_KEY' | 'TEST_KEY' | 'REJUDGE_KEY') { + return ( + this.configService.get(key) ?? + this.configService.get('JUDGE_SUBMISSION_ROUTING_KEY') ?? + DEFAULT_SUBMISSION_KEY + ) + } + /** * 메시지 우선순위를 계산하여 반환 */ diff --git a/apps/backend/libs/constants/src/rabbitmq.constants.ts b/apps/backend/libs/constants/src/rabbitmq.constants.ts index 9810b0283a..5ffb50579a 100644 --- a/apps/backend/libs/constants/src/rabbitmq.constants.ts +++ b/apps/backend/libs/constants/src/rabbitmq.constants.ts @@ -3,7 +3,7 @@ export const CONSUME_CHANNEL = 'result-consume-channel' export const EXCHANGE = 'iris.e.direct.judge' -export const SUBMISSION_KEY = 'judge.submission' +export const DEFAULT_SUBMISSION_KEY = 'judge.submission' export const RESULT_KEY = 'judge.result' export const RESULT_QUEUE = 'iris.q.judge.result' From 48e23728fd227b0f6eefd8a6d364854c207a60fa Mon Sep 17 00:00:00 2001 From: Lee Haesung Date: Tue, 1 Sep 2026 10:16:42 +0900 Subject: [PATCH 2/4] docs(judge): describe workload routing --- .../apps/client/src/submission/submission-pub.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/backend/apps/client/src/submission/submission-pub.service.ts b/apps/backend/apps/client/src/submission/submission-pub.service.ts index a371191e18..9d474e7b17 100644 --- a/apps/backend/apps/client/src/submission/submission-pub.service.ts +++ b/apps/backend/apps/client/src/submission/submission-pub.service.ts @@ -22,7 +22,7 @@ export class SubmissionPublicationService { * 2. `isUserTest` 플래그에 따라 다음 중 하나의 채점 요청 객체를 생성 * - 사용자 테스트인 경우: `UserTestcaseJudgeRequest` 객체를 생성하며, 사용자 정의 테스트케이스를 포함 * - 아닌 경우: 일반 채점 요청인 `JudgeRequest` 객체를 생성 - * 3. AMQP 프로토콜을 사용하여 지정된 EXCHANGE와 라우팅 키(SUBMISSION_KEY)를 통해 채점 요청 메시지를 발행 + * 3. AMQP 프로토콜을 사용하여 workload에 맞는 routing key로 채점 요청 메시지를 발행 * * @param {Object} params - 채점 요청 파라미터 * @param {Snippet[]} params.code - 제출한 코드 스니펫 배열 From cb9c87f26cd124d3055fd071e851f704b34a215a Mon Sep 17 00:00:00 2001 From: Lee Haesung Date: Tue, 1 Sep 2026 10:24:14 +0900 Subject: [PATCH 3/4] test(judge): satisfy routing test lint rules --- .../libs/amqp/src/amqp.service.spec.ts | 36 ++++++++++++------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/apps/backend/libs/amqp/src/amqp.service.spec.ts b/apps/backend/libs/amqp/src/amqp.service.spec.ts index 67fd72ae01..40a2cf4103 100644 --- a/apps/backend/libs/amqp/src/amqp.service.spec.ts +++ b/apps/backend/libs/amqp/src/amqp.service.spec.ts @@ -1,7 +1,7 @@ -import { type ConfigService } from '@nestjs/config' -import { type AmqpConnection } from '@golevelup/nestjs-rabbitmq' +import type { ConfigService } from '@nestjs/config' +import type { AmqpConnection } from '@golevelup/nestjs-rabbitmq' import { expect } from 'chai' -import { type TraceService } from 'nestjs-otel' +import type { TraceService } from 'nestjs-otel' import * as sinon from 'sinon' import { DEFAULT_SUBMISSION_KEY, EXCHANGE } from '@libs/constants' import { JudgeAMQPService } from './amqp.service' @@ -30,7 +30,7 @@ describe('JudgeAMQPService', () => { sandbox.resetHistory() }) - function createService(config: RoutingKeyConfig) { + const createService = function (config: RoutingKeyConfig) { const configService = { get: (key: keyof RoutingKeyConfig) => config[key] } as ConfigService @@ -42,7 +42,7 @@ describe('JudgeAMQPService', () => { ) } - async function expectRoutingKey( + const expectRoutingKey = async function ( service: JudgeAMQPService, routingKey: string, isTest = false, @@ -61,12 +61,20 @@ describe('JudgeAMQPService', () => { sandbox.resetHistory() } + const createRoutingKeyConfig = function ( + entries: [keyof RoutingKeyConfig, string][] + ): RoutingKeyConfig { + return Object.fromEntries(entries) + } + it('uses workload-specific routing keys when configured', async () => { - const service = createService({ - SUBMISSION_KEY: 'submission.key', - TEST_KEY: 'test.key', - REJUDGE_KEY: 'rejudge.key' - }) + const service = createService( + createRoutingKeyConfig([ + ['SUBMISSION_KEY', 'submission.key'], + ['TEST_KEY', 'test.key'], + ['REJUDGE_KEY', 'rejudge.key'] + ]) + ) await expectRoutingKey(service, 'submission.key') await expectRoutingKey(service, 'test.key', true) @@ -75,9 +83,11 @@ describe('JudgeAMQPService', () => { }) it('falls back to the legacy submission routing key for every workload', async () => { - const service = createService({ - JUDGE_SUBMISSION_ROUTING_KEY: 'legacy.submission.key' - }) + const service = createService( + createRoutingKeyConfig([ + ['JUDGE_SUBMISSION_ROUTING_KEY', 'legacy.submission.key'] + ]) + ) await expectRoutingKey(service, 'legacy.submission.key') await expectRoutingKey(service, 'legacy.submission.key', true) From 4d144d00edbcf1631e766c164cb117eb384cb8d6 Mon Sep 17 00:00:00 2001 From: Lee Haesung Date: Tue, 1 Sep 2026 21:25:50 +0900 Subject: [PATCH 4/4] fix(judge): keep split routing keys opt-in --- apps/backend/.env.example | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/apps/backend/.env.example b/apps/backend/.env.example index 057127d879..fc3b584f17 100644 --- a/apps/backend/.env.example +++ b/apps/backend/.env.example @@ -15,11 +15,12 @@ AWS_ACCESS_KEY_ID="skku" AWS_SECRET_ACCESS_KEY="skku1234" ### RabbitMQ judge routing ### -# TEST_KEY and REJUDGE_KEY fall back to JUDGE_SUBMISSION_ROUTING_KEY when unset, -# preserving the former single judge request queue behavior. +# TEST_KEY and REJUDGE_KEY are intentionally unset by default. They fall back +# to JUDGE_SUBMISSION_ROUTING_KEY, preserving the former single judge request +# queue behavior until the split queue topology and workers are provisioned. SUBMISSION_KEY="judge.submission" -TEST_KEY="judge.test" -REJUDGE_KEY="judge.rejudge" +# TEST_KEY="judge.test" +# REJUDGE_KEY="judge.rejudge" # Legacy fallback used by deployments that have not configured the keys above. JUDGE_SUBMISSION_ROUTING_KEY="judge.submission"