forked from quarkusio/quarkus
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Replace read/write lock in JarResource to avoid virtual threads pinning
- Loading branch information
1 parent
c2c55b4
commit cfe6030
Showing
2 changed files
with
182 additions
and
92 deletions.
There are no files selected for viewing
143 changes: 143 additions & 0 deletions
143
...projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/JarFileReference.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
package io.quarkus.bootstrap.runner; | ||
|
||
import java.io.IOException; | ||
import java.nio.file.Path; | ||
import java.util.concurrent.atomic.AtomicInteger; | ||
import java.util.concurrent.atomic.AtomicReference; | ||
import java.util.jar.JarFile; | ||
|
||
import io.smallrye.common.io.jar.JarFiles; | ||
|
||
public class JarFileReference { | ||
// Guarded by an atomic reader counter that emulate the behaviour of a read/write lock. | ||
// To enable virtual threads compatibility and avoid pinning it is not possible to use an explicit read/write lock | ||
// because the jarFile access may happen inside a native call (for example triggered by the RunnerClassLoader) | ||
// and then it is necessary to avoid blocking on it. | ||
private final JarFile jarFile; | ||
|
||
private final AtomicReference<JarFileReference> jarFileReference; | ||
|
||
// The referenceCounter - 1 represents the number of effective readers (#aqcuire - #release), while the first | ||
// reference is used to determine if a close has been required. | ||
// The referenceCounter starts from 2 because a create always implies also a first acquire. | ||
private final AtomicInteger referenceCounter = new AtomicInteger(2); | ||
|
||
private JarFileReference(AtomicReference<JarFileReference> jarFileReference, JarFile jarFile) { | ||
this.jarFileReference = jarFileReference; | ||
this.jarFile = jarFile; | ||
} | ||
|
||
/** | ||
* Increase the readers counter of the jarFile. | ||
* | ||
* @return true if the acquire succeeded: it's now safe to access and use the inner jarFile. | ||
* false if the jar reference is going to be closed and then no longer usable. | ||
*/ | ||
private boolean acquire() { | ||
while (true) { | ||
int count = referenceCounter.get(); | ||
if (count == 0) { | ||
return false; | ||
} | ||
if (referenceCounter.compareAndSet(count, count + 1)) { | ||
return true; | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Decrease the readers counter of the jarFile. | ||
* If the counter drops to 0 and a release has been requested also closes the jarFile. | ||
* | ||
* @return true if the release also closes the underlying jarFile. | ||
*/ | ||
private boolean release() { | ||
while (true) { | ||
int count = referenceCounter.get(); | ||
if (count <= 0) { | ||
throw new IllegalStateException( | ||
"The reference counter cannot be negative, found: " + (referenceCounter.get() - 1)); | ||
} | ||
if (referenceCounter.compareAndSet(count, count - 1)) { | ||
if (count == 1) { | ||
try { | ||
jarFile.close(); | ||
} catch (IOException e) { | ||
// ignore | ||
} finally { | ||
jarFileReference.compareAndSet(this, null); | ||
} | ||
return true; | ||
} | ||
return false; | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Ask to close this reference. | ||
* If there are no readers currently accessing the jarFile also close it, otherwise defer the closing when the last reader | ||
* will leave. | ||
*/ | ||
void close() { | ||
release(); | ||
} | ||
|
||
@FunctionalInterface | ||
interface JarFileConsumer<T> { | ||
T apply(JarFile jarFile, Path jarPath, String resource); | ||
} | ||
|
||
static <T> T withJarFile(AtomicReference<JarFileReference> jarFileReference, Path jarPath, String resource, | ||
JarFileConsumer<T> fileConsumer) { | ||
|
||
// Happy path: the jar reference already exists and it's ready to be used | ||
final var localJarFileRef = jarFileReference.get(); | ||
if (localJarFileRef != null && localJarFileRef.acquire()) { | ||
return consumeSharedJarFile(jarPath, resource, fileConsumer, localJarFileRef); | ||
} | ||
|
||
// There's no valid jar reference, so load a new one | ||
final var newJarFileRef = JarFileReference.loadAcquiredJarFile(jarFileReference, jarPath); | ||
if (jarFileReference.compareAndSet(localJarFileRef, newJarFileRef) || | ||
jarFileReference.compareAndSet(null, newJarFileRef)) { | ||
// The new file reference has been successfully published and can be used by the current and other threads | ||
return consumeSharedJarFile(jarPath, resource, fileConsumer, newJarFileRef); | ||
} | ||
|
||
// The newly created file reference hasn't been published so it can be used exclusively by the current thread | ||
return consumeUnsharedJarFile(jarFileReference, jarPath, resource, fileConsumer, newJarFileRef); | ||
} | ||
|
||
private static <T> T consumeSharedJarFile(Path jarPath, String resource, JarFileConsumer<T> fileConsumer, | ||
JarFileReference jarFileRef) { | ||
try { | ||
return fileConsumer.apply(jarFileRef.jarFile, jarPath, resource); | ||
} finally { | ||
jarFileRef.release(); | ||
} | ||
} | ||
|
||
private static <T> T consumeUnsharedJarFile(AtomicReference<JarFileReference> jarFileReference, Path jarPath, | ||
String resource, JarFileConsumer<T> fileConsumer, JarFileReference jarFileRef) { | ||
try { | ||
return fileConsumer.apply(jarFileRef.jarFile, jarPath, resource); | ||
} finally { | ||
boolean closed = jarFileRef.release(); | ||
assert !closed; | ||
// Check one last time if the file reference can be published and reused by other threads, otherwise close it | ||
if (!jarFileReference.compareAndSet(null, jarFileRef)) { | ||
closed = jarFileRef.release(); | ||
assert closed; | ||
} | ||
} | ||
} | ||
|
||
private static JarFileReference loadAcquiredJarFile(AtomicReference<JarFileReference> jarFileReference, Path jarPath) { | ||
try { | ||
return new JarFileReference(jarFileReference, JarFiles.create(jarPath.toFile())); | ||
} catch (IOException e) { | ||
throw new RuntimeException("Failed to open " + jarPath, e); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters