Skip to content

feat(slayerfs): std-like SDK + fs/file-io layer alignment#403

Merged
mon3stera merged 4 commits intork8s-dev:mainfrom
jerry609:feat/slayerfs-stdlike-sdk-pr
Feb 1, 2026
Merged

feat(slayerfs): std-like SDK + fs/file-io layer alignment#403
mon3stera merged 4 commits intork8s-dev:mainfrom
jerry609:feat/slayerfs-stdlike-sdk-pr

Conversation

@jerry609
Copy link
Contributor

@jerry609 jerry609 commented Jan 29, 2026

Summary

  • 引入/完善 FileSystem + file-io 层,打通 VFS / meta / chunk IO
  • 统一错误映射与 inode/handle 读写路径,清理旧模块

What changed

  • std-like SDK 扩展与修正(stat/rename/truncate/link 等行为校正)
  • VFS 增加 inode 写入路径与 xattr/ACL helpers;错误映射 from_meta 完整化
  • 清理 vfs/inode 兼容层,调整模块声明与 main.rs 引用
  • 多处 clippy/fmt 修复(借用/错误类型/格式化)
  • 对齐 upstream 的 reader/writer:迁移到 file_io 供 FS/VFS 复用,保留 buffer_usage/backpressure/session
    readahead 行为,chunk_id_for 错误统一传播
  • 统一 SDK/VFS 分层:FileSystem wrap VFS、File 持有 handle、sdk_fs API 风格统一,减少路径 API 与 handle API
    逻辑重复

Considerations

  • 复用一套读写pipeline,避免 FS/VFS 行为分叉、后续维护成本降低
  • inode/handle 路径优先,避免路径变更导致的语义错误(如 rename/unlink 后 truncate)
  • 错误类型统一与 Errno 映射完善,便于定位与上层处理
  • 维持与 upstream read-path 行为一致,避免性能回退(缓存/回压/预读)
  • 最小化外部 API 破坏:模块迁移后通过 re-export 维持内部引用路径

close #348

Copilot AI review requested due to automatic review settings January 29, 2026 06:33
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements a std/tokio-like SlayerFS SDK and aligns the VFS/meta/chunk I/O stack by introducing a new path-based fs::FileSystem layer, inode-based write/xattr/ACL helpers, and refactoring file_io as a shared module.

Changes:

  • Add fs::FileSystem (path-based ops + permissions + access logging) and a new sdk_fs module (OpenOptions, File, and convenience functions).
  • Refactor VFS and file_io integration (inode-based write path, statfs, xattr/ACL hooks, chunk-id helpers moved).
  • Add xattr storage in the metadata layer (SeaORM entity + DatabaseMetaStore support), and update docs/examples.

Reviewed changes

Copilot reviewed 26 out of 26 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
project/slayerfs/src/vfs/sdk.rs SDK client switched to FileSystem backend; adds *_io methods and local constructors.
project/slayerfs/src/vfs/mod.rs Re-exports chunk-id helpers from file_io; module visibility adjustments.
project/slayerfs/src/vfs/io/mod.rs Redirects VFS I/O exports to file_io types.
project/slayerfs/src/vfs/fs.rs Adds mkdir/create-file structured variants, inode-based write, xattr/ACL helpers, statfs.
project/slayerfs/src/vfs/error.rs Adds VfsError::from_meta mapping helper.
project/slayerfs/src/sdk_fs.rs New std-like async SDK (SdkClient, OpenOptions, File, helpers + tests).
project/slayerfs/src/meta/stores/database_store.rs Adds xattr table + CRUD wiring; deletes xattr on inode removal.
project/slayerfs/src/meta/store.rs Extends MetaStore trait with xattr getters/listing defaults.
project/slayerfs/src/meta/layer.rs Extends MetaLayer with xattr/ACL methods.
project/slayerfs/src/meta/entities/xattr_meta.rs Adds SeaORM entity model for xattr table.
project/slayerfs/src/meta/entities/mod.rs Exposes xattr entity and adjusts entity module visibility/exports.
project/slayerfs/src/meta/config.rs Introduces MetaClientConfig and TTL normalization helper.
project/slayerfs/src/meta/client.rs Implements xattr/ACL forwarding in MetaClient’s MetaLayer impl.
project/slayerfs/src/main.rs Adds file_io/fs modules to binary; global dead-code allowance added.
project/slayerfs/src/lib.rs Exports new file_io, fs, and sdk_fs modules.
project/slayerfs/src/fuse/mod.rs Uses inode-based write, implements statfs, and adds xattr FUSE ops.
project/slayerfs/src/fs/mod.rs New path-based FileSystem implementation + tests.
project/slayerfs/src/file_io/mod.rs New shared file_io module (chunk-id helpers, registry, IO factory).
project/slayerfs/src/file_io/inode.rs Makes Inode public and cleans up docs.
project/slayerfs/src/file_io/reader.rs Refactors reader internals and invalidation logic; updates tests.
project/slayerfs/src/file_io/writer.rs Refactors writer internals to new file_io layout; updates tests.
project/slayerfs/examples/sdk_fs_demo.rs New example showcasing std-like SDK usage.
project/slayerfs/examples/sdk_demo.rs Updates example to match new SDK client usage.
project/slayerfs/examples/s3_demo.rs Updates S3 demo to use Client::new (FileSystem-backed).
project/slayerfs/README_CN.md Updates architecture description (fs vs vfs separation).
project/slayerfs/README.md Updates architecture description (fs vs vfs separation).
Comments suppressed due to low confidence (5)

project/slayerfs/src/file_io/reader.rs:293

  • read_at now prepares slices and copies available ranges, but it no longer pre-fills buf[..actual_len] with zeros. If there are holes/missing slices, the corresponding bytes in the caller’s buffer will retain their previous contents rather than reading as zero-filled. If read_at is intended to provide the same semantics as read/DataFetcher (zero-fill gaps), reintroduce an explicit zero-fill step.
    project/slayerfs/src/file_io/reader.rs:18
  • There are multiple unused imports here (chunk_id_for, split_chunk_spans, and likely ChunkLayout / TryFrom depending on the final implementation). This will trigger warnings/clippy in new code; please remove unused imports or switch read_at back to using the shared split_chunk_spans helper.
    project/slayerfs/src/file_io/writer.rs:1060
  • WriteConfig::new currently takes a single ChunkLayout argument (see vfs/config.rs), but this test calls it with (layout, 4 * 1024), which will not compile. Either revert to WriteConfig::new(layout).page_size(4 * 1024) or change WriteConfig::new’s signature and update all call sites consistently.
    project/slayerfs/src/file_io/writer.rs:1296
  • Same issue as above: WriteConfig::new(layout, 4 * 1024) does not match the current WriteConfig::new(layout) API and will fail to compile. Use the existing builder (.page_size(...)) or update the constructor signature everywhere.
    project/slayerfs/src/file_io/reader.rs:609
  • This test uses WriteConfig::new(layout, 4 * 1024), but WriteConfig::new currently only accepts ChunkLayout. As written this won’t compile; use WriteConfig::new(layout).page_size(4 * 1024) (or update the constructor signature consistently across the codebase).

Comment on lines 289 to 299
pub type LocalClient = Client<ObjectBlockStore<LocalFsBackend>, Arc<dyn MetaStore>>;

#[allow(dead_code)]
impl LocalClient {
#[allow(dead_code)]
pub async fn new_local<P: AsRef<Path>>(root: P, layout: ChunkLayout) -> Result<Self, VfsError> {
let client = ObjectClient::new(LocalFsBackend::new(root));
let meta_handle = create_meta_store_from_url("sqlite::memory:").await?;
let metadata = meta_handle.layer();
let metadata: Arc<dyn MetaStore> = meta_handle.store();
let store = ObjectBlockStore::new(client);
let fs = FileSystem::new(layout, store, metadata)
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LocalClient uses Arc<dyn MetaStore> as the M type parameter, but Arc<dyn MetaStore> does not implement MetaStore (trait bounds require the concrete type itself to implement the trait, not just deref to it). This will fail to compile unless there is a blanket impl<T: MetaStore + ?Sized> MetaStore for Arc<T> (couldn’t find one). Consider either adding a forwarding impl for Arc<T> or changing Client/FileSystem to store Arc<dyn MetaStore> directly rather than as a generic type parameter.

Copilot uses AI. Check for mistakes.
Comment on lines +2125 to +2133
async fn create_test_fs() -> FileSystem<ObjectBlockStore<LocalFsBackend>, Arc<dyn MetaStore>> {
let tmp = tempdir().unwrap();
let layout = ChunkLayout::default();
let client = ObjectClient::new(LocalFsBackend::new(tmp.path()));
let meta_handle = create_meta_store_from_url("sqlite::memory:").await.unwrap();
let metadata: Arc<dyn MetaStore> = meta_handle.store();
let store = ObjectBlockStore::new(client);
let config = FileSystemConfig::default().with_caller(CallerIdentity::root());
FileSystem::with_config(layout, store, metadata, config)
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test helper returns FileSystem<..., Arc<dyn MetaStore>>, but Arc<dyn MetaStore> does not implement the MetaStore trait (only dyn MetaStore does). Without a blanket forwarding impl (e.g., impl<T: MetaStore + ?Sized> MetaStore for Arc<T>), this generic instantiation won’t compile.

Copilot uses AI. Check for mistakes.
Comment on lines 1 to 2
#![allow(dead_code)]

Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#![allow(dead_code)] at the binary crate root suppresses dead-code warnings across the entire slayerfs CLI, which can hide real regressions and make CI less effective. Prefer scoping the allowance to specific modules/items (or removing it if no longer needed).

Suggested change
#![allow(dead_code)]

Copilot uses AI. Check for mistakes.
Comment on lines 1807 to 1813
let target_size = offset + data.len() as u64;
if target_size > inode.file_size() {
inode.update_size(target_size);
meta_layer
.set_file_size(ino, target_size)
.await
.map_err(|e| meta_error_to_io(path, e))?;
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

write_inode updates target_size using data.len() rather than the actual written byte count returned by write_at. If a partial write occurs, this can incorrectly extend the file size beyond what was persisted. Use offset + written as u64 for size updates.

Copilot uses AI. Check for mistakes.
Comment on lines 880 to 902
self.check_access(&parent_attr, AccessMask::WRITE | AccessMask::EXEC, &dir)?;
let parent_attr = self
.meta_layer
.stat(parent_ino)
.await
.map_err(|e| meta_error_to_io(&dir, e))?
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, dir.clone()))?;
if parent_attr.kind != FileType::Dir {
return Err(io::Error::new(io::ErrorKind::NotADirectory, dir));
}
self.check_access(&parent_attr, AccessMask::WRITE | AccessMask::EXEC, &dir)?;

let parent_attr = self
.meta_layer
.stat(parent_ino)
.await
.map_err(|e| meta_error_to_io(&dir, e))?
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, dir.clone()))?;
if parent_attr.kind != FileType::Dir {
return Err(io::Error::new(io::ErrorKind::NotADirectory, dir));
}
self.check_access(&parent_attr, AccessMask::WRITE | AccessMask::EXEC, &dir)?;

Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mkdir contains duplicated stat(parent_ino) calls and repeated check_access(...) calls. This is likely accidental duplication and adds extra MetaStore round-trips on a hot path. Consider keeping a single parent_attr fetch and a single permission check before proceeding.

Suggested change
self.check_access(&parent_attr, AccessMask::WRITE | AccessMask::EXEC, &dir)?;
let parent_attr = self
.meta_layer
.stat(parent_ino)
.await
.map_err(|e| meta_error_to_io(&dir, e))?
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, dir.clone()))?;
if parent_attr.kind != FileType::Dir {
return Err(io::Error::new(io::ErrorKind::NotADirectory, dir));
}
self.check_access(&parent_attr, AccessMask::WRITE | AccessMask::EXEC, &dir)?;
let parent_attr = self
.meta_layer
.stat(parent_ino)
.await
.map_err(|e| meta_error_to_io(&dir, e))?
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, dir.clone()))?;
if parent_attr.kind != FileType::Dir {
return Err(io::Error::new(io::ErrorKind::NotADirectory, dir));
}
self.check_access(&parent_attr, AccessMask::WRITE | AccessMask::EXEC, &dir)?;

Copilot uses AI. Check for mistakes.
let n = self
.write_ino(ino as i64, offset, data)
.await
.map_err(|_| libc::EIO)? as u32;
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FUSE write currently maps every write_ino failure to EIO, which discards useful errno mapping (e.g., EISDIR, ENOSPC, EACCES, ESTALE, etc.) and makes debugging harder. Prefer mapping the underlying VfsError into Errno (similar to other ops in this file).

Suggested change
.map_err(|_| libc::EIO)? as u32;
.map_err(Into::<Errno>::into)? as u32;

Copilot uses AI. Check for mistakes.
Comment on lines 264 to 315
fn make_log_context() -> LogContext {
let uid = unsafe { geteuid() as u32 };
let gid = unsafe { getegid() as u32 };
let pid = std::process::id();
LogContext::new(uid, gid, pid)
}

fn current_ids() -> (u32, u32) {
let uid = unsafe { geteuid() as u32 };
let gid = unsafe { getegid() as u32 };
(uid, gid)
}

#[derive(Debug, Clone)]
pub struct CallerIdentity {
pub uid: u32,
pub gid: u32,
pub groups: Vec<u32>,
}

impl CallerIdentity {
pub fn new(uid: u32, gid: u32, groups: Vec<u32>) -> Self {
Self { uid, gid, groups }
}

pub fn current() -> Self {
let (uid, gid) = current_ids();
let groups = current_groups(gid);
Self { uid, gid, groups }
}

pub fn root() -> Self {
Self {
uid: 0,
gid: 0,
groups: vec![0],
}
}
}

fn current_groups(primary_gid: u32) -> Vec<u32> {
let mut groups = Vec::new();
unsafe {
let count = getgroups(0, std::ptr::null_mut());
if count > 0 {
let mut buf = vec![0 as libc::gid_t; count as usize];
let res = getgroups(count, buf.as_mut_ptr());
if res >= 0 {
groups.extend(buf.into_iter().take(res as usize));
}
}
}
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This module introduces several unsafe libc calls (geteuid, getegid, getgroups) without a // SAFETY: justification. Per repo guidelines, please document why these calls are safe here (expected invariants, pointer validity, thread-safety) and add/point to tests where appropriate.

Copilot uses AI. Check for mistakes.
Comment on lines 278 to 304
/// Returns the last status change time as seconds since UNIX epoch.
pub fn ctime(&self) -> i64 {
self.0.ctime
}

/// Returns the last access time as SystemTime.
pub fn accessed(&self) -> io::Result<SystemTime> {
if self.0.atime >= 0 {
Ok(UNIX_EPOCH + Duration::from_secs(self.0.atime as u64))
} else {
Err(io::Error::other("negative timestamp not supported"))
}
}

/// Returns the last modification time as SystemTime.
pub fn modified(&self) -> io::Result<SystemTime> {
if self.0.mtime >= 0 {
Ok(UNIX_EPOCH + Duration::from_secs(self.0.mtime as u64))
} else {
Err(io::Error::other("negative timestamp not supported"))
}
}

/// Returns the creation/status change time as SystemTime.
pub fn created(&self) -> io::Result<SystemTime> {
if self.0.ctime >= 0 {
Ok(UNIX_EPOCH + Duration::from_secs(self.0.ctime as u64))
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Metadata::{atime,mtime,ctime} and the accessed/modified/created helpers document/compute timestamps as seconds, but MetaStore timestamps are nanoseconds (e.g., Utc::now().timestamp_nanos_opt() in meta/stores/redis_store.rs). This will produce incorrect SystemTime values. Update the docs and use Duration::from_nanos(...) (and clarify the unit consistently across the SDK).

Suggested change
/// Returns the last status change time as seconds since UNIX epoch.
pub fn ctime(&self) -> i64 {
self.0.ctime
}
/// Returns the last access time as SystemTime.
pub fn accessed(&self) -> io::Result<SystemTime> {
if self.0.atime >= 0 {
Ok(UNIX_EPOCH + Duration::from_secs(self.0.atime as u64))
} else {
Err(io::Error::other("negative timestamp not supported"))
}
}
/// Returns the last modification time as SystemTime.
pub fn modified(&self) -> io::Result<SystemTime> {
if self.0.mtime >= 0 {
Ok(UNIX_EPOCH + Duration::from_secs(self.0.mtime as u64))
} else {
Err(io::Error::other("negative timestamp not supported"))
}
}
/// Returns the creation/status change time as SystemTime.
pub fn created(&self) -> io::Result<SystemTime> {
if self.0.ctime >= 0 {
Ok(UNIX_EPOCH + Duration::from_secs(self.0.ctime as u64))
/// Returns the last status change time as nanoseconds since UNIX epoch.
pub fn ctime(&self) -> i64 {
self.0.ctime
}
/// Returns the last access time as SystemTime, interpreting the stored value as
/// nanoseconds since the UNIX epoch.
pub fn accessed(&self) -> io::Result<SystemTime> {
if self.0.atime >= 0 {
Ok(UNIX_EPOCH + Duration::from_nanos(self.0.atime as u64))
} else {
Err(io::Error::other("negative timestamp not supported"))
}
}
/// Returns the last modification time as SystemTime, interpreting the stored value as
/// nanoseconds since the UNIX epoch.
pub fn modified(&self) -> io::Result<SystemTime> {
if self.0.mtime >= 0 {
Ok(UNIX_EPOCH + Duration::from_nanos(self.0.mtime as u64))
} else {
Err(io::Error::other("negative timestamp not supported"))
}
}
/// Returns the creation/status change time as SystemTime, interpreting the stored value as
/// nanoseconds since the UNIX epoch.
pub fn created(&self) -> io::Result<SystemTime> {
if self.0.ctime >= 0 {
Ok(UNIX_EPOCH + Duration::from_nanos(self.0.ctime as u64))

Copilot uses AI. Check for mistakes.
Comment on lines 992 to 998
use crate::vfs::sdk::LocalClient;
use tempfile::tempdir;

async fn local_client() -> (tempfile::TempDir, DynClient) {
let tmp = tempdir().expect("tempdir");
let layout = ChunkLayout::default();
let cli = LocalClient::new_local(tmp.path(), layout)
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SDK tests create paths under / (e.g., /a.txt, /d/e) but LocalClient::new_local uses default FileSystemConfig which enforces permissions with the current caller. This is likely to fail on CI (non-root cannot write to /). Consider constructing the client with FileSystemConfig::default().with_caller(CallerIdentity::root()) (via new_local_with_config) or disabling permission enforcement for these tests.

Suggested change
use crate::vfs::sdk::LocalClient;
use tempfile::tempdir;
async fn local_client() -> (tempfile::TempDir, DynClient) {
let tmp = tempdir().expect("tempdir");
let layout = ChunkLayout::default();
let cli = LocalClient::new_local(tmp.path(), layout)
use crate::vfs::sdk::{CallerIdentity, FileSystemConfig, LocalClient};
use tempfile::tempdir;
async fn local_client() -> (tempfile::TempDir, DynClient) {
let tmp = tempdir().expect("tempdir");
let layout = ChunkLayout::default();
let cfg = FileSystemConfig::default().with_caller(CallerIdentity::root());
let cli = LocalClient::new_local_with_config(tmp.path(), layout, cfg)

Copilot uses AI. Check for mistakes.
Comment on lines 28 to 29
pub async fn new(layout: ChunkLayout, store: S, meta: M) -> Result<Self, String> {
let fs = FileSystem::new(layout, store, meta).await?;
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Client::new returns Result<_, String>, which drops structured error information (and forces callers to parse strings). Since this SDK is adding std::io-style APIs elsewhere, consider returning a structured error (io::Error/VfsError) here as well to keep error semantics consistent.

Suggested change
pub async fn new(layout: ChunkLayout, store: S, meta: M) -> Result<Self, String> {
let fs = FileSystem::new(layout, store, meta).await?;
pub async fn new(layout: ChunkLayout, store: S, meta: M) -> Result<Self, io::Error> {
let fs = FileSystem::new(layout, store, meta)
.await
.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?;

Copilot uses AI. Check for mistakes.
@jerry609 jerry609 marked this pull request as draft January 29, 2026 07:09
@mon3stera mon3stera self-requested a review January 29, 2026 08:01
/// Slayerfs ensures `close-to-open` semantics; each `open` must see the newest file state.
#[derive(Clone)]
pub(crate) struct Inode {
pub struct Inode {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why make Inode public? It is for internal use only.

// background_fetch to pull data from object storage.
// - read_from_slice waits until the slice is Ready (or Invalid) and copies data into
// the caller buffer. Overlapping slices obey "latest slice wins" ordering.
// - Writer commit will call DataReader::invalidate(...) to mark cached slices stale.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file is older than the current version. Please merge the latest changes from main.

/// - Path-based operations (Open, Stat, Mkdir, etc.)
/// - `File` handle with lazy reader/writer
/// - Optional access logging
pub struct FileSystem<S, M>
Copy link
Collaborator

@mon3stera mon3stera Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not do this with wrapping a VFS<S, M>? Is there any obstacle?

pub struct FileSystem<S, M>
where
S: BlockStore + Send + Sync + 'static,
M: MetaStore + 'static,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider use the MetaLayer for unified abstraction.

pub(crate) mod backend;
pub(crate) mod cache;
pub mod config;
pub mod cache;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be pub(crate).

impl<S: BlockStore + Send + Sync + 'static, M: MetaLayer + Send + Sync + 'static> Client<S, M> {
pub async fn new(layout: ChunkLayout, store: S, meta_layer: Arc<M>) -> Result<Self, VfsError> {
let fs = VFS::with_meta_layer(layout, store, meta_layer)?;
impl<S: BlockStore + Send + Sync + 'static, M: MetaStore + 'static> Client<S, M> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider use MetaLayer.

}
let _ = self.fs.closedir(fh);
Ok(entries)
pub async fn write_at(&self, path: &str, offset: u64, data: &[u8]) -> Result<usize, String> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not a good idea to handle error by String.

@jerry609 jerry609 force-pushed the feat/slayerfs-stdlike-sdk-pr branch from 0794300 to d205f41 Compare January 31, 2026 01:43
@genedna
Copy link
Member

genedna commented Jan 31, 2026

@codex review

@genedna
Copy link
Member

genedna commented Jan 31, 2026

@claude

@github-actions
Copy link

github-actions bot commented Jan 31, 2026

Claude finished @genedna's task in 2m 2s —— View job


Code Review Complete

  • Review PR context and previous feedback
  • Examine core architecture changes (FileSystem, VFS, file_io)
  • Review SDK implementation and error handling
  • Check metadata and xattr implementation
  • Review FUSE integration changes
  • Assess test coverage and quality
  • Review documentation updates
  • Compile comprehensive feedback

Overview

This PR introduces a significant refactoring to align SlayerFS with a std-like SDK pattern, adds a new fs::FileSystem layer for path-based operations, and reorganizes the file I/O module structure. The changes are substantial (5782 additions, 1310 deletions across 29 files) and represent a major architectural shift.

Critical Issues

1. Code Duplication in fs/mod.rs:842-903 ⚠️ HIGH PRIORITY

Lines 842-903 contain severe code duplication - the same parent_attr fetch and permission check are repeated 4 times within the mkdir function. This adds unnecessary MetaStore round-trips on a hot path and makes the code error-prone.

Location: project/slayerfs/src/fs/mod.rs:842-903

// Lines 880-891 (duplicated 4 times!)
self.check_access(&parent_attr, AccessMask::WRITE | AccessMask::EXEC, &dir)?;
let parent_attr = self
    .meta_layer
    .stat(parent_ino)
    .await
    .map_err(|e| meta_error_to_io(&dir, e))?
    .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, dir.clone()))?;

Recommendation: Keep a single parent_attr fetch and a single permission check before proceeding. Fix this →

2. Global Dead Code Allowance in main.rs:1 ⚠️ MEDIUM PRIORITY

The binary crate has #![allow(dead_code)] at the root, which suppresses warnings across the entire CLI and hides potential regressions.

Location: project/slayerfs/src/main.rs:1

Recommendation: Remove the global allowance and scope it to specific modules/items that genuinely need it, or eliminate unused code. This will make CI more effective. Fix this →

3. Unsafe Code Without Safety Justification ⚠️ MEDIUM PRIORITY

Multiple unsafe libc calls in fs/mod.rs (lines 264-318) lack // SAFETY: comments explaining why they're safe:

Locations:

  • project/slayerfs/src/fs/mod.rs:264 - unsafe { geteuid() }
  • project/slayerfs/src/fs/mod.rs:265 - unsafe { getegid() }
  • project/slayerfs/src/fs/mod.rs:306-314 - unsafe { getgroups() }

Recommendation: Add // SAFETY: documentation for each unsafe block explaining invariants, pointer validity, and thread-safety. Fix this →

4. Timestamp Unit Mismatch in sdk_fs.rs ⚠️ HIGH PRIORITY

The Metadata struct in sdk_fs.rs documents timestamps as "seconds since UNIX epoch" (lines 541-553) but converts them using Duration::from_secs(). However, the MetaStore uses nanoseconds (as seen in meta/stores/redis_store.rs using timestamp_nanos_opt()). This produces incorrect SystemTime values.

Location: project/slayerfs/src/sdk_fs.rs:557-581

Current code:

pub fn accessed(&self) -> io::Result<SystemTime> {
    if self.0.atime >= 0 {
        Ok(UNIX_EPOCH + Duration::from_secs(self.0.atime as u64))  // ❌ Wrong!
    } else {
        Err(io::Error::other("negative timestamp not supported"))
    }
}

Recommendation: Change to Duration::from_nanos() and update documentation to specify nanoseconds. Fix this →

5. FUSE Write Error Mapping Loss ⚠️ MEDIUM PRIORITY

The FUSE write handler (line 313) maps every write_ino failure to EIO, discarding useful errno information (EISDIR, ENOSPC, EACCES, ESTALE, etc.) and making debugging harder.

Location: project/slayerfs/src/fuse/mod.rs:310-314

let n = self
    .write_ino(ino as i64, offset, data)
    .await
    .map_err(|_| libc::EIO)? as u32;  // ❌ Loses error context!

Recommendation: Map the underlying VfsError into Errno similar to other ops in the file. Fix this →

Major Concerns

6. Visibility and Modularity Issues

Several previous review comments remain unaddressed:

  • file_io/inode.rs:7: Inode is made pub but is described as "internal use only" in the comment. Consider making it pub(crate) instead.
  • vfs/mod.rs:19: Chunk-id helpers are re-exported as pub but should be pub(crate).

Recommendation: Review module visibility and ensure proper encapsulation. Internal implementation details should not leak into the public API.

7. Architecture Question: VFS Wrapping vs MetaLayer Usage

Location: project/slayerfs/src/fs/mod.rs:398-408

The FileSystem struct owns a VFS instance and wraps its operations rather than using MetaLayer directly:

pub struct FileSystem<S, M> {
    meta_layer: Arc<MetaClient<M>>,
    vfs: VFS<S, MetaClient<M>>,  // Wraps VFS instead of using MetaLayer
    config: FileSystemConfig,
    // ...
}

Previous reviewer question (mon3stera): "Why not do this with wrapping a VFS<S, M>? Is there any obstacle?" and "Consider use the MetaLayer for unified abstraction."

Analysis: The current design creates a layered hierarchy (FileSystem → VFS → MetaClient → MetaStore), which adds an extra indirection layer. While this provides clear separation of concerns, it may introduce unnecessary complexity.

Recommendation: The PR author should clarify the architectural decision: is the extra VFS layer necessary, or could FileSystem interact with MetaLayer directly? This impacts future maintainability.

8. Error Handling Consistency

Previous reviewer comment (mon3stera): "It is not a good idea to handle error by String." referring to error handling in the SDK.

While some progress has been made with proper error types (VfsError::from_meta), there are still areas where error context is lost (see issue #5 above with FUSE write mapping).

Recommendation: Continue improving error handling consistency across the codebase. Use structured error types everywhere rather than string-based errors.

Code Quality Issues

9. Incomplete Test Coverage

While the PR adds tests for the new modules, the test coverage is minimal:

  • fs/mod.rs: Only 5 basic tests (lines 2099-2229) for a 2200-line module
  • sdk_fs.rs: Tests use / as root which will fail on CI without root permissions
  • Missing tests for error conditions, edge cases, and concurrent access patterns

Recommendation: Add comprehensive tests including:

  • Error cases (permission denied, not found, etc.)
  • Concurrent file access
  • Edge cases (large files, symlink cycles, etc.)
  • Use CallerIdentity::root() in test fixtures or disable permission enforcement

10. Documentation Gaps

Several areas lack adequate documentation:

  • The relationship between fs::FileSystem, VFS, and sdk_fs::Client is unclear
  • xattr implementation documentation is minimal
  • Migration guide for users of the old API is missing

Recommendation: Add module-level documentation explaining:

  1. Architecture overview (fs vs vfs separation)
  2. When to use each API layer
  3. Migration guide from old to new SDK

Positive Aspects ✅

  1. Good error mapping layer: VfsError::from_meta provides comprehensive mapping from MetaError to VFS errors
  2. Consistent API design: The std-like SDK API is intuitive and follows Rust conventions
  3. Well-structured file_io module: Clean separation of reader/writer/registry components
  4. Proper use of Arc for sharing: Efficient reference counting for shared data structures
  5. xattr support: Properly integrated with SeaORM and database layer

Performance Considerations

  1. Code duplication impact: The 4x duplication in mkdir (issue 添加 Distribution 目录,用于 Rust 实现 Distribution 协议 #1) creates unnecessary MetaStore round-trips
  2. Append mode locking: Uses global DashMap with weak references for per-file append locks - good design for concurrent appends
  3. Reader cache invalidation: file_io/reader.rs has proper cleanup logic to prevent memory leaks

Security Review

  1. Permission checks: Properly implemented in FileSystem layer with POSIX-style ACL support
  2. Input validation: Path normalization prevents .. traversal
  3. Unsafe code: Needs safety documentation (see issue 更新 rk8s 目录结构和文档信息 #3)
  4. No obvious security vulnerabilities: Standard file operations are implemented safely

Recommendations Summary

High Priority:

  1. Fix code duplication in mkdir function
  2. Fix timestamp unit mismatch in sdk_fs.rs

Medium Priority:
3. Remove global dead code allowance
4. Add safety documentation for unsafe code
5. Fix FUSE write error mapping
6. Address visibility/modularity issues
7. Improve test coverage

For Discussion:
8. Clarify FileSystem vs VFS vs MetaLayer architecture decision
9. Add comprehensive documentation for the new layering

Conclusion

This is a substantial and well-structured refactoring that moves SlayerFS toward a more ergonomic API. However, the critical issues (code duplication, timestamp bugs, and missing safety documentation) should be addressed before merging. The architectural questions about VFS wrapping vs MetaLayer usage should also be clarified to ensure long-term maintainability.

The code quality is generally good, with proper error handling and clean separation of concerns. With the recommended fixes, this will be a solid foundation for the std-like SDK.


Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 155eba404f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 64 to 68
while remaining > 0 {
let chunk_index = layout.chunk_index_of(cursor);
let chunk_offset = layout.within_chunk_offset(cursor) as u32;
let avail = layout.chunk_size.saturating_sub(chunk_offset as u64);
let take = std::cmp::min(remaining, avail) as u32;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Guard against chunk offsets exceeding u32 range

The new split_chunk_spans computes chunk_offset with as u32, which silently truncates when layout.chunk_size (or the intra-chunk offset) exceeds u32::MAX. In that configuration, reads/writes will target the wrong offset within the chunk, corrupting data for large chunk sizes. The previous implementation used u32::try_from to reject oversized offsets; this should either validate and error or constrain ChunkLayout so offsets always fit in u32.

Useful? React with 👍 / 👎.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 29 out of 29 changed files in this pull request and generated 4 comments.

Comment on lines 881 to 903
self.check_access(&parent_attr, AccessMask::WRITE | AccessMask::EXEC, &dir)?;
let parent_attr = self
.meta_layer
.stat(parent_ino)
.await
.map_err(|e| meta_error_to_io(&dir, e))?
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, dir.clone()))?;
if parent_attr.kind != FileType::Dir {
return Err(io::Error::new(io::ErrorKind::NotADirectory, dir));
}
self.check_access(&parent_attr, AccessMask::WRITE | AccessMask::EXEC, &dir)?;

let parent_attr = self
.meta_layer
.stat(parent_ino)
.await
.map_err(|e| meta_error_to_io(&dir, e))?
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, dir.clone()))?;
if parent_attr.kind != FileType::Dir {
return Err(io::Error::new(io::ErrorKind::NotADirectory, dir));
}
self.check_access(&parent_attr, AccessMask::WRITE | AccessMask::EXEC, &dir)?;

Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mkdir() repeats the same stat(parent_ino) + kind check + check_access(...) sequence multiple times, which is redundant and adds extra metadata round-trips. Please keep a single parent_attr fetch and a single access check before proceeding.

Suggested change
self.check_access(&parent_attr, AccessMask::WRITE | AccessMask::EXEC, &dir)?;
let parent_attr = self
.meta_layer
.stat(parent_ino)
.await
.map_err(|e| meta_error_to_io(&dir, e))?
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, dir.clone()))?;
if parent_attr.kind != FileType::Dir {
return Err(io::Error::new(io::ErrorKind::NotADirectory, dir));
}
self.check_access(&parent_attr, AccessMask::WRITE | AccessMask::EXEC, &dir)?;
let parent_attr = self
.meta_layer
.stat(parent_ino)
.await
.map_err(|e| meta_error_to_io(&dir, e))?
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, dir.clone()))?;
if parent_attr.kind != FileType::Dir {
return Err(io::Error::new(io::ErrorKind::NotADirectory, dir));
}
self.check_access(&parent_attr, AccessMask::WRITE | AccessMask::EXEC, &dir)?;

Copilot uses AI. Check for mistakes.
Comment on lines 727 to 733
let fi = self.resolve(&path, true).await?;
if fi.is_dir() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"cannot open directory as file",
));
}
Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

open() returns io::ErrorKind::InvalidInput for "cannot open directory as file". For std/tokio-like semantics this should be ErrorKind::IsADirectory (and helps downstream error mapping stay consistent).

Copilot uses AI. Check for mistakes.
Comment on lines 541 to 581
/// Returns the last access time as seconds since UNIX epoch.
pub fn atime(&self) -> i64 {
self.0.atime
}

/// Returns the last modification time as seconds since UNIX epoch.
pub fn mtime(&self) -> i64 {
self.0.mtime
}

/// Returns the last status change time as seconds since UNIX epoch.
pub fn ctime(&self) -> i64 {
self.0.ctime
}

/// Returns the last access time as SystemTime.
pub fn accessed(&self) -> io::Result<SystemTime> {
if self.0.atime >= 0 {
Ok(UNIX_EPOCH + Duration::from_secs(self.0.atime as u64))
} else {
Err(io::Error::other("negative timestamp not supported"))
}
}

/// Returns the last modification time as SystemTime.
pub fn modified(&self) -> io::Result<SystemTime> {
if self.0.mtime >= 0 {
Ok(UNIX_EPOCH + Duration::from_secs(self.0.mtime as u64))
} else {
Err(io::Error::other("negative timestamp not supported"))
}
}

/// Returns the creation/status change time as SystemTime.
pub fn created(&self) -> io::Result<SystemTime> {
if self.0.ctime >= 0 {
Ok(UNIX_EPOCH + Duration::from_secs(self.0.ctime as u64))
} else {
Err(io::Error::other("negative timestamp not supported"))
}
}
Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Metadata::{accessed,modified,created} (and the atime/mtime/ctime docstrings) treat FileAttr timestamps as seconds, but other parts of this PR (e.g. VFS time updates) use SystemTime::now().duration_since(UNIX_EPOCH).as_nanos() as i64, implying these fields are nanoseconds. This will produce incorrect SystemTime values for SDK consumers; please align the units (and docs) with the actual timestamp representation.

Copilot uses AI. Check for mistakes.
@jerry609 jerry609 marked this pull request as ready for review January 31, 2026 07:37
@jerry609 jerry609 marked this pull request as draft January 31, 2026 07:42
@jerry609 jerry609 force-pushed the feat/slayerfs-stdlike-sdk-pr branch 2 times, most recently from fb9dc00 to 02c3a42 Compare January 31, 2026 09:00
@jerry609 jerry609 marked this pull request as ready for review January 31, 2026 09:08
@jerry609 jerry609 force-pushed the feat/slayerfs-stdlike-sdk-pr branch from 02c3a42 to 0accd10 Compare January 31, 2026 09:15
Ivanbeethoven
Ivanbeethoven previously approved these changes Feb 1, 2026
auto-merge was automatically disabled February 1, 2026 08:24

Head branch was pushed to by a user without write access

@jerry609 jerry609 requested a review from mon3stera February 1, 2026 08:27
@mon3stera mon3stera added this pull request to the merge queue Feb 1, 2026
Merged via the queue into rk8s-dev:main with commit 9fd4e27 Feb 1, 2026
8 of 14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[r2cn] SlayerFS Rust SDK易用性增强 / File API 封装

4 participants