Search code examples
swiftasync-awaitswift-concurrency

How to prevent actor reentrancy resulting in duplicative requests?


In WWDC 2021 video, Protect mutable state with Swift actors, they provide the following code snippet:

actor ImageDownloader {
    private var cache: [URL: Image] = [:]

    func image(from url: URL) async throws -> Image? {
        if let cached = cache[url] {
            return cached
        }

        let image = try await downloadImage(from: url)

        cache[url] = cache[url, default: image]

        return cache[url]
    }

    func downloadImage(from url: URL) async throws -> Image { ... }
}

The issue is that actors offer reentrancy, so cache[url, default: image] reference effectively ensures that even if you performed a duplicative request because of some race, that you at least check the actor’s cache after the continuation, ensuring that you get the same image for the duplicative request.

And in that video, they say:

A better solution would be to avoid redundant downloads entirely. We’ve put that solution in the code associated with this video.

But there is no code associated with that video on the website. So, what is the better solution?

I understand the benefits of actor reentrancy (as discussed in SE-0306). E.g., if downloading four images, one does not want to prohibit reentrancy, losing concurrency of downloads. We would, effectively, like to wait for the result of a duplicative prior request for a particular image if any, and if not, start a new downloadImage.


Solution

  • UPDATE

    Apple’s developer web site now includes the code snippets for WWDC videos (at least for 2021 and later). You can find the “better solution” code on the video’s page by tapping the “Code” tab under the video player and scrolling down to “11:59 - Check your assumptions after an await: A better solution”.

    (If you have the Developer app installed and click the link above in Safari, it will probably open the Developer app instead of visiting the web page. You can also copy the code out of the Developer app.)

    ORIGINAL

    You can find the “better solution” code in the Developer app. Open the session in the Developer app, select the Code tab, and scroll to “11:59 - Check your assumptions after an await: A better solution”.

    screen shot of Developer app

    The screen shot is from my iPad, but the Developer app is also available on iPhone, Mac, and Apple TV. (I don't know if the Apple TV version gives you a way to view and copy the code, though…)

    As far as I can tell, the code is not available on the developer.apple.com web site, either on the WWDC session's page or as part of a sample project.

    For posterity, here is Apple's code. It is extremely similar to that of Andy Ibanez:

    actor ImageDownloader {
    
        private enum CacheEntry {
            case inProgress(Task<Image, Error>)
            case ready(Image)
        }
    
        private var cache: [URL: CacheEntry] = [:]
    
        func image(from url: URL) async throws -> Image? {
            if let cached = cache[url] {
                switch cached {
                case .ready(let image):
                    return image
                case .inProgress(let task):
                    return try await task.value
                }
            }
    
            let task = Task {
                try await downloadImage(from: url)
            }
    
            cache[url] = .inProgress(task)
    
            do {
                let image = try await task.value
                cache[url] = .ready(image)
                return image
            } catch {
                cache[url] = nil
                throw error
            }
        }
    }