Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions curvine-common/proto/common.proto
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ message FileStatusProto {
required uint32 mode = 17;
optional string target = 18;
required uint32 nlink = 19;
optional int64 ctime = 20; // Metadata change time in milliseconds
}

// Describe the worker address information.
Expand Down
12 changes: 12 additions & 0 deletions curvine-common/src/state/file_status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ use orpc::ternary;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

// NUL cannot appear in a FUSE xattr name, so this persisted metadata key cannot
// collide with a user-visible extended attribute.
pub const INTERNAL_CTIME_XATTR: &str = "\0curvine.ctime";
Comment thread
ljy1-lixing marked this conversation as resolved.

Comment thread
ljy1-lixing marked this conversation as resolved.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct FileStatus {
pub id: i64,
Expand Down Expand Up @@ -48,6 +52,14 @@ pub struct FileStatus {
}

impl FileStatus {
pub fn ctime(&self) -> i64 {
self.x_attr
.get(INTERNAL_CTIME_XATTR)
.and_then(|bytes| bytes.as_slice().try_into().ok())
.map(i64::from_le_bytes)
.unwrap_or(self.mtime)
}

pub fn with_name(id: i64, name: String, is_dir: bool) -> Self {
FileStatus {
id,
Expand Down
2 changes: 1 addition & 1 deletion curvine-common/src/state/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ mod block_info;
pub use self::block_info::*;

mod file_status;
pub use self::file_status::FileStatus;
pub use self::file_status::{FileStatus, INTERNAL_CTIME_XATTR};

mod master_info;
pub use self::master_info::MasterInfo;
Expand Down
7 changes: 6 additions & 1 deletion curvine-common/src/state/opts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,11 @@ pub struct SetAttrOpts {
impl SetAttrOpts {
// Recursive setting, only allows setting acl related attributes
pub fn child_opts(&self) -> Self {
let mut add_x_attr = HashMap::new();
if let Some(ctime) = self.add_x_attr.get(INTERNAL_CTIME_XATTR) {
add_x_attr.insert(INTERNAL_CTIME_XATTR.to_string(), ctime.clone());
}

Self {
recursive: false,
replicas: self.replicas,
Expand All @@ -387,7 +392,7 @@ impl SetAttrOpts {
mtime: None,
ttl_ms: None,
ttl_action: None,
add_x_attr: HashMap::default(),
add_x_attr,
remove_x_attr: vec![],
ufs_mtime: None,
}
Expand Down
53 changes: 50 additions & 3 deletions curvine-common/src/utils/proto_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,10 @@ impl ProtoUtils {
}
}

pub fn file_status_to_pb(status: FileStatus) -> FileStatusProto {
pub fn file_status_to_pb(mut status: FileStatus) -> FileStatusProto {
let has_ctime = status.x_attr.contains_key(INTERNAL_CTIME_XATTR);
let ctime = status.ctime();
status.x_attr.remove(INTERNAL_CTIME_XATTR);
FileStatusProto {
id: status.id,
path: status.path,
Expand All @@ -238,10 +241,18 @@ impl ProtoUtils {
mode: status.mode,
target: status.target,
nlink: status.nlink,
ctime: has_ctime.then_some(ctime),
}
}

pub fn file_status_from_pb(status: FileStatusProto) -> FileStatus {
let mut x_attr = status.x_attr;
if let Some(ctime) = status.ctime {
x_attr.insert(
INTERNAL_CTIME_XATTR.to_string(),
ctime.to_le_bytes().to_vec(),
);
}
FileStatus {
id: status.id,
path: status.path,
Expand All @@ -255,7 +266,7 @@ impl ProtoUtils {
replicas: status.replicas,
block_size: status.block_size,
file_type: FileType::from(status.file_type),
x_attr: status.x_attr,
x_attr,
storage_policy: Self::storage_policy_from_pb(status.storage_policy),
owner: status.owner,
group: status.group,
Expand Down Expand Up @@ -724,8 +735,9 @@ impl ProtoUtils {

#[cfg(test)]
mod tests {
use crate::state::{AccessMode, MountInfo, MountOptions};
use crate::state::{AccessMode, FileStatus, MountInfo, MountOptions, INTERNAL_CTIME_XATTR};
use crate::utils::ProtoUtils;
use std::collections::HashMap;

#[test]
fn test_mount_info_proto_round_trip_auto_cache_and_access_mode() {
Expand Down Expand Up @@ -758,4 +770,39 @@ mod tests {
assert_eq!(round_trip.auto_cache, Some(false));
assert_eq!(round_trip.access_mode, Some(AccessMode::ReadWrite));
}

#[test]
fn file_status_proto_preserves_ctime_and_falls_back_for_legacy_peers() {
let status = FileStatus {
mtime: 1_000,
x_attr: HashMap::from([(
INTERNAL_CTIME_XATTR.to_string(),
2_000_i64.to_le_bytes().to_vec(),
)]),
..Default::default()
};

let mut pb = ProtoUtils::file_status_to_pb(status);
assert_eq!(pb.ctime, Some(2_000));
assert!(!pb.x_attr.contains_key(INTERNAL_CTIME_XATTR));
assert_eq!(ProtoUtils::file_status_from_pb(pb.clone()).ctime(), 2_000);

pb.ctime = None;
assert_eq!(ProtoUtils::file_status_from_pb(pb).ctime(), 1_000);
}

#[test]
fn file_status_proto_keeps_legacy_ctime_optional() {
let status = FileStatus {
mtime: 1_000,
..Default::default()
};

let pb = ProtoUtils::file_status_to_pb(status);
assert_eq!(pb.ctime, None);

let round_trip = ProtoUtils::file_status_from_pb(pb);
assert_eq!(round_trip.ctime(), 1_000);
assert!(!round_trip.x_attr.contains_key(INTERNAL_CTIME_XATTR));
}
}
36 changes: 22 additions & 14 deletions curvine-fuse/src/fs/curvine_file_system.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,18 @@ impl CurvineFileSystem {
Ok(())
}

fn encode_visible_xattr_names<'a>(names: impl Iterator<Item = &'a str>) -> Vec<u8> {
let mut encoded = Vec::new();
for name in names {
if FuseUtils::check_xattr(name, XattrOp::Get).is_err() {
continue;
}
encoded.extend_from_slice(name.as_bytes());
encoded.push(0);
}
encoded
}

async fn ensure_writable_path(&self, path: &Path, rpc_code: RpcCode) -> FuseResult<()> {
if self.conf.readonly {
return Err(FsError::read_only(path.full_path()).into());
Expand Down Expand Up @@ -1010,19 +1022,8 @@ impl fs::FileSystem for CurvineFileSystem {
async fn list_xattr(&self, op: ListXAttr<'_>) -> FuseResult<BytesMut> {
let status = self.state.fs_stat(op.header.nodeid, None).await?;

// Build the list of xattr names
let mut xattr_names = Vec::new();

// Add custom xattr names from the file
for name in status.x_attr.keys() {
// Hidden/protected attributes must not be advertised when getxattr
// deliberately makes them unreadable.
if FuseUtils::check_xattr(name, XattrOp::Get).is_err() {
continue;
}
xattr_names.extend_from_slice(name.as_bytes());
xattr_names.push(0); // null terminator
}
let xattr_names =
Self::encode_visible_xattr_names(status.x_attr.keys().map(String::as_str));

let mut buf = FuseBuf::default();

Expand Down Expand Up @@ -1798,7 +1799,7 @@ impl fs::FileSystem for CurvineFileSystem {
#[cfg(test)]
mod tests {
use crate::{FATTR_GID, FATTR_MODE, FATTR_MTIME, FATTR_UID};
use curvine_common::state::FileAllocMode;
use curvine_common::state::{FileAllocMode, INTERNAL_CTIME_XATTR};

#[test]
fn posix_access_requires_mode_check_for_root() {
Expand Down Expand Up @@ -2101,6 +2102,13 @@ mod tests {
);
}

#[test]
fn list_xattr_encoding_hides_internal_ctime() {
let names = ["user.visible", INTERNAL_CTIME_XATTR];
let encoded = CurvineFileSystem::encode_visible_xattr_names(names.into_iter());
assert_eq!(encoded, b"user.visible\0");
}

// #1122 + allowlist: the daemon must never advertise FUSE_ATOMIC_O_TRUNC
// (open does not truncate) or any other unsupported capability, even when
// the kernel offers it. The allowlist mask drops them.
Expand Down
6 changes: 4 additions & 2 deletions curvine-fuse/src/fs/state/node_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -913,8 +913,10 @@ impl NodeState {
if let Some(mtime) = self.get_writer_mtime(attr.ino).await {
attr.mtime = (mtime.max(0) / 1000) as u64;
attr.mtimensec = ((mtime.max(0) % 1000) * 1_000_000) as u32;
attr.ctime = attr.mtime;
attr.ctimensec = attr.mtimensec;
if (attr.mtime, attr.mtimensec) > (attr.ctime, attr.ctimensec) {
attr.ctime = attr.mtime;
attr.ctimensec = attr.mtimensec;
}
}
}

Expand Down
49 changes: 47 additions & 2 deletions curvine-fuse/src/fuse_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,14 @@ impl FuseUtils {
}

pub fn check_xattr(name: &str, op: XattrOp) -> FuseResult<()> {
if name.contains('\0') {
return match op {
XattrOp::Get => err_fuse!(libc::ENODATA, "get_xattr {}", name),
XattrOp::Set => err_fuse!(libc::EOPNOTSUPP, "set_xattr {}", name),
XattrOp::Remove => err_fuse!(libc::EOPNOTSUPP, "remove_xattr {}", name),
};
}

// Handle system extended attributes FIRST, before any path resolution
// This avoids unnecessary operations and provides fastest response
// Kernel may still query these even if FUSE_POSIX_ACL is disabled in init response
Expand Down Expand Up @@ -451,8 +459,11 @@ impl FuseUtils {
let atime_sec = (status.atime.max(0) / 1000) as u64;
let atime_nsec = ((status.atime.max(0) % 1000) * 1_000_000) as u32;

let ctime_sec = mtime_sec;
let ctime_nsec = mtime_nsec;
// Legacy/object-store statuses may not provide ctime yet. Falling back to mtime
// preserves the previous behavior without hiding a real independent ctime.
let ctime = status.ctime();
let ctime_sec = (ctime.max(0) / 1000) as u64;
let ctime_nsec = ((ctime.max(0) % 1000) * 1_000_000) as u32;

let uid = if status.owner.is_empty() {
conf.uid
Expand Down Expand Up @@ -679,6 +690,7 @@ impl FuseUtils {
mod tests {
use super::*;
use crate::raw::fuse_abi::fuse_setattr_in;
use curvine_common::state::INTERNAL_CTIME_XATTR;

#[test]
fn protected_xattr_errors_match_operation() {
Expand Down Expand Up @@ -738,6 +750,24 @@ mod tests {
);
}

#[test]
fn nul_xattr_names_are_internal_only() {
assert_eq!(
FuseUtils::check_xattr(INTERNAL_CTIME_XATTR, XattrOp::Get)
.unwrap_err()
.errno(),
libc::ENODATA
);
for op in [XattrOp::Set, XattrOp::Remove] {
assert_eq!(
FuseUtils::check_xattr(INTERNAL_CTIME_XATTR, op)
.unwrap_err()
.errno(),
libc::EOPNOTSUPP
);
}
}

#[test]
fn file_open_flags_are_built_only_from_response_flags() {
let mut conf = FuseConf::default();
Expand Down Expand Up @@ -944,6 +974,21 @@ mod tests {
assert!(FuseUtils::status_to_attr(&conf, &link).is_ok());
}

#[test]
fn status_to_attr_preserves_independent_ctime() {
let conf = FuseConf::default();
let mut status = file_status(FileType::File, 0, 0o644);
status.mtime = 1_000;
status.x_attr.insert(
INTERNAL_CTIME_XATTR.to_string(),
2_500_i64.to_le_bytes().to_vec(),
);

let attr = FuseUtils::status_to_attr(&conf, &status).unwrap();
assert_eq!((attr.mtime, attr.mtimensec), (1, 0));
assert_eq!((attr.ctime, attr.ctimensec), (2, 500_000_000));
}

#[test]
fn blocks_derived_from_fuse_size() {
let conf = FuseConf::default();
Expand Down
2 changes: 1 addition & 1 deletion curvine-server/src/master/journal/entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ pub struct SymlinkEntry {
pub struct LinkEntry {
pub(crate) op_id: u64,
pub(crate) rpc_id: i64,
/// Link creation time, used during replay to set inode mtime.
/// Link creation time, reused during replay for parent mtime and inode ctime.
pub(crate) mtime: i64,
pub(crate) src_path: String,
pub(crate) dst_path: String,
Expand Down
2 changes: 1 addition & 1 deletion curvine-server/src/master/journal/journal_loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -792,7 +792,7 @@ impl JournalLoader {

if let Some(mut inode_ptr) = old_path.get_last_inode() {
if let File(_) = inode_ptr.as_mut() {
inode_ptr.incr_nlink();
inode_ptr.incr_nlink(entry.mtime);
}
}

Expand Down
3 changes: 2 additions & 1 deletion curvine-server/src/master/journal/journal_writer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -293,11 +293,12 @@ impl JournalWriter {
fs_dir: &FsDir,
src_path: P,
dst_path: P,
mtime: i64,
) -> FsResult<()> {
let entry = LinkEntry {
op_id: fs_dir.next_op_id(),
rpc_id: 0,
mtime: LocalTime::mills() as i64,
mtime,
src_path: src_path.as_ref().to_string(),
dst_path: dst_path.as_ref().to_string(),
};
Expand Down
Loading
Loading