Skip to content

Commit 2ef787b

Browse files
authored
[devtools] Add a query parameter to restart endpoint to invalidate the persistent cache (#79425)
Creates and passes through an option to turbopack to invalidate the cache. The dev overlay can then (not implemented) call this endpoint. Invalidation is performed by writing a marker file to disk, and then actually deleting the cache upon the next startup because: - Getting the database to drop all file handles is tricky. - You can't delete open files on Windows. - Writing an invalidation file is atomic, recursively deleting a directory is not. ### Testing Set up a small app with a large dependency (three.js). Ran the build, restarted the dev server, saw that the cached run was fast. Ran ``` curl -v --request POST --header "Content-Type: application/json" --data '{}' http://localhost:3000/__nextjs_restart_dev\?invalidatePersistentCache\= ``` And saw that the server restarted and the next page load was slow. ### Remaining Work (will come in subsequent PRs) In rough order of priority: - A separate endpoint to poll to see when the server is back up. - A webpack+rspack implementation. It looks like I can do a hack to forcibly change the webpack cache version, which should trigger similar behavior upon the next restart. - Passing a boolean to the dev overlay telling it if persistent caching is enabled or not. - The devtools UI implementation. - Telemetry. - A CLI subcommand that leverages this, so that you can do it from the terminal.
1 parent b7b546e commit 2ef787b

File tree

17 files changed

+225
-27
lines changed

17 files changed

+225
-27
lines changed

crates/napi/src/next_api/project.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,6 +568,18 @@ pub async fn project_update(
568568
Ok(())
569569
}
570570

571+
/// Invalidates the persistent cache so that it will be deleted next time that a turbopack project
572+
/// is created with persistent caching enabled.
573+
#[napi]
574+
pub async fn project_invalidate_persistent_cache(
575+
#[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External<ProjectInstance>,
576+
) -> napi::Result<()> {
577+
tokio::task::spawn_blocking(move || project.turbo_tasks.invalidate_persistent_cache())
578+
.await
579+
.context("panicked while invalidating persistent cache")??;
580+
Ok(())
581+
}
582+
571583
/// Runs exit handlers for the project registered using the [`ExitHandler`] API.
572584
///
573585
/// This is called by `project_shutdown`, so if you're calling that API, you shouldn't call this

crates/napi/src/next_api/utils.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,16 @@ impl NextTurboTasks {
149149
}
150150
}
151151
}
152+
153+
pub fn invalidate_persistent_cache(&self) -> Result<()> {
154+
match self {
155+
NextTurboTasks::Memory(_) => {}
156+
NextTurboTasks::PersistentCaching(turbo_tasks) => {
157+
turbo_tasks.backend().invalidate_storage()?
158+
}
159+
}
160+
Ok(())
161+
}
152162
}
153163

154164
pub fn create_turbo_tasks(

packages/next/src/build/swc/generated-native.d.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,13 @@ export declare function projectUpdate(
204204
project: { __napiType: 'Project' },
205205
options: NapiPartialProjectOptions
206206
): Promise<void>
207+
/**
208+
* Invalidates the persistent cache so that it will be deleted next time that a turbopack project
209+
* is created with persistent caching enabled.
210+
*/
211+
export declare function projectInvalidatePersistentCache(project: {
212+
__napiType: 'Project'
213+
}): Promise<void>
207214
/**
208215
* Runs exit handlers for the project registered using the [`ExitHandler`] API.
209216
*

packages/next/src/build/swc/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -723,6 +723,10 @@ function bindingToApi(
723723
)
724724
}
725725

726+
invalidatePersistentCache(): Promise<void> {
727+
return binding.projectInvalidatePersistentCache(this._nativeProject)
728+
}
729+
726730
shutdown(): Promise<void> {
727731
return binding.projectShutdown(this._nativeProject)
728732
}

packages/next/src/build/swc/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,8 @@ export interface Project {
226226
TurbopackResult<CompilationEvent>
227227
>
228228

229+
invalidatePersistentCache(): Promise<void>
230+
229231
shutdown(): Promise<void>
230232

231233
onExit(): Promise<void>

packages/next/src/client/components/react-dev-overlay/server/restart-dev-server-middleware.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,39 @@ import type { ServerResponse, IncomingMessage } from 'http'
22
import type { Telemetry } from '../../../../telemetry/storage'
33
import { RESTART_EXIT_CODE } from '../../../../server/lib/utils'
44
import { middlewareResponse } from './middleware-response'
5+
import type { Project } from '../../../../build/swc/types'
56

67
const EVENT_DEV_OVERLAY_RESTART_SERVER = 'DEV_OVERLAY_RESTART_SERVER'
78

8-
export function getRestartDevServerMiddleware(telemetry: Telemetry) {
9+
interface RestartDevServerMiddlewareConfig {
10+
telemetry: Telemetry
11+
turbopackProject?: Project
12+
}
13+
14+
export function getRestartDevServerMiddleware({
15+
telemetry,
16+
turbopackProject,
17+
}: RestartDevServerMiddlewareConfig) {
918
return async function (
1019
req: IncomingMessage,
1120
res: ServerResponse,
1221
next: () => void
1322
): Promise<void> {
14-
const { pathname } = new URL(`http://n${req.url}`)
23+
const { pathname, searchParams } = new URL(`http://n${req.url}`)
1524
if (pathname !== '/__nextjs_restart_dev' || req.method !== 'POST') {
1625
return next()
1726
}
1827

28+
const invalidatePersistentCache = searchParams.has(
29+
'invalidatePersistentCache'
30+
)
31+
if (invalidatePersistentCache) {
32+
await turbopackProject?.invalidatePersistentCache()
33+
}
34+
1935
telemetry.record({
2036
eventName: EVENT_DEV_OVERLAY_RESTART_SERVER,
21-
payload: {},
37+
payload: { invalidatePersistentCache },
2238
})
2339

2440
// TODO: Use flushDetached

packages/next/src/server/dev/hot-reloader-turbopack.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -650,7 +650,10 @@ export async function createHotReloaderTurbopack(
650650
getNextErrorFeedbackMiddleware(opts.telemetry),
651651
getDevOverlayFontMiddleware(),
652652
getDisableDevIndicatorMiddleware(),
653-
getRestartDevServerMiddleware(opts.telemetry),
653+
getRestartDevServerMiddleware({
654+
telemetry: opts.telemetry,
655+
turbopackProject: project,
656+
}),
654657
]
655658

656659
const versionInfoPromise = getVersionInfo()

packages/next/src/server/dev/hot-reloader-webpack.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1570,7 +1570,9 @@ export default class HotReloaderWebpack implements NextJsHotReloaderInterface {
15701570
getNextErrorFeedbackMiddleware(this.telemetry),
15711571
getDevOverlayFontMiddleware(),
15721572
getDisableDevIndicatorMiddleware(),
1573-
getRestartDevServerMiddleware(this.telemetry),
1573+
getRestartDevServerMiddleware({
1574+
telemetry: this.telemetry,
1575+
}),
15741576
]
15751577
}
15761578

turbopack/crates/turbo-persistence/src/db.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ impl TurboPersistence {
190190
Ok(db)
191191
}
192192

193-
/// Performas the initial check on the database directory.
193+
/// Performs the initial check on the database directory.
194194
fn open_directory(&mut self) -> Result<()> {
195195
match fs::read_dir(&self.path) {
196196
Ok(entries) => {

turbopack/crates/turbo-tasks-backend/src/backend/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,10 @@ impl<B: BackingStorage> TurboTasksBackend<B> {
207207
backing_storage,
208208
)))
209209
}
210+
211+
pub fn invalidate_storage(&self) -> Result<()> {
212+
self.0.backing_storage.invalidate()
213+
}
210214
}
211215

212216
impl<B: BackingStorage> TurboTasksBackendInner<B> {

turbopack/crates/turbo-tasks-backend/src/backing_storage.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,16 @@ pub trait BackingStorage: 'static + Send + Sync {
6363
category: TaskDataCategory,
6464
) -> Result<Vec<CachedDataItem>>;
6565

66+
/// Called when the database should be invalidated upon re-initialization.
67+
///
68+
/// This typically means that we'll restart the process or `turbo-tasks` soon with a fresh
69+
/// database. If this happens, there's no point in writing anything else to disk, or flushing
70+
/// during [`KeyValueDatabase::shutdown`].
71+
///
72+
/// This can be implemented by calling [`crate::database::db_invalidation::invalidate_db`] with
73+
/// the database's non-versioned base path.
74+
fn invalidate(&self) -> Result<()>;
75+
6676
fn shutdown(&self) -> Result<()> {
6777
Ok(())
6878
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
use std::{
2+
fs::{self, read_dir},
3+
io,
4+
path::Path,
5+
};
6+
7+
use anyhow::Context;
8+
9+
const INVALIDATION_MARKER: &str = "__turbo_tasks_invalidated_db";
10+
11+
/// Atomically write an invalidation marker.
12+
///
13+
/// Because attempting to delete currently open database files could cause issues, actual deletion
14+
/// of files is deferred until the next start-up (in [`check_db_invalidation_and_cleanup`]).
15+
///
16+
/// In the case that no database is currently open (e.g. via a separate CLI subcommand), you should
17+
/// call [`cleanup_db`] *after* this to eagerly remove the database files.
18+
///
19+
/// This should be run with the base (non-versioned) path, as that likely aligns closest with user
20+
/// expectations (e.g. if they're clearing the cache for disk space reasons).
21+
pub fn invalidate_db(base_path: &Path) -> anyhow::Result<()> {
22+
fs::write(base_path.join(INVALIDATION_MARKER), [0u8; 0])?;
23+
Ok(())
24+
}
25+
26+
/// Called during startup. See if the db is in a partially-completed invalidation state. Find and
27+
/// delete any invalidated database files.
28+
///
29+
/// This should be run with the base (non-versioned) path.
30+
pub fn check_db_invalidation_and_cleanup(base_path: &Path) -> anyhow::Result<()> {
31+
if fs::exists(base_path.join(INVALIDATION_MARKER))? {
32+
// if this cleanup fails, we might try to open an invalid database later, so it's best to
33+
// just propagate the error here.
34+
cleanup_db(base_path)?;
35+
};
36+
Ok(())
37+
}
38+
39+
/// Helper for [`check_db_invalidation_and_cleanup`]. You can call this to explicitly clean up a
40+
/// database after running [`invalidate_db`] when turbo-tasks is not running.
41+
///
42+
/// You should not run this if the database has not yet been invalidated, as this operation is not
43+
/// atomic and could result in a partially-deleted and corrupted database.
44+
pub fn cleanup_db(base_path: &Path) -> anyhow::Result<()> {
45+
cleanup_db_inner(base_path).with_context(|| {
46+
format!(
47+
"Unable to remove invalid database. If this issue persists you can work around by \
48+
deleting {base_path:?}."
49+
)
50+
})
51+
}
52+
53+
fn cleanup_db_inner(base_path: &Path) -> io::Result<()> {
54+
let Ok(contents) = read_dir(base_path) else {
55+
return Ok(());
56+
};
57+
58+
// delete everything except the invalidation marker
59+
for entry in contents {
60+
let entry = entry?;
61+
if entry.file_name() != INVALIDATION_MARKER {
62+
fs::remove_dir_all(entry.path())?;
63+
}
64+
}
65+
66+
// delete the invalidation marker last, once we're sure everything is cleaned up
67+
fs::remove_file(base_path.join(INVALIDATION_MARKER))?;
68+
Ok(())
69+
}

turbopack/crates/turbo-tasks-backend/src/database/key_value_database.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,20 @@ pub trait KeyValueDatabase {
5252
&self,
5353
) -> Result<WriteBatch<'_, Self::SerialWriteBatch<'_>, Self::ConcurrentWriteBatch<'_>>>;
5454

55+
/// Called when the database has been invalidated via
56+
/// [`crate::backing_storage::BackingStorage::invalidate`]
57+
///
58+
/// This typically means that we'll restart the process or `turbo-tasks` soon with a fresh
59+
/// database. If this happens, there's no point in writing anything else to disk, or flushing
60+
/// during [`KeyValueDatabase::shutdown`].
61+
///
62+
/// This is a best-effort optimization hint, and the database may choose to ignore this and
63+
/// continue file writes. This happens after the database is invalidated, so it is valid for
64+
/// this to leave the database in a half-updated and corrupted state.
65+
fn prevent_writes(&self) {
66+
// this is an optional performance hint to the database
67+
}
68+
5569
fn shutdown(&self) -> Result<()> {
5670
Ok(())
5771
}

turbopack/crates/turbo-tasks-backend/src/database/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#[cfg(feature = "lmdb")]
22
mod by_key_space;
3+
pub mod db_invalidation;
34
pub mod db_versioning;
45
#[cfg(feature = "lmdb")]
56
pub mod fresh_db_optimization;

turbopack/crates/turbo-tasks-backend/src/database/turbo.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ pub struct TurboKeyValueDatabase {
2323
}
2424

2525
impl TurboKeyValueDatabase {
26-
pub fn new(path: PathBuf) -> Result<Self> {
27-
let db = Arc::new(TurboPersistence::open(path.to_path_buf())?);
26+
pub fn new(versioned_path: PathBuf) -> Result<Self> {
27+
let db = Arc::new(TurboPersistence::open(versioned_path)?);
2828
let mut this = Self {
2929
db: db.clone(),
3030
compact_join_handle: Mutex::new(None),
@@ -99,6 +99,8 @@ impl KeyValueDatabase for TurboKeyValueDatabase {
9999
}))
100100
}
101101

102+
fn prevent_writes(&self) {}
103+
102104
fn shutdown(&self) -> Result<()> {
103105
// Wait for the compaction to finish
104106
if let Some(join_handle) = self.compact_join_handle.lock().take() {

turbopack/crates/turbo-tasks-backend/src/kv_backing_storage.rs

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::{borrow::Borrow, cmp::max, sync::Arc};
1+
use std::{borrow::Borrow, cmp::max, path::PathBuf, sync::Arc};
22

33
use anyhow::{Context, Result, anyhow};
44
use rayon::iter::{IndexedParallelIterator, IntoParallelIterator, ParallelIterator};
@@ -8,10 +8,13 @@ use tracing::Span;
88
use turbo_tasks::{SessionId, TaskId, backend::CachedTaskType, turbo_tasks_scope};
99

1010
use crate::{
11+
GitVersionInfo,
1112
backend::{AnyOperation, TaskDataCategory},
1213
backing_storage::BackingStorage,
1314
data::CachedDataItem,
1415
database::{
16+
db_invalidation::{check_db_invalidation_and_cleanup, invalidate_db},
17+
db_versioning::handle_db_versioning,
1518
key_value_database::{KeySpace, KeyValueDatabase},
1619
write_batch::{
1720
BaseWriteBatch, ConcurrentWriteBatch, SerialWriteBatch, WriteBatch, WriteBatchRef,
@@ -82,11 +85,31 @@ fn as_u32(bytes: impl Borrow<[u8]>) -> Result<u32> {
8285

8386
pub struct KeyValueDatabaseBackingStorage<T: KeyValueDatabase> {
8487
database: T,
88+
/// Used when calling [`BackingStorage::invalidate`]. Can be `None` in the memory-only/no-op
89+
/// storage case.
90+
base_path: Option<PathBuf>,
8591
}
8692

8793
impl<T: KeyValueDatabase> KeyValueDatabaseBackingStorage<T> {
88-
pub fn new(database: T) -> Self {
89-
Self { database }
94+
pub fn new_in_memory(database: T) -> Self {
95+
Self {
96+
database,
97+
base_path: None,
98+
}
99+
}
100+
101+
pub fn open_versioned_on_disk(
102+
base_path: PathBuf,
103+
version_info: &GitVersionInfo,
104+
is_ci: bool,
105+
database: impl FnOnce(PathBuf) -> Result<T>,
106+
) -> Result<Self> {
107+
check_db_invalidation_and_cleanup(&base_path)?;
108+
let versioned_path = handle_db_versioning(&base_path, version_info, is_ci)?;
109+
Ok(Self {
110+
database: (database)(versioned_path)?,
111+
base_path: Some(base_path),
112+
})
90113
}
91114

92115
fn with_tx<R>(
@@ -442,6 +465,18 @@ impl<T: KeyValueDatabase + Send + Sync + 'static> BackingStorage
442465
.with_context(|| format!("Looking up data for {task_id} from database failed"))
443466
}
444467

468+
fn invalidate(&self) -> Result<()> {
469+
// `base_path` can be `None` for a `NoopKvDb`
470+
if let Some(base_path) = &self.base_path {
471+
// Invalidate first, as it's a very fast atomic operation. `prevent_writes` is allowed
472+
// to be slower (e.g. wait for a lock) and is allowed to corrupt the database with
473+
// partial writes.
474+
invalidate_db(base_path)?;
475+
self.database.prevent_writes()
476+
}
477+
Ok(())
478+
}
479+
445480
fn shutdown(&self) -> Result<()> {
446481
self.database.shutdown()
447482
}

0 commit comments

Comments
 (0)