Skip to main content

common_meta/
wal_provider.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15mod selector;
16pub(crate) mod topic_creator;
17mod topic_manager;
18pub(crate) mod topic_pool;
19
20use std::collections::{BTreeSet, HashMap};
21use std::sync::Arc;
22
23use async_trait::async_trait;
24use common_procedure::Context as ProcedureContext;
25use common_procedure::local::DynamicKeyLockGuard;
26use common_wal::config::MetasrvWalConfig;
27use common_wal::options::{KafkaWalOptions, WAL_OPTIONS_KEY, WalOptions};
28use serde::{Deserialize, Deserializer, Serialize, Serializer};
29use snafu::ensure;
30use store_api::storage::{RegionId, RegionNumber};
31
32use crate::ddl::allocator::wal_options::WalOptionsAllocator;
33use crate::error::{InvalidTopicNamePrefixSnafu, Result};
34use crate::key::topic_name::TopicNameKey;
35use crate::key::{TOPIC_NAME_PATTERN_REGEX, TableMetadataManagerRef};
36use crate::kv_backend::KvBackendRef;
37use crate::leadership_notifier::LeadershipChangeListener;
38use crate::lock_key::RemoteWalLock;
39pub use crate::wal_provider::topic_creator::{build_kafka_client, build_kafka_topic_creator};
40use crate::wal_provider::topic_pool::KafkaTopicPool;
41
42/// WAL options allocated for each region.
43pub type RegionWalOptions = HashMap<RegionNumber, WalOptions>;
44
45/// Returns remote WAL topics referenced by region WAL options.
46pub fn remote_wal_topics(region_wal_options: &RegionWalOptions) -> Vec<&str> {
47    region_wal_options
48        .values()
49        .filter_map(|wal_options| match wal_options {
50            WalOptions::Kafka(kafka_options) => Some(kafka_options.topic.as_str()),
51            _ => None,
52        })
53        .collect::<BTreeSet<_>>()
54        .into_iter()
55        .collect()
56}
57
58/// Acquires per-topic read locks for remote WAL topics.
59pub async fn acquire_remote_wal_read_locks(
60    ctx: &ProcedureContext,
61    region_wal_options: &RegionWalOptions,
62) -> Vec<DynamicKeyLockGuard> {
63    let topics = remote_wal_topics(region_wal_options)
64        .into_iter()
65        .map(str::to_string)
66        .collect::<Vec<_>>();
67    let mut guards = Vec::with_capacity(topics.len());
68    for topic in topics {
69        let guard = ctx
70            .provider
71            .acquire_lock(&(RemoteWalLock::Read(topic).into()))
72            .await;
73        guards.push(guard);
74    }
75    guards
76}
77
78/// Refreshes initial pruned entry ids for Kafka WAL options.
79pub async fn refresh_initial_pruned_entry_ids(
80    table_metadata_manager: &TableMetadataManagerRef,
81    region_wal_options: &mut RegionWalOptions,
82) -> Result<()> {
83    let topics = remote_wal_topics(region_wal_options);
84    if topics.is_empty() {
85        return Ok(());
86    }
87
88    let topic_values = table_metadata_manager
89        .topic_name_manager()
90        .batch_get(
91            topics
92                .iter()
93                .map(|topic| TopicNameKey::new(topic))
94                .collect(),
95        )
96        .await?;
97
98    for wal_options in region_wal_options.values_mut() {
99        let WalOptions::Kafka(kafka_options) = wal_options else {
100            continue;
101        };
102        kafka_options.initial_pruned_entry_id = Some(
103            topic_values
104                .get(&kafka_options.topic)
105                .map(|value| value.pruned_entry_id)
106                .unwrap_or_default(),
107        );
108    }
109
110    Ok(())
111}
112
113#[derive(Deserialize)]
114#[serde(untagged)]
115enum WalOptionsCompat {
116    Encoded(String),
117    Structured(WalOptions),
118}
119
120fn deserialize_region_wal_options<E>(
121    values: HashMap<String, WalOptionsCompat>,
122) -> std::result::Result<RegionWalOptions, E>
123where
124    E: serde::de::Error,
125{
126    values
127        .into_iter()
128        .map(|(region_number, wal_options)| {
129            let region_number = region_number.parse::<RegionNumber>().map_err(|err| {
130                E::custom(format!(
131                    "invalid region number in region_wal_options: {region_number}, err: {err}"
132                ))
133            })?;
134            let wal_options = match wal_options {
135                WalOptionsCompat::Encoded(encoded) => serde_json::from_str(&encoded).map_err(|err| {
136                    E::custom(format!(
137                        "failed to decode legacy wal options for region {region_number}: {encoded}, err: {err}"
138                    ))
139                })?,
140                WalOptionsCompat::Structured(wal_options) => wal_options,
141            };
142            Ok((region_number, wal_options))
143        })
144        .collect()
145}
146
147fn serialize_region_wal_options<S>(
148    value: &RegionWalOptions,
149    serializer: S,
150) -> std::result::Result<S::Ok, S::Error>
151where
152    S: Serializer,
153{
154    let values = value
155        .iter()
156        .map(|(region_number, wal_options)| {
157            serde_json::to_string(wal_options).map(|encoded| (*region_number, encoded))
158        })
159        .collect::<std::result::Result<HashMap<_, _>, _>>()
160        .map_err(serde::ser::Error::custom)?;
161    values.serialize(serializer)
162}
163
164/// Serde helpers for [`RegionWalOptions`] persisted in metadata.
165///
166/// Metadata writes WAL options as JSON strings encoded from [`WalOptions`] for
167/// compatibility. The deserializer also accepts structured JSON objects.
168pub mod region_wal_options_serde {
169    use super::*;
170
171    /// Serializes region WAL options as encoded JSON strings.
172    pub fn serialize<S>(
173        value: &RegionWalOptions,
174        serializer: S,
175    ) -> std::result::Result<S::Ok, S::Error>
176    where
177        S: Serializer,
178    {
179        serialize_region_wal_options(value, serializer)
180    }
181
182    /// Deserializes region WAL options from either structured or legacy encoded form.
183    pub fn deserialize<'de, D>(deserializer: D) -> std::result::Result<RegionWalOptions, D::Error>
184    where
185        D: Deserializer<'de>,
186    {
187        let values = HashMap::<String, WalOptionsCompat>::deserialize(deserializer)?;
188        deserialize_region_wal_options(values)
189    }
190}
191
192/// Serde helpers for optional [`RegionWalOptions`] persisted in procedure state.
193pub mod optional_region_wal_options_serde {
194    use super::*;
195
196    /// Serializes optional region WAL options as encoded JSON strings.
197    pub fn serialize<S>(
198        value: &Option<RegionWalOptions>,
199        serializer: S,
200    ) -> std::result::Result<S::Ok, S::Error>
201    where
202        S: Serializer,
203    {
204        match value {
205            Some(value) => serialize_region_wal_options(value, serializer),
206            None => serializer.serialize_none(),
207        }
208    }
209
210    /// Deserializes optional region WAL options from structured or legacy encoded form.
211    pub fn deserialize<'de, D>(
212        deserializer: D,
213    ) -> std::result::Result<Option<RegionWalOptions>, D::Error>
214    where
215        D: Deserializer<'de>,
216    {
217        let Some(values) = Option::<HashMap<String, WalOptionsCompat>>::deserialize(deserializer)?
218        else {
219            return Ok(None);
220        };
221
222        deserialize_region_wal_options(values).map(Some)
223    }
224}
225
226/// Provides wal options in region granularity.
227#[derive(Default, Debug)]
228pub enum WalProvider {
229    #[default]
230    RaftEngine,
231    Kafka(KafkaTopicPool),
232}
233
234/// Arc wrapper of WalProvider.
235pub type WalProviderRef = Arc<WalProvider>;
236
237#[async_trait::async_trait]
238impl WalOptionsAllocator for WalProvider {
239    async fn allocate(
240        &self,
241        region_numbers: &[RegionNumber],
242        skip_wal: bool,
243    ) -> Result<RegionWalOptions> {
244        let wal_options = self.alloc_batch(region_numbers.len(), skip_wal).await?;
245
246        Ok(region_numbers.iter().copied().zip(wal_options).collect())
247    }
248}
249
250impl WalProvider {
251    /// Tries to start the provider.
252    pub async fn start(&self) -> Result<()> {
253        match self {
254            Self::RaftEngine => Ok(()),
255            Self::Kafka(kafka_topic_manager) => kafka_topic_manager.activate().await,
256        }
257    }
258
259    /// Allocates a batch of wal options where each wal options goes to a region.
260    /// If skip_wal is true, the wal options will be set to Noop regardless of the provider type.
261    pub async fn alloc_batch(&self, num_regions: usize, skip_wal: bool) -> Result<Vec<WalOptions>> {
262        if skip_wal {
263            return Ok(vec![WalOptions::Noop; num_regions]);
264        }
265        match self {
266            WalProvider::RaftEngine => Ok(vec![WalOptions::RaftEngine; num_regions]),
267            WalProvider::Kafka(topic_manager) => {
268                let options_batch = topic_manager
269                    .select_batch(num_regions)?
270                    .into_iter()
271                    .map(|topic| WalOptions::Kafka(KafkaWalOptions::new(topic.clone())))
272                    .collect();
273                Ok(options_batch)
274            }
275        }
276    }
277
278    /// Returns true if it's the remote WAL.
279    pub fn is_remote_wal(&self) -> bool {
280        matches!(&self, WalProvider::Kafka(_))
281    }
282}
283
284#[async_trait]
285impl LeadershipChangeListener for WalProvider {
286    fn name(&self) -> &str {
287        "WalProvider"
288    }
289
290    async fn on_leader_start(&self) -> Result<()> {
291        self.start().await
292    }
293
294    async fn on_leader_stop(&self) -> Result<()> {
295        Ok(())
296    }
297}
298
299/// Builds a wal provider based on the given configuration.
300pub async fn build_wal_provider(
301    config: &MetasrvWalConfig,
302    kv_backend: KvBackendRef,
303) -> Result<WalProvider> {
304    match config {
305        MetasrvWalConfig::RaftEngine => Ok(WalProvider::RaftEngine),
306        MetasrvWalConfig::Kafka(kafka_config) => {
307            let prefix = &kafka_config.kafka_topic.topic_name_prefix;
308            ensure!(
309                TOPIC_NAME_PATTERN_REGEX.is_match(prefix),
310                InvalidTopicNamePrefixSnafu { prefix }
311            );
312            let topic_creator =
313                build_kafka_topic_creator(&kafka_config.connection, &kafka_config.kafka_topic)
314                    .await?;
315            let topic_pool = KafkaTopicPool::new(kafka_config, kv_backend, topic_creator);
316            Ok(WalProvider::Kafka(topic_pool))
317        }
318    }
319}
320
321/// Serializes and inserts WAL options into the region options.
322pub fn serialize_wal_options(
323    options: &mut HashMap<String, String>,
324    region_id: RegionId,
325    region_wal_options: &RegionWalOptions,
326) -> std::result::Result<(), serde_json::Error> {
327    if let Some(wal_options) = region_wal_options.get(&region_id.region_number()) {
328        let encoded = serde_json::to_string(wal_options)?;
329        options.insert(WAL_OPTIONS_KEY.to_string(), encoded);
330    }
331    Ok(())
332}
333
334/// Extracts the topic from the wal options.
335pub fn extract_topic_from_wal_options(
336    region_id: RegionId,
337    region_options: &RegionWalOptions,
338) -> Option<String> {
339    region_options
340        .get(&region_id.region_number())
341        .and_then(|wal_options| match wal_options {
342            WalOptions::Kafka(kafka_wal_option) => Some(kafka_wal_option.topic.clone()),
343            _ => None,
344        })
345}
346
347#[cfg(test)]
348mod tests {
349    use std::assert_matches;
350
351    use common_wal::config::kafka::MetasrvKafkaConfig;
352    use common_wal::config::kafka::common::KafkaTopicConfig;
353    use common_wal::maybe_skip_kafka_integration_test;
354    use common_wal::test_util::get_kafka_endpoints;
355
356    use super::*;
357    use crate::error::Error;
358    use crate::kv_backend::memory::MemoryKvBackend;
359    use crate::test_util::test_kafka_topic_pool;
360    use crate::wal_provider::selector::RoundRobinTopicSelector;
361
362    // Tests that the wal provider could successfully allocate raft-engine wal options.
363    #[tokio::test]
364    async fn test_provider_with_raft_engine() {
365        let kv_backend = Arc::new(MemoryKvBackend::new()) as KvBackendRef;
366        let wal_config = MetasrvWalConfig::RaftEngine;
367        let provider = build_wal_provider(&wal_config, kv_backend).await.unwrap();
368        provider.start().await.unwrap();
369
370        let num_regions = 32;
371        let regions = (0..num_regions).collect::<Vec<_>>();
372        let got = provider.allocate(&regions, false).await.unwrap();
373
374        let expected = regions
375            .into_iter()
376            .zip(vec![WalOptions::RaftEngine; num_regions as usize])
377            .collect();
378        assert_eq!(got, expected);
379    }
380
381    #[tokio::test]
382    async fn test_refuse_invalid_topic_name_prefix() {
383        let kv_backend = Arc::new(MemoryKvBackend::new()) as KvBackendRef;
384        let wal_config = MetasrvWalConfig::Kafka(MetasrvKafkaConfig {
385            kafka_topic: KafkaTopicConfig {
386                topic_name_prefix: "``````".to_string(),
387                ..Default::default()
388            },
389            ..Default::default()
390        });
391        let got = build_wal_provider(&wal_config, kv_backend)
392            .await
393            .unwrap_err();
394        assert_matches!(got, Error::InvalidTopicNamePrefix { .. });
395    }
396
397    #[tokio::test]
398    async fn test_provider_with_kafka_allocate_wal_options() {
399        common_telemetry::init_default_ut_logging();
400        maybe_skip_kafka_integration_test!();
401        let num_topics = 5;
402        let mut topic_pool = test_kafka_topic_pool(
403            get_kafka_endpoints(),
404            num_topics,
405            true,
406            Some("test_allocator_with_kafka"),
407        )
408        .await;
409        topic_pool.selector = Arc::new(RoundRobinTopicSelector::default());
410        let topics = topic_pool.topics.clone();
411        // clean up the topics before test
412        let topic_creator = topic_pool.topic_creator();
413        topic_creator.delete_topics(&topics).await.unwrap();
414
415        // Creates an options provider.
416        let provider = WalProvider::Kafka(topic_pool);
417        provider.start().await.unwrap();
418
419        let num_regions = 3;
420        let regions = (0..num_regions).collect::<Vec<_>>();
421        let got = provider.allocate(&regions, false).await.unwrap();
422
423        // Check the allocated wal options contain the expected topics.
424        let expected = (0..num_regions)
425            .map(|i| {
426                let options = WalOptions::Kafka(KafkaWalOptions::new(topics[i as usize].clone()));
427                (i, options)
428            })
429            .collect::<HashMap<_, _>>();
430        assert_eq!(got, expected);
431    }
432
433    #[derive(Debug, PartialEq, Serialize, Deserialize)]
434    struct RegionWalOptionsWrapper {
435        #[serde(with = "region_wal_options_serde")]
436        region_wal_options: RegionWalOptions,
437    }
438
439    #[derive(Debug, PartialEq, Serialize, Deserialize)]
440    struct OptionalRegionWalOptionsWrapper {
441        #[serde(with = "optional_region_wal_options_serde")]
442        region_wal_options: Option<RegionWalOptions>,
443    }
444
445    #[test]
446    fn test_deserialize_legacy_region_wal_options_from_encoded_map() {
447        let legacy_region_wal_options = HashMap::from([
448            (1, serde_json::to_string(&WalOptions::RaftEngine).unwrap()),
449            (
450                2,
451                serde_json::to_string(&WalOptions::Kafka(KafkaWalOptions::new(
452                    "topic_a".to_string(),
453                )))
454                .unwrap(),
455            ),
456        ]);
457        let legacy_json = serde_json::json!({
458            "region_wal_options": legacy_region_wal_options,
459        });
460
461        assert_eq!(
462            legacy_json.to_string(),
463            r#"{"region_wal_options":{"1":"{\"wal.provider\":\"raft_engine\"}","2":"{\"wal.provider\":\"kafka\",\"wal.kafka.topic\":\"topic_a\"}"}}"#
464        );
465
466        let decoded: RegionWalOptionsWrapper = serde_json::from_value(legacy_json).unwrap();
467
468        assert_eq!(
469            decoded.region_wal_options,
470            HashMap::from([
471                (1, WalOptions::RaftEngine),
472                (
473                    2,
474                    WalOptions::Kafka(KafkaWalOptions::new("topic_a".to_string())),
475                ),
476            ])
477        );
478    }
479
480    #[test]
481    fn test_deserialize_structured_region_wal_options() {
482        let json = r#"{
483            "region_wal_options": {
484                "1": {"wal.provider":"raft_engine"},
485                "2": {"wal.provider":"noop"}
486            }
487        }"#;
488
489        let decoded: RegionWalOptionsWrapper = serde_json::from_str(json).unwrap();
490
491        assert_eq!(
492            decoded.region_wal_options,
493            HashMap::from([(1, WalOptions::RaftEngine), (2, WalOptions::Noop)])
494        );
495    }
496
497    #[test]
498    fn test_serialize_legacy_region_wal_options() {
499        let wrapper = RegionWalOptionsWrapper {
500            region_wal_options: HashMap::from([(1, WalOptions::RaftEngine)]),
501        };
502
503        let encoded = serde_json::to_string(&wrapper).unwrap();
504
505        assert_eq!(
506            encoded,
507            r#"{"region_wal_options":{"1":"{\"wal.provider\":\"raft_engine\"}"}}"#
508        );
509    }
510
511    #[test]
512    fn test_serialize_optional_region_wal_options() {
513        let wrapper = OptionalRegionWalOptionsWrapper {
514            region_wal_options: Some(HashMap::from([(1, WalOptions::RaftEngine)])),
515        };
516
517        let encoded = serde_json::to_string(&wrapper).unwrap();
518
519        assert_eq!(
520            encoded,
521            r#"{"region_wal_options":{"1":"{\"wal.provider\":\"raft_engine\"}"}}"#
522        );
523        assert_eq!(
524            serde_json::from_str::<OptionalRegionWalOptionsWrapper>(&encoded).unwrap(),
525            wrapper
526        );
527
528        let none = OptionalRegionWalOptionsWrapper {
529            region_wal_options: None,
530        };
531        assert_eq!(
532            serde_json::to_string(&none).unwrap(),
533            r#"{"region_wal_options":null}"#
534        );
535    }
536
537    #[tokio::test]
538    async fn test_provider_with_skip_wal() {
539        let provider = WalProvider::RaftEngine;
540        provider.start().await.unwrap();
541
542        let num_regions = 32;
543        let regions = (0..num_regions).collect::<Vec<_>>();
544        let got = provider.allocate(&regions, true).await.unwrap();
545        assert_eq!(got.len(), num_regions as usize);
546        for wal_options in got.values() {
547            assert_eq!(wal_options, &WalOptions::Noop);
548        }
549    }
550}