aboutsummaryrefslogtreecommitdiffstats
path: root/log_db/src/common.rs
diff options
context:
space:
mode:
Diffstat (limited to 'log_db/src/common.rs')
-rw-r--r--log_db/src/common.rs436
1 files changed, 405 insertions, 31 deletions
diff --git a/log_db/src/common.rs b/log_db/src/common.rs
index 347b851..fb3124c 100644
--- a/log_db/src/common.rs
+++ b/log_db/src/common.rs
@@ -1,9 +1,12 @@
+use fs2::{lock_contended_error, FileExt};
+use once_cell::sync::Lazy;
use std::cmp::Ordering;
use std::collections::HashSet;
use std::fmt::Display;
-use std::fs::{metadata, File};
-use std::io::{self};
+use std::fs::{self, metadata, File};
+use std::io::{self, Read, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};
+use std::thread;
use uuid::Uuid;
// For Unix-like systems
@@ -138,6 +141,22 @@ impl Ord for LogKeySet {
}
}
+pub static APPEND_MODE: Lazy<fs::OpenOptions> = Lazy::new(|| {
+ let mut options = fs::OpenOptions::new();
+ options.read(true).append(true);
+ options
+});
+pub static READ_MODE: Lazy<fs::OpenOptions> = Lazy::new(|| {
+ let mut options = fs::OpenOptions::new();
+ options.read(true);
+ options
+});
+pub static WRITE_MODE: Lazy<fs::OpenOptions> = Lazy::new(|| {
+ let mut options = fs::OpenOptions::new();
+ options.read(true).write(true);
+ options
+});
+
pub struct MetadataHeader {
pub version: u8,
pub uuid: Uuid,
@@ -169,16 +188,11 @@ impl MetadataHeader {
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum WriteDurability {
- /// Changes are written to an application-level write buffer without flushing to the OS write buffer or syncing to disk.
- /// The buffered writer will batch writes to the OS buffer for maximum performance.
- /// Offers the lowest durability guarantees but is very fast.
- Async,
/// Changes are written to the OS write buffer but not immediately synced to disk.
- /// Offers better durability guarantees than Async but is slower.
/// This is generally recommended. Most OSes will sync the write buffer to disk within a few seconds.
Flush,
/// Changes are written to the OS write buffer and synced to disk immediately.
- /// Offers the best durability guarantees but is the slowest.
+ /// Offers the best durability guarantees but is a lot slower.
FlushSync,
}
@@ -246,7 +260,7 @@ impl RecordField {
}
#[derive(Debug, Clone)]
-pub enum RecordValue {
+pub enum Value {
Null,
Int(i64),
Float(f64),
@@ -254,30 +268,30 @@ pub enum RecordValue {
Bytes(Vec<u8>),
}
-impl RecordValue {
+impl Value {
pub fn serialize(&self) -> Vec<u8> {
match self {
- RecordValue::Null => {
+ Value::Null => {
vec![0] // Tag for Null
}
- RecordValue::Int(i) => {
+ Value::Int(i) => {
let mut bytes = vec![1]; // Tag for Int
bytes.extend(&i.to_be_bytes());
bytes
}
- RecordValue::Float(f) => {
+ Value::Float(f) => {
let mut bytes = vec![2]; // Tag for Float
bytes.extend(&f.to_be_bytes());
bytes
}
- RecordValue::String(s) => {
+ Value::String(s) => {
let mut bytes = vec![3]; // Tag for String
let length = s.len() as u64;
bytes.extend(&length.to_be_bytes());
bytes.extend(s.as_bytes());
bytes
}
- RecordValue::Bytes(b) => {
+ Value::Bytes(b) => {
let mut bytes = vec![4]; // Tag for Bytes
let length = b.len() as u64;
bytes.extend(&length.to_be_bytes());
@@ -287,26 +301,26 @@ impl RecordValue {
}
}
- /// Deserialize a RecordValue from a byte slice.
- /// Returns the deserialized RecordValue and the number of bytes consumed.
- pub fn deserialize(bytes: &[u8]) -> (RecordValue, usize) {
+ /// Deserialize a Value from a byte slice.
+ /// Returns the deserialized Value and the number of bytes consumed.
+ pub fn deserialize(bytes: &[u8]) -> (Value, usize) {
match bytes[0] {
- 0 => (RecordValue::Null, 1),
+ 0 => (Value::Null, 1),
1 => {
let mut int_bytes = [0; 8];
int_bytes.copy_from_slice(&bytes[1..1 + 8]);
- (RecordValue::Int(i64::from_be_bytes(int_bytes)), 1 + 8)
+ (Value::Int(i64::from_be_bytes(int_bytes)), 1 + 8)
}
2 => {
let mut float_bytes = [0; 8];
float_bytes.copy_from_slice(&bytes[1..1 + 8]);
- (RecordValue::Float(f64::from_be_bytes(float_bytes)), 1 + 8)
+ (Value::Float(f64::from_be_bytes(float_bytes)), 1 + 8)
}
3 => {
let length_bytes = &bytes[1..1 + 8];
let length = u64::from_be_bytes(length_bytes.try_into().unwrap()) as usize;
(
- RecordValue::String(
+ Value::String(
String::from_utf8(bytes[1 + 8..1 + 8 + length].to_vec()).unwrap(),
),
1 + 8 + length,
@@ -316,7 +330,7 @@ impl RecordValue {
let length_bytes = &bytes[1..1 + 8];
let length = u64::from_be_bytes(length_bytes.try_into().unwrap()) as usize;
(
- RecordValue::Bytes(bytes[1 + 8..1 + 8 + length].to_vec()),
+ Value::Bytes(bytes[1 + 8..1 + 8 + length].to_vec()),
1 + 8 + length,
)
}
@@ -326,22 +340,20 @@ impl RecordValue {
pub fn as_indexable(&self) -> Option<IndexableValue> {
match self {
- RecordValue::Int(i) => Some(IndexableValue::Int(*i)),
- RecordValue::String(s) => Some(IndexableValue::String(s.clone())),
+ Value::Int(i) => Some(IndexableValue::Int(*i)),
+ Value::String(s) => Some(IndexableValue::String(s.clone())),
_ => None,
}
}
}
#[derive(Debug, Clone)]
-pub struct Record {
- pub values: Vec<RecordValue>,
-}
+pub struct Record(Vec<Value>);
impl Record {
pub fn serialize(&self) -> Vec<u8> {
let mut bytes = Vec::new();
- for value in &self.values {
+ for value in &self.0 {
bytes.extend(value.serialize());
}
bytes
@@ -351,14 +363,91 @@ impl Record {
let mut values = Vec::new();
let mut start = 0;
while start < bytes.len() {
- let (rv, consumed) = RecordValue::deserialize(&bytes[start..]);
+ let (rv, consumed) = Value::deserialize(&bytes[start..]);
values.push(rv);
start += consumed;
}
- Record { values }
+ Record(values)
+ }
+
+ pub fn from(values: &[Value]) -> Record {
+ Record(values.to_vec())
+ }
+
+ pub fn values(&self) -> &[Value] {
+ &self.0
+ }
+
+ pub fn at(&self, index: usize) -> &Value {
+ &self.0[index]
+ }
+
+ pub fn validate<Field: Eq>(&self, schema: &Vec<(Field, RecordField)>) -> Result<(), io::Error> {
+ // Validate the record length
+ if self.0.len() != schema.len() {
+ return Err(io::Error::new(
+ io::ErrorKind::InvalidInput,
+ format!(
+ "Record has an incorrect number of fields: {}, expected {}",
+ self.0.len(),
+ schema.len()
+ ),
+ ));
+ }
+
+ // Validate that record fields match schema types
+ for (i, (_, field)) in schema.iter().enumerate() {
+ match (&self.0[i], field) {
+ (
+ Value::Null,
+ RecordField {
+ nullable: true,
+ field_type: _,
+ },
+ ) => {}
+ (
+ Value::Int(_),
+ RecordField {
+ field_type: RecordFieldType::Int,
+ ..
+ },
+ ) => {}
+ (
+ Value::String(_),
+ RecordField {
+ field_type: RecordFieldType::String,
+ ..
+ },
+ ) => {}
+ (
+ Value::Bytes(_),
+ RecordField {
+ field_type: RecordFieldType::Bytes,
+ ..
+ },
+ ) => {}
+ _ => {
+ return Err(io::Error::new(
+ io::ErrorKind::InvalidInput,
+ format!(
+ "Record field {} has incorrect type: {:?}, expected {:?}",
+ &i, &self.0[i], &field.field_type
+ ),
+ ))
+ }
+ }
+ }
+ Ok(())
}
}
+pub fn get_secondary_memtable_index_by_field<Field: Eq>(
+ sks: &Vec<Field>,
+ field: &Field,
+) -> Option<usize> {
+ sks.iter().position(|schema_field| schema_field == field)
+}
+
/// A path to a log segment file along with its type
pub enum SegmentPath {
/// A symbolic link to the active log file
@@ -401,3 +490,288 @@ pub fn symlink(original: &Path, link: &Path) -> io::Result<()> {
std::os::windows::fs::symlink_file(original, link)
}
}
+
+/// Set the active segment to the segment with the given ordinal number.
+pub fn set_active_segment(data_dir_path: &Path, segment_num: u16) -> Result<(), io::Error> {
+ let tmp_uuid = Uuid::new_v4();
+ let tmp_filename = format!("active_{}", tmp_uuid.to_string());
+ let tmp_path = data_dir_path.join(tmp_filename);
+
+ let metadata_filename = format!("metadata.{}", segment_num);
+ let metadata_path = Path::new(&metadata_filename);
+ let active_symlink = data_dir_path.join(ACTIVE_SYMLINK_FILENAME);
+
+ symlink(&metadata_path, &tmp_path)?;
+ fs::rename(&tmp_path, &active_symlink)?;
+
+ Ok(())
+}
+
+/// Create a new segment metadata file and return its number and path.
+/// A metadata file contains the segment metadata, including the UUID of the data file.
+/// See `ARCHITECTURE.md` for the file format.
+pub fn create_segment_metadata_file(
+ data_dir_path: &Path,
+ data_file_uuid: &Uuid,
+) -> Result<(u16, PathBuf), io::Error> {
+ let current_greatest_num = greatest_segment_number(data_dir_path)?;
+ let new_num = current_greatest_num + 1;
+
+ let metadata_filename = format!("metadata.{}", new_num);
+ let metadata_path = data_dir_path.join(metadata_filename);
+
+ let mut metadata_file = fs::OpenOptions::new()
+ .create(true)
+ .write(true)
+ .append(true)
+ .open(&metadata_path)?;
+
+ let metadata_header = MetadataHeader {
+ version: 1,
+ uuid: *data_file_uuid,
+ };
+
+ metadata_file.write_all(&metadata_header.serialize())?;
+ metadata_file.flush()?;
+
+ let len = metadata_file.seek(io::SeekFrom::End(0))?;
+ assert!(len >= METADATA_FILE_HEADER_SIZE as u64);
+ assert_eq!((len - METADATA_FILE_HEADER_SIZE as u64) % 16, 0);
+
+ Ok((new_num, metadata_path))
+}
+
+/// Get the number of the segment with the greatest ordinal.
+/// This is the newest segment, i.e. the one that is pointed to by the `active` symlink.
+/// If there are no segments yet, returns 0.
+pub fn greatest_segment_number(data_dir_path: &Path) -> Result<u16, io::Error> {
+ let active_symlink = data_dir_path.join(ACTIVE_SYMLINK_FILENAME);
+
+ if !fs::exists(&active_symlink)? {
+ return Ok(0);
+ }
+
+ let segment_metadata_path = fs::read_link(&active_symlink)?;
+ let filename = segment_metadata_path
+ .file_name()
+ .expect("No filename in symlink")
+ .to_str()
+ .expect("Filename was not valid UTF-8");
+
+ // parse number from format "metadata.1"
+ let segment_number = filename
+ .split('.')
+ .last()
+ .expect("Filename did not have a number")
+ .parse::<u16>();
+
+ segment_number.map_err(|_| {
+ io::Error::new(
+ io::ErrorKind::InvalidData,
+ "Failed to parse segment number from filename",
+ )
+ })
+}
+
+/// Create a new segment data file and return its UUID.
+/// A data file contains the segment data, tightly packed without separators.
+/// An accompanying metadata file is required to interpret the data.
+pub fn create_segment_data_file(data_dir_path: &Path) -> Result<(Uuid, PathBuf), io::Error> {
+ let uuid = Uuid::new_v4();
+ let new_segment_path = data_dir_path.join(uuid.to_string());
+ fs::OpenOptions::new()
+ .create(true)
+ .write(true)
+ .append(true)
+ .open(&new_segment_path)?;
+
+ Ok((uuid, new_segment_path))
+}
+
+/// Reads the metadata header from the metadata file.
+/// Leaves the file seek head at the beginning of the records, after the header.
+pub fn read_metadata_header(metadata_file: &mut fs::File) -> Result<MetadataHeader, io::Error> {
+ metadata_file.seek(SeekFrom::Start(0))?;
+ let mut buf = [0u8; METADATA_FILE_HEADER_SIZE];
+ metadata_file.read_exact(&mut buf)?;
+
+ let header = MetadataHeader::deserialize(&buf);
+ Ok(header)
+}
+
+pub fn validate_metadata_header(header: &MetadataHeader) -> Result<(), io::Error> {
+ if header.version != 1 {
+ return Err(io::Error::new(
+ io::ErrorKind::InvalidData,
+ "Unsupported metadata file version",
+ ));
+ }
+
+ Ok(())
+}
+
+pub enum IsMetadatafileValidResult {
+ Ok,
+ ReplaceFile,
+ TruncateToSize(u64),
+}
+
+pub fn is_metadata_file_valid(
+ metadata_file: &mut fs::File,
+) -> Result<IsMetadatafileValidResult, io::Error> {
+ let size = metadata_file.seek(SeekFrom::End(0))? as usize;
+
+ if size < METADATA_FILE_HEADER_SIZE {
+ return Ok(IsMetadatafileValidResult::ReplaceFile);
+ }
+
+ // The data section must be a multiple of 16 bytes.
+ // Otherwise, the non-aligned part of the file is dropped.
+ let data_section_len = size - METADATA_FILE_HEADER_SIZE;
+ let remainder = data_section_len % 16;
+ if remainder != 0 {
+ return Ok(IsMetadatafileValidResult::TruncateToSize(
+ (size - remainder) as u64,
+ ));
+ }
+
+ Ok(IsMetadatafileValidResult::Ok)
+}
+
+/// Check that the active metadata file is well-formed and repair it if necessary.
+/// The metadata file is considered well-formed if its size is, in pseudocode, `header_size + n * record_size`.
+/// If the file is not well-formed, it is truncated to the last well-formed record using
+/// a temporary file and an atomic move operation.
+///
+/// `self.active_metadata_file` must be a locked file handle opened with read permissions.
+/// The function leaves the seek head in an unspecified position.
+///
+/// Returns `false` if the file was repaired and rotated, `true` if no action was taken.
+pub fn ensure_active_metadata_is_valid(
+ data_dir: &Path,
+ metadata_file: &mut fs::File,
+) -> Result<bool, io::Error> {
+ let current_len = metadata_file.seek(SeekFrom::End(0))? as usize;
+
+ match is_metadata_file_valid(metadata_file)? {
+ IsMetadatafileValidResult::Ok => return Ok(true),
+ IsMetadatafileValidResult::ReplaceFile => {
+ let active_target = fs::read_link(data_dir.join(ACTIVE_SYMLINK_FILENAME))?;
+ let active_path = data_dir.join(&active_target);
+ warn!(
+ "Metadata file \"{}\" is malformed ({} bytes), replacing it with an empty file",
+ active_target.display(),
+ current_len,
+ );
+ let mut tmp_file = tempfile::NamedTempFile::new()?;
+
+ let header = MetadataHeader {
+ version: 1,
+ uuid: Uuid::new_v4(),
+ };
+
+ tmp_file.write_all(&header.serialize())?;
+ tmp_file.flush()?;
+
+ fs::rename(tmp_file.path(), active_path)?;
+
+ debug!("Replaced metadata file");
+ return Ok(false);
+ }
+ IsMetadatafileValidResult::TruncateToSize(new_size) => {
+ let active_target = fs::read_link(data_dir.join(ACTIVE_SYMLINK_FILENAME))?;
+ let active_path = data_dir.join(&active_target);
+ warn!(
+ "Metadata file \"{}\" is malformed ({} bytes), truncating it to {} bytes",
+ active_target.display(),
+ current_len,
+ new_size
+ );
+
+ let mut tmp_file = tempfile::NamedTempFile::new()?;
+
+ let mut buf = vec![0; new_size as usize];
+ metadata_file.seek(SeekFrom::Start(0))?;
+ metadata_file.read_exact(&mut buf)?;
+
+ tmp_file.write_all(&buf)?;
+ tmp_file.flush()?;
+
+ fs::rename(tmp_file.path(), active_path)?;
+
+ debug!("Truncated metadata file");
+ return Ok(false);
+ }
+ }
+}
+
+pub fn is_exclusive_lock_requested(data_dir: &Path) -> Result<bool, io::Error> {
+ let lock_request_path = data_dir.join(EXCL_LOCK_REQUEST_FILENAME);
+ let lock_request_file = fs::OpenOptions::new()
+ .create(true)
+ .write(true) // When requesting a lock, we need to have either read or write permissions
+ .open(&lock_request_path)?;
+
+ // Attempt to acquire a shared lock on the lock request file
+ // If the file is already locked, return false
+ match lock_request_file.try_lock_shared() {
+ Err(e) => {
+ if e.kind() == lock_contended_error().kind() {
+ return Ok(true);
+ }
+ return Err(e);
+ }
+ Ok(_) => {
+ // Check that the exclusive lock request file is still the same as the one we opened
+ if !is_file_same_as_path(&lock_request_file, &lock_request_path)? {
+ // The lock request file has been removed
+ return Err(io::Error::new(
+ io::ErrorKind::Other,
+ "Lock request file was removed unexpectedly",
+ ));
+ }
+
+ lock_request_file.unlock()?;
+ return Ok(false);
+ }
+ }
+}
+
+pub fn request_shared_lock(data_dir: &Path, file: &mut fs::File) -> Result<(), io::Error> {
+ const SHARED_LOCK_WAIT_MAX_MS: u64 = 100;
+ let mut timeout = 5;
+ loop {
+ if is_exclusive_lock_requested(data_dir)? {
+ debug!(
+ "Exclusive lock requested, waiting for {}ms before requesting a shared lock again",
+ timeout
+ );
+ thread::sleep(std::time::Duration::from_millis(timeout));
+ timeout = std::cmp::min(timeout * 2, SHARED_LOCK_WAIT_MAX_MS);
+ } else {
+ file.lock_shared()?;
+ return Ok(());
+ }
+ }
+}
+
+pub fn request_exclusive_lock(data_dir: &Path, file: &mut fs::File) -> Result<(), io::Error> {
+ // Create a lock on the exclusive lock request file to signal to readers that they should wait
+ let lock_request_path = data_dir.join(EXCL_LOCK_REQUEST_FILENAME);
+ let lock_request_file = fs::OpenOptions::new()
+ .create(true)
+ .write(true) // When requesting a lock, we need to have either read or write permissions
+ .open(&lock_request_path)?;
+
+ // Attempt to acquire an exclusive lock on the lock request file
+ // This will block until the lock is acquired
+ lock_request_file.lock_exclusive()?;
+
+ // Acquire an exclusive lock on the segment files
+ file.lock_exclusive()?;
+
+ // Unlock the request file
+ lock_request_file.unlock()?;
+
+ Ok(())
+}