Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ name: Code Coverage
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:
coverage:
Expand Down Expand Up @@ -42,5 +40,5 @@ jobs:
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: lcov.info
fail_ci_if_error: true
fail_ci_if_error: false
verbose: true
6 changes: 4 additions & 2 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@ name: Rust
on:
push:
branches: [ main ]
paths-ignore:
- '**.md'
pull_request:
branches: [ main ]
paths-ignore:
- '**.md'

jobs:
build:
Expand Down Expand Up @@ -40,7 +44,5 @@ jobs:
run: cargo test --verbose
- name: Run test with custom_separator
run: cargo test --features custom_separator -- custom_separator_test
- name: Run test with bitcode
run: cargo test --features bitcode --no-default-features
- name: Run integration test with in-memory database
run: cargo test --features memory --no-default-features
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.1.0] - 2024-12-15
## [0.1.0] - 2024-12-17

### Added

Expand All @@ -24,7 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- **Storage Backend** (`nitrite-fjall-adapter`)
- Fjall LSM-tree based persistent storage
- Configurable with bincode or bitcode serialization
- Bincode serialization for efficient binary storage
- High-performance disk-backed storage

- **Full-Text Search** (`nitrite-tantivy-fts`)
Expand Down
37 changes: 0 additions & 37 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 1 addition & 6 deletions nitrite-fjall-adapter/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ categories = ["database"]
[dependencies]
nitrite = { version = "0.1.0", path = "../nitrite" }
fjall = { version = "2.6.3", features = ["bytes"] }
bincode = { version = "2.0.1", features = ["serde"], optional = true }
bitcode = { version = "0.6.5", features = ["serde"], optional = true }
bincode = { version = "2.0.1", features = ["serde"] }
log = "0.4.14"
cargo_toml = "0.22.1"
dashmap = "6.1.0"
Expand All @@ -27,7 +26,3 @@ uuid = { version = "1.15.1", features = ["v4"] }
ctor = "0.4.0"
colog = "1.3.0"

[features]
default = ["bincode"]
bincode = ["dep:bincode"]
bitcode = ["dep:bitcode"]
121 changes: 100 additions & 21 deletions nitrite-fjall-adapter/src/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,76 @@ impl FjallStoreInner {
}
}

/// Delay in milliseconds to wait for file system cleanup when recreating a deleted partition.
/// This allows Fjall's internal cleanup processes to complete before attempting to create
/// a new partition with the same name.
const PARTITION_CLEANUP_DELAY_MS: u64 = 50;

/// Helper function to check if an error indicates a partition was deleted
#[inline]
fn is_partition_deleted_error(err_msg: &str) -> bool {
err_msg.contains("not found") || err_msg.contains("deleted") || err_msg.contains("PartitionDeleted")
}

/// Opens a partition with retry logic for deleted partitions.
///
/// This handles the case where a partition was deleted (e.g., during index rebuild)
/// and needs to be recreated. When Fjall reports a partition is deleted, we simply
/// try to open it again, which will create a new partition.
///
/// Note: This method uses `std::thread::sleep` which blocks the current thread.
/// This is intentional as we need to wait for file system synchronization after
/// partition deletion. The sleep is brief (50ms) and only occurs on retry paths.
fn open_partition_with_retry(
&self,
ks: &Keyspace,
name: &str,
config: &fjall::PartitionCreateOptions,
) -> NitriteResult<fjall::PartitionHandle> {
// Clone config once to avoid multiple clones in retry paths
let config_clone = config.clone();

match ks.open_partition(name, config_clone.clone()) {
Ok(partition) => Ok(partition),
Err(err) => {
let err_msg = err.to_string();

// If partition was deleted, we need to recreate it
if Self::is_partition_deleted_error(&err_msg) {
log::warn!("Partition '{}' was deleted, recreating it", name);

// Clean up any stale references in the registry
self.map_registry.remove(name);

// Fjall's open_partition should create a new partition if it doesn't exist
// If it fails, it might be due to file system cleanup in progress, so retry once
match ks.open_partition(name, config_clone.clone()) {
Ok(partition) => Ok(partition),
Err(retry_err) => {
// If the retry also fails, wait a moment for file system cleanup
log::debug!("First retry failed, waiting briefly for cleanup: {}", retry_err);

// Block briefly to allow Fjall's file system cleanup to complete
std::thread::sleep(std::time::Duration::from_millis(
Self::PARTITION_CLEANUP_DELAY_MS
));

// Final attempt
ks.open_partition(name, config_clone)
.map_err(|e| {
log::error!("Failed to recreate partition '{}' after retries: {}", name, e);
to_nitrite_error(e)
})
}
}
} else {
log::error!("Failed to open partition '{}': {}", name, err);
Err(to_nitrite_error(err))
}
}
}
}

fn initialize(&self, config: NitriteConfig) -> NitriteResult<()> {
// get_or_init() always returns a reference to the initialized value (or initial value if already initialized)
// The None case in pattern matching below is unreachable after get_or_init() completes successfully
Expand Down Expand Up @@ -406,25 +476,22 @@ impl FjallStoreInner {
}

if let Some(ks) = self.keyspace.get() {
match ks.open_partition(name, self.store_config.partition_config()) {
Ok(partition) => {
let fjall_map = FjallMap::new(
name.to_string(),
partition,
fjall_store,
self.store_config.clone(),
);
fjall_map.initialize()?;

self.map_registry
.insert(name.to_string(), fjall_map.clone());
Ok(NitriteMap::new(fjall_map))
}
Err(err) => {
log::error!("Failed to open partition: {}", err);
Err(to_nitrite_error(err))
}
}
let config = self.store_config.partition_config();

// Try to open the partition - with retry logic for deleted partitions
let partition = self.open_partition_with_retry(&ks, name, &config)?;

let fjall_map = FjallMap::new(
name.to_string(),
partition,
fjall_store,
self.store_config.clone(),
);
fjall_map.initialize()?;

self.map_registry
.insert(name.to_string(), fjall_map.clone());
Ok(NitriteMap::new(fjall_map))
} else {
Err(NitriteError::new(
"Keyspace is not initialized",
Expand All @@ -451,6 +518,9 @@ impl FjallStoreInner {
Ok(partition) => {
match ks.delete_partition(partition.clone()) {
Ok(_) => {
// Defensive cleanup: Ensure the map is removed from registry after successful deletion.
// This handles the unlikely race condition where the map might be re-opened
// by another thread after close_map() but before delete_partition() completes.
self.map_registry.remove(name);
Ok(())
}
Expand All @@ -461,8 +531,17 @@ impl FjallStoreInner {
}
}
Err(err) => {
log::error!("Failed to open partition: {}", err);
Err(to_nitrite_error(err))
let err_msg = err.to_string();

// If partition doesn't exist or was already deleted, that's OK
if Self::is_partition_deleted_error(&err_msg) {
self.map_registry.remove(name);
log::debug!("Partition '{}' was already deleted", name);
Ok(())
} else {
log::error!("Failed to open partition for removal: {}", err);
Err(to_nitrite_error(err))
}
}
}
} else {
Expand Down
Loading