-
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 14 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,81 @@ | ||
| /* | ||
| * 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 (!Platform.isAndroid) { | ||
|
yschimke marked this conversation as resolved.
Outdated
|
||
| 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
|
||
| 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" | ||
| println("Locking $lockFile") | ||
|
yschimke marked this conversation as resolved.
Outdated
|
||
| lockFile.toFile().createNewFile() | ||
| val channel = FileChannel.open(lockFile.toNioPath(), StandardOpenOption.APPEND) | ||
|
|
||
| checkNotNull(channel.tryLock()) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A big +1 to okio solving this in a "Swanky" new API
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But I'm not blocked on that.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll need to think more about this case.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Made an attempt. Let me know if it's suitable.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, even if Cache remains unsafe on older devices, having it fail obviously on recent devices will make sure you use it safely, improving all devices. |
||
| "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,126 @@ | ||
| /* | ||
| * 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 okhttp3.testing.PlatformRule | ||
| import okhttp3.testing.PlatformVersion | ||
| import okio.Closeable | ||
| import okio.FileSystem | ||
| import okio.Path.Companion.toOkioPath | ||
| import okio.Path.Companion.toPath | ||
| 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.extension.RegisterExtension | ||
| import org.junit.jupiter.api.io.TempDir | ||
|
|
||
| @org.junit.jupiter.api.parallel.Isolated | ||
|
yschimke marked this conversation as resolved.
Outdated
|
||
| class CacheLockTest { | ||
|
yschimke marked this conversation as resolved.
Outdated
|
||
| @JvmField | ||
| @RegisterExtension | ||
| val platform = PlatformRule() | ||
|
|
||
| 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") | ||
| } | ||
|
|
||
| @Test | ||
| fun testCacheLockDifferentProcess() { | ||
| val lockFile = tempDir / "lock" | ||
| lockFile.toFile().createNewFile() | ||
|
|
||
| val javaExe = if (PlatformVersion.majorVersion >= 9) { | ||
| @Suppress("Since15") | ||
| ProcessHandle.current().info().command().get().toPath() | ||
| } else { | ||
| System.getenv("JAVA_HOME").toPath() / "bin/java" | ||
| } | ||
|
|
||
| assertThat(FileSystem.SYSTEM.exists(javaExe)) | ||
|
yschimke marked this conversation as resolved.
Outdated
|
||
|
|
||
| val process = | ||
| ProcessBuilder().command(javaExe.toString(), "src/test/java/okhttp3/LockTestProgram.java", (lockFile.toString())) | ||
|
yschimke marked this conversation as resolved.
Outdated
|
||
| .redirectErrorStream(true) | ||
| .start() | ||
|
|
||
| val output = process.inputStream.bufferedReader() | ||
|
|
||
| try { | ||
| assertThat(output.readLine()).isEqualTo("Locking $lockFile") | ||
| assertThat(output.readLine()).isEqualTo("Locked $lockFile") | ||
|
|
||
| val ioe = | ||
| assertThrows<IllegalStateException> { | ||
| openCache(tempDir) | ||
| } | ||
| assertThat(ioe.message).isEqualTo("Cache already open at '$tempDir' in another process") | ||
| } finally { | ||
| process.destroy() | ||
| } | ||
| } | ||
|
|
||
| private fun openCache(directory: okio.Path): Cache { | ||
| return Cache(directory, 10_000, FileSystem.SYSTEM).apply { | ||
| // force early LRU initialisation | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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!
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the dominant DI libraries will all be initializing OkHttp on the main thread. You have to do extra work to avoid that. |
||
| initialize() | ||
| toClose.add(this) | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| package okhttp3; | ||
|
yschimke marked this conversation as resolved.
Outdated
|
||
|
|
||
| import java.io.File; | ||
| import java.io.IOException; | ||
| import java.nio.channels.FileChannel; | ||
| import java.nio.file.StandardOpenOption; | ||
|
|
||
| public class LockTestProgram { | ||
| public static void main(String[] args) throws IOException { | ||
| File lockFile = new File(args[0]); | ||
|
|
||
| System.out.println("Locking " + lockFile); | ||
|
|
||
| FileChannel channel = FileChannel.open(lockFile.toPath(), StandardOpenOption.APPEND); | ||
|
|
||
| channel.lock(); | ||
|
|
||
| System.out.println("Locked " + lockFile); | ||
|
|
||
| System.in.read(); | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.