use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use tracing::{debug, info, warn};
use uuid::Uuid;
use crate::block::router::{rpc_endpoint, WorkerRouterView};
use crate::client::master::default_file_mode;
use crate::client::worker::{WorkerClientPool, WriteBlockOptions};
use crate::client::{CompleteFileOptions, MasterClient};
use crate::config::{GoosefsConfig, NO_AUTO_PERSIST};
use crate::context::FileSystemContext;
use crate::error::{Error, Result};
use crate::fs::options::DeleteOptions;
use crate::io::replica_write::{
cache_min_ratio, degrade_replicas, enough_replicas, filter_no_space_workers,
replica_write_plan, should_abort_remaining, ReplicaWritePlan,
};
use crate::io::writer::{owned_chunk, GrpcBlockWriter};
use crate::proto::grpc::block::{RequestType, WorkerInfo};
use crate::proto::grpc::file::{
CreateFilePOptions, FileInfo, FsOpPId, LoadMetadataPType, ScheduleAsyncPersistencePOptions,
};
use crate::proto::grpc::WorkerNetAddress;
use crate::proto::proto::dataserver::CreateUfsFileOptions;
use crate::proto::proto::shared::FileLocation;
#[derive(Clone, Debug)]
struct WriteStrategy {
cache_stream: bool,
ufs_stream: bool,
create_ufs_file_options: Option<CreateUfsFileOptions>,
need_async_persist: bool,
}
fn resolve_write_strategy(write_type: Option<i32>, file_info: &FileInfo) -> WriteStrategy {
let build_ufs_opts = || CreateUfsFileOptions {
ufs_path: file_info.ufs_path.clone(),
owner: file_info.owner.clone(),
group: file_info.group.clone(),
mode: file_info.mode,
mount_id: file_info.mount_id,
acl: None,
};
match write_type {
Some(3) => WriteStrategy {
cache_stream: true,
ufs_stream: true,
create_ufs_file_options: Some(build_ufs_opts()),
need_async_persist: false,
},
Some(4) => WriteStrategy {
cache_stream: false,
ufs_stream: true,
create_ufs_file_options: Some(build_ufs_opts()),
need_async_persist: false,
},
Some(5) => WriteStrategy {
cache_stream: true,
ufs_stream: false,
create_ufs_file_options: Some(build_ufs_opts()),
need_async_persist: true,
},
_ => WriteStrategy {
cache_stream: true,
ufs_stream: false,
create_ufs_file_options: None,
need_async_persist: false,
},
}
}
fn needs_fresh_block(remaining_in_current: Option<u64>) -> bool {
match remaining_in_current {
None => true,
Some(remaining) => remaining == 0,
}
}
fn should_flush_cache_block_on_close(commit_location: bool) -> bool {
commit_location
}
fn uuid_to_fs_op_pid(id: Uuid) -> FsOpPId {
let (high, low) = id.as_u64_pair();
FsOpPId {
most_significant_bits: Some(high as i64),
least_significant_bits: Some(low as i64),
}
}
pub struct GoosefsFileWriter {
config: GoosefsConfig,
path: String,
master: MasterClient,
router: WorkerRouterView,
worker_pool: Arc<WorkerClientPool>,
_context: Option<Arc<FileSystemContext>>,
file_info: FileInfo,
total_bytes_written: u64,
operation_id: Uuid,
cancelled: AtomicBool,
closed: AtomicBool,
write_strategy: WriteStrategy,
should_cache: bool,
ufs_write_enabled: bool,
block_opened: bool,
committed_block_ids: Vec<i64>,
current_block_writer: Option<ActiveBlockWriter>,
ufs_stream: Option<GrpcBlockWriter>,
ufs_worker_addr: Option<String>,
ufs_stream_completed: AtomicBool,
_router_needs_init: AtomicBool,
}
impl GoosefsFileWriter {
pub async fn create_with_context(
ctx: Arc<FileSystemContext>,
path: &str,
options: Option<CreateFilePOptions>,
) -> Result<Self> {
let config = ctx.config().clone();
let master_arc = ctx.acquire_master();
let mut create_options = options.unwrap_or_default();
if create_options.recursive.is_none() {
create_options.recursive = Some(true);
}
if create_options.block_size_bytes.is_none() || create_options.block_size_bytes == Some(0) {
create_options.block_size_bytes = Some(config.block_size as i64);
}
if create_options.mode.is_none() {
create_options.mode = Some(default_file_mode());
}
if create_options.write_type.is_none() {
create_options.write_type = config.write_type;
}
let file_info = master_arc.create_file(path, create_options).await?;
debug!(
path = %path,
file_id = ?file_info.file_id,
"file created on Master (via context)"
);
ctx.invalidate_file_info(path);
let write_strategy = resolve_write_strategy(create_options.write_type, &file_info);
let worker_pool = ctx.acquire_worker_pool();
let router = WorkerRouterView::empty();
let operation_id = Uuid::new_v4();
let master = (*master_arc).clone();
let mut writer = Self {
config,
path: path.to_string(),
master,
router,
worker_pool,
_context: Some(ctx), file_info,
total_bytes_written: 0,
operation_id,
cancelled: AtomicBool::new(false),
closed: AtomicBool::new(false),
should_cache: write_strategy.cache_stream,
ufs_write_enabled: write_strategy.ufs_stream,
block_opened: false,
write_strategy,
committed_block_ids: Vec::new(),
current_block_writer: None,
ufs_stream: None,
ufs_worker_addr: None,
ufs_stream_completed: AtomicBool::new(false),
_router_needs_init: AtomicBool::new(true),
};
if writer.write_strategy.ufs_stream {
let opened = match writer.ensure_router_init().await {
Ok(()) => writer.open_ufs_stream().await,
Err(e) => Err(e),
};
if let Err(e) = opened {
warn!(
path = %path,
error = %e,
"failed to open the UFS stream during create; \
the INCOMPLETE inode is left for a retry to reuse"
);
writer.cancelled.store(true, Ordering::SeqCst);
return Err(e);
}
}
Ok(writer)
}
async fn ensure_router_init(&mut self) -> Result<()> {
if !self._router_needs_init.load(Ordering::Acquire) {
return Ok(());
}
debug_assert!(
self._context.is_some(),
"`_context` must be set in production paths"
);
if let Some(ctx) = &self._context {
let shared = ctx.acquire_router();
if shared.get_workers().await.is_empty() {
return Err(Error::NoWorkerAvailable {
message: "no workers available for writing".to_string(),
});
}
self.router = WorkerRouterView::from_shared(&shared);
self._router_needs_init.store(false, Ordering::Release);
}
Ok(())
}
pub async fn write(&mut self, data: &[u8]) -> Result<()> {
if self.cancelled.load(Ordering::SeqCst) || self.closed.load(Ordering::SeqCst) {
return Err(Error::BlockIoError {
message: "cannot write to a completed or cancelled file".to_string(),
});
}
if data.is_empty() {
return Ok(());
}
self.ensure_router_init().await?;
if self.should_cache {
if let Err(e) = self.write_to_cache_stream(data).await {
self.handle_cache_write_exception(e).await?;
}
}
if self.ufs_write_enabled {
self.write_to_ufs_stream(data).await?;
}
self.total_bytes_written += data.len() as u64;
Ok(())
}
pub async fn flush(&mut self) -> Result<()> {
if self.cancelled.load(Ordering::SeqCst) || self.closed.load(Ordering::SeqCst) {
return Err(Error::BlockIoError {
message: "cannot flush a completed or cancelled file".to_string(),
});
}
if let Some(ufs) = self.ufs_stream.as_mut() {
ufs.flush().await?;
}
if self.write_strategy.need_async_persist && self.config.file_async_persist_flush_enabled {
if let Some(active) = self.current_block_writer.as_mut() {
if active.bytes_written > 0 {
let tail = std::mem::take(&mut active.pending_chunk);
if !tail.is_empty() {
active.write_chunk(tail).await?;
}
active.flush_replicas().await?;
}
}
}
Ok(())
}
async fn write_to_cache_stream(&mut self, data: &[u8]) -> Result<()> {
let block_size = self
.file_info
.block_size_bytes
.unwrap_or(self.config.block_size as i64) as u64;
let chunk_size = self.config.chunk_size as usize;
crate::metrics::counter(crate::metrics::name::CLIENT_BYTES_WRITTEN_LOCAL)
.inc(data.len() as i64);
let mut offset = 0usize;
while offset < data.len() {
if needs_fresh_block(self.current_block_writer.as_ref().map(|w| w.remaining())) {
self.open_next_block(block_size).await?;
}
let emit_result;
{
let writer = self.current_block_writer.as_mut().unwrap();
let remaining_in_block = writer.remaining() as usize;
let remaining_data = data.len() - offset;
let to_accept = remaining_in_block.min(remaining_data);
let slice = &data[offset..offset + to_accept];
writer.bytes_written += to_accept as u64;
offset += to_accept;
emit_result = emit_aligned_chunks(writer, slice, chunk_size).await;
}
emit_result?;
}
Ok(())
}
async fn write_to_ufs_stream(&mut self, data: &[u8]) -> Result<()> {
if self.ufs_stream.is_none() {
self.open_ufs_stream().await?;
}
let chunk_size = self.config.chunk_size as usize;
let ufs = self
.ufs_stream
.as_mut()
.expect("ufs_stream just opened above");
let total = data.len();
match ufs.write_all(data, chunk_size).await {
Ok(()) => {
crate::metrics::counter(crate::metrics::name::CLIENT_BYTES_WRITTEN_UFS)
.inc(total as i64);
Ok(())
}
Err(e) => self.handle_ufs_write_exception(e).await,
}
}
async fn open_next_block(&mut self, block_size: u64) -> Result<()> {
if self.current_block_writer.is_some() {
self.close_current_block(true).await?;
}
let file_id = self.file_info.file_id.unwrap_or(0);
let block_index = self.committed_block_ids.len() as u64;
let block_id = compute_block_id(file_id, block_index);
let async_through = self.write_strategy.need_async_persist;
let plan = replica_write_plan(
async_through,
self.config.file_replication_number,
self.config.file_replication_durable,
self.config.file_replication_durable_min,
self.config.file_write_max_node_retry,
)?;
match self
.open_replica_writers(block_id, block_size, &plan, false)
.await
{
Ok(active) => {
self.current_block_writer = Some(active);
self.block_opened = true;
Ok(())
}
Err(e) => {
warn!(
block_id = block_id,
error = %e,
"failed to open block with hash-picked workers, retrying with all workers"
);
let active = self
.open_replica_writers(block_id, block_size, &plan, true)
.await?;
self.current_block_writer = Some(active);
self.block_opened = true;
Ok(())
}
}
}
async fn open_replica_writers(
&mut self,
block_id: i64,
block_size: u64,
plan: &ReplicaWritePlan,
use_all_workers: bool,
) -> Result<ActiveBlockWriter> {
let async_through = self.write_strategy.need_async_persist;
let mut pool = if use_all_workers {
(*self.router.all_workers()).clone()
} else {
self.router
.select_workers(block_id, plan.max_retry_node)
.await?
};
pool = self.router.filter_not_failed(&pool);
if async_through {
let allow_fallback = block_sequence_number(block_id) > 0;
pool = filter_no_space_workers(
&pool,
allow_fallback,
plan.min_needed_replicas,
self.config.block_worker_available_min_remain_bytes as i64,
self.config.block_worker_available_min_remain_ratio,
cache_min_ratio(self.config.worker_read_cache_min_ratio),
);
}
if pool.is_empty() {
debug!(
block_id = block_id,
"no available GooseFS worker after filtering; \
clearing failed-worker set so the retry can re-pick"
);
self.router.clear_failed();
return Err(Error::NoWorkerAvailable {
message: format!("no available GooseFS worker for block_id={block_id}"),
});
}
let (initial, min_needed) = degrade_replicas(
async_through,
plan.initial_replicas,
plan.min_needed_replicas,
pool.len(),
);
let mut opened: Vec<ReplicaWriter> = Vec::new();
let mut last_open_err: Option<Error> = None;
for worker_info in pool {
if opened.len() >= initial {
break;
}
match self
.try_open_replica(block_id, block_size, &worker_info, opened.len())
.await
{
Ok(r) => opened.push(r),
Err(e) => {
warn!(
block_id = block_id,
error = %e,
"meet block worker exception while opening replica"
);
if let Some(addr) = &worker_info.address {
self.router.mark_failed(addr);
self.worker_pool.invalidate(&rpc_endpoint(addr)).await;
}
last_open_err = Some(e);
if initial == 1 && opened.is_empty() {
continue;
}
}
}
}
let worker_count = opened.len();
if worker_count == 0 || worker_count < min_needed {
for r in opened {
r.writer.cancel().await;
}
if worker_count == 0 {
return Err(last_open_err.unwrap_or_else(|| Error::NoWorkerAvailable {
message: format!("no available GooseFS worker for block_id={block_id}"),
}));
}
return Err(Error::ResourceExhausted {
message: format!(
"Not enough workers for replications of block {block_id}, {worker_count} workers selected but {min_needed} required"
),
});
}
debug!(
block_id = block_id,
replicas = worker_count,
min_needed = min_needed,
parallel = async_through && worker_count > 1,
"opened cache block replica writers"
);
Ok(ActiveBlockWriter {
replicas: opened,
block_id,
block_size,
bytes_written: 0,
pending_chunk: Vec::with_capacity(self.config.chunk_size as usize),
parallel: async_through && worker_count > 1,
min_needed,
})
}
async fn try_open_replica(
&self,
block_id: i64,
block_size: u64,
worker_info: &WorkerInfo,
ordinal: usize,
) -> Result<ReplicaWriter> {
let addr = worker_info
.address
.as_ref()
.ok_or_else(|| Error::Internal {
message: "worker has no address".to_string(),
source: None,
})?;
let worker_addr = rpc_endpoint(addr);
let worker = self.worker_pool.acquire(&worker_addr).await?;
let write_opts = WriteBlockOptions {
request_type: RequestType::GoosefsBlock,
create_ufs_file_options: None,
async_write: self.write_strategy.need_async_persist,
};
let writer =
GrpcBlockWriter::open(&worker, block_id, block_size as i64, write_opts).await?;
Ok(ReplicaWriter {
ordinal,
writer,
worker_id: worker_info.id.unwrap_or(0),
worker_addr,
net_address: addr.clone(),
})
}
async fn close_current_block(&mut self, commit_location: bool) -> Result<Option<FileLocation>> {
let Some(mut active) = self.current_block_writer.take() else {
return Ok(None);
};
let block_id = active.block_id;
let bytes_written = active.bytes_written;
let pending_chunk = std::mem::take(&mut active.pending_chunk);
let block_offset = (self.committed_block_ids.len() as i64) * (active.block_size as i64);
if bytes_written > 0 {
if !pending_chunk.is_empty() {
if let Err(e) = active.write_chunk(pending_chunk).await {
active.cancel_replicas().await;
return Err(e);
}
}
if should_flush_cache_block_on_close(commit_location) {
if let Err(e) = active.flush_replicas().await {
warn!(
block_id = block_id,
error = %e,
"flush failed during close_current_block; cancelling replica streams"
);
active.cancel_replicas().await;
return Err(e);
}
debug!(
block_id = block_id,
bytes_written = bytes_written,
replicas = active.replicas.len(),
"cache block flushed (mid-file switch)"
);
} else {
debug!(
block_id = block_id,
bytes_written = bytes_written,
replicas = active.replicas.len(),
"cache last-block close without flush:true (Java-aligned)"
);
}
let loc = active.file_location(block_offset);
if let Err(e) = active.close_replicas().await {
warn!(
block_id = block_id,
error = %e,
"close failed during close_current_block; \
recording block_id for cancel-cleanup remove_blocks"
);
self.committed_block_ids.push(block_id);
return Err(e);
}
if commit_location && self.write_strategy.need_async_persist {
if let Some(ref loc) = loc {
if let Err(e) = self
.master
.commit_location(
&self.path,
self.file_info.file_id,
block_id,
vec![loc.clone()],
)
.await
{
warn!(
block_id = block_id,
error = %e,
"commitLocation failed after block close"
);
return Err(e);
}
}
}
self.committed_block_ids.push(block_id);
Ok(loc)
} else {
active.cancel_replicas().await;
Ok(None)
}
}
async fn open_ufs_stream(&mut self) -> Result<()> {
const UFS_BLOCK_ID: i64 = -1; const UFS_STREAM_LENGTH: i64 = i64::MAX;
let worker_info = self.router.pick_any_worker().await?;
let addr = worker_info
.address
.as_ref()
.ok_or_else(|| Error::Internal {
message: "ufs-stream worker has no address".to_string(),
source: None,
})?;
let worker_addr = rpc_endpoint(addr);
debug!(
worker = %worker_addr,
path = %self.path,
"opening UFS stream for CACHE_THROUGH/THROUGH"
);
let worker = match self.worker_pool.acquire(&worker_addr).await {
Ok(w) => w,
Err(e) => {
self.router.mark_failed(addr);
self.worker_pool.invalidate(&worker_addr).await;
return Err(e);
}
};
let write_opts = WriteBlockOptions {
request_type: RequestType::UfsFile,
create_ufs_file_options: self.write_strategy.create_ufs_file_options.clone(),
async_write: false,
};
let writer =
match GrpcBlockWriter::open(&worker, UFS_BLOCK_ID, UFS_STREAM_LENGTH, write_opts).await
{
Ok(w) => w,
Err(e) => {
self.router.mark_failed(addr);
self.worker_pool.invalidate(&worker_addr).await;
return Err(e);
}
};
self.ufs_stream = Some(writer);
self.ufs_worker_addr = Some(worker_addr);
Ok(())
}
async fn handle_cache_write_exception(&mut self, err: Error) -> Result<()> {
warn!(
path = %self.path,
error = %err,
block_opened = self.block_opened,
"failed to write into the Goosefs cache"
);
let credentials_rejected = matches!(
err,
Error::AuthenticationFailed { .. } | Error::PermissionDenied { .. }
);
let fatal = cache_write_failure_is_fatal(
&err,
&self.write_strategy,
self.block_opened,
self.config
.local_ufs_client_ignore_block_stream_unknown_status,
);
self.tear_down_cache_block(!credentials_rejected).await;
if fatal {
self.cancelled.store(true, Ordering::SeqCst);
return Err(err);
}
warn!(
path = %self.path,
"degrading to a UFS-only write for the rest of this file"
);
crate::metrics::counter(crate::metrics::name::CLIENT_WRITE_DEGRADED_TO_UFS).inc(1);
self.should_cache = false;
self.ufs_write_enabled = true;
Ok(())
}
async fn tear_down_cache_block(&mut self, blacklist: bool) {
if let Some(active) = self.current_block_writer.take() {
if blacklist {
for r in &active.replicas {
self.router.mark_failed(&r.net_address);
self.worker_pool.invalidate(&r.worker_addr).await;
}
}
active.cancel_replicas().await;
}
}
async fn handle_ufs_write_exception(&mut self, err: Error) -> Result<()> {
warn!(
path = %self.path,
error = %err,
"failed to write to UFS stream"
);
if let Some(writer) = self.ufs_stream.take() {
writer.cancel().await;
}
if let Some(worker_addr) = self.ufs_worker_addr.take() {
let host = worker_addr
.split(':')
.next()
.unwrap_or("unknown")
.to_string();
let port = worker_addr.split(':').nth(1).and_then(|p| p.parse().ok());
self.router
.mark_failed(&crate::proto::grpc::WorkerNetAddress {
host: Some(host),
rpc_port: port,
..Default::default()
});
self.worker_pool.invalidate(&worker_addr).await;
}
Err(err)
}
async fn do_cancel_cleanup(&mut self) {
if let Some(writer) = self.ufs_stream.take() {
writer.cancel().await;
}
self.ufs_worker_addr = None;
if let Some(active) = self.current_block_writer.take() {
active.cancel_replicas().await;
}
if !self.committed_block_ids.is_empty() {
let block_ids = self.committed_block_ids.clone();
debug!(
path = %self.path,
block_count = block_ids.len(),
"cancel: calling remove_blocks on Master"
);
if let Err(e) = self.master.remove_blocks(block_ids).await {
warn!(
path = %self.path,
error = %e,
"remove_blocks failed, falling back to delete(unchecked=true)"
);
if let Err(del_err) = self
.master
.delete_with_options(&self.path, DeleteOptions::for_cancel())
.await
{
warn!(
path = %self.path,
error = %del_err,
"fallback delete also failed — blocks may need manual cleanup"
);
}
}
}
}
pub async fn cancel(&mut self) -> Result<()> {
if self.closed.load(Ordering::SeqCst) {
return Ok(());
}
if self.cancelled.swap(true, Ordering::SeqCst) {
return Ok(());
}
self.do_cancel_cleanup().await;
if let Some(ctx) = &self._context {
ctx.invalidate_file_info(&self.path);
}
info!(
path = %self.path,
committed_blocks = self.committed_block_ids.len(),
"file write cancelled"
);
Ok(())
}
async fn handle_complete_file_error(&mut self, err: Error) -> Result<()> {
let persistable = self.write_strategy.ufs_stream || self.write_strategy.need_async_persist;
if !persistable || !self.ufs_stream_completed.load(Ordering::SeqCst) {
return Err(err);
}
warn!(
path = %self.path,
error = %err,
"completeFile failed after UFS close succeeded; attempting UFS metadata recovery"
);
if let Err(del_err) = self
.master
.delete_with_options(&self.path, DeleteOptions::goosefs_only_unchecked())
.await
{
warn!(
path = %self.path,
error = %del_err,
"recovery step 1/2 (delete goosefs-only) failed — \
manual cleanup may be required"
);
return Err(err);
}
if let Err(reload_err) = self
.master
.get_status_with_load_type(&self.path, Some(LoadMetadataPType::Always), Some(0))
.await
{
warn!(
path = %self.path,
error = %reload_err,
"recovery step 2/2 (loadMetadata ALWAYS) failed — \
the UFS file exists but Goosefs cannot see it yet"
);
return Err(err);
}
warn!(
path = %self.path,
error = %err,
"completeFile failed but the file was recovered from UFS; \
treating the write as successful"
);
if let Some(ctx) = &self._context {
ctx.invalidate_file_info(&self.path);
}
Ok(())
}
pub async fn close(&mut self) -> Result<()> {
if self
.closed
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
.is_err()
{
warn!(path = %self.path, "close() called on already-completed file");
return Ok(());
}
if self.cancelled.load(Ordering::SeqCst) {
return Ok(());
}
if let Some(mut ufs) = self.ufs_stream.take() {
if let Err(e) = ufs.flush().await {
warn!(
path = %self.path,
error = %e,
"failed to flush UFS stream during close, cancelling"
);
ufs.cancel().await;
self.do_cancel_cleanup().await;
return Err(e);
}
if let Err(e) = ufs.close().await {
warn!(
path = %self.path,
error = %e,
"failed to close UFS stream during close, cancelling"
);
self.do_cancel_cleanup().await;
return Err(e);
}
self.ufs_stream_completed.store(true, Ordering::SeqCst);
self.ufs_worker_addr = None;
}
let last_location = match self.close_current_block(false).await {
Ok(loc) => loc,
Err(e) => {
warn!(
path = %self.path,
error = %e,
"failed to close current block during file close, cancelling"
);
self.do_cancel_cleanup().await;
return Err(e);
}
};
let ufs_length = Some(self.total_bytes_written as i64);
let op_id = uuid_to_fs_op_pid(self.operation_id);
let locations =
complete_file_locations(self.write_strategy.need_async_persist, last_location);
let (force_persisted, async_persist_options) = resolve_persist_options(
self.write_strategy.need_async_persist,
self.ufs_stream_completed.load(Ordering::SeqCst),
self.config.file_persistence_initial_wait_time_ms,
);
if let Err(e) = self
.master
.complete_file_with_options(
&self.path,
CompleteFileOptions {
ufs_length,
operation_id: Some(op_id),
locations,
async_persist_options,
force_persisted,
},
)
.await
{
self.handle_complete_file_error(e).await?;
}
info!(
path = %self.path,
total_bytes = self.total_bytes_written,
cache_blocks = self.committed_block_ids.len(),
ufs_stream = self.write_strategy.ufs_stream,
"file write completed"
);
if let Some(ctx) = &self._context {
ctx.invalidate_file_info(&self.path);
}
Ok(())
}
pub async fn write_file_with_context(
ctx: Arc<FileSystemContext>,
path: &str,
data: &[u8],
) -> Result<u64> {
Self::write_file_with_context_and_options(ctx, path, data, None).await
}
pub async fn write_file_with_context_and_options(
ctx: Arc<FileSystemContext>,
path: &str,
data: &[u8],
options: Option<CreateFilePOptions>,
) -> Result<u64> {
let mut writer = Self::create_with_context(ctx, path, options).await?;
writer.write(data).await?;
writer.close().await?;
Ok(writer.total_bytes_written)
}
pub fn bytes_written(&self) -> u64 {
self.total_bytes_written
}
pub fn path(&self) -> &str {
&self.path
}
pub fn is_completed(&self) -> bool {
self.closed.load(Ordering::SeqCst) && !self.cancelled.load(Ordering::SeqCst)
}
pub fn is_cancelled(&self) -> bool {
self.cancelled.load(Ordering::SeqCst)
}
pub fn file_info(&self) -> &FileInfo {
&self.file_info
}
}
fn compute_block_id(file_id: i64, block_index: u64) -> i64 {
const CONTAINER_ID_BITS: u32 = 40;
const SEQUENCE_NUMBER_BITS: u32 = 64 - CONTAINER_ID_BITS; const CONTAINER_ID_MASK: i64 = (1i64 << CONTAINER_ID_BITS) - 1;
const SEQUENCE_NUMBER_MASK: u64 = (1u64 << SEQUENCE_NUMBER_BITS) - 1;
let container_id = (file_id >> SEQUENCE_NUMBER_BITS) & CONTAINER_ID_MASK;
let seq = (block_index & SEQUENCE_NUMBER_MASK) as i64;
(container_id << SEQUENCE_NUMBER_BITS) | seq
}
fn take_completed_pending<'a>(
pending: &mut Vec<u8>,
incoming: &mut &'a [u8],
chunk_size: usize,
) -> Option<Vec<u8>> {
debug_assert!(chunk_size > 0);
debug_assert!(pending.len() < chunk_size);
if pending.is_empty() {
return None;
}
let need = chunk_size - pending.len();
if incoming.len() < need {
pending.extend_from_slice(incoming);
*incoming = &[];
return None;
}
pending.extend_from_slice(&incoming[..need]);
*incoming = &incoming[need..];
let full = std::mem::take(pending);
pending.reserve(chunk_size);
debug_assert!(pending.is_empty());
Some(full)
}
#[cfg(test)]
fn take_full_chunks(pending: &mut Vec<u8>, incoming: &[u8], chunk_size: usize) -> Vec<Vec<u8>> {
let mut chunks = Vec::new();
let mut src = incoming;
if let Some(full) = take_completed_pending(pending, &mut src, chunk_size) {
chunks.push(full);
}
let n_full = src.len() / chunk_size;
for i in 0..n_full {
chunks.push(owned_chunk(&src[i * chunk_size..(i + 1) * chunk_size]));
}
let rem = src.len() % chunk_size;
if rem > 0 {
pending.extend_from_slice(&src[src.len() - rem..]);
}
debug_assert!(pending.len() < chunk_size);
chunks
}
async fn emit_aligned_chunks(
active: &mut ActiveBlockWriter,
incoming: &[u8],
chunk_size: usize,
) -> Result<()> {
if chunk_size == 0 {
return Err(Error::InvalidArgument {
message: "chunk_size must be > 0".into(),
});
}
let mut src = incoming;
if let Some(full) = take_completed_pending(&mut active.pending_chunk, &mut src, chunk_size) {
active.write_chunk(full).await?;
}
let mut offset = 0usize;
while offset + chunk_size <= src.len() {
active
.write_chunk(owned_chunk(&src[offset..offset + chunk_size]))
.await?;
offset += chunk_size;
}
if offset < src.len() {
active.pending_chunk.extend_from_slice(&src[offset..]);
}
debug_assert!(active.pending_chunk.len() < chunk_size);
Ok(())
}
struct ReplicaWriter {
ordinal: usize,
writer: GrpcBlockWriter,
worker_id: i64,
worker_addr: String,
net_address: WorkerNetAddress,
}
struct ActiveBlockWriter {
replicas: Vec<ReplicaWriter>,
block_id: i64,
block_size: u64,
bytes_written: u64,
pending_chunk: Vec<u8>,
parallel: bool,
min_needed: usize,
}
impl ActiveBlockWriter {
fn remaining(&self) -> u64 {
self.block_size - self.bytes_written
}
fn file_location(&self, block_offset: i64) -> Option<FileLocation> {
if self.replicas.is_empty() {
return None;
}
Some(FileLocation {
block_id: Some(self.block_id),
offset: Some(block_offset),
length: Some(self.bytes_written as i64),
worker_id: self.replicas.iter().map(|r| r.worker_id).collect(),
})
}
async fn write_chunk(&mut self, data: Vec<u8>) -> Result<()> {
if self.replicas.is_empty() {
return Err(Error::BlockIoError {
message: format!("no replica writers left for block_id={}", self.block_id),
});
}
if self.parallel && self.replicas.len() > 1 {
self.write_chunk_parallel(data).await
} else {
self.write_chunk_sequential(data).await
}
}
async fn write_chunk_sequential(&mut self, mut data: Vec<u8>) -> Result<()> {
let replicas = std::mem::take(&mut self.replicas);
let last = replicas.len().saturating_sub(1);
let mut kept = Vec::with_capacity(replicas.len());
for (i, mut r) in replicas.into_iter().enumerate() {
let payload = if i == last {
std::mem::take(&mut data)
} else {
data.clone()
};
match r.writer.write_chunk(payload).await {
Ok(()) => kept.push(r),
Err(e) => {
r.writer.cancel().await;
for k in kept {
k.writer.cancel().await;
}
return Err(e);
}
}
}
self.replicas = kept;
Ok(())
}
async fn write_chunk_parallel(&mut self, data: Vec<u8>) -> Result<()> {
fanout_parallel(
&mut self.replicas,
self.min_needed,
self.block_id,
ReplicaOp::Write(data),
)
.await
}
async fn flush_replicas(&mut self) -> Result<i64> {
if self.replicas.is_empty() {
return Ok(self.bytes_written as i64);
}
if self.parallel && self.replicas.len() > 1 {
fanout_parallel(
&mut self.replicas,
self.min_needed,
self.block_id,
ReplicaOp::Flush,
)
.await?;
} else {
for r in &mut self.replicas {
r.writer.flush().await?;
}
}
Ok(self.bytes_written as i64)
}
async fn close_replicas(self) -> Result<()> {
let mut first_err = None;
for r in self.replicas {
if let Err(e) = r.writer.close().await {
if first_err.is_none() {
first_err = Some(e);
}
}
}
match first_err {
Some(e) => Err(e),
None => Ok(()),
}
}
async fn cancel_replicas(self) {
for r in self.replicas {
r.writer.cancel().await;
}
}
}
enum ReplicaOp {
Write(Vec<u8>),
Flush,
}
async fn fanout_parallel(
replicas: &mut Vec<ReplicaWriter>,
min_needed: usize,
block_id: i64,
op: ReplicaOp,
) -> Result<()> {
let writer_size = replicas.len();
if writer_size < min_needed {
return Err(Error::ResourceExhausted {
message: format!(
"Failed to write enough replicas. dataWriters size: {}, Required: {}",
writer_size, min_needed
),
});
}
let taken = std::mem::take(replicas);
let mut join_set = tokio::task::JoinSet::new();
for r in taken {
let op = match &op {
ReplicaOp::Write(data) => ReplicaOp::Write(data.clone()),
ReplicaOp::Flush => ReplicaOp::Flush,
};
join_set.spawn(async move {
let mut r = r;
let result = match op {
ReplicaOp::Write(payload) => r.writer.write_chunk(payload).await,
ReplicaOp::Flush => r.writer.flush().await.map(|_| ()),
};
match result {
Ok(()) => Ok(r),
Err(e) => {
r.writer.cancel().await;
Err(e)
}
}
});
}
let mut kept = Vec::new();
let mut failures = 0usize;
while let Some(joined) = join_set.join_next().await {
match joined {
Ok(Ok(r)) => kept.push(r),
Ok(Err(e)) => {
failures += 1;
tracing::warn!(error = %e, "DataWriter write failed");
if should_abort_remaining(failures, writer_size, min_needed) {
join_set.abort_all();
}
}
Err(_) => {
failures += 1;
if should_abort_remaining(failures, writer_size, min_needed) {
join_set.abort_all();
}
}
}
}
kept.sort_by_key(|r| r.ordinal);
if !enough_replicas(kept.len(), min_needed) {
for r in kept {
r.writer.cancel().await;
}
return Err(Error::ResourceExhausted {
message: format!(
"Failed to write enough replicas. Success: {}, Required: {} (block_id={})",
writer_size.saturating_sub(failures),
min_needed,
block_id
),
});
}
*replicas = kept;
Ok(())
}
fn cache_write_failure_is_fatal(
err: &Error,
strategy: &WriteStrategy,
block_opened: bool,
ignore_unknown_first_block_status: bool,
) -> bool {
if matches!(
err,
Error::ResourceExhausted { .. } | Error::InvalidArgument { .. }
) {
return true;
}
if !strategy.ufs_stream && !strategy.need_async_persist {
return true;
}
if strategy.need_async_persist && block_opened {
return true;
}
if matches!(
err,
Error::AuthenticationFailed { .. } | Error::PermissionDenied { .. }
) {
return true;
}
!block_opened && !ignore_unknown_first_block_status
}
fn resolve_persist_options(
need_async_persist: bool,
ufs_stream_completed: bool,
wait_time_ms: i64,
) -> (Option<bool>, Option<ScheduleAsyncPersistencePOptions>) {
if !need_async_persist {
return (None, None);
}
if ufs_stream_completed {
return (Some(true), None);
}
if wait_time_ms == NO_AUTO_PERSIST {
return (None, None);
}
(
None,
Some(ScheduleAsyncPersistencePOptions {
common_options: None,
persistence_wait_time: Some(wait_time_ms),
}),
)
}
fn complete_file_locations(
async_through: bool,
last_location: Option<FileLocation>,
) -> Vec<FileLocation> {
if async_through {
last_location.into_iter().collect()
} else {
Vec::new()
}
}
fn block_sequence_number(block_id: i64) -> u64 {
const SEQUENCE_NUMBER_BITS: u32 = 24;
(block_id as u64) & ((1u64 << SEQUENCE_NUMBER_BITS) - 1)
}
impl GoosefsFileWriter {
fn perform_drop_cleanup(&mut self) {
let is_closed = self.closed.load(Ordering::SeqCst);
let is_cancelled = self.cancelled.load(Ordering::SeqCst);
if is_closed || is_cancelled {
return;
}
self.cancelled.store(true, Ordering::SeqCst);
warn!(
path = %self.path,
bytes_written = self.total_bytes_written,
committed_blocks = self.committed_block_ids.len(),
"GoosefsFileWriter dropped without close()/cancel() — performing best-effort cleanup"
);
let ufs_stream = self.ufs_stream.take();
let current_block_writer = self.current_block_writer.take();
let committed_block_ids = std::mem::take(&mut self.committed_block_ids);
let master = self.master.clone();
let path = self.path.clone();
let _ctx_keepalive = self._context.take();
if let Ok(rt) = tokio::runtime::Handle::try_current() {
rt.spawn(async move {
let _ctx = _ctx_keepalive;
if let Some(writer) = ufs_stream {
writer.cancel().await;
}
if let Some(active) = current_block_writer {
active.cancel_replicas().await;
}
if !committed_block_ids.is_empty() {
if let Err(e) = master.remove_blocks(committed_block_ids.clone()).await {
warn!(
path = %path,
error = %e,
"Drop cleanup: remove_blocks failed, falling back to delete(unchecked=true)"
);
if let Err(de) = master
.delete_with_options(&path, DeleteOptions::for_cancel())
.await
{
warn!(
path = %path,
error = %de,
"Drop cleanup: fallback delete also failed — manual cleanup may be required"
);
}
}
}
});
} else {
warn!(
path = %self.path,
"Drop cleanup: no tokio runtime available; in-flight blocks/UFS file may leak — \
callers should explicitly call close()/cancel() before dropping"
);
}
}
}
impl Drop for GoosefsFileWriter {
fn drop(&mut self) {
self.perform_drop_cleanup();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_compute_block_id() {
let inode_id = 33554431i64; assert_eq!(compute_block_id(inode_id, 0), 1 << 24);
assert_eq!(compute_block_id(inode_id, 1), (1 << 24) | 1);
let inode_id_2 = 2i64 << 24;
assert_eq!(compute_block_id(inode_id_2, 0), 2 << 24);
}
#[test]
fn test_compute_block_id_container_extraction() {
const SEQUENCE_NUMBER_BITS: u32 = 24;
const CONTAINER_ID_MASK: i64 = (1i64 << 40) - 1;
let file_id = 33554431i64;
let block_id = compute_block_id(file_id, 3);
let container_id = (block_id >> SEQUENCE_NUMBER_BITS) & CONTAINER_ID_MASK;
assert_eq!(container_id, 1);
assert_eq!(block_id & ((1 << SEQUENCE_NUMBER_BITS) - 1), 3);
}
fn make_test_file_info() -> FileInfo {
FileInfo {
file_id: Some(1),
ufs_path: Some("/ufs/data/test.txt".to_string()),
owner: Some("hadoop".to_string()),
group: Some("supergroup".to_string()),
mode: Some(0o644),
mount_id: Some(42),
..Default::default()
}
}
#[test]
fn last_block_close_does_not_send_flush_mid_file_switch_does() {
assert!(
should_flush_cache_block_on_close(true),
"mid-file getNextBlock() must still send flush:true"
);
assert!(
!should_flush_cache_block_on_close(false),
"last-block close() must not send flush:true"
);
}
fn simulate_mid_file_switches(writes: &[u64], block_size: u64) -> u32 {
let mut remaining: Option<u64> = None;
let mut switches = 0;
for &len in writes {
let mut offset = 0u64;
while offset < len {
if needs_fresh_block(remaining) {
if remaining.is_some() {
switches += 1;
}
remaining = Some(block_size);
}
let room = remaining.unwrap();
let accept = room.min(len - offset);
offset += accept;
remaining = Some(room - accept);
}
}
switches
}
#[test]
fn block_filled_by_last_byte_is_not_a_mid_file_switch() {
let bs = 4 * 1024 * 1024u64;
assert_eq!(
simulate_mid_file_switches(&[bs], bs),
0,
"exactly one block"
);
assert_eq!(
simulate_mid_file_switches(&[2 * bs], bs),
1,
"two full blocks"
);
assert_eq!(
simulate_mid_file_switches(&[bs + 1], bs),
1,
"one byte over"
);
assert_eq!(simulate_mid_file_switches(&[2 * bs + 1], bs), 2);
assert_eq!(simulate_mid_file_switches(&[bs - 1], bs), 0);
assert_eq!(simulate_mid_file_switches(&[0], bs), 0);
}
#[test]
fn filled_block_defers_across_write_calls() {
let bs = 4 * 1024 * 1024u64;
assert_eq!(simulate_mid_file_switches(&[bs / 2, bs / 2], bs), 0);
assert_eq!(simulate_mid_file_switches(&[bs, bs], bs), 1);
assert_eq!(simulate_mid_file_switches(&[bs, 1], bs), 1);
}
#[test]
fn test_strategy_must_cache() {
let fi = make_test_file_info();
let s = resolve_write_strategy(Some(1), &fi); assert!(s.cache_stream);
assert!(!s.ufs_stream);
assert!(s.create_ufs_file_options.is_none());
assert!(!s.need_async_persist);
}
#[test]
fn test_strategy_cache_through() {
let fi = make_test_file_info();
let s = resolve_write_strategy(Some(3), &fi); assert!(s.cache_stream, "CACHE_THROUGH must enable cache stream");
assert!(s.ufs_stream, "CACHE_THROUGH must enable UFS stream");
assert!(s.create_ufs_file_options.is_some());
assert!(!s.need_async_persist);
}
#[test]
fn test_strategy_through() {
let fi = make_test_file_info();
let s = resolve_write_strategy(Some(4), &fi); assert!(!s.cache_stream, "THROUGH must NOT enable cache stream");
assert!(s.ufs_stream);
let ufs_opts = s.create_ufs_file_options.as_ref().unwrap();
assert_eq!(ufs_opts.ufs_path, Some("/ufs/data/test.txt".to_string()));
assert_eq!(ufs_opts.owner, Some("hadoop".to_string()));
assert_eq!(ufs_opts.group, Some("supergroup".to_string()));
assert_eq!(ufs_opts.mode, Some(0o644));
assert_eq!(ufs_opts.mount_id, Some(42));
assert!(!s.need_async_persist);
}
#[test]
fn test_strategy_async_through() {
let fi = make_test_file_info();
let s = resolve_write_strategy(Some(5), &fi); assert!(s.cache_stream);
assert!(!s.ufs_stream);
assert!(s.need_async_persist);
assert!(s.create_ufs_file_options.is_some());
}
fn cache_through() -> WriteStrategy {
resolve_write_strategy(Some(3), &FileInfo::default())
}
fn async_through() -> WriteStrategy {
resolve_write_strategy(Some(5), &FileInfo::default())
}
fn must_cache() -> WriteStrategy {
resolve_write_strategy(Some(1), &FileInfo::default())
}
fn io_err() -> Error {
Error::BlockIoError {
message: "worker went away".to_string(),
}
}
#[test]
fn cache_failure_degrades_on_plain_io_error() {
assert!(!cache_write_failure_is_fatal(
&io_err(),
&cache_through(),
true,
true
));
assert!(!cache_write_failure_is_fatal(
&io_err(),
&async_through(),
false,
true
));
}
#[test]
fn cache_failure_is_fatal_when_replication_contract_broken() {
for err in [
Error::ResourceExhausted {
message: "alive < durable.min".to_string(),
},
Error::InvalidArgument {
message: "durable < durable.min".to_string(),
},
] {
assert!(
cache_write_failure_is_fatal(&err, &cache_through(), true, true),
"{err} must not degrade"
);
}
}
#[test]
fn cache_failure_is_fatal_without_a_ufs_destination() {
assert!(cache_write_failure_is_fatal(
&io_err(),
&must_cache(),
true,
true
));
}
#[test]
fn cache_failure_is_fatal_for_async_through_past_the_first_block() {
assert!(cache_write_failure_is_fatal(
&io_err(),
&async_through(),
true,
true
));
assert!(!cache_write_failure_is_fatal(
&io_err(),
&cache_through(),
true,
true
));
}
#[test]
fn cache_failure_is_fatal_when_credentials_are_rejected() {
for err in [
Error::AuthenticationFailed {
message: "bad token".to_string(),
},
Error::PermissionDenied {
message: "no write permission".to_string(),
},
] {
assert!(
cache_write_failure_is_fatal(&err, &cache_through(), true, true),
"{err} must not degrade"
);
}
}
#[test]
fn cache_failure_first_block_ambiguity_follows_the_config() {
assert!(!cache_write_failure_is_fatal(
&io_err(),
&cache_through(),
false,
true
));
assert!(cache_write_failure_is_fatal(
&io_err(),
&cache_through(),
false,
false
));
assert!(!cache_write_failure_is_fatal(
&io_err(),
&cache_through(),
true,
false
));
}
#[test]
fn async_through_carries_ufs_options_for_the_degrade_path() {
let info = FileInfo {
ufs_path: Some("cosn://bucket/f".to_string()),
..Default::default()
};
let strategy = resolve_write_strategy(Some(5), &info);
assert!(!strategy.ufs_stream);
assert_eq!(
strategy
.create_ufs_file_options
.as_ref()
.and_then(|o| o.ufs_path.as_deref()),
Some("cosn://bucket/f")
);
}
#[test]
fn persist_options_force_persisted_after_degrade() {
let (force, async_opts) = resolve_persist_options(true, true, 0);
assert_eq!(force, Some(true));
assert!(
async_opts.is_none(),
"a persisted file must not also be queued for persisting"
);
let (force, async_opts) = resolve_persist_options(true, true, 5_000);
assert_eq!(force, Some(true));
assert!(async_opts.is_none());
}
#[test]
fn persist_options_schedule_job_on_the_normal_path() {
let (force, async_opts) = resolve_persist_options(true, false, 0);
assert!(force.is_none());
assert_eq!(
async_opts,
Some(ScheduleAsyncPersistencePOptions {
common_options: None,
persistence_wait_time: Some(0),
})
);
let (_, async_opts) = resolve_persist_options(true, false, 5_000);
assert_eq!(
async_opts.and_then(|o| o.persistence_wait_time),
Some(5_000),
"the configured wait time must reach the Master"
);
}
#[test]
fn persist_options_no_auto_persist_sends_neither() {
let (force, async_opts) = resolve_persist_options(true, false, NO_AUTO_PERSIST);
assert!(force.is_none());
assert!(async_opts.is_none());
}
#[test]
fn persist_options_untouched_for_non_async_write_types() {
for ufs_completed in [false, true] {
let (force, async_opts) = resolve_persist_options(false, ufs_completed, 0);
assert!(force.is_none(), "ufs_completed={ufs_completed}");
assert!(async_opts.is_none(), "ufs_completed={ufs_completed}");
}
}
#[test]
fn complete_file_locations_only_for_async_through() {
let loc = FileLocation {
block_id: Some(1),
offset: Some(0),
length: Some(64),
worker_id: vec![7],
};
assert!(complete_file_locations(false, Some(loc.clone())).is_empty());
assert!(complete_file_locations(false, None).is_empty());
let got = complete_file_locations(true, Some(loc.clone()));
assert_eq!(got, vec![loc]);
assert!(complete_file_locations(true, None).is_empty());
}
#[test]
fn test_strategy_default_unset() {
let fi = make_test_file_info();
let s = resolve_write_strategy(None, &fi);
assert!(s.cache_stream);
assert!(!s.ufs_stream);
assert!(s.create_ufs_file_options.is_none());
assert!(!s.need_async_persist);
}
#[test]
fn test_strategy_try_cache() {
let fi = make_test_file_info();
let s = resolve_write_strategy(Some(2), &fi); assert!(s.cache_stream);
assert!(!s.ufs_stream);
assert!(s.create_ufs_file_options.is_none());
assert!(!s.need_async_persist);
}
#[test]
fn test_context_field_is_option_arc() {
let ctx_field: Option<Arc<FileSystemContext>> = None;
assert!(ctx_field.is_none());
}
#[test]
fn test_uuid_to_fs_op_pid_bit_layout() {
let high_bytes: [u8; 8] = [0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77];
let low_bytes: [u8; 8] = [0x88u8, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff];
let mut bytes = [0u8; 16];
bytes[..8].copy_from_slice(&high_bytes);
bytes[8..].copy_from_slice(&low_bytes);
let uuid = Uuid::from_bytes(bytes);
let op_id = uuid_to_fs_op_pid(uuid);
let expected_high = i64::from_be_bytes(high_bytes);
let expected_low = i64::from_be_bytes(low_bytes);
assert_eq!(op_id.most_significant_bits, Some(expected_high));
assert_eq!(op_id.least_significant_bits, Some(expected_low));
}
fn make_drop_test_writer() -> GoosefsFileWriter {
use crate::client::{MasterClient, WorkerClientPool};
use tonic::transport::Channel;
let config = GoosefsConfig::new("127.0.0.1:9200");
let channel = Channel::from_static("http://127.0.0.1:1").connect_lazy();
let master = MasterClient::from_channel(channel, config.clone());
let router = WorkerRouterView::empty();
let worker_pool = Arc::new(WorkerClientPool::new(config.clone()));
let file_info = make_test_file_info();
let strategy = resolve_write_strategy(Some(1), &file_info);
GoosefsFileWriter {
config,
path: "/test/drop-without-close.bin".to_string(),
master,
router,
worker_pool,
_context: None,
file_info,
total_bytes_written: 0,
operation_id: Uuid::nil(),
cancelled: AtomicBool::new(false),
closed: AtomicBool::new(false),
should_cache: strategy.cache_stream,
ufs_write_enabled: strategy.ufs_stream,
block_opened: false,
write_strategy: strategy,
committed_block_ids: Vec::new(),
current_block_writer: None,
ufs_stream: None,
ufs_worker_addr: None,
ufs_stream_completed: AtomicBool::new(false),
_router_needs_init: AtomicBool::new(false),
}
}
#[tokio::test]
async fn drop_without_close_marks_cancelled() {
let mut writer = make_drop_test_writer();
assert!(!writer.closed.load(Ordering::SeqCst));
assert!(!writer.cancelled.load(Ordering::SeqCst));
writer.perform_drop_cleanup();
assert!(
writer.cancelled.load(Ordering::SeqCst),
"perform_drop_cleanup must set cancelled=true"
);
assert!(writer.ufs_stream.is_none());
assert!(writer.current_block_writer.is_none());
assert!(writer.committed_block_ids.is_empty());
writer.perform_drop_cleanup();
assert!(writer.cancelled.load(Ordering::SeqCst));
}
#[tokio::test]
async fn drop_after_close_is_noop() {
let writer = make_drop_test_writer();
writer.closed.store(true, Ordering::SeqCst);
drop(writer);
}
#[tokio::test]
async fn drop_after_cancel_is_noop() {
let writer = make_drop_test_writer();
writer.cancelled.store(true, Ordering::SeqCst);
drop(writer);
}
#[tokio::test]
async fn drop_cleanup_takes_context_field() {
let mut writer = make_drop_test_writer();
writer.perform_drop_cleanup();
assert!(
writer._context.is_none(),
"perform_drop_cleanup must take() _context (N2 regression)"
);
}
fn assert_pending_invariant(pending: &[u8], chunk_size: usize) {
assert!(
pending.len() < chunk_size,
"pending_chunk must stay strictly below chunk_size, got {} >= {}",
pending.len(),
chunk_size
);
}
#[test]
fn take_full_chunks_slices_aligned_payload_without_growing_pending() {
let chunk_size = 1024;
let mut pending = Vec::with_capacity(chunk_size);
let n = 17 * chunk_size + 100;
let incoming: Vec<u8> = (0u8..=255).cycle().take(n).collect();
let chunks = take_full_chunks(&mut pending, &incoming, chunk_size);
assert_eq!(chunks.len(), 17);
assert!(chunks.iter().all(|c| c.len() == chunk_size));
assert_eq!(&chunks[0], &incoming[..chunk_size]);
assert_eq!(pending.len(), 100);
assert_eq!(&pending[..], &incoming[incoming.len() - 100..]);
assert_pending_invariant(&pending, chunk_size);
assert!(
pending.capacity() <= chunk_size * 2,
"pending must not hold the whole write(); capacity={}",
pending.capacity()
);
}
#[test]
fn take_full_chunks_completes_existing_tail() {
let chunk_size = 1000;
let mut pending = vec![0u8; 400];
let incoming = vec![1u8; 700];
let chunks = take_full_chunks(&mut pending, &incoming, chunk_size);
assert_eq!(chunks.len(), 1);
assert_eq!(chunks[0].len(), 1000);
assert_eq!(&chunks[0][..400], &[0u8; 400]);
assert_eq!(&chunks[0][400..], &[1u8; 600]);
assert_eq!(pending.len(), 100);
assert_eq!(&pending[..], &[1u8; 100]);
assert_pending_invariant(&pending, chunk_size);
}
#[test]
fn take_full_chunks_holds_short_write_in_pending() {
let chunk_size = 1000;
let mut pending = vec![0u8; 400];
let incoming = vec![1u8; 200];
let chunks = take_full_chunks(&mut pending, &incoming, chunk_size);
assert!(chunks.is_empty());
assert_eq!(pending.len(), 600);
assert_pending_invariant(&pending, chunk_size);
}
}