- Implement two-phase rollback system inspired by apt-tx project - Add package state tracking (newly_installed, upgraded, previously_installed) - Enhance both core and advanced transaction APIs with rollback methods - Add comprehensive rollback documentation in docs/rollback.md - Create rollback_demo.rs example demonstrating functionality - Update README with detailed crate usage instructions - Add rollback features to feature flags documentation - Fix import issues in advanced crate modules - Add get_installed_packages method to AptCommands - Include both crates.io and git installation options in README
6.3 KiB
Rollback Implementation
This document explains how the enhanced rollback functionality works in the APT-DNF Bridge.
Overview
The rollback system provides a comprehensive way to undo package installations and upgrades. It tracks what packages were changed during a transaction and can restore the system to its previous state using a two-phase approach.
How It Works
Package Tracking
Both Transaction (core) and TransactionV2 (advanced) track three types of package information:
previously_installed: Packages that were installed before the transactionnewly_installed: Packages that weren't installed before the transactionupgraded: Packages that were upgraded, with their previous versions stored
Two-Phase Rollback
When rollback() is called, it performs two phases:
- Phase 1 - Remove New Packages: Uses
apt remove -yto remove all packages innewly_installed - Phase 2 - Downgrade Upgraded Packages: Uses
apt install -y package=oldversionto restore previous versions
Example Usage
Core API
use apt_dnf_bridge::{Transaction, Package};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create and commit a transaction
let mut tx = Transaction::new();
tx.add_install(Package::new("vim", "2:8.1.2269-1ubuntu5.14", "amd64")).await?;
tx.add_install(Package::new("curl", "7.68.0-1ubuntu2.18", "amd64")).await?;
tx.resolve().await?;
tx.commit().await?;
// Later, rollback the changes
tx.rollback().await?;
Ok(())
}
Advanced API
use apt_dnf_bridge::{TransactionV2, Package, BackendConfig};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create transaction with backend
let config = BackendConfig::default();
let mut tx = TransactionV2::with_shell_backend(config).await?;
// Add packages and commit
tx.add_install(Package::new("git", "1:2.25.1-1ubuntu3.11", "amd64")).await?;
tx.resolve().await?;
tx.commit().await?;
// Rollback using backend
tx.rollback().await?;
Ok(())
}
API Reference
Core Transaction Methods
impl Transaction {
/// Get packages that were newly installed in this transaction.
pub fn newly_installed(&self) -> &HashSet<String>
/// Get packages that were upgraded in this transaction.
pub fn upgraded(&self) -> &HashMap<String, String>
/// Get packages that were previously installed before this transaction.
pub fn previously_installed(&self) -> &HashSet<String>
/// Check if the transaction has any changes that can be rolled back.
pub fn can_rollback(&self) -> bool
/// Attempt to rollback the transaction (best-effort).
pub async fn rollback(&self) -> Result<()>
}
Advanced Transaction Methods
impl TransactionV2 {
/// Get packages that were newly installed in this transaction.
pub fn newly_installed(&self) -> &HashSet<String>
/// Get packages that were upgraded in this transaction.
pub fn upgraded(&self) -> &HashMap<String, String>
/// Get packages that were previously installed before this transaction.
pub fn previously_installed(&self) -> &HashSet<String>
/// Check if the transaction has any changes that can be rolled back.
pub fn can_rollback(&self) -> bool
/// Get a list of all changed packages (newly installed + upgraded).
pub fn changed_packages(&self) -> Vec<String>
/// Attempt to rollback the transaction using the two-phase approach.
pub async fn rollback(&self) -> Result<()>
}
Error Handling
The rollback system uses a "fail fast" approach:
- Critical errors (package not found, permission denied) cause immediate failure
- Soft errors (package already removed) generate warnings but continue
- If a specific version can't be installed, the package is removed instead
Limitations
This implementation handles the most common rollback scenarios:
- ✅ New package installations
- ✅ Package upgrades with available previous versions
- ✅ Mixed transactions (some new, some upgrades)
It does not handle:
- Complex dependency changes
- Configuration file modifications
- System state beyond package versions
- Concurrent package operations
For more advanced rollback scenarios, consider using OSTree's native checkpoint/rollback functionality.
Implementation Details
Data Structures
pub struct Transaction {
operations: Vec<Operation>,
previously_installed: HashSet<String>, // packages that were installed before
newly_installed: HashSet<String>, // packages that weren't installed before
upgraded: HashMap<String, String>, // package -> old_version for upgrades
log_commands: bool,
}
Key Methods
add_install(): Adds a package to the transactioncommit(): Installs packages and categorizes them as new vs upgradedrollback(): Performs the two-phase rollback processchanged_packages(): Returns a list of what was changed
State Management
The tracking fields (newly_installed, upgraded) are only populated after a successful commit(). This ensures the rollback data reflects what actually happened during the transaction.
Testing
The rollback functionality is tested in the test suite, but actual package installation/removal is not performed in tests to avoid side effects on the test environment.
OSTree Integration
For atomic operations, use OSTree's native checkpoint/rollback:
// 1. Create OSTree checkpoint
let checkpoint = ostree_create_checkpoint()?;
// 2. Run APT transaction
let mut tx = Transaction::new();
tx.add_install(Package::new("vim", "", "")).await?;
tx.commit().await?;
// 3. Commit or rollback based on result
if success {
ostree_commit_changes()?;
} else {
ostree_rollback_to_checkpoint(checkpoint)?;
}
Design Philosophy
The current implementation follows these principles:
- Simplicity over completeness: Handle common cases well
- Fail fast: Clear error messages when things go wrong
- Maintainability: Easy to understand and modify
- Reliability: Works consistently in real-world scenarios
This approach ensures the rollback system is robust, understandable, and maintainable while handling the vast majority of actual use cases.