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.internal.util; 17 18 import static com.google.common.util.concurrent.Futures.immediateFailedFuture; 19 import static com.google.common.util.concurrent.Futures.immediateVoidFuture; 20 21 import androidx.annotation.VisibleForTesting; 22 import com.google.android.libraries.mobiledatadownload.internal.logging.LogUtil; 23 import com.google.android.libraries.mobiledatadownload.tracing.PropagatedExecutionSequencer; 24 import com.google.common.base.Optional; 25 import com.google.common.util.concurrent.ListenableFuture; 26 import java.util.HashMap; 27 import java.util.Map; 28 import java.util.concurrent.Executor; 29 30 /** 31 * Helper class to maintain the state of MDD download futures. 32 * 33 * <p>This follows a limited Map interface and uses {@link ExecutionSequencer} to ensure that all 34 * operations on the map are synchronized. 35 * 36 * <p><b>NOTE:</b> This class is meant to be a container class for download futures and <em>should 37 * not</em> include any download-specific logic. Its sole purpose is to maintain any in-progress 38 * download futures in a synchronized manner. Download-specific logic should be implemented outside 39 * of this class, and can rely on {@link StateChangeCallbacks} to respond to events from this map. 40 */ 41 public final class DownloadFutureMap<T> { 42 private static final String TAG = "DownloadFutureMap"; 43 44 // ExecutionSequencer ensures that enqueued futures are executed sequentially (regardless of the 45 // executor used). This allows us to keep critical state changes sequential. 46 private final PropagatedExecutionSequencer futureSerializer = 47 PropagatedExecutionSequencer.create(); 48 49 private final Executor sequentialControlExecutor; 50 private final StateChangeCallbacks callbacks; 51 52 // Underlying map to store futures -- synchronization of accesses/updates is handled by 53 // ExecutionSequencer. 54 @VisibleForTesting 55 public final Map<String, ListenableFuture<T>> keyToDownloadFutureMap = new HashMap<>(); 56 DownloadFutureMap(Executor sequentialControlExecutor, StateChangeCallbacks callbacks)57 private DownloadFutureMap(Executor sequentialControlExecutor, StateChangeCallbacks callbacks) { 58 this.sequentialControlExecutor = sequentialControlExecutor; 59 this.callbacks = callbacks; 60 } 61 62 /** Convenience creator when no callbacks should be registered. */ create(Executor sequentialControlExecutor)63 public static <T> DownloadFutureMap<T> create(Executor sequentialControlExecutor) { 64 return create(sequentialControlExecutor, new StateChangeCallbacks() {}); 65 } 66 67 /** Creates a new instance of DownloadFutureMap. */ create( Executor sequentialControlExecutor, StateChangeCallbacks callbacks)68 public static <T> DownloadFutureMap<T> create( 69 Executor sequentialControlExecutor, StateChangeCallbacks callbacks) { 70 return new DownloadFutureMap<T>(sequentialControlExecutor, callbacks); 71 } 72 73 /** Callback to support custom events based on the state of the map. */ 74 public static interface StateChangeCallbacks { 75 /** Respond to the event immediately before a new future is added to the map. */ onAdd(String key, int newSize)76 default void onAdd(String key, int newSize) throws Exception {} 77 78 /** Respond to the event immediately after a future is removed from the map. */ onRemove(String key, int newSize)79 default void onRemove(String key, int newSize) throws Exception {} 80 } 81 add(String key, ListenableFuture<T> downloadFuture)82 public ListenableFuture<Void> add(String key, ListenableFuture<T> downloadFuture) { 83 LogUtil.v("%s: submitting request to add in-progress download future with key: %s", TAG, key); 84 return futureSerializer.submitAsync( 85 () -> { 86 try { 87 callbacks.onAdd(key, keyToDownloadFutureMap.size() + 1); 88 keyToDownloadFutureMap.put(key, downloadFuture); 89 } catch (Exception e) { 90 LogUtil.e(e, "%s: Failed to add download future (%s) to map", TAG, key); 91 return immediateFailedFuture(e); 92 } 93 return immediateVoidFuture(); 94 }, 95 sequentialControlExecutor); 96 } 97 98 @SuppressWarnings("FutureReturnValueIgnored") 99 public ListenableFuture<Void> remove(String key) { 100 LogUtil.v( 101 "%s: submitting request to remove in-progress download future with key: %s", TAG, key); 102 return futureSerializer.submitAsync( 103 () -> { 104 try { 105 keyToDownloadFutureMap.remove(key); 106 callbacks.onRemove(key, keyToDownloadFutureMap.size()); 107 } catch (Exception e) { 108 LogUtil.e(e, "%s: Failed to remove download future (%s) from map", TAG, key); 109 return immediateFailedFuture(e); 110 } 111 return immediateVoidFuture(); 112 }, 113 sequentialControlExecutor); 114 } 115 116 public ListenableFuture<Optional<ListenableFuture<T>>> get(String key) { 117 LogUtil.v("%s: submitting request for in-progress download future with key: %s", TAG, key); 118 return futureSerializer.submit( 119 () -> Optional.fromNullable(keyToDownloadFutureMap.get(key)), sequentialControlExecutor); 120 } 121 122 public ListenableFuture<Boolean> containsKey(String key) { 123 LogUtil.v("%s: submitting check for in-progress download future with key: %s", TAG, key); 124 return futureSerializer.submit( 125 () -> keyToDownloadFutureMap.containsKey(key), sequentialControlExecutor); 126 } 127 } 128