I use URLCache to not implement caching myself; it does everything I need out of the box, through I admit my caching needs are pretty basic: essentially a key / data store with ability to specify total size, with persistence & encryption, cleaned up by OS either partially or completely if OS needs disk or RAM. If I were to implement caching myself (which I can) that would be quite some chunk of work I'd rather not do, testing and handling edge cases (like sanitising and deleting cache after app crash that left cache in an inconsistent state). Here I follow the "Best code is no code" strategy (or, like in this case the well tested code that is maintained by the system itself).
Your example with "waste" + "override" is understandable, although not a show stopper. If I read the warning sentence above properly (which I am not sure I do!) that "concurrent read/write calls themselves to the same resource can crash" (similar to how unprotected read / modify of a dictionary can crash), then this is what I plan doing:
extension URLCache {
static let lock = NSLock()
func storeCachedResponse_mt(_ response: CachedURLResponse, for request: URLRequest) {
Self.lock.lock()
defer { Self.lock.unlock() }
storeCachedResponse(response, for: request)
}
func cachedResponse_mt(for request: URLRequest) {
Self.lock.lock()
defer { Self.lock.unlock() }
return cachedResponse(for: request)
}
func removeCachedResponse_mt(for request: URLRequest) {
Self.lock.lock()
defer { Self.lock.unlock() }
removeCachedResponse(for: request)
}
}
Does it make sense? Under what circumstances can this deadlock?
I also found some strange behaviour in a single threaded scenario:
removeCachedResponse(for: request)
cachedResponse(for: request) // still returns data sometimes!
This is a single threaded case when no-one else is writing the cache. ditto for "set + get" with get sometimes returning the old value. It feels like "remove"/"set" is somewhat asynchronous.. There is nothing in the docs abut this behaviour. I put some workaround but I am not totally happy about it.
Cache reservation you are talking about is probably doable via (pseudocode):
func read() {
lock()
result = get()
if result == nil {
set(loadingMarker)
unlock()
loadResource() { data, error in
if not error {
lock()
set(data)
unlock()
} else {
lock()
set(nil) // or set(error)
unlock()
}
}
}
unlock()
}
where loading / error markers can be stored as a userInfo attribute of a cached entry.