• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2012 The Guava Authors
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.google.common.util.concurrent;
18 
19 import static com.google.common.truth.Truth.assertThat;
20 import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
21 import static java.util.Arrays.asList;
22 import static java.util.concurrent.TimeUnit.SECONDS;
23 
24 import com.google.common.collect.ImmutableMap;
25 import com.google.common.collect.ImmutableSet;
26 import com.google.common.collect.Lists;
27 import com.google.common.collect.Sets;
28 import com.google.common.testing.NullPointerTester;
29 import com.google.common.testing.TestLogHandler;
30 import com.google.common.util.concurrent.Service.State;
31 import com.google.common.util.concurrent.ServiceManager.Listener;
32 import java.time.Duration;
33 import java.util.Arrays;
34 import java.util.Collection;
35 import java.util.List;
36 import java.util.Set;
37 import java.util.concurrent.CountDownLatch;
38 import java.util.concurrent.Executor;
39 import java.util.concurrent.TimeUnit;
40 import java.util.concurrent.TimeoutException;
41 import java.util.logging.Formatter;
42 import java.util.logging.Level;
43 import java.util.logging.LogRecord;
44 import java.util.logging.Logger;
45 import junit.framework.TestCase;
46 
47 /**
48  * Tests for {@link ServiceManager}.
49  *
50  * @author Luke Sandberg
51  * @author Chris Nokleberg
52  */
53 public class ServiceManagerTest extends TestCase {
54 
55   private static class NoOpService extends AbstractService {
56     @Override
doStart()57     protected void doStart() {
58       notifyStarted();
59     }
60 
61     @Override
doStop()62     protected void doStop() {
63       notifyStopped();
64     }
65   }
66 
67   /*
68    * A NoOp service that will delay the startup and shutdown notification for a configurable amount
69    * of time.
70    */
71   private static class NoOpDelayedService extends NoOpService {
72     private long delay;
73 
NoOpDelayedService(long delay)74     public NoOpDelayedService(long delay) {
75       this.delay = delay;
76     }
77 
78     @Override
doStart()79     protected void doStart() {
80       new Thread() {
81         @Override
82         public void run() {
83           Uninterruptibles.sleepUninterruptibly(delay, TimeUnit.MILLISECONDS);
84           notifyStarted();
85         }
86       }.start();
87     }
88 
89     @Override
doStop()90     protected void doStop() {
91       new Thread() {
92         @Override
93         public void run() {
94           Uninterruptibles.sleepUninterruptibly(delay, TimeUnit.MILLISECONDS);
95           notifyStopped();
96         }
97       }.start();
98     }
99   }
100 
101   private static class FailStartService extends NoOpService {
102     @Override
doStart()103     protected void doStart() {
104       notifyFailed(new IllegalStateException("start failure"));
105     }
106   }
107 
108   private static class FailRunService extends NoOpService {
109     @Override
doStart()110     protected void doStart() {
111       super.doStart();
112       notifyFailed(new IllegalStateException("run failure"));
113     }
114   }
115 
116   private static class FailStopService extends NoOpService {
117     @Override
doStop()118     protected void doStop() {
119       notifyFailed(new IllegalStateException("stop failure"));
120     }
121   }
122 
123 
testServiceStartupTimes()124   public void testServiceStartupTimes() {
125     Service a = new NoOpDelayedService(150);
126     Service b = new NoOpDelayedService(353);
127     ServiceManager serviceManager = new ServiceManager(asList(a, b));
128     serviceManager.startAsync().awaitHealthy();
129     ImmutableMap<Service, Long> startupTimes = serviceManager.startupTimes();
130     assertThat(startupTimes).hasSize(2);
131     assertThat(startupTimes.get(a)).isAtLeast(150);
132     assertThat(startupTimes.get(b)).isAtLeast(353);
133   }
134 
135 
testServiceStartupDurations()136   public void testServiceStartupDurations() {
137     Service a = new NoOpDelayedService(150);
138     Service b = new NoOpDelayedService(353);
139     ServiceManager serviceManager = new ServiceManager(asList(a, b));
140     serviceManager.startAsync().awaitHealthy();
141     ImmutableMap<Service, Duration> startupTimes = serviceManager.startupDurations();
142     assertThat(startupTimes).hasSize(2);
143     assertThat(startupTimes.get(a)).isAtLeast(Duration.ofMillis(150));
144     assertThat(startupTimes.get(b)).isAtLeast(Duration.ofMillis(353));
145   }
146 
147 
testServiceStartupTimes_selfStartingServices()148   public void testServiceStartupTimes_selfStartingServices() {
149     // This tests to ensure that:
150     // 1. service times are accurate when the service is started by the manager
151     // 2. service times are recorded when the service is not started by the manager (but they may
152     // not be accurate).
153     final Service b =
154         new NoOpDelayedService(353) {
155           @Override
156           protected void doStart() {
157             super.doStart();
158             // This will delay service listener execution at least 150 milliseconds
159             Uninterruptibles.sleepUninterruptibly(150, TimeUnit.MILLISECONDS);
160           }
161         };
162     Service a =
163         new NoOpDelayedService(150) {
164           @Override
165           protected void doStart() {
166             b.startAsync();
167             super.doStart();
168           }
169         };
170     ServiceManager serviceManager = new ServiceManager(asList(a, b));
171     serviceManager.startAsync().awaitHealthy();
172     ImmutableMap<Service, Long> startupTimes = serviceManager.startupTimes();
173     assertThat(startupTimes).hasSize(2);
174     assertThat(startupTimes.get(a)).isAtLeast(150);
175     // Service b startup takes at least 353 millis, but starting the timer is delayed by at least
176     // 150 milliseconds. so in a perfect world the timing would be 353-150=203ms, but since either
177     // of our sleep calls can be arbitrarily delayed we should just assert that there is a time
178     // recorded.
179     assertThat(startupTimes.get(b)).isNotNull();
180   }
181 
182 
testServiceStartStop()183   public void testServiceStartStop() {
184     Service a = new NoOpService();
185     Service b = new NoOpService();
186     ServiceManager manager = new ServiceManager(asList(a, b));
187     RecordingListener listener = new RecordingListener();
188     manager.addListener(listener, directExecutor());
189     assertState(manager, Service.State.NEW, a, b);
190     assertFalse(manager.isHealthy());
191     manager.startAsync().awaitHealthy();
192     assertState(manager, Service.State.RUNNING, a, b);
193     assertTrue(manager.isHealthy());
194     assertTrue(listener.healthyCalled);
195     assertFalse(listener.stoppedCalled);
196     assertTrue(listener.failedServices.isEmpty());
197     manager.stopAsync().awaitStopped();
198     assertState(manager, Service.State.TERMINATED, a, b);
199     assertFalse(manager.isHealthy());
200     assertTrue(listener.stoppedCalled);
201     assertTrue(listener.failedServices.isEmpty());
202   }
203 
204 
testFailStart()205   public void testFailStart() throws Exception {
206     Service a = new NoOpService();
207     Service b = new FailStartService();
208     Service c = new NoOpService();
209     Service d = new FailStartService();
210     Service e = new NoOpService();
211     ServiceManager manager = new ServiceManager(asList(a, b, c, d, e));
212     RecordingListener listener = new RecordingListener();
213     manager.addListener(listener, directExecutor());
214     assertState(manager, Service.State.NEW, a, b, c, d, e);
215     try {
216       manager.startAsync().awaitHealthy();
217       fail();
218     } catch (IllegalStateException expected) {
219     }
220     assertFalse(listener.healthyCalled);
221     assertState(manager, Service.State.RUNNING, a, c, e);
222     assertEquals(ImmutableSet.of(b, d), listener.failedServices);
223     assertState(manager, Service.State.FAILED, b, d);
224     assertFalse(manager.isHealthy());
225 
226     manager.stopAsync().awaitStopped();
227     assertFalse(manager.isHealthy());
228     assertFalse(listener.healthyCalled);
229     assertTrue(listener.stoppedCalled);
230   }
231 
232 
testFailRun()233   public void testFailRun() throws Exception {
234     Service a = new NoOpService();
235     Service b = new FailRunService();
236     ServiceManager manager = new ServiceManager(asList(a, b));
237     RecordingListener listener = new RecordingListener();
238     manager.addListener(listener, directExecutor());
239     assertState(manager, Service.State.NEW, a, b);
240     try {
241       manager.startAsync().awaitHealthy();
242       fail();
243     } catch (IllegalStateException expected) {
244     }
245     assertTrue(listener.healthyCalled);
246     assertEquals(ImmutableSet.of(b), listener.failedServices);
247 
248     manager.stopAsync().awaitStopped();
249     assertState(manager, Service.State.FAILED, b);
250     assertState(manager, Service.State.TERMINATED, a);
251 
252     assertTrue(listener.stoppedCalled);
253   }
254 
255 
testFailStop()256   public void testFailStop() throws Exception {
257     Service a = new NoOpService();
258     Service b = new FailStopService();
259     Service c = new NoOpService();
260     ServiceManager manager = new ServiceManager(asList(a, b, c));
261     RecordingListener listener = new RecordingListener();
262     manager.addListener(listener, directExecutor());
263 
264     manager.startAsync().awaitHealthy();
265     assertTrue(listener.healthyCalled);
266     assertFalse(listener.stoppedCalled);
267     manager.stopAsync().awaitStopped();
268 
269     assertTrue(listener.stoppedCalled);
270     assertEquals(ImmutableSet.of(b), listener.failedServices);
271     assertState(manager, Service.State.FAILED, b);
272     assertState(manager, Service.State.TERMINATED, a, c);
273   }
274 
testToString()275   public void testToString() throws Exception {
276     Service a = new NoOpService();
277     Service b = new FailStartService();
278     ServiceManager manager = new ServiceManager(asList(a, b));
279     String toString = manager.toString();
280     assertThat(toString).contains("NoOpService");
281     assertThat(toString).contains("FailStartService");
282   }
283 
284 
testTimeouts()285   public void testTimeouts() throws Exception {
286     Service a = new NoOpDelayedService(50);
287     ServiceManager manager = new ServiceManager(asList(a));
288     manager.startAsync();
289     try {
290       manager.awaitHealthy(1, TimeUnit.MILLISECONDS);
291       fail();
292     } catch (TimeoutException expected) {
293     }
294     manager.awaitHealthy(5, SECONDS); // no exception thrown
295 
296     manager.stopAsync();
297     try {
298       manager.awaitStopped(1, TimeUnit.MILLISECONDS);
299       fail();
300     } catch (TimeoutException expected) {
301     }
302     manager.awaitStopped(5, SECONDS); // no exception thrown
303   }
304 
305   /**
306    * This covers a case where if the last service to stop failed then the stopped callback would
307    * never be called.
308    */
testSingleFailedServiceCallsStopped()309   public void testSingleFailedServiceCallsStopped() {
310     Service a = new FailStartService();
311     ServiceManager manager = new ServiceManager(asList(a));
312     RecordingListener listener = new RecordingListener();
313     manager.addListener(listener, directExecutor());
314     try {
315       manager.startAsync().awaitHealthy();
316       fail();
317     } catch (IllegalStateException expected) {
318     }
319     assertTrue(listener.stoppedCalled);
320   }
321 
322   /**
323    * This covers a bug where listener.healthy would get called when a single service failed during
324    * startup (it occurred in more complicated cases also).
325    */
testFailStart_singleServiceCallsHealthy()326   public void testFailStart_singleServiceCallsHealthy() {
327     Service a = new FailStartService();
328     ServiceManager manager = new ServiceManager(asList(a));
329     RecordingListener listener = new RecordingListener();
330     manager.addListener(listener, directExecutor());
331     try {
332       manager.startAsync().awaitHealthy();
333       fail();
334     } catch (IllegalStateException expected) {
335     }
336     assertFalse(listener.healthyCalled);
337   }
338 
339   /**
340    * This covers a bug where if a listener was installed that would stop the manager if any service
341    * fails and something failed during startup before service.start was called on all the services,
342    * then awaitStopped would deadlock due to an IllegalStateException that was thrown when trying to
343    * stop the timer(!).
344    */
testFailStart_stopOthers()345   public void testFailStart_stopOthers() throws TimeoutException {
346     Service a = new FailStartService();
347     Service b = new NoOpService();
348     final ServiceManager manager = new ServiceManager(asList(a, b));
349     manager.addListener(
350         new Listener() {
351           @Override
352           public void failure(Service service) {
353             manager.stopAsync();
354           }
355         },
356         directExecutor());
357     manager.startAsync();
358     manager.awaitStopped(10, TimeUnit.MILLISECONDS);
359   }
360 
testDoCancelStart()361   public void testDoCancelStart() throws TimeoutException {
362     Service a =
363         new AbstractService() {
364           @Override
365           protected void doStart() {
366             // Never starts!
367           }
368 
369           @Override
370           protected void doCancelStart() {
371             assertThat(state()).isEqualTo(Service.State.STOPPING);
372             notifyStopped();
373           }
374 
375           @Override
376           protected void doStop() {
377             throw new AssertionError(); // Should not be called.
378           }
379         };
380 
381     final ServiceManager manager = new ServiceManager(asList(a));
382     manager.startAsync();
383     manager.stopAsync();
384     manager.awaitStopped(10, TimeUnit.MILLISECONDS);
385     assertThat(manager.servicesByState().keySet()).containsExactly(Service.State.TERMINATED);
386   }
387 
testNotifyStoppedAfterFailure()388   public void testNotifyStoppedAfterFailure() throws TimeoutException {
389     Service a =
390         new AbstractService() {
391           @Override
392           protected void doStart() {
393             notifyFailed(new IllegalStateException("start failure"));
394             notifyStopped(); // This will be a no-op.
395           }
396 
397           @Override
398           protected void doStop() {
399             notifyStopped();
400           }
401         };
402     final ServiceManager manager = new ServiceManager(asList(a));
403     manager.startAsync();
404     manager.awaitStopped(10, TimeUnit.MILLISECONDS);
405     assertThat(manager.servicesByState().keySet()).containsExactly(Service.State.FAILED);
406   }
407 
assertState( ServiceManager manager, Service.State state, Service... services)408   private static void assertState(
409       ServiceManager manager, Service.State state, Service... services) {
410     Collection<Service> managerServices = manager.servicesByState().get(state);
411     for (Service service : services) {
412       assertEquals(service.toString(), state, service.state());
413       assertEquals(service.toString(), service.isRunning(), state == Service.State.RUNNING);
414       assertTrue(managerServices + " should contain " + service, managerServices.contains(service));
415     }
416   }
417 
418   /**
419    * This is for covering a case where the ServiceManager would behave strangely if constructed with
420    * no service under management. Listeners would never fire because the ServiceManager was healthy
421    * and stopped at the same time. This test ensures that listeners fire and isHealthy makes sense.
422    */
testEmptyServiceManager()423   public void testEmptyServiceManager() {
424     Logger logger = Logger.getLogger(ServiceManager.class.getName());
425     logger.setLevel(Level.FINEST);
426     TestLogHandler logHandler = new TestLogHandler();
427     logger.addHandler(logHandler);
428     ServiceManager manager = new ServiceManager(Arrays.<Service>asList());
429     RecordingListener listener = new RecordingListener();
430     manager.addListener(listener, directExecutor());
431     manager.startAsync().awaitHealthy();
432     assertTrue(manager.isHealthy());
433     assertTrue(listener.healthyCalled);
434     assertFalse(listener.stoppedCalled);
435     assertTrue(listener.failedServices.isEmpty());
436     manager.stopAsync().awaitStopped();
437     assertFalse(manager.isHealthy());
438     assertTrue(listener.stoppedCalled);
439     assertTrue(listener.failedServices.isEmpty());
440     // check that our NoOpService is not directly observable via any of the inspection methods or
441     // via logging.
442     assertEquals("ServiceManager{services=[]}", manager.toString());
443     assertTrue(manager.servicesByState().isEmpty());
444     assertTrue(manager.startupTimes().isEmpty());
445     Formatter logFormatter =
446         new Formatter() {
447           @Override
448           public String format(LogRecord record) {
449             return formatMessage(record);
450           }
451         };
452     for (LogRecord record : logHandler.getStoredLogRecords()) {
453       assertThat(logFormatter.format(record)).doesNotContain("NoOpService");
454     }
455   }
456 
457   /**
458    * Tests that a ServiceManager can be fully shut down if one of its failure listeners is slow or
459    * even permanently blocked.
460    */
461 
testListenerDeadlock()462   public void testListenerDeadlock() throws InterruptedException {
463     final CountDownLatch failEnter = new CountDownLatch(1);
464     final CountDownLatch failLeave = new CountDownLatch(1);
465     final CountDownLatch afterStarted = new CountDownLatch(1);
466     Service failRunService =
467         new AbstractService() {
468           @Override
469           protected void doStart() {
470             new Thread() {
471               @Override
472               public void run() {
473                 notifyStarted();
474                 // We need to wait for the main thread to leave the ServiceManager.startAsync call
475                 // to
476                 // ensure that the thread running the failure callbacks is not the main thread.
477                 Uninterruptibles.awaitUninterruptibly(afterStarted);
478                 notifyFailed(new Exception("boom"));
479               }
480             }.start();
481           }
482 
483           @Override
484           protected void doStop() {
485             notifyStopped();
486           }
487         };
488     final ServiceManager manager =
489         new ServiceManager(Arrays.asList(failRunService, new NoOpService()));
490     manager.addListener(
491         new ServiceManager.Listener() {
492           @Override
493           public void failure(Service service) {
494             failEnter.countDown();
495             // block until after the service manager is shutdown
496             Uninterruptibles.awaitUninterruptibly(failLeave);
497           }
498         },
499         directExecutor());
500     manager.startAsync();
501     afterStarted.countDown();
502     // We do not call awaitHealthy because, due to races, that method may throw an exception.  But
503     // we really just want to wait for the thread to be in the failure callback so we wait for that
504     // explicitly instead.
505     failEnter.await();
506     assertFalse("State should be updated before calling listeners", manager.isHealthy());
507     // now we want to stop the services.
508     Thread stoppingThread =
509         new Thread() {
510           @Override
511           public void run() {
512             manager.stopAsync().awaitStopped();
513           }
514         };
515     stoppingThread.start();
516     // this should be super fast since the only non stopped service is a NoOpService
517     stoppingThread.join(1000);
518     assertFalse("stopAsync has deadlocked!.", stoppingThread.isAlive());
519     failLeave.countDown(); // release the background thread
520   }
521 
522   /**
523    * Catches a bug where when constructing a service manager failed, later interactions with the
524    * service could cause IllegalStateExceptions inside the partially constructed ServiceManager.
525    * This ISE wouldn't actually bubble up but would get logged by ExecutionQueue. This obfuscated
526    * the original error (which was not constructing ServiceManager correctly).
527    */
testPartiallyConstructedManager()528   public void testPartiallyConstructedManager() {
529     Logger logger = Logger.getLogger("global");
530     logger.setLevel(Level.FINEST);
531     TestLogHandler logHandler = new TestLogHandler();
532     logger.addHandler(logHandler);
533     NoOpService service = new NoOpService();
534     service.startAsync();
535     try {
536       new ServiceManager(Arrays.asList(service));
537       fail();
538     } catch (IllegalArgumentException expected) {
539     }
540     service.stopAsync();
541     // Nothing was logged!
542     assertEquals(0, logHandler.getStoredLogRecords().size());
543   }
544 
testPartiallyConstructedManager_transitionAfterAddListenerBeforeStateIsReady()545   public void testPartiallyConstructedManager_transitionAfterAddListenerBeforeStateIsReady() {
546     // The implementation of this test is pretty sensitive to the implementation :( but we want to
547     // ensure that if weird things happen during construction then we get exceptions.
548     final NoOpService service1 = new NoOpService();
549     // This service will start service1 when addListener is called.  This simulates service1 being
550     // started asynchronously.
551     Service service2 =
552         new Service() {
553           final NoOpService delegate = new NoOpService();
554 
555           @Override
556           public final void addListener(Listener listener, Executor executor) {
557             service1.startAsync();
558             delegate.addListener(listener, executor);
559           }
560           // Delegates from here on down
561           @Override
562           public final Service startAsync() {
563             return delegate.startAsync();
564           }
565 
566           @Override
567           public final Service stopAsync() {
568             return delegate.stopAsync();
569           }
570 
571           @Override
572           public final void awaitRunning() {
573             delegate.awaitRunning();
574           }
575 
576           @Override
577           public final void awaitRunning(long timeout, TimeUnit unit) throws TimeoutException {
578             delegate.awaitRunning(timeout, unit);
579           }
580 
581           @Override
582           public final void awaitTerminated() {
583             delegate.awaitTerminated();
584           }
585 
586           @Override
587           public final void awaitTerminated(long timeout, TimeUnit unit) throws TimeoutException {
588             delegate.awaitTerminated(timeout, unit);
589           }
590 
591           @Override
592           public final boolean isRunning() {
593             return delegate.isRunning();
594           }
595 
596           @Override
597           public final State state() {
598             return delegate.state();
599           }
600 
601           @Override
602           public final Throwable failureCause() {
603             return delegate.failureCause();
604           }
605         };
606     try {
607       new ServiceManager(Arrays.asList(service1, service2));
608       fail();
609     } catch (IllegalArgumentException expected) {
610       assertThat(expected.getMessage()).contains("started transitioning asynchronously");
611     }
612   }
613 
614   /**
615    * This test is for a case where two Service.Listener callbacks for the same service would call
616    * transitionService in the wrong order due to a race. Due to the fact that it is a race this test
617    * isn't guaranteed to expose the issue, but it is at least likely to become flaky if the race
618    * sneaks back in, and in this case flaky means something is definitely wrong.
619    *
620    * <p>Before the bug was fixed this test would fail at least 30% of the time.
621    */
622 
testTransitionRace()623   public void testTransitionRace() throws TimeoutException {
624     for (int k = 0; k < 1000; k++) {
625       List<Service> services = Lists.newArrayList();
626       for (int i = 0; i < 5; i++) {
627         services.add(new SnappyShutdownService(i));
628       }
629       ServiceManager manager = new ServiceManager(services);
630       manager.startAsync().awaitHealthy();
631       manager.stopAsync().awaitStopped(10, TimeUnit.SECONDS);
632     }
633   }
634 
635   /**
636    * This service will shutdown very quickly after stopAsync is called and uses a background thread
637    * so that we know that the stopping() listeners will execute on a different thread than the
638    * terminated() listeners.
639    */
640   private static class SnappyShutdownService extends AbstractExecutionThreadService {
641     final int index;
642     final CountDownLatch latch = new CountDownLatch(1);
643 
SnappyShutdownService(int index)644     SnappyShutdownService(int index) {
645       this.index = index;
646     }
647 
648     @Override
run()649     protected void run() throws Exception {
650       latch.await();
651     }
652 
653     @Override
triggerShutdown()654     protected void triggerShutdown() {
655       latch.countDown();
656     }
657 
658     @Override
serviceName()659     protected String serviceName() {
660       return this.getClass().getSimpleName() + "[" + index + "]";
661     }
662   }
663 
testNulls()664   public void testNulls() {
665     ServiceManager manager = new ServiceManager(Arrays.<Service>asList());
666     new NullPointerTester()
667         .setDefault(ServiceManager.Listener.class, new RecordingListener())
668         .testAllPublicInstanceMethods(manager);
669   }
670 
671   private static final class RecordingListener extends ServiceManager.Listener {
672     volatile boolean healthyCalled;
673     volatile boolean stoppedCalled;
674     final Set<Service> failedServices = Sets.newConcurrentHashSet();
675 
676     @Override
healthy()677     public void healthy() {
678       healthyCalled = true;
679     }
680 
681     @Override
stopped()682     public void stopped() {
683       stoppedCalled = true;
684     }
685 
686     @Override
failure(Service service)687     public void failure(Service service) {
688       failedServices.add(service);
689     }
690   }
691 }
692