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::{ArcBytes, 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::{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 NextFreeTaskId = 0,
38 GcRoots = 1,
39}
40
41impl InfraKey {
42 fn key(self) -> ByteKey {
43 ByteKey::new(self as u8)
44 }
45}
46
47struct ByteKey([u8; 1]);
48
49impl ByteKey {
50 fn new(value: u8) -> Self {
51 Self([value])
52 }
53}
54
55impl AsRef<[u8]> for ByteKey {
56 fn as_ref(&self) -> &[u8] {
57 &self.0
58 }
59}
60
61struct IntKey([u8; 4]);
62
63impl IntKey {
64 fn new(value: u32) -> Self {
65 Self(value.to_le_bytes())
66 }
67}
68
69impl AsRef<[u8]> for IntKey {
70 fn as_ref(&self) -> &[u8] {
71 &self.0
72 }
73}
74
75fn as_u32(bytes: impl Borrow<[u8]>) -> Result<u32> {
76 let n = u32::from_le_bytes(bytes.borrow().try_into()?);
77 Ok(n)
78}
79
80fn should_invalidate_on_panic() -> bool {
89 fn env_is_falsy(key: &str) -> bool {
90 env::var_os(key)
91 .is_none_or(|value| ["".as_ref(), "0".as_ref(), "false".as_ref()].contains(&&*value))
92 }
93 static SHOULD_INVALIDATE: LazyLock<bool> = LazyLock::new(|| {
94 env_is_falsy("TURBO_ENGINE_SKIP_INVALIDATE_ON_PANIC") && env_is_falsy("__NEXT_TEST_MODE")
95 });
96 *SHOULD_INVALIDATE
97}
98
99struct TurboBackingStorageInner {
100 database: TurboKeyValueDatabase,
101 base_path: Option<PathBuf>,
104 invalidated: Mutex<bool>,
106 _panic_hook_guard: Option<PanicHookGuard>,
109}
110
111pub struct TurboBackingStorage {
119 inner: Arc<TurboBackingStorageInner>,
121}
122
123impl TurboBackingStorage {
124 pub(crate) fn new_in_memory(database: TurboKeyValueDatabase) -> Self {
125 Self {
126 inner: Arc::new(TurboBackingStorageInner {
127 database,
128 base_path: None,
129 invalidated: Mutex::new(false),
130 _panic_hook_guard: None,
131 }),
132 }
133 }
134
135 pub(crate) fn open_versioned_on_disk(
147 base_path: PathBuf,
148 version_info: &GitVersionInfo,
149 is_ci: bool,
150 database: impl FnOnce(PathBuf) -> Result<TurboKeyValueDatabase>,
151 ) -> Result<(Self, StartupCacheState)> {
152 let startup_cache_state = check_db_invalidation_and_cleanup(&base_path)
153 .context("Failed to check database invalidation and cleanup")?;
154 let versioned_path = handle_db_versioning(&base_path, version_info, is_ci)
155 .context("Failed to handle database versioning")?;
156 let database = (database)(versioned_path).context("Failed to open database")?;
157 let backing_storage = Self {
158 inner: Arc::new_cyclic(move |weak_inner: &Weak<TurboBackingStorageInner>| {
159 let panic_hook_guard = if should_invalidate_on_panic() {
160 let weak_inner = weak_inner.clone();
161 Some(register_panic_hook(Box::new(move |_| {
162 let Some(inner) = weak_inner.upgrade() else {
163 return;
164 };
165 let _ = inner.invalidate(invalidation_reasons::PANIC);
170 })))
171 } else {
172 None
173 };
174 TurboBackingStorageInner {
175 database,
176 base_path: Some(base_path),
177 invalidated: Mutex::new(false),
178 _panic_hook_guard: panic_hook_guard,
179 }
180 }),
181 };
182 Ok((backing_storage, startup_cache_state))
183 }
184}
185
186impl TurboBackingStorageInner {
187 fn invalidate(&self, reason_code: &str) -> Result<()> {
188 if let Some(base_path) = &self.base_path {
190 let mut invalidated_guard = self
193 .invalidated
194 .lock()
195 .unwrap_or_else(PoisonError::into_inner);
196 if *invalidated_guard {
197 return Ok(());
198 }
199 invalidate_db(base_path, reason_code)?;
203 self.database.prevent_writes();
204 *invalidated_guard = true;
206 }
207 Ok(())
208 }
209
210 fn get_infra_u32(&self, key: InfraKey) -> Result<Option<u32>> {
212 self.database
213 .get(KeySpace::Infra, key.key().as_ref())?
214 .map(as_u32)
215 .transpose()
216 }
217}
218
219impl TurboBackingStorage {
220 pub(crate) fn invalidate(&self, reason_code: &str) -> Result<()> {
228 self.inner.invalidate(reason_code)
229 }
230
231 pub(crate) fn next_free_task_id(&self) -> Result<TaskId> {
232 Ok(self
233 .inner
234 .get_infra_u32(InfraKey::NextFreeTaskId)
235 .context("Unable to read next free task id from database")?
236 .map_or(Ok(TaskId::MIN), TaskId::try_from)?)
237 }
238
239 pub(crate) fn roots(&self) -> Result<Vec<(TaskId, TtlCounter)>> {
241 fn get(database: &TurboKeyValueDatabase) -> Result<Vec<(TaskId, TtlCounter)>> {
242 let Some(roots) = database.get(KeySpace::Infra, InfraKey::GcRoots.key().as_ref())?
243 else {
244 return Ok(Vec::new());
245 };
246 let roots = turbo_bincode_decode(roots.borrow())?;
247 Ok(roots)
248 }
249 get(&self.inner.database).context("Unable to read GC roots from database")
250 }
251
252 pub(crate) fn save_snapshot<I>(
253 &self,
254 roots: Option<Vec<(TaskId, TtlCounter)>>,
255 snapshots: Vec<I>,
256 ) -> Result<SnapshotMeta>
257 where
258 I: IntoIterator<Item = SnapshotItem> + Send + Sync,
259 {
260 let _span = tracing::info_span!("save snapshot").entered();
261 let batch = self.inner.database.write_batch()?;
262
263 {
264 let span = tracing::trace_span!("update task data");
265 let mut snapshot_meta =
266 parallel::map_collect_owned::<_, _, Result<Vec<_>>>(snapshots, |shard: I| {
267 let _span = span.clone().entered();
268 let mut max_new_task_id = 0;
269 let mut data_items = 0;
270 let mut meta_items = 0;
271 let mut task_cache_items = 0;
272 for item in shard {
273 match item {
274 SnapshotItem::Put {
275 task_id,
276 meta,
277 data,
278 task_type_hash,
279 ..
280 } => {
281 let key = IntKey::new(*task_id);
282 let key = key.as_ref();
283 if let Some(meta) = meta {
284 batch.put(
285 KeySpace::TaskMeta,
286 WriteBuffer::Borrowed(key),
287 WriteBuffer::SmallVec(meta),
288 )?;
289 meta_items += 1;
290 }
291 if let Some(data) = data {
292 batch.put(
293 KeySpace::TaskData,
294 WriteBuffer::Borrowed(key),
295 WriteBuffer::SmallVec(data),
296 )?;
297 data_items += 1;
298 }
299 if let Some(task_type_hash) = task_type_hash {
301 batch.put(
302 KeySpace::TaskCache,
303 WriteBuffer::Borrowed(&task_type_hash),
304 WriteBuffer::Borrowed(key),
305 )?;
306 task_cache_items += 1;
307 max_new_task_id = max_new_task_id.max(*task_id);
308 }
309 }
310 SnapshotItem::Delete {
311 task_id,
312 task_type_hash,
313 } => {
314 let key = IntKey::new(*task_id);
315 let key = key.as_ref();
316 batch.delete(KeySpace::TaskMeta, WriteBuffer::Borrowed(key))?;
317 batch.delete(KeySpace::TaskData, WriteBuffer::Borrowed(key))?;
318 batch.delete_value(
320 KeySpace::TaskCache,
321 WriteBuffer::Borrowed(&task_type_hash[..]),
322 WriteBuffer::Borrowed(key),
323 )?;
324 }
325 }
326 }
327 Ok(SnapshotMeta {
328 data_items,
329 meta_items,
330 task_cache_items,
331 bytes_written: 0,
334 bytes_deleted: 0,
335 max_next_task_id: max_new_task_id,
336 })
337 })?
338 .into_iter()
339 .reduce(|t1, t2| t1.merge(t2))
340 .unwrap_or_default();
341
342 let span = tracing::trace_span!("flush task data");
343 parallel::try_for_each(
344 &[KeySpace::TaskMeta, KeySpace::TaskData, KeySpace::TaskCache],
345 |&key_space| {
346 let _span = span.clone().entered();
347 unsafe { batch.flush(key_space) }
350 },
351 )?;
352
353 let mut next_task_id = get_next_free_task_id(&batch)?;
354 next_task_id = next_task_id.max(snapshot_meta.max_next_task_id + 1);
355
356 save_infra(&batch, next_task_id, roots)?;
357 {
358 let _span = tracing::trace_span!("commit").entered();
359 let stats = batch.commit().context("Unable to commit snapshot")?;
362 snapshot_meta.bytes_written = stats.bytes_written;
363 snapshot_meta.bytes_deleted = stats.bytes_deleted;
364 }
365 Ok(snapshot_meta)
366 }
367 }
368
369 pub(crate) fn lookup_task_candidates(
370 &self,
371 native_fn: &'static NativeFunction,
372 this: Option<RawVc>,
373 arg: &dyn DynTaskInputs,
374 ) -> Result<SmallVec<[TaskId; 1]>> {
375 let inner = &*self.inner;
376 if inner.database.is_empty() {
377 return Ok(SmallVec::new());
380 }
381 let hash = compute_task_type_hash_from_components(native_fn, this, arg);
382 let buffers = inner
383 .database
384 .get_multiple(KeySpace::TaskCache, &hash)
385 .with_context(|| {
386 format!("Looking up task id for {native_fn:?}(this={this:?}) from database failed")
387 })?;
388
389 let mut task_ids = SmallVec::with_capacity(buffers.len());
390 for bytes in buffers {
391 let bytes = Borrow::<[u8]>::borrow(&bytes).try_into()?;
392 let id = TaskId::try_from(u32::from_le_bytes(bytes)).unwrap();
393 task_ids.push(id);
394 }
395 Ok(task_ids)
396 }
397
398 pub(crate) fn lookup_data(
404 &self,
405 task_id: TaskId,
406 category: SpecificTaskDataCategory,
407 ) -> Result<Option<TaskStorage>> {
408 let inner = &*self.inner;
409 let Some(bytes) = inner
410 .database
411 .get(category.key_space(), IntKey::new(*task_id).as_ref())
412 .with_context(|| {
413 format!("Looking up task storage for {task_id} from database failed")
414 })?
415 else {
416 return Ok(None);
417 };
418 let mut storage = TaskStorage::default();
419 let mut decoder = new_turbo_bincode_decoder(bytes.borrow());
420 storage
421 .decode(category, &mut decoder)
422 .with_context(|| format!("Failed to decode {category:?}"))?;
423 Ok(Some(storage))
424 }
425
426 pub(crate) fn batch_lookup_data(
429 &self,
430 task_ids: &[TaskId],
431 category: SpecificTaskDataCategory,
432 ) -> Result<Vec<Option<TaskStorage>>> {
433 let bytes = self.batch_lookup_raw(task_ids, category)?;
434 decode_task_storage_batch(&bytes, category, PARALLEL_DECODE_THRESHOLD)
435 }
436
437 pub(crate) fn batch_lookup_raw(
439 &self,
440 task_ids: &[TaskId],
441 category: SpecificTaskDataCategory,
442 ) -> Result<Vec<Option<ArcBytes>>> {
443 let inner = &*self.inner;
444 let int_keys: Vec<_> = task_ids.iter().map(|&id| IntKey::new(*id)).collect();
445 let keys = int_keys.iter().map(|k| k.as_ref()).collect::<Vec<_>>();
446 inner
447 .database
448 .batch_get(category.key_space(), &keys)
449 .with_context(|| {
450 format!(
451 "Looking up typed data for {} tasks from database failed",
452 task_ids.len()
453 )
454 })
455 }
456
457 pub(crate) fn compact(&self) -> Result<Option<CommitStats>> {
458 self.inner.database.compact()
459 }
460
461 pub(crate) fn shutdown(&self) {
462 self.inner.database.shutdown()
463 }
464
465 pub(crate) fn has_unrecoverable_write_error(&self) -> bool {
466 self.inner.database.has_unrecoverable_write_error()
467 }
468}
469
470pub const PARALLEL_DECODE_THRESHOLD: usize = 2048;
480
481pub fn decode_task_storage_batch(
486 bytes: &[Option<ArcBytes>],
487 category: SpecificTaskDataCategory,
488 parallel_threshold: usize,
489) -> Result<Vec<Option<TaskStorage>>> {
490 let decode = |opt_bytes: &Option<ArcBytes>| -> Result<Option<TaskStorage>> {
491 let Some(bytes) = opt_bytes else {
492 return Ok(None);
493 };
494 let mut storage = TaskStorage::new();
495 let mut decoder = new_turbo_bincode_decoder(Borrow::<[u8]>::borrow(bytes));
496 storage
497 .decode(category, &mut decoder)
498 .map_err(|e| anyhow::anyhow!("Failed to decode {category:?}: {e:?}"))?;
499 Ok(Some(storage))
500 };
501 if bytes.len() >= parallel_threshold {
502 parallel::map_collect::<_, _, Result<Vec<_>>>(bytes, decode)
503 } else {
504 bytes.iter().map(decode).collect()
505 }
506}
507
508fn get_next_free_task_id(batch: &TurboWriteBatch<'_>) -> Result<u32, anyhow::Error> {
509 Ok(
510 match batch.get(KeySpace::Infra, InfraKey::NextFreeTaskId.key().as_ref())? {
511 Some(bytes) => u32::from_le_bytes(Borrow::<[u8]>::borrow(&bytes).try_into()?),
512 None => 1,
513 },
514 )
515}
516
517fn save_infra(
518 batch: &TurboWriteBatch<'_>,
519 next_task_id: u32,
520 roots: Option<Vec<(TaskId, TtlCounter)>>,
521) -> Result<(), anyhow::Error> {
522 batch
523 .put(
524 KeySpace::Infra,
525 WriteBuffer::Borrowed(InfraKey::NextFreeTaskId.key().as_ref()),
526 WriteBuffer::Borrowed(&next_task_id.to_le_bytes()),
527 )
528 .context("Unable to write next free task id")?;
529 if let Some(roots) = roots {
530 let _span = tracing::trace_span!("update roots", roots = roots.len()).entered();
531 let roots = turbo_bincode_encode(&roots).context("Unable to serialize GC roots")?;
532 batch
533 .put(
534 KeySpace::Infra,
535 WriteBuffer::Borrowed(InfraKey::GcRoots.key().as_ref()),
536 WriteBuffer::SmallVec(roots),
537 )
538 .context("Unable to write GC roots")?;
539 }
540 unsafe { batch.flush(KeySpace::Infra)? };
542 Ok(())
543}
544
545#[cfg(test)]
546mod tests {
547 use std::borrow::Borrow;
548
549 use turbo_tasks::TaskId;
550
551 use super::*;
552 use crate::{
553 BackingStorageOptions,
554 database::{turbo::TurboKeyValueDatabase, write_batch::WriteBuffer},
555 utils::test_temp_dir::test_temp_dir,
556 };
557
558 const TEST_STORAGE_OPTIONS: BackingStorageOptions = BackingStorageOptions {
561 is_ci: false,
562 is_short_session: true,
563 skip_compaction: false,
564 };
565
566 fn write_task_cache_entry(
568 db: &TurboKeyValueDatabase,
569 hash: u64,
570 task_id: TaskId,
571 ) -> Result<()> {
572 let batch = db.write_batch()?;
573 batch.put(
574 KeySpace::TaskCache,
575 WriteBuffer::Borrowed(&hash.to_le_bytes()),
576 WriteBuffer::Borrowed(&(*task_id).to_le_bytes()),
577 )?;
578 batch.commit()?;
579 Ok(())
580 }
581
582 fn task_cache_ids(db: &TurboKeyValueDatabase, hash: u64) -> Result<Vec<TaskId>> {
584 let mut ids: Vec<TaskId> = db
585 .get_multiple(KeySpace::TaskCache, &hash.to_le_bytes())?
586 .iter()
587 .map(|bytes| {
588 let bytes: [u8; 4] = Borrow::<[u8]>::borrow(bytes).try_into().unwrap();
589 TaskId::try_from(u32::from_le_bytes(bytes)).unwrap()
590 })
591 .collect();
592 ids.sort_by_key(|id| **id);
593 Ok(ids)
594 }
595
596 fn encoded_task_storage(seed: u32, category: SpecificTaskDataCategory) -> Vec<u8> {
598 use crate::{
599 backend::{TrackOutcome, storage_schema::TaskStorageAccessors},
600 data::{AggregationNumber, OutputValue},
601 };
602
603 struct Untracked(TaskStorage);
605 impl TaskStorageAccessors for Untracked {
606 fn typed(&self) -> &TaskStorage {
607 &self.0
608 }
609 fn typed_mut(&mut self) -> &mut TaskStorage {
610 &mut self.0
611 }
612 fn track_modification(
613 &mut self,
614 _category: SpecificTaskDataCategory,
615 _name: &str,
616 ) -> TrackOutcome {
617 TrackOutcome::NoChange
618 }
619 fn undo_track_modification(&mut self, _outcome: TrackOutcome) {}
620 fn check_access(&self, _category: SpecificTaskDataCategory) {}
621 }
622
623 let id = |n: u32| TaskId::try_from(n).unwrap();
624 let mut task = Untracked(TaskStorage::new());
625 task.set_output(OutputValue::Output(id(seed + 1)));
626 task.set_aggregation_number(AggregationNumber {
627 base: seed,
628 distance: 1,
629 effective: seed + 1,
630 });
631 task.add_upper(id(seed + 2), 1);
632 assert!(task.add_output_dependencies(id(seed + 3)));
633 assert!(task.add_children(id(seed + 4)));
634 let storage = task.0;
635 let mut buffer = turbo_bincode::TurboBincodeBuffer::new();
636 storage
637 .encode(
638 category,
639 &mut turbo_bincode::new_turbo_bincode_encoder(&mut buffer),
640 )
641 .unwrap();
642 buffer.to_vec()
643 }
644
645 fn reencode(
646 storages: &[Option<TaskStorage>],
647 category: SpecificTaskDataCategory,
648 ) -> Vec<Option<Vec<u8>>> {
649 storages
650 .iter()
651 .map(|storage| {
652 storage.as_ref().map(|storage| {
653 let mut buffer = turbo_bincode::TurboBincodeBuffer::new();
654 storage
655 .encode(
656 category,
657 &mut turbo_bincode::new_turbo_bincode_encoder(&mut buffer),
658 )
659 .unwrap();
660 buffer.to_vec()
661 })
662 })
663 .collect()
664 }
665
666 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
669 async fn test_decode_task_storage_batch_sequential_matches_parallel() {
670 const SEQUENTIAL: usize = usize::MAX;
671 const PARALLEL: usize = 0;
672 for category in [
673 SpecificTaskDataCategory::Meta,
674 SpecificTaskDataCategory::Data,
675 ] {
676 let expected: Vec<Option<Vec<u8>>> = (0..200u32)
677 .map(|i| (i % 7 != 3).then(|| encoded_task_storage(i * 10 + 1, category)))
678 .collect();
679 assert_ne!(expected[0], expected[1], "inputs must be distinct");
680 let to_bytes = || -> Vec<Option<ArcBytes>> {
681 expected
682 .iter()
683 .map(|bytes| {
684 bytes
685 .as_ref()
686 .map(|b| ArcBytes::from(b.clone().into_boxed_slice()))
687 })
688 .collect()
689 };
690
691 for threshold in [SEQUENTIAL, PARALLEL] {
692 let decoded = decode_task_storage_batch(&to_bytes(), category, threshold).unwrap();
693 assert_eq!(
694 reencode(&decoded, category),
695 expected,
696 "{category:?} threshold={threshold}"
697 );
698
699 assert!(
700 decode_task_storage_batch(&[], category, threshold)
701 .unwrap()
702 .is_empty()
703 );
704
705 let mut malformed = to_bytes();
706 malformed[150] = Some(ArcBytes::from(vec![0xff; 3].into_boxed_slice()));
707 assert!(
708 decode_task_storage_batch(&malformed, category, threshold).is_err(),
709 "{category:?} threshold={threshold} must fail on malformed bytes"
710 );
711 }
712 }
713 }
714
715 #[tokio::test(flavor = "multi_thread")]
720 async fn test_batch_lookup_data_roundtrip_at_parallel_threshold() -> Result<()> {
721 let tempdir = test_temp_dir()?;
722 let db = TurboKeyValueDatabase::new(tempdir.path().to_path_buf(), TEST_STORAGE_OPTIONS)?;
723 let stored_ids: Vec<TaskId> = (1..=(PARALLEL_DECODE_THRESHOLD as u32 + 64))
724 .map(|id| TaskId::try_from(id).unwrap())
725 .collect();
726 let batch = db.write_batch()?;
727 for category in [
728 SpecificTaskDataCategory::Meta,
729 SpecificTaskDataCategory::Data,
730 ] {
731 for &task_id in &stored_ids {
732 let key = IntKey::new(*task_id);
733 let value = encoded_task_storage(*task_id * 10, category);
734 batch.put(
735 category.key_space(),
736 WriteBuffer::Borrowed(key.as_ref()),
737 WriteBuffer::Borrowed(&value),
738 )?;
739 }
740 }
741 batch.commit()?;
742 let storage = TurboBackingStorage::new_in_memory(db);
743
744 for category in [
745 SpecificTaskDataCategory::Meta,
746 SpecificTaskDataCategory::Data,
747 ] {
748 for len in [PARALLEL_DECODE_THRESHOLD - 1, PARALLEL_DECODE_THRESHOLD] {
749 let mut requested = stored_ids[..len - 3].to_vec();
750 requested.reverse();
751 requested.insert(len / 2, TaskId::try_from(999_999).unwrap());
752 requested.push(stored_ids[7]);
753 requested.push(TaskId::try_from(999_998).unwrap());
754 assert_eq!(requested.len(), len);
755
756 let expected: Vec<Option<Vec<u8>>> = requested
757 .iter()
758 .map(|task_id| {
759 (**task_id <= stored_ids.len() as u32)
760 .then(|| encoded_task_storage(**task_id * 10, category))
761 })
762 .collect();
763 let decoded = storage.batch_lookup_data(&requested, category)?;
764 assert_eq!(
765 reencode(&decoded, category),
766 expected,
767 "{category:?} len={len}"
768 );
769 }
770 }
771
772 storage.inner.database.shutdown();
773 Ok(())
774 }
775
776 #[tokio::test(flavor = "multi_thread")]
782 async fn test_hash_collision_returns_multiple_candidates() -> Result<()> {
783 let tempdir = test_temp_dir()?;
784 let path = tempdir.path();
785
786 let db = TurboKeyValueDatabase::new(path.to_path_buf(), TEST_STORAGE_OPTIONS)?;
787
788 let collision_hash: u64 = 0xDEADBEEF;
790 let task_id_1 = TaskId::try_from(100u32).unwrap();
791 let task_id_2 = TaskId::try_from(200u32).unwrap();
792 let task_id_3 = TaskId::try_from(300u32).unwrap();
793
794 write_task_cache_entry(&db, collision_hash, task_id_1)?;
797 write_task_cache_entry(&db, collision_hash, task_id_2)?;
798 write_task_cache_entry(&db, collision_hash, task_id_3)?;
799
800 assert_eq!(
802 task_cache_ids(&db, collision_hash)?,
803 vec![task_id_1, task_id_2, task_id_3],
804 "Should return all 3 task IDs for the colliding hash"
805 );
806
807 db.shutdown();
808 Ok(())
809 }
810
811 #[cfg(not(miri))]
815 #[tokio::test(flavor = "multi_thread")]
816 async fn test_batch_write_with_flush_and_reopen() -> Result<()> {
817 let tempdir = test_temp_dir()?;
818 let path = tempdir.path();
819
820 let n = 100_000;
821 let hashes: Vec<u64> = (0..n).map(|i| 0x1000 + i as u64).collect();
822 let task_ids: Vec<TaskId> = (1..=n as u32)
823 .map(|i| TaskId::try_from(i).unwrap())
824 .collect();
825
826 {
828 let db = TurboKeyValueDatabase::new(path.to_path_buf(), TEST_STORAGE_OPTIONS)?;
829 let batch = db.write_batch()?;
830
831 for (hash, task_id) in hashes.iter().zip(task_ids.iter()) {
832 batch.put(
833 KeySpace::TaskCache,
834 WriteBuffer::Borrowed(&hash.to_le_bytes()),
835 WriteBuffer::Borrowed(&(**task_id).to_le_bytes()),
836 )?;
837 }
838 unsafe { batch.flush(KeySpace::TaskCache) }?;
840 batch.commit()?;
841
842 db.shutdown();
843 }
844
845 {
847 let db = TurboKeyValueDatabase::new(path.to_path_buf(), TEST_STORAGE_OPTIONS)?;
848 let mut found = 0;
849 let mut missing = 0;
850 for (hash, expected_id) in hashes.iter().zip(task_ids.iter()) {
851 let results = db.get_multiple(KeySpace::TaskCache, &hash.to_le_bytes())?;
852 if results.is_empty() {
853 missing += 1;
854 } else {
855 found += 1;
856 let bytes: [u8; 4] = Borrow::<[u8]>::borrow(&results[0]).try_into().unwrap();
857 let id = TaskId::try_from(u32::from_le_bytes(bytes)).unwrap();
858 assert_eq!(id, *expected_id, "Task ID mismatch for hash {hash:#x}");
859 }
860 }
861 assert_eq!(missing, 0, "Found {found}/{n} entries, missing {missing}");
862 db.shutdown();
863 }
864
865 Ok(())
866 }
867
868 #[tokio::test(flavor = "multi_thread")]
876 async fn test_save_snapshot_delete_tombstones_task() -> Result<()> {
877 let tempdir = test_temp_dir()?;
878 let path = tempdir.path();
879
880 let collision_hash: u64 = 0xC0FFEE;
881 let deleted_id = TaskId::try_from(111u32).unwrap();
882 let survivor_id = TaskId::try_from(222u32).unwrap();
883 let deleted_key = (*deleted_id).to_le_bytes();
884
885 let db = TurboKeyValueDatabase::new(
886 path.to_path_buf(),
887 BackingStorageOptions {
888 is_ci: false,
889 is_short_session: true,
890 skip_compaction: false,
891 },
892 )?;
893
894 write_task_cache_entry(&db, collision_hash, deleted_id)?;
897 write_task_cache_entry(&db, collision_hash, survivor_id)?;
898 let batch = db.write_batch()?;
899 batch.put(
900 KeySpace::TaskMeta,
901 WriteBuffer::Borrowed(&deleted_key),
902 WriteBuffer::Borrowed(b"meta-bytes"),
903 )?;
904 batch.put(
905 KeySpace::TaskData,
906 WriteBuffer::Borrowed(&deleted_key),
907 WriteBuffer::Borrowed(b"data-bytes"),
908 )?;
909 batch.commit()?;
910
911 assert!(db.get(KeySpace::TaskMeta, &deleted_key)?.is_some());
913 assert!(db.get(KeySpace::TaskData, &deleted_key)?.is_some());
914 assert_eq!(
915 task_cache_ids(&db, collision_hash)?,
916 vec![deleted_id, survivor_id],
917 );
918
919 let storage = TurboBackingStorage::new_in_memory(db);
920
921 storage.save_snapshot(
923 None,
924 vec![vec![SnapshotItem::Delete {
925 task_id: deleted_id,
926 task_type_hash: collision_hash.to_le_bytes(),
927 }]],
928 )?;
929
930 let db = &storage.inner.database;
931 assert!(
932 db.get(KeySpace::TaskMeta, &deleted_key)?.is_none(),
933 "TaskMeta should be tombstoned"
934 );
935 assert!(
936 db.get(KeySpace::TaskData, &deleted_key)?.is_none(),
937 "TaskData should be tombstoned"
938 );
939 assert_eq!(
940 task_cache_ids(db, collision_hash)?,
941 vec![survivor_id],
942 "save_snapshot should delete only the named id from the bucket"
943 );
944
945 db.shutdown();
946 Ok(())
947 }
948}