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

JingsongLi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-rust.git


The following commit(s) were added to refs/heads/main by this push:
     new e62cb8db [io] Propagate User-Agent options to Jindo OSS (#1116)
e62cb8db is described below

commit e62cb8db9ca72724468f03eaf7bc9e7c329b56ea
Author: XiaoHongbo <[email protected]>
AuthorDate: Tue Oct 13 17:43:42 2026 +0800

    [io] Propagate User-Agent options to Jindo OSS (#1116)
---
 crates/paimon/src/io/storage_jindo.rs | 169 +++++++++++++++++++++++++++++++++-
 1 file changed, 165 insertions(+), 4 deletions(-)

diff --git a/crates/paimon/src/io/storage_jindo.rs 
b/crates/paimon/src/io/storage_jindo.rs
index bfda2a3d..2e080aad 100644
--- a/crates/paimon/src/io/storage_jindo.rs
+++ b/crates/paimon/src/io/storage_jindo.rs
@@ -30,6 +30,10 @@ use opendal::{Error as OpendalError, OperationContext, 
Operator, Result as Opend
 use tokio::sync::{OnceCell, Semaphore};
 
 use crate::error::{Error, JINDO_FORK_ERROR};
+use crate::io::user_agent::{
+    DLF_ACCESS_TRACKING_EXTENDED_INFO, OSS_USER_AGENT_EXTENDED, 
OSS_USER_AGENT_FEATURES,
+    OSS_USER_AGENT_MODULE, USER_AGENT_EXTENDED, USER_AGENT_FEATURES, 
USER_AGENT_MODULE,
+};
 use crate::Result;
 
 const OSS_IMPL: &str = "fs.oss.impl";
@@ -159,10 +163,7 @@ pub(crate) fn jindo_config_parse(mut props: 
HashMap<String, String>) -> Result<J
         None => DEFAULT_JINDO_MAX_CONCURRENT_READS,
     };
     props.remove(OSS_IMPL);
-    props.insert(
-        "fs.oss.user.agent.features".to_string(),
-        "paimon-rust".to_string(),
-    );
+    configure_jindo_user_agent(&mut props);
     props
         .entry("logger.appender".to_string())
         .or_insert_with(|| "file".to_string());
@@ -184,6 +185,50 @@ pub(crate) fn jindo_config_parse(mut props: 
HashMap<String, String>) -> Result<J
     })
 }
 
+fn configure_jindo_user_agent(props: &mut HashMap<String, String>) {
+    let effective = |oss_key: &str, common_key: &str| {
+        props
+            .get(oss_key)
+            .filter(|value| !value.trim().is_empty())
+            .or_else(|| {
+                props
+                    .get(common_key)
+                    .filter(|value| !value.trim().is_empty())
+            })
+            .map(|value| value.trim().to_string())
+    };
+    let module = effective(OSS_USER_AGENT_MODULE, USER_AGENT_MODULE);
+    let features = effective(OSS_USER_AGENT_FEATURES, USER_AGENT_FEATURES);
+    let extended = effective(OSS_USER_AGENT_EXTENDED, USER_AGENT_EXTENDED);
+    let access_tracking = props
+        .get(DLF_ACCESS_TRACKING_EXTENDED_INFO)
+        .map(|value| value.trim().to_string())
+        .filter(|value| !value.is_empty());
+
+    if let Some(module) = module {
+        props.insert(OSS_USER_AGENT_MODULE.to_string(), module);
+    }
+    let mut features: Vec<&str> = features
+        .as_deref()
+        .map(str::split_whitespace)
+        .into_iter()
+        .flatten()
+        .collect();
+    if !features.contains(&"paimon-rust") {
+        features.insert(0, "paimon-rust");
+    }
+    props.insert(OSS_USER_AGENT_FEATURES.to_string(), features.join(" "));
+
+    let extended = [extended, access_tracking]
+        .into_iter()
+        .flatten()
+        .collect::<Vec<_>>()
+        .join(" ");
+    if !extended.is_empty() {
+        props.insert(OSS_USER_AGENT_EXTENDED.to_string(), extended);
+    }
+}
+
 pub(crate) fn jindo_config_build(config: &JindoStorageConfig, bucket: &str) -> 
Result<Operator> {
     let builder = JindoBuilder {
         config: Some(config.clone()),
@@ -1257,6 +1302,91 @@ mod tests {
         );
         assert!(!config.properties.contains_key(OSS_IMPL));
         assert!(!config.properties.contains_key(JINDO_MAX_CONCURRENT_READS));
+        assert_eq!(
+            config
+                .properties
+                .get(OSS_USER_AGENT_FEATURES)
+                .map(String::as_str),
+            Some("paimon-rust")
+        );
+    }
+
+    #[test]
+    fn test_jindo_user_agent_from_catalog_options() {
+        let config = jindo_config_parse(HashMap::from([
+            (
+                USER_AGENT_MODULE.to_string(),
+                "training-app/1.0".to_string(),
+            ),
+            (
+                USER_AGENT_FEATURES.to_string(),
+                "sdk-v2 training".to_string(),
+            ),
+            (USER_AGENT_EXTENDED.to_string(), "env=prod".to_string()),
+            (
+                DLF_ACCESS_TRACKING_EXTENDED_INFO.to_string(),
+                "[email protected]".to_string(),
+            ),
+        ]))
+        .unwrap();
+        assert_eq!(
+            config
+                .properties
+                .get(OSS_USER_AGENT_MODULE)
+                .map(String::as_str),
+            Some("training-app/1.0")
+        );
+        assert_eq!(
+            config
+                .properties
+                .get(OSS_USER_AGENT_FEATURES)
+                .map(String::as_str),
+            Some("paimon-rust sdk-v2 training")
+        );
+        assert_eq!(
+            config
+                .properties
+                .get(OSS_USER_AGENT_EXTENDED)
+                .map(String::as_str),
+            Some("env=prod [email protected]")
+        );
+    }
+
+    #[test]
+    fn test_jindo_oss_user_agent_options_take_precedence() {
+        let config = jindo_config_parse(HashMap::from([
+            (USER_AGENT_MODULE.to_string(), "common/1.0".to_string()),
+            (OSS_USER_AGENT_MODULE.to_string(), "oss/1.0".to_string()),
+            (USER_AGENT_FEATURES.to_string(), "common".to_string()),
+            (
+                OSS_USER_AGENT_FEATURES.to_string(),
+                "oss paimon-rust".to_string(),
+            ),
+            (USER_AGENT_EXTENDED.to_string(), "common-info".to_string()),
+            (OSS_USER_AGENT_EXTENDED.to_string(), "oss-info".to_string()),
+        ]))
+        .unwrap();
+        assert_eq!(
+            config
+                .properties
+                .get(OSS_USER_AGENT_MODULE)
+                .map(String::as_str),
+            Some("oss/1.0")
+        );
+        assert_eq!(
+            config
+                .properties
+                .get(OSS_USER_AGENT_FEATURES)
+                .map(String::as_str),
+            Some("oss paimon-rust")
+        );
+        assert_eq!(
+            config
+                .properties
+                .get(OSS_USER_AGENT_EXTENDED)
+                .map(String::as_str),
+            Some("oss-info")
+        );
     }
 
     #[test]
@@ -1510,6 +1640,7 @@ mod tests {
         list_page_requests: Arc<AtomicUsize>,
         range_requests: Arc<AtomicUsize>,
         unavailable_requests: Arc<AtomicUsize>,
+        user_agents: Arc<Mutex<Vec<String>>>,
     }
 
     #[cfg(target_os = "linux")]
@@ -1520,6 +1651,13 @@ mod tests {
         headers: HeaderMap,
     ) -> Response<Body> {
         const DATA: &[u8] = b"0123456789";
+        state.user_agents.lock().unwrap().push(
+            headers
+                .get("user-agent")
+                .and_then(|value| value.to_str().ok())
+                .unwrap_or_default()
+                .to_string(),
+        );
         if method == Method::HEAD {
             if uri.path().ends_with("/collision") {
                 return Response::builder()
@@ -1725,6 +1863,15 @@ mod tests {
                 "true".to_string(),
             ),
             ("fs.oss.retry.count".to_string(), "0".to_string()),
+            (
+                USER_AGENT_MODULE.to_string(),
+                "training-app/1.0".to_string(),
+            ),
+            (
+                USER_AGENT_FEATURES.to_string(),
+                "sdk-v2 training".to_string(),
+            ),
+            (USER_AGENT_EXTENDED.to_string(), "env=test".to_string()),
         ]);
         let config = jindo_config_parse(properties.clone()).unwrap();
         let operator = jindo_config_build(&config, 
"jindo-test-bucket").unwrap();
@@ -1787,6 +1934,20 @@ mod tests {
         assert_eq!(state.list_page_requests.load(Ordering::SeqCst), 3);
         assert_eq!(state.range_requests.load(Ordering::SeqCst), 1);
         assert_eq!(state.unavailable_requests.load(Ordering::SeqCst), 1);
+        {
+            let user_agents = state.user_agents.lock().unwrap();
+            assert!(!user_agents.is_empty());
+            assert!(
+                user_agents.iter().all(|value| {
+                    value.contains("training-app/1.0")
+                        && value.contains("paimon-rust")
+                        && value.contains("sdk-v2")
+                        && value.contains("training")
+                        && value.contains("env=test")
+                }),
+                "unexpected Jindo User-Agent: {user_agents:?}"
+            );
+        }
 
         let fork_operator = jindo_config_build(&config, 
"jindo-test-bucket").unwrap();
         assert_operator_rejects_reuse_after_fork(fork_operator).await;

Reply via email to