Skip to content

Commit 1169364

Browse files
committed
Add bottomless restore interruption integration tests
Add integration tests for libsql-server bottomless replication restore behavior when interrupted by various failure modes. Tests verify sqld can resume and complete an interrupted restore from S3-compatible object storage (minio) without requiring a restart. Test cases: - basic_restore: Sanity check that sqld restores from minio - sqld_interrupted: sqld killed mid-restore, restarted, completes - minio_interrupted: minio stopped mid-restore, restarted, sqld retries - network_partition: sqld disconnected from network mid-restore, reconnected Infrastructure: - Docker-based fixtures with isolated networks per test - Unique container/network names and ports via atomic counters - Port mapping (not host networking) for isolation - Automatic cleanup of Docker resources after each test Files added: - tests/bottomless/mod.rs - tests/bottomless/fixtures.rs - tests/bottomless/basic_restore.rs - tests/bottomless/sqld_interrupted.rs - tests/bottomless/minio_interrupted.rs - tests/bottomless/network_partition.rs - tests/bottomless/README.md Files modified: - tests/tests.rs: Add bottomless module - Cargo.toml: Add reqwest dev-dependency, remove duplicate hex
1 parent e4beaca commit 1169364

10 files changed

Lines changed: 1023 additions & 0 deletions

File tree

Cargo.lock

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

libsql-server/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ indicatif = "0.17.8"
9999
[dev-dependencies]
100100
arbitrary = { version = "1.3.0", features = ["derive_arbitrary"] }
101101
env_logger = "0.10"
102+
hex = "0.4"
102103
hyper = { workspace = true, features = ["client"] }
103104
insta = { version = "1.26.0", features = ["json"] }
104105
libsql = { path = "../libsql/"}
@@ -114,6 +115,7 @@ s3s-fs = "0.8.1"
114115
ring = { version = "0.17.8", features = ["std"] }
115116
tonic-build = "0.11"
116117
prost-build = "0.12"
118+
reqwest = { version = "0.11", features = ["json"] }
117119

118120
[build-dependencies]
119121
vergen = { version = "8", features = ["build", "git", "gitcl"] }
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Bottomless Restore Interruption Tests
2+
3+
Integration tests for libsql-server's (sqld) bottomless S3 backup/restore feature. These tests verify that sqld correctly handles interrupted restores from object storage (minio).
4+
5+
## Test Cases
6+
7+
### 1. `basic_restore`
8+
Sanity check - verifies sqld can restore a database from minio after local files are deleted.
9+
10+
### 2. `sqld_interrupted`
11+
Simulates sqld crashing mid-restore (SIGKILL). After restart, sqld must detect the incomplete restore and complete it successfully.
12+
13+
### 3. `minio_interrupted`
14+
Minio (S3) is stopped mid-restore, then restarted. sqld must retry and complete the restore once S3 is available again.
15+
16+
### 4. `network_partition`
17+
The sqld container is disconnected from the Docker network mid-restore (simulating a network partition), then reconnected. sqld must resume and complete the restore without requiring a restart.
18+
19+
## Running Tests
20+
21+
```bash
22+
# Run all bottomless tests
23+
cargo test --test tests bottomless -- --nocapture
24+
25+
# Run individual tests
26+
cargo test --test tests bottomless::basic_restore -- --nocapture
27+
cargo test --test tests bottomless::sqld_interrupted -- --nocapture
28+
cargo test --test tests bottomless::minio_interrupted -- --nocapture
29+
cargo test --test tests bottomless::network_partition -- --nocapture
30+
```
31+
32+
## Architecture
33+
34+
- **MinIO**: Runs in a Docker container with a dedicated Docker network per test
35+
- **sqld**: Runs in a Docker container on the same network, with port mapping for HTTP access
36+
- Each test gets unique container/network names and ports to avoid conflicts when running in parallel
37+
- Data directory is a temp directory mounted into the sqld container
38+
39+
## Prerequisites
40+
41+
- Docker daemon running
42+
- Port range 20000-30000 available on localhost
43+
44+
## Notes
45+
46+
- Tests never skip or ignore. If the Docker environment is unavailable, they fail loudly.
47+
- All temporary containers and networks are cleaned up after each test.
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
use super::fixtures::{MinioFixture, SqldFixture, TestDatabase};
2+
use std::time::Duration;
3+
4+
#[tokio::test]
5+
async fn test_basic_restore() {
6+
let _ = tracing_subscriber::fmt::try_init();
7+
8+
let minio = MinioFixture::start()
9+
.await
10+
.expect("Failed to start minio");
11+
12+
let data_dir = tempfile::tempdir().expect("Failed to create temp dir");
13+
let mut sqld = SqldFixture::new(&minio);
14+
15+
// Phase 1: Create database and replicate to minio
16+
sqld.start(data_dir.path())
17+
.await
18+
.expect("Failed to start sqld");
19+
sqld.wait_for_ready(Duration::from_secs(30))
20+
.await
21+
.expect("sqld did not become ready");
22+
23+
let endpoint = sqld.http_endpoint();
24+
let db = TestDatabase::new(endpoint.clone());
25+
db.create_schema().await.expect("Failed to create schema");
26+
db.insert_test_data(100)
27+
.await
28+
.expect("Failed to insert data");
29+
db.wait_for_replication()
30+
.await
31+
.expect("Failed to wait for replication");
32+
33+
sqld.stop().await.expect("Failed to stop sqld");
34+
35+
// Phase 2: Delete local database files to force restore
36+
sqld.cleanup_data_dir(data_dir.path())
37+
.await
38+
.expect("Failed to cleanup dbs dir");
39+
40+
// Phase 3: Start sqld - should restore from minio
41+
let endpoint2 = sqld.http_endpoint();
42+
sqld.start(data_dir.path())
43+
.await
44+
.expect("Failed to restart sqld");
45+
sqld.wait_for_ready(Duration::from_secs(60))
46+
.await
47+
.expect("sqld did not become ready after restore");
48+
49+
// Phase 4: Verify database is intact
50+
let db2 = TestDatabase::new(endpoint2);
51+
52+
tokio::time::sleep(Duration::from_secs(2)).await;
53+
54+
let restored_data = db2.query_all().await.expect("Failed to query data");
55+
assert_eq!(restored_data.len(), 100, "Expected 100 rows after restore");
56+
57+
db2.verify_integrity().await.expect("Data integrity check failed");
58+
59+
sqld.cleanup().await.ok();
60+
minio.cleanup().await.ok();
61+
}

0 commit comments

Comments
 (0)