/* * * * Copyright (c) 2016. David Sowerby * * * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with * * the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 * * * * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on * * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * * specific language governing permissions and limitations under the License. * */ package uk.q3c.krail.core.services; import com.google.inject.Inject; import org.slf4j.Logger; import uk.q3c.krail.core.i18n.Translate; import javax.annotation.Nonnull; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.concurrent.*; import static com.google.common.base.Preconditions.checkNotNull; import static org.slf4j.LoggerFactory.getLogger; import static uk.q3c.krail.core.services.RelatedServicesExecutor.Action.START; import static uk.q3c.krail.core.services.Service.State.RUNNING; /** * Default implementation for {@link RelatedServicesExecutor} * <p> * Created by David Sowerby on 11 Jan 2016 */ public class DefaultRelatedServicesExecutor implements RelatedServicesExecutor { private static Logger log = getLogger(DefaultRelatedServicesExecutor.class); private ServicesModel servicesModel; private Translate translate; private Service service; @Inject protected DefaultRelatedServicesExecutor(ServicesModel servicesModel, Translate translate) { this.servicesModel = servicesModel; this.translate = translate; } public Service getService() { return service; } @Override public void setService(@Nonnull Service service) { checkNotNull(service); this.service = service; } protected List<Service> dependencies() { return servicesModel.findInstanceDependencies(service); } protected List<Service> dependants() { return servicesModel.getInstanceGraph() .findDependants(service, ServicesGraph.Selection.ALWAYS_REQUIRED); } @Override public boolean execute(@Nonnull Action action, @Nonnull Service.Cause cause) { checkNotNull(action); checkNotNull(cause); List<Service> servicesToExecute = (action == START) ? dependencies() : dependants(); //Execute in parallel ExecutorService executor = createStartExecutor(servicesToExecute.size(), action); List<Future<ServiceStatus>> futures = execute(executor, servicesToExecute, action, cause); //Get result from each Future (get() will block until result returned) boolean allActionedSuccessfully = true; for (Future<ServiceStatus> future : futures) { try { ServiceStatus result = future.get(); if (action == START) { // if an optional dependency did not start we do not care // considered loading dependency types, as isOptional() makes call to servicesModel - hoever, that call is only made if a dependency // doesn't start, and getting all the depenency info takes a bit more work if (result.getState() != RUNNING && !isOptional(result.getService())) { allActionedSuccessfully = false; } } else { switch (cause) { case DEPENDENCY_FAILED: case FAILED: if (result.getCause() != Service.Cause.DEPENDENCY_FAILED) { allActionedSuccessfully = false; } break; case DEPENDENCY_STOPPED: case STOPPED: if (result.getCause() != Service.Cause.DEPENDENCY_STOPPED) { allActionedSuccessfully = false; } break; case STARTED: case FAILED_TO_START: throw new ServiceStatusException("Service should not be in this state after a stop()"); case FAILED_TO_STOP: allActionedSuccessfully = false; break; default: throw new ServiceStatusException("Service should not be in this state after a stop()"); } } } catch (InterruptedException e) { log.error("ServicesModel thread interrupted while waiting for dependency to start"); } catch (ExecutionException e) { log.error("Error occurred in thread spawned to start a service", e); } } closeExecutor(executor, action); return allActionedSuccessfully; } private boolean isOptional(Service dependency) { return servicesModel.getClassGraph() .isOptionalDependency(dependency .getServiceKey(), service.getServiceKey()); } protected List<Future<ServiceStatus>> execute(ExecutorService executor, List<Service> services, Action action, Service.Cause cause) { List<Future<ServiceStatus>> futures = new ArrayList<>(); //Submit each to executor, and hold the Future for it for (Service dependency : services) { Callable<ServiceStatus> task; switch (action) { case START: task = dependency::start; break; case STOP: Service.Cause causeForDependant; switch (cause) { case STOPPED: case DEPENDENCY_STOPPED: causeForDependant = Service.Cause.DEPENDENCY_STOPPED; break; case FAILED: case DEPENDENCY_FAILED: causeForDependant = Service.Cause.DEPENDENCY_FAILED; break; default: throw new ServiceCauseException("Unexpected cause value of " + cause); } task = () -> dependency.stop(causeForDependant); break; default: throw new ServiceActionException("Unknown action to execute"); } Future<ServiceStatus> future = executor.submit(task); futures.add(future); } return futures; } /** * Stops the @{code executor} with appropriate timeouts and logging * * @param executor the Executor to be shut down. * @param action the action being taken which used the executor */ protected void closeExecutor(ExecutorService executor, Action action) { try { log.info("Closing Executor after an action to {}, initiated by {}", action.name(), translate.from(service.getNameKey(), Locale.UK)); log.debug("attempt to shutdown executor"); executor.shutdown(); executor.awaitTermination(5, TimeUnit.SECONDS); } catch (InterruptedException e) { log.error("Thread interrupted while shutting down Executor"); } finally { if (!executor.isTerminated()) { log.error("forcing shutdown"); } executor.shutdownNow(); log.info("Services Executor shutdown finished"); } } /** * Provides a ExecutorService If you need to use a different thread pool configuration override {@link RelatedServicesExecutor} * this method in a sub-class, and bind that sub-class to {@link ServicesModel} in {@link ServicesModule}. * * @param dependencies the number of services which will be started using this ExecutorService * @param action the action being undertaken, in case a different ExecutorService configuration is required for different actions * @return ExecutorService with ThreadPool set */ @SuppressWarnings("UnusedParameters") @Nonnull protected ExecutorService createStartExecutor(int dependencies, Action action) { return Executors.newWorkStealingPool(); } }