Skip to content

feat(core): support sharing one redb database between different Operators #6173

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
May 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
98 changes: 86 additions & 12 deletions core/src/services/redb/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ use crate::*;
impl Configurator for RedbConfig {
type Builder = RedbBuilder;
fn into_builder(self) -> Self::Builder {
RedbBuilder { config: self }
RedbBuilder {
config: self,
database: None,
}
}
}

Expand All @@ -42,16 +45,47 @@ impl Configurator for RedbConfig {
#[derive(Default, Debug)]
pub struct RedbBuilder {
config: RedbConfig,

database: Option<Arc<redb::Database>>,
}

impl RedbBuilder {
/// Set the database for Redb.
///
/// This method should be called when you want to
/// use multiple tables of one database because
/// Redb doesn't allow opening a database that have been opened.
///
/// <div class="warning">
///
/// `datadir` and `database` should not be set simultaneously.
/// If both are set, `database` will take precedence.
///
/// </div>
pub fn database(mut self, db: Arc<redb::Database>) -> Self {
self.database = Some(db);
self
}

/// Set the path to the redb data directory. Will create if not exists.
///
///
/// <div class="warning">
///
/// Opening redb database via `datadir` takes away the ability to access multiple redb tables.
/// If you need to access multiple redb tables, the correct solution is to
/// create an `Arc<redb::database>` beforehand and then share it via [`database`]
/// with multiple builders where every builder will open one redb table.
///
/// </div>
///
/// [`database`]: RedbBuilder::database
pub fn datadir(mut self, path: &str) -> Self {
self.config.datadir = Some(path.into());
self
}

/// Set the table name for Redb.
/// Set the table name for Redb. Will create if not exists.
pub fn table(mut self, table: &str) -> Self {
self.config.table = Some(table.into());
self
Expand All @@ -69,22 +103,30 @@ impl Builder for RedbBuilder {
type Config = RedbConfig;

fn build(self) -> Result<impl Access> {
let datadir_path = self.config.datadir.ok_or_else(|| {
Error::new(ErrorKind::ConfigInvalid, "datadir is required but not set")
.with_context("service", Scheme::Redb)
})?;

let table_name = self.config.table.ok_or_else(|| {
Error::new(ErrorKind::ConfigInvalid, "table is required but not set")
.with_context("service", Scheme::Redb)
})?;

let db = redb::Database::create(&datadir_path).map_err(parse_database_error)?;
let (datadir, db) = if let Some(db) = self.database {
(None, db)
} else {
let datadir = self.config.datadir.ok_or_else(|| {
Error::new(ErrorKind::ConfigInvalid, "datadir is required but not set")
.with_context("service", Scheme::Redb)
})?;

let db = redb::Database::create(&datadir)
.map_err(parse_database_error)?
.into();

let db = Arc::new(db);
(Some(datadir), db)
};

create_table(&db, &table_name)?;

Ok(RedbBackend::new(Adapter {
datadir: datadir_path,
datadir,
table: table_name,
db,
})
Expand All @@ -97,7 +139,7 @@ pub type RedbBackend = kv::Backend<Adapter>;

#[derive(Clone)]
pub struct Adapter {
datadir: String,
datadir: Option<String>,
table: String,
db: Arc<redb::Database>,
}
Expand All @@ -116,7 +158,7 @@ impl kv::Adapter for Adapter {
fn info(&self) -> kv::Info {
kv::Info::new(
Scheme::Redb,
&self.datadir,
&self.table,
Capability {
read: true,
write: true,
Expand Down Expand Up @@ -238,3 +280,35 @@ fn parse_database_error(e: redb::DatabaseError) -> Error {
fn parse_commit_error(e: redb::CommitError) -> Error {
Error::new(ErrorKind::Unexpected, "error from redb").set_source(e)
}

/// Check if a table exists, otherwise create it.
fn create_table(db: &redb::Database, table: &str) -> Result<()> {
// Only one `WriteTransaction` is permitted at same time,
// applying new one will block until it available.
//
// So we first try checking table existence via `ReadTransaction`.
{
let read_txn = db.begin_read().map_err(parse_transaction_error)?;

let table_define: redb::TableDefinition<&str, &[u8]> = redb::TableDefinition::new(table);

match read_txn.open_table(table_define) {
Ok(_) => return Ok(()),
Err(redb::TableError::TableDoesNotExist(_)) => (),
Err(e) => return Err(parse_table_error(e)),
}
}

{
let write_txn = db.begin_write().map_err(parse_transaction_error)?;

let table_define: redb::TableDefinition<&str, &[u8]> = redb::TableDefinition::new(table);

write_txn
.open_table(table_define)
.map_err(parse_table_error)?;
write_txn.commit().map_err(parse_commit_error)?;
}

Ok(())
}
5 changes: 3 additions & 2 deletions core/src/services/redb/docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ This service can be used to:

## Configuration

- `datadir`: Set the path to the redb data directory
- `datadir`: Set the path to the redb data directory.
- `table`: Set the table name for Redb.

You can refer to [`RedbBuilder`]'s docs for more information
You can refer to [`RedbBuilder`]'s docs for more information.

## Example

Expand Down
Loading