개발 가이드¶
이 가이드는 Continuum Router 프로젝트의 개발 환경 설정, 아키텍처, 테스트, 기여 방법을 다룹니다.
목차¶
사전 요구 사항¶
필수 도구¶
- Rust: 1.97 이상 (Rust 설치); 루트 라우터 패키지와 독립
perf/하네스는 Rust edition 2024를 사용하고 저장소는 재현 가능한 CI와 로컬 린트를 위해rust-toolchain.toml에서 Rust 1.97.1을 고정합니다. - Git: 버전 관리용
- C 컴파일러: gcc, clang, 또는 MSVC (네이티브 의존성용)
Rust edition, MSRV, 그리고 포매팅¶
루트 continuum-router 패키지와 독립 perf/ 하네스는 Rust edition 2024와 rust-version = "1.97"를 선언합니다. 벤더링된 crates/continuum-protocol 패키지는 upstream Continuum Hub 프로토콜 크레이트를 반영하므로 의도적으로 edition 2021에 남겨 두고, MSRV는 허브 워크스페이스와 같은 Rust 1.97을 씁니다. Cargo는 이 혼합 edition 그래프를 지원하며 라우터 CI는 해당 path dependency를 별도로 검증합니다. 고정된 허브 리비전과 바이트 단위로 같은지도 CI가 확인합니다(벤더링된 continuum-protocol 크레이트 참고). rustfmt.toml은 style_edition = "2021"을 고정해 별도 포매팅 전용 마이그레이션이 승인되기 전까지 cargo fmt와 에디터 통합이 기존 포매팅 스타일을 유지하도록 합니다.
권장 도구¶
- cargo-watch: 개발 중 자동 재컴파일
- cargo-edit: 의존성 관리
- cargo-audit: 보안 취약점 검사
- cargo-flamegraph: 성능 프로파일링
- rust-analyzer: IDE 지원 (VS Code, IntelliJ, vim/neovim)
개발 도구 설치¶
# 필수 개발 도구 설치
cargo install cargo-watch cargo-edit cargo-audit cargo-flamegraph
# 코드 커버리지 도구 설치
cargo install cargo-tarpaulin
# 벤치마킹 도구 설치
cargo install cargo-criterion
개발 설정¶
복제 및 설정¶
# 저장소 복제
git clone https://github.com/lablup/continuum-router.git
cd continuum-router
# 컴파일 확인
cargo check
# 초기 테스트 스위트 실행
cargo test
# 개발 버전 빌드
cargo build
make setup은 개발 도구를 설치하고, Git이 저장소에 포함된 .githooks/의 훅을 사용하도록 core.hooksPath를 설정합니다. pre-commit 훅은 cargo fmt와 Clippy를 실행하며, 둘 중 하나가 작업 트리를 바꾸면 커밋을 중단하므로, 변경 사항을 스테이징한 뒤 다시 커밋하세요. core.hooksPath가 이미 다른 디렉터리로 설정되어 있으면 make setup은 값을 바꾸지 않고 전환 명령을 출력합니다.
IDE 설정¶
VS Code¶
rust-analyzer확장 설치- 디버깅을 위해
CodeLLDB확장 설치 - 제공된
.vscode/settings.json사용:
{
"rust-analyzer.cargo.features": "all",
"rust-analyzer.checkOnSave.command": "clippy",
"editor.formatOnSave": true,
"[rust]": {
"editor.defaultFormatter": "rust-lang.rust-analyzer"
}
}
IntelliJ IDEA / CLion¶
- Rust 플러그인 설치
- 프로젝트 루트 열기
- Preferences → Build → Cargo에서 Cargo 설정 구성
Vim/Neovim¶
- Mason 또는 수동으로
rust-analyzer설치 - LSP 클라이언트 구성 (nvim-lspconfig, coc.nvim 등)
- nvim-lspconfig 설정 예제:
require('lspconfig').rust_analyzer.setup({
settings = {
["rust-analyzer"] = {
cargo = { features = "all" },
checkOnSave = { command = "clippy" }
}
}
})
프로젝트 구조¶
계층 아키텍처¶
프로젝트는 깔끔한 4계층 아키텍처를 따릅니다:
디렉토리 구조¶
src/
├── main.rs # 애플리케이션 진입점 및 CLI
├── lib.rs # 라이브러리 익스포트 및 모듈 선언
│
├── core/ # 핵심 도메인 계층 (외부 의존성 없음)
│ ├── mod.rs # Core 모듈 익스포트
│ ├── models/ # 도메인 모델
│ │ ├── backend.rs # 백엔드 설정 및 상태
│ │ ├── model.rs # LLM 모델 표현
│ │ ├── request.rs # 요청/응답 모델
│ │ └── health.rs # 헬스 체크 모델
│ ├── errors.rs # 오류 타입 및 처리
│ ├── traits.rs # 도메인 트레이트 및 인터페이스
│ ├── retry/ # 재시도 로직 및 정책
│ │ ├── policy.rs # 재시도 정책
│ │ └── backoff.rs # 백오프 전략
│ └── config/ # 설정 모델
│ ├── mod.rs # Config 모듈 익스포트
│ ├── models.rs # 설정 구조체
│ └── validation.rs # 설정 유효성 검사 로직
│
├── services/ # 비즈니스 로직 계층
│ ├── mod.rs # 모듈 익스포트
│ ├── model_service.rs # 모델 집계 및 캐싱
│ ├── proxy_service.rs # 요청 라우팅 및 프록시
│ ├── health_service.rs # 헬스 모니터링
│ └── streaming/ # SSE 스트리밍 지원
│ ├── mod.rs # Streaming 모듈 익스포트
│ ├── parser.rs # SSE 이벤트 파싱
│ └── handler.rs # 스트림 처리 로직
│
├── infrastructure/ # 외부 통합 계층
│ ├── mod.rs # Infrastructure 익스포트
│ ├── backends/ # 백엔드 클라이언트 구현
│ │ ├── mod.rs # 백엔드 트레이트 및 팩토리
│ │ ├── openai.rs # OpenAI 호환 백엔드
│ │ ├── ollama.rs # Ollama 백엔드
│ │ └── vllm.rs # vLLM 백엔드
│ ├── cache/ # 캐싱 구현
│ │ ├── mod.rs # 캐시 트레이트
│ │ ├── lru.rs # LRU 캐시
│ │ └── ttl.rs # TTL 기반 캐시
│ ├── config/ # 설정 로딩
│ │ ├── loader.rs # 설정 파일 로딩
│ │ └── watcher.rs # 핫 리로드 지원
│ └── common/http_client.rs # 풀링이 있는 HTTP 클라이언트 팩토리
│
├── http/ # HTTP 계층
│ ├── mod.rs # HTTP 모듈 익스포트
│ ├── routes.rs # 라우트 정의
│ ├── handlers/ # 요청 핸들러
│ │ ├── chat.rs # 채팅 완료 엔드포인트
│ │ ├── models.rs # 모델 목록 엔드포인트
│ │ ├── health.rs # 헬스 체크 엔드포인트
│ │ └── admin.rs # 관리자 엔드포인트
│ ├── middleware/ # HTTP 미들웨어
│ │ ├── auth.rs # 인증
│ │ ├── logging.rs # 요청 로깅
│ │ ├── metrics.rs # 메트릭 수집
│ │ └── error.rs # 오류 처리
│ └── dto/ # Data Transfer Objects
│ ├── request.rs # 요청 DTO
│ └── response.rs # 응답 DTO
│
└── metrics/ # 메트릭 및 모니터링
├── mod.rs # Metrics 모듈 익스포트
├── backend.rs # 백엔드별 메트릭
├── routing.rs # 라우팅 메트릭
├── model.rs # 모델 서비스 메트릭
└── streaming.rs # 스트리밍 메트릭
tests/
├── rate_limiting_test.rs # 최상위 타깃: CI가 이름으로 선택하거나
│ # 프로세스 전역 상태를 읽는 파일
├── streaming/ # 그룹 타깃 (이슈 #1620)
│ ├── main.rs # 멤버마다 `pub mod <stem>;`, 헬퍼는 #[path]로 선언
│ ├── streaming_test.rs # 멤버: 이전 최상위 파일을 그대로 옮긴 것
│ └── ...
├── server/, routing/, guardrails/, ... # 나머지 그룹 타깃
├── tool_calling/ # 도구 호출 스위트, 독자 타깃 (main.rs)
├── common/ # 공용 헬퍼 (모의 서버, 픽스처)
├── *_support/, *_common/ # 이를 사용하는 타깃이 선언하는 그 밖의 공용 헬퍼
└── fixtures/ # 테스트 데이터
examples/
├── standalone_server.rs # 설정 파일로 최소 서버 실행
├── axum_integration.rs # 기존 Axum 앱에 라우터 임베딩
├── programmatic_config.rs # YAML 없이 코드로 설정
└── hot_reload.rs # 핫 리로드 및 설정 변경 구독
예제 실행¶
examples/ 디렉토리에는 라이브러리 API를 시연하는 실행 가능한 Rust 프로그램이 포함되어 있습니다. 다음 명령으로 예제를 실행할 수 있습니다:
예시:
# 독립 실행 서버 시작
cargo run --example standalone_server -- --config config.yaml
# 커스텀 Axum 앱에 라우터 임베딩
cargo run --example axum_integration -- --config config.yaml
# 프로그래밍 방식으로 설정 (YAML 파일 불필요)
OPENAI_API_KEY=sk-... cargo run --example programmatic_config
# 핫 리로드 시연
cargo run --example hot_reload -- --config config.yaml
자세한 내용은 라이브러리 사용법에서 임베딩 가능한 크레이트 API에 대한 전체 가이드를 참조하세요.
아키텍처 개요¶
의존성 흐름¶
HTTP Layer → Services Layer → Infrastructure Layer → Core Layer
↓ ↓ ↓ ↑
└────────────┴────────────────┴────────────────────┘
Core 타입 사용
주요 디자인 패턴¶
- 생성자 주입: 서비스를 생성자 인자로 전달하며, 런타임 DI 컨테이너는 사용하지 않음
- 리포지토리 패턴: 백엔드 구현이 데이터 접근을 추상화
- 전략 패턴: 로드 밸런싱 전략이 플러그 가능
- 옵저버 패턴: 헬스 모니터링이 이벤트 기반 업데이트 사용
- 서킷 브레이커 상태 머신: 독립 상태 추적과 Admin 제어
- 백오프와 재시도: 일시적 실패 처리
빌드 및 실행¶
개발 빌드¶
# 빠른 개발 빌드
cargo build
# 디버그 출력과 함께 실행
RUST_LOG=debug ./target/debug/continuum-router
# 자동 리로드와 함께 실행
cargo watch -x run
# 특정 설정과 함께 실행
cargo run -- --config dev-config.yaml
릴리스 빌드¶
# 최적화된 릴리스 빌드 (기본적으로 모든 기능 활성화)
cargo build --release
# 릴리스 빌드 실행
./target/release/continuum-router
# 특정 기능만으로 빌드 (최소 라이브러리 빌드)
cargo build --lib --no-default-features --features "metrics,hot-reload"
커밋된 .cargo/config.toml은 target-cpu를 지정하지 않으므로 위 빌드 모두 아키텍처별 이식 가능한(portable) 기본값을 대상으로 하며, 이는 실제 릴리스 바이너리가 배포되는 방식과 같습니다. 로컬 빌드에서 네이티브 CPU 기능을 사용하려면 커밋된 파일을 수정하지 말고 해당 실행에만 RUSTFLAGS를 지정하세요(RUSTFLAGS="-C target-cpu=native" cargo build --release). 사용자 수준 ~/.cargo/config.toml도 같은 방식으로 동작하며, 두 방법 모두 커밋되거나 CI 캐시로 공유되지 않습니다. 특정 CPU 세대용으로 빌드된 캐시된 proc-macro 동적 라이브러리가 GitHub의 이종(heterogeneous) 호스티드 러너에서 다른 CPU로 복원되면 rustc가 SIGILL로 비정상 종료되기 때문입니다.
Cargo 기능 플래그¶
크레이트는 선택적 구성 요소를 활성화하는 기능 플래그를 제공합니다. 바이너리 빌드 시에는 기본적으로 full 기능 세트가 활성화됩니다.
| 기능 | 설명 | 기본값 |
|---|---|---|
full | 아래 모든 기본 기능 활성화 | yes (default 경유) |
cli | clap을 통한 CLI 바이너리 지원 | yes |
admin | 관리자 API 엔드포인트 | yes |
metrics | Prometheus 메트릭 엔드포인트 | yes |
files | Files API (/v1/files) | yes |
health-check | 백그라운드 헬스 모니터링 | yes |
hot-reload | 라이브 리로드를 위한 설정 파일 감시 | yes |
webui | 임베디드 WebUI 에셋 | yes |
gcp-auth | Gemini 서비스 계정용 Google Cloud 인증 | yes |
redis-cache | 응답 캐시 및 분산 속도 제한을 위한 Redis/Valkey 백엔드(deadpool-redis). default/full에는 포함되지 않으나 공식 릴리스 바이너리와 컨테이너 이미지에는 포함되어 컴파일됩니다. 지원되는 엔드포인트 계약은 배포 가이드를 참고하세요. | no (릴리스 빌드에 포함) |
embed | 라우터를 라이브러리로 임베딩하기 위한 최소 인프로세스 서빙 표면(예: iOS). 추가 구성 요소를 켜지 않으며 iOS 환경에 부적합한 선택적 의존성(rusqlite, redis, prometheus, rust-embed, aws-*, clap, notify, rmcp, object_store, gcp_auth)을 모두 제외합니다. | no |
continuum-router를 라이브러리 크레이트로 사용할 때, 필요한 기능만 선택하여 컴파일 시간과 바이너리 크기를 줄일 수 있습니다:
[dependencies]
continuum-router = { git = "...", default-features = false, features = ["metrics", "hot-reload"] }
빌드 예시:
# 최소 코어 라이브러리 (선택적 의존성 없음)
cargo build --lib --no-default-features
# 메트릭 지원 라이브러리
cargo build --lib --no-default-features --features "metrics"
# 관리자 및 핫 리로드 라이브러리
cargo build --lib --no-default-features --features "admin,hot-reload"
# 최소 인프로세스 임베딩 표면 (예: iOS 앱에 링크)
cargo check --lib --no-default-features --features "embed"
cargo check --lib --no-default-features --features "embed" --target aarch64-apple-ios
# 전체 바이너리 (기본값)
cargo build --release
embed 기능은 continuum-router를 Rust 라이브러리로 링크해 HTTP 서버를 인프로세스로 실행하는 공식 방법입니다(예: 앱 샌드박스가 라우터를 사이드카 서브프로세스로 띄우는 것을 금지하는 iOS 환경). 모바일 타깃으로 크로스 컴파일하기 어렵거나 불가능한 크레이트를 의존성 그래프에서 배제합니다. 진입점은 serve_embedded이며, 제외된 의존성은 다음 명령으로 확인할 수 있습니다:
크로스 컴파일¶
# cross 설치
cargo install cross
# Linux x86_64용 빌드
cross build --release --target x86_64-unknown-linux-gnu
# macOS ARM64용 빌드
cross build --release --target aarch64-apple-darwin
# Windows용 빌드
cross build --release --target x86_64-pc-windows-gnu
CI 러너 사용과 빌드 재사용¶
자동 self-hosted CI는 [self-hosted, macOS]에서 실행되며 Intel과 Apple Silicon 머신을 모두 지원합니다:
- 동시 실행(Concurrency) 그룹: 저장소 공용 concurrency 그룹 두 개가 여러 PR, 주간 의존성 검사, 문서 빌드의 동시 실행을 제한합니다:
continuum-router-self-hosted-1: default graph, isolated graphs, 주간 dependency float를 실행합니다.continuum-router-self-hosted-2: 변경 감지, release graph, 배포 자산 검사, IDE 규칙 검사, 문서 빌드를 실행합니다.- 각 그룹은
queue: max,cancel-in-progress: false를 사용해 다른 PR의 대기 중인 검사를 덮어쓰지 않고 최대 100개의 대기 작업을 FIFO 순서로 보존합니다(같은 PR에 새 push가 들어오면 워크플로 수준 concurrency 설정에 따라 이전 실행만 취소됩니다). 이 제한은 이 저장소의 자동 작업에만 적용되며, 조직 내 다른 저장소나 사용자 지정 러너 레이블로 수동 실행하는 Dynamo E2E 작업에는 적용되지 않습니다(GitHub concurrency 문서 참고).
- 2회 빌드 기반 테스트 실행: 워크플로와
make ci-local모두 두 번의 라이브러리 빌드로 테스트를 실행합니다:- 기본 feature 빌드(
ci-default의cargo test --tests):not(feature = "control-plane")또는not(feature = "bedrock-sigv4")로 조건부 컴파일되는 테스트 6개가 기본 빌드에만 존재하므로 유지됩니다. - 전체 feature 빌드(
ci-release-graph의cargo test --all-features --no-fail-fast --lib --tests):--no-fail-fast를 통해 한 바이너리가 실패해도 나머지 바이너리를 끝까지 실행합니다. - 서로 다른 feature 구성, 일반 빌드 대 테스트 빌드,
clippy대 실행 테스트는 별도 컴파일이 필요합니다(반복되는Compiling continuum-router출력은src/lib.rs재빌드가 아니라 새 통합 테스트 바이너리 컴파일을 뜻할 수 있습니다). 불필요한Fresh출력은 생략하고,cargo-deny는 긴 포함 그래프 대신 중복 버전 경고만 간결하게 출력합니다.
- 기본 feature 빌드(
- 로컬 CI 헬퍼:
scripts/local-ci.sh --only <테스트 타깃>: 지정한 단일 타깃을 모든 feature 조합에서 다시 실행합니다(이름이 타깃과 정확히 일치해야 하며, 일치하는 단계가 없거나 선택된 모든 단계가 건너뛰어져No step ran이 출력되면 종료 코드2로 종료되고, 건너뛴 단계가 있으면 마지막 줄에 개수를 보고합니다).make ci-local-gated: 호스팅 게이트가 해당 브랜치에서 건너뛸 feature graph 단계를 생략합니다(origin/main과의 merge base가 없거나, 작업 트리가 더럽거나, 게이트가 오류를 내거나 범위를 넓히면 전체를 실행합니다). 최종 머지 게이트는 생략 없는make ci-local입니다.
- 툴체인, 캐시 및 디버그 심볼: Rust 작업은
.github/actions/rust-toolchain을 통해rust-toolchain.toml의 숫자 버전을 직접 설치하며, self-hosted 네이티브 산출물은 체크아웃 정리 범위 밖인$HOME/actions-cargo-target/continuum-router에 보관합니다(80 GB 상한 적용, Intel과 Apple Silicon 머신별로 각각 분리 보관). CI와 로컬 미러는 기본적으로CARGO_PROFILE_DEV_DEBUG=0,CARGO_PROFILE_TEST_DEBUG=0을 사용해 어서션(assertion)은 유지하면서 디버그 산출물 크기와 링크 비용을 줄입니다(심볼이 필요하면 로컬에서 해당 변수를2로 설정하세요). 성능 워크플로는 크래시 분석용 디버그 심볼과 용량 측정용 배포 릴리스 프로필을 그대로 유지합니다.
| 활성 workflow 또는 작업 | 러너 정책 | 남은 요구 사항 |
|---|---|---|
| CI Rust graph, 변경 감지, IDE 검사 | Self-hosted macOS 공용 대기열 2개 | 네이티브 Rust와 지속 빌드 캐시 |
| 배포 자산 검사 | Self-hosted macOS 대기열 2 | Go, Helm, kubectl, kubeconform, PyYAML 설치; 실제 클러스터나 Docker 불필요 |
| 문서 빌드 / 배포 | 빌드는 self-hosted macOS 대기열 2; Pages 배포는 GitHub-hosted Ubuntu | Pages 쓰기와 OIDC 권한은 배포 작업에만 부여 |
| Dependency float | Self-hosted macOS 대기열 1 | 업데이트한 의존성으로 전체 매트릭스 순차 검사 |
| CI Redis 공유 상태 검사 | GitHub-hosted Ubuntu | Linux Actions 서비스 컨테이너; Linux VM 준비 후 이동 |
| 성능 harness와 capacity gate | GitHub-hosted Ubuntu | 기존 Linux CPU 기준과 gdb/core dump 분석; VM 이동 시 기준 재검토 |
| KV listener 컨테이너 검사 / Dynamo E2E 기본값 | GitHub-hosted Ubuntu | Linux Docker/Compose; Dynamo E2E는 수동 실행하며 별도 라벨 지정 가능 |
| Release와 Debian 패키징 | Hosted Linux/Windows ARM64 빌더와 self-hosted macOS/Windows 서명 러너 | 플랫폼 도구, 서명·게시 자격 증명, Docker/Buildx; Docker와 Debian 작업은 이미 release 바이너리 재사용 |
| Project automation / staging 배포 | GitHub-hosted Ubuntu | App 또는 배포 자격 증명; 신뢰할 수 있는 격리 VM 필요 |
update_homebrew_formula.yml_과 launchpad_ppa.yml_은 예약 실행되지 않는 비활성 파일입니다. 필요한 VM 기능이 준비될 때까지 해당 GitHub-hosted 작업을 유지하며, 효율 개선 작업으로 인해 새로 추가된 호스티드 작업은 없습니다.
릴리스 패키징과 코드 서명¶
릴리스 워크플로는 서명을 선택적인 후처리가 아니라 아티팩트 구성 단계로 취급합니다. Linux 매트릭스 네 항목은 서명하지 않으며 기존 구성을 그대로 유지합니다. macOS와 Windows 경로는 서명 계약을 만족하지 못하면 아카이브, 체크섬, 릴리스 업로드 전에 실패합니다.
플랫폼별 흐름¶
- macOS ARM64는
self-hosted-macos-26-arm64에서 실행됩니다..github/actions/macos-signing-setup은PATH로 fallback하기 전에 알려진/opt/homebrew와/usr/local의 keg 및brew경로에서 호환 OpenSSL을 찾고,.p12비밀번호를 환경 변수를 통해 전달하며, Developer ID Application 인증서를 추출해 키체인 없이rcodesign이 사용하게 하고,rcodesign아카이브와 실행 파일 각각의 SHA-256 digest를 고정합니다. 아카이브는 압축을 풀기 전에 검증하고 추출하거나 캐시한 실행 파일은 실행 또는 재사용 전에 검증합니다..github/actions/macos-sign-notarize-binary는continuum-router와continuum-kv-listener모두를 hardened runtime으로 서명하고 authority와 identifier를 검증한 다음 zip을 만들고notarytool의status: Accepted를 요구합니다. 다른 결과는 공증 로그를 가져온 뒤 실패합니다. 직후의always()정리 단계가 임시.p12와 PEM을 삭제합니다. Apple은 bare CLI 실행 파일이나 zip 컨테이너에 staple하지 않으므로 Gatekeeper가 온라인으로 ticket을 조회합니다. - Windows x86_64는
windows-on-macmini02-x64에서 빌드하고 서명합니다. 두 실행 파일을 깨끗한 staging 디렉터리에 복사하고scripts/sign-windows.ps1로 서명한 뒤 zip을 만들기 전에signtool verify /pa /all /v로 다시 검증합니다.LICENSE와NOTICE도 실행 파일 옆에 포함합니다. - Windows ARM64는 GitHub-hosted
windows-11-arm에서 빌드합니다. lock된ring과aws-lc-sys의존성은 Clang 호환 ARM64 assembly 지원이 필요하므로.github/actions/setup-clang-windows-arm64가 Cargo 실행 전에clang과clang-cl을 검증합니다. 빌더는 서명되지 않은 PE 파일 두 개와LICENSE,NOTICE만 하루 동안 유지되는 workflow artifact로 업로드합니다. x86_64 서명 러너는 두 파일의 PE machine field가 ARM64인지 확인하고 두 파일 모두 서명 및 fail-closed 검증한 다음continuum-router-windows-aarch64.zip, 체크섬, 릴리스 업로드를 만듭니다. 서명되지 않은 ARM64 아카이브는 만들거나 업로드하지 않습니다. 서명 러너는scripts/sign-windows.ps1만 별도의signing-checkout/디렉터리에 checkout합니다. 이 러너는 x86_64 빌드와 workspace를 함께 쓰는데, 예전에 workspace 루트에 sparse checkout을 했다가 다음 빌드 job에 저장소 파일이 남지 않은 적이 있기 때문입니다.
두 Windows 타깃 모두 static MSVC CRT를 사용합니다. Authenticode는 PE 파일을 실행하지 않고 해시하므로 x86_64 서명 호스트가 ARM64 출력도 서명할 수 있습니다. 릴리스 알림과 컨테이너 게시를 포함해 완료된 build를 소비하는 모든 작업은 ARM64 서명 작업에도 의존하므로 해당 아티팩트가 서명되지 않았거나 없을 때 성공을 보고할 수 없습니다.
안전한 서명 테스트 모드¶
수동 workflow_dispatch에서 boolean signing_test 입력을 true로 설정하면 릴리스를 만들거나 변경하지 않고 운영 서명 경로를 실행할 수 있습니다. 기본값은 false이므로 기존 release event와 수동 운영 dispatch의 동작은 그대로 유지됩니다. 테스트 모드는 일곱 build matrix 항목을 모두 실행하고, 실제 macOS 서명과 공증, Windows x86_64 서명과 검증, hosted Windows ARM64 build에서 제한된 서명 러너로 이어지는 handoff까지 완료합니다.
완성된 아카이브와 체크섬은 GitHub Actions artifact로만 3일 동안 보존합니다. Windows ARM64 최종 artifact 이름은 signing-test-final-signed-aarch64-pc-windows-msvc이며 하루 동안 보존하는 unsigned-aarch64-pc-windows-msvc handoff와 구분됩니다. signing_test가 true이면 GitHub Release 업로드 두 경로를 모두 실행하지 않고 tag나 release를 만들지 않으며, 컨테이너 이미지 게시, Harbor mirror, Teams 알림, Homebrew 업데이트도 실행하지 않습니다. 명시적인 Signing test (no publication) run title을 사용하므로 downstream Debian packaging workflow도 완료 event를 무시하고 기존 릴리스를 변경하지 않으며 staging workflow는 원래부터 release 완료 event만 허용합니다.
최상위 workflow 이름은 Release에서 Release Packaging으로 바꾸고 두 workflow_run consumer도 함께 갱신했습니다. GitHub는 workflow_run consumer를 default branch에서 평가하므로 debian_build.yml의 branch-only 조건만으로는 merge 전 첫 테스트를 보호할 수 없습니다. 서로 다른 이름이 이 rollout 공백을 닫습니다. 이 변경이 PR에만 있는 동안 default branch consumer는 이전 Release 이름만 구독하므로 branch의 Release Packaging 실행이 consumer를 시작할 수 없습니다. merge 후에는 두 consumer가 Release Packaging을 구독하고 run title 및 event type guard가 정상 상태의 보호를 담당합니다. 이름 변경은 orchestration identity만 바꾸며 release event, 수동 운영 dispatch, asset 업로드, 컨테이너, Debian packaging, staging의 정상 동작은 유지합니다.
검토 중인 revision을 대상으로 테스트를 dispatch하고 release_tag는 비워 두세요.
packaging 환경¶
보호된 packaging 환경에 아래 이름을 설정합니다. 값은 커밋하거나 출력하지 마세요.
| 이름 | 필수 여부 | 용도 |
|---|---|---|
APPLE_CERTIFICATE | 필수 | Base64로 인코딩한 Developer ID Application .p12 |
APPLE_CERTIFICATE_PASSWORD | 필수 | .p12 비밀번호 |
AC_API_KEY_ID | 필수 | 공증용 App Store Connect API key ID |
AC_API_PRIVATE_KEY_P8 | 필수 | Base64로 인코딩하거나 원문 그대로 저장한 App Store Connect private key |
AC_API_ISSUER_ID | Team key에서만 필수 | App Store Connect issuer ID. Individual key에서는 생략 |
BUNDLE_ID | 필수(환경 변수 권장) | 안정적인 reverse-DNS 코드 서명 identifier |
WINDOWS_SIGN_CERT_PATH | 필수 | 서명 러너에 있는 공개 코드 서명 인증서 경로 |
WINDOWS_SIGN_CA_CERT_PATH | 필수 | 서명 러너에 있는 발급 CA 인증서 경로 |
WINDOWS_SIGN_KEY_CONTAINER | 필수 | Windows CSP에 전달할 KMS-backed key container |
WINDOWS_SIGNTOOL_PATH | 선택 | Windows SDK signtool.exe 경로 override |
WINDOWS_SIGN_CSP | 선택 | KMS CSP provider 이름 override |
WINDOWS_SIGN_TIMESTAMP_URL | 선택 | RFC 3161 timestamp 서비스 override. 기본값은 Sectigo |
Windows private key는 GitHub로 export하지 않습니다. self-hosted 서명 러너에는 Windows SDK와 KMS CSP가 설치되어 있어야 하고, 설정한 인증서 chain을 읽고 key를 사용할 권한이 있어야 합니다. helper는 설정된 경로나 key container를 직접 출력하지 않고 파일이 없을 때 변수 이름으로 보고합니다. 배포별 값은 모두 마스킹되는 환경 secret으로 저장해야 합니다.
검증과 rollout¶
서명 자격 증명이 필요 없는 정적 검사는 개발 머신에서도 안전하게 실행할 수 있습니다.
python3 scripts/check_release_signing.py
python3 scripts/test_check_release_signing.py
python3 scripts/check_action_pins.py
actionlint -shellcheck= .github/workflows/release.yml
cargo tree --locked --target aarch64-pc-windows-msvc --features control-plane,appproxy-router,hot-reload,redis-cache,otel,dynamo,dynamo-kv,dynamo-event-bus
cargo tree --locked --manifest-path crates/continuum-kv-listener/Cargo.toml --target aarch64-pc-windows-msvc
cargo tree는 타깃별 dependency resolution만 검증합니다. 설정한 러너에서 통제된 signing_test dispatch를 실행하고 아래 항목을 모두 확인해야 rollout이 끝납니다.
- 두 Windows 아카이브 모두
continuum-router.exe,continuum-kv-listener.exe,LICENSE,NOTICE를 포함하고.sha256검증을 통과합니다. - 깨끗한 x86_64 및 ARM64 Windows 머신에서 두 실행 파일 모두
Get-AuthenticodeSignature와signtool verify /pa /all /v로 유효한 예상 publisher를 표시합니다. - 네트워크에 연결된 깨끗한 macOS 머신에서 압축을 푼 두 실행 파일 모두 Developer ID Application authority와 hardened runtime flag를 표시하고 미확인 개발자 경고 없이 Gatekeeper를 통과합니다.
- macOS 공증 로그가
Accepted를 보고하고 Windows ARM64 최종 Actions artifact가 서명되지 않은 handoff와 구분되며 릴리스 업로드, 컨테이너 게시, Harbor mirror, 알림, Homebrew 경로가 모두 skip되었습니다.
서명 테스트 증거를 보존하기 전에는 이전 credential을 제거하거나 러너 provisioning을 바꾸지 마세요. Secret 생성, key 접근, clean-machine 검증은 운영 단계이며 저장소의 정적 테스트만으로 증명할 수 없습니다.
테스트¶
테스트 실행¶
# 모든 테스트 실행
cargo test
# 출력과 함께 실행
cargo test -- --nocapture
# 특정 테스트 실행
cargo test test_health_check
# 단일 스레드로 테스트 실행 (디버깅용)
cargo test -- --test-threads=1
# 통합 테스트만 실행
cargo test --test '*'
# 단위 테스트만 실행
cargo test --lib
# 문서 테스트 실행
cargo test --doc
테스트 작성¶
단위 테스트 예제¶
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_backend_selection() {
let backends = vec![
Backend::new("backend1", "http://localhost:8000"),
Backend::new("backend2", "http://localhost:8001"),
];
let selector = RoundRobinSelector::new(backends);
let selected = selector.select().unwrap();
assert_eq!(selected.name(), "backend1");
}
#[tokio::test]
async fn test_async_health_check() {
let backend = Backend::new("test", "http://localhost:8000");
let result = backend.health_check().await;
assert!(result.is_ok());
}
}
통합 테스트 예제¶
통합 테스트 구조 및 CI 탐색 규칙:
- 타깃 탐색 및 도메인 그룹: Cargo는 최상위
tests/*.rs파일과tests/<group>/main.rs를 통합 테스트 타깃으로 인식합니다. 대부분의 통합 테스트는tests/streaming/이나tests/guardrails/같은 도메인 그룹의 모듈로 구성되며, 그룹의main.rs가 각 멤버를pub mod <stem>;으로 선언해 그룹당 한 번만 링크합니다(CLAUDE.md의 'Where a new test file may live' 참고).- 그룹 멤버 하나만 실행하려면
cargo test --test <group> <stem>::(부분 문자열 일치)을 사용하고, 특정 테스트 함수 하나만 정확히 실행하려면cargo test --test <group> -- --exact <stem>::<test>를 사용합니다. - CI가 이름으로 직접 선택하는 파일과 프로세스 전역 상태를 읽는 파일은 최상위
tests/*.rs타깃으로 유지합니다.
- 그룹 멤버 하나만 실행하려면
- 공유 상태 격리: 테스트가 지정한 ID(업스트림 응답, 파일, 대화 ID 등)를 키로 사용하는 상태는 저장소가 개별
AppState에 격리되어 있지 않은 한(Responses 세션 저장소는AppState별로 격리됨) 같은 그룹 바이너리의 모든 멤버가 공유합니다. 충돌을 막기 위해 ID에 파일별 접두사를 붙이세요. - 미선언 파일 감사: 어떤 타깃도 선언하지 않은
tests/하위.rs파일은 컴파일되지 않아 조용히 누락되므로, 이러한 파일이 존재하면tests/cargo_test_discovery_audit_test.rs가 빌드를 실패시킵니다. - Feature 게이트 테스트 감사: 기본값이 아닌 기능으로 조건부 컴파일되는 테스트(
#![cfg(feature = ...)]파일, 게이트된#[test], 게이트된 인라인mod)는 기본cargo test --tests단계에서는 컴파일되지 않고.github/workflows/ci.yml과scripts/local-ci.sh양쪽의 all-features 일괄 실행 단계(cargo test --all-features --no-fail-fast --lib --tests)에서 실행됩니다:- 두 파일 중 어느 한쪽이라도 해당 일괄 실행 단계나 명시적인
cargo test --features <F> --test <target>줄로 게이트를 포함하지 않으면 감사 테스트가 빌드를 실패시킵니다. ci.yml에서는name과run만 가진 스텝의 한 줄짜리run:전체여야 하며(continue-on-error가 없고if:가 고정된 값과 일치해야 함),scripts/local-ci.sh에서는scripts/local-ci.sh --list가 출력하는 고유 이름을 가진step "<이름>" cargo test ...한 줄이어야 합니다. 이 줄을 나누거나 줄바꿈하거나 조건·이름 필터를 추가하지 마세요.
- 두 파일 중 어느 한쪽이라도 해당 일괄 실행 단계나 명시적인
- PR 변경 감지: 풀 리퀘스트에서 호스팅 게이트는 변경된 테스트 타깃에 feature 게이트 테스트가 포함된 경우에만 해당 타깃 때문에
ci-release-graph를 예약합니다. 스캐너가 판단할 수 없는 경우(매크로 인자로 전달된feature이름,#[path]나include!로 불러오지만 읽을 수 없는 비.rs모듈 파일, 닫히지 않은 따옴표가 많은 소스 파일 등)에는 전체 그래프를 실행합니다.
// tests/chat_test.rs
use continuum_router::test_utils::TestServer;
#[tokio::test]
async fn test_chat_completion() {
let server = TestServer::new().await;
let response = server
.post("/v1/chat/completions")
.json(&json!({
"model": "gpt-5.6-luna",
"messages": [{"role": "user", "content": "Hello"}]
}))
.send()
.await;
assert_eq!(response.status(), 200);
}
테스트 커버리지¶
# 커버리지 리포트 생성
cargo tarpaulin --out Html
# 특정 임계값과 함께 커버리지 생성
cargo tarpaulin --fail-under 80
# Codecov 리포트 생성
cargo tarpaulin --out Xml
스마트 라우팅 분류 정확도¶
tests/smart_routing_accuracy_test.rs는 규칙 기반 분류기의 classify_only 출력(src/services/smart_routing/router.rs)이 tests/data/smart_routing/dataset.json에 있는 레이블 데이터셋과 얼마나 일치하는지 측정하고 분류기 모드별 혼동 행렬을 보고합니다. 전체 리포트 실행:
같은 명령이 CI 게이트로도 동작합니다. accuracy_meets_the_regression_gate는 두 모드 중 하나라도 도메인 또는 복잡도 정확도가 테스트 파일의 DOMAIN_ACCURACY_GATE, COMPLEXITY_ACCURACY_GATE 옆에 기록된 임계값 아래로 떨어지면 빌드를 실패시킵니다. 실패하는 실행에서는 리포트가 자동으로 출력되며, 통과하는 실행에서 리포트를 보려면 --nocapture를 붙이면 됩니다.
두 가지 규칙 분류기 배포 모드를 측정합니다(LLM/하이브리드 모드는 실제 또는 모의 백엔드 왕복이 필요해 빠르고 결정론적인 검사가 아니므로 제외됩니다): rule_default(primary_language 미설정)와 rule_primary_ko(primary_language: ko, 한국어 배포용 분류기)입니다. 이는 tests/routing/classifier_language_parity_test.rs가 검증하는 것과 동일한 두 설정이며, 도메인 의도 신호가 없는 한국어 요청을 rule_default에서는 multilingual로, rule_primary_ko에서는 general로 분류한다는 점만 다릅니다.
세 번째 모드인 system_one은 같은 사례들을 classifier.method: system_one으로 tests/systemone_support/의 System One 픽스처 서버에 재생합니다:
- 충실도 검증(100% 게이트): 픽스처는
rule_primary_ko레이블(한국어 인사는general이며 질문 묶음의multilingual선택지 정의인 '번역 또는 명시적으로 다국어인 작업'과도 일치함)로 작성되고 분류기가 각 사례에 대해 보내는 정확한 state를 키로 사용합니다. 따라서 이 모드는 결정 모델 자체의 정확도가 아니라 한국어 절반을 포함한 모든 사례에서 결정 모델 응답에 대한 분류기의 매핑 충실도를 측정하며, 두 축 모두 게이트가 100%입니다(하나라도 틀리면 점수 반올림 수준, 도메인 태그 매핑, 또는 state 구성이 픽스처와 어긋난 것입니다). - 실제 모델 보정: 결정 모델 자체의 정확도는
make calibrate-systemone으로 측정합니다. 이 명령은 같은 코퍼스를 각 소비자의 state 빌더와 질문 묶음으로 실제/v1/systemone엔드포인트에 재생하고 임계값을 도출합니다(결정 모델 참고).
리포트 읽는 법¶
각 모드마다 전체 및 언어별 정확도, 도메인 혼동 행렬, 전체 복잡도 혼동 행렬, 언어별 복잡도 혼동 행렬, 그리고 복잡도 밴드가 틀린 케이스마다 한 줄씩 출력합니다. 합산된 행렬만으로는 혼동 셀이 코퍼스의 어느 언어에서 발생했는지 알 수 없고, 두 언어의 오분류 양상이 다르기 때문입니다.
오분류(miss) 줄은 id lang 기대값 -> 실제값 (방향) [신호] 형태로 출력됩니다. 대괄호 안은 determine_complexity에 입력된 모든 신호이며, 이 함수가 직접 읽는 세 가지 도메인 신호(code_blocks, math_notation, tool_definitions)를 포함해 각각 이름=강도로 표시됩니다. 잘못된 밴드를 추측하지 말고 이 목록으로 원인 신호를 추적하세요. 신호 벡터가 동일한 두 케이스는 어떤 가중치나 경계값 변경으로도 분리할 수 없으며, COMPLEXITY_ACCURACY_GATE에 문서화된 상한도 이 방식으로 도출되었습니다.
determine_complexity의 가중치나 밴드 경계를 변경할 때는 개별 픽스처의 통과 여부가 아니라 혼동 행렬 셀의 이동으로 근거를 제시하세요. 82개 케이스에서는 셀 하나에 한두 건만 들어 있을 수 있어 개별 케이스에 맞춘 조정은 과적합과 구분할 수 없습니다.
레이블이 붙은 케이스 추가하기¶
dataset.json의 각 항목은 다음과 같은 형태입니다:
{
"id": "code-fence-en",
"language": "en",
"text": "Please implement a function to reverse a string:\n```python\ndef reverse_string(s):\n pass\n```",
"expected": {
"rule_default": { "domain": "code", "complexity": "moderate" },
"rule_primary_ko": { "domain": "code", "complexity": "moderate" }
},
"note": "선택 사항: 이 레이블을 선택한 이유, 또는 이 케이스가 드러내려는 한계"
}
id는 고유하고 안정적이어야 합니다. 이후 리포트에서 특정 케이스의 결과 변화를 참조할 때 사용됩니다.language는"en"또는"ko"이며,context가 다른 언어여도 분류 대상 턴인text의 언어를 적습니다. 리포트는 이 필드를 기준으로 정확도를 집계합니다.context는 선택 항목입니다. 멀티턴 대화의 이전 메시지들을 채팅 메시지 객체({"role": "user", "content": "..."},{"role": "assistant", "content": "..."}) 배열로 적으며 순서대로text앞에 배치됩니다(단일 턴 사례에서는 생략). 증명 하한(proof floor)이 마지막 사용자 턴만 읽는 것처럼 동작이 대화 이력에 좌우될 때 사용합니다(reasoning-proof-explain-ko-after-en참고).system_one모드도 동일한 페이로드로 픽스처 상태를 구성하므로 추가 작업이 필요 없습니다.text는 분류 대상인 마지막 사용자 메시지입니다. 코드 도메인 신호를 테스트하려면 펜스 코드 블록(```)을, 수학 표기 신호를 테스트하려면 LaTeX 구분자($...$,\frac,\sum)를, 특정 의도 신호를 테스트하려면src/services/smart_routing/classifier_language.rs표의 한국어 또는 영어 키워드를 사용합니다.expected에는 러너가 실행하는 각 모드(rule_default,rule_primary_ko)의 기대 결과가 모두 있어야 합니다. 둘 중 하나라도 누락되면 조용히 건너뛰지 않고 명확한 패닉으로 테스트를 실패시킵니다.- 레이블은 분류기의 현재 출력값이 아니라
src/services/smart_routing/types.rs의DomainTag/ComplexityLevel문서 주석을 읽은 사람 검토자의 판단을 기준으로 작성합니다:- 분류기 출력을 그대로 복사하면 정확도가 무조건 100%가 되어 회귀 게이트가 무의미해집니다.
- 분류기의 실제 출력이 레이블과 다르면
note에 문서화할 만한 실제 한계이거나(데이터셋의creative-poem-ko,code-unmarked-ko참고) 레이블을 재검토해야 한다는 신호이므로, 출력값에 임의로 맞추지 마세요.
- 케이스의
note는 해당 오분류 줄 아래에 출력됩니다.expert로 레이블된 케이스가 오분류될 때는 반드시note가 있어야 하며, 없으면 게이트가 해당 케이스를 지목하며 실패합니다. 규칙 분류기는 분류 체계상 expert인 모든 요청에서expert에 도달할 수 없으므로(COMPLEXITY_ACCURACY_GATE에 문서화된 상한 및 고급 설정 가이드의 복잡도 수준 절 참고), 해당 오분류가 상한의 어느 부분에 해당하는지 명시해야 합니다. - 데이터셋을 수정한 뒤에는 리포트 명령을 다시 실행하고
tests/smart_routing_accuracy_test.rs의 게이트 상수 문서 주석(측정된 백분율, 두 실패 여유분, 각 게이트의 검증 범위)을 새로 측정된 값에 맞게 갱신하세요. 실제로 측정한 값보다 게이트를 높게 설정하지 마세요.
System One 픽스처 서버와 평가 코퍼스¶
tests/systemone_support/는 System One 결정 모델 접점을 위한 공용 테스트 하네스입니다:
- 하네스 구성 요소: YAML 테이블(
tests/systemone_support/answers.yaml) 기반으로 응답하는 wiremock/v1/systemone픽스처,429·529·422·타임아웃·손상된 본문에 대한 장애 주입, 보정 지표(정확도, Brier, 10-bin ECE, AUROC, 혼동 행렬), 그리고 재생 러너를 제공합니다. - 테스트에서 불러오기: 최상위 통합 테스트에서는
mod systemone_support;로, 도메인 그룹 멤버에서는 그룹의main.rs가#[path = "../systemone_support/mod.rs"]로 한 번 선언한 뒤use crate::systemone_support;로 접근합니다(CLAUDE.md의 'Where a new test file may live' 참고). 와이어 타입은 테스트 로컬로 분리해src/services/systemone/공용 클라이언트를 픽스처와 독립적으로 검증합니다. - 평가 코퍼스: 레이블된 코퍼스는
tests/fixtures/systemone/(guardrail.jsonl,routing.jsonl,tool_calls.jsonl, 질문 묶음batteries.yaml)에 위치하며, 행 형식·출처·라이선스·유해 의도 행 처리 방식은tests/fixtures/systemone/README.md에 문서화되어 있습니다:tests/systemone_corpus_test.rs는 레이블이 질문 묶음 타입에 맞지 않거나, 라이선스가 누락되었거나, 행 개수가 최소 기준(코퍼스당 200행, 가드레일 코퍼스는 한국어 40행 및 비라틴 문자 20행 포함)에 미달하면 빌드를 실패시킵니다.tests/systemone/systemone_cookbook_regression_test.rs는 러너와 지표 계산 코드가cookbook_cache.yaml에 기록된 TypeSafe 쿡북의 공개 판정을 그대로 재현하는지 검증합니다.
실제 엔드포인트에서 단일 코퍼스를 빠르게 확인하려면 JSON 리포트를 출력하는 #[ignore] 러너로 재생합니다(모든 소비자를 재생하고 임계값을 도출하는 전체 보정 절차는 make calibrate-systemone이며 결정 모델을 참고하세요):
SYSTEMONE_ENDPOINT=http://127.0.0.1:8000/v1/systemone \
SYSTEMONE_API_KEY_ENV=LAYA_SERVE_API_KEY \
cargo test --test systemone_corpus_test -- --ignored run_routing --nocapture
SYSTEMONE_API_KEY_ENV는 API 키가 담긴 환경 변수 이름이고, SYSTEMONE_MODEL은 요청할 모델을 지정합니다(기본값 jev-latest). 로컬 실행 시에는 deploy/docker-compose.systemone.yml로 루프백에 자체 호스팅 laya-serve를 띄울 수 있습니다.
벤더링된 continuum-protocol 크레이트¶
crates/continuum-protocol/src와 tests는 단일 Continuum Hub 리비전을 바이트 단위로 그대로 복사한 것이며, 기준 리비전은 crates/continuum-protocol/Cargo.toml의 출처 줄(# Vendored verbatim from lablup/continuum-hub crates/continuum-protocol at rev <sha>.)에 기록됩니다:
- 매니페스트 및 드리프트 검사:
crates/continuum-protocol/UPSTREAM.sha256은 해당 리비전의 허브src/및tests/아래 모든 파일의 SHA-256 해시(및 허브 자체 크레이트Cargo.toml의 다이제스트)를 기록합니다.scripts/check-vendored-protocol.sh는 매니페스트 리비전이 출처 줄과 다르거나, 파일이 수정·누락·추가되었거나,LOCAL-AHEAD항목이 잘못되었거나 오래된 경우 문제 경로를 모두 출력하며 실패합니다. - 빌드 입력 제한:
src/와tests/만 비교하므로 빌드에 영향을 줄 수 있는 다른 요소도 모두 차단합니다.build.rs같은 추가 크레이트 루트 파일, 허용된 섹션([package],[features], 기본 타깃 레지스트리 의존성 테이블)을 벗어난 크레이트Cargo.toml,cargo metadata가src/와tests/밖에서 보고하는 타깃, 그리고continuum-protocol을path = "crates/continuum-protocol"외의 경로에서 참조하는 라우터Cargo.toml은 모두 거부됩니다. - 실행 방식: 네트워크나 허브 자격 증명이 필요 없으며, CI와
make ci-local의 release-graph 잡에서 가장 먼저 실행됩니다(scripts/test-check-vendored-protocol.sh가 임시 트리에서 모든 실패 모드를 검증합니다).
# 오프라인 검사
bash scripts/check-vendored-protocol.sh
# CI와 make ci-local이 실행하는 검사: rev 줄은 그대로 둔 채 해시만 바꾼 매니페스트도 차단
bash scripts/check-vendored-protocol.sh --base "$(git merge-base origin/main HEAD)"
# 허브 클론에서 매니페스트를 다시 생성해 커밋된 매니페스트와 비교
CONTINUUM_HUB_DIR=../continuum-hub bash scripts/check-vendored-protocol.sh
# 허브 리비전에서 재벤더링: src/와 tests/를 교체하고 UPSTREAM.sha256을 갱신
bash scripts/refresh-vendored-protocol-manifest.sh --hub ../continuum-hub --rev <sha> --sync
--base 옵션은 "digest differs" 오류를 보고 단순히 해시만 고치는 실수를 막기 위해 존재합니다. # rev: 줄은 그대로인데 해시가 바뀐 매니페스트는 벤더링된 파일과 매니페스트 줄을 함께 수정한 경우뿐입니다. 로컬에 허브 클론이 있다면 머지 게이트인 make ci-local을 실행할 때 CONTINUUM_HUB_DIR를 지정하세요.
refresh 스크립트는 Cargo.toml을 수정하지 않으며, git 작업 트리에서 src/나 tests/ 아래에 커밋되지 않았거나 추적되지 않는 변경이 있으면 --force 없이는 --sync를 거부합니다. 재벤더링 후에는 스크립트가 출력한 출처 줄을 crates/continuum-protocol/Cargo.toml에 반영하고, git -C ../continuum-hub show <sha>:crates/continuum-protocol/Cargo.toml의 의존성 변경을 옮긴 뒤(해당 파일이 변경되었으면 스크립트가 알려 줍니다) 검사를 실행하세요.
벤더링된 파일을 허브보다 먼저 수정해야 한다면 허브 이슈를 먼저 등록한 뒤, 변경과 같은 PR에서 crates/continuum-protocol/LOCAL-AHEAD에 <path> lablup/continuum-hub#<N> <한 줄 사유>를 추가하고 UPSTREAM.sha256은 그대로 두세요. 항목은 src/ 또는 tests/ 아래의 개별 파일 경로여야 하며 디렉터리나 글로브(glob)는 허용되지 않습니다. 파일이 이미 고정된 리비전과 일치하는 항목은 검사가 실패시키므로, 이후 재벤더링으로 허브 변경이 반영되면 해당 항목을 제거하세요.
코드 품질¶
포매팅¶
린팅¶
# 경고를 오류로 처리하여 clippy 실행
cargo clippy -- -D warnings
# pedantic 린트와 함께 clippy 실행
cargo clippy -- -W clippy::pedantic
# 자동 수정과 함께 clippy 실행
cargo clippy --fix
보안 감사¶
# 알려진 취약점 확인
cargo audit
# 취약한 의존성 수정
cargo audit fix
# 보안 리포트 생성
cargo audit --json > audit-report.json
문서화¶
# 문서 빌드
cargo doc
# 문서 빌드 및 열기
cargo doc --open
# 비공개 항목 포함하여 문서 빌드
cargo doc --document-private-items
# 문서 예제 확인
cargo test --doc
디버깅¶
디버그 로깅¶
# 디버그 로깅 활성화
RUST_LOG=debug cargo run
# 특정 모듈에 대해 trace 로깅 활성화
RUST_LOG=continuum_router::services=trace cargo run
# 구조화된 로깅 활성화
RUST_LOG=info RUST_LOG_FORMAT=json cargo run
디버거 사용¶
VS Code¶
.vscode/launch.json생성:
{
"version": "0.2.0",
"configurations": [
{
"type": "lldb",
"request": "launch",
"name": "Debug Router",
"cargo": {
"args": ["build", "--bin=continuum-router"],
"filter": {
"name": "continuum-router",
"kind": "bin"
}
},
"args": ["--config", "dev-config.yaml"],
"cwd": "${workspaceFolder}"
}
]
}
- 브레이크포인트 설정 후 F5 눌러 디버그
명령줄 (lldb)¶
# 디버그 심볼과 함께 빌드
cargo build
# lldb로 실행
lldb target/debug/continuum-router
# 브레이크포인트 설정
(lldb) b src/main.rs:50
# 프로그램 실행
(lldb) r --config config.yaml
메모리 디버깅¶
# valgrind 사용 (Linux)
valgrind --leak-check=full ./target/debug/continuum-router
# Address Sanitizer 사용
RUSTFLAGS="-Z sanitizer=address" cargo build -Z build-std --target x86_64-unknown-linux-gnu
./target/x86_64-unknown-linux-gnu/debug/continuum-router
성능 프로파일링¶
CPU 프로파일링¶
# 플레임 그래프 생성
cargo flamegraph -- --config config.yaml
# 특정 시나리오 프로파일
cargo flamegraph --bin continuum-router -- --benchmark
# perf 사용 (Linux)
perf record -g ./target/release/continuum-router
perf report
벤치마킹¶
# 벤치마크 실행
cargo bench
# 특정 벤치마크 실행
cargo bench routing
# 벤치마크 결과 비교
cargo bench -- --save-baseline main
git checkout feature-branch
cargo bench -- --baseline main
벤치마크 작성¶
// benches/routing_bench.rs
use criterion::{black_box, criterion_group, criterion_main, Criterion};
fn benchmark_routing(c: &mut Criterion) {
c.bench_function("round_robin_selection", |b| {
let selector = RoundRobinSelector::new(backends);
b.iter(|| {
selector.select(black_box(&request))
});
});
}
criterion_group!(benches, benchmark_routing);
criterion_main!(benches);
메모리 프로파일링¶
# heaptrack 사용 (Linux)
heaptrack ./target/release/continuum-router
heaptrack_gui heaptrack.continuum-router.*.gz
# Instruments 사용 (macOS)
cargo instruments -t Allocations
기여하기¶
개발 워크플로우¶
-
Fork 및 Clone
-
Feature 브랜치 생성
-
변경 사항 작성
- Rust 규칙을 따르는 코드 작성
- 새 기능에 대한 테스트 추가
-
문서 업데이트
-
변경 사항 테스트
-
변경 사항 커밋
-
Push 및 PR 생성
코드 표준¶
- Rust 명명 규칙 준수 (함수는 snake_case, 타입은 CamelCase)
- 모든 공개 API와 주요 분기에 테스트 작성 (커버리지 >90% 목표)
- 공개 API에 doc 주석으로 문서화
- 일관된 포매팅을 위해
cargo fmt사용 - 모든
cargo clippy경고 해결 - 함수를 작고 집중되게 유지
- 상속보다 합성 선호
- 적절한 오류 처리 사용 (Result/Option)
커밋 메시지 형식¶
Conventional Commits 사양 준수:
타입:
feat: 새 기능fix: 버그 수정docs: 문서만style: 코드 스타일 변경refactor: 코드 리팩토링perf: 성능 개선test: 테스트 추가chore: 유지보수 작업
Pull Request 가이드라인¶
- 제목: 명확하고 설명적
- 설명: 무엇을, 왜, 어떻게 설명
- 테스트: 새 기능에 대한 테스트 포함
- 문서: 관련 문서 업데이트
- Breaking Changes: 명확히 표시
- Issues: 관련 이슈 참조
리뷰 프로세스¶
- 자동화된 검사 통과 필수
- 최소 한 명의 메인테이너 승인
- 해결되지 않은 대화 없음
- main 브랜치와 최신 상태
문제 해결¶
일반적인 문제¶
컴파일 오류¶
테스트 실패¶
# 상세하게 테스트 실행
cargo test -- --nocapture --test-threads=1
# 경쟁 조건 확인
cargo test -- --test-threads=1
# 테스트 환경 확인
cargo test -- --ignored
성능 문제¶
# 릴리스 빌드 프로파일
cargo build --release
perf record ./target/release/continuum-router
perf report
# 블로킹 연산 확인
RUST_LOG=trace cargo run 2>&1 | grep "blocking"