/* * Copyright 2016-present Facebook, Inc. * * 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.facebook.buck.util.concurrent; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; import java.util.Iterator; import java.util.LinkedList; import java.util.List; /** * A semaphore using {@link ListenableFuture}s for acquisition of different resource types rather * than blocking. */ public class ListeningMultiSemaphore { private ResourceAmounts usedValues; private final ResourceAmounts maximumValues; private final List<ListeningSemaphoreArrayPendingItem> pending = new LinkedList<>(); private final ResourceAllocationFairness fairness; public ListeningMultiSemaphore( ResourceAmounts availableResources, ResourceAllocationFairness fairness) { this.usedValues = ResourceAmounts.ZERO; this.maximumValues = availableResources; this.fairness = fairness; } /** * Returns the future which will be completed by the moment when resources will be acquired. * Future may be returned already completed. You should subscribe to the future and perform your * resource requiring job once the future will be completed and not cancelled. When you finish * your job, you must release acquired resources by calling release() method below. * * @param resources Resource amounts that need to be acquired. If they are higher than maximum * amounts, they will be capped to them. * @return Future that will be completed once resource will be acquired. */ public synchronized ListenableFuture<Void> acquire(ResourceAmounts resources) { if (resources.equals(ResourceAmounts.ZERO)) { return Futures.immediateFuture(null); } resources = capResourceAmounts(resources); if (!checkIfResourcesAvailable(resources)) { SettableFuture<Void> pendingFuture = SettableFuture.create(); pending.add(ListeningSemaphoreArrayPendingItem.of(pendingFuture, resources)); return pendingFuture; } increaseUsedResources(resources); return Futures.immediateFuture(null); } /** * Releases previously acquired resources. * * @param resources Resource amounts that need to be released. This argument should match one you * used during resource acquiring. */ public void release(ResourceAmounts resources) { if (resources.equals(ResourceAmounts.ZERO)) { return; } resources = capResourceAmounts(resources); decreaseUsedResources(resources); processPendingFutures(getPendingItemsThatCanBeProcessed()); } private synchronized ImmutableList<ListeningSemaphoreArrayPendingItem> getPendingItemsThatCanBeProcessed() { ImmutableList.Builder<ListeningSemaphoreArrayPendingItem> builder = ImmutableList.builder(); Iterator<ListeningSemaphoreArrayPendingItem> iterator = pending.iterator(); while (!getAvailableResources().equals(ResourceAmounts.ZERO) && iterator.hasNext()) { ListeningSemaphoreArrayPendingItem item = iterator.next(); if (checkIfResourcesAvailable(item.getResources())) { builder.add(item); increaseUsedResources(item.getResources()); iterator.remove(); } else if (!fairnessAllowsReordering()) { break; } } return builder.build(); } public synchronized ResourceAmounts getAvailableResources() { return maximumValues.subtract(usedValues); } public synchronized ResourceAmounts getMaximumValues() { return maximumValues; } public synchronized int getQueueLength() { return pending.size(); } /** * We assume that if requested amounts are larger than we have in maximumValues, then intention * was to request all possible resources. This method caps the given resources to the maximum * possible value. Without capping the deadlock will happen. */ private ResourceAmounts capResourceAmounts(ResourceAmounts amounts) { return ResourceAmounts.of( Math.min(amounts.getCpu(), maximumValues.getCpu()), Math.min(amounts.getMemory(), maximumValues.getMemory()), Math.min(amounts.getDiskIO(), maximumValues.getDiskIO()), Math.min(amounts.getNetworkIO(), maximumValues.getNetworkIO())); } private synchronized boolean checkIfResourcesAvailable(ResourceAmounts resources) { Preconditions.checkState( resources.allValuesLessThanOrEqual(maximumValues), "Resource amounts (%s) must be capped to the maximum amounts (%s)", resources, maximumValues); return usedValues.append(resources).allValuesLessThanOrEqual(maximumValues); } private synchronized void increaseUsedResources(ResourceAmounts resources) { usedValues = usedValues.append(resources); } private synchronized void decreaseUsedResources(ResourceAmounts resources) { ResourceAmounts updatedAmounts = usedValues.subtract(resources); Preconditions.checkArgument( !updatedAmounts.containsValuesLessThan(ResourceAmounts.ZERO), "Cannot increase available resources by %s. Current: %s, Maximum: %s", resources, usedValues, maximumValues); usedValues = updatedAmounts; } private void processPendingFutures(ImmutableList<ListeningSemaphoreArrayPendingItem> items) { ResourceAmounts failedAmounts = ResourceAmounts.ZERO; for (ListeningSemaphoreArrayPendingItem item : items) { if (!item.getFuture().set(null)) { failedAmounts = failedAmounts.append(item.getResources()); } } if (!failedAmounts.equals(ResourceAmounts.ZERO)) { release(failedAmounts); } } private boolean fairnessAllowsReordering() { return fairness == ResourceAllocationFairness.FAST; } }