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