diff --git a/curvine-common/proto/common.proto b/curvine-common/proto/common.proto index 25a26d34c..b60f14114 100644 --- a/curvine-common/proto/common.proto +++ b/curvine-common/proto/common.proto @@ -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. diff --git a/curvine-common/src/state/file_status.rs b/curvine-common/src/state/file_status.rs index f345dd011..f167db896 100644 --- a/curvine-common/src/state/file_status.rs +++ b/curvine-common/src/state/file_status.rs @@ -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"; + #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct FileStatus { pub id: i64, @@ -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, diff --git a/curvine-common/src/state/mod.rs b/curvine-common/src/state/mod.rs index 1b65905a4..24f6cd70b 100644 --- a/curvine-common/src/state/mod.rs +++ b/curvine-common/src/state/mod.rs @@ -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; diff --git a/curvine-common/src/state/opts.rs b/curvine-common/src/state/opts.rs index 495695b57..ca8af0f59 100644 --- a/curvine-common/src/state/opts.rs +++ b/curvine-common/src/state/opts.rs @@ -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, @@ -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, } diff --git a/curvine-common/src/utils/proto_utils.rs b/curvine-common/src/utils/proto_utils.rs index e480a57df..7fdee1f2b 100644 --- a/curvine-common/src/utils/proto_utils.rs +++ b/curvine-common/src/utils/proto_utils.rs @@ -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, @@ -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, @@ -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, @@ -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() { @@ -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)); + } } diff --git a/curvine-fuse/src/fs/curvine_file_system.rs b/curvine-fuse/src/fs/curvine_file_system.rs index e7e947d9e..267eafcd7 100644 --- a/curvine-fuse/src/fs/curvine_file_system.rs +++ b/curvine-fuse/src/fs/curvine_file_system.rs @@ -181,6 +181,18 @@ impl CurvineFileSystem { Ok(()) } + fn encode_visible_xattr_names<'a>(names: impl Iterator) -> Vec { + 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()); @@ -1010,19 +1022,8 @@ impl fs::FileSystem for CurvineFileSystem { async fn list_xattr(&self, op: ListXAttr<'_>) -> FuseResult { 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(); @@ -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() { @@ -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. diff --git a/curvine-fuse/src/fs/state/node_state.rs b/curvine-fuse/src/fs/state/node_state.rs index 506da7955..2361fa1f8 100644 --- a/curvine-fuse/src/fs/state/node_state.rs +++ b/curvine-fuse/src/fs/state/node_state.rs @@ -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; + } } } diff --git a/curvine-fuse/src/fuse_utils.rs b/curvine-fuse/src/fuse_utils.rs index 31568427d..71965f5ff 100644 --- a/curvine-fuse/src/fuse_utils.rs +++ b/curvine-fuse/src/fuse_utils.rs @@ -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 @@ -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 @@ -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() { @@ -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(); @@ -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(); diff --git a/curvine-server/src/master/journal/entry.rs b/curvine-server/src/master/journal/entry.rs index 8cbab628c..730ab930a 100644 --- a/curvine-server/src/master/journal/entry.rs +++ b/curvine-server/src/master/journal/entry.rs @@ -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, diff --git a/curvine-server/src/master/journal/journal_loader.rs b/curvine-server/src/master/journal/journal_loader.rs index 4fe5e59ce..9f1c55dde 100644 --- a/curvine-server/src/master/journal/journal_loader.rs +++ b/curvine-server/src/master/journal/journal_loader.rs @@ -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); } } diff --git a/curvine-server/src/master/journal/journal_writer.rs b/curvine-server/src/master/journal/journal_writer.rs index 6674bcca6..6deeb1571 100644 --- a/curvine-server/src/master/journal/journal_writer.rs +++ b/curvine-server/src/master/journal/journal_writer.rs @@ -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(), }; diff --git a/curvine-server/src/master/meta/fs_dir.rs b/curvine-server/src/master/meta/fs_dir.rs index edf3a653c..3d69e4155 100644 --- a/curvine-server/src/master/meta/fs_dir.rs +++ b/curvine-server/src/master/meta/fs_dir.rs @@ -25,6 +25,7 @@ use curvine_common::error::FsError; use curvine_common::state::{ BlockLocation, CommitBlock, CreateFileOpts, ExtendedBlock, FileAllocOpts, FileLock, FileStatus, FreeResult, ListOptions, MkdirOpts, MountInfo, RenameFlags, SetAttrOpts, WorkerAddress, + INTERNAL_CTIME_XATTR, }; use curvine_common::FsResult; use log::{debug, info, warn}; @@ -223,27 +224,32 @@ impl FsDir { if f.nlink() > 1 { let target_inode = target.clone(); if let File(ref mut nf) = target_inode.as_mut() { - nf.decrement_nlink(); + nf.decrement_nlink(mtime); } self.store - .apply_unlink(parent.as_ref(), child, child_name)? + .apply_unlink(parent.as_ref(), child, child_name, mtime)? } else { // This is the last link, delete the inode self.store - .apply_delete(parent.as_ref(), child, child_name)? + .apply_delete(parent.as_ref(), child, child_name, mtime)? } } FileEntry(e) => { // This is a link entry, just remove the directory entry // The actual inode's nlink count should be decremented - self.store - .apply_unlink_file_entry(parent.as_ref(), child, child_name, e.id)? + self.store.apply_unlink_file_entry( + parent.as_ref(), + child, + child_name, + e.id, + mtime, + )? } Dir(_) => { - parent.dec_nlink(); + parent.dec_nlink(mtime); // Directories are always deleted self.store - .apply_delete(parent.as_ref(), child, child_name)? + .apply_delete(parent.as_ref(), child, child_name, mtime)? } }; @@ -384,6 +390,7 @@ impl FsDir { let mut new_inode = src_inode.as_ref().clone(); new_inode.change_name(new_name); new_inode.set_parent_id(dst_parent.id()); + new_inode.update_ctime(mtime); // Update the parent directory for the last modification time. src_parent.update_mtime(mtime); @@ -451,7 +458,7 @@ impl FsDir { // Update the parent directory for the last modification time. parent.update_mtime(child.mtime()); if child.is_dir() { - parent.incr_nlink(); + parent.incr_nlink(child.mtime()); } let added = parent.add_child(child)?; @@ -848,12 +855,21 @@ impl FsDir { self.store.get_mount_point(id) } - pub fn set_attr(&mut self, inp: InodePath, opts: SetAttrOpts) -> FsResult { + pub fn set_attr(&mut self, inp: InodePath, mut opts: SetAttrOpts) -> FsResult { let inode = match inp.get_last_inode() { Some(v) => v, None => return err_ext!(FsError::file_not_found(inp.path())), }; + // Internal metadata is master-owned. Persist the operation timestamp in the + // journal so replay restores it without changing the bincode inode layout. + opts.add_x_attr.remove(INTERNAL_CTIME_XATTR); + opts.remove_x_attr.retain(|key| key != INTERNAL_CTIME_XATTR); + let ctime = LocalTime::mills() as i64; + opts.add_x_attr.insert( + INTERNAL_CTIME_XATTR.to_string(), + ctime.to_le_bytes().to_vec(), + ); self.unprotected_set_attr(inode.clone(), opts.clone())?; self.journal_writer.log_set_attr(self, &inp, opts)?; Ok(inode.to_file_status(inp.path())?) @@ -996,7 +1012,7 @@ impl FsDir { // If we have the original inode in memory, increment its nlink count if let Some(ref mut inode_ptr) = original_inode_ptr { if let File(_) = inode_ptr.as_mut() { - inode_ptr.incr_nlink(); + inode_ptr.incr_nlink(op_ms as i64); } } @@ -1006,7 +1022,7 @@ impl FsDir { // Log the operation self.journal_writer - .log_link(self, src_path.path(), &dst_path_str)?; + .log_link(self, src_path.path(), &dst_path_str, op_ms as i64)?; Ok(()) } @@ -1041,8 +1057,12 @@ impl FsDir { new_path.append(added.clone())?; // Apply changes to storage - this creates an edge pointing to the original inode - self.store - .apply_link(parent.as_ref(), added.as_ref(), original_inode_id)?; + self.store.apply_link( + parent.as_ref(), + added.as_ref(), + original_inode_id, + op_ms as i64, + )?; Ok(new_path) } diff --git a/curvine-server/src/master/meta/inode/inode_dir.rs b/curvine-server/src/master/meta/inode/inode_dir.rs index 4dbec3c7b..faac9848f 100644 --- a/curvine-server/src/master/meta/inode/inode_dir.rs +++ b/curvine-server/src/master/meta/inode/inode_dir.rs @@ -17,7 +17,7 @@ use crate::master::meta::inode::inodes_children::InodeChildren; use crate::master::meta::inode::{ ChildrenIter, Inode, InodeFile, InodePtr, InodeView, EMPTY_PARENT_ID, }; -use curvine_common::state::{ListOptions, MkdirOpts, StoragePolicy}; +use curvine_common::state::{ListOptions, MkdirOpts, StoragePolicy, INTERNAL_CTIME_XATTR}; use glob::Pattern; use orpc::CommonResult; use serde::{Deserialize, Serialize}; @@ -95,7 +95,17 @@ impl InodeDir { pub fn update_mtime(&mut self, time: i64) { if time > self.mtime { - self.mtime = time + self.mtime = time; + self.update_ctime(time); + } + } + + pub fn update_ctime(&mut self, time: i64) { + if time > self.ctime() { + self.features.x_attr.insert( + INTERNAL_CTIME_XATTR.to_string(), + time.to_le_bytes().to_vec(), + ); } } @@ -160,6 +170,15 @@ impl Inode for InodeDir { fn atime(&self) -> i64 { self.atime } + + fn ctime(&self) -> i64 { + self.features + .x_attr + .get(INTERNAL_CTIME_XATTR) + .and_then(|bytes| bytes.as_slice().try_into().ok()) + .map(i64::from_le_bytes) + .unwrap_or(self.mtime) + } } impl PartialEq for InodeDir { diff --git a/curvine-server/src/master/meta/inode/inode_file.rs b/curvine-server/src/master/meta/inode/inode_file.rs index ba560878d..2a4b49329 100644 --- a/curvine-server/src/master/meta/inode/inode_file.rs +++ b/curvine-server/src/master/meta/inode/inode_file.rs @@ -18,7 +18,7 @@ use crate::master::meta::store::InodeStore; use crate::master::meta::{BlockMeta, InodeId}; use curvine_common::state::{ is_special_file_type, BlockLocation, CommitBlock, CreateFileOpts, ExtendedBlock, FileAllocOpts, - FileType, StoragePolicy, + FileType, StoragePolicy, INTERNAL_CTIME_XATTR, }; use curvine_common::FsResult; use orpc::common::LocalTime; @@ -261,9 +261,10 @@ impl InodeFile { } // Decrement link count - pub fn decrement_nlink(&mut self) -> u32 { + pub fn decrement_nlink(&mut self, ctime: i64) -> u32 { if self.nlink > 0 { self.nlink -= 1; + self.update_ctime(ctime); } self.nlink } @@ -289,6 +290,7 @@ impl InodeFile { self.block_size = opts.block_size as u32; self.storage_policy.overwrite(opts.storage_policy); self.mtime = mtime; + self.update_ctime(mtime); // Reset file writing state for new write operation self.features.set_writing(opts.client_name); @@ -344,7 +346,9 @@ impl InodeFile { ); } - self.mtime = LocalTime::mills() as i64; + let mtime = LocalTime::mills() as i64; + self.mtime = mtime; + self.update_ctime(mtime); if !only_flush { self.features.complete_write(client_name); } @@ -354,6 +358,7 @@ impl InodeFile { pub fn free(&mut self, mtime: i64) -> bool { if self.storage_policy.free() { self.mtime = mtime; + self.update_ctime(mtime); self.blocks.clear(); true } else { @@ -552,6 +557,22 @@ impl InodeFile { self.cv_exists() && !self.blocks.is_empty() } } + + pub fn update_mtime(&mut self, time: i64) { + if time > self.mtime { + self.mtime = time; + self.update_ctime(time); + } + } + + pub fn update_ctime(&mut self, time: i64) { + if time > self.ctime() { + self.features.x_attr.insert( + INTERNAL_CTIME_XATTR.to_string(), + time.to_le_bytes().to_vec(), + ); + } + } } impl Inode for InodeFile { @@ -575,6 +596,15 @@ impl Inode for InodeFile { self.atime } + fn ctime(&self) -> i64 { + self.features + .x_attr + .get(INTERNAL_CTIME_XATTR) + .and_then(|bytes| bytes.as_slice().try_into().ok()) + .map(i64::from_le_bytes) + .unwrap_or(self.mtime) + } + fn nlink(&self) -> u32 { self.nlink } @@ -585,3 +615,22 @@ impl PartialEq for InodeFile { self.id == other.id } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn complete_advances_existing_independent_ctime_with_mtime() { + let mut file = InodeFile::new(1, 1); + file.features.x_attr.insert( + INTERNAL_CTIME_XATTR.to_string(), + 2_i64.to_le_bytes().to_vec(), + ); + + file.complete(0, &[], "", true).unwrap(); + + assert!(file.mtime > 2); + assert_eq!(file.ctime(), file.mtime); + } +} diff --git a/curvine-server/src/master/meta/inode/inode_view.rs b/curvine-server/src/master/meta/inode/inode_view.rs index c64ddd59e..775f87415 100644 --- a/curvine-server/src/master/meta/inode/inode_view.rs +++ b/curvine-server/src/master/meta/inode/inode_view.rs @@ -19,7 +19,9 @@ use crate::master::meta::inode::InodeView::{Dir, File, FileEntry}; use crate::master::meta::inode::{ Inode, InodeDir, InodeFile, InodePtr, PATH_SEPARATOR, ROOT_INODE_ID, }; -use curvine_common::state::{FileStatus, FileType, SetAttrOpts, StoragePolicy, TtlAction}; +use curvine_common::state::{ + FileStatus, FileType, SetAttrOpts, StoragePolicy, TtlAction, INTERNAL_CTIME_XATTR, +}; use curvine_common::utils::SerdeUtils; use orpc::common::{LocalTime, Utils}; use orpc::{err_box, CommonResult}; @@ -236,40 +238,56 @@ impl InodeView { } } + pub fn ctime(&self) -> i64 { + match self { + File(f) => f.ctime(), + Dir(d) => d.ctime(), + FileEntry(..) => 0, + } + } + pub fn update_mtime(&mut self, time: i64) { match self { - File(f) => { - if time > f.mtime { - f.mtime = time - } - } - Dir(d) => { - if time > d.mtime { - d.mtime = time - } - } + File(f) => f.update_mtime(time), + Dir(d) => d.update_mtime(time), FileEntry(..) => (), } } - pub fn incr_nlink(&mut self) { + pub fn update_ctime(&mut self, time: i64) { match self { - File(f) => f.nlink += 1, - Dir(d) => d.nlink += 1, + File(f) => f.update_ctime(time), + Dir(d) => d.update_ctime(time), FileEntry(..) => (), } } - pub fn dec_nlink(&mut self) { + pub fn incr_nlink(&mut self, ctime: i64) { + match self { + File(f) => { + f.nlink += 1; + f.update_ctime(ctime); + } + Dir(d) => { + d.nlink += 1; + d.update_ctime(ctime); + } + FileEntry(..) => (), + } + } + + pub fn dec_nlink(&mut self, ctime: i64) { match self { File(f) => { if f.nlink > 0 { - f.nlink -= 1 + f.nlink -= 1; + f.update_ctime(ctime); } } Dir(d) => { if d.nlink > 0 { - d.nlink -= 1 + d.nlink -= 1; + d.update_ctime(ctime); } } FileEntry(..) => (), @@ -461,7 +479,9 @@ impl InodeView { } for key in opts.remove_x_attr { - let _ = self.x_attr_mut()?.remove(&key); + if key != INTERNAL_CTIME_XATTR { + let _ = self.x_attr_mut()?.remove(&key); + } } if let Some(ufs_mtime) = opts.ufs_mtime { @@ -614,3 +634,106 @@ impl PartialEq for InodeView { self.id() == other.id() } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn setattr_updates_ctime_when_mtime_is_omitted() { + let mut inode = InodeView::new_file("file".to_string(), InodeFile::new(1, 1)); + inode + .set_attr(SetAttrOpts { + atime: Some(2), + mtime: None, + add_x_attr: HashMap::from([( + INTERNAL_CTIME_XATTR.to_string(), + 3_i64.to_le_bytes().to_vec(), + )]), + ..Default::default() + }) + .unwrap(); + + let status = inode.to_file_status("/file").unwrap(); + assert_eq!(status.atime, 2); + assert_eq!(status.mtime, 1); + assert_eq!(status.ctime(), 3); + } + + #[test] + fn legacy_setattr_replay_keeps_ctime_fallback_deterministic() { + let mut inode = InodeView::new_file("file".to_string(), InodeFile::new(1, 1)); + inode + .set_attr(SetAttrOpts { + atime: Some(2), + ..Default::default() + }) + .unwrap(); + + let status = inode.to_file_status("/file").unwrap(); + assert_eq!(status.ctime(), status.mtime); + assert!(!status.x_attr.contains_key(INTERNAL_CTIME_XATTR)); + } + + #[test] + fn setattr_cannot_remove_internal_ctime() { + let mut inode = InodeView::new_file("file".to_string(), InodeFile::new(1, 1)); + inode + .set_attr(SetAttrOpts { + add_x_attr: HashMap::from([( + INTERNAL_CTIME_XATTR.to_string(), + 3_i64.to_le_bytes().to_vec(), + )]), + ..Default::default() + }) + .unwrap(); + inode + .set_attr(SetAttrOpts { + remove_x_attr: vec![INTERNAL_CTIME_XATTR.to_string()], + ..Default::default() + }) + .unwrap(); + + assert_eq!(inode.ctime(), 3); + } + + #[test] + fn mtime_and_nlink_updates_advance_persisted_ctime() { + let mut inode = InodeView::new_file("file".to_string(), InodeFile::new(1, 1)); + inode + .set_attr(SetAttrOpts { + add_x_attr: HashMap::from([( + INTERNAL_CTIME_XATTR.to_string(), + 2_i64.to_le_bytes().to_vec(), + )]), + ..Default::default() + }) + .unwrap(); + + inode.update_mtime(3); + assert_eq!(inode.ctime(), 3); + + inode.incr_nlink(4); + assert_eq!(inode.ctime(), 4); + + inode.dec_nlink(5); + assert_eq!(inode.ctime(), 5); + } + + #[test] + fn setattr_reuses_persisted_ctime_during_replay() { + let mut inode = InodeView::new_file("file".to_string(), InodeFile::new(1, 1)); + inode + .set_attr(SetAttrOpts { + atime: Some(2), + add_x_attr: HashMap::from([( + INTERNAL_CTIME_XATTR.to_string(), + 3_i64.to_le_bytes().to_vec(), + )]), + ..Default::default() + }) + .unwrap(); + + assert_eq!(inode.ctime(), 3); + } +} diff --git a/curvine-server/src/master/meta/inode/mod.rs b/curvine-server/src/master/meta/inode/mod.rs index 57ae33be1..944476247 100644 --- a/curvine-server/src/master/meta/inode/mod.rs +++ b/curvine-server/src/master/meta/inode/mod.rs @@ -61,6 +61,9 @@ pub trait Inode { // Access time fn atime(&self) -> i64; + // Metadata change time + fn ctime(&self) -> i64; + // nlink fn nlink(&self) -> u32; } diff --git a/curvine-server/src/master/meta/store/inode_store.rs b/curvine-server/src/master/meta/store/inode_store.rs index 0d3aaec56..0b441b488 100644 --- a/curvine-server/src/master/meta/store/inode_store.rs +++ b/curvine-server/src/master/meta/store/inode_store.rs @@ -143,6 +143,7 @@ impl InodeStore { parent: &InodeView, del: &InodeView, del_name: &str, + ctime: i64, ) -> CommonResult { let mut batch = self.store.new_batch(); batch.write_inode(parent)?; @@ -174,7 +175,7 @@ impl InodeStore { _ => { deleted_files += 1; - let res = self.decrement_inode_nlink(inode.id(), &mut batch)?; + let res = self.decrement_inode_nlink(inode.id(), ctime, &mut batch)?; del_res.blocks.extend(res.blocks); } } @@ -322,6 +323,7 @@ impl InodeStore { parent: &InodeView, new_entry: &InodeView, original_inode_id: i64, + ctime: i64, ) -> CommonResult<()> { let mut batch = self.store.new_batch(); @@ -331,7 +333,7 @@ impl InodeStore { batch.add_child(parent.id(), new_entry.name(), original_inode_id)?; // Increment nlink count of the original inode (in the same batch for atomicity) - self.increment_inode_nlink(original_inode_id, &mut batch)?; + self.increment_inode_nlink(original_inode_id, ctime, &mut batch)?; batch.commit()?; @@ -343,12 +345,13 @@ impl InodeStore { fn increment_inode_nlink( &self, inode_id: i64, + ctime: i64, batch: &mut InodeWriteBatch<'_>, ) -> CommonResult<()> { if let Some(mut inode_view) = self.get_inode(inode_id, None)? { match &mut inode_view { InodeView::File(_) => { - inode_view.incr_nlink(); + inode_view.incr_nlink(ctime); batch.write_inode(&inode_view)?; } _ => { @@ -366,6 +369,7 @@ impl InodeStore { parent: &InodeView, child: &InodeView, child_name: &str, + ctime: i64, ) -> CommonResult { let mut batch = self.store.new_batch(); @@ -378,7 +382,7 @@ impl InodeStore { // Decrement nlink count of the file being unlinked. // If nlink reaches 0 the inode is also deleted and del_res.blocks will be populated. let del_res = if let InodeView::File(_) = child { - self.decrement_inode_nlink(child.id(), &mut batch)? + self.decrement_inode_nlink(child.id(), ctime, &mut batch)? } else { DeleteResult::new() }; @@ -396,6 +400,7 @@ impl InodeStore { child: &InodeView, child_name: &str, inode_id: i64, + ctime: i64, ) -> CommonResult { if child.id() != inode_id { return err_box!( @@ -415,7 +420,7 @@ impl InodeStore { // Decrement nlink count of the original inode. // If nlink reaches 0 the inode is also deleted and del_res.blocks will be populated. - let del_res = self.decrement_inode_nlink(inode_id, &mut batch)?; + let del_res = self.decrement_inode_nlink(inode_id, ctime, &mut batch)?; batch.commit()?; @@ -428,6 +433,7 @@ impl InodeStore { fn decrement_inode_nlink( &self, inode_id: i64, + ctime: i64, batch: &mut InodeWriteBatch<'_>, ) -> CommonResult { let mut del_res = DeleteResult::new(); @@ -435,7 +441,7 @@ impl InodeStore { if let Some(mut inode_view) = self.get_inode(inode_id, None)? { match &mut inode_view { InodeView::File(f) => { - let remaining_links = f.decrement_nlink(); + let remaining_links = f.decrement_nlink(ctime); if remaining_links == 0 { batch.delete_inode(inode_id)?;