meta_srv/handler/
persist_stats_handler.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
15use std::time::{Duration, Instant};
16
17use api::v1::meta::{HeartbeatRequest, Role};
18use api::v1::value::ValueData;
19use api::v1::{ColumnSchema, Row, RowInsertRequest, RowInsertRequests, Rows, Value};
20use client::DEFAULT_CATALOG_NAME;
21use client::inserter::{Context as InserterContext, Inserter};
22use common_catalog::consts::DEFAULT_PRIVATE_SCHEMA_NAME;
23use common_macro::{Schema, ToRow};
24use common_meta::DatanodeId;
25use common_meta::datanode::RegionStat;
26use common_telemetry::warn;
27use dashmap::DashMap;
28use store_api::region_engine::RegionRole;
29use store_api::storage::RegionId;
30
31use crate::error::Result;
32use crate::handler::{HandleControl, HeartbeatAccumulator, HeartbeatHandler};
33use crate::metasrv::Context;
34
35/// The handler to persist stats.
36pub struct PersistStatsHandler {
37    inserter: Box<dyn Inserter>,
38    last_persisted_region_stats: DashMap<RegionId, PersistedRegionStat>,
39    last_persisted_time: DashMap<DatanodeId, Instant>,
40    persist_interval: Duration,
41}
42
43/// The name of the table to persist region stats.
44const META_REGION_STATS_HISTORY_TABLE_NAME: &str = "region_statistics_history";
45/// The default context to persist region stats.
46const DEFAULT_CONTEXT: InserterContext = InserterContext {
47    catalog: DEFAULT_CATALOG_NAME,
48    schema: DEFAULT_PRIVATE_SCHEMA_NAME,
49};
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52struct PersistedRegionStat {
53    region_id: RegionId,
54    written_bytes: u64,
55}
56
57impl From<&RegionStat> for PersistedRegionStat {
58    fn from(stat: &RegionStat) -> Self {
59        Self {
60            region_id: stat.id,
61            written_bytes: stat.written_bytes,
62        }
63    }
64}
65
66#[derive(ToRow, Schema)]
67struct PersistRegionStat<'a> {
68    table_id: u32,
69    region_id: u64,
70    region_number: u32,
71    manifest_size: u64,
72    datanode_id: u64,
73    #[col(datatype = "string")]
74    engine: &'a str,
75    num_rows: u64,
76    sst_num: u64,
77    sst_size: u64,
78    write_bytes_delta: u64,
79    #[col(
80        // This col name is for the information schema table, so we don't touch it
81        name = "greptime_timestamp",
82        semantic = "Timestamp",
83        datatype = "TimestampMillisecond"
84    )]
85    timestamp_millis: i64,
86}
87
88/// Compute the region stat to persist.
89fn compute_persist_region_stat(
90    region_stat: &RegionStat,
91    datanode_id: DatanodeId,
92    timestamp_millis: i64,
93    persisted_region_stat: Option<PersistedRegionStat>,
94) -> PersistRegionStat<'_> {
95    let write_bytes_delta = persisted_region_stat
96        .and_then(|persisted_region_stat| {
97            region_stat
98                .written_bytes
99                .checked_sub(persisted_region_stat.written_bytes)
100        })
101        .unwrap_or_default();
102
103    PersistRegionStat {
104        table_id: region_stat.id.table_id(),
105        region_id: region_stat.id.as_u64(),
106        region_number: region_stat.id.region_number(),
107        manifest_size: region_stat.manifest_size,
108        datanode_id,
109        engine: region_stat.engine.as_str(),
110        num_rows: region_stat.num_rows,
111        sst_num: region_stat.sst_num,
112        sst_size: region_stat.sst_size,
113        write_bytes_delta,
114        timestamp_millis,
115    }
116}
117
118fn to_persisted_if_leader(
119    region_stat: &RegionStat,
120    last_persisted_region_stats: &DashMap<RegionId, PersistedRegionStat>,
121    datanode_id: DatanodeId,
122    timestamp_millis: i64,
123) -> Option<(Row, PersistedRegionStat)> {
124    if matches!(region_stat.role, RegionRole::Leader) {
125        let persisted_region_stat = last_persisted_region_stats.get(&region_stat.id).map(|s| *s);
126        Some((
127            compute_persist_region_stat(
128                region_stat,
129                datanode_id,
130                timestamp_millis,
131                persisted_region_stat,
132            )
133            .to_row(),
134            PersistedRegionStat::from(region_stat),
135        ))
136    } else {
137        None
138    }
139}
140
141/// Align the timestamp to the nearest interval.
142///
143/// # Panics
144/// Panics if `interval` as milliseconds is zero.
145fn align_ts(ts: i64, interval: Duration) -> i64 {
146    assert!(
147        interval.as_millis() != 0,
148        "interval must be greater than zero"
149    );
150    ts / interval.as_millis() as i64 * interval.as_millis() as i64
151}
152
153impl PersistStatsHandler {
154    /// Creates a new [`PersistStatsHandler`].
155    pub fn new(inserter: Box<dyn Inserter>, mut persist_interval: Duration) -> Self {
156        if persist_interval < Duration::from_mins(10) {
157            warn!("persist_interval is less than 10 minutes, set to 10 minutes");
158            persist_interval = Duration::from_mins(10);
159        }
160
161        Self {
162            inserter,
163            last_persisted_region_stats: DashMap::new(),
164            last_persisted_time: DashMap::new(),
165            persist_interval,
166        }
167    }
168
169    fn should_persist(&self, datanode_id: DatanodeId) -> bool {
170        let Some(last_persisted_time) = self.last_persisted_time.get(&datanode_id) else {
171            return true;
172        };
173
174        last_persisted_time.elapsed() >= self.persist_interval
175    }
176
177    async fn persist(
178        &self,
179        timestamp_millis: i64,
180        datanode_id: DatanodeId,
181        region_stats: &[RegionStat],
182    ) {
183        // Safety: persist_interval is greater than zero.
184        let aligned_ts = align_ts(timestamp_millis, self.persist_interval);
185        let (rows, incoming_region_stats): (Vec<_>, Vec<_>) = region_stats
186            .iter()
187            .flat_map(|region_stat| {
188                to_persisted_if_leader(
189                    region_stat,
190                    &self.last_persisted_region_stats,
191                    datanode_id,
192                    aligned_ts,
193                )
194            })
195            .unzip();
196
197        if rows.is_empty() {
198            return;
199        }
200
201        if let Err(err) = self
202            .inserter
203            .insert_rows(
204                &DEFAULT_CONTEXT,
205                RowInsertRequests {
206                    inserts: vec![RowInsertRequest {
207                        table_name: META_REGION_STATS_HISTORY_TABLE_NAME.to_string(),
208                        rows: Some(Rows {
209                            schema: PersistRegionStat::schema(),
210                            rows,
211                        }),
212                    }],
213                },
214            )
215            .await
216        {
217            warn!(
218                "Failed to persist region stats, datanode_id: {}, error: {:?}",
219                datanode_id, err
220            );
221            return;
222        }
223
224        self.last_persisted_time.insert(datanode_id, Instant::now());
225        for s in incoming_region_stats {
226            self.last_persisted_region_stats.insert(s.region_id, s);
227        }
228    }
229}
230
231#[async_trait::async_trait]
232impl HeartbeatHandler for PersistStatsHandler {
233    fn is_acceptable(&self, role: Role) -> bool {
234        role == Role::Datanode
235    }
236
237    async fn handle(
238        &self,
239        _req: &HeartbeatRequest,
240        _: &mut Context,
241        acc: &mut HeartbeatAccumulator,
242    ) -> Result<HandleControl> {
243        let Some(current_stat) = acc.stat.as_ref() else {
244            return Ok(HandleControl::Continue);
245        };
246
247        if !self.should_persist(current_stat.id) {
248            return Ok(HandleControl::Continue);
249        }
250
251        self.persist(
252            current_stat.timestamp_millis,
253            current_stat.id,
254            &current_stat.region_stats,
255        )
256        .await;
257
258        Ok(HandleControl::Continue)
259    }
260}
261
262#[cfg(test)]
263mod tests {
264    use std::sync::{Arc, Mutex};
265
266    use client::inserter::{Context as InserterContext, InsertOptions};
267    use common_meta::datanode::{RegionManifestInfo, RegionStat, Stat};
268    use store_api::region_engine::RegionRole;
269    use store_api::storage::RegionId;
270
271    use super::*;
272    use crate::handler::test_utils::TestEnv;
273
274    fn create_test_region_stat(
275        table_id: u32,
276        region_number: u32,
277        written_bytes: u64,
278        engine: &str,
279    ) -> RegionStat {
280        let region_id = RegionId::new(table_id, region_number);
281        RegionStat {
282            id: region_id,
283            rcus: 100,
284            wcus: 200,
285            approximate_bytes: 1024,
286            engine: engine.to_string(),
287            role: RegionRole::Leader,
288            num_rows: 1000,
289            memtable_size: 512,
290            manifest_size: 256,
291            sst_size: 2048,
292            sst_num: 5,
293            index_size: 128,
294            region_manifest: RegionManifestInfo::Mito {
295                manifest_version: 1,
296                flushed_entry_id: 100,
297            },
298            written_bytes,
299            data_topic_latest_entry_id: 200,
300            metadata_topic_latest_entry_id: 200,
301        }
302    }
303
304    #[test]
305    fn test_compute_persist_region_stat_with_no_persisted_stat() {
306        let region_stat = create_test_region_stat(1, 1, 1000, "mito");
307        let datanode_id = 123;
308        let timestamp_millis = 1640995200000; // 2022-01-01 00:00:00 UTC
309        let result = compute_persist_region_stat(&region_stat, datanode_id, timestamp_millis, None);
310        assert_eq!(result.table_id, 1);
311        assert_eq!(result.region_id, region_stat.id.as_u64());
312        assert_eq!(result.region_number, 1);
313        assert_eq!(result.manifest_size, 256);
314        assert_eq!(result.datanode_id, datanode_id);
315        assert_eq!(result.engine, "mito");
316        assert_eq!(result.num_rows, 1000);
317        assert_eq!(result.sst_num, 5);
318        assert_eq!(result.sst_size, 2048);
319        assert_eq!(result.write_bytes_delta, 0); // No previous stat, so delta is 0
320        assert_eq!(result.timestamp_millis, timestamp_millis);
321    }
322
323    #[test]
324    fn test_compute_persist_region_stat_with_persisted_stat_increase() {
325        let region_stat = create_test_region_stat(2, 3, 1500, "mito");
326        let datanode_id = 456;
327        let timestamp_millis = 1640995260000; // 2022-01-01 00:01:00 UTC
328        let persisted_stat = PersistedRegionStat {
329            region_id: region_stat.id,
330            written_bytes: 1000, // Previous write bytes
331        };
332        let result = compute_persist_region_stat(
333            &region_stat,
334            datanode_id,
335            timestamp_millis,
336            Some(persisted_stat),
337        );
338        assert_eq!(result.table_id, 2);
339        assert_eq!(result.region_id, region_stat.id.as_u64());
340        assert_eq!(result.region_number, 3);
341        assert_eq!(result.manifest_size, 256);
342        assert_eq!(result.datanode_id, datanode_id);
343        assert_eq!(result.engine, "mito");
344        assert_eq!(result.num_rows, 1000);
345        assert_eq!(result.sst_num, 5);
346        assert_eq!(result.sst_size, 2048);
347        assert_eq!(result.write_bytes_delta, 500); // 1500 - 1000 = 500
348        assert_eq!(result.timestamp_millis, timestamp_millis);
349    }
350
351    #[test]
352    fn test_compute_persist_region_stat_with_persisted_stat_decrease() {
353        let region_stat = create_test_region_stat(3, 5, 800, "mito");
354        let datanode_id = 789;
355        let timestamp_millis = 1640995320000; // 2022-01-01 00:02:00 UTC
356        let persisted_stat = PersistedRegionStat {
357            region_id: region_stat.id,
358            written_bytes: 1200, // Previous write bytes (higher than current)
359        };
360        let result = compute_persist_region_stat(
361            &region_stat,
362            datanode_id,
363            timestamp_millis,
364            Some(persisted_stat),
365        );
366        assert_eq!(result.table_id, 3);
367        assert_eq!(result.region_id, region_stat.id.as_u64());
368        assert_eq!(result.region_number, 5);
369        assert_eq!(result.manifest_size, 256);
370        assert_eq!(result.datanode_id, datanode_id);
371        assert_eq!(result.engine, "mito");
372        assert_eq!(result.num_rows, 1000);
373        assert_eq!(result.sst_num, 5);
374        assert_eq!(result.sst_size, 2048);
375        assert_eq!(result.write_bytes_delta, 0); // 800 - 1200 would be negative, so 0 due to checked_sub
376        assert_eq!(result.timestamp_millis, timestamp_millis);
377    }
378
379    #[test]
380    fn test_compute_persist_region_stat_with_persisted_stat_equal() {
381        let region_stat = create_test_region_stat(4, 7, 2000, "mito");
382        let datanode_id = 101;
383        let timestamp_millis = 1640995380000; // 2022-01-01 00:03:00 UTC
384        let persisted_stat = PersistedRegionStat {
385            region_id: region_stat.id,
386            written_bytes: 2000, // Same as current write bytes
387        };
388        let result = compute_persist_region_stat(
389            &region_stat,
390            datanode_id,
391            timestamp_millis,
392            Some(persisted_stat),
393        );
394        assert_eq!(result.table_id, 4);
395        assert_eq!(result.region_id, region_stat.id.as_u64());
396        assert_eq!(result.region_number, 7);
397        assert_eq!(result.manifest_size, 256);
398        assert_eq!(result.datanode_id, datanode_id);
399        assert_eq!(result.engine, "mito");
400        assert_eq!(result.num_rows, 1000);
401        assert_eq!(result.sst_num, 5);
402        assert_eq!(result.sst_size, 2048);
403        assert_eq!(result.write_bytes_delta, 0); // 2000 - 2000 = 0
404        assert_eq!(result.timestamp_millis, timestamp_millis);
405    }
406
407    #[test]
408    fn test_compute_persist_region_stat_with_overflow_protection() {
409        let region_stat = create_test_region_stat(8, 15, 500, "mito");
410        let datanode_id = 505;
411        let timestamp_millis = 1640995620000; // 2022-01-01 00:07:00 UTC
412        let persisted_stat = PersistedRegionStat {
413            region_id: region_stat.id,
414            written_bytes: 1000, // Higher than current, would cause underflow
415        };
416        let result = compute_persist_region_stat(
417            &region_stat,
418            datanode_id,
419            timestamp_millis,
420            Some(persisted_stat),
421        );
422        assert_eq!(result.table_id, 8);
423        assert_eq!(result.region_id, region_stat.id.as_u64());
424        assert_eq!(result.region_number, 15);
425        assert_eq!(result.manifest_size, 256);
426        assert_eq!(result.datanode_id, datanode_id);
427        assert_eq!(result.engine, "mito");
428        assert_eq!(result.num_rows, 1000);
429        assert_eq!(result.sst_num, 5);
430        assert_eq!(result.sst_size, 2048);
431        assert_eq!(result.write_bytes_delta, 0); // checked_sub returns None, so default to 0
432        assert_eq!(result.timestamp_millis, timestamp_millis);
433    }
434
435    struct MockInserter {
436        requests: Arc<Mutex<Vec<api::v1::RowInsertRequest>>>,
437    }
438
439    #[async_trait::async_trait]
440    impl Inserter for MockInserter {
441        async fn insert_rows(
442            &self,
443            _context: &InserterContext<'_>,
444            requests: api::v1::RowInsertRequests,
445        ) -> client::error::Result<()> {
446            self.requests.lock().unwrap().extend(requests.inserts);
447
448            Ok(())
449        }
450
451        fn set_options(&mut self, _options: &InsertOptions) {}
452    }
453
454    #[tokio::test]
455    async fn test_not_persist_region_stats() {
456        let env = TestEnv::new();
457        let mut ctx = env.ctx();
458
459        let requests = Arc::new(Mutex::new(vec![]));
460        let inserter = MockInserter {
461            requests: requests.clone(),
462        };
463        let handler = PersistStatsHandler::new(Box::new(inserter), Duration::from_secs(10));
464        let mut acc = HeartbeatAccumulator {
465            stat: Some(Stat {
466                id: 1,
467                timestamp_millis: 1640995200000,
468                region_stats: vec![create_test_region_stat(1, 1, 1000, "mito")],
469                ..Default::default()
470            }),
471            ..Default::default()
472        };
473        handler.last_persisted_time.insert(1, Instant::now());
474        // Do not persist
475        handler
476            .handle(&HeartbeatRequest::default(), &mut ctx, &mut acc)
477            .await
478            .unwrap();
479        assert!(requests.lock().unwrap().is_empty());
480    }
481
482    #[tokio::test]
483    async fn test_persist_region_stats() {
484        let env = TestEnv::new();
485        let mut ctx = env.ctx();
486        let requests = Arc::new(Mutex::new(vec![]));
487        let inserter = MockInserter {
488            requests: requests.clone(),
489        };
490
491        let handler = PersistStatsHandler::new(Box::new(inserter), Duration::from_secs(10));
492
493        let region_stat = create_test_region_stat(1, 1, 1000, "mito");
494        let timestamp_millis = 1640995200000;
495        let datanode_id = 1;
496        let region_id = RegionId::new(1, 1);
497        let mut acc = HeartbeatAccumulator {
498            stat: Some(Stat {
499                id: datanode_id,
500                timestamp_millis,
501                region_stats: vec![region_stat.clone()],
502                ..Default::default()
503            }),
504            ..Default::default()
505        };
506
507        handler.last_persisted_region_stats.insert(
508            region_id,
509            PersistedRegionStat {
510                region_id,
511                written_bytes: 500,
512            },
513        );
514        let (expected_row, expected_persisted_region_stat) = to_persisted_if_leader(
515            &region_stat,
516            &handler.last_persisted_region_stats,
517            datanode_id,
518            timestamp_millis,
519        )
520        .unwrap();
521        let before_insert_time = Instant::now();
522        // Persist
523        handler
524            .handle(&HeartbeatRequest::default(), &mut ctx, &mut acc)
525            .await
526            .unwrap();
527        let request = {
528            let mut requests = requests.lock().unwrap();
529            assert_eq!(requests.len(), 1);
530            requests.pop().unwrap()
531        };
532        assert_eq!(
533            request.table_name,
534            META_REGION_STATS_HISTORY_TABLE_NAME.to_string()
535        );
536        assert_eq!(request.rows.unwrap().rows, vec![expected_row]);
537
538        // Check last persisted time
539        assert!(
540            handler
541                .last_persisted_time
542                .get(&datanode_id)
543                .unwrap()
544                .gt(&before_insert_time)
545        );
546
547        // Check last persisted region stats
548        assert_eq!(
549            handler
550                .last_persisted_region_stats
551                .get(&region_id)
552                .unwrap()
553                .value(),
554            &expected_persisted_region_stat
555        );
556    }
557}