Versioned storage (#4581)
Co-authored-by: Tobie Morgan Hitchcock <tobie@surrealdb.com>
This commit is contained in:
parent
f2e598379b
commit
70584115ab
11 changed files with 204 additions and 12 deletions
|
@ -1093,6 +1093,14 @@ pub enum Error {
|
||||||
RangeTooBig {
|
RangeTooBig {
|
||||||
max: usize,
|
max: usize,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// There was an invalid storage version stored in the database
|
||||||
|
#[error("There was an invalid storage version stored in the database")]
|
||||||
|
InvalidStorageVersion,
|
||||||
|
|
||||||
|
/// There was an outdated storage version stored in the database
|
||||||
|
#[error("The data stored on disk is out-of-date with this version. Please follow the upgrade guides in the documentation")]
|
||||||
|
OutdatedStorageVersion,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Error> for String {
|
impl From<Error> for String {
|
||||||
|
|
|
@ -10,6 +10,8 @@ pub(crate) trait Categorise {
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
pub enum Category {
|
pub enum Category {
|
||||||
|
/// crate::key::storage::version /sv
|
||||||
|
Version,
|
||||||
/// crate::key::root::all /
|
/// crate::key::root::all /
|
||||||
Root,
|
Root,
|
||||||
/// crate::key::root::access::ac /!ac{ac}
|
/// crate::key::root::access::ac /!ac{ac}
|
||||||
|
@ -154,6 +156,7 @@ pub enum Category {
|
||||||
impl Display for Category {
|
impl Display for Category {
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
let name = match self {
|
let name = match self {
|
||||||
|
Self::Version => "StorageVersion",
|
||||||
Self::Root => "Root",
|
Self::Root => "Root",
|
||||||
Self::Access => "Access",
|
Self::Access => "Access",
|
||||||
Self::AccessRoot => "AccessRoot",
|
Self::AccessRoot => "AccessRoot",
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
//! How the keys are structured in the key value store
|
//! How the keys are structured in the key value store
|
||||||
///
|
///
|
||||||
|
/// crate::key::version !v
|
||||||
|
///
|
||||||
/// crate::key::root::all /
|
/// crate::key::root::all /
|
||||||
/// crate::key::root::access::all /*{ac}
|
/// crate::key::root::access::all /*{ac}
|
||||||
/// crate::key::root::access::ac /!ac{ac}
|
/// crate::key::root::access::ac /!ac{ac}
|
||||||
|
@ -74,3 +76,4 @@ pub(crate) mod node;
|
||||||
pub(crate) mod root;
|
pub(crate) mod root;
|
||||||
pub(crate) mod table;
|
pub(crate) mod table;
|
||||||
pub(crate) mod thing;
|
pub(crate) mod thing;
|
||||||
|
pub(crate) mod version;
|
||||||
|
|
57
core/src/key/version/mod.rs
Normal file
57
core/src/key/version/mod.rs
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
//! Stores a record document
|
||||||
|
use crate::key::category::Categorise;
|
||||||
|
use crate::key::category::Category;
|
||||||
|
use derive::Key;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::ops::Range;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Key)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub struct Version {
|
||||||
|
__: u8,
|
||||||
|
_a: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new() -> Version {
|
||||||
|
Version::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn proceeding() -> Range<Vec<u8>> {
|
||||||
|
vec![b'!', b'v', 0x00]..vec![0xff]
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Categorise for Version {
|
||||||
|
fn categorise(&self) -> Category {
|
||||||
|
Category::Version
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Version {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
__: b'!',
|
||||||
|
_a: b'v',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Version {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
#[test]
|
||||||
|
fn key() {
|
||||||
|
use super::*;
|
||||||
|
#[rustfmt::skip]
|
||||||
|
let val = Version::new();
|
||||||
|
let enc = Version::encode(&val).unwrap();
|
||||||
|
assert_eq!(enc, b"!v");
|
||||||
|
|
||||||
|
let dec = Version::decode(&enc).unwrap();
|
||||||
|
assert_eq!(val, dec);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
use super::tr::Transactor;
|
use super::tr::Transactor;
|
||||||
use super::tx::Transaction;
|
use super::tx::Transaction;
|
||||||
|
use super::version::Version;
|
||||||
use crate::cf;
|
use crate::cf;
|
||||||
use crate::ctx::MutableContext;
|
use crate::ctx::MutableContext;
|
||||||
#[cfg(feature = "jwks")]
|
#[cfg(feature = "jwks")]
|
||||||
|
@ -508,13 +509,59 @@ impl Datastore {
|
||||||
|
|
||||||
// Initialise the cluster and run bootstrap utilities
|
// Initialise the cluster and run bootstrap utilities
|
||||||
#[instrument(err, level = "trace", target = "surrealdb::core::kvs::ds", skip_all)]
|
#[instrument(err, level = "trace", target = "surrealdb::core::kvs::ds", skip_all)]
|
||||||
pub async fn bootstrap(&self) -> Result<(), Error> {
|
pub async fn check_version(&self) -> Result<Version, Error> {
|
||||||
// Insert this node in the cluster
|
// Start a new writeable transaction
|
||||||
self.insert_node(self.id).await?;
|
let txn = self.transaction(Write, Pessimistic).await?.enclose();
|
||||||
// Mark expired nodes as archived
|
// Create the key where the version is stored
|
||||||
self.expire_nodes().await?;
|
let key = crate::key::version::new();
|
||||||
|
// Check if a version is already set in storage
|
||||||
|
let val = match catch!(txn, txn.get(key.clone(), None)) {
|
||||||
|
// There is a version set in the storage
|
||||||
|
Some(v) => {
|
||||||
|
// Attempt to decode the current stored version
|
||||||
|
let val = TryInto::<Version>::try_into(v);
|
||||||
|
// Check for errors, and cancel the transaction
|
||||||
|
match val {
|
||||||
|
// There was en error getting the version
|
||||||
|
Err(err) => {
|
||||||
|
// We didn't write anything, so just rollback
|
||||||
|
txn.cancel().await?;
|
||||||
|
// Return the error
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
|
// We could decode the version correctly
|
||||||
|
Ok(val) => val,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// There is no version set in the storage
|
||||||
|
None => {
|
||||||
|
// Fetch any keys immediately following the version key
|
||||||
|
let rng = crate::key::version::proceeding();
|
||||||
|
let keys = catch!(txn, txn.keys(rng, 1));
|
||||||
|
// Check the storage if there are any other keys set
|
||||||
|
let val = if keys.is_empty() {
|
||||||
|
// There are no keys set in storage, so this is a new database
|
||||||
|
Version::latest()
|
||||||
|
} else {
|
||||||
|
// There were keys in storage, so this is an upgrade
|
||||||
|
Version::v1()
|
||||||
|
};
|
||||||
|
// Convert the version to binary
|
||||||
|
let bytes: Vec<u8> = val.into();
|
||||||
|
// Attempt to set the current version in storage
|
||||||
|
catch!(txn, txn.set(key, bytes));
|
||||||
|
// We set the version, so commit the transaction
|
||||||
|
catch!(txn, txn.commit());
|
||||||
|
// Return the current version
|
||||||
|
val
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Check we are running the latest version
|
||||||
|
if !val.is_latest() {
|
||||||
|
return Err(Error::OutdatedStorageVersion);
|
||||||
|
}
|
||||||
// Everything ok
|
// Everything ok
|
||||||
Ok(())
|
Ok(val)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Setup the initial cluster access credentials
|
/// Setup the initial cluster access credentials
|
||||||
|
@ -546,6 +593,17 @@ impl Datastore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialise the cluster and run bootstrap utilities
|
||||||
|
#[instrument(err, level = "trace", target = "surrealdb::core::kvs::ds", skip_all)]
|
||||||
|
pub async fn bootstrap(&self) -> Result<(), Error> {
|
||||||
|
// Insert this node in the cluster
|
||||||
|
self.insert_node(self.id).await?;
|
||||||
|
// Mark expired nodes as archived
|
||||||
|
self.expire_nodes().await?;
|
||||||
|
// Everything ok
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
// tick is called periodically to perform maintenance tasks.
|
// tick is called periodically to perform maintenance tasks.
|
||||||
// This is called every TICK_INTERVAL.
|
// This is called every TICK_INTERVAL.
|
||||||
#[instrument(err, level = "trace", target = "surrealdb::core::kvs::ds", skip(self))]
|
#[instrument(err, level = "trace", target = "surrealdb::core::kvs::ds", skip(self))]
|
||||||
|
|
|
@ -24,6 +24,7 @@ mod scanner;
|
||||||
mod stash;
|
mod stash;
|
||||||
mod tr;
|
mod tr;
|
||||||
mod tx;
|
mod tx;
|
||||||
|
mod version;
|
||||||
|
|
||||||
mod fdb;
|
mod fdb;
|
||||||
mod indxdb;
|
mod indxdb;
|
||||||
|
|
58
core/src/kvs/version.rs
Normal file
58
core/src/kvs/version.rs
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
use crate::err::Error;
|
||||||
|
|
||||||
|
#[derive(Copy, Debug, Clone)]
|
||||||
|
pub struct Version(u16);
|
||||||
|
|
||||||
|
impl From<u16> for Version {
|
||||||
|
fn from(version: u16) -> Self {
|
||||||
|
Version(version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Option<u16>> for Version {
|
||||||
|
fn from(v: Option<u16>) -> Self {
|
||||||
|
v.unwrap_or(0).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Version> for u16 {
|
||||||
|
fn from(v: Version) -> Self {
|
||||||
|
v.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Version> for Vec<u8> {
|
||||||
|
fn from(v: Version) -> Self {
|
||||||
|
v.0.to_be_bytes().to_vec()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<Vec<u8>> for Version {
|
||||||
|
type Error = Error;
|
||||||
|
fn try_from(v: Vec<u8>) -> Result<Self, Self::Error> {
|
||||||
|
let bin = v.try_into().map_err(|_| Error::InvalidStorageVersion)?;
|
||||||
|
let val = u16::from_be_bytes(bin).into();
|
||||||
|
Ok(val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Version {
|
||||||
|
/// The latest version
|
||||||
|
pub const LATEST: u16 = 2;
|
||||||
|
/// The latest version
|
||||||
|
pub fn latest() -> Self {
|
||||||
|
Self(2)
|
||||||
|
}
|
||||||
|
/// SurrealDB version 1
|
||||||
|
pub fn v1() -> Self {
|
||||||
|
Self(1)
|
||||||
|
}
|
||||||
|
/// SurrealDB version 2
|
||||||
|
pub fn v2() -> Self {
|
||||||
|
Self(2)
|
||||||
|
}
|
||||||
|
/// Check if we are running the latest version
|
||||||
|
pub fn is_latest(&self) -> bool {
|
||||||
|
self.0 == Self::LATEST
|
||||||
|
}
|
||||||
|
}
|
|
@ -199,9 +199,9 @@ impl Command {
|
||||||
} => {
|
} => {
|
||||||
let table = match what {
|
let table = match what {
|
||||||
Some(w) => {
|
Some(w) => {
|
||||||
let mut tmp = CoreTable::default();
|
let mut table = CoreTable::default();
|
||||||
tmp.0 = w.clone();
|
table.0.clone_from(&w);
|
||||||
CoreValue::from(tmp)
|
CoreValue::from(table)
|
||||||
}
|
}
|
||||||
None => CoreValue::None,
|
None => CoreValue::None,
|
||||||
};
|
};
|
||||||
|
|
|
@ -62,10 +62,10 @@ where
|
||||||
}
|
}
|
||||||
Resource::RecordId(record) => {
|
Resource::RecordId(record) => {
|
||||||
let record = record.into_inner();
|
let record = record.into_inner();
|
||||||
table.0 = record.tb.clone();
|
table.0.clone_from(&record.tb);
|
||||||
stmt.what = table.into();
|
stmt.what = table.into();
|
||||||
let mut ident = Ident::default();
|
let mut ident = Ident::default();
|
||||||
ident.0 = ID.to_owned();
|
ID.clone_into(&mut ident.0);
|
||||||
let mut idiom = Idiom::default();
|
let mut idiom = Idiom::default();
|
||||||
idiom.0 = vec![Part::from(ident)];
|
idiom.0 = vec![Part::from(ident)];
|
||||||
let mut cond = Cond::default();
|
let mut cond = Cond::default();
|
||||||
|
@ -81,7 +81,7 @@ where
|
||||||
Resource::Edge(_) => return Err(Error::LiveOnEdges.into()),
|
Resource::Edge(_) => return Err(Error::LiveOnEdges.into()),
|
||||||
Resource::Range(range) => {
|
Resource::Range(range) => {
|
||||||
let range = range.into_inner();
|
let range = range.into_inner();
|
||||||
table.0 = range.tb.clone();
|
table.0.clone_from(&range.tb);
|
||||||
stmt.what = table.into();
|
stmt.what = table.into();
|
||||||
stmt.cond = range.to_cond();
|
stmt.cond = range.to_cond();
|
||||||
}
|
}
|
||||||
|
|
|
@ -254,6 +254,7 @@ impl Test {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub async fn new(sql: &str) -> Result<Self, Error> {
|
pub async fn new(sql: &str) -> Result<Self, Error> {
|
||||||
Self::with_ds(new_ds().await?, sql).await
|
Self::with_ds(new_ds().await?, sql).await
|
||||||
}
|
}
|
||||||
|
@ -261,6 +262,7 @@ impl Test {
|
||||||
/// Simulates restarting the Datastore
|
/// Simulates restarting the Datastore
|
||||||
/// - Data are persistent (including memory store)
|
/// - Data are persistent (including memory store)
|
||||||
/// - Flushing caches (jwks, IndexStore, ...)
|
/// - Flushing caches (jwks, IndexStore, ...)
|
||||||
|
#[allow(dead_code)]
|
||||||
pub async fn restart(self, sql: &str) -> Result<Self, Error> {
|
pub async fn restart(self, sql: &str) -> Result<Self, Error> {
|
||||||
Self::with_ds(self.ds.restart(), sql).await
|
Self::with_ds(self.ds.restart(), sql).await
|
||||||
}
|
}
|
||||||
|
|
|
@ -239,6 +239,8 @@ pub async fn init(
|
||||||
.with_auth_enabled(!unauthenticated)
|
.with_auth_enabled(!unauthenticated)
|
||||||
.with_temporary_directory(temporary_directory)
|
.with_temporary_directory(temporary_directory)
|
||||||
.with_capabilities(capabilities);
|
.with_capabilities(capabilities);
|
||||||
|
// Ensure the storage version is up-to-date to prevent corruption
|
||||||
|
dbs.check_version().await?;
|
||||||
// Setup initial server auth credentials
|
// Setup initial server auth credentials
|
||||||
if let (Some(user), Some(pass)) = (opt.user.as_ref(), opt.pass.as_ref()) {
|
if let (Some(user), Some(pass)) = (opt.user.as_ref(), opt.pass.as_ref()) {
|
||||||
dbs.initialise_credentials(user, pass).await?;
|
dbs.initialise_credentials(user, pass).await?;
|
||||||
|
|
Loading…
Reference in a new issue