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