/*:#localized(key: "Reentrancy")
 # Reentrancy: the subtle part

 Here's where actors surprise people. An actor guarantees one task inside at a
 time, but "at a time" has a precise meaning. Whenever an actor method hits an
 `await` and **suspends**, it *steps out of the actor* while it waits. The
 actor is now free, so **another task can enter**. This is called
 **reentrancy**: the actor can be re-entered during a suspension.

 The consequence is sharp: **any value you read before an `await` may have
 changed by the time you resume.** State does not stay frozen across a
 suspension.

 Watch it cause a real bug. This vault checks the balance, does an async step
 mid-transaction (imagine contacting an approval service), then deducts.
 */
//#-editable-code
import Foundation
import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

actor Vault {
    private(set) var balance: Int

    init(balance: Int) {
        self.balance = balance
    }

    func withdraw(_ amount: Int, by name: String) async -> Bool {
        guard balance >= amount else {
            print("\(name): denied (only \(balance) available)")
            return false
        }

        // An async step in the middle of the transaction. We use a full
        // second so BOTH tellers provably reach this suspension (and clear
        // the guard above) before either resumes to deduct.
        try? await Task.sleep(for: .seconds(1))
        balance -= amount
        print("\(name): withdrew \(amount), balance now \(balance)")
        return true
    }
}

let vault = Vault(balance: 100)

Task {
    async let a = vault.withdraw(80, by: "Teller A")
    async let b = vault.withdraw(80, by: "Teller B")
    _ = await (a, b)
    print("Final balance: \(await vault.balance)")
    PlaygroundPage.current.finishExecution()
}
//#-end-editable-code
/*:#localized(key: "Reentrancy-2")
 **Watch it run:** both tellers report success, and the final balance is
 **`-60`**: the account is overdrawn, even though each withdrawal carefully
 checked that enough money was there. Here's the sequence:

 1. Teller A enters, checks `100 >= 80` (passes), then hits `await` and
 **suspends**, releasing the actor.
 2. With the actor free, Teller B enters, checks `100 >= 80` (passes too;
 A hasn't deducted yet!), then hits `await` and suspends as well.
 3. Both wake up and deduct: `100 - 80 = 20`, then `20 - 80 = -60`.

 The guard was true for both because the balance didn't change *while either
 was suspended*, but it changed *between the check and the deduction*. That
 gap is the `await`. The check-then-act was not atomic after all.

 * callout(Try it): Change the sleep to `.seconds(2)`; the overdraft still
 happens. Longer waiting isn't the fix; closing the gap across the `await` is.

 [Previous](@previous)  |  page 4 of 7  |  [Next: Guarding Against Reentrancy](@next)
 */
