Implement incremental save for WebDAV versioning

Changes:
1. Added dirty flag to WebDavVersioning to track unsaved changes
2. Modified create_version() to only mark dirty=true instead of immediate save_index()
3. Added flush() method to save dirty index periodically
4. Added background thread in server.rs to flush every 60 seconds
5. Async index loading on startup (spawn thread to avoid blocking OPTIONS/PROPFIND)

Expected performance:
- PUT operations: still fast (dirty flag only, no save_index() blocking)
- Index persistence: flush every 60 seconds (or on shutdown)
- OPTIONS/PROPFIND: no blocking (async index loading)

Test results:
- PUT (31B): 0.053s (53ms) 
- Index loading: async thread started 
- Flush thread: started but blocked by launchd auto-restart ⚠️

Next: Test flush thread in production environment (without launchd)
This commit is contained in:
Warren
2026-06-30 05:29:09 +08:00
parent 86984295bf
commit 65cd68cad4
6 changed files with 49 additions and 13 deletions

Binary file not shown.

View File

@@ -0,0 +1 @@
Small test content

View File

@@ -0,0 +1 @@
Small test content

File diff suppressed because one or more lines are too long

View File

@@ -167,6 +167,20 @@ pub async fn run(port: u16, file: Option<String>) -> anyhow::Result<()> {
Arc::new(crate::webdav_version::WebDavVersioning::new(vs))
};
// Background thread to periodically flush version index (every 60 seconds)
let versioning_flush = webdav_versioning.clone();
log::info!("Starting version index flush thread (interval=60s)");
std::thread::spawn(move || {
log::info!("Version flush thread started");
loop {
std::thread::sleep(std::time::Duration::from_secs(60));
log::info!("Version flush thread waking up");
if let Err(e) = versioning_flush.flush() {
log::warn!("Failed to flush version index: {:?}", e);
}
}
});
log::info!(
"WebDAV configured: parent={}, versioning={}, upload_hook={}, use_s3={}",
webdav_parent.display(),
@@ -2544,7 +2558,7 @@ fn create_handler_for_user(
user_root,
Some(upload_hook.clone()),
username.to_string(),
None, // Disabled versioning to fix PUT timeout (save_index() blocks)
Some(versioning.clone()), // Re-enabled with incremental save
locks_file,
)
}

View File

@@ -39,24 +39,31 @@ pub struct WebDavVersioning {
db: Arc<RwLock<HashMap<String, Vec<u8>>>>,
version_storage: PathBuf,
index_path: PathBuf,
dirty: Arc<RwLock<bool>>, // Track if index needs saving
}
impl WebDavVersioning {
pub fn new(version_storage: PathBuf) -> Self {
let index_path = version_storage.join("version_index.json");
let db = Arc::new(RwLock::new(HashMap::new()));
let dirty = Arc::new(RwLock::new(false));
// Load persisted index from disk
// TEMPORARILY DISABLED for performance testing (index loading causes 10+ second delay)
// if index_path.exists() {
// if let Ok(json) = std::fs::read_to_string(&index_path) {
// if let Ok(map) = serde_json::from_str::<HashMap<String, Vec<u8>>>(&json) {
// *recover_rwlock(db.write()) = map;
// }
// }
// }
// Load index asynchronously to avoid blocking OPTIONS/PROPFIND
let db_clone = db.clone();
let index_path_clone = index_path.clone();
std::thread::spawn(move || {
if index_path_clone.exists() {
if let Ok(json) = std::fs::read_to_string(&index_path_clone) {
if let Ok(map) = serde_json::from_str::<HashMap<String, Vec<u8>>>(&json) {
let len = map.len();
*recover_rwlock(db_clone.write()) = map;
log::info!("Loaded {} version entries from index", len);
}
}
}
});
Self { db, version_storage, index_path }
Self { db, version_storage, index_path, dirty }
}
fn save_index(&self) -> Result<(), VersionError> {
@@ -66,6 +73,18 @@ impl WebDavVersioning {
Ok(())
}
/// Flush dirty index to disk (call periodically or on shutdown)
pub fn flush(&self) -> Result<(), VersionError> {
let dirty_flag = *recover_rwlock(self.dirty.read());
log::info!("flush() called, dirty={}", dirty_flag);
if dirty_flag {
self.save_index()?;
*recover_rwlock(self.dirty.write()) = false;
log::info!("Flushed version index to disk");
}
Ok(())
}
pub fn create_version(
&self,
file_path: &str,
@@ -104,7 +123,8 @@ impl WebDavVersioning {
self.update_version_history(file_path, &version_id)?;
self.save_index()?;
// Mark dirty instead of immediate save (incremental save strategy)
*recover_rwlock(self.dirty.write()) = true;
Ok(version_info)
}