1use std::{
2 borrow::Borrow,
3 env,
4 path::PathBuf,
5 sync::{Arc, LazyLock, Mutex, PoisonError, Weak},
6};
7
8use anyhow::{Context, Result};
9use smallvec::SmallVec;
10use turbo_bincode::{new_turbo_bincode_decoder, turbo_bincode_decode, turbo_bincode_encode};
11use turbo_persistence::CommitStats;
12use turbo_tasks::{
13 DynTaskInputs, RawVc, TaskId,
14 macro_helpers::NativeFunction,
15 panic_hooks::{PanicHookGuard, register_panic_hook},
16 parallel,
17};
18
19use crate::{
20 GitVersionInfo,
21 backend::{AnyOperation, SpecificTaskDataCategory, TtlCounter, storage_schema::TaskStorage},
22 backing_storage::{SnapshotItem, SnapshotMeta, compute_task_type_hash_from_components},
23 database::{
24 db_invalidation::{StartupCacheState, check_db_invalidation_and_cleanup, invalidate_db},
25 db_versioning::handle_db_versioning,
26 key_value_database::KeySpace,
27 turbo::{TurboKeyValueDatabase, TurboWriteBatch},
28 write_batch::WriteBuffer,
29 },
30 db_invalidation::invalidation_reasons,
31};
32
33#[derive(Clone, Copy)]
35#[repr(u8)]
36enum InfraKey {
37 Operations = 0,
38 NextFreeTaskId = 1,
39 GcRoots = 2,
40}
41
42impl InfraKey {
43 fn key(self) -> ByteKey {
44 ByteKey::new(self as u8)
45 }
46}
47
48struct ByteKey([u8; 1]);
49
50impl ByteKey {
51 fn new(value: u8) -> Self {
52 Self([value])
53 }
54}
55
56impl AsRef<[u8]> for ByteKey {
57 fn as_ref(&self) -> &[u8] {
58 &self.0
59 }
60}
61
62struct IntKey([u8; 4]);
63
64impl IntKey {
65 fn new(value: u32) -> Self {
66 Self(value.to_le_bytes())
67 }
68}
69
70impl AsRef<[u8]> for IntKey {
71 fn as_ref(&self) -> &[u8] {
72 &self.0
73 }
74}
75
76fn as_u32(bytes: impl Borrow<[u8]>) -> Result<u32> {
77 let n = u32::from_le_bytes(bytes.borrow().try_into()?);
78 Ok(n)
79}
80
81fn should_invalidate_on_panic() -> bool {
90 fn env_is_falsy(key: &str) -> bool {
91 env::var_os(key)
92 .is_none_or(|value| ["".as_ref(), "0".as_ref(), "false".as_ref()].contains(&&*value))
93 }
94 static SHOULD_INVALIDATE: LazyLock<bool> = LazyLock::new(|| {
95 env_is_falsy("TURBO_ENGINE_SKIP_INVALIDATE_ON_PANIC") && env_is_falsy("__NEXT_TEST_MODE")
96 });
97 *SHOULD_INVALIDATE
98}
99
100struct TurboBackingStorageInner {
101 database: TurboKeyValueDatabase,
102 base_path: Option<PathBuf>,
105 invalidated: Mutex<bool>,
107 _panic_hook_guard: Option<PanicHookGuard>,
110}
111
112pub struct TurboBackingStorage {
120 inner: Arc<TurboBackingStorageInner>,
122}
123
124impl TurboBackingStorage {
125 pub(crate) fn new_in_memory(database: TurboKeyValueDatabase) -> Self {
126 Self {
127 inner: Arc::new(TurboBackingStorageInner {
128 database,
129 base_path: None,
130 invalidated: Mutex::new(false),
131 _panic_hook_guard: None,
132 }),
133 }
134 }
135
136 pub(crate) fn open_versioned_on_disk(
148 base_path: PathBuf,
149 version_info: &GitVersionInfo,
150 is_ci: bool,
151 database: impl FnOnce(PathBuf) -> Result<TurboKeyValueDatabase>,
152 ) -> Result<(Self, StartupCacheState)> {
153 let startup_cache_state = check_db_invalidation_and_cleanup(&base_path)
154 .context("Failed to check database invalidation and cleanup")?;
155 let versioned_path = handle_db_versioning(&base_path, version_info, is_ci)
156 .context("Failed to handle database versioning")?;
157 let database = (database)(versioned_path).context("Failed to open database")?;
158 let backing_storage = Self {
159 inner: Arc::new_cyclic(move |weak_inner: &Weak<TurboBackingStorageInner>| {
160 let panic_hook_guard = if should_invalidate_on_panic() {
161 let weak_inner = weak_inner.clone();
162 Some(register_panic_hook(Box::new(move |_| {
163 let Some(inner) = weak_inner.upgrade() else {
164 return;
165 };
166 let _ = inner.invalidate(invalidation_reasons::PANIC);
171 })))
172 } else {
173 None
174 };
175 TurboBackingStorageInner {
176 database,
177 base_path: Some(base_path),
178 invalidated: Mutex::new(false),
179 _panic_hook_guard: panic_hook_guard,
180 }
181 }),
182 };
183 Ok((backing_storage, startup_cache_state))
184 }
185}
186
187impl TurboBackingStorageInner {
188 fn invalidate(&self, reason_code: &str) -> Result<()> {
189 if let Some(base_path) = &self.base_path {
191 let mut invalidated_guard = self
194 .invalidated
195 .lock()
196 .unwrap_or_else(PoisonError::into_inner);
197 if *invalidated_guard {
198 return Ok(());
199 }
200 invalidate_db(base_path, reason_code)?;
204 self.database.prevent_writes();
205 *invalidated_guard = true;
207 }
208 Ok(())
209 }
210
211 fn get_infra_u32(&self, key: InfraKey) -> Result<Option<u32>> {
213 self.database
214 .get(KeySpace::Infra, key.key().as_ref())?
215 .map(as_u32)
216 .transpose()
217 }
218}
219
220impl TurboBackingStorage {
221 pub(crate) fn invalidate(&self, reason_code: &str) -> Result<()> {
229 self.inner.invalidate(reason_code)
230 }
231
232 pub(crate) fn next_free_task_id(&self) -> Result<TaskId> {
233 Ok(self
234 .inner
235 .get_infra_u32(InfraKey::NextFreeTaskId)
236 .context("Unable to read next free task id from database")?
237 .map_or(Ok(TaskId::MIN), TaskId::try_from)?)
238 }
239
240 pub(crate) fn uncompleted_operations(&self) -> Result<Vec<AnyOperation>> {
241 fn get(database: &TurboKeyValueDatabase) -> Result<Vec<AnyOperation>> {
242 let Some(operations) =
243 database.get(KeySpace::Infra, InfraKey::Operations.key().as_ref())?
244 else {
245 return Ok(Vec::new());
246 };
247 let operations = turbo_bincode_decode(operations.borrow())?;
248 Ok(operations)
249 }
250 get(&self.inner.database).context("Unable to read uncompleted operations from database")
251 }
252
253 pub(crate) fn roots(&self) -> Result<Vec<(TaskId, TtlCounter)>> {
255 fn get(database: &TurboKeyValueDatabase) -> Result<Vec<(TaskId, TtlCounter)>> {
256 let Some(roots) = database.get(KeySpace::Infra, InfraKey::GcRoots.key().as_ref())?
257 else {
258 return Ok(Vec::new());
259 };
260 let roots = turbo_bincode_decode(roots.borrow())?;
261 Ok(roots)
262 }
263 get(&self.inner.database).context("Unable to read GC roots from database")
264 }
265
266 pub(crate) fn save_snapshot<I>(
267 &self,
268 operations: Vec<Arc<AnyOperation>>,
269 roots: Option<Vec<(TaskId, TtlCounter)>>,
270 snapshots: Vec<I>,
271 ) -> Result<SnapshotMeta>
272 where
273 I: IntoIterator<Item = SnapshotItem> + Send + Sync,
274 {
275 let _span = tracing::info_span!("save snapshot", operations = operations.len()).entered();
276 let batch = self.inner.database.write_batch()?;
277
278 {
279 let span = tracing::trace_span!("update task data");
280 let mut snapshot_meta =
281 parallel::map_collect_owned::<_, _, Result<Vec<_>>>(snapshots, |shard: I| {
282 let _span = span.clone().entered();
283 let mut max_new_task_id = 0;
284 let mut data_items = 0;
285 let mut meta_items = 0;
286 let mut task_cache_items = 0;
287 for item in shard {
288 match item {
289 SnapshotItem::Put {
290 task_id,
291 meta,
292 data,
293 task_type_hash,
294 } => {
295 let key = IntKey::new(*task_id);
296 let key = key.as_ref();
297 if let Some(meta) = meta {
298 batch.put(
299 KeySpace::TaskMeta,
300 WriteBuffer::Borrowed(key),
301 WriteBuffer::SmallVec(meta),
302 )?;
303 meta_items += 1;
304 }
305 if let Some(data) = data {
306 batch.put(
307 KeySpace::TaskData,
308 WriteBuffer::Borrowed(key),
309 WriteBuffer::SmallVec(data),
310 )?;
311 data_items += 1;
312 }
313 if let Some(task_type_hash) = task_type_hash {
315 batch.put(
316 KeySpace::TaskCache,
317 WriteBuffer::Borrowed(&task_type_hash),
318 WriteBuffer::Borrowed(key),
319 )?;
320 task_cache_items += 1;
321 max_new_task_id = max_new_task_id.max(*task_id);
322 }
323 }
324 SnapshotItem::Delete {
325 task_id,
326 task_type_hash,
327 } => {
328 let key = IntKey::new(*task_id);
329 let key = key.as_ref();
330 batch.delete(KeySpace::TaskMeta, WriteBuffer::Borrowed(key))?;
331 batch.delete(KeySpace::TaskData, WriteBuffer::Borrowed(key))?;
332 batch.delete_value(
334 KeySpace::TaskCache,
335 WriteBuffer::Borrowed(&task_type_hash[..]),
336 WriteBuffer::Borrowed(key),
337 )?;
338 }
339 }
340 }
341 Ok(SnapshotMeta {
342 data_items,
343 meta_items,
344 task_cache_items,
345 bytes_written: 0,
348 bytes_deleted: 0,
349 max_next_task_id: max_new_task_id,
350 })
351 })?
352 .into_iter()
353 .reduce(|t1, t2| t1.merge(t2))
354 .unwrap_or_default();
355
356 let span = tracing::trace_span!("flush task data");
357 parallel::try_for_each(
358 &[KeySpace::TaskMeta, KeySpace::TaskData, KeySpace::TaskCache],
359 |&key_space| {
360 let _span = span.clone().entered();
361 unsafe { batch.flush(key_space) }
364 },
365 )?;
366
367 let mut next_task_id = get_next_free_task_id(&batch)?;
368 next_task_id = next_task_id.max(snapshot_meta.max_next_task_id + 1);
369
370 save_infra(&batch, next_task_id, operations, roots)?;
371 {
372 let _span = tracing::trace_span!("commit").entered();
373 let stats = batch.commit().context("Unable to commit snapshot")?;
376 snapshot_meta.bytes_written = stats.bytes_written;
377 snapshot_meta.bytes_deleted = stats.bytes_deleted;
378 }
379 Ok(snapshot_meta)
380 }
381 }
382
383 pub(crate) fn lookup_task_candidates(
384 &self,
385 native_fn: &'static NativeFunction,
386 this: Option<RawVc>,
387 arg: &dyn DynTaskInputs,
388 ) -> Result<SmallVec<[TaskId; 1]>> {
389 let inner = &*self.inner;
390 if inner.database.is_empty() {
391 return Ok(SmallVec::new());
394 }
395 let hash = compute_task_type_hash_from_components(native_fn, this, arg);
396 let buffers = inner
397 .database
398 .get_multiple(KeySpace::TaskCache, &hash)
399 .with_context(|| {
400 format!("Looking up task id for {native_fn:?}(this={this:?}) from database failed")
401 })?;
402
403 let mut task_ids = SmallVec::with_capacity(buffers.len());
404 for bytes in buffers {
405 let bytes = Borrow::<[u8]>::borrow(&bytes).try_into()?;
406 let id = TaskId::try_from(u32::from_le_bytes(bytes)).unwrap();
407 task_ids.push(id);
408 }
409 Ok(task_ids)
410 }
411
412 pub(crate) fn lookup_data(
418 &self,
419 task_id: TaskId,
420 category: SpecificTaskDataCategory,
421 ) -> Result<Option<TaskStorage>> {
422 let inner = &*self.inner;
423 let Some(bytes) = inner
424 .database
425 .get(category.key_space(), IntKey::new(*task_id).as_ref())
426 .with_context(|| {
427 format!("Looking up task storage for {task_id} from database failed")
428 })?
429 else {
430 return Ok(None);
431 };
432 let mut storage = TaskStorage::default();
433 let mut decoder = new_turbo_bincode_decoder(bytes.borrow());
434 storage
435 .decode(category, &mut decoder)
436 .with_context(|| format!("Failed to decode {category:?}"))?;
437 Ok(Some(storage))
438 }
439
440 pub(crate) fn batch_lookup_data(
441 &self,
442 task_ids: &[TaskId],
443 category: SpecificTaskDataCategory,
444 ) -> Result<Vec<TaskStorage>> {
445 let inner = &*self.inner;
446 let int_keys: Vec<_> = task_ids.iter().map(|&id| IntKey::new(*id)).collect();
447 let keys = int_keys.iter().map(|k| k.as_ref()).collect::<Vec<_>>();
448 let bytes = inner
449 .database
450 .batch_get(category.key_space(), &keys)
451 .with_context(|| {
452 format!(
453 "Looking up typed data for {} tasks from database failed",
454 task_ids.len()
455 )
456 })?;
457 bytes
458 .into_iter()
459 .map(|opt_bytes| {
460 let mut storage = TaskStorage::new();
461 if let Some(bytes) = opt_bytes {
462 let mut decoder = new_turbo_bincode_decoder(bytes.borrow());
463 storage
464 .decode(category, &mut decoder)
465 .map_err(|e| anyhow::anyhow!("Failed to decode {category:?}: {e:?}"))?;
466 }
467 Ok(storage)
468 })
469 .collect::<Result<Vec<_>>>()
470 }
471
472 pub(crate) fn compact(&self) -> Result<Option<CommitStats>> {
473 self.inner.database.compact()
474 }
475
476 pub(crate) fn shutdown(&self) -> Result<()> {
477 self.inner.database.shutdown()
478 }
479
480 pub(crate) fn has_unrecoverable_write_error(&self) -> bool {
481 self.inner.database.has_unrecoverable_write_error()
482 }
483}
484
485fn get_next_free_task_id(batch: &TurboWriteBatch<'_>) -> Result<u32, anyhow::Error> {
486 Ok(
487 match batch.get(KeySpace::Infra, InfraKey::NextFreeTaskId.key().as_ref())? {
488 Some(bytes) => u32::from_le_bytes(Borrow::<[u8]>::borrow(&bytes).try_into()?),
489 None => 1,
490 },
491 )
492}
493
494fn save_infra(
495 batch: &TurboWriteBatch<'_>,
496 next_task_id: u32,
497 operations: Vec<Arc<AnyOperation>>,
498 roots: Option<Vec<(TaskId, TtlCounter)>>,
499) -> Result<(), anyhow::Error> {
500 batch
501 .put(
502 KeySpace::Infra,
503 WriteBuffer::Borrowed(InfraKey::NextFreeTaskId.key().as_ref()),
504 WriteBuffer::Borrowed(&next_task_id.to_le_bytes()),
505 )
506 .context("Unable to write next free task id")?;
507 {
508 let _span =
509 tracing::trace_span!("update operations", operations = operations.len()).entered();
510 let operations =
511 turbo_bincode_encode(&operations).context("Unable to serialize operations")?;
512 batch
513 .put(
514 KeySpace::Infra,
515 WriteBuffer::Borrowed(InfraKey::Operations.key().as_ref()),
516 WriteBuffer::SmallVec(operations),
517 )
518 .context("Unable to write operations")?;
519 }
520 if let Some(roots) = roots {
521 let _span = tracing::trace_span!("update roots", roots = roots.len()).entered();
522 let roots = turbo_bincode_encode(&roots).context("Unable to serialize GC roots")?;
523 batch
524 .put(
525 KeySpace::Infra,
526 WriteBuffer::Borrowed(InfraKey::GcRoots.key().as_ref()),
527 WriteBuffer::SmallVec(roots),
528 )
529 .context("Unable to write GC roots")?;
530 }
531 unsafe { batch.flush(KeySpace::Infra)? };
533 Ok(())
534}
535
536#[cfg(test)]
537mod tests {
538 use std::borrow::Borrow;
539
540 use turbo_tasks::TaskId;
541
542 use super::*;
543 use crate::{
544 BackingStorageOptions,
545 database::{turbo::TurboKeyValueDatabase, write_batch::WriteBuffer},
546 };
547
548 const TEST_STORAGE_OPTIONS: BackingStorageOptions = BackingStorageOptions {
551 is_ci: false,
552 is_short_session: true,
553 skip_compaction: false,
554 };
555
556 fn write_task_cache_entry(
558 db: &TurboKeyValueDatabase,
559 hash: u64,
560 task_id: TaskId,
561 ) -> Result<()> {
562 let batch = db.write_batch()?;
563 batch.put(
564 KeySpace::TaskCache,
565 WriteBuffer::Borrowed(&hash.to_le_bytes()),
566 WriteBuffer::Borrowed(&(*task_id).to_le_bytes()),
567 )?;
568 batch.commit()?;
569 Ok(())
570 }
571
572 fn task_cache_ids(db: &TurboKeyValueDatabase, hash: u64) -> Result<Vec<TaskId>> {
574 let mut ids: Vec<TaskId> = db
575 .get_multiple(KeySpace::TaskCache, &hash.to_le_bytes())?
576 .iter()
577 .map(|bytes| {
578 let bytes: [u8; 4] = Borrow::<[u8]>::borrow(bytes).try_into().unwrap();
579 TaskId::try_from(u32::from_le_bytes(bytes)).unwrap()
580 })
581 .collect();
582 ids.sort_by_key(|id| **id);
583 Ok(ids)
584 }
585
586 #[tokio::test(flavor = "multi_thread")]
592 async fn test_hash_collision_returns_multiple_candidates() -> Result<()> {
593 let tempdir = tempfile::tempdir()?;
594 let path = tempdir.path();
595
596 let db = TurboKeyValueDatabase::new(path.to_path_buf(), TEST_STORAGE_OPTIONS)?;
597
598 let collision_hash: u64 = 0xDEADBEEF;
600 let task_id_1 = TaskId::try_from(100u32).unwrap();
601 let task_id_2 = TaskId::try_from(200u32).unwrap();
602 let task_id_3 = TaskId::try_from(300u32).unwrap();
603
604 write_task_cache_entry(&db, collision_hash, task_id_1)?;
607 write_task_cache_entry(&db, collision_hash, task_id_2)?;
608 write_task_cache_entry(&db, collision_hash, task_id_3)?;
609
610 assert_eq!(
612 task_cache_ids(&db, collision_hash)?,
613 vec![task_id_1, task_id_2, task_id_3],
614 "Should return all 3 task IDs for the colliding hash"
615 );
616
617 db.shutdown()?;
618 Ok(())
619 }
620
621 #[tokio::test(flavor = "multi_thread")]
624 async fn test_batch_write_with_flush_and_reopen() -> Result<()> {
625 let tempdir = tempfile::tempdir()?;
626 let path = tempdir.path();
627
628 let n = 100_000;
629 let hashes: Vec<u64> = (0..n).map(|i| 0x1000 + i as u64).collect();
630 let task_ids: Vec<TaskId> = (1..=n as u32)
631 .map(|i| TaskId::try_from(i).unwrap())
632 .collect();
633
634 {
636 let db = TurboKeyValueDatabase::new(path.to_path_buf(), TEST_STORAGE_OPTIONS)?;
637 let batch = db.write_batch()?;
638
639 for (hash, task_id) in hashes.iter().zip(task_ids.iter()) {
640 batch.put(
641 KeySpace::TaskCache,
642 WriteBuffer::Borrowed(&hash.to_le_bytes()),
643 WriteBuffer::Borrowed(&(**task_id).to_le_bytes()),
644 )?;
645 }
646 unsafe { batch.flush(KeySpace::TaskCache) }?;
648 batch.commit()?;
649
650 db.shutdown()?;
651 }
652
653 {
655 let db = TurboKeyValueDatabase::new(path.to_path_buf(), TEST_STORAGE_OPTIONS)?;
656 let mut found = 0;
657 let mut missing = 0;
658 for (hash, expected_id) in hashes.iter().zip(task_ids.iter()) {
659 let results = db.get_multiple(KeySpace::TaskCache, &hash.to_le_bytes())?;
660 if results.is_empty() {
661 missing += 1;
662 } else {
663 found += 1;
664 let bytes: [u8; 4] = Borrow::<[u8]>::borrow(&results[0]).try_into().unwrap();
665 let id = TaskId::try_from(u32::from_le_bytes(bytes)).unwrap();
666 assert_eq!(id, *expected_id, "Task ID mismatch for hash {hash:#x}");
667 }
668 }
669 assert_eq!(missing, 0, "Found {found}/{n} entries, missing {missing}");
670 db.shutdown()?;
671 }
672
673 Ok(())
674 }
675
676 #[tokio::test(flavor = "multi_thread")]
684 async fn test_save_snapshot_delete_tombstones_task() -> Result<()> {
685 let tempdir = tempfile::tempdir()?;
686 let path = tempdir.path();
687
688 let collision_hash: u64 = 0xC0FFEE;
689 let deleted_id = TaskId::try_from(111u32).unwrap();
690 let survivor_id = TaskId::try_from(222u32).unwrap();
691 let deleted_key = (*deleted_id).to_le_bytes();
692
693 let db = TurboKeyValueDatabase::new(
694 path.to_path_buf(),
695 BackingStorageOptions {
696 is_ci: false,
697 is_short_session: true,
698 skip_compaction: false,
699 },
700 )?;
701
702 write_task_cache_entry(&db, collision_hash, deleted_id)?;
705 write_task_cache_entry(&db, collision_hash, survivor_id)?;
706 let batch = db.write_batch()?;
707 batch.put(
708 KeySpace::TaskMeta,
709 WriteBuffer::Borrowed(&deleted_key),
710 WriteBuffer::Borrowed(b"meta-bytes"),
711 )?;
712 batch.put(
713 KeySpace::TaskData,
714 WriteBuffer::Borrowed(&deleted_key),
715 WriteBuffer::Borrowed(b"data-bytes"),
716 )?;
717 batch.commit()?;
718
719 assert!(db.get(KeySpace::TaskMeta, &deleted_key)?.is_some());
721 assert!(db.get(KeySpace::TaskData, &deleted_key)?.is_some());
722 assert_eq!(
723 task_cache_ids(&db, collision_hash)?,
724 vec![deleted_id, survivor_id],
725 );
726
727 let storage = TurboBackingStorage::new_in_memory(db);
728
729 storage.save_snapshot(
731 Vec::new(),
732 None,
733 vec![vec![SnapshotItem::Delete {
734 task_id: deleted_id,
735 task_type_hash: collision_hash.to_le_bytes(),
736 }]],
737 )?;
738
739 let db = &storage.inner.database;
740 assert!(
741 db.get(KeySpace::TaskMeta, &deleted_key)?.is_none(),
742 "TaskMeta should be tombstoned"
743 );
744 assert!(
745 db.get(KeySpace::TaskData, &deleted_key)?.is_none(),
746 "TaskData should be tombstoned"
747 );
748 assert_eq!(
749 task_cache_ids(db, collision_hash)?,
750 vec![survivor_id],
751 "save_snapshot should delete only the named id from the bucket"
752 );
753
754 db.shutdown()?;
755 Ok(())
756 }
757}