mirror of
https://gitlab.computer.surgery/matrix/grapevine.git
synced 2025-12-18 16:21:24 +01:00
grapevine-specific db version namespace
This allows us to break db compatibility, but such that the worst thing that happens if somebody tries to run our db against an incompatible server is that it exits immediately with an error.
This commit is contained in:
parent
9add9a1e96
commit
82a75dc2c1
5 changed files with 174 additions and 32 deletions
|
|
@ -25,6 +25,7 @@ use tracing::{debug, error, info, info_span, warn, Instrument};
|
|||
use crate::{
|
||||
config::DatabaseBackend,
|
||||
service::{
|
||||
globals::DbVersion,
|
||||
media::MediaFileKey,
|
||||
rooms::{
|
||||
short::{ShortEventId, ShortStateHash, ShortStateKey},
|
||||
|
|
@ -534,11 +535,20 @@ impl KeyValueDatabase {
|
|||
#[allow(clippy::too_many_lines)]
|
||||
pub(crate) async fn apply_migrations(&self) -> Result<()> {
|
||||
// If the database has any data, perform data migrations before starting
|
||||
let latest_database_version = 13;
|
||||
let latest_database_version = DbVersion::Conduit(13);
|
||||
|
||||
let current_database_version = services().globals.database_version()?;
|
||||
if !current_database_version.is_compatible() {
|
||||
error!(version = ?current_database_version, "current db version is unsupported");
|
||||
return Err(Error::bad_database(
|
||||
"Database was last written with a conduit schema version \
|
||||
newer than we support.",
|
||||
));
|
||||
}
|
||||
|
||||
if services().users.count()? > 0 {
|
||||
// MIGRATIONS
|
||||
migration(1, || {
|
||||
migration(DbVersion::Conduit(1), || {
|
||||
for (roomserverid, _) in self.roomserverids.iter() {
|
||||
let mut parts = roomserverid.split(|&b| b == 0xFF);
|
||||
let room_id =
|
||||
|
|
@ -556,7 +566,7 @@ impl KeyValueDatabase {
|
|||
Ok(())
|
||||
})?;
|
||||
|
||||
migration(2, || {
|
||||
migration(DbVersion::Conduit(2), || {
|
||||
// We accidentally inserted hashed versions of "" into the db
|
||||
// instead of just ""
|
||||
for (userid, password) in self.userid_password.iter() {
|
||||
|
|
@ -574,7 +584,7 @@ impl KeyValueDatabase {
|
|||
Ok(())
|
||||
})?;
|
||||
|
||||
migration(3, || {
|
||||
migration(DbVersion::Conduit(3), || {
|
||||
// Move media to filesystem
|
||||
for (key, content) in self.mediaid_file.iter() {
|
||||
let key = MediaFileKey::new(key);
|
||||
|
|
@ -590,7 +600,7 @@ impl KeyValueDatabase {
|
|||
Ok(())
|
||||
})?;
|
||||
|
||||
migration(4, || {
|
||||
migration(DbVersion::Conduit(4), || {
|
||||
// Add federated users to services() as deactivated
|
||||
for our_user in services().users.iter() {
|
||||
let our_user = our_user?;
|
||||
|
|
@ -616,7 +626,7 @@ impl KeyValueDatabase {
|
|||
Ok(())
|
||||
})?;
|
||||
|
||||
migration(5, || {
|
||||
migration(DbVersion::Conduit(5), || {
|
||||
// Upgrade user data store
|
||||
for (roomuserdataid, _) in
|
||||
self.roomuserdataid_accountdata.iter()
|
||||
|
|
@ -639,7 +649,7 @@ impl KeyValueDatabase {
|
|||
Ok(())
|
||||
})?;
|
||||
|
||||
migration(6, || {
|
||||
migration(DbVersion::Conduit(6), || {
|
||||
// Set room member count
|
||||
for (roomid, _) in self.roomid_shortstatehash.iter() {
|
||||
let string = utils::string_from_bytes(&roomid).unwrap();
|
||||
|
|
@ -652,7 +662,7 @@ impl KeyValueDatabase {
|
|||
Ok(())
|
||||
})?;
|
||||
|
||||
migration(7, || {
|
||||
migration(DbVersion::Conduit(7), || {
|
||||
// Upgrade state store
|
||||
let mut last_roomstates: HashMap<OwnedRoomId, ShortStateHash> =
|
||||
HashMap::new();
|
||||
|
|
@ -787,7 +797,7 @@ impl KeyValueDatabase {
|
|||
Ok(())
|
||||
})?;
|
||||
|
||||
migration(8, || {
|
||||
migration(DbVersion::Conduit(8), || {
|
||||
// Generate short room ids for all rooms
|
||||
for (room_id, _) in self.roomid_shortstatehash.iter() {
|
||||
let shortroomid =
|
||||
|
|
@ -843,7 +853,7 @@ impl KeyValueDatabase {
|
|||
Ok(())
|
||||
})?;
|
||||
|
||||
migration(9, || {
|
||||
migration(DbVersion::Conduit(9), || {
|
||||
// Update tokenids db layout
|
||||
let mut iter = self
|
||||
.tokenids
|
||||
|
|
@ -891,7 +901,7 @@ impl KeyValueDatabase {
|
|||
Ok(())
|
||||
})?;
|
||||
|
||||
migration(10, || {
|
||||
migration(DbVersion::Conduit(10), || {
|
||||
// Add other direction for shortstatekeys
|
||||
for (statekey, shortstatekey) in
|
||||
self.statekey_shortstatekey.iter()
|
||||
|
|
@ -908,14 +918,14 @@ impl KeyValueDatabase {
|
|||
Ok(())
|
||||
})?;
|
||||
|
||||
migration(11, || {
|
||||
migration(DbVersion::Conduit(11), || {
|
||||
self.db
|
||||
.open_tree("userdevicesessionid_uiaarequest")?
|
||||
.clear()?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
migration(12, || {
|
||||
migration(DbVersion::Conduit(12), || {
|
||||
for username in services().users.list_local_users()? {
|
||||
let user = match UserId::parse_with_server_name(
|
||||
username.clone(),
|
||||
|
|
@ -1017,7 +1027,7 @@ impl KeyValueDatabase {
|
|||
|
||||
// This migration can be reused as-is anytime the server-default
|
||||
// rules are updated.
|
||||
migration(13, || {
|
||||
migration(DbVersion::Conduit(13), || {
|
||||
for username in services().users.list_local_users()? {
|
||||
let user = match UserId::parse_with_server_name(
|
||||
username.clone(),
|
||||
|
|
@ -1079,7 +1089,7 @@ impl KeyValueDatabase {
|
|||
|
||||
info!(
|
||||
backend = %services().globals.config.database.backend,
|
||||
version = latest_database_version,
|
||||
version = ?latest_database_version,
|
||||
"Loaded database",
|
||||
);
|
||||
} else {
|
||||
|
|
@ -1092,7 +1102,7 @@ impl KeyValueDatabase {
|
|||
|
||||
info!(
|
||||
backend = %services().globals.config.database.backend,
|
||||
version = latest_database_version,
|
||||
version = ?latest_database_version,
|
||||
"Created new database",
|
||||
);
|
||||
}
|
||||
|
|
@ -1151,7 +1161,7 @@ impl KeyValueDatabase {
|
|||
|
||||
/// If the current version is older than `new_version`, execute a migration
|
||||
/// function.
|
||||
fn migration<F>(new_version: u64, migration: F) -> Result<(), Error>
|
||||
fn migration<F>(new_version: DbVersion, migration: F) -> Result<(), Error>
|
||||
where
|
||||
F: FnOnce() -> Result<(), Error>,
|
||||
{
|
||||
|
|
@ -1159,7 +1169,7 @@ where
|
|||
if current_version < new_version {
|
||||
migration()?;
|
||||
services().globals.bump_database_version(new_version)?;
|
||||
warn!("Migration: {current_version} -> {new_version} finished");
|
||||
warn!("Migration: {current_version:?} -> {new_version:?} finished");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,12 +11,25 @@ use ruma::{
|
|||
|
||||
use crate::{
|
||||
database::KeyValueDatabase,
|
||||
service::{self, globals::SigningKeys},
|
||||
service::{
|
||||
self,
|
||||
globals::{DbVersion, SigningKeys},
|
||||
},
|
||||
services, utils, Error, Result,
|
||||
};
|
||||
|
||||
pub(crate) const COUNTER: &[u8] = b"c";
|
||||
|
||||
/// Placeholder stored in `globals.version` to indicate that
|
||||
/// `globals.namespaced_version` should be used instead. See the documentation
|
||||
/// on [`DbVersion`] for more details.
|
||||
// Allowed because there isn't another way to do the conversion in const context
|
||||
#[allow(clippy::as_conversions)]
|
||||
const CONDUIT_PLACEHOLDER_VERSION: u64 = i64::MAX as u64;
|
||||
|
||||
/// Namespace tag for [`DbVersion::Grapevine`].
|
||||
const GRAPEVINE_VERSION_NAMESPACE: &[u8] = b"grapevine";
|
||||
|
||||
#[async_trait]
|
||||
impl service::globals::Data for KeyValueDatabase {
|
||||
fn next_count(&self) -> Result<u64> {
|
||||
|
|
@ -349,16 +362,63 @@ lasttimelinecount_cache: {lasttimelinecount_cache}\n"
|
|||
Ok(signingkeys)
|
||||
}
|
||||
|
||||
fn database_version(&self) -> Result<u64> {
|
||||
self.global.get(b"version")?.map_or(Ok(0), |version| {
|
||||
utils::u64_from_bytes(&version).map_err(|_| {
|
||||
Error::bad_database("Database version id is invalid.")
|
||||
})
|
||||
})
|
||||
fn database_version(&self) -> Result<DbVersion> {
|
||||
let version =
|
||||
self.global.get(b"version")?.map_or(Ok(0), |version| {
|
||||
utils::u64_from_bytes(&version).map_err(|_| {
|
||||
Error::bad_database("Database version id is invalid.")
|
||||
})
|
||||
})?;
|
||||
|
||||
if version == CONDUIT_PLACEHOLDER_VERSION {
|
||||
let value =
|
||||
self.global.get(b"namespaced_version")?.ok_or_else(|| {
|
||||
Error::bad_database("'namespace_version' is missing")
|
||||
})?;
|
||||
let mut parts = value.splitn(2, |&b| b == 0xFF);
|
||||
let namespace = parts
|
||||
.next()
|
||||
.expect("splitn always returns at least one element");
|
||||
let version_bytes = parts.next().ok_or_else(|| {
|
||||
Error::bad_database(
|
||||
"Invalid 'namespaced_version' value: missing separator",
|
||||
)
|
||||
})?;
|
||||
let version =
|
||||
utils::u64_from_bytes(version_bytes).map_err(|_| {
|
||||
Error::bad_database(
|
||||
"Invalid 'namespaced_version' value: version is not a \
|
||||
u64",
|
||||
)
|
||||
})?;
|
||||
|
||||
if namespace == GRAPEVINE_VERSION_NAMESPACE {
|
||||
Ok(DbVersion::Grapevine(version))
|
||||
} else {
|
||||
Err(Error::UnknownDbVersionNamespace(namespace.to_owned()))
|
||||
}
|
||||
} else {
|
||||
Ok(DbVersion::Conduit(version))
|
||||
}
|
||||
}
|
||||
|
||||
fn bump_database_version(&self, new_version: u64) -> Result<()> {
|
||||
self.global.insert(b"version", &new_version.to_be_bytes())?;
|
||||
fn bump_database_version(&self, new_version: DbVersion) -> Result<()> {
|
||||
match new_version {
|
||||
DbVersion::Grapevine(version) => {
|
||||
let mut value = GRAPEVINE_VERSION_NAMESPACE.to_vec();
|
||||
value.push(0xFF);
|
||||
value.extend(&version.to_be_bytes());
|
||||
self.global.insert(
|
||||
b"version",
|
||||
&CONDUIT_PLACEHOLDER_VERSION.to_be_bytes(),
|
||||
)?;
|
||||
self.global.insert(b"namespaced_version", &value)?;
|
||||
}
|
||||
DbVersion::Conduit(version) => {
|
||||
self.global.insert(b"version", &version.to_be_bytes())?;
|
||||
self.global.remove(b"namespaced_version")?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ use std::{
|
|||
};
|
||||
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
pub(crate) use data::{Data, SigningKeys};
|
||||
pub(crate) use data::{Data, DbVersion, SigningKeys};
|
||||
use futures_util::FutureExt;
|
||||
use hyper::service::Service as _;
|
||||
use hyper_util::{
|
||||
|
|
@ -592,11 +592,14 @@ impl Service {
|
|||
})
|
||||
}
|
||||
|
||||
pub(crate) fn database_version(&self) -> Result<u64> {
|
||||
pub(crate) fn database_version(&self) -> Result<DbVersion> {
|
||||
self.db.database_version()
|
||||
}
|
||||
|
||||
pub(crate) fn bump_database_version(&self, new_version: u64) -> Result<()> {
|
||||
pub(crate) fn bump_database_version(
|
||||
&self,
|
||||
new_version: DbVersion,
|
||||
) -> Result<()> {
|
||||
self.db.bump_database_version(new_version)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
use std::{
|
||||
cmp::Ordering,
|
||||
collections::BTreeMap,
|
||||
time::{Duration, SystemTime},
|
||||
};
|
||||
|
|
@ -14,6 +15,70 @@ use serde::Deserialize;
|
|||
|
||||
use crate::{services, Result};
|
||||
|
||||
/// Database schema version.
|
||||
///
|
||||
/// In conduit, database versions were tracked with a single integer value in
|
||||
/// `globals.version`, incremented with every migration. Now, we want to make
|
||||
/// our own db schema changes, which may not be compatible the schema used by
|
||||
/// conduit and other conduit forks. We also want to support users moving
|
||||
/// existing databases between conduit, grapevine, and other conduit forks.
|
||||
///
|
||||
/// To handle this, we have namespacing for db versions. Version numbers that
|
||||
/// match the upstream conduit versions are still stored directly in
|
||||
/// `globals.version`. Versions that are grapevine-specific are stored in
|
||||
/// `globals.namespaced_version`, with the value `"grapevine" + 0xFF + version`.
|
||||
///
|
||||
/// When a non-conduit compatible version is set, the value of `globals.version`
|
||||
/// is set to `i64::MAX`. This ensures that software that only knows about the
|
||||
/// conduit-compatible versioning will treat the db as a unknown future version
|
||||
/// and refuse to interact with it.
|
||||
///
|
||||
/// The last shared version is `Conduit(13)`. Past this point, we start counting
|
||||
/// at `Grapevine(0)`.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||
pub(crate) enum DbVersion {
|
||||
/// Version namespace shared with conduit and other conduit forks.
|
||||
Conduit(u64),
|
||||
/// Grapevine-specific version namespace.
|
||||
Grapevine(u64),
|
||||
}
|
||||
|
||||
impl DbVersion {
|
||||
/// Return whether this version is part of the compatible version history
|
||||
/// for grapevine.
|
||||
///
|
||||
/// Version changes that were made to conduit after the fork, or made in
|
||||
/// other conduit forks are not compatible.
|
||||
pub(crate) fn is_compatible(self) -> bool {
|
||||
match self {
|
||||
DbVersion::Conduit(version) => {
|
||||
// Conduit db version 13 is the last supported version in the
|
||||
// shared namespace. Our schema diverges past
|
||||
// this point.
|
||||
version <= 13
|
||||
}
|
||||
DbVersion::Grapevine(_) => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for DbVersion {
|
||||
fn partial_cmp(&self, other: &DbVersion) -> Option<Ordering> {
|
||||
use DbVersion::{Conduit, Grapevine};
|
||||
match (self, other) {
|
||||
(Conduit(a), Conduit(b)) | (Grapevine(a), Grapevine(b)) => {
|
||||
Some(a.cmp(b))
|
||||
}
|
||||
(&Conduit(_), Grapevine(_)) => {
|
||||
self.is_compatible().then_some(Ordering::Less)
|
||||
}
|
||||
(Grapevine(_), &Conduit(_)) => {
|
||||
other.is_compatible().then_some(Ordering::Greater)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Similar to [`ServerSigningKeys`], but drops a few unnecessary fields we
|
||||
/// don't require post-validation
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
|
|
@ -117,6 +182,7 @@ pub(crate) trait Data: Send + Sync {
|
|||
&self,
|
||||
origin: &ServerName,
|
||||
) -> Result<Option<SigningKeys>>;
|
||||
fn database_version(&self) -> Result<u64>;
|
||||
fn bump_database_version(&self, new_version: u64) -> Result<()>;
|
||||
|
||||
fn database_version(&self) -> Result<DbVersion>;
|
||||
fn bump_database_version(&self, new_version: DbVersion) -> Result<()>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,6 +84,9 @@ pub(crate) enum Error {
|
|||
UnsupportedRoomVersion(ruma::RoomVersionId),
|
||||
#[error("{0} in {1}")]
|
||||
InconsistentRoomState(&'static str, ruma::OwnedRoomId),
|
||||
|
||||
#[error("unknown db version namespace {}", String::from_utf8_lossy(_0))]
|
||||
UnknownDbVersionNamespace(Vec<u8>),
|
||||
}
|
||||
|
||||
impl Error {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue