use std::sync::Arc;
use arc_swap::ArcSwap;
use tonic::service::interceptor::InterceptedService;
use tonic::transport::Channel;
use tracing::{debug, instrument, warn};
use crate::auth::{ChannelAuthenticator, ChannelIdInterceptor, SaslStreamGuard};
use crate::client::master_inquire::{create_master_inquire_client, MasterInquireClient};
use crate::config::GoosefsConfig;
use crate::error::{Error, Result};
use crate::fs::options::DeleteOptions;
use crate::metrics::registry::Counter;
use crate::proto::grpc::file::{
file_system_master_client_service_client::FileSystemMasterClientServiceClient,
CompleteFilePOptions, CompleteFilePRequest, CreateDirectoryPOptions, CreateDirectoryPRequest,
CreateFilePOptions, CreateFilePRequest, DeletePOptions, DeletePRequest, FileInfo,
FileSystemMasterCommonPOptions, FsOpPId, GetStatusPOptions, GetStatusPRequest,
ListStatusPOptions, ListStatusPRequest, LoadMetadataPType, RemoveBlocksPRequest,
RenamePOptions, RenamePRequest, ScheduleAsyncPersistencePOptions,
ScheduleAsyncPersistencePRequest,
};
use crate::proto::grpc::{Bits, PMode};
const MAX_RPC_RETRIES: u32 = 2;
type AuthenticatedFsClient =
FileSystemMasterClientServiceClient<InterceptedService<Channel, ChannelIdInterceptor>>;
struct AuthedState {
client: AuthenticatedFsClient,
_sasl_guard: Option<SaslStreamGuard>,
}
pub fn default_dir_mode() -> PMode {
PMode {
owner_bits: Bits::All as i32, group_bits: Bits::ReadExecute as i32, other_bits: Bits::ReadExecute as i32, }
}
pub fn default_file_mode() -> PMode {
PMode {
owner_bits: Bits::ReadWrite as i32, group_bits: Bits::Read as i32, other_bits: Bits::Read as i32, }
}
#[derive(Clone)]
pub struct MasterClient {
state: Arc<ArcSwap<AuthedState>>,
config: GoosefsConfig,
inquire_client: Arc<dyn MasterInquireClient>,
counter_get_status_ops: Arc<Counter>,
counter_get_status_latency_us: Arc<Counter>,
counter_list_status_ops: Arc<Counter>,
counter_list_status_latency_us: Arc<Counter>,
counter_create_file_ops: Arc<Counter>,
counter_create_dir_ops: Arc<Counter>,
counter_delete_ops: Arc<Counter>,
counter_rename_ops: Arc<Counter>,
counter_rpc_errors_total: Arc<Counter>,
counter_rpc_auth_errors: Arc<Counter>,
counter_rpc_unavailable_errors: Arc<Counter>,
}
impl MasterClient {
pub async fn connect(config: &GoosefsConfig) -> Result<Self> {
let inquire_client = create_master_inquire_client(config);
Self::connect_with_inquire(config, inquire_client).await
}
pub async fn connect_with_inquire(
config: &GoosefsConfig,
inquire_client: Arc<dyn MasterInquireClient>,
) -> Result<Self> {
let primary_addr = inquire_client.get_primary_rpc_address().await?;
let (client, sasl_guard) = Self::build_authenticated_client(config, &primary_addr).await?;
debug!(addr = %primary_addr, auth_type = %config.auth_type, "connected to Goosefs Master");
Ok(Self::from_parts(
AuthedState {
client,
_sasl_guard: sasl_guard,
},
config.clone(),
inquire_client,
))
}
fn from_parts(
state: AuthedState,
config: GoosefsConfig,
inquire_client: Arc<dyn MasterInquireClient>,
) -> Self {
Self {
state: Arc::new(ArcSwap::from_pointee(state)),
config,
inquire_client,
counter_get_status_ops: crate::metrics::counter(
crate::metrics::name::CLIENT_GET_STATUS_OPS,
),
counter_get_status_latency_us: crate::metrics::counter(
crate::metrics::name::CLIENT_GET_STATUS_LATENCY_US,
),
counter_list_status_ops: crate::metrics::counter(
crate::metrics::name::CLIENT_LIST_STATUS_OPS,
),
counter_list_status_latency_us: crate::metrics::counter(
crate::metrics::name::CLIENT_LIST_STATUS_LATENCY_US,
),
counter_create_file_ops: crate::metrics::counter(
crate::metrics::name::CLIENT_CREATE_FILE_OPS,
),
counter_create_dir_ops: crate::metrics::counter(
crate::metrics::name::CLIENT_CREATE_DIR_OPS,
),
counter_delete_ops: crate::metrics::counter(crate::metrics::name::CLIENT_DELETE_OPS),
counter_rename_ops: crate::metrics::counter(crate::metrics::name::CLIENT_RENAME_OPS),
counter_rpc_errors_total: crate::metrics::counter(
crate::metrics::name::CLIENT_RPC_ERRORS_TOTAL,
),
counter_rpc_auth_errors: crate::metrics::counter(
crate::metrics::name::CLIENT_RPC_AUTH_ERRORS,
),
counter_rpc_unavailable_errors: crate::metrics::counter(
crate::metrics::name::CLIENT_RPC_UNAVAILABLE_ERRORS,
),
}
}
pub fn from_channel(channel: Channel, config: GoosefsConfig) -> Self {
let inquire_client = create_master_inquire_client(&config);
let interceptor = ChannelIdInterceptor::new("test-no-auth".to_string());
let intercepted = InterceptedService::new(channel, interceptor);
Self::from_parts(
AuthedState {
client: FileSystemMasterClientServiceClient::new(intercepted),
_sasl_guard: None,
},
config,
inquire_client,
)
}
async fn build_authenticated_client(
config: &GoosefsConfig,
addr: &str,
) -> Result<(AuthenticatedFsClient, Option<SaslStreamGuard>)> {
let channel = Self::build_raw_channel(config, addr).await?;
let authenticator = ChannelAuthenticator::new(
config.auth_type,
config.auth_username.clone(),
None, )
.with_auth_timeout(config.auth_timeout);
let mut auth_channel = authenticator.authenticate(channel).await?;
let sasl_guard = auth_channel.take_sasl_guard();
Ok((
FileSystemMasterClientServiceClient::new(auth_channel.channel),
sasl_guard,
))
}
async fn build_raw_channel(config: &GoosefsConfig, addr: &str) -> Result<Channel> {
let endpoint_uri = format!("http://{}", addr);
let endpoint = Channel::from_shared(endpoint_uri)
.map_err(|e| Error::ConfigError {
message: format!("invalid master endpoint: {}", e),
})?
.connect_timeout(config.connect_timeout)
.timeout(config.request_timeout);
let channel = endpoint.connect().await?;
Ok(channel)
}
async fn reconnect(&self) -> Result<()> {
self.inquire_client.reset_cached_primary().await;
let primary_addr = self.inquire_client.get_primary_rpc_address().await?;
let (client, sasl_guard) =
Self::build_authenticated_client(&self.config, &primary_addr).await?;
self.state.store(Arc::new(AuthedState {
client,
_sasl_guard: sasl_guard,
}));
debug!(addr = %primary_addr, "reconnected to Goosefs Master after failover");
Ok(())
}
async fn with_retry<F, Fut, T>(&self, op_name: &str, mut f: F) -> Result<T>
where
F: FnMut(AuthenticatedFsClient) -> Fut,
Fut: std::future::Future<Output = Result<T>>,
{
let mut last_err: Option<Error> = None;
for attempt in 0..=MAX_RPC_RETRIES {
if attempt > 0 {
if let Err(reconnect_err) = self.reconnect().await {
warn!(
op = op_name,
attempt = attempt + 1,
error = %reconnect_err,
"reconnect failed; will retry reconnect on next attempt"
);
last_err = Some(Error::Internal {
message: format!("master reconnect failed: {}", reconnect_err),
source: None,
});
continue;
}
}
let client: AuthenticatedFsClient = self.state.load().client.clone();
match f(client).await {
Ok(result) => return Ok(result),
Err(err) => {
self.counter_rpc_errors_total.inc(1);
if err.is_authentication_error() {
self.counter_rpc_auth_errors.inc(1);
} else if err.is_unavailable() {
self.counter_rpc_unavailable_errors.inc(1);
}
if err.is_retriable() && attempt < MAX_RPC_RETRIES {
warn!(
op = op_name,
attempt = attempt + 1,
max = MAX_RPC_RETRIES,
error = %err,
"retriable error; will reconnect and retry"
);
last_err = Some(err);
} else {
return Err(err);
}
}
}
}
Err(last_err.unwrap_or_else(|| Error::Internal {
message: format!("{}: exhausted all retries", op_name),
source: None,
}))
}
#[instrument(skip(self), fields(path = %path))]
pub async fn get_status(&self, path: &str) -> Result<FileInfo> {
let start = std::time::Instant::now();
let mut path_owned: Option<String> = Some(path.to_string());
let result = self
.with_retry("get_status", |mut client| {
let req_path = path_owned.take().unwrap_or_else(|| path.to_string());
async move {
let req = GetStatusPRequest {
path: Some(req_path),
options: Some(GetStatusPOptions::default()),
request_id: None,
};
let resp = client.get_status(req).await?;
resp.into_inner()
.file_info
.ok_or_else(|| Error::missing_field("file_info"))
}
})
.await;
self.counter_get_status_ops.inc(1);
self.counter_get_status_latency_us
.inc(start.elapsed().as_micros() as i64);
result
}
#[instrument(skip(self), fields(path = %path))]
pub async fn list_status(&self, path: &str, recursive: bool) -> Result<Vec<FileInfo>> {
let start = std::time::Instant::now();
let path = path.to_string();
let load_metadata_type = if recursive {
Some(LoadMetadataPType::Always as i32)
} else {
None
};
let result = self
.with_retry("list_status", |mut client| {
let path = path.clone();
async move {
let req = ListStatusPRequest {
path: Some(path),
options: Some(ListStatusPOptions {
recursive: Some(recursive),
load_metadata_type,
..Default::default()
}),
request_id: None,
};
let mut stream = client.list_status(req).await?.into_inner();
let mut result = Vec::new();
while let Some(resp) = stream.message().await? {
result.extend(resp.file_infos);
}
Ok(result)
}
})
.await;
self.counter_list_status_ops.inc(1);
self.counter_list_status_latency_us
.inc(start.elapsed().as_micros() as i64);
result
}
#[instrument(skip(self, options), fields(path = %path))]
pub async fn create_file(&self, path: &str, options: CreateFilePOptions) -> Result<FileInfo> {
let path = path.to_string();
let result = self
.with_retry("create_file", |mut client| {
let path = path.clone();
let options = options.clone();
async move {
let req = CreateFilePRequest {
path: Some(path),
options: Some(options),
};
let resp = client.create_file(req).await?;
resp.into_inner()
.file_info
.ok_or_else(|| Error::missing_field("file_info"))
}
})
.await;
self.counter_create_file_ops.inc(1);
result
}
#[instrument(skip(self), fields(path = %path))]
pub async fn complete_file(
&self,
path: &str,
ufs_length: Option<i64>,
operation_id: Option<FsOpPId>,
) -> Result<()> {
let path = path.to_string();
self.with_retry("complete_file", |mut client| {
let path = path.clone();
async move {
let common_options = operation_id.map(|op_id| FileSystemMasterCommonPOptions {
operation_id: Some(op_id),
..Default::default()
});
let req = CompleteFilePRequest {
path: Some(path),
options: Some(CompleteFilePOptions {
ufs_length,
common_options,
..Default::default()
}),
inode_id: None,
};
client.complete_file(req).await?;
Ok(())
}
})
.await
}
#[instrument(skip(self, block_ids), fields(block_count = block_ids.len()))]
pub async fn remove_blocks(&self, block_ids: Vec<i64>) -> Result<()> {
if block_ids.is_empty() {
return Ok(());
}
let block_ids_clone = block_ids.clone();
self.with_retry("remove_blocks", |mut client| {
let block_ids = block_ids_clone.clone();
async move {
let req = RemoveBlocksPRequest { block_ids };
client.remove_blocks(req).await?;
Ok(())
}
})
.await
}
#[instrument(skip(self, opts), fields(path = %path))]
pub async fn delete_with_options(&self, path: &str, opts: DeleteOptions) -> Result<()> {
let path = path.to_string();
self.with_retry("delete_with_options", |mut client| {
let path = path.clone();
let opts = opts.clone();
async move {
let req = DeletePRequest {
path: Some(path),
options: Some(DeletePOptions {
recursive: Some(opts.recursive),
unchecked: Some(opts.unchecked),
goosefs_only: Some(opts.goosefs_only),
..Default::default()
}),
};
client.remove(req).await?;
Ok(())
}
})
.await
}
#[instrument(skip(self), fields(path = %path, recursive = %recursive))]
pub async fn delete(&self, path: &str, recursive: bool) -> Result<()> {
let result = self
.delete_with_options(
path,
DeleteOptions {
recursive,
..Default::default()
},
)
.await;
self.counter_delete_ops.inc(1);
result
}
#[instrument(skip(self), fields(src = %src, dst = %dst))]
pub async fn rename(&self, src: &str, dst: &str) -> Result<()> {
let src = src.to_string();
let dst = dst.to_string();
let result = self
.with_retry("rename", |mut client| {
let src = src.clone();
let dst = dst.clone();
async move {
let req = RenamePRequest {
path: Some(src),
dst_path: Some(dst),
options: Some(RenamePOptions::default()),
};
client.rename(req).await?;
Ok(())
}
})
.await;
self.counter_rename_ops.inc(1);
result
}
#[instrument(skip(self), fields(path = %path))]
pub async fn create_directory(&self, path: &str, recursive: bool) -> Result<()> {
let path = path.to_string();
let result = self
.with_retry("create_directory", |mut client| {
let path = path.clone();
async move {
let req = CreateDirectoryPRequest {
path: Some(path),
options: Some(CreateDirectoryPOptions {
recursive: Some(recursive),
allow_exists: Some(true),
mode: Some(default_dir_mode()),
..Default::default()
}),
};
client.create_directory(req).await?;
Ok(())
}
})
.await;
self.counter_create_dir_ops.inc(1);
result
}
#[instrument(skip(self), fields(path = %path))]
pub async fn schedule_async_persistence(
&self,
path: &str,
persistence_wait_time: Option<i64>,
) -> Result<()> {
let path = path.to_string();
self.with_retry("schedule_async_persistence", |mut client| {
let path = path.clone();
async move {
let req = ScheduleAsyncPersistencePRequest {
path: Some(path),
options: Some(ScheduleAsyncPersistencePOptions {
common_options: None,
persistence_wait_time,
}),
};
client.schedule_async_persistence(req).await?;
Ok(())
}
})
.await
}
pub fn config(&self) -> &GoosefsConfig {
&self.config
}
pub fn inquire_client(&self) -> &Arc<dyn MasterInquireClient> {
&self.inquire_client
}
}
use std::sync::atomic::{AtomicUsize, Ordering};
pub struct MasterClientPool {
clients: Vec<Arc<MasterClient>>,
next: AtomicUsize,
}
impl MasterClientPool {
pub async fn connect_with_inquire(
config: &GoosefsConfig,
inquire_client: Arc<dyn MasterInquireClient>,
) -> Result<Self> {
let size = config.master_connection_pool_size.max(1);
let mut clients = Vec::with_capacity(size);
for _ in 0..size {
let client = MasterClient::connect_with_inquire(config, inquire_client.clone()).await?;
clients.push(Arc::new(client));
}
debug!(pool_size = size, "MasterClientPool connected");
Ok(Self {
clients,
next: AtomicUsize::new(0),
})
}
pub fn pick(&self) -> Arc<MasterClient> {
if self.clients.len() == 1 {
return self.clients[0].clone();
}
let i = self.next.fetch_add(1, Ordering::Relaxed) % self.clients.len();
self.clients[i].clone()
}
pub fn size(&self) -> usize {
self.clients.len()
}
}
#[cfg(test)]
mod tests {
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::Arc;
use std::thread;
use std::time::{Duration, Instant};
use arc_swap::ArcSwap;
struct AuthedStateLike {
epoch: u64,
guard_epoch: Arc<u64>,
drop_counter: Arc<AtomicUsize>,
}
impl Drop for AuthedStateLike {
fn drop(&mut self) {
self.drop_counter.fetch_add(1, Ordering::SeqCst);
}
}
fn new_state(epoch: u64, drop_counter: Arc<AtomicUsize>) -> Arc<AuthedStateLike> {
Arc::new(AuthedStateLike {
epoch,
guard_epoch: Arc::new(epoch),
drop_counter,
})
}
#[test]
fn arcswap_publication_is_atomic_under_concurrent_readers() {
const READERS: usize = 32;
const RECONNECT_ROUNDS: usize = 200;
let drop_counter = Arc::new(AtomicUsize::new(0));
let state = Arc::new(ArcSwap::from(new_state(0, drop_counter.clone())));
let stop = Arc::new(AtomicBool::new(false));
let mut readers = Vec::with_capacity(READERS);
for _ in 0..READERS {
let state = state.clone();
let stop = stop.clone();
readers.push(thread::spawn(move || {
let mut observed_epochs: Vec<u64> = Vec::new();
while !stop.load(Ordering::Relaxed) {
let snap = state.load();
assert_eq!(
snap.epoch, *snap.guard_epoch,
"torn read: ArcSwap published a half-swapped snapshot",
);
observed_epochs.push(snap.epoch);
}
observed_epochs
}));
}
for round in 1..=RECONNECT_ROUNDS {
state.store(new_state(round as u64, drop_counter.clone()));
thread::sleep(Duration::from_micros(50));
}
stop.store(true, Ordering::Relaxed);
for r in readers {
let observed = r.join().expect("reader thread panicked");
assert!(!observed.is_empty(), "reader observed nothing");
let max = observed.iter().copied().max().unwrap();
assert!(
max >= 1,
"reader never saw a reconnect-published epoch (max={})",
max,
);
}
}
#[test]
fn old_snapshot_outlives_concurrent_swap_until_reader_releases() {
let drop_counter = Arc::new(AtomicUsize::new(0));
let state = Arc::new(ArcSwap::from(new_state(1, drop_counter.clone())));
let held = state.load_full();
assert_eq!(held.epoch, 1);
for round in 2..=50 {
state.store(new_state(round, drop_counter.clone()));
}
let observed_drops = drop_counter.load(Ordering::SeqCst);
assert!(
observed_drops <= 48,
"old snapshot was dropped while a reader still held it: \
drops = {} (expected <= 48)",
observed_drops,
);
assert_eq!(held.epoch, 1, "held snapshot was mutated in place");
drop(held);
state.store(new_state(999, drop_counter.clone()));
let deadline = Instant::now() + Duration::from_secs(2);
loop {
if drop_counter.load(Ordering::SeqCst) >= 50 {
break;
}
if Instant::now() > deadline {
panic!(
"expected >= 50 drops after releasing the held snapshot, \
observed {}",
drop_counter.load(Ordering::SeqCst),
);
}
thread::sleep(Duration::from_millis(5));
}
}
}