Repository navigation
refactor(worker): replace healthy AtomicBool with status AtomicU8 #1101
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -10,7 +10,7 @@ use std::{ | |||||
| os::raw::c_char, | ||||||
| ptr, | ||||||
| sync::{ | ||||||
| atomic::{AtomicBool, AtomicUsize, Ordering}, | ||||||
| atomic::{AtomicU8, AtomicUsize, Ordering}, | ||||||
| Arc, | ||||||
| }, | ||||||
| }; | ||||||
|
|
@@ -19,7 +19,7 @@ use async_trait::async_trait; | |||||
| use llm_tokenizer::{create_tokenizer_from_file, traits::Tokenizer}; | ||||||
| use openai_protocol::{ | ||||||
| chat::ChatCompletionRequest, | ||||||
| worker::{HealthCheckConfig, WorkerSpec}, | ||||||
| worker::{HealthCheckConfig, WorkerSpec, WorkerStatus}, | ||||||
| }; | ||||||
| use smg::{ | ||||||
| policies::{ | ||||||
|
|
@@ -54,7 +54,7 @@ use super::{ | |||||
| pub struct GrpcWorker { | ||||||
| pub(crate) client: Arc<SglangSchedulerClient>, | ||||||
| pub(crate) endpoint: String, | ||||||
| pub(crate) healthy: AtomicBool, | ||||||
| pub(crate) status: AtomicU8, | ||||||
| pub(crate) load: AtomicUsize, | ||||||
| pub(crate) processed: AtomicUsize, | ||||||
| pub(crate) circuit_breaker: CircuitBreaker, | ||||||
|
|
@@ -80,7 +80,7 @@ impl GrpcWorker { | |||||
| client, | ||||||
| routing_key_load: WorkerRoutingKeyLoad::new(&endpoint), | ||||||
| endpoint, | ||||||
| healthy: AtomicBool::new(true), | ||||||
| status: AtomicU8::new(WorkerStatus::Ready as u8), | ||||||
| load: AtomicUsize::new(0), | ||||||
| processed: AtomicUsize::new(0), | ||||||
| circuit_breaker: CircuitBreaker::new(), | ||||||
|
|
@@ -96,7 +96,10 @@ impl std::fmt::Debug for GrpcWorker { | |||||
| fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||||||
| f.debug_struct("GrpcWorker") | ||||||
| .field("endpoint", &self.endpoint) | ||||||
| .field("healthy", &self.healthy.load(Ordering::Relaxed)) | ||||||
| .field( | ||||||
| "status", | ||||||
| &WorkerStatus::from_u8(self.status.load(Ordering::Relaxed)), | ||||||
| ) | ||||||
| .finish() | ||||||
| } | ||||||
| } | ||||||
|
|
@@ -119,12 +122,12 @@ impl Worker for GrpcWorker { | |||||
| &self.metadata.spec.connection_mode | ||||||
| } | ||||||
|
|
||||||
| fn is_healthy(&self) -> bool { | ||||||
| self.healthy.load(Ordering::Relaxed) | ||||||
| fn status(&self) -> WorkerStatus { | ||||||
| WorkerStatus::from_u8(self.status.load(Ordering::Relaxed)) | ||||||
| } | ||||||
|
|
||||||
| fn set_healthy(&self, healthy: bool) { | ||||||
| self.healthy.store(healthy, Ordering::Relaxed); | ||||||
| fn set_status(&self, status: WorkerStatus) { | ||||||
| self.status.store(status as u8, Ordering::Relaxed); | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Suggested change
References
|
||||||
| } | ||||||
|
|
||||||
| async fn check_health_async(&self) -> WorkerResult<()> { | ||||||
|
|
@@ -178,11 +181,11 @@ impl Worker for GrpcWorker { | |||||
| } | ||||||
|
|
||||||
| async fn grpc_health_check(&self) -> WorkerResult<bool> { | ||||||
| Ok(self.healthy.load(Ordering::Relaxed)) | ||||||
| Ok(self.is_healthy()) | ||||||
| } | ||||||
|
|
||||||
| async fn http_health_check(&self) -> WorkerResult<bool> { | ||||||
| Ok(self.healthy.load(Ordering::Relaxed)) | ||||||
| Ok(self.is_healthy()) | ||||||
| } | ||||||
|
Comment on lines
183
to
189
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Health check stubs return current health state — verify intent. Both Given the comment "FFI workers don't do their own health checks", the intent seems correct — but returning the current health state from a "health check" method is a bit unusual. The caller expects this to perform a check, not reflect cached state. Consider clarifying with a comment or renaming the semantics in a follow-up: async fn grpc_health_check(&self) -> WorkerResult<bool> {
// FFI workers don't perform active health checks; report current state
Ok(self.is_healthy())
}🤖 Prompt for AI Agents |
||||||
| } | ||||||
|
|
||||||
|
|
@@ -377,7 +380,7 @@ pub unsafe extern "C" fn sgl_multi_client_healthy_count( | |||||
| (*handle) | ||||||
| .grpc_workers | ||||||
| .iter() | ||||||
| .filter(|w| w.healthy.load(Ordering::Relaxed)) | ||||||
| .filter(|w| w.is_healthy()) | ||||||
| .count() | ||||||
| } | ||||||
|
|
||||||
|
|
@@ -398,9 +401,7 @@ pub unsafe extern "C" fn sgl_multi_client_set_worker_health( | |||||
| if worker_index >= client.grpc_workers.len() { | ||||||
| return SglErrorCode::InvalidArgument; | ||||||
| } | ||||||
| client.grpc_workers[worker_index] | ||||||
| .healthy | ||||||
| .store(healthy, Ordering::Relaxed); | ||||||
| client.grpc_workers[worker_index].set_healthy(healthy); | ||||||
| SglErrorCode::Success | ||||||
| } | ||||||
|
|
||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,7 @@ | ||
| use std::{ | ||
| fmt, | ||
| sync::{ | ||
| atomic::{AtomicBool, AtomicUsize, Ordering}, | ||
| atomic::{AtomicU8, AtomicUsize, Ordering}, | ||
| Arc, LazyLock, | ||
| }, | ||
| time::Duration, | ||
|
|
@@ -141,11 +141,35 @@ pub trait Worker: Send + Sync + fmt::Debug { | |
| self.metadata().spec.bootstrap_port | ||
| } | ||
|
|
||
| /// Check if the worker is currently healthy | ||
| fn is_healthy(&self) -> bool; | ||
| /// Get the worker's lifecycle status. | ||
| fn status(&self) -> WorkerStatus; | ||
|
|
||
| /// Set the worker's health status | ||
| fn set_healthy(&self, healthy: bool); | ||
| /// Set the worker's lifecycle status. | ||
| fn set_status(&self, status: WorkerStatus); | ||
|
|
||
| /// Check if the worker is currently healthy (status == Ready). | ||
| /// | ||
| /// This is a routing predicate — returns true only for `Ready` workers. | ||
| /// A `Pending` worker is not "unhealthy", just unverified. | ||
| fn is_healthy(&self) -> bool { | ||
| self.status() == WorkerStatus::Ready | ||
| } | ||
|
|
||
| /// Set the worker's health status (compatibility shim). | ||
| /// | ||
| /// Maps `true` → `Ready`, `false` → `NotReady`. | ||
| /// Prefer `set_status()` for explicit state transitions. | ||
| fn set_healthy(&self, healthy: bool) { | ||
| if healthy { | ||
| self.set_status(WorkerStatus::Ready); | ||
| } else { | ||
| // Only transition to NotReady if currently Ready. | ||
| // Don't transition Pending→NotReady (hasn't proven itself). | ||
| if self.status() == WorkerStatus::Ready { | ||
| self.set_status(WorkerStatus::NotReady); | ||
| } | ||
| } | ||
| } | ||
|
Comment on lines
+162
to
+172
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Minor TOCTOU window in The read-then-write pattern between
If future usage requires stricter atomicity, consider a compare-and-swap pattern. For now, this is fine. 🤖 Prompt for AI Agents |
||
|
|
||
| /// Perform an async health check on the worker | ||
| async fn check_health_async(&self) -> WorkerResult<()>; | ||
|
|
@@ -500,7 +524,7 @@ pub struct BasicWorker { | |
| pub load_counter: Arc<AtomicUsize>, | ||
| pub worker_routing_key_load: Arc<WorkerRoutingKeyLoad>, | ||
| pub processed_counter: Arc<AtomicUsize>, | ||
| pub healthy: Arc<AtomicBool>, | ||
| pub status: Arc<AtomicU8>, | ||
| pub consecutive_failures: Arc<AtomicUsize>, | ||
| pub consecutive_successes: Arc<AtomicUsize>, | ||
| pub circuit_breaker: CircuitBreaker, | ||
|
|
@@ -521,7 +545,10 @@ impl fmt::Debug for BasicWorker { | |
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
| f.debug_struct("BasicWorker") | ||
| .field("metadata", &self.metadata) | ||
| .field("healthy", &self.healthy.load(Ordering::Relaxed)) | ||
| .field( | ||
| "status", | ||
| &WorkerStatus::from_u8(self.status.load(Ordering::Relaxed)), | ||
| ) | ||
| .field("circuit_breaker", &self.circuit_breaker) | ||
| .field("grpc_client", &"<OnceCell>") | ||
| .finish() | ||
|
|
@@ -553,13 +580,13 @@ impl Worker for BasicWorker { | |
| &self.metadata.spec.connection_mode | ||
| } | ||
|
|
||
| fn is_healthy(&self) -> bool { | ||
| self.healthy.load(Ordering::Acquire) | ||
| fn status(&self) -> WorkerStatus { | ||
| WorkerStatus::from_u8(self.status.load(Ordering::Acquire)) | ||
| } | ||
|
|
||
| fn set_healthy(&self, healthy: bool) { | ||
| self.healthy.store(healthy, Ordering::Release); | ||
| Metrics::set_worker_health(self.url(), healthy); | ||
| fn set_status(&self, status: WorkerStatus) { | ||
| self.status.store(status as u8, Ordering::Release); | ||
| Metrics::set_worker_health(self.url(), status == WorkerStatus::Ready); | ||
| } | ||
|
|
||
| async fn check_health_async(&self) -> WorkerResult<()> { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
status()method currently usesOrdering::Relaxedfor loading the worker status. While this might be acceptable for a simple health flag, usingOrdering::Acquireis more consistent with theBasicWorkerimplementation in the core gateway and ensures that any state changes synchronized via the status update are visible to the thread performing the load. This is particularly important when the status is used by load balancing policies to make routing decisions.References