Section 6/161 menit

6. Actor Isolation sebagai Safety Boundary

6. Actor Isolation sebagai Safety Boundary

Isolation Domains yang Ada

swift
// 1. Actor isolation — custom serial queue
actor BankAccount {
    private var balance: Decimal
    private var transactions: [Transaction] = []
    
    init(initialBalance: Decimal) {
        self.balance = initialBalance
    }
    
    // Hanya bisa diakses dengan await dari luar actor
    func transfer(amount: Decimal, to destination: BankAccount) async throws {
        guard balance >= amount else {
            throw BankError.insufficientFunds
        }
        // Ini adalah suspension point — actor bisa handle request lain di sini
        // (re-entrancy) — penting untuk dipahami!
        try await destination.receive(amount: amount)
        
        // Balance mungkin sudah berubah jika ada concurrent call!
        // Cek ulang setelah await
        guard balance >= amount else {
            throw BankError.insufficientFunds  // rollback scenario
        }
        balance -= amount
        transactions.append(.debit(amount: amount))
    }
    
    func receive(amount: Decimal) {
        balance += amount
        transactions.append(.credit(amount: amount))
    }
    
    var currentBalance: Decimal { balance }
}

// 2. @MainActor — main thread isolation
@MainActor
class UIStateManager {
    var isLoading: Bool = false
    var errorMessage: String?
    var items: [DisplayItem] = []
    
    // Method ini hanya bisa dipanggil dari main thread
    func showLoading() { isLoading = true }
    func hideLoading() { isLoading = false }
}

// 3. Custom global actor
@globalActor
actor DatabaseActor {
    static let shared = DatabaseActor()
}

@DatabaseActor
class DatabaseManager {
    private var connection: DatabaseConnection?
    
    func query(_ sql: String) async throws -> [Row] {
        guard let conn = connection else { throw DBError.notConnected }
        return try await conn.execute(sql)
    }
}

Actor Re-Entrancy — Masalah Tersembunyi

swift
// ❌ Re-entrancy menyebabkan invariant yang rusak
actor InventoryManager {
    private var stock: [ProductID: Int] = [:]
    
    func reserveItem(productId: ProductID, quantity: Int) async throws {
        guard let available = stock[productId], available >= quantity else {
            throw InventoryError.outOfStock
        }
        
        // ⚠️ SUSPENSION POINT — actor bisa handle request lain di sini!
        try await chargePayment(for: productId, quantity: quantity)
        
        // Saat kembali, stock[productId] mungkin sudah berubah
        // karena ada request lain yang masuk saat kita suspend
        stock[productId]! -= quantity  // bisa jadi negative!
    }
}

// ✓ Pola aman: check-then-act dalam satu atomic operation
actor InventoryManager {
    private var stock: [ProductID: Int] = [:]
    private var reservations: Set<ReservationID> = []
    
    func reserveItem(productId: ProductID, quantity: Int) async throws -> ReservationID {
        // Atomic check-and-reserve sebelum suspend
        guard let available = stock[productId], available >= quantity else {
            throw InventoryError.outOfStock
        }
        
        // Reserve dulu (tanpa suspend)
        let reservationId = ReservationID()
        stock[productId]! -= quantity
        reservations.insert(reservationId)
        
        // Baru payment — jika gagal, rollback
        do {
            try await chargePayment(for: productId, quantity: quantity)
        } catch {
            // Rollback reservation
            stock[productId]! += quantity
            reservations.remove(reservationId)
            throw error
        }
        
        return reservationId
    }
}