add database backup with admin commands

Signed-off-by: Jason Volk <jason@zemos.net>
This commit is contained in:
Jason Volk 2024-03-19 04:37:35 -07:00 committed by June
parent ece817c562
commit fa942aedd7
8 changed files with 144 additions and 2 deletions

10
Cargo.lock generated
View file

@ -355,6 +355,15 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
[[package]]
name = "chrono"
version = "0.4.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a"
dependencies = [
"num-traits",
]
[[package]] [[package]]
name = "clang-sys" name = "clang-sys"
version = "1.7.0" version = "1.7.0"
@ -421,6 +430,7 @@ dependencies = [
"axum-server-dual-protocol", "axum-server-dual-protocol",
"base64 0.22.0", "base64 0.22.0",
"bytes", "bytes",
"chrono",
"clap", "clap",
"cyborgtime", "cyborgtime",
"either", "either",

View file

@ -67,6 +67,12 @@ cyborgtime = "2.1.1"
bytes = "1.5.0" bytes = "1.5.0"
http = "0.2.12" http = "0.2.12"
# standard date and time tools
[dependencies.chrono]
version = "0.4.35"
features = ["alloc"]
default-features = false
# Web framework # Web framework
[dependencies.axum] [dependencies.axum]
version = "0.6.20" version = "0.6.20"

View file

@ -42,6 +42,9 @@ pub struct Config {
#[serde(default = "default_database_backend")] #[serde(default = "default_database_backend")]
pub database_backend: String, pub database_backend: String,
pub database_path: String, pub database_path: String,
pub database_backup_path: Option<String>,
#[serde(default = "default_database_backups_to_keep")]
pub database_backups_to_keep: i16,
#[serde(default = "default_db_cache_capacity_mb")] #[serde(default = "default_db_cache_capacity_mb")]
pub db_cache_capacity_mb: f64, pub db_cache_capacity_mb: f64,
#[serde(default = "default_new_user_displayname_suffix")] #[serde(default = "default_new_user_displayname_suffix")]
@ -252,6 +255,14 @@ impl fmt::Display for Config {
("Server name", self.server_name.host()), ("Server name", self.server_name.host()),
("Database backend", &self.database_backend), ("Database backend", &self.database_backend),
("Database path", &self.database_path), ("Database path", &self.database_path),
(
"Database backup path",
match self.database_backup_path.as_ref() {
Some(path) => path,
None => "",
},
),
("Database backups to keep", &self.database_backups_to_keep.to_string()),
("Database cache capacity (MB)", &self.db_cache_capacity_mb.to_string()), ("Database cache capacity (MB)", &self.db_cache_capacity_mb.to_string()),
("Cache capacity modifier", &self.conduit_cache_capacity_modifier.to_string()), ("Cache capacity modifier", &self.conduit_cache_capacity_modifier.to_string()),
("PDU cache capacity", &self.pdu_cache_capacity.to_string()), ("PDU cache capacity", &self.pdu_cache_capacity.to_string()),
@ -446,6 +457,8 @@ fn default_port() -> ListeningPort {
fn default_unix_socket_perms() -> u32 { 660 } fn default_unix_socket_perms() -> u32 { 660 }
fn default_database_backups_to_keep() -> i16 { 1 }
fn default_database_backend() -> String { "rocksdb".to_owned() } fn default_database_backend() -> String { "rocksdb".to_owned() }
fn default_db_cache_capacity_mb() -> f64 { 256.0 } fn default_db_cache_capacity_mb() -> f64 { 256.0 }

View file

@ -1,4 +1,4 @@
use std::{future::Future, pin::Pin, sync::Arc}; use std::{error::Error, future::Future, pin::Pin, sync::Arc};
use super::Config; use super::Config;
use crate::Result; use crate::Result;
@ -26,6 +26,14 @@ pub(crate) trait KeyValueDatabaseEngine: Send + Sync {
#[allow(dead_code)] #[allow(dead_code)]
fn clear_caches(&self) {} fn clear_caches(&self) {}
fn backup(&self) -> Result<(), Box<dyn Error>> {
unimplemented!()
}
fn backup_list(&self) -> Result<String> {
Ok(String::new())
}
} }
pub(crate) trait KvTree: Send + Sync { pub(crate) trait KvTree: Send + Sync {

View file

@ -3,12 +3,17 @@ use std::{
pin::Pin, pin::Pin,
sync::{Arc, RwLock}, sync::{Arc, RwLock},
}; };
use chrono::{
DateTime,
Utc,
};
use rust_rocksdb::{ use rust_rocksdb::{
backup::{BackupEngine, BackupEngineOptions},
LogLevel::{Debug, Error, Fatal, Info, Warn}, LogLevel::{Debug, Error, Fatal, Info, Warn},
WriteBatchWithTransaction, WriteBatchWithTransaction,
}; };
use tracing::{debug, info}; use tracing::{debug, error, info};
use super::{super::Config, watchers::Watchers, KeyValueDatabaseEngine, KvTree}; use super::{super::Config, watchers::Watchers, KeyValueDatabaseEngine, KvTree};
use crate::{utils, Result}; use crate::{utils, Result};
@ -221,6 +226,68 @@ impl KeyValueDatabaseEngine for Arc<Engine> {
Ok(()) Ok(())
} }
fn backup(&self) -> Result<(), Box<dyn std::error::Error>> {
let path = self.config.database_backup_path.as_ref();
if path.is_none() || path.is_some_and(String::is_empty) {
return Ok(());
}
let options = BackupEngineOptions::new(&path.unwrap())?;
let mut engine = BackupEngine::open(&options, &self.env)?;
let ret = if self.config.database_backups_to_keep > 0 {
match engine.create_new_backup_flush(&self.rocks, true) {
Err(e) => return Err(Box::new(e)),
Ok(_) => {
let _info = engine.get_backup_info();
let info = &_info.last().unwrap();
info!(
"Created database backup #{} using {} bytes in {} files",
info.backup_id,
info.size,
info.num_files,
);
Ok(())
},
}
} else {
Ok(())
};
if self.config.database_backups_to_keep >= 0 {
let keep = u32::try_from(self.config.database_backups_to_keep)?;
if let Err(e) = engine.purge_old_backups(keep.try_into()?) {
error!("Failed to purge old backup: {:?}", e.to_string())
}
}
ret
}
fn backup_list(&self) -> Result<String> {
let path = self.config.database_backup_path.as_ref();
if path.is_none() || path.is_some_and(String::is_empty) {
return Ok("Configure database_backup_path to enable backups".to_owned());
}
let mut res = String::new();
let options = BackupEngineOptions::new(&path.unwrap())?;
let engine = BackupEngine::open(&options, &self.env)?;
for info in engine.get_backup_info() {
std::fmt::write(&mut res, format_args!(
"#{} {}: {} bytes, {} files\n",
info.backup_id,
DateTime::<Utc>::from_timestamp(info.timestamp, 0)
.unwrap()
.to_rfc2822(),
info.size,
info.num_files,
))
.unwrap();
}
Ok(res)
}
// TODO: figure out if this is needed for rocksdb // TODO: figure out if this is needed for rocksdb
#[allow(dead_code)] #[allow(dead_code)]
fn clear_caches(&self) {} fn clear_caches(&self) {}

View file

@ -272,4 +272,12 @@ lasttimelinecount_cache: {lasttimelinecount_cache}\n"
self.global.insert(b"version", &new_version.to_be_bytes())?; self.global.insert(b"version", &new_version.to_be_bytes())?;
Ok(()) Ok(())
} }
fn backup(&self) -> Result<(), Box<dyn std::error::Error>> {
self.db.backup()
}
fn backup_list(&self) -> Result<String> {
self.db.backup_list()
}
} }

View file

@ -436,6 +436,12 @@ enum ServerCommand {
ClearServiceCaches { ClearServiceCaches {
amount: u32, amount: u32,
}, },
/// - Backup the database
BackupDatabase,
/// - List database backups
ListBackups,
} }
#[derive(Debug)] #[derive(Debug)]
@ -1866,6 +1872,27 @@ impl Service {
RoomMessageEventContent::text_plain("Done.") RoomMessageEventContent::text_plain("Done.")
}, },
ServerCommand::ListBackups => {
let result = services().globals.db.backup_list()?;
RoomMessageEventContent::text_plain(result)
},
ServerCommand::BackupDatabase => {
let mut result = tokio::task::spawn_blocking(move || {
match services().globals.db.backup() {
Ok(_) => String::new(),
Err(e) => (*e).to_string(),
}
})
.await
.unwrap();
if result.is_empty() {
result = services().globals.db.backup_list()?;
}
RoomMessageEventContent::text_plain(&result)
},
}, },
AdminCommand::Debug(command) => match command { AdminCommand::Debug(command) => match command {
DebugCommand::GetAuthChain { DebugCommand::GetAuthChain {

View file

@ -1,4 +1,5 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::error::Error;
use async_trait::async_trait; use async_trait::async_trait;
use ruma::{ use ruma::{
@ -32,4 +33,6 @@ pub trait Data: Send + Sync {
fn signing_keys_for(&self, origin: &ServerName) -> Result<BTreeMap<OwnedServerSigningKeyId, VerifyKey>>; fn signing_keys_for(&self, origin: &ServerName) -> Result<BTreeMap<OwnedServerSigningKeyId, VerifyKey>>;
fn database_version(&self) -> Result<u64>; fn database_version(&self) -> Result<u64>;
fn bump_database_version(&self, new_version: u64) -> Result<()>; fn bump_database_version(&self, new_version: u64) -> Result<()>;
fn backup(&self) -> Result<(), Box<dyn Error>> { unimplemented!() }
fn backup_list(&self) -> Result<String> { Ok(String::new()) }
} }