/*
* 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 static org.hamcrest.junit.MatcherAssert.assertThat;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import java.util.concurrent.atomic.AtomicBoolean;
import org.hamcrest.Matchers;
import org.junit.Test;
public class ListeningMultiSemaphoreTest {
@Test
public void testCreatingWithMaximumValues() {
ResourceAmounts values = ResourceAmounts.of(2, 10, 0, 0);
ListeningMultiSemaphore array = getFairListeningMultiSemaphore(values);
assertThat(array.getAvailableResources(), Matchers.equalTo(values));
assertThat(array.getMaximumValues(), Matchers.equalTo(values));
assertThat(array.getQueueLength(), Matchers.equalTo(0));
}
@Test
public void testAcquiringResources() {
ResourceAmounts values = amountsOfCpu(2);
ListeningMultiSemaphore array = getFairListeningMultiSemaphore(values);
ListenableFuture<Void> future = array.acquire(amountsOfCpu(1));
assertThat(future.isDone(), Matchers.equalTo(true));
assertThat(array.getQueueLength(), Matchers.equalTo(0));
assertThat(array.getAvailableResources(), Matchers.equalTo(amountsOfCpu(1)));
}
@Test
public void testReleasingResources() {
ResourceAmounts values = amountsOfCpu(2);
ListeningMultiSemaphore array = getFairListeningMultiSemaphore(values);
array.acquire(amountsOfCpu(1));
array.release(amountsOfCpu(1));
assertThat(array.getAvailableResources(), Matchers.equalTo(values));
}
@Test
public void testProcessingPendingQueue() {
ResourceAmounts values = amountsOfCpu(7);
ListeningMultiSemaphore array = getFairListeningMultiSemaphore(values);
ListenableFuture<Void> f1 = array.acquire(amountsOfCpu(3));
assertThat(f1.isDone(), Matchers.equalTo(true));
assertThat(array.getAvailableResources(), Matchers.equalTo(amountsOfCpu(4)));
ListenableFuture<Void> f2 = array.acquire(amountsOfCpu(5));
assertThat(f2.isDone(), Matchers.equalTo(false));
assertThat(array.getQueueLength(), Matchers.equalTo(1));
assertThat(array.getAvailableResources(), Matchers.equalTo(amountsOfCpu(4)));
final AtomicBoolean f2HasBeenReleased = new AtomicBoolean(false);
f2.addListener(() -> f2HasBeenReleased.set(true), MoreExecutors.newDirectExecutorService());
array.release(amountsOfCpu(3));
assertThat(f2HasBeenReleased.get(), Matchers.equalTo(true));
assertThat(array.getAvailableResources(), Matchers.equalTo(amountsOfCpu(2)));
array.release(amountsOfCpu(5));
assertThat(array.getAvailableResources(), Matchers.equalTo(array.getMaximumValues()));
}
@Test
public void testProcessingPendingQueueWithCancelledFuturesReleasesPendingItems() {
ResourceAmounts values = amountsOfCpu(7);
ListeningMultiSemaphore array = getFairListeningMultiSemaphore(values);
// this step acquired some resources.
ListenableFuture<Void> f1 = array.acquire(amountsOfCpu(5));
assertThat(f1.isDone(), Matchers.equalTo(true));
assertThat(array.getAvailableResources(), Matchers.equalTo(amountsOfCpu(2)));
// then, this step tried to acquire, but we cancel this future. When pending queue is being
// processed later, this acquisition should not happen - future is cancelled!
final ListenableFuture<Void> toBeCancelled = array.acquire(amountsOfCpu(5));
assertThat(toBeCancelled.isDone(), Matchers.equalTo(false));
assertThat(array.getQueueLength(), Matchers.equalTo(1));
assertThat(array.getAvailableResources(), Matchers.equalTo(amountsOfCpu(2)));
toBeCancelled.cancel(true);
final AtomicBoolean toBeCancelledIsReleased = new AtomicBoolean(false);
toBeCancelled.addListener(
() -> toBeCancelledIsReleased.set(toBeCancelled.isCancelled()),
MoreExecutors.newDirectExecutorService());
// this should be released, because previous future is cancelled,
// so resources should become free
final ListenableFuture<Void> toBeReleaseAfterCancellation = array.acquire(amountsOfCpu(6));
assertThat(toBeReleaseAfterCancellation.isDone(), Matchers.equalTo(false));
assertThat(array.getQueueLength(), Matchers.equalTo(2));
assertThat(array.getAvailableResources(), Matchers.equalTo(amountsOfCpu(2)));
// this should happen
final AtomicBoolean toBeReleaseAfterCancellationIsReleased = new AtomicBoolean(false);
toBeReleaseAfterCancellation.addListener(
() ->
toBeReleaseAfterCancellationIsReleased.set(toBeReleaseAfterCancellation.isCancelled()),
MoreExecutors.newDirectExecutorService());
// release resources acquired by f1
array.release(amountsOfCpu(5));
// isCancelled():
assertThat(toBeCancelledIsReleased.get(), Matchers.equalTo(true));
// !isCancelled():
assertThat(toBeReleaseAfterCancellationIsReleased.get(), Matchers.equalTo(false));
assertThat(array.getQueueLength(), Matchers.equalTo(0));
assertThat(array.getAvailableResources(), Matchers.equalTo(amountsOfCpu(1)));
array.release(amountsOfCpu(6));
assertThat(array.getAvailableResources(), Matchers.equalTo(array.getMaximumValues()));
}
@Test
public void testAcquiringAndReleasingMultipleResources() {
ResourceAmounts values = amountsOfCpuAndMemory(7, 7);
ListeningMultiSemaphore array = getFairListeningMultiSemaphore(values);
ListenableFuture<Void> cpuOnly = array.acquire(amountsOfCpu(5));
ListenableFuture<Void> memOnly = array.acquire(amountsOfMemory(5));
assertThat(cpuOnly.isDone(), Matchers.equalTo(true));
assertThat(memOnly.isDone(), Matchers.equalTo(true));
assertThat(array.getAvailableResources(), Matchers.equalTo(ResourceAmounts.of(2, 2, 0, 0)));
ListenableFuture<Void> cpuAndMem1 = array.acquire(amountsOfCpuAndMemory(4, 4));
assertThat(cpuAndMem1.isDone(), Matchers.equalTo(false));
ListenableFuture<Void> cpuAndMem2 = array.acquire(amountsOfCpuAndMemory(2, 2));
assertThat(cpuAndMem2.isDone(), Matchers.equalTo(true));
assertThat(array.getAvailableResources(), Matchers.equalTo(amountsOfCpuAndMemory(0, 0)));
ListenableFuture<Void> cpuAndMem3 = array.acquire(amountsOfCpuAndMemory(3, 3));
assertThat(cpuAndMem3.isDone(), Matchers.equalTo(false));
assertThat(array.getQueueLength(), Matchers.equalTo(2));
array.release(amountsOfCpu(5));
assertThat(cpuAndMem1.isDone(), Matchers.equalTo(false));
assertThat(cpuAndMem3.isDone(), Matchers.equalTo(false));
assertThat(array.getAvailableResources(), Matchers.equalTo(amountsOfCpuAndMemory(5, 0)));
array.release(amountsOfMemory(5));
assertThat(cpuAndMem1.isDone(), Matchers.equalTo(true));
assertThat(cpuAndMem3.isDone(), Matchers.equalTo(false));
assertThat(array.getAvailableResources(), Matchers.equalTo(amountsOfCpuAndMemory(1, 1)));
assertThat(array.getQueueLength(), Matchers.equalTo(1));
array.release(amountsOfCpuAndMemory(2, 2));
assertThat(cpuAndMem3.isDone(), Matchers.equalTo(true));
assertThat(array.getAvailableResources(), Matchers.equalTo(amountsOfCpuAndMemory(0, 0)));
}
@Test
public void testCappingToMaximumAmounts() {
ListeningMultiSemaphore semaphore =
new ListeningMultiSemaphore(amountsOfCpu(5), ResourceAllocationFairness.FAST);
// Try to acquire more permits than we have, which should block.
ListenableFuture<Void> first = semaphore.acquire(amountsOfCpu(100500));
assertThat(semaphore.getAvailableResources(), Matchers.equalTo(amountsOfCpu(0)));
assertThat(first.isDone(), Matchers.equalTo(true));
semaphore.release(amountsOfCpu(100500));
assertThat(semaphore.getAvailableResources(), Matchers.equalTo(semaphore.getMaximumValues()));
}
@Test
public void fastFairness() {
ListeningMultiSemaphore semaphore =
new ListeningMultiSemaphore(amountsOfCpu(4), ResourceAllocationFairness.FAST);
semaphore.acquire(amountsOfCpu(2));
// Try to acquire more permits than we have, which should block.
ListenableFuture<Void> first = semaphore.acquire(amountsOfCpu(4));
assertThat(semaphore.getAvailableResources(), Matchers.equalTo(amountsOfCpu(2)));
assertThat(semaphore.getQueueLength(), Matchers.equalTo(1));
assertThat(first.isDone(), Matchers.equalTo(false));
// Acquire a single permit and verify it goes through.
ListenableFuture<Void> second = semaphore.acquire(amountsOfCpu(1));
assertThat(semaphore.getAvailableResources(), Matchers.equalTo(amountsOfCpu(1)));
assertThat(semaphore.getQueueLength(), Matchers.equalTo(1));
assertThat(second.isDone(), Matchers.equalTo(true));
}
private ListeningMultiSemaphore getFairListeningMultiSemaphore(ResourceAmounts values) {
return new ListeningMultiSemaphore(values, ResourceAllocationFairness.FAIR);
}
private static ResourceAmounts amountsOfCpu(int cpu) {
return amountsOfCpuAndMemory(cpu, 0);
}
private static ResourceAmounts amountsOfMemory(int memory) {
return amountsOfCpuAndMemory(0, memory);
}
private static ResourceAmounts amountsOfCpuAndMemory(int cpu, int memory) {
return ResourceAmounts.builder()
.setCpu(cpu)
.setMemory(memory)
.setDiskIO(0)
.setNetworkIO(0)
.build();
}
}