blob: e4f5eec8e26460935e3d57a0f9cdad65a62a2c5f [file] [log] [blame]
/*
* Copyright 2022 Google LLC
*
* 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 com.google.android.libraries.mobiledatadownload.testing;
import com.google.android.libraries.mobiledatadownload.downloader.DownloadRequest;
import com.google.android.libraries.mobiledatadownload.downloader.FileDownloader;
import com.google.common.base.Optional;
import com.google.common.flogger.GoogleLogger;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import java.util.concurrent.CountDownLatch;
/**
* File Downloader that allows control over how long the download takes.
*
* <p>An optional delegate FileDownloader can be provided to perform downloading while maintaining
* control over the download state.
*
* <h3>The states of a download</h3>
*
* <p>The following "states" are defined and using the BlockingFileDownloader will allow the
* download to be paused so assertions can be made:
*
* <ol>
* <li>Idle - The download has not started yet.
* <li>In Progress - The download has started, but has not completed yet.
* <li>Finishing* - The download is finishing (Only applicable when delegate is provided).
* <li>Finished - The download has finished.
* </ol>
*
* <h5>The "Finishing" state</h5>
*
* <p>The Finishing state is a special state only applicable when a delegate FileDownloader is
* provided. The Finishing state can be considered most like the In Progress state, the main
* distinction being that no action is being performed during the In Progress state, but the
* delegate is running during the Finishing state.
*
* <p><em>Why not just run the delegate during In Progress?</em>
*
* <p>Because the delgate could be performing actual work (i.e. network calls, I/O), there could be
* some variability introduced that causes test assertions to become flaky. The In Progress is
* reserved to effectively be a Frozen state that ensure no work is being done. This ensures that
* test assertions remain consistent.
*
* <h5>Controlling the states</h5>
*
* <p>After creating an instance of BlockingFileDownloader, the following example shows how the
* BlockingFileDownloader can control the state as well as when assertions can be made.
*
* <pre>{@code
* // Before the call to MDD's downloadFileGroup, the state is considered "Idle" and assertions
* // can be made at this time.
*
* mdd.downloadFileGroup(...);
*
* // After calling downloadFileGroup, the state is in a transition period from "Idle" to
* // "In Progress." assertions should not be made during this time.
*
* blockingFileDownloader.waitForDownloadStarted();
*
* // The above method blocks until the "In Progress" state has been reached. Assertions can be made
* // after this call for the "In Progress" state. If no assertions need to be made during this
* // state, the above call can be skipped.
*
* blockingFileDownloader.finishDownloading();
*
* // The above method moves the state from "In Progress" to "Finishing" (if a delegate is provided)
* // or "Finished" (if no delegate is provided). This is another transition period, so assertions
* // should not be made during this time.
*
* blockingFileDownloader.waitForDownloadCompleted();
*
* // The above method ensures the state has changed from "In Progress"/"Finishing" to "Finished."
* // After this point, assertions can be made safely again.
* //
* // Optionally, the future returned from mdd.downloadFileGroup can be awaited instead, since that
* // also ensures the download has completed.
* }</pre>
*/
public final class BlockingFileDownloader implements FileDownloader {
private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
private final ListeningExecutorService downloadExecutor;
private final Optional<FileDownloader> delegateFileDownloaderOptional;
private CountDownLatch downloadInProgressLatch = new CountDownLatch(1);
private CountDownLatch downloadFinishingLatch = new CountDownLatch(1);
private CountDownLatch delegateInProgressLatch = new CountDownLatch(1);
private CountDownLatch downloadFinishedLatch = new CountDownLatch(1);
public BlockingFileDownloader(ListeningExecutorService downloadExecutor) {
this.downloadExecutor = downloadExecutor;
this.delegateFileDownloaderOptional = Optional.absent();
}
public BlockingFileDownloader(
ListeningExecutorService downloadExecutor, FileDownloader delegateFileDownloader) {
this.downloadExecutor = downloadExecutor;
this.delegateFileDownloaderOptional = Optional.of(delegateFileDownloader);
}
@Override
public ListenableFuture<Void> startDownloading(DownloadRequest downloadRequest) {
ListenableFuture<Void> downloadFuture =
Futures.submitAsync(
() -> {
logger.atInfo().log("Download Started, state changed to: In Progress");
downloadInProgressLatch.countDown();
logger.atInfo().log("Waiting for download to continue");
downloadFinishingLatch.await();
ListenableFuture<Void> result;
if (delegateFileDownloaderOptional.isPresent()) {
logger.atInfo().log("Download State Changed to: Finishing (delegate in progress)");
// Delegate was provided, so perform its download
result = delegateFileDownloaderOptional.get().startDownloading(downloadRequest);
delegateInProgressLatch.countDown();
} else {
result = Futures.immediateVoidFuture();
}
return result;
},
downloadExecutor);
// Add a callback to ensure the state transitions from "In Progress"/"Finishing" to "Finished."
Futures.addCallback(
downloadFuture,
new FutureCallback<Void>() {
@Override
public void onSuccess(Void unused) {
BlockingFileDownloader.logger.atInfo().log("Download State Changed to: Finished");
downloadFinishedLatch.countDown();
}
@Override
public void onFailure(Throwable t) {
BlockingFileDownloader.logger
.atSevere()
.withCause(t)
.log("Download State Changed to: Finished (with error)");
downloadFinishedLatch.countDown();
}
},
downloadExecutor);
return downloadFuture;
}
/** Blocks the caller thread until the download has moved to the "In Progress" state. */
public void waitForDownloadStarted() throws InterruptedException {
downloadInProgressLatch.await();
}
/** Blocks the caller thread until the download has moved to the "Finishing" state. */
public void waitForDownloadCompleted() throws InterruptedException {
downloadFinishedLatch.await();
}
/**
* Blocks the caller thread until the delegate downloader has been invoked. This method will never
* complete if no delegate was provided.
*/
public void waitForDelegateStarted() throws InterruptedException {
delegateInProgressLatch.await();
}
/**
* Finishes the current download lifecycle.
*
* <p>The in progress latch is triggered if it hasn't yet been triggered to prevent a deadlock
* from occurring.
*/
public void finishDownloading() {
if (downloadInProgressLatch.getCount() > 0) {
downloadInProgressLatch.countDown();
}
downloadFinishingLatch.countDown();
}
/**
* Resets the current state of the download lifecycle.
*
* <p>An existing download cycle is finished before resetting the state to prevent a deadlock from
* occurring.
*/
public void resetState() {
// Force a finish download if it was previously in progress to prevent deadlock.
if (downloadFinishedLatch.getCount() > 0) {
finishDownloading();
}
downloadFinishedLatch.countDown();
logger.atInfo().log("Reset State back to: Idle");
downloadInProgressLatch = new CountDownLatch(1);
downloadFinishingLatch = new CountDownLatch(1);
downloadFinishedLatch = new CountDownLatch(1);
delegateInProgressLatch = new CountDownLatch(1);
}
}