001/*
002 * Syncany, www.syncany.org
003 * Copyright (C) 2011-2016 Philipp C. Heckel <philipp.heckel@gmail.com>
004 *
005 * This program is free software: you can redistribute it and/or modify
006 * it under the terms of the GNU General Public License as published by
007 * the Free Software Foundation, either version 3 of the License, or
008 * (at your option) any later version.
009 *
010 * This program is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
013 * GNU General Public License for more details.
014 *
015 * You should have received a copy of the GNU General Public License
016 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
017 */
018package org.syncany.plugins.transfer.features;
019
020import java.io.File;
021import java.util.Map;
022import java.util.logging.Level;
023import java.util.logging.Logger;
024
025import org.syncany.config.Config;
026import org.syncany.plugins.transfer.StorageException;
027import org.syncany.plugins.transfer.StorageMoveException;
028import org.syncany.plugins.transfer.StorageTestResult;
029import org.syncany.plugins.transfer.TransferManager;
030import org.syncany.plugins.transfer.files.RemoteFile;
031
032/**
033 * The retriable transfer manager implements a simple try-sleep-retry mechanism
034 * for regular {@link org.syncany.plugins.transfer.TransferManager}s. 
035 * 
036 * <p>It encapsules a single transfer manager and proxies all of its methods. If a
037 * method fails with a {@link org.syncany.plugins.transfer.StorageException}, the
038 * method is retried N times before the exception is actually thrown to the caller.
039 * Between retries, the method waits M seconds.
040 *
041 * @author Philipp C. Heckel (philipp.heckel@gmail.com)
042 */
043public class RetriableFeatureTransferManager implements FeatureTransferManager {
044        private static final Logger logger = Logger.getLogger(RetriableFeatureTransferManager.class.getSimpleName());
045
046        private interface RetriableMethod {
047                public Object execute() throws StorageException;
048        }
049
050        private TransferManager underlyingTransferManager;
051        private int retryMaxCount;
052        private int retrySleepMillis;
053
054        private int tryCount;
055
056        public RetriableFeatureTransferManager(TransferManager originalTransferManager, TransferManager underlyingTransferManager, Config config, Retriable retriableAnnotation) {
057                this.underlyingTransferManager = underlyingTransferManager;
058                this.retryMaxCount = retriableAnnotation.numberRetries();
059                this.retrySleepMillis = retriableAnnotation.sleepInterval();
060
061                this.tryCount = 0;
062        }
063
064        @Override
065        public void connect() throws StorageException {
066                retryMethod(new RetriableMethod() {
067                        @Override
068                        public Object execute() throws StorageException {
069                                underlyingTransferManager.connect();
070                                return null;
071                        }
072                });
073        }
074
075        @Override
076        public void disconnect() throws StorageException {
077                retryMethod(new RetriableMethod() {
078                        @Override
079                        public Object execute() throws StorageException {
080                                underlyingTransferManager.disconnect();
081                                return null;
082                        }
083                });
084        }
085
086        @Override
087        public void init(final boolean createIfRequired) throws StorageException {
088                retryMethod(new RetriableMethod() {
089                        @Override
090                        public Object execute() throws StorageException {
091                                underlyingTransferManager.init(createIfRequired);
092                                return null;
093                        }
094                });
095        }
096
097        @Override
098        public void download(final RemoteFile remoteFile, final File localFile) throws StorageException {
099                retryMethod(new RetriableMethod() {
100                        @Override
101                        public Object execute() throws StorageException {
102                                underlyingTransferManager.download(remoteFile, localFile);
103                                return null;
104                        }
105                });
106        }
107
108        @Override
109        public void move(final RemoteFile sourceFile, final RemoteFile targetFile) throws StorageException {
110                retryMethod(new RetriableMethod() {
111                        @Override
112                        public Object execute() throws StorageException {
113                                underlyingTransferManager.move(sourceFile, targetFile);
114                                return null;
115                        }
116                });
117        }
118
119        @Override
120        public void upload(final File localFile, final RemoteFile remoteFile) throws StorageException {
121                retryMethod(new RetriableMethod() {
122                        @Override
123                        public Object execute() throws StorageException {
124                                underlyingTransferManager.upload(localFile, remoteFile);
125                                return null;
126                        }
127                });
128        }
129
130        @Override
131        public boolean delete(final RemoteFile remoteFile) throws StorageException {
132                return (Boolean) retryMethod(new RetriableMethod() {
133                        @Override
134                        public Object execute() throws StorageException {
135                                return underlyingTransferManager.delete(remoteFile);
136                        }
137                });
138        }
139
140        @Override
141        @SuppressWarnings("unchecked")
142        public <T extends RemoteFile> Map<String, T> list(final Class<T> remoteFileClass) throws StorageException {
143                return (Map<String, T>) retryMethod(new RetriableMethod() {
144                        @Override
145                        public Object execute() throws StorageException {
146                                return underlyingTransferManager.list(remoteFileClass);
147                        }
148                });
149        }
150
151        @Override
152        public String getRemoteFilePath(Class<? extends RemoteFile> remoteFileClass) {
153                return underlyingTransferManager.getRemoteFilePath(remoteFileClass);
154        }
155
156        @Override
157        public StorageTestResult test(boolean testCreateTarget) {
158                return underlyingTransferManager.test(testCreateTarget);
159        }
160
161        @Override
162        public boolean testTargetExists() throws StorageException {
163                return (Boolean) retryMethod(new RetriableMethod() {
164                        @Override
165                        public Object execute() throws StorageException {
166                                return underlyingTransferManager.testTargetExists();
167                        }
168                });
169        }
170
171        @Override
172        public boolean testTargetCanWrite() throws StorageException {
173                return (Boolean) retryMethod(new RetriableMethod() {
174                        @Override
175                        public Object execute() throws StorageException {
176                                return underlyingTransferManager.testTargetCanWrite();
177                        }
178                });
179        }
180
181        @Override
182        public boolean testTargetCanCreate() throws StorageException {
183                return (Boolean) retryMethod(new RetriableMethod() {
184                        @Override
185                        public Object execute() throws StorageException {
186                                return underlyingTransferManager.testTargetCanCreate();
187                        }
188                });
189        }
190
191        @Override
192        public boolean testRepoFileExists() throws StorageException {
193                return (Boolean) retryMethod(new RetriableMethod() {
194                        @Override
195                        public Object execute() throws StorageException {
196                                return underlyingTransferManager.testRepoFileExists();
197                        }
198                });
199        }
200
201        private Object retryMethod(RetriableMethod retryableMethod) throws StorageException {
202                tryCount = 0;
203
204                while (true) {
205                        try {
206                                if (tryCount > 0) {
207                                        logger.log(Level.WARNING, "Retrying method: " + tryCount + "/" + retryMaxCount + " ...");
208                                }
209
210                                Object result = retryableMethod.execute();
211
212                                tryCount = 0;
213                                return result;
214                        }
215                        catch (StorageMoveException e) {
216                                // StorageFileNotFoundException used to be caught here. It no longer is,
217                                // since the transaction concept can cause some very ephemeral discrepancies.
218                                // These can be caught by simply trying again. 
219                                // The reason this exists is a fuzzy stress test (#433)
220                                logger.log(Level.INFO, "StorageException caused by missing file, not the connection. Not retrying.");
221                                throw e;
222                        }
223                        catch (StorageException e) {
224                                tryCount++;
225
226                                if (tryCount >= retryMaxCount) {
227                                        logger.log(Level.WARNING, "Transfer method failed. No retries left. Throwing exception.", e);
228                                        throw e;
229                                }
230                                else {
231                                        logger.log(Level.WARNING, "Transfer method failed. " + tryCount + "/" + retryMaxCount + " retries. Sleeping "
232                                                        + retrySleepMillis
233                                                        + "ms ...", e);
234
235                                        try {
236                                                Thread.sleep(retrySleepMillis);
237                                        }
238                                        catch (Exception e1) {
239                                                throw new StorageException(e1);
240                                        }
241                                }
242                        }
243                }
244        }
245}