-
-
Notifications
You must be signed in to change notification settings - Fork 161
Description
Introduction
When multiple SQLPage instances start simultaneously in the same directory, they encounter a race condition that causes a panic during initialization. This occurs because SQLPage's create_default_database() function creates a temporary file to test directory writability, then unconditionally removes it using .expect(). When multiple instances try to remove the same file concurrently, approximately 75% of them panic with "No such file or directory" error.
To Reproduce
- Create a test directory and navigate to it:
mkdir sqlpage_test && cd sqlpage_test- Start multiple SQLPage instances simultaneously in the same directory:
for i in {1..10}; do
sqlpage --port $((8080 + i)) &
done
wait- Observe that approximately 7-8 out of 10 instances crash during startup
Actual behavior
Approximately 75% of the instances panic during initialization with the following error:
thread 'main' panicked at sqlpage-0.41.0/src/app_config.rs:498:52:
removing temp file: Os {
code: 2,
kind: NotFound,
message: "No such file or directory"
}
Race condition sequence:
- Instance A creates temp file
sqlpage/sqlpage.dbto test writability - Instance B creates the same file (overwrites A's file)
- Instance A tries to remove the file (line 498)
- Instance B has already removed it
- Instance A panics with "No such file or directory"
Expected behavior
All instances should start successfully without panicking. The temp file creation is only for testing directory writability - if the file no longer exists during removal (because another process removed it), this should be handled gracefully rather than causing a panic.
Version information
- OS: macOS Darwin 24.6.0 (also affects Linux/Unix)
- Database: SQLite
- SQLPage Version: 0.41.0
Additional context
Root cause in source code:
In src/app_config.rs, lines 492-499, the create_default_database() function:
if let Ok(tmp_file) = std::fs::File::create(&default_db_path) {
log::info!(
"No DATABASE_URL provided, {} is writable, creating a new database file.",
default_db_path.display()
);
drop(tmp_file);
std::fs::remove_file(&default_db_path).expect("removing temp file"); // ⚠️ Panics here
return prefix + &encode_uri(&default_db_path) + "?mode=rwc";
}Proposed fix:
Replace the .expect() with graceful error handling:
if let Ok(tmp_file) = std::fs::File::create(&default_db_path) {
log::info!(
"No DATABASE_URL provided, {} is writable, creating a new database file.",
default_db_path.display()
);
drop(tmp_file);
// Gracefully handle removal - file might already be removed by another instance
if let Err(e) = std::fs::remove_file(&default_db_path) {
log::debug!("Temp file already removed or not found: {}", e);
}
return prefix + &encode_uri(&default_db_path) + "?mode=rwc";
}Rationale:
- The temp file's purpose is to test writability, which already succeeded
- Whether the file exists during removal is irrelevant to this test
- Other instances or processes may have legitimately removed it
- This is standard practice in concurrent environments
Impact:
- Severity: High - prevents reliable multi-instance deployments
- Affected use cases: Container orchestration (Kubernetes, Docker Swarm), proxy servers, load balancers, any concurrent startup scenario
- Discovery context: Found while building a proxy server spawning multiple surveilr instances (which uses SQLPage 0.41.0)
Workaround:
Set DATABASE_URL environment variable to bypass the create_default_database() code path:
export DATABASE_URL="sqlite://./sqlpage/sqlpage.db"
sqlpageI'm happy to submit a PR with this fix if that would be helpful.