/* * Copyright (C) 2009 The Guava Authors * * 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.common.util.concurrent; import static com.google.common.truth.Truth.assertThat; import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static java.lang.Thread.currentThread; import static java.util.concurrent.TimeUnit.SECONDS; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.util.concurrent.Service.Listener; import com.google.common.util.concurrent.Service.State; import java.lang.Thread.UncaughtExceptionHandler; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import javax.annotation.concurrent.GuardedBy; import junit.framework.TestCase; /** * Unit test for {@link AbstractService}. * * @author Jesse Wilson */ public class AbstractServiceTest extends TestCase { private static final long LONG_TIMEOUT_MILLIS = 2500; private Thread executionThread; private Throwable thrownByExecutionThread; public void testNoOpServiceStartStop() throws Exception { NoOpService service = new NoOpService(); RecordingListener listener = RecordingListener.record(service); assertEquals(State.NEW, service.state()); assertFalse(service.isRunning()); assertFalse(service.running); service.startAsync(); assertEquals(State.RUNNING, service.state()); assertTrue(service.isRunning()); assertTrue(service.running); service.stopAsync(); assertEquals(State.TERMINATED, service.state()); assertFalse(service.isRunning()); assertFalse(service.running); assertEquals( ImmutableList.of( State.STARTING, State.RUNNING, State.STOPPING, State.TERMINATED), listener.getStateHistory()); } public void testNoOpServiceStartAndWaitStopAndWait() throws Exception { NoOpService service = new NoOpService(); service.startAsync().awaitRunning(); assertEquals(State.RUNNING, service.state()); service.stopAsync().awaitTerminated(); assertEquals(State.TERMINATED, service.state()); } public void testNoOpServiceStartAsyncAndAwaitStopAsyncAndAwait() throws Exception { NoOpService service = new NoOpService(); service.startAsync().awaitRunning(); assertEquals(State.RUNNING, service.state()); service.stopAsync().awaitTerminated(); assertEquals(State.TERMINATED, service.state()); } public void testNoOpServiceStopIdempotence() throws Exception { NoOpService service = new NoOpService(); RecordingListener listener = RecordingListener.record(service); service.startAsync().awaitRunning(); assertEquals(State.RUNNING, service.state()); service.stopAsync(); service.stopAsync(); assertEquals(State.TERMINATED, service.state()); assertEquals( ImmutableList.of( State.STARTING, State.RUNNING, State.STOPPING, State.TERMINATED), listener.getStateHistory()); } public void testNoOpServiceStopIdempotenceAfterWait() throws Exception { NoOpService service = new NoOpService(); service.startAsync().awaitRunning(); service.stopAsync().awaitTerminated(); service.stopAsync(); assertEquals(State.TERMINATED, service.state()); } public void testNoOpServiceStopIdempotenceDoubleWait() throws Exception { NoOpService service = new NoOpService(); service.startAsync().awaitRunning(); assertEquals(State.RUNNING, service.state()); service.stopAsync().awaitTerminated(); service.stopAsync().awaitTerminated(); assertEquals(State.TERMINATED, service.state()); } public void testNoOpServiceStartStopAndWaitUninterruptible() throws Exception { NoOpService service = new NoOpService(); currentThread().interrupt(); try { service.startAsync().awaitRunning(); assertEquals(State.RUNNING, service.state()); service.stopAsync().awaitTerminated(); assertEquals(State.TERMINATED, service.state()); assertTrue(currentThread().isInterrupted()); } finally { Thread.interrupted(); // clear interrupt for future tests } } private static class NoOpService extends AbstractService { boolean running = false; @Override protected void doStart() { assertFalse(running); running = true; notifyStarted(); } @Override protected void doStop() { assertTrue(running); running = false; notifyStopped(); } } public void testManualServiceStartStop() throws Exception { ManualSwitchedService service = new ManualSwitchedService(); RecordingListener listener = RecordingListener.record(service); service.startAsync(); assertEquals(State.STARTING, service.state()); assertFalse(service.isRunning()); assertTrue(service.doStartCalled); service.notifyStarted(); // usually this would be invoked by another thread assertEquals(State.RUNNING, service.state()); assertTrue(service.isRunning()); service.stopAsync(); assertEquals(State.STOPPING, service.state()); assertFalse(service.isRunning()); assertTrue(service.doStopCalled); service.notifyStopped(); // usually this would be invoked by another thread assertEquals(State.TERMINATED, service.state()); assertFalse(service.isRunning()); assertEquals( ImmutableList.of( State.STARTING, State.RUNNING, State.STOPPING, State.TERMINATED), listener.getStateHistory()); } public void testManualServiceNotifyStoppedWhileRunning() throws Exception { ManualSwitchedService service = new ManualSwitchedService(); RecordingListener listener = RecordingListener.record(service); service.startAsync(); service.notifyStarted(); service.notifyStopped(); assertEquals(State.TERMINATED, service.state()); assertFalse(service.isRunning()); assertFalse(service.doStopCalled); assertEquals( ImmutableList.of( State.STARTING, State.RUNNING, State.TERMINATED), listener.getStateHistory()); } public void testManualServiceStopWhileStarting() throws Exception { ManualSwitchedService service = new ManualSwitchedService(); RecordingListener listener = RecordingListener.record(service); service.startAsync(); assertEquals(State.STARTING, service.state()); assertFalse(service.isRunning()); assertTrue(service.doStartCalled); service.stopAsync(); assertEquals(State.STOPPING, service.state()); assertFalse(service.isRunning()); assertFalse(service.doStopCalled); service.notifyStarted(); assertEquals(State.STOPPING, service.state()); assertFalse(service.isRunning()); assertTrue(service.doStopCalled); service.notifyStopped(); assertEquals(State.TERMINATED, service.state()); assertFalse(service.isRunning()); assertEquals( ImmutableList.of( State.STARTING, State.STOPPING, State.TERMINATED), listener.getStateHistory()); } /** * This tests for a bug where if {@link Service#stopAsync()} was called while the service was * {@link State#STARTING} more than once, the {@link Listener#stopping(State)} callback would get * called multiple times. */ public void testManualServiceStopMultipleTimesWhileStarting() throws Exception { ManualSwitchedService service = new ManualSwitchedService(); final AtomicInteger stopppingCount = new AtomicInteger(); service.addListener(new Listener() { @Override public void stopping(State from) { stopppingCount.incrementAndGet(); } }, directExecutor()); service.startAsync(); service.stopAsync(); assertEquals(1, stopppingCount.get()); service.stopAsync(); assertEquals(1, stopppingCount.get()); } public void testManualServiceStopWhileNew() throws Exception { ManualSwitchedService service = new ManualSwitchedService(); RecordingListener listener = RecordingListener.record(service); service.stopAsync(); assertEquals(State.TERMINATED, service.state()); assertFalse(service.isRunning()); assertFalse(service.doStartCalled); assertFalse(service.doStopCalled); assertEquals(ImmutableList.of(State.TERMINATED), listener.getStateHistory()); } public void testManualServiceFailWhileStarting() throws Exception { ManualSwitchedService service = new ManualSwitchedService(); RecordingListener listener = RecordingListener.record(service); service.startAsync(); service.notifyFailed(EXCEPTION); assertEquals(ImmutableList.of(State.STARTING, State.FAILED), listener.getStateHistory()); } public void testManualServiceFailWhileRunning() throws Exception { ManualSwitchedService service = new ManualSwitchedService(); RecordingListener listener = RecordingListener.record(service); service.startAsync(); service.notifyStarted(); service.notifyFailed(EXCEPTION); assertEquals(ImmutableList.of(State.STARTING, State.RUNNING, State.FAILED), listener.getStateHistory()); } public void testManualServiceFailWhileStopping() throws Exception { ManualSwitchedService service = new ManualSwitchedService(); RecordingListener listener = RecordingListener.record(service); service.startAsync(); service.notifyStarted(); service.stopAsync(); service.notifyFailed(EXCEPTION); assertEquals(ImmutableList.of(State.STARTING, State.RUNNING, State.STOPPING, State.FAILED), listener.getStateHistory()); } public void testManualServiceUnrequestedStop() { ManualSwitchedService service = new ManualSwitchedService(); service.startAsync(); service.notifyStarted(); assertEquals(State.RUNNING, service.state()); assertTrue(service.isRunning()); assertFalse(service.doStopCalled); service.notifyStopped(); assertEquals(State.TERMINATED, service.state()); assertFalse(service.isRunning()); assertFalse(service.doStopCalled); } /** * The user of this service should call {@link #notifyStarted} and {@link * #notifyStopped} after calling {@link #startAsync} and {@link #stopAsync}. */ private static class ManualSwitchedService extends AbstractService { boolean doStartCalled = false; boolean doStopCalled = false; @Override protected void doStart() { assertFalse(doStartCalled); doStartCalled = true; } @Override protected void doStop() { assertFalse(doStopCalled); doStopCalled = true; } } public void testAwaitTerminated() throws Exception { final NoOpService service = new NoOpService(); Thread waiter = new Thread() { @Override public void run() { service.awaitTerminated(); } }; waiter.start(); service.startAsync().awaitRunning(); assertEquals(State.RUNNING, service.state()); service.stopAsync(); waiter.join(LONG_TIMEOUT_MILLIS); // ensure that the await in the other thread is triggered assertFalse(waiter.isAlive()); } public void testAwaitTerminated_FailedService() throws Exception { final ManualSwitchedService service = new ManualSwitchedService(); final AtomicReference<Throwable> exception = Atomics.newReference(); Thread waiter = new Thread() { @Override public void run() { try { service.awaitTerminated(); fail("Expected an IllegalStateException"); } catch (Throwable t) { exception.set(t); } } }; waiter.start(); service.startAsync(); service.notifyStarted(); assertEquals(State.RUNNING, service.state()); service.notifyFailed(EXCEPTION); assertEquals(State.FAILED, service.state()); waiter.join(LONG_TIMEOUT_MILLIS); assertFalse(waiter.isAlive()); assertThat(exception.get()).isInstanceOf(IllegalStateException.class); assertEquals(EXCEPTION, exception.get().getCause()); } public void testThreadedServiceStartAndWaitStopAndWait() throws Throwable { ThreadedService service = new ThreadedService(); RecordingListener listener = RecordingListener.record(service); service.startAsync().awaitRunning(); assertEquals(State.RUNNING, service.state()); service.awaitRunChecks(); service.stopAsync().awaitTerminated(); assertEquals(State.TERMINATED, service.state()); throwIfSet(thrownByExecutionThread); assertEquals( ImmutableList.of( State.STARTING, State.RUNNING, State.STOPPING, State.TERMINATED), listener.getStateHistory()); } public void testThreadedServiceStopIdempotence() throws Throwable { ThreadedService service = new ThreadedService(); service.startAsync().awaitRunning(); assertEquals(State.RUNNING, service.state()); service.awaitRunChecks(); service.stopAsync(); service.stopAsync().awaitTerminated(); assertEquals(State.TERMINATED, service.state()); throwIfSet(thrownByExecutionThread); } public void testThreadedServiceStopIdempotenceAfterWait() throws Throwable { ThreadedService service = new ThreadedService(); service.startAsync().awaitRunning(); assertEquals(State.RUNNING, service.state()); service.awaitRunChecks(); service.stopAsync().awaitTerminated(); service.stopAsync(); assertEquals(State.TERMINATED, service.state()); executionThread.join(); throwIfSet(thrownByExecutionThread); } public void testThreadedServiceStopIdempotenceDoubleWait() throws Throwable { ThreadedService service = new ThreadedService(); service.startAsync().awaitRunning(); assertEquals(State.RUNNING, service.state()); service.awaitRunChecks(); service.stopAsync().awaitTerminated(); service.stopAsync().awaitTerminated(); assertEquals(State.TERMINATED, service.state()); throwIfSet(thrownByExecutionThread); } public void testManualServiceFailureIdempotence() { ManualSwitchedService service = new ManualSwitchedService(); /* * Set up a RecordingListener to perform its built-in assertions, even though we won't look at * its state history. */ RecordingListener unused = RecordingListener.record(service); service.startAsync(); service.notifyFailed(new Exception("1")); service.notifyFailed(new Exception("2")); assertThat(service.failureCause()).hasMessage("1"); try { service.awaitRunning(); fail(); } catch (IllegalStateException e) { assertThat(e.getCause()).hasMessage("1"); } } private class ThreadedService extends AbstractService { final CountDownLatch hasConfirmedIsRunning = new CountDownLatch(1); /* * The main test thread tries to stop() the service shortly after * confirming that it is running. Meanwhile, the service itself is trying * to confirm that it is running. If the main thread's stop() call happens * before it has the chance, the test will fail. To avoid this, the main * thread calls this method, which waits until the service has performed * its own "running" check. */ void awaitRunChecks() throws InterruptedException { assertTrue("Service thread hasn't finished its checks. " + "Exception status (possibly stale): " + thrownByExecutionThread, hasConfirmedIsRunning.await(10, SECONDS)); } @Override protected void doStart() { assertEquals(State.STARTING, state()); invokeOnExecutionThreadForTest(new Runnable() { @Override public void run() { assertEquals(State.STARTING, state()); notifyStarted(); assertEquals(State.RUNNING, state()); hasConfirmedIsRunning.countDown(); } }); } @Override protected void doStop() { assertEquals(State.STOPPING, state()); invokeOnExecutionThreadForTest(new Runnable() { @Override public void run() { assertEquals(State.STOPPING, state()); notifyStopped(); assertEquals(State.TERMINATED, state()); } }); } } private void invokeOnExecutionThreadForTest(Runnable runnable) { executionThread = new Thread(runnable); executionThread.setUncaughtExceptionHandler(new UncaughtExceptionHandler() { @Override public void uncaughtException(Thread thread, Throwable e) { thrownByExecutionThread = e; } }); executionThread.start(); } private static void throwIfSet(Throwable t) throws Throwable { if (t != null) { throw t; } } public void testStopUnstartedService() throws Exception { NoOpService service = new NoOpService(); RecordingListener listener = RecordingListener.record(service); service.stopAsync(); assertEquals(State.TERMINATED, service.state()); try { service.startAsync(); fail(); } catch (IllegalStateException expected) {} assertEquals(State.TERMINATED, Iterables.getOnlyElement(listener.getStateHistory())); } public void testFailingServiceStartAndWait() throws Exception { StartFailingService service = new StartFailingService(); RecordingListener listener = RecordingListener.record(service); try { service.startAsync().awaitRunning(); fail(); } catch (IllegalStateException e) { assertEquals(EXCEPTION, service.failureCause()); assertEquals(EXCEPTION, e.getCause()); } assertEquals( ImmutableList.of( State.STARTING, State.FAILED), listener.getStateHistory()); } public void testFailingServiceStopAndWait_stopFailing() throws Exception { StopFailingService service = new StopFailingService(); RecordingListener listener = RecordingListener.record(service); service.startAsync().awaitRunning(); try { service.stopAsync().awaitTerminated(); fail(); } catch (IllegalStateException e) { assertEquals(EXCEPTION, service.failureCause()); assertEquals(EXCEPTION, e.getCause()); } assertEquals( ImmutableList.of( State.STARTING, State.RUNNING, State.STOPPING, State.FAILED), listener.getStateHistory()); } public void testFailingServiceStopAndWait_runFailing() throws Exception { RunFailingService service = new RunFailingService(); RecordingListener listener = RecordingListener.record(service); service.startAsync(); try { service.awaitRunning(); fail(); } catch (IllegalStateException e) { assertEquals(EXCEPTION, service.failureCause()); assertEquals(EXCEPTION, e.getCause()); } assertEquals( ImmutableList.of( State.STARTING, State.RUNNING, State.FAILED), listener.getStateHistory()); } public void testThrowingServiceStartAndWait() throws Exception { StartThrowingService service = new StartThrowingService(); RecordingListener listener = RecordingListener.record(service); try { service.startAsync().awaitRunning(); fail(); } catch (IllegalStateException e) { assertEquals(service.exception, service.failureCause()); assertEquals(service.exception, e.getCause()); } assertEquals( ImmutableList.of( State.STARTING, State.FAILED), listener.getStateHistory()); } public void testThrowingServiceStopAndWait_stopThrowing() throws Exception { StopThrowingService service = new StopThrowingService(); RecordingListener listener = RecordingListener.record(service); service.startAsync().awaitRunning(); try { service.stopAsync().awaitTerminated(); fail(); } catch (IllegalStateException e) { assertEquals(service.exception, service.failureCause()); assertEquals(service.exception, e.getCause()); } assertEquals( ImmutableList.of( State.STARTING, State.RUNNING, State.STOPPING, State.FAILED), listener.getStateHistory()); } public void testThrowingServiceStopAndWait_runThrowing() throws Exception { RunThrowingService service = new RunThrowingService(); RecordingListener listener = RecordingListener.record(service); service.startAsync(); try { service.awaitTerminated(); fail(); } catch (IllegalStateException e) { assertEquals(service.exception, service.failureCause()); assertEquals(service.exception, e.getCause()); } assertEquals( ImmutableList.of( State.STARTING, State.RUNNING, State.FAILED), listener.getStateHistory()); } public void testFailureCause_throwsIfNotFailed() { StopFailingService service = new StopFailingService(); try { service.failureCause(); fail(); } catch (IllegalStateException e) { // expected } service.startAsync().awaitRunning(); try { service.failureCause(); fail(); } catch (IllegalStateException e) { // expected } try { service.stopAsync().awaitTerminated(); fail(); } catch (IllegalStateException e) { assertEquals(EXCEPTION, service.failureCause()); assertEquals(EXCEPTION, e.getCause()); } } public void testAddListenerAfterFailureDoesntCauseDeadlock() throws InterruptedException { final StartFailingService service = new StartFailingService(); service.startAsync(); assertEquals(State.FAILED, service.state()); service.addListener(new RecordingListener(service), directExecutor()); Thread thread = new Thread() { @Override public void run() { // Internally stopAsync() grabs a lock, this could be any such method on AbstractService. service.stopAsync(); } }; thread.start(); thread.join(LONG_TIMEOUT_MILLIS); assertFalse(thread + " is deadlocked", thread.isAlive()); } public void testListenerDoesntDeadlockOnStartAndWaitFromRunning() throws Exception { final NoOpThreadedService service = new NoOpThreadedService(); service.addListener(new Listener() { @Override public void running() { service.awaitRunning(); } }, directExecutor()); service.startAsync().awaitRunning(LONG_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); service.stopAsync(); } public void testListenerDoesntDeadlockOnStopAndWaitFromTerminated() throws Exception { final NoOpThreadedService service = new NoOpThreadedService(); service.addListener(new Listener() { @Override public void terminated(State from) { service.stopAsync().awaitTerminated(); } }, directExecutor()); service.startAsync().awaitRunning(); Thread thread = new Thread() { @Override public void run() { service.stopAsync().awaitTerminated(); } }; thread.start(); thread.join(LONG_TIMEOUT_MILLIS); assertFalse(thread + " is deadlocked", thread.isAlive()); } private static class NoOpThreadedService extends AbstractExecutionThreadService { final CountDownLatch latch = new CountDownLatch(1); @Override protected void run() throws Exception { latch.await(); } @Override protected void triggerShutdown() { latch.countDown(); } } private static class StartFailingService extends AbstractService { @Override protected void doStart() { notifyFailed(EXCEPTION); } @Override protected void doStop() { fail(); } } private static class RunFailingService extends AbstractService { @Override protected void doStart() { notifyStarted(); notifyFailed(EXCEPTION); } @Override protected void doStop() { fail(); } } private static class StopFailingService extends AbstractService { @Override protected void doStart() { notifyStarted(); } @Override protected void doStop() { notifyFailed(EXCEPTION); } } private static class StartThrowingService extends AbstractService { final RuntimeException exception = new RuntimeException("deliberate"); @Override protected void doStart() { throw exception; } @Override protected void doStop() { fail(); } } private static class RunThrowingService extends AbstractService { final RuntimeException exception = new RuntimeException("deliberate"); @Override protected void doStart() { notifyStarted(); throw exception; } @Override protected void doStop() { fail(); } } private static class StopThrowingService extends AbstractService { final RuntimeException exception = new RuntimeException("deliberate"); @Override protected void doStart() { notifyStarted(); } @Override protected void doStop() { throw exception; } } private static class RecordingListener extends Listener { static RecordingListener record(Service service) { RecordingListener listener = new RecordingListener(service); service.addListener(listener, directExecutor()); return listener; } final Service service; RecordingListener(Service service) { this.service = service; } @GuardedBy("this") final List<State> stateHistory = Lists.newArrayList(); final CountDownLatch completionLatch = new CountDownLatch(1); ImmutableList<State> getStateHistory() throws Exception { completionLatch.await(); synchronized (this) { return ImmutableList.copyOf(stateHistory); } } @Override public synchronized void starting() { assertTrue(stateHistory.isEmpty()); assertNotSame(State.NEW, service.state()); stateHistory.add(State.STARTING); } @Override public synchronized void running() { assertEquals(State.STARTING, Iterables.getOnlyElement(stateHistory)); stateHistory.add(State.RUNNING); service.awaitRunning(); assertNotSame(State.STARTING, service.state()); } @Override public synchronized void stopping(State from) { assertEquals(from, Iterables.getLast(stateHistory)); stateHistory.add(State.STOPPING); if (from == State.STARTING) { try { service.awaitRunning(); fail(); } catch (IllegalStateException expected) { assertNull(expected.getCause()); assertTrue(expected.getMessage().equals( "Expected the service " + service + " to be RUNNING, but was STOPPING")); } } assertNotSame(from, service.state()); } @Override public synchronized void terminated(State from) { assertEquals(from, Iterables.getLast(stateHistory, State.NEW)); stateHistory.add(State.TERMINATED); assertEquals(State.TERMINATED, service.state()); if (from == State.NEW) { try { service.awaitRunning(); fail(); } catch (IllegalStateException expected) { assertNull(expected.getCause()); assertTrue(expected.getMessage().equals( "Expected the service " + service + " to be RUNNING, but was TERMINATED")); } } completionLatch.countDown(); } @Override public synchronized void failed(State from, Throwable failure) { assertEquals(from, Iterables.getLast(stateHistory)); stateHistory.add(State.FAILED); assertEquals(State.FAILED, service.state()); assertEquals(failure, service.failureCause()); if (from == State.STARTING) { try { service.awaitRunning(); fail(); } catch (IllegalStateException e) { assertEquals(failure, e.getCause()); } } try { service.awaitTerminated(); fail(); } catch (IllegalStateException e) { assertEquals(failure, e.getCause()); } completionLatch.countDown(); } } public void testNotifyStartedWhenNotStarting() { AbstractService service = new DefaultService(); try { service.notifyStarted(); fail(); } catch (IllegalStateException expected) {} } public void testNotifyStoppedWhenNotRunning() { AbstractService service = new DefaultService(); try { service.notifyStopped(); fail(); } catch (IllegalStateException expected) {} } public void testNotifyFailedWhenNotStarted() { AbstractService service = new DefaultService(); try { service.notifyFailed(new Exception()); fail(); } catch (IllegalStateException expected) {} } public void testNotifyFailedWhenTerminated() { NoOpService service = new NoOpService(); service.startAsync().awaitRunning(); service.stopAsync().awaitTerminated(); try { service.notifyFailed(new Exception()); fail(); } catch (IllegalStateException expected) {} } private static class DefaultService extends AbstractService { @Override protected void doStart() {} @Override protected void doStop() {} } private static final Exception EXCEPTION = new Exception(); }