use std::sync::{
Arc,
atomic::{AtomicI64, Ordering},
};
use wdev::Device;
use wkv::WedbStore;
use super::{
aof_address::AofAddress,
aof_entry_type::AofEntryType,
aof_header::{AofHeader, AofHeaderType, AofShardedHeader},
garnet_append_only_file::GarnetAppendOnlyFile,
garnet_log::GarnetLog,
legacy_resp_command::LegacyRespCommand,
readconsistency::read_consistency_manager::ReadConsistencyManager,
replaycoordinator::{
aof_replay_context::{ReplayOperation, TransactionGroup},
aof_replay_coordinator::AofReplayCoordinator,
},
};
use crate::{
storage::session::{mainstore::advanced_ops::StringRMWOp, storage_session::StorageSession},
types::RespCommand,
};
#[derive(Debug, thiserror::Error)]
pub enum AofReplayError {
#[error("AOF replay: {0}")]
Replay(String),
#[error(transparent)]
Store(#[from] wkv::Error),
}
impl From<String> for AofReplayError {
fn from(message: String) -> Self {
Self::Replay(message)
}
}
impl From<&str> for AofReplayError {
fn from(message: &str) -> Self {
Self::Replay(message.to_string())
}
}
pub const REPLAY_INPUT_HEADER_SIZE: usize = 32;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReplayInput {
pub cmd: RespCommand,
pub flags: u8,
pub sub_id: u8,
pub arg1: i64,
pub arg2: i64,
pub arg3: i64,
pub args: Vec<Vec<u8>>,
}
impl ReplayInput {
pub fn serialize(&self, into: &mut Vec<u8>) {
let raw: u16 = self.cmd.into();
into.extend_from_slice(&raw.to_le_bytes());
into.push(self.flags);
into.push(self.sub_id);
into.extend_from_slice(&[0u8; 4]);
into.extend_from_slice(&self.arg1.to_le_bytes());
into.extend_from_slice(&self.arg2.to_le_bytes());
into.extend_from_slice(&self.arg3.to_le_bytes());
into.extend_from_slice(&(self.args.len() as u32).to_le_bytes());
for arg in &self.args {
into.extend_from_slice(&(arg.len() as u32).to_le_bytes());
into.extend_from_slice(arg);
}
}
pub fn deserialize(bytes: &[u8]) -> Option<Self> {
if bytes.len() < REPLAY_INPUT_HEADER_SIZE {
return None;
}
let cmd = RespCommand::try_from(u16::from_le_bytes([bytes[0], bytes[1]])).ok()?;
let mut cursor = REPLAY_INPUT_HEADER_SIZE;
if cursor + 4 > bytes.len() {
return None;
}
let args_count = u32::from_le_bytes(bytes[cursor..cursor + 4].try_into().ok()?) as usize;
cursor += 4;
let mut args = Vec::with_capacity(args_count);
for _ in 0..args_count {
if cursor + 4 > bytes.len() {
return None;
}
let len = u32::from_le_bytes(bytes[cursor..cursor + 4].try_into().ok()?) as usize;
cursor += 4;
if cursor + len > bytes.len() {
return None;
}
args.push(bytes[cursor..cursor + len].to_vec());
cursor += len;
}
Some(Self {
cmd,
flags: bytes[2],
sub_id: bytes[3],
arg1: i64::from_le_bytes(bytes[8..16].try_into().ok()?),
arg2: i64::from_le_bytes(bytes[16..24].try_into().ok()?),
arg3: i64::from_le_bytes(bytes[24..32].try_into().ok()?),
args,
})
}
}
pub struct PreparedParameters {
pub key: Vec<u8>,
pub key_hash: i64,
pub payload: Vec<u8>,
}
pub struct ReplayTarget<'a, 'b, D: Device> {
pub session: &'b StorageSession<'a, D>,
pub store: Arc<WedbStore<D>>,
pub store_version: i64,
}
pub struct AofProcessor {
append_only_file: Arc<GarnetAppendOnlyFile>,
coordinator: AofReplayCoordinator,
active_db_id: AtomicI64,
using_sharded_log: bool,
using_single_physical_log_multi_replay: bool,
}
impl AofProcessor {
pub fn new(append_only_file: Arc<GarnetAppendOnlyFile>) -> Self {
let options_physical = append_only_file.log().size();
let virtual_count = append_only_file.virtual_sublog_count();
let coordinator =
AofReplayCoordinator::new(virtual_count, append_only_file.multi_log_enabled());
if let Some(manager) = append_only_file.read_consistency_manager() {
coordinator.set_consistency_manager(manager);
}
Self {
append_only_file,
coordinator,
active_db_id: AtomicI64::new(0),
using_sharded_log: options_physical > 1,
using_single_physical_log_multi_replay: options_physical == 1 && virtual_count > 1,
}
}
pub fn coordinator(&self) -> &AofReplayCoordinator {
&self.coordinator
}
pub fn append_only_file(&self) -> &Arc<GarnetAppendOnlyFile> {
&self.append_only_file
}
pub fn read_consistency_manager(&self) -> Option<Arc<ReadConsistencyManager>> {
self.append_only_file.read_consistency_manager()
}
pub fn switch_active_database_context(&self, db_id: i64) {
self.active_db_id.store(db_id, Ordering::Release);
}
pub fn active_db_id(&self) -> i64 {
self.active_db_id.load(Ordering::Acquire)
}
pub fn prepare_key(
&self,
virtual_sublog_idx: usize,
entry: &[u8],
log_address_sequence_number: i64,
) -> Option<PreparedParameters> {
let header = AofHeader::parse(entry)?;
let (header_size, sequence_number) =
if header.header_type() == Some(AofHeaderType::ShardedHeader) {
let sh = AofShardedHeader::parse(entry)?;
(AofShardedHeader::TOTAL_SIZE, sh.sequence_number)
} else {
(AofHeader::TOTAL_SIZE, log_address_sequence_number)
};
let payload = &entry[header_size..];
let key_len = u32::from_le_bytes(*payload.first_chunk::<4>()?) as usize;
let key = payload.get(4..4 + key_len)?;
let key_hash = GarnetLog::hash(key);
let rest = payload.get(4 + key_len..)?;
if (self.using_sharded_log || self.using_single_physical_log_multi_replay)
&& let Some(manager) = self.read_consistency_manager()
{
manager.update_virtual_sublog_key_sequence_number(
virtual_sublog_idx,
key_hash,
sequence_number,
);
}
Some(PreparedParameters {
key: key.to_vec(),
key_hash,
payload: rest.to_vec(),
})
}
fn split_value_input(payload: &[u8]) -> Option<(&[u8], &[u8])> {
let len = u32::from_le_bytes(*payload.first_chunk::<4>()?) as usize;
let value = payload.get(4..4 + len)?;
Some((value, &payload[4 + len..]))
}
pub fn get_synchronized_operation_params(
&self,
entry: &[u8],
entry_address: i64,
) -> Option<(i64, i16)> {
let header = AofHeader::parse(entry)?;
match header.header_type()? {
AofHeaderType::BasicHeader | AofHeaderType::BasicChunkHeader => {
Some((entry_address, self.replay_task_count() as i16))
}
AofHeaderType::ShardedHeader | AofHeaderType::ShardedChunkHeader => {
let sh = AofShardedHeader::parse(entry)?;
Some((sh.sequence_number, self.replay_task_count() as i16))
}
_ => None,
}
}
fn replay_task_count(&self) -> usize {
self.append_only_file.virtual_sublog_count() / self.append_only_file.log().size().max(1)
}
pub async fn process_aof_record_internal<D: Device>(
&self,
virtual_sublog_idx: usize,
entry: &[u8],
as_replica: bool,
log_address_sequence_number: i64,
target: &ReplayTarget<'_, '_, D>,
) -> Result<bool, AofReplayError> {
if self
.coordinator
.context(virtual_sublog_idx)
.has_in_progress_chunk()
{
let completed = self
.coordinator
.context(virtual_sublog_idx)
.chunked_reader
.read_chunk(entry);
if let Some(acc) = completed {
return super::aof_processor__chunk_replay::process_chunked_record(
self,
virtual_sublog_idx,
acc,
as_replica,
log_address_sequence_number,
target,
)
.await;
}
return Ok(false);
}
let header = AofHeader::parse(entry).ok_or("AOF 条目头损坏")?;
if header.aof_header_version > AofHeader::MAX_SUPPORTED_AOF_HEADER_VERSION {
return Err(
format!(
"Unsupported AOF header version {}; this build supports up to version {}",
header.aof_header_version,
AofHeader::MAX_SUPPORTED_AOF_HEADER_VERSION
)
.into(),
);
}
if header.is_chunked() {
let completed = self
.coordinator
.context(virtual_sublog_idx)
.chunked_reader
.read_chunk(entry);
if let Some(acc) = completed {
return super::aof_processor__chunk_replay::process_chunked_record(
self,
virtual_sublog_idx,
acc,
as_replica,
log_address_sequence_number,
target,
)
.await;
}
return Ok(false);
}
let op_type = AofEntryType::try_from(header.op_type).unwrap_or(AofEntryType::StoreUpsert);
let action = self.coordinator.add_or_replay_transaction_operation(
virtual_sublog_idx,
entry,
as_replica,
log_address_sequence_number,
);
match action {
super::replaycoordinator::aof_replay_coordinator::TxnAction::Handled => return Ok(false),
super::replaycoordinator::aof_replay_coordinator::TxnAction::Commit { session_id } => {
let group = self
.coordinator
.take_transaction_group(virtual_sublog_idx, session_id);
if let Some(group) = group {
self
.process_transaction_group_operations(virtual_sublog_idx, &group, target)
.await;
}
return Ok(false);
}
super::replaycoordinator::aof_replay_coordinator::TxnAction::None => {}
}
let mut is_checkpoint_start = false;
match op_type {
AofEntryType::CheckpointStartCommit => {
is_checkpoint_start = true;
if header.aof_header_version > 1 {
if self
.coordinator
.context(virtual_sublog_idx)
.in_fuzzy_region()
{
self
.coordinator
.clear_fuzzy_region_buffer(virtual_sublog_idx);
}
self
.coordinator
.context(virtual_sublog_idx)
.set_in_fuzzy_region(true);
}
if self.using_sharded_log || self.using_single_physical_log_multi_replay {
let sequence_number = if self.using_single_physical_log_multi_replay {
log_address_sequence_number
} else {
AofShardedHeader::parse(entry).map_or(0, |sh| sh.sequence_number)
};
if let Some(manager) = self.read_consistency_manager() {
manager.update_virtual_sublog_max_sequence_number(virtual_sublog_idx, sequence_number);
}
}
}
AofEntryType::CheckpointEndCommit => {
if header.aof_header_version > 1 {
if !self
.coordinator
.context(virtual_sublog_idx)
.in_fuzzy_region()
{
} else {
self
.coordinator
.context(virtual_sublog_idx)
.set_in_fuzzy_region(false);
self
.process_fuzzy_region_operations(virtual_sublog_idx, target)
.await?;
self
.coordinator
.clear_fuzzy_region_buffer(virtual_sublog_idx);
}
}
}
AofEntryType::FlushAll => {
let _ = header.unsafe_truncate_log();
target
.store
.flush_all()
.await
.map_err(|e| format!("FlushAll replay failed: {e}"))?;
}
AofEntryType::FlushDb => {
target
.store
.flush_all()
.await
.map_err(|e| format!("FlushDb replay failed: {e}"))?;
}
AofEntryType::StoredProcedure => {
}
AofEntryType::TxnCommit => {
self
.process_fuzzy_region_transaction_group(virtual_sublog_idx, target)
.await?;
}
_ => {
self
.replay_op_dispatch(
virtual_sublog_idx,
header,
entry,
as_replica,
log_address_sequence_number,
target,
)
.await?;
}
}
Ok(is_checkpoint_start)
}
pub async fn process_fuzzy_region_operations<D: Device>(
&self,
sublog_idx: usize,
target: &ReplayTarget<'_, '_, D>,
) -> Result<(), AofReplayError> {
let operations = self.coordinator.take_fuzzy_region_operations(sublog_idx);
for op in operations {
match op {
ReplayOperation::Record(entry) => {
let header = AofHeader::parse(&entry).ok_or("模糊区条目头损坏")?;
self
.replay_op_dispatch(sublog_idx, header, &entry, true, 0, target)
.await?;
}
ReplayOperation::Chunk(acc) => {
super::aof_processor__chunk_replay::replay_chunk(self, sublog_idx, *acc, target).await?;
}
}
}
Ok(())
}
pub async fn process_fuzzy_region_transaction_group<D: Device>(
&self,
sublog_idx: usize,
target: &ReplayTarget<'_, '_, D>,
) -> Result<(), AofReplayError> {
let Some(group) = self.coordinator.dequeue_txn_group(sublog_idx) else {
return Ok(());
};
self
.process_transaction_group_operations(sublog_idx, &group, target)
.await;
Ok(())
}
pub async fn process_transaction_group_operations<D: Device>(
&self,
sublog_idx: usize,
group: &TransactionGroup,
target: &ReplayTarget<'_, '_, D>,
) {
for op in &group.operations {
let result = match op {
ReplayOperation::Record(entry) => match AofHeader::parse(entry) {
Some(header) => {
self
.replay_op_dispatch(
sublog_idx,
header,
entry,
true,
group.start_sequence_number,
target,
)
.await
}
None => Err("模糊区条目头损坏".to_string().into()),
},
ReplayOperation::Chunk(acc) => {
super::aof_processor__chunk_replay::replay_chunk(
self,
sublog_idx,
(**acc).clone(),
target,
)
.await
}
};
let _ = result;
}
}
pub async fn replay_op_dispatch<D: Device>(
&self,
virtual_sublog_idx: usize,
header: AofHeader,
entry: &[u8],
as_replica: bool,
log_address_sequence_number: i64,
target: &ReplayTarget<'_, '_, D>,
) -> Result<(), AofReplayError> {
let op_type = AofEntryType::try_from(header.op_type).map_err(|_| "未知 AOF 操作类型")?;
let skip = self.should_skip_record(virtual_sublog_idx, entry, as_replica, target.store_version);
if !self.begin_replay_op(skip) {
return Ok(());
}
let prepared = self
.prepare_key(virtual_sublog_idx, entry, log_address_sequence_number)
.ok_or("AOF 条目负载损坏")?;
let legacy_cmd_format = header.aof_header_version < 4;
self
.replay_op(op_type, prepared, legacy_cmd_format, target)
.await
}
pub fn begin_replay_op(&self, skip: bool) -> bool {
!skip
}
pub async fn replay_op<D: Device>(
&self,
op_type: AofEntryType,
prepared: PreparedParameters,
legacy_cmd_format: bool,
target: &ReplayTarget<'_, '_, D>,
) -> Result<(), AofReplayError> {
let PreparedParameters {
key,
key_hash: _,
payload,
} = prepared;
match op_type {
AofEntryType::StoreUpsert => {
let (value, input) = Self::split_value_input(&payload).ok_or("StoreUpsert 负载损坏")?;
Self::store_upsert(target.session, &key, value, input, legacy_cmd_format).await
}
AofEntryType::StoreRMW => {
Self::store_rmw(target.session, &key, &payload, legacy_cmd_format).await
}
AofEntryType::StoreDelete => Self::store_delete(target.session, &key).await,
AofEntryType::ObjectStoreUpsert => {
let (value, _) = Self::split_value_input(&payload).ok_or("ObjectStoreUpsert 负载损坏")?;
Self::object_store_upsert(target.session, &key, value).await
}
AofEntryType::ObjectStoreRMW => {
Self::object_store_rmw(target.session, &key, &payload, legacy_cmd_format).await
}
AofEntryType::ObjectStoreDelete => Self::object_store_delete(target.session, &key).await,
AofEntryType::UnifiedStoreStringUpsert => {
let (value, input) =
Self::split_value_input(&payload).ok_or("UnifiedStoreStringUpsert 负载损坏")?;
Self::unified_store_string_upsert(target.session, &key, value, input, legacy_cmd_format)
.await
}
AofEntryType::UnifiedStoreObjectUpsert => {
let (value, _) =
Self::split_value_input(&payload).ok_or("UnifiedStoreObjectUpsert 负载损坏")?;
Self::object_store_upsert(target.session, &key, value).await
}
AofEntryType::UnifiedStoreRMW => {
Self::store_rmw(target.session, &key, &payload, legacy_cmd_format).await
}
AofEntryType::UnifiedStoreDelete => Self::store_delete(target.session, &key).await,
AofEntryType::RangeIndexStreamChunk => Err(
"RangeIndexPreview disabled; Replay failed"
.to_string()
.into(),
),
_ => Err(format!("Unknown AOF header operation type {op_type:?}").into()),
}
}
pub async fn store_upsert<D: Device>(
session: &StorageSession<'_, D>,
key: &[u8],
value: &[u8],
input: &[u8],
legacy_cmd_format: bool,
) -> Result<(), AofReplayError> {
let mut input = ReplayInput::deserialize(input).ok_or("StoreUpsert input 损坏")?;
if legacy_cmd_format {
input.cmd = LegacyRespCommand::from_v3(input.cmd);
}
let _ = input;
session
.upsert_string(key, value)
.await
.map_err(AofReplayError::Store)
}
pub async fn store_rmw<D: Device>(
session: &StorageSession<'_, D>,
key: &[u8],
input: &[u8],
legacy_cmd_format: bool,
) -> Result<(), AofReplayError> {
let mut input = ReplayInput::deserialize(input).ok_or("StoreRMW input 损坏")?;
if legacy_cmd_format {
input.cmd = LegacyRespCommand::from_v3(input.cmd);
}
if matches!(
input.cmd,
RespCommand::Vadd | RespCommand::Vrem | RespCommand::Vsetattr
) {
return Err(
"vector replay requires vector domain; unsupported"
.to_string()
.into(),
);
}
if matches!(
input.cmd,
RespCommand::Ricreate | RespCommand::Riset | RespCommand::Ridel
) {
return Err(
"RangeIndexPreview disabled; Replay failed"
.to_string()
.into(),
);
}
let op = match input.cmd {
RespCommand::Incr | RespCommand::Incrby => StringRMWOp::Incr { delta: input.arg1 },
RespCommand::Decr | RespCommand::Decrby => StringRMWOp::Incr { delta: -input.arg1 },
RespCommand::Incrbyfloat => StringRMWOp::IncrFloat {
delta: f64::from_bits(input.arg1 as u64),
},
RespCommand::Append => {
let data = input.args.first().map_or(&[][..], Vec::as_slice);
StringRMWOp::Append(data)
}
RespCommand::Setrange => {
let data = input.args.first().map_or(&[][..], Vec::as_slice);
StringRMWOp::SetRange {
offset: input.arg2.max(0) as usize,
data,
}
}
_ => {
return Ok(());
}
};
session
.rmw_main_store(key, op)
.await
.map_err(|e| format!("StoreRMW replay failed: {e}"))?;
Ok(())
}
pub async fn store_delete<D: Device>(
session: &StorageSession<'_, D>,
key: &[u8],
) -> Result<(), AofReplayError> {
session
.delete_string(key)
.await
.map_err(|e| format!("StoreDelete replay failed: {e}"))?;
Ok(())
}
pub async fn object_store_upsert<D: Device>(
session: &StorageSession<'_, D>,
key: &[u8],
value: &[u8],
) -> Result<(), AofReplayError> {
let Some((&tag, payload)) = value.split_first() else {
return Err("ObjectStoreUpsert 值缺少类型标签".to_string().into());
};
session
.obj_save(key, tag, payload)
.await
.map_err(AofReplayError::Store)
}
pub async fn object_store_rmw<D: Device>(
session: &StorageSession<'_, D>,
key: &[u8],
input: &[u8],
legacy_cmd_format: bool,
) -> Result<(), AofReplayError> {
let mut input = ReplayInput::deserialize(input).ok_or("ObjectStoreRMW input 损坏")?;
if legacy_cmd_format {
input.sub_id = input.flags & 0x1F;
input.flags &= !0x1F;
}
let _ = (key, session);
Ok(())
}
pub async fn object_store_delete<D: Device>(
session: &StorageSession<'_, D>,
key: &[u8],
) -> Result<(), AofReplayError> {
session
.delete_object_store(key)
.await
.map_err(|e| format!("ObjectStoreDelete replay failed: {e}"))?;
Ok(())
}
pub async fn unified_store_string_upsert<D: Device>(
session: &StorageSession<'_, D>,
key: &[u8],
value: &[u8],
input: &[u8],
legacy_cmd_format: bool,
) -> Result<(), AofReplayError> {
Self::store_upsert(session, key, value, input, legacy_cmd_format).await
}
pub fn relocate_legacy_object_sub_id(flags: u8) -> (u8, u8) {
const LEGACY_SUB_ID_MASK: u8 = 0x1F;
(flags & LEGACY_SUB_ID_MASK, flags & !LEGACY_SUB_ID_MASK)
}
pub fn should_skip_record(
&self,
sublog_idx: usize,
entry: &[u8],
as_replica: bool,
store_version: i64,
) -> bool {
let Some(header) = AofHeader::parse(entry) else {
return true;
};
if as_replica && self.coordinator.context(sublog_idx).in_fuzzy_region() {
if header.store_version > store_version {
self
.coordinator
.add_fuzzy_region_operation(sublog_idx, ReplayOperation::Record(entry.to_vec()));
return true;
}
return false;
}
header.store_version < store_version
}
pub fn should_skip_record_chunk(
&self,
sublog_idx: usize,
acc: &super::aof_chunked_record_reader::ChunkedAccumulator,
store_version: i64,
) -> bool {
if as_replica_fuzzy(self, sublog_idx) {
if acc.store_version > store_version {
self
.coordinator
.add_fuzzy_region_operation(sublog_idx, ReplayOperation::Chunk(Box::new(acc.clone())));
return true;
}
return false;
}
acc.store_version < store_version
}
pub fn is_old_version_record(header: &AofHeader, store_version: i64) -> bool {
header.store_version < store_version
}
pub fn is_new_version_record(header: &AofHeader, store_version: i64) -> bool {
header.store_version > store_version
}
pub fn can_replay(
&self,
entry: &[u8],
replay_task_idx: usize,
entry_address: i64,
) -> Option<(bool, i64)> {
let header = AofHeader::parse(entry)?;
let log = self.append_only_file.log();
match header.header_type()? {
AofHeaderType::BasicHeader | AofHeaderType::BasicChunkHeader => {
let op_type = AofEntryType::try_from(header.op_type).ok()?;
if !op_type.has_key() {
return Some((true, entry_address));
}
let chunk = header.is_chunked();
let routing = if chunk {
let (_, ch) = AofHeader::get_chunked_header_ref(entry)?;
ch.key_hash
} else {
let offset = AofHeader::skip_header(entry)?;
let len = u32::from_le_bytes(entry[offset..offset + 4].try_into().ok()?) as usize;
let key = entry.get(offset + 4..offset + 4 + len)?;
GarnetLog::hash(key)
};
Some((
replay_task_idx == log.get_replay_task_idx(routing),
entry_address,
))
}
AofHeaderType::ShardedHeader | AofHeaderType::ShardedChunkHeader => {
let sh = AofShardedHeader::parse(entry)?;
let op_type = AofEntryType::try_from(header.op_type).ok()?;
if !op_type.has_key() {
return Some((replay_task_idx == 0, sh.sequence_number));
}
let offset = AofHeader::skip_header(entry)?;
let len = u32::from_le_bytes(*entry.get(offset..)?.first_chunk::<4>()?) as usize;
let key = entry.get(offset + 4..offset + 4 + len)?;
Some((
replay_task_idx == log.get_replay_task_idx(GarnetLog::hash(key)),
sh.sequence_number,
))
}
_ => None,
}
}
pub fn skip_replay(
&self,
entry: &[u8],
until_sequence_number: i64,
log_address_sequence_number: i64,
) -> Option<(bool, i64)> {
if until_sequence_number == -1 {
return Some((true, -1));
}
let header = AofHeader::parse(entry)?;
let sequence_number = match header.header_type()? {
AofHeaderType::BasicHeader | AofHeaderType::BasicChunkHeader => log_address_sequence_number,
AofHeaderType::ShardedHeader | AofHeaderType::ShardedChunkHeader => {
AofShardedHeader::parse(entry)?.sequence_number
}
_ => log_address_sequence_number,
};
Some((sequence_number > until_sequence_number, sequence_number))
}
pub fn peek_entry_key(entry: &[u8]) -> Option<&[u8]> {
let offset = AofHeader::skip_header(entry)?;
let len = u32::from_le_bytes(*entry.get(offset..)?.first_chunk::<4>()?) as usize;
entry.get(offset + 4..offset + 4 + len)
}
}
fn as_replica_fuzzy(processor: &AofProcessor, sublog_idx: usize) -> bool {
processor.coordinator.context(sublog_idx).in_fuzzy_region
}
pub mod encode {
use super::{AofHeader, AofShardedHeader};
fn frame(header: &AofHeader, body: impl FnOnce(&mut Vec<u8>)) -> Vec<u8> {
let mut entry = header.to_bytes().to_vec();
body(&mut entry);
entry
}
pub fn upsert_entry(header: &AofHeader, key: &[u8], value: &[u8], input: &[u8]) -> Vec<u8> {
frame(header, |out| {
out.extend_from_slice(&(key.len() as u32).to_le_bytes());
out.extend_from_slice(key);
out.extend_from_slice(&(value.len() as u32).to_le_bytes());
out.extend_from_slice(value);
out.extend_from_slice(input);
})
}
pub fn keyed_entry(header: &AofHeader, key: &[u8], tail: &[u8]) -> Vec<u8> {
frame(header, |out| {
out.extend_from_slice(&(key.len() as u32).to_le_bytes());
out.extend_from_slice(key);
out.extend_from_slice(tail);
})
}
pub fn sharded_entry(
header: &AofHeader,
sequence_number: i64,
key: &[u8],
value: &[u8],
input: &[u8],
) -> Vec<u8> {
let mut sharded = AofShardedHeader {
basic: *header,
sequence_number,
};
sharded
.basic
.set_header_type(super::super::aof_header::AofHeaderType::ShardedHeader);
let mut entry = sharded.basic.to_bytes().to_vec();
entry.extend_from_slice(&sequence_number.to_le_bytes());
entry.extend_from_slice(&(key.len() as u32).to_le_bytes());
entry.extend_from_slice(key);
entry.extend_from_slice(&(value.len() as u32).to_le_bytes());
entry.extend_from_slice(value);
entry.extend_from_slice(input);
entry
}
pub fn keyless_entry(header: &AofHeader) -> Vec<u8> {
frame(header, |_| {})
}
}
pub fn invalid_aof_address(physical_sublog_count: usize) -> AofAddress {
AofAddress::create(physical_sublog_count as i32, -1)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn replay_input_parses_integration_bytes() {
let bytes: Vec<u8> = [
0x4a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x6b, 0x02, 0x00, 0x00, 0x00,
0x76, 0x31,
]
.to_vec();
let parsed = ReplayInput::deserialize(&bytes).expect("集成字节应可解析");
assert_eq!(parsed.cmd, RespCommand::Set);
assert_eq!(parsed.args, vec![b"k".to_vec(), b"v1".to_vec()]);
}
#[test]
fn replay_input_roundtrip() {
let input = ReplayInput {
cmd: RespCommand::Set,
flags: 0,
sub_id: 0,
arg1: 32,
arg2: 0,
arg3: 0,
args: vec![b"cnt".to_vec(), b"32".to_vec()],
};
let mut bytes = Vec::new();
input.serialize(&mut bytes);
assert_eq!(bytes.len(), 8 + 24 + 4 + (4 + 3) + (4 + 2));
let parsed = ReplayInput::deserialize(&bytes).expect("应可反序列化");
assert_eq!(parsed.cmd, RespCommand::Set);
assert_eq!(parsed.arg1, 32);
assert_eq!(parsed.args, vec![b"cnt".to_vec(), b"32".to_vec()]);
}
}