blob: 04cbb2eb15a95706127b447b084105a2ceb3b659 [file] [log] [blame]
// Copyright 2022 The Bazel Authors. All rights reserved.
//
// 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.devtools.build.lib.worker;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.truth.Truth.assertThat;
import static com.google.devtools.build.lib.worker.WorkerTestUtils.createWorkerKey;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.spy;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Maps;
import com.google.devtools.build.lib.clock.BlazeClock;
import com.google.devtools.build.lib.vfs.DigestHashFunction;
import com.google.devtools.build.lib.vfs.FileSystem;
import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem;
import com.google.devtools.build.lib.worker.WorkerProcessStatus.Status;
import java.io.IOException;
import java.time.Instant;
import java.util.Arrays;
import java.util.List;
import java.util.Map.Entry;
import java.util.function.Supplier;
import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.impl.DefaultPooledObject;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;
import org.junit.runners.Parameterized.Parameters;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
@RunWith(Parameterized.class)
public final class WorkerLifecycleManagerTest {
@Rule public final MockitoRule mockito = MockitoJUnit.rule();
private interface WorkerPoolSupplier {
WorkerPool get(
WorkerFactory factory,
List<Entry<String, Integer>> singlexplexMaxInstances,
List<Entry<String, Integer>> multiplexMaxInstance);
}
@Parameter(0)
public WorkerPoolSupplier workerPoolSupplier;
@Parameter(1)
public Supplier<WorkerFactory> workerFactorySupplier;
@Mock WorkerFactory factoryMock;
public static final FileSystem fileSystem =
new InMemoryFileSystem(BlazeClock.instance(), DigestHashFunction.SHA256);
private static final WorkerOptions options = new WorkerOptions();
private static final String DUMMY_MNEMONIC = "dummy";
private static final long PROCESS_ID_1 = 1L;
private static final long PROCESS_ID_2 = 2L;
private static final long PROCESS_ID_3 = 3L;
private static final long PROCESS_ID_4 = 4L;
private static final long PROCESS_ID_5 = 5L;
private int workerIds = 1;
@Parameters
public static List<Object[]> data() throws IOException {
Supplier<WorkerFactory> workerFactorySupplier =
() -> spy(new WorkerFactory(fileSystem.getPath("/outputbase/bazel-workers"), options));
return Arrays.asList(
new Object[][] {
{
(WorkerPoolSupplier)
(factory, singleplexMaxInstances, multiplexMaxInstances) ->
new WorkerPoolImplLegacy(
factory,
new WorkerPoolConfig(singleplexMaxInstances, multiplexMaxInstances)),
workerFactorySupplier,
},
{
(WorkerPoolSupplier)
(factory, singleplexMaxInstances, multiplexMaxInstances) ->
new WorkerPoolImpl(
factory,
new WorkerPoolConfig(singleplexMaxInstances, multiplexMaxInstances)),
workerFactorySupplier,
}
});
}
@Before
public void setUp() throws Exception {
factoryMock = workerFactorySupplier.get();
WorkerOptions options = new WorkerOptions();
doAnswer(
args -> {
WorkerKey key = args.getArgument(0);
if (key.isMultiplex()) {
WorkerMultiplexer multiplexer =
WorkerMultiplexerManager.getInstance(key, fileSystem.getPath("/logDir"));
return new DefaultPooledObject<>(
new WorkerProxy(
key,
workerIds++,
multiplexer.getLogFile(),
multiplexer,
key.getExecRoot()));
}
return new DefaultPooledObject<>(
new SingleplexWorker(
key,
workerIds++,
fileSystem.getPath("/workDir"),
fileSystem.getPath("/logDir"),
options,
null));
})
.when(factoryMock)
.makeObject(any());
doAnswer(
args -> {
PooledObject<Worker> obj = args.getArgument(1);
return obj.getObject().getStatus().isValid();
})
.when(factoryMock)
.validateObject(any(), any());
doAnswer(
args -> {
PooledObject<Worker> obj = args.getArgument(1);
Worker worker = obj.getObject();
worker.destroy();
return null;
})
.when(factoryMock)
.destroyObject(any(), any());
}
@Test
public void testEvictWorkers_doNothing_lowMemoryUsage() throws Exception {
WorkerPool workerPool =
workerPoolSupplier.get(factoryMock, entryList(DUMMY_MNEMONIC, 1), emptyEntryList());
WorkerKey key = createWorkerKey(DUMMY_MNEMONIC, fileSystem);
Worker w1 = workerPool.borrowObject(key);
workerPool.returnObject(key, w1);
ImmutableList<WorkerProcessMetrics> workerMetrics =
ImmutableList.of(createWorkerMetric(w1, PROCESS_ID_1, /* memoryInKb= */ 1000));
WorkerOptions options = new WorkerOptions();
options.totalWorkerMemoryLimitMb = 1000 * 100;
WorkerLifecycleManager manager = new WorkerLifecycleManager(workerPool, options);
assertThat(workerPool.getIdleWorkers()).hasSize(1);
assertThat(workerPool.getNumActive(key)).isEqualTo(0);
manager.evictWorkers(workerMetrics);
assertThat(workerPool.getIdleWorkers()).hasSize(1);
assertThat(workerPool.getNumActive(key)).isEqualTo(0);
// It should still have a valid status since it was not killed / marked to be killed.
assertThat(w1.getStatus().isValid()).isTrue();
}
@Test
public void testEvictWorkers_doNothing_zeroThreshold() throws Exception {
WorkerPool workerPool =
workerPoolSupplier.get(factoryMock, entryList(DUMMY_MNEMONIC, 1), emptyEntryList());
WorkerKey key = createWorkerKey(DUMMY_MNEMONIC, fileSystem);
Worker w1 = workerPool.borrowObject(key);
workerPool.returnObject(key, w1);
ImmutableList<WorkerProcessMetrics> workerMetrics =
ImmutableList.of(createWorkerMetric(w1, PROCESS_ID_1, /* memoryInKb= */ 1000));
WorkerOptions options = new WorkerOptions();
options.totalWorkerMemoryLimitMb = 0;
WorkerLifecycleManager manager = new WorkerLifecycleManager(workerPool, options);
assertThat(workerPool.getIdleWorkers()).hasSize(1);
assertThat(workerPool.getNumActive(key)).isEqualTo(0);
manager.evictWorkers(workerMetrics);
assertThat(workerPool.getIdleWorkers()).hasSize(1);
assertThat(workerPool.getNumActive(key)).isEqualTo(0);
// It should still have a valid status since it was not killed / marked to be killed.
assertThat(w1.getStatus().isValid()).isTrue();
}
@Test
public void testEvictWorkers_doNothing_emptyMetrics() throws Exception {
WorkerPool workerPool =
workerPoolSupplier.get(factoryMock, entryList(DUMMY_MNEMONIC, 1), emptyEntryList());
WorkerKey key = createWorkerKey(DUMMY_MNEMONIC, fileSystem);
Worker w1 = workerPool.borrowObject(key);
workerPool.returnObject(key, w1);
ImmutableList<WorkerProcessMetrics> workerMetrics = ImmutableList.of();
WorkerOptions options = new WorkerOptions();
options.totalWorkerMemoryLimitMb = 1;
WorkerLifecycleManager manager = new WorkerLifecycleManager(workerPool, options);
assertThat(workerPool.getIdleWorkers()).hasSize(1);
assertThat(workerPool.getNumActive(key)).isEqualTo(0);
manager.evictWorkers(workerMetrics);
assertThat(workerPool.getIdleWorkers()).hasSize(1);
assertThat(workerPool.getNumActive(key)).isEqualTo(0);
// It should still have a valid status since it was not killed / marked to be killed.
assertThat(w1.getStatus().isValid()).isTrue();
}
@Test
public void testGetEvictionCandidates_selectOnlyWorker() throws Exception {
WorkerPool workerPool =
workerPoolSupplier.get(factoryMock, entryList(DUMMY_MNEMONIC, 1), emptyEntryList());
WorkerKey key = createWorkerKey(DUMMY_MNEMONIC, fileSystem);
Worker w1 = workerPool.borrowObject(key);
workerPool.returnObject(key, w1);
ImmutableList<WorkerProcessMetrics> workerMetrics =
ImmutableList.of(createWorkerMetric(w1, PROCESS_ID_1, /* memoryInKb= */ 2000));
WorkerOptions options = new WorkerOptions();
options.totalWorkerMemoryLimitMb = 1;
WorkerLifecycleManager manager = new WorkerLifecycleManager(workerPool, options);
assertThat(workerPool.getIdleWorkers()).hasSize(1);
assertThat(workerPool.getNumActive(key)).isEqualTo(0);
// It should still have a valid status since it was not killed / marked to be killed.
assertThat(w1.getStatus().isValid()).isTrue();
manager.evictWorkers(workerMetrics);
assertThat(workerPool.getIdleWorkers()).isEmpty();
assertThat(workerPool.getNumActive(key)).isEqualTo(0);
// Directly killed since it is already returned.
assertThat(w1.getStatus().get()).isEqualTo(Status.KILLED_DUE_TO_MEMORY_PRESSURE);
}
@Test
public void testGetEvictionCandidates_evictLargestWorkers() throws Exception {
WorkerPool workerPool =
workerPoolSupplier.get(factoryMock, entryList(DUMMY_MNEMONIC, 3), emptyEntryList());
WorkerKey key = createWorkerKey(DUMMY_MNEMONIC, fileSystem);
Worker w1 = workerPool.borrowObject(key);
Worker w2 = workerPool.borrowObject(key);
Worker w3 = workerPool.borrowObject(key);
workerPool.returnObject(key, w1);
workerPool.returnObject(key, w2);
workerPool.returnObject(key, w3);
ImmutableList<WorkerProcessMetrics> workerMetrics =
ImmutableList.of(
createWorkerMetric(w1, PROCESS_ID_1, /* memoryInKb= */ 2000),
createWorkerMetric(w2, PROCESS_ID_2, /* memoryInKb= */ 1000),
createWorkerMetric(w3, PROCESS_ID_3, /* memoryInKb= */ 4000));
WorkerOptions options = new WorkerOptions();
options.totalWorkerMemoryLimitMb = 2;
WorkerLifecycleManager manager = new WorkerLifecycleManager(workerPool, options);
assertThat(workerPool.getIdleWorkers()).hasSize(3);
assertThat(workerPool.getNumActive(key)).isEqualTo(0);
assertThat(w1.getStatus().isValid()).isTrue();
assertThat(w2.getStatus().isValid()).isTrue();
assertThat(w3.getStatus().isValid()).isTrue();
manager.evictWorkers(workerMetrics);
assertThat(workerPool.getIdleWorkers()).hasSize(1);
assertThat(workerPool.getNumActive(key)).isEqualTo(0);
// Only w1 and w3 should have been killed.
assertThat(w1.getStatus().get()).isEqualTo(Status.KILLED_DUE_TO_MEMORY_PRESSURE);
assertThat(w2.getStatus().isValid()).isTrue();
assertThat(w3.getStatus().get()).isEqualTo(Status.KILLED_DUE_TO_MEMORY_PRESSURE);
}
@Test
public void testGetEvictionCandidates_numberOfWorkersIsMoreThanDefaultNumTests()
throws Exception {
WorkerPool workerPool =
workerPoolSupplier.get(factoryMock, entryList(DUMMY_MNEMONIC, 4), emptyEntryList());
WorkerKey key = createWorkerKey(DUMMY_MNEMONIC, fileSystem);
Worker w1 = workerPool.borrowObject(key);
Worker w2 = workerPool.borrowObject(key);
Worker w3 = workerPool.borrowObject(key);
Worker w4 = workerPool.borrowObject(key);
workerPool.returnObject(key, w1);
workerPool.returnObject(key, w2);
workerPool.returnObject(key, w3);
workerPool.returnObject(key, w4);
ImmutableList<WorkerProcessMetrics> workerMetrics =
ImmutableList.of(
createWorkerMetric(w1, PROCESS_ID_1, /* memoryInKb= */ 2000),
createWorkerMetric(w2, PROCESS_ID_2, /* memoryInKb= */ 2000),
createWorkerMetric(w3, PROCESS_ID_3, /* memoryInKb= */ 4000),
createWorkerMetric(w4, PROCESS_ID_4, /* memoryInKb= */ 4000));
WorkerOptions options = new WorkerOptions();
options.totalWorkerMemoryLimitMb = 1;
WorkerLifecycleManager manager = new WorkerLifecycleManager(workerPool, options);
assertThat(workerPool.getIdleWorkers()).hasSize(4);
assertThat(workerPool.getNumActive(key)).isEqualTo(0);
manager.evictWorkers(workerMetrics);
assertThat(workerPool.getIdleWorkers()).isEmpty();
assertThat(workerPool.getNumActive(key)).isEqualTo(0);
assertThat(w1.getStatus().get()).isEqualTo(Status.KILLED_DUE_TO_MEMORY_PRESSURE);
assertThat(w2.getStatus().get()).isEqualTo(Status.KILLED_DUE_TO_MEMORY_PRESSURE);
assertThat(w3.getStatus().get()).isEqualTo(Status.KILLED_DUE_TO_MEMORY_PRESSURE);
assertThat(w4.getStatus().get()).isEqualTo(Status.KILLED_DUE_TO_MEMORY_PRESSURE);
}
@Test
public void testGetEvictionCandidates_evictWorkerWithSameMenmonicButDifferentKeys()
throws Exception {
WorkerPool workerPool =
workerPoolSupplier.get(factoryMock, entryList(DUMMY_MNEMONIC, 3), emptyEntryList());
WorkerKey key1 = createWorkerKey(DUMMY_MNEMONIC, fileSystem);
WorkerKey key2 = createWorkerKey(DUMMY_MNEMONIC, fileSystem, true);
Worker w1 = workerPool.borrowObject(key1);
Worker w2 = workerPool.borrowObject(key2);
Worker w3 = workerPool.borrowObject(key2);
workerPool.returnObject(key1, w1);
workerPool.returnObject(key2, w2);
workerPool.returnObject(key2, w3);
ImmutableList<WorkerProcessMetrics> workerMetrics =
ImmutableList.of(
createWorkerMetric(w1, PROCESS_ID_1, /* memoryInKb= */ 3000),
createWorkerMetric(w2, PROCESS_ID_2, /* memoryInKb= */ 3000),
createWorkerMetric(w3, PROCESS_ID_3, /* memoryInKb= */ 1000));
WorkerOptions options = new WorkerOptions();
options.totalWorkerMemoryLimitMb = 2;
options.workerVerbose = true;
WorkerLifecycleManager manager = new WorkerLifecycleManager(workerPool, options);
assertThat(workerPool.getIdleWorkers())
.containsExactly(w1.getWorkerId(), w2.getWorkerId(), w3.getWorkerId());
assertThat(w1.getStatus().isValid()).isTrue();
assertThat(w2.getStatus().isValid()).isTrue();
assertThat(w3.getStatus().isValid()).isTrue();
manager.evictWorkers(workerMetrics);
// Only w3 shouldn't be killed.
assertThat(workerPool.getIdleWorkers()).containsExactly(w3.getWorkerId());
assertThat(workerPool.getNumActive(key1)).isEqualTo(0);
assertThat(workerPool.getNumActive(key2)).isEqualTo(0);
assertThat(w1.getStatus().get()).isEqualTo(Status.KILLED_DUE_TO_MEMORY_PRESSURE);
assertThat(w2.getStatus().get()).isEqualTo(Status.KILLED_DUE_TO_MEMORY_PRESSURE);
assertThat(w3.getStatus().isValid()).isTrue();
}
@Test
public void testGetEvictionCandidates_evictOnlyIdleWorkers() throws Exception {
WorkerPool workerPool =
workerPoolSupplier.get(factoryMock, entryList(DUMMY_MNEMONIC, 3), emptyEntryList());
WorkerKey key = createWorkerKey(DUMMY_MNEMONIC, fileSystem);
Worker w1 = workerPool.borrowObject(key);
Worker w2 = workerPool.borrowObject(key);
Worker w3 = workerPool.borrowObject(key);
workerPool.returnObject(key, w1);
workerPool.returnObject(key, w2);
ImmutableList<WorkerProcessMetrics> workerMetrics =
ImmutableList.of(
createWorkerMetric(w1, PROCESS_ID_1, /* memoryInKb= */ 2000),
createWorkerMetric(w2, PROCESS_ID_2, /* memoryInKb= */ 1000),
createWorkerMetric(w3, PROCESS_ID_3, /* memoryInKb= */ 4000));
WorkerOptions options = new WorkerOptions();
options.totalWorkerMemoryLimitMb = 2;
WorkerLifecycleManager manager = new WorkerLifecycleManager(workerPool, options);
assertThat(workerPool.getIdleWorkers()).hasSize(2);
assertThat(workerPool.getNumActive(key)).isEqualTo(1);
assertThat(w1.getStatus().isValid()).isTrue();
assertThat(w2.getStatus().isValid()).isTrue();
assertThat(w3.getStatus().isValid()).isTrue();
manager.evictWorkers(workerMetrics);
assertThat(workerPool.getIdleWorkers()).isEmpty();
assertThat(workerPool.getNumActive(key)).isEqualTo(1);
assertThat(w1.getStatus().get()).isEqualTo(Status.KILLED_DUE_TO_MEMORY_PRESSURE);
assertThat(w2.getStatus().get()).isEqualTo(Status.KILLED_DUE_TO_MEMORY_PRESSURE);
// w3 is not killed because we're not shrinking the worker pool.
assertThat(w3.getStatus().isValid()).isTrue();
}
@Test
public void testGetEvictionCandidates_evictDifferentWorkerKeys() throws Exception {
WorkerPool workerPool =
workerPoolSupplier.get(
factoryMock, entryList(DUMMY_MNEMONIC, 2, "smart", 2), emptyEntryList());
WorkerKey key1 = createWorkerKey(DUMMY_MNEMONIC, fileSystem);
WorkerKey key2 = createWorkerKey("smart", fileSystem);
Worker w1 = workerPool.borrowObject(key1);
Worker w2 = workerPool.borrowObject(key1);
Worker w3 = workerPool.borrowObject(key2);
Worker w4 = workerPool.borrowObject(key2);
workerPool.returnObject(key1, w1);
workerPool.returnObject(key1, w2);
workerPool.returnObject(key2, w3);
workerPool.returnObject(key2, w4);
ImmutableList<WorkerProcessMetrics> workerMetrics =
ImmutableList.of(
createWorkerMetric(w1, PROCESS_ID_1, /* memoryInKb= */ 1000),
createWorkerMetric(w2, PROCESS_ID_2, /* memoryInKb= */ 4000),
createWorkerMetric(w3, PROCESS_ID_3, /* memoryInKb= */ 3000),
createWorkerMetric(w4, PROCESS_ID_4, /* memoryInKb= */ 1000));
WorkerOptions options = new WorkerOptions();
options.totalWorkerMemoryLimitMb = 2;
WorkerLifecycleManager manager = new WorkerLifecycleManager(workerPool, options);
assertThat(workerPool.getIdleWorkers()).hasSize(4);
assertThat(workerPool.getNumActive(key1)).isEqualTo(0);
assertThat(workerPool.getNumActive(key2)).isEqualTo(0);
assertThat(w1.getStatus().isValid()).isTrue();
assertThat(w2.getStatus().isValid()).isTrue();
assertThat(w3.getStatus().isValid()).isTrue();
assertThat(w4.getStatus().isValid()).isTrue();
manager.evictWorkers(workerMetrics);
// Only w1 and w4 should be alive.
assertThat(workerPool.getIdleWorkers()).containsExactly(w1.getWorkerId(), w4.getWorkerId());
assertThat(workerPool.getNumActive(key1)).isEqualTo(0);
assertThat(workerPool.getNumActive(key2)).isEqualTo(0);
assertThat(workerPool.borrowObject(key1).getWorkerId()).isEqualTo(w1.getWorkerId());
assertThat(workerPool.borrowObject(key2).getWorkerId()).isEqualTo(w4.getWorkerId());
assertThat(w1.getStatus().isValid()).isTrue();
assertThat(w2.getStatus().get()).isEqualTo(Status.KILLED_DUE_TO_MEMORY_PRESSURE);
assertThat(w3.getStatus().get()).isEqualTo(Status.KILLED_DUE_TO_MEMORY_PRESSURE);
assertThat(w4.getStatus().isValid()).isTrue();
}
@Test
public void testGetEvictionCandidates_testDoomedWorkers() throws Exception {
WorkerPool workerPool =
workerPoolSupplier.get(factoryMock, entryList(DUMMY_MNEMONIC, 2), emptyEntryList());
WorkerKey key = createWorkerKey(DUMMY_MNEMONIC, fileSystem);
Worker w1 = workerPool.borrowObject(key);
Worker w2 = workerPool.borrowObject(key);
ImmutableList<WorkerProcessMetrics> workerMetrics =
ImmutableList.of(
createWorkerMetric(w1, PROCESS_ID_1, /* memoryInKb= */ 2000),
createWorkerMetric(w2, PROCESS_ID_2, /* memoryInKb= */ 2000));
WorkerOptions options = new WorkerOptions();
options.totalWorkerMemoryLimitMb = 1;
options.shrinkWorkerPool = true;
WorkerLifecycleManager manager = new WorkerLifecycleManager(workerPool, options);
assertThat(workerPool.getIdleWorkers()).isEmpty();
assertThat(workerPool.getNumActive(key)).isEqualTo(2);
assertThat(w1.getStatus().isValid()).isTrue();
assertThat(w2.getStatus().isValid()).isTrue();
manager.evictWorkers(workerMetrics);
assertThat(w1.getStatus().get()).isEqualTo(Status.PENDING_KILL_DUE_TO_MEMORY_PRESSURE);
assertThat(w2.getStatus().get()).isEqualTo(Status.PENDING_KILL_DUE_TO_MEMORY_PRESSURE);
// Return only one worker.
workerPool.returnObject(key, w1);
// w1 gets destroyed when it is returned, so there are 0 idle workers.
assertThat(workerPool.getIdleWorkers()).isEmpty();
assertThat(workerPool.getNumActive(key)).isEqualTo(1);
// Since w1 is already returned, it is killed on return.
assertThat(w1.getStatus().get()).isEqualTo(Status.KILLED_DUE_TO_MEMORY_PRESSURE);
// Since w2 is still active, it is marked to be killed which will happen when it is returned.
assertThat(w2.getStatus().get()).isEqualTo(Status.PENDING_KILL_DUE_TO_MEMORY_PRESSURE);
// Return the remaining worker.
workerPool.returnObject(key, w2);
assertThat(w2.getStatus().get()).isEqualTo(Status.KILLED_DUE_TO_MEMORY_PRESSURE);
}
@Test
public void testGetEvictionCandidates_testDoomedAndIdleWorkers() throws Exception {
WorkerPool workerPool =
workerPoolSupplier.get(factoryMock, entryList(DUMMY_MNEMONIC, 5), emptyEntryList());
WorkerKey key = createWorkerKey(DUMMY_MNEMONIC, fileSystem);
Worker w1 = workerPool.borrowObject(key);
Worker w2 = workerPool.borrowObject(key);
Worker w3 = workerPool.borrowObject(key);
Worker w4 = workerPool.borrowObject(key);
Worker w5 = workerPool.borrowObject(key);
workerPool.returnObject(key, w1);
workerPool.returnObject(key, w2);
ImmutableList<WorkerProcessMetrics> workerMetrics =
ImmutableList.of(
createWorkerMetric(w1, PROCESS_ID_1, /* memoryInKb= */ 2000),
createWorkerMetric(w2, PROCESS_ID_2, /* memoryInKb= */ 1000),
createWorkerMetric(w3, PROCESS_ID_3, /* memoryInKb= */ 4000),
createWorkerMetric(w4, PROCESS_ID_4, /* memoryInKb= */ 5000),
createWorkerMetric(w5, PROCESS_ID_5, /* memoryInKb= */ 1000));
WorkerOptions options = new WorkerOptions();
options.totalWorkerMemoryLimitMb = 2;
options.shrinkWorkerPool = true;
WorkerLifecycleManager manager = new WorkerLifecycleManager(workerPool, options);
assertThat(workerPool.getIdleWorkers()).hasSize(2);
assertThat(workerPool.getNumActive(key)).isEqualTo(3);
assertThat(w1.getStatus().isValid()).isTrue();
assertThat(w2.getStatus().isValid()).isTrue();
assertThat(w3.getStatus().isValid()).isTrue();
assertThat(w4.getStatus().isValid()).isTrue();
assertThat(w5.getStatus().isValid()).isTrue();
manager.evictWorkers(workerMetrics);
assertThat(workerPool.getIdleWorkers()).isEmpty();
assertThat(workerPool.getNumActive(key)).isEqualTo(3);
// w1 and w2 are killed immediately.
assertThat(w1.getStatus().get()).isEqualTo(Status.KILLED_DUE_TO_MEMORY_PRESSURE);
assertThat(w2.getStatus().get()).isEqualTo(Status.KILLED_DUE_TO_MEMORY_PRESSURE);
// w3 and w4 are killed only when returned.
assertThat(w3.getStatus().get()).isEqualTo(Status.PENDING_KILL_DUE_TO_MEMORY_PRESSURE);
assertThat(w4.getStatus().get()).isEqualTo(Status.PENDING_KILL_DUE_TO_MEMORY_PRESSURE);
assertThat(w5.getStatus().isValid()).isTrue();
}
@Test
public void evictWorkers_testMultiplexWorkers() throws Exception {
WorkerPool workerPool =
workerPoolSupplier.get(factoryMock, emptyEntryList(), entryList(DUMMY_MNEMONIC, 2));
WorkerKey key =
createWorkerKey(DUMMY_MNEMONIC, fileSystem, /* multiplex= */ true, /* sandboxed= */ false);
Worker w1 = workerPool.borrowObject(key);
Worker w2 = workerPool.borrowObject(key);
// Multiplex workers should share the same status instance.
assertThat(w1.getStatus()).isSameInstanceAs(w2.getStatus());
workerPool.returnObject(key, w1);
workerPool.returnObject(key, w2);
ImmutableList<WorkerProcessMetrics> workerMetrics =
ImmutableList.of(
createMultiplexWorkerMetric(
ImmutableList.of(w1, w2), PROCESS_ID_1, /* memoryInKb= */ 4000));
WorkerOptions options = new WorkerOptions();
options.totalWorkerMemoryLimitMb = 1;
WorkerLifecycleManager manager = new WorkerLifecycleManager(workerPool, options);
manager.evictWorkers(workerMetrics);
assertThat(workerPool.getIdleWorkers()).isEmpty();
assertThat(workerPool.getNumActive(key)).isEqualTo(0);
// Since both w1 and w2 have been returned, it is killed.
assertThat(w1.getStatus().get()).isEqualTo(Status.KILLED_DUE_TO_MEMORY_PRESSURE);
}
@Test
public void evictWorkers_doomMultiplexWorker() throws Exception {
WorkerPool workerPool =
workerPoolSupplier.get(factoryMock, emptyEntryList(), entryList(DUMMY_MNEMONIC, 2));
WorkerKey key =
createWorkerKey(DUMMY_MNEMONIC, fileSystem, /* multiplex= */ true, /* sandboxed= */ false);
Worker w1 = workerPool.borrowObject(key);
Worker w2 = workerPool.borrowObject(key);
// Multiplex workers should share the same status instance.
assertThat(w1.getStatus()).isSameInstanceAs(w2.getStatus());
workerPool.returnObject(key, w1);
ImmutableList<WorkerProcessMetrics> workerMetrics =
ImmutableList.of(
createMultiplexWorkerMetric(
ImmutableList.of(w1, w2), PROCESS_ID_1, /* memoryInKb= */ 4000));
WorkerOptions options = new WorkerOptions();
options.totalWorkerMemoryLimitMb = 1;
options.shrinkWorkerPool = true;
WorkerLifecycleManager manager = new WorkerLifecycleManager(workerPool, options);
manager.evictWorkers(workerMetrics);
// w1 should have been evicted already.
assertThat(workerPool.getIdleWorkers()).isEmpty();
assertThat(workerPool.getNumActive(key)).isEqualTo(1);
// Not yet killed because w2 is still alive (and both share a WorkerProcessStatus).
assertThat(w1.getStatus().get()).isEqualTo(Status.PENDING_KILL_DUE_TO_MEMORY_PRESSURE);
workerPool.returnObject(key, w2);
assertThat(workerPool.getIdleWorkers()).isEmpty();
assertThat(workerPool.getNumActive(key)).isEqualTo(0);
// Status is only set to killed after the last worker proxy is destroyed.
assertThat(w2.getStatus().get()).isEqualTo(Status.KILLED_DUE_TO_MEMORY_PRESSURE);
}
private static final Instant DEFAULT_INSTANT = BlazeClock.instance().now();
private static WorkerProcessMetrics createWorkerMetric(
Worker worker, long processId, int memoryInKb) {
// We need to override the processId.
WorkerProcessMetrics wm =
new WorkerProcessMetrics(
worker.getWorkerId(),
processId,
worker.getStatus(),
worker.getWorkerKey().getMnemonic(),
worker.getWorkerKey().isMultiplex(),
worker.getWorkerKey().isSandboxed(),
worker.getWorkerKey().hashCode());
wm.addCollectedMetrics(memoryInKb, /* collectionTime= */ DEFAULT_INSTANT);
return wm;
}
private static WorkerProcessMetrics createMultiplexWorkerMetric(
ImmutableList<Worker> workers, long processId, int memoryInKb) {
WorkerProcessMetrics workerProcessMetrics =
new WorkerProcessMetrics(
workers.stream().map(Worker::getWorkerId).collect(toImmutableList()),
processId,
workers.get(0).getStatus(),
workers.get(0).getWorkerKey().getMnemonic(),
workers.get(0).getWorkerKey().isMultiplex(),
workers.get(0).getWorkerKey().isSandboxed(),
workers.get(0).getWorkerKey().hashCode());
workerProcessMetrics.addCollectedMetrics(memoryInKb, /* collectionTime= */ DEFAULT_INSTANT);
return workerProcessMetrics;
}
private static ImmutableList<Entry<String, Integer>> emptyEntryList() {
return ImmutableList.of();
}
private static ImmutableList<Entry<String, Integer>> entryList(String key1, int value1) {
return ImmutableList.of(Maps.immutableEntry(key1, value1));
}
private static ImmutableList<Entry<String, Integer>> entryList(
String key1, int value1, String key2, int value2) {
return ImmutableList.of(Maps.immutableEntry(key1, value1), Maps.immutableEntry(key2, value2));
}
}