1pub mod kafka;
16pub mod object_store;
17pub mod raft_engine;
18
19use std::time::Duration;
20
21use serde::{Deserialize, Serialize};
22
23use crate::config::kafka::common::{
24 DEFAULT_AUTO_PRUNE_INTERVAL, DEFAULT_AUTO_PRUNE_LOGICAL_DELETE, DEFAULT_AUTO_PRUNE_PARALLELISM,
25 DEFAULT_CHECKPOINT_TRIGGER_SIZE, DEFAULT_FLUSH_TRIGGER_SIZE,
26 DEFAULT_PERIODIC_CHECKPOINT_PERSIST_INTERVAL, DEFAULT_REGION_FLUSH_TRIGGER_INTERVAL,
27};
28use crate::config::kafka::{DatanodeKafkaConfig, MetasrvKafkaConfig};
29use crate::config::object_store::ObjectStoreWalConfig;
30use crate::config::raft_engine::RaftEngineConfig;
31use crate::error::{Error, UnsupportedWalProviderSnafu};
32
33#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
35#[serde(tag = "provider", rename_all = "snake_case")]
36#[allow(clippy::large_enum_variant)]
37pub enum MetasrvWalConfig {
38 #[default]
39 RaftEngine,
40 Kafka(MetasrvKafkaConfig),
41}
42
43#[allow(clippy::large_enum_variant)]
44#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
46#[serde(tag = "provider", rename_all = "snake_case")]
47pub enum DatanodeWalConfig {
48 RaftEngine(RaftEngineConfig),
49 Kafka(DatanodeKafkaConfig),
50 Noop,
51 #[serde(rename = "experimental_object_store")]
52 ObjectStore(ObjectStoreWalConfig),
53}
54
55impl Default for DatanodeWalConfig {
56 fn default() -> Self {
57 Self::RaftEngine(RaftEngineConfig::default())
58 }
59}
60
61impl TryFrom<DatanodeWalConfig> for MetasrvWalConfig {
62 type Error = Error;
63
64 fn try_from(config: DatanodeWalConfig) -> Result<Self, Self::Error> {
65 match config {
66 DatanodeWalConfig::RaftEngine(_) => Ok(Self::RaftEngine),
67 DatanodeWalConfig::Kafka(config) => Ok(Self::Kafka(MetasrvKafkaConfig {
68 connection: config.connection,
69 kafka_topic: config.kafka_topic,
70 auto_create_topics: config.auto_create_topics,
71 auto_prune_interval: DEFAULT_AUTO_PRUNE_INTERVAL,
73 auto_prune_logical_delete: DEFAULT_AUTO_PRUNE_LOGICAL_DELETE,
75 auto_prune_parallelism: DEFAULT_AUTO_PRUNE_PARALLELISM,
77 flush_trigger_size: DEFAULT_FLUSH_TRIGGER_SIZE,
79 checkpoint_trigger_size: DEFAULT_CHECKPOINT_TRIGGER_SIZE,
81 region_flush_trigger_interval: DEFAULT_REGION_FLUSH_TRIGGER_INTERVAL,
83 periodic_checkpoint_persist_interval: DEFAULT_PERIODIC_CHECKPOINT_PERSIST_INTERVAL,
85 })),
86 DatanodeWalConfig::Noop => UnsupportedWalProviderSnafu {
87 provider: "noop".to_string(),
88 }
89 .fail(),
90 DatanodeWalConfig::ObjectStore(_) => UnsupportedWalProviderSnafu {
91 provider: "experimental_object_store".to_string(),
92 }
93 .fail(),
94 }
95 }
96}
97
98impl MetasrvWalConfig {
99 pub fn enable_active_wal_pruning(&self) -> bool {
101 match self {
102 MetasrvWalConfig::RaftEngine => false,
103 MetasrvWalConfig::Kafka(config) => config.auto_prune_interval > Duration::ZERO,
104 }
105 }
106
107 pub fn remote_wal_options(&self) -> Option<&MetasrvKafkaConfig> {
109 match self {
110 MetasrvWalConfig::RaftEngine => None,
111 MetasrvWalConfig::Kafka(config) => Some(config),
112 }
113 }
114}
115
116impl From<MetasrvWalConfig> for DatanodeWalConfig {
117 fn from(config: MetasrvWalConfig) -> Self {
118 match config {
119 MetasrvWalConfig::RaftEngine => Self::RaftEngine(RaftEngineConfig::default()),
120 MetasrvWalConfig::Kafka(config) => Self::Kafka(DatanodeKafkaConfig {
121 connection: config.connection,
122 kafka_topic: config.kafka_topic,
123 ..Default::default()
124 }),
125 }
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use std::time::Duration;
132
133 use common_base::readable_size::ReadableSize;
134 use kafka::common::{
135 KafkaClientSasl, KafkaClientSaslConfig, KafkaClientTls, KafkaConnectionConfig,
136 };
137 use tests::kafka::common::KafkaTopicConfig;
138
139 use super::*;
140 use crate::TopicSelectorType;
141 use crate::config::object_store::{AckMode, CorruptedSegmentAction};
142 use crate::config::{DatanodeKafkaConfig, MetasrvKafkaConfig};
143
144 #[test]
145 fn test_toml_raft_engine() {
146 let toml_str = r#"
148 provider = "raft_engine"
149 "#;
150 let metasrv_wal_config: MetasrvWalConfig = toml::from_str(toml_str).unwrap();
151 assert_eq!(metasrv_wal_config, MetasrvWalConfig::RaftEngine);
152
153 let datanode_wal_config: DatanodeWalConfig = toml::from_str(toml_str).unwrap();
154 assert_eq!(
155 datanode_wal_config,
156 DatanodeWalConfig::RaftEngine(RaftEngineConfig::default())
157 );
158
159 let toml_str = r#"
161 provider = "raft_engine"
162 broker_endpoints = ["127.0.0.1:9092"]
163 num_topics = 32
164 "#;
165 let datanode_wal_config: DatanodeWalConfig = toml::from_str(toml_str).unwrap();
166 assert_eq!(
167 datanode_wal_config,
168 DatanodeWalConfig::RaftEngine(RaftEngineConfig::default())
169 );
170
171 let toml_str = r#"
173 provider = "raft_engine"
174 file_size = "4MB"
175 purge_threshold = "1GB"
176 purge_interval = "5mins"
177 "#;
178 let datanode_wal_config: DatanodeWalConfig = toml::from_str(toml_str).unwrap();
179 let expected = RaftEngineConfig {
180 file_size: ReadableSize::mb(4),
181 purge_threshold: ReadableSize::gb(1),
182 purge_interval: Duration::from_secs(5 * 60),
183 ..Default::default()
184 };
185 assert_eq!(datanode_wal_config, DatanodeWalConfig::RaftEngine(expected));
186 }
187
188 #[test]
189 fn test_toml_kafka() {
190 let toml_str = r#"
191 provider = "kafka"
192 broker_endpoints = ["127.0.0.1:9092"]
193 max_batch_bytes = "1MB"
194 consumer_wait_timeout = "100ms"
195 num_topics = 32
196 num_partitions = 1
197 selector_type = "round_robin"
198 replication_factor = 1
199 create_topic_timeout = "30s"
200 topic_name_prefix = "greptimedb_wal_topic"
201 [tls]
202 server_ca_cert_path = "/path/to/server.pem"
203 [sasl]
204 type = "SCRAM-SHA-512"
205 username = "hi"
206 password = "test"
207 "#;
208
209 let metasrv_wal_config: MetasrvWalConfig = toml::from_str(toml_str).unwrap();
211 let expected = MetasrvKafkaConfig {
212 connection: KafkaConnectionConfig {
213 broker_endpoints: vec!["127.0.0.1:9092".to_string()],
214 sasl: Some(KafkaClientSasl {
215 config: KafkaClientSaslConfig::ScramSha512 {
216 username: "hi".to_string(),
217 password: "test".to_string(),
218 },
219 }),
220 tls: Some(KafkaClientTls {
221 server_ca_cert_path: Some("/path/to/server.pem".to_string()),
222 client_cert_path: None,
223 client_key_path: None,
224 }),
225 connect_timeout: Duration::from_secs(3),
226 timeout: Duration::from_secs(5),
227 },
228 kafka_topic: KafkaTopicConfig {
229 num_topics: 32,
230 selector_type: TopicSelectorType::RoundRobin,
231 topic_name_prefix: "greptimedb_wal_topic".to_string(),
232 num_partitions: 1,
233 replication_factor: 1,
234 create_topic_timeout: Duration::from_secs(30),
235 },
236 auto_create_topics: true,
237 auto_prune_interval: Duration::from_secs(30 * 60),
238 auto_prune_logical_delete: false,
239 auto_prune_parallelism: 10,
240 flush_trigger_size: ReadableSize::mb(512),
241 checkpoint_trigger_size: ReadableSize::mb(128),
242 region_flush_trigger_interval: Duration::from_secs(60),
243 periodic_checkpoint_persist_interval: Duration::from_secs(60 * 60),
244 };
245 assert_eq!(metasrv_wal_config, MetasrvWalConfig::Kafka(expected));
246
247 let datanode_wal_config: DatanodeWalConfig = toml::from_str(toml_str).unwrap();
249 let expected = DatanodeKafkaConfig {
250 connection: KafkaConnectionConfig {
251 broker_endpoints: vec!["127.0.0.1:9092".to_string()],
252 sasl: Some(KafkaClientSasl {
253 config: KafkaClientSaslConfig::ScramSha512 {
254 username: "hi".to_string(),
255 password: "test".to_string(),
256 },
257 }),
258 tls: Some(KafkaClientTls {
259 server_ca_cert_path: Some("/path/to/server.pem".to_string()),
260 client_cert_path: None,
261 client_key_path: None,
262 }),
263 connect_timeout: Duration::from_secs(3),
264 timeout: Duration::from_secs(5),
265 },
266 max_batch_bytes: ReadableSize::mb(1),
267 consumer_wait_timeout: Duration::from_millis(100),
268 kafka_topic: KafkaTopicConfig {
269 num_topics: 32,
270 selector_type: TopicSelectorType::RoundRobin,
271 topic_name_prefix: "greptimedb_wal_topic".to_string(),
272 num_partitions: 1,
273 replication_factor: 1,
274 create_topic_timeout: Duration::from_secs(30),
275 },
276 ..Default::default()
277 };
278 assert_eq!(datanode_wal_config, DatanodeWalConfig::Kafka(expected));
279 }
280
281 #[test]
282 fn test_toml_object_store() {
283 let toml_str = r#"
284 provider = "experimental_object_store"
285 "#;
286 let datanode_wal_config: DatanodeWalConfig = toml::from_str(toml_str).unwrap();
287 assert_eq!(
288 datanode_wal_config,
289 DatanodeWalConfig::ObjectStore(ObjectStoreWalConfig::default())
290 );
291 let DatanodeWalConfig::ObjectStore(config) = &datanode_wal_config else {
292 unreachable!()
293 };
294 assert_eq!(config.prefix, "wal");
295 assert_eq!(config.flush_interval, Duration::from_millis(100));
296 assert_eq!(config.max_batch_bytes, ReadableSize::mb(8));
297 assert_eq!(config.ack_mode, AckMode::Durable);
298 assert_eq!(config.max_unpersisted_bytes, ReadableSize::mb(64));
299 assert_eq!(config.max_unpersisted_age, Duration::from_secs(8));
300 assert_eq!(config.on_corrupted_segment, CorruptedSegmentAction::Skip);
301 assert!(matches!(
302 MetasrvWalConfig::try_from(datanode_wal_config).unwrap_err(),
303 Error::UnsupportedWalProvider { .. }
304 ));
305
306 let toml_str = r#"
307 provider = "experimental_object_store"
308 storage_provider = "s3"
309 prefix = "cluster-a/wal"
310 flush_interval = "500ms"
311 max_batch_bytes = "4MB"
312 ack_mode = "enqueued"
313 max_unpersisted_bytes = "32MB"
314 max_unpersisted_age = "4s"
315 on_corrupted_segment = "fail"
316 "#;
317 let datanode_wal_config: DatanodeWalConfig = toml::from_str(toml_str).unwrap();
318 let expected = ObjectStoreWalConfig {
319 storage_provider: "s3".to_string(),
320 prefix: "cluster-a/wal".to_string(),
321 flush_interval: Duration::from_millis(500),
322 max_batch_bytes: ReadableSize::mb(4),
323 ack_mode: AckMode::Enqueued,
324 max_unpersisted_bytes: ReadableSize::mb(32),
325 max_unpersisted_age: Duration::from_secs(4),
326 on_corrupted_segment: CorruptedSegmentAction::Fail,
327 };
328 assert_eq!(
329 datanode_wal_config,
330 DatanodeWalConfig::ObjectStore(expected)
331 );
332 let serialized = toml::to_string(&datanode_wal_config).unwrap();
333 let table: toml::Table = toml::from_str(&serialized).unwrap();
334 assert_eq!(table["ack_mode"].as_str(), Some("enqueued"));
335 assert_eq!(table["max_unpersisted_bytes"].as_str(), Some("32MiB"));
336 assert_eq!(table["max_unpersisted_age"].as_str(), Some("4s"));
337 assert_eq!(
338 datanode_wal_config,
339 toml::from_str::<DatanodeWalConfig>(&serialized).unwrap()
340 );
341
342 let toml_str = r#"
344 provider = "object_store"
345 "#;
346 assert!(toml::from_str::<DatanodeWalConfig>(toml_str).is_err());
347 }
348
349 #[test]
350 fn test_kafka_wal_config_debug_redacts_password() {
351 let config = MetasrvWalConfig::Kafka(MetasrvKafkaConfig {
352 connection: KafkaConnectionConfig {
353 sasl: Some(KafkaClientSasl {
354 config: KafkaClientSaslConfig::Plain {
355 username: "greptime".to_string(),
356 password: "kafka-secret".to_string(),
357 },
358 }),
359 ..Default::default()
360 },
361 ..Default::default()
362 });
363
364 let debug = format!("{config:#?}");
365
366 assert!(debug.contains("greptime"));
367 assert!(debug.contains("<REDACTED>"));
368 assert!(!debug.contains("kafka-secret"));
369 }
370}
371
372#[cfg(test)]
373mod rskafka_contract_tests {
374 use std::sync::Arc;
375 use std::time::Duration;
376
377 use rskafka::build_info::DEFAULT_CLIENT_ID;
378 use rskafka::messenger::{Messenger, RequestError, SyncVersionsError};
379
380 #[tokio::test]
381 async fn test_send_timeout_poisoned_messenger() {
382 let (_peer, stream) = tokio::io::duplex(1);
383 let mut messenger = Messenger::new(
384 stream,
385 1_000,
386 Arc::from(DEFAULT_CLIENT_ID),
387 Some(Duration::from_millis(50)),
388 );
389
390 let error = messenger.sync_versions().await.unwrap_err();
391 assert!(matches!(
392 error,
393 SyncVersionsError::RequestError(RequestError::Poisoned(error))
394 if matches!(
395 error.as_ref(),
396 RequestError::IO(error) if error.kind() == std::io::ErrorKind::TimedOut
397 )
398 ));
399 }
400}