mirror of
https://github.com/square/okhttp.git
synced 2025-11-24 18:41:06 +03:00
Fix DiskLruCache to work on Windows
As originally designed DiskLruCache assumes an inode-like file system, where it's fine to delete files that are currently being read or written. On Windows the file system forbids this, so we must be more careful when deleting and renaming files. These operations come up a lot internally in the cache: - deleting to evict an entry - renaming to commit a dirty file to a clean file The workaround is simple if unsatisfying: we don't permit concurrent reads and writes on Windows. We can have multiple concurrent reders, or a single writer. One challenge in this implementation is detecting whether we're running on Windows or a good operating system. We deliberately don't look at System properties here because the OS and file system may disagree, such as when a Windows machine has an ext4 partition, or when a Linux machine has an NTFS partition. Instead of detecting we just attempt an edit and see what happens. Another challenge in this implementation is what to do when a file needs to be deleted but cannot be because it is currently open. In such cases we now mark the cache entry as a 'zombie'. When the files are later closed they now check for zombie status and delete the files if necessary. Note that it is not possible to store a new cache entry while it is a zombie. Closes: https://github.com/square/okhttp/issues/5761
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
package okhttp3
|
||||
|
||||
import java.io.File
|
||||
import java.net.InetAddress
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.UnknownHostException
|
||||
@@ -42,6 +43,12 @@ object TestUtil {
|
||||
return String(array)
|
||||
}
|
||||
|
||||
tailrec fun File.isDescendentOf(directory: File): Boolean {
|
||||
val parentFile = parentFile ?: return false
|
||||
if (parentFile == directory) return true
|
||||
return parentFile.isDescendentOf(directory)
|
||||
}
|
||||
|
||||
/**
|
||||
* See FinalizationTester for discussion on how to best trigger GC in tests.
|
||||
* https://android.googlesource.com/platform/libcore/+/master/support/src/test/java/libcore/
|
||||
|
||||
@@ -19,6 +19,7 @@ import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
import java.util.IdentityHashMap
|
||||
import okhttp3.TestUtil.isDescendentOf
|
||||
import okio.Buffer
|
||||
import okio.ForwardingSink
|
||||
import okio.ForwardingSource
|
||||
@@ -30,16 +31,13 @@ import org.junit.runners.model.Statement
|
||||
|
||||
/** A simple file system where all files are held in memory. Not safe for concurrent use. */
|
||||
class InMemoryFileSystem : FileSystem, TestRule {
|
||||
private val files: MutableMap<File, Buffer> = mutableMapOf()
|
||||
private val openSources: MutableMap<Source, File> = IdentityHashMap()
|
||||
private val openSinks: MutableMap<Sink, File> = IdentityHashMap()
|
||||
private val files = mutableMapOf<File, Buffer>()
|
||||
private val openSources = IdentityHashMap<Source, File>()
|
||||
private val openSinks = IdentityHashMap<Sink, File>()
|
||||
|
||||
override fun apply(
|
||||
base: Statement,
|
||||
description: Description
|
||||
): Statement {
|
||||
override fun apply(base: Statement, description: Description): Statement {
|
||||
return object : Statement() {
|
||||
@Throws(Throwable::class) override fun evaluate() {
|
||||
override fun evaluate() {
|
||||
base.evaluate()
|
||||
ensureResourcesClosed()
|
||||
}
|
||||
@@ -47,32 +45,25 @@ class InMemoryFileSystem : FileSystem, TestRule {
|
||||
}
|
||||
|
||||
fun ensureResourcesClosed() {
|
||||
val openResources: MutableList<String> = mutableListOf()
|
||||
val openResources = mutableListOf<String>()
|
||||
for (file in openSources.values) {
|
||||
openResources.add("Source for $file")
|
||||
}
|
||||
for (file in openSinks.values) {
|
||||
openResources.add("Sink for $file")
|
||||
}
|
||||
if (!openResources.isEmpty()) {
|
||||
val builder =
|
||||
StringBuilder("Resources acquired but not closed:")
|
||||
for (resource in openResources) {
|
||||
builder.append("\n * ")
|
||||
.append(resource)
|
||||
}
|
||||
throw IllegalStateException(builder.toString())
|
||||
check(openResources.isEmpty()) {
|
||||
"Resources acquired but not closed:\n * ${openResources.joinToString(separator = "\n * ")}"
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(
|
||||
FileNotFoundException::class
|
||||
) override fun source(file: File): Source {
|
||||
@Throws(FileNotFoundException::class)
|
||||
override fun source(file: File): Source {
|
||||
val result = files[file] ?: throw FileNotFoundException()
|
||||
val source: Source = result.clone()
|
||||
openSources[source] = file
|
||||
return object : ForwardingSource(source) {
|
||||
@Throws(IOException::class) override fun close() {
|
||||
override fun close() {
|
||||
openSources.remove(source)
|
||||
super.close()
|
||||
}
|
||||
@@ -80,20 +71,12 @@ class InMemoryFileSystem : FileSystem, TestRule {
|
||||
}
|
||||
|
||||
@Throws(FileNotFoundException::class)
|
||||
override fun sink(file: File): Sink {
|
||||
return sink(file, false)
|
||||
}
|
||||
override fun sink(file: File) = sink(file, false)
|
||||
|
||||
@Throws(
|
||||
FileNotFoundException::class
|
||||
) override fun appendingSink(file: File): Sink {
|
||||
return sink(file, true)
|
||||
}
|
||||
@Throws(FileNotFoundException::class)
|
||||
override fun appendingSink(file: File) = sink(file, true)
|
||||
|
||||
private fun sink(
|
||||
file: File,
|
||||
appending: Boolean
|
||||
): Sink {
|
||||
private fun sink(file: File, appending: Boolean): Sink {
|
||||
var result: Buffer? = null
|
||||
if (appending) {
|
||||
result = files[file]
|
||||
@@ -105,7 +88,7 @@ class InMemoryFileSystem : FileSystem, TestRule {
|
||||
val sink: Sink = result
|
||||
openSinks[sink] = file
|
||||
return object : ForwardingSink(sink) {
|
||||
@Throws(IOException::class) override fun close() {
|
||||
override fun close() {
|
||||
openSinks.remove(sink)
|
||||
super.close()
|
||||
}
|
||||
@@ -117,33 +100,21 @@ class InMemoryFileSystem : FileSystem, TestRule {
|
||||
files.remove(file)
|
||||
}
|
||||
|
||||
override fun exists(file: File): Boolean {
|
||||
return files.containsKey(file)
|
||||
override fun exists(file: File) = files.containsKey(file)
|
||||
|
||||
override fun size(file: File) = files[file]?.size ?: 0L
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun rename(from: File, to: File) {
|
||||
files[to] = files.remove(from) ?: throw FileNotFoundException()
|
||||
}
|
||||
|
||||
override fun size(file: File): Long {
|
||||
val buffer = files[file]
|
||||
return buffer?.size ?: 0L
|
||||
}
|
||||
|
||||
@Throws(IOException::class) override fun rename(
|
||||
from: File,
|
||||
to: File
|
||||
) {
|
||||
val buffer = files.remove(from) ?: throw FileNotFoundException()
|
||||
files[to] = buffer
|
||||
}
|
||||
|
||||
@Throws(
|
||||
IOException::class
|
||||
) override fun deleteContents(directory: File) {
|
||||
val prefix = "$directory/"
|
||||
@Throws(IOException::class)
|
||||
override fun deleteContents(directory: File) {
|
||||
val i = files.keys.iterator()
|
||||
while (i.hasNext()) {
|
||||
val file = i.next()
|
||||
if (file.toString()
|
||||
.startsWith(prefix)
|
||||
) i.remove()
|
||||
if (file.isDescendentOf(directory)) i.remove()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ package okhttp3.internal.io
|
||||
|
||||
import java.io.File
|
||||
import java.util.Collections
|
||||
import okhttp3.TestUtil.isDescendentOf
|
||||
import okio.ForwardingSink
|
||||
import okio.ForwardingSource
|
||||
import okio.IOException
|
||||
@@ -65,12 +66,6 @@ class WindowsFileSystem(val delegate: FileSystem) : FileSystem {
|
||||
delegate.deleteContents(directory)
|
||||
}
|
||||
|
||||
private tailrec fun File.isDescendentOf(directory: File): Boolean {
|
||||
val parentFile = parentFile ?: return false
|
||||
if (parentFile == directory) return true
|
||||
return parentFile.isDescendentOf(directory)
|
||||
}
|
||||
|
||||
private inner class FileSink(val file: File, delegate: Sink) : ForwardingSink(delegate) {
|
||||
var closed = false
|
||||
|
||||
|
||||
Reference in New Issue
Block a user