Skip to content

Commit 2e806d0

Browse files
committed
test+ci: unit-test phase_for and add CI workflow
Move phase_for from main.rs into lib.rs as first-class domain logic — it is the core of the operator (mapping ready replicas to the Pending/Warming/Ready lifecycle that encodes "warm, not merely running"), not a main-local helper. main.rs now imports it. Add five unit tests covering the phase transitions and edge cases: zero ready -> Pending, partial -> Warming, all ready -> Ready, the scale-to-one case, and the brief scale-down state where ready exceeds desired (still a serving state). Add a CI workflow (fmt --check, clippy, test, release build) with RUSTFLAGS=-D warnings enforced workflow-wide, matching the pre-push discipline used across the other repos. An end-to-end job that exercises the controller against an ephemeral kind cluster will follow.
1 parent 9bde76a commit 2e806d0

3 files changed

Lines changed: 110 additions & 27 deletions

File tree

.github/workflows/ci.yml

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
env:
10+
CARGO_TERM_COLOR: always
11+
RUSTFLAGS: "-D warnings"
12+
13+
jobs:
14+
check:
15+
name: fmt + clippy + test + build
16+
runs-on: ubuntu-latest
17+
steps:
18+
- uses: actions/checkout@v4
19+
20+
- name: Install Rust toolchain
21+
uses: dtolnay/rust-toolchain@stable
22+
with:
23+
components: rustfmt, clippy
24+
25+
- name: Cache cargo registry and target
26+
uses: actions/cache@v4
27+
with:
28+
path: |
29+
~/.cargo/registry
30+
~/.cargo/git
31+
target
32+
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
33+
34+
- name: Format check
35+
run: cargo fmt --all --check
36+
37+
- name: Clippy
38+
run: cargo clippy --workspace --all-targets
39+
40+
- name: Test
41+
run: cargo test --workspace
42+
43+
- name: Build release
44+
run: cargo build --release

src/lib.rs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,68 @@ pub struct VllmServiceStatus {
5252
/// Human-readable detail about the current phase.
5353
pub message: String,
5454
}
55+
56+
/// Lifecycle phase derived from the owned Deployment's ready replicas.
57+
///
58+
/// This is the heart of the operator: a vLLM pod that Kubernetes considers
59+
/// "Running" is not yet able to serve a token — it still has to load weights
60+
/// and warm up. The cold-start study quantified that gap; here it becomes an
61+
/// observable phase. "Ready" means warm, not merely alive.
62+
pub fn phase_for(desired: i32, ready: i32) -> (&'static str, String) {
63+
if ready == 0 {
64+
(
65+
"Pending",
66+
"Deployment created; no replica ready yet".to_string(),
67+
)
68+
} else if ready < desired {
69+
(
70+
"Warming",
71+
format!("{ready}/{desired} replicas ready; warming up"),
72+
)
73+
} else {
74+
(
75+
"Ready",
76+
format!("{ready}/{desired} replicas ready and warm"),
77+
)
78+
}
79+
}
80+
81+
#[cfg(test)]
82+
mod tests {
83+
use super::*;
84+
85+
#[test]
86+
fn zero_ready_is_pending() {
87+
let (phase, _) = phase_for(3, 0);
88+
assert_eq!(phase, "Pending");
89+
}
90+
91+
#[test]
92+
fn partial_ready_is_warming() {
93+
let (phase, msg) = phase_for(3, 1);
94+
assert_eq!(phase, "Warming");
95+
assert!(msg.contains("1/3"));
96+
}
97+
98+
#[test]
99+
fn all_ready_is_ready() {
100+
let (phase, msg) = phase_for(3, 3);
101+
assert_eq!(phase, "Ready");
102+
assert!(msg.contains("3/3"));
103+
}
104+
105+
#[test]
106+
fn single_replica_ready() {
107+
// The common scale-to-one case: one desired, one ready -> Ready.
108+
let (phase, _) = phase_for(1, 1);
109+
assert_eq!(phase, "Ready");
110+
}
111+
112+
#[test]
113+
fn ready_exceeding_desired_is_still_ready() {
114+
// During a scale-down a Deployment can briefly report more ready
115+
// than desired; that is still a serving state, not a warming one.
116+
let (phase, _) = phase_for(2, 3);
117+
assert_eq!(phase, "Ready");
118+
}
119+
}

src/main.rs

Lines changed: 1 addition & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ use serde_json::json;
1818
use thiserror::Error;
1919
use tracing::{info, warn};
2020

21-
use vllm_coldstart_operator::{VllmService, VllmServiceStatus, WarmupStrategy};
21+
use vllm_coldstart_operator::{phase_for, VllmService, VllmServiceStatus, WarmupStrategy};
2222

2323
const MANAGER: &str = "vllm-coldstart-operator";
2424

@@ -34,32 +34,6 @@ struct Context {
3434
client: Client,
3535
}
3636

37-
/// Lifecycle phase derived from the owned Deployment's ready replicas.
38-
///
39-
/// This is the heart of the operator: a vLLM pod that is "Running" from
40-
/// Kubernetes' point of view is not yet able to serve a token — it still
41-
/// has to load weights and warm up. The cold-start study quantified that
42-
/// gap; here it becomes an observable phase. "Ready" means warm, not just
43-
/// alive.
44-
fn phase_for(desired: i32, ready: i32) -> (&'static str, String) {
45-
if ready == 0 {
46-
(
47-
"Pending",
48-
"Deployment created; no replica ready yet".to_string(),
49-
)
50-
} else if ready < desired {
51-
(
52-
"Warming",
53-
format!("{ready}/{desired} replicas ready; warming up"),
54-
)
55-
} else {
56-
(
57-
"Ready",
58-
format!("{ready}/{desired} replicas ready and warm"),
59-
)
60-
}
61-
}
62-
6337
fn build_deployment(svc: &VllmService) -> Result<Deployment, Error> {
6438
let name = svc.name_any();
6539
let ns = svc.namespace().ok_or(Error::MissingNamespace)?;

0 commit comments

Comments
 (0)