-
Notifications
You must be signed in to change notification settings - Fork 9.3k
[Blocked] Exclusive Cache Lock #8209
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
base: master
Are you sure you want to change the base?
Changes from 12 commits
8674a8b
c3df60f
166a255
b02738d
ac0cc79
2525edf
7072358
5ca131f
0a2fbb6
c9db9d4
97e566b
ee1daa6
3041258
7faba3e
44d3640
792b689
5117f46
a922b1d
e24e146
04c057f
5c6c60d
12b7eb9
ae3830d
99a5ee5
cc97769
6dbb03f
19be22b
feafff9
72359a6
b76aa7f
89e9d80
28025e2
fa707bc
c5a87f0
bb0d4b0
192e93b
b4115b2
2f959af
7af5705
4e9d4b3
019560c
433f6f7
b650c9a
f9479e8
498ec0c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
|
|
||
| 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.
Outdated
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.
Outdated
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()) { | ||
|
||
| "Cache already open at '$directory' in another process" | ||
| } | ||
|
|
||
| return okio.Closeable { | ||
| memoryLock.close() | ||
| channel.close() | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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.
Outdated
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 | ||
|
||
| initialize() | ||
| toClose.add(this) | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.