| // 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( |
| new WorkerPoolConfig( |
| factory, singleplexMaxInstances, multiplexMaxInstances)), |
| workerFactorySupplier, |
| }, |
| { |
| (WorkerPoolSupplier) |
| (factory, singleplexMaxInstances, multiplexMaxInstances) -> |
| new WorkerPoolImpl( |
| new WorkerPoolConfig( |
| factory, 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)); |
| }) |
| .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)); |
| } |
| } |