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> | |||
| <td valign="top" align="center">No; defaults to false.</td> | |||
| </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> | |||
| <td colspan="3"> | |||
| @@ -15,6 +15,7 @@ | |||
| <property name="server.timestamp.granularity.millis" value="60000"/> | |||
| <property name="ftp.server.timezone" value="GMT"/> | |||
| <property name="ftp.listing.file" value="/dev/null"/> | |||
| <property name="ftp.retries" value="2"/> | |||
| <fileset dir="${tmp.get.dir}" id="fileset-destination-with-selector"> | |||
| <include name="alpha/**"/> | |||
| @@ -272,5 +273,17 @@ | |||
| <fileset dir="${tmp.local}"/> | |||
| </ftp> | |||
| </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> | |||
| @@ -51,6 +51,8 @@ import org.apache.tools.ant.types.EnumeratedAttribute; | |||
| import org.apache.tools.ant.types.FileSet; | |||
| import org.apache.tools.ant.types.selectors.SelectorUtils; | |||
| 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: | |||
| @@ -126,6 +128,7 @@ public class FTP | |||
| private String shortMonthNamesConfig = null; | |||
| private Granularity timestampGranularity = Granularity.getDefault(); | |||
| private boolean isConfigurationSet = false; | |||
| private int retriesAllowed = 0; | |||
| protected static final String[] ACTION_STRS = { | |||
| "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. | |||
| */ | |||
| @@ -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 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 { | |||
| DirectoryScanner ds; | |||
| if (action == SEND_FILES) { | |||
| @@ -1512,38 +1552,51 @@ public class FTP | |||
| } | |||
| bw = new BufferedWriter(new FileWriter(listing)); | |||
| } | |||
| RetryHandler h = new RetryHandler(this.retriesAllowed, this); | |||
| if (action == RM_DIR) { | |||
| // to remove directories, start by the end of the list | |||
| // the trunk does not let itself be removed before the leaves | |||
| 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 { | |||
| final BufferedWriter fbw = bw; | |||
| final String fdir = dir; | |||
| if (this.newerOnly) { | |||
| this.granularityMillis = | |||
| this.timestampGranularity.getMilliseconds(action); | |||
| } | |||
| 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 { | |||
| @@ -2198,7 +2251,13 @@ public class FTP | |||
| // directory is the directory to create. | |||
| 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 { | |||
| if (remotedir != null) { | |||
| 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.HashMap; | |||
| import java.util.Map; | |||
| import java.util.Random; | |||
| import java.util.Vector; | |||
| import org.apache.commons.net.ftp.FTPClient; | |||
| import org.apache.tools.ant.BuildEvent; | |||
| import org.apache.tools.ant.BuildException; | |||
| import org.apache.tools.ant.BuildFileTest; | |||
| import org.apache.tools.ant.ComponentHelper; | |||
| import org.apache.tools.ant.DefaultLogger; | |||
| import org.apache.tools.ant.DirectoryScanner; | |||
| import org.apache.tools.ant.Project; | |||
| import org.apache.tools.ant.taskdefs.condition.Os; | |||
| 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.RegexpMatcherFactory; | |||
| @@ -779,6 +783,88 @@ public class FTPTest extends BuildFileTest{ | |||
| public String resolveFile(String 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."); | |||
| } | |||
| } | |||
| } | |||