Skip to content

Transaction and Recovery

Recep Samet Yıldız edited this page Sep 1, 2026 · 1 revision

Transaction and Recovery

UEBPCracker wraps all Blueprint mutations in Unreal's transaction system and provides durable recovery for interrupted operations.


FScopedTransaction

Every write operation opens an FScopedTransaction:

FScopedTransaction Transaction(
    TEXT("UEBPCracker"),           // context (FString, not FName)
    LOCTEXT("OpName", "..."),      // display text
    Blueprint,                     // primary UObject
    /*bShouldActuallyTransact=*/true
);

The transaction captures all subsequent Modify() calls on Unreal objects. On success, the transaction is committed to the undo buffer. On failure, rollback is performed.

Important UE 5.8 facts

  • FScopedTransaction::Cancel() does NOT roll back — it only drops the undo record. Actual rollback requires calling GEditor->UndoTransaction() explicitly.
  • The 2-arg FScopedTransaction(FText, bool) ctor uses an EMPTY context — use the 4-arg ctor with GEditor->BeginTransaction.
  • FTransactionContext::Context is FString, not FName (don't call .ToString()).
  • GEditor->CanTransact() requires !UE::GetIsEditorLoadingPackage() && !IsRoutingPostLoad.
  • UE 5.8 compile/reinstancing creates a transaction with no recorded changes → UTransBuffer::End() pops it from the undo buffer. Strict top-context checks will fail after compile; use a bounded undo loop instead.

Rollback (Undo Loop)

When a postcondition assertion fails after mutation, UEBPCracker performs a bounded undo loop:

1. Record BeforeFullFingerprint (before mutation started)
2. Mutation + postcondition check
3. [Postcondition fails]
4. Loop: GEditor->UndoTransaction() → up to 16 times
5. After each undo: check if current fingerprint == BeforeFullFingerprint
6. If match: rollback verified ✅
7. If 16 undos exhausted without match: flag bRecoveryRequired=true

Why ≤16 undos? The compile/reinstancing transaction can silently appear between the mutation and our transaction on the undo stack, so simple "pop top" isn't reliable.


Nested Transaction Guard

Inner services (Variable, Component, Graph) must not open their own FScopedTransaction when called from within a patch/spec transaction.

FUEBPTransactionGuard enforces this:

// Opens a transaction ONLY if no transaction is currently active
if (GEditor && GEditor->Trans && !GEditor->Trans->IsActive()) {
    Transaction = MakeUnique<FScopedTransaction>(...);
}

This prevents nested transactions from breaking the "patch txn is the top context" invariant.


Durable Backup

Before any patch or spec operation that could fail mid-way, UEBPCracker creates a durable backup:

1. DuplicateAsset(Blueprint, BackupRoot, BackupName)
2. Record backup path in durable journal (on disk)
3. Proceed with operation
4. [On success]: mark journal entry "committed"
5. [On failure]: journal entry remains "open" → GetRecoveryStatus shows it

DuplicateAsset caveats (UE 5.8)

UEditorAssetSubsystem::DuplicateAsset regenerates all GUIDs (node/graph/component/variable) and drops package metadata tags (ownership manifest lost). Consequences:

  1. Exact full-fingerprint verification is impossible — use content fingerprint (GUID-normalized) instead
  2. Node array order is unstable — sort nodes by (identity_kind, identity_name, x, y) before ordinal assignment
  3. Ownership manifest is stale after restore — must call RecommitOwnership (see Patch and Idempotency)

Restore Flow

GetRecoveryStatus
    → lists pending recovery entries with operation IDs

RestoreOperationBackup (operation_id, expected_content_fingerprint)
    → loads backup asset from BackupRoot
    → verifies content fingerprint (GUID-normalized comparison)
    → replaces current Blueprint with backup content
    → calls RecommitOwnership to fix stale manifest GUIDs
    → marks journal entry "restored"
    → returns new fingerprint

[After successful restore:]
DiscardOperationBackup (operation_id)
    → deletes backup asset from BackupRoot
    → marks journal entry "discarded"

After discard

RestoreOperationBackup on a discarded entry returns:

{ "success": false, "errorCode": "UEBP.BACKUP_INVALID", "message": "Asset not found in the Asset Registry" }

This is irreversible — once discarded, there is no further restore option from the plugin's side.


Fault Injection (Testing Only)

FUEBPFailpoints provides test-only injection points that are production no-ops:

Failpoint Fires after...
AfterVariableMutation Variable written to Blueprint
AfterNodeCreation K2 node created in graph
BeforeCompile Before engine compile call
AfterSave After successful save
AfterBackup After DuplicateAsset
BeforeManifestCommit Before writing ownership manifest
AfterManifestCommit After writing ownership manifest

Usage in test scripts:

# Arm failpoint before apply
call_tool SetFailpoint AfterVariableMutation enabled=true

# Apply spec (will fault mid-way)
call_tool ApplyBlueprintSpec file=...

# Verify recovery state
call_tool GetRecoveryStatus
# → should show pending restore entry

# Restore
call_tool RestoreOperationBackup operation_id=...

Recovery Error Codes

Code Meaning
UEBP.BACKUP_INVALID Backup asset not found (discarded or never created)
UEBP.RECOVERY_BLOCKS_UPGRADE Pending recovery prevents version upgrade
UEBP.STATE_INTEGRITY_FAILED Journal/state file checksum mismatch

Versioning Interaction

If there is a pending recovery operation (journal entry not closed), FUEBPVersionInfo::CheckUpgradeSafety returns a warning. The upgrade gate (check_completion criterion t07) will not pass until all recovery entries are resolved.


See Also

Clone this wiki locally