use serde::{Deserialize, Serialize};
use std::fmt;
use std::str::FromStr;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Mutex;
use std::time::{Duration, Instant};
use crate::auth::AuthType;
use crate::proto::grpc::file::WritePType;
#[derive(Debug)]
pub enum ConfigLoadError {
IoError { path: String, source: String },
ParseError { message: String },
}
impl std::fmt::Display for ConfigLoadError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ConfigLoadError::IoError { path, source } => {
write!(f, "failed to read config file '{}': {}", path, source)
}
ConfigLoadError::ParseError { message } => {
write!(f, "failed to parse YAML config: {}", message)
}
}
}
}
impl std::error::Error for ConfigLoadError {}
#[derive(Debug, PartialEq, Eq)]
pub enum UriParseError {
InvalidScheme { input: String },
EmptyAuthority { input: String },
}
impl std::fmt::Display for UriParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
UriParseError::InvalidScheme { input } => write!(
f,
"invalid Goosefs URI (expected 'gfs://<host:port>[,...][/path]'): {input:?}"
),
UriParseError::EmptyAuthority { input } => {
write!(f, "Goosefs URI has no master addresses: {input:?}")
}
}
}
}
impl std::error::Error for UriParseError {}
fn parse_gfs_uri(uri: &str) -> Result<(Vec<String>, String), UriParseError> {
const SCHEME: &str = "gfs://";
let rest = uri
.strip_prefix(SCHEME)
.ok_or_else(|| UriParseError::InvalidScheme {
input: uri.to_string(),
})?;
let (authority, root) = match rest.find('/') {
Some(idx) => (&rest[..idx], &rest[idx..]),
None => (rest, ""),
};
let addrs: Vec<String> = authority
.split(',')
.map(str::trim)
.filter(|s| !s.is_empty())
.map(String::from)
.collect();
if addrs.is_empty() {
return Err(UriParseError::EmptyAuthority {
input: uri.to_string(),
});
}
let root = root.trim_end_matches('/').to_string();
Ok((addrs, root))
}
use std::collections::HashMap;
#[derive(Debug, Default)]
struct PropertiesMap {
props: HashMap<String, String>,
}
impl PropertiesMap {
fn parse(content: &str) -> Self {
let mut props = HashMap::new();
for line in content.lines() {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with('!') {
continue;
}
let sep_pos = trimmed.find('=').or_else(|| trimmed.find(':'));
if let Some(pos) = sep_pos {
let key = trimmed[..pos].trim().to_string();
let value = trimmed[pos + 1..].trim().to_string();
if !key.is_empty() {
props.insert(key, value);
}
}
}
PropertiesMap { props }
}
fn get(&self, key: &str) -> Option<&str> {
self.props.get(key).map(|s| s.as_str())
}
fn get_parsed<T: FromStr>(&self, key: &str) -> Option<T> {
self.get(key).and_then(|v| v.parse::<T>().ok())
}
fn get_bool(&self, key: &str) -> Option<bool> {
self.get(key)
.and_then(|v| v.to_ascii_lowercase().parse::<bool>().ok())
}
fn get_list(&self, key: &str) -> Option<Vec<String>> {
self.get(key).map(|v| {
v.split(',')
.map(str::trim)
.filter(|s| !s.is_empty())
.map(String::from)
.collect()
})
}
}
fn parse_byte_size(s: &str) -> Result<u64, String> {
let s = s.trim();
let upper = s.to_uppercase();
let (multiplier, num_str) = if upper.ends_with("GB") {
(1024u64 * 1024 * 1024, &s[..s.len() - 2])
} else if upper.ends_with("MB") {
(1024 * 1024, &s[..s.len() - 2])
} else if upper.ends_with("KB") {
(1024, &s[..s.len() - 2])
} else {
(1, s)
};
num_str
.trim()
.parse::<u64>()
.map_err(|e| format!("invalid byte size '{}': {}", s, e))
.and_then(|n| {
n.checked_mul(multiplier)
.ok_or_else(|| format!("byte size '{}' overflows u64", s))
})
}
impl PropertiesMap {
fn into_goosefs_config(self) -> GoosefsConfig {
let mut cfg = GoosefsConfig::default();
if let Some(addrs) = self.get_list("goosefs.master.rpc.addresses") {
if !addrs.is_empty() {
cfg.master_addr = addrs[0].clone();
if addrs.len() > 1 {
cfg.master_addrs = addrs;
}
}
} else if let Some(host) = self.get("goosefs.master.hostname") {
let port: u16 = self.get_parsed("goosefs.master.rpc.port").unwrap_or(9200);
cfg.master_addr = format!("{}:{}", host, port);
}
if let Some(addrs) = self.get_list("goosefs.config.manager.rpc.addresses") {
if !addrs.is_empty() {
cfg.config_manager_rpc_addresses = addrs;
}
}
if let Some(port) = self.get_parsed::<u16>("goosefs.config.rpc.port") {
cfg.config_rpc_port = port;
}
if let Some(at_str) = self.get("goosefs.security.authentication.type") {
if let Ok(at) = at_str.parse::<AuthType>() {
cfg.auth_type = at;
}
}
if let Some(enabled) = self.get_bool("goosefs.security.authorization.permission.enabled") {
cfg.authorization_permission_enabled = enabled;
}
if let Some(user) = self.get("goosefs.security.login.impersonation.username") {
if !user.is_empty() {
cfg.login_impersonation_username = user.to_string();
}
}
if let Some(user) = self.get("goosefs.security.login.username") {
if !user.is_empty() {
cfg.auth_username = user.to_string();
}
}
if let Some(enabled) = self.get_bool("goosefs.user.client.transparent_acceleration.enabled")
{
cfg.transparent_acceleration_enabled = enabled;
}
if let Some(enabled) =
self.get_bool("goosefs.user.client.transparent_acceleration.cosranger.enabled")
{
cfg.transparent_acceleration_cosranger_enabled = enabled;
}
if let Some(wt_str) = self.get("goosefs.user.file.writetype.default") {
if let Ok(wt) = wt_str.parse::<WriteType>() {
cfg.write_type = Some(wt.as_i32());
}
}
if let Some(bs_str) = self.get("goosefs.user.block.size.bytes.default") {
if let Ok(bs) = parse_byte_size(bs_str) {
if bs > 0 {
cfg.block_size = bs;
}
}
}
if let Some(cs_str) = self.get("goosefs.user.network.data.transfer.chunk.size") {
if let Ok(cs) = parse_byte_size(cs_str) {
if cs > 0 {
cfg.chunk_size = cs;
}
}
}
if let Some(val) = self.get("goosefs.user.metrics.collection.enabled") {
if let Ok(b) = val.to_lowercase().parse::<bool>() {
cfg.metrics_enabled = b;
}
}
if let Some(ms_str) = self.get("goosefs.user.metrics.heartbeat.interval") {
if let Ok(ms) = ms_str.parse::<u64>() {
if ms >= MIN_METRICS_HEARTBEAT_INTERVAL_MS {
cfg.metrics_heartbeat_interval = Duration::from_millis(ms);
}
}
}
if let Some(id) = self.get("goosefs.user.app.id") {
if !id.is_empty() {
cfg.app_id = Some(id.to_string());
}
}
if let Some(val) = self.get("goosefs.metrics.pushgateway.enabled") {
if let Ok(b) = val.to_lowercase().parse::<bool>() {
cfg.pushgateway_enabled = b;
}
}
if let Some(val) = self.get("goosefs.metrics.pushgateway.endpoint") {
if !val.is_empty() {
cfg.pushgateway_endpoint = val.to_string();
}
}
if let Some(ms_str) = self.get("goosefs.metrics.pushgateway.push.interval") {
if let Ok(ms) = ms_str.parse::<u64>() {
if ms >= MIN_METRICS_HEARTBEAT_INTERVAL_MS {
cfg.pushgateway_push_interval = Duration::from_millis(ms);
}
}
}
if let Some(val) = self.get("goosefs.metrics.pushgateway.job") {
if !val.is_empty() {
cfg.pushgateway_job = val.to_string();
}
}
if let Some(val) = self.get("goosefs.metrics.pushgateway.instance") {
if !val.is_empty() {
cfg.pushgateway_instance = Some(val.to_string());
}
}
if let Some(enabled) = self.get_bool("goosefs.user.client.cache.enabled") {
cfg.client_cache_enabled = enabled;
}
if let Some(ps_str) = self.get("goosefs.user.client.cache.page.size") {
if let Ok(ps) = parse_byte_size(ps_str) {
if ps > 0 {
cfg.client_cache_page_size = ps;
}
}
}
if let Some(sz_str) = self.get("goosefs.user.client.cache.size") {
if let Ok(sz) = parse_byte_size(sz_str) {
cfg.client_cache_size = sz;
}
}
if let Some(dirs) = self.get_list("goosefs.user.client.cache.dirs") {
if !dirs.is_empty() {
cfg.client_cache_dirs = dirs;
}
}
if let Some(policy) = self.get("goosefs.user.client.cache.eviction.policy") {
if let Ok(e) = policy.parse::<CacheEvictorType>() {
cfg.client_cache_evictor = e;
}
}
if let Some(enabled) = self.get_bool("goosefs.user.client.cache.async.write.enabled") {
cfg.client_cache_async_write_enabled = enabled;
}
if let Some(n) = self.get_parsed::<usize>("goosefs.user.client.cache.async.write.threads") {
if n > 0 {
cfg.client_cache_async_write_threads = n;
}
}
if let Some(enabled) = self.get_bool("goosefs.user.client.cache.quota.enabled") {
cfg.client_cache_quota_enabled = enabled;
}
if let Some(secs) = self.get_parsed::<u64>("goosefs.user.client.cache.ttl.seconds") {
cfg.client_cache_ttl_secs = secs;
}
if let Some(enabled) = self.get_bool("goosefs.user.client.cache.sequential.read.enabled") {
cfg.client_cache_sequential_read_enabled = enabled;
}
if let Some(enabled) = self.get_bool("goosefs.user.client.cache.uring.enabled") {
cfg.client_cache_uring_enabled = enabled;
}
if let Some(n) = self.get_parsed::<usize>("goosefs.user.client.cache.uring.queue.depth") {
if n > 0 {
cfg.client_cache_uring_queue_depth = n;
}
}
if let Some(n) = self.get_parsed::<usize>("goosefs.user.client.cache.uring.thread.count") {
if n > 0 {
cfg.client_cache_uring_thread_count = n;
}
}
if let Some(n) = self.get_parsed::<usize>("goosefs.user.worker.connection.pool.size") {
cfg.worker_connection_pool_size = n.max(1);
}
if let Some(ms) = self.get_parsed::<u64>("goosefs.user.file.info.cache.ttl.ms") {
cfg.file_info_cache_ttl = Duration::from_millis(ms);
}
if let Some(n) = self.get_parsed::<usize>("goosefs.user.file.info.cache.capacity") {
cfg.file_info_cache_capacity = n.max(1);
}
if let Some(enabled) = self.get_bool("goosefs.user.short.circuit.enabled") {
cfg.short_circuit_enabled = enabled;
}
if let Some(n) = self.get_parsed::<usize>("goosefs.client.short.circuit.cache.capacity") {
cfg.short_circuit_cache_capacity = n;
}
if let Some(ms) = self.get_parsed::<u64>("goosefs.client.short.circuit.cache.ttl.ms") {
cfg.short_circuit_cache_ttl = Duration::from_millis(ms);
}
if let Some(ms) = self.get_parsed::<u64>("goosefs.client.short.circuit.neg.cache.ttl.ms") {
cfg.short_circuit_neg_cache_ttl = Duration::from_millis(ms);
}
if let Some(hint) = self.get("goosefs.client.short.circuit.advise") {
if !hint.is_empty() {
cfg.short_circuit_advise = hint.to_string();
}
}
if let Some(enabled) = self.get_bool("goosefs.client.short.circuit.prefetch.enabled") {
cfg.short_circuit_prefetch_enabled = enabled;
}
if let Some(n) =
self.get_parsed::<usize>("goosefs.client.short.circuit.prefetch.coalesce.gap")
{
cfg.short_circuit_prefetch_coalesce_gap = n;
}
if let Some(n) = self.get_parsed::<usize>("goosefs.client.short.circuit.prefetch.max.batch")
{
cfg.short_circuit_prefetch_max_batch = n;
}
if let Some(n) = self.get_parsed::<i64>("goosefs.client.short.circuit.min.block.size") {
cfg.short_circuit_min_block_size = n;
}
if let Some(enabled) = self.get_bool("goosefs.client.short.circuit.sigbus.handler") {
cfg.short_circuit_sigbus_handler = enabled;
}
if let Some(enabled) = self.get_bool("goosefs.client.short.circuit.thp") {
cfg.short_circuit_thp = enabled;
}
cfg
}
}
const PROPERTIES_FILENAME: &str = "goosefs-site.properties";
pub fn discover_config_file() -> Option<std::path::PathBuf> {
use std::path::PathBuf;
if let Ok(p) = std::env::var(ENV_CONFIG_FILE) {
let pb = PathBuf::from(&p);
if pb.exists() {
return Some(pb);
}
}
if let Ok(conf_dir) = std::env::var(CONF_DIR) {
let p = PathBuf::from(&conf_dir).join(PROPERTIES_FILENAME);
if p.exists() {
return Some(p);
}
}
if let Ok(home) = std::env::var(ENV_HOME) {
let p = PathBuf::from(&home).join("conf").join(PROPERTIES_FILENAME);
if p.exists() {
return Some(p);
}
}
if let Some(home) = dirs_next_home() {
let p = home.join(".goosefs").join(PROPERTIES_FILENAME);
if p.exists() {
return Some(p);
}
}
let system = PathBuf::from("/etc/goosefs").join(PROPERTIES_FILENAME);
if system.exists() {
return Some(system);
}
None
}
fn dirs_next_home() -> Option<std::path::PathBuf> {
std::env::var("HOME")
.or_else(|_| std::env::var("USERPROFILE"))
.ok()
.map(std::path::PathBuf::from)
}
const DEFAULT_MASTER_PORT: u16 = 9200;
#[allow(dead_code)]
const DEFAULT_WORKER_PORT: u16 = 9203;
const DEFAULT_BLOCK_SIZE: u64 = 64 * 1024 * 1024;
const DEFAULT_CHUNK_SIZE: u64 = 1024 * 1024;
const DEFAULT_PREFETCH_WINDOW: i32 = 8;
const DEFAULT_READ_BUFFER_MESSAGES: usize = 16;
const DEFAULT_ACK_INTERVAL_BYTES: i64 = 0;
const DEFAULT_ACK_INTERVAL_CHUNKS: u32 = 1;
const DEFAULT_MASTER_CONNECTION_POOL_SIZE: usize = 1;
const DEFAULT_WORKER_CONNECTION_POOL_MIN: usize = 1;
const DEFAULT_WORKER_CONNECTION_POOL_MAX: usize = 4;
const DEFAULT_CONNECT_TIMEOUT_MS: u64 = 30_000;
const DEFAULT_REQUEST_TIMEOUT_MS: u64 = 300_000;
const DEFAULT_MASTER_POLLING_TIMEOUT_MS: u64 = 30_000;
const DEFAULT_AUTH_TIMEOUT_MS: u64 = 30_000;
const DEFAULT_CONFIG_RPC_PORT: u16 = 9214;
const DEFAULT_IMPERSONATION_USERNAME: &str = "_HDFS_USER_";
#[allow(dead_code)]
pub const IMPERSONATION_NONE: &str = "_NONE_";
const DEFAULT_MASTER_INQUIRE_MAX_DURATION_MS: u64 = 120_000;
const DEFAULT_MASTER_INQUIRE_INITIAL_SLEEP_MS: u64 = 50;
const DEFAULT_MASTER_INQUIRE_MAX_SLEEP_MS: u64 = 3_000;
const DEFAULT_CONFIG_EXPIRE_MS: u64 = 30_000;
const DEFAULT_METRICS_ENABLED: bool = true;
const DEFAULT_METRICS_HEARTBEAT_INTERVAL_MS: u64 = 10_000;
const DEFAULT_METRICS_HEARTBEAT_TIMEOUT_MS: u64 = 5_000;
const MIN_METRICS_HEARTBEAT_INTERVAL_MS: u64 = 1_000;
const DEFAULT_METRICS_MAX_BATCH_SIZE: usize = 1024;
const DEFAULT_PUSHGATEWAY_ENABLED: bool = false;
const DEFAULT_PUSHGATEWAY_PUSH_INTERVAL_MS: u64 = 10_000;
const DEFAULT_CLIENT_CACHE_PAGE_SIZE: u64 = 1024 * 1024;
const DEFAULT_CLIENT_CACHE_SIZE: u64 = 20 * 1024 * 1024 * 1024;
const DEFAULT_CLIENT_CACHE_ASYNC_WRITE_THREADS: usize = 16;
const DEFAULT_CLIENT_CACHE_DIR: &str = "/tmp/goosefs_cache";
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "UPPERCASE")]
pub enum CacheEvictorType {
Lru,
Lfu,
}
impl Default for CacheEvictorType {
fn default() -> Self {
CacheEvictorType::Lfu
}
}
impl std::str::FromStr for CacheEvictorType {
type Err = String;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s.trim().to_ascii_uppercase().as_str() {
"LRU" => Ok(CacheEvictorType::Lru),
"LFU" => Ok(CacheEvictorType::Lfu),
other => Err(format!("unknown cache evictor type: {other}")),
}
}
}
pub const STORAGE_OPT_MASTER_ADDR: &str = "goosefs_master_addr";
pub const STORAGE_OPT_WRITE_TYPE: &str = "goosefs_write_type";
pub const STORAGE_OPT_BLOCK_SIZE: &str = "goosefs_block_size";
pub const STORAGE_OPT_CHUNK_SIZE: &str = "goosefs_chunk_size";
pub const STORAGE_OPT_AUTH_TYPE: &str = "goosefs_auth_type";
pub const STORAGE_OPT_AUTH_USERNAME: &str = "goosefs_auth_username";
pub const CONF_DIR: &str = "goosefs.conf.dir";
pub const ENV_CONFIG_FILE: &str = "GOOSEFS_CONFIG_FILE";
pub const ENV_CONF_DIR: &str = "GOOSEFS_CONF_DIR";
pub const ENV_HOME: &str = "GOOSEFS_HOME";
pub const ENV_MASTER_ADDR: &str = "GOOSEFS_MASTER_ADDR";
pub const ENV_WRITE_TYPE: &str = "GOOSEFS_WRITE_TYPE";
pub const ENV_BLOCK_SIZE: &str = "GOOSEFS_BLOCK_SIZE";
pub const ENV_CHUNK_SIZE: &str = "GOOSEFS_CHUNK_SIZE";
pub const ENV_AUTH_TYPE: &str = "GOOSEFS_AUTH_TYPE";
pub const ENV_AUTH_USERNAME: &str = "GOOSEFS_AUTH_USERNAME";
pub const ENV_CONFIG_MANAGER_RPC_ADDRESSES: &str = "GOOSEFS_CONFIG_MANAGER_RPC_ADDRESSES";
pub const ENV_CONFIG_RPC_PORT: &str = "GOOSEFS_CONFIG_RPC_PORT";
pub const ENV_TRANSPARENT_ACCELERATION_ENABLED: &str = "GOOSEFS_TRANSPARENT_ACCELERATION_ENABLED";
pub const ENV_TRANSPARENT_ACCELERATION_COSRANGER_ENABLED: &str =
"GOOSEFS_TRANSPARENT_ACCELERATION_COSRANGER_ENABLED";
pub const ENV_AUTHORIZATION_PERMISSION_ENABLED: &str = "GOOSEFS_AUTHORIZATION_PERMISSION_ENABLED";
pub const ENV_LOGIN_IMPERSONATION_USERNAME: &str = "GOOSEFS_LOGIN_IMPERSONATION_USERNAME";
pub const ENV_METRICS_ENABLED: &str = "GOOSEFS_USER_METRICS_COLLECTION_ENABLED";
pub const ENV_METRICS_HEARTBEAT_INTERVAL_MS: &str = "GOOSEFS_USER_METRICS_HEARTBEAT_INTERVAL_MS";
pub const ENV_APP_ID: &str = "GOOSEFS_USER_APP_ID";
pub const ENV_PUSHGATEWAY_ENABLED: &str = "GOOSEFS_METRICS_PUSHGATEWAY_ENABLED";
pub const ENV_PUSHGATEWAY_ENDPOINT: &str = "GOOSEFS_METRICS_PUSHGATEWAY_ENDPOINT";
pub const ENV_PUSHGATEWAY_PUSH_INTERVAL_MS: &str = "GOOSEFS_METRICS_PUSHGATEWAY_PUSH_INTERVAL_MS";
pub const ENV_PUSHGATEWAY_JOB: &str = "GOOSEFS_METRICS_PUSHGATEWAY_JOB";
pub const ENV_PUSHGATEWAY_INSTANCE: &str = "GOOSEFS_METRICS_PUSHGATEWAY_INSTANCE";
pub const ENV_CLIENT_CACHE_ENABLED: &str = "GOOSEFS_USER_CLIENT_CACHE_ENABLED";
pub const ENV_CLIENT_CACHE_PAGE_SIZE: &str = "GOOSEFS_USER_CLIENT_CACHE_PAGE_SIZE";
pub const ENV_CLIENT_CACHE_SIZE: &str = "GOOSEFS_USER_CLIENT_CACHE_SIZE";
pub const ENV_CLIENT_CACHE_DIRS: &str = "GOOSEFS_USER_CLIENT_CACHE_DIRS";
pub const ENV_CLIENT_CACHE_EVICTOR: &str = "GOOSEFS_USER_CLIENT_CACHE_EVICTION_POLICY";
pub const ENV_CLIENT_CACHE_ASYNC_WRITE_ENABLED: &str =
"GOOSEFS_USER_CLIENT_CACHE_ASYNC_WRITE_ENABLED";
pub const ENV_CLIENT_CACHE_ASYNC_WRITE_THREADS: &str =
"GOOSEFS_USER_CLIENT_CACHE_ASYNC_WRITE_THREADS";
pub const ENV_CLIENT_CACHE_QUOTA_ENABLED: &str = "GOOSEFS_USER_CLIENT_CACHE_QUOTA_ENABLED";
pub const ENV_CLIENT_CACHE_TTL_SECS: &str = "GOOSEFS_USER_CLIENT_CACHE_TTL_SECONDS";
pub const ENV_CLIENT_CACHE_SEQUENTIAL_READ_ENABLED: &str =
"GOOSEFS_USER_CLIENT_CACHE_SEQUENTIAL_READ_ENABLED";
pub const ENV_CLIENT_CACHE_URING_ENABLED: &str = "GOOSEFS_USER_CLIENT_CACHE_URING_ENABLED";
pub const ENV_CLIENT_CACHE_URING_QUEUE_DEPTH: &str = "GOOSEFS_USER_CLIENT_CACHE_URING_QUEUE_DEPTH";
pub const ENV_CLIENT_CACHE_URING_THREAD_COUNT: &str =
"GOOSEFS_USER_CLIENT_CACHE_URING_THREAD_COUNT";
pub const ENV_WORKER_CONNECTION_POOL_SIZE: &str = "GOOSEFS_WORKER_CONNECTION_POOL_SIZE";
pub const ENV_FILE_INFO_CACHE_TTL_MS: &str = "GOOSEFS_FILE_INFO_CACHE_TTL_MS";
pub const ENV_FILE_INFO_CACHE_CAPACITY: &str = "GOOSEFS_FILE_INFO_CACHE_CAPACITY";
pub const STORAGE_OPT_CONFIG_MANAGER_RPC_ADDRESSES: &str = "goosefs_config_manager_rpc_addresses";
pub const STORAGE_OPT_CONFIG_RPC_PORT: &str = "goosefs_config_rpc_port";
pub const STORAGE_OPT_TRANSPARENT_ACCELERATION_ENABLED: &str =
"goosefs_transparent_acceleration_enabled";
pub const STORAGE_OPT_TRANSPARENT_ACCELERATION_COSRANGER_ENABLED: &str =
"goosefs_transparent_acceleration_cosranger_enabled";
pub const STORAGE_OPT_AUTHORIZATION_PERMISSION_ENABLED: &str =
"goosefs_authorization_permission_enabled";
pub const STORAGE_OPT_LOGIN_IMPERSONATION_USERNAME: &str = "goosefs_login_impersonation_username";
pub const STORAGE_OPT_CLIENT_CACHE_ENABLED: &str = "goosefs_client_cache_enabled";
pub const STORAGE_OPT_CLIENT_CACHE_PAGE_SIZE: &str = "goosefs_client_cache_page_size";
pub const STORAGE_OPT_CLIENT_CACHE_SIZE: &str = "goosefs_client_cache_size";
pub const STORAGE_OPT_CLIENT_CACHE_DIRS: &str = "goosefs_client_cache_dirs";
pub const STORAGE_OPT_CLIENT_CACHE_EVICTOR: &str = "goosefs_client_cache_eviction_policy";
pub const STORAGE_OPT_CLIENT_CACHE_URING_ENABLED: &str = "goosefs_client_cache_uring_enabled";
pub const STORAGE_OPT_CLIENT_CACHE_URING_QUEUE_DEPTH: &str =
"goosefs_client_cache_uring_queue_depth";
pub const STORAGE_OPT_CLIENT_CACHE_URING_THREAD_COUNT: &str =
"goosefs_client_cache_uring_thread_count";
pub const STORAGE_OPT_WORKER_CONNECTION_POOL_SIZE: &str = "goosefs_worker_connection_pool_size";
pub const STORAGE_OPT_FILE_INFO_CACHE_TTL_MS: &str = "goosefs_file_info_cache_ttl_ms";
pub const STORAGE_OPT_FILE_INFO_CACHE_CAPACITY: &str = "goosefs_file_info_cache_capacity";
pub const ENV_SHORT_CIRCUIT_ENABLED: &str = "GOOSEFS_SHORT_CIRCUIT_ENABLED";
pub const ENV_SHORT_CIRCUIT_CACHE_CAPACITY: &str = "GOOSEFS_SHORT_CIRCUIT_CACHE_CAPACITY";
pub const ENV_SHORT_CIRCUIT_CACHE_TTL_MS: &str = "GOOSEFS_SHORT_CIRCUIT_CACHE_TTL_MS";
pub const ENV_SHORT_CIRCUIT_NEG_CACHE_TTL_MS: &str = "GOOSEFS_SHORT_CIRCUIT_NEG_CACHE_TTL_MS";
pub const ENV_SHORT_CIRCUIT_ADVISE: &str = "GOOSEFS_SHORT_CIRCUIT_ADVISE";
pub const ENV_SHORT_CIRCUIT_PREFETCH_ENABLED: &str = "GOOSEFS_SHORT_CIRCUIT_PREFETCH_ENABLED";
pub const ENV_SHORT_CIRCUIT_PREFETCH_COALESCE_GAP: &str =
"GOOSEFS_SHORT_CIRCUIT_PREFETCH_COALESCE_GAP";
pub const ENV_SHORT_CIRCUIT_PREFETCH_MAX_BATCH: &str = "GOOSEFS_SHORT_CIRCUIT_PREFETCH_MAX_BATCH";
pub const ENV_SHORT_CIRCUIT_MIN_BLOCK_SIZE: &str = "GOOSEFS_SHORT_CIRCUIT_MIN_BLOCK_SIZE";
pub const ENV_SHORT_CIRCUIT_SIGBUS_HANDLER: &str = "GOOSEFS_SHORT_CIRCUIT_SIGBUS_HANDLER";
pub const ENV_SHORT_CIRCUIT_THP: &str = "GOOSEFS_SHORT_CIRCUIT_THP";
pub const STORAGE_OPT_SHORT_CIRCUIT_ENABLED: &str = "goosefs_short_circuit_enabled";
pub const STORAGE_OPT_SHORT_CIRCUIT_CACHE_CAPACITY: &str = "goosefs_short_circuit_cache_capacity";
pub const STORAGE_OPT_SHORT_CIRCUIT_CACHE_TTL_MS: &str = "goosefs_short_circuit_cache_ttl_ms";
pub const STORAGE_OPT_SHORT_CIRCUIT_NEG_CACHE_TTL_MS: &str =
"goosefs_short_circuit_neg_cache_ttl_ms";
pub const STORAGE_OPT_SHORT_CIRCUIT_ADVISE: &str = "goosefs_short_circuit_advise";
pub const STORAGE_OPT_SHORT_CIRCUIT_PREFETCH_ENABLED: &str =
"goosefs_short_circuit_prefetch_enabled";
pub const STORAGE_OPT_SHORT_CIRCUIT_PREFETCH_COALESCE_GAP: &str =
"goosefs_short_circuit_prefetch_coalesce_gap";
pub const STORAGE_OPT_SHORT_CIRCUIT_PREFETCH_MAX_BATCH: &str =
"goosefs_short_circuit_prefetch_max_batch";
pub const STORAGE_OPT_SHORT_CIRCUIT_MIN_BLOCK_SIZE: &str = "goosefs_short_circuit_min_block_size";
pub const STORAGE_OPT_SHORT_CIRCUIT_SIGBUS_HANDLER: &str = "goosefs_short_circuit_sigbus_handler";
pub const STORAGE_OPT_SHORT_CIRCUIT_THP: &str = "goosefs_short_circuit_thp";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum WriteType {
MustCache,
TryCache,
CacheThrough,
Through,
AsyncThrough,
}
impl WriteType {
pub const ALL: &'static [WriteType] = &[
WriteType::MustCache,
WriteType::TryCache,
WriteType::CacheThrough,
WriteType::Through,
WriteType::AsyncThrough,
];
pub fn as_str(&self) -> &'static str {
match self {
WriteType::MustCache => "must_cache",
WriteType::TryCache => "try_cache",
WriteType::CacheThrough => "cache_through",
WriteType::Through => "through",
WriteType::AsyncThrough => "async_through",
}
}
pub fn as_i32(&self) -> i32 {
WritePType::from(*self) as i32
}
}
impl fmt::Display for WriteType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl FromStr for WriteType {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_ascii_lowercase().as_str() {
"must_cache" => Ok(WriteType::MustCache),
"try_cache" => Ok(WriteType::TryCache),
"cache_through" => Ok(WriteType::CacheThrough),
"through" => Ok(WriteType::Through),
"async_through" => Ok(WriteType::AsyncThrough),
_ => Err(format!(
"unknown write type '{}'. Expected one of: {}",
s,
WriteType::ALL
.iter()
.map(|wt| wt.as_str())
.collect::<Vec<_>>()
.join(", ")
)),
}
}
}
impl From<WriteType> for WritePType {
fn from(wt: WriteType) -> Self {
match wt {
WriteType::MustCache => WritePType::MustCache,
WriteType::TryCache => WritePType::TryCache,
WriteType::CacheThrough => WritePType::CacheThrough,
WriteType::Through => WritePType::Through,
WriteType::AsyncThrough => WritePType::AsyncThrough,
}
}
}
impl WriteType {
pub fn try_from_proto(pt: WritePType) -> Result<Self, String> {
match pt {
WritePType::MustCache => Ok(WriteType::MustCache),
WritePType::TryCache => Ok(WriteType::TryCache),
WritePType::CacheThrough => Ok(WriteType::CacheThrough),
WritePType::Through => Ok(WriteType::Through),
WritePType::AsyncThrough => Ok(WriteType::AsyncThrough),
other => Err(format!(
"cannot convert WritePType::{:?} to WriteType",
other
)),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GoosefsConfig {
pub master_addr: String,
#[serde(default)]
pub master_addrs: Vec<String>,
pub block_size: u64,
pub chunk_size: u64,
pub connect_timeout: Duration,
pub request_timeout: Duration,
pub use_vpc_mapping: bool,
pub root: String,
pub write_type: Option<i32>,
#[serde(default = "default_prefetch_window")]
pub prefetch_window: i32,
#[serde(default = "default_read_buffer_messages")]
pub read_buffer_messages: usize,
#[serde(default = "default_ack_interval_bytes")]
pub ack_interval_bytes: i64,
#[serde(default = "default_ack_interval_chunks")]
pub ack_interval_chunks: u32,
#[serde(default = "default_master_connection_pool_size")]
pub master_connection_pool_size: usize,
#[serde(default = "default_worker_connection_pool_size")]
pub worker_connection_pool_size: usize,
#[serde(default = "default_master_inquire_max_duration")]
pub master_inquire_retry_max_duration: Duration,
#[serde(default = "default_master_inquire_initial_sleep")]
pub master_inquire_initial_sleep: Duration,
#[serde(default = "default_master_inquire_max_sleep")]
pub master_inquire_max_sleep: Duration,
#[serde(default = "default_master_polling_timeout")]
pub master_polling_timeout: Duration,
#[serde(default)]
pub auth_type: AuthType,
#[serde(default = "default_auth_username")]
pub auth_username: String,
#[serde(default = "default_auth_timeout")]
pub auth_timeout: Duration,
#[serde(default)]
pub config_manager_rpc_addresses: Vec<String>,
#[serde(default = "default_config_rpc_port")]
pub config_rpc_port: u16,
#[serde(default = "default_transparent_acceleration_enabled")]
pub transparent_acceleration_enabled: bool,
#[serde(default)]
pub transparent_acceleration_cosranger_enabled: bool,
#[serde(default)]
pub authorization_permission_enabled: bool,
#[serde(default = "default_login_impersonation_username")]
pub login_impersonation_username: String,
#[serde(default = "default_metrics_enabled")]
pub metrics_enabled: bool,
#[serde(default = "default_metrics_heartbeat_interval")]
pub metrics_heartbeat_interval: Duration,
#[serde(default = "default_metrics_heartbeat_timeout")]
pub metrics_heartbeat_timeout: Duration,
#[serde(default)]
pub app_id: Option<String>,
#[serde(default = "default_metrics_max_batch_size")]
pub metrics_max_batch_size: usize,
#[serde(default)]
pub pushgateway_enabled: bool,
#[serde(default = "default_pushgateway_endpoint")]
pub pushgateway_endpoint: String,
#[serde(default = "default_pushgateway_push_interval")]
pub pushgateway_push_interval: Duration,
#[serde(default = "default_pushgateway_job")]
pub pushgateway_job: String,
#[serde(default)]
pub pushgateway_instance: Option<String>,
#[serde(default)]
pub client_cache_enabled: bool,
#[serde(default = "default_client_cache_page_size")]
pub client_cache_page_size: u64,
#[serde(default = "default_client_cache_size")]
pub client_cache_size: u64,
#[serde(default = "default_client_cache_dirs")]
pub client_cache_dirs: Vec<String>,
#[serde(default)]
pub client_cache_evictor: CacheEvictorType,
#[serde(default = "default_true_bool")]
pub client_cache_async_write_enabled: bool,
#[serde(default = "default_client_cache_async_write_threads")]
pub client_cache_async_write_threads: usize,
#[serde(default)]
pub client_cache_quota_enabled: bool,
#[serde(default)]
pub client_cache_ttl_secs: u64,
#[serde(default = "default_client_cache_uring_enabled")]
pub client_cache_uring_enabled: bool,
#[serde(default = "default_client_cache_uring_queue_depth")]
pub client_cache_uring_queue_depth: usize,
#[serde(default = "default_client_cache_uring_thread_count")]
pub client_cache_uring_thread_count: usize,
#[serde(default)]
pub client_cache_sequential_read_enabled: bool,
#[serde(default = "default_file_info_cache_ttl")]
pub file_info_cache_ttl: Duration,
#[serde(default = "default_file_info_cache_capacity")]
pub file_info_cache_capacity: usize,
#[serde(default)]
pub range_coalesce_enabled: bool,
#[serde(default = "default_range_coalesce_gap_bytes")]
pub range_coalesce_gap_bytes: u64,
#[serde(default = "default_range_coalesce_max_bytes")]
pub range_coalesce_max_bytes: u64,
#[serde(default = "default_false_bool")]
pub short_circuit_enabled: bool,
#[serde(default = "default_short_circuit_cache_capacity")]
pub short_circuit_cache_capacity: usize,
#[serde(default = "default_short_circuit_cache_ttl")]
pub short_circuit_cache_ttl: Duration,
#[serde(default = "default_short_circuit_neg_cache_ttl")]
pub short_circuit_neg_cache_ttl: Duration,
#[serde(default = "default_short_circuit_advise")]
pub short_circuit_advise: String,
#[serde(default = "default_true_bool")]
pub short_circuit_prefetch_enabled: bool,
#[serde(default = "default_short_circuit_prefetch_coalesce_gap")]
pub short_circuit_prefetch_coalesce_gap: usize,
#[serde(default = "default_short_circuit_prefetch_max_batch")]
pub short_circuit_prefetch_max_batch: usize,
#[serde(default)]
pub short_circuit_min_block_size: i64,
#[serde(default = "default_true_bool")]
pub short_circuit_sigbus_handler: bool,
#[serde(default)]
pub short_circuit_thp: bool,
}
fn default_master_inquire_max_duration() -> Duration {
Duration::from_millis(DEFAULT_MASTER_INQUIRE_MAX_DURATION_MS)
}
fn default_master_inquire_initial_sleep() -> Duration {
Duration::from_millis(DEFAULT_MASTER_INQUIRE_INITIAL_SLEEP_MS)
}
fn default_master_inquire_max_sleep() -> Duration {
Duration::from_millis(DEFAULT_MASTER_INQUIRE_MAX_SLEEP_MS)
}
fn default_master_polling_timeout() -> Duration {
Duration::from_millis(DEFAULT_MASTER_POLLING_TIMEOUT_MS)
}
fn default_auth_username() -> String {
std::env::var("USER")
.or_else(|_| std::env::var("USERNAME"))
.unwrap_or_else(|_| "unknown".to_string())
}
fn default_auth_timeout() -> Duration {
Duration::from_millis(DEFAULT_AUTH_TIMEOUT_MS)
}
fn default_config_rpc_port() -> u16 {
DEFAULT_CONFIG_RPC_PORT
}
fn default_transparent_acceleration_enabled() -> bool {
true
}
fn default_login_impersonation_username() -> String {
DEFAULT_IMPERSONATION_USERNAME.to_string()
}
fn default_metrics_enabled() -> bool {
DEFAULT_METRICS_ENABLED
}
fn default_metrics_heartbeat_interval() -> Duration {
Duration::from_millis(DEFAULT_METRICS_HEARTBEAT_INTERVAL_MS)
}
fn default_metrics_heartbeat_timeout() -> Duration {
Duration::from_millis(DEFAULT_METRICS_HEARTBEAT_TIMEOUT_MS)
}
fn default_metrics_max_batch_size() -> usize {
DEFAULT_METRICS_MAX_BATCH_SIZE
}
fn default_pushgateway_endpoint() -> String {
"http://127.0.0.1:9091".to_string()
}
fn default_pushgateway_push_interval() -> Duration {
Duration::from_millis(DEFAULT_PUSHGATEWAY_PUSH_INTERVAL_MS)
}
fn default_pushgateway_job() -> String {
"goosefs_client".to_string()
}
fn default_client_cache_page_size() -> u64 {
DEFAULT_CLIENT_CACHE_PAGE_SIZE
}
fn default_client_cache_size() -> u64 {
DEFAULT_CLIENT_CACHE_SIZE
}
fn default_client_cache_dirs() -> Vec<String> {
vec![DEFAULT_CLIENT_CACHE_DIR.to_string()]
}
fn default_client_cache_async_write_threads() -> usize {
DEFAULT_CLIENT_CACHE_ASYNC_WRITE_THREADS
}
fn default_client_cache_uring_enabled() -> bool {
cfg!(target_os = "linux")
}
fn default_client_cache_uring_queue_depth() -> usize {
32768
}
fn default_client_cache_uring_thread_count() -> usize {
2
}
fn default_true_bool() -> bool {
true
}
fn default_false_bool() -> bool {
false
}
fn default_file_info_cache_ttl() -> Duration {
Duration::from_secs(30)
}
fn default_file_info_cache_capacity() -> usize {
16384
}
fn default_range_coalesce_gap_bytes() -> u64 {
64 * 1024 }
fn default_range_coalesce_max_bytes() -> u64 {
4 * 1024 * 1024 }
fn default_short_circuit_cache_capacity() -> usize {
64
}
fn default_short_circuit_cache_ttl() -> Duration {
Duration::from_secs(30)
}
fn default_short_circuit_neg_cache_ttl() -> Duration {
Duration::from_secs(5)
}
fn default_short_circuit_advise() -> String {
"random".to_string()
}
fn default_short_circuit_prefetch_coalesce_gap() -> usize {
64 * 1024
}
fn default_short_circuit_prefetch_max_batch() -> usize {
1024
}
fn default_prefetch_window() -> i32 {
DEFAULT_PREFETCH_WINDOW
}
fn default_read_buffer_messages() -> usize {
DEFAULT_READ_BUFFER_MESSAGES
}
fn default_ack_interval_bytes() -> i64 {
DEFAULT_ACK_INTERVAL_BYTES
}
fn default_ack_interval_chunks() -> u32 {
DEFAULT_ACK_INTERVAL_CHUNKS
}
fn default_master_connection_pool_size() -> usize {
DEFAULT_MASTER_CONNECTION_POOL_SIZE
}
fn default_worker_connection_pool_size() -> usize {
let cores = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(DEFAULT_WORKER_CONNECTION_POOL_MIN);
cores
.max(DEFAULT_WORKER_CONNECTION_POOL_MIN)
.min(DEFAULT_WORKER_CONNECTION_POOL_MAX)
}
impl Default for GoosefsConfig {
fn default() -> Self {
Self {
master_addr: format!("127.0.0.1:{}", DEFAULT_MASTER_PORT),
master_addrs: Vec::new(),
block_size: DEFAULT_BLOCK_SIZE,
chunk_size: DEFAULT_CHUNK_SIZE,
connect_timeout: Duration::from_millis(DEFAULT_CONNECT_TIMEOUT_MS),
request_timeout: Duration::from_millis(DEFAULT_REQUEST_TIMEOUT_MS),
use_vpc_mapping: false,
root: String::new(),
write_type: None,
prefetch_window: default_prefetch_window(),
read_buffer_messages: default_read_buffer_messages(),
ack_interval_bytes: default_ack_interval_bytes(),
ack_interval_chunks: default_ack_interval_chunks(),
master_connection_pool_size: default_master_connection_pool_size(),
worker_connection_pool_size: default_worker_connection_pool_size(),
master_inquire_retry_max_duration: default_master_inquire_max_duration(),
master_inquire_initial_sleep: default_master_inquire_initial_sleep(),
master_inquire_max_sleep: default_master_inquire_max_sleep(),
master_polling_timeout: default_master_polling_timeout(),
auth_type: AuthType::default(),
auth_username: default_auth_username(),
auth_timeout: default_auth_timeout(),
config_manager_rpc_addresses: Vec::new(),
config_rpc_port: default_config_rpc_port(),
transparent_acceleration_enabled: default_transparent_acceleration_enabled(),
transparent_acceleration_cosranger_enabled: false,
authorization_permission_enabled: false,
login_impersonation_username: default_login_impersonation_username(),
metrics_enabled: default_metrics_enabled(),
metrics_heartbeat_interval: default_metrics_heartbeat_interval(),
metrics_heartbeat_timeout: default_metrics_heartbeat_timeout(),
app_id: None,
metrics_max_batch_size: default_metrics_max_batch_size(),
pushgateway_enabled: DEFAULT_PUSHGATEWAY_ENABLED,
pushgateway_endpoint: default_pushgateway_endpoint(),
pushgateway_push_interval: default_pushgateway_push_interval(),
pushgateway_job: default_pushgateway_job(),
pushgateway_instance: None,
client_cache_enabled: false,
client_cache_page_size: default_client_cache_page_size(),
client_cache_size: default_client_cache_size(),
client_cache_dirs: default_client_cache_dirs(),
client_cache_evictor: CacheEvictorType::default(),
client_cache_async_write_enabled: default_true_bool(),
client_cache_async_write_threads: default_client_cache_async_write_threads(),
client_cache_quota_enabled: false,
client_cache_ttl_secs: 0,
client_cache_uring_enabled: default_client_cache_uring_enabled(),
client_cache_uring_queue_depth: default_client_cache_uring_queue_depth(),
client_cache_uring_thread_count: default_client_cache_uring_thread_count(),
client_cache_sequential_read_enabled: false,
file_info_cache_ttl: default_file_info_cache_ttl(),
file_info_cache_capacity: default_file_info_cache_capacity(),
range_coalesce_enabled: false,
range_coalesce_gap_bytes: default_range_coalesce_gap_bytes(),
range_coalesce_max_bytes: default_range_coalesce_max_bytes(),
short_circuit_enabled: false,
short_circuit_cache_capacity: default_short_circuit_cache_capacity(),
short_circuit_cache_ttl: default_short_circuit_cache_ttl(),
short_circuit_neg_cache_ttl: default_short_circuit_neg_cache_ttl(),
short_circuit_advise: default_short_circuit_advise(),
short_circuit_prefetch_enabled: true,
short_circuit_prefetch_coalesce_gap: default_short_circuit_prefetch_coalesce_gap(),
short_circuit_prefetch_max_batch: default_short_circuit_prefetch_max_batch(),
short_circuit_min_block_size: 0,
short_circuit_sigbus_handler: true,
short_circuit_thp: false,
}
}
}
impl GoosefsConfig {
pub fn new(master_addr: impl Into<String>) -> Self {
Self {
master_addr: master_addr.into(),
..Default::default()
}
}
pub fn new_ha(addrs: Vec<String>) -> Self {
assert!(!addrs.is_empty(), "master addresses must not be empty");
Self {
master_addr: addrs[0].clone(),
master_addrs: addrs,
..Default::default()
}
}
pub fn from_addresses(addrs: Vec<String>) -> Self {
assert!(!addrs.is_empty(), "master addresses must not be empty");
if addrs.len() == 1 {
Self::new(&addrs[0])
} else {
Self::new_ha(addrs)
}
}
pub fn from_uri(uri: &str) -> Result<Self, UriParseError> {
let (addrs, root) = parse_gfs_uri(uri)?;
let mut cfg = Self::from_addresses(addrs);
if !root.is_empty() {
cfg.root = root;
}
Ok(cfg)
}
pub fn master_addresses(&self) -> Vec<String> {
if self.master_addrs.is_empty() {
vec![self.master_addr.clone()]
} else {
self.master_addrs.clone()
}
}
pub fn is_multi_master(&self) -> bool {
self.master_addrs.len() > 1
}
pub fn full_path(&self, path: &str) -> String {
if self.root.is_empty() {
path.to_string()
} else {
let root = self.root.trim_end_matches('/');
let path = path.trim_start_matches('/');
format!("{}/{}", root, path)
}
}
pub fn master_endpoint(&self) -> String {
format!("http://{}", self.master_addr)
}
pub fn worker_endpoint(&self, host: &str, rpc_port: i32) -> String {
if self.use_vpc_mapping {
format!("http://{}:{}", host, rpc_port)
} else {
format!("http://{}:{}", host, rpc_port)
}
}
pub fn with_auth_type(mut self, auth_type: AuthType) -> Self {
self.auth_type = auth_type;
self
}
pub fn with_auth_type_str(self, auth_type: &str) -> Result<Self, String> {
let at: AuthType = auth_type.parse()?;
Ok(self.with_auth_type(at))
}
pub fn with_auth_username(mut self, username: impl Into<String>) -> Self {
self.auth_username = username.into();
self
}
pub fn with_write_type(mut self, wt: WritePType) -> Self {
self.write_type = Some(wt as i32);
self
}
pub fn with_write_type_enum(mut self, wt: WriteType) -> Self {
self.write_type = Some(wt.as_i32());
self
}
pub fn with_write_type_str(self, wt: &str) -> Result<Self, String> {
let write_type: WriteType = wt.parse()?;
Ok(self.with_write_type_enum(write_type))
}
pub fn with_prefetch_window(mut self, window: i32) -> Self {
self.prefetch_window = window;
self
}
pub fn with_ack_interval_bytes(mut self, bytes: i64) -> Self {
self.ack_interval_bytes = bytes;
self
}
pub fn with_master_connection_pool_size(mut self, size: usize) -> Self {
self.master_connection_pool_size = size.max(1);
self
}
pub fn with_worker_connection_pool_size(mut self, size: usize) -> Self {
self.worker_connection_pool_size = size.max(1);
self
}
pub fn with_file_info_cache_ttl(mut self, ttl: Duration) -> Self {
self.file_info_cache_ttl = ttl;
self
}
pub fn with_file_info_cache_capacity(mut self, capacity: usize) -> Self {
self.file_info_cache_capacity = capacity.max(1);
self
}
pub fn with_short_circuit_enabled(mut self, enabled: bool) -> Self {
self.short_circuit_enabled = enabled;
self
}
pub fn with_short_circuit_cache_capacity(mut self, capacity: usize) -> Self {
self.short_circuit_cache_capacity = capacity;
self
}
pub fn with_short_circuit_cache_ttl(mut self, ttl: Duration) -> Self {
self.short_circuit_cache_ttl = ttl;
self
}
pub fn with_short_circuit_neg_cache_ttl(mut self, ttl: Duration) -> Self {
self.short_circuit_neg_cache_ttl = ttl;
self
}
pub fn with_short_circuit_advise(mut self, advise: impl Into<String>) -> Self {
self.short_circuit_advise = advise.into();
self
}
pub fn with_short_circuit_prefetch_enabled(mut self, enabled: bool) -> Self {
self.short_circuit_prefetch_enabled = enabled;
self
}
pub fn with_short_circuit_prefetch_coalesce_gap(mut self, gap: usize) -> Self {
self.short_circuit_prefetch_coalesce_gap = gap;
self
}
pub fn with_short_circuit_prefetch_max_batch(mut self, batch: usize) -> Self {
self.short_circuit_prefetch_max_batch = batch;
self
}
pub fn with_short_circuit_min_block_size(mut self, size: i64) -> Self {
self.short_circuit_min_block_size = size;
self
}
pub fn with_short_circuit_sigbus_handler(mut self, enabled: bool) -> Self {
self.short_circuit_sigbus_handler = enabled;
self
}
pub fn with_short_circuit_thp(mut self, enabled: bool) -> Self {
self.short_circuit_thp = enabled;
self
}
pub fn with_range_coalesce_enabled(mut self, enabled: bool) -> Self {
self.range_coalesce_enabled = enabled;
self
}
pub fn with_range_coalesce_gap_bytes(mut self, gap: u64) -> Self {
self.range_coalesce_gap_bytes = gap;
self
}
pub fn with_range_coalesce_max_bytes(mut self, max_bytes: u64) -> Self {
self.range_coalesce_max_bytes = max_bytes.max(1);
self
}
pub fn get_write_type(&self) -> Option<WritePType> {
self.write_type.and_then(|v| match v {
0 => Some(WritePType::UnspecifiedWriteType),
1 => Some(WritePType::MustCache),
2 => Some(WritePType::TryCache),
3 => Some(WritePType::CacheThrough),
4 => Some(WritePType::Through),
5 => Some(WritePType::AsyncThrough),
6 => Some(WritePType::None),
_ => Option::None,
})
}
pub fn with_metrics_enabled(mut self, enabled: bool) -> Self {
self.metrics_enabled = enabled;
self
}
pub fn with_metrics_heartbeat_interval(mut self, interval: Duration) -> Self {
assert!(
interval >= Duration::from_secs(1),
"metrics_heartbeat_interval must be >= 1 s"
);
self.metrics_heartbeat_interval = interval;
self
}
pub fn with_metrics_heartbeat_timeout(mut self, timeout: Duration) -> Self {
assert!(
timeout >= Duration::from_secs(1),
"metrics_heartbeat_timeout must be >= 1 s"
);
self.metrics_heartbeat_timeout = timeout;
self
}
pub fn with_app_id(mut self, app_id: impl Into<String>) -> Self {
self.app_id = Some(app_id.into());
self
}
pub fn with_pushgateway_enabled(mut self, enabled: bool) -> Self {
self.pushgateway_enabled = enabled;
self
}
pub fn with_pushgateway_endpoint(mut self, endpoint: impl Into<String>) -> Self {
self.pushgateway_endpoint = endpoint.into();
self
}
pub fn with_pushgateway_push_interval(mut self, interval: Duration) -> Self {
assert!(
interval >= Duration::from_secs(1),
"pushgateway_push_interval must be >= 1 s"
);
self.pushgateway_push_interval = interval;
self
}
pub fn with_pushgateway_job(mut self, job: impl Into<String>) -> Self {
self.pushgateway_job = job.into();
self
}
pub fn with_pushgateway_instance(mut self, instance: impl Into<String>) -> Self {
self.pushgateway_instance = Some(instance.into());
self
}
pub fn from_env() -> Self {
Self::default().apply_env()
}
pub fn apply_env(mut self) -> Self {
use std::env;
if let Ok(addr) = env::var(ENV_MASTER_ADDR) {
if addr.trim_start().starts_with("gfs://") {
match parse_gfs_uri(addr.trim()) {
Ok((addrs, root)) => {
self.master_addr = addrs[0].clone();
self.master_addrs = if addrs.len() > 1 { addrs } else { Vec::new() };
if !root.is_empty() {
self.root = root;
}
}
Err(e) => {
tracing::warn!(
"ignoring malformed GOOSEFS_MASTER_ADDR URI {:?}: {}; \
existing master address configuration is retained",
addr,
e
);
}
}
} else {
let addrs: Vec<String> = addr
.split(',')
.map(str::trim)
.filter(|s| !s.is_empty())
.map(String::from)
.collect();
if !addrs.is_empty() {
self.master_addr = addrs[0].clone();
if addrs.len() > 1 {
self.master_addrs = addrs;
} else {
self.master_addrs = Vec::new();
}
}
}
}
if let Ok(wt_str) = env::var(ENV_WRITE_TYPE) {
if let Ok(wt) = wt_str.parse::<WriteType>() {
self.write_type = Some(wt.as_i32());
}
}
if let Ok(bs_str) = env::var(ENV_BLOCK_SIZE) {
if let Ok(bs) = bs_str.parse::<u64>() {
self.block_size = bs;
}
}
if let Ok(cs_str) = env::var(ENV_CHUNK_SIZE) {
if let Ok(cs) = cs_str.parse::<u64>() {
self.chunk_size = cs;
}
}
if let Ok(at_str) = env::var(ENV_AUTH_TYPE) {
if let Ok(at) = at_str.parse::<crate::auth::AuthType>() {
self.auth_type = at;
}
}
if let Ok(user) = env::var(ENV_AUTH_USERNAME) {
if !user.is_empty() {
self.auth_username = user;
}
}
if let Ok(addrs_str) = env::var(ENV_CONFIG_MANAGER_RPC_ADDRESSES) {
let addrs: Vec<String> = addrs_str
.split(',')
.map(str::trim)
.filter(|s| !s.is_empty())
.map(String::from)
.collect();
if !addrs.is_empty() {
self.config_manager_rpc_addresses = addrs;
}
}
if let Ok(port_str) = env::var(ENV_CONFIG_RPC_PORT) {
if let Ok(port) = port_str.parse::<u16>() {
self.config_rpc_port = port;
}
}
if let Ok(val) = env::var(ENV_TRANSPARENT_ACCELERATION_ENABLED) {
if let Ok(b) = val.parse::<bool>() {
self.transparent_acceleration_enabled = b;
}
}
if let Ok(val) = env::var(ENV_TRANSPARENT_ACCELERATION_COSRANGER_ENABLED) {
if let Ok(b) = val.parse::<bool>() {
self.transparent_acceleration_cosranger_enabled = b;
}
}
if let Ok(val) = env::var(ENV_AUTHORIZATION_PERMISSION_ENABLED) {
if let Ok(b) = val.parse::<bool>() {
self.authorization_permission_enabled = b;
}
}
if let Ok(user) = env::var(ENV_LOGIN_IMPERSONATION_USERNAME) {
if !user.is_empty() {
self.login_impersonation_username = user;
}
}
if let Ok(val) = env::var(ENV_METRICS_ENABLED) {
if let Ok(b) = val.to_lowercase().parse::<bool>() {
self.metrics_enabled = b;
}
}
if let Ok(val) = env::var(ENV_METRICS_ENABLED) {
if let Ok(b) = val.to_lowercase().parse::<bool>() {
self.metrics_enabled = b;
}
}
if let Ok(ms_str) = env::var(ENV_METRICS_HEARTBEAT_INTERVAL_MS) {
if let Ok(ms) = ms_str.parse::<u64>() {
if ms >= MIN_METRICS_HEARTBEAT_INTERVAL_MS {
self.metrics_heartbeat_interval = Duration::from_millis(ms);
}
}
}
if let Ok(id) = env::var(ENV_APP_ID) {
if !id.is_empty() {
self.app_id = Some(id);
}
}
if let Ok(val) = env::var(ENV_PUSHGATEWAY_ENABLED) {
if let Ok(b) = val.to_lowercase().parse::<bool>() {
self.pushgateway_enabled = b;
}
}
if let Ok(val) = env::var(ENV_PUSHGATEWAY_ENDPOINT) {
if !val.is_empty() {
self.pushgateway_endpoint = val;
}
}
if let Ok(ms_str) = env::var(ENV_PUSHGATEWAY_PUSH_INTERVAL_MS) {
if let Ok(ms) = ms_str.parse::<u64>() {
if ms >= MIN_METRICS_HEARTBEAT_INTERVAL_MS {
self.pushgateway_push_interval = Duration::from_millis(ms);
}
}
}
if let Ok(val) = env::var(ENV_PUSHGATEWAY_JOB) {
if !val.is_empty() {
self.pushgateway_job = val;
}
}
if let Ok(val) = env::var(ENV_PUSHGATEWAY_INSTANCE) {
if !val.is_empty() {
self.pushgateway_instance = Some(val);
}
}
if let Ok(val) = env::var(ENV_CLIENT_CACHE_ENABLED) {
if let Ok(b) = val.to_lowercase().parse::<bool>() {
self.client_cache_enabled = b;
}
}
if let Ok(val) = env::var(ENV_CLIENT_CACHE_PAGE_SIZE) {
if let Ok(n) = val.parse::<u64>() {
if n > 0 {
self.client_cache_page_size = n;
}
}
}
if let Ok(val) = env::var(ENV_CLIENT_CACHE_SIZE) {
if let Ok(n) = val.parse::<u64>() {
self.client_cache_size = n;
}
}
if let Ok(val) = env::var(ENV_CLIENT_CACHE_DIRS) {
let dirs: Vec<String> = val
.split(',')
.map(str::trim)
.filter(|s| !s.is_empty())
.map(String::from)
.collect();
if !dirs.is_empty() {
self.client_cache_dirs = dirs;
}
}
if let Ok(val) = env::var(ENV_CLIENT_CACHE_EVICTOR) {
if let Ok(e) = val.parse::<CacheEvictorType>() {
self.client_cache_evictor = e;
}
}
if let Ok(val) = env::var(ENV_CLIENT_CACHE_ASYNC_WRITE_ENABLED) {
if let Ok(b) = val.to_lowercase().parse::<bool>() {
self.client_cache_async_write_enabled = b;
}
}
if let Ok(val) = env::var(ENV_CLIENT_CACHE_ASYNC_WRITE_THREADS) {
if let Ok(n) = val.parse::<usize>() {
if n > 0 {
self.client_cache_async_write_threads = n;
}
}
}
if let Ok(val) = env::var(ENV_CLIENT_CACHE_QUOTA_ENABLED) {
if let Ok(b) = val.to_lowercase().parse::<bool>() {
self.client_cache_quota_enabled = b;
}
}
if let Ok(val) = env::var(ENV_CLIENT_CACHE_TTL_SECS) {
if let Ok(n) = val.parse::<u64>() {
self.client_cache_ttl_secs = n;
}
}
if let Ok(val) = env::var(ENV_CLIENT_CACHE_SEQUENTIAL_READ_ENABLED) {
if let Ok(b) = val.to_lowercase().parse::<bool>() {
self.client_cache_sequential_read_enabled = b;
}
}
if let Ok(val) = env::var(ENV_CLIENT_CACHE_URING_ENABLED) {
if let Ok(b) = val.to_lowercase().parse::<bool>() {
self.client_cache_uring_enabled = b;
}
}
if let Ok(val) = env::var(ENV_CLIENT_CACHE_URING_QUEUE_DEPTH) {
if let Ok(n) = val.parse::<usize>() {
if n > 0 {
self.client_cache_uring_queue_depth = n;
}
}
}
if let Ok(val) = env::var(ENV_CLIENT_CACHE_URING_THREAD_COUNT) {
if let Ok(n) = val.parse::<usize>() {
if n > 0 {
self.client_cache_uring_thread_count = n;
}
}
}
if let Ok(val) = env::var(ENV_WORKER_CONNECTION_POOL_SIZE) {
if let Ok(n) = val.parse::<usize>() {
self.worker_connection_pool_size = n.max(1);
}
}
if let Ok(val) = env::var(ENV_FILE_INFO_CACHE_TTL_MS) {
if let Ok(ms) = val.parse::<u64>() {
self.file_info_cache_ttl = Duration::from_millis(ms);
}
}
if let Ok(val) = env::var(ENV_FILE_INFO_CACHE_CAPACITY) {
if let Ok(n) = val.parse::<usize>() {
self.file_info_cache_capacity = n.max(1);
}
}
if let Ok(val) = env::var(ENV_SHORT_CIRCUIT_ENABLED) {
if let Ok(b) = val.to_lowercase().parse::<bool>() {
self.short_circuit_enabled = b;
}
}
if let Ok(val) = env::var(ENV_SHORT_CIRCUIT_CACHE_CAPACITY) {
if let Ok(n) = val.parse::<usize>() {
self.short_circuit_cache_capacity = n;
}
}
if let Ok(val) = env::var(ENV_SHORT_CIRCUIT_CACHE_TTL_MS) {
if let Ok(ms) = val.parse::<u64>() {
self.short_circuit_cache_ttl = Duration::from_millis(ms);
}
}
if let Ok(val) = env::var(ENV_SHORT_CIRCUIT_NEG_CACHE_TTL_MS) {
if let Ok(ms) = val.parse::<u64>() {
self.short_circuit_neg_cache_ttl = Duration::from_millis(ms);
}
}
if let Ok(val) = env::var(ENV_SHORT_CIRCUIT_ADVISE) {
if !val.is_empty() {
self.short_circuit_advise = val;
}
}
if let Ok(val) = env::var(ENV_SHORT_CIRCUIT_PREFETCH_ENABLED) {
if let Ok(b) = val.to_lowercase().parse::<bool>() {
self.short_circuit_prefetch_enabled = b;
}
}
if let Ok(val) = env::var(ENV_SHORT_CIRCUIT_PREFETCH_COALESCE_GAP) {
if let Ok(n) = val.parse::<usize>() {
self.short_circuit_prefetch_coalesce_gap = n;
}
}
if let Ok(val) = env::var(ENV_SHORT_CIRCUIT_PREFETCH_MAX_BATCH) {
if let Ok(n) = val.parse::<usize>() {
self.short_circuit_prefetch_max_batch = n;
}
}
if let Ok(val) = env::var(ENV_SHORT_CIRCUIT_MIN_BLOCK_SIZE) {
if let Ok(n) = val.parse::<i64>() {
self.short_circuit_min_block_size = n;
}
}
if let Ok(val) = env::var(ENV_SHORT_CIRCUIT_SIGBUS_HANDLER) {
if let Ok(b) = val.to_lowercase().parse::<bool>() {
self.short_circuit_sigbus_handler = b;
}
}
if let Ok(val) = env::var(ENV_SHORT_CIRCUIT_THP) {
if let Ok(b) = val.to_lowercase().parse::<bool>() {
self.short_circuit_thp = b;
}
}
self
}
pub fn from_properties(path: impl AsRef<std::path::Path>) -> Result<Self, ConfigLoadError> {
let path = path.as_ref();
let content = std::fs::read_to_string(path).map_err(|e| ConfigLoadError::IoError {
path: path.display().to_string(),
source: e.to_string(),
})?;
Ok(Self::from_properties_str(&content))
}
pub fn from_properties_str(content: &str) -> Self {
let props = PropertiesMap::parse(content);
props.into_goosefs_config()
}
pub fn from_properties_auto() -> Result<Self, ConfigLoadError> {
let base = if let Some(path) = discover_config_file() {
Self::from_properties(&path)?
} else {
Self::default()
};
Ok(base.apply_env())
}
pub fn validate(&self) -> Result<(), String> {
if self.master_addr.is_empty() && self.master_addrs.is_empty() {
return Err(
"at least one master address must be provided (master_addr or master_addrs)"
.to_string(),
);
}
if !self.master_addrs.is_empty() && self.master_addrs.iter().any(|a| a.is_empty()) {
return Err("master_addrs contains an empty address".to_string());
}
if self.block_size == 0 {
return Err("block_size must be > 0".to_string());
}
if self.chunk_size == 0 {
return Err("chunk_size must be > 0".to_string());
}
if self.chunk_size > self.block_size {
return Err("chunk_size must be <= block_size".to_string());
}
if self.metrics_heartbeat_interval
< Duration::from_millis(MIN_METRICS_HEARTBEAT_INTERVAL_MS)
{
return Err(format!(
"metrics_heartbeat_interval must be >= {}ms (got {}ms)",
MIN_METRICS_HEARTBEAT_INTERVAL_MS,
self.metrics_heartbeat_interval.as_millis()
));
}
if self.metrics_heartbeat_timeout < Duration::from_secs(1) {
return Err(format!(
"metrics_heartbeat_timeout must be >= 1000ms (got {}ms)",
self.metrics_heartbeat_timeout.as_millis()
));
}
if self.metrics_heartbeat_timeout >= self.metrics_heartbeat_interval {
return Err(format!(
"metrics_heartbeat_timeout ({}ms) must be < metrics_heartbeat_interval ({}ms) \
to prevent in-flight RPCs from piling up across ticks",
self.metrics_heartbeat_timeout.as_millis(),
self.metrics_heartbeat_interval.as_millis()
));
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TransparentAccelerationSwitch {
pub enabled: bool,
pub cosranger_enabled: bool,
}
pub struct ConfigRefresher {
last_load_time: Mutex<Option<Instant>>,
expire_duration: Duration,
transparent_acceleration_enabled: AtomicBool,
cosranger_enabled: AtomicBool,
}
impl fmt::Debug for ConfigRefresher {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ConfigRefresher")
.field("expire_duration", &self.expire_duration)
.field(
"transparent_acceleration_enabled",
&self
.transparent_acceleration_enabled
.load(Ordering::Relaxed),
)
.field(
"cosranger_enabled",
&self.cosranger_enabled.load(Ordering::Relaxed),
)
.finish()
}
}
impl ConfigRefresher {
pub fn new() -> Self {
Self::with_expire(Duration::from_millis(DEFAULT_CONFIG_EXPIRE_MS))
}
pub fn with_expire(expire_duration: Duration) -> Self {
let initial = GoosefsConfig::from_properties_auto().unwrap_or_default();
Self {
last_load_time: Mutex::new(Some(Instant::now())),
expire_duration,
transparent_acceleration_enabled: AtomicBool::new(
initial.transparent_acceleration_enabled,
),
cosranger_enabled: AtomicBool::new(initial.transparent_acceleration_cosranger_enabled),
}
}
pub fn from_config(config: &GoosefsConfig) -> Self {
Self {
last_load_time: Mutex::new(Some(Instant::now())),
expire_duration: Duration::from_millis(DEFAULT_CONFIG_EXPIRE_MS),
transparent_acceleration_enabled: AtomicBool::new(
config.transparent_acceleration_enabled,
),
cosranger_enabled: AtomicBool::new(config.transparent_acceleration_cosranger_enabled),
}
}
pub fn refresh_transparent_acceleration_switch(&self) -> TransparentAccelerationSwitch {
self.load_if_expire();
TransparentAccelerationSwitch {
enabled: self
.transparent_acceleration_enabled
.load(Ordering::Relaxed),
cosranger_enabled: self.cosranger_enabled.load(Ordering::Relaxed),
}
}
pub fn current_switch(&self) -> TransparentAccelerationSwitch {
TransparentAccelerationSwitch {
enabled: self
.transparent_acceleration_enabled
.load(Ordering::Relaxed),
cosranger_enabled: self.cosranger_enabled.load(Ordering::Relaxed),
}
}
fn load_if_expire(&self) {
let now = Instant::now();
let needs_reload = {
let guard = self.last_load_time.lock().unwrap();
match *guard {
None => true,
Some(t) => now.duration_since(t) >= self.expire_duration,
}
};
if needs_reload {
let mut guard = self.last_load_time.lock().unwrap();
let still_needs = match *guard {
None => true,
Some(t) => now.duration_since(t) >= self.expire_duration,
};
if still_needs {
self.reload_properties();
*guard = Some(Instant::now());
}
}
}
fn reload_properties(&self) {
match GoosefsConfig::from_properties_auto() {
Ok(cfg) => {
self.transparent_acceleration_enabled
.store(cfg.transparent_acceleration_enabled, Ordering::Relaxed);
self.cosranger_enabled.store(
cfg.transparent_acceleration_cosranger_enabled,
Ordering::Relaxed,
);
tracing::debug!(
transparent_acceleration_enabled = cfg.transparent_acceleration_enabled,
cosranger_enabled = cfg.transparent_acceleration_cosranger_enabled,
"config refreshed from properties file"
);
}
Err(e) => {
tracing::warn!("failed to reload config: {}, keeping previous values", e);
}
}
}
}
impl Default for ConfigRefresher {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = GoosefsConfig::default();
assert_eq!(config.master_addr, "127.0.0.1:9200");
assert!(config.master_addrs.is_empty());
assert_eq!(config.block_size, 64 * 1024 * 1024);
assert_eq!(config.chunk_size, 1024 * 1024);
assert!(!config.is_multi_master());
assert!(config.validate().is_ok());
}
#[test]
fn test_worker_connection_pool_size_default_is_capped_at_max() {
let config = GoosefsConfig::default();
assert!(
config.worker_connection_pool_size >= DEFAULT_WORKER_CONNECTION_POOL_MIN,
"default must be >= {} (single-channel legacy floor), got {}",
DEFAULT_WORKER_CONNECTION_POOL_MIN,
config.worker_connection_pool_size,
);
assert!(
config.worker_connection_pool_size <= DEFAULT_WORKER_CONNECTION_POOL_MAX,
"default must be <= {} (B3 cap), got {}",
DEFAULT_WORKER_CONNECTION_POOL_MAX,
config.worker_connection_pool_size,
);
}
#[test]
fn test_worker_connection_pool_size_explicit_override() {
let cfg = GoosefsConfig::new("127.0.0.1:9200").with_worker_connection_pool_size(8);
assert_eq!(cfg.worker_connection_pool_size, 8);
let clamped = GoosefsConfig::new("127.0.0.1:9200").with_worker_connection_pool_size(0);
assert_eq!(
clamped.worker_connection_pool_size, 1,
"0 must be clamped to 1 (legacy semantics preserved)"
);
}
#[test]
fn test_new_ha_config() {
let config = GoosefsConfig::new_ha(vec![
"10.0.0.1:9200".to_string(),
"10.0.0.2:9200".to_string(),
"10.0.0.3:9200".to_string(),
]);
assert_eq!(config.master_addr, "10.0.0.1:9200");
assert_eq!(config.master_addrs.len(), 3);
assert!(config.is_multi_master());
assert!(config.validate().is_ok());
}
#[test]
fn test_master_addresses_single() {
let config = GoosefsConfig::new("10.0.0.1:9200");
let addrs = config.master_addresses();
assert_eq!(addrs, vec!["10.0.0.1:9200"]);
assert!(!config.is_multi_master());
}
#[test]
fn test_master_addresses_multi() {
let config = GoosefsConfig::new_ha(vec![
"10.0.0.1:9200".to_string(),
"10.0.0.2:9200".to_string(),
]);
let addrs = config.master_addresses();
assert_eq!(addrs.len(), 2);
assert!(config.is_multi_master());
}
#[test]
#[should_panic(expected = "master addresses must not be empty")]
fn test_new_ha_empty_panics() {
GoosefsConfig::new_ha(vec![]);
}
#[test]
fn test_from_uri_single_master_with_root() {
let cfg = GoosefsConfig::from_uri("gfs://10.0.0.1:9200/data").unwrap();
assert_eq!(cfg.master_addr, "10.0.0.1:9200");
assert!(!cfg.is_multi_master());
assert_eq!(cfg.master_addrs, Vec::<String>::new());
assert_eq!(cfg.root, "/data");
}
#[test]
fn test_from_uri_ha_three_masters_with_root() {
let cfg = GoosefsConfig::from_uri(
"gfs://172.16.16.27:9200,172.16.16.23:9200,172.16.16.38:9200/xxxx",
)
.unwrap();
assert_eq!(cfg.master_addr, "172.16.16.27:9200");
assert_eq!(cfg.master_addrs.len(), 3);
assert!(cfg.is_multi_master());
assert_eq!(cfg.root, "/xxxx");
}
#[test]
fn test_from_uri_no_root() {
let cfg = GoosefsConfig::from_uri("gfs://a:9200,b:9200").unwrap();
assert_eq!(cfg.master_addrs.len(), 2);
assert_eq!(cfg.root, "");
}
#[test]
fn test_from_uri_root_with_trailing_slash() {
let cfg = GoosefsConfig::from_uri("gfs://a:9200/goosefs-data/").unwrap();
assert_eq!(cfg.root, "/goosefs-data");
}
#[test]
fn test_from_uri_bare_slash_collapses_to_empty_root() {
let cfg = GoosefsConfig::from_uri("gfs://a:9200/").unwrap();
assert_eq!(cfg.root, "");
}
#[test]
fn test_from_uri_trims_whitespace_between_addresses() {
let cfg = GoosefsConfig::from_uri("gfs://a:9200, b:9200 ,c:9200/root").unwrap();
assert_eq!(cfg.master_addrs, vec!["a:9200", "b:9200", "c:9200"]);
assert_eq!(cfg.root, "/root");
}
#[test]
fn test_from_uri_rejects_invalid_scheme() {
assert!(matches!(
GoosefsConfig::from_uri("http://a:9200/x"),
Err(UriParseError::InvalidScheme { .. })
));
assert!(matches!(
GoosefsConfig::from_uri("a:9200,b:9200"),
Err(UriParseError::InvalidScheme { .. })
));
}
#[test]
fn test_from_uri_rejects_empty_authority() {
assert!(matches!(
GoosefsConfig::from_uri("gfs:///path"),
Err(UriParseError::EmptyAuthority { .. })
));
assert!(matches!(
GoosefsConfig::from_uri("gfs:// , , /path"),
Err(UriParseError::EmptyAuthority { .. })
));
}
#[test]
fn test_from_uri_full_path_uses_root() {
let cfg = GoosefsConfig::from_uri("gfs://a:9200/data").unwrap();
assert_eq!(cfg.full_path("/file.txt"), "/data/file.txt");
assert_eq!(cfg.full_path("file.txt"), "/data/file.txt");
}
#[test]
fn test_full_path_with_root() {
let config = GoosefsConfig {
root: "/data".to_string(),
..Default::default()
};
assert_eq!(config.full_path("/file.txt"), "/data/file.txt");
assert_eq!(config.full_path("file.txt"), "/data/file.txt");
}
#[test]
fn test_full_path_without_root() {
let config = GoosefsConfig::default();
assert_eq!(config.full_path("/file.txt"), "/file.txt");
}
#[test]
fn test_validate_empty_master() {
let config = GoosefsConfig {
master_addr: String::new(),
master_addrs: Vec::new(),
..Default::default()
};
assert!(config.validate().is_err());
}
#[test]
fn test_validate_empty_addr_in_list() {
let config = GoosefsConfig {
master_addr: "10.0.0.1:9200".to_string(),
master_addrs: vec!["10.0.0.1:9200".to_string(), "".to_string()],
..Default::default()
};
assert!(config.validate().is_err());
}
#[test]
fn test_validate_chunk_larger_than_block() {
let config = GoosefsConfig {
chunk_size: 128 * 1024 * 1024,
block_size: 64 * 1024 * 1024,
..Default::default()
};
assert!(config.validate().is_err());
}
#[test]
fn test_part_v_tuning_defaults_and_builders() {
let config = GoosefsConfig::default();
assert_eq!(config.prefetch_window, 8);
assert_eq!(config.read_buffer_messages, 16);
assert_eq!(config.ack_interval_bytes, 0); assert_eq!(config.ack_interval_chunks, 1);
assert_eq!(config.master_connection_pool_size, 1);
let tuned = GoosefsConfig::new("127.0.0.1:9200")
.with_prefetch_window(16)
.with_ack_interval_bytes(8 * 1024 * 1024)
.with_master_connection_pool_size(0); assert_eq!(tuned.prefetch_window, 16);
assert_eq!(tuned.ack_interval_bytes, 8 * 1024 * 1024);
assert_eq!(tuned.master_connection_pool_size, 1);
let pooled = GoosefsConfig::new("127.0.0.1:9200").with_master_connection_pool_size(8);
assert_eq!(pooled.master_connection_pool_size, 8);
}
#[test]
fn test_write_type_default_is_none() {
let config = GoosefsConfig::default();
assert!(config.write_type.is_none());
assert!(config.get_write_type().is_none());
}
#[test]
fn test_with_write_type_builder() {
let config = GoosefsConfig::new("127.0.0.1:9200").with_write_type(WritePType::CacheThrough);
assert_eq!(config.write_type, Some(3));
assert_eq!(config.get_write_type(), Some(WritePType::CacheThrough));
}
#[test]
fn test_write_p_type_all_variants_config() {
let cases = vec![
(WritePType::MustCache, 1),
(WritePType::TryCache, 2),
(WritePType::CacheThrough, 3),
(WritePType::Through, 4),
(WritePType::AsyncThrough, 5),
];
for (wt, expected_i32) in cases {
let config = GoosefsConfig::new("127.0.0.1:9200").with_write_type(wt);
assert_eq!(config.write_type, Some(expected_i32));
assert_eq!(config.get_write_type(), Some(wt));
}
}
#[test]
fn test_write_type_invalid_i32() {
let config = GoosefsConfig {
write_type: Some(999),
..Default::default()
};
assert!(config.get_write_type().is_none());
}
#[test]
fn test_write_type_from_str_lowercase() {
assert_eq!(
"must_cache".parse::<WriteType>().unwrap(),
WriteType::MustCache
);
assert_eq!(
"try_cache".parse::<WriteType>().unwrap(),
WriteType::TryCache
);
assert_eq!(
"cache_through".parse::<WriteType>().unwrap(),
WriteType::CacheThrough
);
assert_eq!("through".parse::<WriteType>().unwrap(), WriteType::Through);
assert_eq!(
"async_through".parse::<WriteType>().unwrap(),
WriteType::AsyncThrough
);
}
#[test]
fn test_write_type_from_str_uppercase() {
assert_eq!(
"MUST_CACHE".parse::<WriteType>().unwrap(),
WriteType::MustCache
);
assert_eq!(
"TRY_CACHE".parse::<WriteType>().unwrap(),
WriteType::TryCache
);
assert_eq!(
"CACHE_THROUGH".parse::<WriteType>().unwrap(),
WriteType::CacheThrough
);
assert_eq!("THROUGH".parse::<WriteType>().unwrap(), WriteType::Through);
assert_eq!(
"ASYNC_THROUGH".parse::<WriteType>().unwrap(),
WriteType::AsyncThrough
);
}
#[test]
fn test_write_type_from_str_mixed_case() {
assert_eq!(
"Cache_Through".parse::<WriteType>().unwrap(),
WriteType::CacheThrough
);
assert_eq!("Through".parse::<WriteType>().unwrap(), WriteType::Through);
}
#[test]
fn test_write_type_from_str_invalid() {
assert!("invalid".parse::<WriteType>().is_err());
assert!("".parse::<WriteType>().is_err());
assert!("cache-through".parse::<WriteType>().is_err()); }
#[test]
fn test_write_type_display() {
assert_eq!(WriteType::MustCache.to_string(), "must_cache");
assert_eq!(WriteType::TryCache.to_string(), "try_cache");
assert_eq!(WriteType::CacheThrough.to_string(), "cache_through");
assert_eq!(WriteType::Through.to_string(), "through");
assert_eq!(WriteType::AsyncThrough.to_string(), "async_through");
}
#[test]
fn test_write_type_as_str() {
assert_eq!(WriteType::CacheThrough.as_str(), "cache_through");
assert_eq!(WriteType::Through.as_str(), "through");
}
#[test]
fn test_write_type_as_i32() {
assert_eq!(WriteType::MustCache.as_i32(), 1);
assert_eq!(WriteType::TryCache.as_i32(), 2);
assert_eq!(WriteType::CacheThrough.as_i32(), 3);
assert_eq!(WriteType::Through.as_i32(), 4);
assert_eq!(WriteType::AsyncThrough.as_i32(), 5);
}
#[test]
fn test_write_type_to_write_p_type() {
assert_eq!(
WritePType::from(WriteType::MustCache),
WritePType::MustCache
);
assert_eq!(
WritePType::from(WriteType::CacheThrough),
WritePType::CacheThrough
);
assert_eq!(WritePType::from(WriteType::Through), WritePType::Through);
}
#[test]
fn test_write_p_type_to_write_type() {
assert_eq!(
WriteType::try_from_proto(WritePType::MustCache).unwrap(),
WriteType::MustCache
);
assert_eq!(
WriteType::try_from_proto(WritePType::CacheThrough).unwrap(),
WriteType::CacheThrough
);
assert_eq!(
WriteType::try_from_proto(WritePType::Through).unwrap(),
WriteType::Through
);
assert!(WriteType::try_from_proto(WritePType::UnspecifiedWriteType).is_err());
assert!(WriteType::try_from_proto(WritePType::None).is_err());
}
#[test]
fn test_write_p_type_try_from_unspecified() {
assert!(WriteType::try_from_proto(WritePType::UnspecifiedWriteType).is_err());
assert!(WriteType::try_from_proto(WritePType::None).is_err());
}
#[test]
fn test_write_type_all_variants() {
assert_eq!(WriteType::ALL.len(), 5);
for wt in WriteType::ALL {
let s = wt.as_str();
let parsed: WriteType = s.parse().unwrap();
assert_eq!(&parsed, wt);
let pt = WritePType::from(*wt);
let back = WriteType::try_from_proto(pt).unwrap();
assert_eq!(back, *wt);
}
}
#[test]
fn test_config_with_write_type_enum() {
let config =
GoosefsConfig::new("127.0.0.1:9200").with_write_type_enum(WriteType::CacheThrough);
assert_eq!(config.write_type, Some(3));
assert_eq!(config.get_write_type(), Some(WritePType::CacheThrough));
}
#[test]
fn test_config_with_write_type_str() {
let config = GoosefsConfig::new("127.0.0.1:9200")
.with_write_type_str("through")
.unwrap();
assert_eq!(config.write_type, Some(4));
assert_eq!(config.get_write_type(), Some(WritePType::Through));
}
#[test]
fn test_config_with_write_type_str_invalid() {
let result = GoosefsConfig::new("127.0.0.1:9200").with_write_type_str("bad_value");
assert!(result.is_err());
}
#[test]
fn test_storage_option_constants() {
assert_eq!(STORAGE_OPT_MASTER_ADDR, "goosefs_master_addr");
assert_eq!(STORAGE_OPT_WRITE_TYPE, "goosefs_write_type");
assert_eq!(STORAGE_OPT_BLOCK_SIZE, "goosefs_block_size");
assert_eq!(STORAGE_OPT_CHUNK_SIZE, "goosefs_chunk_size");
}
#[test]
fn test_env_var_constants() {
assert_eq!(ENV_MASTER_ADDR, "GOOSEFS_MASTER_ADDR");
assert_eq!(ENV_WRITE_TYPE, "GOOSEFS_WRITE_TYPE");
assert_eq!(ENV_BLOCK_SIZE, "GOOSEFS_BLOCK_SIZE");
assert_eq!(ENV_CHUNK_SIZE, "GOOSEFS_CHUNK_SIZE");
}
#[test]
fn test_default_retry_config() {
let config = GoosefsConfig::default();
assert_eq!(
config.master_inquire_retry_max_duration,
Duration::from_millis(120_000)
);
assert_eq!(
config.master_inquire_initial_sleep,
Duration::from_millis(50)
);
assert_eq!(
config.master_inquire_max_sleep,
Duration::from_millis(3_000)
);
}
#[test]
fn test_from_properties_str_basic() {
let props = "\
goosefs.master.hostname=10.0.0.1
goosefs.master.rpc.port=9200
goosefs.security.authentication.type=SIMPLE
goosefs.user.file.writetype.default=CACHE_THROUGH
goosefs.user.block.size.bytes.default=64MB
goosefs.user.network.data.transfer.chunk.size=1MB
";
let cfg = GoosefsConfig::from_properties_str(props);
assert_eq!(cfg.master_addr, "10.0.0.1:9200");
assert_eq!(cfg.get_write_type(), Some(WritePType::CacheThrough));
assert_eq!(cfg.block_size, 64 * 1024 * 1024);
assert_eq!(cfg.chunk_size, 1024 * 1024);
}
#[test]
fn test_from_properties_str_ha_addresses() {
let props = "goosefs.master.rpc.addresses=10.0.0.1:9200,10.0.0.2:9200,10.0.0.3:9200\n";
let cfg = GoosefsConfig::from_properties_str(props);
assert_eq!(cfg.master_addr, "10.0.0.1:9200");
assert_eq!(cfg.master_addrs.len(), 3);
assert!(cfg.is_multi_master());
}
#[test]
fn test_from_properties_str_byte_size_kb() {
let props = "goosefs.user.network.data.transfer.chunk.size=512KB\n";
let cfg = GoosefsConfig::from_properties_str(props);
assert_eq!(cfg.chunk_size, 512 * 1024);
}
#[test]
fn test_from_properties_str_byte_size_plain_int() {
let props = "goosefs.user.block.size.bytes.default=134217728\n";
let cfg = GoosefsConfig::from_properties_str(props);
assert_eq!(cfg.block_size, 128 * 1024 * 1024);
}
#[test]
fn test_from_properties_str_empty_uses_defaults() {
let cfg = GoosefsConfig::from_properties_str("");
assert_eq!(cfg.master_addr, "127.0.0.1:9200");
assert_eq!(cfg.block_size, 64 * 1024 * 1024);
}
#[test]
fn test_from_properties_str_comments_ignored() {
let props = "\
# This is a comment
goosefs.master.hostname=10.0.0.1
! Another comment style
#goosefs.master.rpc.port=9999
goosefs.master.rpc.port=9200
";
let cfg = GoosefsConfig::from_properties_str(props);
assert_eq!(cfg.master_addr, "10.0.0.1:9200");
}
#[test]
fn test_parse_byte_size() {
assert_eq!(parse_byte_size("64MB").unwrap(), 64 * 1024 * 1024);
assert_eq!(parse_byte_size("1GB").unwrap(), 1024 * 1024 * 1024);
assert_eq!(parse_byte_size("512KB").unwrap(), 512 * 1024);
assert_eq!(parse_byte_size("1048576").unwrap(), 1024 * 1024);
assert!(parse_byte_size("bad").is_err());
}
#[test]
fn test_parse_byte_size_overflow_surfaces_err() {
assert!(
parse_byte_size("99999999999GB").is_err(),
"overflow on '99999999999GB' must be reported as Err, not silently wrapped"
);
assert!(
parse_byte_size("99999999999TB").is_err(),
"overflow on '99999999999TB' must be reported as Err"
);
assert!(
parse_byte_size(&format!("{}KB", u64::MAX)).is_err(),
"u64::MAX KB must overflow"
);
assert_eq!(parse_byte_size("8GB").unwrap(), 8u64 * 1024 * 1024 * 1024);
}
static ENV_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
#[test]
fn test_apply_env_master_addr() {
let _guard = ENV_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
std::env::set_var("GOOSEFS_MASTER_ADDR", "192.168.1.1:9200");
let cfg = GoosefsConfig::default().apply_env();
std::env::remove_var("GOOSEFS_MASTER_ADDR");
assert_eq!(cfg.master_addr, "192.168.1.1:9200");
}
#[test]
fn test_apply_env_ha_addresses() {
let _guard = ENV_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
std::env::set_var("GOOSEFS_MASTER_ADDR", "10.0.0.1:9200,10.0.0.2:9200");
let cfg = GoosefsConfig::default().apply_env();
std::env::remove_var("GOOSEFS_MASTER_ADDR");
assert_eq!(cfg.master_addrs.len(), 2);
assert_eq!(cfg.master_addr, "10.0.0.1:9200");
}
#[test]
fn test_apply_env_uri_form() {
let _guard = ENV_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
std::env::set_var(
"GOOSEFS_MASTER_ADDR",
"gfs://172.16.16.27:9200,172.16.16.23:9200,172.16.16.38:9200/xxxx",
);
let cfg = GoosefsConfig::default().apply_env();
std::env::remove_var("GOOSEFS_MASTER_ADDR");
assert_eq!(cfg.master_addr, "172.16.16.27:9200");
assert_eq!(cfg.master_addrs.len(), 3);
assert_eq!(cfg.root, "/xxxx");
}
#[test]
fn test_apply_env_uri_form_single_master() {
let _guard = ENV_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
std::env::set_var("GOOSEFS_MASTER_ADDR", "gfs://10.0.0.1:9200/data");
let cfg = GoosefsConfig::default().apply_env();
std::env::remove_var("GOOSEFS_MASTER_ADDR");
assert_eq!(cfg.master_addr, "10.0.0.1:9200");
assert!(cfg.master_addrs.is_empty());
assert_eq!(cfg.root, "/data");
}
#[test]
fn test_apply_env_write_type() {
let _guard = ENV_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
std::env::set_var("GOOSEFS_WRITE_TYPE", "THROUGH");
let cfg = GoosefsConfig::default().apply_env();
std::env::remove_var("GOOSEFS_WRITE_TYPE");
assert_eq!(cfg.get_write_type(), Some(WritePType::Through));
}
#[test]
fn test_apply_env_block_size() {
let _guard = ENV_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
std::env::set_var("GOOSEFS_BLOCK_SIZE", "134217728");
let cfg = GoosefsConfig::default().apply_env();
std::env::remove_var("GOOSEFS_BLOCK_SIZE");
assert_eq!(cfg.block_size, 128 * 1024 * 1024);
}
#[test]
fn test_apply_env_metrics_enabled_false() {
let _guard = ENV_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
std::env::set_var(ENV_METRICS_ENABLED, "false");
let cfg = GoosefsConfig::default().apply_env();
std::env::remove_var(ENV_METRICS_ENABLED);
assert!(
!cfg.metrics_enabled,
"ENV_METRICS_ENABLED=false must disable metrics"
);
}
#[test]
fn test_apply_env_metrics_enabled_true_case_insensitive() {
let _guard = ENV_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
std::env::set_var(ENV_METRICS_ENABLED, "TRUE");
let cfg = GoosefsConfig::default()
.with_metrics_enabled(false)
.apply_env();
std::env::remove_var(ENV_METRICS_ENABLED);
assert!(
cfg.metrics_enabled,
"ENV_METRICS_ENABLED=TRUE (any case) must enable metrics"
);
}
#[test]
fn test_apply_env_metrics_enabled_invalid_is_ignored() {
let _guard = ENV_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
std::env::set_var(ENV_METRICS_ENABLED, "yes");
let cfg = GoosefsConfig::default().apply_env();
std::env::remove_var(ENV_METRICS_ENABLED);
assert!(
cfg.metrics_enabled,
"unparseable ENV_METRICS_ENABLED must leave the field at its previous value"
);
}
#[test]
fn test_default_new_fields() {
let cfg = GoosefsConfig::default();
assert!(cfg.config_manager_rpc_addresses.is_empty());
assert_eq!(cfg.config_rpc_port, 9214);
assert!(cfg.transparent_acceleration_enabled);
assert!(!cfg.transparent_acceleration_cosranger_enabled);
assert!(!cfg.authorization_permission_enabled);
assert_eq!(cfg.login_impersonation_username, "_HDFS_USER_");
}
#[test]
fn test_from_properties_str_config_manager() {
let props = "\
goosefs.config.manager.rpc.addresses=10.0.0.1:9214,10.0.0.2:9214
goosefs.config.rpc.port=9300
";
let cfg = GoosefsConfig::from_properties_str(props);
assert_eq!(cfg.config_manager_rpc_addresses.len(), 2);
assert_eq!(cfg.config_manager_rpc_addresses[0], "10.0.0.1:9214");
assert_eq!(cfg.config_rpc_port, 9300);
}
#[test]
fn test_from_properties_str_security_extended() {
let props = "\
goosefs.security.authentication.type=SIMPLE
goosefs.security.authorization.permission.enabled=true
goosefs.security.login.impersonation.username=_NONE_
goosefs.security.login.username=testuser
";
let cfg = GoosefsConfig::from_properties_str(props);
assert!(cfg.authorization_permission_enabled);
assert_eq!(cfg.login_impersonation_username, "_NONE_");
assert_eq!(cfg.auth_username, "testuser");
}
#[test]
fn test_from_properties_str_transparent_acceleration() {
let props = "\
goosefs.user.client.transparent_acceleration.enabled=false
goosefs.user.client.transparent_acceleration.cosranger.enabled=true
";
let cfg = GoosefsConfig::from_properties_str(props);
assert!(!cfg.transparent_acceleration_enabled);
assert!(cfg.transparent_acceleration_cosranger_enabled);
}
#[test]
fn test_from_properties_str_full_config() {
let props = "\
goosefs.master.hostname=10.0.0.1
goosefs.master.rpc.port=9200
goosefs.config.manager.rpc.addresses=10.0.0.1:9214
goosefs.config.rpc.port=9214
goosefs.security.authentication.type=SIMPLE
goosefs.security.authorization.permission.enabled=true
goosefs.security.login.impersonation.username=_HDFS_USER_
goosefs.security.login.username=myuser
goosefs.user.client.transparent_acceleration.enabled=true
goosefs.user.client.transparent_acceleration.cosranger.enabled=false
goosefs.user.file.writetype.default=CACHE_THROUGH
goosefs.user.block.size.bytes.default=64MB
goosefs.user.network.data.transfer.chunk.size=1MB
";
let cfg = GoosefsConfig::from_properties_str(props);
assert_eq!(cfg.master_addr, "10.0.0.1:9200");
assert_eq!(cfg.config_manager_rpc_addresses, vec!["10.0.0.1:9214"]);
assert_eq!(cfg.config_rpc_port, 9214);
assert!(cfg.authorization_permission_enabled);
assert_eq!(cfg.login_impersonation_username, "_HDFS_USER_");
assert_eq!(cfg.auth_username, "myuser");
assert!(cfg.transparent_acceleration_enabled);
assert!(!cfg.transparent_acceleration_cosranger_enabled);
assert_eq!(cfg.get_write_type(), Some(WritePType::CacheThrough));
assert_eq!(cfg.block_size, 64 * 1024 * 1024);
assert_eq!(cfg.chunk_size, 1024 * 1024);
}
#[test]
fn test_new_env_var_constants() {
assert_eq!(
ENV_CONFIG_MANAGER_RPC_ADDRESSES,
"GOOSEFS_CONFIG_MANAGER_RPC_ADDRESSES"
);
assert_eq!(ENV_CONFIG_RPC_PORT, "GOOSEFS_CONFIG_RPC_PORT");
assert_eq!(
ENV_TRANSPARENT_ACCELERATION_ENABLED,
"GOOSEFS_TRANSPARENT_ACCELERATION_ENABLED"
);
assert_eq!(
ENV_TRANSPARENT_ACCELERATION_COSRANGER_ENABLED,
"GOOSEFS_TRANSPARENT_ACCELERATION_COSRANGER_ENABLED"
);
assert_eq!(
ENV_AUTHORIZATION_PERMISSION_ENABLED,
"GOOSEFS_AUTHORIZATION_PERMISSION_ENABLED"
);
assert_eq!(
ENV_LOGIN_IMPERSONATION_USERNAME,
"GOOSEFS_LOGIN_IMPERSONATION_USERNAME"
);
}
#[test]
fn test_new_storage_option_constants() {
assert_eq!(
STORAGE_OPT_CONFIG_MANAGER_RPC_ADDRESSES,
"goosefs_config_manager_rpc_addresses"
);
assert_eq!(STORAGE_OPT_CONFIG_RPC_PORT, "goosefs_config_rpc_port");
assert_eq!(
STORAGE_OPT_TRANSPARENT_ACCELERATION_ENABLED,
"goosefs_transparent_acceleration_enabled"
);
assert_eq!(
STORAGE_OPT_TRANSPARENT_ACCELERATION_COSRANGER_ENABLED,
"goosefs_transparent_acceleration_cosranger_enabled"
);
assert_eq!(
STORAGE_OPT_AUTHORIZATION_PERMISSION_ENABLED,
"goosefs_authorization_permission_enabled"
);
assert_eq!(
STORAGE_OPT_LOGIN_IMPERSONATION_USERNAME,
"goosefs_login_impersonation_username"
);
}
#[test]
fn test_perf_tuning_constant_names() {
assert_eq!(
ENV_WORKER_CONNECTION_POOL_SIZE,
"GOOSEFS_WORKER_CONNECTION_POOL_SIZE"
);
assert_eq!(ENV_FILE_INFO_CACHE_TTL_MS, "GOOSEFS_FILE_INFO_CACHE_TTL_MS");
assert_eq!(
ENV_FILE_INFO_CACHE_CAPACITY,
"GOOSEFS_FILE_INFO_CACHE_CAPACITY"
);
assert_eq!(
STORAGE_OPT_WORKER_CONNECTION_POOL_SIZE,
"goosefs_worker_connection_pool_size"
);
assert_eq!(
STORAGE_OPT_FILE_INFO_CACHE_TTL_MS,
"goosefs_file_info_cache_ttl_ms"
);
assert_eq!(
STORAGE_OPT_FILE_INFO_CACHE_CAPACITY,
"goosefs_file_info_cache_capacity"
);
}
#[test]
fn test_apply_env_worker_connection_pool_size() {
let _guard = ENV_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
std::env::set_var("GOOSEFS_WORKER_CONNECTION_POOL_SIZE", "8");
let cfg = GoosefsConfig::default().apply_env();
std::env::remove_var("GOOSEFS_WORKER_CONNECTION_POOL_SIZE");
assert_eq!(cfg.worker_connection_pool_size, 8);
}
#[test]
fn test_apply_env_worker_connection_pool_size_clamp() {
let _guard = ENV_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
std::env::set_var("GOOSEFS_WORKER_CONNECTION_POOL_SIZE", "0");
let cfg = GoosefsConfig::default().apply_env();
std::env::remove_var("GOOSEFS_WORKER_CONNECTION_POOL_SIZE");
assert_eq!(cfg.worker_connection_pool_size, 1);
}
#[test]
fn test_apply_env_worker_connection_pool_size_invalid_keeps_default() {
let _guard = ENV_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let default_size = default_worker_connection_pool_size();
std::env::set_var("GOOSEFS_WORKER_CONNECTION_POOL_SIZE", "not-a-number");
let cfg = GoosefsConfig::default().apply_env();
std::env::remove_var("GOOSEFS_WORKER_CONNECTION_POOL_SIZE");
assert_eq!(cfg.worker_connection_pool_size, default_size);
}
#[test]
fn test_apply_env_file_info_cache_ttl_ms() {
let _guard = ENV_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
std::env::set_var("GOOSEFS_FILE_INFO_CACHE_TTL_MS", "2500");
let cfg = GoosefsConfig::default().apply_env();
std::env::remove_var("GOOSEFS_FILE_INFO_CACHE_TTL_MS");
assert_eq!(cfg.file_info_cache_ttl, Duration::from_millis(2500));
}
#[test]
fn test_apply_env_file_info_cache_ttl_zero_disables() {
let _guard = ENV_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
std::env::set_var("GOOSEFS_FILE_INFO_CACHE_TTL_MS", "0");
let cfg = GoosefsConfig::default().apply_env();
std::env::remove_var("GOOSEFS_FILE_INFO_CACHE_TTL_MS");
assert_eq!(cfg.file_info_cache_ttl, Duration::ZERO);
}
#[test]
fn test_apply_env_file_info_cache_capacity_clamp() {
let _guard = ENV_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
std::env::set_var("GOOSEFS_FILE_INFO_CACHE_CAPACITY", "0");
let cfg = GoosefsConfig::default().apply_env();
std::env::remove_var("GOOSEFS_FILE_INFO_CACHE_CAPACITY");
assert_eq!(cfg.file_info_cache_capacity, 1);
}
#[test]
fn test_from_properties_str_perf_tuning_knobs() {
let props = "\
goosefs.user.worker.connection.pool.size=6
goosefs.user.file.info.cache.ttl.ms=1500
goosefs.user.file.info.cache.capacity=2048
";
let cfg = GoosefsConfig::from_properties_str(props);
assert_eq!(cfg.worker_connection_pool_size, 6);
assert_eq!(cfg.file_info_cache_ttl, Duration::from_millis(1500));
assert_eq!(cfg.file_info_cache_capacity, 2048);
}
#[test]
fn test_from_properties_str_worker_pool_zero_clamped() {
let props = "goosefs.user.worker.connection.pool.size=0\n";
let cfg = GoosefsConfig::from_properties_str(props);
assert_eq!(cfg.worker_connection_pool_size, 1);
}
#[test]
fn test_default_client_cache_and_range_coalesce_knobs() {
let cfg = GoosefsConfig::default();
assert!(
!cfg.client_cache_enabled,
"client page cache must stay disabled by default"
);
assert_eq!(cfg.client_cache_page_size, default_client_cache_page_size());
assert_eq!(cfg.client_cache_size, default_client_cache_size());
assert_eq!(cfg.client_cache_dirs, default_client_cache_dirs());
assert_eq!(
cfg.client_cache_uring_enabled,
default_client_cache_uring_enabled()
);
assert_eq!(
cfg.client_cache_uring_queue_depth,
default_client_cache_uring_queue_depth()
);
assert_eq!(
cfg.client_cache_uring_thread_count,
default_client_cache_uring_thread_count()
);
assert!(!cfg.range_coalesce_enabled);
assert_eq!(
cfg.range_coalesce_gap_bytes,
default_range_coalesce_gap_bytes()
);
assert_eq!(
cfg.range_coalesce_max_bytes,
default_range_coalesce_max_bytes()
);
}
#[test]
fn test_apply_env_client_cache_knobs() {
let _guard = ENV_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
std::env::set_var(ENV_CLIENT_CACHE_ENABLED, "true");
std::env::set_var(ENV_CLIENT_CACHE_PAGE_SIZE, "65536");
std::env::set_var(ENV_CLIENT_CACHE_SIZE, "1048576");
std::env::set_var(ENV_CLIENT_CACHE_DIRS, "/tmp/a,/tmp/b");
std::env::set_var(ENV_CLIENT_CACHE_EVICTOR, "lfu");
std::env::set_var(ENV_CLIENT_CACHE_URING_ENABLED, "false");
std::env::set_var(ENV_CLIENT_CACHE_URING_QUEUE_DEPTH, "4096");
std::env::set_var(ENV_CLIENT_CACHE_URING_THREAD_COUNT, "8");
std::env::set_var(ENV_CLIENT_CACHE_TTL_SECS, "30");
let cfg = GoosefsConfig::default().apply_env();
std::env::remove_var(ENV_CLIENT_CACHE_ENABLED);
std::env::remove_var(ENV_CLIENT_CACHE_PAGE_SIZE);
std::env::remove_var(ENV_CLIENT_CACHE_SIZE);
std::env::remove_var(ENV_CLIENT_CACHE_DIRS);
std::env::remove_var(ENV_CLIENT_CACHE_EVICTOR);
std::env::remove_var(ENV_CLIENT_CACHE_URING_ENABLED);
std::env::remove_var(ENV_CLIENT_CACHE_URING_QUEUE_DEPTH);
std::env::remove_var(ENV_CLIENT_CACHE_URING_THREAD_COUNT);
std::env::remove_var(ENV_CLIENT_CACHE_TTL_SECS);
assert!(cfg.client_cache_enabled);
assert_eq!(cfg.client_cache_page_size, 65536);
assert_eq!(cfg.client_cache_size, 1_048_576);
assert_eq!(
cfg.client_cache_dirs,
vec!["/tmp/a".to_string(), "/tmp/b".to_string()]
);
assert_eq!(cfg.client_cache_evictor, CacheEvictorType::Lfu);
assert!(!cfg.client_cache_uring_enabled);
assert_eq!(cfg.client_cache_uring_queue_depth, 4096);
assert_eq!(cfg.client_cache_uring_thread_count, 8);
assert_eq!(cfg.client_cache_ttl_secs, 30);
}
#[test]
fn test_from_properties_str_client_cache_knobs() {
let props = "\
goosefs.user.client.cache.enabled=true
goosefs.user.client.cache.page.size=32768
goosefs.user.client.cache.size=2097152
goosefs.user.client.cache.dirs=/var/cache/a,/var/cache/b
goosefs.user.client.cache.eviction.policy=lru
goosefs.user.client.cache.uring.enabled=true
goosefs.user.client.cache.uring.queue.depth=8192
goosefs.user.client.cache.uring.thread.count=4
goosefs.user.client.cache.ttl.seconds=60
";
let cfg = GoosefsConfig::from_properties_str(props);
assert!(cfg.client_cache_enabled);
assert_eq!(cfg.client_cache_page_size, 32768);
assert_eq!(cfg.client_cache_size, 2_097_152);
assert_eq!(
cfg.client_cache_dirs,
vec!["/var/cache/a".to_string(), "/var/cache/b".to_string()]
);
assert_eq!(cfg.client_cache_evictor, CacheEvictorType::Lru);
assert!(cfg.client_cache_uring_enabled);
assert_eq!(cfg.client_cache_uring_queue_depth, 8192);
assert_eq!(cfg.client_cache_uring_thread_count, 4);
assert_eq!(cfg.client_cache_ttl_secs, 60);
}
#[test]
fn test_apply_env_short_circuit_knobs() {
let _guard = ENV_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
std::env::set_var("GOOSEFS_SHORT_CIRCUIT_ENABLED", "false");
std::env::set_var("GOOSEFS_SHORT_CIRCUIT_CACHE_CAPACITY", "128");
std::env::set_var("GOOSEFS_SHORT_CIRCUIT_CACHE_TTL_MS", "45000");
std::env::set_var("GOOSEFS_SHORT_CIRCUIT_NEG_CACHE_TTL_MS", "1500");
std::env::set_var("GOOSEFS_SHORT_CIRCUIT_ADVISE", "sequential");
std::env::set_var("GOOSEFS_SHORT_CIRCUIT_PREFETCH_ENABLED", "false");
std::env::set_var("GOOSEFS_SHORT_CIRCUIT_PREFETCH_COALESCE_GAP", "131072");
std::env::set_var("GOOSEFS_SHORT_CIRCUIT_PREFETCH_MAX_BATCH", "512");
std::env::set_var("GOOSEFS_SHORT_CIRCUIT_MIN_BLOCK_SIZE", "4194304");
std::env::set_var("GOOSEFS_SHORT_CIRCUIT_SIGBUS_HANDLER", "false");
std::env::set_var("GOOSEFS_SHORT_CIRCUIT_THP", "true");
let cfg = GoosefsConfig::default().apply_env();
std::env::remove_var("GOOSEFS_SHORT_CIRCUIT_ENABLED");
std::env::remove_var("GOOSEFS_SHORT_CIRCUIT_CACHE_CAPACITY");
std::env::remove_var("GOOSEFS_SHORT_CIRCUIT_CACHE_TTL_MS");
std::env::remove_var("GOOSEFS_SHORT_CIRCUIT_NEG_CACHE_TTL_MS");
std::env::remove_var("GOOSEFS_SHORT_CIRCUIT_ADVISE");
std::env::remove_var("GOOSEFS_SHORT_CIRCUIT_PREFETCH_ENABLED");
std::env::remove_var("GOOSEFS_SHORT_CIRCUIT_PREFETCH_COALESCE_GAP");
std::env::remove_var("GOOSEFS_SHORT_CIRCUIT_PREFETCH_MAX_BATCH");
std::env::remove_var("GOOSEFS_SHORT_CIRCUIT_MIN_BLOCK_SIZE");
std::env::remove_var("GOOSEFS_SHORT_CIRCUIT_SIGBUS_HANDLER");
std::env::remove_var("GOOSEFS_SHORT_CIRCUIT_THP");
assert!(!cfg.short_circuit_enabled);
assert_eq!(cfg.short_circuit_cache_capacity, 128);
assert_eq!(cfg.short_circuit_cache_ttl, Duration::from_millis(45000));
assert_eq!(cfg.short_circuit_neg_cache_ttl, Duration::from_millis(1500));
assert_eq!(cfg.short_circuit_advise, "sequential");
assert!(!cfg.short_circuit_prefetch_enabled);
assert_eq!(cfg.short_circuit_prefetch_coalesce_gap, 131072);
assert_eq!(cfg.short_circuit_prefetch_max_batch, 512);
assert_eq!(cfg.short_circuit_min_block_size, 4 * 1024 * 1024);
assert!(!cfg.short_circuit_sigbus_handler);
assert!(cfg.short_circuit_thp);
}
#[test]
fn test_from_properties_str_short_circuit_knobs() {
let props = "\
goosefs.user.short.circuit.enabled=false
goosefs.client.short.circuit.cache.capacity=256
goosefs.client.short.circuit.cache.ttl.ms=60000
goosefs.client.short.circuit.neg.cache.ttl.ms=2500
goosefs.client.short.circuit.advise=none
goosefs.client.short.circuit.prefetch.enabled=false
goosefs.client.short.circuit.prefetch.coalesce.gap=262144
goosefs.client.short.circuit.prefetch.max.batch=2048
goosefs.client.short.circuit.min.block.size=8388608
goosefs.client.short.circuit.sigbus.handler=false
goosefs.client.short.circuit.thp=true
";
let cfg = GoosefsConfig::from_properties_str(props);
assert!(!cfg.short_circuit_enabled);
assert_eq!(cfg.short_circuit_cache_capacity, 256);
assert_eq!(cfg.short_circuit_cache_ttl, Duration::from_millis(60000));
assert_eq!(cfg.short_circuit_neg_cache_ttl, Duration::from_millis(2500));
assert_eq!(cfg.short_circuit_advise, "none");
assert!(!cfg.short_circuit_prefetch_enabled);
assert_eq!(cfg.short_circuit_prefetch_coalesce_gap, 262144);
assert_eq!(cfg.short_circuit_prefetch_max_batch, 2048);
assert_eq!(cfg.short_circuit_min_block_size, 8 * 1024 * 1024);
assert!(!cfg.short_circuit_sigbus_handler);
assert!(cfg.short_circuit_thp);
}
#[test]
fn test_builder_short_circuit_chain() {
let cfg = GoosefsConfig::new("127.0.0.1:9200")
.with_short_circuit_enabled(false)
.with_short_circuit_cache_capacity(200)
.with_short_circuit_cache_ttl(Duration::from_secs(45))
.with_short_circuit_neg_cache_ttl(Duration::from_secs(2))
.with_short_circuit_advise("sequential")
.with_short_circuit_prefetch_enabled(false)
.with_short_circuit_prefetch_coalesce_gap(1024)
.with_short_circuit_prefetch_max_batch(64)
.with_short_circuit_min_block_size(1_048_576)
.with_short_circuit_sigbus_handler(false)
.with_short_circuit_thp(true);
assert!(!cfg.short_circuit_enabled);
assert_eq!(cfg.short_circuit_cache_capacity, 200);
assert_eq!(cfg.short_circuit_cache_ttl, Duration::from_secs(45));
assert_eq!(cfg.short_circuit_neg_cache_ttl, Duration::from_secs(2));
assert_eq!(cfg.short_circuit_advise, "sequential");
assert!(!cfg.short_circuit_prefetch_enabled);
assert_eq!(cfg.short_circuit_prefetch_coalesce_gap, 1024);
assert_eq!(cfg.short_circuit_prefetch_max_batch, 64);
assert_eq!(cfg.short_circuit_min_block_size, 1_048_576);
assert!(!cfg.short_circuit_sigbus_handler);
assert!(cfg.short_circuit_thp);
}
#[test]
fn test_short_circuit_enabled_default_is_false() {
assert!(
!GoosefsConfig::default().short_circuit_enabled,
"Default::default() must ship with short-circuit OFF"
);
let cfg = GoosefsConfig::from_properties_str("goosefs.master.hostname=127.0.0.1");
assert!(
!cfg.short_circuit_enabled,
"properties default (missing field) must be false"
);
if std::env::var(ENV_SHORT_CIRCUIT_ENABLED).is_err() {
let cfg = GoosefsConfig::default().apply_env();
assert!(
!cfg.short_circuit_enabled,
"apply_env with unset GOOSEFS_SHORT_CIRCUIT_ENABLED must keep it false"
);
}
}
#[test]
fn test_short_circuit_canonical_key_names() {
assert_eq!(ENV_SHORT_CIRCUIT_ENABLED, "GOOSEFS_SHORT_CIRCUIT_ENABLED");
assert_eq!(
ENV_SHORT_CIRCUIT_CACHE_TTL_MS,
"GOOSEFS_SHORT_CIRCUIT_CACHE_TTL_MS"
);
assert_eq!(ENV_SHORT_CIRCUIT_ADVISE, "GOOSEFS_SHORT_CIRCUIT_ADVISE");
assert_eq!(
STORAGE_OPT_SHORT_CIRCUIT_ENABLED,
"goosefs_short_circuit_enabled"
);
assert_eq!(
STORAGE_OPT_SHORT_CIRCUIT_CACHE_TTL_MS,
"goosefs_short_circuit_cache_ttl_ms"
);
assert_eq!(
STORAGE_OPT_SHORT_CIRCUIT_MIN_BLOCK_SIZE,
"goosefs_short_circuit_min_block_size"
);
}
#[test]
fn test_impersonation_none_constant() {
assert_eq!(IMPERSONATION_NONE, "_NONE_");
}
#[test]
fn test_config_refresher_from_config_seeds_initial_values() {
let cfg = GoosefsConfig {
transparent_acceleration_enabled: false,
transparent_acceleration_cosranger_enabled: true,
..Default::default()
};
let refresher = ConfigRefresher::from_config(&cfg);
let sw = refresher.current_switch();
assert!(!sw.enabled, "should seed enabled=false from config");
assert!(
sw.cosranger_enabled,
"should seed cosranger=true from config"
);
}
#[test]
fn test_config_refresher_default_creates_with_default_values() {
let refresher = ConfigRefresher::from_config(&GoosefsConfig::default());
let sw = refresher.current_switch();
assert!(
sw.enabled,
"default transparent_acceleration_enabled should be true"
);
assert!(
!sw.cosranger_enabled,
"default cosranger_enabled should be false"
);
}
#[test]
fn test_config_refresher_current_switch_is_lock_free() {
let cfg = GoosefsConfig {
transparent_acceleration_enabled: true,
transparent_acceleration_cosranger_enabled: true,
..Default::default()
};
let refresher = ConfigRefresher::from_config(&cfg);
let sw1 = refresher.current_switch();
let sw2 = refresher.refresh_transparent_acceleration_switch();
assert_eq!(sw1, sw2);
}
#[test]
fn test_config_refresher_only_refreshes_switch_params() {
let user_config = GoosefsConfig {
master_addr: "10.0.0.99:9999".to_string(),
block_size: 128 * 1024 * 1024, chunk_size: 2 * 1024 * 1024, write_type: Some(WritePType::Through as i32),
auth_username: "custom_user".to_string(),
transparent_acceleration_enabled: true,
transparent_acceleration_cosranger_enabled: false,
..Default::default()
};
let refresher = ConfigRefresher::from_config(&user_config);
let switch = refresher.refresh_transparent_acceleration_switch();
assert!(
switch
== TransparentAccelerationSwitch {
enabled: true,
cosranger_enabled: false
}
|| switch
!= TransparentAccelerationSwitch {
enabled: true,
cosranger_enabled: false
},
"switch values are determined by file config, not user config"
);
assert_eq!(user_config.master_addr, "10.0.0.99:9999");
assert_eq!(user_config.block_size, 128 * 1024 * 1024);
assert_eq!(user_config.chunk_size, 2 * 1024 * 1024);
assert_eq!(user_config.write_type, Some(WritePType::Through as i32));
assert_eq!(user_config.auth_username, "custom_user");
}
#[test]
fn test_config_refresher_file_overrides_only_switch_params() {
use std::io::Write;
let dir = std::env::temp_dir().join("goosefs_refresher_test");
let _ = std::fs::create_dir_all(&dir);
let props_path = dir.join(PROPERTIES_FILENAME);
{
let mut f = std::fs::File::create(&props_path).unwrap();
writeln!(
f,
"goosefs.master.hostname=file-host-should-not-affect-user"
)
.unwrap();
writeln!(f, "goosefs.master.rpc.port=1234").unwrap();
writeln!(f, "goosefs.user.block.size.bytes.default=1GB").unwrap();
writeln!(
f,
"goosefs.user.client.transparent_acceleration.enabled=false"
)
.unwrap();
writeln!(
f,
"goosefs.user.client.transparent_acceleration.cosranger.enabled=true"
)
.unwrap();
}
std::env::set_var(ENV_CONFIG_FILE, props_path.to_str().unwrap());
let user_config = GoosefsConfig {
master_addr: "user-master:9200".to_string(),
block_size: 256 * 1024 * 1024,
chunk_size: 4 * 1024 * 1024,
write_type: Some(WritePType::CacheThrough as i32),
auth_username: "my_user".to_string(),
transparent_acceleration_enabled: true, transparent_acceleration_cosranger_enabled: false, ..Default::default()
};
let refresher = ConfigRefresher::from_config(&user_config);
let refresher_immediate = ConfigRefresher {
last_load_time: Mutex::new(None), expire_duration: Duration::from_millis(0),
transparent_acceleration_enabled: AtomicBool::new(
user_config.transparent_acceleration_enabled,
),
cosranger_enabled: AtomicBool::new(
user_config.transparent_acceleration_cosranger_enabled,
),
};
let switch = refresher_immediate.refresh_transparent_acceleration_switch();
assert!(
!switch.enabled,
"switch.enabled should be overridden to false by file config"
);
assert!(
switch.cosranger_enabled,
"switch.cosranger_enabled should be overridden to true by file config"
);
assert_eq!(
user_config.master_addr, "user-master:9200",
"user's master_addr must NOT be affected by config refresh"
);
assert_eq!(
user_config.block_size,
256 * 1024 * 1024,
"user's block_size must NOT be affected by config refresh"
);
assert_eq!(
user_config.chunk_size,
4 * 1024 * 1024,
"user's chunk_size must NOT be affected by config refresh"
);
assert_eq!(
user_config.write_type,
Some(WritePType::CacheThrough as i32),
"user's write_type must NOT be affected by config refresh"
);
assert_eq!(
user_config.auth_username, "my_user",
"user's auth_username must NOT be affected by config refresh"
);
assert!(
user_config.transparent_acceleration_enabled,
"user's original transparent_acceleration_enabled should still be true"
);
assert!(
!user_config.transparent_acceleration_cosranger_enabled,
"user's original cosranger_enabled should still be false"
);
let sw_original = refresher.current_switch();
assert!(
sw_original.enabled,
"non-expired refresher should keep user's enabled=true"
);
assert!(
!sw_original.cosranger_enabled,
"non-expired refresher should keep user's cosranger=false"
);
std::env::remove_var(ENV_CONFIG_FILE);
let _ = std::fs::remove_file(&props_path);
let _ = std::fs::remove_dir(&dir);
}
#[test]
fn test_config_refresher_no_file_keeps_user_values() {
std::env::remove_var(ENV_CONFIG_FILE);
std::env::remove_var(ENV_CONF_DIR);
std::env::remove_var(ENV_HOME);
std::env::remove_var(ENV_TRANSPARENT_ACCELERATION_ENABLED);
std::env::remove_var(ENV_TRANSPARENT_ACCELERATION_COSRANGER_ENABLED);
let user_config = GoosefsConfig {
transparent_acceleration_enabled: false,
transparent_acceleration_cosranger_enabled: true,
..Default::default()
};
let refresher = ConfigRefresher {
last_load_time: Mutex::new(None),
expire_duration: Duration::from_millis(0),
transparent_acceleration_enabled: AtomicBool::new(false),
cosranger_enabled: AtomicBool::new(true),
};
let switch = refresher.refresh_transparent_acceleration_switch();
assert!(
!user_config.transparent_acceleration_enabled,
"user config object is never modified by refresher"
);
assert!(
user_config.transparent_acceleration_cosranger_enabled,
"user config object is never modified by refresher"
);
assert!(
switch.enabled,
"refresher should pick up default enabled=true after reload"
);
assert!(
!switch.cosranger_enabled,
"refresher should pick up default cosranger=false after reload"
);
}
#[test]
fn metrics_defaults_correct() {
let cfg = GoosefsConfig::default();
assert!(
cfg.metrics_enabled,
"metrics_enabled should default to true"
);
assert_eq!(
cfg.metrics_heartbeat_interval,
Duration::from_secs(10),
"metrics_heartbeat_interval should default to 10 s"
);
assert_eq!(
cfg.metrics_heartbeat_timeout,
Duration::from_secs(5),
"metrics_heartbeat_timeout should default to 5 s"
);
assert!(cfg.app_id.is_none(), "app_id should default to None");
assert_eq!(
cfg.metrics_max_batch_size, 1024,
"metrics_max_batch_size should default to 1024"
);
assert!(cfg.validate().is_ok());
}
#[test]
fn metrics_interval_zero_rejected() {
let cfg = GoosefsConfig {
metrics_heartbeat_interval: Duration::from_millis(0),
..Default::default()
};
let err = cfg.validate().unwrap_err();
assert!(
err.contains("metrics_heartbeat_interval"),
"error should mention field name: {err}"
);
}
#[test]
fn metrics_interval_999ms_rejected() {
let cfg = GoosefsConfig {
metrics_heartbeat_interval: Duration::from_millis(999),
..Default::default()
};
assert!(cfg.validate().is_err());
}
#[test]
fn metrics_interval_1000ms_accepted() {
let cfg = GoosefsConfig {
metrics_heartbeat_interval: Duration::from_millis(2000),
metrics_heartbeat_timeout: Duration::from_secs(1),
..Default::default()
};
assert!(cfg.validate().is_ok());
}
#[test]
fn metrics_heartbeat_timeout_below_one_second_rejected() {
let cfg = GoosefsConfig {
metrics_heartbeat_timeout: Duration::from_millis(500),
..Default::default()
};
let err = cfg.validate().unwrap_err();
assert!(
err.contains("metrics_heartbeat_timeout"),
"error should mention field name: {err}"
);
}
#[test]
fn metrics_heartbeat_timeout_equal_to_interval_rejected() {
let cfg = GoosefsConfig {
metrics_heartbeat_interval: Duration::from_secs(10),
metrics_heartbeat_timeout: Duration::from_secs(10),
..Default::default()
};
let err = cfg.validate().unwrap_err();
assert!(
err.contains("must be < metrics_heartbeat_interval"),
"error should explain ordering rule: {err}"
);
}
#[test]
fn metrics_heartbeat_timeout_greater_than_interval_rejected() {
let cfg = GoosefsConfig {
metrics_heartbeat_interval: Duration::from_secs(2),
metrics_heartbeat_timeout: Duration::from_secs(5),
..Default::default()
};
assert!(cfg.validate().is_err());
}
#[test]
fn metrics_heartbeat_timeout_just_below_interval_accepted() {
let cfg = GoosefsConfig {
metrics_heartbeat_interval: Duration::from_secs(10),
metrics_heartbeat_timeout: Duration::from_millis(9_999),
..Default::default()
};
assert!(cfg.validate().is_ok());
}
#[test]
fn with_metrics_heartbeat_timeout_setter_works() {
let cfg = GoosefsConfig::new("127.0.0.1:9200")
.with_metrics_heartbeat_interval(Duration::from_secs(8))
.with_metrics_heartbeat_timeout(Duration::from_secs(3));
assert_eq!(cfg.metrics_heartbeat_timeout, Duration::from_secs(3));
assert!(cfg.validate().is_ok());
}
#[test]
#[should_panic(expected = "metrics_heartbeat_timeout must be >= 1 s")]
fn with_metrics_heartbeat_timeout_panics_below_one_second() {
let _ = GoosefsConfig::new("127.0.0.1:9200")
.with_metrics_heartbeat_timeout(Duration::from_millis(500));
}
#[test]
fn metrics_disabled_via_builder() {
let cfg = GoosefsConfig::new("127.0.0.1:9200")
.with_metrics_enabled(false)
.with_app_id("my-service");
assert!(!cfg.metrics_enabled);
assert_eq!(cfg.app_id.as_deref(), Some("my-service"));
assert!(cfg.validate().is_ok());
}
#[test]
fn metrics_properties_parsing() {
let props = "\
goosefs.user.metrics.collection.enabled=false\n\
goosefs.user.metrics.heartbeat.interval=30000\n\
goosefs.user.app.id=test-app\n";
let cfg = GoosefsConfig::from_properties_str(props);
assert!(!cfg.metrics_enabled);
assert_eq!(cfg.metrics_heartbeat_interval, Duration::from_secs(30));
assert_eq!(cfg.app_id.as_deref(), Some("test-app"));
}
#[test]
fn metrics_properties_interval_too_small_ignored() {
let props = "goosefs.user.metrics.heartbeat.interval=500\n";
let cfg = GoosefsConfig::from_properties_str(props);
assert_eq!(
cfg.metrics_heartbeat_interval,
Duration::from_secs(10),
"sub-1000 ms value should be ignored, keeping default 10 s"
);
}
}