Skip to main content

goosefs_sdk/
config.rs

1//! Client configuration for Goosefs gRPC connections.
2
3use serde::{Deserialize, Serialize};
4use std::fmt;
5use std::str::FromStr;
6use std::sync::atomic::{AtomicBool, Ordering};
7use std::sync::Mutex;
8use std::time::{Duration, Instant};
9
10use crate::auth::AuthType;
11use crate::proto::grpc::file::WritePType;
12
13// ── Config load error ─────────────────────────────────────────
14
15/// Error returned by properties/auto configuration loading.
16#[derive(Debug)]
17pub enum ConfigLoadError {
18    /// The config file could not be read.
19    IoError { path: String, source: String },
20    /// The YAML content could not be parsed.
21    ParseError { message: String },
22}
23
24impl std::fmt::Display for ConfigLoadError {
25    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26        match self {
27            ConfigLoadError::IoError { path, source } => {
28                write!(f, "failed to read config file '{}': {}", path, source)
29            }
30            ConfigLoadError::ParseError { message } => {
31                write!(f, "failed to parse YAML config: {}", message)
32            }
33        }
34    }
35}
36
37impl std::error::Error for ConfigLoadError {}
38
39// ── Properties file parsing ───────────────────────────────────
40//
41// Parse Java-style `goosefs-site.properties` files.
42// Format: `key=value` lines, `#` comments, blank lines ignored.
43
44use std::collections::HashMap;
45
46/// Parsed properties map from a `goosefs-site.properties` file.
47#[derive(Debug, Default)]
48struct PropertiesMap {
49    props: HashMap<String, String>,
50}
51
52impl PropertiesMap {
53    /// Parse a properties string into a map.
54    ///
55    /// Rules (matching Java `Properties.load()`):
56    /// - Lines starting with `#` or `!` are comments.
57    /// - Blank lines are ignored.
58    /// - Key and value are separated by `=` or `:` (first occurrence).
59    /// - Leading/trailing whitespace on key and value is trimmed.
60    fn parse(content: &str) -> Self {
61        let mut props = HashMap::new();
62        for line in content.lines() {
63            let trimmed = line.trim();
64            if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with('!') {
65                continue;
66            }
67            // Find the separator: first `=` or `:`
68            let sep_pos = trimmed.find('=').or_else(|| trimmed.find(':'));
69            if let Some(pos) = sep_pos {
70                let key = trimmed[..pos].trim().to_string();
71                let value = trimmed[pos + 1..].trim().to_string();
72                if !key.is_empty() {
73                    props.insert(key, value);
74                }
75            }
76        }
77        PropertiesMap { props }
78    }
79
80    /// Get a string value by key.
81    fn get(&self, key: &str) -> Option<&str> {
82        self.props.get(key).map(|s| s.as_str())
83    }
84
85    /// Get a value parsed as the given type.
86    fn get_parsed<T: FromStr>(&self, key: &str) -> Option<T> {
87        self.get(key).and_then(|v| v.parse::<T>().ok())
88    }
89
90    /// Get a boolean value (accepts `true`/`false`, case-insensitive).
91    fn get_bool(&self, key: &str) -> Option<bool> {
92        self.get(key)
93            .and_then(|v| v.to_ascii_lowercase().parse::<bool>().ok())
94    }
95
96    /// Get a comma-separated list of strings.
97    fn get_list(&self, key: &str) -> Option<Vec<String>> {
98        self.get(key).map(|v| {
99            v.split(',')
100                .map(str::trim)
101                .filter(|s| !s.is_empty())
102                .map(String::from)
103                .collect()
104        })
105    }
106}
107
108/// Parse a byte size string like `"64MB"`, `"512KB"`, `"1GB"`, `"4MB"` or plain bytes.
109///
110/// This matches the format used in `goosefs-site.properties`, e.g.
111/// `goosefs.user.block.size.bytes.default=4MB`.
112fn parse_byte_size(s: &str) -> Result<u64, String> {
113    let s = s.trim();
114    let upper = s.to_uppercase();
115    let (multiplier, num_str) = if upper.ends_with("GB") {
116        (1024u64 * 1024 * 1024, &s[..s.len() - 2])
117    } else if upper.ends_with("MB") {
118        (1024 * 1024, &s[..s.len() - 2])
119    } else if upper.ends_with("KB") {
120        (1024, &s[..s.len() - 2])
121    } else {
122        (1, s)
123    };
124    num_str
125        .trim()
126        .parse::<u64>()
127        .map(|n| n * multiplier)
128        .map_err(|e| format!("invalid byte size '{}': {}", s, e))
129}
130
131impl PropertiesMap {
132    /// Convert the parsed properties into a `GoosefsConfig`.
133    fn into_goosefs_config(self) -> GoosefsConfig {
134        let mut cfg = GoosefsConfig::default();
135
136        // Master addresses: goosefs.master.rpc.addresses (comma-separated)
137        if let Some(addrs) = self.get_list("goosefs.master.rpc.addresses") {
138            if !addrs.is_empty() {
139                cfg.master_addr = addrs[0].clone();
140                if addrs.len() > 1 {
141                    cfg.master_addrs = addrs;
142                }
143            }
144        } else if let Some(host) = self.get("goosefs.master.hostname") {
145            let port: u16 = self.get_parsed("goosefs.master.rpc.port").unwrap_or(9200);
146            cfg.master_addr = format!("{}:{}", host, port);
147        }
148
149        // Config manager addresses: goosefs.config.manager.rpc.addresses
150        if let Some(addrs) = self.get_list("goosefs.config.manager.rpc.addresses") {
151            if !addrs.is_empty() {
152                cfg.config_manager_rpc_addresses = addrs;
153            }
154        }
155
156        // Config RPC port: goosefs.config.rpc.port
157        if let Some(port) = self.get_parsed::<u16>("goosefs.config.rpc.port") {
158            cfg.config_rpc_port = port;
159        }
160
161        // Security / auth type: goosefs.security.authentication.type
162        if let Some(at_str) = self.get("goosefs.security.authentication.type") {
163            if let Ok(at) = at_str.parse::<AuthType>() {
164                cfg.auth_type = at;
165            }
166        }
167
168        // Security / authorization permission enabled
169        if let Some(enabled) = self.get_bool("goosefs.security.authorization.permission.enabled") {
170            cfg.authorization_permission_enabled = enabled;
171        }
172
173        // Security / login impersonation username
174        if let Some(user) = self.get("goosefs.security.login.impersonation.username") {
175            if !user.is_empty() {
176                cfg.login_impersonation_username = user.to_string();
177            }
178        }
179
180        // Security / login username
181        if let Some(user) = self.get("goosefs.security.login.username") {
182            if !user.is_empty() {
183                cfg.auth_username = user.to_string();
184            }
185        }
186
187        // Transparent acceleration: goosefs.user.client.transparent_acceleration.enabled
188        if let Some(enabled) = self.get_bool("goosefs.user.client.transparent_acceleration.enabled")
189        {
190            cfg.transparent_acceleration_enabled = enabled;
191        }
192
193        // Transparent acceleration cosranger
194        if let Some(enabled) =
195            self.get_bool("goosefs.user.client.transparent_acceleration.cosranger.enabled")
196        {
197            cfg.transparent_acceleration_cosranger_enabled = enabled;
198        }
199
200        // Write type: goosefs.user.file.writetype.default
201        if let Some(wt_str) = self.get("goosefs.user.file.writetype.default") {
202            if let Ok(wt) = wt_str.parse::<WriteType>() {
203                cfg.write_type = Some(wt.as_i32());
204            }
205        }
206
207        // Block size: goosefs.user.block.size.bytes.default
208        if let Some(bs_str) = self.get("goosefs.user.block.size.bytes.default") {
209            if let Ok(bs) = parse_byte_size(bs_str) {
210                if bs > 0 {
211                    cfg.block_size = bs;
212                }
213            }
214        }
215
216        // Chunk size: goosefs.user.network.data.transfer.chunk.size
217        if let Some(cs_str) = self.get("goosefs.user.network.data.transfer.chunk.size") {
218            if let Ok(cs) = parse_byte_size(cs_str) {
219                if cs > 0 {
220                    cfg.chunk_size = cs;
221                }
222            }
223        }
224
225        cfg
226    }
227}
228
229/// Name of the properties config file.
230const PROPERTIES_FILENAME: &str = "goosefs-site.properties";
231
232/// Discover a config file from the standard search paths.
233///
234/// The search order mirrors the Java `SITE_CONF_DIR` property:
235///   `${goosefs.conf.dir}/, ${user.home}/.goosefs/, /etc/goosefs/`
236///
237/// Search order:
238/// 1. `$GOOSEFS_CONFIG_FILE` env var — explicit file path (Rust-only convenience)
239/// 2. `$GOOSEFS_CONF_DIR/goosefs-site.properties` — mirrors Java `goosefs.conf.dir`
240/// 3. `$GOOSEFS_HOME/conf/goosefs-site.properties` — fallback when `GOOSEFS_CONF_DIR` is unset
241/// 4. `~/.goosefs/goosefs-site.properties`          — user home
242/// 5. `/etc/goosefs/goosefs-site.properties`        — system-wide
243pub fn discover_config_file() -> Option<std::path::PathBuf> {
244    use std::path::PathBuf;
245
246    // 1. Explicit env var pointing to a file (highest priority, Rust-only convenience)
247    if let Ok(p) = std::env::var(ENV_CONFIG_FILE) {
248        let pb = PathBuf::from(&p);
249        if pb.exists() {
250            return Some(pb);
251        }
252    }
253
254    // 2. $GOOSEFS_CONF_DIR/goosefs-site.properties  (≈ Java `goosefs.conf.dir`)
255    if let Ok(conf_dir) = std::env::var(CONF_DIR) {
256        let p = PathBuf::from(&conf_dir).join(PROPERTIES_FILENAME);
257        if p.exists() {
258            return Some(p);
259        }
260    }
261
262    // 3. $GOOSEFS_HOME/conf/goosefs-site.properties  (fallback for CONF_DIR)
263    if let Ok(home) = std::env::var(ENV_HOME) {
264        let p = PathBuf::from(&home).join("conf").join(PROPERTIES_FILENAME);
265        if p.exists() {
266            return Some(p);
267        }
268    }
269
270    // 4. ~/.goosefs/goosefs-site.properties (user home)
271    if let Some(home) = dirs_next_home() {
272        let p = home.join(".goosefs").join(PROPERTIES_FILENAME);
273        if p.exists() {
274            return Some(p);
275        }
276    }
277
278    // 5. /etc/goosefs/goosefs-site.properties (system-wide)
279    let system = PathBuf::from("/etc/goosefs").join(PROPERTIES_FILENAME);
280    if system.exists() {
281        return Some(system);
282    }
283
284    None
285}
286
287/// Return the user's home directory without depending on the `dirs` crate.
288fn dirs_next_home() -> Option<std::path::PathBuf> {
289    std::env::var("HOME")
290        .or_else(|_| std::env::var("USERPROFILE"))
291        .ok()
292        .map(std::path::PathBuf::from)
293}
294
295// ── Default constants ────────────────────────────────────────
296
297/// Default Goosefs Master RPC port.
298const DEFAULT_MASTER_PORT: u16 = 9200;
299/// Default Goosefs Worker data port.
300#[allow(dead_code)]
301const DEFAULT_WORKER_PORT: u16 = 9203;
302/// Default block size: 64 MiB (matches Goosefs default).
303const DEFAULT_BLOCK_SIZE: u64 = 64 * 1024 * 1024;
304/// Default chunk size for streaming reads: 1 MiB.
305const DEFAULT_CHUNK_SIZE: u64 = 1024 * 1024;
306/// Default connect timeout: 30 seconds.
307const DEFAULT_CONNECT_TIMEOUT_MS: u64 = 30_000;
308/// Default request timeout: 5 minutes.
309const DEFAULT_REQUEST_TIMEOUT_MS: u64 = 300_000;
310/// Default master polling timeout: 30 seconds (mirrors Java `USER_MASTER_POLLING_TIMEOUT`).
311const DEFAULT_MASTER_POLLING_TIMEOUT_MS: u64 = 30_000;
312
313/// Default authentication timeout: 30 seconds.
314const DEFAULT_AUTH_TIMEOUT_MS: u64 = 30_000;
315
316/// Default config manager RPC port.
317const DEFAULT_CONFIG_RPC_PORT: u16 = 9214;
318
319/// Default impersonation username (mirrors Java `Constants.IMPERSONATION_HDFS_USER`).
320const DEFAULT_IMPERSONATION_USERNAME: &str = "_HDFS_USER_";
321/// Impersonation disabled sentinel (mirrors Java `Constants.IMPERSONATION_NONE`).
322#[allow(dead_code)]
323pub const IMPERSONATION_NONE: &str = "_NONE_";
324
325/// Default max duration for master inquire retry: 2 minutes.
326const DEFAULT_MASTER_INQUIRE_MAX_DURATION_MS: u64 = 120_000;
327/// Default initial sleep for master inquire retry: 50 ms.
328const DEFAULT_MASTER_INQUIRE_INITIAL_SLEEP_MS: u64 = 50;
329/// Default max sleep for master inquire retry: 3 seconds.
330const DEFAULT_MASTER_INQUIRE_MAX_SLEEP_MS: u64 = 3_000;
331
332/// Default config expiry time: 30 seconds (mirrors Java `ConfigurationUtils.expireTime`).
333const DEFAULT_CONFIG_EXPIRE_MS: u64 = 30_000;
334
335// ── Storage option key constants ─────────────────────────────
336//
337// These are the canonical key names used in `storage_options` maps
338// (e.g. Lance's `DatasetBuilder::with_storage_option` or OpenDAL config).
339// Using these constants avoids hard-coded "magic strings" scattered across
340// the codebase and test code.
341
342/// Storage option key for Goosefs master address(es).
343///
344/// Supports HA: `"addr1:port,addr2:port"`.
345///
346/// Corresponding environment variable: `GOOSEFS_MASTER_ADDR`.
347pub const STORAGE_OPT_MASTER_ADDR: &str = "goosefs_master_addr";
348
349/// Storage option key for the default write type.
350///
351/// Accepted values: `"must_cache"`, `"try_cache"`, `"cache_through"`,
352/// `"through"`, `"async_through"` (case-insensitive).
353///
354/// Corresponding environment variable: `GOOSEFS_WRITE_TYPE`.
355pub const STORAGE_OPT_WRITE_TYPE: &str = "goosefs_write_type";
356
357/// Storage option key for block size (in bytes).
358///
359/// Corresponding environment variable: `GOOSEFS_BLOCK_SIZE`.
360pub const STORAGE_OPT_BLOCK_SIZE: &str = "goosefs_block_size";
361
362/// Storage option key for chunk size (in bytes).
363///
364/// Corresponding environment variable: `GOOSEFS_CHUNK_SIZE`.
365pub const STORAGE_OPT_CHUNK_SIZE: &str = "goosefs_chunk_size";
366
367/// Storage option key for authentication type.
368///
369/// Accepted values: `"nosasl"`, `"simple"` (case-insensitive).
370///
371/// Corresponding environment variable: `GOOSEFS_AUTH_TYPE`.
372pub const STORAGE_OPT_AUTH_TYPE: &str = "goosefs_auth_type";
373
374/// Storage option key for authentication username.
375///
376/// Corresponding environment variable: `GOOSEFS_AUTH_USERNAME`.
377pub const STORAGE_OPT_AUTH_USERNAME: &str = "goosefs_auth_username";
378
379/// Goosefs configuration directory property name.
380///
381/// Mirrors Java's `public static final String CONF_DIR = "goosefs.conf.dir"`.
382/// In the Rust client, the corresponding environment variable is [`ENV_CONF_DIR`].
383pub const CONF_DIR: &str = "goosefs.conf.dir";
384
385/// Environment variable: explicit config file path (Rust-only convenience).
386pub const ENV_CONFIG_FILE: &str = "GOOSEFS_CONFIG_FILE";
387
388/// Environment variable: Goosefs configuration directory.
389///
390/// Corresponds to the Java property [`CONF_DIR`] (`goosefs.conf.dir`).
391pub const ENV_CONF_DIR: &str = "GOOSEFS_CONF_DIR";
392
393/// Environment variable: Goosefs installation home directory.
394pub const ENV_HOME: &str = "GOOSEFS_HOME";
395
396/// Environment variable: Goosefs master address(es).
397pub const ENV_MASTER_ADDR: &str = "GOOSEFS_MASTER_ADDR";
398
399/// Environment variable: default write type.
400pub const ENV_WRITE_TYPE: &str = "GOOSEFS_WRITE_TYPE";
401
402/// Environment variable: block size.
403pub const ENV_BLOCK_SIZE: &str = "GOOSEFS_BLOCK_SIZE";
404
405/// Environment variable: chunk size.
406pub const ENV_CHUNK_SIZE: &str = "GOOSEFS_CHUNK_SIZE";
407
408/// Environment variable: authentication type.
409pub const ENV_AUTH_TYPE: &str = "GOOSEFS_AUTH_TYPE";
410
411/// Environment variable: authentication username.
412pub const ENV_AUTH_USERNAME: &str = "GOOSEFS_AUTH_USERNAME";
413
414/// Environment variable: config manager RPC addresses.
415pub const ENV_CONFIG_MANAGER_RPC_ADDRESSES: &str = "GOOSEFS_CONFIG_MANAGER_RPC_ADDRESSES";
416
417/// Environment variable: config RPC port.
418pub const ENV_CONFIG_RPC_PORT: &str = "GOOSEFS_CONFIG_RPC_PORT";
419
420/// Environment variable: transparent acceleration enabled.
421pub const ENV_TRANSPARENT_ACCELERATION_ENABLED: &str = "GOOSEFS_TRANSPARENT_ACCELERATION_ENABLED";
422
423/// Environment variable: transparent acceleration cosranger enabled.
424pub const ENV_TRANSPARENT_ACCELERATION_COSRANGER_ENABLED: &str =
425    "GOOSEFS_TRANSPARENT_ACCELERATION_COSRANGER_ENABLED";
426
427/// Environment variable: authorization permission enabled.
428pub const ENV_AUTHORIZATION_PERMISSION_ENABLED: &str = "GOOSEFS_AUTHORIZATION_PERMISSION_ENABLED";
429
430/// Environment variable: login impersonation username.
431pub const ENV_LOGIN_IMPERSONATION_USERNAME: &str = "GOOSEFS_LOGIN_IMPERSONATION_USERNAME";
432
433/// Storage option key for config manager RPC addresses.
434pub const STORAGE_OPT_CONFIG_MANAGER_RPC_ADDRESSES: &str = "goosefs_config_manager_rpc_addresses";
435
436/// Storage option key for config RPC port.
437pub const STORAGE_OPT_CONFIG_RPC_PORT: &str = "goosefs_config_rpc_port";
438
439/// Storage option key for transparent acceleration enabled.
440pub const STORAGE_OPT_TRANSPARENT_ACCELERATION_ENABLED: &str =
441    "goosefs_transparent_acceleration_enabled";
442
443/// Storage option key for transparent acceleration cosranger enabled.
444pub const STORAGE_OPT_TRANSPARENT_ACCELERATION_COSRANGER_ENABLED: &str =
445    "goosefs_transparent_acceleration_cosranger_enabled";
446
447/// Storage option key for authorization permission enabled.
448pub const STORAGE_OPT_AUTHORIZATION_PERMISSION_ENABLED: &str =
449    "goosefs_authorization_permission_enabled";
450
451/// Storage option key for login impersonation username.
452pub const STORAGE_OPT_LOGIN_IMPERSONATION_USERNAME: &str = "goosefs_login_impersonation_username";
453
454// ── WriteType: ergonomic Rust enum wrapping WritePType ───────
455
456/// High-level write type for Goosefs file creation.
457///
458/// This enum provides:
459/// - **String ↔ enum conversion** (`FromStr` / `Display`) — like Java `Enum.valueOf()`.
460/// - **`WritePType` interop** — zero-cost conversion to/from the protobuf enum.
461///
462/// # String representation (case-insensitive)
463///
464/// | Variant       | Strings                              |
465/// |---------------|--------------------------------------|
466/// | `MustCache`   | `must_cache`, `MUST_CACHE`            |
467/// | `TryCache`    | `try_cache`, `TRY_CACHE`              |
468/// | `CacheThrough`| `cache_through`, `CACHE_THROUGH`      |
469/// | `Through`     | `through`, `THROUGH`                  |
470/// | `AsyncThrough`| `async_through`, `ASYNC_THROUGH`      |
471///
472/// # Examples
473/// ```
474/// use goosefs_sdk::config::WriteType;
475///
476/// // Parse from string (case-insensitive)
477/// let wt: WriteType = "cache_through".parse().unwrap();
478/// assert_eq!(wt, WriteType::CacheThrough);
479///
480/// // Display as canonical lowercase string
481/// assert_eq!(wt.to_string(), "cache_through");
482/// assert_eq!(wt.as_str(), "cache_through");
483///
484/// // Convert to protobuf WritePType
485/// use goosefs_sdk::WritePType;
486/// assert_eq!(WritePType::from(wt), WritePType::CacheThrough);
487///
488/// // Convert from protobuf WritePType
489/// assert_eq!(WriteType::from(WritePType::Through), WriteType::Through);
490/// ```
491#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
492pub enum WriteType {
493    /// Write to Goosefs cache only; no UFS persistence.
494    MustCache,
495    /// Try to cache; fall back to `Through` if cache is full.
496    TryCache,
497    /// Write to cache **and** synchronously persist to UFS.
498    CacheThrough,
499    /// Write directly to UFS, bypassing cache.
500    Through,
501    /// Write to cache, asynchronously persist to UFS later.
502    AsyncThrough,
503}
504
505impl WriteType {
506    /// All supported write type variants (useful for iteration / help text).
507    pub const ALL: &'static [WriteType] = &[
508        WriteType::MustCache,
509        WriteType::TryCache,
510        WriteType::CacheThrough,
511        WriteType::Through,
512        WriteType::AsyncThrough,
513    ];
514
515    /// Return the canonical lowercase string representation.
516    ///
517    /// This is the string accepted by OpenDAL / Lance `storage_options`.
518    pub fn as_str(&self) -> &'static str {
519        match self {
520            WriteType::MustCache => "must_cache",
521            WriteType::TryCache => "try_cache",
522            WriteType::CacheThrough => "cache_through",
523            WriteType::Through => "through",
524            WriteType::AsyncThrough => "async_through",
525        }
526    }
527
528    /// Return the protobuf `i32` value (same as `WritePType as i32`).
529    pub fn as_i32(&self) -> i32 {
530        WritePType::from(*self) as i32
531    }
532}
533
534impl fmt::Display for WriteType {
535    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
536        f.write_str(self.as_str())
537    }
538}
539
540/// Parse a `WriteType` from a string (case-insensitive).
541///
542/// Accepts both `snake_case` and `UPPER_SNAKE_CASE` forms.
543impl FromStr for WriteType {
544    type Err = String;
545
546    fn from_str(s: &str) -> Result<Self, Self::Err> {
547        match s.to_ascii_lowercase().as_str() {
548            "must_cache" => Ok(WriteType::MustCache),
549            "try_cache" => Ok(WriteType::TryCache),
550            "cache_through" => Ok(WriteType::CacheThrough),
551            "through" => Ok(WriteType::Through),
552            "async_through" => Ok(WriteType::AsyncThrough),
553            _ => Err(format!(
554                "unknown write type '{}'. Expected one of: {}",
555                s,
556                WriteType::ALL
557                    .iter()
558                    .map(|wt| wt.as_str())
559                    .collect::<Vec<_>>()
560                    .join(", ")
561            )),
562        }
563    }
564}
565
566/// Convert `WriteType` → protobuf `WritePType`.
567impl From<WriteType> for WritePType {
568    fn from(wt: WriteType) -> Self {
569        match wt {
570            WriteType::MustCache => WritePType::MustCache,
571            WriteType::TryCache => WritePType::TryCache,
572            WriteType::CacheThrough => WritePType::CacheThrough,
573            WriteType::Through => WritePType::Through,
574            WriteType::AsyncThrough => WritePType::AsyncThrough,
575        }
576    }
577}
578
579/// Convert protobuf `WritePType` → `WriteType`.
580///
581/// Returns `Err` for `UnspecifiedWriteType` and `None` (proto).
582impl WriteType {
583    pub fn try_from_proto(pt: WritePType) -> Result<Self, String> {
584        match pt {
585            WritePType::MustCache => Ok(WriteType::MustCache),
586            WritePType::TryCache => Ok(WriteType::TryCache),
587            WritePType::CacheThrough => Ok(WriteType::CacheThrough),
588            WritePType::Through => Ok(WriteType::Through),
589            WritePType::AsyncThrough => Ok(WriteType::AsyncThrough),
590            other => Err(format!(
591                "cannot convert WritePType::{:?} to WriteType",
592                other
593            )),
594        }
595    }
596}
597
598/// Convenience: `WritePType` → `WriteType` (panics on Unspecified/None).
599impl From<WritePType> for WriteType {
600    fn from(pt: WritePType) -> Self {
601        Self::try_from_proto(pt).expect("cannot convert Unspecified/None WritePType to WriteType")
602    }
603}
604
605/// Configuration for the Goosefs Rust gRPC client.
606#[derive(Debug, Clone, Serialize, Deserialize)]
607pub struct GoosefsConfig {
608    /// Primary master address in `host:port` format (backward-compatible).
609    ///
610    /// When only a single master is used, set this field.
611    /// For HA deployments, use [`master_addrs`](Self::master_addrs) instead (or both — `master_addr`
612    /// is automatically included if `master_addrs` is also provided).
613    pub master_addr: String,
614
615    /// Multiple master addresses for HA deployments.
616    ///
617    /// When this list contains more than one address, the client will
618    /// automatically use [`PollingMasterInquireClient`](crate::client::master_inquire::PollingMasterInquireClient)
619    /// to discover the
620    /// Primary Master via the `getServiceVersion` RPC.
621    ///
622    /// If empty, `master_addr` is used as the sole address.
623    #[serde(default)]
624    pub master_addrs: Vec<String>,
625
626    /// Default block size in bytes for new files.
627    pub block_size: u64,
628
629    /// Chunk size for streaming read/write RPCs.
630    pub chunk_size: u64,
631
632    /// Connect timeout for gRPC channels.
633    pub connect_timeout: Duration,
634
635    /// Request timeout for individual RPCs.
636    pub request_timeout: Duration,
637
638    /// Whether to use VPC mapping addresses from WorkerNetAddress.
639    pub use_vpc_mapping: bool,
640
641    /// Root path prefix for all operations (e.g. `/goosefs-data`).
642    pub root: String,
643
644    /// Default write type for newly created files.
645    ///
646    /// Controls how data is persisted when writing files.
647    /// Use the `WritePType` enum values (as `i32`):
648    /// - `1` (`MustCache`) — Write to Goosefs cache only, no UFS persistence.
649    /// - `2` (`TryCache`) — Try to cache; fall back to THROUGH if cache is full.
650    /// - `3` (`CacheThrough`) — Write to cache AND synchronously persist to UFS.
651    /// - `4` (`Through`) — Write directly to UFS, bypass cache.
652    /// - `5` (`AsyncThrough`) — Write to cache, asynchronously persist to UFS.
653    ///
654    /// If not set (`None`), the server-side default is used (typically `MustCache`).
655    /// Use [`GoosefsConfig::with_write_type`] for a type-safe builder.
656    pub write_type: Option<i32>,
657
658    // ── Master Inquire / HA retry configuration ──────────────
659    /// Maximum total duration for master inquire retries (default: 2 min).
660    #[serde(default = "default_master_inquire_max_duration")]
661    pub master_inquire_retry_max_duration: Duration,
662
663    /// Initial sleep time between master inquire polling rounds (default: 50 ms).
664    #[serde(default = "default_master_inquire_initial_sleep")]
665    pub master_inquire_initial_sleep: Duration,
666
667    /// Maximum sleep time between master inquire polling rounds (default: 3 s).
668    #[serde(default = "default_master_inquire_max_sleep")]
669    pub master_inquire_max_sleep: Duration,
670
671    /// Timeout for a single master polling ping RPC (default: 30 s).
672    ///
673    /// This is independent of [`connect_timeout`](Self::connect_timeout) — it controls only the
674    /// `getServiceVersion` probe used to discover the Primary Master.
675    /// Mirrors Java's `goosefs.user.master.polling.timeout`.
676    #[serde(default = "default_master_polling_timeout")]
677    pub master_polling_timeout: Duration,
678
679    // ── Authentication configuration ─────────────────────────
680    /// Authentication type (default: `Simple`).
681    ///
682    /// Controls how the client authenticates with Goosefs Master/Worker.
683    /// Mirrors Java's `goosefs.security.authentication.type`.
684    ///
685    /// Currently supported:
686    /// - `NoSasl` — no authentication
687    /// - `Simple` — PLAIN SASL with username (default)
688    ///
689    /// TODO: `Custom`, `Kerberos`, `DelegationToken`, `CapabilityToken`
690    #[serde(default)]
691    pub auth_type: AuthType,
692
693    /// Username for authentication (default: current OS user).
694    ///
695    /// Used in SIMPLE mode as the login identity.
696    /// Mirrors Java's `goosefs.security.login.username`.
697    #[serde(default = "default_auth_username")]
698    pub auth_username: String,
699
700    /// Authentication timeout (default: 30 s).
701    ///
702    /// Maximum time to wait for SASL handshake completion.
703    /// Mirrors Java's `goosefs.network.connection.auth.timeout`.
704    #[serde(default = "default_auth_timeout")]
705    pub auth_timeout: Duration,
706
707    // ── Config Manager configuration ─────────────────────────
708    /// Config manager RPC addresses.
709    ///
710    /// Mirrors Java's `goosefs.config.manager.rpc.addresses`.
711    /// When set, the client can fetch dynamic configuration from the config manager.
712    #[serde(default)]
713    pub config_manager_rpc_addresses: Vec<String>,
714
715    /// Config manager RPC port (default: 9214).
716    ///
717    /// Mirrors Java's `goosefs.config.rpc.port`.
718    #[serde(default = "default_config_rpc_port")]
719    pub config_rpc_port: u16,
720
721    // ── Transparent acceleration configuration ───────────────
722    /// Whether transparent acceleration is enabled (default: true).
723    ///
724    /// Mirrors Java's `goosefs.user.client.transparent_acceleration.enabled`.
725    #[serde(default = "default_transparent_acceleration_enabled")]
726    pub transparent_acceleration_enabled: bool,
727
728    /// Whether transparent acceleration cosranger is enabled (default: false).
729    ///
730    /// Mirrors Java's `goosefs.user.client.transparent_acceleration.cosranger.enabled`.
731    #[serde(default)]
732    pub transparent_acceleration_cosranger_enabled: bool,
733
734    // ── Authorization configuration ──────────────────────────
735    /// Whether access control based on file permission is enabled (default: false).
736    ///
737    /// Mirrors Java's `goosefs.security.authorization.permission.enabled`.
738    #[serde(default)]
739    pub authorization_permission_enabled: bool,
740
741    /// Impersonation username for SIMPLE/CUSTOM authentication.
742    ///
743    /// When set to `"_HDFS_USER_"` (default), the client impersonates the
744    /// Hadoop client user. Set to `"_NONE_"` to disable impersonation.
745    ///
746    /// Mirrors Java's `goosefs.security.login.impersonation.username`.
747    #[serde(default = "default_login_impersonation_username")]
748    pub login_impersonation_username: String,
749}
750
751fn default_master_inquire_max_duration() -> Duration {
752    Duration::from_millis(DEFAULT_MASTER_INQUIRE_MAX_DURATION_MS)
753}
754fn default_master_inquire_initial_sleep() -> Duration {
755    Duration::from_millis(DEFAULT_MASTER_INQUIRE_INITIAL_SLEEP_MS)
756}
757fn default_master_inquire_max_sleep() -> Duration {
758    Duration::from_millis(DEFAULT_MASTER_INQUIRE_MAX_SLEEP_MS)
759}
760fn default_master_polling_timeout() -> Duration {
761    Duration::from_millis(DEFAULT_MASTER_POLLING_TIMEOUT_MS)
762}
763fn default_auth_username() -> String {
764    std::env::var("USER")
765        .or_else(|_| std::env::var("USERNAME"))
766        .unwrap_or_else(|_| "unknown".to_string())
767}
768fn default_auth_timeout() -> Duration {
769    Duration::from_millis(DEFAULT_AUTH_TIMEOUT_MS)
770}
771fn default_config_rpc_port() -> u16 {
772    DEFAULT_CONFIG_RPC_PORT
773}
774fn default_transparent_acceleration_enabled() -> bool {
775    true
776}
777fn default_login_impersonation_username() -> String {
778    DEFAULT_IMPERSONATION_USERNAME.to_string()
779}
780
781impl Default for GoosefsConfig {
782    fn default() -> Self {
783        Self {
784            master_addr: format!("127.0.0.1:{}", DEFAULT_MASTER_PORT),
785            master_addrs: Vec::new(),
786            block_size: DEFAULT_BLOCK_SIZE,
787            chunk_size: DEFAULT_CHUNK_SIZE,
788            connect_timeout: Duration::from_millis(DEFAULT_CONNECT_TIMEOUT_MS),
789            request_timeout: Duration::from_millis(DEFAULT_REQUEST_TIMEOUT_MS),
790            use_vpc_mapping: false,
791            root: String::new(),
792            write_type: None,
793            master_inquire_retry_max_duration: default_master_inquire_max_duration(),
794            master_inquire_initial_sleep: default_master_inquire_initial_sleep(),
795            master_inquire_max_sleep: default_master_inquire_max_sleep(),
796            master_polling_timeout: default_master_polling_timeout(),
797            auth_type: AuthType::default(),
798            auth_username: default_auth_username(),
799            auth_timeout: default_auth_timeout(),
800            config_manager_rpc_addresses: Vec::new(),
801            config_rpc_port: default_config_rpc_port(),
802            transparent_acceleration_enabled: default_transparent_acceleration_enabled(),
803            transparent_acceleration_cosranger_enabled: false,
804            authorization_permission_enabled: false,
805            login_impersonation_username: default_login_impersonation_username(),
806        }
807    }
808}
809
810impl GoosefsConfig {
811    /// Create a new config with the given single master address.
812    pub fn new(master_addr: impl Into<String>) -> Self {
813        Self {
814            master_addr: master_addr.into(),
815            ..Default::default()
816        }
817    }
818
819    /// Create a new config for HA (High Availability) with multiple master addresses.
820    ///
821    /// The first address in the list is also set as `master_addr` for
822    /// backward compatibility.
823    ///
824    /// # Panics
825    /// Panics if `addrs` is empty.
826    pub fn new_ha(addrs: Vec<String>) -> Self {
827        assert!(!addrs.is_empty(), "master addresses must not be empty");
828        Self {
829            master_addr: addrs[0].clone(),
830            master_addrs: addrs,
831            ..Default::default()
832        }
833    }
834
835    /// Create a config from one or more master addresses.
836    ///
837    /// Automatically selects the right mode:
838    /// - 1 address  → single-master (same as [`new`](Self::new)).
839    /// - 2+ addresses → multi-master (same as [`new_ha`](Self::new_ha)).
840    ///
841    /// # Panics
842    /// Panics if `addrs` is empty.
843    pub fn from_addresses(addrs: Vec<String>) -> Self {
844        assert!(!addrs.is_empty(), "master addresses must not be empty");
845        if addrs.len() == 1 {
846            Self::new(&addrs[0])
847        } else {
848            Self::new_ha(addrs)
849        }
850    }
851
852    /// Return the effective list of master addresses.
853    ///
854    /// If [`master_addrs`](Self::master_addrs) is non-empty, returns it directly.
855    /// Otherwise, returns a single-element list containing [`master_addr`](Self::master_addr).
856    pub fn master_addresses(&self) -> Vec<String> {
857        if self.master_addrs.is_empty() {
858            vec![self.master_addr.clone()]
859        } else {
860            self.master_addrs.clone()
861        }
862    }
863
864    /// Returns `true` if the client is configured with multiple masters.
865    pub fn is_multi_master(&self) -> bool {
866        self.master_addrs.len() > 1
867    }
868
869    /// Resolve the full path by prepending the root.
870    pub fn full_path(&self, path: &str) -> String {
871        if self.root.is_empty() {
872            path.to_string()
873        } else {
874            let root = self.root.trim_end_matches('/');
875            let path = path.trim_start_matches('/');
876            format!("{}/{}", root, path)
877        }
878    }
879
880    /// Build the gRPC endpoint URI for the master.
881    pub fn master_endpoint(&self) -> String {
882        format!("http://{}", self.master_addr)
883    }
884
885    /// Build the gRPC endpoint URI for a worker.
886    pub fn worker_endpoint(&self, host: &str, rpc_port: i32) -> String {
887        if self.use_vpc_mapping {
888            // VPC mapping is handled at the caller level
889            format!("http://{}:{}", host, rpc_port)
890        } else {
891            format!("http://{}:{}", host, rpc_port)
892        }
893    }
894
895    /// Set the authentication type.
896    ///
897    /// # Example
898    /// ```
899    /// use goosefs_sdk::config::GoosefsConfig;
900    /// use goosefs_sdk::auth::AuthType;
901    ///
902    /// let config = GoosefsConfig::new("127.0.0.1:9200")
903    ///     .with_auth_type(AuthType::NoSasl);
904    /// ```
905    pub fn with_auth_type(mut self, auth_type: AuthType) -> Self {
906        self.auth_type = auth_type;
907        self
908    }
909
910    /// Set the authentication type from a string (case-insensitive).
911    ///
912    /// Accepted values: `"nosasl"`, `"simple"`.
913    pub fn with_auth_type_str(self, auth_type: &str) -> Result<Self, String> {
914        let at: AuthType = auth_type.parse()?;
915        Ok(self.with_auth_type(at))
916    }
917
918    /// Set the authentication username.
919    pub fn with_auth_username(mut self, username: impl Into<String>) -> Self {
920        self.auth_username = username.into();
921        self
922    }
923
924    /// Set the default write type using the protobuf `WritePType` enum.
925    ///
926    /// # Example
927    /// ```
928    /// use goosefs_sdk::config::GoosefsConfig;
929    /// use goosefs_sdk::WritePType;
930    ///
931    /// let config = GoosefsConfig::new("127.0.0.1:9200")
932    ///     .with_write_type(WritePType::CacheThrough);
933    /// ```
934    pub fn with_write_type(mut self, wt: WritePType) -> Self {
935        self.write_type = Some(wt as i32);
936        self
937    }
938
939    /// Set the default write type using the high-level [`WriteType`] enum.
940    ///
941    /// # Example
942    /// ```
943    /// use goosefs_sdk::config::{GoosefsConfig, WriteType};
944    ///
945    /// let config = GoosefsConfig::new("127.0.0.1:9200")
946    ///     .with_write_type_enum(WriteType::CacheThrough);
947    /// ```
948    pub fn with_write_type_enum(mut self, wt: WriteType) -> Self {
949        self.write_type = Some(wt.as_i32());
950        self
951    }
952
953    /// Set the default write type from a string (case-insensitive).
954    ///
955    /// Accepted values: `"must_cache"`, `"cache_through"`, `"through"`,
956    /// `"try_cache"`, `"async_through"`.
957    ///
958    /// # Example
959    /// ```
960    /// use goosefs_sdk::config::GoosefsConfig;
961    ///
962    /// let config = GoosefsConfig::new("127.0.0.1:9200")
963    ///     .with_write_type_str("cache_through")
964    ///     .unwrap();
965    /// ```
966    pub fn with_write_type_str(self, wt: &str) -> Result<Self, String> {
967        let write_type: WriteType = wt.parse()?;
968        Ok(self.with_write_type_enum(write_type))
969    }
970
971    /// Get the configured `WritePType`, if set.
972    ///
973    /// Returns `None` if `write_type` is unset or contains an unrecognised value.
974    pub fn get_write_type(&self) -> Option<WritePType> {
975        self.write_type.and_then(|v| match v {
976            0 => Some(WritePType::UnspecifiedWriteType),
977            1 => Some(WritePType::MustCache),
978            2 => Some(WritePType::TryCache),
979            3 => Some(WritePType::CacheThrough),
980            4 => Some(WritePType::Through),
981            5 => Some(WritePType::AsyncThrough),
982            6 => Some(WritePType::None),
983            _ => Option::None,
984        })
985    }
986
987    // ── YAML / env configuration loading ───────────────────────────────────
988
989    /// Load configuration from environment variables.
990    ///
991    /// Reads the following variables (all optional):
992    ///
993    /// | Variable              | Field           |
994    /// |-----------------------|-----------------|
995    /// | `GOOSEFS_MASTER_ADDR` | `master_addr` / `master_addrs` |
996    /// | `GOOSEFS_WRITE_TYPE`  | `write_type`    |
997    /// | `GOOSEFS_BLOCK_SIZE`  | `block_size`    |
998    /// | `GOOSEFS_CHUNK_SIZE`  | `chunk_size`    |
999    /// | `GOOSEFS_AUTH_TYPE`   | `auth_type`     |
1000    /// | `GOOSEFS_AUTH_USERNAME` | `auth_username` |
1001    ///
1002    /// Returns a config reflecting any variables that are set, falling back to
1003    /// defaults for unset variables.
1004    ///
1005    /// # Priority
1006    ///
1007    /// This is intended to be called as part of the auto-load chain:
1008    /// `from_properties_auto()` then `apply_env()`.  Call `apply_env()` on an
1009    /// existing config to overlay env-var values on top of properties values.
1010    pub fn from_env() -> Self {
1011        Self::default().apply_env()
1012    }
1013
1014    /// Apply environment variables on top of the current config (in-place).
1015    ///
1016    /// Variables that are set override the corresponding field; unset
1017    /// variables leave the field unchanged.
1018    pub fn apply_env(mut self) -> Self {
1019        use std::env;
1020
1021        // Master address(es)
1022        if let Ok(addr) = env::var(ENV_MASTER_ADDR) {
1023            let addrs: Vec<String> = addr
1024                .split(',')
1025                .map(str::trim)
1026                .filter(|s| !s.is_empty())
1027                .map(String::from)
1028                .collect();
1029            if !addrs.is_empty() {
1030                self.master_addr = addrs[0].clone();
1031                if addrs.len() > 1 {
1032                    self.master_addrs = addrs;
1033                } else {
1034                    self.master_addrs = Vec::new();
1035                }
1036            }
1037        }
1038
1039        // Write type
1040        if let Ok(wt_str) = env::var(ENV_WRITE_TYPE) {
1041            if let Ok(wt) = wt_str.parse::<WriteType>() {
1042                self.write_type = Some(wt.as_i32());
1043            }
1044        }
1045
1046        // Block size
1047        if let Ok(bs_str) = env::var(ENV_BLOCK_SIZE) {
1048            if let Ok(bs) = bs_str.parse::<u64>() {
1049                self.block_size = bs;
1050            }
1051        }
1052
1053        // Chunk size
1054        if let Ok(cs_str) = env::var(ENV_CHUNK_SIZE) {
1055            if let Ok(cs) = cs_str.parse::<u64>() {
1056                self.chunk_size = cs;
1057            }
1058        }
1059
1060        // Auth type
1061        if let Ok(at_str) = env::var(ENV_AUTH_TYPE) {
1062            if let Ok(at) = at_str.parse::<crate::auth::AuthType>() {
1063                self.auth_type = at;
1064            }
1065        }
1066
1067        // Auth username
1068        if let Ok(user) = env::var(ENV_AUTH_USERNAME) {
1069            if !user.is_empty() {
1070                self.auth_username = user;
1071            }
1072        }
1073
1074        // Config manager RPC addresses
1075        if let Ok(addrs_str) = env::var(ENV_CONFIG_MANAGER_RPC_ADDRESSES) {
1076            let addrs: Vec<String> = addrs_str
1077                .split(',')
1078                .map(str::trim)
1079                .filter(|s| !s.is_empty())
1080                .map(String::from)
1081                .collect();
1082            if !addrs.is_empty() {
1083                self.config_manager_rpc_addresses = addrs;
1084            }
1085        }
1086
1087        // Config RPC port
1088        if let Ok(port_str) = env::var(ENV_CONFIG_RPC_PORT) {
1089            if let Ok(port) = port_str.parse::<u16>() {
1090                self.config_rpc_port = port;
1091            }
1092        }
1093
1094        // Transparent acceleration enabled
1095        if let Ok(val) = env::var(ENV_TRANSPARENT_ACCELERATION_ENABLED) {
1096            if let Ok(b) = val.parse::<bool>() {
1097                self.transparent_acceleration_enabled = b;
1098            }
1099        }
1100
1101        // Transparent acceleration cosranger enabled
1102        if let Ok(val) = env::var(ENV_TRANSPARENT_ACCELERATION_COSRANGER_ENABLED) {
1103            if let Ok(b) = val.parse::<bool>() {
1104                self.transparent_acceleration_cosranger_enabled = b;
1105            }
1106        }
1107
1108        // Authorization permission enabled
1109        if let Ok(val) = env::var(ENV_AUTHORIZATION_PERMISSION_ENABLED) {
1110            if let Ok(b) = val.parse::<bool>() {
1111                self.authorization_permission_enabled = b;
1112            }
1113        }
1114
1115        // Login impersonation username
1116        if let Ok(user) = env::var(ENV_LOGIN_IMPERSONATION_USERNAME) {
1117            if !user.is_empty() {
1118                self.login_impersonation_username = user;
1119            }
1120        }
1121
1122        self
1123    }
1124
1125    /// Load configuration from a Java-style properties file.
1126    ///
1127    /// The file format is `goosefs-site.properties` with `key=value` lines:
1128    ///
1129    /// ```text
1130    /// goosefs.master.hostname=10.0.0.1
1131    /// goosefs.master.rpc.port=9200
1132    /// goosefs.security.authentication.type=SIMPLE
1133    /// goosefs.user.file.writetype.default=CACHE_THROUGH
1134    /// goosefs.user.block.size.bytes.default=4MB
1135    /// ```
1136    ///
1137    /// Returns an error if the file cannot be read.
1138    pub fn from_properties(path: impl AsRef<std::path::Path>) -> Result<Self, ConfigLoadError> {
1139        let path = path.as_ref();
1140        let content = std::fs::read_to_string(path).map_err(|e| ConfigLoadError::IoError {
1141            path: path.display().to_string(),
1142            source: e.to_string(),
1143        })?;
1144        Ok(Self::from_properties_str(&content))
1145    }
1146
1147    /// Parse configuration from a properties-format string.
1148    ///
1149    /// Useful for testing or embedding config in code.
1150    pub fn from_properties_str(content: &str) -> Self {
1151        let props = PropertiesMap::parse(content);
1152        props.into_goosefs_config()
1153    }
1154
1155    /// Auto-discover and load configuration with the full priority chain.
1156    ///
1157    /// # Priority (highest to lowest)
1158    ///
1159    /// 1. Environment variables (`GOOSEFS_*`)
1160    /// 2. Properties config file (see search paths below)
1161    /// 3. Built-in defaults
1162    ///
1163    /// # Config file search paths
1164    ///
1165    /// Mirrors the Java `SITE_CONF_DIR` property:
1166    ///   `${goosefs.conf.dir}/, ${user.home}/.goosefs/, /etc/goosefs/`
1167    ///
1168    /// 1. `$GOOSEFS_CONFIG_FILE` environment variable (if set and file exists)
1169    /// 2. `$GOOSEFS_CONF_DIR/goosefs-site.properties` (mirrors Java `goosefs.conf.dir`)
1170    /// 3. `$GOOSEFS_HOME/conf/goosefs-site.properties` (fallback when `GOOSEFS_CONF_DIR` unset)
1171    /// 4. `~/.goosefs/goosefs-site.properties` (user home directory)
1172    /// 5. `/etc/goosefs/goosefs-site.properties` (system-wide)
1173    ///
1174    /// If no config file is found, falls back to defaults.
1175    /// Then env vars are overlaid on top.
1176    ///
1177    /// # Errors
1178    ///
1179    /// Returns an error only if a config file is found but cannot be read.
1180    /// If no file is found, returns `Ok` with defaults + env vars applied.
1181    pub fn from_properties_auto() -> Result<Self, ConfigLoadError> {
1182        let base = if let Some(path) = discover_config_file() {
1183            Self::from_properties(&path)?
1184        } else {
1185            Self::default()
1186        };
1187
1188        // Overlay env vars (highest priority)
1189        Ok(base.apply_env())
1190    }
1191
1192    /// Validate configuration. Returns an error message if invalid.
1193    pub fn validate(&self) -> Result<(), String> {
1194        if self.master_addr.is_empty() && self.master_addrs.is_empty() {
1195            return Err(
1196                "at least one master address must be provided (master_addr or master_addrs)"
1197                    .to_string(),
1198            );
1199        }
1200        if !self.master_addrs.is_empty() && self.master_addrs.iter().any(|a| a.is_empty()) {
1201            return Err("master_addrs contains an empty address".to_string());
1202        }
1203        if self.block_size == 0 {
1204            return Err("block_size must be > 0".to_string());
1205        }
1206        if self.chunk_size == 0 {
1207            return Err("chunk_size must be > 0".to_string());
1208        }
1209        if self.chunk_size > self.block_size {
1210            return Err("chunk_size must be <= block_size".to_string());
1211        }
1212        Ok(())
1213    }
1214}
1215
1216// ── ConfigRefresher: periodic config reload ──────────────────
1217//
1218// Mirrors Java's `ConfigurationUtils.loadIfExpire()` +
1219// `AbstractCompatibleFileSystem.refreshTransparentAccelerationSwitch()`.
1220//
1221// The refresher caches the last-loaded config and only re-reads the
1222// properties file from disk when the expiry time has elapsed.
1223
1224/// Result of a config refresh — the two switches that may change at runtime.
1225#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1226pub struct TransparentAccelerationSwitch {
1227    /// Whether transparent acceleration is enabled.
1228    pub enabled: bool,
1229    /// Whether transparent acceleration cosranger is enabled.
1230    pub cosranger_enabled: bool,
1231}
1232
1233/// Thread-safe config refresher that periodically reloads `goosefs-site.properties`.
1234///
1235/// Mirrors the Java pattern:
1236/// ```text
1237/// ConfigurationUtils.loadIfExpire();          // reload if stale
1238/// GoosefsProperties props = ConfigurationUtils.defaults();
1239/// InstancedConfiguration cfg = new InstancedConfiguration(props);
1240/// boolean enable = cfg.getBoolean(TRANSPARENT_ACCELERATION_ENABLED);
1241/// boolean cosRangerEnable = cfg.getBoolean(COSRANGER_ENABLED);
1242/// ```
1243///
1244/// # Usage
1245///
1246/// ```rust,no_run
1247/// use goosefs_sdk::config::ConfigRefresher;
1248///
1249/// let refresher = ConfigRefresher::new();
1250/// // In a background loop:
1251/// let switch = refresher.refresh_transparent_acceleration_switch();
1252/// println!("acceleration={}, cosranger={}", switch.enabled, switch.cosranger_enabled);
1253/// ```
1254pub struct ConfigRefresher {
1255    /// Last time the config was loaded from disk.
1256    last_load_time: Mutex<Option<Instant>>,
1257    /// Config expiry duration (default: 30s, mirrors Java `expireTime`).
1258    expire_duration: Duration,
1259    /// Cached transparent acceleration enabled flag (AtomicBool for lock-free reads).
1260    transparent_acceleration_enabled: AtomicBool,
1261    /// Cached cosranger enabled flag.
1262    cosranger_enabled: AtomicBool,
1263}
1264
1265impl fmt::Debug for ConfigRefresher {
1266    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1267        f.debug_struct("ConfigRefresher")
1268            .field("expire_duration", &self.expire_duration)
1269            .field(
1270                "transparent_acceleration_enabled",
1271                &self
1272                    .transparent_acceleration_enabled
1273                    .load(Ordering::Relaxed),
1274            )
1275            .field(
1276                "cosranger_enabled",
1277                &self.cosranger_enabled.load(Ordering::Relaxed),
1278            )
1279            .finish()
1280    }
1281}
1282
1283impl ConfigRefresher {
1284    /// Create a new refresher with the default expiry time (30s).
1285    ///
1286    /// The initial switch values come from the current config (loaded once
1287    /// via `from_properties_auto`).
1288    pub fn new() -> Self {
1289        Self::with_expire(Duration::from_millis(DEFAULT_CONFIG_EXPIRE_MS))
1290    }
1291
1292    /// Create a new refresher with a custom expiry duration.
1293    pub fn with_expire(expire_duration: Duration) -> Self {
1294        // Load the initial config to seed the switch values.
1295        let initial = GoosefsConfig::from_properties_auto().unwrap_or_default();
1296        Self {
1297            last_load_time: Mutex::new(Some(Instant::now())),
1298            expire_duration,
1299            transparent_acceleration_enabled: AtomicBool::new(
1300                initial.transparent_acceleration_enabled,
1301            ),
1302            cosranger_enabled: AtomicBool::new(initial.transparent_acceleration_cosranger_enabled),
1303        }
1304    }
1305
1306    /// Create a refresher seeded from an existing config.
1307    ///
1308    /// Useful when the caller already has a `GoosefsConfig` (e.g. from
1309    /// `FileSystemContext::connect`).
1310    pub fn from_config(config: &GoosefsConfig) -> Self {
1311        Self {
1312            last_load_time: Mutex::new(Some(Instant::now())),
1313            expire_duration: Duration::from_millis(DEFAULT_CONFIG_EXPIRE_MS),
1314            transparent_acceleration_enabled: AtomicBool::new(
1315                config.transparent_acceleration_enabled,
1316            ),
1317            cosranger_enabled: AtomicBool::new(config.transparent_acceleration_cosranger_enabled),
1318        }
1319    }
1320
1321    /// Reload config from disk if the expiry time has elapsed, then return
1322    /// the current transparent acceleration switch values.
1323    ///
1324    /// This mirrors Java's:
1325    /// ```java
1326    /// boolean refreshTransparentAccelerationSwitch() {
1327    ///     ConfigurationUtils.loadIfExpire();
1328    ///     GoosefsProperties props = ConfigurationUtils.defaults();
1329    ///     InstancedConfiguration cfg = new InstancedConfiguration(props);
1330    ///     cfg.validate();
1331    ///     boolean enable = cfg.getBoolean(TRANSPARENT_ACCELERATION_ENABLED);
1332    ///     boolean cosRangerEnable = cfg.getBoolean(COSRANGER_ENABLED);
1333    ///     transparentAccelerationEnabled.set(enable);
1334    ///     cosRangerEnabled.set(cosRangerEnable);
1335    ///     return transparentAccelerationEnabled.get();
1336    /// }
1337    /// ```
1338    pub fn refresh_transparent_acceleration_switch(&self) -> TransparentAccelerationSwitch {
1339        self.load_if_expire();
1340        TransparentAccelerationSwitch {
1341            enabled: self
1342                .transparent_acceleration_enabled
1343                .load(Ordering::Relaxed),
1344            cosranger_enabled: self.cosranger_enabled.load(Ordering::Relaxed),
1345        }
1346    }
1347
1348    /// Return the current switch values **without** triggering a reload.
1349    ///
1350    /// This is a lock-free read of the cached atomic flags.
1351    pub fn current_switch(&self) -> TransparentAccelerationSwitch {
1352        TransparentAccelerationSwitch {
1353            enabled: self
1354                .transparent_acceleration_enabled
1355                .load(Ordering::Relaxed),
1356            cosranger_enabled: self.cosranger_enabled.load(Ordering::Relaxed),
1357        }
1358    }
1359
1360    /// Reload the config from disk if the cached config has expired.
1361    ///
1362    /// Mirrors Java's `ConfigurationUtils.loadIfExpire()` — uses a mutex to
1363    /// prevent multiple threads from reloading simultaneously, and double-checks
1364    /// the expiry inside the lock.
1365    fn load_if_expire(&self) {
1366        let now = Instant::now();
1367        let needs_reload = {
1368            let guard = self.last_load_time.lock().unwrap();
1369            match *guard {
1370                None => true,
1371                Some(t) => now.duration_since(t) >= self.expire_duration,
1372            }
1373        };
1374
1375        if needs_reload {
1376            // Acquire the lock and double-check (mirrors Java's synchronized + double-check).
1377            let mut guard = self.last_load_time.lock().unwrap();
1378            let still_needs = match *guard {
1379                None => true,
1380                Some(t) => now.duration_since(t) >= self.expire_duration,
1381            };
1382            if still_needs {
1383                self.reload_properties();
1384                *guard = Some(Instant::now());
1385            }
1386        }
1387    }
1388
1389    /// Re-read the properties file and update the atomic switch flags.
1390    fn reload_properties(&self) {
1391        match GoosefsConfig::from_properties_auto() {
1392            Ok(cfg) => {
1393                self.transparent_acceleration_enabled
1394                    .store(cfg.transparent_acceleration_enabled, Ordering::Relaxed);
1395                self.cosranger_enabled.store(
1396                    cfg.transparent_acceleration_cosranger_enabled,
1397                    Ordering::Relaxed,
1398                );
1399                tracing::debug!(
1400                    transparent_acceleration_enabled = cfg.transparent_acceleration_enabled,
1401                    cosranger_enabled = cfg.transparent_acceleration_cosranger_enabled,
1402                    "config refreshed from properties file"
1403                );
1404            }
1405            Err(e) => {
1406                tracing::warn!("failed to reload config: {}, keeping previous values", e);
1407            }
1408        }
1409    }
1410}
1411
1412impl Default for ConfigRefresher {
1413    fn default() -> Self {
1414        Self::new()
1415    }
1416}
1417
1418#[cfg(test)]
1419mod tests {
1420    use super::*;
1421
1422    #[test]
1423    fn test_default_config() {
1424        let config = GoosefsConfig::default();
1425        assert_eq!(config.master_addr, "127.0.0.1:9200");
1426        assert!(config.master_addrs.is_empty());
1427        assert_eq!(config.block_size, 64 * 1024 * 1024);
1428        assert_eq!(config.chunk_size, 1024 * 1024);
1429        assert!(!config.is_multi_master());
1430        assert!(config.validate().is_ok());
1431    }
1432
1433    #[test]
1434    fn test_new_ha_config() {
1435        let config = GoosefsConfig::new_ha(vec![
1436            "10.0.0.1:9200".to_string(),
1437            "10.0.0.2:9200".to_string(),
1438            "10.0.0.3:9200".to_string(),
1439        ]);
1440        assert_eq!(config.master_addr, "10.0.0.1:9200");
1441        assert_eq!(config.master_addrs.len(), 3);
1442        assert!(config.is_multi_master());
1443        assert!(config.validate().is_ok());
1444    }
1445
1446    #[test]
1447    fn test_master_addresses_single() {
1448        let config = GoosefsConfig::new("10.0.0.1:9200");
1449        let addrs = config.master_addresses();
1450        assert_eq!(addrs, vec!["10.0.0.1:9200"]);
1451        assert!(!config.is_multi_master());
1452    }
1453
1454    #[test]
1455    fn test_master_addresses_multi() {
1456        let config = GoosefsConfig::new_ha(vec![
1457            "10.0.0.1:9200".to_string(),
1458            "10.0.0.2:9200".to_string(),
1459        ]);
1460        let addrs = config.master_addresses();
1461        assert_eq!(addrs.len(), 2);
1462        assert!(config.is_multi_master());
1463    }
1464
1465    #[test]
1466    #[should_panic(expected = "master addresses must not be empty")]
1467    fn test_new_ha_empty_panics() {
1468        GoosefsConfig::new_ha(vec![]);
1469    }
1470
1471    #[test]
1472    fn test_full_path_with_root() {
1473        let config = GoosefsConfig {
1474            root: "/data".to_string(),
1475            ..Default::default()
1476        };
1477        assert_eq!(config.full_path("/file.txt"), "/data/file.txt");
1478        assert_eq!(config.full_path("file.txt"), "/data/file.txt");
1479    }
1480
1481    #[test]
1482    fn test_full_path_without_root() {
1483        let config = GoosefsConfig::default();
1484        assert_eq!(config.full_path("/file.txt"), "/file.txt");
1485    }
1486
1487    #[test]
1488    fn test_validate_empty_master() {
1489        let config = GoosefsConfig {
1490            master_addr: String::new(),
1491            master_addrs: Vec::new(),
1492            ..Default::default()
1493        };
1494        assert!(config.validate().is_err());
1495    }
1496
1497    #[test]
1498    fn test_validate_empty_addr_in_list() {
1499        let config = GoosefsConfig {
1500            master_addr: "10.0.0.1:9200".to_string(),
1501            master_addrs: vec!["10.0.0.1:9200".to_string(), "".to_string()],
1502            ..Default::default()
1503        };
1504        assert!(config.validate().is_err());
1505    }
1506
1507    #[test]
1508    fn test_validate_chunk_larger_than_block() {
1509        let config = GoosefsConfig {
1510            chunk_size: 128 * 1024 * 1024,
1511            block_size: 64 * 1024 * 1024,
1512            ..Default::default()
1513        };
1514        assert!(config.validate().is_err());
1515    }
1516
1517    #[test]
1518    fn test_write_type_default_is_none() {
1519        let config = GoosefsConfig::default();
1520        assert!(config.write_type.is_none());
1521        assert!(config.get_write_type().is_none());
1522    }
1523
1524    #[test]
1525    fn test_with_write_type_builder() {
1526        let config = GoosefsConfig::new("127.0.0.1:9200").with_write_type(WritePType::CacheThrough);
1527        assert_eq!(config.write_type, Some(3));
1528        assert_eq!(config.get_write_type(), Some(WritePType::CacheThrough));
1529    }
1530
1531    #[test]
1532    fn test_write_p_type_all_variants_config() {
1533        let cases = vec![
1534            (WritePType::MustCache, 1),
1535            (WritePType::TryCache, 2),
1536            (WritePType::CacheThrough, 3),
1537            (WritePType::Through, 4),
1538            (WritePType::AsyncThrough, 5),
1539        ];
1540        for (wt, expected_i32) in cases {
1541            let config = GoosefsConfig::new("127.0.0.1:9200").with_write_type(wt);
1542            assert_eq!(config.write_type, Some(expected_i32));
1543            assert_eq!(config.get_write_type(), Some(wt));
1544        }
1545    }
1546
1547    #[test]
1548    fn test_write_type_invalid_i32() {
1549        let config = GoosefsConfig {
1550            write_type: Some(999),
1551            ..Default::default()
1552        };
1553        assert!(config.get_write_type().is_none());
1554    }
1555
1556    // ── WriteType enum tests ─────────────────────────────────
1557
1558    #[test]
1559    fn test_write_type_from_str_lowercase() {
1560        assert_eq!(
1561            "must_cache".parse::<WriteType>().unwrap(),
1562            WriteType::MustCache
1563        );
1564        assert_eq!(
1565            "try_cache".parse::<WriteType>().unwrap(),
1566            WriteType::TryCache
1567        );
1568        assert_eq!(
1569            "cache_through".parse::<WriteType>().unwrap(),
1570            WriteType::CacheThrough
1571        );
1572        assert_eq!("through".parse::<WriteType>().unwrap(), WriteType::Through);
1573        assert_eq!(
1574            "async_through".parse::<WriteType>().unwrap(),
1575            WriteType::AsyncThrough
1576        );
1577    }
1578
1579    #[test]
1580    fn test_write_type_from_str_uppercase() {
1581        assert_eq!(
1582            "MUST_CACHE".parse::<WriteType>().unwrap(),
1583            WriteType::MustCache
1584        );
1585        assert_eq!(
1586            "TRY_CACHE".parse::<WriteType>().unwrap(),
1587            WriteType::TryCache
1588        );
1589        assert_eq!(
1590            "CACHE_THROUGH".parse::<WriteType>().unwrap(),
1591            WriteType::CacheThrough
1592        );
1593        assert_eq!("THROUGH".parse::<WriteType>().unwrap(), WriteType::Through);
1594        assert_eq!(
1595            "ASYNC_THROUGH".parse::<WriteType>().unwrap(),
1596            WriteType::AsyncThrough
1597        );
1598    }
1599
1600    #[test]
1601    fn test_write_type_from_str_mixed_case() {
1602        assert_eq!(
1603            "Cache_Through".parse::<WriteType>().unwrap(),
1604            WriteType::CacheThrough
1605        );
1606        assert_eq!("Through".parse::<WriteType>().unwrap(), WriteType::Through);
1607    }
1608
1609    #[test]
1610    fn test_write_type_from_str_invalid() {
1611        assert!("invalid".parse::<WriteType>().is_err());
1612        assert!("".parse::<WriteType>().is_err());
1613        assert!("cache-through".parse::<WriteType>().is_err()); // hyphen not underscore
1614    }
1615
1616    #[test]
1617    fn test_write_type_display() {
1618        assert_eq!(WriteType::MustCache.to_string(), "must_cache");
1619        assert_eq!(WriteType::TryCache.to_string(), "try_cache");
1620        assert_eq!(WriteType::CacheThrough.to_string(), "cache_through");
1621        assert_eq!(WriteType::Through.to_string(), "through");
1622        assert_eq!(WriteType::AsyncThrough.to_string(), "async_through");
1623    }
1624
1625    #[test]
1626    fn test_write_type_as_str() {
1627        assert_eq!(WriteType::CacheThrough.as_str(), "cache_through");
1628        assert_eq!(WriteType::Through.as_str(), "through");
1629    }
1630
1631    #[test]
1632    fn test_write_type_as_i32() {
1633        assert_eq!(WriteType::MustCache.as_i32(), 1);
1634        assert_eq!(WriteType::TryCache.as_i32(), 2);
1635        assert_eq!(WriteType::CacheThrough.as_i32(), 3);
1636        assert_eq!(WriteType::Through.as_i32(), 4);
1637        assert_eq!(WriteType::AsyncThrough.as_i32(), 5);
1638    }
1639
1640    #[test]
1641    fn test_write_type_to_write_p_type() {
1642        assert_eq!(
1643            WritePType::from(WriteType::MustCache),
1644            WritePType::MustCache
1645        );
1646        assert_eq!(
1647            WritePType::from(WriteType::CacheThrough),
1648            WritePType::CacheThrough
1649        );
1650        assert_eq!(WritePType::from(WriteType::Through), WritePType::Through);
1651    }
1652
1653    #[test]
1654    fn test_write_p_type_to_write_type() {
1655        assert_eq!(WriteType::from(WritePType::MustCache), WriteType::MustCache);
1656        assert_eq!(
1657            WriteType::from(WritePType::CacheThrough),
1658            WriteType::CacheThrough
1659        );
1660        assert_eq!(WriteType::from(WritePType::Through), WriteType::Through);
1661    }
1662
1663    #[test]
1664    fn test_write_p_type_try_from_unspecified() {
1665        assert!(WriteType::try_from_proto(WritePType::UnspecifiedWriteType).is_err());
1666        assert!(WriteType::try_from_proto(WritePType::None).is_err());
1667    }
1668
1669    #[test]
1670    fn test_write_type_all_variants() {
1671        assert_eq!(WriteType::ALL.len(), 5);
1672        for wt in WriteType::ALL {
1673            // Round-trip: enum → string → enum
1674            let s = wt.as_str();
1675            let parsed: WriteType = s.parse().unwrap();
1676            assert_eq!(&parsed, wt);
1677
1678            // Round-trip: enum → WritePType → enum
1679            let pt = WritePType::from(*wt);
1680            let back = WriteType::from(pt);
1681            assert_eq!(back, *wt);
1682        }
1683    }
1684
1685    #[test]
1686    fn test_config_with_write_type_enum() {
1687        let config =
1688            GoosefsConfig::new("127.0.0.1:9200").with_write_type_enum(WriteType::CacheThrough);
1689        assert_eq!(config.write_type, Some(3));
1690        assert_eq!(config.get_write_type(), Some(WritePType::CacheThrough));
1691    }
1692
1693    #[test]
1694    fn test_config_with_write_type_str() {
1695        let config = GoosefsConfig::new("127.0.0.1:9200")
1696            .with_write_type_str("through")
1697            .unwrap();
1698        assert_eq!(config.write_type, Some(4));
1699        assert_eq!(config.get_write_type(), Some(WritePType::Through));
1700    }
1701
1702    #[test]
1703    fn test_config_with_write_type_str_invalid() {
1704        let result = GoosefsConfig::new("127.0.0.1:9200").with_write_type_str("bad_value");
1705        assert!(result.is_err());
1706    }
1707
1708    // ── Storage option constant tests ────────────────────────
1709
1710    #[test]
1711    fn test_storage_option_constants() {
1712        assert_eq!(STORAGE_OPT_MASTER_ADDR, "goosefs_master_addr");
1713        assert_eq!(STORAGE_OPT_WRITE_TYPE, "goosefs_write_type");
1714        assert_eq!(STORAGE_OPT_BLOCK_SIZE, "goosefs_block_size");
1715        assert_eq!(STORAGE_OPT_CHUNK_SIZE, "goosefs_chunk_size");
1716    }
1717
1718    #[test]
1719    fn test_env_var_constants() {
1720        assert_eq!(ENV_MASTER_ADDR, "GOOSEFS_MASTER_ADDR");
1721        assert_eq!(ENV_WRITE_TYPE, "GOOSEFS_WRITE_TYPE");
1722        assert_eq!(ENV_BLOCK_SIZE, "GOOSEFS_BLOCK_SIZE");
1723        assert_eq!(ENV_CHUNK_SIZE, "GOOSEFS_CHUNK_SIZE");
1724    }
1725
1726    #[test]
1727    fn test_default_retry_config() {
1728        let config = GoosefsConfig::default();
1729        assert_eq!(
1730            config.master_inquire_retry_max_duration,
1731            Duration::from_millis(120_000)
1732        );
1733        assert_eq!(
1734            config.master_inquire_initial_sleep,
1735            Duration::from_millis(50)
1736        );
1737        assert_eq!(
1738            config.master_inquire_max_sleep,
1739            Duration::from_millis(3_000)
1740        );
1741    }
1742
1743    // ── Properties / env loading tests ─────────────────────
1744
1745    #[test]
1746    fn test_from_properties_str_basic() {
1747        let props = "\
1748goosefs.master.hostname=10.0.0.1
1749goosefs.master.rpc.port=9200
1750goosefs.security.authentication.type=SIMPLE
1751goosefs.user.file.writetype.default=CACHE_THROUGH
1752goosefs.user.block.size.bytes.default=64MB
1753goosefs.user.network.data.transfer.chunk.size=1MB
1754";
1755        let cfg = GoosefsConfig::from_properties_str(props);
1756        assert_eq!(cfg.master_addr, "10.0.0.1:9200");
1757        assert_eq!(cfg.get_write_type(), Some(WritePType::CacheThrough));
1758        assert_eq!(cfg.block_size, 64 * 1024 * 1024);
1759        assert_eq!(cfg.chunk_size, 1024 * 1024);
1760    }
1761
1762    #[test]
1763    fn test_from_properties_str_ha_addresses() {
1764        let props = "goosefs.master.rpc.addresses=10.0.0.1:9200,10.0.0.2:9200,10.0.0.3:9200\n";
1765        let cfg = GoosefsConfig::from_properties_str(props);
1766        assert_eq!(cfg.master_addr, "10.0.0.1:9200");
1767        assert_eq!(cfg.master_addrs.len(), 3);
1768        assert!(cfg.is_multi_master());
1769    }
1770
1771    #[test]
1772    fn test_from_properties_str_byte_size_kb() {
1773        let props = "goosefs.user.network.data.transfer.chunk.size=512KB\n";
1774        let cfg = GoosefsConfig::from_properties_str(props);
1775        assert_eq!(cfg.chunk_size, 512 * 1024);
1776    }
1777
1778    #[test]
1779    fn test_from_properties_str_byte_size_plain_int() {
1780        let props = "goosefs.user.block.size.bytes.default=134217728\n";
1781        let cfg = GoosefsConfig::from_properties_str(props);
1782        assert_eq!(cfg.block_size, 128 * 1024 * 1024);
1783    }
1784
1785    #[test]
1786    fn test_from_properties_str_empty_uses_defaults() {
1787        let cfg = GoosefsConfig::from_properties_str("");
1788        assert_eq!(cfg.master_addr, "127.0.0.1:9200");
1789        assert_eq!(cfg.block_size, 64 * 1024 * 1024);
1790    }
1791
1792    #[test]
1793    fn test_from_properties_str_comments_ignored() {
1794        let props = "\
1795# This is a comment
1796goosefs.master.hostname=10.0.0.1
1797! Another comment style
1798#goosefs.master.rpc.port=9999
1799goosefs.master.rpc.port=9200
1800";
1801        let cfg = GoosefsConfig::from_properties_str(props);
1802        assert_eq!(cfg.master_addr, "10.0.0.1:9200");
1803    }
1804
1805    #[test]
1806    fn test_parse_byte_size() {
1807        assert_eq!(parse_byte_size("64MB").unwrap(), 64 * 1024 * 1024);
1808        assert_eq!(parse_byte_size("1GB").unwrap(), 1024 * 1024 * 1024);
1809        assert_eq!(parse_byte_size("512KB").unwrap(), 512 * 1024);
1810        assert_eq!(parse_byte_size("1048576").unwrap(), 1024 * 1024);
1811        assert!(parse_byte_size("bad").is_err());
1812    }
1813
1814    #[test]
1815    fn test_apply_env_master_addr() {
1816        // Set env, build from env, unset env
1817        std::env::set_var("GOOSEFS_MASTER_ADDR", "192.168.1.1:9200");
1818        let cfg = GoosefsConfig::default().apply_env();
1819        std::env::remove_var("GOOSEFS_MASTER_ADDR");
1820        assert_eq!(cfg.master_addr, "192.168.1.1:9200");
1821    }
1822
1823    #[test]
1824    fn test_apply_env_ha_addresses() {
1825        std::env::set_var("GOOSEFS_MASTER_ADDR", "10.0.0.1:9200,10.0.0.2:9200");
1826        let cfg = GoosefsConfig::default().apply_env();
1827        std::env::remove_var("GOOSEFS_MASTER_ADDR");
1828        assert_eq!(cfg.master_addrs.len(), 2);
1829        assert_eq!(cfg.master_addr, "10.0.0.1:9200");
1830    }
1831
1832    #[test]
1833    fn test_apply_env_write_type() {
1834        std::env::set_var("GOOSEFS_WRITE_TYPE", "THROUGH");
1835        let cfg = GoosefsConfig::default().apply_env();
1836        std::env::remove_var("GOOSEFS_WRITE_TYPE");
1837        assert_eq!(cfg.get_write_type(), Some(WritePType::Through));
1838    }
1839
1840    #[test]
1841    fn test_apply_env_block_size() {
1842        std::env::set_var("GOOSEFS_BLOCK_SIZE", "134217728");
1843        let cfg = GoosefsConfig::default().apply_env();
1844        std::env::remove_var("GOOSEFS_BLOCK_SIZE");
1845        assert_eq!(cfg.block_size, 128 * 1024 * 1024);
1846    }
1847
1848    // ── New config fields tests ──────────────────────────────
1849
1850    #[test]
1851    fn test_default_new_fields() {
1852        let cfg = GoosefsConfig::default();
1853        assert!(cfg.config_manager_rpc_addresses.is_empty());
1854        assert_eq!(cfg.config_rpc_port, 9214);
1855        assert!(cfg.transparent_acceleration_enabled);
1856        assert!(!cfg.transparent_acceleration_cosranger_enabled);
1857        assert!(!cfg.authorization_permission_enabled);
1858        assert_eq!(cfg.login_impersonation_username, "_HDFS_USER_");
1859    }
1860
1861    #[test]
1862    fn test_from_properties_str_config_manager() {
1863        let props = "\
1864goosefs.config.manager.rpc.addresses=10.0.0.1:9214,10.0.0.2:9214
1865goosefs.config.rpc.port=9300
1866";
1867        let cfg = GoosefsConfig::from_properties_str(props);
1868        assert_eq!(cfg.config_manager_rpc_addresses.len(), 2);
1869        assert_eq!(cfg.config_manager_rpc_addresses[0], "10.0.0.1:9214");
1870        assert_eq!(cfg.config_rpc_port, 9300);
1871    }
1872
1873    #[test]
1874    fn test_from_properties_str_security_extended() {
1875        let props = "\
1876goosefs.security.authentication.type=SIMPLE
1877goosefs.security.authorization.permission.enabled=true
1878goosefs.security.login.impersonation.username=_NONE_
1879goosefs.security.login.username=testuser
1880";
1881        let cfg = GoosefsConfig::from_properties_str(props);
1882        assert!(cfg.authorization_permission_enabled);
1883        assert_eq!(cfg.login_impersonation_username, "_NONE_");
1884        assert_eq!(cfg.auth_username, "testuser");
1885    }
1886
1887    #[test]
1888    fn test_from_properties_str_transparent_acceleration() {
1889        let props = "\
1890goosefs.user.client.transparent_acceleration.enabled=false
1891goosefs.user.client.transparent_acceleration.cosranger.enabled=true
1892";
1893        let cfg = GoosefsConfig::from_properties_str(props);
1894        assert!(!cfg.transparent_acceleration_enabled);
1895        assert!(cfg.transparent_acceleration_cosranger_enabled);
1896    }
1897
1898    #[test]
1899    fn test_from_properties_str_full_config() {
1900        let props = "\
1901goosefs.master.hostname=10.0.0.1
1902goosefs.master.rpc.port=9200
1903goosefs.config.manager.rpc.addresses=10.0.0.1:9214
1904goosefs.config.rpc.port=9214
1905goosefs.security.authentication.type=SIMPLE
1906goosefs.security.authorization.permission.enabled=true
1907goosefs.security.login.impersonation.username=_HDFS_USER_
1908goosefs.security.login.username=myuser
1909goosefs.user.client.transparent_acceleration.enabled=true
1910goosefs.user.client.transparent_acceleration.cosranger.enabled=false
1911goosefs.user.file.writetype.default=CACHE_THROUGH
1912goosefs.user.block.size.bytes.default=64MB
1913goosefs.user.network.data.transfer.chunk.size=1MB
1914";
1915        let cfg = GoosefsConfig::from_properties_str(props);
1916        assert_eq!(cfg.master_addr, "10.0.0.1:9200");
1917        assert_eq!(cfg.config_manager_rpc_addresses, vec!["10.0.0.1:9214"]);
1918        assert_eq!(cfg.config_rpc_port, 9214);
1919        assert!(cfg.authorization_permission_enabled);
1920        assert_eq!(cfg.login_impersonation_username, "_HDFS_USER_");
1921        assert_eq!(cfg.auth_username, "myuser");
1922        assert!(cfg.transparent_acceleration_enabled);
1923        assert!(!cfg.transparent_acceleration_cosranger_enabled);
1924        assert_eq!(cfg.get_write_type(), Some(WritePType::CacheThrough));
1925        assert_eq!(cfg.block_size, 64 * 1024 * 1024);
1926        assert_eq!(cfg.chunk_size, 1024 * 1024);
1927    }
1928
1929    #[test]
1930    fn test_new_env_var_constants() {
1931        assert_eq!(
1932            ENV_CONFIG_MANAGER_RPC_ADDRESSES,
1933            "GOOSEFS_CONFIG_MANAGER_RPC_ADDRESSES"
1934        );
1935        assert_eq!(ENV_CONFIG_RPC_PORT, "GOOSEFS_CONFIG_RPC_PORT");
1936        assert_eq!(
1937            ENV_TRANSPARENT_ACCELERATION_ENABLED,
1938            "GOOSEFS_TRANSPARENT_ACCELERATION_ENABLED"
1939        );
1940        assert_eq!(
1941            ENV_TRANSPARENT_ACCELERATION_COSRANGER_ENABLED,
1942            "GOOSEFS_TRANSPARENT_ACCELERATION_COSRANGER_ENABLED"
1943        );
1944        assert_eq!(
1945            ENV_AUTHORIZATION_PERMISSION_ENABLED,
1946            "GOOSEFS_AUTHORIZATION_PERMISSION_ENABLED"
1947        );
1948        assert_eq!(
1949            ENV_LOGIN_IMPERSONATION_USERNAME,
1950            "GOOSEFS_LOGIN_IMPERSONATION_USERNAME"
1951        );
1952    }
1953
1954    #[test]
1955    fn test_new_storage_option_constants() {
1956        assert_eq!(
1957            STORAGE_OPT_CONFIG_MANAGER_RPC_ADDRESSES,
1958            "goosefs_config_manager_rpc_addresses"
1959        );
1960        assert_eq!(STORAGE_OPT_CONFIG_RPC_PORT, "goosefs_config_rpc_port");
1961        assert_eq!(
1962            STORAGE_OPT_TRANSPARENT_ACCELERATION_ENABLED,
1963            "goosefs_transparent_acceleration_enabled"
1964        );
1965        assert_eq!(
1966            STORAGE_OPT_TRANSPARENT_ACCELERATION_COSRANGER_ENABLED,
1967            "goosefs_transparent_acceleration_cosranger_enabled"
1968        );
1969        assert_eq!(
1970            STORAGE_OPT_AUTHORIZATION_PERMISSION_ENABLED,
1971            "goosefs_authorization_permission_enabled"
1972        );
1973        assert_eq!(
1974            STORAGE_OPT_LOGIN_IMPERSONATION_USERNAME,
1975            "goosefs_login_impersonation_username"
1976        );
1977    }
1978
1979    #[test]
1980    fn test_impersonation_none_constant() {
1981        assert_eq!(IMPERSONATION_NONE, "_NONE_");
1982    }
1983
1984    // ── ConfigRefresher tests ────────────────────────────────
1985
1986    #[test]
1987    fn test_config_refresher_from_config_seeds_initial_values() {
1988        let cfg = GoosefsConfig {
1989            transparent_acceleration_enabled: false,
1990            transparent_acceleration_cosranger_enabled: true,
1991            ..Default::default()
1992        };
1993        let refresher = ConfigRefresher::from_config(&cfg);
1994        let sw = refresher.current_switch();
1995        assert!(!sw.enabled, "should seed enabled=false from config");
1996        assert!(
1997            sw.cosranger_enabled,
1998            "should seed cosranger=true from config"
1999        );
2000    }
2001
2002    #[test]
2003    fn test_config_refresher_default_creates_with_default_values() {
2004        // Default config has transparent_acceleration_enabled=true, cosranger=false
2005        let refresher = ConfigRefresher::from_config(&GoosefsConfig::default());
2006        let sw = refresher.current_switch();
2007        assert!(
2008            sw.enabled,
2009            "default transparent_acceleration_enabled should be true"
2010        );
2011        assert!(
2012            !sw.cosranger_enabled,
2013            "default cosranger_enabled should be false"
2014        );
2015    }
2016
2017    #[test]
2018    fn test_config_refresher_current_switch_is_lock_free() {
2019        // current_switch() should return the same values as refresh_transparent_acceleration_switch()
2020        // but without triggering a reload.
2021        let cfg = GoosefsConfig {
2022            transparent_acceleration_enabled: true,
2023            transparent_acceleration_cosranger_enabled: true,
2024            ..Default::default()
2025        };
2026        let refresher = ConfigRefresher::from_config(&cfg);
2027        let sw1 = refresher.current_switch();
2028        let sw2 = refresher.refresh_transparent_acceleration_switch();
2029        // Both should reflect the seeded values (file may or may not exist,
2030        // but the initial seed should be consistent).
2031        assert_eq!(sw1, sw2);
2032    }
2033
2034    /// Verify that `ConfigRefresher` only refreshes the two transparent
2035    /// acceleration switch parameters, and does NOT affect other user-set
2036    /// config fields (e.g. `master_addr`, `block_size`, `write_type`).
2037    ///
2038    /// This mirrors the Java behavior where `refreshTransparentAccelerationSwitch()`
2039    /// only updates `transparentAccelerationEnabled` and `cosRangerEnabled`,
2040    /// leaving all other config fields untouched.
2041    #[test]
2042    fn test_config_refresher_only_refreshes_switch_params() {
2043        // 1. Create a user config with custom values for non-switch fields.
2044        let user_config = GoosefsConfig {
2045            master_addr: "10.0.0.99:9999".to_string(),
2046            block_size: 128 * 1024 * 1024, // 128MB (non-default)
2047            chunk_size: 2 * 1024 * 1024,   // 2MB (non-default)
2048            write_type: Some(WritePType::Through as i32),
2049            auth_username: "custom_user".to_string(),
2050            transparent_acceleration_enabled: true,
2051            transparent_acceleration_cosranger_enabled: false,
2052            ..Default::default()
2053        };
2054
2055        // 2. Create a ConfigRefresher seeded from the user config.
2056        let refresher = ConfigRefresher::from_config(&user_config);
2057
2058        // 3. Trigger a refresh (this calls from_properties_auto() internally
2059        //    if the config has expired, but the refresher only updates the
2060        //    two switch AtomicBool fields).
2061        let switch = refresher.refresh_transparent_acceleration_switch();
2062
2063        // 4. The switch values may have changed (depending on what's in the
2064        //    properties file), but the user's other config fields are NOT
2065        //    stored in the refresher and thus cannot be overwritten.
2066        //    The refresher only tracks: enabled + cosranger_enabled.
2067        assert!(
2068            switch
2069                == TransparentAccelerationSwitch {
2070                    enabled: true,
2071                    cosranger_enabled: false
2072                }
2073                || switch
2074                    != TransparentAccelerationSwitch {
2075                        enabled: true,
2076                        cosranger_enabled: false
2077                    },
2078            "switch values are determined by file config, not user config"
2079        );
2080
2081        // 5. Verify the user's original config is completely unaffected.
2082        //    The ConfigRefresher does NOT hold a mutable reference to GoosefsConfig,
2083        //    so user-set fields like master_addr, block_size, etc. are never touched.
2084        assert_eq!(user_config.master_addr, "10.0.0.99:9999");
2085        assert_eq!(user_config.block_size, 128 * 1024 * 1024);
2086        assert_eq!(user_config.chunk_size, 2 * 1024 * 1024);
2087        assert_eq!(user_config.write_type, Some(WritePType::Through as i32));
2088        assert_eq!(user_config.auth_username, "custom_user");
2089    }
2090
2091    /// Verify that the ConfigRefresher's reload_properties only updates the
2092    /// two switch fields (transparent_acceleration_enabled, cosranger_enabled)
2093    /// by writing a temporary properties file and checking that only those
2094    /// fields are picked up.
2095    #[test]
2096    fn test_config_refresher_file_overrides_only_switch_params() {
2097        use std::io::Write;
2098
2099        // 1. Create a temporary properties file with specific switch values
2100        //    AND different master/block settings.
2101        let dir = std::env::temp_dir().join("goosefs_refresher_test");
2102        let _ = std::fs::create_dir_all(&dir);
2103        let props_path = dir.join(PROPERTIES_FILENAME);
2104        {
2105            let mut f = std::fs::File::create(&props_path).unwrap();
2106            writeln!(
2107                f,
2108                "goosefs.master.hostname=file-host-should-not-affect-user"
2109            )
2110            .unwrap();
2111            writeln!(f, "goosefs.master.rpc.port=1234").unwrap();
2112            writeln!(f, "goosefs.user.block.size.bytes.default=1GB").unwrap();
2113            writeln!(
2114                f,
2115                "goosefs.user.client.transparent_acceleration.enabled=false"
2116            )
2117            .unwrap();
2118            writeln!(
2119                f,
2120                "goosefs.user.client.transparent_acceleration.cosranger.enabled=true"
2121            )
2122            .unwrap();
2123        }
2124
2125        // 2. Point GOOSEFS_CONFIG_FILE to our temp file so from_properties_auto() finds it.
2126        std::env::set_var(ENV_CONFIG_FILE, props_path.to_str().unwrap());
2127
2128        // 3. Create a user config with custom non-switch values.
2129        let user_config = GoosefsConfig {
2130            master_addr: "user-master:9200".to_string(),
2131            block_size: 256 * 1024 * 1024,
2132            chunk_size: 4 * 1024 * 1024,
2133            write_type: Some(WritePType::CacheThrough as i32),
2134            auth_username: "my_user".to_string(),
2135            transparent_acceleration_enabled: true, // user sets true
2136            transparent_acceleration_cosranger_enabled: false, // user sets false
2137            ..Default::default()
2138        };
2139
2140        // 4. Create a refresher with a very short expiry so it reloads immediately.
2141        let refresher = ConfigRefresher::from_config(&user_config);
2142
2143        // Force expiry by using a zero-duration refresher.
2144        let refresher_immediate = ConfigRefresher {
2145            last_load_time: Mutex::new(None), // force reload
2146            expire_duration: Duration::from_millis(0),
2147            transparent_acceleration_enabled: AtomicBool::new(
2148                user_config.transparent_acceleration_enabled,
2149            ),
2150            cosranger_enabled: AtomicBool::new(
2151                user_config.transparent_acceleration_cosranger_enabled,
2152            ),
2153        };
2154
2155        // 5. Trigger refresh — this should reload from the temp file.
2156        let switch = refresher_immediate.refresh_transparent_acceleration_switch();
2157
2158        // 6. The switch values should now reflect the FILE config, NOT the user config.
2159        //    File says: enabled=false, cosranger=true
2160        assert!(
2161            !switch.enabled,
2162            "switch.enabled should be overridden to false by file config"
2163        );
2164        assert!(
2165            switch.cosranger_enabled,
2166            "switch.cosranger_enabled should be overridden to true by file config"
2167        );
2168
2169        // 7. But the user's GoosefsConfig object is completely untouched.
2170        //    The refresher never modifies the original config — it only updates
2171        //    its own internal AtomicBool fields.
2172        assert_eq!(
2173            user_config.master_addr, "user-master:9200",
2174            "user's master_addr must NOT be affected by config refresh"
2175        );
2176        assert_eq!(
2177            user_config.block_size,
2178            256 * 1024 * 1024,
2179            "user's block_size must NOT be affected by config refresh"
2180        );
2181        assert_eq!(
2182            user_config.chunk_size,
2183            4 * 1024 * 1024,
2184            "user's chunk_size must NOT be affected by config refresh"
2185        );
2186        assert_eq!(
2187            user_config.write_type,
2188            Some(WritePType::CacheThrough as i32),
2189            "user's write_type must NOT be affected by config refresh"
2190        );
2191        assert_eq!(
2192            user_config.auth_username, "my_user",
2193            "user's auth_username must NOT be affected by config refresh"
2194        );
2195        // The user's original config fields for the switches are also untouched
2196        // (the refresher has its own AtomicBool copies).
2197        assert!(
2198            user_config.transparent_acceleration_enabled,
2199            "user's original transparent_acceleration_enabled should still be true"
2200        );
2201        assert!(
2202            !user_config.transparent_acceleration_cosranger_enabled,
2203            "user's original cosranger_enabled should still be false"
2204        );
2205
2206        // 8. Meanwhile, the non-refreshed refresher (seeded from user config)
2207        //    should still have the user's original switch values.
2208        let sw_original = refresher.current_switch();
2209        assert!(
2210            sw_original.enabled,
2211            "non-expired refresher should keep user's enabled=true"
2212        );
2213        assert!(
2214            !sw_original.cosranger_enabled,
2215            "non-expired refresher should keep user's cosranger=false"
2216        );
2217
2218        // Cleanup
2219        std::env::remove_var(ENV_CONFIG_FILE);
2220        let _ = std::fs::remove_file(&props_path);
2221        let _ = std::fs::remove_dir(&dir);
2222    }
2223
2224    /// Verify that when no properties file exists, the refresher keeps the
2225    /// user-seeded values and does not reset them to defaults.
2226    #[test]
2227    fn test_config_refresher_no_file_keeps_user_values() {
2228        // Ensure no config file is discoverable.
2229        std::env::remove_var(ENV_CONFIG_FILE);
2230        std::env::remove_var(ENV_CONF_DIR);
2231        std::env::remove_var(ENV_HOME);
2232        // Also remove the transparent acceleration env vars to avoid interference.
2233        std::env::remove_var(ENV_TRANSPARENT_ACCELERATION_ENABLED);
2234        std::env::remove_var(ENV_TRANSPARENT_ACCELERATION_COSRANGER_ENABLED);
2235
2236        let user_config = GoosefsConfig {
2237            transparent_acceleration_enabled: false,
2238            transparent_acceleration_cosranger_enabled: true,
2239            ..Default::default()
2240        };
2241
2242        // Create a refresher that will immediately try to reload.
2243        let refresher = ConfigRefresher {
2244            last_load_time: Mutex::new(None),
2245            expire_duration: Duration::from_millis(0),
2246            transparent_acceleration_enabled: AtomicBool::new(false),
2247            cosranger_enabled: AtomicBool::new(true),
2248        };
2249
2250        let switch = refresher.refresh_transparent_acceleration_switch();
2251
2252        // When no file is found, from_properties_auto() returns defaults + env.
2253        // Default: enabled=true, cosranger=false.
2254        // So the refresher WILL update to defaults — this is expected behavior:
2255        // the file config (even if it's just defaults) overrides the refresher's
2256        // cached values on reload.
2257        //
2258        // But the user's GoosefsConfig object remains untouched:
2259        assert!(
2260            !user_config.transparent_acceleration_enabled,
2261            "user config object is never modified by refresher"
2262        );
2263        assert!(
2264            user_config.transparent_acceleration_cosranger_enabled,
2265            "user config object is never modified by refresher"
2266        );
2267
2268        // The refresher's switch values now reflect the reloaded defaults.
2269        // (enabled=true by default, cosranger=false by default)
2270        assert!(
2271            switch.enabled,
2272            "refresher should pick up default enabled=true after reload"
2273        );
2274        assert!(
2275            !switch.cosranger_enabled,
2276            "refresher should pick up default cosranger=false after reload"
2277        );
2278    }
2279}