Skip to content

Commit d334a4d

Browse files
chore: retry duckdb init when file is locked, improve errors
Signed-off-by: Henry <mail@henrygressmann.de>
1 parent 4d418fd commit d334a4d

File tree

11 files changed

+75
-126
lines changed

11 files changed

+75
-126
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Since this is not a library, this changelog focuses on the changes that are rele
2020

2121
- Updated to the latest version of DuckDB (1.5.1)
2222
- Fixed error when both `listen` and `port` configuration options are set
23+
- Added retry logic when loading the DuckDB database to handle potential locking issues on startup (e.g. when the database is being updated by another process or when using a shared network drive)
2324

2425
## [v1.4.0] - 2026-03-14
2526

Cargo.lock

Lines changed: 4 additions & 83 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,13 +76,13 @@ aide={version="0.16.0-alpha.3", default-features=false, features=[
7676
"axum-extra-headers",
7777
]}
7878
schemars={version="1.2", features=["derive", "chrono04"]}
79-
79+
url="2.5"
8080
ua-parser="0.2"
8181
rust-embed={version="8.11", features=["mime-guess"]}
8282
reqwest={version="0.13", default-features=false, features=["json", "stream", "charset", "rustls"]}
8383

8484
# database
85-
duckdb={version="1.10501", features=["buildtime_bindgen", "chrono", "bundled", "r2d2"]}
85+
duckdb={version="1.10501", features=["chrono", "bundled", "r2d2"]}
8686
rusqlite={version="0.39", features=["bundled", "modern_sqlite", "chrono"]}
8787
r2d2={version="0.8", default-features=false}
8888
refinery={version="0.9", default-features=false}

data/config.example.toml

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
1-
# The base URL of the Liwan instance
2-
base_url="http://localhost:9042"
3-
4-
# The port to listen on (http)
5-
port=9042
1+
base_url="http://localhost:9042" # The base URL of the Liwan instance
2+
listen=9042 # The port to listen on (http)
63

74
# # Folder to store the database in (Will be created if it doesn't exist)
85
# # defaults to $HOME/.local/share/liwan/data on linux/macos

data/licenses-cargo.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

data/licenses-npm.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

src/app/db.rs

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use crate::utils::refinery_duckdb::DuckDBConnection;
33
use crate::utils::refinery_sqlite::RqlConnection;
44

55
use crate::utils::r2d2_sqlite::SqliteConnectionManager;
6-
use anyhow::{Result, bail};
6+
use anyhow::{Context, Result, bail};
77
use duckdb::DuckdbConnectionManager;
88
use refinery::Runner;
99
use std::path::PathBuf;
@@ -13,24 +13,40 @@ pub(super) fn init_duckdb(
1313
duckdb_config: DuckdbConfig,
1414
mut migrations_runner: Runner,
1515
) -> Result<r2d2::Pool<DuckdbConnectionManager>> {
16-
let mut flags = duckdb::Config::default()
17-
.enable_autoload_extension(true)?
18-
.access_mode(duckdb::AccessMode::ReadWrite)?
19-
.with("enable_fsst_vectors", "true")?
20-
.with("allocator_background_threads", "true")?;
21-
22-
if let Some(memory_limit) = duckdb_config.memory_limit {
23-
flags = flags.max_memory(&memory_limit)?;
24-
}
16+
let mut tries = 10;
17+
let conn = loop {
18+
let mut flags = duckdb::Config::default()
19+
.enable_autoload_extension(true)?
20+
.access_mode(duckdb::AccessMode::ReadWrite)?
21+
.with("enable_fsst_vectors", "true")?
22+
.with("allocator_background_threads", "true")?;
23+
24+
if let Some(memory_limit) = &duckdb_config.memory_limit {
25+
flags = flags.max_memory(memory_limit)?;
26+
}
2527

26-
if let Some(threads) = duckdb_config.threads {
27-
flags = flags.threads(threads.get().into())?;
28-
}
28+
if let Some(threads) = duckdb_config.threads {
29+
flags = flags.threads(threads.get().into())?;
30+
}
2931

30-
let conn = DuckdbConnectionManager::file_with_flags(path, flags).map_err(|e| {
31-
tracing::warn!("Failed to create DuckDB connection. If you've just upgraded to Liwan 1.2, please downgrade to version 1.1.1 first, start and stop the server, and then upgrade to 1.2 again.");
32-
anyhow::anyhow!("Failed to create DuckDB connection: {}", e)
33-
})?;
32+
match DuckdbConnectionManager::file_with_flags(path, flags) {
33+
Ok(conn) => break conn,
34+
Err(e) => {
35+
if tries <= 0 {
36+
tracing::warn!("");
37+
return Err(e).context("Failed to load DuckDB Database after 10 attempts");
38+
}
39+
40+
if e.to_string().contains("Could not set lock on file") {
41+
tracing::warn!("DuckDB database is locked. Retrying... ({} tries left)", tries);
42+
tries -= 1;
43+
std::thread::sleep(std::time::Duration::from_secs(1));
44+
} else {
45+
return Err(e).context("Failed to load DuckDB Database");
46+
}
47+
}
48+
}
49+
};
3450

3551
let pool = r2d2::Pool::new(conn)?;
3652
{

src/config.rs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
use anyhow::{Context, Result, bail};
22
use figment::Figment;
33
use figment::providers::{Env, Format, Toml};
4-
use http::Uri;
54
use serde::{Deserialize, Serialize};
65
use std::net::SocketAddr;
76
use std::num::NonZeroU16;
87
use std::str::FromStr;
8+
use url::Url;
99

1010
fn default_base() -> String {
1111
"http://localhost:9042".to_string()
@@ -162,12 +162,22 @@ impl Config {
162162
}))
163163
.extract()?;
164164

165-
let url: Uri = Uri::from_str(&config.base_url).context("Invalid base URL")?;
165+
let url: Url = Url::from_str(&config.base_url).context("Invalid base URL")?;
166166

167-
if ![Some("http"), Some("https")].contains(&url.scheme_str()) {
167+
if !["http", "https"].contains(&url.scheme()) {
168168
bail!("Invalid base URL: protocol must be either http or https");
169169
}
170170

171+
if url.scheme() != "https" {
172+
tracing::warn!("Base URL is not using HTTPS");
173+
}
174+
175+
if config.listen.is_some() && config.port.is_some() {
176+
tracing::warn!(
177+
"Both `listen` and `port` configuration options are set. The `listen` option will take precedence over `port`."
178+
);
179+
}
180+
171181
Ok(config)
172182
}
173183

src/web/mod.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ pub mod routes;
22
pub mod session;
33
pub mod webext;
44

5-
use std::net::SocketAddr;
5+
use std::net::{SocketAddr, ToSocketAddrs};
66
use std::ops::Deref;
77
use std::sync::{Arc, mpsc::Sender};
88

@@ -140,9 +140,13 @@ pub async fn start_webserver(app: Arc<Liwan>, events: Sender<Event>) -> Result<(
140140
#[cfg(debug_assertions)]
141141
save_spec(router.1)?;
142142

143-
let listener = tokio::net::TcpListener::bind(app.config.listen_addr())
143+
let socket_addrs: Vec<_> =
144+
app.config.listen_addr().to_socket_addrs().context("Failed to resolve listen address")?.collect();
145+
146+
let listener = tokio::net::TcpListener::bind(socket_addrs.as_slice())
144147
.await
145148
.with_context(|| format!("Failed to bind to address {}", app.config.listen_addr()))?;
149+
146150
let service = router.0.into_make_service_with_connect_info::<SocketAddr>();
147151
axum::serve(listener, service).await.context("server exited unexpectedly")
148152
}

0 commit comments

Comments
 (0)