/*
* 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.internal.util;
import static com.google.common.util.concurrent.Futures.immediateFailedFuture;
import static com.google.common.util.concurrent.Futures.immediateVoidFuture;
import androidx.annotation.VisibleForTesting;
import com.google.android.libraries.mobiledatadownload.internal.logging.LogUtil;
import com.google.android.libraries.mobiledatadownload.tracing.PropagatedExecutionSequencer;
import com.google.common.base.Optional;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Executor;
/**
* Helper class to maintain the state of MDD download futures.
*
*
This follows a limited Map interface and uses {@link ExecutionSequencer} to ensure that all
* operations on the map are synchronized.
*
*
NOTE: This class is meant to be a container class for download futures and should
* not include any download-specific logic. Its sole purpose is to maintain any in-progress
* download futures in a synchronized manner. Download-specific logic should be implemented outside
* of this class, and can rely on {@link StateChangeCallbacks} to respond to events from this map.
*/
public final class DownloadFutureMap {
private static final String TAG = "DownloadFutureMap";
// ExecutionSequencer ensures that enqueued futures are executed sequentially (regardless of the
// executor used). This allows us to keep critical state changes sequential.
private final PropagatedExecutionSequencer futureSerializer =
PropagatedExecutionSequencer.create();
private final Executor sequentialControlExecutor;
private final StateChangeCallbacks callbacks;
// Underlying map to store futures -- synchronization of accesses/updates is handled by
// ExecutionSequencer.
@VisibleForTesting
public final Map> keyToDownloadFutureMap = new HashMap<>();
private DownloadFutureMap(Executor sequentialControlExecutor, StateChangeCallbacks callbacks) {
this.sequentialControlExecutor = sequentialControlExecutor;
this.callbacks = callbacks;
}
/** Convenience creator when no callbacks should be registered. */
public static DownloadFutureMap create(Executor sequentialControlExecutor) {
return create(sequentialControlExecutor, new StateChangeCallbacks() {});
}
/** Creates a new instance of DownloadFutureMap. */
public static DownloadFutureMap create(
Executor sequentialControlExecutor, StateChangeCallbacks callbacks) {
return new DownloadFutureMap(sequentialControlExecutor, callbacks);
}
/** Callback to support custom events based on the state of the map. */
public static interface StateChangeCallbacks {
/** Respond to the event immediately before a new future is added to the map. */
default void onAdd(String key, int newSize) throws Exception {}
/** Respond to the event immediately after a future is removed from the map. */
default void onRemove(String key, int newSize) throws Exception {}
}
public ListenableFuture add(String key, ListenableFuture downloadFuture) {
LogUtil.v("%s: submitting request to add in-progress download future with key: %s", TAG, key);
return futureSerializer.submitAsync(
() -> {
try {
callbacks.onAdd(key, keyToDownloadFutureMap.size() + 1);
keyToDownloadFutureMap.put(key, downloadFuture);
} catch (Exception e) {
LogUtil.e(e, "%s: Failed to add download future (%s) to map", TAG, key);
return immediateFailedFuture(e);
}
return immediateVoidFuture();
},
sequentialControlExecutor);
}
@SuppressWarnings("FutureReturnValueIgnored")
public ListenableFuture remove(String key) {
LogUtil.v(
"%s: submitting request to remove in-progress download future with key: %s", TAG, key);
return futureSerializer.submitAsync(
() -> {
try {
keyToDownloadFutureMap.remove(key);
callbacks.onRemove(key, keyToDownloadFutureMap.size());
} catch (Exception e) {
LogUtil.e(e, "%s: Failed to remove download future (%s) from map", TAG, key);
return immediateFailedFuture(e);
}
return immediateVoidFuture();
},
sequentialControlExecutor);
}
public ListenableFuture>> get(String key) {
LogUtil.v("%s: submitting request for in-progress download future with key: %s", TAG, key);
return futureSerializer.submit(
() -> Optional.fromNullable(keyToDownloadFutureMap.get(key)), sequentialControlExecutor);
}
public ListenableFuture containsKey(String key) {
LogUtil.v("%s: submitting check for in-progress download future with key: %s", TAG, key);
return futureSerializer.submit(
() -> keyToDownloadFutureMap.containsKey(key), sequentialControlExecutor);
}
}