From 4a84567693b6da26c9c0c025be1b9993f658dcb2 Mon Sep 17 00:00:00 2001 From: Noble Paul Date: Wed, 7 May 2014 10:12:29 +0000 Subject: [PATCH] SOLR-5340: Add support for named snapshots git-svn-id: https://svn.apache.org/repos/asf/lucene/dev/trunk@1592957 13f79535-47bb-0310-9956-ffa450edef68 --- solr/CHANGES.txt | 2 + .../solr/handler/ReplicationHandler.java | 27 +++- .../org/apache/solr/handler/SnapShooter.java | 111 +++++++++++--- .../solr/handler/TestReplicationHandler.java | 142 ++++++++++++++---- 4 files changed, 228 insertions(+), 54 deletions(-) diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt index c6465614233..062c59e10d7 100644 --- a/solr/CHANGES.txt +++ b/solr/CHANGES.txt @@ -168,6 +168,8 @@ Other Changes * SOLR_3671: Fix DIHWriter interface usage so users may implement writers that output documents to a location external to Solr (ex. a NoSql db). (Roman Chyla via James Dyer) +* SOLR-5340: Add support for named snapshots (Varun Thacker via Noble Paul) + Build --------------------- diff --git a/solr/core/src/java/org/apache/solr/handler/ReplicationHandler.java b/solr/core/src/java/org/apache/solr/handler/ReplicationHandler.java index 5298a4c9c5b..a89f2455e22 100644 --- a/solr/core/src/java/org/apache/solr/handler/ReplicationHandler.java +++ b/solr/core/src/java/org/apache/solr/handler/ReplicationHandler.java @@ -208,7 +208,10 @@ public class ReplicationHandler extends RequestHandlerBase implements SolrCoreAw } else if (command.equals(CMD_GET_FILE_LIST)) { getFileList(solrParams, rsp); } else if (command.equalsIgnoreCase(CMD_BACKUP)) { - doSnapShoot(new ModifiableSolrParams(solrParams), rsp,req); + doSnapShoot(new ModifiableSolrParams(solrParams), rsp, req); + rsp.add(STATUS, OK_STATUS); + } else if (command.equalsIgnoreCase(CMD_DELETE_BACKUP)) { + deleteSnapshot(new ModifiableSolrParams(solrParams), rsp, req); rsp.add(STATUS, OK_STATUS); } else if (command.equalsIgnoreCase(CMD_FETCH_INDEX)) { String masterUrl = solrParams.get(MASTER_URL); @@ -257,7 +260,7 @@ public class ReplicationHandler extends RequestHandlerBase implements SolrCoreAw } else if (command.equals(CMD_SHOW_COMMITS)) { rsp.add(CMD_SHOW_COMMITS, getCommits()); } else if (command.equals(CMD_DETAILS)) { - rsp.add(CMD_DETAILS, getReplicationDetails(solrParams.getBool("slave",true))); + rsp.add(CMD_DETAILS, getReplicationDetails(solrParams.getBool("slave", true))); RequestHandlerUtils.addExperimentalFormatWarning(rsp); } else if (CMD_ENABLE_REPL.equalsIgnoreCase(command)) { replicationEnabled.set(true); @@ -268,6 +271,17 @@ public class ReplicationHandler extends RequestHandlerBase implements SolrCoreAw } } + private void deleteSnapshot(ModifiableSolrParams params, SolrQueryResponse rsp, SolrQueryRequest req) { + String name = params.get("name"); + if(name == null) { + throw new SolrException(ErrorCode.BAD_REQUEST, "Missing mandatory param: name"); + } + + SnapShooter snapShooter = new SnapShooter(core, params.get("location"), params.get("name")); + snapShooter.validateDeleteSnapshot(); + snapShooter.deleteSnapAsync(this); + } + private List> getCommits() { Map commits = core.getDeletionPolicy().getCommits(); List> l = new ArrayList<>(); @@ -359,8 +373,9 @@ public class ReplicationHandler extends RequestHandlerBase implements SolrCoreAw } // small race here before the commit point is saved - new SnapShooter(core, params.get("location")).createSnapAsync( - indexCommit, numberToKeep, this); + SnapShooter snapShooter = new SnapShooter(core, params.get("location"), params.get("name")); + snapShooter.validateCreateSnapshot(); + snapShooter.createSnapAsync(indexCommit, numberToKeep, this); } catch (Exception e) { LOG.warn("Exception during creating a snapshot", e); @@ -1067,7 +1082,7 @@ public class ReplicationHandler extends RequestHandlerBase implements SolrCoreAw if (numberToKeep < 1) { numberToKeep = Integer.MAX_VALUE; } - SnapShooter snapShooter = new SnapShooter(core, null); + SnapShooter snapShooter = new SnapShooter(core, null, null); snapShooter.createSnapAsync(currentCommitPoint, numberToKeep, ReplicationHandler.this); } catch (Exception e) { LOG.error("Exception while snapshooting", e); @@ -1305,6 +1320,8 @@ public class ReplicationHandler extends RequestHandlerBase implements SolrCoreAw public static final String CMD_SHOW_COMMITS = "commits"; + public static final String CMD_DELETE_BACKUP = "deletebackup"; + public static final String GENERATION = "generation"; public static final String OFFSET = "offset"; diff --git a/solr/core/src/java/org/apache/solr/handler/SnapShooter.java b/solr/core/src/java/org/apache/solr/handler/SnapShooter.java index 14df3d39c18..83bec1d0c86 100644 --- a/solr/core/src/java/org/apache/solr/handler/SnapShooter.java +++ b/solr/core/src/java/org/apache/solr/handler/SnapShooter.java @@ -33,6 +33,7 @@ import org.apache.lucene.store.Directory; import org.apache.lucene.store.FSDirectory; import org.apache.lucene.store.Lock; import org.apache.lucene.store.SimpleFSLockFactory; +import org.apache.solr.common.SolrException; import org.apache.solr.common.util.NamedList; import org.apache.solr.core.DirectoryFactory; import org.apache.solr.core.DirectoryFactory.DirContext; @@ -52,8 +53,12 @@ public class SnapShooter { private String snapDir = null; private SolrCore solrCore; private SimpleFSLockFactory lockFactory; - - public SnapShooter(SolrCore core, String location) { + private String snapshotName = null; + private String directoryName = null; + private File snapShotDir = null; + private Lock lock = null; + + public SnapShooter(SolrCore core, String location, String snapshotName) { solrCore = core; if (location == null) snapDir = core.getDataDir(); else { @@ -63,10 +68,14 @@ public class SnapShooter { if (!dir.exists()) dir.mkdirs(); } lockFactory = new SimpleFSLockFactory(snapDir); - } - - void createSnapAsync(final IndexCommit indexCommit, final ReplicationHandler replicationHandler) { - createSnapAsync(indexCommit, Integer.MAX_VALUE, replicationHandler); + this.snapshotName = snapshotName; + + if(snapshotName != null) { + directoryName = "snapshot." + snapshotName; + } else { + SimpleDateFormat fmt = new SimpleDateFormat(DATE_FMT, Locale.ROOT); + directoryName = "snapshot." + fmt.format(new Date()); + } } void createSnapAsync(final IndexCommit indexCommit, final int numberToKeep, final ReplicationHandler replicationHandler) { @@ -75,34 +84,67 @@ public class SnapShooter { new Thread() { @Override public void run() { - createSnapshot(indexCommit, numberToKeep, replicationHandler); + if(snapshotName != null) { + createSnapshot(indexCommit, replicationHandler); + } else { + deleteOldBackups(numberToKeep); + createSnapshot(indexCommit, replicationHandler); + } } }.start(); } - void createSnapshot(final IndexCommit indexCommit, int numberToKeep, ReplicationHandler replicationHandler) { + public void validateDeleteSnapshot() { + boolean dirFound = false; + File[] files = new File(snapDir).listFiles(); + for(File f : files) { + if (f.getName().equals("snapshot." + snapshotName)) { + dirFound = true; + break; + } + } + if(dirFound == false) { + throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Snapshot cannot be found in directory: " + snapDir); + } + } + + protected void deleteSnapAsync(final ReplicationHandler replicationHandler) { + new Thread() { + @Override + public void run() { + deleteNamedSnapshot(replicationHandler); + } + }.start(); + } + + void validateCreateSnapshot() throws IOException { + + Lock lock = lockFactory.makeLock(directoryName + ".lock"); + if (lock.isLocked()) { + throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, + "Unable to acquire lock for snapshot directory: " + snapShotDir.getAbsolutePath()); + } + snapShotDir = new File(snapDir, directoryName); + if (snapShotDir.exists()) { + throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, + "Snapshot directory already exists: " + snapShotDir.getAbsolutePath()); + } + if (!snapShotDir.mkdirs()) { + throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, + "Unable to create snapshot directory: " + snapShotDir.getAbsolutePath()); + } + } + + void createSnapshot(final IndexCommit indexCommit, ReplicationHandler replicationHandler) { LOG.info("Creating backup snapshot..."); NamedList details = new NamedList<>(); details.add("startTime", new Date().toString()); - File snapShotDir = null; String directoryName = null; - Lock lock = null; + try { - if(numberToKeep files = indexCommit.getFileNames(); FileCopier fileCopier = new FileCopier(); - + Directory dir = solrCore.getDirectoryFactory().get(solrCore.getIndexDir(), DirContext.DEFAULT, solrCore.getSolrConfig().indexConfig.lockType); try { fileCopier.copyFiles(dir, files, snapShotDir); @@ -129,6 +171,7 @@ public class SnapShooter { } } } + private void deleteOldBackups(int numberToKeep) { File[] files = new File(snapDir).listFiles(); List dirs = new ArrayList<>(); @@ -138,6 +181,10 @@ public class SnapShooter { dirs.add(obd); } } + if(numberToKeep > dirs.size()) { + return; + } + Collections.sort(dirs); int i=1; for(OldBackupDirectory dir : dirs) { @@ -146,6 +193,24 @@ public class SnapShooter { } } } + + protected void deleteNamedSnapshot(ReplicationHandler replicationHandler) { + LOG.info("Deleting snapshot: " + snapshotName); + + NamedList details = new NamedList<>(); + boolean isSuccess = false; + File f = new File(snapDir, "snapshot." + snapshotName); + isSuccess = SnapPuller.delTree(f); + + if(isSuccess) { + details.add("status", "success"); + } else { + details.add("status", "Unable to delete snapshot: " + snapshotName); + LOG.warn("Unable to delete snapshot: " + snapshotName); + } + replicationHandler.snapShootDetails = details; + } + private class OldBackupDirectory implements Comparable{ File dir; Date timestamp; diff --git a/solr/core/src/test/org/apache/solr/handler/TestReplicationHandler.java b/solr/core/src/test/org/apache/solr/handler/TestReplicationHandler.java index 08e20773aed..cb88f987067 100644 --- a/solr/core/src/test/org/apache/solr/handler/TestReplicationHandler.java +++ b/solr/core/src/test/org/apache/solr/handler/TestReplicationHandler.java @@ -1319,7 +1319,7 @@ public class TestReplicationHandler extends SolrTestCaseJ4 { addNumberToKeepInRequest = false; backupKeepParamName = ReplicationHandler.NUMBER_BACKUPS_TO_KEEP_INIT_PARAM; } - + masterJetty.stop(); master.copyConfigFile(CONF_DIR + configFile, "solrconfig.xml"); @@ -1339,15 +1339,29 @@ public class TestReplicationHandler extends SolrTestCaseJ4 { volatile String fail = null; final boolean addNumberToKeepInRequest; String backupKeepParamName; - BackupThread(boolean addNumberToKeepInRequest, String backupKeepParamName) { + String backupName; + String cmd; + BackupThread(boolean addNumberToKeepInRequest, String backupKeepParamName, String command) { this.addNumberToKeepInRequest = addNumberToKeepInRequest; this.backupKeepParamName = backupKeepParamName; + this.cmd = command; + } + BackupThread(String backupName, String command) { + this.backupName = backupName; + addNumberToKeepInRequest = false; + this.cmd = command; } @Override public void run() { - String masterUrl = - buildUrl(masterJetty.getLocalPort()) + "/replication?command=" + ReplicationHandler.CMD_BACKUP + - (addNumberToKeepInRequest ? "&" + backupKeepParamName + "=1" : ""); + String masterUrl = null; + if(backupName != null) { + masterUrl = buildUrl(masterJetty.getLocalPort()) + "/replication?command=" + cmd + + "&name=" + backupName; + } else { + masterUrl = buildUrl(masterJetty.getLocalPort()) + "/replication?command=" + cmd + + (addNumberToKeepInRequest ? "&" + backupKeepParamName + "=1" : ""); + } + URL url; InputStream stream = null; try { @@ -1362,20 +1376,45 @@ public class TestReplicationHandler extends SolrTestCaseJ4 { }; }; + + class CheckDeleteBackupStatus { + String response = null; + boolean success = false; + String fail = null; + + public void fetchStatus() { + String masterUrl = buildUrl(masterJetty.getLocalPort()) + "/replication?command=" + ReplicationHandler.CMD_DETAILS; + URL url; + InputStream stream = null; + try { + url = new URL(masterUrl); + stream = url.openStream(); + response = IOUtils.toString(stream, "UTF-8"); + if(response.contains("success")) { + success = true; + } + stream.close(); + } catch (Exception e) { + fail = e.getMessage(); + } finally { + IOUtils.closeQuietly(stream); + } + }; + } - class CheckStatus extends Thread { - volatile String fail = null; - volatile String response = null; - volatile boolean success = false; - volatile String backupTimestamp = null; + class CheckBackupStatus { + String fail = null; + String response = null; + boolean success = false; + String backupTimestamp = null; final String lastBackupTimestamp; final Pattern p = Pattern.compile("(.*?)"); - CheckStatus(String lastBackupTimestamp) { + CheckBackupStatus(String lastBackupTimestamp) { this.lastBackupTimestamp = lastBackupTimestamp; } - @Override - public void run() { + + public void fetchStatus() { String masterUrl = buildUrl(masterJetty.getLocalPort()) + "/replication?command=" + ReplicationHandler.CMD_DETAILS; URL url; InputStream stream = null; @@ -1405,29 +1444,41 @@ public class TestReplicationHandler extends SolrTestCaseJ4 { File[] snapDir = new File[2]; String firstBackupTimestamp = null; + boolean namedBackup = true; + String[] backupNames = null; + if(namedBackup) { + backupNames = new String[2]; + } for(int i=0 ; i<2 ; i++) { - BackupThread backupThread = new BackupThread(addNumberToKeepInRequest, backupKeepParamName); + BackupThread backupThread; + final String backupName = TestUtil.randomSimpleString(random(), 1, 20); + if(!namedBackup) { + backupThread = new BackupThread(addNumberToKeepInRequest, backupKeepParamName, ReplicationHandler.CMD_BACKUP); + } else { + backupThread = new BackupThread(backupName, ReplicationHandler.CMD_BACKUP); + backupNames[i] = backupName; + } backupThread.start(); File dataDir = new File(master.getDataDir()); int waitCnt = 0; - CheckStatus checkStatus = new CheckStatus(firstBackupTimestamp); + CheckBackupStatus checkBackupStatus = new CheckBackupStatus(firstBackupTimestamp); while(true) { - checkStatus.run(); - if(checkStatus.fail != null) { - fail(checkStatus.fail); + checkBackupStatus.fetchStatus(); + if(checkBackupStatus.fail != null) { + fail(checkBackupStatus.fail); } - if(checkStatus.success) { + if(checkBackupStatus.success) { if(i==0) { - firstBackupTimestamp = checkStatus.backupTimestamp; + firstBackupTimestamp = checkBackupStatus.backupTimestamp; Thread.sleep(1000); //ensure the next backup will have a different timestamp. } break; } Thread.sleep(200); if(waitCnt == 20) { - fail("Backup success not detected:" + checkStatus.response); + fail("Backup success not detected:" + checkBackupStatus.response); } waitCnt++; } @@ -1435,9 +1486,9 @@ public class TestReplicationHandler extends SolrTestCaseJ4 { if(backupThread.fail != null) { fail(backupThread.fail); } - - File[] files = dataDir.listFiles(new FilenameFilter() { - + File[] files = null; + if(!namedBackup) { + files = dataDir.listFiles(new FilenameFilter() { @Override public boolean accept(File dir, String name) { if(name.startsWith("snapshot")) { @@ -1446,6 +1497,17 @@ public class TestReplicationHandler extends SolrTestCaseJ4 { return false; } }); + } else { + files = dataDir.listFiles(new FilenameFilter() { + @Override + public boolean accept(File dir, String name) { + if(name.startsWith("snapshot." + backupName)) { + return true; + } + return false; + } + }); + } assertEquals(1, files.length); snapDir[i] = files[0]; Directory dir = new SimpleFSDirectory(snapDir[i].getAbsoluteFile()); @@ -1455,11 +1517,39 @@ public class TestReplicationHandler extends SolrTestCaseJ4 { assertEquals(nDocs, hits.totalHits); reader.close(); dir.close(); + + if(!namedBackup && snapDir[0].exists()) { + fail("The first backup should have been cleaned up because " + backupKeepParamName + " was set to 1."); + } } - if(snapDir[0].exists()) { - fail("The first backup should have been cleaned up because " + backupKeepParamName + " was set to 1."); + + for(int i=0; i<2; i++) { + //Test Deletion of named backup + BackupThread deleteBackupThread = new BackupThread(backupNames[i], ReplicationHandler.CMD_DELETE_BACKUP); + deleteBackupThread.start(); + int waitCnt = 0; + CheckDeleteBackupStatus checkDeleteBackupStatus = new CheckDeleteBackupStatus(); + while(true) { + checkDeleteBackupStatus.fetchStatus(); + if(checkDeleteBackupStatus.fail != null) { + fail(checkDeleteBackupStatus.fail); + } + if(checkDeleteBackupStatus.success) { + break; + } + Thread.sleep(200); + if(waitCnt == 20) { + fail("Delete Backup success not detected:" + checkDeleteBackupStatus.response); + } + waitCnt++; + } + + if(deleteBackupThread.fail != null) { + fail(deleteBackupThread.fail); + } } + //nocommit - Should move this to tearDown as it fails to delete the dir in case it fails? for(int i=0 ; i< snapDir.length ; i++) { AbstractSolrTestCase.recurseDelete(snapDir[i]); // clean up the snap dir }