From b847d3cc559a2bb0eb24a63fe1077a4cc520dffd Mon Sep 17 00:00:00 2001 From: Lee Haesung Date: Tue, 1 Sep 2026 10:16:06 +0900 Subject: [PATCH 1/7] 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 a25470a51423f7485581d9ea130e077bae2a0630 Mon Sep 17 00:00:00 2001 From: Lee Haesung Date: Tue, 1 Sep 2026 10:16:42 +0900 Subject: [PATCH 2/7] 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 6e6006e36892148676022dcb7ec0914c9afe688c Mon Sep 17 00:00:00 2001 From: Lee Haesung Date: Tue, 1 Sep 2026 10:24:14 +0900 Subject: [PATCH 3/7] 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 ec45abb53530da453c797a67b7bbb2f95ac28ff7 Mon Sep 17 00:00:00 2001 From: Lee Haesung Date: Tue, 1 Sep 2026 21:25:50 +0900 Subject: [PATCH 4/7] 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" From c775f74f984bce627c05ff65276868356d79d3c4 Mon Sep 17 00:00:00 2001 From: Lee Haesung Date: Tue, 1 Sep 2026 10:19:39 +0900 Subject: [PATCH 5/7] feat(judge): deploy workload-specific worker pools --- .envrc | 13 +++- apps/iris/.env.example | 10 +++ apps/iris/README.md | 51 +++++++++++++ apps/iris/main.go | 10 +-- infra/k8s/client-api/base/configmap.yaml | 3 + infra/k8s/iris/base/configmap.yaml | 10 +-- infra/k8s/iris/base/deployment-rejudge.yaml | 67 +++++++++++++++++ infra/k8s/iris/base/deployment-test.yaml | 67 +++++++++++++++++ infra/k8s/iris/base/kustomization.yaml | 2 + .../overlays/production/kustomization.yaml | 8 +++ .../iris/overlays/stage/kustomization.yaml | 8 +++ infra/k8s/rabbitmq/base/topology-judging.yaml | 58 +++++++++++++++ scripts/init-rabbitmq.ts | 71 +++++++++++++------ 13 files changed, 344 insertions(+), 34 deletions(-) create mode 100644 apps/iris/README.md create mode 100644 infra/k8s/iris/base/deployment-rejudge.yaml create mode 100644 infra/k8s/iris/base/deployment-test.yaml diff --git a/.envrc b/.envrc index b0ae683268..9198631886 100644 --- a/.envrc +++ b/.envrc @@ -18,13 +18,20 @@ export RABBITMQ_DEFAULT_VHOST="vh" export JUDGE_EXCHANGE_NAME="iris.e.direct.judge" export JUDGE_SUBMISSION_QUEUE_NAME="client.q.judge.submission" +export JUDGE_TEST_QUEUE_NAME="client.q.judge.test" +export JUDGE_REJUDGE_QUEUE_NAME="client.q.judge.rejudge" export JUDGE_SUBMISSION_ROUTING_KEY="judge.submission" +export SUBMISSION_KEY="judge.submission" +export TEST_KEY="judge.test" +export REJUDGE_KEY="judge.rejudge" export JUDGE_RESULT_QUEUE_NAME="iris.q.judge.result" export JUDGE_RESULT_ROUTING_KEY="judge.result" -export JUDGE_SUBMISSION_CONSUMER_CONNECTION_NAME="iris-consumer" -export JUDGE_SUBMISSION_TAG="consumer-tag" -export JUDGE_SUBMISSION_PRODUCER_CONNECTION_NAME="iris-producer" +export JUDGE_REQUEST_CONSUMER_CONNECTION_NAME="iris-consumer" +export JUDGE_REQUEST_QUEUE_NAME="$JUDGE_SUBMISSION_QUEUE_NAME" +export JUDGE_REQUEST_CONSUMER_TAG="consumer-tag" +export JUDGE_RESULT_PRODUCER_CONNECTION_NAME="iris-producer" +export JUDGE_RESULT_EXCHANGE_NAME="$JUDGE_EXCHANGE_NAME" export CHECK_EXCHANGE_NAME="plag.e.direct.check" export CHECK_QUEUE_NAME="client.q.check.request" diff --git a/apps/iris/.env.example b/apps/iris/.env.example index 819a39cde4..cab39ffac9 100644 --- a/apps/iris/.env.example +++ b/apps/iris/.env.example @@ -9,6 +9,16 @@ DATABASE_URL="postgresql://postgres:1234@127.0.0.1:5433/skkuding?schema=public" AWS_ACCESS_KEY_ID="skku" AWS_SECRET_ACCESS_KEY="skku1234" +### RabbitMQ judge ### +# By default Iris consumes the submission queue. Override the three request +# values together to run a test or rejudge worker; see README.md. +JUDGE_REQUEST_CONSUMER_CONNECTION_NAME="iris-consumer" +JUDGE_REQUEST_QUEUE_NAME="client.q.judge.submission" +JUDGE_REQUEST_CONSUMER_TAG="consumer-tag" +JUDGE_RESULT_PRODUCER_CONNECTION_NAME="iris-producer" +JUDGE_RESULT_EXCHANGE_NAME="iris.e.direct.judge" +JUDGE_RESULT_ROUTING_KEY="judge.result" + ### Polygon tools ### POLYGON_TOOL_TIME_LIMIT_MS="2000" POLYGON_TOOL_MEMORY_LIMIT_BYTES="536870912" diff --git a/apps/iris/README.md b/apps/iris/README.md new file mode 100644 index 0000000000..59975cab68 --- /dev/null +++ b/apps/iris/README.md @@ -0,0 +1,51 @@ +# Iris local judge workers + +Iris is configured as a generic judge request consumer. The queue it consumes +is selected by `JUDGE_REQUEST_*`; it always publishes results through +`JUDGE_RESULT_*`. + +## Split local topology + +After loading the root `.envrc` and starting local RabbitMQ, initialize the +three request queues and the shared result queue: + +```sh +pnpm init:rabbitmq +``` + +Start one process for each workload in separate terminals: + +```sh +# submission +go run . + +# test +JUDGE_REQUEST_CONSUMER_CONNECTION_NAME=iris-test-consumer \ +JUDGE_REQUEST_QUEUE_NAME="$JUDGE_TEST_QUEUE_NAME" \ +JUDGE_REQUEST_CONSUMER_TAG=iris-test-consumer-tag \ +go run . + +# rejudge +JUDGE_REQUEST_CONSUMER_CONNECTION_NAME=iris-rejudge-consumer \ +JUDGE_REQUEST_QUEUE_NAME="$JUDGE_REJUDGE_QUEUE_NAME" \ +JUDGE_REQUEST_CONSUMER_TAG=iris-rejudge-consumer-tag \ +go run . +``` + +`SUBMISSION_KEY`, `TEST_KEY`, and `REJUDGE_KEY` default to distinct routing +keys in `.envrc`, so each workload reaches only its corresponding queue. + +## Legacy single-queue mode + +To reproduce the pre-split local topology, omit the new test and rejudge keys +or set them equal to `SUBMISSION_KEY` before initializing RabbitMQ, then run +only the submission worker: + +```sh +unset TEST_KEY REJUDGE_KEY +pnpm init:rabbitmq +go run . +``` + +Nest falls back to `JUDGE_SUBMISSION_ROUTING_KEY`, so submission, test, and +rejudge requests are all routed to the submission queue. diff --git a/apps/iris/main.go b/apps/iris/main.go index 8aaa48447c..067c6ad8ca 100644 --- a/apps/iris/main.go +++ b/apps/iris/main.go @@ -140,14 +140,14 @@ func main() { connector.Providers{Router: routeProvider, Logger: logProvider}, rabbitmq.ConsumerConfig{ AmqpURI: uri, - ConnectionName: utils.MustGetenvOrElseThrow("JUDGE_SUBMISSION_CONSUMER_CONNECTION_NAME", logProvider), - QueueName: utils.MustGetenvOrElseThrow("JUDGE_SUBMISSION_QUEUE_NAME", logProvider), - Ctag: utils.MustGetenvOrElseThrow("JUDGE_SUBMISSION_TAG", logProvider), + ConnectionName: utils.MustGetenvOrElseThrow("JUDGE_REQUEST_CONSUMER_CONNECTION_NAME", logProvider), + QueueName: utils.MustGetenvOrElseThrow("JUDGE_REQUEST_QUEUE_NAME", logProvider), + Ctag: utils.MustGetenvOrElseThrow("JUDGE_REQUEST_CONSUMER_TAG", logProvider), }, rabbitmq.ProducerConfig{ AmqpURI: uri, - ConnectionName: utils.MustGetenvOrElseThrow("JUDGE_SUBMISSION_PRODUCER_CONNECTION_NAME", logProvider), - ExchangeName: utils.MustGetenvOrElseThrow("JUDGE_EXCHANGE_NAME", logProvider), + ConnectionName: utils.MustGetenvOrElseThrow("JUDGE_RESULT_PRODUCER_CONNECTION_NAME", logProvider), + ExchangeName: utils.MustGetenvOrElseThrow("JUDGE_RESULT_EXCHANGE_NAME", logProvider), RoutingKey: utils.MustGetenvOrElseThrow("JUDGE_RESULT_ROUTING_KEY", logProvider), }, ).Connect(context.Background()) diff --git a/infra/k8s/client-api/base/configmap.yaml b/infra/k8s/client-api/base/configmap.yaml index ebb451fcbd..68733d36e5 100644 --- a/infra/k8s/client-api/base/configmap.yaml +++ b/infra/k8s/client-api/base/configmap.yaml @@ -11,6 +11,9 @@ data: RABBITMQ_HOST: 'rabbitmq.rabbitmq.svc.cluster.local' RABBITMQ_PORT: '5671' RABBITMQ_SSL: 'true' + SUBMISSION_KEY: 'judge.submission' + TEST_KEY: 'judge.test' + REJUDGE_KEY: 'judge.rejudge' REDIS_HOST: 'redis.redis.svc.cluster.local' REDIS_PORT: '6379' TESTCASE_BUCKET_NAME: 'codedang-testcase' diff --git a/infra/k8s/iris/base/configmap.yaml b/infra/k8s/iris/base/configmap.yaml index 572192b982..8c96484abb 100644 --- a/infra/k8s/iris/base/configmap.yaml +++ b/infra/k8s/iris/base/configmap.yaml @@ -12,9 +12,9 @@ data: RABBITMQ_SSL: 'true' RABBITMQ_DEFAULT_VHOST: 'vh' OTEL_EXPORTER_OTLP_ENDPOINT_URL: 'simplest-collector.monitoring-otel.svc.cluster.local:4317' - JUDGE_SUBMISSION_CONSUMER_CONNECTION_NAME: 'iris-consumer' - JUDGE_SUBMISSION_QUEUE_NAME: 'client.q.judge.submission' - JUDGE_SUBMISSION_TAG: 'consumer-tag' - JUDGE_SUBMISSION_PRODUCER_CONNECTION_NAME: 'iris-producer' - JUDGE_EXCHANGE_NAME: 'iris.e.direct.judge' + JUDGE_REQUEST_CONSUMER_CONNECTION_NAME: 'iris-consumer' + JUDGE_REQUEST_QUEUE_NAME: 'client.q.judge.submission' + JUDGE_REQUEST_CONSUMER_TAG: 'consumer-tag' + JUDGE_RESULT_PRODUCER_CONNECTION_NAME: 'iris-producer' + JUDGE_RESULT_EXCHANGE_NAME: 'iris.e.direct.judge' JUDGE_RESULT_ROUTING_KEY: 'judge.result' diff --git a/infra/k8s/iris/base/deployment-rejudge.yaml b/infra/k8s/iris/base/deployment-rejudge.yaml new file mode 100644 index 0000000000..be3f054266 --- /dev/null +++ b/infra/k8s/iris/base/deployment-rejudge.yaml @@ -0,0 +1,67 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: iris-rejudge + namespace: iris +spec: + replicas: 1 + selector: + matchLabels: + app: iris-rejudge + template: + metadata: + labels: + app: iris-rejudge + spec: + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + app: iris-rejudge + containers: + - name: iris + image: ghcr.io/skkuding/codedang-iris + resources: + limits: + cpu: 1 + memory: 1.5Gi + requests: + cpu: 1 + memory: 1.5Gi + volumeMounts: + - name: cgroup + mountPath: /sys/fs/cgroup + readOnly: false + securityContext: + privileged: true + envFrom: + - configMapRef: + name: iris-env + - secretRef: + name: aws-credentials + - secretRef: + name: database-credentials + env: + - name: JUDGE_REQUEST_CONSUMER_CONNECTION_NAME + value: iris-rejudge-consumer + - name: JUDGE_REQUEST_QUEUE_NAME + value: client.q.judge.rejudge + - name: JUDGE_REQUEST_CONSUMER_TAG + value: iris-rejudge-consumer-tag + - name: RABBITMQ_DEFAULT_USER + valueFrom: + secretKeyRef: + name: rabbitmq-credentials + key: username + - name: RABBITMQ_DEFAULT_PASS + valueFrom: + secretKeyRef: + name: rabbitmq-credentials + key: password + volumes: + - name: cgroup + hostPath: + path: /sys/fs/cgroup + type: Directory diff --git a/infra/k8s/iris/base/deployment-test.yaml b/infra/k8s/iris/base/deployment-test.yaml new file mode 100644 index 0000000000..65ff4d82d4 --- /dev/null +++ b/infra/k8s/iris/base/deployment-test.yaml @@ -0,0 +1,67 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: iris-test + namespace: iris +spec: + replicas: 1 + selector: + matchLabels: + app: iris-test + template: + metadata: + labels: + app: iris-test + spec: + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + app: iris-test + containers: + - name: iris + image: ghcr.io/skkuding/codedang-iris + resources: + limits: + cpu: 1 + memory: 1.5Gi + requests: + cpu: 1 + memory: 1.5Gi + volumeMounts: + - name: cgroup + mountPath: /sys/fs/cgroup + readOnly: false + securityContext: + privileged: true + envFrom: + - configMapRef: + name: iris-env + - secretRef: + name: aws-credentials + - secretRef: + name: database-credentials + env: + - name: JUDGE_REQUEST_CONSUMER_CONNECTION_NAME + value: iris-test-consumer + - name: JUDGE_REQUEST_QUEUE_NAME + value: client.q.judge.test + - name: JUDGE_REQUEST_CONSUMER_TAG + value: iris-test-consumer-tag + - name: RABBITMQ_DEFAULT_USER + valueFrom: + secretKeyRef: + name: rabbitmq-credentials + key: username + - name: RABBITMQ_DEFAULT_PASS + valueFrom: + secretKeyRef: + name: rabbitmq-credentials + key: password + volumes: + - name: cgroup + hostPath: + path: /sys/fs/cgroup + type: Directory diff --git a/infra/k8s/iris/base/kustomization.yaml b/infra/k8s/iris/base/kustomization.yaml index 93588ffa1f..1f8ca32790 100644 --- a/infra/k8s/iris/base/kustomization.yaml +++ b/infra/k8s/iris/base/kustomization.yaml @@ -5,6 +5,8 @@ namespace: iris resources: - configmap.yaml - deployment.yaml + - deployment-test.yaml + - deployment-rejudge.yaml - namespace.yaml - rabbitmq-credentials.yaml diff --git a/infra/k8s/iris/overlays/production/kustomization.yaml b/infra/k8s/iris/overlays/production/kustomization.yaml index d5be5eba8e..be4055b342 100644 --- a/infra/k8s/iris/overlays/production/kustomization.yaml +++ b/infra/k8s/iris/overlays/production/kustomization.yaml @@ -16,3 +16,11 @@ patches: target: kind: Deployment name: iris + - path: deployment-patch.yaml + target: + kind: Deployment + name: iris-test + - path: deployment-patch.yaml + target: + kind: Deployment + name: iris-rejudge diff --git a/infra/k8s/iris/overlays/stage/kustomization.yaml b/infra/k8s/iris/overlays/stage/kustomization.yaml index f7826e1434..e1e81a2cb1 100644 --- a/infra/k8s/iris/overlays/stage/kustomization.yaml +++ b/infra/k8s/iris/overlays/stage/kustomization.yaml @@ -20,3 +20,11 @@ patches: target: kind: Deployment name: iris + - path: deployment-patch.yaml + target: + kind: Deployment + name: iris-test + - path: deployment-patch.yaml + target: + kind: Deployment + name: iris-rejudge diff --git a/infra/k8s/rabbitmq/base/topology-judging.yaml b/infra/k8s/rabbitmq/base/topology-judging.yaml index 5a8edf3c2c..267342c62a 100644 --- a/infra/k8s/rabbitmq/base/topology-judging.yaml +++ b/infra/k8s/rabbitmq/base/topology-judging.yaml @@ -27,6 +27,34 @@ spec: rabbitmqClusterReference: name: rabbitmq --- +# Queue for Test +apiVersion: rabbitmq.com/v1beta1 +kind: Queue +metadata: + name: test-queue + namespace: rabbitmq +spec: + name: client.q.judge.test + vhost: vh + autoDelete: false + durable: true + rabbitmqClusterReference: + name: rabbitmq +--- +# Queue for Rejudge +apiVersion: rabbitmq.com/v1beta1 +kind: Queue +metadata: + name: rejudge-queue + namespace: rabbitmq +spec: + name: client.q.judge.rejudge + vhost: vh + autoDelete: false + durable: true + rabbitmqClusterReference: + name: rabbitmq +--- # Queue for Result apiVersion: rabbitmq.com/v1beta1 kind: Queue @@ -56,6 +84,36 @@ spec: rabbitmqClusterReference: name: rabbitmq --- +# Binding for Test +apiVersion: rabbitmq.com/v1beta1 +kind: Binding +metadata: + name: binding-test + namespace: rabbitmq +spec: + vhost: vh + source: iris.e.direct.judge + destination: client.q.judge.test + destinationType: queue + routingKey: judge.test + rabbitmqClusterReference: + name: rabbitmq +--- +# Binding for Rejudge +apiVersion: rabbitmq.com/v1beta1 +kind: Binding +metadata: + name: binding-rejudge + namespace: rabbitmq +spec: + vhost: vh + source: iris.e.direct.judge + destination: client.q.judge.rejudge + destinationType: queue + routingKey: judge.rejudge + rabbitmqClusterReference: + name: rabbitmq +--- # Binding for Result apiVersion: rabbitmq.com/v1beta1 kind: Binding diff --git a/scripts/init-rabbitmq.ts b/scripts/init-rabbitmq.ts index ccc693a9c2..33709d5dc3 100644 --- a/scripts/init-rabbitmq.ts +++ b/scripts/init-rabbitmq.ts @@ -8,6 +8,12 @@ const config = { vhost: process.env.RABBITMQ_DEFAULT_VHOST } +function requireEnv(name: string): string { + const value = process.env[name] + if (!value) throw new Error(`${name} is required`) + return value +} + async function setupRabbitMQ() { const url = `amqp://${config.username}:${config.password}@${config.host}:${config.port}/${config.vhost}` const connection = await connect(url) @@ -17,48 +23,71 @@ async function setupRabbitMQ() { console.log('Connection to RabbitMQ successful.') - const exchangeName = process.env.JUDGE_EXCHANGE_NAME! + const exchangeName = requireEnv('JUDGE_EXCHANGE_NAME') await channel.assertExchange(exchangeName, 'direct', { durable: true }) - const resultQueueName = process.env.JUDGE_RESULT_QUEUE_NAME! + const resultQueueName = requireEnv('JUDGE_RESULT_QUEUE_NAME') await channel.assertQueue(resultQueueName, { durable: true }) - const submissionQueueName = process.env.JUDGE_SUBMISSION_QUEUE_NAME! - await channel.assertQueue(submissionQueueName, { - durable: true, - arguments: { 'x-max-priority': 3 } - }) - - const resultRoutingKey = process.env.JUDGE_RESULT_ROUTING_KEY! + const resultRoutingKey = requireEnv('JUDGE_RESULT_ROUTING_KEY') await channel.bindQueue(resultQueueName, exchangeName, resultRoutingKey) - const submissionRoutingKey = process.env.JUDGE_SUBMISSION_ROUTING_KEY! - await channel.bindQueue( - submissionQueueName, - exchangeName, - submissionRoutingKey - ) - - const checkExchangeName = process.env.CHECK_EXCHANGE_NAME! + const submissionRoutingKey = + process.env.SUBMISSION_KEY ?? + process.env.JUDGE_SUBMISSION_ROUTING_KEY ?? + 'judge.submission' + const requestQueues: { name: string; routingKey: string }[] = [ + { + name: requireEnv('JUDGE_SUBMISSION_QUEUE_NAME'), + routingKey: submissionRoutingKey + } + ] + + const testRoutingKey = process.env.TEST_KEY + if (testRoutingKey && testRoutingKey !== submissionRoutingKey) { + requestQueues.push({ + name: requireEnv('JUDGE_TEST_QUEUE_NAME'), + routingKey: testRoutingKey + }) + } + + const rejudgeRoutingKey = process.env.REJUDGE_KEY + if (rejudgeRoutingKey && rejudgeRoutingKey !== submissionRoutingKey) { + requestQueues.push({ + name: requireEnv('JUDGE_REJUDGE_QUEUE_NAME'), + routingKey: rejudgeRoutingKey + }) + } + + for (const requestQueue of requestQueues) { + await channel.assertQueue(requestQueue.name, { durable: true }) + await channel.bindQueue( + requestQueue.name, + exchangeName, + requestQueue.routingKey + ) + } + + const checkExchangeName = requireEnv('CHECK_EXCHANGE_NAME') await channel.assertExchange(checkExchangeName, 'direct', { durable: true }) - const checkResultQueueName = process.env.CHECK_RESULT_QUEUE_NAME! + const checkResultQueueName = requireEnv('CHECK_RESULT_QUEUE_NAME') await channel.assertQueue(checkResultQueueName, { durable: true }) - const checkRequestQueueName = process.env.CHECK_QUEUE_NAME! + const checkRequestQueueName = requireEnv('CHECK_QUEUE_NAME') await channel.assertQueue(checkRequestQueueName, { durable: true, arguments: { 'x-max-priority': 1 } }) - const checkResultRoutingKey = process.env.CHECK_RESULT_ROUTING_KEY! + const checkResultRoutingKey = requireEnv('CHECK_RESULT_ROUTING_KEY') await channel.bindQueue( checkResultQueueName, checkExchangeName, checkResultRoutingKey ) - const checkRequestRoutingKey = process.env.CHECK_ROUTING_KEY! + const checkRequestRoutingKey = requireEnv('CHECK_ROUTING_KEY') await channel.bindQueue( checkRequestQueueName, checkExchangeName, From ebfd858457640680bb55a1fbfab223484b75c4e6 Mon Sep 17 00:00:00 2001 From: Lee Haesung Date: Tue, 1 Sep 2026 10:27:17 +0900 Subject: [PATCH 6/7] fix(judge): preserve local submission queue priority --- scripts/init-rabbitmq.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/scripts/init-rabbitmq.ts b/scripts/init-rabbitmq.ts index 33709d5dc3..4216bb8f4a 100644 --- a/scripts/init-rabbitmq.ts +++ b/scripts/init-rabbitmq.ts @@ -36,10 +36,15 @@ async function setupRabbitMQ() { process.env.SUBMISSION_KEY ?? process.env.JUDGE_SUBMISSION_ROUTING_KEY ?? 'judge.submission' - const requestQueues: { name: string; routingKey: string }[] = [ + const requestQueues: { + name: string + routingKey: string + maxPriority?: number + }[] = [ { name: requireEnv('JUDGE_SUBMISSION_QUEUE_NAME'), - routingKey: submissionRoutingKey + routingKey: submissionRoutingKey, + maxPriority: 3 } ] @@ -60,7 +65,12 @@ async function setupRabbitMQ() { } for (const requestQueue of requestQueues) { - await channel.assertQueue(requestQueue.name, { durable: true }) + await channel.assertQueue(requestQueue.name, { + durable: true, + ...(requestQueue.maxPriority && { + arguments: { 'x-max-priority': requestQueue.maxPriority } + }) + }) await channel.bindQueue( requestQueue.name, exchangeName, From bbbc6b74e8e290c2cda10da0ecc9de1774b45f09 Mon Sep 17 00:00:00 2001 From: Lee Haesung Date: Wed, 2 Sep 2026 17:13:15 +0900 Subject: [PATCH 7/7] feat(dev): configure local Iris worker launches --- .vscode/launch.json | 33 ++++++++++++++++++++++++++++++++- apps/iris/.env.example | 6 ++++++ apps/iris/README.md | 4 ++++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 6981cca6c0..49ad1b9dd5 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -34,10 +34,41 @@ { "type": "go", "request": "launch", - "name": "Iris 🐶", + "name": "Iris Submission 🐶", "cwd": "${workspaceFolder}/apps/iris", "program": "${workspaceFolder}/apps/iris/main.go", "console": "integratedTerminal", + "envFile": "${workspaceFolder}/apps/iris/.env", + "asRoot": true + }, + { + "type": "go", + "request": "launch", + "name": "Iris Test 🐶", + "cwd": "${workspaceFolder}/apps/iris", + "program": "${workspaceFolder}/apps/iris/main.go", + "console": "integratedTerminal", + "envFile": "${workspaceFolder}/apps/iris/.env", + "env": { + "JUDGE_REQUEST_CONSUMER_CONNECTION_NAME": "iris-test-consumer", + "JUDGE_REQUEST_QUEUE_NAME": "client.q.judge.test", + "JUDGE_REQUEST_CONSUMER_TAG": "iris-test-consumer-tag" + }, + "asRoot": true + }, + { + "type": "go", + "request": "launch", + "name": "Iris Rejudge 🐶", + "cwd": "${workspaceFolder}/apps/iris", + "program": "${workspaceFolder}/apps/iris/main.go", + "console": "integratedTerminal", + "envFile": "${workspaceFolder}/apps/iris/.env", + "env": { + "JUDGE_REQUEST_CONSUMER_CONNECTION_NAME": "iris-rejudge-consumer", + "JUDGE_REQUEST_QUEUE_NAME": "client.q.judge.rejudge", + "JUDGE_REQUEST_CONSUMER_TAG": "iris-rejudge-consumer-tag" + }, "asRoot": true } ] diff --git a/apps/iris/.env.example b/apps/iris/.env.example index cab39ffac9..0f8b2ca143 100644 --- a/apps/iris/.env.example +++ b/apps/iris/.env.example @@ -12,6 +12,12 @@ AWS_SECRET_ACCESS_KEY="skku1234" ### RabbitMQ judge ### # By default Iris consumes the submission queue. Override the three request # values together to run a test or rejudge worker; see README.md. +RABBITMQ_HOST="127.0.0.1" +RABBITMQ_PORT="5672" +RABBITMQ_SSL="false" +RABBITMQ_DEFAULT_USER="skku" +RABBITMQ_DEFAULT_PASS="1234" +RABBITMQ_DEFAULT_VHOST="vh" JUDGE_REQUEST_CONSUMER_CONNECTION_NAME="iris-consumer" JUDGE_REQUEST_QUEUE_NAME="client.q.judge.submission" JUDGE_REQUEST_CONSUMER_TAG="consumer-tag" diff --git a/apps/iris/README.md b/apps/iris/README.md index 59975cab68..afdea1ce4b 100644 --- a/apps/iris/README.md +++ b/apps/iris/README.md @@ -32,6 +32,10 @@ JUDGE_REQUEST_CONSUMER_TAG=iris-rejudge-consumer-tag \ go run . ``` +Inside the devcontainer, the VS Code launch configurations `Iris Submission`, +`Iris Test`, and `Iris Rejudge` provide the same three worker processes. Run +all three configurations to exercise the split topology locally. + `SUBMISSION_KEY`, `TEST_KEY`, and `REJUDGE_KEY` default to distinct routing keys in `.envrc`, so each workload reaches only its corresponding queue.