diff --git a/Cargo.lock b/Cargo.lock
index e97dd203ee3..77e60390cb0 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4624,15 +4624,6 @@ dependencies = [
"uuid",
]
-[[package]]
-name = "delta-encoding"
-version = "0.4.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cabe72c7de5725dcd4597e7d2bc8a4b7a4698879481607385fbc50a23203eeb9"
-dependencies = [
- "num-traits",
-]
-
[[package]]
name = "der"
version = "0.7.10"
@@ -8119,7 +8110,6 @@ dependencies = [
"common-wal",
"crc32fast",
"dashmap",
- "delta-encoding",
"derive_builder 0.20.2",
"futures",
"futures-util",
diff --git a/config/config.md b/config/config.md
index 381abde24c4..6faae09c347 100644
--- a/config/config.md
+++ b/config/config.md
@@ -604,8 +604,6 @@
| `wal.timeout` | String | `5s` | The total request timeout for kafka client. **It's only used when the provider is `kafka`**. |
| `wal.max_batch_bytes` | String | `1MB` | The max size of a single producer batch. Warning: Kafka has a default limit of 1MB per message in a topic. Defaults to `8MB` when the provider is `experimental_object_store`. **It's only used when the provider is `kafka` or `experimental_object_store`**. |
| `wal.consumer_wait_timeout` | String | `100ms` | The consumer wait timeout. **It's only used when the provider is `kafka`**. |
-| `wal.create_index` | Bool | `false` | Whether to enable WAL index creation. **It's only used when the provider is `kafka`**. |
-| `wal.dump_index_interval` | String | `60s` | The interval for dumping WAL indexes. **It's only used when the provider is `kafka`**. |
| `wal.overwrite_entry_start_id` | Bool | `false` | Ignore missing entries during read WAL. **It's only used when the provider is `kafka`**.
This option ensures that when Kafka messages are deleted, the system can still successfully replay memtable data without throwing an out-of-range error. However, enabling this option might lead to unexpected data loss, as the system will skip over missing entries instead of treating them as critical errors. |
| `wal.storage_provider` | String | `""` | The name of the storage provider that holds the WAL objects, an empty name selects the default object store. **It's only used when the provider is `experimental_object_store`**. |
| `wal.prefix` | String | `wal` | The path prefix of the WAL objects inside the storage provider. The objects are written under `/datanodes//epochs/`, which is derived from this prefix. **It's only used when the provider is `experimental_object_store`**. |
diff --git a/config/datanode.example.toml b/config/datanode.example.toml
index a2586908b70..2d72f795189 100644
--- a/config/datanode.example.toml
+++ b/config/datanode.example.toml
@@ -211,14 +211,6 @@ max_batch_bytes = "1MB"
## **It's only used when the provider is `kafka`**.
consumer_wait_timeout = "100ms"
-## Whether to enable WAL index creation.
-## **It's only used when the provider is `kafka`**.
-create_index = false
-
-## The interval for dumping WAL indexes.
-## **It's only used when the provider is `kafka`**.
-dump_index_interval = "60s"
-
## Ignore missing entries during read WAL.
## **It's only used when the provider is `kafka`**.
##
diff --git a/src/cmd/src/datanode/scanbench.rs b/src/cmd/src/datanode/scanbench.rs
index 42eac50ec45..9fbde16fb7b 100644
--- a/src/cmd/src/datanode/scanbench.rs
+++ b/src/cmd/src/datanode/scanbench.rs
@@ -836,7 +836,7 @@ impl ScanbenchCommand {
}
ScanbenchLogStore::Kafka(kafka_config) => {
let log_store = Arc::new(
- KafkaLogStore::try_new(kafka_config, None)
+ KafkaLogStore::try_new(kafka_config)
.await
.map_err(BoxedError::new)
.context(error::BuildCliSnafu)?,
diff --git a/src/common/meta/src/instruction.rs b/src/common/meta/src/instruction.rs
index 827f06e944d..0291f3a0946 100644
--- a/src/common/meta/src/instruction.rs
+++ b/src/common/meta/src/instruction.rs
@@ -381,7 +381,7 @@ pub struct UpgradeRegion {
/// it's helpful to verify whether the leader region is ready.
#[serde(with = "humantime_serde")]
pub replay_timeout: Duration,
- /// The hint for replaying memtable.
+ /// Legacy WAL index location hint, retained for older datanodes.
#[serde(default)]
pub location_id: Option,
#[serde(default, skip_serializing_if = "Option::is_none")]
diff --git a/src/common/wal/src/config.rs b/src/common/wal/src/config.rs
index 0cbebfb20ab..b92c3d13d36 100644
--- a/src/common/wal/src/config.rs
+++ b/src/common/wal/src/config.rs
@@ -346,6 +346,30 @@ mod tests {
assert!(toml::from_str::(toml_str).is_err());
}
+ #[test]
+ fn test_removed_datanode_wal_index_config() {
+ let expected = DatanodeWalConfig::Kafka(DatanodeKafkaConfig::default());
+ for create_index in [false, true] {
+ let config = format!(
+ r#"
+ provider = "kafka"
+ create_index = {create_index}
+ dump_index_interval = "10s"
+ "#
+ );
+ let parsed: DatanodeWalConfig = toml::from_str(&config).unwrap();
+ assert_eq!(parsed, expected);
+ let serialized = toml::to_string(&parsed).unwrap();
+ let table: toml::Table = toml::from_str(&serialized).unwrap();
+ assert!(!table.contains_key("create_index"));
+ assert!(!table.contains_key("dump_index_interval"));
+ assert_eq!(
+ toml::from_str::(&serialized).unwrap(),
+ expected
+ );
+ }
+ }
+
#[test]
fn test_kafka_wal_config_debug_redacts_password() {
let config = MetasrvWalConfig::Kafka(MetasrvKafkaConfig {
diff --git a/src/common/wal/src/config/kafka/datanode.rs b/src/common/wal/src/config/kafka/datanode.rs
index 4003b52e4bc..38836509f0a 100644
--- a/src/common/wal/src/config/kafka/datanode.rs
+++ b/src/common/wal/src/config/kafka/datanode.rs
@@ -40,10 +40,6 @@ pub struct DatanodeKafkaConfig {
pub kafka_topic: KafkaTopicConfig,
// Automatically create topics for WAL.
pub auto_create_topics: bool,
- // Create index for WAL.
- pub create_index: bool,
- #[serde(with = "humantime_serde")]
- pub dump_index_interval: Duration,
/// Internal interval for fetching latest Kafka topic offsets.
#[serde(with = "humantime_serde")]
pub topic_latest_offset_fetch_interval: Duration,
@@ -60,8 +56,6 @@ impl Default for DatanodeKafkaConfig {
consumer_wait_timeout: Duration::from_millis(100),
kafka_topic: KafkaTopicConfig::default(),
auto_create_topics: true,
- create_index: false,
- dump_index_interval: Duration::from_secs(60),
topic_latest_offset_fetch_interval: DEFAULT_TOPIC_LATEST_OFFSET_FETCH_INTERVAL,
overwrite_entry_start_id: false,
}
diff --git a/src/datanode/src/datanode.rs b/src/datanode/src/datanode.rs
index cfbcf948b96..8edcd2ce4a1 100644
--- a/src/datanode/src/datanode.rs
+++ b/src/datanode/src/datanode.rs
@@ -46,7 +46,6 @@ use common_wal::config::raft_engine::RaftEngineConfig;
use file_engine::engine::FileRegionEngine;
use log_store::ObjectStoreLogStore;
use log_store::kafka::log_store::KafkaLogStore;
-use log_store::kafka::{GlobalIndexCollector, default_index_file};
use log_store::noop::log_store::NoopLogStore;
use log_store::raft_engine::log_store::RaftEngineLogStore;
use meta_client::MetaClientRef;
@@ -88,7 +87,7 @@ use crate::heartbeat::HeartbeatTask;
use crate::partition_expr_fetcher::MetaPartitionExprFetcher;
use crate::region_query::DatanodeRegionQueryHandler;
use crate::region_server::{DummyTableProviderFactory, RegionServer};
-use crate::store::{self, new_object_store_without_cache};
+use crate::store;
use crate::utils::{RegionOpenRequests, build_region_open_requests};
/// Datanode service.
@@ -683,29 +682,7 @@ impl DatanodeBuilder {
builder.try_build().await.context(BuildMitoEngineSnafu)?
}
DatanodeWalConfig::Kafka(kafka_config) => {
- if kafka_config.create_index && opts.node_id.is_none() {
- warn!("The WAL index creation only available in distributed mode.")
- }
- let global_index_collector = if kafka_config.create_index
- && let Some(node_id) = opts.node_id
- {
- let operator = new_object_store_without_cache(
- &opts.storage.store,
- &opts.storage.data_home,
- )
- .await?;
- let path = default_index_file(node_id);
- Some(Self::build_global_index_collector(
- kafka_config.dump_index_interval,
- operator,
- path,
- ))
- } else {
- None
- };
-
- let log_store =
- Self::build_kafka_log_store(kafka_config, global_index_collector).await?;
+ let log_store = Self::build_kafka_log_store(kafka_config).await?;
self.topic_stats_reporter = Some(log_store.topic_stats_reporter());
let builder = MitoEngineBuilder::new(
&opts.storage.data_home,
@@ -825,25 +802,13 @@ impl DatanodeBuilder {
}
/// Builds [`KafkaLogStore`].
- async fn build_kafka_log_store(
- config: &DatanodeKafkaConfig,
- global_index_collector: Option,
- ) -> Result> {
- KafkaLogStore::try_new(config, global_index_collector)
+ async fn build_kafka_log_store(config: &DatanodeKafkaConfig) -> Result> {
+ KafkaLogStore::try_new(config)
.await
.map_err(Box::new)
.context(OpenLogStoreSnafu)
.map(Arc::new)
}
-
- /// Builds [`GlobalIndexCollector`]
- fn build_global_index_collector(
- dump_index_interval: Duration,
- operator: object_store::ObjectStore,
- path: String,
- ) -> GlobalIndexCollector {
- GlobalIndexCollector::new(dump_index_interval, operator, path)
- }
}
/// Keeps the first shutdown error and logs the later ones.
diff --git a/src/datanode/src/heartbeat/handler/upgrade_region.rs b/src/datanode/src/heartbeat/handler/upgrade_region.rs
index e2a5677fcad..08f96753ae9 100644
--- a/src/datanode/src/heartbeat/handler/upgrade_region.rs
+++ b/src/datanode/src/heartbeat/handler/upgrade_region.rs
@@ -130,7 +130,6 @@ impl UpgradeRegionsHandler {
let UpgradeRegion {
last_entry_id,
metadata_last_entry_id,
- location_id,
replay_entry_id,
metadata_replay_entry_id,
replay_timeout,
@@ -161,7 +160,6 @@ impl UpgradeRegionsHandler {
set_writable: true,
entry_id: last_entry_id,
metadata_entry_id: metadata_last_entry_id,
- location_id,
checkpoint,
},
));
@@ -320,6 +318,49 @@ mod tests {
}
}
+ #[tokio::test]
+ async fn test_legacy_wal_index_hint() {
+ for location_id in [None, Some(42)] {
+ let mock_region_server = mock_region_server();
+ let region_id = RegionId::new(1024, 1);
+ let (mock_engine, _) =
+ MockRegionEngine::with_custom_apply_fn(MITO_ENGINE_NAME, |engine| {
+ engine.mock_role = Some(Some(RegionRole::Follower));
+ engine.handle_request_mock_fn = Some(Box::new(|_, request| {
+ let store_api::region_request::RegionRequest::Catchup(request) = request
+ else {
+ panic!("expected catchup request");
+ };
+ assert!(request.set_writable);
+ assert_eq!(request.entry_id, Some(10));
+ assert_eq!(request.metadata_entry_id, Some(5));
+ Ok(0)
+ }));
+ });
+ mock_region_server.register_test_region(region_id, mock_engine);
+ let ctx =
+ HandlerContext::new_for_test(mock_region_server, Arc::new(MemoryKvBackend::new()));
+ let reply = UpgradeRegionsHandler::new_test()
+ .handle(
+ &ctx,
+ vec![UpgradeRegion {
+ region_id,
+ last_entry_id: Some(10),
+ metadata_last_entry_id: Some(5),
+ location_id,
+ replay_timeout: Duration::from_secs(1),
+ ..Default::default()
+ }],
+ )
+ .await
+ .unwrap();
+ let reply = &reply.expect_upgrade_regions_reply()[0];
+ assert!(reply.ready);
+ assert!(reply.exists);
+ assert!(reply.error.is_none());
+ }
+ }
+
#[tokio::test]
async fn test_region_not_ready() {
let mock_region_server = mock_region_server();
diff --git a/src/datanode/src/store.rs b/src/datanode/src/store.rs
index 78db3d61033..3369de5aedc 100644
--- a/src/datanode/src/store.rs
+++ b/src/datanode/src/store.rs
@@ -23,25 +23,6 @@ use snafu::prelude::*;
use crate::config::ObjectStoreConfig;
use crate::error::{self, Result};
-pub(crate) async fn new_object_store_without_cache(
- store: &ObjectStoreConfig,
- data_home: &str,
-) -> Result {
- let object_store = new_raw_object_store(store, data_home)
- .await
- .context(error::ObjectStoreSnafu)?;
- // Enable retry layer and cache layer for non-fs object storages
- let object_store = if store.is_object_storage() {
- // Adds retry layer
- with_retry_layers(object_store)
- } else {
- object_store
- };
-
- let object_store = with_instrument_layers(object_store, true);
- Ok(object_store)
-}
-
/// Cleans up old LRU read cache directories that were removed.
fn clean_old_read_cache(store: &ObjectStoreConfig, data_home: &str) {
if !store.is_object_storage() {
diff --git a/src/log-store/Cargo.toml b/src/log-store/Cargo.toml
index 5870110be67..54e0b48c41d 100644
--- a/src/log-store/Cargo.toml
+++ b/src/log-store/Cargo.toml
@@ -30,7 +30,6 @@ common-time.workspace = true
common-wal.workspace = true
crc32fast.workspace = true
dashmap.workspace = true
-delta-encoding = "0.4"
derive_builder.workspace = true
futures.workspace = true
futures-util.workspace = true
diff --git a/src/log-store/src/error.rs b/src/log-store/src/error.rs
index 64d918d5d78..836d1fbfe7d 100644
--- a/src/log-store/src/error.rs
+++ b/src/log-store/src/error.rs
@@ -259,39 +259,6 @@ pub enum Error {
error: tokio::sync::oneshot::error::RecvError,
},
- #[snafu(display("Failed to wait for result of DumpIndex"))]
- WaitDumpIndex {
- #[snafu(implicit)]
- location: Location,
- #[snafu(source)]
- error: tokio::sync::oneshot::error::RecvError,
- },
-
- #[snafu(display("Failed to create writer"))]
- CreateWriter {
- #[snafu(implicit)]
- location: Location,
- #[snafu(source)]
- error: object_store::Error,
- },
-
- #[snafu(display("Failed to write index"))]
- WriteIndex {
- #[snafu(implicit)]
- location: Location,
- #[snafu(source)]
- error: object_store::Error,
- },
-
- #[snafu(display("Failed to read index, path: {path}"))]
- ReadIndex {
- #[snafu(implicit)]
- location: Location,
- #[snafu(source)]
- error: object_store::Error,
- path: String,
- },
-
#[snafu(display(
"The length of meta if exceeded the limit: {}, actual: {}",
limit,
@@ -516,7 +483,6 @@ impl ErrorExt for Error {
| DiscontinuousLogIndex { .. }
| OrderedBatchProducerStopped { .. }
| WaitProduceResultReceiver { .. }
- | WaitDumpIndex { .. }
| MetaLengthExceededLimit { .. }
| ObjectStoreWalStopped { .. }
| WalSegmentFetch { .. } => StatusCode::Internal,
@@ -531,10 +497,7 @@ impl ErrorExt for Error {
ObjectStoreWal { source, .. } => source.status_code(),
// Object store related errors
- CreateWriter { .. }
- | WriteIndex { .. }
- | ReadIndex { .. }
- | WalObjectStore { .. }
+ WalObjectStore { .. }
| StaleWalObject { .. }
| UnconfirmedWalEpochStart { .. }
| Io { .. } => StatusCode::StorageUnavailable,
@@ -563,10 +526,7 @@ impl ErrorExt for Error {
use Error::*;
match self {
- CreateWriter { error, .. }
- | WriteIndex { error, .. }
- | ReadIndex { error, .. }
- | WalObjectStore { error, .. } => retry_hint_from_opendal_error(error),
+ WalObjectStore { error, .. } => retry_hint_from_opendal_error(error),
ObjectStoreWal { source, .. } => source.retry_hint(),
Io { error, .. } => retry_hint_from_io_error(error),
FetchEntry { .. }
diff --git a/src/log-store/src/kafka.rs b/src/log-store/src/kafka.rs
index 2e123cc059c..810e06bb103 100644
--- a/src/log-store/src/kafka.rs
+++ b/src/log-store/src/kafka.rs
@@ -14,7 +14,6 @@
pub(crate) mod client_manager;
pub(crate) mod consumer;
-pub(crate) mod index;
pub mod log_store;
mod periodic_offset_fetcher;
pub(crate) mod producer;
@@ -24,7 +23,6 @@ pub(crate) mod util;
pub(crate) mod worker;
pub use client_manager::DEFAULT_PARTITION;
-pub use index::{GlobalIndexCollector, default_index_file};
use serde::{Deserialize, Serialize};
use store_api::logstore::entry::Id as EntryId;
diff --git a/src/log-store/src/kafka/client_manager.rs b/src/log-store/src/kafka/client_manager.rs
index e594c9148eb..d1cfa2dba41 100644
--- a/src/log-store/src/kafka/client_manager.rs
+++ b/src/log-store/src/kafka/client_manager.rs
@@ -25,7 +25,6 @@ use store_api::logstore::provider::KafkaProvider;
use tokio::sync::{Mutex, RwLock};
use crate::error::{BuildClientSnafu, BuildPartitionClientSnafu, Result, TlsConfigSnafu};
-use crate::kafka::index::{GlobalIndexCollector, NoopCollector};
use crate::kafka::log_store::TopicStat;
use crate::kafka::producer::{OrderedBatchProducer, OrderedBatchProducerRef};
@@ -60,7 +59,6 @@ pub(crate) struct ClientManager {
/// Used to initialize a new [Client].
mutex: Mutex<()>,
instances: RwLock, Client>>,
- global_index_collector: Option,
flush_batch_size: usize,
compression: Compression,
@@ -73,7 +71,6 @@ impl ClientManager {
/// Tries to create a ClientManager.
pub(crate) async fn try_new(
config: &DatanodeKafkaConfig,
- global_index_collector: Option,
topic_stats: Arc, TopicStat>>,
) -> Result {
// Sets backoff config for the top-level kafka client and all clients constructed by it.
@@ -98,7 +95,6 @@ impl ClientManager {
instances: RwLock::new(HashMap::new()),
flush_batch_size: config.max_batch_bytes.as_bytes() as usize,
compression: Compression::Lz4,
- global_index_collector,
topic_stats,
})
}
@@ -151,30 +147,18 @@ impl ClientManager {
.map(Arc::new)?;
let (tx, rx) = OrderedBatchProducer::channel();
- let index_collector = if let Some(global_collector) = self.global_index_collector.as_ref() {
- global_collector
- .provider_level_index_collector(provider.clone(), tx.clone())
- .await
- } else {
- Box::new(NoopCollector)
- };
let producer = Arc::new(OrderedBatchProducer::new(
(tx, rx),
provider.clone(),
client.clone(),
self.compression,
self.flush_batch_size,
- index_collector,
self.topic_stats.clone(),
));
Ok(Client { client, producer })
}
- pub(crate) fn global_index_collector(&self) -> Option<&GlobalIndexCollector> {
- self.global_index_collector.as_ref()
- }
-
/// Lists all topics.
pub(crate) async fn list_topics(&self) -> Vec> {
self.instances.read().await.keys().cloned().collect()
diff --git a/src/log-store/src/kafka/consumer.rs b/src/log-store/src/kafka/consumer.rs
index d064a9bc9ac..d8c22109974 100644
--- a/src/log-store/src/kafka/consumer.rs
+++ b/src/log-store/src/kafka/consumer.rs
@@ -12,6 +12,10 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+mod range;
+
+pub(crate) use range::RegionWalRange;
+
use std::collections::VecDeque;
use std::ops::Range;
use std::pin::Pin;
@@ -26,7 +30,7 @@ use pin_project::pin_project;
use rskafka::client::partition::PartitionClient;
use rskafka::record::RecordAndOffset;
-use crate::kafka::index::{NextBatchHint, RegionWalIndexIterator};
+use crate::kafka::consumer::range::NextBatchHint;
pub struct FetchResult {
/// The offsets of the fetched records.
@@ -88,7 +92,7 @@ const MAX_BATCH_SIZE: usize = 52428800;
const AVG_RECORD_SIZE: usize = 256 * 1024;
/// The [`Consumer`] struct represents a Kafka consumer that fetches messages from
-/// a Kafka cluster. Yielding records respecting the [`RegionWalIndexIterator`].
+/// a Kafka cluster. Yielding records respecting the [`RegionWalRange`].
#[pin_project]
#[derive(Builder)]
#[builder(pattern = "owned")]
@@ -142,12 +146,12 @@ impl Consumer {
pub(crate) struct RecordsBuffer {
buffer: VecDeque,
- index: Box,
+ index: RegionWalRange,
}
impl RecordsBuffer {
/// Creates an empty [`RecordsBuffer`]
- pub fn new(index: Box) -> Self {
+ pub fn new(index: RegionWalRange) -> Self {
RecordsBuffer {
buffer: VecDeque::new(),
index,
@@ -297,7 +301,6 @@ mod tests {
use super::*;
use crate::kafka::consumer::{Consumer, RecordsBuffer};
- use crate::kafka::index::{MultipleRegionWalIndexIterator, RegionWalRange, RegionWalVecIndex};
#[derive(Debug)]
struct MockFetchClient {
@@ -345,40 +348,6 @@ mod tests {
}
}
- #[tokio::test]
- async fn test_consumer_with_index() {
- common_telemetry::init_default_ut_logging();
- let record = test_record();
- let record_size = record.approximate_size();
- let mock_client = MockFetchClient {
- record: record.clone(),
- };
- let index = RegionWalVecIndex::new([1, 3, 4, 8, 10, 12], record_size * 3);
- let consumer = Consumer {
- last_high_watermark: -1,
- client: Arc::new(mock_client),
- max_batch_size: usize::MAX,
- max_wait_ms: 500,
- avg_record_size: record_size,
- terminated: false,
- buffer: RecordsBuffer {
- buffer: VecDeque::new(),
- index: Box::new(index),
- },
- fetch_fut: Fuse::terminated(),
- total_fetched_bytes: 0,
- };
-
- let records = consumer.try_collect::>().await.unwrap();
- assert_eq!(
- records
- .into_iter()
- .map(|(x, _)| x.offset)
- .collect::>(),
- vec![1, 3, 4, 8, 10, 12]
- )
- }
-
#[tokio::test]
async fn test_consumer_without_index() {
common_telemetry::init_default_ut_logging();
@@ -396,7 +365,7 @@ mod tests {
terminated: false,
buffer: RecordsBuffer {
buffer: VecDeque::new(),
- index: Box::new(index),
+ index,
},
fetch_fut: Fuse::terminated(),
total_fetched_bytes: 0,
@@ -413,44 +382,34 @@ mod tests {
}
#[tokio::test]
- async fn test_consumer_with_multiple_index() {
- common_telemetry::init_default_ut_logging();
- let record = test_record();
- let mock_client = MockFetchClient {
- record: record.clone(),
- };
-
- let iter0 = Box::new(RegionWalRange::new(0..0, 1024)) as _;
- let iter1 = Box::new(RegionWalVecIndex::new(
- [0, 1, 2, 7, 8, 11],
- record.approximate_size() * 4,
- )) as _;
- let iter2 = Box::new(RegionWalRange::new(12..12, 1024)) as _;
- let iter3 = Box::new(RegionWalRange::new(1024..1028, 1024)) as _;
- let iter = MultipleRegionWalIndexIterator::new([iter0, iter1, iter2, iter3]);
-
- let consumer = Consumer {
- last_high_watermark: -1,
- client: Arc::new(mock_client),
- max_batch_size: usize::MAX,
- max_wait_ms: 500,
- avg_record_size: record.approximate_size(),
- terminated: false,
- buffer: RecordsBuffer {
- buffer: VecDeque::new(),
- index: Box::new(iter),
- },
- fetch_fut: Fuse::terminated(),
- total_fetched_bytes: 0,
- };
-
- let records = consumer.try_collect::>().await.unwrap();
- assert_eq!(
- records
+ async fn test_consumer_range_boundaries() {
+ for (start, end, max_batch_size) in [
+ (0, 0, 1024),
+ (5, 5, 1024),
+ (6, 5, 1024),
+ (5, 6, 1024),
+ (5, 35, 128),
+ ] {
+ let record = test_record();
+ let consumer = ConsumerBuilder::default()
+ .client(Arc::new(MockFetchClient {
+ record: record.clone(),
+ }))
+ .buffer(RecordsBuffer::new(RegionWalRange::new(
+ start..end,
+ max_batch_size,
+ )))
+ .avg_record_size(record.approximate_size())
+ .build()
+ .unwrap();
+ let offsets = consumer
+ .try_collect::>()
+ .await
+ .unwrap()
.into_iter()
- .map(|(x, _)| x.offset)
- .collect::>(),
- [0, 1, 2, 7, 8, 11, 1024, 1025, 1026, 1027]
- )
+ .map(|(record, _)| record.offset as u64)
+ .collect::>();
+ assert_eq!(offsets, (start..end).collect::>());
+ }
}
}
diff --git a/src/log-store/src/kafka/consumer/range.rs b/src/log-store/src/kafka/consumer/range.rs
new file mode 100644
index 00000000000..d4593b6f20c
--- /dev/null
+++ b/src/log-store/src/kafka/consumer/range.rs
@@ -0,0 +1,118 @@
+// Copyright 2023 Greptime Team
+//
+// Licensed 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 std::cmp::min;
+use std::ops::Range;
+
+use store_api::logstore::EntryId;
+
+#[derive(Debug, PartialEq, Eq)]
+pub(crate) struct NextBatchHint {
+ pub(crate) bytes: usize,
+ pub(crate) len: usize,
+}
+
+/// Represents a range [next_entry_id, end_entry_id) of WAL entries for a region.
+#[derive(Debug)]
+pub struct RegionWalRange {
+ current_entry_id: EntryId,
+ end_entry_id: EntryId,
+ max_batch_size: usize,
+}
+
+impl RegionWalRange {
+ pub fn new(range: Range, max_batch_size: usize) -> Self {
+ Self {
+ current_entry_id: range.start,
+ end_entry_id: range.end,
+ max_batch_size,
+ }
+ }
+
+ fn next_batch_size(&self) -> Option {
+ if self.current_entry_id < self.end_entry_id {
+ Some(self.end_entry_id.saturating_sub(self.current_entry_id))
+ } else {
+ None
+ }
+ }
+
+ pub(crate) fn next_batch_hint(&self, avg_size: usize) -> Option {
+ if let Some(size) = self.next_batch_size() {
+ let bytes = min(size as usize * avg_size, self.max_batch_size);
+ let len = bytes / avg_size;
+
+ return Some(NextBatchHint { bytes, len });
+ }
+
+ None
+ }
+
+ pub(crate) fn peek(&self) -> Option {
+ if self.current_entry_id < self.end_entry_id {
+ Some(self.current_entry_id)
+ } else {
+ None
+ }
+ }
+
+ pub(crate) fn next(&mut self) -> Option {
+ if self.current_entry_id < self.end_entry_id {
+ let next = self.current_entry_id;
+ self.current_entry_id += 1;
+ Some(next)
+ } else {
+ None
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_region_wal_range() {
+ let range = RegionWalRange::new(0..1024, 1024);
+ assert_eq!(
+ range.next_batch_hint(10),
+ Some(NextBatchHint {
+ bytes: 1024,
+ len: 102
+ })
+ );
+
+ let mut range = RegionWalRange::new(0..1, 1024);
+
+ assert_eq!(range.next_batch_size(), Some(1));
+ assert_eq!(range.peek(), Some(0));
+
+ // Advance 1 step
+ assert_eq!(range.next(), Some(0));
+ assert_eq!(range.next_batch_size(), None);
+
+ // Advance 1 step
+ assert_eq!(range.next(), None);
+ assert_eq!(range.next_batch_size(), None);
+ // No effect
+ assert_eq!(range.next(), None);
+ assert_eq!(range.next_batch_size(), None);
+
+ let mut range = RegionWalRange::new(0..0, 1024);
+ assert_eq!(range.next_batch_size(), None);
+ // No effect
+ assert_eq!(range.next(), None);
+ assert_eq!(range.next_batch_size(), None);
+ }
+}
diff --git a/src/log-store/src/kafka/index.rs b/src/log-store/src/kafka/index.rs
deleted file mode 100644
index 23a9378ccb2..00000000000
--- a/src/log-store/src/kafka/index.rs
+++ /dev/null
@@ -1,30 +0,0 @@
-// Copyright 2023 Greptime Team
-//
-// Licensed 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.
-
-mod collector;
-mod encoder;
-mod iterator;
-
-pub use collector::GlobalIndexCollector;
-pub(crate) use collector::{IndexCollector, NoopCollector};
-pub(crate) use encoder::{IndexEncoder, JsonIndexEncoder};
-pub(crate) use iterator::{
- MIN_BATCH_WINDOW_SIZE, NextBatchHint, RegionWalIndexIterator, build_region_wal_index_iterator,
-};
-#[cfg(test)]
-pub(crate) use iterator::{MultipleRegionWalIndexIterator, RegionWalRange, RegionWalVecIndex};
-
-pub fn default_index_file(location_id: u64) -> String {
- format!("__wal/{location_id}/index.json")
-}
diff --git a/src/log-store/src/kafka/index/collector.rs b/src/log-store/src/kafka/index/collector.rs
deleted file mode 100644
index 54b9b21553d..00000000000
--- a/src/log-store/src/kafka/index/collector.rs
+++ /dev/null
@@ -1,414 +0,0 @@
-// Copyright 2023 Greptime Team
-//
-// Licensed 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 std::collections::{BTreeSet, HashMap};
-use std::sync::Arc;
-use std::sync::atomic::{AtomicBool, Ordering};
-use std::time::Duration;
-
-use common_telemetry::{error, info};
-use futures::future::try_join_all;
-use object_store::ErrorKind;
-use serde::{Deserialize, Serialize};
-use snafu::ResultExt;
-use store_api::logstore::EntryId;
-use store_api::logstore::provider::KafkaProvider;
-use store_api::storage::RegionId;
-use tokio::select;
-use tokio::sync::Mutex as TokioMutex;
-use tokio::sync::mpsc::Sender;
-
-use crate::error::{self, Result};
-use crate::kafka::index::encoder::{DatanodeWalIndexes, IndexEncoder};
-use crate::kafka::index::{JsonIndexEncoder, default_index_file};
-use crate::kafka::worker::{DumpIndexRequest, TruncateIndexRequest, WorkerRequest};
-
-/// The [`IndexCollector`] trait defines the operations for managing and collecting index entries.
-pub trait IndexCollector: Send + Sync {
- /// Appends an [`EntryId`] for a specific region.
- fn append(&mut self, region_id: RegionId, entry_id: EntryId);
-
- /// Truncates the index for a specific region up to a given [`EntryId`].
- ///
- /// It removes all [`EntryId`]s smaller than `entry_id`.
- fn truncate(&mut self, region_id: RegionId, entry_id: EntryId);
-
- /// Sets the latest [`EntryId`].
- fn set_latest_entry_id(&mut self, entry_id: EntryId);
-
- /// Dumps the index.
- fn dump(&mut self, encoder: &dyn IndexEncoder);
-}
-
-/// The [`GlobalIndexCollector`] struct is responsible for managing index entries
-/// across multiple providers.
-#[derive(Debug)]
-pub struct GlobalIndexCollector {
- providers: Arc, Sender>>>,
- operator: object_store::ObjectStore,
- _handle: CollectionTaskHandle,
-}
-
-#[derive(Debug, Clone)]
-pub struct CollectionTask {
- providers: Arc, Sender>>>,
- dump_index_interval: Duration,
- operator: object_store::ObjectStore,
- path: String,
- running: Arc,
-}
-
-impl CollectionTask {
- async fn dump_index(&self) -> Result<()> {
- let encoder = Arc::new(JsonIndexEncoder::default());
- let receivers = {
- let providers = self.providers.lock().await;
- let mut receivers = Vec::with_capacity(providers.len());
- for (provider, sender) in providers.iter() {
- let (req, rx) = DumpIndexRequest::new(encoder.clone());
- receivers.push(rx);
- if sender.send(WorkerRequest::DumpIndex(req)).await.is_err() {
- error!(
- "BackgroundProducerWorker is stopped, topic: {}",
- provider.topic
- )
- }
- }
- receivers
- };
- try_join_all(receivers)
- .await
- .context(error::WaitDumpIndexSnafu)?;
- let bytes = encoder.finish()?;
- let mut writer = self
- .operator
- .writer(&self.path)
- .await
- .context(error::CreateWriterSnafu)?;
- writer.write(bytes).await.context(error::WriteIndexSnafu)?;
- writer.close().await.context(error::WriteIndexSnafu)?;
-
- Ok(())
- }
-
- /// The background task performs two main operations:
- /// - Persists the WAL index to the specified `path` at every `dump_index_interval`.
- /// - Updates the latest index ID for each WAL provider at every `checkpoint_interval`.
- fn run(self) -> CollectionTaskHandle {
- let mut dump_index_interval = tokio::time::interval(self.dump_index_interval);
- let running = self.running.clone();
- let moved_self = self.clone();
- common_runtime::spawn_global(async move {
- loop {
- if !running.load(Ordering::Relaxed) {
- info!("shutdown the index collection task");
- break;
- }
- select! {
- _ = dump_index_interval.tick() => {
- if let Err(err) = moved_self.dump_index().await {
- error!(err; "Failed to persist the WAL index");
- }
- },
- }
- }
- });
- CollectionTaskHandle {
- running: self.running.clone(),
- }
- }
-}
-
-impl Drop for CollectionTaskHandle {
- fn drop(&mut self) {
- self.running.store(false, Ordering::Relaxed);
- }
-}
-
-#[derive(Debug, Default)]
-struct CollectionTaskHandle {
- running: Arc,
-}
-
-impl GlobalIndexCollector {
- /// Constructs a [`GlobalIndexCollector`].
- ///
- /// This method initializes a `GlobalIndexCollector` instance and starts a background task
- /// for managing WAL (Write-Ahead Logging) indexes.
- ///
- /// The background task persists the WAL index to the specified `path` at every `dump_index_interval`.
- pub fn new(
- dump_index_interval: Duration,
- operator: object_store::ObjectStore,
- path: String,
- ) -> Self {
- let providers: Arc, Sender>>> =
- Arc::new(Default::default());
- let task = CollectionTask {
- providers: providers.clone(),
- dump_index_interval,
- operator: operator.clone(),
- path,
- running: Arc::new(AtomicBool::new(true)),
- };
- let handle = task.run();
- Self {
- providers,
- operator,
- _handle: handle,
- }
- }
-
- #[cfg(test)]
- pub fn new_for_test(operator: object_store::ObjectStore) -> Self {
- Self {
- providers: Default::default(),
- operator,
- _handle: Default::default(),
- }
- }
-}
-
-impl GlobalIndexCollector {
- /// Retrieve [`EntryId`]s for a specified `region_id` in `datanode_id`
- /// that are greater than or equal to a given `entry_id`.
- pub(crate) async fn read_remote_region_index(
- &self,
- location_id: u64,
- provider: &KafkaProvider,
- region_id: RegionId,
- entry_id: EntryId,
- ) -> Result