Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions apps/backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,16 @@ REDIS_PASSWORD="skku1234"
AWS_ACCESS_KEY_ID="skku"
AWS_SECRET_ACCESS_KEY="skku1234"

### RabbitMQ judge routing ###
# 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"
# 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 - 제출한 코드 스니펫 배열
Expand Down
106 changes: 106 additions & 0 deletions apps/backend/libs/amqp/src/amqp.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
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()
})

const createService = function (config: RoutingKeyConfig) {
const configService = {
get: (key: keyof RoutingKeyConfig) => config[key]
} as ConfigService

return new JudgeAMQPService(
{ publish } as unknown as AmqpConnection,
traceService,
configService
)
}

const expectRoutingKey = async function (
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()
}

const createRoutingKeyConfig = function (
entries: [keyof RoutingKeyConfig, string][]
): RoutingKeyConfig {
return Object.fromEntries(entries)
}

it('uses workload-specific routing keys when configured', async () => {
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)
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(
createRoutingKeyConfig([
['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)
})
})
48 changes: 39 additions & 9 deletions apps/backend/libs/amqp/src/amqp.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Injectable, Logger } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
import {
AmqpConnection,
Nack,
Expand All @@ -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()
Expand All @@ -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() {
Expand Down Expand Up @@ -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()
}

Expand All @@ -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<string>(key) ??
this.configService.get<string>('JUDGE_SUBMISSION_ROUTING_KEY') ??
DEFAULT_SUBMISSION_KEY
)
}

/**
* 메시지 우선순위를 계산하여 반환
*/
Expand Down
2 changes: 1 addition & 1 deletion apps/backend/libs/constants/src/rabbitmq.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Loading