Skip to content

Commit 2eab461

Browse files
jtnordtimja
andauthored
race condition and missing results in aggregation (#732)
Co-authored-by: Tim Jacomb <[email protected]>
1 parent f0bf02d commit 2eab461

File tree

1 file changed

+35
-37
lines changed

1 file changed

+35
-37
lines changed

src/main/java/hudson/tasks/test/AggregatedTestResultPublisher.java

Lines changed: 35 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@
4040
import hudson.model.Job;
4141
import hudson.model.Result;
4242
import hudson.model.Run;
43-
import hudson.model.TaskListener;
4443
import hudson.model.listeners.RunListener;
4544
import hudson.tasks.BuildStepDescriptor;
4645
import hudson.tasks.BuildStepMonitor;
@@ -53,6 +52,8 @@
5352
import java.util.Collection;
5453
import java.util.Collections;
5554
import java.util.List;
55+
import java.util.Set;
56+
import java.util.stream.Collectors;
5657
import jenkins.model.Jenkins;
5758
import net.sf.json.JSONObject;
5859
import org.kohsuke.accmod.Restricted;
@@ -93,7 +94,7 @@ public AggregatedTestResultPublisher(String jobs, boolean includeFailedBuilds) {
9394
public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener)
9495
throws InterruptedException, IOException {
9596
// add a TestResult just so that it can show up later.
96-
build.addAction(new TestResultAction(jobs, includeFailedBuilds, build));
97+
build.addAction(new TestResultAction(jobs, includeFailedBuilds));
9798
return true;
9899
}
99100

@@ -126,13 +127,13 @@ public static final class TestResultAction extends AbstractTestResultAction {
126127
private final boolean includeFailedBuilds;
127128

128129
/**
129-
* The last time the fields of this object is computed from the rest.
130+
* The reference of the lastChangedReference when we last recomputed the aggregation. if this is != lastChangedReference then we need to recomupte the results.
130131
*/
131-
private transient long lastUpdated = 0;
132+
private transient Object lastRecomputedReference = null;
132133
/**
133-
* When was the last time any build completed?
134+
* Unique Object set whenever a build completes.
134135
*/
135-
private static long lastChanged = 0;
136+
private static volatile Object lastChangedReference = new Object();
136137

137138
private transient int failCount;
138139
private transient int totalCount;
@@ -144,43 +145,39 @@ public static final class TestResultAction extends AbstractTestResultAction {
144145

145146
private transient List<AbstractProject> noFingerprints;
146147

147-
@SuppressWarnings("deprecation") // calls getProject in constructor, so needs owner immediately
148+
/**
149+
* @deprecated use {@link #TestResultAction(String, boolean)}
150+
*/
151+
@Deprecated(forRemoval = true)
148152
public TestResultAction(String jobs, boolean includeFailedBuilds, AbstractBuild<?, ?> owner) {
149-
super(owner);
150-
this.includeFailedBuilds = includeFailedBuilds;
153+
this(jobs, includeFailedBuilds);
154+
}
151155

152-
if (jobs == null) {
153-
// resolve null as the transitive downstream jobs
154-
StringBuilder buf = new StringBuilder();
155-
for (AbstractProject p : getProject().getTransitiveDownstreamProjects()) {
156-
if (buf.length() > 0) {
157-
buf.append(',');
158-
}
159-
buf.append(p.getFullName());
160-
}
161-
jobs = buf.toString();
162-
}
156+
public TestResultAction(String jobs, boolean includeFailedBuilds) {
157+
this.includeFailedBuilds = includeFailedBuilds;
163158
this.jobs = jobs;
164159
}
165160

166161
/**
167162
* Gets the jobs to be monitored.
168163
*/
169164
public Collection<AbstractProject> getJobs() {
165+
if (jobs == null) {
166+
Set<AbstractProject> projects = getProject().getTransitiveDownstreamProjects();
167+
return projects.stream().filter(p -> p.hasPermission(Item.READ)).collect(Collectors.toSet());
168+
}
170169
List<AbstractProject> r = new ArrayList<>();
171-
if (jobs != null) {
172-
for (String job : Util.tokenize(jobs, ",")) {
173-
try {
174-
AbstractProject j = Jenkins.get().getItemByFullName(job.trim(), AbstractProject.class);
175-
if (j != null) {
176-
r.add(j);
177-
}
178-
} catch (RuntimeException x) {
179-
if (x.getClass().getSimpleName().startsWith("AccessDeniedException")) {
180-
// just skip it
181-
} else {
182-
throw x;
183-
}
170+
for (String job : Util.tokenize(jobs, ",")) {
171+
try {
172+
AbstractProject j = Jenkins.get().getItemByFullName(job.trim(), AbstractProject.class);
173+
if (j != null) {
174+
r.add(j);
175+
}
176+
} catch (RuntimeException x) {
177+
if (x.getClass().getSimpleName().startsWith("AccessDeniedException")) {
178+
// just skip it
179+
} else {
180+
throw x;
184181
}
185182
}
186183
}
@@ -271,10 +268,10 @@ public List<AbstractProject> getNoFingerprints() {
271268
justification = "False positive. Short-circuited")
272269
private synchronized void upToDateCheck() {
273270
// up to date check
274-
if (lastUpdated > lastChanged) {
271+
if (lastRecomputedReference == lastChangedReference) {
275272
return;
276273
}
277-
lastUpdated = lastChanged + 1;
274+
lastRecomputedReference = lastChangedReference;
278275

279276
int failCount = 0;
280277
int totalCount = 0;
@@ -350,9 +347,10 @@ public String getUrlName() {
350347

351348
@Extension
352349
public static class RunListenerImpl extends RunListener<Run> {
350+
@SuppressFBWarnings(value = "ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD", justification = "intended by design")
353351
@Override
354-
public void onCompleted(Run run, TaskListener listener) {
355-
lastChanged = System.currentTimeMillis();
352+
public void onFinalized(Run run) {
353+
lastChangedReference = new Object();
356354
}
357355
}
358356
}

0 commit comments

Comments
 (0)