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."); | |||||
| } | |||||
| } | |||||
| } | } | ||||