This is an automated email from the ASF dual-hosted git repository.
yuxia pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fluss-rust.git
The following commit(s) were added to refs/heads/main by this push:
new ee927b0 feat: Add get_server_nodes to Admin (#363)
ee927b0 is described below
commit ee927b0e9ded17fd522236641b14d203c1707599
Author: Kaiqi Dong <[email protected]>
AuthorDate: Sat Feb 28 05:11:59 2026 +0100
feat: Add get_server_nodes to Admin (#363)
---
bindings/cpp/include/fluss.hpp | 10 +++
bindings/cpp/src/admin.cpp | 18 ++++
bindings/cpp/src/lib.rs | 41 +++++++++
bindings/cpp/test/test_admin.cpp | 25 ++++++
bindings/python/fluss/__init__.pyi | 32 +++++++
bindings/python/src/admin.rs | 81 +++++++++++++++++
bindings/python/src/lib.rs | 1 +
bindings/python/test/test_admin.py | 17 ++++
crates/fluss/src/client/admin.rs | 8 ++
crates/fluss/src/client/metadata.rs | 2 +-
crates/fluss/src/cluster/cluster.rs | 112 ++++++++++++++++++++++++
crates/fluss/src/cluster/mod.rs | 24 ++++-
crates/fluss/src/lib.rs | 1 +
crates/fluss/src/rpc/server_connection.rs | 2 +-
crates/fluss/tests/integration/admin.rs | 43 +++++++++
website/docs/user-guide/cpp/api-reference.md | 16 ++++
website/docs/user-guide/python/api-reference.md | 11 +++
website/docs/user-guide/rust/api-reference.md | 16 ++++
18 files changed, 457 insertions(+), 3 deletions(-)
diff --git a/bindings/cpp/include/fluss.hpp b/bindings/cpp/include/fluss.hpp
index 0a62af9..cb06028 100644
--- a/bindings/cpp/include/fluss.hpp
+++ b/bindings/cpp/include/fluss.hpp
@@ -881,6 +881,14 @@ struct PartitionInfo {
std::string partition_name;
};
+struct ServerNode {
+ int32_t id;
+ std::string host;
+ uint32_t port;
+ std::string server_type;
+ std::string uid;
+};
+
/// Descriptor for create_database (optional). Leave comment and properties
empty for default.
struct DatabaseDescriptor {
std::string comment;
@@ -1073,6 +1081,8 @@ class Admin {
Result TableExists(const TablePath& table_path, bool& out);
+ Result GetServerNodes(std::vector<ServerNode>& out);
+
private:
Result DoListOffsets(const TablePath& table_path, const
std::vector<int32_t>& bucket_ids,
const OffsetSpec& offset_spec,
std::unordered_map<int32_t, int64_t>& out,
diff --git a/bindings/cpp/src/admin.cpp b/bindings/cpp/src/admin.cpp
index 8deb182..49300c1 100644
--- a/bindings/cpp/src/admin.cpp
+++ b/bindings/cpp/src/admin.cpp
@@ -346,4 +346,22 @@ Result Admin::TableExists(const TablePath& table_path,
bool& out) {
return result;
}
+Result Admin::GetServerNodes(std::vector<ServerNode>& out) {
+ if (!Available()) {
+ return utils::make_client_error("Admin not available");
+ }
+
+ auto ffi_result = admin_->get_server_nodes();
+ auto result = utils::from_ffi_result(ffi_result.result);
+ if (result.Ok()) {
+ out.clear();
+ out.reserve(ffi_result.server_nodes.size());
+ for (const auto& node : ffi_result.server_nodes) {
+ out.push_back({node.node_id, std::string(node.host), node.port,
+ std::string(node.server_type),
std::string(node.uid)});
+ }
+ }
+ return result;
+}
+
} // namespace fluss
diff --git a/bindings/cpp/src/lib.rs b/bindings/cpp/src/lib.rs
index 9b01d32..d26af6a 100644
--- a/bindings/cpp/src/lib.rs
+++ b/bindings/cpp/src/lib.rs
@@ -229,6 +229,19 @@ mod ffi {
value: bool,
}
+ struct FfiServerNode {
+ node_id: i32,
+ host: String,
+ port: u32,
+ server_type: String,
+ uid: String,
+ }
+
+ struct FfiServerNodesResult {
+ result: FfiResult,
+ server_nodes: Vec<FfiServerNode>,
+ }
+
extern "Rust" {
type Connection;
type Admin;
@@ -319,6 +332,7 @@ mod ffi {
fn get_database_info(self: &Admin, database_name: &str) ->
FfiDatabaseInfoResult;
fn list_tables(self: &Admin, database_name: &str) ->
FfiListTablesResult;
fn table_exists(self: &Admin, table_path: &FfiTablePath) ->
FfiBoolResult;
+ fn get_server_nodes(self: &Admin) -> FfiServerNodesResult;
// Table
unsafe fn delete_table(table: *mut Table);
@@ -1104,6 +1118,33 @@ impl Admin {
},
}
}
+
+ fn get_server_nodes(&self) -> ffi::FfiServerNodesResult {
+ let result = RUNTIME.block_on(async {
self.inner.get_server_nodes().await });
+
+ match result {
+ Ok(nodes) => {
+ let server_nodes: Vec<ffi::FfiServerNode> = nodes
+ .into_iter()
+ .map(|node| ffi::FfiServerNode {
+ node_id: node.id(),
+ host: node.host().to_string(),
+ port: node.port(),
+ server_type: node.server_type().to_string(),
+ uid: node.uid().to_string(),
+ })
+ .collect();
+ ffi::FfiServerNodesResult {
+ result: ok_result(),
+ server_nodes,
+ }
+ }
+ Err(e) => ffi::FfiServerNodesResult {
+ result: err_from_core_error(&e),
+ server_nodes: vec![],
+ },
+ }
+ }
}
// Table implementation
diff --git a/bindings/cpp/test/test_admin.cpp b/bindings/cpp/test/test_admin.cpp
index b6bb25b..99f93fc 100644
--- a/bindings/cpp/test/test_admin.cpp
+++ b/bindings/cpp/test/test_admin.cpp
@@ -285,6 +285,31 @@ TEST_F(AdminTest, ErrorTableAlreadyExist) {
ASSERT_OK(adm.DropDatabase(db_name, true, true));
}
+TEST_F(AdminTest, GetServerNodes) {
+ auto& adm = admin();
+
+ std::vector<fluss::ServerNode> nodes;
+ ASSERT_OK(adm.GetServerNodes(nodes));
+
+ ASSERT_GT(nodes.size(), 0u) << "Expected at least one server node";
+
+ bool has_coordinator = false;
+ bool has_tablet = false;
+ for (const auto& node : nodes) {
+ EXPECT_FALSE(node.host.empty()) << "Server node host should not be
empty";
+ EXPECT_GT(node.port, 0u) << "Server node port should be > 0";
+ EXPECT_FALSE(node.uid.empty()) << "Server node uid should not be
empty";
+
+ if (node.server_type == "CoordinatorServer") {
+ has_coordinator = true;
+ } else if (node.server_type == "TabletServer") {
+ has_tablet = true;
+ }
+ }
+ EXPECT_TRUE(has_coordinator) << "Expected a coordinator server node";
+ EXPECT_TRUE(has_tablet) << "Expected at least one tablet server node";
+}
+
TEST_F(AdminTest, ErrorTableNotExist) {
auto& adm = admin();
diff --git a/bindings/python/fluss/__init__.pyi
b/bindings/python/fluss/__init__.pyi
index 514d011..4c2142d 100644
--- a/bindings/python/fluss/__init__.pyi
+++ b/bindings/python/fluss/__init__.pyi
@@ -185,6 +185,31 @@ class FlussConnection:
) -> bool: ...
def __repr__(self) -> str: ...
+class ServerNode:
+ """Information about a server node in the Fluss cluster."""
+
+ @property
+ def id(self) -> int:
+ """The server node ID."""
+ ...
+ @property
+ def host(self) -> str:
+ """The hostname of the server."""
+ ...
+ @property
+ def port(self) -> int:
+ """The port number of the server."""
+ ...
+ @property
+ def server_type(self) -> str:
+ """The type of server ('CoordinatorServer' or 'TabletServer')."""
+ ...
+ @property
+ def uid(self) -> str:
+ """The unique identifier of the server (e.g. 'cs-0', 'ts-1')."""
+ ...
+ def __repr__(self) -> str: ...
+
class FlussAdmin:
async def create_database(
self,
@@ -307,6 +332,13 @@ class FlussAdmin:
List of PartitionInfo objects
"""
...
+ async def get_server_nodes(self) -> List[ServerNode]:
+ """Get all alive server nodes in the cluster.
+
+ Returns:
+ List of ServerNode objects (coordinator and tablet servers)
+ """
+ ...
def __repr__(self) -> str: ...
diff --git a/bindings/python/src/admin.rs b/bindings/python/src/admin.rs
index 30db375..703b133 100644
--- a/bindings/python/src/admin.rs
+++ b/bindings/python/src/admin.rs
@@ -501,6 +501,30 @@ impl FlussAdmin {
})
}
+ /// Get all alive server nodes in the cluster.
+ ///
+ /// Returns:
+ /// List[ServerNode]: List of server nodes (coordinator and tablet
servers)
+ pub fn get_server_nodes<'py>(&self, py: Python<'py>) ->
PyResult<Bound<'py, PyAny>> {
+ let admin = self.__admin.clone();
+
+ future_into_py(py, async move {
+ let nodes = admin
+ .get_server_nodes()
+ .await
+ .map_err(|e| FlussError::from_core_error(&e))?;
+
+ Python::attach(|py| {
+ let py_list = pyo3::types::PyList::empty(py);
+ for node in nodes {
+ let py_node = ServerNode::from_core(node);
+ py_list.append(Py::new(py, py_node)?)?;
+ }
+ Ok(py_list.unbind())
+ })
+ })
+ }
+
fn __repr__(&self) -> String {
"FlussAdmin()".to_string()
}
@@ -552,3 +576,60 @@ impl PartitionInfo {
}
}
}
+
+/// Information about a server node in the Fluss cluster
+#[pyclass]
+pub struct ServerNode {
+ id: i32,
+ host: String,
+ port: u32,
+ server_type: String,
+ uid: String,
+}
+
+#[pymethods]
+impl ServerNode {
+ #[getter]
+ fn id(&self) -> i32 {
+ self.id
+ }
+
+ #[getter]
+ fn host(&self) -> &str {
+ &self.host
+ }
+
+ #[getter]
+ fn port(&self) -> u32 {
+ self.port
+ }
+
+ #[getter]
+ fn server_type(&self) -> &str {
+ &self.server_type
+ }
+
+ #[getter]
+ fn uid(&self) -> &str {
+ &self.uid
+ }
+
+ fn __repr__(&self) -> String {
+ format!(
+ "ServerNode(id={}, host='{}', port={}, server_type='{}')",
+ self.id, self.host, self.port, self.server_type
+ )
+ }
+}
+
+impl ServerNode {
+ pub fn from_core(node: fcore::ServerNode) -> Self {
+ Self {
+ id: node.id(),
+ host: node.host().to_string(),
+ port: node.port(),
+ server_type: node.server_type().to_string(),
+ uid: node.uid().to_string(),
+ }
+ }
+}
diff --git a/bindings/python/src/lib.rs b/bindings/python/src/lib.rs
index ebc0d54..6890e08 100644
--- a/bindings/python/src/lib.rs
+++ b/bindings/python/src/lib.rs
@@ -125,6 +125,7 @@ fn _fluss(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<ScanRecords>()?;
m.add_class::<RecordBatch>()?;
m.add_class::<PartitionInfo>()?;
+ m.add_class::<ServerNode>()?;
m.add_class::<OffsetSpec>()?;
m.add_class::<WriteResultHandle>()?;
m.add_class::<DatabaseDescriptor>()?;
diff --git a/bindings/python/test/test_admin.py
b/bindings/python/test/test_admin.py
index f203400..e2f4343 100644
--- a/bindings/python/test/test_admin.py
+++ b/bindings/python/test/test_admin.py
@@ -272,6 +272,23 @@ async def test_error_table_not_exist(admin):
await admin.drop_table(table_path, ignore_if_not_exists=True)
+async def test_get_server_nodes(admin):
+ """Test get_server_nodes returns coordinator and tablet servers."""
+ nodes = await admin.get_server_nodes()
+
+ assert len(nodes) > 0, "Expected at least one server node"
+
+ server_types = [n.server_type for n in nodes]
+ assert "CoordinatorServer" in server_types, "Expected a coordinator server"
+ assert "TabletServer" in server_types, "Expected at least one tablet
server"
+
+ for node in nodes:
+ assert node.host, "Server node host should not be empty"
+ assert node.port > 0, "Server node port should be > 0"
+ assert node.uid, "Server node uid should not be empty"
+ assert repr(node).startswith("ServerNode(")
+
+
async def test_error_table_not_partitioned(admin):
"""Test error when calling partition operations on non-partitioned
table."""
db_name = "py_test_error_not_partitioned_db"
diff --git a/crates/fluss/src/client/admin.rs b/crates/fluss/src/client/admin.rs
index 3012f85..7a79e5e 100644
--- a/crates/fluss/src/client/admin.rs
+++ b/crates/fluss/src/client/admin.rs
@@ -16,6 +16,7 @@
// under the License.
use crate::client::metadata::Metadata;
+use crate::cluster::ServerNode;
use crate::metadata::{
DatabaseDescriptor, DatabaseInfo, JsonSerde, LakeSnapshot, PartitionInfo,
PartitionSpec,
PhysicalTablePath, TableBucket, TableDescriptor, TableInfo, TablePath,
@@ -267,6 +268,13 @@ impl FlussAdmin {
))
}
+ /// Get all alive server nodes in the cluster, including the coordinator
+ /// and all tablet servers. Refreshes cluster metadata before returning.
+ pub async fn get_server_nodes(&self) -> Result<Vec<ServerNode>> {
+ self.metadata.reinit_cluster().await?;
+ Ok(self.metadata.get_cluster().get_server_nodes())
+ }
+
/// Get the latest lake snapshot for a table
pub async fn get_latest_lake_snapshot(&self, table_path: &TablePath) ->
Result<LakeSnapshot> {
let response = self
diff --git a/crates/fluss/src/client/metadata.rs
b/crates/fluss/src/client/metadata.rs
index 3d8e77b..8581464 100644
--- a/crates/fluss/src/client/metadata.rs
+++ b/crates/fluss/src/client/metadata.rs
@@ -89,7 +89,7 @@ impl Metadata {
Cluster::from_metadata_response(response, None)
}
- async fn reinit_cluster(&self) -> Result<()> {
+ pub(crate) async fn reinit_cluster(&self) -> Result<()> {
let cluster = Self::init_cluster(&self.bootstrap,
self.connections.clone()).await?;
*self.cluster.write() = cluster.into();
Ok(())
diff --git a/crates/fluss/src/cluster/cluster.rs
b/crates/fluss/src/cluster/cluster.rs
index 5b1e083..d551870 100644
--- a/crates/fluss/src/cluster/cluster.rs
+++ b/crates/fluss/src/cluster/cluster.rs
@@ -369,6 +369,15 @@ impl Cluster {
.unwrap_or(&EMPTY)
}
+ pub fn get_server_nodes(&self) -> Vec<ServerNode> {
+ let mut nodes = Vec::new();
+ if let Some(coordinator) = &self.coordinator_server {
+ nodes.push(coordinator.clone());
+ }
+ nodes.extend(self.alive_tablet_servers.iter().cloned());
+ nodes
+ }
+
pub fn get_one_available_server(&self) -> Option<&ServerNode> {
if self.alive_tablet_servers.is_empty() {
return None;
@@ -427,3 +436,106 @@ fn get_bucket_locations(
}
bucket_locations
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ fn make_coordinator() -> ServerNode {
+ ServerNode::new(
+ 0,
+ "coord-host".to_string(),
+ 9123,
+ ServerType::CoordinatorServer,
+ )
+ }
+
+ fn make_tablet_servers() -> HashMap<i32, ServerNode> {
+ let mut servers = HashMap::new();
+ servers.insert(
+ 1,
+ ServerNode::new(1, "ts1-host".to_string(), 9124,
ServerType::TabletServer),
+ );
+ servers.insert(
+ 2,
+ ServerNode::new(2, "ts2-host".to_string(), 9125,
ServerType::TabletServer),
+ );
+ servers
+ }
+
+ #[test]
+ fn test_server_node_getters() {
+ let node = ServerNode::new(5, "myhost".to_string(), 8080,
ServerType::TabletServer);
+ assert_eq!(node.id(), 5);
+ assert_eq!(node.host(), "myhost");
+ assert_eq!(node.port(), 8080);
+ assert_eq!(node.server_type(), &ServerType::TabletServer);
+ assert_eq!(node.uid(), "ts-5");
+ assert_eq!(node.url(), "myhost:8080");
+ }
+
+ #[test]
+ fn test_server_type_display() {
+ assert_eq!(ServerType::TabletServer.to_string(), "TabletServer");
+ assert_eq!(
+ ServerType::CoordinatorServer.to_string(),
+ "CoordinatorServer"
+ );
+ }
+
+ #[test]
+ fn test_get_server_nodes_with_coordinator_and_tablets() {
+ let cluster = Cluster::new(
+ Some(make_coordinator()),
+ make_tablet_servers(),
+ HashMap::new(),
+ HashMap::new(),
+ HashMap::new(),
+ HashMap::new(),
+ HashMap::new(),
+ );
+
+ let nodes = cluster.get_server_nodes();
+ assert_eq!(nodes.len(), 3);
+
+ let coordinator_count = nodes
+ .iter()
+ .filter(|n| *n.server_type() == ServerType::CoordinatorServer)
+ .count();
+ assert_eq!(coordinator_count, 1);
+
+ let tablet_count = nodes
+ .iter()
+ .filter(|n| *n.server_type() == ServerType::TabletServer)
+ .count();
+ assert_eq!(tablet_count, 2);
+ }
+
+ #[test]
+ fn test_get_server_nodes_no_coordinator() {
+ let cluster = Cluster::new(
+ None,
+ make_tablet_servers(),
+ HashMap::new(),
+ HashMap::new(),
+ HashMap::new(),
+ HashMap::new(),
+ HashMap::new(),
+ );
+
+ let nodes = cluster.get_server_nodes();
+ assert_eq!(nodes.len(), 2);
+ assert!(
+ nodes
+ .iter()
+ .all(|n| *n.server_type() == ServerType::TabletServer)
+ );
+ }
+
+ #[test]
+ fn test_get_server_nodes_empty_cluster() {
+ let cluster = Cluster::default();
+ let nodes = cluster.get_server_nodes();
+ assert!(nodes.is_empty());
+ }
+}
diff --git a/crates/fluss/src/cluster/mod.rs b/crates/fluss/src/cluster/mod.rs
index 58e80c0..8b825ee 100644
--- a/crates/fluss/src/cluster/mod.rs
+++ b/crates/fluss/src/cluster/mod.rs
@@ -17,6 +17,7 @@
use crate::BucketId;
use crate::metadata::{PhysicalTablePath, TableBucket};
+use std::fmt;
use std::sync::Arc;
#[allow(clippy::module_inception)]
@@ -47,7 +48,7 @@ impl ServerNode {
}
}
- pub fn uid(&self) -> &String {
+ pub fn uid(&self) -> &str {
&self.uid
}
@@ -58,6 +59,18 @@ impl ServerNode {
pub fn id(&self) -> i32 {
self.id
}
+
+ pub fn host(&self) -> &str {
+ &self.host
+ }
+
+ pub fn port(&self) -> u32 {
+ self.port
+ }
+
+ pub fn server_type(&self) -> &ServerType {
+ &self.server_type
+ }
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
@@ -66,6 +79,15 @@ pub enum ServerType {
CoordinatorServer,
}
+impl fmt::Display for ServerType {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ ServerType::TabletServer => write!(f, "TabletServer"),
+ ServerType::CoordinatorServer => write!(f, "CoordinatorServer"),
+ }
+ }
+}
+
#[derive(Debug, Clone)]
pub struct BucketLocation {
pub table_bucket: TableBucket,
diff --git a/crates/fluss/src/lib.rs b/crates/fluss/src/lib.rs
index f079db2..689c37c 100644
--- a/crates/fluss/src/lib.rs
+++ b/crates/fluss/src/lib.rs
@@ -22,6 +22,7 @@ pub mod row;
pub mod rpc;
mod cluster;
+pub use cluster::{ServerNode, ServerType};
pub mod config;
pub mod error;
diff --git a/crates/fluss/src/rpc/server_connection.rs
b/crates/fluss/src/rpc/server_connection.rs
index c8fe9ae..a345c2f 100644
--- a/crates/fluss/src/rpc/server_connection.rs
+++ b/crates/fluss/src/rpc/server_connection.rs
@@ -84,7 +84,7 @@ impl RpcClient {
}
}
- connections.insert(server_id.clone(), new_server.clone());
+ connections.insert(server_id.to_owned(), new_server.clone());
}
Ok(new_server)
}
diff --git a/crates/fluss/tests/integration/admin.rs
b/crates/fluss/tests/integration/admin.rs
index c0745dc..3502923 100644
--- a/crates/fluss/tests/integration/admin.rs
+++ b/crates/fluss/tests/integration/admin.rs
@@ -519,6 +519,49 @@ mod admin_test {
.expect("drop_table with ignore_if_not_exists should succeed");
}
+ #[tokio::test]
+ async fn test_get_server_nodes() {
+ let cluster = get_fluss_cluster();
+ let connection = cluster.get_fluss_connection().await;
+ let admin = connection.get_admin().await.unwrap();
+
+ let nodes = admin
+ .get_server_nodes()
+ .await
+ .expect("should get server nodes");
+
+ assert!(
+ !nodes.is_empty(),
+ "Expected at least one server node in the cluster"
+ );
+
+ let has_coordinator = nodes
+ .iter()
+ .any(|n| *n.server_type() == fluss::ServerType::CoordinatorServer);
+ assert!(has_coordinator, "Expected a coordinator server node");
+
+ let tablet_count = nodes
+ .iter()
+ .filter(|n| *n.server_type() == fluss::ServerType::TabletServer)
+ .count();
+ assert!(
+ tablet_count >= 1,
+ "Expected at least one tablet server node"
+ );
+
+ for node in &nodes {
+ assert!(
+ !node.host().is_empty(),
+ "Server node host should not be empty"
+ );
+ assert!(node.port() > 0, "Server node port should be > 0");
+ assert!(
+ !node.uid().is_empty(),
+ "Server node uid should not be empty"
+ );
+ }
+ }
+
#[tokio::test]
async fn test_error_table_not_partitioned() {
let cluster = get_fluss_cluster();
diff --git a/website/docs/user-guide/cpp/api-reference.md
b/website/docs/user-guide/cpp/api-reference.md
index 30d89a9..489f13a 100644
--- a/website/docs/user-guide/cpp/api-reference.md
+++ b/website/docs/user-guide/cpp/api-reference.md
@@ -78,6 +78,22 @@ Complete API reference for the Fluss C++ client.
|-----------------------------------------------------------------------------|------------------------------|
| `GetLatestLakeSnapshot(const TablePath& path, LakeSnapshot& out) -> Result`
| Get the latest lake snapshot |
+### Cluster Operations
+
+| Method | Description
|
+|-----------------------------------------------------------|----------------------------------------------------|
+| `GetServerNodes(std::vector<ServerNode>& out) -> Result` | Get all alive
server nodes (coordinator + tablets) |
+
+## `ServerNode`
+
+| Field | Type | Description
|
+|---------------|---------------|----------------------------------------------------------|
+| `id` | `int32_t` | Server node ID
|
+| `host` | `std::string` | Hostname of the server
|
+| `port` | `uint32_t` | Port number
|
+| `server_type` | `std::string` | Server type (`"CoordinatorServer"` or
`"TabletServer"`) |
+| `uid` | `std::string` | Unique identifier (e.g. `"cs-0"`, `"ts-1"`)
|
+
## `Table`
| Method | Description |
diff --git a/website/docs/user-guide/python/api-reference.md
b/website/docs/user-guide/python/api-reference.md
index fa62fd9..1c97066 100644
--- a/website/docs/user-guide/python/api-reference.md
+++ b/website/docs/user-guide/python/api-reference.md
@@ -50,6 +50,17 @@ Supports `with` statement (context manager).
| `await drop_partition(table_path, partition_spec,
ignore_if_not_exists=False)` | Drop a
partition |
| `await list_partition_infos(table_path) -> list[PartitionInfo]`
| List partitions
|
| `await get_latest_lake_snapshot(table_path) -> LakeSnapshot`
| Get latest lake snapshot
|
+| `await get_server_nodes() -> list[ServerNode]`
| Get all alive server nodes
|
+
+## `ServerNode`
+
+| Property | Description
|
+|--------------------------|------------------------------------------------------------|
+| `.id -> int` | Server node ID
|
+| `.host -> str` | Hostname of the server
|
+| `.port -> int` | Port number
|
+| `.server_type -> str` | Server type (`"CoordinatorServer"` or
`"TabletServer"`) |
+| `.uid -> str` | Unique identifier (e.g. `"cs-0"`, `"ts-1"`)
|
## `FlussTable`
diff --git a/website/docs/user-guide/rust/api-reference.md
b/website/docs/user-guide/rust/api-reference.md
index a38cd7d..2d149aa 100644
--- a/website/docs/user-guide/rust/api-reference.md
+++ b/website/docs/user-guide/rust/api-reference.md
@@ -71,6 +71,22 @@ Complete API reference for the Fluss Rust client.
|--------------------------------------------------------------------------------------------|------------------------------|
| `async fn get_latest_lake_snapshot(&self, table_path: &TablePath) ->
Result<LakeSnapshot>` | Get the latest lake snapshot |
+### Cluster Operations
+
+| Method | Description
|
+|---------------------------------------------------------------|-----------------------------------------------------|
+| `async fn get_server_nodes(&self) -> Result<Vec<ServerNode>>` | Get all
alive server nodes (coordinator + tablets) |
+
+## `ServerNode`
+
+| Method | Description
|
+|-----------------------------------|------------------------------------------------------|
+| `fn id(&self) -> i32` | Server node ID
|
+| `fn host(&self) -> &str` | Hostname of the server
|
+| `fn port(&self) -> u32` | Port number
|
+| `fn server_type(&self) -> &ServerType` | Server type (`CoordinatorServer` or
`TabletServer`) |
+| `fn uid(&self) -> &str` | Unique identifier (e.g. `"cs-0"`,
`"ts-1"`) |
+
## `FlussTable<'a>`
| Method | Description
|