Skip to main content

risingwave_connector/source/cdc/enumerator/
mod.rs

1// Copyright 2022 RisingWave Labs
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
15use std::collections::BTreeMap;
16use std::marker::PhantomData;
17use std::ops::Deref;
18use std::str::FromStr;
19use std::sync::Arc;
20
21use anyhow::{Context, anyhow};
22use async_trait::async_trait;
23use itertools::Itertools;
24use mysql_async::Row;
25use mysql_async::prelude::*;
26use prost::Message;
27use risingwave_common::global_jvm::Jvm;
28use risingwave_common::id::SourceId;
29use risingwave_common::metrics::{LabelGuardedIntGauge, LabelGuardedIntGaugeVec};
30use risingwave_common::util::addr::HostAddr;
31use risingwave_jni_core::call_static_method;
32use risingwave_jni_core::jvm_runtime::execute_with_jni_env;
33use risingwave_pb::connector_service::{SourceType, ValidateSourceRequest, ValidateSourceResponse};
34use tiberius::Config;
35use tokio_postgres::types::PgLsn;
36
37use crate::connector_common::{SslMode, create_pg_client, pg_connection_config_from_properties};
38use crate::error::ConnectorResult;
39use crate::sink::sqlserver::SqlServerClient;
40use crate::source::cdc::external::mysql::build_mysql_connection_pool;
41use crate::source::cdc::split::{extract_binlog_file_seq, parse_sql_server_lsn_str};
42use crate::source::cdc::{
43    CdcProperties, CdcSourceTypeTrait, Citus, DebeziumCdcSplit, Mongodb, Mysql, Postgres,
44    SqlServer, table_schema_exclude_additional_columns,
45};
46use crate::source::monitor::metrics::EnumeratorMetrics;
47use crate::source::{SourceEnumeratorContextRef, SplitEnumerator};
48
49pub const DATABASE_SERVERS_KEY: &str = "database.servers";
50
51#[derive(Debug)]
52pub struct DebeziumSplitEnumerator<T: CdcSourceTypeTrait> {
53    /// The `source_id` in the catalog
54    source_id: SourceId,
55    worker_node_addrs: Vec<HostAddr>,
56    metrics: Arc<EnumeratorMetrics>,
57    pg_cdc_upstream_max_lsn: Option<LabelGuardedIntGauge>,
58    pg_cdc_confirmed_flush_lsn: Option<LabelGuardedIntGauge>,
59    mysql_cdc_binlog_file_seq_min: Option<LabelGuardedIntGauge>,
60    mysql_cdc_binlog_file_seq_max: Option<LabelGuardedIntGauge>,
61    sqlserver_cdc_upstream_min_lsn: Option<LabelGuardedIntGauge>,
62    sqlserver_cdc_upstream_max_lsn: Option<LabelGuardedIntGauge>,
63    /// Properties specified in the WITH clause by user for database connection
64    properties: Arc<BTreeMap<String, String>>,
65    _phantom: PhantomData<T>,
66}
67
68fn get_or_create_guarded_int_gauge<'a>(
69    metric: &'a mut Option<LabelGuardedIntGauge>,
70    metric_vec: &LabelGuardedIntGaugeVec,
71    labels: &[String],
72) -> &'a LabelGuardedIntGauge {
73    metric.get_or_insert_with(|| metric_vec.with_guarded_label_values(labels))
74}
75
76#[async_trait]
77impl<T: CdcSourceTypeTrait> SplitEnumerator for DebeziumSplitEnumerator<T>
78where
79    Self: ListCdcSplits<CdcSourceType = T> + CdcMonitor,
80{
81    type Properties = CdcProperties<T>;
82    type Split = DebeziumCdcSplit<T>;
83
84    async fn new(
85        props: CdcProperties<T>,
86        context: SourceEnumeratorContextRef,
87    ) -> ConnectorResult<Self> {
88        let server_addrs = props
89            .properties
90            .get(DATABASE_SERVERS_KEY)
91            .map(|s| {
92                s.split(',')
93                    .map(HostAddr::from_str)
94                    .collect::<Result<Vec<_>, _>>()
95            })
96            .transpose()?
97            .unwrap_or_default();
98
99        assert_eq!(
100            props.get_source_type_pb(),
101            SourceType::from(T::source_type())
102        );
103
104        let jvm = Jvm::get_or_init()?;
105        let source_id = context.info.source_id;
106
107        // Extract fields before moving props
108        let source_type_pb = props.get_source_type_pb();
109
110        // Create Arc once and share it
111        let properties_arc = Arc::new(props.properties);
112        let properties_arc_for_validation = properties_arc.clone();
113        let table_schema_for_validation = props.table_schema;
114
115        tokio::task::spawn_blocking(move || -> anyhow::Result<()> {
116            execute_with_jni_env(jvm, |env| {
117                let validate_source_request = ValidateSourceRequest {
118                    source_id: source_id.as_raw_id() as u64,
119                    source_type: source_type_pb as _,
120                    properties: (*properties_arc_for_validation).clone(),
121                    table_schema: Some(table_schema_exclude_additional_columns(
122                        &table_schema_for_validation,
123                    )),
124                    is_source_job: props.is_cdc_source_job,
125                    is_backfill_table: props.is_backfill_table,
126                };
127
128                let validate_source_request_bytes =
129                    env.byte_array_from_slice(&Message::encode_to_vec(&validate_source_request))?;
130
131                let validate_source_response_bytes = call_static_method!(
132                    env,
133                    {com.risingwave.connector.source.JniSourceValidateHandler},
134                    {byte[] validate(byte[] validateSourceRequestBytes)},
135                    &validate_source_request_bytes
136                )?;
137
138                let validate_source_response: ValidateSourceResponse = Message::decode(
139                    risingwave_jni_core::to_guarded_slice(&validate_source_response_bytes, env)?
140                        .deref(),
141                )?;
142
143                if let Some(error) = validate_source_response.error {
144                    return Err(
145                        anyhow!(error.error_message).context("source cannot pass validation")
146                    );
147                }
148
149                Ok(())
150            })
151        })
152        .await
153        .context("failed to validate source")??;
154
155        tracing::debug!("validate cdc source properties success");
156        Ok(Self {
157            source_id,
158            worker_node_addrs: server_addrs,
159            metrics: context.metrics.clone(),
160            pg_cdc_upstream_max_lsn: None,
161            pg_cdc_confirmed_flush_lsn: None,
162            mysql_cdc_binlog_file_seq_min: None,
163            mysql_cdc_binlog_file_seq_max: None,
164            sqlserver_cdc_upstream_min_lsn: None,
165            sqlserver_cdc_upstream_max_lsn: None,
166            properties: properties_arc,
167            _phantom: PhantomData,
168        })
169    }
170
171    async fn list_splits(&mut self) -> ConnectorResult<Vec<DebeziumCdcSplit<T>>> {
172        Ok(self.list_cdc_splits())
173    }
174
175    async fn on_tick(&mut self) -> ConnectorResult<()> {
176        self.monitor_cdc().await
177    }
178}
179
180impl<T: CdcSourceTypeTrait> DebeziumSplitEnumerator<T> {
181    fn sql_server_lsn_to_i64(lsn: &str) -> Option<i64> {
182        parse_sql_server_lsn_str(lsn).map(|v| v.min(i64::MAX as u128) as i64)
183    }
184
185    fn pg_cdc_lsn_metric_labels(&self, slot_name: &str) -> Vec<String> {
186        vec![self.source_id.to_string(), slot_name.to_owned()]
187    }
188
189    async fn monitor_postgres_confirmed_flush_lsn(&mut self) -> ConnectorResult<()> {
190        // Query upstream LSNs and update metrics.
191        let lsns = self.query_postgres_lsns().await.with_context(|| {
192            format!(
193                "failed to query PostgreSQL LSNs for source {}",
194                self.source_id
195            )
196        })?;
197        match lsns {
198            Some((confirmed_flush_lsn, upstream_max_lsn, slot_name)) => {
199                let labels = self.pg_cdc_lsn_metric_labels(&slot_name);
200
201                get_or_create_guarded_int_gauge(
202                    &mut self.pg_cdc_upstream_max_lsn,
203                    &self.metrics.pg_cdc_upstream_max_lsn,
204                    &labels,
205                )
206                .set(upstream_max_lsn as i64);
207
208                if let Some(lsn) = confirmed_flush_lsn {
209                    get_or_create_guarded_int_gauge(
210                        &mut self.pg_cdc_confirmed_flush_lsn,
211                        &self.metrics.pg_cdc_confirmed_flush_lsn,
212                        &labels,
213                    )
214                    .set(lsn as i64);
215                    tracing::debug!(
216                        "Updated confirmed_flush_lsn for source {} slot {}: {}",
217                        self.source_id,
218                        slot_name,
219                        lsn
220                    );
221                } else {
222                    tracing::warn!(
223                        "confirmed_flush_lsn is NULL for source {} slot {}",
224                        self.source_id,
225                        slot_name
226                    );
227                }
228            }
229            None => {
230                tracing::warn!(
231                    "No replication slot found when querying LSNs for source {}",
232                    self.source_id
233                );
234            }
235        };
236        Ok(())
237    }
238
239    /// Query LSNs from PostgreSQL, return (`confirmed_flush_lsn`, `upstream_max_lsn`, `slot_name`).
240    async fn query_postgres_lsns(&self) -> ConnectorResult<Option<(Option<u64>, u64, String)>> {
241        let pg_conn = pg_connection_config_from_properties(&self.properties)?;
242
243        let slot_name = self
244            .properties
245            .get("slot.name")
246            .ok_or_else(|| anyhow::anyhow!("slot.name not found in CDC properties"))?;
247
248        // No TCP keepalive for CDC enumerator
249        let client = create_pg_client(&pg_conn, None)
250            .await
251            .context("Failed to create PostgreSQL client")?;
252
253        let query = "SELECT confirmed_flush_lsn, pg_current_wal_lsn() \
254            FROM pg_replication_slots WHERE slot_name = $1";
255        let row = client
256            .query_opt(query, &[&slot_name])
257            .await
258            .context("PostgreSQL query LSNs error")?;
259        match row {
260            Some(row) => {
261                let confirmed_flush_lsn: Option<PgLsn> = row.get(0);
262                let upstream_max_lsn: PgLsn = row.get(1);
263                Ok(Some((
264                    confirmed_flush_lsn.map(Into::into),
265                    upstream_max_lsn.into(),
266                    slot_name.clone(),
267                )))
268            }
269            None => {
270                tracing::warn!("No replication slot found with name: {}", slot_name);
271                Ok(None)
272            }
273        }
274    }
275
276    /// Query min/max LSNs from SQL Server CDC.
277    async fn query_sql_server_lsns(&self) -> ConnectorResult<Option<(String, String)>> {
278        let hostname = self
279            .properties
280            .get("hostname")
281            .ok_or_else(|| anyhow!("hostname not found in CDC properties"))?;
282        let port = self
283            .properties
284            .get("port")
285            .ok_or_else(|| anyhow!("port not found in CDC properties"))?
286            .parse::<u16>()
287            .context("failed to parse port as u16")?;
288        let username = self
289            .properties
290            .get("username")
291            .ok_or_else(|| anyhow!("username not found in CDC properties"))?;
292        let password = self
293            .properties
294            .get("password")
295            .ok_or_else(|| anyhow!("password not found in CDC properties"))?;
296        let database = self
297            .properties
298            .get("database.name")
299            .ok_or_else(|| anyhow!("database.name not found in CDC properties"))?;
300
301        let mut config = Config::new();
302        config.host(hostname);
303        config.port(port);
304        config.database(database);
305        config.authentication(tiberius::AuthMethod::sql_server(username, password));
306        config.trust_cert();
307
308        let mut client = SqlServerClient::new_with_config(config).await?;
309        let row = client
310            .inner_client
311            .simple_query(
312                "SELECT \
313                    sys.fn_cdc_get_max_lsn() AS max_lsn, \
314                    (SELECT MIN(sys.fn_cdc_get_min_lsn(capture_instance)) FROM cdc.change_tables) AS min_lsn"
315                    .to_owned(),
316            )
317            .await?
318            .into_row()
319            .await?
320            .ok_or_else(|| anyhow!("No result returned when querying SQL Server max/min LSN"))?;
321
322        let lsn_bytes_to_hex = |bytes: &[u8]| -> ConnectorResult<String> {
323            if bytes.len() != 10 {
324                return Err(anyhow!(
325                    "SQL Server LSN should be 10 bytes, got {} bytes",
326                    bytes.len()
327                )
328                .into());
329            }
330            let mut hex_string = String::with_capacity(22);
331            for byte in &bytes[0..4] {
332                hex_string.push_str(&format!("{:02x}", byte));
333            }
334            hex_string.push(':');
335            for byte in &bytes[4..8] {
336                hex_string.push_str(&format!("{:02x}", byte));
337            }
338            hex_string.push(':');
339            for byte in &bytes[8..10] {
340                hex_string.push_str(&format!("{:02x}", byte));
341            }
342            Ok(hex_string)
343        };
344
345        let max_lsn = row
346            .try_get::<&[u8], usize>(0)?
347            .map(lsn_bytes_to_hex)
348            .transpose()?
349            .ok_or_else(|| anyhow!("SQL Server max_lsn is NULL"))?;
350        let min_lsn = row
351            .try_get::<&[u8], usize>(1)?
352            .map(lsn_bytes_to_hex)
353            .transpose()?
354            .ok_or_else(|| anyhow!("SQL Server min_lsn is NULL"))?;
355
356        Ok(Some((min_lsn, max_lsn)))
357    }
358
359    async fn monitor_sql_server_lsns(&mut self) -> ConnectorResult<()> {
360        let lsns = self.query_sql_server_lsns().await.with_context(|| {
361            format!(
362                "failed to query SQL Server LSNs for source {}",
363                self.source_id
364            )
365        })?;
366        if let Some((min_lsn, max_lsn)) = lsns {
367            let labels = vec![self.source_id.to_string()];
368
369            if let Some(value) = Self::sql_server_lsn_to_i64(&min_lsn) {
370                get_or_create_guarded_int_gauge(
371                    &mut self.sqlserver_cdc_upstream_min_lsn,
372                    &self.metrics.sqlserver_cdc_upstream_min_lsn,
373                    &labels,
374                )
375                .set(value);
376            }
377
378            if let Some(value) = Self::sql_server_lsn_to_i64(&max_lsn) {
379                get_or_create_guarded_int_gauge(
380                    &mut self.sqlserver_cdc_upstream_max_lsn,
381                    &self.metrics.sqlserver_cdc_upstream_max_lsn,
382                    &labels,
383                )
384                .set(value);
385            }
386        }
387
388        Ok(())
389    }
390}
391
392pub trait ListCdcSplits {
393    type CdcSourceType: CdcSourceTypeTrait;
394    /// Generates a single split for shared source.
395    fn list_cdc_splits(&mut self) -> Vec<DebeziumCdcSplit<Self::CdcSourceType>>;
396}
397
398/// Trait for CDC-specific monitoring behavior
399#[async_trait]
400pub trait CdcMonitor {
401    async fn monitor_cdc(&mut self) -> ConnectorResult<()>;
402}
403
404#[async_trait]
405impl<T: CdcSourceTypeTrait> CdcMonitor for DebeziumSplitEnumerator<T> {
406    default async fn monitor_cdc(&mut self) -> ConnectorResult<()> {
407        Ok(())
408    }
409}
410
411impl DebeziumSplitEnumerator<Mysql> {
412    async fn monitor_mysql_binlog_files(&mut self) -> ConnectorResult<()> {
413        // Get hostname and port for metrics labels
414        let hostname = self
415            .properties
416            .get("hostname")
417            .map(|s| s.as_str())
418            .ok_or_else(|| {
419                anyhow::anyhow!("missing required property 'hostname' for MySQL CDC source")
420            })?;
421        let port = self
422            .properties
423            .get("port")
424            .map(|s| s.as_str())
425            .ok_or_else(|| {
426                anyhow::anyhow!("missing required property 'port' for MySQL CDC source")
427            })?;
428
429        // Query binlog files and update metrics
430        let binlog_files = self.query_binlog_files().await.with_context(|| {
431            format!(
432                "failed to query binlog files for MySQL CDC source {} ({}:{})",
433                self.source_id, hostname, port
434            )
435        })?;
436        if let Some((oldest_file, oldest_size)) = binlog_files.first()
437            && let Some(seq) = extract_binlog_file_seq(oldest_file)
438        {
439            let labels = vec![hostname.to_owned(), port.to_owned()];
440            get_or_create_guarded_int_gauge(
441                &mut self.mysql_cdc_binlog_file_seq_min,
442                &self.metrics.mysql_cdc_binlog_file_seq_min,
443                &labels,
444            )
445            .set(seq as i64);
446            tracing::debug!(
447                "MySQL CDC source {} ({}:{}): oldest binlog = {}, seq = {}, size = {}",
448                self.source_id,
449                hostname,
450                port,
451                oldest_file,
452                seq,
453                oldest_size
454            );
455        }
456        if let Some((newest_file, newest_size)) = binlog_files.last()
457            && let Some(seq) = extract_binlog_file_seq(newest_file)
458        {
459            let labels = vec![hostname.to_owned(), port.to_owned()];
460            get_or_create_guarded_int_gauge(
461                &mut self.mysql_cdc_binlog_file_seq_max,
462                &self.metrics.mysql_cdc_binlog_file_seq_max,
463                &labels,
464            )
465            .set(seq as i64);
466            tracing::debug!(
467                "MySQL CDC source {} ({}:{}): newest binlog = {}, seq = {}, size = {}",
468                self.source_id,
469                hostname,
470                port,
471                newest_file,
472                seq,
473                newest_size
474            );
475        }
476        tracing::debug!(
477            "MySQL CDC source {} ({}:{}): total {} binlog files",
478            self.source_id,
479            hostname,
480            port,
481            binlog_files.len()
482        );
483        Ok(())
484    }
485
486    /// Query binlog files from MySQL, returns Vec<(filename, size)>
487    async fn query_binlog_files(&self) -> ConnectorResult<Vec<(String, u64)>> {
488        // Extract connection parameters from CDC properties
489        let hostname = self
490            .properties
491            .get("hostname")
492            .ok_or_else(|| anyhow::anyhow!("hostname not found in CDC properties"))?;
493        let port = self
494            .properties
495            .get("port")
496            .ok_or_else(|| anyhow::anyhow!("port not found in CDC properties"))?
497            .parse::<u16>()
498            .context("failed to parse port as u16")?;
499        let username = self
500            .properties
501            .get("username")
502            .ok_or_else(|| anyhow::anyhow!("username not found in CDC properties"))?;
503        let password = self
504            .properties
505            .get("password")
506            .ok_or_else(|| anyhow::anyhow!("password not found in CDC properties"))?;
507        let database = self
508            .properties
509            .get("database.name")
510            .ok_or_else(|| anyhow::anyhow!("database.name not found in CDC properties"))?;
511
512        // Get SSL mode configuration (default to Disabled if not specified)
513        let ssl_mode = self
514            .properties
515            .get("ssl.mode")
516            .and_then(|s| s.parse().ok())
517            .unwrap_or(SslMode::Preferred);
518
519        // Build MySQL connection pool with proper SSL configuration
520        let pool =
521            build_mysql_connection_pool(hostname, port, username, password, database, ssl_mode);
522        let mut conn = pool
523            .get_conn()
524            .await
525            .context("Failed to connect to MySQL")?;
526
527        // Query binlog files using SHOW BINARY LOGS.
528        // MySQL 8.0+ may return 3 columns (Log_name, File_size, Encrypted), while some variants
529        // only return the first 2. Decode the row manually so we don't panic on column-count
530        // differences.
531        let rows: Vec<Row> = conn
532            .query("SHOW BINARY LOGS")
533            .await
534            .context("Failed to execute SHOW BINARY LOGS")?;
535        let query_result = rows
536            .into_iter()
537            .map(|mut row| -> ConnectorResult<(String, u64)> {
538                let log_name = row
539                    .take_opt::<String, _>(0)
540                    .transpose()
541                    .context("SHOW BINARY LOGS: failed to decode Log_name")?
542                    .ok_or_else(|| anyhow!("SHOW BINARY LOGS: missing Log_name column"))?;
543                let file_size = row
544                    .take_opt::<u64, _>(1)
545                    .transpose()
546                    .context("SHOW BINARY LOGS: failed to decode File_size")?
547                    .ok_or_else(|| anyhow!("SHOW BINARY LOGS: missing File_size column"))?;
548                Ok((log_name, file_size))
549            })
550            .collect::<ConnectorResult<Vec<_>>>()?;
551
552        drop(conn);
553        pool.disconnect().await.ok();
554
555        Ok(query_result)
556    }
557}
558
559impl ListCdcSplits for DebeziumSplitEnumerator<Mysql> {
560    type CdcSourceType = Mysql;
561
562    fn list_cdc_splits(&mut self) -> Vec<DebeziumCdcSplit<Self::CdcSourceType>> {
563        // CDC source only supports single split
564        vec![DebeziumCdcSplit::<Self::CdcSourceType>::new(
565            self.source_id.as_raw_id(),
566            None,
567            None,
568        )]
569    }
570}
571
572#[async_trait]
573impl CdcMonitor for DebeziumSplitEnumerator<Mysql> {
574    async fn monitor_cdc(&mut self) -> ConnectorResult<()> {
575        // For MySQL CDC, query the upstream MySQL binlog files and monitor them.
576        self.monitor_mysql_binlog_files().await?;
577        Ok(())
578    }
579}
580
581impl ListCdcSplits for DebeziumSplitEnumerator<Postgres> {
582    type CdcSourceType = Postgres;
583
584    fn list_cdc_splits(&mut self) -> Vec<DebeziumCdcSplit<Self::CdcSourceType>> {
585        // CDC source only supports single split
586        vec![DebeziumCdcSplit::<Self::CdcSourceType>::new(
587            self.source_id.as_raw_id(),
588            None,
589            None,
590        )]
591    }
592}
593
594#[async_trait]
595impl CdcMonitor for DebeziumSplitEnumerator<Postgres> {
596    async fn monitor_cdc(&mut self) -> ConnectorResult<()> {
597        // For PostgreSQL CDC, query the upstream Postgres confirmed flush lsn and monitor it.
598        self.monitor_postgres_confirmed_flush_lsn().await?;
599        Ok(())
600    }
601}
602
603impl ListCdcSplits for DebeziumSplitEnumerator<Citus> {
604    type CdcSourceType = Citus;
605
606    fn list_cdc_splits(&mut self) -> Vec<DebeziumCdcSplit<Self::CdcSourceType>> {
607        self.worker_node_addrs
608            .iter()
609            .enumerate()
610            .map(|(id, addr)| {
611                DebeziumCdcSplit::<Self::CdcSourceType>::new(
612                    id as u32,
613                    None,
614                    Some(addr.to_string()),
615                )
616            })
617            .collect_vec()
618    }
619}
620impl ListCdcSplits for DebeziumSplitEnumerator<Mongodb> {
621    type CdcSourceType = Mongodb;
622
623    fn list_cdc_splits(&mut self) -> Vec<DebeziumCdcSplit<Self::CdcSourceType>> {
624        // CDC source only supports single split
625        vec![DebeziumCdcSplit::<Self::CdcSourceType>::new(
626            self.source_id.as_raw_id(),
627            None,
628            None,
629        )]
630    }
631}
632
633impl ListCdcSplits for DebeziumSplitEnumerator<SqlServer> {
634    type CdcSourceType = SqlServer;
635
636    fn list_cdc_splits(&mut self) -> Vec<DebeziumCdcSplit<Self::CdcSourceType>> {
637        vec![DebeziumCdcSplit::<Self::CdcSourceType>::new(
638            self.source_id.as_raw_id(),
639            None,
640            None,
641        )]
642    }
643}
644
645#[async_trait]
646impl CdcMonitor for DebeziumSplitEnumerator<SqlServer> {
647    async fn monitor_cdc(&mut self) -> ConnectorResult<()> {
648        self.monitor_sql_server_lsns().await
649    }
650}
651
652#[cfg(test)]
653mod tests {
654    use prometheus::core::Collector;
655    use risingwave_common::metrics::LabelGuardedIntGaugeVec;
656
657    use super::get_or_create_guarded_int_gauge;
658
659    #[test]
660    fn cached_guarded_metric_survives_repeated_collections() {
661        let metric_vec = LabelGuardedIntGaugeVec::test_int_gauge_vec::<2>();
662        let labels = vec!["source_id".to_owned(), "slot_name".to_owned()];
663        let mut metric = None;
664
665        get_or_create_guarded_int_gauge(&mut metric, &metric_vec, &labels).set(1);
666
667        assert_eq!(1, metric_vec.collect().pop().unwrap().get_metric().len());
668        assert_eq!(1, metric_vec.collect().pop().unwrap().get_metric().len());
669
670        drop(metric);
671        assert_eq!(1, metric_vec.collect().pop().unwrap().get_metric().len());
672        assert_eq!(0, metric_vec.collect().pop().unwrap().get_metric().len());
673    }
674}