1use std::collections::{HashMap, HashSet};
16use std::str::FromStr;
17
18use api::v1::meta::{DatanodeWorkloads, HeartbeatRequest, RequestHeader};
19use common_time::{Timestamp, util as time_util};
20use lazy_static::lazy_static;
21use regex::Regex;
22use serde::{Deserialize, Serialize};
23use snafu::{OptionExt, ResultExt, ensure};
24use store_api::region_engine::{RegionRole, RegionStatistic};
25use store_api::storage::RegionId;
26use table::metadata::TableId;
27
28use crate::error::{self, DeserializeFromJsonSnafu, Result};
29use crate::heartbeat::utils::get_datanode_workloads;
30
31const DATANODE_STAT_PREFIX: &str = "__meta_datanode_stat";
32
33pub const REGION_STATISTIC_KEY: &str = "__region_statistic";
34pub const REGION_STATS_HISTORY_TABLE_NAME: &str = "region_statistics_history";
36
37lazy_static! {
38 pub(crate) static ref DATANODE_LEASE_KEY_PATTERN: Regex =
39 Regex::new("^__meta_datanode_lease-([0-9]+)-([0-9]+)$").unwrap();
40 static ref DATANODE_STAT_KEY_PATTERN: Regex =
41 Regex::new(&format!("^{DATANODE_STAT_PREFIX}-([0-9]+)-([0-9]+)$")).unwrap();
42 static ref INACTIVE_REGION_KEY_PATTERN: Regex =
43 Regex::new("^__meta_inactive_region-([0-9]+)-([0-9]+)-([0-9]+)$").unwrap();
44}
45
46#[derive(Debug, Clone, Default, Serialize, Deserialize)]
50pub struct Stat {
51 pub timestamp_millis: i64,
52 pub id: u64,
54 pub addr: String,
56 pub rcus: i64,
58 pub wcus: i64,
60 pub region_num: u64,
62 pub region_stats: Vec<RegionStat>,
64 pub topic_stats: Vec<TopicStat>,
66 pub node_epoch: u64,
68 pub datanode_workloads: DatanodeWorkloads,
70 pub gc_stat: Option<GcStat>,
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
76pub struct RegionStat {
77 pub id: RegionId,
79 pub rcus: i64,
81 pub wcus: i64,
83 pub approximate_bytes: u64,
85 pub engine: String,
87 pub role: RegionRole,
89 pub num_rows: u64,
91 pub memtable_size: u64,
93 pub manifest_size: u64,
95 pub sst_size: u64,
97 pub sst_num: u64,
99 pub index_size: u64,
101 pub region_manifest: RegionManifestInfo,
103 pub written_bytes: u64,
105 #[serde(default, alias = "flushed_parquet_bytes")]
111 pub ingested_parquet_bytes: u64,
112 #[serde(default)]
116 pub query_cpu_time: u64,
117 #[serde(default)]
119 pub query_scanned_bytes: u64,
120 pub data_topic_latest_entry_id: u64,
123 pub metadata_topic_latest_entry_id: u64,
127 #[serde(default)]
129 pub min_timestamp: Option<Timestamp>,
130 #[serde(default)]
132 pub max_timestamp: Option<Timestamp>,
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct TopicStat {
137 pub topic: String,
139 pub latest_entry_id: u64,
141 pub record_size: u64,
143 pub record_num: u64,
145}
146
147pub trait TopicStatsReporter: Send + Sync {
149 fn reportable_topics(&mut self) -> Vec<TopicStat>;
151}
152
153#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
154pub enum RegionManifestInfo {
155 Mito {
156 manifest_version: u64,
157 flushed_entry_id: u64,
158 file_removed_cnt: u64,
160 },
161 Metric {
162 data_manifest_version: u64,
163 data_flushed_entry_id: u64,
164 metadata_manifest_version: u64,
165 metadata_flushed_entry_id: u64,
166 },
167}
168
169impl Stat {
170 #[inline]
171 pub fn is_empty(&self) -> bool {
172 self.region_stats.is_empty()
173 }
174
175 pub fn stat_key(&self) -> DatanodeStatKey {
176 DatanodeStatKey { node_id: self.id }
177 }
178
179 pub fn regions(&self) -> Vec<(RegionId, RegionRole)> {
181 self.region_stats.iter().map(|s| (s.id, s.role)).collect()
182 }
183
184 pub fn table_ids(&self) -> HashSet<TableId> {
186 self.region_stats.iter().map(|s| s.id.table_id()).collect()
187 }
188
189 pub fn retain_active_region_stats(&mut self, inactive_region_ids: &HashSet<RegionId>) {
191 if inactive_region_ids.is_empty() {
192 return;
193 }
194
195 self.region_stats
196 .retain(|r| !inactive_region_ids.contains(&r.id));
197 self.rcus = self.region_stats.iter().map(|s| s.rcus).sum();
198 self.wcus = self.region_stats.iter().map(|s| s.wcus).sum();
199 self.region_num = self.region_stats.len() as u64;
200 }
201
202 pub fn memory_size(&self) -> usize {
203 std::mem::size_of::<i64>() * 3 +
205 std::mem::size_of::<u64>() * 3 +
207 std::mem::size_of::<String>() + self.addr.capacity() +
209 self.region_stats.iter().map(|s| s.memory_size()).sum::<usize>()
211 }
212}
213
214impl RegionStat {
215 pub fn memory_size(&self) -> usize {
216 std::mem::size_of::<RegionRole>() +
218 std::mem::size_of::<RegionId>() +
220 std::mem::size_of::<i64>() * 4 +
222 std::mem::size_of::<u64>() * 5 +
224 std::mem::size_of::<String>() + self.engine.capacity() +
226 self.region_manifest.memory_size()
228 }
229}
230
231impl RegionManifestInfo {
232 pub fn memory_size(&self) -> usize {
233 match self {
234 RegionManifestInfo::Mito { .. } => std::mem::size_of::<u64>() * 2,
235 RegionManifestInfo::Metric { .. } => std::mem::size_of::<u64>() * 4,
236 }
237 }
238}
239
240impl TryFrom<&HeartbeatRequest> for Stat {
241 type Error = Option<RequestHeader>;
242
243 fn try_from(value: &HeartbeatRequest) -> std::result::Result<Self, Self::Error> {
244 let HeartbeatRequest {
245 header,
246 peer,
247 region_stats,
248 node_epoch,
249 node_workloads,
250 topic_stats,
251 extensions,
252 ..
253 } = value;
254
255 match (header, peer) {
256 (Some(header), Some(peer)) => {
257 let region_stats = region_stats
258 .iter()
259 .map(RegionStat::from)
260 .collect::<Vec<_>>();
261 let topic_stats = topic_stats.iter().map(TopicStat::from).collect::<Vec<_>>();
262
263 let datanode_workloads = get_datanode_workloads(node_workloads.as_ref());
264
265 let gc_stat = GcStat::from_extensions(extensions).map_err(|err| {
266 common_telemetry::error!(
267 "Failed to deserialize GcStat from extensions: {}",
268 err
269 );
270 header.clone()
271 })?;
272 Ok(Self {
273 timestamp_millis: time_util::current_time_millis(),
274 id: peer.id,
276 addr: peer.addr.clone(),
278 rcus: region_stats.iter().map(|s| s.rcus).sum(),
279 wcus: region_stats.iter().map(|s| s.wcus).sum(),
280 region_num: region_stats.len() as u64,
281 region_stats,
282 topic_stats,
283 node_epoch: *node_epoch,
284 datanode_workloads,
285 gc_stat,
286 })
287 }
288 (header, _) => Err(header.clone()),
289 }
290 }
291}
292
293impl From<store_api::region_engine::RegionManifestInfo> for RegionManifestInfo {
294 fn from(value: store_api::region_engine::RegionManifestInfo) -> Self {
295 match value {
296 store_api::region_engine::RegionManifestInfo::Mito {
297 manifest_version,
298 flushed_entry_id,
299 file_removed_cnt,
300 } => RegionManifestInfo::Mito {
301 manifest_version,
302 flushed_entry_id,
303 file_removed_cnt,
304 },
305 store_api::region_engine::RegionManifestInfo::Metric {
306 data_manifest_version,
307 data_flushed_entry_id,
308 metadata_manifest_version,
309 metadata_flushed_entry_id,
310 } => RegionManifestInfo::Metric {
311 data_manifest_version,
312 data_flushed_entry_id,
313 metadata_manifest_version,
314 metadata_flushed_entry_id,
315 },
316 }
317 }
318}
319
320impl From<&api::v1::meta::RegionStat> for RegionStat {
321 fn from(value: &api::v1::meta::RegionStat) -> Self {
322 let region_stat = value
323 .extensions
324 .get(REGION_STATISTIC_KEY)
325 .and_then(|value| RegionStatistic::deserialize_from_slice(value))
326 .unwrap_or_default();
327
328 Self {
329 id: RegionId::from_u64(value.region_id),
330 rcus: value.rcus,
331 wcus: value.wcus,
332 approximate_bytes: value.approximate_bytes as u64,
333 engine: value.engine.clone(),
334 role: RegionRole::from(value.role()),
335 num_rows: region_stat.num_rows,
336 memtable_size: region_stat.memtable_size,
337 manifest_size: region_stat.manifest_size,
338 sst_size: region_stat.sst_size,
339 sst_num: region_stat.sst_num,
340 index_size: region_stat.index_size,
341 region_manifest: region_stat.manifest.into(),
342 written_bytes: region_stat.written_bytes,
343 ingested_parquet_bytes: region_stat.ingested_parquet_bytes,
344 query_cpu_time: region_stat.query_cpu_time,
345 query_scanned_bytes: region_stat.query_scanned_bytes,
346 data_topic_latest_entry_id: region_stat.data_topic_latest_entry_id,
347 metadata_topic_latest_entry_id: region_stat.metadata_topic_latest_entry_id,
348 min_timestamp: region_stat.min_timestamp,
349 max_timestamp: region_stat.max_timestamp,
350 }
351 }
352}
353
354impl From<&api::v1::meta::TopicStat> for TopicStat {
355 fn from(value: &api::v1::meta::TopicStat) -> Self {
356 Self {
357 topic: value.topic_name.clone(),
358 latest_entry_id: value.latest_entry_id,
359 record_size: value.record_size,
360 record_num: value.record_num,
361 }
362 }
363}
364
365#[derive(Debug, Clone, Serialize, Deserialize, Default)]
366pub struct GcStat {
367 pub running_gc_tasks: u32,
369 pub gc_concurrency: u32,
371}
372
373impl GcStat {
374 pub const GC_STAT_KEY: &str = "__gc_stat";
375
376 pub fn new(running_gc_tasks: u32, gc_concurrency: u32) -> Self {
377 Self {
378 running_gc_tasks,
379 gc_concurrency,
380 }
381 }
382
383 pub fn into_extensions(&self, extensions: &mut std::collections::HashMap<String, Vec<u8>>) {
384 let bytes = serde_json::to_vec(self).unwrap_or_default();
385 extensions.insert(Self::GC_STAT_KEY.to_string(), bytes);
386 }
387
388 pub fn from_extensions(
389 extensions: &std::collections::HashMap<String, Vec<u8>>,
390 ) -> Result<Option<Self>> {
391 extensions
392 .get(Self::GC_STAT_KEY)
393 .map(|bytes| {
394 serde_json::from_slice(bytes).with_context(|_| DeserializeFromJsonSnafu {
395 input: String::from_utf8_lossy(bytes).to_string(),
396 })
397 })
398 .transpose()
399 }
400}
401
402#[derive(Debug, Clone, Serialize, Deserialize, Default)]
404pub struct EnvVars {
405 pub vars: HashMap<String, String>,
406}
407
408impl EnvVars {
409 pub const ENV_VARS_KEY: &str = "__env_vars";
410
411 pub fn new(vars: HashMap<String, String>) -> Self {
412 Self { vars }
413 }
414
415 pub fn from_config(keys: &[String]) -> Self {
417 let vars = keys
418 .iter()
419 .filter_map(|key| std::env::var(key).ok().map(|value| (key.clone(), value)))
420 .collect();
421 Self { vars }
422 }
423
424 pub fn into_extensions(&self, extensions: &mut HashMap<String, Vec<u8>>) {
425 if self.vars.is_empty() {
426 return;
427 }
428 let bytes = serde_json::to_vec(self).unwrap_or_default();
429 extensions.insert(Self::ENV_VARS_KEY.to_string(), bytes);
430 }
431
432 pub fn from_extensions(extensions: &HashMap<String, Vec<u8>>) -> Result<Option<Self>> {
433 extensions
434 .get(Self::ENV_VARS_KEY)
435 .map(|bytes| {
436 serde_json::from_slice(bytes).with_context(|_| DeserializeFromJsonSnafu {
437 input: String::from_utf8_lossy(bytes).to_string(),
438 })
439 })
440 .transpose()
441 }
442}
443
444#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
448pub struct DatanodeStatKey {
449 pub node_id: u64,
450}
451
452impl DatanodeStatKey {
453 pub fn prefix_key() -> Vec<u8> {
455 format!("{DATANODE_STAT_PREFIX}-0-").into_bytes()
457 }
458}
459
460impl From<DatanodeStatKey> for Vec<u8> {
461 fn from(value: DatanodeStatKey) -> Self {
462 format!("{}-0-{}", DATANODE_STAT_PREFIX, value.node_id).into_bytes()
464 }
465}
466
467impl FromStr for DatanodeStatKey {
468 type Err = error::Error;
469
470 fn from_str(key: &str) -> Result<Self> {
471 let caps = DATANODE_STAT_KEY_PATTERN
472 .captures(key)
473 .context(error::InvalidStatKeySnafu { key })?;
474
475 ensure!(caps.len() == 3, error::InvalidStatKeySnafu { key });
476 let node_id = caps[2].to_string();
477 let node_id: u64 = node_id.parse().context(error::ParseNumSnafu {
478 err_msg: format!("invalid node_id: {node_id}"),
479 })?;
480
481 Ok(Self { node_id })
482 }
483}
484
485impl TryFrom<Vec<u8>> for DatanodeStatKey {
486 type Error = error::Error;
487
488 fn try_from(bytes: Vec<u8>) -> Result<Self> {
489 String::from_utf8(bytes)
490 .context(error::FromUtf8Snafu {
491 name: "DatanodeStatKey",
492 })
493 .map(|x| x.parse())?
494 }
495}
496
497#[derive(Debug, Clone, Serialize, Deserialize)]
499#[serde(transparent)]
500pub struct DatanodeStatValue {
501 pub stats: Vec<Stat>,
502}
503
504impl DatanodeStatValue {
505 pub fn region_num(&self) -> Option<u64> {
507 self.stats.last().map(|x| x.region_num)
508 }
509
510 pub fn node_addr(&self) -> Option<String> {
512 self.stats.last().map(|x| x.addr.clone())
513 }
514}
515
516impl TryFrom<DatanodeStatValue> for Vec<u8> {
517 type Error = error::Error;
518
519 fn try_from(stats: DatanodeStatValue) -> Result<Self> {
520 Ok(serde_json::to_string(&stats)
521 .context(error::SerializeToJsonSnafu {
522 input: format!("{stats:?}"),
523 })?
524 .into_bytes())
525 }
526}
527
528impl FromStr for DatanodeStatValue {
529 type Err = error::Error;
530
531 fn from_str(value: &str) -> Result<Self> {
532 serde_json::from_str(value).context(error::DeserializeFromJsonSnafu { input: value })
533 }
534}
535
536impl TryFrom<Vec<u8>> for DatanodeStatValue {
537 type Error = error::Error;
538
539 fn try_from(value: Vec<u8>) -> Result<Self> {
540 String::from_utf8(value)
541 .context(error::FromUtf8Snafu {
542 name: "DatanodeStatValue",
543 })
544 .map(|x| x.parse())?
545 }
546}
547
548#[cfg(test)]
549mod tests {
550 use super::*;
551
552 #[test]
553 fn test_stat_key() {
554 let stat = Stat {
555 id: 101,
556 region_num: 10,
557 ..Default::default()
558 };
559
560 let stat_key = stat.stat_key();
561
562 assert_eq!(101, stat_key.node_id);
563 }
564
565 #[test]
566 fn test_stat_val_round_trip() {
567 let stat = Stat {
568 id: 101,
569 region_num: 100,
570 ..Default::default()
571 };
572
573 let stat_val = DatanodeStatValue { stats: vec![stat] };
574
575 let bytes: Vec<u8> = stat_val.try_into().unwrap();
576 let stat_val: DatanodeStatValue = bytes.try_into().unwrap();
577 let stats = stat_val.stats;
578
579 assert_eq!(1, stats.len());
580
581 let stat = stats.first().unwrap();
582 assert_eq!(101, stat.id);
583 assert_eq!(100, stat.region_num);
584 }
585
586 #[test]
587 fn test_stat_val_deserializes_without_optional_counters() {
588 let stat = Stat {
589 region_stats: vec![RegionStat {
590 id: RegionId::new(1024, 1),
591 rcus: 0,
592 wcus: 0,
593 approximate_bytes: 0,
594 engine: "mito".to_string(),
595 role: RegionRole::Leader,
596 num_rows: 0,
597 memtable_size: 0,
598 manifest_size: 0,
599 sst_size: 0,
600 sst_num: 0,
601 index_size: 0,
602 region_manifest: RegionManifestInfo::Mito {
603 manifest_version: 0,
604 flushed_entry_id: 0,
605 file_removed_cnt: 0,
606 },
607 written_bytes: 0,
608 ingested_parquet_bytes: 4096,
609 query_cpu_time: 10,
610 query_scanned_bytes: 20,
611 data_topic_latest_entry_id: 0,
612 metadata_topic_latest_entry_id: 0,
613 min_timestamp: None,
614 max_timestamp: None,
615 }],
616 ..Default::default()
617 };
618 let stat_val = DatanodeStatValue { stats: vec![stat] };
619 let mut value = serde_json::to_value(stat_val).unwrap();
620 let region_stat = value[0]["region_stats"][0].as_object_mut().unwrap();
621 region_stat.remove("ingested_parquet_bytes");
622 region_stat.remove("query_cpu_time");
623 region_stat.remove("query_scanned_bytes");
624
625 let stat_val: DatanodeStatValue = serde_json::from_value(value).unwrap();
626 let region_stat = &stat_val.stats[0].region_stats[0];
627
628 assert_eq!(region_stat.ingested_parquet_bytes, 0);
629 assert_eq!(region_stat.query_cpu_time, 0);
630 assert_eq!(region_stat.query_scanned_bytes, 0);
631 }
632
633 #[test]
634 fn test_get_addr_from_stat_val() {
635 let empty = DatanodeStatValue { stats: vec![] };
636 let addr = empty.node_addr();
637 assert!(addr.is_none());
638
639 let stat_val = DatanodeStatValue {
640 stats: vec![
641 Stat {
642 addr: "1".to_string(),
643 ..Default::default()
644 },
645 Stat {
646 addr: "2".to_string(),
647 ..Default::default()
648 },
649 Stat {
650 addr: "3".to_string(),
651 ..Default::default()
652 },
653 ],
654 };
655 let addr = stat_val.node_addr().unwrap();
656 assert_eq!("3", addr);
657 }
658
659 #[test]
660 fn test_get_region_num_from_stat_val() {
661 let empty = DatanodeStatValue { stats: vec![] };
662 let region_num = empty.region_num();
663 assert!(region_num.is_none());
664
665 let wrong = DatanodeStatValue {
666 stats: vec![Stat {
667 region_num: 0,
668 ..Default::default()
669 }],
670 };
671 let right = wrong.region_num();
672 assert_eq!(Some(0), right);
673
674 let stat_val = DatanodeStatValue {
675 stats: vec![
676 Stat {
677 region_num: 1,
678 ..Default::default()
679 },
680 Stat {
681 region_num: 0,
682 ..Default::default()
683 },
684 Stat {
685 region_num: 2,
686 ..Default::default()
687 },
688 ],
689 };
690 let region_num = stat_val.region_num().unwrap();
691 assert_eq!(2, region_num);
692 }
693
694 #[test]
695 fn test_region_stat_from_heartbeat_preserves_staging_leader_role() {
696 let request = HeartbeatRequest {
697 header: Some(RequestHeader::default()),
698 peer: Some(api::v1::meta::Peer {
699 id: 1,
700 addr: "127.0.0.1:3001".to_string(),
701 }),
702 region_stats: vec![api::v1::meta::RegionStat {
703 region_id: RegionId::new(1024, 1).as_u64(),
704 engine: "mito".to_string(),
705 role: api::v1::meta::RegionRole::StagingLeader.into(),
706 ..Default::default()
707 }],
708 ..Default::default()
709 };
710
711 let stat = Stat::try_from(&request).unwrap();
712
713 assert_eq!(stat.region_stats.len(), 1);
714 assert_eq!(stat.region_stats[0].role, RegionRole::StagingLeader);
715 }
716
717 #[test]
718 fn test_ingested_parquet_bytes_from_heartbeat() {
719 for bytes in [0, 4096, u64::MAX] {
720 let statistic = RegionStatistic {
721 ingested_parquet_bytes: bytes,
722 ..Default::default()
723 };
724 let mut region = api::v1::meta::RegionStat::default();
725 region.extensions.insert(
726 REGION_STATISTIC_KEY.to_string(),
727 serde_json::to_vec(&statistic).unwrap(),
728 );
729 let request = HeartbeatRequest {
730 header: Some(RequestHeader::default()),
731 peer: Some(api::v1::meta::Peer {
732 id: 1,
733 addr: "127.0.0.1:3001".to_string(),
734 }),
735 region_stats: vec![region.clone()],
736 ..Default::default()
737 };
738 let stat = Stat::try_from(&request).unwrap();
739 let stat = &stat.region_stats[0];
740 assert_eq!(stat.ingested_parquet_bytes, bytes);
741 let encoded = serde_json::to_vec(stat).unwrap();
742 let wire: serde_json::Value = serde_json::from_slice(&encoded).unwrap();
743 assert_eq!(wire["ingested_parquet_bytes"], bytes);
744 assert!(wire.get("flushed_parquet_bytes").is_none());
745 let mut old_wire = wire.clone();
746 old_wire
747 .as_object_mut()
748 .unwrap()
749 .remove("ingested_parquet_bytes");
750 old_wire["flushed_parquet_bytes"] = bytes.into();
751 assert_eq!(
752 &serde_json::from_value::<RegionStat>(old_wire).unwrap(),
753 stat
754 );
755 let decoded: RegionStat = serde_json::from_slice(&encoded).unwrap();
756 assert_eq!(&decoded, stat);
757
758 let mut legacy = serde_json::to_value(&statistic).unwrap();
759 legacy
760 .as_object_mut()
761 .unwrap()
762 .remove("ingested_parquet_bytes");
763 legacy["flushed_parquet_bytes"] = bytes.into();
764 region.extensions.insert(
765 REGION_STATISTIC_KEY.to_string(),
766 serde_json::to_vec(&legacy).unwrap(),
767 );
768 assert_eq!(RegionStat::from(®ion).ingested_parquet_bytes, bytes);
769 legacy
770 .as_object_mut()
771 .unwrap()
772 .remove("flushed_parquet_bytes");
773 region.extensions.insert(
774 REGION_STATISTIC_KEY.to_string(),
775 serde_json::to_vec(&legacy).unwrap(),
776 );
777 assert_eq!(RegionStat::from(®ion).ingested_parquet_bytes, 0);
778 }
779 assert_eq!(
780 RegionStat::from(&api::v1::meta::RegionStat::default()).ingested_parquet_bytes,
781 0
782 );
783 }
784
785 #[test]
786 fn test_env_vars_round_trip() {
787 let mut vars = HashMap::new();
788 vars.insert("AZ".to_string(), "us-east-1a".to_string());
789 vars.insert("REGION".to_string(), "us-east-1".to_string());
790 let env_vars = EnvVars::new(vars);
791
792 let mut extensions = HashMap::new();
793 env_vars.into_extensions(&mut extensions);
794
795 let extracted = EnvVars::from_extensions(&extensions).unwrap().unwrap();
796 assert_eq!(extracted.vars.get("AZ").unwrap(), "us-east-1a");
797 assert_eq!(extracted.vars.get("REGION").unwrap(), "us-east-1");
798 }
799
800 #[test]
801 fn test_env_vars_empty_not_written() {
802 let env_vars = EnvVars::default();
803 let mut extensions = HashMap::new();
804 env_vars.into_extensions(&mut extensions);
805 assert!(extensions.is_empty());
806 }
807
808 #[test]
809 fn test_env_vars_from_extensions_missing() {
810 let extensions = HashMap::new();
811 let result = EnvVars::from_extensions(&extensions).unwrap();
812 assert!(result.is_none());
813 }
814}