/* * Licensed to Elasticsearch under one or more contributor * license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright * ownership. Elasticsearch licenses this file to you 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 org.elasticsearch.threadpool; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.util.concurrent.BaseFuture; import org.elasticsearch.common.util.concurrent.EsRejectedExecutionException; import org.elasticsearch.node.Node; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.threadpool.ThreadPool.Cancellable; import org.elasticsearch.threadpool.ThreadPool.Names; import org.elasticsearch.threadpool.ThreadPool.ReschedulingRunnable; import org.junit.After; import org.junit.Before; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.isOneOf; import static org.hamcrest.Matchers.sameInstance; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; /** * Unit tests for the scheduling of tasks with a fixed delay */ public class ScheduleWithFixedDelayTests extends ESTestCase { private ThreadPool threadPool; @Before public void setup() { threadPool = new ThreadPool(Settings.builder().put(Node.NODE_NAME_SETTING.getKey(), "fixed delay tests").build()); } @After public void shutdown() throws Exception { terminate(threadPool); } public void testDoesNotRescheduleUntilExecutionFinished() throws Exception { final TimeValue delay = TimeValue.timeValueMillis(100L); final CountDownLatch startLatch = new CountDownLatch(1); final CountDownLatch pauseLatch = new CountDownLatch(1); ThreadPool threadPool = mock(ThreadPool.class); final Runnable runnable = () -> { // notify that the runnable is started startLatch.countDown(); try { // wait for other thread to un-pause pauseLatch.await(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }; ReschedulingRunnable reschedulingRunnable = new ReschedulingRunnable(runnable, delay, Names.GENERIC, threadPool); // this call was made during construction of the runnable verify(threadPool, times(1)).schedule(delay, Names.GENERIC, reschedulingRunnable); // create a thread and start the runnable Thread runThread = new Thread() { @Override public void run() { reschedulingRunnable.run(); } }; runThread.start(); // wait for the runnable to be started and ensure the runnable hasn't used the threadpool again startLatch.await(); verifyNoMoreInteractions(threadPool); // un-pause the runnable and allow it to complete execution pauseLatch.countDown(); runThread.join(); // validate schedule was called again verify(threadPool, times(2)).schedule(delay, Names.GENERIC, reschedulingRunnable); } public void testThatRunnableIsRescheduled() throws Exception { final CountDownLatch latch = new CountDownLatch(scaledRandomIntBetween(2, 16)); final Runnable countingRunnable = () -> { if (rarely()) { throw new ElasticsearchException("sometimes we throw before counting down"); } latch.countDown(); if (randomBoolean()) { throw new ElasticsearchException("this shouldn't cause the test to fail!"); } }; Cancellable cancellable = threadPool.scheduleWithFixedDelay(countingRunnable, TimeValue.timeValueMillis(10L), Names.GENERIC); assertNotNull(cancellable); // wait for the number of successful count down operations latch.await(); // cancel cancellable.cancel(); assertTrue(cancellable.isCancelled()); } public void testCancellingRunnable() throws Exception { final boolean shouldThrow = randomBoolean(); final AtomicInteger counter = new AtomicInteger(scaledRandomIntBetween(2, 16)); final CountDownLatch doneLatch = new CountDownLatch(1); final AtomicReference<Cancellable> cancellableRef = new AtomicReference<>(); final AtomicBoolean runAfterDone = new AtomicBoolean(false); final Runnable countingRunnable = () -> { if (doneLatch.getCount() == 0) { runAfterDone.set(true); logger.warn("this runnable ran after it was cancelled"); } final Cancellable cancellable = cancellableRef.get(); if (cancellable == null) { // wait for the cancellable to be present before we really start so we can accurately know we cancelled return; } // rarely throw an exception before counting down if (shouldThrow && rarely()) { throw new RuntimeException("throw before count down"); } final int count = counter.decrementAndGet(); // see if we have counted down to zero or below yet. the exception throwing could make us count below zero if (count <= 0) { cancellable.cancel(); doneLatch.countDown(); } // rarely throw an exception after execution if (shouldThrow && rarely()) { throw new RuntimeException("throw at end"); } }; Cancellable cancellable = threadPool.scheduleWithFixedDelay(countingRunnable, TimeValue.timeValueMillis(10L), Names.GENERIC); cancellableRef.set(cancellable); // wait for the runnable to finish doneLatch.await(); // the runnable should have cancelled itself assertTrue(cancellable.isCancelled()); assertFalse(runAfterDone.get()); // rarely wait and make sure the runnable didn't run at the next interval if (rarely()) { assertFalse(awaitBusy(runAfterDone::get, 1L, TimeUnit.SECONDS)); } } public void testBlockingCallOnSchedulerThreadFails() throws Exception { final BaseFuture<Object> future = new BaseFuture<Object>() {}; final TestFuture resultsFuture = new TestFuture(); final boolean getWithTimeout = randomBoolean(); final Runnable runnable = () -> { try { Object obj; if (getWithTimeout) { obj = future.get(1L, TimeUnit.SECONDS); } else { obj = future.get(); } resultsFuture.futureDone(obj); } catch (Throwable t) { resultsFuture.futureDone(t); } }; Cancellable cancellable = threadPool.scheduleWithFixedDelay(runnable, TimeValue.timeValueMillis(10L), Names.SAME); Object resultingObject = resultsFuture.get(); assertNotNull(resultingObject); assertThat(resultingObject, instanceOf(Throwable.class)); Throwable t = (Throwable) resultingObject; assertThat(t, instanceOf(AssertionError.class)); assertThat(t.getMessage(), containsString("Blocking")); assertFalse(cancellable.isCancelled()); } public void testBlockingCallOnNonSchedulerThreadAllowed() throws Exception { final TestFuture future = new TestFuture(); final TestFuture resultsFuture = new TestFuture(); final boolean rethrow = randomBoolean(); final boolean getWithTimeout = randomBoolean(); final Runnable runnable = () -> { try { Object obj; if (getWithTimeout) { obj = future.get(1, TimeUnit.MINUTES); } else { obj = future.get(); } resultsFuture.futureDone(obj); } catch (Throwable t) { resultsFuture.futureDone(t); if (rethrow) { throw new RuntimeException(t); } } }; final Cancellable cancellable = threadPool.scheduleWithFixedDelay(runnable, TimeValue.timeValueMillis(10L), Names.GENERIC); assertFalse(resultsFuture.isDone()); final Object o = new Object(); future.futureDone(o); final Object resultingObject = resultsFuture.get(); assertThat(resultingObject, sameInstance(o)); assertFalse(cancellable.isCancelled()); } public void testOnRejectionCausesCancellation() throws Exception { final TimeValue delay = TimeValue.timeValueMillis(10L); terminate(threadPool); threadPool = new ThreadPool(Settings.builder().put(Node.NODE_NAME_SETTING.getKey(), "fixed delay tests").build()) { @Override public ScheduledFuture<?> schedule(TimeValue delay, String executor, Runnable command) { if (command instanceof ReschedulingRunnable) { ((ReschedulingRunnable) command).onRejection(new EsRejectedExecutionException()); } else { fail("this should only be called with a rescheduling runnable in this test"); } return null; } }; Runnable runnable = () -> {}; ReschedulingRunnable reschedulingRunnable = new ReschedulingRunnable(runnable, delay, Names.GENERIC, threadPool); assertTrue(reschedulingRunnable.isCancelled()); } public void testRunnableRunsAtMostOnceAfterCancellation() throws Exception { final int iterations = scaledRandomIntBetween(1, 12); final AtomicInteger counter = new AtomicInteger(); final CountDownLatch doneLatch = new CountDownLatch(iterations); final Runnable countingRunnable = () -> { counter.incrementAndGet(); doneLatch.countDown(); }; final Cancellable cancellable = threadPool.scheduleWithFixedDelay(countingRunnable, TimeValue.timeValueMillis(10L), Names.GENERIC); doneLatch.await(); cancellable.cancel(); final int counterValue = counter.get(); assertThat(counterValue, isOneOf(iterations, iterations + 1)); if (rarely()) { awaitBusy(() -> { final int value = counter.get(); return value == iterations || value == iterations + 1; }, 50L, TimeUnit.MILLISECONDS); } } static final class TestFuture extends BaseFuture<Object> { boolean futureDone(Object value) { return set(value); } } }