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.lang.reflect.Constructor;
022import java.lang.reflect.InvocationTargetException;
023import java.util.Map;
024import java.util.logging.Level;
025import java.util.logging.Logger;
026
027import org.syncany.config.Config;
028import org.syncany.plugins.transfer.StorageException;
029import org.syncany.plugins.transfer.StorageTestResult;
030import org.syncany.plugins.transfer.TransferManager;
031import org.syncany.plugins.transfer.files.RemoteFile;
032import org.syncany.util.ReflectionUtil;
033
034/**
035 * <p>The ReadWriteConsistentFeatureTransferManager waits specific amount of time after
036 * {@link #upload(File, RemoteFile)} and {@link #move(RemoteFile, RemoteFile)} operations
037 * because some storage backends do no guarantee that a file immediately exists after
038 * creation.
039 *
040 * <p>It throttles existence check using a simple exponential method:<br>
041 *
042 * <code>throttle(n) = 3 ^ n * 100 ms</code>, with n being the current iteration
043 *
044 * @author Christian Roth (christian.roth@port17.de)
045 */
046public class ReadAfterWriteConsistentFeatureTransferManager implements FeatureTransferManager {
047        private static final Logger logger = Logger.getLogger(ReadAfterWriteConsistentFeatureTransferManager.class.getSimpleName());
048
049        private final TransferManager underlyingTransferManager;
050        private final Throttler throttler;
051        private final ReadAfterWriteConsistentFeatureExtension readAfterWriteConsistentFeatureExtension;
052
053        public ReadAfterWriteConsistentFeatureTransferManager(TransferManager originalTransferManager, TransferManager underlyingTransferManager, Config config, ReadAfterWriteConsistent readAfterWriteConsistentAnnotation) {
054                this.underlyingTransferManager = underlyingTransferManager;
055                this.throttler = new Throttler(readAfterWriteConsistentAnnotation.maxRetries(), readAfterWriteConsistentAnnotation.maxWaitTime());
056                this.readAfterWriteConsistentFeatureExtension = getReadAfterWriteConsistentFeatureExtension(originalTransferManager, readAfterWriteConsistentAnnotation);
057        }
058
059        @SuppressWarnings("unchecked")
060        private ReadAfterWriteConsistentFeatureExtension getReadAfterWriteConsistentFeatureExtension(TransferManager originalTransferManager, ReadAfterWriteConsistent readAfterWriteConsistentAnnotation) {
061                Class<? extends TransferManager> originalTransferManagerClass = originalTransferManager.getClass();
062                Class<ReadAfterWriteConsistentFeatureExtension> readAfterWriteConsistentFeatureExtensionClass = (Class<ReadAfterWriteConsistentFeatureExtension>) readAfterWriteConsistentAnnotation.extension();
063
064                try {
065                        Constructor<?> constructor = ReflectionUtil.getMatchingConstructorForClass(readAfterWriteConsistentFeatureExtensionClass, originalTransferManagerClass);
066
067                        if (constructor != null) {
068                                return (ReadAfterWriteConsistentFeatureExtension) constructor.newInstance(originalTransferManager);
069                        }
070
071                        return readAfterWriteConsistentFeatureExtensionClass.newInstance();
072                }
073                catch (InvocationTargetException | InstantiationException | IllegalAccessException | NullPointerException e) {
074                        throw new RuntimeException("Cannot instantiate ReadWriteConsistentFeatureExtension (perhaps " + readAfterWriteConsistentFeatureExtensionClass + " does not exist?)", e);
075                }
076        }
077
078        @Override
079        public void connect() throws StorageException {
080                underlyingTransferManager.connect();
081        }
082
083        @Override
084        public void disconnect() throws StorageException {
085                underlyingTransferManager.disconnect();
086        }
087
088        @Override
089        public void init(final boolean createIfRequired) throws StorageException {
090                underlyingTransferManager.init(createIfRequired);
091        }
092
093        @Override
094        public void download(final RemoteFile remoteFile, final File localFile) throws StorageException {
095                underlyingTransferManager.download(remoteFile, localFile);
096        }
097
098        @Override
099        public void move(final RemoteFile sourceFile, final RemoteFile targetFile) throws StorageException {
100                underlyingTransferManager.move(sourceFile, targetFile);
101                waitForFile(targetFile);
102        }
103
104        @Override
105        public void upload(final File localFile, final RemoteFile remoteFile) throws StorageException {
106                underlyingTransferManager.upload(localFile, remoteFile);
107                waitForFile(remoteFile);
108        }
109
110        @Override
111        public boolean delete(final RemoteFile remoteFile) throws StorageException {
112                return underlyingTransferManager.delete(remoteFile);
113        }
114
115        @Override
116        public <T extends RemoteFile> Map<String, T> list(final Class<T> remoteFileClass) throws StorageException {
117                return underlyingTransferManager.list(remoteFileClass);
118        }
119
120        @Override
121        public String getRemoteFilePath(Class<? extends RemoteFile> remoteFileClass) {
122                return underlyingTransferManager.getRemoteFilePath(remoteFileClass);
123        }
124
125        @Override
126        public StorageTestResult test(boolean testCreateTarget) {
127                return underlyingTransferManager.test(testCreateTarget);
128        }
129
130        @Override
131        public boolean testTargetExists() throws StorageException {
132                return underlyingTransferManager.testTargetExists();
133        }
134
135        @Override
136        public boolean testTargetCanWrite() throws StorageException {
137                return underlyingTransferManager.testTargetCanWrite();
138        }
139
140        @Override
141        public boolean testTargetCanCreate() throws StorageException {
142                return underlyingTransferManager.testTargetCanCreate();
143        }
144
145        @Override
146        public boolean testRepoFileExists() throws StorageException {
147                return underlyingTransferManager.testRepoFileExists();
148        }
149
150        private void waitForFile(RemoteFile remoteFile) throws StorageException {
151                while (true) {
152                        if (readAfterWriteConsistentFeatureExtension.exists(remoteFile)) {
153                                logger.log(Level.FINER, remoteFile + " exists on the remote side");
154                                throttler.reset();
155                                break;
156                        }
157
158                        try {
159                                long waitForMs = throttler.next();
160                                logger.log(Level.FINER, "File not found on the remote side, perhaps its in transit, waiting " + waitForMs + "ms ...");
161                                Thread.sleep(waitForMs);
162                        }
163                        catch (InterruptedException e) {
164                                throw new StorageException("Unable to wait anymore", e);
165                        }
166                }
167        }
168
169        private class Throttler {
170                private final int maxRetries;
171                private final int maxWait;
172                private int currentIteration = 0;
173
174                public Throttler(int maxRetries, int maxWait) {
175                        this.maxRetries = maxRetries;
176                        this.maxWait = maxWait;
177                }
178
179                public long next() throws InterruptedException {
180                        long waitFor = (long) Math.pow(3, currentIteration++) * 100;
181
182                        if (waitFor > maxWait || currentIteration > maxRetries) {
183                                throw new InterruptedException("Unable to wait anymore, because ending criteria reached");
184                        }
185
186                        return waitFor;
187                }
188
189                public void reset() {
190                        currentIteration = 0;
191                }
192        }
193}