mirror of
https://github.com/jenkinsci/jenkins.git
synced 2026-11-10 02:26:18 +00:00
[JENKINS-62755] Introduce Fingerprint Cleanup in External Storage API (#4817)
* Create new FingerprintCleanupThread * Moved test to jenkins * Add license * Fix license * Fix some tests * Fix all tests * Remove redundant imports * Add documentation; add test * Fix errors in fingerprintertest * Remove default fingerprint cleanup implementation * Remove override from execute * Undo author deletion * Remove redundant import * Move fingerprint cleanup to storage * Fix spacing * Add new line * Minor fixes * Fix tests * Add documentation * Introduce fingerprint cleanup toggle * Link cleanup config to thread * Add file system cleanup * Add test * Introduce external fingerprint storage cleanup tests * Remove redundant statements * Fix test * Improve docstring * Introduce shouldNotCleanFingerprintsWhenDisabled test * Fix default cleanup value * Minor fixes * Enabled->Disabled * Fix tests * get -> is
This commit is contained in:
1 parent
8024742e1a
commit
be28d586b5
6 files changed
+322
-113
No files matched your search
@@ -25,20 +25,14 @@ package hudson.model;
|
||||
|
||||
import hudson.Extension;
|
||||
import hudson.ExtensionList;
|
||||
import hudson.Functions;
|
||||
import jenkins.fingerprints.FileFingerprintStorage;
|
||||
import jenkins.fingerprints.FingerprintStorage;
|
||||
import jenkins.model.FingerprintFacet;
|
||||
import jenkins.model.Jenkins;
|
||||
import jenkins.fingerprints.GlobalFingerprintConfiguration;
|
||||
import org.jenkinsci.Symbol;
|
||||
import org.kohsuke.accmod.Restricted;
|
||||
import org.kohsuke.accmod.restrictions.NoExternalUse;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Date;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Scans the fingerprint database and remove old records
|
||||
@@ -54,8 +48,6 @@ import java.util.regex.Pattern;
|
||||
@Restricted(NoExternalUse.class)
|
||||
public class FingerprintCleanupThread extends AsyncPeriodicWork {
|
||||
|
||||
static final String FINGERPRINTS_DIR_NAME = "fingerprints";
|
||||
private static final Pattern FINGERPRINT_FILE_PATTERN = Pattern.compile("[0-9a-f]{28}\\.xml");
|
||||
private static final Logger LOGGER = Logger.getLogger(FingerprintCleanupThread.class.getName());
|
||||
|
||||
public FingerprintCleanupThread() {
|
||||
@@ -74,81 +66,21 @@ public class FingerprintCleanupThread extends AsyncPeriodicWork {
|
||||
return ExtensionList.lookup(AsyncPeriodicWork.class).get(FingerprintCleanupThread.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates the cleanup of fingerprints IF enabled.
|
||||
* In case of configured external storage, the file system based storage cleanup is also performed.
|
||||
*/
|
||||
public void execute(TaskListener listener) {
|
||||
Object fingerprintStorage = FingerprintStorage.get();
|
||||
if (!(fingerprintStorage instanceof FileFingerprintStorage)) {
|
||||
LOGGER.fine("External fingerprint storage is configured. Skipping execution");
|
||||
if (GlobalFingerprintConfiguration.get().isFingerprintCleanupDisabled()) {
|
||||
LOGGER.fine("Fingerprint cleanup is disabled. Skipping execution");
|
||||
return;
|
||||
}
|
||||
FingerprintStorage.get().iterateAndCleanupFingerprints(listener);
|
||||
|
||||
int numFiles = 0;
|
||||
|
||||
File root = new File(getRootDir(), FINGERPRINTS_DIR_NAME);
|
||||
File[] files1 = root.listFiles(f -> f.isDirectory() && f.getName().length()==2);
|
||||
if(files1!=null) {
|
||||
for (File file1 : files1) {
|
||||
File[] files2 = file1.listFiles(f -> f.isDirectory() && f.getName().length()==2);
|
||||
for(File file2 : files2) {
|
||||
File[] files3 = file2.listFiles(f -> f.isFile() && FINGERPRINT_FILE_PATTERN.matcher(f.getName()).matches());
|
||||
for(File file3 : files3) {
|
||||
if(check(file3, listener))
|
||||
numFiles++;
|
||||
}
|
||||
deleteIfEmpty(file2);
|
||||
}
|
||||
deleteIfEmpty(file1);
|
||||
}
|
||||
if (!(FingerprintStorage.get() instanceof FileFingerprintStorage) &&
|
||||
FingerprintStorage.getFileFingerprintStorage().isReady()) {
|
||||
FileFingerprintStorage.getFileFingerprintStorage().iterateAndCleanupFingerprints(listener);
|
||||
}
|
||||
|
||||
listener.getLogger().println("Cleaned up "+numFiles+" records");
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a directory if it's empty.
|
||||
*/
|
||||
private void deleteIfEmpty(File dir) {
|
||||
String[] r = dir.list();
|
||||
if(r==null) return; // can happen in a rare occasion
|
||||
if(r.length==0)
|
||||
dir.delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Examines the file and returns true if a file was deleted.
|
||||
*/
|
||||
private boolean check(File fingerprintFile, TaskListener listener) {
|
||||
try {
|
||||
Fingerprint fp = loadFingerprint(fingerprintFile);
|
||||
if (fp == null || (!fp.isAlive() && fp.getFacetBlockingDeletion() == null) ) {
|
||||
listener.getLogger().println("deleting obsolete " + fingerprintFile);
|
||||
fingerprintFile.delete();
|
||||
return true;
|
||||
} else {
|
||||
if (!fp.isAlive()) {
|
||||
FingerprintFacet deletionBlockerFacet = fp.getFacetBlockingDeletion();
|
||||
listener.getLogger().println(deletionBlockerFacet.getClass().getName() + " created on " + new Date(deletionBlockerFacet.getTimestamp()) + " blocked deletion of " + fingerprintFile);
|
||||
}
|
||||
// get the fingerprint in the official map so have the changes visible to Jenkins
|
||||
// otherwise the mutation made in FingerprintMap can override our trimming.
|
||||
fp = getFingerprint(fp);
|
||||
return fp.trim();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Functions.printStackTrace(e, listener.error("Failed to process " + fingerprintFile));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected Fingerprint loadFingerprint(File fingerprintFile) throws IOException {
|
||||
return FileFingerprintStorage.load(fingerprintFile);
|
||||
}
|
||||
|
||||
protected Fingerprint getFingerprint(Fingerprint fp) throws IOException {
|
||||
return Jenkins.get()._getFingerprint(fp.getHashString());
|
||||
}
|
||||
|
||||
protected File getRootDir() {
|
||||
return Jenkins.get().getRootDir();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -25,14 +25,17 @@ package jenkins.fingerprints;
|
||||
|
||||
import com.thoughtworks.xstream.converters.basic.DateConverter;
|
||||
import hudson.Extension;
|
||||
import hudson.Functions;
|
||||
import hudson.Util;
|
||||
import hudson.XmlFile;
|
||||
import hudson.model.Fingerprint;
|
||||
import hudson.model.TaskListener;
|
||||
import hudson.model.listeners.SaveableListener;
|
||||
import hudson.util.AtomicFileWriter;
|
||||
|
||||
import edu.umd.cs.findbugs.annotations.CheckForNull;
|
||||
import edu.umd.cs.findbugs.annotations.NonNull;
|
||||
import jenkins.model.FingerprintFacet;
|
||||
import jenkins.model.Jenkins;
|
||||
import org.jenkinsci.Symbol;
|
||||
import org.kohsuke.accmod.Restricted;
|
||||
@@ -44,9 +47,11 @@ import java.io.EOFException;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Default file system storage implementation for fingerprints.
|
||||
@@ -60,6 +65,8 @@ public class FileFingerprintStorage extends FingerprintStorage {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(FileFingerprintStorage.class.getName());
|
||||
private static final DateConverter DATE_CONVERTER = new DateConverter();
|
||||
public static final String FINGERPRINTS_DIR_NAME = "fingerprints";
|
||||
private static final Pattern FINGERPRINT_FILE_PATTERN = Pattern.compile("[0-9a-f]{28}\\.xml");
|
||||
|
||||
@DataBoundConstructor
|
||||
public FileFingerprintStorage () {}
|
||||
@@ -216,6 +223,56 @@ public class FileFingerprintStorage extends FingerprintStorage {
|
||||
return new File(Jenkins.get().getRootDir(),"fingerprints").exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform Fingerprint cleanup.
|
||||
*/
|
||||
@Override
|
||||
public void iterateAndCleanupFingerprints(TaskListener taskListener) {
|
||||
int numFiles = 0;
|
||||
|
||||
File root = new File(getRootDir(), FINGERPRINTS_DIR_NAME);
|
||||
File[] files1 = root.listFiles(f -> f.isDirectory() && f.getName().length()==2);
|
||||
if(files1!=null) {
|
||||
for (File file1 : files1) {
|
||||
File[] files2 = file1.listFiles(f -> f.isDirectory() && f.getName().length()==2);
|
||||
for(File file2 : files2) {
|
||||
File[] files3 = file2.listFiles(f -> f.isFile() && FINGERPRINT_FILE_PATTERN.matcher(f.getName()).matches());
|
||||
for(File file3 : files3) {
|
||||
if(cleanFingerprint(file3, taskListener))
|
||||
numFiles++;
|
||||
}
|
||||
deleteIfEmpty(file2);
|
||||
}
|
||||
deleteIfEmpty(file1);
|
||||
}
|
||||
}
|
||||
|
||||
taskListener.getLogger().println("Cleaned up "+numFiles+" records");
|
||||
}
|
||||
|
||||
private boolean cleanFingerprint(File fingerprintFile, TaskListener listener) {
|
||||
try {
|
||||
Fingerprint fp = loadFingerprint(fingerprintFile);
|
||||
if (fp == null || (!fp.isAlive() && fp.getFacetBlockingDeletion() == null) ) {
|
||||
listener.getLogger().println("deleting obsolete " + fingerprintFile);
|
||||
fingerprintFile.delete();
|
||||
return true;
|
||||
} else {
|
||||
if (!fp.isAlive()) {
|
||||
FingerprintFacet deletionBlockerFacet = fp.getFacetBlockingDeletion();
|
||||
listener.getLogger().println(deletionBlockerFacet.getClass().getName() + " created on " + new Date(deletionBlockerFacet.getTimestamp()) + " blocked deletion of " + fingerprintFile);
|
||||
}
|
||||
// get the fingerprint in the official map so have the changes visible to Jenkins
|
||||
// otherwise the mutation made in FingerprintMap can override our trimming.
|
||||
fp = getFingerprint(fp);
|
||||
return fp.trim();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Functions.printStackTrace(e, listener.error("Failed to process " + fingerprintFile));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The file we save our configuration.
|
||||
*/
|
||||
@@ -261,6 +318,28 @@ public class FileFingerprintStorage extends FingerprintStorage {
|
||||
return buf.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a directory if it's empty.
|
||||
*/
|
||||
private void deleteIfEmpty(File dir) {
|
||||
String[] r = dir.list();
|
||||
if(r==null) return; // can happen in a rare occasion
|
||||
if(r.length==0)
|
||||
dir.delete();
|
||||
}
|
||||
|
||||
protected Fingerprint loadFingerprint(File fingerprintFile) throws IOException {
|
||||
return FileFingerprintStorage.load(fingerprintFile);
|
||||
}
|
||||
|
||||
protected Fingerprint getFingerprint(Fingerprint fp) throws IOException {
|
||||
return Jenkins.get()._getFingerprint(fp.getHashString());
|
||||
}
|
||||
|
||||
protected File getRootDir() {
|
||||
return Jenkins.get().getRootDir();
|
||||
}
|
||||
|
||||
@Extension
|
||||
public static class DescriptorImpl extends FingerprintStorageDescriptor {
|
||||
|
||||
|
||||
@@ -24,13 +24,19 @@
|
||||
package jenkins.fingerprints;
|
||||
|
||||
import edu.umd.cs.findbugs.annotations.CheckForNull;
|
||||
import edu.umd.cs.findbugs.annotations.NonNull;
|
||||
import hudson.ExtensionList;
|
||||
import hudson.ExtensionPoint;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Date;
|
||||
|
||||
import hudson.Functions;
|
||||
import hudson.model.AbstractDescribableImpl;
|
||||
import hudson.model.Fingerprint;
|
||||
import hudson.model.TaskListener;
|
||||
import jenkins.model.FingerprintFacet;
|
||||
import jenkins.model.Jenkins;
|
||||
import org.kohsuke.accmod.restrictions.Beta;
|
||||
import org.kohsuke.accmod.Restricted;
|
||||
|
||||
@@ -49,6 +55,13 @@ public abstract class FingerprintStorage extends AbstractDescribableImpl<Fingerp
|
||||
return ExtensionList.lookupSingleton(GlobalFingerprintConfiguration.class).getStorage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the file system based {@link FileFingerprintStorage} configured on the system.
|
||||
*/
|
||||
public static FingerprintStorage getFileFingerprintStorage() {
|
||||
return ExtensionList.lookup(FingerprintStorage.class).get(ExtensionList.lookup(FingerprintStorage.class).size()-1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the given Fingerprint in the storage.
|
||||
* This acts as a blocking operation. For file system based default storage, throws IOException when it fails.
|
||||
@@ -80,8 +93,58 @@ public abstract class FingerprintStorage extends AbstractDescribableImpl<Fingerp
|
||||
*/
|
||||
public abstract boolean isReady();
|
||||
|
||||
/**
|
||||
* Iterates a set of fingerprints, and cleans them up. Cleaning up a fingerprint implies deleting the builds
|
||||
* associated with the fingerprints, once they are no longer available on the system. If all the builds have been
|
||||
* deleted, the fingerprint itself is deleted.
|
||||
*
|
||||
* This method is called periodically by {@link hudson.model.FingerprintCleanupThread}.
|
||||
* For reference, see {@link FileFingerprintStorage#iterateAndCleanupFingerprints(TaskListener)}
|
||||
* For cleaning up the fingerprint {@link #cleanFingerprint(Fingerprint, TaskListener)} may be used.
|
||||
*
|
||||
* @since TODO
|
||||
*/
|
||||
public abstract void iterateAndCleanupFingerprints(TaskListener taskListener);
|
||||
|
||||
/**
|
||||
* This method performs the cleanup of the given fingerprint.
|
||||
*
|
||||
* @since TODO
|
||||
*/
|
||||
public boolean cleanFingerprint(@NonNull Fingerprint fingerprint, TaskListener taskListener) {
|
||||
try {
|
||||
if (!fingerprint.isAlive() && fingerprint.getFacetBlockingDeletion() == null) {
|
||||
taskListener.getLogger().println("deleting obsolete " + fingerprint.toString());
|
||||
Fingerprint.delete(fingerprint.getHashString());
|
||||
return true;
|
||||
} else {
|
||||
if (!fingerprint.isAlive()) {
|
||||
FingerprintFacet deletionBlockerFacet = fingerprint.getFacetBlockingDeletion();
|
||||
taskListener.getLogger().println(deletionBlockerFacet.getClass().getName() + " created on " +
|
||||
new Date(deletionBlockerFacet.getTimestamp()) + " blocked deletion of " +
|
||||
fingerprint.getHashString());
|
||||
}
|
||||
// get the fingerprint in the official map so have the changes visible to Jenkins
|
||||
// otherwise the mutation made in FingerprintMap can override our trimming.
|
||||
fingerprint = getFingerprint(fingerprint);
|
||||
return fingerprint.trim();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Functions.printStackTrace(e, taskListener.error("Failed to process " + fingerprint.getHashString()));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected Fingerprint getFingerprint(Fingerprint fp) throws IOException {
|
||||
return Jenkins.get()._getFingerprint(fp.getHashString());
|
||||
}
|
||||
|
||||
/**
|
||||
* @since TODO
|
||||
*/
|
||||
@Override public FingerprintStorageDescriptor getDescriptor() {
|
||||
return (FingerprintStorageDescriptor) super.getDescriptor();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -43,6 +43,7 @@ public class GlobalFingerprintConfiguration extends GlobalConfiguration {
|
||||
|
||||
private FingerprintStorage storage = ExtensionList.lookupSingleton(FileFingerprintStorage.class);
|
||||
private static final Logger LOGGER = Logger.getLogger(GlobalFingerprintConfiguration.class.getName());
|
||||
private boolean fingerprintCleanupDisabled;
|
||||
|
||||
public GlobalFingerprintConfiguration() {
|
||||
load();
|
||||
@@ -63,6 +64,15 @@ public class GlobalFingerprintConfiguration extends GlobalConfiguration {
|
||||
fingerprintStorage.getDescriptor().getDisplayName());
|
||||
}
|
||||
|
||||
public boolean isFingerprintCleanupDisabled() {
|
||||
return fingerprintCleanupDisabled;
|
||||
}
|
||||
|
||||
@DataBoundSetter
|
||||
public void setFingerprintCleanupDisabled(boolean fingerprintCleanupDisabled) {
|
||||
this.fingerprintCleanupDisabled = fingerprintCleanupDisabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean configure(StaplerRequest req, JSONObject json) {
|
||||
req.bindJSON(this, json);
|
||||
|
||||
Reference in new issue
Block a user