/** * Updates the state with the given service transition. * * <p>This method performs the main logic of ServiceManager in the following steps. * * <ol> * <li>Update the {@link #servicesByState()} * <li>Update the {@link #startupTimers} * <li>Based on the new state queue listeners to run * <li>Run the listeners (outside of the lock) * </ol> */ void transitionService(final Service service, State from, State to) { checkNotNull(service); checkArgument(from != to); monitor.enter(); try { transitioned = true; if (!ready) { return; } // Update state. checkState( servicesByState.remove(from, service), "Service %s not at the expected location in the state map %s", service, from); checkState( servicesByState.put(to, service), "Service %s in the state map unexpectedly at %s", service, to); // Update the timer Stopwatch stopwatch = startupTimers.get(service); if (stopwatch == null) { // This means the service was started by some means other than ServiceManager.startAsync stopwatch = Stopwatch.createStarted(); startupTimers.put(service, stopwatch); } if (to.compareTo(RUNNING) >= 0 && stopwatch.isRunning()) { // N.B. if we miss the STARTING event then we may never record a startup time. stopwatch.stop(); if (!(service instanceof NoOpService)) { logger.log(Level.FINE, "Started {0} in {1}.", new Object[] {service, stopwatch}); } } // Queue our listeners // Did a service fail? if (to == FAILED) { fireFailedListeners(service); } if (states.count(RUNNING) == numberOfServices) { // This means that the manager is currently healthy. N.B. If other threads call isHealthy // they are not guaranteed to get 'true', because any service could fail right now. fireHealthyListeners(); } else if (states.count(TERMINATED) + states.count(FAILED) == numberOfServices) { fireStoppedListeners(); } } finally { monitor.leave(); // Run our executors outside of the lock executeListeners(); } }
ImmutableMultimap<State, Service> servicesByState() { ImmutableSetMultimap.Builder<State, Service> builder = ImmutableSetMultimap.builder(); monitor.enter(); try { for (Entry<State, Service> entry : servicesByState.entries()) { if (!(entry.getValue() instanceof NoOpService)) { builder.put(entry.getKey(), entry.getValue()); } } } finally { monitor.leave(); } return builder.build(); }
/** * It is implicitly assumed that all the services are NEW and that they will all remain NEW * until all the Listeners are installed and {@link #markReady()} is called. It is our caller's * responsibility to only call {@link #markReady()} if all services were new at the time this * method was called and when all the listeners were installed. */ ServiceManagerState(ImmutableCollection<Service> services) { this.numberOfServices = services.size(); servicesByState.putAll(NEW, services); }
/** * An encapsulation of all the mutable state of the {@link ServiceManager} that needs to be * accessed by instances of {@link ServiceListener}. */ private static final class ServiceManagerState { final Monitor monitor = new Monitor(); @GuardedBy("monitor") final SetMultimap<State, Service> servicesByState = Multimaps.newSetMultimap( new EnumMap<State, Collection<Service>>(State.class), new Supplier<Set<Service>>() { @Override public Set<Service> get() { return Sets.newLinkedHashSet(); } }); @GuardedBy("monitor") final Multiset<State> states = servicesByState.keys(); @GuardedBy("monitor") final Map<Service, Stopwatch> startupTimers = Maps.newIdentityHashMap(); /** * These two booleans are used to mark the state as ready to start. {@link #ready}: is set by * {@link #markReady} to indicate that all listeners have been correctly installed {@link * #transitioned}: is set by {@link #transitionService} to indicate that some transition has * been performed. * * <p>Together, they allow us to enforce that all services have their listeners installed prior * to any service performing a transition, then we can fail in the ServiceManager constructor * rather than in a Service.Listener callback. */ @GuardedBy("monitor") boolean ready; @GuardedBy("monitor") boolean transitioned; final int numberOfServices; /** * Controls how long to wait for all the services to either become healthy or reach a state from * which it is guaranteed that it can never become healthy. */ final Monitor.Guard awaitHealthGuard = new Monitor.Guard(monitor) { @Override public boolean isSatisfied() { // All services have started or some service has terminated/failed. return states.count(RUNNING) == numberOfServices || states.contains(STOPPING) || states.contains(TERMINATED) || states.contains(FAILED); } }; /** Controls how long to wait for all services to reach a terminal state. */ final Monitor.Guard stoppedGuard = new Monitor.Guard(monitor) { @Override public boolean isSatisfied() { return states.count(TERMINATED) + states.count(FAILED) == numberOfServices; } }; /** The listeners to notify during a state transition. */ @GuardedBy("monitor") final List<ListenerCallQueue<Listener>> listeners = Collections.synchronizedList(new ArrayList<ListenerCallQueue<Listener>>()); /** * It is implicitly assumed that all the services are NEW and that they will all remain NEW * until all the Listeners are installed and {@link #markReady()} is called. It is our caller's * responsibility to only call {@link #markReady()} if all services were new at the time this * method was called and when all the listeners were installed. */ ServiceManagerState(ImmutableCollection<Service> services) { this.numberOfServices = services.size(); servicesByState.putAll(NEW, services); } /** * Attempts to start the timer immediately prior to the service being started via {@link * Service#startAsync()}. */ void tryStartTiming(Service service) { monitor.enter(); try { Stopwatch stopwatch = startupTimers.get(service); if (stopwatch == null) { startupTimers.put(service, Stopwatch.createStarted()); } } finally { monitor.leave(); } } /** * Marks the {@link State} as ready to receive transitions. Returns true if no transitions have * been observed yet. */ void markReady() { monitor.enter(); try { if (!transitioned) { // nothing has transitioned since construction, good. ready = true; } else { // This should be an extremely rare race condition. List<Service> servicesInBadStates = Lists.newArrayList(); for (Service service : servicesByState().values()) { if (service.state() != NEW) { servicesInBadStates.add(service); } } throw new IllegalArgumentException( "Services started transitioning asynchronously before " + "the ServiceManager was constructed: " + servicesInBadStates); } } finally { monitor.leave(); } } void addListener(Listener listener, Executor executor) { checkNotNull(listener, "listener"); checkNotNull(executor, "executor"); monitor.enter(); try { // no point in adding a listener that will never be called if (!stoppedGuard.isSatisfied()) { listeners.add(new ListenerCallQueue<Listener>(listener, executor)); } } finally { monitor.leave(); } } void awaitHealthy() { monitor.enterWhenUninterruptibly(awaitHealthGuard); try { checkHealthy(); } finally { monitor.leave(); } } void awaitHealthy(long timeout, TimeUnit unit) throws TimeoutException { monitor.enter(); try { if (!monitor.waitForUninterruptibly(awaitHealthGuard, timeout, unit)) { throw new TimeoutException( "Timeout waiting for the services to become healthy. The " + "following services have not started: " + Multimaps.filterKeys(servicesByState, in(ImmutableSet.of(NEW, STARTING)))); } checkHealthy(); } finally { monitor.leave(); } } void awaitStopped() { monitor.enterWhenUninterruptibly(stoppedGuard); monitor.leave(); } void awaitStopped(long timeout, TimeUnit unit) throws TimeoutException { monitor.enter(); try { if (!monitor.waitForUninterruptibly(stoppedGuard, timeout, unit)) { throw new TimeoutException( "Timeout waiting for the services to stop. The following " + "services have not stopped: " + Multimaps.filterKeys( servicesByState, not(in(ImmutableSet.of(TERMINATED, FAILED))))); } } finally { monitor.leave(); } } ImmutableMultimap<State, Service> servicesByState() { ImmutableSetMultimap.Builder<State, Service> builder = ImmutableSetMultimap.builder(); monitor.enter(); try { for (Entry<State, Service> entry : servicesByState.entries()) { if (!(entry.getValue() instanceof NoOpService)) { builder.put(entry.getKey(), entry.getValue()); } } } finally { monitor.leave(); } return builder.build(); } ImmutableMap<Service, Long> startupTimes() { List<Entry<Service, Long>> loadTimes; monitor.enter(); try { loadTimes = Lists.newArrayListWithCapacity(startupTimers.size()); // N.B. There will only be an entry in the map if the service has started for (Entry<Service, Stopwatch> entry : startupTimers.entrySet()) { Service service = entry.getKey(); Stopwatch stopWatch = entry.getValue(); if (!stopWatch.isRunning() && !(service instanceof NoOpService)) { loadTimes.add(Maps.immutableEntry(service, stopWatch.elapsed(MILLISECONDS))); } } } finally { monitor.leave(); } Collections.sort( loadTimes, Ordering.<Long>natural() .onResultOf( new Function<Entry<Service, Long>, Long>() { @Override public Long apply(Map.Entry<Service, Long> input) { return input.getValue(); } })); ImmutableMap.Builder<Service, Long> builder = ImmutableMap.builder(); for (Entry<Service, Long> entry : loadTimes) { builder.put(entry); } return builder.build(); } /** * Updates the state with the given service transition. * * <p>This method performs the main logic of ServiceManager in the following steps. * * <ol> * <li>Update the {@link #servicesByState()} * <li>Update the {@link #startupTimers} * <li>Based on the new state queue listeners to run * <li>Run the listeners (outside of the lock) * </ol> */ void transitionService(final Service service, State from, State to) { checkNotNull(service); checkArgument(from != to); monitor.enter(); try { transitioned = true; if (!ready) { return; } // Update state. checkState( servicesByState.remove(from, service), "Service %s not at the expected location in the state map %s", service, from); checkState( servicesByState.put(to, service), "Service %s in the state map unexpectedly at %s", service, to); // Update the timer Stopwatch stopwatch = startupTimers.get(service); if (stopwatch == null) { // This means the service was started by some means other than ServiceManager.startAsync stopwatch = Stopwatch.createStarted(); startupTimers.put(service, stopwatch); } if (to.compareTo(RUNNING) >= 0 && stopwatch.isRunning()) { // N.B. if we miss the STARTING event then we may never record a startup time. stopwatch.stop(); if (!(service instanceof NoOpService)) { logger.log(Level.FINE, "Started {0} in {1}.", new Object[] {service, stopwatch}); } } // Queue our listeners // Did a service fail? if (to == FAILED) { fireFailedListeners(service); } if (states.count(RUNNING) == numberOfServices) { // This means that the manager is currently healthy. N.B. If other threads call isHealthy // they are not guaranteed to get 'true', because any service could fail right now. fireHealthyListeners(); } else if (states.count(TERMINATED) + states.count(FAILED) == numberOfServices) { fireStoppedListeners(); } } finally { monitor.leave(); // Run our executors outside of the lock executeListeners(); } } @GuardedBy("monitor") void fireStoppedListeners() { STOPPED_CALLBACK.enqueueOn(listeners); } @GuardedBy("monitor") void fireHealthyListeners() { HEALTHY_CALLBACK.enqueueOn(listeners); } @GuardedBy("monitor") void fireFailedListeners(final Service service) { new Callback<Listener>("failed({service=" + service + "})") { @Override void call(Listener listener) { listener.failure(service); } }.enqueueOn(listeners); } /** Attempts to execute all the listeners in {@link #listeners}. */ void executeListeners() { checkState( !monitor.isOccupiedByCurrentThread(), "It is incorrect to execute listeners with the monitor held."); // iterate by index to avoid concurrent modification exceptions for (int i = 0; i < listeners.size(); i++) { listeners.get(i).execute(); } } @GuardedBy("monitor") void checkHealthy() { if (states.count(RUNNING) != numberOfServices) { IllegalStateException exception = new IllegalStateException( "Expected to be healthy after starting. The following services are not running: " + Multimaps.filterKeys(servicesByState, not(equalTo(RUNNING)))); throw exception; } } }