use std::path::{Path, PathBuf};
use std::sync::{Mutex, PoisonError};
use std::time::{Duration, Instant};
use ahash::AHashMap;
use serde::{Deserialize, Serialize};
use crate::config::{self, Config};
use crate::scanner::{self, EmbedMode, ScanSource, ScanStats};
use crate::store::{self, LockHolder, Store, VIEW_WORKING};
pub(crate) const DEFAULT_HOT_CAP: usize = 16;
#[derive(Debug, thiserror::Error)]
pub(crate) enum WorkspacePoolError {
#[error("open workspace store: {0}")]
Store(#[from] store::StoreError),
#[error("scan workspace: {0}")]
Scan(#[from] scanner::ScanError),
#[error("load workspace config: {0}")]
Config(#[from] config::ConfigError),
}
struct WorkspaceEntry {
store: Mutex<Store>,
config: Config,
root: PathBuf,
key: String,
last_used: Mutex<Instant>,
}
impl WorkspaceEntry {
fn last_used(&self) -> Instant {
*self.last_used.lock().unwrap_or_else(PoisonError::into_inner)
}
fn touch(&self) {
*self.last_used.lock().unwrap_or_else(PoisonError::into_inner) = Instant::now();
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct AccessedWorkspace {
pub root: PathBuf,
pub key: String,
pub idle_secs: u64,
}
pub(crate) struct WorkspacePool {
map: Mutex<AHashMap<String, std::sync::Arc<WorkspaceEntry>>>,
open_lock: Mutex<()>,
cap: usize,
}
impl WorkspacePool {
pub(crate) fn new(cap: usize) -> Self {
Self {
map: Mutex::new(AHashMap::new()),
open_lock: Mutex::new(()),
cap: cap.max(1),
}
}
fn lock_map(&self) -> std::sync::MutexGuard<'_, AHashMap<String, std::sync::Arc<WorkspaceEntry>>> {
self.map.lock().unwrap_or_else(PoisonError::into_inner)
}
pub(crate) fn rescan(
&self,
root: &Path,
paths: Option<Vec<PathBuf>>,
full: bool,
embed: bool,
) -> Result<ScanStats, WorkspacePoolError> {
let entry = self.get_or_open(root)?;
entry.touch();
let mode = if embed { EmbedMode::Inline } else { EmbedMode::Deferred };
let mut store = entry.store.lock().unwrap_or_else(PoisonError::into_inner);
let report = match paths {
Some(ref paths) if !full && !paths.is_empty() => {
scanner::scan_paths(&entry.root, &mut store, &entry.config, paths, mode)?
}
_ => scanner::scan(&entry.root, &mut store, &entry.config, ScanSource::WorkingTree, mode)?,
};
Ok(report.stats)
}
pub(crate) fn with_workspace<R>(&self, root: &Path, f: impl FnOnce(&Store) -> R) -> Result<R, WorkspacePoolError> {
let entry = self.get_or_open(root)?;
entry.touch();
let store = entry.store.lock().unwrap_or_else(PoisonError::into_inner);
Ok(f(&store))
}
#[cfg(feature = "memory")]
pub(crate) fn with_workspace_mut<R>(
&self,
root: &Path,
f: impl FnOnce(&mut Store) -> R,
) -> Result<R, WorkspacePoolError> {
let entry = self.get_or_open(root)?;
entry.touch();
let mut store = entry.store.lock().unwrap_or_else(PoisonError::into_inner);
Ok(f(&mut store))
}
fn get_or_open(&self, root: &Path) -> Result<std::sync::Arc<WorkspaceEntry>, WorkspacePoolError> {
let key = store::workspace_key(root);
{
let map = self.lock_map();
if let Some(entry) = map.get(&key) {
return Ok(entry.clone());
}
}
let _opening = self.open_lock.lock().unwrap_or_else(PoisonError::into_inner);
{
let map = self.lock_map();
if let Some(entry) = map.get(&key) {
return Ok(entry.clone());
}
}
let store = Store::open_with_holder(root, VIEW_WORKING, LockHolder::Rescan)?;
let config = load_config(root)?;
let entry = std::sync::Arc::new(WorkspaceEntry {
store: Mutex::new(store),
config,
root: root.to_path_buf(),
key: key.clone(),
last_used: Mutex::new(Instant::now()),
});
let mut map = self.lock_map();
while map.len() >= self.cap {
let victim = map.values().min_by_key(|e| e.last_used()).map(|e| e.key.clone());
match victim {
Some(victim) => {
map.remove(&victim);
}
None => break,
}
}
map.insert(key, entry.clone());
Ok(entry)
}
pub(crate) fn accessed(&self) -> Vec<AccessedWorkspace> {
let map = self.lock_map();
let mut rows: Vec<AccessedWorkspace> = map
.values()
.map(|e| AccessedWorkspace {
root: e.root.clone(),
key: e.key.clone(),
idle_secs: e.last_used().elapsed().as_secs(),
})
.collect();
rows.sort_by_key(|r| r.idle_secs);
rows
}
pub(crate) fn evict_idle(&self, idle: Duration) -> usize {
let mut map = self.lock_map();
let stale: Vec<String> = map
.values()
.filter(|e| e.last_used().elapsed() >= idle)
.map(|e| e.key.clone())
.collect();
for key in &stale {
map.remove(key);
}
stale.len()
}
#[cfg(test)]
pub(crate) fn len(&self) -> usize {
self.lock_map().len()
}
}
fn load_config(root: &Path) -> Result<Config, WorkspacePoolError> {
match config::load_with_overrides(root, None, None) {
Ok(loaded) => Ok(loaded.config),
Err(config::ConfigError::NotFound(_)) => Ok(config::default_for_root(root)),
Err(error) => Err(error.into()),
}
}
#[cfg(test)]
#[path = "workspace_pool_tests.rs"]
mod tests;