1
0
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:
Jesse Wilson
2020-04-11 17:52:41 -04:00
parent 67453eeb40
commit 64d3b079f2
7 changed files with 433 additions and 121 deletions

View File

@@ -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/

View File

@@ -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()
}
}

View File

@@ -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