feat(engine): write-through persistence (M13 D2)

- facade storage session manager: project handles bind to the default
  SQLite library on project_new/load; every undo push/group_end/jump
  write-throughs via DatabaseBackend::save (diff journal); project_free
  flushes and unbinds
- background snapshot thread (Storage/SnapshotIntervalSec, latest-wins,
  newest 3 kept) with exit flush (oakengine_storage_flush)
- config-gated: storage only activates with an explicit
  Storage/Backend=sqlite, so headless consumers and tests never touch
  the real library; new exports: storage_flush/is_bound/last_error
- it_storage: kill -9 recovery, cross-session undo, snapshot pruning,
  multi-project isolation, graceful degradation
- also fixes a real config test polluting the user config.ini and the
  undo-stack test races
This commit is contained in:
2026-08-16 08:52:23 +08:00
parent b35f3b49bc
commit 5fabad8efd
18 changed files with 1386 additions and 16 deletions
+22 -3
View File
@@ -88,9 +88,25 @@ const OAKTASK_E_NOT_FOUND: c_int = -80004;
/// does not cascade into `PoisonError` failures in every later test.
static SERIAL: Mutex<()> = Mutex::new(());
/// Take the [`SERIAL`] lock, recovering from any poisoning.
pub(crate) fn serial() -> std::sync::MutexGuard<'static, ()> {
SERIAL.lock().unwrap_or_else(|e| e.into_inner())
/// Both lock guards held by [`serial`] (the local task lock plus the
/// facade-wide undo-stack lock).
pub(crate) struct SerialGuard {
/// The [`SERIAL`] lock.
_task: std::sync::MutexGuard<'static, ()>,
/// The facade's process-wide undo-stack lock (it_undo's), so the
/// `oakengine_project_new` calls in these tests (which clear the stack)
/// never race the it_undo / it_storage stack tests.
_stack: std::sync::MutexGuard<'static, ()>,
}
/// Take the [`SERIAL`] lock AND the global undo-stack lock, recovering
/// from any poisoning.
pub(crate) fn serial() -> SerialGuard {
let _task = SERIAL.lock().unwrap_or_else(|e| e.into_inner());
let _stack = super::it_undo::GLOBAL_STACK_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner());
SerialGuard { _task, _stack }
}
/// The facade's live task-payload counter (the engine-side replacement
@@ -904,6 +920,9 @@ fn export_task_creation() {
fn export_task_run_real_encoder() {
let _g = serial();
common::force_link();
// The import/add-track/add-clip commands below are undoable; disable
// the write-through backend so they never touch a library.
let _storage = common::storage_off_guard();
let media = std::env::temp_dir().join(format!(
"oakengine-it-task-export-src-{}.mp4",