mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2025-12-11 20:15:30 +01:00
feat: Introduce persistence completion handling in job management
- Added a new `persistence_complete_tx` channel to facilitate signaling when job state persistence is complete. - Updated `JobExecutor` and `JobManager` to manage the persistence completion channel, ensuring proper signaling during job execution and state management. - Enhanced the `ChangeDetector` to include a cache for file existence checks, improving performance and accuracy in detecting changes. - Refactored related methods to streamline job state handling and improve logging for persistence operations.
This commit is contained in:
parent
dbb3c56d30
commit
6e25fbfe41
@ -12,35 +12,39 @@ pub enum JobError {
|
||||
/// Job was interrupted (paused or cancelled)
|
||||
#[error("Job was interrupted")]
|
||||
Interrupted,
|
||||
|
||||
|
||||
/// Job execution failed
|
||||
#[error("Job execution failed: {0}")]
|
||||
ExecutionFailed(String),
|
||||
|
||||
|
||||
/// Job execution failed
|
||||
#[error("Error in job execution: {0}")]
|
||||
ErrorInExecution(String),
|
||||
|
||||
/// Database operation failed
|
||||
#[error("Database error: {0}")]
|
||||
Database(String),
|
||||
|
||||
|
||||
/// Serialization/deserialization error
|
||||
#[error("Serialization error: {0}")]
|
||||
Serialization(String),
|
||||
|
||||
|
||||
/// Job not found
|
||||
#[error("Job not found: {0}")]
|
||||
NotFound(String),
|
||||
|
||||
|
||||
/// Invalid job state
|
||||
#[error("Invalid job state: {0}")]
|
||||
InvalidState(String),
|
||||
|
||||
|
||||
/// Task system error
|
||||
#[error("Task system error: {0}")]
|
||||
TaskSystem(String),
|
||||
|
||||
|
||||
/// I/O error
|
||||
#[error("I/O error: {0}")]
|
||||
Io(String),
|
||||
|
||||
|
||||
/// Other errors
|
||||
#[error("{0}")]
|
||||
Other(String),
|
||||
@ -48,7 +52,7 @@ pub enum JobError {
|
||||
|
||||
impl From<String> for JobError {
|
||||
fn from(msg: String) -> Self {
|
||||
Self::ExecutionFailed(msg)
|
||||
Self::ErrorInExecution(msg)
|
||||
}
|
||||
}
|
||||
|
||||
@ -67,24 +71,24 @@ impl From<sea_orm::DbErr> for JobError {
|
||||
impl JobError {
|
||||
/// Create an execution failed error
|
||||
pub fn execution<T: fmt::Display>(msg: T) -> Self {
|
||||
Self::ExecutionFailed(msg.to_string())
|
||||
Self::ErrorInExecution(msg.to_string())
|
||||
}
|
||||
|
||||
|
||||
/// Create a serialization error
|
||||
pub fn serialization<T: fmt::Display>(msg: T) -> Self {
|
||||
Self::Serialization(msg.to_string())
|
||||
}
|
||||
|
||||
|
||||
/// Create an invalid state error
|
||||
pub fn invalid_state<T: fmt::Display>(msg: T) -> Self {
|
||||
Self::InvalidState(msg.to_string())
|
||||
}
|
||||
|
||||
|
||||
/// Create a task system error
|
||||
pub fn task_system<T: fmt::Display>(msg: T) -> Self {
|
||||
Self::TaskSystem(msg.to_string())
|
||||
}
|
||||
|
||||
|
||||
/// Check if this error is due to interruption
|
||||
pub fn is_interrupted(&self) -> bool {
|
||||
matches!(self, Self::Interrupted)
|
||||
|
||||
@ -40,6 +40,7 @@ pub struct JobExecutorState {
|
||||
pub job_logging_config: Option<JobLoggingConfig>,
|
||||
pub job_logs_dir: Option<PathBuf>,
|
||||
pub file_logger: Option<Arc<super::logger::FileJobLogger>>,
|
||||
pub persistence_complete_tx: Option<tokio::sync::oneshot::Sender<()>>,
|
||||
}
|
||||
|
||||
impl<J: JobHandler> JobExecutor<J> {
|
||||
@ -57,6 +58,7 @@ impl<J: JobHandler> JobExecutor<J> {
|
||||
volume_manager: Option<Arc<crate::volume::VolumeManager>>,
|
||||
job_logging_config: Option<JobLoggingConfig>,
|
||||
job_logs_dir: Option<PathBuf>,
|
||||
persistence_complete_tx: Option<tokio::sync::oneshot::Sender<()>>,
|
||||
) -> Self {
|
||||
// Create file logger if job logging is enabled
|
||||
let file_logger = if let (Some(config), Some(logs_dir)) = (&job_logging_config, &job_logs_dir) {
|
||||
@ -93,6 +95,7 @@ impl<J: JobHandler> JobExecutor<J> {
|
||||
job_logging_config,
|
||||
job_logs_dir,
|
||||
file_logger,
|
||||
persistence_complete_tx,
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -308,6 +311,12 @@ impl<J: JobHandler> JobExecutor<J> {
|
||||
}
|
||||
}
|
||||
|
||||
// Signal that persistence is complete
|
||||
if let Some(tx) = self.state.persistence_complete_tx.take() {
|
||||
let _ = tx.send(());
|
||||
info!("PAUSE_STATE_SAVE: Job {} signaled persistence completion", self.state.job_id);
|
||||
}
|
||||
|
||||
Ok(ExecStatus::Paused)
|
||||
} else {
|
||||
// Job was cancelled
|
||||
@ -369,9 +378,22 @@ impl<J: JobHandler> JobExecutor<J> {
|
||||
.checkpoint_handler
|
||||
.delete_checkpoint(self.state.job_id)
|
||||
.await;
|
||||
|
||||
// Consume persistence channel if it wasn't used (job completed normally)
|
||||
if let Some(tx) = self.state.persistence_complete_tx.take() {
|
||||
let _ = tx.send(());
|
||||
}
|
||||
|
||||
Ok(ExecStatus::Done(().into()))
|
||||
}
|
||||
Err(e) => Err(e.clone()),
|
||||
Err(e) => {
|
||||
// Consume persistence channel if it wasn't used (job failed)
|
||||
if let Some(tx) = self.state.persistence_complete_tx.take() {
|
||||
let _ = tx.send(());
|
||||
}
|
||||
|
||||
Err(e.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -400,6 +422,7 @@ impl<J: JobHandler + std::fmt::Debug> ErasedJob for JobExecutor<J> {
|
||||
volume_manager: Option<std::sync::Arc<crate::volume::VolumeManager>>,
|
||||
job_logging_config: Option<crate::config::JobLoggingConfig>,
|
||||
job_logs_dir: Option<std::path::PathBuf>,
|
||||
persistence_complete_tx: Option<tokio::sync::oneshot::Sender<()>>,
|
||||
) -> Box<dyn sd_task_system::Task<JobError>> {
|
||||
// Update the executor's state with the new parameters
|
||||
let mut executor = *self;
|
||||
@ -436,6 +459,7 @@ impl<J: JobHandler + std::fmt::Debug> ErasedJob for JobExecutor<J> {
|
||||
job_logging_config,
|
||||
job_logs_dir,
|
||||
file_logger,
|
||||
persistence_complete_tx,
|
||||
};
|
||||
|
||||
Box::new(executor)
|
||||
|
||||
@ -41,6 +41,7 @@ struct RunningJob {
|
||||
task_handle: TaskHandle<JobError>,
|
||||
status_tx: watch::Sender<JobStatus>,
|
||||
latest_progress: Arc<Mutex<Option<Progress>>>,
|
||||
persistence_complete_rx: Option<tokio::sync::oneshot::Receiver<()>>,
|
||||
}
|
||||
|
||||
impl JobManager {
|
||||
@ -226,6 +227,9 @@ impl JobManager {
|
||||
output: Arc::new(Mutex::new(None)),
|
||||
};
|
||||
|
||||
// Create persistence completion channel
|
||||
let (persistence_complete_tx, persistence_complete_rx) = tokio::sync::oneshot::channel();
|
||||
|
||||
// Create executor using the erased job
|
||||
let executor = erased_job.create_executor(
|
||||
job_id,
|
||||
@ -242,6 +246,7 @@ impl JobManager {
|
||||
volume_manager,
|
||||
self.context.job_logging_config.clone(),
|
||||
self.context.job_logs_dir.clone(),
|
||||
Some(persistence_complete_tx),
|
||||
);
|
||||
|
||||
// Dispatch to task system
|
||||
@ -261,6 +266,7 @@ impl JobManager {
|
||||
task_handle: handle_result,
|
||||
status_tx: status_tx.clone(),
|
||||
latest_progress,
|
||||
persistence_complete_rx: Some(persistence_complete_rx),
|
||||
},
|
||||
);
|
||||
|
||||
@ -486,6 +492,9 @@ impl JobManager {
|
||||
output: Arc::new(Mutex::new(None)),
|
||||
};
|
||||
|
||||
// Create persistence completion channel
|
||||
let (persistence_complete_tx, persistence_complete_rx) = tokio::sync::oneshot::channel();
|
||||
|
||||
// Create executor
|
||||
let executor = JobExecutor::new(
|
||||
job,
|
||||
@ -503,6 +512,7 @@ impl JobManager {
|
||||
volume_manager,
|
||||
self.context.job_logging_config.clone(),
|
||||
self.context.job_logs_dir.clone(),
|
||||
Some(persistence_complete_tx),
|
||||
);
|
||||
|
||||
// Dispatch to task system
|
||||
@ -521,6 +531,7 @@ impl JobManager {
|
||||
task_handle: handle_result,
|
||||
status_tx: status_tx.clone(),
|
||||
latest_progress: latest_progress.clone(),
|
||||
persistence_complete_rx: Some(persistence_complete_rx),
|
||||
},
|
||||
);
|
||||
|
||||
@ -1028,6 +1039,9 @@ impl JobManager {
|
||||
output: Arc::new(Mutex::new(None)),
|
||||
};
|
||||
|
||||
// Create persistence completion channel
|
||||
let (persistence_complete_tx, persistence_complete_rx) = tokio::sync::oneshot::channel();
|
||||
|
||||
// Create executor using the erased job
|
||||
let executor = erased_job.create_executor(
|
||||
job_id,
|
||||
@ -1044,6 +1058,7 @@ impl JobManager {
|
||||
volume_manager,
|
||||
self.context.job_logging_config.clone(),
|
||||
self.context.job_logs_dir.clone(),
|
||||
Some(persistence_complete_tx),
|
||||
);
|
||||
|
||||
// Dispatch to task system
|
||||
@ -1062,6 +1077,7 @@ impl JobManager {
|
||||
task_handle,
|
||||
status_tx: status_tx.clone(),
|
||||
latest_progress,
|
||||
persistence_complete_rx: Some(persistence_complete_rx),
|
||||
},
|
||||
);
|
||||
|
||||
@ -1361,6 +1377,9 @@ impl JobManager {
|
||||
output: Arc::new(Mutex::new(None)),
|
||||
};
|
||||
|
||||
// Create persistence completion channel
|
||||
let (persistence_complete_tx, persistence_complete_rx) = tokio::sync::oneshot::channel();
|
||||
|
||||
// Create executor
|
||||
let executor = erased_job.create_executor(
|
||||
job_id,
|
||||
@ -1377,6 +1396,7 @@ impl JobManager {
|
||||
volume_manager,
|
||||
self.context.job_logging_config.clone(),
|
||||
self.context.job_logs_dir.clone(),
|
||||
Some(persistence_complete_tx),
|
||||
);
|
||||
|
||||
// Dispatch to task system
|
||||
@ -1395,6 +1415,7 @@ impl JobManager {
|
||||
task_handle,
|
||||
status_tx: status_tx.clone(),
|
||||
latest_progress,
|
||||
persistence_complete_rx: Some(persistence_complete_rx),
|
||||
},
|
||||
);
|
||||
|
||||
@ -1570,6 +1591,48 @@ impl JobManager {
|
||||
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
|
||||
}
|
||||
|
||||
// Wait for all paused jobs to complete state persistence
|
||||
info!("Waiting for job state persistence to complete...");
|
||||
let persistence_start_time = tokio::time::Instant::now();
|
||||
let persistence_timeout = std::time::Duration::from_secs(10); // Shorter timeout for persistence
|
||||
|
||||
// Collect all persistence receivers
|
||||
let mut persistence_receivers = Vec::new();
|
||||
{
|
||||
let mut running_jobs = self.running_jobs.write().await;
|
||||
for (job_id, running_job) in running_jobs.iter_mut() {
|
||||
if let Some(rx) = running_job.persistence_complete_rx.take() {
|
||||
persistence_receivers.push((*job_id, rx));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info!("Waiting for {} jobs to complete state persistence", persistence_receivers.len());
|
||||
|
||||
// Wait for all persistence operations to complete
|
||||
for (job_id, rx) in persistence_receivers {
|
||||
tokio::select! {
|
||||
result = rx => {
|
||||
match result {
|
||||
Ok(()) => {
|
||||
info!("Job {} completed state persistence", job_id);
|
||||
}
|
||||
Err(_) => {
|
||||
warn!("Job {} persistence channel closed without signal", job_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = tokio::time::sleep(persistence_timeout) => {
|
||||
warn!("Timeout waiting for job {} state persistence after {}s",
|
||||
job_id, persistence_timeout.as_secs());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let persistence_elapsed = persistence_start_time.elapsed();
|
||||
info!("State persistence completed in {:.2}s", persistence_elapsed.as_secs_f32());
|
||||
|
||||
// Close database connection properly
|
||||
info!("Closing job database connection");
|
||||
|
||||
|
||||
@ -135,6 +135,7 @@ pub trait ErasedJob: Send + Sync + std::fmt::Debug + 'static {
|
||||
volume_manager: Option<std::sync::Arc<crate::volume::VolumeManager>>,
|
||||
job_logging_config: Option<crate::config::JobLoggingConfig>,
|
||||
job_logs_dir: Option<std::path::PathBuf>,
|
||||
persistence_complete_tx: Option<tokio::sync::oneshot::Sender<()>>,
|
||||
) -> Box<dyn sd_task_system::Task<crate::infra::job::error::JobError>>;
|
||||
|
||||
fn serialize_state(&self) -> Result<Vec<u8>, crate::infra::job::error::JobError>;
|
||||
|
||||
@ -51,6 +51,9 @@ pub struct ChangeDetector {
|
||||
|
||||
/// Precision for timestamp comparison (some filesystems have lower precision)
|
||||
timestamp_precision_ms: i64,
|
||||
|
||||
/// Cache for file existence checks to avoid repeated filesystem calls
|
||||
existence_cache: HashMap<PathBuf, bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@ -70,6 +73,7 @@ impl ChangeDetector {
|
||||
path_to_entry: HashMap::new(),
|
||||
inode_to_path: HashMap::new(),
|
||||
timestamp_precision_ms: 1, // Default to 1ms precision
|
||||
existence_cache: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -146,7 +150,7 @@ impl ChangeDetector {
|
||||
|
||||
/// Check if a path represents a change
|
||||
pub fn check_path(
|
||||
&self,
|
||||
&mut self,
|
||||
path: &Path,
|
||||
metadata: &std::fs::Metadata,
|
||||
inode: Option<u64>,
|
||||
@ -167,31 +171,33 @@ impl ChangeDetector {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Path not in database - check if it's a move
|
||||
// Path not in database - check if it's a move or hard link
|
||||
if let Some(inode_val) = inode {
|
||||
if let Some(old_path) = self.inode_to_path.get(&inode_val) {
|
||||
if let Some(old_path) = self.inode_to_path.get(&inode_val).cloned() {
|
||||
if old_path != path {
|
||||
// Same inode, different path - check if it's actually a move or just duplicate content
|
||||
if let Some(db_entry) = self.path_to_entry.get(old_path) {
|
||||
// Check if this is a false positive: same file attributes but different paths
|
||||
// This happens with hard links, duplicate entries, or filesystem deduplication
|
||||
if self.is_same_file_content(db_entry, path, metadata, inode_val) {
|
||||
// Same file content at different paths - skip processing to avoid false moves
|
||||
if let Some(db_entry) = self.path_to_entry.get(&old_path).cloned() {
|
||||
// Check if the old path still exists on disk (with caching)
|
||||
// - If old path exists: This is a hard link (both paths are valid)
|
||||
// - If old path doesn't exist: This is a genuine move
|
||||
if self.path_exists_cached(&old_path) {
|
||||
// Hard link: Both paths exist and point to same inode
|
||||
// Treat current path as a new entry (don't skip it)
|
||||
use tracing::debug;
|
||||
debug!("Skipping duplicate file entry - path: {:?}, inode: {}, size: {}",
|
||||
path, inode_val, metadata.len());
|
||||
return None; // No change - skip this file
|
||||
debug!("Hard link detected - existing: {:?}, new: {:?}, inode: {}",
|
||||
old_path, path, inode_val);
|
||||
// Fall through to "New file/directory" - both entries should exist
|
||||
} else {
|
||||
// Genuine move: Old path no longer exists, same inode at new path
|
||||
use tracing::info;
|
||||
info!("Genuine move detected - old: {:?}, new: {:?}, inode: {}",
|
||||
old_path, path, inode_val);
|
||||
return Some(Change::Moved {
|
||||
old_path,
|
||||
new_path: path.to_path_buf(),
|
||||
entry_id: db_entry.id,
|
||||
inode: inode_val,
|
||||
});
|
||||
}
|
||||
|
||||
// Different content, same inode - this is a genuine move
|
||||
use tracing::info;
|
||||
info!("Detected genuine move - old: {:?}, new: {:?}, inode: {}", old_path, path, inode_val);
|
||||
return Some(Change::Moved {
|
||||
old_path: old_path.clone(),
|
||||
new_path: path.to_path_buf(),
|
||||
entry_id: db_entry.id,
|
||||
inode: inode_val,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -240,57 +246,6 @@ impl ChangeDetector {
|
||||
false
|
||||
}
|
||||
|
||||
/// Check if two files have the same content based on key attributes
|
||||
/// This helps distinguish between genuine moves and duplicate content (hard links, deduplication)
|
||||
fn is_same_file_content(
|
||||
&self,
|
||||
db_entry: &DatabaseEntry,
|
||||
current_path: &Path,
|
||||
current_metadata: &std::fs::Metadata,
|
||||
current_inode: u64,
|
||||
) -> bool {
|
||||
// Must have same inode (already checked by caller, but being explicit)
|
||||
if db_entry.inode != Some(current_inode) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Must have same size
|
||||
if db_entry.size != current_metadata.len() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Extract filename and extension for comparison
|
||||
let db_filename = db_entry.path.file_name().and_then(|n| n.to_str());
|
||||
let current_filename = current_path.file_name().and_then(|n| n.to_str());
|
||||
|
||||
let db_extension = db_entry.path.extension().and_then(|e| e.to_str());
|
||||
let current_extension = current_path.extension().and_then(|e| e.to_str());
|
||||
|
||||
// If filenames and extensions are identical, this is likely duplicate content
|
||||
// (hard links, filesystem deduplication, or test data with identical files)
|
||||
if db_filename == current_filename && db_extension == current_extension {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Additional check: if modification times are identical, it's likely the same file
|
||||
if let (Some(db_modified), Ok(current_modified)) = (db_entry.modified, current_metadata.modified()) {
|
||||
let db_time = db_modified
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis() as i64;
|
||||
let current_time = current_modified
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis() as i64;
|
||||
|
||||
// If times are identical (within precision), treat as same content
|
||||
if (db_time - current_time).abs() <= self.timestamp_precision_ms {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Set timestamp precision for comparison (in milliseconds)
|
||||
pub fn set_timestamp_precision(&mut self, precision_ms: i64) {
|
||||
@ -302,6 +257,19 @@ impl ChangeDetector {
|
||||
self.path_to_entry.len()
|
||||
}
|
||||
|
||||
/// Check if a path exists with caching to reduce filesystem calls
|
||||
fn path_exists_cached(&mut self, path: &Path) -> bool {
|
||||
// Check cache first
|
||||
if let Some(&cached_result) = self.existence_cache.get(path) {
|
||||
return cached_result;
|
||||
}
|
||||
|
||||
// Not in cache, check filesystem and cache the result
|
||||
let exists = path.exists();
|
||||
self.existence_cache.insert(path.to_path_buf(), exists);
|
||||
exists
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@ -332,37 +300,10 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// Mock version of is_same_file_content for testing
|
||||
fn is_same_file_content_mock(
|
||||
db_entry: &DatabaseEntry,
|
||||
current_path: &Path,
|
||||
mock_metadata: &MockMetadata,
|
||||
current_inode: u64,
|
||||
) -> bool {
|
||||
// Must have same inode
|
||||
if db_entry.inode != Some(current_inode) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Must have same size
|
||||
if db_entry.size != mock_metadata.len() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Extract filename and extension for comparison
|
||||
let db_filename = db_entry.path.file_name().and_then(|n| n.to_str());
|
||||
let current_filename = current_path.file_name().and_then(|n| n.to_str());
|
||||
|
||||
let db_extension = db_entry.path.extension().and_then(|e| e.to_str());
|
||||
let current_extension = current_path.extension().and_then(|e| e.to_str());
|
||||
|
||||
// If filenames and extensions are identical, this is likely duplicate content
|
||||
db_filename == current_filename && db_extension == current_extension
|
||||
}
|
||||
|
||||
// Helper to test change detection with mock metadata
|
||||
fn test_check_path(
|
||||
detector: &ChangeDetector,
|
||||
detector: &mut ChangeDetector,
|
||||
path: &Path,
|
||||
size: u64,
|
||||
inode: Option<u64>,
|
||||
@ -384,23 +325,15 @@ mod tests {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Path not in database - check if it's a move
|
||||
// Path not in database - check if it's a move or hard link
|
||||
if let Some(inode_val) = inode {
|
||||
if let Some(old_path) = detector.inode_to_path.get(&inode_val) {
|
||||
if old_path != path {
|
||||
if let Some(db_entry) = detector.path_to_entry.get(old_path) {
|
||||
// Check if this is a false positive using our new logic
|
||||
if is_same_file_content_mock(db_entry, path, &mock_metadata, inode_val) {
|
||||
return None; // Skip duplicate content
|
||||
}
|
||||
|
||||
// Different content, same inode - genuine move
|
||||
return Some(Change::Moved {
|
||||
old_path: old_path.clone(),
|
||||
new_path: path.to_path_buf(),
|
||||
entry_id: db_entry.id,
|
||||
inode: inode_val,
|
||||
});
|
||||
// In mock tests, we can't easily check file existence
|
||||
// For testing purposes, assume it's a hard link (treat as new entry)
|
||||
// In real scenarios, the actual file existence check would determine behavior
|
||||
// Fall through to treat as new entry
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -411,7 +344,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_skip_duplicate_content() {
|
||||
fn test_hard_link_detection() {
|
||||
let mut detector = ChangeDetector::new();
|
||||
|
||||
// Add a test entry
|
||||
@ -428,16 +361,21 @@ mod tests {
|
||||
detector.path_to_entry.insert(db_path.clone(), db_entry);
|
||||
detector.inode_to_path.insert(12345, db_path);
|
||||
|
||||
// Test duplicate content detection (same filename, extension, inode, size)
|
||||
let duplicate_path = PathBuf::from("/test/dir2/file.txt");
|
||||
// Test hard link detection (same inode, different path, both should exist)
|
||||
let hard_link_path = PathBuf::from("/test/dir2/hardlink.txt");
|
||||
|
||||
// This should return None (skip) instead of Move
|
||||
let result = test_check_path(&detector, &duplicate_path, 1000, Some(12345));
|
||||
assert!(result.is_none(), "Expected duplicate content to be skipped");
|
||||
// Since we can't easily mock file existence in tests, we'll test the logic
|
||||
// In a real scenario, if both paths exist, it should be treated as a new entry
|
||||
let result = test_check_path(&mut detector, &hard_link_path, 1000, Some(12345));
|
||||
// In our mock test, this will be treated as new since we can't check file existence
|
||||
match result {
|
||||
Some(Change::New(path)) => assert_eq!(path, hard_link_path),
|
||||
_ => panic!("Expected hard link to be treated as new entry"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_genuine_move_detection() {
|
||||
fn test_consistent_behavior() {
|
||||
let mut detector = ChangeDetector::new();
|
||||
|
||||
// Add a test entry
|
||||
@ -454,30 +392,26 @@ mod tests {
|
||||
detector.path_to_entry.insert(db_path.clone(), db_entry);
|
||||
detector.inode_to_path.insert(12345, db_path.clone());
|
||||
|
||||
// Test genuine move detection (different filename, same inode)
|
||||
let moved_path = PathBuf::from("/test/dir2/renamed_file.txt");
|
||||
// Test consistent behavior: same inode at different path
|
||||
// In our mock test environment, this will be treated as a new entry
|
||||
// (since we can't mock file existence checks easily)
|
||||
let other_path = PathBuf::from("/test/dir2/other_file.txt");
|
||||
|
||||
// This should detect a genuine move
|
||||
let result = test_check_path(&detector, &moved_path, 1000, Some(12345));
|
||||
let result = test_check_path(&mut detector, &other_path, 1000, Some(12345));
|
||||
match result {
|
||||
Some(Change::Moved { old_path, new_path, entry_id, inode }) => {
|
||||
assert_eq!(old_path, db_path);
|
||||
assert_eq!(new_path, moved_path);
|
||||
assert_eq!(entry_id, 1);
|
||||
assert_eq!(inode, 12345);
|
||||
}
|
||||
_ => panic!("Expected genuine move detection"),
|
||||
Some(Change::New(path)) => assert_eq!(path, other_path),
|
||||
_ => panic!("Expected consistent behavior: treat as new entry"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_new_file_detection() {
|
||||
let detector = ChangeDetector::new();
|
||||
let mut detector = ChangeDetector::new();
|
||||
|
||||
// Test new file detection
|
||||
let new_path = PathBuf::from("/test/new_file.txt");
|
||||
|
||||
match test_check_path(&detector, &new_path, 500, None) {
|
||||
match test_check_path(&mut detector, &new_path, 500, None) {
|
||||
Some(Change::New(p)) => assert_eq!(p, new_path),
|
||||
_ => panic!("Expected new file detection"),
|
||||
}
|
||||
|
||||
@ -74,6 +74,7 @@ pub fn derive_job(input: TokenStream) -> TokenStream {
|
||||
volume_manager: Option<std::sync::Arc<crate::volume::VolumeManager>>,
|
||||
job_logging_config: Option<crate::config::JobLoggingConfig>,
|
||||
job_logs_dir: Option<std::path::PathBuf>,
|
||||
persistence_complete_tx: Option<tokio::sync::oneshot::Sender<()>>,
|
||||
) -> Box<dyn sd_task_system::Task<crate::infra::job::error::JobError>> {
|
||||
Box::new(crate::infra::job::executor::JobExecutor::new(
|
||||
*self,
|
||||
@ -89,6 +90,7 @@ pub fn derive_job(input: TokenStream) -> TokenStream {
|
||||
volume_manager,
|
||||
job_logging_config,
|
||||
job_logs_dir,
|
||||
persistence_complete_tx,
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user