예제 #1
0
    /**
     * 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();
      }
    }
예제 #2
0
 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();
 }
예제 #3
0
 /**
  * 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);
 }
예제 #4
0
  /**
   * 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;
      }
    }
  }