servers are unreliable for unknown - this allows for a retry count to be specified to accomodate work on such flaky servers. git-svn-id: https://svn.apache.org/repos/asf/ant/core/trunk@278374 13f79535-47bb-0310-9956-ffa450edef68master
@@ -192,6 +192,14 @@ coming from your ftp server (ls -l on the ftp prompt). | |||||
(<em>Note</em>: Ignored on Java 1.1)</td> | (<em>Note</em>: Ignored on Java 1.1)</td> | ||||
<td valign="top" align="center">No; defaults to false.</td> | <td valign="top" align="center">No; defaults to false.</td> | ||||
</tr> | </tr> | ||||
<tr> | |||||
<td valign="top">retriesAllowed</td> | |||||
<td valign="top">Set the number of retries allowed on an file-transfer operation. | |||||
If a number > 0 specified, each file transfer can fail up to that | |||||
many times before the operation is failed. If -1 or "forever" specified, the | |||||
operation will keep trying until it succeeds.</td> | |||||
<td valign="top" align="center">No; defaults to 0</td> | |||||
</tr> | |||||
<tr> | <tr> | ||||
<td colspan="3"> | <td colspan="3"> | ||||
@@ -15,6 +15,7 @@ | |||||
<property name="server.timestamp.granularity.millis" value="60000"/> | <property name="server.timestamp.granularity.millis" value="60000"/> | ||||
<property name="ftp.server.timezone" value="GMT"/> | <property name="ftp.server.timezone" value="GMT"/> | ||||
<property name="ftp.listing.file" value="/dev/null"/> | <property name="ftp.listing.file" value="/dev/null"/> | ||||
<property name="ftp.retries" value="2"/> | |||||
<fileset dir="${tmp.get.dir}" id="fileset-destination-with-selector"> | <fileset dir="${tmp.get.dir}" id="fileset-destination-with-selector"> | ||||
<include name="alpha/**"/> | <include name="alpha/**"/> | ||||
@@ -272,5 +273,17 @@ | |||||
<fileset dir="${tmp.local}"/> | <fileset dir="${tmp.local}"/> | ||||
</ftp> | </ftp> | ||||
</target> | </target> | ||||
<target name="ftp-get-with-selector-retryable"> | |||||
<ftp action="get" | |||||
server="${ftp.host}" | |||||
userid="${ftp.user}" | |||||
password="${ftp.password}" | |||||
separator="${ftp.filesep}" | |||||
remotedir="${tmp.dir}" | |||||
retriesAllowed="${ftp.retries}" | |||||
> | |||||
<fileset refid="fileset-destination-with-selector"/> | |||||
</ftp> | |||||
</target> | |||||
</project> | </project> |
@@ -51,6 +51,8 @@ import org.apache.tools.ant.types.EnumeratedAttribute; | |||||
import org.apache.tools.ant.types.FileSet; | import org.apache.tools.ant.types.FileSet; | ||||
import org.apache.tools.ant.types.selectors.SelectorUtils; | import org.apache.tools.ant.types.selectors.SelectorUtils; | ||||
import org.apache.tools.ant.util.FileUtils; | import org.apache.tools.ant.util.FileUtils; | ||||
import org.apache.tools.ant.util.RetryHandler; | |||||
import org.apache.tools.ant.util.Retryable; | |||||
/** | /** | ||||
* Basic FTP client. Performs the following actions: | * Basic FTP client. Performs the following actions: | ||||
@@ -126,6 +128,7 @@ public class FTP | |||||
private String shortMonthNamesConfig = null; | private String shortMonthNamesConfig = null; | ||||
private Granularity timestampGranularity = Granularity.getDefault(); | private Granularity timestampGranularity = Granularity.getDefault(); | ||||
private boolean isConfigurationSet = false; | private boolean isConfigurationSet = false; | ||||
private int retriesAllowed = 0; | |||||
protected static final String[] ACTION_STRS = { | protected static final String[] ACTION_STRS = { | ||||
"sending", | "sending", | ||||
@@ -1360,6 +1363,37 @@ public class FTP | |||||
} | } | ||||
/** | |||||
* How many times to retry executing FTP command before giving up? | |||||
* Default is 0 - try once and if failure then give up. | |||||
* | |||||
* @param retriesAllowed number of retries to allow. -1 means | |||||
* keep trying forever. "forever" may also be specified as a | |||||
* synonym for -1. | |||||
*/ | |||||
public void setRetriesAllowed(String retriesAllowed) { | |||||
if ("FOREVER".equalsIgnoreCase(retriesAllowed)) { | |||||
this.retriesAllowed = Retryable.RETRY_FOREVER; | |||||
} else { | |||||
try { | |||||
int retries = Integer.parseInt(retriesAllowed); | |||||
if (retries < Retryable.RETRY_FOREVER) { | |||||
throw new BuildException( | |||||
"Invalid value for retriesAllowed attribute: " | |||||
+ retriesAllowed); | |||||
} | |||||
this.retriesAllowed = retries; | |||||
} catch (NumberFormatException px) { | |||||
throw new BuildException( | |||||
"Invalid value for retriesAllowed attribute: " | |||||
+ retriesAllowed); | |||||
} | |||||
} | |||||
} | |||||
/** | /** | ||||
* @return Returns the systemTypeKey. | * @return Returns the systemTypeKey. | ||||
*/ | */ | ||||
@@ -1451,6 +1485,12 @@ public class FTP | |||||
} | } | ||||
} | } | ||||
} | } | ||||
protected void executeRetryable(RetryHandler h, Retryable r, String filename) | |||||
throws IOException | |||||
{ | |||||
h.execute(r, filename); | |||||
} | |||||
/** | /** | ||||
@@ -1465,7 +1505,7 @@ public class FTP | |||||
* @throws IOException if there is a problem reading a file | * @throws IOException if there is a problem reading a file | ||||
* @throws BuildException if there is a problem in the configuration. | * @throws BuildException if there is a problem in the configuration. | ||||
*/ | */ | ||||
protected int transferFiles(FTPClient ftp, FileSet fs) | |||||
protected int transferFiles(final FTPClient ftp, FileSet fs) | |||||
throws IOException, BuildException { | throws IOException, BuildException { | ||||
DirectoryScanner ds; | DirectoryScanner ds; | ||||
if (action == SEND_FILES) { | if (action == SEND_FILES) { | ||||
@@ -1512,38 +1552,51 @@ public class FTP | |||||
} | } | ||||
bw = new BufferedWriter(new FileWriter(listing)); | bw = new BufferedWriter(new FileWriter(listing)); | ||||
} | } | ||||
RetryHandler h = new RetryHandler(this.retriesAllowed, this); | |||||
if (action == RM_DIR) { | if (action == RM_DIR) { | ||||
// to remove directories, start by the end of the list | // to remove directories, start by the end of the list | ||||
// the trunk does not let itself be removed before the leaves | // the trunk does not let itself be removed before the leaves | ||||
for (int i = dsfiles.length - 1; i >= 0; i--) { | for (int i = dsfiles.length - 1; i >= 0; i--) { | ||||
rmDir(ftp, dsfiles[i]); | |||||
final String dsfile = dsfiles[i]; | |||||
executeRetryable(h, new Retryable() { | |||||
public void execute() throws IOException { | |||||
rmDir(ftp, dsfile); | |||||
} | |||||
}, dsfile); | |||||
} | } | ||||
} else { | } else { | ||||
final BufferedWriter fbw = bw; | |||||
final String fdir = dir; | |||||
if (this.newerOnly) { | if (this.newerOnly) { | ||||
this.granularityMillis = | this.granularityMillis = | ||||
this.timestampGranularity.getMilliseconds(action); | this.timestampGranularity.getMilliseconds(action); | ||||
} | } | ||||
for (int i = 0; i < dsfiles.length; i++) { | for (int i = 0; i < dsfiles.length; i++) { | ||||
switch (action) { | |||||
case SEND_FILES: | |||||
sendFile(ftp, dir, dsfiles[i]); | |||||
break; | |||||
case GET_FILES: | |||||
getFile(ftp, dir, dsfiles[i]); | |||||
break; | |||||
case DEL_FILES: | |||||
delFile(ftp, dsfiles[i]); | |||||
break; | |||||
case LIST_FILES: | |||||
listFile(ftp, bw, dsfiles[i]); | |||||
break; | |||||
case CHMOD: | |||||
doSiteCommand(ftp, "chmod " + chmod + " " + resolveFile(dsfiles[i])); | |||||
transferred++; | |||||
break; | |||||
default: | |||||
throw new BuildException("unknown ftp action " + action); | |||||
} | |||||
final String dsfile = dsfiles[i]; | |||||
executeRetryable(h, new Retryable() { | |||||
public void execute() throws IOException { | |||||
switch (action) { | |||||
case SEND_FILES: | |||||
sendFile(ftp, fdir, dsfile); | |||||
break; | |||||
case GET_FILES: | |||||
getFile(ftp, fdir, dsfile); | |||||
break; | |||||
case DEL_FILES: | |||||
delFile(ftp, dsfile); | |||||
break; | |||||
case LIST_FILES: | |||||
listFile(ftp, fbw, dsfile); | |||||
break; | |||||
case CHMOD: | |||||
doSiteCommand(ftp, "chmod " + chmod + " " + resolveFile(dsfile)); | |||||
transferred++; | |||||
break; | |||||
default: | |||||
throw new BuildException("unknown ftp action " + action); | |||||
} | |||||
} | |||||
}, dsfile); | |||||
} | } | ||||
} | } | ||||
} finally { | } finally { | ||||
@@ -2198,7 +2251,13 @@ public class FTP | |||||
// directory is the directory to create. | // directory is the directory to create. | ||||
if (action == MK_DIR) { | if (action == MK_DIR) { | ||||
makeRemoteDir(ftp, remotedir); | |||||
RetryHandler h = new RetryHandler(this.retriesAllowed, this); | |||||
final FTPClient lftp = ftp; | |||||
executeRetryable(h, new Retryable() { | |||||
public void execute() throws IOException { | |||||
makeRemoteDir(lftp, remotedir); | |||||
} | |||||
}, remotedir); | |||||
} else { | } else { | ||||
if (remotedir != null) { | if (remotedir != null) { | ||||
log("changing the remote directory", Project.MSG_VERBOSE); | log("changing the remote directory", Project.MSG_VERBOSE); | ||||
@@ -0,0 +1,72 @@ | |||||
/* | |||||
* Copyright 2005 The Apache Software Foundation | |||||
* | |||||
* Licensed under the Apache License, Version 2.0 (the "License"); | |||||
* you may not use this file except in compliance with the License. | |||||
* You may obtain a copy of the License at | |||||
* | |||||
* http://www.apache.org/licenses/LICENSE-2.0 | |||||
* | |||||
* Unless required by applicable law or agreed to in writing, software | |||||
* distributed under the License is distributed on an "AS IS" BASIS, | |||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
* See the License for the specific language governing permissions and | |||||
* limitations under the License. | |||||
* | |||||
*/ | |||||
package org.apache.tools.ant.util; | |||||
import java.io.IOException; | |||||
import org.apache.tools.ant.Project; | |||||
import org.apache.tools.ant.Task; | |||||
/** | |||||
* A simple utility class to take a piece of code (that implements | |||||
* <code>Retryable</code> interface) and executes that with possibility to | |||||
* retry the execution in case of IOException. | |||||
*/ | |||||
public class RetryHandler { | |||||
private int retriesAllowed = 0; | |||||
private Task task; | |||||
/** | |||||
* Create a new RetryingHandler. | |||||
* | |||||
* @param retriesAllowed how many times to retry | |||||
* @param task the Ant task that is is executed from, used for logging only | |||||
*/ | |||||
public RetryHandler(int retriesAllowed, Task task) { | |||||
this.retriesAllowed = retriesAllowed; | |||||
this.task = task; | |||||
} | |||||
/** | |||||
* Execute the <code>Retryable</code> code with specified number of retries. | |||||
* | |||||
* @param exe the code to execute | |||||
* @param desc some descriptive text for this piece of code, used for logging | |||||
* @throws IOException if the number of retries has exceeded the allowed limit | |||||
*/ | |||||
public void execute(Retryable exe, String desc) throws IOException { | |||||
int retries = 0; | |||||
while (true) { | |||||
try { | |||||
exe.execute(); | |||||
break; | |||||
} catch (IOException e) { | |||||
retries++; | |||||
if (retries > this.retriesAllowed && this.retriesAllowed > -1) { | |||||
task.log("try #" + retries + ": IO error (" | |||||
+ desc + "), number of maximum retries reached (" | |||||
+ this.retriesAllowed + "), giving up", Project.MSG_WARN); | |||||
throw e; | |||||
} else { | |||||
task.log("try #" + retries + ": IO error (" + desc + "), retrying", Project.MSG_WARN); | |||||
} | |||||
} | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,32 @@ | |||||
/* | |||||
* Copyright 2005 The Apache Software Foundation | |||||
* | |||||
* Licensed under the Apache License, Version 2.0 (the "License"); | |||||
* you may not use this file except in compliance with the License. | |||||
* You may obtain a copy of the License at | |||||
* | |||||
* http://www.apache.org/licenses/LICENSE-2.0 | |||||
* | |||||
* Unless required by applicable law or agreed to in writing, software | |||||
* distributed under the License is distributed on an "AS IS" BASIS, | |||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
* See the License for the specific language governing permissions and | |||||
* limitations under the License. | |||||
* | |||||
*/ | |||||
package org.apache.tools.ant.util; | |||||
import java.io.IOException; | |||||
/** | |||||
* Simple interface for executing a piece of code. Used for writing anonymous inner | |||||
* classes in FTP task for retry-on-IOException behaviour. | |||||
* | |||||
* @see RetryHandler | |||||
*/ | |||||
public interface Retryable { | |||||
public static final int RETRY_FOREVER = -1; | |||||
void execute() throws IOException; | |||||
} |
@@ -21,17 +21,21 @@ import java.io.IOException; | |||||
import java.util.Arrays; | import java.util.Arrays; | ||||
import java.util.HashMap; | import java.util.HashMap; | ||||
import java.util.Map; | import java.util.Map; | ||||
import java.util.Random; | |||||
import java.util.Vector; | import java.util.Vector; | ||||
import org.apache.commons.net.ftp.FTPClient; | import org.apache.commons.net.ftp.FTPClient; | ||||
import org.apache.tools.ant.BuildEvent; | import org.apache.tools.ant.BuildEvent; | ||||
import org.apache.tools.ant.BuildException; | import org.apache.tools.ant.BuildException; | ||||
import org.apache.tools.ant.BuildFileTest; | import org.apache.tools.ant.BuildFileTest; | ||||
import org.apache.tools.ant.ComponentHelper; | |||||
import org.apache.tools.ant.DefaultLogger; | import org.apache.tools.ant.DefaultLogger; | ||||
import org.apache.tools.ant.DirectoryScanner; | import org.apache.tools.ant.DirectoryScanner; | ||||
import org.apache.tools.ant.Project; | import org.apache.tools.ant.Project; | ||||
import org.apache.tools.ant.taskdefs.condition.Os; | import org.apache.tools.ant.taskdefs.condition.Os; | ||||
import org.apache.tools.ant.types.FileSet; | import org.apache.tools.ant.types.FileSet; | ||||
import org.apache.tools.ant.util.RetryHandler; | |||||
import org.apache.tools.ant.util.Retryable; | |||||
import org.apache.tools.ant.util.regexp.RegexpMatcher; | import org.apache.tools.ant.util.regexp.RegexpMatcher; | ||||
import org.apache.tools.ant.util.regexp.RegexpMatcherFactory; | import org.apache.tools.ant.util.regexp.RegexpMatcherFactory; | ||||
@@ -779,6 +783,88 @@ public class FTPTest extends BuildFileTest{ | |||||
public String resolveFile(String file) { | public String resolveFile(String file) { | ||||
return super.resolveFile(file); | return super.resolveFile(file); | ||||
} | } | ||||
} | |||||
public abstract static class myRetryableFTP extends FTP { | |||||
private final int numberOfFailuresToSimulate; | |||||
private int simulatedFailuresLeft; | |||||
protected myRetryableFTP(int numberOfFailuresToSimulate) { | |||||
this.numberOfFailuresToSimulate = numberOfFailuresToSimulate; | |||||
this.simulatedFailuresLeft = numberOfFailuresToSimulate; | |||||
} | |||||
protected void getFile(FTPClient ftp, String dir, String filename) | |||||
throws IOException, BuildException | |||||
{ | |||||
if (this.simulatedFailuresLeft > 0) { | |||||
this.simulatedFailuresLeft--; | |||||
throw new IOException("Simulated failure for testing"); | |||||
} | |||||
super.getFile(ftp, dir, filename); | |||||
} | |||||
protected void executeRetryable(RetryHandler h, Retryable r, | |||||
String filename) throws IOException | |||||
{ | |||||
this.simulatedFailuresLeft = this.numberOfFailuresToSimulate; | |||||
super.executeRetryable(h, r, filename); | |||||
} | |||||
} | } | ||||
public static class oneFailureFTP extends myRetryableFTP { | |||||
public oneFailureFTP() { | |||||
super(1); | |||||
} | |||||
} | |||||
public static class twoFailureFTP extends myRetryableFTP { | |||||
public twoFailureFTP() { | |||||
super(2); | |||||
} | |||||
} | |||||
public static class threeFailureFTP extends myRetryableFTP { | |||||
public threeFailureFTP() { | |||||
super(3); | |||||
} | |||||
} | |||||
public static class randomFailureFTP extends myRetryableFTP { | |||||
public randomFailureFTP() { | |||||
super(new Random(30000).nextInt()); | |||||
} | |||||
} | |||||
public void testGetWithSelectorRetryable1() { | |||||
getProject().addTaskDefinition("ftp", oneFailureFTP.class); | |||||
try { | |||||
getProject().executeTarget("ftp-get-with-selector-retryable"); | |||||
} catch (BuildException bx) { | |||||
fail("Two retries expected, failed after one."); | |||||
} | |||||
} | |||||
public void testGetWithSelectorRetryable2() { | |||||
getProject().addTaskDefinition("ftp", twoFailureFTP.class); | |||||
try { | |||||
getProject().executeTarget("ftp-get-with-selector-retryable"); | |||||
} catch (BuildException bx) { | |||||
fail("Two retries expected, failed after two."); | |||||
} | |||||
} | |||||
public void testGetWithSelectorRetryable3() { | |||||
getProject().addTaskDefinition("ftp", threeFailureFTP.class); | |||||
try { | |||||
getProject().executeTarget("ftp-get-with-selector-retryable"); | |||||
fail("Two retries expected, continued after two."); | |||||
} catch (BuildException bx) { | |||||
} | |||||
} | |||||
public void testGetWithSelectorRetryableRandom() { | |||||
getProject().addTaskDefinition("ftp", threeFailureFTP.class); | |||||
try { | |||||
getProject().setProperty("ftp.retries", "forever"); | |||||
getProject().executeTarget("ftp-get-with-selector-retryable"); | |||||
} catch (BuildException bx) { | |||||
fail("Retry forever specified, but failed."); | |||||
} | |||||
} | |||||
} | } |