Skip to content

Commit d52c847

Browse files
authored
Merge pull request #48751 from aloubyansky/retry-failed-collect-requests
Retry deployment dependency collecting requests failed due to missing temporary files
2 parents a4d400a + fd79246 commit d52c847

File tree

4 files changed

+353
-40
lines changed

4 files changed

+353
-40
lines changed

core/deployment/src/test/java/io/quarkus/deployment/runnerjar/PackageAppTestBase.java

Lines changed: 9 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
package io.quarkus.deployment.runnerjar;
22

3+
import static org.assertj.core.api.Assertions.assertThat;
34
import static org.junit.jupiter.api.Assertions.assertEquals;
45
import static org.junit.jupiter.api.Assertions.assertNotNull;
56
import static org.junit.jupiter.api.Assertions.assertTrue;
6-
import static org.junit.jupiter.api.Assertions.fail;
77

88
import java.io.BufferedWriter;
99
import java.io.IOException;
@@ -41,7 +41,7 @@
4141

4242
public abstract class PackageAppTestBase extends BootstrapTestBase {
4343

44-
private static final String LIB_PREFIX = "lib/";
44+
private static final String LIB_BOOT_PREFIX = "lib/boot/";
4545
private static final String MAIN_CLS = "io.quarkus.bootstrap.runner.QuarkusEntryPoint";
4646

4747
protected List<String> expectedLib = new ArrayList<>();
@@ -209,44 +209,16 @@ private void assertAugmentOutcome(AugmentResult outcome) throws IOException {
209209
.toArray(String[]::new);
210210
assertEquals(actualBootLib.size(), cpEntries.length);
211211
for (String entry : cpEntries) {
212-
assertTrue(entry.startsWith(LIB_PREFIX));
213-
assertTrue(actualBootLib.contains(entry.substring(LIB_PREFIX.length())));
214-
}
215-
}
216-
217-
List<String> missingEntries = List.of();
218-
for (String entry : expectedLib) {
219-
if (!actualMainLib.remove(entry)) {
220-
if (missingEntries.isEmpty()) {
221-
missingEntries = new ArrayList<>();
222-
}
223-
missingEntries.add(entry);
212+
assertThat(entry).startsWith(LIB_BOOT_PREFIX);
213+
String entryFile = entry.substring(LIB_BOOT_PREFIX.length());
214+
assertThat(actualBootLib).contains(entryFile);
224215
}
225216
}
217+
assertLibDirectoryContent(actualMainLib);
218+
}
226219

227-
StringBuilder buf = null;
228-
if (!missingEntries.isEmpty()) {
229-
buf = new StringBuilder();
230-
buf.append("Missing entries: ").append(missingEntries.get(0));
231-
for (int i = 1; i < missingEntries.size(); ++i) {
232-
buf.append(", ").append(missingEntries.get(i));
233-
}
234-
}
235-
if (!actualMainLib.isEmpty()) {
236-
if (buf == null) {
237-
buf = new StringBuilder();
238-
} else {
239-
buf.append("; ");
240-
}
241-
final Iterator<String> i = actualMainLib.iterator();
242-
buf.append("Extra entries: ").append(i.next());
243-
while (i.hasNext()) {
244-
buf.append(", ").append(i.next());
245-
}
246-
}
247-
if (buf != null) {
248-
fail(buf.toString());
249-
}
220+
protected void assertLibDirectoryContent(Set<String> actualMainLib) {
221+
assertThat(actualMainLib).containsExactlyInAnyOrderElementsOf(expectedLib);
250222
}
251223

252224
protected Set<String> getDirContent(final Path dir) throws IOException {
Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
package io.quarkus.deployment.runnerjar;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
5+
import java.io.BufferedWriter;
6+
import java.io.FileNotFoundException;
7+
import java.io.IOException;
8+
import java.nio.file.DirectoryStream;
9+
import java.nio.file.Files;
10+
import java.nio.file.NoSuchFileException;
11+
import java.nio.file.Path;
12+
import java.util.HashSet;
13+
import java.util.Map;
14+
import java.util.Set;
15+
import java.util.concurrent.CompletableFuture;
16+
import java.util.concurrent.ConcurrentHashMap;
17+
import java.util.concurrent.atomic.AtomicBoolean;
18+
19+
import org.apache.maven.settings.Profile;
20+
import org.apache.maven.settings.Repository;
21+
import org.apache.maven.settings.RepositoryPolicy;
22+
import org.apache.maven.settings.Settings;
23+
import org.apache.maven.settings.io.DefaultSettingsWriter;
24+
import org.eclipse.aether.AbstractRepositoryListener;
25+
import org.eclipse.aether.DefaultRepositorySystemSession;
26+
import org.eclipse.aether.RepositoryEvent;
27+
import org.eclipse.aether.RequestTrace;
28+
import org.eclipse.aether.collection.CollectRequest;
29+
30+
import io.quarkus.bootstrap.model.ApplicationModel;
31+
import io.quarkus.bootstrap.resolver.TsArtifact;
32+
import io.quarkus.bootstrap.resolver.maven.BootstrapMavenContext;
33+
import io.quarkus.bootstrap.resolver.maven.BootstrapMavenException;
34+
import io.quarkus.bootstrap.resolver.maven.MavenArtifactResolver;
35+
import io.quarkus.bootstrap.resolver.maven.workspace.LocalProject;
36+
import io.quarkus.bootstrap.util.DependencyUtils;
37+
import io.quarkus.maven.dependency.ArtifactCoords;
38+
import io.quarkus.maven.dependency.DependencyFlags;
39+
40+
public class RetryResolutionOnMissingTmpFileErrorsTest extends BootstrapFromOriginalJarTestBase {
41+
42+
private static final String QUARKUS_VERSION = System.getProperty("project.version");
43+
44+
private static class TempFileCleaner implements Runnable {
45+
46+
private final Path targetRepositoryFile;
47+
48+
private TempFileCleaner(Path targetRepositoryFile) {
49+
this.targetRepositoryFile = targetRepositoryFile;
50+
}
51+
52+
@Override
53+
public void run() {
54+
final String targetFileName = targetRepositoryFile.getFileName().toString();
55+
final Path artifactDir = targetRepositoryFile.getParent();
56+
boolean terminate = false;
57+
while (!terminate) {
58+
if (Files.exists(artifactDir)) {
59+
try (DirectoryStream<Path> stream = Files.newDirectoryStream(artifactDir)) {
60+
for (var p : stream) {
61+
final String fileName = p.getFileName().toString();
62+
if (fileName.equals(targetFileName)) {
63+
terminate = true;
64+
} else if (fileName.endsWith(".tmp") || fileName.endsWith(".part")) {
65+
// delete in-progress files to fail artifact resolution
66+
if (Files.deleteIfExists(p)) {
67+
System.out.println("Deleted " + p.getFileName());
68+
terminate = true;
69+
}
70+
}
71+
}
72+
} catch (Exception e) {
73+
e.printStackTrace();
74+
}
75+
}
76+
// try {
77+
// Thread.sleep(50);
78+
// } catch (InterruptedException e) {
79+
// }
80+
if (!terminate) {
81+
terminate = Files.exists(targetRepositoryFile);
82+
}
83+
}
84+
}
85+
}
86+
87+
final AtomicBoolean sawFileMissingErrors = new AtomicBoolean(false);
88+
89+
@Override
90+
protected MavenArtifactResolver newArtifactResolver(LocalProject currentProject) throws BootstrapMavenException {
91+
92+
/**
93+
* This is the tricky part.
94+
* Here we initialize a Maven artifact resolver to be used by Quarkus to resolve the ApplicationModel.
95+
*
96+
* First, it will capture events indicating that certain artifacts will be resolved. For non-SNAPSHOT
97+
* artifacts, we register tasks that will be scanning those artifact directories in the local Maven repository
98+
* trying to capture temporary (in-progress) files that the "Maven resolver" writes artifact content to
99+
* before renaming them to the final artifact files.
100+
* Once those in-progress files are detected, they are removed. This will cause Quarkus deployment dependency
101+
* CollectRequest to fail, which the {@link io.quarkus.bootstrap.resolver.maven.ApplicationDependencyResolver}
102+
* is expected to re-try.
103+
*
104+
* This is done exclusively for deployment dependencies, since this is what is parallelized
105+
* in the {@link io.quarkus.bootstrap.resolver.maven.ApplicationDependencyResolver}.
106+
*
107+
* The test uses a custom location for a local Maven repository. To be able to resolve locally built Quarkus artifacts,
108+
* we create custom settings an active profile where the current local repository is added as remote repository.
109+
*/
110+
111+
var originalMvnCtx = new BootstrapMavenContext((BootstrapMavenContext.config().setWorkspaceDiscovery(false)));
112+
final Settings settings = originalMvnCtx.getEffectiveSettings();
113+
final String originalLocalRepo = originalMvnCtx.getLocalRepo();
114+
115+
Path settingsXml;
116+
try {
117+
Profile profile = new Profile();
118+
settings.addActiveProfile("original-local");
119+
profile.setId("original-local");
120+
121+
final Repository repo = new Repository();
122+
repo.setId("original-local");
123+
repo.setLayout("default");
124+
repo.setUrl(Path.of(originalLocalRepo).toUri().toURL().toExternalForm());
125+
RepositoryPolicy releases = new RepositoryPolicy();
126+
releases.setEnabled(false);
127+
releases.setChecksumPolicy("ignore");
128+
releases.setUpdatePolicy("never");
129+
repo.setReleases(releases);
130+
RepositoryPolicy snapshots = new RepositoryPolicy();
131+
snapshots.setEnabled(true);
132+
snapshots.setChecksumPolicy("ignore");
133+
snapshots.setUpdatePolicy("never");
134+
repo.setSnapshots(snapshots);
135+
136+
profile.addRepository(repo);
137+
profile.addPluginRepository(repo);
138+
139+
settings.addProfile(profile);
140+
141+
settingsXml = workDir.resolve("settings.xml");
142+
try (BufferedWriter writer = Files.newBufferedWriter(settingsXml)) {
143+
new DefaultSettingsWriter().write(writer, Map.of(), settings);
144+
}
145+
} catch (Exception e) {
146+
throw new RuntimeException("Failed to create test settings.xml", e);
147+
}
148+
149+
var ctx = new BootstrapMavenContext(BootstrapMavenContext.config()
150+
// A bit of a peculiar detail, there appears to be a difference between running`mvn test`
151+
// and `mvn test -Dtest=RetryResolutionOnTmpFilesMissingTest`.
152+
// Unless the current project is set to the module directory, the test belongs to, `mvn test` will fail
153+
// while `mvn test -Dtest=RetryResolutionOnTmpFilesMissingTest` will not.
154+
.setCurrentProject(Path.of("").normalize().toAbsolutePath().toString())
155+
.setWorkspaceDiscovery(true)
156+
.setArtifactTransferLogging(false)
157+
.setUserSettings(settingsXml.toFile())
158+
.setLocalRepository(getLocalRepoHome().toString()));
159+
160+
var session = new DefaultRepositorySystemSession(ctx.getRepositorySystemSession());
161+
final Map<ArtifactCoords, CompletableFuture<?>> tmpFileCleaners = new ConcurrentHashMap<>();
162+
final Set<String> ignoredGroupIds = Set.of("org.acme", "io.quarkus.bootstrap.test");
163+
session.setRepositoryListener(new AbstractRepositoryListener() {
164+
@Override
165+
public void artifactResolving(RepositoryEvent event) {
166+
// we are looking exclusively for deployment dependency requests
167+
if (!isPartOfCollectingDeploymentDeps(event)) {
168+
return;
169+
}
170+
var a = event.getArtifact();
171+
// delete non-snapshot artifacts to trigger re-download but only once
172+
if (!ignoredGroupIds.contains(a.getGroupId()) && !a.isSnapshot()) {
173+
tmpFileCleaners.computeIfAbsent(
174+
DependencyUtils.getCoords(a),
175+
k -> {
176+
final Path targetRepositoryFile = event.getSession().getLocalRepository().getBasedir().toPath()
177+
.resolve(event.getSession().getLocalRepositoryManager().getPathForLocalArtifact(a));
178+
try {
179+
if (Files.deleteIfExists(targetRepositoryFile)) {
180+
System.out.println("To be (re)resolved " + event.getArtifact());
181+
}
182+
} catch (IOException e) {
183+
}
184+
return CompletableFuture.runAsync(new TempFileCleaner(targetRepositoryFile));
185+
});
186+
}
187+
}
188+
189+
@Override
190+
public void artifactResolved(RepositoryEvent event) {
191+
// just to catch the fact that some requests failed due to missing files
192+
if (!sawFileMissingErrors.get() && !event.getExceptions().isEmpty()) {
193+
for (var e : event.getExceptions()) {
194+
if (isCausedByMissingFile(e)) {
195+
sawFileMissingErrors.set(true);
196+
break;
197+
}
198+
}
199+
}
200+
}
201+
202+
@Override
203+
public void artifactDownloading(RepositoryEvent event) {
204+
final ArtifactCoords coords = DependencyUtils.getCoords(event.getArtifact());
205+
StringBuilder sb = new StringBuilder();
206+
sb.append(tmpFileCleaners.containsKey(coords) ? "Re-downloading" : "Downloading")
207+
.append(" from ").append(event.getRepository().getId()).append(": ").append(coords.toCompactCoords());
208+
System.out.println(sb);
209+
}
210+
});
211+
212+
return MavenArtifactResolver.builder()
213+
.setRepositorySystem(ctx.getRepositorySystem())
214+
.setRepositorySystemSession(session)
215+
.setRemoteRepositories(ctx.getRemoteRepositories())
216+
.setRemoteRepositoryManager(ctx.getRemoteRepositoryManager())
217+
.setSettingsDecrypter(ctx.getSettingsDecrypter())
218+
.build();
219+
}
220+
221+
@Override
222+
protected TsArtifact composeApplication() {
223+
assertThat(QUARKUS_VERSION).isNotNull();
224+
return TsArtifact.jar("app")
225+
.addManagedDependency(platformDescriptor())
226+
.addManagedDependency(platformProperties())
227+
.addDependency(TsArtifact.jar("io.quarkus", "quarkus-core", QUARKUS_VERSION));
228+
}
229+
230+
@Override
231+
protected void assertAppModel(ApplicationModel model) throws Exception {
232+
// make sure there were resolution failures related to missing files
233+
assertThat(sawFileMissingErrors).isTrue();
234+
235+
// basic model assertion
236+
Set<ArtifactCoords> topCoords = new HashSet<>(1);
237+
for (var topExt : model.getDependencies(DependencyFlags.TOP_LEVEL_RUNTIME_EXTENSION_ARTIFACT)) {
238+
topCoords.add(ArtifactCoords.of(topExt.getGroupId(), topExt.getArtifactId(), topExt.getClassifier(),
239+
topExt.getType(), topExt.getVersion()));
240+
}
241+
assertThat(topCoords).containsExactly(ArtifactCoords.jar("io.quarkus", "quarkus-core", QUARKUS_VERSION));
242+
}
243+
244+
@Override
245+
protected void assertLibDirectoryContent(Set<String> actualMainLib) {
246+
// skipping, since it involves iterating over quarkus-core dependencies
247+
}
248+
249+
private static boolean isCausedByMissingFile(Exception e) {
250+
Throwable t = e;
251+
while (t != null) {
252+
// It looks like in Maven 3.9 it's NoSuchFileException, while in Maven 3.8 it's FileNotFoundException
253+
if (t instanceof NoSuchFileException || t instanceof FileNotFoundException) {
254+
return true;
255+
}
256+
t = t.getCause();
257+
}
258+
return false;
259+
}
260+
261+
private static boolean isPartOfCollectingDeploymentDeps(RepositoryEvent event) {
262+
RequestTrace requestTrace = getRootRequest(event);
263+
if (requestTrace.getData() instanceof CollectRequest cr) {
264+
if (cr.getRootArtifact().getArtifactId().endsWith("-deployment")) {
265+
return true;
266+
}
267+
}
268+
;
269+
return false;
270+
}
271+
272+
private static RequestTrace getRootRequest(RepositoryEvent event) {
273+
RequestTrace requestTrace = event.getTrace();
274+
RequestTrace parent;
275+
while ((parent = requestTrace.getParent()) != null) {
276+
requestTrace = parent;
277+
}
278+
return requestTrace;
279+
}
280+
}

independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/resolver/ResolverSetupCleanup.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,10 @@ protected Path getSettingsXml() {
138138
return settingsXml;
139139
}
140140

141+
protected Path getLocalRepoHome() {
142+
return localRepoHome;
143+
}
144+
141145
protected boolean cleanWorkDir() {
142146
return true;
143147
}

0 commit comments

Comments
 (0)