Repository: ktorio/ktor
File: ktor-client/ktor-client-core/jvm/src/io/ktor/client/plugins/cache/storage/FileCacheStorage.kt (Kotlin)

Code before:

    init {
        directory.mkdirs()
    }

    override suspend fun store(url: Url, data: CachedResponseData): Unit = withContext(dispatcher) {
        val urlHex = key(url)
        val caches = readCache(urlHex).filterNot { it.varyKeys == data.varyKeys } + data
        writeCache(urlHex, caches)
    }

    override suspend fun findAll(url: Url): Set<CachedResponseData> {
        return readCache(key(url)).toSet()
    }

    override suspend fun find(url: Url, varyKeys: Map<String, String>): CachedResponseData? {
        val data = readCache(key(url))
        return data.find {
            varyKeys.all { (key, value) -> it.varyKeys[key] == value } && varyKeys.size == it.varyKeys.size
        }
    }

    override suspend fun remove(url: Url, varyKeys: Map<String, String>) {
        val urlHex = key(url)
        val caches = readCache(urlHex).filterNot { it.varyKeys == varyKeys }
        writeCache(urlHex, caches)
    }

    override suspend fun removeAll(url: Url) {
        val urlHex = key(url)
        deleteCache(urlHex)
    }

    private fun key(url: Url) = hex(MessageDigest.getInstance("SHA-256").digest(url.toString().encodeToByteArray()))

    private suspend fun writeCache(urlHex: String, caches: List<CachedResponseData>) = coroutineScope {
        val mutex = mutexes.computeIfAbsent(urlHex) { Mutex() }
        mutex.withLock {
            val channel = ByteChannel()
            try {
                File(directory, urlHex).outputStream().buffered().use { output ->
                    launch {
                        channel.writeInt(caches.size)
                        for (cache in caches) {
                            writeCache(channel, cache)
                        }
                        channel.close()
                    }
                    channel.copyTo(output)
                }
            } catch (cause: Exception) {

Review comment (bug):
This is not an atomic change so we can get race condition here. The simplest solution is to extract `readCacheUnsafe(...)` from `readCache(...)` and `writeCacheUnsafe(...)` from `writeCache(...)` and then use them behind a single lock. Maybe @e5l knows better solution.

```kotlin
private fun readCache(urlHex: String): Set<CachedResponseData> {
    val mutex = mutexes.computeIfAbsent(urlHex) { Mutex() }
    return mutex.withLock { readCacheUnsafe(urlHex) }
}

private fun writeCache(urlHex: String, caches: Set<CachedResponseData>)  {
    val mutex = mutexes.computeIfAbsent(urlHex) { Mutex() }
    mutex.withLock { writeCacheUnsafe(urlHex, caches) }
}

// New function, reading and writing behind a single mutex
private inline fun updateCache(urlHex: String, transform: (Set<CachedResponseData>) -> Set<CachedResponseData>) {
    val mutex = mutexes.computeIfAbsent(urlHex) { Mutex() }
    mutex.withLock {
        val caches = readCacheUnsafe(urlHex)
        writeCacheUnsafe(urlHex, transform(caches))
    }
}
```

Code after:
    private val mutexes = ConcurrentMap<String, Mutex>()

    init {
        directory.mkdirs()
    }

    override suspend fun store(url: Url, data: CachedResponseData): Unit = withContext(dispatcher) {
        val urlHex = key(url)
        updateCache(urlHex) { caches ->
            caches.filterNot { it.varyKeys == data.varyKeys } + data
        }
    }

    override suspend fun findAll(url: Url): Set<CachedResponseData> {
        return readCache(key(url)).toSet()
    }

    override suspend fun find(url: Url, varyKeys: Map<String, String>): CachedResponseData? {
        val data = readCache(key(url))
        return data.find {
            varyKeys.all { (key, value) -> it.varyKeys[key] == value }
        }
    }

    override suspend fun remove(url: Url, varyKeys: Map<String, String>) {
        val urlHex = key(url)
        updateCache(urlHex) { caches ->
            caches.filterNot { it.varyKeys == varyKeys }
        }
    }

    override suspend fun removeAll(url: Url) {
        val urlHex = key(url)
        deleteCache(urlHex)
    }

    private fun key(url: Url) = hex(MessageDigest.getInstance("SHA-256").digest(url.toString().encodeToByteArray()))

    private suspend fun readCache(urlHex: String): Set<CachedResponseData> {
        val mutex = mutexes.computeIfAbsent(urlHex) { Mutex() }
        return mutex.withLock { readCacheUnsafe(urlHex) }
    }

    private suspend inline fun updateCache(
        urlHex: String,
        transform: (Set<CachedResponseData>) -> List<CachedResponseData>
    ) {
        val mutex = mutexes.computeIfAbsent(urlHex) { Mutex() }
        mutex.withLock {
            val caches = readCacheUnsafe(urlHex)
            writeCacheUnsafe(urlHex, transform(caches))