/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.ce.taskprocessor;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListenableScheduledFuture;
import com.google.common.util.concurrent.ListeningScheduledExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.Queue;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.DisableOnDebug;
import org.junit.rules.TestRule;
import org.junit.rules.Timeout;
import org.sonar.ce.configuration.CeConfigurationRule;
import static com.google.common.collect.ImmutableList.copyOf;
import static java.util.Collections.emptySet;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class CeProcessingSchedulerImplTest {
private static final Error ERROR_TO_INTERRUPT_CHAINING = new Error("Error should stop scheduling");
@Rule
// due to risks of infinite chaining of tasks/futures, a timeout is required for safety
public TestRule safeguardTimeout = new DisableOnDebug(Timeout.seconds(60));
@Rule
public CeConfigurationRule ceConfiguration = new CeConfigurationRule();
// Required to prevent an infinite loop
private CeWorker ceWorker = mock(CeWorker.class);
private CeWorkerFactory ceWorkerFactory = new TestCeWorkerFactory(ceWorker);
private StubCeProcessingSchedulerExecutorService processingExecutorService = new StubCeProcessingSchedulerExecutorService();
private SchedulerCall regularDelayedPoll = new SchedulerCall(ceWorker, 2000L, MILLISECONDS);
private SchedulerCall notDelayedPoll = new SchedulerCall(ceWorker);
private CeProcessingSchedulerImpl underTest = new CeProcessingSchedulerImpl(ceConfiguration, processingExecutorService, ceWorkerFactory);
@Test
public void polls_without_delay_when_CeWorkerCallable_returns_true() throws Exception {
when(ceWorker.call())
.thenReturn(true)
.thenThrow(ERROR_TO_INTERRUPT_CHAINING);
startSchedulingAndRun();
assertThat(processingExecutorService.getSchedulerCalls()).containsOnly(
regularDelayedPoll,
notDelayedPoll
);
}
@Test
public void polls_without_delay_when_CeWorkerCallable_throws_Exception_but_not_Error() throws Exception {
when(ceWorker.call())
.thenThrow(new Exception("Exception is followed by a poll without delay"))
.thenThrow(ERROR_TO_INTERRUPT_CHAINING);
startSchedulingAndRun();
assertThat(processingExecutorService.getSchedulerCalls()).containsExactly(
regularDelayedPoll,
notDelayedPoll
);
}
@Test
public void polls_with_regular_delay_when_CeWorkerCallable_returns_false() throws Exception {
when(ceWorker.call())
.thenReturn(false)
.thenThrow(ERROR_TO_INTERRUPT_CHAINING);
startSchedulingAndRun();
assertThat(processingExecutorService.getSchedulerCalls()).containsExactly(
regularDelayedPoll,
regularDelayedPoll
);
}
@Test
public void startScheduling_schedules_CeWorkerCallable_at_fixed_rate_run_head_of_queue() throws Exception {
when(ceWorker.call())
.thenReturn(true)
.thenReturn(true)
.thenReturn(false)
.thenReturn(true)
.thenReturn(false)
.thenThrow(new Exception("IAE should not cause scheduling to stop"))
.thenReturn(false)
.thenReturn(false)
.thenReturn(false)
.thenThrow(ERROR_TO_INTERRUPT_CHAINING);
startSchedulingAndRun();
assertThat(processingExecutorService.getSchedulerCalls()).containsExactly(
regularDelayedPoll,
notDelayedPoll,
notDelayedPoll,
regularDelayedPoll,
notDelayedPoll,
regularDelayedPoll,
notDelayedPoll,
regularDelayedPoll,
regularDelayedPoll,
regularDelayedPoll
);
}
@Test
public void stop_cancels_next_polling_and_does_not_add_any_new_one() throws Exception {
when(ceWorker.call())
.thenReturn(false)
.thenReturn(true)
.thenReturn(false)
.thenReturn(false)
.thenReturn(false)
.thenReturn(false)
.thenReturn(false)
.thenThrow(ERROR_TO_INTERRUPT_CHAINING);
underTest.startScheduling();
int cancelledTaskFutureCount = 0;
int i = 0;
while (processingExecutorService.futures.peek() != null) {
Future<?> future = processingExecutorService.futures.poll();
if (future.isCancelled()) {
cancelledTaskFutureCount++;
} else {
future.get();
}
// call stop after second delayed polling
if (i == 1) {
underTest.stop();
}
i++;
}
assertThat(cancelledTaskFutureCount).isEqualTo(1);
assertThat(processingExecutorService.getSchedulerCalls()).containsExactly(
regularDelayedPoll,
regularDelayedPoll,
notDelayedPoll,
regularDelayedPoll
);
}
@Test
public void when_workerCount_is_more_than_1_as_many_CeWorkerCallable_are_scheduled() throws Exception {
int workerCount = Math.abs(new Random().nextInt(10)) + 1;
ceConfiguration.setWorkerCount(workerCount);
CeWorker[] workers = new CeWorker[workerCount];
for (int i = 0; i < workerCount; i++) {
workers[i] = mock(CeWorker.class);
when(workers[i].call())
.thenReturn(false)
.thenThrow(ERROR_TO_INTERRUPT_CHAINING);
}
ListenableScheduledFuture listenableScheduledFuture = mock(ListenableScheduledFuture.class);
CeProcessingSchedulerExecutorService processingExecutorService = mock(CeProcessingSchedulerExecutorService.class);
when(processingExecutorService.schedule(any(CeWorker.class), any(Long.class),any(TimeUnit.class))).thenReturn(listenableScheduledFuture);
CeWorkerFactory ceWorkerFactory = spy(new TestCeWorkerFactory(workers));
CeProcessingSchedulerImpl underTest = new CeProcessingSchedulerImpl(ceConfiguration, processingExecutorService, ceWorkerFactory);
when(processingExecutorService.schedule(ceWorker, ceConfiguration.getQueuePollingDelay(), MILLISECONDS))
.thenReturn(listenableScheduledFuture);
underTest.startScheduling();
// No exception from TestCeWorkerFactory must be thrown
// Verify that schedule has been called on all workers
for (int i = 0; i < workerCount; i++) {
verify(processingExecutorService).schedule(workers[i], ceConfiguration.getQueuePollingDelay(), MILLISECONDS);
}
verify(listenableScheduledFuture, times(workerCount)).addListener(any(Runnable.class), eq(processingExecutorService));
verify(ceWorkerFactory, times(workerCount)).create();
}
private void startSchedulingAndRun() throws ExecutionException, InterruptedException {
underTest.startScheduling();
// execute future synchronously
processingExecutorService.runFutures();
}
private class TestCeWorkerFactory implements CeWorkerFactory {
private final Iterator<CeWorker> ceWorkers;
private TestCeWorkerFactory(CeWorker... ceWorkers) {
this.ceWorkers = copyOf(ceWorkers).iterator();
}
@Override
public CeWorker create() {
// This will throw an NoSuchElementException if there are too many calls
return ceWorkers.next();
}
@Override
public Set<String> getWorkerUUIDs() {
return emptySet();
}
}
/**
* A synchronous implementation of {@link CeProcessingSchedulerExecutorService} which exposes a synchronous
* method to execute futures it creates and exposes a method to retrieve logs of calls to
* {@link CeProcessingSchedulerExecutorService#schedule(Callable, long, TimeUnit)} which is used by
* {@link CeProcessingSchedulerImpl}.
*/
private static class StubCeProcessingSchedulerExecutorService implements CeProcessingSchedulerExecutorService {
private final Queue<Future<?>> futures = new ConcurrentLinkedQueue<>();
private final ListeningScheduledExecutorService delegate = MoreExecutors.listeningDecorator(new SynchronousStubExecutorService());
private final List<SchedulerCall> schedulerCalls = new ArrayList<>();
public List<SchedulerCall> getSchedulerCalls() {
return schedulerCalls;
}
public void runFutures() throws ExecutionException, InterruptedException {
while (futures.peek() != null) {
Future<?> future = futures.poll();
if (!future.isCancelled()) {
future.get();
}
}
}
@Override
public <V> ListenableScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit) {
this.schedulerCalls.add(new SchedulerCall(callable, delay, unit));
return delegate.schedule(callable, delay, unit);
}
@Override
public <T> ListenableFuture<T> submit(Callable<T> task) {
this.schedulerCalls.add(new SchedulerCall(task));
return delegate.submit(task);
}
@Override
public void stop() {
throw new UnsupportedOperationException("stop() not implemented");
}
// ////////////// delegated methods ////////////////
@Override
public ListenableScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) {
return delegate.schedule(command, delay, unit);
}
@Override
public ListenableScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) {
return delegate.scheduleAtFixedRate(command, initialDelay, period, unit);
}
@Override
public ListenableScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) {
return delegate.scheduleWithFixedDelay(command, initialDelay, delay, unit);
}
@Override
public void shutdown() {
delegate.shutdown();
}
@Override
public List<Runnable> shutdownNow() {
return delegate.shutdownNow();
}
@Override
public boolean isShutdown() {
return delegate.isShutdown();
}
@Override
public boolean isTerminated() {
return delegate.isTerminated();
}
@Override
public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
return delegate.awaitTermination(timeout, unit);
}
@Override
public <T> ListenableFuture<T> submit(Runnable task, T result) {
return delegate.submit(task, result);
}
@Override
public ListenableFuture<?> submit(Runnable task) {
return delegate.submit(task);
}
@Override
public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException {
return delegate.invokeAll(tasks);
}
@Override
public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException {
return delegate.invokeAll(tasks, timeout, unit);
}
@Override
public <T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException {
return delegate.invokeAny(tasks);
}
@Override
public <T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
return delegate.invokeAny(tasks, timeout, unit);
}
@Override
public void execute(Runnable command) {
delegate.execute(command);
}
/**
* A partial (only 3 methods) implementation of ScheduledExecutorService which stores futures it creates into
* {@link StubCeProcessingSchedulerExecutorService#futures}.
*/
private class SynchronousStubExecutorService implements ScheduledExecutorService {
@Override
public ScheduledFuture<?> schedule(final Runnable command, long delay, TimeUnit unit) {
ScheduledFuture<Void> res = new AbstractPartiallyImplementedScheduledFuture<Void>() {
@Override
public Void get() throws InterruptedException, ExecutionException {
command.run();
return null;
}
};
futures.add(res);
return res;
}
@Override
public <V> ScheduledFuture<V> schedule(final Callable<V> callable, long delay, TimeUnit unit) {
ScheduledFuture<V> res = new AbstractPartiallyImplementedScheduledFuture<V>() {
@Override
public V get() throws InterruptedException, ExecutionException {
try {
return callable.call();
} catch (Exception e) {
throw new ExecutionException(e);
}
}
};
futures.add(res);
return res;
}
@Override
public <T> Future<T> submit(final Callable<T> task) {
Future<T> res = new AbstractPartiallyImplementedFuture<T>() {
@Override
public T get() throws InterruptedException, ExecutionException {
try {
return task.call();
} catch (Exception e) {
throw new ExecutionException(e);
}
}
};
futures.add(res);
return res;
}
@Override
public void execute(Runnable command) {
command.run();
}
/////////// unsupported operations ///////////
@Override
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) {
throw new UnsupportedOperationException("scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) not implemented");
}
@Override
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) {
throw new UnsupportedOperationException("scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) not implemented");
}
@Override
public void shutdown() {
throw new UnsupportedOperationException("shutdown() not implemented");
}
@Override
public List<Runnable> shutdownNow() {
throw new UnsupportedOperationException("shutdownNow() not implemented");
}
@Override
public boolean isShutdown() {
throw new UnsupportedOperationException("isShutdown() not implemented");
}
@Override
public boolean isTerminated() {
throw new UnsupportedOperationException("isTerminated() not implemented");
}
@Override
public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
throw new UnsupportedOperationException("awaitTermination(long timeout, TimeUnit unit) not implemented");
}
@Override
public <T> Future<T> submit(Runnable task, T result) {
throw new UnsupportedOperationException("submit(Runnable task, T result) not implemented");
}
@Override
public Future<?> submit(Runnable task) {
throw new UnsupportedOperationException("submit(Runnable task) not implemented");
}
@Override
public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException {
throw new UnsupportedOperationException("invokeAll(Collection<? extends Callable<T>> tasks) not implemented");
}
@Override
public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException {
throw new UnsupportedOperationException("invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) not implemented");
}
@Override
public <T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException {
throw new UnsupportedOperationException("invokeAny(Collection<? extends Callable<T>> tasks) not implemented");
}
@Override
public <T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
throw new UnsupportedOperationException("invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) not implemented");
}
}
}
private static abstract class AbstractPartiallyImplementedScheduledFuture<V> extends AbstractPartiallyImplementedFuture<V> implements ScheduledFuture<V> {
@Override
public long getDelay(TimeUnit unit) {
throw new UnsupportedOperationException("getDelay(TimeUnit unit) not implemented");
}
@Override
public int compareTo(Delayed o) {
throw new UnsupportedOperationException("compareTo(Delayed o) not implemented");
}
}
private static abstract class AbstractPartiallyImplementedFuture<T> implements Future<T> {
private boolean cancelled = false;
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
this.cancelled = true;
return true;
}
@Override
public boolean isCancelled() {
return this.cancelled;
}
@Override
public boolean isDone() {
throw new UnsupportedOperationException("isDone() not implemented");
}
@Override
public T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
throw new UnsupportedOperationException("get(long timeout, TimeUnit unit) not implemented");
}
}
/**
* Used to log parameters of calls to {@link CeProcessingSchedulerExecutorService#schedule(Callable, long, TimeUnit)}
*/
@Immutable
private static final class SchedulerCall {
private final Callable<?> callable;
private final long delay;
private final TimeUnit unit;
private SchedulerCall(Callable<?> callable, long delay, TimeUnit unit) {
this.callable = callable;
this.delay = delay;
this.unit = unit;
}
private SchedulerCall(Callable<?> callable) {
this.callable = callable;
this.delay = -63366;
this.unit = TimeUnit.NANOSECONDS;
}
@Override
public boolean equals(@Nullable Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
SchedulerCall that = (SchedulerCall) o;
return delay == that.delay && callable == that.callable && unit.equals(that.unit);
}
@Override
public int hashCode() {
return Objects.hash(callable, delay, unit);
}
@Override
public String toString() {
return "SchedulerCall{" +
"callable=" + callable +
", delay=" + delay +
", unit=" + unit +
'}';
}
}
}