1 // Copyright 2020 Google Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 // use this file except in compliance with the License. You may obtain a copy 5 // 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, WITHOUT 11 // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 // License for the specific language governing permissions and limitations 13 // under the License. 14 15 use {fuse, IdGenerator}; 16 use nodes::{ArcNode, Cache, Dir, File, Symlink}; 17 use std::collections::HashMap; 18 use std::fs; 19 use std::path::{Path, PathBuf}; 20 use std::sync::Mutex; 21 22 /// Node factory without any caching. 23 #[derive(Default)] 24 pub struct NoCache { 25 } 26 27 impl Cache for NoCache { get_or_create(&self, ids: &IdGenerator, underlying_path: &Path, attr: &fs::Metadata, writable: bool) -> ArcNode28 fn get_or_create(&self, ids: &IdGenerator, underlying_path: &Path, attr: &fs::Metadata, 29 writable: bool) -> ArcNode { 30 if attr.is_dir() { 31 Dir::new_mapped(ids.next(), underlying_path, attr, writable) 32 } else if attr.file_type().is_symlink() { 33 Symlink::new_mapped(ids.next(), underlying_path, attr, writable) 34 } else { 35 File::new_mapped(ids.next(), underlying_path, attr, writable) 36 } 37 } 38 delete(&self, _path: &Path, _file_type: fuse::FileType)39 fn delete(&self, _path: &Path, _file_type: fuse::FileType) { 40 // Nothing to do. 41 } 42 rename(&self, _old_path: &Path, _new_path: PathBuf, _file_type: fuse::FileType)43 fn rename(&self, _old_path: &Path, _new_path: PathBuf, _file_type: fuse::FileType) { 44 // Nothing to do. 45 } 46 } 47 48 /// Cache of sandboxfs nodes indexed by their underlying path. 49 /// 50 /// This cache is critical to offer good performance during reconfigurations: if the identity of an 51 /// underlying file changes across reconfigurations, the kernel will think it's a different file 52 /// (even if it may not be) and will therefore not be able to take advantage of any caches. You 53 /// would think that avoiding kernel cache invalidations during the reconfiguration itself (e.g. if 54 /// file `A` was mapped and is still mapped now, don't invalidate it) would be sufficient to avoid 55 /// this problem, but it's not: `A` could be mapped, then unmapped, and then remapped again in three 56 /// different reconfigurations, and we'd still not want to lose track of it. 57 /// 58 /// Nodes should be inserted in this cache at creation time and removed from it when explicitly 59 /// deleted by the user (because there is a chance they'll be recreated, and at that point we truly 60 /// want to reload the data from disk). 61 /// 62 /// TODO(jmmv): There currently is no cache expiration, which means that memory usage can grow 63 /// unboundedly. A preliminary attempt at expiring cache entries on a node's forget handler sounded 64 /// promising (because then cache expiration would be delegated to the kernel)... but, on Linux, the 65 /// kernel seems to be calling this very eagerly, rendering our cache useless. I did not track down 66 /// what exactly triggered the forget notifications though. 67 /// 68 /// TODO(jmmv): This cache has proven to be problematic in some cases and should probably be 69 /// removed. See https://jmmv.dev/2020/01/osxfuse-hardlinks-dladdr.html for details. 70 #[derive(Default)] 71 pub struct PathCache { 72 entries: Mutex<HashMap<PathBuf, ArcNode>>, 73 } 74 75 impl Cache for PathCache { get_or_create(&self, ids: &IdGenerator, underlying_path: &Path, attr: &fs::Metadata, writable: bool) -> ArcNode76 fn get_or_create(&self, ids: &IdGenerator, underlying_path: &Path, attr: &fs::Metadata, 77 writable: bool) -> ArcNode { 78 if attr.is_dir() { 79 // Directories cannot be cached because they contain entries that are created only 80 // in memory based on the mappings configuration. 81 // 82 // TODO(jmmv): Actually, they *could* be cached, but it's hard. Investigate doing so 83 // after quantifying how much it may benefit performance. 84 return Dir::new_mapped(ids.next(), underlying_path, attr, writable); 85 } 86 87 let mut entries = self.entries.lock().unwrap(); 88 89 if let Some(node) = entries.get(underlying_path) { 90 if node.writable() == writable { 91 // We have a match from the cache! Return it immediately. 92 // 93 // It is tempting to ensure that the type of the cached node matches the type we 94 // want to return based on the metadata we have now in `attr`... but doing so does 95 // not really prevent problems: the type of the underlying file can change at any 96 // point in time. We could check this here and the type could change immediately 97 // afterwards behind our backs, so don't bother. 98 return node.clone(); 99 } 100 101 // We had a match... but node writability has changed; recreate the node. 102 // 103 // You may wonder why we care about this and not the file type as described above: the 104 // reason is that the writability property is a setting of the mappings, not a property 105 // of the underlying files, and thus it's a setting that we fully control and must keep 106 // correct across reconfigurations or across different mappings of the same files. 107 info!("Missed node caching opportunity because writability has changed for {:?}", 108 underlying_path) 109 } 110 111 let node: ArcNode = if attr.is_dir() { 112 panic!("Directory entries cannot be cached and are handled above"); 113 } else if attr.file_type().is_symlink() { 114 Symlink::new_mapped(ids.next(), underlying_path, attr, writable) 115 } else { 116 File::new_mapped(ids.next(), underlying_path, attr, writable) 117 }; 118 entries.insert(underlying_path.to_path_buf(), node.clone()); 119 node 120 } 121 delete(&self, path: &Path, file_type: fuse::FileType)122 fn delete(&self, path: &Path, file_type: fuse::FileType) { 123 let mut entries = self.entries.lock().unwrap(); 124 if file_type == fuse::FileType::Directory { 125 debug_assert!(!entries.contains_key(path), "Directories are not currently cached"); 126 } else { 127 entries.remove(path).expect("Tried to delete unknown path from the cache"); 128 } 129 } 130 rename(&self, old_path: &Path, new_path: PathBuf, file_type: fuse::FileType)131 fn rename(&self, old_path: &Path, new_path: PathBuf, file_type: fuse::FileType) { 132 let mut entries = self.entries.lock().unwrap(); 133 if file_type == fuse::FileType::Directory { 134 debug_assert!(!entries.contains_key(old_path), "Directories are not currently cached"); 135 } else { 136 let node = entries.remove(old_path).expect("Tried to rename unknown path in the cache"); 137 entries.insert(new_path, node); 138 } 139 } 140 } 141 142 #[cfg(test)] 143 mod tests { 144 use super::*; 145 use tempfile::tempdir; 146 use testutils; 147 148 #[test] path_cache_behavior()149 fn path_cache_behavior() { 150 let root = tempdir().unwrap(); 151 152 let dir1 = root.path().join("dir1"); 153 fs::create_dir(&dir1).unwrap(); 154 let dir1attr = fs::symlink_metadata(&dir1).unwrap(); 155 156 let file1 = root.path().join("file1"); 157 drop(fs::File::create(&file1).unwrap()); 158 let file1attr = fs::symlink_metadata(&file1).unwrap(); 159 160 let file2 = root.path().join("file2"); 161 drop(fs::File::create(&file2).unwrap()); 162 let file2attr = fs::symlink_metadata(&file2).unwrap(); 163 164 let ids = IdGenerator::new(1); 165 let cache = PathCache::default(); 166 167 // Directories are not cached no matter what. 168 assert_eq!(1, cache.get_or_create(&ids, &dir1, &dir1attr, false).inode()); 169 assert_eq!(2, cache.get_or_create(&ids, &dir1, &dir1attr, false).inode()); 170 assert_eq!(3, cache.get_or_create(&ids, &dir1, &dir1attr, true).inode()); 171 172 // Different files get different nodes. 173 assert_eq!(4, cache.get_or_create(&ids, &file1, &file1attr, false).inode()); 174 assert_eq!(5, cache.get_or_create(&ids, &file2, &file2attr, true).inode()); 175 176 // Files we queried before but with different writability get different nodes. 177 assert_eq!(6, cache.get_or_create(&ids, &file1, &file1attr, true).inode()); 178 assert_eq!(7, cache.get_or_create(&ids, &file2, &file2attr, false).inode()); 179 180 // We get cache hits when everything matches previous queries. 181 assert_eq!(6, cache.get_or_create(&ids, &file1, &file1attr, true).inode()); 182 assert_eq!(7, cache.get_or_create(&ids, &file2, &file2attr, false).inode()); 183 184 // We don't get cache hits for nodes whose writability changed. 185 assert_eq!(8, cache.get_or_create(&ids, &file1, &file1attr, false).inode()); 186 assert_eq!(9, cache.get_or_create(&ids, &file2, &file2attr, true).inode()); 187 } 188 189 #[test] path_cache_nodes_support_all_file_types()190 fn path_cache_nodes_support_all_file_types() { 191 let ids = IdGenerator::new(1); 192 let cache = PathCache::default(); 193 194 for (_fuse_type, path) in testutils::AllFileTypes::new().entries { 195 let fs_attr = fs::symlink_metadata(&path).unwrap(); 196 // The following panics if it's impossible to represent the given file type, which is 197 // what we are testing. 198 cache.get_or_create(&ids, &path, &fs_attr, false); 199 } 200 } 201 } 202