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