1use 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
35pub 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
43const META_REGION_STATS_HISTORY_TABLE_NAME: &str = "region_statistics_history";
45const 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 name = "greptime_timestamp",
82 semantic = "Timestamp",
83 datatype = "TimestampMillisecond"
84 )]
85 timestamp_millis: i64,
86}
87
88fn 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!(
125 region_stat.role,
126 RegionRole::Leader | RegionRole::StagingLeader
127 ) {
128 let persisted_region_stat = last_persisted_region_stats.get(®ion_stat.id).map(|s| *s);
129 Some((
130 compute_persist_region_stat(
131 region_stat,
132 datanode_id,
133 timestamp_millis,
134 persisted_region_stat,
135 )
136 .to_row(),
137 PersistedRegionStat::from(region_stat),
138 ))
139 } else {
140 None
141 }
142}
143
144fn align_ts(ts: i64, interval: Duration) -> i64 {
149 assert!(
150 interval.as_millis() != 0,
151 "interval must be greater than zero"
152 );
153 ts / interval.as_millis() as i64 * interval.as_millis() as i64
154}
155
156impl PersistStatsHandler {
157 pub fn new(inserter: Box<dyn Inserter>, mut persist_interval: Duration) -> Self {
159 if persist_interval < Duration::from_mins(10) {
160 warn!("persist_interval is less than 10 minutes, set to 10 minutes");
161 persist_interval = Duration::from_mins(10);
162 }
163
164 Self {
165 inserter,
166 last_persisted_region_stats: DashMap::new(),
167 last_persisted_time: DashMap::new(),
168 persist_interval,
169 }
170 }
171
172 fn should_persist(&self, datanode_id: DatanodeId) -> bool {
173 let Some(last_persisted_time) = self.last_persisted_time.get(&datanode_id) else {
174 return true;
175 };
176
177 last_persisted_time.elapsed() >= self.persist_interval
178 }
179
180 async fn persist(
181 &self,
182 timestamp_millis: i64,
183 datanode_id: DatanodeId,
184 region_stats: &[RegionStat],
185 ) {
186 let aligned_ts = align_ts(timestamp_millis, self.persist_interval);
188 let (rows, incoming_region_stats): (Vec<_>, Vec<_>) = region_stats
189 .iter()
190 .flat_map(|region_stat| {
191 to_persisted_if_leader(
192 region_stat,
193 &self.last_persisted_region_stats,
194 datanode_id,
195 aligned_ts,
196 )
197 })
198 .unzip();
199
200 if rows.is_empty() {
201 return;
202 }
203
204 if let Err(err) = self
205 .inserter
206 .insert_rows(
207 &DEFAULT_CONTEXT,
208 RowInsertRequests {
209 inserts: vec![RowInsertRequest {
210 table_name: META_REGION_STATS_HISTORY_TABLE_NAME.to_string(),
211 rows: Some(Rows {
212 schema: PersistRegionStat::schema(),
213 rows,
214 }),
215 }],
216 },
217 )
218 .await
219 {
220 warn!(
221 "Failed to persist region stats, datanode_id: {}, error: {:?}",
222 datanode_id, err
223 );
224 return;
225 }
226
227 self.last_persisted_time.insert(datanode_id, Instant::now());
228 for s in incoming_region_stats {
229 self.last_persisted_region_stats.insert(s.region_id, s);
230 }
231 }
232}
233
234#[async_trait::async_trait]
235impl HeartbeatHandler for PersistStatsHandler {
236 fn is_acceptable(&self, role: Role) -> bool {
237 role == Role::Datanode
238 }
239
240 async fn handle(
241 &self,
242 _req: &HeartbeatRequest,
243 _: &mut Context,
244 acc: &mut HeartbeatAccumulator,
245 ) -> Result<HandleControl> {
246 let Some(current_stat) = acc.stat.as_ref() else {
247 return Ok(HandleControl::Continue);
248 };
249
250 if !self.should_persist(current_stat.id) {
251 return Ok(HandleControl::Continue);
252 }
253
254 self.persist(
255 current_stat.timestamp_millis,
256 current_stat.id,
257 ¤t_stat.region_stats,
258 )
259 .await;
260
261 Ok(HandleControl::Continue)
262 }
263}
264
265#[cfg(test)]
266mod tests {
267 use std::sync::{Arc, Mutex};
268
269 use client::inserter::{Context as InserterContext, InsertOptions};
270 use common_meta::datanode::{RegionManifestInfo, RegionStat, Stat};
271 use store_api::region_engine::RegionRole;
272 use store_api::storage::RegionId;
273
274 use super::*;
275 use crate::handler::test_utils::TestEnv;
276
277 fn create_test_region_stat(
278 table_id: u32,
279 region_number: u32,
280 written_bytes: u64,
281 engine: &str,
282 ) -> RegionStat {
283 let region_id = RegionId::new(table_id, region_number);
284 RegionStat {
285 id: region_id,
286 rcus: 100,
287 wcus: 200,
288 approximate_bytes: 1024,
289 engine: engine.to_string(),
290 role: RegionRole::Leader,
291 num_rows: 1000,
292 memtable_size: 512,
293 manifest_size: 256,
294 sst_size: 2048,
295 sst_num: 5,
296 index_size: 128,
297 region_manifest: RegionManifestInfo::Mito {
298 manifest_version: 1,
299 flushed_entry_id: 100,
300 file_removed_cnt: 0,
301 },
302 written_bytes,
303 query_cpu_time: 0,
304 query_scanned_bytes: 0,
305 data_topic_latest_entry_id: 200,
306 metadata_topic_latest_entry_id: 200,
307 }
308 }
309
310 #[test]
311 fn test_compute_persist_region_stat_with_no_persisted_stat() {
312 let region_stat = create_test_region_stat(1, 1, 1000, "mito");
313 let datanode_id = 123;
314 let timestamp_millis = 1640995200000; let result = compute_persist_region_stat(®ion_stat, datanode_id, timestamp_millis, None);
316 assert_eq!(result.table_id, 1);
317 assert_eq!(result.region_id, region_stat.id.as_u64());
318 assert_eq!(result.region_number, 1);
319 assert_eq!(result.manifest_size, 256);
320 assert_eq!(result.datanode_id, datanode_id);
321 assert_eq!(result.engine, "mito");
322 assert_eq!(result.num_rows, 1000);
323 assert_eq!(result.sst_num, 5);
324 assert_eq!(result.sst_size, 2048);
325 assert_eq!(result.write_bytes_delta, 0); assert_eq!(result.timestamp_millis, timestamp_millis);
327 }
328
329 #[test]
330 fn test_compute_persist_region_stat_with_persisted_stat_increase() {
331 let region_stat = create_test_region_stat(2, 3, 1500, "mito");
332 let datanode_id = 456;
333 let timestamp_millis = 1640995260000; let persisted_stat = PersistedRegionStat {
335 region_id: region_stat.id,
336 written_bytes: 1000, };
338 let result = compute_persist_region_stat(
339 ®ion_stat,
340 datanode_id,
341 timestamp_millis,
342 Some(persisted_stat),
343 );
344 assert_eq!(result.table_id, 2);
345 assert_eq!(result.region_id, region_stat.id.as_u64());
346 assert_eq!(result.region_number, 3);
347 assert_eq!(result.manifest_size, 256);
348 assert_eq!(result.datanode_id, datanode_id);
349 assert_eq!(result.engine, "mito");
350 assert_eq!(result.num_rows, 1000);
351 assert_eq!(result.sst_num, 5);
352 assert_eq!(result.sst_size, 2048);
353 assert_eq!(result.write_bytes_delta, 500); assert_eq!(result.timestamp_millis, timestamp_millis);
355 }
356
357 #[test]
358 fn test_compute_persist_region_stat_with_persisted_stat_decrease() {
359 let region_stat = create_test_region_stat(3, 5, 800, "mito");
360 let datanode_id = 789;
361 let timestamp_millis = 1640995320000; let persisted_stat = PersistedRegionStat {
363 region_id: region_stat.id,
364 written_bytes: 1200, };
366 let result = compute_persist_region_stat(
367 ®ion_stat,
368 datanode_id,
369 timestamp_millis,
370 Some(persisted_stat),
371 );
372 assert_eq!(result.table_id, 3);
373 assert_eq!(result.region_id, region_stat.id.as_u64());
374 assert_eq!(result.region_number, 5);
375 assert_eq!(result.manifest_size, 256);
376 assert_eq!(result.datanode_id, datanode_id);
377 assert_eq!(result.engine, "mito");
378 assert_eq!(result.num_rows, 1000);
379 assert_eq!(result.sst_num, 5);
380 assert_eq!(result.sst_size, 2048);
381 assert_eq!(result.write_bytes_delta, 0); assert_eq!(result.timestamp_millis, timestamp_millis);
383 }
384
385 #[test]
386 fn test_compute_persist_region_stat_with_persisted_stat_equal() {
387 let region_stat = create_test_region_stat(4, 7, 2000, "mito");
388 let datanode_id = 101;
389 let timestamp_millis = 1640995380000; let persisted_stat = PersistedRegionStat {
391 region_id: region_stat.id,
392 written_bytes: 2000, };
394 let result = compute_persist_region_stat(
395 ®ion_stat,
396 datanode_id,
397 timestamp_millis,
398 Some(persisted_stat),
399 );
400 assert_eq!(result.table_id, 4);
401 assert_eq!(result.region_id, region_stat.id.as_u64());
402 assert_eq!(result.region_number, 7);
403 assert_eq!(result.manifest_size, 256);
404 assert_eq!(result.datanode_id, datanode_id);
405 assert_eq!(result.engine, "mito");
406 assert_eq!(result.num_rows, 1000);
407 assert_eq!(result.sst_num, 5);
408 assert_eq!(result.sst_size, 2048);
409 assert_eq!(result.write_bytes_delta, 0); assert_eq!(result.timestamp_millis, timestamp_millis);
411 }
412
413 #[test]
414 fn test_compute_persist_region_stat_with_overflow_protection() {
415 let region_stat = create_test_region_stat(8, 15, 500, "mito");
416 let datanode_id = 505;
417 let timestamp_millis = 1640995620000; let persisted_stat = PersistedRegionStat {
419 region_id: region_stat.id,
420 written_bytes: 1000, };
422 let result = compute_persist_region_stat(
423 ®ion_stat,
424 datanode_id,
425 timestamp_millis,
426 Some(persisted_stat),
427 );
428 assert_eq!(result.table_id, 8);
429 assert_eq!(result.region_id, region_stat.id.as_u64());
430 assert_eq!(result.region_number, 15);
431 assert_eq!(result.manifest_size, 256);
432 assert_eq!(result.datanode_id, datanode_id);
433 assert_eq!(result.engine, "mito");
434 assert_eq!(result.num_rows, 1000);
435 assert_eq!(result.sst_num, 5);
436 assert_eq!(result.sst_size, 2048);
437 assert_eq!(result.write_bytes_delta, 0); assert_eq!(result.timestamp_millis, timestamp_millis);
439 }
440
441 struct MockInserter {
442 requests: Arc<Mutex<Vec<api::v1::RowInsertRequest>>>,
443 }
444
445 #[async_trait::async_trait]
446 impl Inserter for MockInserter {
447 async fn insert_rows(
448 &self,
449 _context: &InserterContext<'_>,
450 requests: api::v1::RowInsertRequests,
451 ) -> client::error::Result<()> {
452 self.requests.lock().unwrap().extend(requests.inserts);
453
454 Ok(())
455 }
456
457 fn set_options(&mut self, _options: &InsertOptions) {}
458 }
459
460 #[tokio::test]
461 async fn test_not_persist_region_stats() {
462 let env = TestEnv::new();
463 let mut ctx = env.ctx();
464
465 let requests = Arc::new(Mutex::new(vec![]));
466 let inserter = MockInserter {
467 requests: requests.clone(),
468 };
469 let handler = PersistStatsHandler::new(Box::new(inserter), Duration::from_secs(10));
470 let mut acc = HeartbeatAccumulator {
471 stat: Some(Stat {
472 id: 1,
473 timestamp_millis: 1640995200000,
474 region_stats: vec![create_test_region_stat(1, 1, 1000, "mito")],
475 ..Default::default()
476 }),
477 ..Default::default()
478 };
479 handler.last_persisted_time.insert(1, Instant::now());
480 handler
482 .handle(&HeartbeatRequest::default(), &mut ctx, &mut acc)
483 .await
484 .unwrap();
485 assert!(requests.lock().unwrap().is_empty());
486 }
487
488 #[tokio::test]
489 async fn test_persist_region_stats() {
490 let env = TestEnv::new();
491 let mut ctx = env.ctx();
492 let requests = Arc::new(Mutex::new(vec![]));
493 let inserter = MockInserter {
494 requests: requests.clone(),
495 };
496
497 let handler = PersistStatsHandler::new(Box::new(inserter), Duration::from_secs(10));
498
499 let region_stat = create_test_region_stat(1, 1, 1000, "mito");
500 let timestamp_millis = 1640995200000;
501 let datanode_id = 1;
502 let region_id = RegionId::new(1, 1);
503 let mut acc = HeartbeatAccumulator {
504 stat: Some(Stat {
505 id: datanode_id,
506 timestamp_millis,
507 region_stats: vec![region_stat.clone()],
508 ..Default::default()
509 }),
510 ..Default::default()
511 };
512
513 handler.last_persisted_region_stats.insert(
514 region_id,
515 PersistedRegionStat {
516 region_id,
517 written_bytes: 500,
518 },
519 );
520 let (expected_row, expected_persisted_region_stat) = to_persisted_if_leader(
521 ®ion_stat,
522 &handler.last_persisted_region_stats,
523 datanode_id,
524 timestamp_millis,
525 )
526 .unwrap();
527 let before_insert_time = Instant::now();
528 handler
530 .handle(&HeartbeatRequest::default(), &mut ctx, &mut acc)
531 .await
532 .unwrap();
533 let request = {
534 let mut requests = requests.lock().unwrap();
535 assert_eq!(requests.len(), 1);
536 requests.pop().unwrap()
537 };
538 assert_eq!(
539 request.table_name,
540 META_REGION_STATS_HISTORY_TABLE_NAME.to_string()
541 );
542 assert_eq!(request.rows.unwrap().rows, vec![expected_row]);
543
544 assert!(
546 handler
547 .last_persisted_time
548 .get(&datanode_id)
549 .unwrap()
550 .gt(&before_insert_time)
551 );
552
553 assert_eq!(
555 handler
556 .last_persisted_region_stats
557 .get(®ion_id)
558 .unwrap()
559 .value(),
560 &expected_persisted_region_stat
561 );
562 }
563}