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/.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/backend/.env.example b/apps/backend/.env.example index d5b25d8373..fc3b584f17 100644 --- a/apps/backend/.env.example +++ b/apps/backend/.env.example @@ -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. 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 - ์ œ์ถœํ•œ ์ฝ”๋“œ ์Šค๋‹ˆํŽซ ๋ฐฐ์—ด 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..40a2cf4103 --- /dev/null +++ b/apps/backend/libs/amqp/src/amqp.service.spec.ts @@ -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) + }) +}) 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' diff --git a/apps/iris/.env.example b/apps/iris/.env.example index 819a39cde4..0f8b2ca143 100644 --- a/apps/iris/.env.example +++ b/apps/iris/.env.example @@ -9,6 +9,22 @@ 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. +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" +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..afdea1ce4b --- /dev/null +++ b/apps/iris/README.md @@ -0,0 +1,55 @@ +# 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 . +``` + +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. + +## 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..4216bb8f4a 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,81 @@ 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 + maxPriority?: number + }[] = [ + { + name: requireEnv('JUDGE_SUBMISSION_QUEUE_NAME'), + routingKey: submissionRoutingKey, + maxPriority: 3 + } + ] + + 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, + ...(requestQueue.maxPriority && { + arguments: { 'x-max-priority': requestQueue.maxPriority } + }) + }) + 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,