Skip to content

Commit c3baf93

Browse files
mariofuscogsmet
authored andcommitted
Revert "Replace read/write lock in JarResource to avoid virtual threads pinning"
This reverts commit ff4b32b. (cherry picked from commit 090d0e6)
1 parent f459f72 commit c3baf93

File tree

4 files changed

+127
-330
lines changed

4 files changed

+127
-330
lines changed

independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/JarFileReference.java

Lines changed: 0 additions & 172 deletions
This file was deleted.

independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/JarResource.java

Lines changed: 94 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -11,28 +11,44 @@
1111
import java.security.ProtectionDomain;
1212
import java.security.cert.Certificate;
1313
import java.util.Objects;
14-
import java.util.concurrent.CompletableFuture;
15-
import java.util.concurrent.atomic.AtomicReference;
14+
import java.util.concurrent.locks.Lock;
15+
import java.util.concurrent.locks.ReadWriteLock;
16+
import java.util.concurrent.locks.ReentrantReadWriteLock;
1617
import java.util.jar.JarEntry;
1718
import java.util.jar.JarFile;
1819
import java.util.zip.ZipEntry;
20+
import java.util.zip.ZipFile;
1921

2022
import io.smallrye.common.io.jar.JarEntries;
23+
import io.smallrye.common.io.jar.JarFiles;
2124

2225
/**
2326
* A jar resource
2427
*/
2528
public class JarResource implements ClassLoadingResource {
2629

27-
private volatile ProtectionDomain protectionDomain;
2830
private final ManifestInfo manifestInfo;
31+
private final Path jarPath;
32+
33+
private final Lock readLock;
34+
private final Lock writeLock;
35+
36+
private volatile ProtectionDomain protectionDomain;
2937

30-
final Path jarPath;
31-
final AtomicReference<CompletableFuture<JarFileReference>> jarFileReference = new AtomicReference<>();
38+
//Guarded by the read/write lock; open/close operations on the JarFile require the exclusive lock,
39+
//while using an existing open reference can use the shared lock.
40+
//If a lock is acquired, and as long as it's owned, we ensure that the zipFile reference
41+
//points to an open JarFile instance, and read operations are valid.
42+
//To close the jar, the exclusive lock must be owned, and reference will be set to null before releasing it.
43+
//Likewise, opening a JarFile requires the exclusive lock.
44+
private volatile JarFile zipFile;
3245

3346
public JarResource(ManifestInfo manifestInfo, Path jarPath) {
3447
this.manifestInfo = manifestInfo;
3548
this.jarPath = jarPath;
49+
final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
50+
this.readLock = readWriteLock.readLock();
51+
this.writeLock = readWriteLock.writeLock();
3652
}
3753

3854
@Override
@@ -53,48 +69,38 @@ public void init() {
5369

5470
@Override
5571
public byte[] getResourceData(String resource) {
56-
return JarFileReference.withJarFile(this, resource, JarResourceDataProvider.INSTANCE);
57-
}
58-
59-
private static class JarResourceDataProvider implements JarFileReference.JarFileConsumer<byte[]> {
60-
private static final JarResourceDataProvider INSTANCE = new JarResourceDataProvider();
61-
62-
@Override
63-
public byte[] apply(JarFile jarFile, Path path, String res) {
64-
ZipEntry entry = jarFile.getEntry(res);
72+
final ZipFile zipFile = readLockAcquireAndGetJarReference();
73+
try {
74+
ZipEntry entry = zipFile.getEntry(resource);
6575
if (entry == null) {
6676
return null;
6777
}
68-
try (InputStream is = jarFile.getInputStream(entry)) {
78+
try (InputStream is = zipFile.getInputStream(entry)) {
6979
byte[] data = new byte[(int) entry.getSize()];
7080
int pos = 0;
7181
int rem = data.length;
7282
while (rem > 0) {
7383
int read = is.read(data, pos, rem);
7484
if (read == -1) {
75-
throw new RuntimeException("Failed to read all data for " + res);
85+
throw new RuntimeException("Failed to read all data for " + resource);
7686
}
7787
pos += read;
7888
rem -= read;
7989
}
8090
return data;
8191
} catch (IOException e) {
82-
throw new RuntimeException("Failed to read zip entry " + res, e);
92+
throw new RuntimeException("Failed to read zip entry " + resource, e);
8393
}
94+
} finally {
95+
readLock.unlock();
8496
}
8597
}
8698

8799
@Override
88100
public URL getResourceURL(String resource) {
89-
return JarFileReference.withJarFile(this, resource, JarResourceURLProvider.INSTANCE);
90-
}
91-
92-
private static class JarResourceURLProvider implements JarFileReference.JarFileConsumer<URL> {
93-
private static final JarResourceURLProvider INSTANCE = new JarResourceURLProvider();
94-
95-
@Override
96-
public URL apply(JarFile jarFile, Path path, String res) {
97-
JarEntry entry = jarFile.getJarEntry(res);
101+
final JarFile jarFile = readLockAcquireAndGetJarReference();
102+
try {
103+
JarEntry entry = jarFile.getJarEntry(resource);
98104
if (entry == null) {
99105
return null;
100106
}
@@ -104,7 +110,15 @@ public URL apply(JarFile jarFile, Path path, String res) {
104110
if (realName.endsWith("/")) {
105111
realName = realName.substring(0, realName.length() - 1);
106112
}
107-
final URL resUrl = getUrl(path, realName);
113+
final URI jarUri = jarPath.toUri();
114+
// first create a URI which includes both the jar file path and the relative resource name
115+
// and then invoke a toURL on it. The URI reconstruction allows for any encoding to be done
116+
// for the "path" which includes the "realName"
117+
var ssp = new StringBuilder(jarUri.getPath().length() + realName.length() + 2);
118+
ssp.append(jarUri.getPath());
119+
ssp.append("!/");
120+
ssp.append(realName);
121+
final URL resUrl = new URI(jarUri.getScheme(), ssp.toString(), null).toURL();
108122
// wrap it up into a "jar" protocol URL
109123
//horrible hack to deal with '?' characters in the URL
110124
//seems to be the only way, the URI constructor just does not let you handle them in a sane way
@@ -122,18 +136,8 @@ public URL apply(JarFile jarFile, Path path, String res) {
122136
} catch (MalformedURLException | URISyntaxException e) {
123137
throw new RuntimeException(e);
124138
}
125-
}
126-
127-
private static URL getUrl(Path jarPath, String realName) throws MalformedURLException, URISyntaxException {
128-
final URI jarUri = jarPath.toUri();
129-
// first create a URI which includes both the jar file path and the relative resource name
130-
// and then invoke a toURL on it. The URI reconstruction allows for any encoding to be done
131-
// for the "path" which includes the "realName"
132-
var ssp = new StringBuilder(jarUri.getPath().length() + realName.length() + 2);
133-
ssp.append(jarUri.getPath());
134-
ssp.append("!/");
135-
ssp.append(realName);
136-
return new URI(jarUri.getScheme(), ssp.toString(), null).toURL();
139+
} finally {
140+
readLock.unlock();
137141
}
138142
}
139143

@@ -147,16 +151,60 @@ public ProtectionDomain getProtectionDomain() {
147151
return protectionDomain;
148152
}
149153

154+
private JarFile readLockAcquireAndGetJarReference() {
155+
while (true) {
156+
readLock.lock();
157+
final JarFile zipFileLocal = this.zipFile;
158+
if (zipFileLocal != null) {
159+
//Expected fast path: returns a reference to the open JarFile while owning the readLock
160+
return zipFileLocal;
161+
} else {
162+
//This Lock implementation doesn't allow upgrading a readLock to a writeLock, so release it
163+
//as we're going to need the WriteLock.
164+
readLock.unlock();
165+
//trigger the JarFile being (re)opened.
166+
ensureJarFileIsOpen();
167+
//Now since we no longer own any lock, we need to try again to obtain the readLock
168+
//and check for the reference still being valid.
169+
//This exposes us to a race with closing the just-opened JarFile;
170+
//however this should be extremely rare, so we can trust we won't loop much;
171+
//A counter doesn't seem necessary, as in fact we know that methods close()
172+
//and resetInternalCaches() are invoked each at most once, which limits the amount
173+
//of loops here in practice.
174+
}
175+
}
176+
}
177+
178+
private void ensureJarFileIsOpen() {
179+
writeLock.lock();
180+
try {
181+
if (this.zipFile == null) {
182+
try {
183+
this.zipFile = JarFiles.create(jarPath.toFile());
184+
} catch (IOException e) {
185+
throw new RuntimeException("Failed to open " + jarPath, e);
186+
}
187+
}
188+
} finally {
189+
writeLock.unlock();
190+
}
191+
}
192+
150193
@Override
151194
public void close() {
152-
var futureRef = jarFileReference.get();
153-
if (futureRef != null) {
154-
// The jarfile has been already used and it's going to be removed from the cache,
155-
// so the future must be already completed
156-
var ref = futureRef.getNow(null);
157-
if (ref != null) {
158-
ref.close(this);
195+
writeLock.lock();
196+
try {
197+
final JarFile zipFileLocal = this.zipFile;
198+
if (zipFileLocal != null) {
199+
try {
200+
this.zipFile = null;
201+
zipFileLocal.close();
202+
} catch (Throwable e) {
203+
//ignore
204+
}
159205
}
206+
} finally {
207+
writeLock.unlock();
160208
}
161209
}
162210

0 commit comments

Comments
 (0)