• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2022 Google LLC
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.google.android.libraries.mobiledatadownload.testing;
17 
18 import com.google.android.libraries.mobiledatadownload.downloader.DownloadRequest;
19 import com.google.android.libraries.mobiledatadownload.downloader.FileDownloader;
20 import com.google.common.base.Optional;
21 import com.google.common.flogger.GoogleLogger;
22 import com.google.common.util.concurrent.FutureCallback;
23 import com.google.common.util.concurrent.Futures;
24 import com.google.common.util.concurrent.ListenableFuture;
25 import com.google.common.util.concurrent.ListeningExecutorService;
26 import java.util.concurrent.CountDownLatch;
27 
28 /**
29  * File Downloader that allows control over how long the download takes.
30  *
31  * <p>An optional delegate FileDownloader can be provided to perform downloading while maintaining
32  * control over the download state.
33  *
34  * <h3>The states of a download</h3>
35  *
36  * <p>The following "states" are defined and using the BlockingFileDownloader will allow the
37  * download to be paused so assertions can be made:
38  *
39  * <ol>
40  *   <li>Idle - The download has not started yet.
41  *   <li>In Progress - The download has started, but has not completed yet.
42  *   <li>Finishing* - The download is finishing (Only applicable when delegate is provided).
43  *   <li>Finished - The download has finished.
44  * </ol>
45  *
46  * <h5>The "Finishing" state</h5>
47  *
48  * <p>The Finishing state is a special state only applicable when a delegate FileDownloader is
49  * provided. The Finishing state can be considered most like the In Progress state, the main
50  * distinction being that no action is being performed during the In Progress state, but the
51  * delegate is running during the Finishing state.
52  *
53  * <p><em>Why not just run the delegate during In Progress?</em>
54  *
55  * <p>Because the delgate could be performing actual work (i.e. network calls, I/O), there could be
56  * some variability introduced that causes test assertions to become flaky. The In Progress is
57  * reserved to effectively be a Frozen state that ensure no work is being done. This ensures that
58  * test assertions remain consistent.
59  *
60  * <h5>Controlling the states</h5>
61  *
62  * <p>After creating an instance of BlockingFileDownloader, the following example shows how the
63  * BlockingFileDownloader can control the state as well as when assertions can be made.
64  *
65  * <pre>{@code
66  * // Before the call to MDD's downloadFileGroup, the state is considered "Idle" and assertions
67  * // can be made at this time.
68  *
69  * mdd.downloadFileGroup(...);
70  *
71  * // After calling downloadFileGroup, the state is in a transition period from "Idle" to
72  * // "In Progress." assertions should not be made during this time.
73  *
74  * blockingFileDownloader.waitForDownloadStarted();
75  *
76  * // The above method blocks until the "In Progress" state has been reached. Assertions can be made
77  * // after this call for the "In Progress" state. If no assertions need to be made during this
78  * // state, the above call can be skipped.
79  *
80  * blockingFileDownloader.finishDownloading();
81  *
82  * // The above method moves the state from "In Progress" to "Finishing" (if a delegate is provided)
83  * // or "Finished" (if no delegate is provided). This is another transition period, so assertions
84  * // should not be made during this time.
85  *
86  * blockingFileDownloader.waitForDownloadCompleted();
87  *
88  * // The above method ensures the state has changed from "In Progress"/"Finishing" to "Finished."
89  * // After this point, assertions can be made safely again.
90  * //
91  * // Optionally, the future returned from mdd.downloadFileGroup can be awaited instead, since that
92  * // also ensures the download has completed.
93  * }</pre>
94  */
95 public final class BlockingFileDownloader implements FileDownloader {
96   private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
97 
98   private final ListeningExecutorService downloadExecutor;
99   private final Optional<FileDownloader> delegateFileDownloaderOptional;
100 
101   private CountDownLatch downloadInProgressLatch = new CountDownLatch(1);
102   private CountDownLatch downloadFinishingLatch = new CountDownLatch(1);
103   private CountDownLatch delegateInProgressLatch = new CountDownLatch(1);
104   private CountDownLatch downloadFinishedLatch = new CountDownLatch(1);
105 
BlockingFileDownloader(ListeningExecutorService downloadExecutor)106   public BlockingFileDownloader(ListeningExecutorService downloadExecutor) {
107     this.downloadExecutor = downloadExecutor;
108     this.delegateFileDownloaderOptional = Optional.absent();
109   }
110 
BlockingFileDownloader( ListeningExecutorService downloadExecutor, FileDownloader delegateFileDownloader)111   public BlockingFileDownloader(
112       ListeningExecutorService downloadExecutor, FileDownloader delegateFileDownloader) {
113     this.downloadExecutor = downloadExecutor;
114     this.delegateFileDownloaderOptional = Optional.of(delegateFileDownloader);
115   }
116 
117   @Override
startDownloading(DownloadRequest downloadRequest)118   public ListenableFuture<Void> startDownloading(DownloadRequest downloadRequest) {
119     ListenableFuture<Void> downloadFuture =
120         Futures.submitAsync(
121             () -> {
122               logger.atInfo().log("Download Started, state changed to: In Progress");
123               downloadInProgressLatch.countDown();
124 
125               logger.atInfo().log("Waiting for download to continue");
126               downloadFinishingLatch.await();
127 
128               ListenableFuture<Void> result;
129               if (delegateFileDownloaderOptional.isPresent()) {
130                 logger.atInfo().log("Download State Changed to: Finishing (delegate in progress)");
131                 // Delegate was provided, so perform its download
132                 result = delegateFileDownloaderOptional.get().startDownloading(downloadRequest);
133                 delegateInProgressLatch.countDown();
134               } else {
135                 result = Futures.immediateVoidFuture();
136               }
137 
138               return result;
139             },
140             downloadExecutor);
141 
142     // Add a callback to ensure the state transitions from "In Progress"/"Finishing" to "Finished."
143     Futures.addCallback(
144         downloadFuture,
145         new FutureCallback<Void>() {
146           @Override
147           public void onSuccess(Void unused) {
148             BlockingFileDownloader.logger.atInfo().log("Download State Changed to: Finished");
149             downloadFinishedLatch.countDown();
150           }
151 
152           @Override
153           public void onFailure(Throwable t) {
154             BlockingFileDownloader.logger
155                 .atSevere()
156                 .withCause(t)
157                 .log("Download State Changed to: Finished (with error)");
158             downloadFinishedLatch.countDown();
159           }
160         },
161         downloadExecutor);
162 
163     return downloadFuture;
164   }
165 
166   /** Blocks the caller thread until the download has moved to the "In Progress" state. */
waitForDownloadStarted()167   public void waitForDownloadStarted() throws InterruptedException {
168     downloadInProgressLatch.await();
169   }
170 
171   /** Blocks the caller thread until the download has moved to the "Finishing" state. */
waitForDownloadCompleted()172   public void waitForDownloadCompleted() throws InterruptedException {
173     downloadFinishedLatch.await();
174   }
175 
176   /**
177    * Blocks the caller thread until the delegate downloader has been invoked. This method will never
178    * complete if no delegate was provided.
179    */
waitForDelegateStarted()180   public void waitForDelegateStarted() throws InterruptedException {
181     delegateInProgressLatch.await();
182   }
183 
184   /**
185    * Finishes the current download lifecycle.
186    *
187    * <p>The in progress latch is triggered if it hasn't yet been triggered to prevent a deadlock
188    * from occurring.
189    */
finishDownloading()190   public void finishDownloading() {
191     if (downloadInProgressLatch.getCount() > 0) {
192       downloadInProgressLatch.countDown();
193     }
194     downloadFinishingLatch.countDown();
195   }
196 
197   /**
198    * Resets the current state of the download lifecycle.
199    *
200    * <p>An existing download cycle is finished before resetting the state to prevent a deadlock from
201    * occurring.
202    */
resetState()203   public void resetState() {
204     // Force a finish download if it was previously in progress to prevent deadlock.
205     if (downloadFinishedLatch.getCount() > 0) {
206       finishDownloading();
207     }
208     downloadFinishedLatch.countDown();
209 
210     logger.atInfo().log("Reset State back to: Idle");
211     downloadInProgressLatch = new CountDownLatch(1);
212     downloadFinishingLatch = new CountDownLatch(1);
213     downloadFinishedLatch = new CountDownLatch(1);
214     delegateInProgressLatch = new CountDownLatch(1);
215   }
216 }
217