blob: 6a6d434ad09c928fc4f1b502dd3deb6c3a9510d5 [file]
/* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
use crate::streaming::common::test_setup::TestSetup;
use crate::streaming::create_message;
use bytes::Bytes;
use iggy::prelude::locking::IggySharedMutFn;
use iggy::prelude::*;
use server::configs::cluster::ClusterConfig;
use server::configs::server::{DataMaintenanceConfig, PersonalAccessTokenConfig};
use server::configs::system::{PartitionConfig, SegmentConfig, SystemConfig};
use server::streaming::segments::*;
use server::streaming::session::Session;
use server::streaming::systems::system::System;
use std::fs::DirEntry;
use std::net::{Ipv4Addr, SocketAddr};
use std::str::FromStr;
use std::sync::Arc;
use std::sync::atomic::AtomicU64;
use std::time::Duration;
use tokio::fs;
use tokio::time::sleep;
#[tokio::test]
async fn should_persist_segment() {
let setup = TestSetup::init().await;
let stream_id = 1;
let topic_id = 2;
let partition_id = 3;
let start_offsets = get_start_offsets();
for start_offset in start_offsets {
let mut segment = Segment::create(
stream_id,
topic_id,
partition_id,
start_offset,
setup.config.clone(),
IggyExpiry::NeverExpire,
Arc::new(AtomicU64::new(0)),
Arc::new(AtomicU64::new(0)),
Arc::new(AtomicU64::new(0)),
Arc::new(AtomicU64::new(0)),
Arc::new(AtomicU64::new(0)),
Arc::new(AtomicU64::new(0)),
true,
);
setup
.create_partition_directory(stream_id, topic_id, partition_id)
.await;
segment.persist().await.unwrap();
assert_persisted_segment(
&setup
.config
.get_partition_path(stream_id, topic_id, partition_id),
start_offset,
)
.await;
}
}
#[tokio::test]
async fn should_load_existing_segment_from_disk() {
let setup = TestSetup::init().await;
let stream_id = 1;
let topic_id = 2;
let partition_id = 3;
let start_offsets = get_start_offsets();
for start_offset in start_offsets {
let mut segment = Segment::create(
stream_id,
topic_id,
partition_id,
start_offset,
setup.config.clone(),
IggyExpiry::NeverExpire,
Arc::new(AtomicU64::new(0)),
Arc::new(AtomicU64::new(0)),
Arc::new(AtomicU64::new(0)),
Arc::new(AtomicU64::new(0)),
Arc::new(AtomicU64::new(0)),
Arc::new(AtomicU64::new(0)),
true,
);
setup
.create_partition_directory(stream_id, topic_id, partition_id)
.await;
segment.persist().await.unwrap();
assert_persisted_segment(
&setup
.config
.get_partition_path(stream_id, topic_id, partition_id),
start_offset,
)
.await;
let mut loaded_segment = Segment::create(
stream_id,
topic_id,
partition_id,
start_offset,
setup.config.clone(),
IggyExpiry::NeverExpire,
Arc::new(AtomicU64::new(0)),
Arc::new(AtomicU64::new(0)),
Arc::new(AtomicU64::new(0)),
Arc::new(AtomicU64::new(0)),
Arc::new(AtomicU64::new(0)),
Arc::new(AtomicU64::new(0)),
false,
);
loaded_segment.load_from_disk().await.unwrap();
let loaded_messages = loaded_segment.get_messages_by_offset(0, 10).await.unwrap();
assert_eq!(loaded_segment.partition_id(), segment.partition_id());
assert_eq!(loaded_segment.start_offset(), segment.start_offset());
assert_eq!(loaded_segment.end_offset(), segment.end_offset());
assert_eq!(
loaded_segment.get_messages_size(),
segment.get_messages_size()
);
assert_eq!(loaded_segment.is_closed(), segment.is_closed());
assert_eq!(
loaded_segment.messages_file_path(),
segment.messages_file_path()
);
assert_eq!(loaded_segment.index_file_path(), segment.index_file_path());
assert!(loaded_messages.is_empty());
}
}
#[tokio::test]
async fn should_persist_and_load_segment_with_messages() {
let setup = TestSetup::init().await;
let stream_id = 1;
let topic_id = 2;
let partition_id = 3;
let start_offset = 0;
let mut segment = Segment::create(
stream_id,
topic_id,
partition_id,
start_offset,
setup.config.clone(),
IggyExpiry::NeverExpire,
Arc::new(AtomicU64::new(0)),
Arc::new(AtomicU64::new(0)),
Arc::new(AtomicU64::new(0)),
Arc::new(AtomicU64::new(0)),
Arc::new(AtomicU64::new(0)),
Arc::new(AtomicU64::new(0)),
true,
);
setup
.create_partition_directory(stream_id, topic_id, partition_id)
.await;
segment.persist().await.unwrap();
assert_persisted_segment(
&setup
.config
.get_partition_path(stream_id, topic_id, partition_id),
start_offset,
)
.await;
let messages_count = 10;
let mut messages = Vec::new();
let mut messages_size = 0;
for i in 0..messages_count {
let message = IggyMessage::builder()
.id(i as u128)
.payload(Bytes::from("test"))
.build()
.expect("Failed to create message");
messages_size += message.get_size_bytes().as_bytes_u32();
messages.push(message);
}
let batch = IggyMessagesBatchMut::from_messages(&messages, messages_size);
segment.append_batch(0, batch, None).await.unwrap();
segment.persist_messages(None).await.unwrap();
let mut loaded_segment = Segment::create(
stream_id,
topic_id,
partition_id,
start_offset,
setup.config.clone(),
IggyExpiry::NeverExpire,
Arc::new(AtomicU64::new(0)),
Arc::new(AtomicU64::new(0)),
Arc::new(AtomicU64::new(0)),
Arc::new(AtomicU64::new(0)),
Arc::new(AtomicU64::new(0)),
Arc::new(AtomicU64::new(0)),
false,
);
loaded_segment.load_from_disk().await.unwrap();
let messages = loaded_segment
.get_messages_by_offset(0, messages_count)
.await
.unwrap();
assert_eq!(messages.count(), messages_count);
}
#[tokio::test]
async fn should_persist_and_load_segment_with_messages_with_nowait_confirmation() {
let setup = TestSetup::init_with_config(SystemConfig {
segment: SegmentConfig {
server_confirmation: Confirmation::NoWait,
..Default::default()
},
..Default::default()
})
.await;
let stream_id = 1;
let topic_id = 2;
let partition_id = 3;
let start_offset = 0;
let mut segment = Segment::create(
stream_id,
topic_id,
partition_id,
start_offset,
setup.config.clone(),
IggyExpiry::NeverExpire,
Arc::new(AtomicU64::new(0)),
Arc::new(AtomicU64::new(0)),
Arc::new(AtomicU64::new(0)),
Arc::new(AtomicU64::new(0)),
Arc::new(AtomicU64::new(0)),
Arc::new(AtomicU64::new(0)),
true,
);
setup
.create_partition_directory(stream_id, topic_id, partition_id)
.await;
segment.persist().await.unwrap();
assert_persisted_segment(
&setup
.config
.get_partition_path(stream_id, topic_id, partition_id),
start_offset,
)
.await;
let messages_count = 10;
let mut messages = Vec::new();
let mut messages_size = 0;
for i in 0..messages_count {
let message = IggyMessage::builder()
.id(i as u128)
.payload(Bytes::from("test"))
.build()
.expect("Failed to create message");
messages_size += message.get_size_bytes().as_bytes_u32();
messages.push(message);
}
let batch = IggyMessagesBatchMut::from_messages(&messages, messages_size);
segment.append_batch(0, batch, None).await.unwrap();
segment
.persist_messages(Some(Confirmation::NoWait))
.await
.unwrap();
sleep(Duration::from_millis(200)).await;
let mut loaded_segment = Segment::create(
stream_id,
topic_id,
partition_id,
start_offset,
setup.config.clone(),
IggyExpiry::NeverExpire,
Arc::new(AtomicU64::new(0)),
Arc::new(AtomicU64::new(0)),
Arc::new(AtomicU64::new(0)),
Arc::new(AtomicU64::new(0)),
Arc::new(AtomicU64::new(0)),
Arc::new(AtomicU64::new(0)),
false,
);
loaded_segment.load_from_disk().await.unwrap();
let messages = loaded_segment
.get_messages_by_offset(0, messages_count)
.await
.unwrap();
assert_eq!(messages.count(), messages_count);
}
#[tokio::test]
async fn given_all_expired_messages_segment_should_be_expired() {
let config = SystemConfig {
partition: PartitionConfig {
enforce_fsync: true,
..Default::default()
},
segment: SegmentConfig {
size: IggyByteSize::from_str("10B").unwrap(), // small size to force expiration
..Default::default()
},
..Default::default()
};
let setup = TestSetup::init_with_config(config).await;
let stream_id = 1;
let topic_id = 2;
let partition_id = 3;
let start_offset = 0;
let message_expiry_us = 100000;
let message_expiry = message_expiry_us.into();
let mut segment = Segment::create(
stream_id,
topic_id,
partition_id,
start_offset,
setup.config.clone(),
message_expiry,
Arc::new(AtomicU64::new(0)),
Arc::new(AtomicU64::new(0)),
Arc::new(AtomicU64::new(0)),
Arc::new(AtomicU64::new(0)),
Arc::new(AtomicU64::new(0)),
Arc::new(AtomicU64::new(0)),
true,
);
setup
.create_partition_directory(stream_id, topic_id, partition_id)
.await;
segment.persist().await.unwrap();
assert_persisted_segment(
&setup
.config
.get_partition_path(stream_id, topic_id, partition_id),
start_offset,
)
.await;
let messages_count = 10;
let mut messages = Vec::new();
let mut messages_size = 0;
for i in 0..messages_count {
let message = IggyMessage::builder()
.id(i as u128)
.payload(Bytes::from("test"))
.build()
.expect("Failed to create message");
messages_size += message.get_size_bytes().as_bytes_u32();
messages.push(message);
}
let batch = IggyMessagesBatchMut::from_messages(&messages, messages_size);
segment.append_batch(0, batch, None).await.unwrap();
segment.persist_messages(None).await.unwrap();
let not_expired_ts = IggyTimestamp::now();
let expired_ts = not_expired_ts + IggyDuration::from(message_expiry_us + 1);
assert!(segment.is_expired(expired_ts).await);
assert!(!segment.is_expired(not_expired_ts).await);
}
#[tokio::test]
async fn given_at_least_one_not_expired_message_segment_should_not_be_expired() {
let config = SystemConfig {
partition: PartitionConfig {
enforce_fsync: true,
..Default::default()
},
segment: SegmentConfig {
size: IggyByteSize::from_str("10B").unwrap(), // small size to force expiration
..Default::default()
},
..Default::default()
};
let setup = TestSetup::init_with_config(config).await;
let stream_id = 1;
let topic_id = 2;
let partition_id = 3;
let start_offset = 0;
let message_expiry_us = 50000;
let message_expiry = message_expiry_us.into();
let mut segment = Segment::create(
stream_id,
topic_id,
partition_id,
start_offset,
setup.config.clone(),
message_expiry,
Arc::new(AtomicU64::new(0)),
Arc::new(AtomicU64::new(0)),
Arc::new(AtomicU64::new(0)),
Arc::new(AtomicU64::new(0)),
Arc::new(AtomicU64::new(0)),
Arc::new(AtomicU64::new(0)),
true,
);
setup
.create_partition_directory(stream_id, topic_id, partition_id)
.await;
segment.persist().await.unwrap();
assert_persisted_segment(
&setup
.config
.get_partition_path(stream_id, topic_id, partition_id),
start_offset,
)
.await;
let nothing_expired_ts = IggyTimestamp::now();
let first_message = vec![
IggyMessage::builder()
.payload(Bytes::from("expired"))
.build()
.expect("Failed to create message"),
];
let first_message_size = first_message[0].get_size_bytes().as_bytes_u32();
let first_batch = IggyMessagesBatchMut::from_messages(&first_message, first_message_size);
segment.append_batch(0, first_batch, None).await.unwrap();
sleep(Duration::from_micros(message_expiry_us / 2)).await;
let first_message_expired_ts = IggyTimestamp::now();
let second_message = vec![
IggyMessage::builder()
.payload(Bytes::from("not-expired"))
.build()
.expect("Failed to create message"),
];
let second_message_size = second_message[0].get_size_bytes().as_bytes_u32();
let second_batch = IggyMessagesBatchMut::from_messages(&second_message, second_message_size);
segment.append_batch(1, second_batch, None).await.unwrap();
let second_message_expired_ts =
IggyTimestamp::now() + IggyDuration::from(message_expiry_us * 2);
segment.persist_messages(None).await.unwrap();
assert!(
!segment.is_expired(nothing_expired_ts).await,
"Segment should not be expired for nothing expired timestamp"
);
assert!(
!segment.is_expired(first_message_expired_ts).await,
"Segment should not be expired for first message expired timestamp"
);
assert!(
segment.is_expired(second_message_expired_ts).await,
"Segment should be expired for second message expired timestamp"
);
}
#[tokio::test]
async fn should_delete_persisted_segments() -> Result<(), Box<dyn std::error::Error>> {
let config = SystemConfig {
segment: SegmentConfig {
size: IggyByteSize::from_str("10B").unwrap(), // small size to force segment closure
..Default::default()
},
..Default::default()
};
let setup = TestSetup::init_with_config(config).await;
let mut system = System::new(
setup.config.clone(),
ClusterConfig::default(),
DataMaintenanceConfig::default(),
PersonalAccessTokenConfig::default(),
);
// Properties
let stream_id = Identifier::numeric(1)?;
let stream_name = "test";
let topic_name = "test_topic";
let topic_id = Identifier::numeric(1)?;
let partition_id = 1;
setup
.create_partition_directory(
stream_id.get_u32_value()?,
topic_id.get_u32_value()?,
partition_id,
)
.await;
let session = Session::new(1, 1, SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 1234));
system.init().await.unwrap();
system
.create_stream(&session, Some(stream_id.get_u32_value()?), stream_name)
.await
.unwrap();
system
.create_topic(
&session,
&stream_id,
Some(topic_id.get_u32_value()?),
topic_name,
partition_id,
IggyExpiry::default(),
CompressionAlgorithm::default(),
MaxTopicSize::default(),
None,
)
.await?;
let topic = system.find_topic(&session, &stream_id, &topic_id)?;
let partitions = topic.get_partitions();
let partition = partitions
.first()
.ok_or(IggyError::Error)
.inspect_err(|_| log::error!("Cannot retrieve initial partition."))?;
// Create multiple segments by sending messages one by one
// Each message should be large enough to cause a segment to close
for i in 0..5 {
let mut partition_lock = partition.write().await;
let payload = "payload that will force segment closure due to size";
let message = create_message(i, payload);
let messages = vec![message];
let messages_size = messages
.iter()
.map(|m| m.get_size_bytes().as_bytes_u32())
.sum();
let batch = IggyMessagesBatchMut::from_messages(&messages, messages_size);
partition_lock.append_messages(batch, None).await.unwrap();
drop(partition_lock);
sleep(Duration::from_millis(100)).await;
}
let partition_path = setup.config.get_partition_path(
stream_id.get_u32_value()?,
topic_id.get_u32_value()?,
partition_id,
);
let initial_segments = get_segment_paths_for_partition(&partition_path);
println!(
"Created segments: {:?}",
initial_segments
.iter()
.map(|e| e.file_name())
.collect::<Vec<_>>()
);
// Since we've created 5 messages with large payloads, we should have multiple segments
assert!(
initial_segments.len() >= 4,
"Expected at least 4 segments but got {}",
initial_segments.len()
);
let mut initial_offsets = get_segment_offsets(&initial_segments);
initial_offsets.sort();
println!("Initial segment offsets (sorted): {initial_offsets:?}");
let first_keep_count = 3usize;
system
.delete_segments(
&session,
&stream_id,
&topic_id,
partition_id,
first_keep_count as u32,
)
.await?;
let segments_after_first_delete = get_segment_paths_for_partition(&partition_path);
println!(
"Segments after first deletion (keep={}): {:?}",
first_keep_count,
segments_after_first_delete
.iter()
.map(|e| e.file_name())
.collect::<Vec<_>>()
);
// We should have at most first_keep_count + 1 segments after first deletion
// (keep_count closed segments + 1 open segment)
assert!(
segments_after_first_delete.len() <= first_keep_count + 1,
"Should have at most {} segments after first deletion but got {}",
first_keep_count + 1,
segments_after_first_delete.len()
);
// Second attempt to delete segments - keeping only 1 closed segment + 1 open
let second_keep_count = 1usize;
system
.delete_segments(
&session,
&stream_id,
&topic_id,
partition_id,
second_keep_count as u32,
)
.await?;
let segments_after_second_delete = get_segment_paths_for_partition(&partition_path);
println!(
"Segments after second deletion (keep={}): {:?}",
second_keep_count,
segments_after_second_delete
.iter()
.map(|e| e.file_name())
.collect::<Vec<_>>()
);
// We should have at most second_keep_count + 1 segments after second deletion
// (keep_count closed segments + 1 open segment)
assert!(
segments_after_second_delete.len() <= second_keep_count + 1,
"Should have at most {} segments after second deletion but got {}",
second_keep_count + 1,
segments_after_second_delete.len()
);
let mut final_offsets = get_segment_offsets(&segments_after_second_delete);
final_offsets.sort();
let highest_initial_offsets: Vec<u64> = initial_offsets
.iter()
.rev()
.take(second_keep_count + 1)
.copied()
.collect();
for &offset in &final_offsets {
assert!(
highest_initial_offsets.contains(&offset),
"Offset {offset} should not remain after final deletion"
);
}
Ok(())
}
// Helper function to extract segment offsets from DirEntry list
fn get_segment_offsets(segments: &[DirEntry]) -> Vec<u64> {
segments
.iter()
.filter_map(|entry| {
entry.file_name().to_str().and_then(|name| {
name.split('.')
.next()
.and_then(|offset_str| offset_str.parse::<u64>().ok())
})
})
.collect()
}
async fn assert_persisted_segment(partition_path: &str, start_offset: u64) {
let segment_path = format!("{partition_path}/{start_offset:0>20}");
let messages_file_path = format!("{segment_path}.{LOG_EXTENSION}");
let index_path = format!("{segment_path}.{INDEX_EXTENSION}");
assert!(fs::metadata(&messages_file_path).await.is_ok());
assert!(fs::metadata(&index_path).await.is_ok());
}
fn get_segment_paths_for_partition(partition_path: &str) -> Vec<DirEntry> {
std::fs::read_dir(partition_path)
.map(|read_dir| {
read_dir
.filter_map(|dir_entry| {
dir_entry
.map(|dir_entry| {
match dir_entry
.path()
.extension()
.is_some_and(|ext| ext == LOG_EXTENSION)
{
true => Some(dir_entry),
false => None,
}
})
.ok()
.flatten()
})
.collect::<Vec<_>>()
})
.unwrap_or_default()
}
fn get_start_offsets() -> Vec<u64> {
vec![
0, 1, 2, 9, 10, 99, 100, 110, 200, 1000, 1234, 12345, 100000, 9999999,
]
}