Add Windows impl
This commit is contained in:
+34
-107
@@ -8,13 +8,23 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use proc_pidinfo::{
|
||||
ProcFDInfo, ProcFDType, VnodeFdInfoWithPath, proc_pidfdinfo_self, proc_pidinfo_list_self,
|
||||
};
|
||||
#[cfg(windows)]
|
||||
#[path = "windows.rs"]
|
||||
mod imp;
|
||||
|
||||
const SQL_CLOSE_MAX_ATTEMPTS: u8 = 10;
|
||||
const SQL_CLOSE_RETRY_DELAY: Duration = Duration::from_millis(100);
|
||||
#[cfg(target_os = "macos")]
|
||||
#[path = "macos.rs"]
|
||||
mod imp;
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
#[path = "linux.rs"]
|
||||
mod imp;
|
||||
|
||||
/// Max number of retry attempts
|
||||
const CHECK_FILES_CLOSED_MAX_ATTEMPTS: u8 = 10;
|
||||
|
||||
/// Delay between file checks
|
||||
const CHECK_FILES_CLOSED_RETRY_DELAY: Duration = Duration::from_millis(100);
|
||||
|
||||
pub struct SqlitePoolGuard {
|
||||
database_path: PathBuf,
|
||||
@@ -42,7 +52,7 @@ impl SqlitePoolGuard {
|
||||
}
|
||||
}
|
||||
|
||||
/// Close udnerlying sqlite pool
|
||||
/// Close udnerlying sqlite pool and wait for files to be closed before returning.
|
||||
pub async fn close_pool(&self) {
|
||||
_ = self.close_pool_inner();
|
||||
}
|
||||
@@ -50,7 +60,7 @@ impl SqlitePoolGuard {
|
||||
async fn close_pool_inner(&self) -> std::io::Result<()> {
|
||||
self.connection_pool.close().await;
|
||||
|
||||
if let Err(e) = self.wait_io_close().await {
|
||||
if let Err(e) = self.wait_for_db_files_close().await {
|
||||
log::error!("Failed to wait for file to close: {e}");
|
||||
}
|
||||
|
||||
@@ -82,25 +92,17 @@ impl SqlitePoolGuard {
|
||||
database_files
|
||||
}
|
||||
|
||||
/// Wait for I/O close to the database files
|
||||
///
|
||||
/// - macOS: uses `proc_pidinfo` (`sys/proc_info.h`)
|
||||
/// See: http://blog.palominolabs.com/2012/06/19/getting-the-files-being-used-by-a-process-on-mac-os-x/
|
||||
///
|
||||
/// - Linux, Android: uses `/proc/self/fd/` to list open file descriptors
|
||||
/// See: https://stackoverflow.com/a/59797198/351305
|
||||
///
|
||||
/// - Windows: attempts to open files to detect whether they are still open.
|
||||
async fn wait_io_close(&self) -> std::io::Result<()> {
|
||||
/// Wait for database files to be closed before returning.
|
||||
async fn wait_for_db_files_close(&self) -> std::io::Result<()> {
|
||||
let database_files = self.all_database_files();
|
||||
let paths: Vec<&Path> = database_files.iter().map(PathBuf::as_path).collect();
|
||||
|
||||
for _ in 0..SQL_CLOSE_MAX_ATTEMPTS {
|
||||
match Self::check_io_close(&paths)
|
||||
for _ in 0..CHECK_FILES_CLOSED_MAX_ATTEMPTS {
|
||||
match imp::check_files_closed(&paths)
|
||||
.await
|
||||
.inspect_err(|e| log::error!("check_io_close() failure: {e}"))
|
||||
.inspect_err(|e| log::error!("imp::check_files_closed() failure: {e}"))
|
||||
{
|
||||
Ok(false) | Err(_) => tokio::time::sleep(SQL_CLOSE_RETRY_DELAY).await,
|
||||
Ok(false) | Err(_) => tokio::time::sleep(CHECK_FILES_CLOSED_RETRY_DELAY).await,
|
||||
Ok(true) => return Ok(()),
|
||||
}
|
||||
}
|
||||
@@ -110,95 +112,12 @@ impl SqlitePoolGuard {
|
||||
"timed out waiting for sqlite files to be closed",
|
||||
))
|
||||
}
|
||||
|
||||
/// Check if no more open file descriptors exist for the given files.
|
||||
#[cfg(target_os = "macos")]
|
||||
async fn check_io_close(file_paths: &[&Path]) -> io::Result<bool> {
|
||||
let fd_list = proc_pidinfo_list_self::<ProcFDInfo>()?;
|
||||
|
||||
for fd in fd_list
|
||||
.iter()
|
||||
.filter(|s| s.fd_type() == Ok(ProcFDType::VNODE))
|
||||
{
|
||||
let Some(vnode) = proc_pidfdinfo_self::<VnodeFdInfoWithPath>(fd.proc_fd)
|
||||
.inspect_err(|e| {
|
||||
log::warn!("proc_pidfdinfo_self::<VnodeFdInfoWithPath>() failure: {e}");
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if let Ok(true) = vnode
|
||||
.path()
|
||||
.map(|vnode_path| file_paths.contains(&vnode_path))
|
||||
.inspect_err(|e| {
|
||||
log::warn!("vnode.path() failure: {e:?}");
|
||||
})
|
||||
{
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// Check if no more open file descriptors exist for the given files.
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
async fn check_io_close(file_paths: &[&Path]) -> io::Result<bool> {
|
||||
let mut dir = tokio::fs::read_dir("/proc/self/fd/").await?;
|
||||
|
||||
while let Ok(Some(entry)) = dir.next_entry().await {
|
||||
if entry
|
||||
.file_type()
|
||||
.await
|
||||
.inspect_err(|e| log::warn!("entry.file_type() failure: {e}"))
|
||||
.is_ok_and(|entry_type| entry_type.is_symlink())
|
||||
{
|
||||
match tokio::fs::read_link(entry.path()).await {
|
||||
Ok(resolved_path) => {
|
||||
if file_paths.contains(&resolved_path.as_ref()) {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to read symlink: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
async fn check_io_close(file_paths: &[&Path]) -> io::Result<bool> {
|
||||
// Error code returned when file is still in use.
|
||||
const FILE_IN_USE_ERR: i32 = 32;
|
||||
|
||||
for file_path in file_paths {
|
||||
if let Err(e) = tokio::fs::OpenOptions::new()
|
||||
.read(true)
|
||||
.open(file_path)
|
||||
.await
|
||||
{
|
||||
if e.raw_os_error() == Some(FILE_IN_USE_ERR) {
|
||||
return Ok(false);
|
||||
} else if e.kind() != io::ErrorKind::NotFound {
|
||||
log::error!("Failed to open file: {}", file_path.display());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use sqlx::{
|
||||
ConnectOptions,
|
||||
ConnectOptions, Executor,
|
||||
sqlite::{SqliteAutoVacuum, SqliteSynchronous},
|
||||
};
|
||||
|
||||
@@ -208,6 +127,9 @@ mod tests {
|
||||
async fn test_wait_close() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let database_path = temp_dir.path().join("storage.sqlite");
|
||||
|
||||
println!("Database path: {}", database_path.display());
|
||||
|
||||
let opts = sqlx::sqlite::SqliteConnectOptions::new()
|
||||
.journal_mode(sqlx::sqlite::SqliteJournalMode::Wal)
|
||||
.synchronous(SqliteSynchronous::Normal)
|
||||
@@ -217,10 +139,15 @@ mod tests {
|
||||
.disable_statement_logging();
|
||||
let connection_pool = sqlx::SqlitePool::connect_with(opts).await.unwrap();
|
||||
|
||||
connection_pool
|
||||
.execute("create table test (col int)")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let guard = SqlitePoolGuard::new(database_path, connection_pool);
|
||||
assert!(
|
||||
guard
|
||||
.wait_io_close()
|
||||
.wait_for_db_files_close()
|
||||
.await
|
||||
.err()
|
||||
.is_some_and(|e| e.kind() == io::ErrorKind::TimedOut)
|
||||
|
||||
Reference in New Issue
Block a user