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
13 changes: 10 additions & 3 deletions .envrc
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
33 changes: 32 additions & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
]
Expand Down
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
16 changes: 16 additions & 0 deletions apps/iris/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
55 changes: 55 additions & 0 deletions apps/iris/README.md
Original file line number Diff line number Diff line change
@@ -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.
10 changes: 5 additions & 5 deletions apps/iris/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
3 changes: 3 additions & 0 deletions infra/k8s/client-api/base/configmap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

REJUDGE_KEY is configured only for client-api, but rejudge requests are published by admin-api.

REJUDGE_KEY should also be published to admin-api or rejudge code invoke logic should be moved into the client-api.

REDIS_HOST: 'redis.redis.svc.cluster.local'
REDIS_PORT: '6379'
TESTCASE_BUCKET_NAME: 'codedang-testcase'
Expand Down
Loading
Loading