Skip to content

Commit

Permalink
[JENKINS-16089] Revising 88feabb
Browse files Browse the repository at this point in the history
Based on comments from Jesse, revising the fix.

I'm now putting permlinks inside the builds/ directory to avoid the computing hassle involved in the split $JENKINS_HOME.

What we historically had in $JENKINS_HOME/jobs/JOB/lastSuccessfulBuild is also now subsumed by this feature. I initially attempted to create these permalinks in the buidl root directory, but turns out those symlinks aren't the same name as the ID of permalinks, so it doesn't mesh well.

And finally, a test!
  • Loading branch information
kohsuke committed Mar 13, 2013
1 parent 776c868 commit f7c9e81
Show file tree
Hide file tree
Showing 5 changed files with 165 additions and 90 deletions.
56 changes: 17 additions & 39 deletions core/src/main/java/hudson/model/AbstractBuild.java
Expand Up @@ -37,6 +37,7 @@
import hudson.matrix.MatrixConfiguration;
import hudson.model.Fingerprint.BuildPtr;
import hudson.model.Fingerprint.RangeSet;
import hudson.model.PermalinkProjectAction.Permalink;
import hudson.model.listeners.RunListener;
import hudson.model.listeners.SCMListener;
import hudson.scm.ChangeLogParser;
Expand All @@ -58,6 +59,7 @@
import hudson.tasks.test.AggregatedTestResultAction;
import hudson.util.*;
import jenkins.model.Jenkins;
import jenkins.model.PeepholePermalink;
import jenkins.model.lazy.AbstractLazyLoadRunMap.Direction;
import jenkins.model.lazy.BuildReference;
import org.kohsuke.stapler.HttpResponse;
Expand Down Expand Up @@ -368,7 +370,7 @@ protected void setWorkspace(FilePath ws) {
public final FilePath getModuleRoot() {
FilePath ws = getWorkspace();
if (ws==null) return null;
return getParent().getScm().getModuleRoot(ws,this);
return getParent().getScm().getModuleRoot(ws, this);
}

/**
Expand Down Expand Up @@ -469,42 +471,21 @@ public String getHudsonVersion() {
return hudsonVersion;
}

@Override
public synchronized void delete() throws IOException {
// Need to check if deleting this build affects lastSuccessful/lastStable symlinks
R lastSuccessful = getProject().getLastSuccessfulBuild(),
lastStable = getProject().getLastStableBuild();

super.delete();

try {
if (lastSuccessful == this)
updateSymlink("lastSuccessful", getProject().getLastSuccessfulBuild());
if (lastStable == this)
updateSymlink("lastStable", getProject().getLastStableBuild());
} catch (InterruptedException ex) {
LOGGER.warning("Interrupted update of lastSuccessful/lastStable symlinks for "
+ getProject().getDisplayName());
// handle it later
Thread.currentThread().interrupt();
}
}

private void updateSymlink(String name, AbstractBuild<?,?> newTarget) throws InterruptedException {
if (newTarget != null)
newTarget.createSymlink(new LogTaskListener(LOGGER, Level.WARNING), name);
else
new File(getProject().getRootDir(), name).delete();
}

private void createSymlink(TaskListener listener, String name) throws InterruptedException {
String target;
/**
* Backward compatibility.
*
* We used to have $JENKINS_HOME/jobs/JOBNAME/lastStable and lastSuccessful symlinked to the appropriate
* builds, but now those are done in {@link PeepholePermalink}. So here, we simply create symlinks that
* resolves to the symlink created by {@link PeepholePermalink}.
*/
private void createSymlink(TaskListener listener, String name, Permalink target) throws InterruptedException {
String targetDir;
if (getProject().getBuildDir().equals(new File(getProject().getRootDir(), "builds"))) {
target = "builds/" + getId();
targetDir = "builds/" + target.getId();
} else {
target = getRootDir().getAbsolutePath();
targetDir = getProject().getBuildDir()+target.getId();

This comment has been minimized.

Copy link
@jglick

jglick Mar 13, 2013

Member

AbstractProjectTest.testExternalBuildDirectorySymlinks failure

}
Util.createSymlink(getProject().getRootDir(), target, name, listener);
Util.createSymlink(getProject().getRootDir(), targetDir, name, listener);
}

/**
Expand Down Expand Up @@ -738,11 +719,8 @@ public final void post(BuildListener listener) throws Exception {
try {
post2(listener);

if (result.isBetterOrEqualTo(Result.UNSTABLE))
createSymlink(listener, "lastSuccessful");

if (result.isBetterOrEqualTo(Result.SUCCESS))
createSymlink(listener, "lastStable");
createSymlink(listener, "lastSuccessful", Permalink.LAST_SUCCESSFUL_BUILD);
createSymlink(listener, "lastStable", Permalink.LAST_STABLE_BUILD);
} finally {
// update the culprit list
HashSet<String> r = new HashSet<String>();
Expand Down
2 changes: 1 addition & 1 deletion core/src/main/java/hudson/model/Job.java
Expand Up @@ -744,7 +744,7 @@ public Object getDynamic(String token, StaplerRequest req,
*
* @see RunMap
*/
protected File getBuildDir() {
public File getBuildDir() {
return Jenkins.getInstance().getBuildDirFor(this);
}

Expand Down
120 changes: 71 additions & 49 deletions core/src/main/java/jenkins/model/PeepholePermalink.java
@@ -1,15 +1,19 @@
package jenkins.model;

import com.google.common.base.Predicate;
import hudson.Functions;
import hudson.Extension;
import hudson.Util;
import hudson.model.Job;
import hudson.model.PermalinkProjectAction.Permalink;
import hudson.model.Run;
import hudson.model.TaskListener;
import hudson.model.listeners.RunListener;
import hudson.util.AtomicFileWriter;
import hudson.util.StreamTaskListener;
import org.apache.commons.io.FileUtils;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.File;
import java.io.IOException;
import java.io.StringWriter;
Expand Down Expand Up @@ -48,14 +52,14 @@
* (it simply scans the history till find the new matching build.) To tolerate G(B)
* that goes from false to true, you need to be able to intercept whenever G(B) changes
* from false to true, then call {@link #resolve(Job)} to check the current permalink target
* is up to date, then call {@link #updateCache(Job, int)} if it needs updating.
* is up to date, then call {@link #updateCache(Job, Run)} if it needs updating.
*
* @author Kohsuke Kawaguchi
* @since 1.507
*/
public abstract class PeepholePermalink extends Permalink implements Predicate<Run<?,?>> {
/**
* Checks if the given build satifies the peep-hole criteria.
* Checks if the given build satisfies the peep-hole criteria.
*
* This is the "G(B)" as described in the class javadoc.
*/
Expand All @@ -65,7 +69,7 @@ public abstract class PeepholePermalink extends Permalink implements Predicate<R
* The file in which the permalink target gets recorded.
*/
protected File getPermalinkFile(Job<?,?> job) {
return new File(job.getRootDir(),"permalinks/"+getId());
return new File(job.getBuildDir(),getId());
}

/**
Expand All @@ -77,18 +81,10 @@ protected File getPermalinkFile(Job<?,?> job) {
Run<?,?> b=null;

try {
String target = null;
if (USE_SYMLINK) { // f.exists() return false if symlink exists but point to a non-existent directory
target = Util.resolveSymlink(f);
if (target==null && f.exists()) {
// if this file isn't a symlink, it must be a regular file
target = FileUtils.readFileToString(f,"UTF-8").trim();
}
} else {
if (f.exists()) {
// if this file isn't a symlink, it must be a regular file
target = FileUtils.readFileToString(f,"UTF-8").trim();
}
String target = Util.resolveSymlink(f);
if (target==null && f.exists()) {
// if this file isn't a symlink, it must be a regular file
target = FileUtils.readFileToString(f,"UTF-8").trim();
}

if (target!=null) {
Expand All @@ -107,78 +103,104 @@ protected File getPermalinkFile(Job<?,?> job) {
LOGGER.log(Level.WARNING, "Failed to read permalink cache:" + f, e);
// if we fail to read the cache, fall back to the re-computation
} catch (IOException e) {
LOGGER.log(Level.WARNING, "Failed to read permalink cache:" + f, e);
// if we fail to read the cache, fall back to the re-computation
// this happens when the symlink doesn't exist
// (and it cannot be distinguished from the case when the actual I/O error happened
}

if (b==null) {
// no cache
b = job.getLastBuild();
}

int n;
// start from the build 'b' and locate the build that matches the criteria going back in time
while (true) {
if (b==null) {
n = RESOLVES_TO_NONE;
break;
}
if (apply(b)) {
n = b.getNumber();
break;
}
b = find(b);

b=b.getPreviousBuild();
}
updateCache(job,b);
return b;
}

updateCache(job,n);
/**
* Start from the build 'b' and locate the build that matches the criteria going back in time
*/
private Run<?,?> find(Run<?,?> b) {
for ( ; b!=null && !apply(b); b=b.getPreviousBuild())
;
return b;
}

/**
* Remembers the value 'n' in the cache for future {@link #resolve(Job)}.
*/
protected void updateCache(Job<?,?> job, int n) {
protected void updateCache(@Nonnull Job<?,?> job, @Nullable Run<?,?> b) {
final int n = b==null ? RESOLVES_TO_NONE : b.getNumber();

File cache = getPermalinkFile(job);
File tmp = new File(cache.getPath()+".tmp");
cache.getParentFile().mkdirs();

try {
StringWriter w = new StringWriter();
StreamTaskListener listener = new StreamTaskListener(w);

if (USE_SYMLINK) {
Util.createSymlink(cache.getParentFile(),"../builds/"+n,cache.getName(),listener);
} else {
// symlink not supported. use a regular
Util.createSymlink(tmp.getParentFile(),String.valueOf(n),tmp.getName(),listener);
if (Util.resolveSymlink(tmp)==null) {
// symlink not supported. use a regular file
AtomicFileWriter cw = new AtomicFileWriter(cache);
try {
cw.write(String.valueOf(n));
cw.commit();
} finally {
cw.abort();
}
} else {
cache.delete();
tmp.renameTo(cache);
}
} catch (IOException e) {
LOGGER.log(Level.WARNING, "Failed to update permalink cache for " + job, e);
LOGGER.log(Level.WARNING, "Failed to update "+job+" "+getId()+" permalink for " + b, e);
cache.delete();
} catch (InterruptedException e) {
LOGGER.log(Level.WARNING, "Failed to update permalink cache for "+job,e);
LOGGER.log(Level.WARNING, "Failed to update "+job+" "+getId()+" permalink for " + b, e);
cache.delete();
} finally {
tmp.delete();
}
}

@Extension
public static class RunListenerImpl extends RunListener<Run<?,?>> {

This comment has been minimized.

Copy link
@jglick

jglick Mar 13, 2013

Member

Should we start marking things like this @Restricted(NoExternalUse.class)?

/**
* If any of the peephole permalink points to the build to be deleted, update it to point to the new location.
*/
@Override
public void onDeleted(Run run) {
Job<?, ?> j = run.getParent();
for (PeepholePermalink pp : Util.filter(j.getPermalinks(), PeepholePermalink.class)) {
if (pp.apply(run)) {
if (pp.resolve(j)==run) {
pp.updateCache(j,pp.find(run.getPreviousBuild()));
}
}
}
}

/**
* See if the new build matches any of the peephole permalink.
*/
@Override
public void onCompleted(Run<?,?> run, @Nonnull TaskListener listener) {
Job<?, ?> j = run.getParent();
for (PeepholePermalink pp : Util.filter(j.getPermalinks(), PeepholePermalink.class)) {
if (pp.apply(run)) {
Run<?, ?> cur = pp.resolve(j);
if (cur==null || cur.getNumber()<run.getNumber())
pp.updateCache(j,run);
}
}
}
}

private static final int RESOLVES_TO_NONE = -1;

private static final Logger LOGGER = Logger.getLogger(PeepholePermalink.class.getName());

/**
* True if we use the symlink as cache, false if plain text file.
*
* <p>
* On Windows, even with Java7, using symlinks require one to go through quite a few hoops
* (you need to change the security policy to specifically have this permission, then
* you better not be in the administrator group because this token gets filtered out
* on UAC-enabled Windows.)
*/
public static boolean USE_SYMLINK = !Functions.isWindows();
}
2 changes: 1 addition & 1 deletion core/src/test/java/hudson/model/StubJob.java
Expand Up @@ -57,7 +57,7 @@ protected void removeRun(Run run) {

}

@Override protected File getBuildDir() {
@Override public File getBuildDir() {
return new File(System.getProperty("java.io.tmpdir"));
}

Expand Down
75 changes: 75 additions & 0 deletions test/src/test/groovy/jenkins/model/PeepholePermalinkTest.groovy
@@ -0,0 +1,75 @@
package jenkins.model

import hudson.Functions
import hudson.Util
import hudson.model.Run
import org.jvnet.hudson.test.FailureBuilder
import org.jvnet.hudson.test.HudsonTestCase

/**
*
*
* @author Kohsuke Kawaguchi
*/
class PeepholePermalinkTest extends HudsonTestCase {
/**
* Basic operation of the permalink generation.
*/
void testBasics() {
if (Functions.isWindows()) return; // can't run on windows because we rely on symlinks

This comment has been minimized.

Copy link
@jglick

jglick Mar 13, 2013

Member

Use JUnit 4 and JenkinsRule and then you can assumeFalse("need symlinks", Functions.isWindows()).


def p = createFreeStyleProject()
def b1 = assertBuildStatusSuccess(p.scheduleBuild2(0))

def lsb = new File(p.buildDir, "lastSuccessfulBuild")
def lfb = new File(p.buildDir, "lastFailedBuild")

assertLink(lsb,b1)

// now another build that fails
p.buildersList.add(new FailureBuilder())
def b2 = p.scheduleBuild2(0).get()

assertLink(lsb,b1)
assertLink(lfb,b2)

// one more build and this time it succeeds
p.buildersList.clear()
def b3 = assertBuildStatusSuccess(p.scheduleBuild2(0))

assertLink(lsb,b3)
assertLink(lfb,b2)

// delete b3 and symlinks should update properly
b3.delete()
assertLink(lsb,b1)
assertLink(lfb,b2)

b1.delete()
assertLink(lsb,null)
assertLink(lfb,b2)

b2.delete()
assertLink(lsb,null)
assertLink(lfb,null)
}

def assertLink(File symlink, Run build) {
assert Util.resolveSymlink(symlink)==(build==null ? "-1" : build.number as String);
}

/**
* job/JOBNAME/lastStable and job/JOBNAME/lastSuccessful symlinks that we used to generate should still work
*/
void testLegacyCompatibility() {
if (Functions.isWindows()) return; // can't run on windows because we rely on symlinks

def p = createFreeStyleProject()
def b1 = assertBuildStatusSuccess(p.scheduleBuild2(0))

["lastStable","lastSuccessful"].each { n ->
// test if they both point to b1
assert new File(p.rootDir,"$n/build.xml").length() == new File(b1.rootDir,"build.xml").length()

This comment has been minimized.

Copy link
@jglick

jglick Mar 13, 2013

Member

Comparing .canonicalFile is more direct and reliable.

}
}
}

0 comments on commit f7c9e81

Please sign in to comment.