Skip to content

Commit 52dff5a

Browse files
authored
Merge pull request #825 from jimklimov/JENKINS-76294-CME
Investigate and address `ConcurrentModificationException` which occasionally happens during XStream saving of running workflows [JENKINS-76294]
2 parents 1f4557d + d8708a0 commit 52dff5a

4 files changed

Lines changed: 2128 additions & 6 deletions

File tree

pom.xml

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@
6262
<spotbugs.threshold>Low</spotbugs.threshold>
6363
<spotless.check.skip>false</spotless.check.skip>
6464
<ban-junit4-imports.skip>false</ban-junit4-imports.skip>
65+
<system-stubs.version>2.1.8</system-stubs.version>
66+
<!-- FIXME: Get this into parent BOM? -->
6567
</properties>
6668

6769
<dependencyManagement>
@@ -110,6 +112,8 @@
110112
<groupId>org.jenkins-ci.plugins.workflow</groupId>
111113
<artifactId>workflow-support</artifactId>
112114
</dependency>
115+
116+
<!-- Testing scope -->
113117
<dependency>
114118
<groupId>io.jenkins</groupId>
115119
<artifactId>configuration-as-code</artifactId>
@@ -135,8 +139,6 @@
135139
<artifactId>workflow-job</artifactId>
136140
<scope>test</scope>
137141
</dependency>
138-
139-
<!-- Testing scope -->
140142
<dependency>
141143
<groupId>org.jenkins-ci.plugins.workflow</groupId>
142144
<artifactId>workflow-support</artifactId>
@@ -153,6 +155,14 @@
153155
<artifactId>mockito-junit-jupiter</artifactId>
154156
<scope>test</scope>
155157
</dependency>
158+
<dependency>
159+
<!-- https://www.baeldung.com/java-system-stubs -->
160+
<groupId>uk.org.webcompere</groupId>
161+
<artifactId>system-stubs-jupiter</artifactId>
162+
<version>${system-stubs.version}</version>
163+
<!-- FIXME: Get this into parent BOM? -->
164+
<scope>test</scope>
165+
</dependency>
156166
</dependencies>
157167

158168
<repositories>

src/main/java/org/jenkins/plugins/lockableresources/actions/LockedResourcesBuildAction.java

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import java.util.Collections;
77
import java.util.Date;
88
import java.util.List;
9+
import net.jcip.annotations.GuardedBy;
910
import org.jenkins.plugins.lockableresources.Messages;
1011
import org.kohsuke.accmod.Restricted;
1112
import org.kohsuke.accmod.restrictions.NoExternalUse;
@@ -18,8 +19,10 @@
1819
@Restricted(NoExternalUse.class)
1920
public class LockedResourcesBuildAction implements Action {
2021

22+
@GuardedBy("logs")
2123
private final List<LogEntry> logs = new ArrayList<>();
22-
private final transient Object syncLogs = new Object();
24+
25+
@GuardedBy("resourcesInUse")
2326
private final List<String> resourcesInUse = new ArrayList<>();
2427

2528
public LockedResourcesBuildAction() {}
@@ -47,13 +50,13 @@ public List<String> getCurrentUsedResourceNames() {
4750
}
4851

4952
public void addUsedResources(List<String> resourceNames) {
50-
synchronized (resourcesInUse) {
53+
synchronized (this.resourcesInUse) {
5154
resourcesInUse.addAll(resourceNames);
5255
}
5356
}
5457

5558
public void removeUsedResources(List<String> resourceNames) {
56-
synchronized (resourcesInUse) {
59+
synchronized (this.resourcesInUse) {
5760
resourcesInUse.removeAll(resourceNames);
5861
}
5962
}
@@ -94,7 +97,6 @@ public static void addLog(
9497
}
9598

9699
public void addLog(final String resourceName, final String step, final String action) {
97-
98100
synchronized (this.logs) {
99101
this.logs.add(new LogEntry(step, action, resourceName));
100102
}
@@ -107,6 +109,37 @@ public List<LogEntry> getReadOnlyLogs() {
107109
}
108110
}
109111

112+
@Restricted(NoExternalUse.class)
113+
public List<String> getReadOnlyResourcesInUse() {
114+
synchronized (this.resourcesInUse) {
115+
return new ArrayList<>(Collections.unmodifiableCollection(this.resourcesInUse));
116+
}
117+
}
118+
119+
/** Copy constructor, primarily for {@link #writeReplace} */
120+
private LockedResourcesBuildAction(LockedResourcesBuildAction other) {
121+
synchronized (other.logs) {
122+
synchronized (other.resourcesInUse) {
123+
this.logs.addAll(other.getReadOnlyLogs());
124+
this.resourcesInUse.addAll(other.getReadOnlyResourcesInUse());
125+
}
126+
}
127+
}
128+
/**
129+
* Ensure iteration during XStream marshalling is also synchronized,
130+
* otherwise we tend to get {@link java.util.ConcurrentModificationException}.<br/>
131+
*
132+
* The recommended approach is to copy-on-write the properties so a
133+
* snapshot can always be scraped consistently. But this can be costly
134+
* at run-time, so we use the next-best option: produce a consistent
135+
* replica of the current object for actual saving only on demand.<br/>
136+
*
137+
* This method is found by XStream via reflection.<br/>
138+
*/
139+
protected synchronized Object writeReplace() {
140+
return new LockedResourcesBuildAction(this);
141+
}
142+
110143
public static class LogEntry {
111144

112145
private final String step;

0 commit comments

Comments
 (0)