Section 8/161 menit

8. Actor Re-entrancy: Patterns dan Pitfalls

8. Actor Re-entrancy: Patterns dan Pitfalls

Re-entrancy adalah properti fundamental actor di Swift: saat actor await sesuatu, actor bisa melayani request lain sebelum await selesai.

Visualisasi Re-entrancy

swift
Actor UserCache menjalankan dua task konkuren:

Task A: fetchUser("alice")        Task B: fetchUser("alice")
   │                                  │
   ├─ guard !cache["alice"]            │
   │   (cache kosong, lanjut)         │
   │                                  ├─ guard !cache["alice"]
   ├─ await network.fetch("alice")◄──suspend (actor bisa layani task lain)
   │                                  │   (cache masih kosong!)
   │                               ◄──┤─ await network.fetch("alice")
   │                                  │                      suspend
   │◄── resume: alice data tiba        │
   ├─ cache["alice"] = alice           │
   ├─ return alice                     │
                                    ◄──┤─ resume: alice data tiba (DUPLIKAT!)
                                       ├─ cache["alice"] = alice (redundant)
                                       ├─ return alice

Pola Aman: Check-Then-Act Tanpa Suspension di Antara

swift
actor UserCache {
    private var cache: [String: User] = [:]
    private var inFlight: [String: Task<User, Error>] = [:]

    // ❌ Pattern berbahaya: check dan set dipisah oleh await
    func fetchUserUnsafe(id: String) async throws -> User {
        if let cached = cache[id] { return cached }

        let user = try await NetworkClient.fetchUser(id: id)  // Suspension!
        // State bisa berubah di sini — task lain bisa sudah isi cache[id]
        cache[id] = user  // Mungkin overwrite pekerjaan task lain
        return user
    }

    // ✓ Pattern aman: deduplication dengan task tracking
    func fetchUser(id: String) async throws -> User {
        // Check cache — tidak ada suspension
        if let cached = cache[id] { return cached }

        // Check apakah sudah ada request yang in-flight
        if let existingTask = inFlight[id] {
            return try await existingTask.value  // Tunggu task yang sudah ada
        }

        // Buat task baru dan daftarkan ke inFlight SEBELUM await
        let task = Task<User, Error> {
            try await NetworkClient.fetchUser(id: id)
        }
        inFlight[id] = task

        do {
            let user = try await task.value  // Suspension di sini
            // Setelah resume: update cache
            cache[id] = user
            inFlight.removeValue(forKey: id)
            return user
        } catch {
            inFlight.removeValue(forKey: id)
            throw error
        }
    }
}

Pola Aman: Snapshot Sebelum Suspension

swift
actor OrderProcessor {
    var pendingOrders: [Order] = []
    var processedCount: Int = 0

    // ❌ Rentan re-entrancy
    func processAllUnsafe() async {
        for order in pendingOrders {  // Iterasi array yang bisa berubah!
            await process(order)       // Suspension: pendingOrders bisa berubah
        }
    }

    // ✓ Snapshot dulu, baru iterasi
    func processAll() async {
        let snapshot = pendingOrders  // Copy — tidak terpengaruh mutation lain
        pendingOrders = []            // Clear list sebelum suspension
        processedCount += snapshot.count  // Update sebelum suspension

        for order in snapshot {
            await process(order)  // Suspension aman — snapshot tidak berubah
        }
    }
}

Pola Aman: State Machine Eksplisit

swift
actor FileDownloader {
    enum State {
        case idle
        case downloading(URL)
        case processing(URL, Data)
        case done(URL, Data)
        case failed(URL, Error)
    }

    private var state: State = .idle

    func download(from url: URL) async throws -> Data {
        // State machine mencegah operasi konkuren yang bertentangan
        guard case .idle = state else {
            throw DownloadError.alreadyInProgress
        }

        state = .downloading(url)  // Set SEBELUM await

        do {
            let data = try await URLSession.shared.data(from: url).0
            state = .processing(url, data)

            let processed = try await processData(data)
            state = .done(url, processed)

            return processed
        } catch {
            state = .failed(url, error)
            throw error
        }
    }

    var isIdle: Bool {
        if case .idle = state { return true }
        return false
    }
}