Skip to content

Commit f089ff0

Browse files
authored
Merge pull request #81 from ChristianMa97/bugfix/prevent-oom
Serialize metadata to prevent OOM
2 parents 1092192 + 423aa8d commit f089ff0

File tree

15 files changed

+1217
-152
lines changed

15 files changed

+1217
-152
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@
55
.settings
66
work
77
.idea
8+
/jenkins_data

pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,11 @@
7171
<groupId>org.jenkins-ci.plugins.workflow</groupId>
7272
<artifactId>workflow-cps</artifactId>
7373
</dependency>
74+
<dependency>
75+
<groupId>org.jenkins-ci.plugins.workflow</groupId>
76+
<artifactId>workflow-basic-steps</artifactId>
77+
<scope>test</scope>
78+
</dependency>
7479
<dependency>
7580
<groupId>org.kohsuke</groupId>
7681
<artifactId>access-modifier-suppressions</artifactId>

src/main/java/io/jenkins/plugins/agent_build_history/AgentBuildHistory.java

Lines changed: 146 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,14 @@
11
package io.jenkins.plugins.agent_build_history;
22

3-
import edu.umd.cs.findbugs.annotations.NonNull;
43
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
5-
import hudson.Extension;
64
import hudson.model.AbstractBuild;
75
import hudson.model.Action;
86
import hudson.model.Computer;
9-
import hudson.model.Item;
107
import hudson.model.Job;
118
import hudson.model.Node;
129
import hudson.model.Run;
13-
import hudson.model.listeners.ItemListener;
14-
import hudson.model.listeners.RunListener;
1510
import hudson.util.RunList;
16-
import java.util.Collections;
17-
import java.util.HashMap;
18-
import java.util.Map;
19-
import java.util.Set;
20-
import java.util.TreeSet;
21-
import java.util.concurrent.TimeUnit;
22-
import java.util.logging.Level;
23-
import java.util.logging.Logger;
2411
import jenkins.model.Jenkins;
25-
import jenkins.model.NodeListener;
2612
import jenkins.util.Timer;
2713
import org.jenkinsci.plugins.workflow.cps.nodes.StepStartNode;
2814
import org.jenkinsci.plugins.workflow.flow.FlowExecution;
@@ -34,58 +20,40 @@
3420
import org.jenkinsci.plugins.workflow.support.steps.ExecutorStep;
3521
import org.kohsuke.accmod.Restricted;
3622
import org.kohsuke.accmod.restrictions.NoExternalUse;
23+
import org.kohsuke.stapler.Stapler;
24+
import org.kohsuke.stapler.StaplerRequest;
25+
26+
import javax.servlet.http.Cookie;
27+
import java.util.ArrayList;
28+
import java.util.List;
29+
import java.util.concurrent.TimeUnit;
30+
import java.util.logging.Level;
31+
import java.util.logging.Logger;
3732

3833
@Restricted(NoExternalUse.class)
3934
public class AgentBuildHistory implements Action {
4035

4136
private static final Logger LOGGER = Logger.getLogger(AgentBuildHistory.class.getName());
42-
43-
private static final Map<String, Set<AgentExecution>> agentExecutions = new HashMap<>();
44-
private static final Map<Run<?, ?>, AgentExecution> agentExecutionsMap = new HashMap<>();
45-
4637
private final Computer computer;
47-
38+
private int totalPages = 1;
4839
private static boolean loaded = false;
49-
5040
private static boolean loadingComplete = false;
5141

52-
@Extension
53-
public static class HistoryRunListener extends RunListener<Run<?, ?>> {
54-
55-
@Override
56-
public void onDeleted(Run run) {
57-
for (Set<AgentExecution> executions: agentExecutions.values()) {
58-
executions.removeIf(exec -> run == exec.getRun());
59-
}
60-
agentExecutionsMap.remove(run);
61-
}
42+
public AgentBuildHistory(Computer computer) {
43+
this.computer = computer;
44+
LOGGER.log(Level.CONFIG, () -> "Creating AgentBuildHistory for " + computer.getName());
6245
}
6346

64-
@Extension
65-
public static class HistoryItemListener extends ItemListener {
66-
67-
@Override
68-
public void onDeleted(Item item) {
69-
if (item instanceof Job) {
70-
for (Set<AgentExecution> executions: agentExecutions.values()) {
71-
executions.removeIf( exec -> exec.getRun().getParent() == item);
47+
public static String getCookieValue(StaplerRequest req, String name, String defaultValue) {
48+
Cookie[] cookies = req.getCookies();
49+
if (cookies != null) {
50+
for (Cookie cookie : cookies) {
51+
if (cookie.getName().equals(name)) {
52+
return cookie.getValue();
7253
}
73-
agentExecutionsMap.keySet().removeIf(key -> key.getParent() == item);
7454
}
7555
}
76-
}
77-
78-
@Extension
79-
public static class HistoryNodeListener extends NodeListener {
80-
81-
@Override
82-
protected void onDeleted(@NonNull Node node) {
83-
agentExecutions.remove(node.getNodeName());
84-
}
85-
}
86-
87-
public AgentBuildHistory(Computer computer) {
88-
this.computer = computer;
56+
return defaultValue; // Fallback to default if cookie not found
8957
}
9058

9159
/*
@@ -94,33 +62,148 @@ public AgentBuildHistory(Computer computer) {
9462
public Computer getComputer() {
9563
return computer;
9664
}
97-
65+
/*
66+
* used by jelly
67+
*/
9868
public boolean isLoadingComplete() {
9969
return loadingComplete;
10070
}
10171

72+
public static void setLoaded(boolean loaded) {
73+
AgentBuildHistory.loaded = loaded;
74+
AgentBuildHistory.loadingComplete = loaded;
75+
}
76+
77+
public int getTotalPages() {
78+
return totalPages;
79+
}
80+
10281
@SuppressFBWarnings(value = "ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD")
10382
public RunListTable getHandler() {
10483
if (!loaded) {
10584
loaded = true;
10685
Timer.get().schedule(AgentBuildHistory::load, 0, TimeUnit.SECONDS);
10786
}
10887
RunListTable runListTable = new RunListTable(computer.getName());
109-
runListTable.setRuns(getExecutions());
88+
//Get Parameters from URL
89+
StaplerRequest req = Stapler.getCurrentRequest();
90+
int page = req.getParameter("page") != null ? Integer.parseInt(req.getParameter("page")) : 1;
91+
int pageSize = req.getParameter("pageSize") != null ? Integer.parseInt(req.getParameter("pageSize")) : Integer.parseInt(getCookieValue(req, "pageSize", "20"));
92+
String sortColumn = req.getParameter("sortColumn") != null ? req.getParameter("sortColumn") : getCookieValue(req, "sortColumn", "startTime");
93+
String sortOrder = req.getParameter("sortOrder") != null ? req.getParameter("sortOrder") : getCookieValue(req, "sortOrder", "desc");
94+
//Update totalPages depending on pageSize
95+
int totalEntries = BuildHistoryFileManager.readIndexFile(computer.getName(), AgentBuildHistoryConfig.get().getStorageDir()).size();
96+
totalPages = (int) Math.ceil((double) totalEntries / pageSize);
97+
98+
LOGGER.finer("Getting runs for node: " + computer.getName() + " page: " + page + " pageSize: " + pageSize + " sortColumn: " + sortColumn + " sortOrder: " + sortOrder);
99+
100+
int start = (page-1)*pageSize;
101+
runListTable.setRuns(getExecutionsForNode(computer.getName(), start, pageSize, sortColumn, sortOrder));
110102
return runListTable;
111103
}
112104

105+
public List<AgentExecution> getExecutionsForNode(String nodeName, int start, int limit, String sortColumn, String sortOrder) {
106+
String storageDir = AgentBuildHistoryConfig.get().getStorageDir();
107+
List<String> indexLines = BuildHistoryFileManager.readIndexFile(nodeName, storageDir);
108+
LOGGER.finer("Found " + indexLines.size() + " entries for node " + nodeName);
109+
if (indexLines.isEmpty()) {
110+
return List.of();
111+
}
112+
// Sort index lines based on start time or build
113+
indexLines.sort((a, b) -> {
114+
int comparison = 0;
115+
switch(sortColumn){
116+
case "startTime":
117+
long timeA = Long.parseLong(a.split(BuildHistoryFileManager.separator)[2]);
118+
long timeB = Long.parseLong(b.split(BuildHistoryFileManager.separator)[2]);
119+
comparison = Long.compare(timeA, timeB);
120+
break;
121+
case "build":
122+
comparison = a.split(BuildHistoryFileManager.separator)[0].compareTo(b.split(BuildHistoryFileManager.separator)[0]);
123+
if (comparison == 0) {
124+
// Only compare build numbers if the job names are the same
125+
int buildNumberA = Integer.parseInt(a.split(BuildHistoryFileManager.separator)[1]);
126+
int buildNumberB = Integer.parseInt(b.split(BuildHistoryFileManager.separator)[1]);
127+
comparison = Integer.compare(buildNumberA, buildNumberB);
128+
}
129+
break;
130+
default:
131+
comparison = 0;
132+
}
133+
return sortOrder.equals("asc") ? comparison : -comparison;
134+
});
135+
// Apply pagination
136+
int end = Math.min(start + limit, indexLines.size());
137+
List<String> page = indexLines.subList(start, end);
138+
List<AgentExecution> result = new ArrayList<>();
139+
140+
for (String line : page) {
141+
String[] parts = line.split(BuildHistoryFileManager.separator);
142+
String jobName = parts[0];
143+
int buildNumber = Integer.parseInt(parts[1]);
144+
// Load execution using deserialization
145+
AgentExecution execution = loadSingleExecution(jobName, buildNumber);
146+
if (execution != null) {
147+
result.add(execution);
148+
}
149+
}
150+
LOGGER.finer("Returning " + result.size() + " entries for node " + nodeName);
151+
return result;
152+
}
153+
154+
public static AgentExecution loadSingleExecution(String jobName, int buildNumber) {
155+
Job<?, ?> job = Jenkins.get().getItemByFullName(jobName, Job.class);
156+
Run<?, ?> run = null;
157+
if (job != null) {
158+
run = job.getBuildByNumber(buildNumber);
159+
}
160+
if (run == null) {
161+
LOGGER.info("Run not found for " + jobName + " #" + buildNumber);
162+
return null;
163+
}
164+
LOGGER.finer("Loading run " + run.getFullDisplayName());
165+
AgentExecution execution = new AgentExecution(run);
166+
167+
if (run instanceof AbstractBuild) {
168+
Node node = ((AbstractBuild<?, ?>) run).getBuiltOn();
169+
if (node != null) {
170+
LOGGER.finer("Loading AbstractBuild on node: " + node.getNodeName());
171+
return execution;
172+
}
173+
} else if (run instanceof WorkflowRun) {
174+
WorkflowRun wfr = (WorkflowRun) run;
175+
LOGGER.finer("Loading WorkflowRun: " + wfr.getFullDisplayName());
176+
FlowExecution flowExecution = wfr.getExecution();
177+
if (flowExecution != null) {
178+
for (FlowNode flowNode : new DepthFirstScanner().allNodes(flowExecution)) {
179+
if (! (flowNode instanceof StepStartNode)) {
180+
continue;
181+
}
182+
for (WorkspaceActionImpl action : flowNode.getActions(WorkspaceActionImpl.class)) {
183+
StepStartNode startNode = (StepStartNode) flowNode;
184+
StepDescriptor descriptor = startNode.getDescriptor();
185+
if (descriptor instanceof ExecutorStep.DescriptorImpl) {
186+
String nodeName = action.getNode();
187+
execution.addFlowNode(flowNode, nodeName);
188+
LOGGER.finer("Loading WorkflowRun FlowNode on node: " + nodeName);
189+
}
190+
}
191+
}
192+
}
193+
}
194+
return execution;
195+
}
196+
113197
private static void load() {
114-
LOGGER.log(Level.FINE, () -> "Starting to load all runs");
198+
LOGGER.log(Level.INFO, () -> "Starting to synchronize all runs");
115199
RunList<Run<?, ?>> runList = RunList.fromJobs((Iterable) Jenkins.get().allItems(Job.class));
116200
runList.forEach(run -> {
117-
LOGGER.log(Level.FINER, () -> "Loading run " + run.getFullDisplayName());
118-
AgentExecution execution = getAgentExecution(run);
201+
LOGGER.finer("Loading run " + run.getFullDisplayName());
202+
119203
if (run instanceof AbstractBuild) {
120204
Node node = ((AbstractBuild<?, ?>) run).getBuiltOn();
121205
if (node != null) {
122-
Set<AgentExecution> executions = loadExecutions(node.getNodeName());
123-
executions.add(execution);
206+
BuildHistoryFileManager.addRunToNodeIndex(node.getNodeName(), run, AgentBuildHistoryConfig.get().getStorageDir());
124207
}
125208
} else if (run instanceof WorkflowRun) {
126209
WorkflowRun wfr = (WorkflowRun) run;
@@ -135,56 +218,23 @@ private static void load() {
135218
StepDescriptor descriptor = startNode.getDescriptor();
136219
if (descriptor instanceof ExecutorStep.DescriptorImpl) {
137220
String nodeName = action.getNode();
138-
execution.addFlowNode(flowNode, nodeName);
139-
Set<AgentExecution> executions = loadExecutions(nodeName);
140-
executions.add(execution);
221+
BuildHistoryFileManager.addRunToNodeIndex(nodeName, run, AgentBuildHistoryConfig.get().getStorageDir());
141222
}
142223
}
143224
}
144225
}
145226
}
146227
});
147228
loadingComplete = true;
148-
}
149-
150-
private static Set<AgentExecution> loadExecutions(String computerName) {
151-
Set<AgentExecution> executions = agentExecutions.get(computerName);
152-
if (executions == null) {
153-
LOGGER.log(Level.FINER, () -> "Creating executions for computer " + computerName);
154-
executions = Collections.synchronizedSet(new TreeSet<>());
155-
agentExecutions.put(computerName, executions);
156-
}
157-
return executions;
158-
}
159-
160-
/* use by jelly */
161-
public Set<AgentExecution> getExecutions() {
162-
Set<AgentExecution> executions = agentExecutions.get(computer.getName());
163-
if (executions == null) {
164-
return Collections.emptySet();
165-
}
166-
return Collections.unmodifiableSet(new TreeSet<>(executions));
167-
}
168-
169-
@NonNull
170-
private static AgentExecution getAgentExecution(Run<?, ?> run) {
171-
AgentExecution exec = agentExecutionsMap.get(run);
172-
if (exec == null) {
173-
LOGGER.log(Level.FINER, () -> "Creating execution for run " + run.getFullDisplayName());
174-
exec = new AgentExecution(run);
175-
agentExecutionsMap.put(run, exec);
176-
}
177-
return exec;
229+
LOGGER.log(Level.INFO, () -> "Synchronizing all runs complete");
178230
}
179231

180232
public static void startJobExecution(Computer c, Run<?, ?> run) {
181-
loadExecutions(c.getName()).add(getAgentExecution(run));
233+
BuildHistoryFileManager.addRunToNodeIndex(c.getName(), run, AgentBuildHistoryConfig.get().getStorageDir());
182234
}
183235

184236
public static void startFlowNodeExecution(Computer c, WorkflowRun run, FlowNode node) {
185-
AgentExecution exec = getAgentExecution(run);
186-
exec.addFlowNode(node, c.getName());
187-
loadExecutions(c.getName()).add(exec);
237+
BuildHistoryFileManager.addRunToNodeIndex(c.getName(), run, AgentBuildHistoryConfig.get().getStorageDir());
188238
}
189239

190240
@Override

0 commit comments

Comments
 (0)