Skip to main content

common_meta/
datanode.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::collections::{HashMap, HashSet};
16use std::str::FromStr;
17
18use api::v1::meta::{DatanodeWorkloads, HeartbeatRequest, RequestHeader};
19use common_time::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";
34/// The private table name for persisted region statistics.
35pub 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/// The key of the datanode stat in the storage.
47///
48/// The format is `__meta_datanode_stat-0-{node_id}`.
49#[derive(Debug, Clone, Default, Serialize, Deserialize)]
50pub struct Stat {
51    pub timestamp_millis: i64,
52    // The datanode Id.
53    pub id: u64,
54    // The datanode address.
55    pub addr: String,
56    /// The read capacity units during this period
57    pub rcus: i64,
58    /// The write capacity units during this period
59    pub wcus: i64,
60    /// How many regions on this node
61    pub region_num: u64,
62    /// The region stats of the datanode.
63    pub region_stats: Vec<RegionStat>,
64    /// The topic stats of the datanode.
65    pub topic_stats: Vec<TopicStat>,
66    // The node epoch is used to check whether the node has restarted or redeployed.
67    pub node_epoch: u64,
68    /// The datanode workloads.
69    pub datanode_workloads: DatanodeWorkloads,
70    /// The GC statistics of the datanode.
71    pub gc_stat: Option<GcStat>,
72}
73
74/// The statistics of a region.
75#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
76pub struct RegionStat {
77    /// The region_id.
78    pub id: RegionId,
79    /// The read capacity units during this period
80    pub rcus: i64,
81    /// The write capacity units during this period
82    pub wcus: i64,
83    /// Approximate disk bytes of this region, including sst, index, manifest and wal
84    pub approximate_bytes: u64,
85    /// The engine name.
86    pub engine: String,
87    /// The region role.
88    pub role: RegionRole,
89    /// The number of rows
90    pub num_rows: u64,
91    /// The size of the memtable in bytes.
92    pub memtable_size: u64,
93    /// The size of the manifest in bytes.
94    pub manifest_size: u64,
95    /// The size of the SST data files in bytes.
96    pub sst_size: u64,
97    /// The num of the SST data files.
98    pub sst_num: u64,
99    /// The size of the SST index files in bytes.
100    pub index_size: u64,
101    /// The manifest infoof the region.
102    pub region_manifest: RegionManifestInfo,
103    /// The total bytes written of the region since region opened.
104    pub written_bytes: u64,
105    /// The total query CPU time of the region since region opened.
106    ///
107    /// Unit: nanoseconds.
108    #[serde(default)]
109    pub query_cpu_time: u64,
110    /// The total scanned bytes of the region since region opened.
111    #[serde(default)]
112    pub query_scanned_bytes: u64,
113    /// The latest entry id of topic used by data.
114    /// **Only used by remote WAL prune.**
115    pub data_topic_latest_entry_id: u64,
116    /// The latest entry id of topic used by metadata.
117    /// **Only used by remote WAL prune.**
118    /// In mito engine, this is the same as `data_topic_latest_entry_id`.
119    pub metadata_topic_latest_entry_id: u64,
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct TopicStat {
124    /// The topic name.
125    pub topic: String,
126    /// The latest entry id of the topic.
127    pub latest_entry_id: u64,
128    /// The total size in bytes of records appended to the topic.
129    pub record_size: u64,
130    /// The total number of records appended to the topic.
131    pub record_num: u64,
132}
133
134/// Trait for reporting statistics about topics.
135pub trait TopicStatsReporter: Send + Sync {
136    /// Returns a list of topic statistics that can be reported.
137    fn reportable_topics(&mut self) -> Vec<TopicStat>;
138}
139
140#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
141pub enum RegionManifestInfo {
142    Mito {
143        manifest_version: u64,
144        flushed_entry_id: u64,
145        /// Number of files removed in the manifest's `removed_files` field.
146        file_removed_cnt: u64,
147    },
148    Metric {
149        data_manifest_version: u64,
150        data_flushed_entry_id: u64,
151        metadata_manifest_version: u64,
152        metadata_flushed_entry_id: u64,
153    },
154}
155
156impl Stat {
157    #[inline]
158    pub fn is_empty(&self) -> bool {
159        self.region_stats.is_empty()
160    }
161
162    pub fn stat_key(&self) -> DatanodeStatKey {
163        DatanodeStatKey { node_id: self.id }
164    }
165
166    /// Returns a tuple array containing [RegionId] and [RegionRole].
167    pub fn regions(&self) -> Vec<(RegionId, RegionRole)> {
168        self.region_stats.iter().map(|s| (s.id, s.role)).collect()
169    }
170
171    /// Returns all table ids in the region stats.
172    pub fn table_ids(&self) -> HashSet<TableId> {
173        self.region_stats.iter().map(|s| s.id.table_id()).collect()
174    }
175
176    /// Retains the active region stats and updates the rcus, wcus, and region_num.
177    pub fn retain_active_region_stats(&mut self, inactive_region_ids: &HashSet<RegionId>) {
178        if inactive_region_ids.is_empty() {
179            return;
180        }
181
182        self.region_stats
183            .retain(|r| !inactive_region_ids.contains(&r.id));
184        self.rcus = self.region_stats.iter().map(|s| s.rcus).sum();
185        self.wcus = self.region_stats.iter().map(|s| s.wcus).sum();
186        self.region_num = self.region_stats.len() as u64;
187    }
188
189    pub fn memory_size(&self) -> usize {
190        // timestamp_millis, rcus, wcus
191        std::mem::size_of::<i64>() * 3 +
192        // id, region_num, node_epoch
193        std::mem::size_of::<u64>() * 3 +
194        // addr
195        std::mem::size_of::<String>() + self.addr.capacity() +
196        // region_stats
197        self.region_stats.iter().map(|s| s.memory_size()).sum::<usize>()
198    }
199}
200
201impl RegionStat {
202    pub fn memory_size(&self) -> usize {
203        // role
204        std::mem::size_of::<RegionRole>() +
205        // id
206        std::mem::size_of::<RegionId>() +
207        // rcus, wcus, approximate_bytes, num_rows
208        std::mem::size_of::<i64>() * 4 +
209        // memtable_size, manifest_size, sst_size, sst_num, index_size
210        std::mem::size_of::<u64>() * 5 +
211        // engine
212        std::mem::size_of::<String>() + self.engine.capacity() +
213        // region_manifest
214        self.region_manifest.memory_size()
215    }
216}
217
218impl RegionManifestInfo {
219    pub fn memory_size(&self) -> usize {
220        match self {
221            RegionManifestInfo::Mito { .. } => std::mem::size_of::<u64>() * 2,
222            RegionManifestInfo::Metric { .. } => std::mem::size_of::<u64>() * 4,
223        }
224    }
225}
226
227impl TryFrom<&HeartbeatRequest> for Stat {
228    type Error = Option<RequestHeader>;
229
230    fn try_from(value: &HeartbeatRequest) -> std::result::Result<Self, Self::Error> {
231        let HeartbeatRequest {
232            header,
233            peer,
234            region_stats,
235            node_epoch,
236            node_workloads,
237            topic_stats,
238            extensions,
239            ..
240        } = value;
241
242        match (header, peer) {
243            (Some(header), Some(peer)) => {
244                let region_stats = region_stats
245                    .iter()
246                    .map(RegionStat::from)
247                    .collect::<Vec<_>>();
248                let topic_stats = topic_stats.iter().map(TopicStat::from).collect::<Vec<_>>();
249
250                let datanode_workloads = get_datanode_workloads(node_workloads.as_ref());
251
252                let gc_stat = GcStat::from_extensions(extensions).map_err(|err| {
253                    common_telemetry::error!(
254                        "Failed to deserialize GcStat from extensions: {}",
255                        err
256                    );
257                    header.clone()
258                })?;
259                Ok(Self {
260                    timestamp_millis: time_util::current_time_millis(),
261                    // datanode id
262                    id: peer.id,
263                    // datanode address
264                    addr: peer.addr.clone(),
265                    rcus: region_stats.iter().map(|s| s.rcus).sum(),
266                    wcus: region_stats.iter().map(|s| s.wcus).sum(),
267                    region_num: region_stats.len() as u64,
268                    region_stats,
269                    topic_stats,
270                    node_epoch: *node_epoch,
271                    datanode_workloads,
272                    gc_stat,
273                })
274            }
275            (header, _) => Err(header.clone()),
276        }
277    }
278}
279
280impl From<store_api::region_engine::RegionManifestInfo> for RegionManifestInfo {
281    fn from(value: store_api::region_engine::RegionManifestInfo) -> Self {
282        match value {
283            store_api::region_engine::RegionManifestInfo::Mito {
284                manifest_version,
285                flushed_entry_id,
286                file_removed_cnt,
287            } => RegionManifestInfo::Mito {
288                manifest_version,
289                flushed_entry_id,
290                file_removed_cnt,
291            },
292            store_api::region_engine::RegionManifestInfo::Metric {
293                data_manifest_version,
294                data_flushed_entry_id,
295                metadata_manifest_version,
296                metadata_flushed_entry_id,
297            } => RegionManifestInfo::Metric {
298                data_manifest_version,
299                data_flushed_entry_id,
300                metadata_manifest_version,
301                metadata_flushed_entry_id,
302            },
303        }
304    }
305}
306
307impl From<&api::v1::meta::RegionStat> for RegionStat {
308    fn from(value: &api::v1::meta::RegionStat) -> Self {
309        let region_stat = value
310            .extensions
311            .get(REGION_STATISTIC_KEY)
312            .and_then(|value| RegionStatistic::deserialize_from_slice(value))
313            .unwrap_or_default();
314
315        Self {
316            id: RegionId::from_u64(value.region_id),
317            rcus: value.rcus,
318            wcus: value.wcus,
319            approximate_bytes: value.approximate_bytes as u64,
320            engine: value.engine.clone(),
321            role: RegionRole::from(value.role()),
322            num_rows: region_stat.num_rows,
323            memtable_size: region_stat.memtable_size,
324            manifest_size: region_stat.manifest_size,
325            sst_size: region_stat.sst_size,
326            sst_num: region_stat.sst_num,
327            index_size: region_stat.index_size,
328            region_manifest: region_stat.manifest.into(),
329            written_bytes: region_stat.written_bytes,
330            query_cpu_time: region_stat.query_cpu_time,
331            query_scanned_bytes: region_stat.query_scanned_bytes,
332            data_topic_latest_entry_id: region_stat.data_topic_latest_entry_id,
333            metadata_topic_latest_entry_id: region_stat.metadata_topic_latest_entry_id,
334        }
335    }
336}
337
338impl From<&api::v1::meta::TopicStat> for TopicStat {
339    fn from(value: &api::v1::meta::TopicStat) -> Self {
340        Self {
341            topic: value.topic_name.clone(),
342            latest_entry_id: value.latest_entry_id,
343            record_size: value.record_size,
344            record_num: value.record_num,
345        }
346    }
347}
348
349#[derive(Debug, Clone, Serialize, Deserialize, Default)]
350pub struct GcStat {
351    /// Number of GC tasks currently running on the datanode.
352    pub running_gc_tasks: u32,
353    /// The maximum number of concurrent GC tasks the datanode can handle.
354    pub gc_concurrency: u32,
355}
356
357impl GcStat {
358    pub const GC_STAT_KEY: &str = "__gc_stat";
359
360    pub fn new(running_gc_tasks: u32, gc_concurrency: u32) -> Self {
361        Self {
362            running_gc_tasks,
363            gc_concurrency,
364        }
365    }
366
367    pub fn into_extensions(&self, extensions: &mut std::collections::HashMap<String, Vec<u8>>) {
368        let bytes = serde_json::to_vec(self).unwrap_or_default();
369        extensions.insert(Self::GC_STAT_KEY.to_string(), bytes);
370    }
371
372    pub fn from_extensions(
373        extensions: &std::collections::HashMap<String, Vec<u8>>,
374    ) -> Result<Option<Self>> {
375        extensions
376            .get(Self::GC_STAT_KEY)
377            .map(|bytes| {
378                serde_json::from_slice(bytes).with_context(|_| DeserializeFromJsonSnafu {
379                    input: String::from_utf8_lossy(bytes).to_string(),
380                })
381            })
382            .transpose()
383    }
384}
385
386/// Environment variables reported by a node in heartbeat messages.
387#[derive(Debug, Clone, Serialize, Deserialize, Default)]
388pub struct EnvVars {
389    pub vars: HashMap<String, String>,
390}
391
392impl EnvVars {
393    pub const ENV_VARS_KEY: &str = "__env_vars";
394
395    pub fn new(vars: HashMap<String, String>) -> Self {
396        Self { vars }
397    }
398
399    /// Read the configured env var keys from the environment and build an EnvVars.
400    pub fn from_config(keys: &[String]) -> Self {
401        let vars = keys
402            .iter()
403            .filter_map(|key| std::env::var(key).ok().map(|value| (key.clone(), value)))
404            .collect();
405        Self { vars }
406    }
407
408    pub fn into_extensions(&self, extensions: &mut HashMap<String, Vec<u8>>) {
409        if self.vars.is_empty() {
410            return;
411        }
412        let bytes = serde_json::to_vec(self).unwrap_or_default();
413        extensions.insert(Self::ENV_VARS_KEY.to_string(), bytes);
414    }
415
416    pub fn from_extensions(extensions: &HashMap<String, Vec<u8>>) -> Result<Option<Self>> {
417        extensions
418            .get(Self::ENV_VARS_KEY)
419            .map(|bytes| {
420                serde_json::from_slice(bytes).with_context(|_| DeserializeFromJsonSnafu {
421                    input: String::from_utf8_lossy(bytes).to_string(),
422                })
423            })
424            .transpose()
425    }
426}
427
428/// The key of the datanode stat in the memory store.
429///
430/// The format is `__meta_datanode_stat-0-{node_id}`.
431#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
432pub struct DatanodeStatKey {
433    pub node_id: u64,
434}
435
436impl DatanodeStatKey {
437    /// The key prefix.
438    pub fn prefix_key() -> Vec<u8> {
439        // todo(hl): remove cluster id in prefix
440        format!("{DATANODE_STAT_PREFIX}-0-").into_bytes()
441    }
442}
443
444impl From<DatanodeStatKey> for Vec<u8> {
445    fn from(value: DatanodeStatKey) -> Self {
446        // todo(hl): remove cluster id in prefix
447        format!("{}-0-{}", DATANODE_STAT_PREFIX, value.node_id).into_bytes()
448    }
449}
450
451impl FromStr for DatanodeStatKey {
452    type Err = error::Error;
453
454    fn from_str(key: &str) -> Result<Self> {
455        let caps = DATANODE_STAT_KEY_PATTERN
456            .captures(key)
457            .context(error::InvalidStatKeySnafu { key })?;
458
459        ensure!(caps.len() == 3, error::InvalidStatKeySnafu { key });
460        let node_id = caps[2].to_string();
461        let node_id: u64 = node_id.parse().context(error::ParseNumSnafu {
462            err_msg: format!("invalid node_id: {node_id}"),
463        })?;
464
465        Ok(Self { node_id })
466    }
467}
468
469impl TryFrom<Vec<u8>> for DatanodeStatKey {
470    type Error = error::Error;
471
472    fn try_from(bytes: Vec<u8>) -> Result<Self> {
473        String::from_utf8(bytes)
474            .context(error::FromUtf8Snafu {
475                name: "DatanodeStatKey",
476            })
477            .map(|x| x.parse())?
478    }
479}
480
481/// The value of the datanode stat in the memory store.
482#[derive(Debug, Clone, Serialize, Deserialize)]
483#[serde(transparent)]
484pub struct DatanodeStatValue {
485    pub stats: Vec<Stat>,
486}
487
488impl DatanodeStatValue {
489    /// Get the latest number of regions.
490    pub fn region_num(&self) -> Option<u64> {
491        self.stats.last().map(|x| x.region_num)
492    }
493
494    /// Get the latest node addr.
495    pub fn node_addr(&self) -> Option<String> {
496        self.stats.last().map(|x| x.addr.clone())
497    }
498}
499
500impl TryFrom<DatanodeStatValue> for Vec<u8> {
501    type Error = error::Error;
502
503    fn try_from(stats: DatanodeStatValue) -> Result<Self> {
504        Ok(serde_json::to_string(&stats)
505            .context(error::SerializeToJsonSnafu {
506                input: format!("{stats:?}"),
507            })?
508            .into_bytes())
509    }
510}
511
512impl FromStr for DatanodeStatValue {
513    type Err = error::Error;
514
515    fn from_str(value: &str) -> Result<Self> {
516        serde_json::from_str(value).context(error::DeserializeFromJsonSnafu { input: value })
517    }
518}
519
520impl TryFrom<Vec<u8>> for DatanodeStatValue {
521    type Error = error::Error;
522
523    fn try_from(value: Vec<u8>) -> Result<Self> {
524        String::from_utf8(value)
525            .context(error::FromUtf8Snafu {
526                name: "DatanodeStatValue",
527            })
528            .map(|x| x.parse())?
529    }
530}
531
532#[cfg(test)]
533mod tests {
534    use super::*;
535
536    #[test]
537    fn test_stat_key() {
538        let stat = Stat {
539            id: 101,
540            region_num: 10,
541            ..Default::default()
542        };
543
544        let stat_key = stat.stat_key();
545
546        assert_eq!(101, stat_key.node_id);
547    }
548
549    #[test]
550    fn test_stat_val_round_trip() {
551        let stat = Stat {
552            id: 101,
553            region_num: 100,
554            ..Default::default()
555        };
556
557        let stat_val = DatanodeStatValue { stats: vec![stat] };
558
559        let bytes: Vec<u8> = stat_val.try_into().unwrap();
560        let stat_val: DatanodeStatValue = bytes.try_into().unwrap();
561        let stats = stat_val.stats;
562
563        assert_eq!(1, stats.len());
564
565        let stat = stats.first().unwrap();
566        assert_eq!(101, stat.id);
567        assert_eq!(100, stat.region_num);
568    }
569
570    #[test]
571    fn test_stat_val_deserializes_without_query_stats() {
572        let stat = Stat {
573            region_stats: vec![RegionStat {
574                id: RegionId::new(1024, 1),
575                rcus: 0,
576                wcus: 0,
577                approximate_bytes: 0,
578                engine: "mito".to_string(),
579                role: RegionRole::Leader,
580                num_rows: 0,
581                memtable_size: 0,
582                manifest_size: 0,
583                sst_size: 0,
584                sst_num: 0,
585                index_size: 0,
586                region_manifest: RegionManifestInfo::Mito {
587                    manifest_version: 0,
588                    flushed_entry_id: 0,
589                    file_removed_cnt: 0,
590                },
591                written_bytes: 0,
592                query_cpu_time: 10,
593                query_scanned_bytes: 20,
594                data_topic_latest_entry_id: 0,
595                metadata_topic_latest_entry_id: 0,
596            }],
597            ..Default::default()
598        };
599        let stat_val = DatanodeStatValue { stats: vec![stat] };
600        let mut value = serde_json::to_value(stat_val).unwrap();
601        let region_stat = value[0]["region_stats"][0].as_object_mut().unwrap();
602        region_stat.remove("query_cpu_time");
603        region_stat.remove("query_scanned_bytes");
604
605        let stat_val: DatanodeStatValue = serde_json::from_value(value).unwrap();
606        let region_stat = &stat_val.stats[0].region_stats[0];
607
608        assert_eq!(region_stat.query_cpu_time, 0);
609        assert_eq!(region_stat.query_scanned_bytes, 0);
610    }
611
612    #[test]
613    fn test_get_addr_from_stat_val() {
614        let empty = DatanodeStatValue { stats: vec![] };
615        let addr = empty.node_addr();
616        assert!(addr.is_none());
617
618        let stat_val = DatanodeStatValue {
619            stats: vec![
620                Stat {
621                    addr: "1".to_string(),
622                    ..Default::default()
623                },
624                Stat {
625                    addr: "2".to_string(),
626                    ..Default::default()
627                },
628                Stat {
629                    addr: "3".to_string(),
630                    ..Default::default()
631                },
632            ],
633        };
634        let addr = stat_val.node_addr().unwrap();
635        assert_eq!("3", addr);
636    }
637
638    #[test]
639    fn test_get_region_num_from_stat_val() {
640        let empty = DatanodeStatValue { stats: vec![] };
641        let region_num = empty.region_num();
642        assert!(region_num.is_none());
643
644        let wrong = DatanodeStatValue {
645            stats: vec![Stat {
646                region_num: 0,
647                ..Default::default()
648            }],
649        };
650        let right = wrong.region_num();
651        assert_eq!(Some(0), right);
652
653        let stat_val = DatanodeStatValue {
654            stats: vec![
655                Stat {
656                    region_num: 1,
657                    ..Default::default()
658                },
659                Stat {
660                    region_num: 0,
661                    ..Default::default()
662                },
663                Stat {
664                    region_num: 2,
665                    ..Default::default()
666                },
667            ],
668        };
669        let region_num = stat_val.region_num().unwrap();
670        assert_eq!(2, region_num);
671    }
672
673    #[test]
674    fn test_region_stat_from_heartbeat_preserves_staging_leader_role() {
675        let request = HeartbeatRequest {
676            header: Some(RequestHeader::default()),
677            peer: Some(api::v1::meta::Peer {
678                id: 1,
679                addr: "127.0.0.1:3001".to_string(),
680            }),
681            region_stats: vec![api::v1::meta::RegionStat {
682                region_id: RegionId::new(1024, 1).as_u64(),
683                engine: "mito".to_string(),
684                role: api::v1::meta::RegionRole::StagingLeader.into(),
685                ..Default::default()
686            }],
687            ..Default::default()
688        };
689
690        let stat = Stat::try_from(&request).unwrap();
691
692        assert_eq!(stat.region_stats.len(), 1);
693        assert_eq!(stat.region_stats[0].role, RegionRole::StagingLeader);
694    }
695
696    #[test]
697    fn test_env_vars_round_trip() {
698        let mut vars = HashMap::new();
699        vars.insert("AZ".to_string(), "us-east-1a".to_string());
700        vars.insert("REGION".to_string(), "us-east-1".to_string());
701        let env_vars = EnvVars::new(vars);
702
703        let mut extensions = HashMap::new();
704        env_vars.into_extensions(&mut extensions);
705
706        let extracted = EnvVars::from_extensions(&extensions).unwrap().unwrap();
707        assert_eq!(extracted.vars.get("AZ").unwrap(), "us-east-1a");
708        assert_eq!(extracted.vars.get("REGION").unwrap(), "us-east-1");
709    }
710
711    #[test]
712    fn test_env_vars_empty_not_written() {
713        let env_vars = EnvVars::default();
714        let mut extensions = HashMap::new();
715        env_vars.into_extensions(&mut extensions);
716        assert!(extensions.is_empty());
717    }
718
719    #[test]
720    fn test_env_vars_from_extensions_missing() {
721        let extensions = HashMap::new();
722        let result = EnvVars::from_extensions(&extensions).unwrap();
723        assert!(result.is_none());
724    }
725}