[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:
stellargo authored and GitHub committed 2020-07-21 06:20:47 +02:00
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);