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(anyhow!(error.error_message).context("source validation failed"));
145                }
146
147                Ok(())
148            })
149        })
150        .await
151        .context("failed to validate source")??;
152
153        tracing::debug!("validate cdc source properties success");
154        Ok(Self {
155            source_id,
156            worker_node_addrs: server_addrs,
157            metrics: context.metrics.clone(),
158            pg_cdc_upstream_max_lsn: None,
159            pg_cdc_confirmed_flush_lsn: None,
160            mysql_cdc_binlog_file_seq_min: None,
161            mysql_cdc_binlog_file_seq_max: None,
162            sqlserver_cdc_upstream_min_lsn: None,
163            sqlserver_cdc_upstream_max_lsn: None,
164            properties: properties_arc,
165            _phantom: PhantomData,
166        })
167    }
168
169    async fn list_splits(&mut self) -> ConnectorResult<Vec<DebeziumCdcSplit<T>>> {
170        Ok(self.list_cdc_splits())
171    }
172
173    async fn on_tick(&mut self) -> ConnectorResult<()> {
174        self.monitor_cdc().await
175    }
176}
177
178impl<T: CdcSourceTypeTrait> DebeziumSplitEnumerator<T> {
179    fn sql_server_lsn_to_i64(lsn: &str) -> Option<i64> {
180        parse_sql_server_lsn_str(lsn).map(|v| v.min(i64::MAX as u128) as i64)
181    }
182
183    fn pg_cdc_lsn_metric_labels(&self, slot_name: &str) -> Vec<String> {
184        vec![self.source_id.to_string(), slot_name.to_owned()]
185    }
186
187    async fn monitor_postgres_confirmed_flush_lsn(&mut self) -> ConnectorResult<()> {
188        // Query upstream LSNs and update metrics.
189        let lsns = self.query_postgres_lsns().await.with_context(|| {
190            format!(
191                "failed to query PostgreSQL LSNs for source {}",
192                self.source_id
193            )
194        })?;
195        match lsns {
196            Some((confirmed_flush_lsn, upstream_max_lsn, slot_name)) => {
197                let labels = self.pg_cdc_lsn_metric_labels(&slot_name);
198
199                get_or_create_guarded_int_gauge(
200                    &mut self.pg_cdc_upstream_max_lsn,
201                    &self.metrics.pg_cdc_upstream_max_lsn,
202                    &labels,
203                )
204                .set(upstream_max_lsn as i64);
205
206                if let Some(lsn) = confirmed_flush_lsn {
207                    get_or_create_guarded_int_gauge(
208                        &mut self.pg_cdc_confirmed_flush_lsn,
209                        &self.metrics.pg_cdc_confirmed_flush_lsn,
210                        &labels,
211                    )
212                    .set(lsn as i64);
213                    tracing::debug!(
214                        "Updated confirmed_flush_lsn for source {} slot {}: {}",
215                        self.source_id,
216                        slot_name,
217                        lsn
218                    );
219                } else {
220                    tracing::warn!(
221                        "confirmed_flush_lsn is NULL for source {} slot {}",
222                        self.source_id,
223                        slot_name
224                    );
225                }
226            }
227            None => {
228                tracing::warn!(
229                    "no replication slot was found while querying LSNs for source {}",
230                    self.source_id
231                );
232            }
233        };
234        Ok(())
235    }
236
237    /// Query LSNs from PostgreSQL, return (`confirmed_flush_lsn`, `upstream_max_lsn`, `slot_name`).
238    async fn query_postgres_lsns(&self) -> ConnectorResult<Option<(Option<u64>, u64, String)>> {
239        let pg_conn = pg_connection_config_from_properties(&self.properties)?;
240
241        let slot_name = self
242            .properties
243            .get("slot.name")
244            .ok_or_else(|| anyhow::anyhow!("missing `slot.name` in CDC properties"))?;
245
246        // No TCP keepalive for CDC enumerator
247        let client = create_pg_client(&pg_conn, None)
248            .await
249            .context("failed to create the PostgreSQL client")?;
250
251        let query = "SELECT confirmed_flush_lsn, pg_current_wal_lsn() \
252            FROM pg_replication_slots WHERE slot_name = $1";
253        let row = client
254            .query_opt(query, &[&slot_name])
255            .await
256            .context("failed to query PostgreSQL LSNs")?;
257        match row {
258            Some(row) => {
259                let confirmed_flush_lsn: Option<PgLsn> = row.get(0);
260                let upstream_max_lsn: PgLsn = row.get(1);
261                Ok(Some((
262                    confirmed_flush_lsn.map(Into::into),
263                    upstream_max_lsn.into(),
264                    slot_name.clone(),
265                )))
266            }
267            None => {
268                tracing::warn!("no replication slot found with name: {}", slot_name);
269                Ok(None)
270            }
271        }
272    }
273
274    /// Query min/max LSNs from SQL Server CDC.
275    async fn query_sql_server_lsns(&self) -> ConnectorResult<Option<(String, String)>> {
276        let hostname = self
277            .properties
278            .get("hostname")
279            .ok_or_else(|| anyhow!("missing `hostname` in CDC properties"))?;
280        let port = self
281            .properties
282            .get("port")
283            .ok_or_else(|| anyhow!("missing `port` in CDC properties"))?
284            .parse::<u16>()
285            .context("failed to parse `port` as a u16")?;
286        let username = self
287            .properties
288            .get("username")
289            .ok_or_else(|| anyhow!("missing `username` in CDC properties"))?;
290        let password = self
291            .properties
292            .get("password")
293            .ok_or_else(|| anyhow!("missing `password` in CDC properties"))?;
294        let database = self
295            .properties
296            .get("database.name")
297            .ok_or_else(|| anyhow!("missing `database.name` in CDC properties"))?;
298
299        let mut config = Config::new();
300        config.host(hostname);
301        config.port(port);
302        config.database(database);
303        config.authentication(tiberius::AuthMethod::sql_server(username, password));
304        config.trust_cert();
305
306        let mut client = SqlServerClient::new_with_config(config).await?;
307        let row = client
308            .inner_client
309            .simple_query(
310                "SELECT \
311                    sys.fn_cdc_get_max_lsn() AS max_lsn, \
312                    (SELECT MIN(sys.fn_cdc_get_min_lsn(capture_instance)) FROM cdc.change_tables) AS min_lsn"
313                    .to_owned(),
314            )
315            .await?
316            .into_row()
317            .await?
318            .ok_or_else(|| anyhow!("No result returned when querying SQL Server max/min LSN"))?;
319
320        let lsn_bytes_to_hex = |bytes: &[u8]| -> ConnectorResult<String> {
321            if bytes.len() != 10 {
322                return Err(anyhow!(
323                    "SQL Server LSN should be 10 bytes, got {} bytes",
324                    bytes.len()
325                )
326                .into());
327            }
328            let mut hex_string = String::with_capacity(22);
329            for byte in &bytes[0..4] {
330                hex_string.push_str(&format!("{:02x}", byte));
331            }
332            hex_string.push(':');
333            for byte in &bytes[4..8] {
334                hex_string.push_str(&format!("{:02x}", byte));
335            }
336            hex_string.push(':');
337            for byte in &bytes[8..10] {
338                hex_string.push_str(&format!("{:02x}", byte));
339            }
340            Ok(hex_string)
341        };
342
343        let max_lsn = row
344            .try_get::<&[u8], usize>(0)?
345            .map(lsn_bytes_to_hex)
346            .transpose()?
347            .ok_or_else(|| anyhow!("SQL Server max_lsn is NULL"))?;
348        let min_lsn = row
349            .try_get::<&[u8], usize>(1)?
350            .map(lsn_bytes_to_hex)
351            .transpose()?
352            .ok_or_else(|| anyhow!("SQL Server min_lsn is NULL"))?;
353
354        Ok(Some((min_lsn, max_lsn)))
355    }
356
357    async fn monitor_sql_server_lsns(&mut self) -> ConnectorResult<()> {
358        let lsns = self.query_sql_server_lsns().await.with_context(|| {
359            format!(
360                "failed to query SQL Server LSNs for source {}",
361                self.source_id
362            )
363        })?;
364        if let Some((min_lsn, max_lsn)) = lsns {
365            let labels = vec![self.source_id.to_string()];
366
367            if let Some(value) = Self::sql_server_lsn_to_i64(&min_lsn) {
368                get_or_create_guarded_int_gauge(
369                    &mut self.sqlserver_cdc_upstream_min_lsn,
370                    &self.metrics.sqlserver_cdc_upstream_min_lsn,
371                    &labels,
372                )
373                .set(value);
374            }
375            if let Some(value) = Self::sql_server_lsn_to_i64(&max_lsn) {
376                get_or_create_guarded_int_gauge(
377                    &mut self.sqlserver_cdc_upstream_max_lsn,
378                    &self.metrics.sqlserver_cdc_upstream_max_lsn,
379                    &labels,
380                )
381                .set(value);
382            }
383        }
384
385        Ok(())
386    }
387}
388
389pub trait ListCdcSplits {
390    type CdcSourceType: CdcSourceTypeTrait;
391    /// Generates a single split for shared source.
392    fn list_cdc_splits(&mut self) -> Vec<DebeziumCdcSplit<Self::CdcSourceType>>;
393}
394
395/// Trait for CDC-specific monitoring behavior
396#[async_trait]
397pub trait CdcMonitor {
398    async fn monitor_cdc(&mut self) -> ConnectorResult<()>;
399}
400
401#[async_trait]
402impl<T: CdcSourceTypeTrait> CdcMonitor for DebeziumSplitEnumerator<T> {
403    default async fn monitor_cdc(&mut self) -> ConnectorResult<()> {
404        Ok(())
405    }
406}
407
408impl DebeziumSplitEnumerator<Mysql> {
409    async fn monitor_mysql_binlog_files(&mut self) -> ConnectorResult<()> {
410        // Get hostname and port for metrics labels
411        let hostname = self
412            .properties
413            .get("hostname")
414            .map(|s| s.as_str())
415            .ok_or_else(|| {
416                anyhow::anyhow!("missing required property 'hostname' for MySQL CDC source")
417            })?;
418        let port = self
419            .properties
420            .get("port")
421            .map(|s| s.as_str())
422            .ok_or_else(|| {
423                anyhow::anyhow!("missing required property 'port' for MySQL CDC source")
424            })?;
425
426        // Query binlog files and update metrics
427        let binlog_files = self.query_binlog_files().await.with_context(|| {
428            format!(
429                "failed to query binlog files for MySQL CDC source {} ({}:{})",
430                self.source_id, hostname, port
431            )
432        })?;
433        if let Some((oldest_file, oldest_size)) = binlog_files.first()
434            && let Some(seq) = extract_binlog_file_seq(oldest_file)
435        {
436            let labels = vec![hostname.to_owned(), port.to_owned()];
437            get_or_create_guarded_int_gauge(
438                &mut self.mysql_cdc_binlog_file_seq_min,
439                &self.metrics.mysql_cdc_binlog_file_seq_min,
440                &labels,
441            )
442            .set(seq as i64);
443            tracing::debug!(
444                "MySQL CDC source {} ({}:{}): oldest binlog = {}, seq = {}, size = {}",
445                self.source_id,
446                hostname,
447                port,
448                oldest_file,
449                seq,
450                oldest_size
451            );
452        }
453        if let Some((newest_file, newest_size)) = binlog_files.last()
454            && let Some(seq) = extract_binlog_file_seq(newest_file)
455        {
456            let labels = vec![hostname.to_owned(), port.to_owned()];
457            get_or_create_guarded_int_gauge(
458                &mut self.mysql_cdc_binlog_file_seq_max,
459                &self.metrics.mysql_cdc_binlog_file_seq_max,
460                &labels,
461            )
462            .set(seq as i64);
463            tracing::debug!(
464                "MySQL CDC source {} ({}:{}): newest binlog = {}, seq = {}, size = {}",
465                self.source_id,
466                hostname,
467                port,
468                newest_file,
469                seq,
470                newest_size
471            );
472        }
473        tracing::debug!(
474            "MySQL CDC source {} ({}:{}): total {} binlog files",
475            self.source_id,
476            hostname,
477            port,
478            binlog_files.len()
479        );
480        Ok(())
481    }
482
483    /// Query binlog files from MySQL, returns Vec<(filename, size)>
484    async fn query_binlog_files(&self) -> ConnectorResult<Vec<(String, u64)>> {
485        // Extract connection parameters from CDC properties
486        let hostname = self
487            .properties
488            .get("hostname")
489            .ok_or_else(|| anyhow::anyhow!("missing `hostname` in CDC properties"))?;
490        let port = self
491            .properties
492            .get("port")
493            .ok_or_else(|| anyhow::anyhow!("missing `port` in CDC properties"))?
494            .parse::<u16>()
495            .context("failed to parse `port` as a u16")?;
496        let username = self
497            .properties
498            .get("username")
499            .ok_or_else(|| anyhow::anyhow!("missing `username` in CDC properties"))?;
500        let password = self
501            .properties
502            .get("password")
503            .ok_or_else(|| anyhow::anyhow!("missing `password` in CDC properties"))?;
504        let database = self
505            .properties
506            .get("database.name")
507            .ok_or_else(|| anyhow::anyhow!("missing `database.name` in CDC properties"))?;
508
509        // Get SSL mode configuration (default to Disabled if not specified)
510        let ssl_mode = self
511            .properties
512            .get("ssl.mode")
513            .and_then(|s| s.parse().ok())
514            .unwrap_or(SslMode::Preferred);
515
516        // Build MySQL connection pool with proper SSL configuration
517        let pool =
518            build_mysql_connection_pool(hostname, port, username, password, database, ssl_mode);
519        let mut conn = pool
520            .get_conn()
521            .await
522            .context("failed to connect to MySQL")?;
523
524        // Query binlog files using SHOW BINARY LOGS.
525        // MySQL 8.0+ may return 3 columns (Log_name, File_size, Encrypted), while some variants
526        // only return the first 2. Decode the row manually so we don't panic on column-count
527        // differences.
528        let rows: Vec<Row> = conn
529            .query("SHOW BINARY LOGS")
530            .await
531            .context("failed to execute `SHOW BINARY LOGS`")?;
532        let query_result = rows
533            .into_iter()
534            .map(|mut row| -> ConnectorResult<(String, u64)> {
535                let log_name = row
536                    .take_opt::<String, _>(0)
537                    .transpose()
538                    .context("`SHOW BINARY LOGS`: failed to decode `Log_name`")?
539                    .ok_or_else(|| anyhow!("`SHOW BINARY LOGS`: missing `Log_name` column"))?;
540                let file_size = row
541                    .take_opt::<u64, _>(1)
542                    .transpose()
543                    .context("`SHOW BINARY LOGS`: failed to decode `File_size`")?
544                    .ok_or_else(|| anyhow!("`SHOW BINARY LOGS`: missing `File_size` column"))?;
545                Ok((log_name, file_size))
546            })
547            .collect::<ConnectorResult<Vec<_>>>()?;
548
549        drop(conn);
550        pool.disconnect().await.ok();
551
552        Ok(query_result)
553    }
554}
555
556impl ListCdcSplits for DebeziumSplitEnumerator<Mysql> {
557    type CdcSourceType = Mysql;
558
559    fn list_cdc_splits(&mut self) -> Vec<DebeziumCdcSplit<Self::CdcSourceType>> {
560        // CDC source only supports single split
561        vec![DebeziumCdcSplit::<Self::CdcSourceType>::new(
562            self.source_id.as_raw_id(),
563            None,
564            None,
565        )]
566    }
567}
568
569#[async_trait]
570impl CdcMonitor for DebeziumSplitEnumerator<Mysql> {
571    async fn monitor_cdc(&mut self) -> ConnectorResult<()> {
572        // For MySQL CDC, query the upstream MySQL binlog files and monitor them.
573        self.monitor_mysql_binlog_files().await?;
574        Ok(())
575    }
576}
577
578impl ListCdcSplits for DebeziumSplitEnumerator<Postgres> {
579    type CdcSourceType = Postgres;
580
581    fn list_cdc_splits(&mut self) -> Vec<DebeziumCdcSplit<Self::CdcSourceType>> {
582        // CDC source only supports single split
583        vec![DebeziumCdcSplit::<Self::CdcSourceType>::new(
584            self.source_id.as_raw_id(),
585            None,
586            None,
587        )]
588    }
589}
590
591#[async_trait]
592impl CdcMonitor for DebeziumSplitEnumerator<Postgres> {
593    async fn monitor_cdc(&mut self) -> ConnectorResult<()> {
594        // For PostgreSQL CDC, query the upstream Postgres confirmed flush lsn and monitor it.
595        self.monitor_postgres_confirmed_flush_lsn().await?;
596        Ok(())
597    }
598}
599
600impl ListCdcSplits for DebeziumSplitEnumerator<Citus> {
601    type CdcSourceType = Citus;
602
603    fn list_cdc_splits(&mut self) -> Vec<DebeziumCdcSplit<Self::CdcSourceType>> {
604        self.worker_node_addrs
605            .iter()
606            .enumerate()
607            .map(|(id, addr)| {
608                DebeziumCdcSplit::<Self::CdcSourceType>::new(
609                    id as u32,
610                    None,
611                    Some(addr.to_string()),
612                )
613            })
614            .collect_vec()
615    }
616}
617impl ListCdcSplits for DebeziumSplitEnumerator<Mongodb> {
618    type CdcSourceType = Mongodb;
619
620    fn list_cdc_splits(&mut self) -> Vec<DebeziumCdcSplit<Self::CdcSourceType>> {
621        // CDC source only supports single split
622        vec![DebeziumCdcSplit::<Self::CdcSourceType>::new(
623            self.source_id.as_raw_id(),
624            None,
625            None,
626        )]
627    }
628}
629
630impl ListCdcSplits for DebeziumSplitEnumerator<SqlServer> {
631    type CdcSourceType = SqlServer;
632
633    fn list_cdc_splits(&mut self) -> Vec<DebeziumCdcSplit<Self::CdcSourceType>> {
634        vec![DebeziumCdcSplit::<Self::CdcSourceType>::new(
635            self.source_id.as_raw_id(),
636            None,
637            None,
638        )]
639    }
640}
641
642#[async_trait]
643impl CdcMonitor for DebeziumSplitEnumerator<SqlServer> {
644    async fn monitor_cdc(&mut self) -> ConnectorResult<()> {
645        self.monitor_sql_server_lsns().await
646    }
647}
648
649#[cfg(test)]
650mod tests {
651    use prometheus::core::Collector;
652    use risingwave_common::metrics::LabelGuardedIntGaugeVec;
653
654    use super::get_or_create_guarded_int_gauge;
655
656    #[test]
657    fn cached_guarded_metric_survives_repeated_collections() {
658        let metric_vec = LabelGuardedIntGaugeVec::test_int_gauge_vec::<2>();
659        let labels = vec!["source_id".to_owned(), "slot_name".to_owned()];
660        let mut metric = None;
661
662        get_or_create_guarded_int_gauge(&mut metric, &metric_vec, &labels).set(1);
663
664        assert_eq!(1, metric_vec.collect().pop().unwrap().get_metric().len());
665        assert_eq!(1, metric_vec.collect().pop().unwrap().get_metric().len());
666
667        drop(metric);
668        assert_eq!(1, metric_vec.collect().pop().unwrap().get_metric().len());
669        assert_eq!(0, metric_vec.collect().pop().unwrap().get_metric().len());
670    }
671}