use snafu::{ResultExt, Snafu};
use crate::{
storage::TableLocation,
table::{TableError, TimeSeriesTable},
transaction_log::{CommitError, TableKind, TransactionLogStore},
};
#[derive(Debug, Snafu)]
#[snafu(module, visibility(pub(crate)))]
#[non_exhaustive]
pub enum OpenTableError {
#[snafu(display("Cannot open table with no commits"))]
EmptyTable,
#[snafu(display("Table kind is {kind:?}, expected a time-series table"))]
NotTimeSeries {
kind: TableKind,
},
#[snafu(context(false), display("Table open transaction-log error: {source}"))]
Commit {
#[snafu(source, backtrace)]
source: CommitError,
},
}
impl TimeSeriesTable {
#[tracing::instrument(
name = "table.open",
target = "timeseries_table_format::table",
level = "debug",
skip_all,
fields(
table_version = tracing::field::Empty,
index_kind = tracing::field::Empty,
outcome = tracing::field::Empty
)
)]
pub async fn open(location: TableLocation) -> Result<Self, TableError> {
let result: Result<Self, OpenTableError> = async {
let log = TransactionLogStore::new(location);
let current_version = log
.load_current_version()
.await
.map_err(OpenTableError::from)?;
tracing::Span::current().record("table_version", current_version);
if current_version == 0 {
return Err(OpenTableError::EmptyTable);
}
let state = log
.rebuild_table_state()
.await
.map_err(OpenTableError::from)?;
let index = match &state.table_meta.kind {
TableKind::TimeSeries(index) => index.clone(),
kind => return Err(OpenTableError::NotTimeSeries { kind: kind.clone() }),
};
tracing::Span::current().record("index_kind", index.kind.name());
Ok(Self { log, state, index })
}
.await;
tracing::Span::current().record(
"outcome",
if result.is_ok() {
"succeeded"
} else {
"failed"
},
);
result.context(crate::table::error::OpenSnafu)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
metadata::protocol::TABLE_PROTOCOL_VERSION,
table::test_util::{
TestResult, TraceCapture, assert_capture_excludes, assert_debug_span, assert_no_event,
captured_span, make_basic_table_meta,
},
transaction_log::{LogAction, TableProtocolError, TransactionLogStore},
};
use tempfile::TempDir;
#[tokio::test]
async fn open_round_trips_a_created_table() -> TestResult {
let tmp = TempDir::new()?;
let location = TableLocation::local(tmp.path());
let created = TimeSeriesTable::create(location.clone(), make_basic_table_meta()).await?;
let capture = TraceCapture::default();
let reopened = capture.run(TimeSeriesTable::open(location)).await?;
assert_eq!(created.state().version, reopened.state().version);
assert_eq!(created.index_spec(), reopened.index_spec());
assert_debug_span(
&capture,
"table.open",
&[
("table_version", Some("1")),
("index_kind", Some("timestamp")),
("outcome", Some("succeeded")),
],
);
assert_eq!(
captured_span(&capture, "table.open").target,
"timeseries_table_format::table"
);
assert_no_event(&capture, "table.open");
assert_capture_excludes(&capture, &[&tmp.path().display().to_string()]);
Ok(())
}
#[tokio::test]
async fn open_rejects_an_empty_location() -> TestResult {
let tmp = TempDir::new()?;
let capture = TraceCapture::default();
let error = capture
.run(TimeSeriesTable::open(TableLocation::local(tmp.path())))
.await
.expect_err("empty table must fail");
assert!(matches!(
error,
TableError::Open {
source: OpenTableError::EmptyTable
}
));
assert_debug_span(
&capture,
"table.open",
&[
("table_version", Some("0")),
("index_kind", None),
("outcome", Some("failed")),
],
);
Ok(())
}
#[tokio::test]
async fn open_preserves_protocol_and_table_kind_failures() -> TestResult {
for found in [TABLE_PROTOCOL_VERSION - 1, TABLE_PROTOCOL_VERSION + 1] {
let tmp = TempDir::new()?;
let location = TableLocation::local(tmp.path());
let mut meta = make_basic_table_meta();
meta.protocol_version = found;
TransactionLogStore::new(location.clone())
.commit_with_expected_version(0, vec![LogAction::UpdateTableMeta(meta)])
.await?;
assert!(matches!(
TimeSeriesTable::open(location)
.await
.expect_err("unsupported protocol must fail"),
TableError::Open {
source: OpenTableError::Commit {
source: CommitError::Protocol {
source: TableProtocolError::UnsupportedVersion {
expected: TABLE_PROTOCOL_VERSION,
found: actual,
},
..
}
}
} if actual == u64::from(found)
));
}
let tmp = TempDir::new()?;
let location = TableLocation::local(tmp.path());
let mut meta = make_basic_table_meta();
meta.kind = TableKind::Generic;
TransactionLogStore::new(location.clone())
.commit_with_expected_version(0, vec![LogAction::UpdateTableMeta(meta)])
.await?;
assert!(matches!(
TimeSeriesTable::open(location)
.await
.expect_err("generic table must fail"),
TableError::Open {
source: OpenTableError::NotTimeSeries {
kind: TableKind::Generic
}
}
));
Ok(())
}
#[tokio::test]
async fn open_applies_reader_requirements_without_requiring_writer_support() -> TestResult {
let writer_tmp = TempDir::new()?;
let writer_location = TableLocation::local(writer_tmp.path());
let mut writer_meta = make_basic_table_meta();
writer_meta
.required_writer_features
.insert("future_writer".to_string());
TransactionLogStore::new(writer_location.clone())
.commit_with_expected_version(0, vec![LogAction::UpdateTableMeta(writer_meta)])
.await?;
let table = TimeSeriesTable::open(writer_location).await?;
assert_eq!(
table.state().table_meta.required_writer_features(),
&["future_writer".to_string()].into_iter().collect()
);
let reader_tmp = TempDir::new()?;
let reader_location = TableLocation::local(reader_tmp.path());
let mut reader_meta = make_basic_table_meta();
reader_meta
.required_reader_features
.insert("future_reader".to_string());
TransactionLogStore::new(reader_location.clone())
.commit_with_expected_version(0, vec![LogAction::UpdateTableMeta(reader_meta)])
.await?;
assert!(matches!(
TimeSeriesTable::open(reader_location)
.await
.expect_err("unknown reader feature must reject open"),
TableError::Open {
source: OpenTableError::Commit {
source: CommitError::Protocol {
source: TableProtocolError::UnsupportedReaderFeatures { features },
..
}
}
} if features == ["future_reader"]
));
Ok(())
}
}