1
1
use std:: fmt:: Debug ;
2
2
use std:: fs:: { self , File } ;
3
3
use std:: hash:: Hasher ;
4
- use std:: io:: { self , BufReader , BufWriter , Write } ;
4
+ use std:: io:: { self , BufReader , Write } ;
5
5
use std:: path:: { Path , PathBuf } ;
6
6
use std:: sync:: atomic:: { AtomicU64 , Ordering } ;
7
7
use std:: sync:: Mutex ;
@@ -15,6 +15,7 @@ use rayon::iter::ParallelIterator;
15
15
use rayon:: iter:: { IntoParallelIterator , ParallelBridge } ;
16
16
use rustc_hash:: FxHashMap ;
17
17
use serde:: { Deserialize , Serialize } ;
18
+ use tempfile:: NamedTempFile ;
18
19
19
20
use ruff_cache:: { CacheKey , CacheKeyHasher } ;
20
21
use ruff_diagnostics:: { DiagnosticKind , Fix }
8000
span>;
@@ -165,15 +166,29 @@ impl Cache {
165
166
return Ok ( ( ) ) ;
166
167
}
167
168
168
- let file = File :: create ( & self . path )
169
- . with_context ( || format ! ( "Failed to create cache file '{}'" , self . path. display( ) ) ) ?;
170
- let writer = BufWriter :: new ( file) ;
171
- bincode:: serialize_into ( writer, & self . package ) . with_context ( || {
169
+ // Write the cache to a temporary file first and then rename it for an "atomic" write.
170
+ // Protects against data loss if the process is killed during the write and races between different ruff
171
+ // processes, resulting in a corrupted cache file. https://github.com/astral-sh/ruff/issues/8147#issuecomment-1943345964
172
+ let mut temp_file =
173
+ NamedTempFile :: new_in ( self . path . parent ( ) . expect ( "Write path must have a parent" ) )
174
+ . context ( "Failed to create temporary file" ) ?;
175
+
176
+ // Serialize to in-memory buffer because hyperfine benchmark showed that it's faster than
177
+ // using a `BufWriter` and our cache files are small enough that streaming isn't necessary.
178
+ let serialized =
179
+ bincode:: serialize ( & self . package ) . context ( "Failed to serialize cache data" ) ?;
180
+ temp_file
181
+ . write_all ( & serialized)
182
+ . context ( "Failed to write serialized cache to temporary file." ) ?;
183
+
184
+ temp_file. persist ( & self . path ) . with_context ( || {
172
185
format ! (
173
- "Failed to serialise cache to file '{}' " ,
186
+ "Failed to rename temporary cache file to {} " ,
174
187
self . path. display( )
175
188
)
176
- } )
189
+ } ) ?;
190
+
191
+ Ok ( ( ) )
177
192
}
178
193
179
194
/// Applies the pending changes without storing the cache to disk.
0 commit comments