Skip to content

Commit c769824

Browse files
Revert "[JENKINS-75465] Delete RunIdMigrator as it has been 10 years since this migration" (jenkinsci#10517)
* Revert "[JENKINS-75465] Delete RunIdMigrator as it has been 10 years since this migration" * Fast incremental build * Revert "Fast incremental build" Leave the commit as only the revert of the original change This reverts commit 2cf37a1. --------- Co-authored-by: Kris Stern <krisstern@outlook.com>
1 parent 76c461b commit c769824

File tree

18 files changed

+888
-12
lines changed

18 files changed

+888
-12
lines changed

core/src/main/java/hudson/model/Job.java

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@
9797
import jenkins.model.ModelObjectWithChildren;
9898
import jenkins.model.PeepholePermalink;
9999
import jenkins.model.ProjectNamingStrategy;
100+
import jenkins.model.RunIdMigrator;
100101
import jenkins.model.lazy.LazyBuildMixIn;
101102
import jenkins.scm.RunWithSCM;
102103
import jenkins.security.HexStringConfidentialKey;
@@ -191,6 +192,10 @@ public abstract class Job<JobT extends Job<JobT, RunT>, RunT extends Run<JobT, R
191192
// this should have been DescribableList but now it's too late
192193
protected CopyOnWriteList<JobProperty<? super JobT>> properties = new CopyOnWriteList<>();
193194

195+
@Restricted(NoExternalUse.class)
196+
@SuppressFBWarnings(value = "PA_PUBLIC_PRIMITIVE_ATTRIBUTE", justification = "Preserve API compatibility")
197+
public transient RunIdMigrator runIdMigrator;
198+
194199
protected Job(ItemGroup parent, String name) {
195200
super(parent, name);
196201
}
@@ -201,19 +206,20 @@ public synchronized void save() throws IOException {
201206
holdOffBuildUntilSave = holdOffBuildUntilUserSave;
202207
}
203208

209+
@Override public void onCreatedFromScratch() {
210+
super.onCreatedFromScratch();
211+
runIdMigrator = new RunIdMigrator();
212+
runIdMigrator.created(getBuildDir());
213+
}
214+
204215
@Override
205216
public void onLoad(ItemGroup<? extends Item> parent, String name)
206217
throws IOException {
207218
super.onLoad(parent, name);
208219

209-
// see https://github.com/jenkinsci/jenkins/pull/10456#issuecomment-2748112449
210-
// This code can be deleted after several Jenkins releases,
211-
// when it is likely that everyone is running a version equal or higher to this version.
212-
var buildDirPath = getBuildDir().toPath();
213-
if (Files.deleteIfExists(buildDirPath.resolve("legacyIds"))) {
214-
LOGGER.info("Deleting legacyIds file in " + buildDirPath + ". See https://issues.jenkins"
215-
+ ".io/browse/JENKINS-75465 for more information.");
216-
}
220+
File buildDir = getBuildDir();
221+
runIdMigrator = new RunIdMigrator();
222+
runIdMigrator.migrate(buildDir, Jenkins.get().getRootDir());
217223

218224
TextFile f = getNextBuildNumberFile();
219225
if (f.exists()) {

core/src/main/java/hudson/model/RunMap.java

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,10 @@
4545
import java.util.SortedMap;
4646
import java.util.logging.Level;
4747
import java.util.logging.Logger;
48+
import jenkins.model.RunIdMigrator;
4849
import jenkins.model.lazy.AbstractLazyLoadRunMap;
4950
import jenkins.model.lazy.BuildReference;
51+
import jenkins.model.lazy.LazyBuildMixIn;
5052
import org.kohsuke.accmod.Restricted;
5153
import org.kohsuke.accmod.restrictions.NoExternalUse;
5254

@@ -70,6 +72,10 @@ public final class RunMap<R extends Run<?, R>> extends AbstractLazyLoadRunMap<R>
7072

7173
private Constructor<R> cons;
7274

75+
/** Normally overwritten by {@link LazyBuildMixIn#onLoad} or {@link LazyBuildMixIn#onCreatedFromScratch}, in turn created during {@link Job#onLoad}. */
76+
@Restricted(NoExternalUse.class)
77+
public RunIdMigrator runIdMigrator = new RunIdMigrator();
78+
7379
// TODO: before first complete build
7480
// patch up next/previous build link
7581

@@ -150,6 +156,7 @@ public void remove() {
150156
@Override
151157
public boolean removeValue(R run) {
152158
run.dropLinks();
159+
runIdMigrator.delete(dir, run.getId());
153160
return super.removeValue(run);
154161
}
155162

@@ -220,13 +227,14 @@ public R put(R r) {
220227
return super._put(r);
221228
}
222229

223-
@CheckForNull
224230
@Override public R getById(String id) {
231+
int n;
225232
try {
226-
return getByNumber(Integer.parseInt(id));
227-
} catch (NumberFormatException e) { // see https://issues.jenkins.io/browse/JENKINS-75476
228-
return null;
233+
n = Integer.parseInt(id);
234+
} catch (NumberFormatException x) {
235+
n = runIdMigrator.findNumber(id);
229236
}
237+
return getByNumber(n);
230238
}
231239

232240
/**
Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
/*
2+
* The MIT License
3+
*
4+
* Copyright 2014 Jesse Glick.
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in
14+
* all copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
* THE SOFTWARE.
23+
*/
24+
25+
package jenkins.model;
26+
27+
import static java.util.logging.Level.FINE;
28+
import static java.util.logging.Level.FINER;
29+
import static java.util.logging.Level.INFO;
30+
import static java.util.logging.Level.WARNING;
31+
32+
import edu.umd.cs.findbugs.annotations.CheckForNull;
33+
import edu.umd.cs.findbugs.annotations.NonNull;
34+
import hudson.Util;
35+
import hudson.model.Job;
36+
import hudson.model.Run;
37+
import hudson.util.AtomicFileWriter;
38+
import java.io.File;
39+
import java.io.IOException;
40+
import java.nio.charset.StandardCharsets;
41+
import java.nio.file.Files;
42+
import java.text.DateFormat;
43+
import java.text.ParseException;
44+
import java.text.SimpleDateFormat;
45+
import java.util.ArrayList;
46+
import java.util.Arrays;
47+
import java.util.Iterator;
48+
import java.util.List;
49+
import java.util.Map;
50+
import java.util.TreeMap;
51+
import java.util.logging.Logger;
52+
import java.util.regex.Matcher;
53+
import java.util.regex.Pattern;
54+
import org.kohsuke.accmod.Restricted;
55+
import org.kohsuke.accmod.restrictions.NoExternalUse;
56+
57+
/**
58+
* Converts legacy {@code builds} directories to the current format.
59+
*
60+
* There would be one instance associated with each {@link Job}, to retain ID → build# mapping.
61+
*
62+
* The {@link Job#getBuildDir} is passed to every method call (rather than being cached) in case it is moved.
63+
*/
64+
@Restricted(NoExternalUse.class)
65+
public final class RunIdMigrator {
66+
67+
private final DateFormat legacyIdFormatter = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss");
68+
69+
static final Logger LOGGER = Logger.getLogger(RunIdMigrator.class.getName());
70+
private static final String MAP_FILE = "legacyIds";
71+
/** avoids wasting a map for new jobs */
72+
private static final Map<String, Integer> EMPTY = new TreeMap<>();
73+
74+
private @NonNull Map<String, Integer> idToNumber = EMPTY;
75+
76+
public RunIdMigrator() {}
77+
78+
/**
79+
* @return whether there was a file to load
80+
*/
81+
private boolean load(File dir) {
82+
File f = new File(dir, MAP_FILE);
83+
if (!f.isFile()) {
84+
return false;
85+
}
86+
if (f.length() == 0) {
87+
return true;
88+
}
89+
idToNumber = new TreeMap<>();
90+
try {
91+
for (String line : Files.readAllLines(Util.fileToPath(f), StandardCharsets.UTF_8)) {
92+
int i = line.indexOf(' ');
93+
idToNumber.put(line.substring(0, i), Integer.parseInt(line.substring(i + 1)));
94+
}
95+
} catch (Exception x) { // IOException, IndexOutOfBoundsException, NumberFormatException
96+
LOGGER.log(WARNING, "could not read from " + f, x);
97+
}
98+
return true;
99+
}
100+
101+
private void save(File dir) {
102+
File f = new File(dir, MAP_FILE);
103+
try (AtomicFileWriter w = new AtomicFileWriter(f)) {
104+
try {
105+
synchronized (this) {
106+
for (Map.Entry<String, Integer> entry : idToNumber.entrySet()) {
107+
w.write(entry.getKey() + ' ' + entry.getValue() + '\n');
108+
}
109+
}
110+
w.commit();
111+
} finally {
112+
w.abort();
113+
}
114+
} catch (IOException x) {
115+
LOGGER.log(WARNING, "could not save changes to " + f, x);
116+
}
117+
}
118+
119+
/**
120+
* Called when a job is first created.
121+
* Just saves an empty marker indicating that this job needs no migration.
122+
* @param dir as in {@link Job#getBuildDir}
123+
*/
124+
public void created(File dir) {
125+
save(dir);
126+
}
127+
128+
/**
129+
* Perform one-time migration if this has not been done already.
130+
* Where previously there would be a {@code 2014-01-02_03-04-05/build.xml} specifying {@code <number>99</number>} plus a symlink {@code 99 → 2014-01-02_03-04-05},
131+
* after migration there will be just {@code 99/build.xml} specifying {@code <id>2014-01-02_03-04-05</id>} and {@code <timestamp>…</timestamp>} according to local time zone at time of migration.
132+
* Newly created builds are untouched.
133+
* Does not throw {@link IOException} since we make a best effort to migrate but do not consider it fatal to job loading if we cannot.
134+
* @param dir as in {@link Job#getBuildDir}
135+
* @param jenkinsHome root directory of Jenkins (for logging only)
136+
* @return true if migration was performed
137+
*/
138+
public synchronized boolean migrate(File dir, @CheckForNull File jenkinsHome) {
139+
if (load(dir)) {
140+
LOGGER.log(FINER, "migration already performed for {0}", dir);
141+
return false;
142+
}
143+
if (!dir.isDirectory()) {
144+
LOGGER.log(/* normal during Job.movedTo */FINE, "{0} was unexpectedly missing", dir);
145+
return false;
146+
}
147+
LOGGER.log(INFO, "Migrating build records in {0}. See https://www.jenkins.io/redirect/build-record-migration for more information.", dir);
148+
doMigrate(dir);
149+
save(dir);
150+
return true;
151+
}
152+
153+
private static final Pattern NUMBER_ELT = Pattern.compile("(?m)^ <number>(\\d+)</number>(\r?\n)");
154+
155+
private void doMigrate(File dir) {
156+
idToNumber = new TreeMap<>();
157+
File[] kids = dir.listFiles();
158+
// Need to process symlinks first so we can rename to them.
159+
List<File> kidsList = new ArrayList<>(Arrays.asList(kids));
160+
Iterator<File> it = kidsList.iterator();
161+
while (it.hasNext()) {
162+
File kid = it.next();
163+
String name = kid.getName();
164+
try {
165+
Integer.parseInt(name);
166+
} catch (NumberFormatException x) {
167+
LOGGER.log(FINE, "ignoring nonnumeric entry {0}", name);
168+
continue;
169+
}
170+
try {
171+
if (Util.isSymlink(kid)) {
172+
LOGGER.log(FINE, "deleting build number symlink {0} → {1}", new Object[] {name, Util.resolveSymlink(kid)});
173+
} else if (kid.isDirectory()) {
174+
LOGGER.log(FINE, "ignoring build directory {0}", name);
175+
continue;
176+
} else {
177+
LOGGER.log(WARNING, "need to delete anomalous file entry {0}", name);
178+
}
179+
Util.deleteFile(kid);
180+
it.remove();
181+
} catch (Exception x) {
182+
LOGGER.log(WARNING, "failed to process " + kid, x);
183+
}
184+
}
185+
it = kidsList.iterator();
186+
while (it.hasNext()) {
187+
File kid = it.next();
188+
try {
189+
String name = kid.getName();
190+
try {
191+
Integer.parseInt(name);
192+
LOGGER.log(FINE, "skipping new build dir {0}", name);
193+
continue;
194+
} catch (NumberFormatException x) {
195+
// OK, next…
196+
}
197+
if (!kid.isDirectory()) {
198+
LOGGER.log(FINE, "skipping non-directory {0}", name);
199+
continue;
200+
}
201+
long timestamp;
202+
try {
203+
synchronized (legacyIdFormatter) {
204+
timestamp = legacyIdFormatter.parse(name).getTime();
205+
}
206+
} catch (ParseException x) {
207+
LOGGER.log(WARNING, "found unexpected dir {0}", name);
208+
continue;
209+
}
210+
File buildXml = new File(kid, "build.xml");
211+
if (!buildXml.isFile()) {
212+
LOGGER.log(WARNING, "found no build.xml in {0}", name);
213+
continue;
214+
}
215+
String xml = Files.readString(Util.fileToPath(buildXml), StandardCharsets.UTF_8);
216+
Matcher m = NUMBER_ELT.matcher(xml);
217+
if (!m.find()) {
218+
LOGGER.log(WARNING, "could not find <number> in {0}/build.xml", name);
219+
continue;
220+
}
221+
int number = Integer.parseInt(m.group(1));
222+
String nl = m.group(2);
223+
xml = m.replaceFirst(" <id>" + name + "</id>" + nl + " <timestamp>" + timestamp + "</timestamp>" + nl);
224+
File newKid = new File(dir, Integer.toString(number));
225+
move(kid, newKid);
226+
Files.writeString(Util.fileToPath(newKid).resolve("build.xml"), xml, StandardCharsets.UTF_8);
227+
LOGGER.log(FINE, "fully processed {0} → {1}", new Object[] {name, number});
228+
idToNumber.put(name, number);
229+
} catch (Exception x) {
230+
LOGGER.log(WARNING, "failed to process " + kid, x);
231+
}
232+
}
233+
}
234+
235+
/**
236+
* Tries to move/rename a file from one path to another.
237+
* Uses {@link java.nio.file.Files#move} when available.
238+
* Does not use {@link java.nio.file.StandardCopyOption#REPLACE_EXISTING} or any other options.
239+
* TODO candidate for moving to {@link Util}
240+
*/
241+
static void move(File src, File dest) throws IOException {
242+
try {
243+
Files.move(src.toPath(), dest.toPath());
244+
} catch (IOException x) {
245+
throw x;
246+
} catch (RuntimeException x) {
247+
throw new IOException(x);
248+
}
249+
}
250+
251+
/**
252+
* Look up a historical run by ID.
253+
* @param id a nonnumeric ID which may be a valid {@link Run#getId}
254+
* @return the corresponding {@link Run#number}, or 0 if unknown
255+
*/
256+
public synchronized int findNumber(@NonNull String id) {
257+
Integer number = idToNumber.get(id);
258+
return number != null ? number : 0;
259+
}
260+
261+
/**
262+
* Delete the record of a build.
263+
* @param dir as in {@link Job#getBuildDir}
264+
* @param id a {@link Run#getId}
265+
*/
266+
public synchronized void delete(File dir, String id) {
267+
if (idToNumber.remove(id) != null) {
268+
save(dir);
269+
}
270+
}
271+
}

core/src/main/java/jenkins/model/lazy/LazyBuildMixIn.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
import java.util.Objects;
5050
import java.util.logging.Level;
5151
import java.util.logging.Logger;
52+
import jenkins.model.RunIdMigrator;
5253
import org.kohsuke.accmod.Restricted;
5354
import org.kohsuke.accmod.restrictions.DoNotUse;
5455

@@ -146,6 +147,9 @@ public RunT create(File dir) throws IOException {
146147
return loadBuild(dir);
147148
}
148149
});
150+
RunIdMigrator runIdMigrator = asJob().runIdMigrator;
151+
assert runIdMigrator != null;
152+
r.runIdMigrator = runIdMigrator;
149153
return r;
150154
}
151155

0 commit comments

Comments
 (0)