1
1
use std:: {
2
- fs:: { self , read_dir} ,
3
- io:: { self , ErrorKind } ,
2
+ borrow:: Cow ,
3
+ fs:: { self , File , read_dir} ,
4
+ io:: { self , BufReader , BufWriter , ErrorKind , Write } ,
4
5
path:: Path ,
5
6
} ;
6
7
7
8
use anyhow:: Context ;
9
+ use serde:: { Deserialize , Serialize } ;
8
10
9
11
const INVALIDATION_MARKER : & str = "__turbo_tasks_invalidated_db" ;
10
12
11
- /// Atomically write an invalidation marker.
13
+ const EXPLANATION : & str = "The cache database has been invalidated. The existence of this file \
14
+ will cause the cache directory to be cleaned up the next time \
15
+ Turbopack starts up.";
16
+ const EASTER_EGG : & str =
17
+ "you just wrote me, and this is crazy, but if you see me, delete everything maybe?" ;
18
+
19
+ /// The data written to the file at [`INVALIDATION_MARKER`].
20
+ #[ derive( Serialize , Deserialize ) ]
21
+ struct InvalidationFile < ' a > {
22
+ #[ serde( skip_deserializing) ]
23
+ _explanation : Option < & ' static str > ,
24
+ #[ serde( skip_deserializing) ]
25
+ _easter_egg : Option < & ' static str > ,
26
+ /// See [`StartupCacheState::Invalidated::reason_code`].
27
+ reason_code : Cow < ' a , str > ,
28
+ }
29
+
30
+ /// Information about if there's was a pre-existing cache or if the cache was detected as
31
+ /// invalidated during startup.
32
+ ///
33
+ /// If the cache was invalidated, the application may choose to show a warning to the user or log it
34
+ /// to telemetry.
35
+ ///
36
+ /// This value is returned by [`crate::turbo_backing_storage`] and
37
+ /// [`crate::default_backing_storage`].
38
+ pub enum StartupCacheState {
39
+ NoCache ,
40
+ Cached ,
41
+ Invalidated {
42
+ /// A short code passed to [`invalidate_db`]. This value is application-specific.
43
+ ///
44
+ /// If the value is `None` or doesn't match an expected value, the application should just
45
+ /// treat this reason as unknown. The invalidation file may have been corrupted or
46
+ /// modified by an external tool.
47
+ ///
48
+ /// See [`invalidation_reasons`] for some common reason codes.
49
+ reason_code : Option < String > ,
50
+ } ,
51
+ }
52
+
53
+ /// Common invalidation reason codes. The application or libraries it uses may choose to use these
54
+ /// reasons, or it may define it's own reasons.
55
+ pub mod invalidation_reasons {
56
+ /// This invalidation reason is used by [`crate::turbo_backing_storage`] when the database was
57
+ /// invalidated by a panic.
58
+ pub const PANIC : & str = concat ! ( module_path!( ) , "::PANIC" ) ;
59
+ /// This invalidation reason is used by [`crate::turbo_backing_storage`] when the database was
60
+ /// invalidated by a panic.
61
+ pub const USER_REQUEST : & str = concat ! ( module_path!( ) , "::USER_REQUEST" ) ;
62
+ }
63
+
64
+ /// Atomically create an invalidation marker.
65
+ ///
66
+ /// Makes a best-effort attempt to write `reason_code` to the file, but ignores any failure with
67
+ /// writing to the file.
12
68
///
13
69
/// Because attempting to delete currently open database files could cause issues, actual deletion
14
70
/// of files is deferred until the next start-up (in [`check_db_invalidation_and_cleanup`]).
@@ -18,9 +74,27 @@ const INVALIDATION_MARKER: &str = "__turbo_tasks_invalidated_db";
18
74
///
19
75
/// This should be run with the base (non-versioned) path, as that likely aligns closest with user
20
76
/// expectations (e.g. if they're clearing the cache for disk space reasons).
21
- pub fn invalidate_db ( base_path : & Path ) -> anyhow:: Result < ( ) > {
22
- match fs:: write ( base_path. join ( INVALIDATION_MARKER ) , [ 0u8 ; 0 ] ) {
23
- Ok ( _) => Ok ( ( ) ) ,
77
+ ///
78
+ /// In most cases, you should prefer a higher-level API like
79
+ /// [`crate::backing_storage::BackingStorage::invalidate`] to this one.
80
+ pub ( crate ) fn invalidate_db ( base_path : & Path , reason_code : & str ) -> anyhow:: Result < ( ) > {
81
+ match File :: create_new ( base_path. join ( INVALIDATION_MARKER ) ) {
82
+ Ok ( file) => {
83
+ let mut writer = BufWriter :: new ( file) ;
84
+ let _ = serde_json:: to_writer_pretty (
85
+ & mut writer,
86
+ & InvalidationFile {
87
+ _explanation : Some ( EXPLANATION ) ,
88
+ _easter_egg : Some ( EASTER_EGG ) ,
89
+ reason_code : Cow :: Borrowed ( reason_code) ,
90
+ } ,
91
+ ) ;
92
+ let _ = writer. flush ( ) ;
93
+ Ok ( ( ) )
94
+ }
95
+ // the database was already invalidated, avoid overwriting that reason or risking concurrent
96
+ // writes to the same file.
97
+ Err ( err) if err. kind ( ) == ErrorKind :: AlreadyExists => Ok ( ( ) ) ,
24
98
// just ignore if the cache directory doesn't exist at all
25
99
Err ( err) if err. kind ( ) == ErrorKind :: NotFound => Ok ( ( ) ) ,
26
100
Err ( err) => Err ( err) . context ( "Failed to invalidate database" ) ,
@@ -31,21 +105,45 @@ pub fn invalidate_db(base_path: &Path) -> anyhow::Result<()> {
31
105
/// delete any invalidated database files.
32
106
///
33
107
/// This should be run with the base (non-versioned) path.
34
- pub fn check_db_invalidation_and_cleanup ( base_path : & Path ) -> anyhow:: Result < ( ) > {
35
- if fs:: exists ( base_path. join ( INVALIDATION_MARKER ) ) ? {
36
- // if this cleanup fails, we might try to open an invalid database later, so it's best to
37
- // just propagate the error here.
38
- cleanup_db ( base_path) ?;
39
- } ;
40
- Ok ( ( ) )
108
+ ///
109
+ /// In most cases, you should prefer a higher-level API like
110
+ /// [`crate::KeyValueDatabaseBackingStorage::open_versioned_on_disk`] to this one.
111
+ pub ( crate ) fn check_db_invalidation_and_cleanup (
112
+ base_path : & Path ,
113
+ ) -> anyhow:: Result < StartupCacheState > {
114
+ match File :: open ( base_path. join ( INVALIDATION_MARKER ) ) {
115
+ Ok ( file) => {
116
+ // Best-effort: Try to read the reason_code from the file, if the file format is
117
+ // corrupted (or anything else) just use `None`.
118
+ let reason_code = serde_json:: from_reader :: < _ , InvalidationFile > ( BufReader :: new ( file) )
119
+ . ok ( )
120
+ . map ( |contents| contents. reason_code . into_owned ( ) ) ;
121
+ // `file` is dropped at this point: That's important for Windows where we can't delete
122
+ // open files.
123
+
124
+ // if this cleanup fails, we might try to open an invalid database later, so it's best
125
+ // to just propagate the error here.
126
+ cleanup_db ( base_path) ?;
127
+ Ok ( StartupCacheState :: Invalidated { reason_code } )
128
+ }
129
+ Err ( err) if err. kind ( ) == ErrorKind :: NotFound => {
130
+ if fs:: exists ( base_path) ? {
131
+ Ok ( StartupCacheState :: Cached )
132
+ } else {
133
+ Ok ( StartupCacheState :: NoCache )
134
+ }
135
+ }
136
+ Err ( err) => Err ( err)
137
+ . with_context ( || format ! ( "Failed to check for {INVALIDATION_MARKER} in {base_path:?}" ) ) ,
138
+ }
41
139
}
42
140
43
141
/// Helper for [`check_db_invalidation_and_cleanup`]. You can call this to explicitly clean up a
44
142
/// database after running [`invalidate_db`] when turbo-tasks is not running.
45
143
///
46
144
/// You should not run this if the database has not yet been invalidated, as this operation is not
47
145
/// atomic and could result in a partially-deleted and corrupted database.
48
- pub fn cleanup_db ( base_path : & Path ) -> anyhow:: Result < ( ) > {
146
+ pub ( crate ) fn cleanup_db ( base_path : & Path ) -> anyhow:: Result < ( ) > {
49
147
cleanup_db_inner ( base_path) . with_context ( || {
50
148
format ! (
51
149
"Unable to remove invalid database. If this issue persists you can work around by \
0 commit comments