This is an automated email from the ASF dual-hosted git repository.

fresh-borzoni 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 bf94c1c7 [client] Allow bootstrap endpoint with unknown server type 
(#622)
bf94c1c7 is described below

commit bf94c1c731432c8097326b224d632edb9f5bd0d6
Author: Xiaobing Fang <[email protected]>
AuthorDate: Sat Jun 20 01:03:43 2026 +0800

    [client] Allow bootstrap endpoint with unknown server type (#622)
    
    * fix: allow unknown bootstrap server type
    
    * test: cover tablet bootstrap server endpoint
    
    * fix: align unknown server type mapping with Java client
    
    * docs: align cpp/python binding docs with Unknown server type
    
    Co-Authored-By: Claude Opus 4.8 <[email protected]>
    
    ---------
    
    Co-authored-by: Claude Opus 4.8 <[email protected]>
---
 bindings/elixir/lib/fluss/server_node.ex        |  2 +-
 bindings/elixir/native/fluss_nif/src/admin.rs   |  2 ++
 bindings/python/fluss/__init__.pyi              |  2 +-
 crates/fluss-test-cluster/src/lib.rs            | 21 +++++++++++++++
 crates/fluss/src/client/metadata.rs             |  2 +-
 crates/fluss/src/cluster/cluster.rs             |  8 ++++++
 crates/fluss/src/cluster/mod.rs                 | 21 +++++++++------
 crates/fluss/src/rpc/server_connection.rs       | 30 ++++++++++++++++-----
 crates/fluss/tests/integration/admin.rs         | 35 +++++++++++++++++++++++++
 website/docs/user-guide/cpp/api-reference.md    |  4 ++-
 website/docs/user-guide/python/api-reference.md |  4 ++-
 website/docs/user-guide/rust/api-reference.md   |  5 +++-
 12 files changed, 115 insertions(+), 21 deletions(-)

diff --git a/bindings/elixir/lib/fluss/server_node.ex 
b/bindings/elixir/lib/fluss/server_node.ex
index 5441aa23..3ce70418 100644
--- a/bindings/elixir/lib/fluss/server_node.ex
+++ b/bindings/elixir/lib/fluss/server_node.ex
@@ -29,7 +29,7 @@ defmodule Fluss.ServerNode do
           uid: String.t(),
           host: String.t(),
           port: non_neg_integer(),
-          server_type: :tablet_server | :coordinator_server
+          server_type: :tablet_server | :coordinator_server | :unknown
         }
 
   @spec url(t()) :: String.t()
diff --git a/bindings/elixir/native/fluss_nif/src/admin.rs 
b/bindings/elixir/native/fluss_nif/src/admin.rs
index 7a206a5d..6f79a97f 100644
--- a/bindings/elixir/native/fluss_nif/src/admin.rs
+++ b/bindings/elixir/native/fluss_nif/src/admin.rs
@@ -30,6 +30,7 @@ use std::sync::Arc;
 pub enum NifServerType {
     TabletServer,
     CoordinatorServer,
+    Unknown,
 }
 
 #[derive(NifStruct)]
@@ -52,6 +53,7 @@ impl NifServerNode {
             server_type: match node.server_type() {
                 ServerType::TabletServer => NifServerType::TabletServer,
                 ServerType::CoordinatorServer => 
NifServerType::CoordinatorServer,
+                ServerType::Unknown => NifServerType::Unknown,
             },
         }
     }
diff --git a/bindings/python/fluss/__init__.pyi 
b/bindings/python/fluss/__init__.pyi
index 7b808dc9..2ba50a42 100644
--- a/bindings/python/fluss/__init__.pyi
+++ b/bindings/python/fluss/__init__.pyi
@@ -293,7 +293,7 @@ class ServerNode:
         ...
     @property
     def server_type(self) -> str:
-        """The type of server ('CoordinatorServer' or 'TabletServer')."""
+        """The type of server ('CoordinatorServer', 'TabletServer', or 
'Unknown')."""
         ...
     @property
     def uid(self) -> str:
diff --git a/crates/fluss-test-cluster/src/lib.rs 
b/crates/fluss-test-cluster/src/lib.rs
index 144d725e..d4b4f124 100644
--- a/crates/fluss-test-cluster/src/lib.rs
+++ b/crates/fluss-test-cluster/src/lib.rs
@@ -240,6 +240,18 @@ impl FlussTestingClusterBuilder {
         }
     }
 
+    fn plaintext_tablet_bootstrap_servers(&self) -> HashMap<u16, String> {
+        let base_port = 
self.plain_client_port.unwrap_or(self.coordinator_host_port);
+        (0..self.number_of_tablet_servers)
+            .map(|server_id| {
+                (
+                    server_id,
+                    format!("127.0.0.1:{}", base_port + 1 + server_id),
+                )
+            })
+            .collect()
+    }
+
     async fn all_containers_exist(&self) -> bool {
         let Some(docker) = docker_client().await else {
             return false;
@@ -301,12 +313,14 @@ impl FlussTestingClusterBuilder {
         }
 
         let (bootstrap_servers, sasl_bootstrap_servers) = 
self.bootstrap_addresses();
+        let plaintext_tablet_bootstrap_servers = 
self.plaintext_tablet_bootstrap_servers();
 
         FlussTestingCluster {
             zookeeper,
             coordinator_server,
             tablet_servers,
             bootstrap_servers,
+            plaintext_tablet_bootstrap_servers,
             sasl_bootstrap_servers,
             remote_data_dir: self.remote_data_dir.clone(),
             sasl_users: self.sasl_users.clone(),
@@ -483,6 +497,7 @@ pub struct FlussTestingCluster {
     coordinator_server: Arc<ContainerAsync<GenericImage>>,
     tablet_servers: HashMap<u16, Arc<ContainerAsync<GenericImage>>>,
     bootstrap_servers: String,
+    plaintext_tablet_bootstrap_servers: HashMap<u16, String>,
     sasl_bootstrap_servers: Option<String>,
     remote_data_dir: Option<std::path::PathBuf>,
     sasl_users: Vec<(String, String)>,
@@ -512,6 +527,12 @@ impl FlussTestingCluster {
         &self.bootstrap_servers
     }
 
+    pub fn plaintext_tablet_bootstrap_server(&self, server_id: u16) -> 
Option<&str> {
+        self.plaintext_tablet_bootstrap_servers
+            .get(&server_id)
+            .map(String::as_str)
+    }
+
     pub async fn get_fluss_connection(&self) -> FlussConnection {
         let config = Config {
             writer_acks: "all".to_string(),
diff --git a/crates/fluss/src/client/metadata.rs 
b/crates/fluss/src/client/metadata.rs
index 1e3ee7fe..e27392f1 100644
--- a/crates/fluss/src/client/metadata.rs
+++ b/crates/fluss/src/client/metadata.rs
@@ -88,7 +88,7 @@ impl Metadata {
             -1,
             socket_address.ip().to_string(),
             socket_address.port() as u32,
-            ServerType::CoordinatorServer,
+            ServerType::Unknown,
         );
         let con = connections.get_connection(&server_node).await?;
 
diff --git a/crates/fluss/src/cluster/cluster.rs 
b/crates/fluss/src/cluster/cluster.rs
index d5518709..1b6a3685 100644
--- a/crates/fluss/src/cluster/cluster.rs
+++ b/crates/fluss/src/cluster/cluster.rs
@@ -483,6 +483,14 @@ mod tests {
         );
     }
 
+    #[test]
+    fn test_server_type_from_type_id() {
+        assert_eq!(ServerType::from_type_id(1), ServerType::CoordinatorServer);
+        assert_eq!(ServerType::from_type_id(2), ServerType::TabletServer);
+        assert_eq!(ServerType::from_type_id(-1), ServerType::Unknown);
+        assert_eq!(ServerType::from_type_id(99), ServerType::Unknown);
+    }
+
     #[test]
     fn test_get_server_nodes_with_coordinator_and_tablets() {
         let cluster = Cluster::new(
diff --git a/crates/fluss/src/cluster/mod.rs b/crates/fluss/src/cluster/mod.rs
index 863f8ed5..d300f7f9 100644
--- a/crates/fluss/src/cluster/mod.rs
+++ b/crates/fluss/src/cluster/mod.rs
@@ -36,12 +36,14 @@ pub struct ServerNode {
 
 impl ServerNode {
     pub fn new(id: i32, host: String, port: u32, server_type: ServerType) -> 
ServerNode {
+        let uid = match &server_type {
+            ServerType::CoordinatorServer => format!("cs-{id}"),
+            ServerType::TabletServer => format!("ts-{id}"),
+            ServerType::Unknown => format!("unknown-{host}:{port}"),
+        };
         ServerNode {
             id,
-            uid: match server_type {
-                ServerType::CoordinatorServer => format!("cs-{id}"),
-                ServerType::TabletServer => format!("ts-{id}"),
-            },
+            uid,
             host,
             port,
             server_type,
@@ -77,6 +79,7 @@ impl ServerNode {
 pub enum ServerType {
     TabletServer,
     CoordinatorServer,
+    Unknown,
 }
 
 impl ServerType {
@@ -84,14 +87,15 @@ impl ServerType {
         match self {
             ServerType::CoordinatorServer => 1,
             ServerType::TabletServer => 2,
+            ServerType::Unknown => -1,
         }
     }
 
-    pub fn from_type_id(type_id: i32) -> Option<ServerType> {
+    pub fn from_type_id(type_id: i32) -> ServerType {
         match type_id {
-            1 => Some(ServerType::CoordinatorServer),
-            2 => Some(ServerType::TabletServer),
-            _ => None,
+            1 => ServerType::CoordinatorServer,
+            2 => ServerType::TabletServer,
+            _ => ServerType::Unknown,
         }
     }
 }
@@ -101,6 +105,7 @@ impl fmt::Display for ServerType {
         match self {
             ServerType::TabletServer => write!(f, "TabletServer"),
             ServerType::CoordinatorServer => write!(f, "CoordinatorServer"),
+            ServerType::Unknown => write!(f, "Unknown"),
         }
     }
 }
diff --git a/crates/fluss/src/rpc/server_connection.rs 
b/crates/fluss/src/rpc/server_connection.rs
index 762c7ea9..a1ea4657 100644
--- a/crates/fluss/src/rpc/server_connection.rs
+++ b/crates/fluss/src/rpc/server_connection.rs
@@ -156,20 +156,21 @@ fn validate_server_type(
     expected: &ServerType,
     response_server_type: Option<i32>,
 ) -> Result<(), Error> {
+    if *expected == ServerType::Unknown {
+        return Ok(());
+    }
+
     // For forward-compat with servers that do not populate `server_type`, 
validation is skipped.
     let Some(type_id) = response_server_type else {
         return Ok(());
     };
     let actual = ServerType::from_type_id(type_id);
-    if actual.as_ref() == Some(expected) {
+    if &actual == expected {
         return Ok(());
     }
-    let actual_desc = actual
-        .map(|t| t.to_string())
-        .unwrap_or_else(|| format!("Unknown(type_id={type_id})"));
     Err(Error::InvalidServerType {
         message: format!(
-            "Expected server type {expected} but the server advertised 
{actual_desc}. \
+            "Expected server type {expected} but the server advertised 
{actual}. \
              The client may be talking to the wrong endpoint \
              (e.g. coordinator vs tablet server)."
         ),
@@ -1268,6 +1269,21 @@ mod tests {
             )
             .is_ok()
         );
+        assert!(
+            validate_server_type(
+                &ServerType::Unknown,
+                Some(ServerType::CoordinatorServer.to_type_id()),
+            )
+            .is_ok()
+        );
+        assert!(
+            validate_server_type(
+                &ServerType::Unknown,
+                Some(ServerType::TabletServer.to_type_id()),
+            )
+            .is_ok()
+        );
+        assert!(validate_server_type(&ServerType::Unknown, Some(99),).is_ok());
 
         // Mismatch: connected to a coordinator while expecting a tablet server
         // (and vice versa).
@@ -1290,8 +1306,8 @@ mod tests {
         ));
 
         validate_server_type(&ServerType::TabletServer, None).ok();
-        // Unknown / unmapped type id still fails, with the raw id surfaced so
-        // operators can diagnose protocol drift.
+        // Unknown / unmapped type id is treated as Unknown, which still fails
+        // when the client expects a concrete server type.
         assert!(matches!(
             validate_server_type(&ServerType::CoordinatorServer, Some(99),),
             Err(Error::InvalidServerType { .. })
diff --git a/crates/fluss/tests/integration/admin.rs 
b/crates/fluss/tests/integration/admin.rs
index 0860cbef..dfb00b0d 100644
--- a/crates/fluss/tests/integration/admin.rs
+++ b/crates/fluss/tests/integration/admin.rs
@@ -18,6 +18,8 @@
 #[cfg(test)]
 mod admin_test {
     use crate::integration::utils::get_shared_cluster;
+    use fluss::client::FlussConnection;
+    use fluss::config::Config;
     use fluss::error::FlussError;
     use fluss::metadata::{
         DataTypes, DatabaseDescriptorBuilder, KvFormat, LogFormat, 
PartitionSpec, Schema,
@@ -521,6 +523,39 @@ mod admin_test {
         }
     }
 
+    #[tokio::test]
+    async fn test_bootstrap_from_tablet_server() {
+        let cluster = get_shared_cluster();
+        let bootstrap_servers = cluster
+            .plaintext_tablet_bootstrap_server(0)
+            .expect("shared cluster should expose tablet server bootstrap")
+            .to_string();
+
+        let connection = FlussConnection::new(Config {
+            writer_acks: "all".to_string(),
+            bootstrap_servers,
+            ..Default::default()
+        })
+        .await
+        .expect("should bootstrap from tablet server");
+        let admin = connection.get_admin().expect("should get admin");
+
+        let nodes = admin
+            .get_server_nodes()
+            .await
+            .expect("should get server nodes");
+
+        let has_coordinator = nodes
+            .iter()
+            .any(|n| *n.server_type() == fluss::ServerType::CoordinatorServer);
+        assert!(has_coordinator, "Expected a coordinator server node");
+
+        let has_tablet = nodes
+            .iter()
+            .any(|n| *n.server_type() == fluss::ServerType::TabletServer);
+        assert!(has_tablet, "Expected a tablet server node");
+    }
+
     #[tokio::test]
     async fn test_error_table_not_partitioned() {
         let cluster = get_shared_cluster();
diff --git a/website/docs/user-guide/cpp/api-reference.md 
b/website/docs/user-guide/cpp/api-reference.md
index f2977114..ee287e19 100644
--- a/website/docs/user-guide/cpp/api-reference.md
+++ b/website/docs/user-guide/cpp/api-reference.md
@@ -105,9 +105,11 @@ Complete API reference for the Fluss C++ client.
 | `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"`)  |
+| `server_type` | `std::string` | Server type (`"CoordinatorServer"`, 
`"TabletServer"`, or `"Unknown"`)  |
 | `uid`         | `std::string` | Unique identifier (e.g. `"cs-0"`, `"ts-1"`)  
           |
 
+`"Unknown"` is used when the client has not yet determined the endpoint type, 
such as bootstrap endpoints.
+
 ## `Table`
 
 | Method                        | Description                              |
diff --git a/website/docs/user-guide/python/api-reference.md 
b/website/docs/user-guide/python/api-reference.md
index 341919c0..eeac5acc 100644
--- a/website/docs/user-guide/python/api-reference.md
+++ b/website/docs/user-guide/python/api-reference.md
@@ -73,9 +73,11 @@ Supports `async with` statement (async context manager).
 | `.id -> int`             | Server node ID                                    
         |
 | `.host -> str`           | Hostname of the server                            
         |
 | `.port -> int`           | Port number                                       
         |
-| `.server_type -> str`    | Server type (`"CoordinatorServer"` or 
`"TabletServer"`)    |
+| `.server_type -> str`    | Server type (`"CoordinatorServer"`, 
`"TabletServer"`, or `"Unknown"`)    |
 | `.uid -> str`            | Unique identifier (e.g. `"cs-0"`, `"ts-1"`)       
        |
 
+`"Unknown"` is used when the client has not yet determined the endpoint type, 
such as bootstrap endpoints.
+
 ## `FlussTable`
 
 | Method                          |  Description                            |
diff --git a/website/docs/user-guide/rust/api-reference.md 
b/website/docs/user-guide/rust/api-reference.md
index 7ef39577..49c54823 100644
--- a/website/docs/user-guide/rust/api-reference.md
+++ b/website/docs/user-guide/rust/api-reference.md
@@ -99,9 +99,12 @@ Complete API reference for the Fluss Rust client.
 | `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 server_type(&self) -> &ServerType` | Server type (`CoordinatorServer`, 
`TabletServer`, or `Unknown`) |
 | `fn uid(&self) -> &str`          | Unique identifier (e.g. `"cs-0"`, 
`"ts-1"`)         |
 
+`ServerType::Unknown` is used when the client has not yet determined the 
endpoint
+type, such as bootstrap endpoints.
+
 ## `FlussTable<'a>`
 
 | Method                                        | Description                  
           |

Reply via email to