Skip to content

Commit 6b33822

Browse files
feat: Add retry mechanism on locked/busy mbtiles files (#2572)
Fix #1591 Hello ! This is my first-ever PR to an open-source project. Currently, martin crashes when trying to fetch the metadata of a busy mbtile file. It was proposed in the open issue: #1591 to be more lenient and perform retries when the file was locked. ### Technical note: The `SQLITE_BUSY` error is hardcoded as the sqlite wrapper used in martin (sqlx) doesn't offer an error enum match for specific sqlite error codes (unlike rusqlite). ### Retry strategy: The retrial strategy is an exponential backoff with the following delays: 50ms, 0.1s, 0.2s, 0.4s, 0.8s, 1.6s, 3.2s, and finally 6.4s. ### I would happily take feedback on: - Is limiting the retry behavior strictly to SQLITE_BUSY errors the intended approach ? - Are the exponential backoff delays acceptable for this use case ? - Is the warning message too verbose for the logs ? - Is there anything else that I might have missed ? :) Thanks ! <img width="1621" height="923" alt="Screenshot from 2026-02-24 14-09-21" src="https://github.com/user-attachments/assets/dd0099ee-c14d-40e3-9be8-64b765bbd316" /> --------- Co-authored-by: Frank Elsinga <frank@elsinga.de>
1 parent 7cb748b commit 6b33822

File tree

4 files changed

+71
-13
lines changed

4 files changed

+71
-13
lines changed

Cargo.lock

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

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ actix-web-static-files = "4"
4545
anyhow = "1.0"
4646
approx = "0.5.1"
4747
async-trait = "0.1"
48+
backon = "1.6.0"
4849
base64 = "0.22.1"
4950
bit-set = "0.8"
5051
brotli = ">=5, <9"

martin-core/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ sprites = [
5757
"dep:dashmap",
5858
]
5959
styles = ["tokio/fs", "dep:dashmap"]
60-
mbtiles = ["dep:mbtiles", "_tiles"]
60+
mbtiles = ["dep:backon", "dep:mbtiles", "dep:tokio", "_tiles"]
6161
pmtiles = ["dep:pmtiles", "dep:object_store", "_tiles"]
6262
_tiles = ["dep:base64"]
6363
test-pg = ["postgres"]
@@ -69,6 +69,7 @@ maplibre_native = { workspace = true, optional = true }
6969
[dependencies]
7070
actix-web = { workspace = true, optional = true }
7171
async-trait.workspace = true
72+
backon = { workspace = true, optional = true }
7273
base64 = { workspace = true, optional = true }
7374
bit-set = { workspace = true, optional = true }
7475
dashmap = { workspace = true, optional = true }

martin-core/src/tiles/mbtiles/source.rs

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@ use std::fmt::{Debug, Formatter};
44
use std::io;
55
use std::path::PathBuf;
66
use std::sync::Arc;
7+
use std::time::Duration;
78

89
use async_trait::async_trait;
10+
use backon::{FibonacciBuilder, Retryable as _};
911
use martin_tile_utils::{TileCoord, TileData, TileInfo};
12+
use mbtiles::sqlx::error::DatabaseError;
1013
use mbtiles::{MbtError, MbtilesPool};
1114
use tilejson::TileJSON;
12-
use tracing::trace;
15+
use tracing::{trace, warn};
1316

1417
use crate::tiles::mbtiles::MbtilesError;
1518
use crate::tiles::{BoxedSource, MartinCoreResult, Source, UrlQuery};
@@ -33,6 +36,19 @@ impl Debug for MbtSource {
3336
}
3437
}
3538

39+
// SQLITE_BUSY (code: 5)
40+
// https://sqlite.org/rescode.html#busy
41+
fn is_sqlite_busy(err: &MbtError) -> bool {
42+
matches!(
43+
err,
44+
MbtError::SqlxError(se)
45+
if se
46+
.as_database_error()
47+
.and_then(DatabaseError::code)
48+
.is_some_and(|code| code == "5")
49+
)
50+
}
51+
3652
impl MbtSource {
3753
/// Creates a new `MBTiles` source from the given file path.
3854
pub async fn new(id: String, path: PathBuf) -> Result<Self, MbtilesError> {
@@ -41,12 +57,28 @@ impl MbtSource {
4157
.map_err(|e| io::Error::other(format!("{e:?}: Cannot open file {}", path.display())))
4258
.map_err(|e| MbtilesError::IoError(e, path.clone()))?;
4359

44-
let meta = mbt
45-
.get_metadata()
60+
// Attempt to fetch metadata with backoff
61+
let start_delay = Duration::from_millis(50);
62+
let max_attempts = 10; // from 50ms to 2.75s
63+
let meta = (|| async { mbt.get_metadata().await })
64+
.retry(
65+
FibonacciBuilder::default()
66+
.with_min_delay(start_delay)
67+
.with_max_times(max_attempts)
68+
.with_jitter(),
69+
)
70+
.sleep(tokio::time::sleep)
71+
.when(is_sqlite_busy)
72+
.notify(|_err, dur| {
73+
warn!(
74+
"Database file {:?} locked (SQLITE_BUSY). Retrying in {:.2}s...",
75+
path.display(),
76+
dur.as_secs_f64()
77+
);
78+
})
4679
.await
4780
.map_err(|e| MbtilesError::InvalidMetadata(e.to_string(), path.clone()))?;
4881

49-
// Empty mbtiles should cause an error
5082
let tile_info = mbt
5183
.detect_format(&meta.tilejson)
5284
.await

0 commit comments

Comments
 (0)