diff options
| author | Jan Tuomi <jan@jantuomi.fi> | 2024-11-09 10:22:31 +0200 |
|---|---|---|
| committer | Jan Tuomi <jan@jantuomi.fi> | 2024-11-09 18:57:45 +0200 |
| commit | 2975980717626b12eb959c1ecd93038b5be1d7c6 (patch) | |
| tree | 5722d65b7bcb3efc2caf8a5f4889048d10d2c96a /log_db/src | |
| parent | 3087e26f47eb9c5b83d5aec61ca9248856075d8b (diff) | |
Refactor static DB methods to common module
Diffstat (limited to 'log_db/src')
| -rw-r--r-- | log_db/src/common.rs | 436 | ||||
| -rw-r--r-- | log_db/src/lib.rs | 586 | ||||
| -rw-r--r-- | log_db/src/log_reader_forward.rs | 4 | ||||
| -rw-r--r-- | log_db/src/log_reader_reverse.rs | 4 |
4 files changed, 518 insertions, 512 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(()) +} diff --git a/log_db/src/lib.rs b/log_db/src/lib.rs index 1082467..68db7fd 100644 --- a/log_db/src/lib.rs +++ b/log_db/src/lib.rs @@ -8,7 +8,6 @@ mod memtable_primary; mod memtable_secondary; pub use common::*; -use fs2::lock_contended_error; use fs2::FileExt; pub use log_reader_forward::ForwardLogReader; pub use log_reader_reverse::ReverseLogReader; @@ -19,10 +18,9 @@ use std::fmt::Debug; use std::fs::{self}; use std::io::Seek; use std::io::SeekFrom; -use std::io::{self, Read, Write}; +use std::io::{self, Write}; use std::os::unix::fs::MetadataExt; use std::path::{Path, PathBuf}; -use std::thread; use uuid::Uuid; pub struct ConfigBuilder<Field: Eq + Clone + Debug> { @@ -71,8 +69,8 @@ impl<'a, Field: Eq + Clone + Debug> ConfigBuilder<Field> { } /// The field schema of the database. - pub fn fields(&mut self, fields: Vec<(Field, RecordField)>) -> &mut Self { - self.fields = Some(fields.clone()); + pub fn fields(&mut self, fields: &[(Field, RecordField)]) -> &mut Self { + self.fields = Some(fields.to_vec()); self } @@ -148,12 +146,6 @@ pub struct DB<Field: Eq + Clone + Debug> { secondary_memtables: Vec<SecondaryMemtable>, } -enum IsActiveMetadataValidResult { - Ok, - ReplaceFile, - TruncateToSize(u64), -} - impl<Field: Eq + Clone + Debug> DB<Field> { /// Create a new database configuration builder. pub fn configure() -> ConfigBuilder<Field> { @@ -189,10 +181,9 @@ impl<Field: Eq + Clone + Debug> DB<Field> { // We have acquired the lock, check if the data directory is in a complete state // If not, initialize it, otherwise skip. if !fs::exists(data_dir_path.join(ACTIVE_SYMLINK_FILENAME))? { - let (segment_uuid, _) = DB::<Field>::create_segment_data_file(data_dir_path)?; - let (segment_num, _) = - DB::<Field>::create_segment_metadata_file(data_dir_path, &segment_uuid)?; - DB::<Field>::set_active_segment(data_dir_path, segment_num)?; + let (segment_uuid, _) = create_segment_data_file(data_dir_path)?; + let (segment_num, _) = create_segment_metadata_file(data_dir_path, &segment_uuid)?; + set_active_segment(data_dir_path, segment_num)?; // Create the exclusive lock request file fs::OpenOptions::new() @@ -250,26 +241,14 @@ impl<Field: Eq + Clone + Debug> DB<Field> { let active_target = fs::read_link(&active_symlink)?; let active_metadata_path = Path::new(&config.data_dir).join(active_target); - let mut active_metadata_file = fs::OpenOptions::new() - .read(true) - .append(true) - .open(&active_metadata_path)?; + let mut active_metadata_file = APPEND_MODE.open(&active_metadata_path)?; - let active_metadata_header = DB::<Field>::read_metadata_header(&mut active_metadata_file)?; - - if active_metadata_header.version != 1 { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "Unsupported segment version", - )); - } + let active_metadata_header = read_metadata_header(&mut active_metadata_file)?; + validate_metadata_header(&active_metadata_header)?; let active_data_path = Path::new(&config.data_dir).join(active_metadata_header.uuid.to_string()); - let active_data_file = fs::OpenOptions::new() - .read(true) - .append(true) - .open(&active_data_path)?; + let active_data_file = APPEND_MODE.open(&active_data_path)?; let db = DB::<Field> { config: config.clone(), @@ -293,74 +272,25 @@ impl<Field: Eq + Clone + Debug> DB<Field> { /// the existing record will be replaced by the supplied one. pub fn upsert(&mut self, record: &Record) -> Result<(), io::Error> { debug!("Upserting record: {:?}", record); - // Validate the record length - if record.values.len() != self.config.fields.len() { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - format!( - "Record has an incorrect number of fields: {}, expected {}", - record.values.len(), - self.config.fields.len() - ), - )); - } - // Validate that record fields match schema types - for (i, (_, field)) in self.config.fields.iter().enumerate() { - match (&record.values[i], field) { - ( - RecordValue::Null, - RecordField { - nullable: true, - field_type: _, - }, - ) => {} - ( - RecordValue::Int(_), - RecordField { - field_type: RecordFieldType::Int, - .. - }, - ) => {} - ( - RecordValue::String(_), - RecordField { - field_type: RecordFieldType::String, - .. - }, - ) => {} - ( - RecordValue::Bytes(_), - RecordField { - field_type: RecordFieldType::Bytes, - .. - }, - ) => {} - _ => { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - format!( - "Record field {} has incorrect type: {:?}, expected {:?}", - &i, &record.values[i], &field.field_type - ), - )) - } - } - } + record.validate(&self.config.fields)?; debug!("Record is valid"); debug!("Opening file in append mode and acquiring exclusive lock..."); // Acquire an exclusive lock for writing - self.request_exclusive_lock_on_active()?; + request_exclusive_lock(&self.data_dir, &mut self.active_metadata_file)?; - if !self.ensure_active_file_is_open()? || !self.ensure_active_metadata_is_valid()? { + if !self.ensure_metadata_file_is_active()? + || !ensure_active_metadata_is_valid(&self.data_dir, &mut self.active_metadata_file)? + { // The log file has been rotated, so we must try again self.active_metadata_file.unlock()?; - self.active_data_file.unlock()?; return self.upsert(record); } + self.active_data_file.lock_exclusive()?; + debug!("Exclusive lock acquired, appending to log file"); // Write the record to the log @@ -395,6 +325,7 @@ impl<Field: Eq + Clone + Debug> DB<Field> { self.active_metadata_file.sync_all()?; } + // Manually release the locks because the file handles are left open self.active_data_file.unlock()?; self.active_metadata_file.unlock()?; @@ -403,15 +334,17 @@ impl<Field: Eq + Clone + Debug> DB<Field> { let len = self.active_metadata_file.seek(SeekFrom::End(0))?; assert!(len >= METADATA_FILE_HEADER_SIZE as u64); assert_eq!((len - METADATA_FILE_HEADER_SIZE as u64) % 16, 0); - let data_file_len = self.active_data_file.seek(SeekFrom::End(0))?; - assert_eq!(data_file_len, record_offset + record_length); + + // Depending on write durability, the data might not be written to disk yet + //let data_file_len = self.active_data_file.seek(SeekFrom::End(0))?; + //assert_eq!(data_file_len, record_offset + record_length); Ok(()) } /// Get a record by its primary index value. - /// E.g. `db.get(RecordValue::Int(10))`. - pub fn get(&mut self, query_key: &RecordValue) -> Result<Option<Record>, io::Error> { + /// E.g. `db.get(Value::Int(10))`. + pub fn get(&mut self, query_key: &Value) -> Result<Option<Record>, io::Error> { let query_key_original = query_key; debug!( "Getting record with field {:?} = {:?}", @@ -422,16 +355,19 @@ impl<Field: Eq + Clone + Debug> DB<Field> { "Queried value must be indexable", ))?; - match self.is_active_metadata_valid()? { - IsActiveMetadataValidResult::Ok => {} + match is_metadata_file_valid(&mut self.active_metadata_file)? { + IsMetadatafileValidResult::Ok => {} _ => { debug!("Active metadata file is invalid, acquiring exclusive lock to start autorepair..."); - self.request_exclusive_lock_on_active()?; - - self.ensure_active_metadata_is_valid()?; + request_exclusive_lock(&self.data_dir, &mut self.active_metadata_file)?; + ensure_active_metadata_is_valid(&self.data_dir, &mut self.active_metadata_file)?; + self.ensure_metadata_file_is_active()?; + // The lock should be dropped by RAII, but just in case self.active_metadata_file.unlock()?; - self.active_data_file.unlock()?; + + debug!("Active metadata file is now valid, retrying get operation..."); + return self.get(query_key_original); } }; @@ -452,7 +388,7 @@ impl<Field: Eq + Clone + Debug> DB<Field> { &self.primary_key_index ); - let greatest = DB::<Field>::greatest_segment_number(&self.data_dir)?; + let greatest = greatest_segment_number(&self.data_dir)?; debug!("Searching segments {} through 1", greatest); let mut found_record: Option<Record> = None; @@ -464,21 +400,16 @@ impl<Field: Eq + Clone + Debug> DB<Field> { segment_num ); - let mut metadata_file = fs::OpenOptions::new().read(true).open(&segment_path)?; + let mut metadata_file = READ_MODE.open(&segment_path)?; - self.request_shared_lock(&mut metadata_file)?; + request_shared_lock(&self.data_dir, &mut metadata_file)?; - let metadata_header = DB::<Field>::read_metadata_header(&mut metadata_file)?; + let metadata_header = read_metadata_header(&mut metadata_file)?; - if metadata_header.version != 1 { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "Unsupported segment version", - )); - } + validate_metadata_header(&metadata_header)?; let data_path = &self.data_dir.join(metadata_header.uuid.to_string()); - let data_file = fs::OpenOptions::new().read(true).open(&data_path)?; + let data_file = READ_MODE.open(&data_path)?; // We should not "request_shared_lock()" here because we do not want // to give way to writers at this point. That would possibly lead to a deadlock. @@ -487,7 +418,8 @@ impl<Field: Eq + Clone + Debug> DB<Field> { let mut reader = ReverseLogReader::new(metadata_file, data_file)?; if let Some(found) = reader.find(|record| { - let record_key = record.values[self.primary_key_index] + let record_key = record + .at(self.primary_key_index) .as_indexable() .expect("Primary key must be indexable"); record_key == query_key @@ -505,11 +437,7 @@ impl<Field: Eq + Clone + Debug> DB<Field> { /// Get a collection of records based on a field value. /// Indexes will be used if they contain the requested key. - pub fn find_all( - &mut self, - field: &Field, - query_key: &RecordValue, - ) -> Result<Vec<Record>, io::Error> { + pub fn find_all(&mut self, field: &Field, query_key: &Value) -> Result<Vec<Record>, io::Error> { // If querying by primary key, return the result of `get` wrapped in a vec. if field == &self.config.primary_key { return match self.get(query_key)? { @@ -529,21 +457,25 @@ impl<Field: Eq + Clone + Debug> DB<Field> { "Queried value must be indexable", ))?; - match self.is_active_metadata_valid()? { - IsActiveMetadataValidResult::Ok => {} + match is_metadata_file_valid(&mut self.active_metadata_file)? { + IsMetadatafileValidResult::Ok => {} _ => { debug!("Active metadata file is invalid, acquiring exclusive lock to start autorepair..."); - self.request_exclusive_lock_on_active()?; - - self.ensure_active_metadata_is_valid()?; + request_exclusive_lock(&self.data_dir, &mut self.active_metadata_file)?; + ensure_active_metadata_is_valid(&self.data_dir, &mut self.active_metadata_file)?; + self.ensure_metadata_file_is_active()?; + // The lock should be dropped by RAII, but just in case self.active_metadata_file.unlock()?; - self.active_data_file.unlock()?; + + debug!("Active metadata file is now valid, retrying find_all operation..."); + return self.find_all(field, query_key_original); } }; // Try to find a memtable with the queried key - let found_memtable_index = self.get_secondary_memtable_index_by_field(field); + let found_memtable_index = + get_secondary_memtable_index_by_field(&self.config.secondary_keys, field); if let Some(memtable_index) = found_memtable_index { debug!( @@ -572,7 +504,7 @@ impl<Field: Eq + Clone + Debug> DB<Field> { "Key not found in schema after initialize", ))?; - let greatest = DB::<Field>::greatest_segment_number(&self.data_dir)?; + let greatest = greatest_segment_number(&self.data_dir)?; let mut found_records = vec![]; for segment_num in (1..=greatest).rev() { @@ -583,11 +515,11 @@ impl<Field: Eq + Clone + Debug> DB<Field> { segment_num ); - let mut metadata_file = fs::OpenOptions::new().read(true).open(&segment_path)?; + let mut metadata_file = READ_MODE.open(&segment_path)?; - self.request_shared_lock(&mut metadata_file)?; + request_shared_lock(&self.data_dir, &mut metadata_file)?; - let metadata_header = DB::<Field>::read_metadata_header(&mut metadata_file)?; + let metadata_header = read_metadata_header(&mut metadata_file)?; if metadata_header.version != 1 { return Err(io::Error::new( @@ -597,7 +529,7 @@ impl<Field: Eq + Clone + Debug> DB<Field> { } let data_path = &self.data_dir.join(metadata_header.uuid.to_string()); - let data_file = fs::OpenOptions::new().read(true).open(&data_path)?; + let data_file = READ_MODE.open(&data_path)?; // We should not "request_shared_lock()" here because we do not want // to give way to writers at this point. That would possibly lead to a deadlock. @@ -606,7 +538,8 @@ impl<Field: Eq + Clone + Debug> DB<Field> { let reader = ReverseLogReader::new(metadata_file, data_file)?; for record in reader { - let record_key = record.values[key_index] + let record_key = record + .at(key_index) .as_indexable() .expect("Secondary key must be indexable"); if record_key == query_key { @@ -627,7 +560,7 @@ impl<Field: Eq + Clone + Debug> DB<Field> { let primary_values: Vec<IndexableValue> = found_records .iter() .map(|r| { - r.values[self.primary_key_index] + r.at(self.primary_key_index) .as_indexable() .expect("A non-indexable value was stored at primary key index") }) @@ -638,44 +571,28 @@ impl<Field: Eq + Clone + Debug> DB<Field> { Ok(found_records) } - fn get_secondary_memtable_index_by_field(&self, field: &Field) -> Option<usize> { - self.config - .secondary_keys - .iter() - .position(|schema_field| schema_field == field) - } - /// Ensures that the `self.metadata_file` and `self.data_file` handles are still pointing to the correct files. /// If the segment has been rotated, the handle will be closed and reopened. /// Returns `false` if the file has been rotated and the handle has been reopened, `true` otherwise. - fn ensure_active_file_is_open(&mut self) -> Result<bool, io::Error> { - let active_target = fs::read_link(&self.data_dir.join("active"))?; + fn ensure_metadata_file_is_active(&mut self) -> Result<bool, io::Error> { + let active_target = fs::read_link(&self.data_dir.join(ACTIVE_SYMLINK_FILENAME))?; let active_metadata_path = &self.data_dir.join(active_target); let correct = is_file_same_as_path(&self.active_metadata_file, &active_metadata_path)?; if !correct { debug!("Metadata file has been rotated. Reopening..."); - let mut metadata_file = fs::OpenOptions::new() - .read(true) - .write(true) - .open(&active_metadata_path)?; + let mut metadata_file = APPEND_MODE.open(&active_metadata_path)?; - self.request_shared_lock(&mut metadata_file)?; + request_shared_lock(&self.data_dir, &mut metadata_file)?; - let metadata_header = - DB::<Field>::read_metadata_header(&mut self.active_metadata_file)?; + let metadata_header = read_metadata_header(&mut self.active_metadata_file)?; - if metadata_header.version != 1 { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "Unsupported segment version", - )); - } + validate_metadata_header(&metadata_header)?; let data_file_path = &self.data_dir.join(metadata_header.uuid.to_string()); self.active_metadata_file = metadata_file; - self.active_data_file = fs::OpenOptions::new().append(true).open(&data_file_path)?; + self.active_data_file = APPEND_MODE.open(&data_file_path)?; return Ok(false); } else { @@ -683,85 +600,6 @@ impl<Field: Eq + Clone + Debug> DB<Field> { } } - fn request_exclusive_lock_on_active(&mut self) -> Result<(), io::Error> { - // Create a lock on the exclusive lock request file to signal to readers that they should wait - let lock_request_path = &self.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()?; - - // Check that the exclusive lock request file is still the same as the one we opened - // NOTE: this isn't strictly necessary, but it's a good sanity check. Disabled for now. - // 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", - // )); - // } - - // Acquire an exclusive lock on the segment files - self.active_metadata_file.lock_exclusive()?; - self.active_data_file.lock_exclusive()?; - - // Unlock the request file - lock_request_file.unlock()?; - - Ok(()) - } - - fn is_exclusive_lock_requested(&self) -> Result<bool, io::Error> { - let lock_request_path = &self.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); - } - } - } - - fn request_shared_lock(&self, file: &mut fs::File) -> Result<(), io::Error> { - const SHARED_LOCK_WAIT_MAX_MS: u64 = 100; - let mut timeout = 5; - loop { - if self.is_exclusive_lock_requested()? { - 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(()); - } - } - } - /// Check if there are any pending tasks and do them. Tasks include: /// - Rotating the active log file if it has reached capacity and compacting it. /// @@ -776,14 +614,14 @@ impl<Field: Eq + Clone + Debug> DB<Field> { let active_log_md = fs::metadata(&active_log_path)?; let mut already_locked = false; - match self.is_active_metadata_valid()? { - IsActiveMetadataValidResult::Ok => {} + match is_metadata_file_valid(&mut self.active_metadata_file)? { + IsMetadatafileValidResult::Ok => {} _ => { debug!("Active metadata is invalid, acquiring exclusive lock..."); - self.request_exclusive_lock_on_active()?; + request_exclusive_lock(&self.data_dir, &mut self.active_metadata_file)?; already_locked = true; - self.ensure_active_metadata_is_valid()?; + ensure_active_metadata_is_valid(&self.data_dir, &mut self.active_metadata_file)?; } }; @@ -793,29 +631,23 @@ impl<Field: Eq + Clone + Debug> DB<Field> { debug!("Starting rotation"); if !already_locked { debug!("Requesting exclusive lock on active log file..."); - self.request_exclusive_lock_on_active()?; + request_exclusive_lock(&self.data_dir, &mut self.active_metadata_file)?; } debug!("Exclusive lock acquired, rotating active log file..."); // Create a new active log segment - let (data_file_uuid, _) = DB::<Field>::create_segment_data_file(&self.data_dir)?; - let (new_segment_num, _) = - DB::<Field>::create_segment_metadata_file(&self.data_dir, &data_file_uuid)?; - DB::<Field>::set_active_segment(&self.data_dir, new_segment_num)?; + let (data_uuid, _) = create_segment_data_file(&self.data_dir)?; + let (new_segment_num, _) = create_segment_metadata_file(&self.data_dir, &data_uuid)?; + set_active_segment(&self.data_dir, new_segment_num)?; // The new active log file is not locked by this client so it cannot be touched. debug!("Active log file rotated, new segment: {}", new_segment_num); - self.active_metadata_file = fs::OpenOptions::new() - .read(true) - .append(true) - .open(&self.data_dir.join(format!("metadata.{}", new_segment_num)))?; - - self.active_data_file = fs::OpenOptions::new() - .read(true) - .append(true) - .open(&self.data_dir.join(data_file_uuid.to_string()))?; + let new_metadata_path = &self.data_dir.join(format!("metadata.{}", new_segment_num)); + self.active_metadata_file = APPEND_MODE.open(&new_metadata_path)?; + let new_data_path = &self.data_dir.join(data_uuid.to_string()); + self.active_data_file = APPEND_MODE.open(&new_data_path)?; // Compact the rotated segment without a lock. // Since the rotated segment and the compacted segment based on it will be @@ -835,24 +667,19 @@ impl<Field: Eq + Clone + Debug> DB<Field> { fn compact_segment(&self, metadata_path: &Path) -> Result<(), io::Error> { debug!("Opening segment file {:?} for compaction", metadata_path); - let mut metadata_file = fs::OpenOptions::new().read(true).open(metadata_path)?; - let metadata_header = DB::<Field>::read_metadata_header(&mut metadata_file)?; - - if metadata_header.version != 1 { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "Unsupported segment version", - )); - } + let mut metadata_file = READ_MODE.open(metadata_path)?; + let metadata_header = read_metadata_header(&mut metadata_file)?; + validate_metadata_header(&metadata_header)?; let data_file_path = &self.data_dir.join(metadata_header.uuid.to_string()); - let data_file = fs::OpenOptions::new().read(true).open(&data_file_path)?; + let data_file = READ_MODE.open(&data_file_path)?; debug!("Reading segment data into a BTreeMap"); let mut map = BTreeMap::new(); let forward_log_reader = ForwardLogReader::new(metadata_file, data_file); for entry in forward_log_reader { - let primary_key = entry.values[self.primary_key_index] + let primary_key = entry + .at(self.primary_key_index) .as_indexable() .expect("Primary key was not indexable"); map.insert(primary_key, entry); @@ -861,17 +688,11 @@ impl<Field: Eq + Clone + Debug> DB<Field> { debug!("Opening temporary files for writing compacted data"); let temp_data_file = tempfile::NamedTempFile::new()?; let temp_data_path = temp_data_file.as_ref(); - let mut temp_data_file = fs::OpenOptions::new() - .create(true) - .append(true) - .open(temp_data_path)?; + let mut temp_data_file = APPEND_MODE.open(temp_data_path)?; let temp_metadata_file = tempfile::NamedTempFile::new()?; let temp_metadata_path = temp_metadata_file.as_ref(); - let mut temp_metadata_file = fs::OpenOptions::new() - .create(true) - .append(true) - .open(temp_metadata_path)?; + let mut temp_metadata_file = APPEND_MODE.open(temp_metadata_path)?; let new_data_uuid = Uuid::new_v4(); @@ -897,6 +718,12 @@ impl<Field: Eq + Clone + Debug> DB<Field> { offset += len; } + // Sync the temporary files to disk + // This is fine to do without consulting WriteDurability because this is a one-off + // operation that is not part of the normal write path. + temp_metadata_file.flush()?; + temp_data_file.flush()?; + let final_len = temp_metadata_file.seek(io::SeekFrom::End(0))?; debug!("Moving temporary files to their final locations"); @@ -907,193 +734,6 @@ impl<Field: Eq + Clone + Debug> DB<Field> { debug!("Compaction complete, resulting size: {}", final_len); Ok(()) } - - /// 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. - 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. - 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)) - } - - /// 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. - fn create_segment_metadata_file( - data_dir_path: &Path, - data_file_uuid: &Uuid, - ) -> Result<(u16, PathBuf), io::Error> { - let current_greatest_num = DB::<Field>::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())?; - - 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)) - } - - /// Set the active segment to the segment with the given ordinal number. - 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(()) - } - - /// Reads the metadata header from the metadata file. - /// Leaves the file seek head at the beginning of the records, after the header. - 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) - } - - fn is_active_metadata_valid(&mut self) -> Result<IsActiveMetadataValidResult, io::Error> { - let size = self.active_metadata_file.seek(SeekFrom::End(0))? as usize; - - if size < METADATA_FILE_HEADER_SIZE { - return Ok(IsActiveMetadataValidResult::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(IsActiveMetadataValidResult::TruncateToSize( - (size - remainder) as u64, - )); - } - - Ok(IsActiveMetadataValidResult::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. - fn ensure_active_metadata_is_valid(&mut self) -> Result<bool, io::Error> { - let current_len = self.active_metadata_file.seek(SeekFrom::End(0))? as usize; - - match self.is_active_metadata_valid()? { - IsActiveMetadataValidResult::Ok => return Ok(true), - IsActiveMetadataValidResult::ReplaceFile => { - let active_target = fs::read_link(&self.data_dir.join(ACTIVE_SYMLINK_FILENAME))?; - let active_path = &self.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())?; - fs::rename(tmp_file.path(), active_path)?; - - debug!("Replaced metadata file"); - return Ok(false); - } - IsActiveMetadataValidResult::TruncateToSize(new_size) => { - let active_target = fs::read_link(&self.data_dir.join(ACTIVE_SYMLINK_FILENAME))?; - let active_path = &self.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]; - self.active_metadata_file.seek(SeekFrom::Start(0))?; - self.active_metadata_file.read_exact(&mut buf)?; - - tmp_file.write_all(&buf)?; - fs::rename(tmp_file.path(), active_path)?; - - debug!("Truncated metadata file"); - return Ok(false); - } - } - } } #[cfg(test)] @@ -1116,16 +756,14 @@ mod tests { let mut db = DB::configure() .data_dir(data_dir.to_str().unwrap()) .segment_size(segment_size) - .fields(vec![(Field::Id, RecordField::int())]) + .fields(&[(Field::Id, RecordField::int())]) .primary_key(Field::Id) .initialize() .expect("Failed to create DB"); // Insert records with same value until we reach the capacity for _ in 0..capacity { - let record = Record { - values: vec![RecordValue::Int(0 as i64)], - }; + let record = Record::from(&[Value::Int(0 as i64)]); db.upsert(&record).expect("Failed to insert record"); } @@ -1134,9 +772,7 @@ mod tests { .expect("Failed to do maintenance tasks"); // Insert one extra with different value, this goes into another segment - let record = Record { - values: vec![RecordValue::Int(1 as i64)], - }; + let record = Record::from(&[Value::Int(1 as i64)]); db.upsert(&record).expect("Failed to insert record"); // Check that rotation resulted in 2 segments @@ -1151,22 +787,22 @@ mod tests { // Check that the records can be read let rec0 = db - .get(&RecordValue::Int(0 as i64)) + .get(&Value::Int(0 as i64)) .expect("Failed to get record") .expect("Record not found"); - assert!(match rec0.values[0] { - RecordValue::Int(0) => true, + assert!(match rec0.at(0) { + Value::Int(0) => true, _ => false, }); let rec1 = db - .get(&RecordValue::Int(1 as i64)) + .get(&Value::Int(1 as i64)) .expect("Failed to get record") .expect("Record not found"); - assert!(match rec1.values[0] { - RecordValue::Int(1) => true, + assert!(match rec1.at(0) { + Value::Int(1) => true, _ => false, }); } @@ -1180,7 +816,7 @@ mod tests { let mut db = DB::configure() .data_dir(data_dir.to_str().unwrap()) .memtable_capacity(0) - .fields(vec![(Field::Id, RecordField::int())]) + .fields(&[(Field::Id, RecordField::int())]) .primary_key(Field::Id) .initialize() .expect("Failed to create DB"); @@ -1188,32 +824,28 @@ mod tests { // Insert records let n_recs = 100; for i in 0..n_recs { - let record = Record { - values: vec![RecordValue::Int(i as i64)], - }; + let record = Record::from(&[Value::Int(i as i64)]); db.upsert(&record).expect("Failed to insert record"); } // Open the segment file and write garbage to it to simulate corruption let segment_metadata_path = data_dir.join("metadata.1"); - let mut file = fs::OpenOptions::new() - .read(true) - .append(true) + let mut file = APPEND_MODE .open(&segment_metadata_path) .expect("Failed to open file"); file.write_all(&[0, 1, 2, 3]) .expect("Failed to write garbage"); + file.flush().unwrap(); let len = file.seek(SeekFrom::End(0)).expect("Failed to seek"); assert_ne!(len, METADATA_FILE_HEADER_SIZE as u64 + n_recs * 16); // Try to read from the file, triggering autorepair - db.get(&RecordValue::Int(0)).expect("Failed to get record"); + db.get(&Value::Int(0)).expect("Failed to get record"); // Reopen file and check that it has the correct size - let mut file = fs::OpenOptions::new() - .read(true) + let mut file = READ_MODE .open(&segment_metadata_path) .expect("Failed to open file"); let len = file.seek(SeekFrom::End(0)).expect("Failed to seek"); diff --git a/log_db/src/log_reader_forward.rs b/log_db/src/log_reader_forward.rs index 2d13f7a..343d7ff 100644 --- a/log_db/src/log_reader_forward.rs +++ b/log_db/src/log_reader_forward.rs @@ -86,8 +86,8 @@ mod tests { let first_record = forward_log_reader .next() .expect("Failed to read the first record"); - assert!(match first_record.values.as_slice() { - [RecordValue::Bytes(bytes)] => bytes.len() == 256, + assert!(match first_record.values() { + [Value::Bytes(bytes)] => bytes.len() == 256, _ => false, }); diff --git a/log_db/src/log_reader_reverse.rs b/log_db/src/log_reader_reverse.rs index 1023524..0e930c8 100644 --- a/log_db/src/log_reader_reverse.rs +++ b/log_db/src/log_reader_reverse.rs @@ -89,8 +89,8 @@ mod tests { let last_record = reverse_log_reader .next() .expect("Failed to read the last record"); - assert!(match last_record.values.as_slice() { - [RecordValue::Bytes(bytes)] => bytes.len() == 256, + assert!(match last_record.values() { + [Value::Bytes(bytes)] => bytes.len() == 256, _ => false, }); |
