Skip to main content

nydus_storage/cache/state/
indexed_chunk_map.rs

1// Copyright 2021 Ant Group. All rights reserved.
2// Copyright (C) 2021 Alibaba Cloud. All rights reserved.
3//
4// SPDX-License-Identifier: Apache-2.0
5
6//! A chunk state tracking driver based on a bitmap file.
7//!
8//! This module provides a chunk state tracking driver based on a bitmap file. There's a state bit
9//! in the bitmap file for each chunk, and atomic operations are used to manipulate the bitmap.
10//! So it supports concurrent downloading.
11use std::io::Result;
12
13use crate::cache::state::persist_map::PersistMap;
14use crate::cache::state::{ChunkIndexGetter, ChunkMap, RangeMap};
15use crate::device::BlobChunkInfo;
16
17/// The name suffix of blob chunk_map file, named $blob_id.chunk_map.
18const FILE_SUFFIX: &str = "chunk_map";
19
20/// An implementation of [ChunkMap] to support chunk state tracking by using a bitmap file.
21///
22/// The `IndexedChunkMap` is an implementation of [ChunkMap] which uses a bitmap file and atomic
23/// bitmap operations to track readiness state. It creates or opens a file with the name
24/// `$blob_id.chunk_map` to record whether a chunk has been cached by the blob cache, and atomic
25/// bitmap operations are used to manipulate the state bit. The bitmap file will be persisted to
26/// disk.
27///
28/// This approach can be used to share chunk ready state between multiple nydusd instances.
29/// For example: the bitmap file layout is [0b00000000, 0b00000000], when blobcache calls
30/// set_ready(3), the layout should be changed to [0b00010000, 0b00000000].
31pub struct IndexedChunkMap {
32    map: PersistMap,
33}
34
35impl IndexedChunkMap {
36    /// Create a new instance of `IndexedChunkMap`.
37    pub fn new(blob_path: &str, chunk_count: u32, persist: bool) -> Result<Self> {
38        let filename = format!("{}.{}", blob_path, FILE_SUFFIX);
39
40        PersistMap::open(&filename, chunk_count, true, persist).map(|map| IndexedChunkMap { map })
41    }
42}
43
44impl ChunkMap for IndexedChunkMap {
45    fn is_ready(&self, chunk: &dyn BlobChunkInfo) -> Result<bool> {
46        if self.is_range_all_ready() {
47            Ok(true)
48        } else {
49            let index = self.map.validate_index(chunk.id())?;
50            Ok(self.map.is_chunk_ready(index).0)
51        }
52    }
53
54    fn set_ready_and_clear_pending(&self, chunk: &dyn BlobChunkInfo) -> Result<()> {
55        self.map.set_chunk_ready(chunk.id())
56    }
57
58    fn is_persist(&self) -> bool {
59        true
60    }
61
62    fn as_range_map(&self) -> Option<&dyn RangeMap<I = u32>> {
63        Some(self)
64    }
65}
66
67impl RangeMap for IndexedChunkMap {
68    type I = u32;
69
70    #[inline]
71    fn is_range_all_ready(&self) -> bool {
72        self.map.is_range_all_ready()
73    }
74
75    fn is_range_ready(&self, start_index: u32, count: u32) -> Result<bool> {
76        if !self.is_range_all_ready() {
77            for idx in 0..count {
78                let index = self
79                    .map
80                    .validate_index(start_index.checked_add(idx).ok_or_else(|| einval!())?)?;
81                if !self.map.is_chunk_ready(index).0 {
82                    return Ok(false);
83                }
84            }
85        }
86
87        Ok(true)
88    }
89
90    fn check_range_ready_and_mark_pending(
91        &self,
92        start_index: u32,
93        count: u32,
94    ) -> Result<Option<Vec<u32>>> {
95        if self.is_range_all_ready() {
96            return Ok(None);
97        }
98
99        let mut vec = Vec::with_capacity(count as usize);
100        let count = std::cmp::min(count, u32::MAX - start_index);
101        let end = start_index + count;
102
103        for index in start_index..end {
104            if !self.map.is_chunk_ready(index).0 {
105                vec.push(index);
106            }
107        }
108
109        if vec.is_empty() {
110            Ok(None)
111        } else {
112            Ok(Some(vec))
113        }
114    }
115
116    fn set_range_ready_and_clear_pending(&self, start_index: u32, count: u32) -> Result<()> {
117        let count = std::cmp::min(count, u32::MAX - start_index);
118        let end = start_index + count;
119
120        for index in start_index..end {
121            self.map.set_chunk_ready(index)?;
122        }
123
124        Ok(())
125    }
126}
127
128impl ChunkIndexGetter for IndexedChunkMap {
129    type Index = u32;
130
131    fn get_index(chunk: &dyn BlobChunkInfo) -> Self::Index {
132        chunk.id()
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use std::fs::OpenOptions;
139    use std::io::Write;
140    use std::sync::atomic::Ordering;
141    use vmm_sys_util::tempdir::TempDir;
142
143    use super::super::persist_map::*;
144    use super::*;
145    use crate::device::v5::BlobV5ChunkInfo;
146    use crate::test::MockChunkInfo;
147
148    #[test]
149    fn test_indexed_new_invalid_file_size() {
150        let dir = TempDir::new().unwrap();
151        let blob_path = dir.as_path().join("blob-1");
152        let blob_path = blob_path.as_os_str().to_str().unwrap().to_string();
153
154        assert!(IndexedChunkMap::new(&blob_path, 0, false).is_err());
155
156        let cache_path = format!("{}.{}", blob_path, FILE_SUFFIX);
157        let mut file = OpenOptions::new()
158            .read(true)
159            .write(true)
160            .create(true)
161            .truncate(false)
162            .open(&cache_path)
163            .map_err(|err| {
164                einval!(format!(
165                    "failed to open/create blob chunk_map file {:?}: {:?}",
166                    cache_path, err
167                ))
168            })
169            .unwrap();
170        file.write_all(&[0x0u8]).unwrap();
171
172        let chunk = MockChunkInfo::new();
173        assert_eq!(chunk.id(), 0);
174
175        assert!(IndexedChunkMap::new(&blob_path, 1, true).is_err());
176    }
177
178    #[test]
179    fn test_indexed_new_zero_file_size() {
180        let dir = TempDir::new().unwrap();
181        let blob_path = dir.as_path().join("blob-1");
182        let blob_path = blob_path.as_os_str().to_str().unwrap().to_string();
183
184        assert!(IndexedChunkMap::new(&blob_path, 0, true).is_err());
185
186        let cache_path = format!("{}.{}", blob_path, FILE_SUFFIX);
187        let _file = OpenOptions::new()
188            .read(true)
189            .write(true)
190            .create(true)
191            .truncate(false)
192            .open(&cache_path)
193            .map_err(|err| {
194                einval!(format!(
195                    "failed to open/create blob chunk_map file {:?}: {:?}",
196                    cache_path, err
197                ))
198            })
199            .unwrap();
200
201        let chunk = MockChunkInfo::new();
202        assert_eq!(chunk.id(), 0);
203
204        let map = IndexedChunkMap::new(&blob_path, 1, true).unwrap();
205        assert_eq!(map.map.not_ready_count.load(Ordering::Acquire), 1);
206        assert_eq!(map.map.count, 1);
207        assert_eq!(map.map.size(), 0x1001);
208        assert!(!map.is_range_all_ready());
209        assert!(!map.is_ready(chunk.as_base()).unwrap());
210        map.set_ready_and_clear_pending(chunk.as_base()).unwrap();
211        assert!(map.is_ready(chunk.as_base()).unwrap());
212    }
213
214    #[test]
215    fn test_indexed_new_header_not_ready() {
216        let dir = TempDir::new().unwrap();
217        let blob_path = dir.as_path().join("blob-1");
218        let blob_path = blob_path.as_os_str().to_str().unwrap().to_string();
219
220        assert!(IndexedChunkMap::new(&blob_path, 0, true).is_err());
221
222        let cache_path = format!("{}.{}", blob_path, FILE_SUFFIX);
223        let file = OpenOptions::new()
224            .read(true)
225            .write(true)
226            .create(true)
227            .truncate(false)
228            .open(&cache_path)
229            .map_err(|err| {
230                einval!(format!(
231                    "failed to open/create blob chunk_map file {:?}: {:?}",
232                    cache_path, err
233                ))
234            })
235            .unwrap();
236        file.set_len(0x1001).unwrap();
237
238        let chunk = MockChunkInfo::new();
239        assert_eq!(chunk.id(), 0);
240
241        let map = IndexedChunkMap::new(&blob_path, 1, true).unwrap();
242        assert_eq!(map.map.not_ready_count.load(Ordering::Acquire), 1);
243        assert_eq!(map.map.count, 1);
244        assert_eq!(map.map.size(), 0x1001);
245        assert!(!map.is_range_all_ready());
246        assert!(!map.is_ready(chunk.as_base()).unwrap());
247        map.set_ready_and_clear_pending(chunk.as_base()).unwrap();
248        assert!(map.is_ready(chunk.as_base()).unwrap());
249    }
250
251    #[test]
252    fn test_indexed_new_all_ready() {
253        let dir = TempDir::new().unwrap();
254        let blob_path = dir.as_path().join("blob-1");
255        let blob_path = blob_path.as_os_str().to_str().unwrap().to_string();
256
257        assert!(IndexedChunkMap::new(&blob_path, 0, true).is_err());
258
259        let cache_path = format!("{}.{}", blob_path, FILE_SUFFIX);
260        let mut file = OpenOptions::new()
261            .read(true)
262            .write(true)
263            .create(true)
264            .truncate(false)
265            .open(&cache_path)
266            .map_err(|err| {
267                einval!(format!(
268                    "failed to open/create blob chunk_map file {:?}: {:?}",
269                    cache_path, err
270                ))
271            })
272            .unwrap();
273        let header = Header {
274            magic: MAGIC1,
275            version: 1,
276            magic2: MAGIC2,
277            all_ready: MAGIC_ALL_READY,
278            reserved: [0x0u8; HEADER_RESERVED_SIZE],
279        };
280
281        // write file header and sync to disk.
282        file.write_all(header.as_slice()).unwrap();
283        file.write_all(&[0x0u8]).unwrap();
284
285        let chunk = MockChunkInfo::new();
286        assert_eq!(chunk.id(), 0);
287
288        let map = IndexedChunkMap::new(&blob_path, 1, true).unwrap();
289        assert!(map.is_range_all_ready());
290        assert_eq!(map.map.count, 1);
291        assert_eq!(map.map.size(), 0x1001);
292        assert!(map.is_ready(chunk.as_base()).unwrap());
293        map.set_ready_and_clear_pending(chunk.as_base()).unwrap();
294        assert!(map.is_ready(chunk.as_base()).unwrap());
295    }
296
297    #[test]
298    fn test_indexed_new_load_v0() {
299        let dir = TempDir::new().unwrap();
300        let blob_path = dir.as_path().join("blob-1");
301        let blob_path = blob_path.as_os_str().to_str().unwrap().to_string();
302
303        assert!(IndexedChunkMap::new(&blob_path, 0, true).is_err());
304
305        let cache_path = format!("{}.{}", blob_path, FILE_SUFFIX);
306        let mut file = OpenOptions::new()
307            .read(true)
308            .write(true)
309            .create(true)
310            .truncate(false)
311            .open(&cache_path)
312            .map_err(|err| {
313                einval!(format!(
314                    "failed to open/create blob chunk_map file {:?}: {:?}",
315                    cache_path, err
316                ))
317            })
318            .unwrap();
319        let header = Header {
320            magic: MAGIC1,
321            version: 0,
322            magic2: 0,
323            all_ready: 0,
324            reserved: [0x0u8; HEADER_RESERVED_SIZE],
325        };
326
327        // write file header and sync to disk.
328        file.write_all(header.as_slice()).unwrap();
329        file.write_all(&[0x0u8]).unwrap();
330
331        let chunk = MockChunkInfo::new();
332        assert_eq!(chunk.id(), 0);
333
334        let map = IndexedChunkMap::new(&blob_path, 1, true).unwrap();
335        assert_eq!(map.map.not_ready_count.load(Ordering::Acquire), 1);
336        assert_eq!(map.map.count, 1);
337        assert_eq!(map.map.size(), 0x1001);
338        assert!(!map.is_range_all_ready());
339        assert!(!map.is_ready(chunk.as_base()).unwrap());
340        map.set_ready_and_clear_pending(chunk.as_base()).unwrap();
341        assert!(map.is_ready(chunk.as_base()).unwrap());
342    }
343}