From 91d6d3e5e88146f0619212c4432c417c97b1f9ae Mon Sep 17 00:00:00 2001 From: charlesdong1991 Date: Sun, 22 Feb 2026 12:38:29 +0100 Subject: [PATCH 1/2] feat: Add get_server_nodes to Admin --- 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 | 22 ++++ crates/fluss/src/lib.rs | 1 + crates/fluss/tests/integration/admin.rs | 43 +++++++ website/docs/user-guide/cpp/api-reference.md | 16 +++ .../docs/user-guide/python/api-reference.md | 11 ++ website/docs/user-guide/rust/api-reference.md | 16 +++ 17 files changed, 455 insertions(+), 1 deletion(-) diff --git a/bindings/cpp/include/fluss.hpp b/bindings/cpp/include/fluss.hpp index 9a62828c..12f2cabb 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; @@ -1068,6 +1076,8 @@ class Admin { Result TableExists(const TablePath& table_path, bool& out); + Result GetServerNodes(std::vector& out); + private: Result DoListOffsets(const TablePath& table_path, const std::vector& bucket_ids, const OffsetSpec& offset_spec, std::unordered_map& out, diff --git a/bindings/cpp/src/admin.cpp b/bindings/cpp/src/admin.cpp index 8deb182d..49300c15 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& 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 fad98cf1..eb251bf8 100644 --- a/bindings/cpp/src/lib.rs +++ b/bindings/cpp/src/lib.rs @@ -226,6 +226,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, + } + extern "Rust" { type Connection; type Admin; @@ -316,6 +329,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); @@ -1089,6 +1103,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 = 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 b6bb25b7..99f93fcf 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 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 6f9ae0b3..cdb5d13d 100644 --- a/bindings/python/fluss/__init__.pyi +++ b/bindings/python/fluss/__init__.pyi @@ -181,6 +181,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, @@ -303,6 +328,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 30db3750..703b1334 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> { + 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 ebc0d54c..6890e088 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::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/bindings/python/test/test_admin.py b/bindings/python/test/test_admin.py index f203400f..e2f43431 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 3012f85c..7a79e5ed 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> { + 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 { let response = self diff --git a/crates/fluss/src/client/metadata.rs b/crates/fluss/src/client/metadata.rs index 3d8e77b2..85814647 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 5b1e0836..d5518709 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 { + 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 { + 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 58e80c00..9f257f22 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)] @@ -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 f079db28..689c37ca 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/tests/integration/admin.rs b/crates/fluss/tests/integration/admin.rs index 121158c1..425f49f1 100644 --- a/crates/fluss/tests/integration/admin.rs +++ b/crates/fluss/tests/integration/admin.rs @@ -513,6 +513,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 c18778b2..39b59390 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& 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 fa62fd9a..1c97066c 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 a38cd7d0..f1729a44 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` | Get the latest lake snapshot | +### Cluster Operations + +| Method | Description | +|---------------------------------------------------------------|-----------------------------------------------------| +| `async fn get_server_nodes(&self) -> Result>` | 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) -> String` | Unique identifier (e.g. `"cs-0"`, `"ts-1"`) | + ## `FlussTable<'a>` | Method | Description | From 3a96d9a2e4ce3737b86cfd472dd04cb2796246be Mon Sep 17 00:00:00 2001 From: charlesdong1991 Date: Mon, 23 Feb 2026 09:18:12 +0100 Subject: [PATCH 2/2] use &str --- crates/fluss/src/cluster/mod.rs | 2 +- crates/fluss/src/rpc/server_connection.rs | 2 +- website/docs/user-guide/rust/api-reference.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/fluss/src/cluster/mod.rs b/crates/fluss/src/cluster/mod.rs index 9f257f22..8b825eee 100644 --- a/crates/fluss/src/cluster/mod.rs +++ b/crates/fluss/src/cluster/mod.rs @@ -48,7 +48,7 @@ impl ServerNode { } } - pub fn uid(&self) -> &String { + pub fn uid(&self) -> &str { &self.uid } diff --git a/crates/fluss/src/rpc/server_connection.rs b/crates/fluss/src/rpc/server_connection.rs index c8fe9ae3..a345c2fd 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/website/docs/user-guide/rust/api-reference.md b/website/docs/user-guide/rust/api-reference.md index f1729a44..2d149aac 100644 --- a/website/docs/user-guide/rust/api-reference.md +++ b/website/docs/user-guide/rust/api-reference.md @@ -85,7 +85,7 @@ Complete API reference for the Fluss Rust client. | `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) -> String` | Unique identifier (e.g. `"cs-0"`, `"ts-1"`) | +| `fn uid(&self) -> &str` | Unique identifier (e.g. `"cs-0"`, `"ts-1"`) | ## `FlussTable<'a>`