/* * 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 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 junit.framework.TestCase; import java.lang.Thread.UncaughtExceptionHandler; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import javax.annotation.concurrent.GuardedBy; /** * Unit test for {@link AbstractService}. * * @author Jesse Wilson */ public class AbstractServiceTest extends TestCase { 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.start(); assertEquals(State.RUNNING, service.state()); assertTrue(service.isRunning()); assertTrue(service.running); service.stop(); 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.start().get(); assertEquals(State.RUNNING, service.state()); service.stop().get(); assertEquals(State.TERMINATED, service.state()); } public void testNoOpServiceStartStopIdempotence() throws Exception { NoOpService service = new NoOpService(); RecordingListener listener = RecordingListener.record(service); service.start(); service.start(); assertEquals(State.RUNNING, service.state()); service.stop(); service.stop(); assertEquals(State.TERMINATED, service.state()); assertEquals( ImmutableList.of( State.STARTING, State.RUNNING, State.STOPPING, State.TERMINATED), listener.getStateHistory()); } public void testNoOpServiceStartStopIdempotenceAfterWait() throws Exception { NoOpService service = new NoOpService(); service.start().get(); service.start(); assertEquals(State.RUNNING, service.state()); service.stop().get(); service.stop(); assertEquals(State.TERMINATED, service.state()); } public void testNoOpServiceStartStopIdempotenceDoubleWait() throws Exception { NoOpService service = new NoOpService(); service.start().get(); service.start().get(); assertEquals(State.RUNNING, service.state()); service.stop().get(); service.stop().get(); assertEquals(State.TERMINATED, service.state()); } public void testNoOpServiceStartStopAndWaitUninterruptible() throws Exception { NoOpService service = new NoOpService(); currentThread().interrupt(); try { service.startAndWait(); assertEquals(State.RUNNING, service.state()); service.stopAndWait(); 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.start(); 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.stop(); 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.start(); 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.start(); assertEquals(State.STARTING, service.state()); assertFalse(service.isRunning()); assertTrue(service.doStartCalled); service.stop(); 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()); } public void testManualServiceStopWhileNew() throws Exception { ManualSwitchedService service = new ManualSwitchedService(); RecordingListener listener = RecordingListener.record(service); service.stop(); 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.start(); 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.start(); 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.start(); service.notifyStarted(); service.stop(); service.notifyFailed(EXCEPTION); assertEquals(ImmutableList.of(State.STARTING, State.RUNNING, State.STOPPING, State.FAILED), listener.getStateHistory()); } public void testManualServiceUnrequestedStop() { ManualSwitchedService service = new ManualSwitchedService(); service.start(); 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 #start} and {@link #stop}. */ 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 testThreadedServiceStartAndWaitStopAndWait() throws Throwable { ThreadedService service = new ThreadedService(); RecordingListener listener = RecordingListener.record(service); service.start().get(); assertEquals(State.RUNNING, service.state()); service.awaitRunChecks(); service.stopAndWait(); assertEquals(State.TERMINATED, service.state()); throwIfSet(thrownByExecutionThread); assertEquals( ImmutableList.of( State.STARTING, State.RUNNING, State.STOPPING, State.TERMINATED), listener.getStateHistory()); } public void testThreadedServiceStartStopIdempotence() throws Throwable { ThreadedService service = new ThreadedService(); service.start(); service.start().get(); assertEquals(State.RUNNING, service.state()); service.awaitRunChecks(); service.stop(); service.stop().get(); assertEquals(State.TERMINATED, service.state()); throwIfSet(thrownByExecutionThread); } public void testThreadedServiceStartStopIdempotenceAfterWait() throws Throwable { ThreadedService service = new ThreadedService(); service.start().get(); service.start(); assertEquals(State.RUNNING, service.state()); service.awaitRunChecks(); service.stop().get(); service.stop(); assertEquals(State.TERMINATED, service.state()); executionThread.join(); throwIfSet(thrownByExecutionThread); } public void testThreadedServiceStartStopIdempotenceDoubleWait() throws Throwable { ThreadedService service = new ThreadedService(); service.start().get(); service.start().get(); assertEquals(State.RUNNING, service.state()); service.awaitRunChecks(); service.stop().get(); service.stop().get(); assertEquals(State.TERMINATED, service.state()); throwIfSet(thrownByExecutionThread); } public void testManualServiceFailureIdempotence() { ManualSwitchedService service = new ManualSwitchedService(); RecordingListener.record(service); service.start(); service.notifyFailed(new Exception("1")); service.notifyFailed(new Exception("2")); try { service.startAndWait(); fail(); } catch (UncheckedExecutionException e) { assertEquals("1", e.getCause().getMessage()); } } 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); Future<State> stopResult = service.stop(); assertEquals(State.TERMINATED, service.state()); assertEquals(State.TERMINATED, stopResult.get()); Future<State> startResult = service.start(); assertEquals(State.TERMINATED, service.state()); assertEquals(State.TERMINATED, startResult.get()); assertEquals(State.TERMINATED, Iterables.getOnlyElement(listener.getStateHistory())); } public void testFailingServiceStartAndWait() throws Exception { StartFailingService service = new StartFailingService(); RecordingListener listener = RecordingListener.record(service); try { service.startAndWait(); fail(); } catch (UncheckedExecutionException e) { 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.startAndWait(); try { service.stopAndWait(); fail(); } catch (UncheckedExecutionException e) { assertEquals(EXCEPTION, e.getCause()); } assertEquals( ImmutableList.of( State.STARTING, State.RUNNING, State.STOPPING, State.FAILED), listener.getStateHistory()); } public void testFailingServiceStopAndWait_runFailinging() throws Exception { RunFailingService service = new RunFailingService(); RecordingListener listener = RecordingListener.record(service); service.startAndWait(); try { service.stopAndWait(); fail(); } catch (UncheckedExecutionException e) { assertEquals(EXCEPTION, e.getCause().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.startAndWait(); fail(); } catch (UncheckedExecutionException e) { 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.startAndWait(); try { service.stopAndWait(); fail(); } catch (UncheckedExecutionException e) { 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.startAndWait(); try { service.stopAndWait(); fail(); } catch (UncheckedExecutionException e) { assertEquals(service.exception, e.getCause().getCause()); } assertEquals( ImmutableList.of( State.STARTING, State.RUNNING, State.FAILED), listener.getStateHistory()); } public void testAddListenerAfterFailureDoesntCauseDeadlock() throws InterruptedException { final StartFailingService service = new StartFailingService(); service.start(); assertEquals(State.FAILED, service.state()); service.addListener(new RecordingListener(service), MoreExecutors.sameThreadExecutor()); Thread thread = new Thread() { @Override public void run() { // Internally start() grabs a lock, this could be any such method on AbstractService. service.start(); } }; thread.start(); thread.join(100); assertFalse(thread + " is deadlocked", thread.isAlive()); } public void testListenerDoesntDeadlockOnStartAndWaitFromRunning() throws Exception { final NoOpThreadedService service = new NoOpThreadedService(); service.addListener(new Listener() { @Override public void starting() { } @Override public void running() { service.startAndWait(); } @Override public void stopping(State from) { } @Override public void terminated(State from) { } @Override public void failed(State from, Throwable failure) { } }, MoreExecutors.sameThreadExecutor()); service.start().get(10, TimeUnit.MILLISECONDS); service.stop(); } public void testListenerDoesntDeadlockOnStopAndWaitFromTerminated() throws Exception { final NoOpThreadedService service = new NoOpThreadedService(); service.addListener(new Listener() { @Override public void starting() { } @Override public void running() { } @Override public void stopping(State from) { } @Override public void terminated(State from) { service.stopAndWait(); } @Override public void failed(State from, Throwable failure) { } }, MoreExecutors.sameThreadExecutor()); service.startAndWait(); Thread thread = new Thread() { @Override public void run() { service.stopAndWait(); } }; thread.start(); thread.join(100); assertFalse(thread + " is deadlocked", thread.isAlive()); } private static class NoOpThreadedService extends AbstractExecutionThreadService { @Override protected void run() throws Exception {} } 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 implements Listener { static RecordingListener record(Service service) { RecordingListener listener = new RecordingListener(service); service.addListener(listener, MoreExecutors.sameThreadExecutor()); 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); assertTrue(service.start().isDone()); assertEquals(State.RUNNING, service.startAndWait()); 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) { assertTrue(service.start().isDone()); assertEquals(State.STOPPING, service.startAndWait()); } 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()); assertTrue(service.start().isDone()); if (from == State.NEW) { assertEquals(State.TERMINATED, service.startAndWait()); } assertTrue(service.stop().isDone()); assertEquals(State.TERMINATED, service.stopAndWait()); 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()); if (from == State.STARTING) { try { service.startAndWait(); fail(); } catch (UncheckedExecutionException e) { assertEquals(failure, e.getCause()); } } try { service.stopAndWait(); fail(); } catch (UncheckedExecutionException e) { if (from == State.STOPPING) { assertEquals(failure, e.getCause()); } else { assertEquals(failure, e.getCause().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.startAndWait(); service.stopAndWait(); 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(); }