Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Exclusive Cache Lock #8209

Open
wants to merge 33 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
8674a8b
Cache Lock
yschimke Jan 20, 2024
c3df60f
Merge branch 'master' into cache_singleton
yschimke Apr 1, 2024
166a255
Merge branch 'master' into cache_singleton
yschimke Apr 1, 2024
b02738d
Rework
yschimke Apr 1, 2024
ac0cc79
Rework
yschimke Apr 1, 2024
2525edf
Add test
yschimke Apr 1, 2024
7072358
Cleanup
yschimke Apr 1, 2024
5ca131f
Activate missed test
yschimke Apr 1, 2024
0a2fbb6
Cleanup
yschimke Apr 1, 2024
c9db9d4
Avoid checking on non system file systems
yschimke Apr 1, 2024
97e566b
Revert "Avoid checking on non system file systems"
yschimke Apr 1, 2024
ee1daa6
Avoid failing on dns tests
yschimke Apr 1, 2024
3041258
Adding a test
yschimke Apr 2, 2024
7faba3e
Fixes
yschimke Apr 2, 2024
44d3640
Test on more platforms
yschimke Apr 3, 2024
792b689
Cleanup
yschimke Apr 3, 2024
5117f46
Merge branch 'master' into cache_singleton
yschimke Apr 3, 2024
a922b1d
Cleanup
yschimke Apr 3, 2024
e24e146
Fixes
yschimke Apr 6, 2024
04c057f
Fixes
yschimke Apr 6, 2024
5c6c60d
Cache fixes
yschimke Apr 6, 2024
12b7eb9
Cache fixes
yschimke Apr 6, 2024
ae3830d
Cache fixes
yschimke Apr 6, 2024
99a5ee5
Cache fixes
yschimke Apr 6, 2024
cc97769
Cache fixes
yschimke Apr 6, 2024
6dbb03f
Fix test
yschimke Apr 6, 2024
19be22b
Merge branch 'refs/heads/master' into cache_singleton
yschimke Apr 6, 2024
feafff9
Fix test
yschimke Apr 6, 2024
72359a6
avoid windows for now
yschimke Apr 6, 2024
b76aa7f
Fix or skip on windows
yschimke Apr 7, 2024
89e9d80
cleanup
yschimke Apr 7, 2024
28025e2
skip on windows
yschimke Apr 7, 2024
fa707bc
Update DuplexTest.kt
yschimke Apr 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -859,6 +859,7 @@ class OkHttpTest {
assertEquals(setOf(OkHttpTest::class.java.name), testHandler.calls.keys)
}

@Test
fun testCachedRequest() {
enableTls()

Expand Down
Expand Up @@ -170,7 +170,7 @@ class DnsOverHttpsTest {
// 5. unsuccessful response
@Test
fun usesCache() {
val cache = Cache("cache".toPath(), (100 * 1024).toLong(), cacheFs)
val cache = Cache("cache-usesCache".toPath(), (100 * 1024).toLong(), cacheFs)
val cachedClient = bootstrapClient.newBuilder().cache(cache).build()
val cachedDns = buildLocalhost(cachedClient, false)

Expand Down Expand Up @@ -208,7 +208,7 @@ class DnsOverHttpsTest {

@Test
fun usesCacheEvenForPost() {
val cache = Cache("cache".toPath(), (100 * 1024).toLong(), cacheFs)
val cache = Cache("cache-usesCacheEvenForPost".toPath(), (100 * 1024).toLong(), cacheFs)
val cachedClient = bootstrapClient.newBuilder().cache(cache).build()
val cachedDns = buildLocalhost(cachedClient, false, post = true)
repeat(2) {
Expand Down Expand Up @@ -241,6 +241,8 @@ class DnsOverHttpsTest {
assertThat(recordedRequest.method).isEqualTo("POST")
assertThat(recordedRequest.path)
.isEqualTo("/lookup?ct")

cache.close()
}

@Test
Expand Down Expand Up @@ -282,6 +284,8 @@ class DnsOverHttpsTest {
assertThat(recordedRequest!!.method).isEqualTo("GET")
assertThat(recordedRequest.path)
.isEqualTo("/lookup?ct&dns=AAABAAABAAAAAAAABmdvb2dsZQNjb20AAAEAAQ")

cache.close()
}

private fun dnsResponse(s: String): MockResponse {
Expand Down
80 changes: 80 additions & 0 deletions okhttp/src/main/kotlin/okhttp3/internal/cache/CacheLock.kt
@@ -0,0 +1,80 @@
/*
* Copyright (C) 2024 Block, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3.internal.cache
yschimke marked this conversation as resolved.
Show resolved Hide resolved

import android.annotation.SuppressLint
import java.io.Closeable
import java.nio.channels.FileChannel
import java.nio.file.StandardOpenOption
import java.util.Collections
import okhttp3.internal.platform.Platform
import okio.FileSystem
import okio.Path
import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement

internal object CacheLock {
private val openCaches = Collections.synchronizedMap(mutableMapOf<Path, Exception>())

fun openLock(
fileSystem: FileSystem,
directory: Path,
): Closeable {
return if (fileSystem == FileSystem.SYSTEM && !Platform.isAndroid) {
yschimke marked this conversation as resolved.
Show resolved Hide resolved
fileSystemLock(inMemoryLock(directory), directory)
} else {
inMemoryLock(directory)
}
}

/**
* Create an in-memory lock, avoiding two open Cache instances.
*/
@SuppressLint("NewApi")
@IgnoreJRERequirement // D8 supports put if absent
fun inMemoryLock(directory: Path): Closeable {
val existing = openCaches.putIfAbsent(directory, Exception("CacheLock($directory)"))
yschimke marked this conversation as resolved.
Show resolved Hide resolved
if (existing != null) {
throw IllegalStateException("Cache already open at '$directory' in same process", existing)
}
return okio.Closeable {
openCaches.remove(directory)
}
}

/**
* Create a file system lock, that excludes other processes. However within the process a
* memory lock is also needed, since locks don't work within a single process.
*/
@SuppressLint("NewApi")
@IgnoreJRERequirement // only called on JVM
fun fileSystemLock(
memoryLock: Closeable,
directory: Path,
): Closeable {
val lockFile = directory / "lock"
lockFile.toFile().createNewFile()
val channel = FileChannel.open(lockFile.toNioPath(), StandardOpenOption.APPEND)

checkNotNull(channel.tryLock()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m anxious that this is going to fail for reasons related to the host file system’s lack of support, and not because the file is positively locked.

Would it be horrible to probe the file system if a lock fails to see if EVERY lock is going to fail? Maybe we attempt to lock a file named with a random token or a timestamp? If that also fails we know it’s the file system’s fault?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A big +1 to okio solving this in a "Swanky" new API

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But I'm not blocked on that.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll need to think more about this case.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made an attempt. Let me know if it's suitable.

"Cache already open at '$directory' in another process"
}

return okio.Closeable {
memoryLock.close()
channel.close()
}
}
}
7 changes: 7 additions & 0 deletions okhttp/src/main/kotlin/okhttp3/internal/cache/DiskLruCache.kt
Expand Up @@ -20,6 +20,7 @@ import java.io.EOFException
import java.io.Flushable
import java.io.IOException
import okhttp3.internal.assertThreadHoldsLock
import okhttp3.internal.cache.CacheLock.openLock
import okhttp3.internal.cache.DiskLruCache.Editor
import okhttp3.internal.closeQuietly
import okhttp3.internal.concurrent.Task
Expand Down Expand Up @@ -95,6 +96,8 @@ class DiskLruCache(
/** Used for asynchronous journal rebuilds. */
taskRunner: TaskRunner,
) : Closeable, Flushable {
lateinit var cacheLock: Closeable

internal val fileSystem: FileSystem =
object : ForwardingFileSystem(fileSystem) {
override fun sink(
Expand Down Expand Up @@ -242,6 +245,8 @@ class DiskLruCache(

civilizedFileSystem = fileSystem.isCivilized(journalFileBackup)

cacheLock = openLock(fileSystem, directory)
yschimke marked this conversation as resolved.
Show resolved Hide resolved

// Prefer to pick up where we left off.
if (fileSystem.exists(journalFile)) {
try {
Expand Down Expand Up @@ -705,6 +710,8 @@ class DiskLruCache(
return
}

cacheLock.close()

// Copying for concurrent iteration.
for (entry in lruEntries.values.toTypedArray()) {
if (entry.currentEditor != null) {
Expand Down
82 changes: 82 additions & 0 deletions okhttp/src/test/java/okhttp3/CacheLockTest.kt
@@ -0,0 +1,82 @@
/*
* Copyright (C) 2024 Block, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3

import assertk.assertThat
import assertk.assertions.isEqualTo
import java.nio.file.Path
import okio.Closeable
import okio.FileSystem
import okio.Path.Companion.toOkioPath
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.junit.jupiter.api.io.TempDir

class CacheLockTest {
yschimke marked this conversation as resolved.
Show resolved Hide resolved
private lateinit var tempDir: okio.Path
private val toClose = mutableListOf<Closeable>()

@BeforeEach
fun setup(
@TempDir tempDir: Path,
) {
this.tempDir = tempDir.toOkioPath()
}

@AfterEach
fun cleanup() {
toClose.forEach {
it.close()
}
}

@Test
fun testCacheLock() {
openCache(tempDir)

val ioe =
assertThrows<IllegalStateException> {
openCache(tempDir)
}
assertThat(ioe.message).isEqualTo("Cache already open at '$tempDir' in same process")
}

@Test
fun testCacheLockAfterClose() {
val cache1 = openCache(tempDir)

cache1.close()

openCache(tempDir)
}

@Test
fun testCacheLockDifferentPath() {
openCache(tempDir / "a")

openCache(tempDir / "b")
}

private fun openCache(directory: okio.Path): Cache {
return Cache(directory, 10_000, FileSystem.SYSTEM).apply {
// force early LRU initialisation
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m a bit torn on this. You don’t get a failure immediately; you get it eventually.

This is probably fine; this new feature is advisory.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this currently works until it doesn't. Then it ends up as 40+ bugs raised.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't really do IO operations on the main thread, so this should always be the case.

Which reminds me, for SSL init on main thread #8248

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with ‘We can’t really do I/O operations on the main thread’, but I don’t agree that OkHttpClient is being initialized on the main thread. We’re an I/O library!

initialize()
toClose.add(this)
}
}
}