Skip to content

feat(spur-client): add client-side controller failover with endpoint rotation#384

Open
shiv-tyagi wants to merge 1 commit into
ROCm:mainfrom
shiv-tyagi:feat/multi-controller-failover
Open

feat(spur-client): add client-side controller failover with endpoint rotation#384
shiv-tyagi wants to merge 1 commit into
ROCm:mainfrom
shiv-tyagi:feat/multi-controller-failover

Conversation

@shiv-tyagi

Copy link
Copy Markdown
Member

What

Closes #338.

spurd and the CLI could only be pointed at a single controller address, so an HA quorum offered no client-side resilience: if the one configured controller was down, connections failed even when other peers were healthy. This adds client-side failover across a list of controller endpoints.

Approach

New spur-client crate provides a service-agnostic dialer:

  • connect_channel(endpoints) takes a comma-separated list, tries each in order, and rotates to the next on connection failure, returning a tonic::transport::Channel. If every endpoint fails, the last error is returned.
  • Callers wrap the channel in whatever generated client they need (SlurmControllerClient::new(channel)), so the crate depends on neither spur-proto nor spur-core.

A single reachable controller is sufficient: server-side Raft leader forwarding already routes writes to the current leader, so the client only needs to reach any live peer.

Config plumbing:

  • ControllerConfig::endpoints() expands hosts + the listen_addr port into an endpoint list.
  • The CLI exports it as a comma-joined SPUR_CONTROLLER_ADDR; the same env var and --controller flag accept a comma-separated list.

Call sites in spur-cli, spurd, and spur-ffi switch from SlurmControllerClient::connect(addr) to building a client from connect_channel(&addr).

Design notes

  • Endpoints keep the http:// scheme because tonic requires a full URI for the h2c transport; entries without a scheme are normalized to http://.
  • The crate is intentionally minimal (only tonic + tracing) to keep layering clean and avoid pulling proto/config into the transport helper.

Testing

  • spur-client unit tests: endpoint parsing (scheme normalization, whitespace, empty entries) and rotation (dead-first reaches a live server, all-down returns an error).
  • spur-core unit tests for ControllerConfig::endpoints().
  • Native-host e2e (test_controller_failover.py) run against the 4-node bare-metal cluster, all passing: rotation past a dead first endpoint, live-first list unaffected by a trailing dead endpoint, all-down list fails cleanly with a connection error (no hang), and a job submitted through a failover list completes.
  • cargo clippy --workspace --exclude spur-ffi --all-targets clean; cargo fmt applied.
  • Manual 3-peer Raft failover on the bare-metal testbed: killing the first controller in the list, the CLI rotates to a survivor and commands succeed after re-election.

Full multi-controller Raft failover in the automated native-host harness is deferred; the harness needs Raft support first.

…rotation

Add a service-agnostic spur-client crate that dials a tonic Channel from a
comma-separated endpoint list, rotating to the next endpoint when one is
unreachable. spurctld, spurd, and the CLI now build clients from this channel
instead of connecting to a single hardcoded address, so a single reachable
controller in an HA quorum is enough (server-side leader forwarding handles the
rest).

- spur-client: parse_endpoints + connect_channel, with unit tests for parsing
  and rotation (dead-first, all-down).
- ControllerConfig::endpoints(): expand hosts + listen_addr port into an
  endpoint list; CLI exports it as a comma-joined SPUR_CONTROLLER_ADDR.
- Refactor call sites in spur-cli, spurd, and spur-ffi to
  SlurmControllerClient::new(connect_channel(&addr).await?).
- Docs + examples: document the comma-separated HA controller list.
- Native-host e2e: test_controller_failover covers rotation past a dead
  endpoint, live-first lists, clean all-down failure, and submit-through-list.
Copilot AI review requested due to automatic review settings July 5, 2026 04:01
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 64.55026% with 67 lines in your changes missing coverage. Please review.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #384      +/-   ##
==========================================
+ Coverage   68.22%   68.29%   +0.07%     
==========================================
  Files         134      135       +1     
  Lines       36098    36235     +137     
==========================================
+ Hits        24625    24744     +119     
- Misses      11473    11491      +18     
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copilot AI left a comment

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.

Pull request overview

This PR introduces client-side controller failover for HA deployments by adding a new spur-client crate that dials a comma-separated list of controller endpoints and updates spurd, spur-cli, and spur-ffi call sites to use it. It also adds config/documentation plumbing and E2E coverage to validate endpoint rotation behavior.

Changes:

  • Add crates/spur-client with endpoint parsing + connection rotation and unit tests.
  • Switch controller connections in spurd, spur-cli, and spur-ffi from SlurmControllerClient::connect(addr) to SlurmControllerClient::new(connect_channel(...)).
  • Export multi-endpoint controller addresses via config -> SPUR_CONTROLLER_ADDR, document usage, and add native-host E2E failover tests.

Reviewed changes

Copilot reviewed 30 out of 31 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/native_host/e2e/test_controller_failover.py Adds E2E coverage for comma-separated controller endpoint rotation via CLI.
tests/native_host/e2e/cluster.py Extends CLI helpers to override SPUR_CONTROLLER_ADDR per invocation.
examples/spur.conf Documents controller.hosts HA configuration for client-side failover.
docs/deployment/native-host.rst Documents comma-separated --controller / SPUR_CONTROLLER_ADDR usage for HA.
crates/spurd/src/reporter.rs Uses spur_client::connect_channel for registration/deregistration/heartbeat connections.
crates/spurd/src/agent_server.rs Uses spur_client::connect_channel when reporting job completion to controllers.
crates/spurd/Cargo.toml Adds spur-client dependency to spurd.
crates/spur-ffi/src/lib.rs Routes FFI controller calls through spur_client::connect_channel.
crates/spur-ffi/Cargo.toml Adds spur-client dependency to spur-ffi.
crates/spur-core/src/config.rs Adds ControllerConfig::endpoints() and unit tests for endpoint expansion.
crates/spur-client/src/lib.rs New failover dialer: parse/normalize endpoints, rotate on failure, unit tests.
crates/spur-client/Cargo.toml New crate manifest and dev-deps for transport tests.
crates/spur-cli/src/token.rs Switches token subcommands to build client from connect_channel.
crates/spur-cli/src/sstat.rs Switches sstat controller connection to connect_channel.
crates/spur-cli/src/srun.rs Switches srun controller connections to connect_channel.
crates/spur-cli/src/squeue.rs Switches squeue controller connection to connect_channel.
crates/spur-cli/src/sprio.rs Switches sprio controller connection to connect_channel.
crates/spur-cli/src/smd.rs Switches smd controller connection to connect_channel.
crates/spur-cli/src/sinfo.rs Switches sinfo controller connection to connect_channel.
crates/spur-cli/src/sdiag.rs Switches sdiag controller connection to connect_channel.
crates/spur-cli/src/scontrol.rs Switches scontrol controller connections to connect_channel.
crates/spur-cli/src/scancel.rs Switches scancel controller connection to connect_channel.
crates/spur-cli/src/sbatch.rs Switches sbatch controller connection to connect_channel.
crates/spur-cli/src/sattach.rs Switches sattach controller connection to connect_channel.
crates/spur-cli/src/salloc.rs Switches salloc controller connection to connect_channel.
crates/spur-cli/src/node.rs Switches node subcommands controller connections to connect_channel.
crates/spur-cli/src/main.rs Loads config-derived multi-endpoint controller list into SPUR_CONTROLLER_ADDR.
crates/spur-cli/src/exec.rs Switches exec controller connection to connect_channel.
crates/spur-cli/Cargo.toml Adds spur-client dependency to spur-cli.
Cargo.toml Adds crates/spur-client to the workspace members.
Cargo.lock Records new crate/dependency resolution from adding spur-client + dev-deps.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +69 to +71
async fn try_connect(endpoint: &str) -> Result<Channel, tonic::transport::Error> {
Endpoint::from_shared(endpoint.to_string())?.connect().await
}
Comment on lines +328 to +334
pub fn endpoints(&self) -> Vec<String> {
let port = self.listen_addr.rsplit(':').next().unwrap_or("6817");
self.hosts
.iter()
.map(|host| format!("http://{host}:{port}"))
.collect()
}
Comment on lines +58 to +62
Err(e) => warn!(
%endpoint,
error = %e,
"controller endpoint unreachable, trying next"
),
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(spurd,spur-cli): support multiple controller endpoints with client-side failover

3 participants