/*
*
* * 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 edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import net.engio.mbassy.bus.common.PubSubSupport;
import net.engio.mbassy.listener.Listener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import uk.q3c.krail.core.eventbus.BusMessage;
import uk.q3c.krail.core.eventbus.GlobalBus;
import uk.q3c.krail.core.eventbus.GlobalBusProvider;
import uk.q3c.krail.core.eventbus.SubscribeTo;
import uk.q3c.krail.core.i18n.I18NKey;
import uk.q3c.krail.core.i18n.Translate;
import javax.annotation.Nonnull;
import javax.annotation.concurrent.ThreadSafe;
import static uk.q3c.krail.core.services.Service.State.*;
/**
* The easiest way to provide a {@link Service} is to sub-class either this class or {@link AbstractService}. For management of dependencies between services,
* see {@link ServicesGraph}
* <p>
* Dedicated start and stop listeners can be used to respond to dependencies changing their state to started or stopped
* respectively, and are used to respond to state changes in dependencies. service change listeners are fired every
* time
* there is a change of state (and is used by the {@link DefaultServicesMonitor})<br>
* <p>
* All service events are published on the GlobalBus, and all instances of {@link AbstractService} are subscribed to the GlobalBus; this enables the some of
* the logic of service dependencies - for example, when a service needs to respond when a service it depends on stops.
* <p>
* This also means that it is not necessary to annotate a sub-class of AbstractService with a {@link Listener}, unless: <ol>
* <li>you want to specify strong references, </li>
* <li>you want to subscribe to another event bus as well the {@link GlobalBus}, in which case you will need both {@link Listener} and {@link SubscribeTo}
* annotations</ol>
* <p>
*
* @author David Sowerby
*/
@Listener
@ThreadSafe
public abstract class AbstractService implements Service {
private static Logger log = LoggerFactory.getLogger(AbstractService.class);
private final Translate translate;
protected State state = INITIAL;
private RelatedServicesExecutor servicesExecutor;
private I18NKey descriptionKey;
private I18NKey nameKey;
private PubSubSupport<BusMessage> eventBus;
private int instanceNumber = 0;
private Cause cause;
@Inject
protected AbstractService(Translate translate, GlobalBusProvider globalBusProvider, RelatedServicesExecutor servicesExecutor) {
super();
this.translate = translate;
this.servicesExecutor = servicesExecutor;
servicesExecutor.setService(this);
eventBus = globalBusProvider.get();
}
@Override
public I18NKey getNameKey() {
return nameKey;
}
public void setNameKey(@Nonnull I18NKey nameKey) {
this.nameKey = nameKey;
}
@Override
public synchronized Cause getCause() {
return cause;
}
@Override
public synchronized boolean isStarted() {
return state == RUNNING;
}
@Override
public ServiceStatus stop() {
return stop(Cause.STOPPED);
}
@Nonnull
@Override
public synchronized ServiceStatus stop(@Nonnull Cause cause) {
if (state == STOPPED || state == STOPPING || state == FAILED || state == RESETTING) {
log.debug("Attempting to stop service {}, but it is already stopped or resetting. No action taken", getName());
return new ServiceStatus(this, this.state, this.cause);
}
if (state == INITIAL) {
log.debug("Currently in INITIAL state, stop or fail ignored");
return new ServiceStatus(this, this.state, this.cause);
}
log.info("Stopping service: {}", getName());
setState(STOPPING, cause);
//boolean dependantsRequiringThisAreStopped
servicesExecutor.execute(RelatedServicesExecutor.Action.STOP, cause); // also stop / fail dependants which
// always require this service
try {
doStop();
setState(stopStateFromCause(cause), cause);
} catch (Exception e) {
log.error("Exception occurred while trying to stop {}.", getName());
if (cause == Cause.FAILED) {
//service has already failed, not just failed to stop
setState(stopStateFromCause(Cause.FAILED), Cause.FAILED);
} else {
setState(stopStateFromCause(Cause.FAILED_TO_STOP), Cause.FAILED_TO_STOP);
}
}
return new ServiceStatus(this, this.state, this.cause);
}
private State stopStateFromCause(Cause cause) {
if (cause == Cause.FAILED || cause == Cause.FAILED_TO_STOP) {
return FAILED;
}
return STOPPED;
}
protected abstract void doStop() throws Exception;
@Override
public synchronized String getName() {
return translate.from(getNameKey());
}
@Override
public synchronized boolean isStopped() {
return state == STOPPED;
}
@Override
public ServiceStatus fail() {
return stop(Cause.FAILED);
}
@Override
public synchronized ServiceStatus reset() {
if (state == INITIAL || state == RESETTING) {
return new ServiceStatus(this, this.state, this.cause);
}
if (state != STOPPED && state != FAILED) {
throw new ServiceStatusException("Must be in a STOPPED state before reset()");
}
log.info("Resetting service: {}", getName());
setState(State.RESETTING, Cause.RESET);
try {
doReset();
setState(INITIAL, Cause.RESET);
return new ServiceStatus(this, this.state, this.cause);
} catch (Exception e) {
log.error("Exception while trying to reset {}", getName(), e);
setState(State.FAILED, Cause.FAILED_TO_RESET);
return new ServiceStatus(this, this.state, this.cause);
}
}
/**
* Often not needed to do anything - but override if it does
*/
@SuppressFBWarnings("ACEM_ABSTRACT_CLASS_EMPTY_METHODS")
protected void doReset() {
}
protected synchronized ServiceStatus start(Cause cause) {
if (state == RUNNING || state == STARTING) {
log.debug("{} already started, no action taken", getName());
return new ServiceStatus(this, this.state, this.cause);
}
if (state == STOPPING) {
throw new ServiceStatusException("Cannot start() when state is " + state.name());
}
if (state == FAILED) {
throw new ServiceStatusException("Cannot start() when state is " + state.name() + ". Call reset() first");
}
State beginningState = getState();
log.info("Starting service: {}", getName());
setState(STARTING, cause);
if (servicesExecutor.execute(RelatedServicesExecutor.Action.START, cause)) {
try {
doStart();
setState(RUNNING, cause);
this.cause = cause;
} catch (Exception e) {
String msg = "Exception occurred while trying to start " + getName();
log.error(msg, e);
setState(FAILED, Cause.FAILED_TO_START);
}
} else {
//revert to beginning state, as we could not complete
setState(beginningState, Cause.DEPENDENCY_FAILED);
}
return new ServiceStatus(this, this.state, this.cause);
}
@Override
public ServiceStatus start() {
return start(Cause.STARTED);
}
protected abstract void doStart() throws Exception;
@Override
public synchronized State getState() {
return state;
}
// @SuppressFBWarnings("IS2_INCONSISTENT_SYNC") // disagree with FB on this. This method is private, and is only access via synchronized methods
private synchronized void setState(State state, Cause cause) {
if (state != this.state) {
State previousState = this.state;
this.state = state;
this.cause = cause;
log.debug("{} has changed status from {} to {}", getName(), previousState, getState());
publishStatusChange(previousState, cause);
}
}
@Override
public ServiceStatus dependencyFail() {
return stop(Cause.DEPENDENCY_FAILED);
}
@Override
public ServiceStatus dependencyStop() {
return stop(Cause.DEPENDENCY_STOPPED);
}
@Override
public synchronized I18NKey getDescriptionKey() {
return descriptionKey;
}
@Override
public synchronized void setDescriptionKey(I18NKey descriptionKey) {
this.descriptionKey = descriptionKey;
}
@Override
public synchronized String getDescription() {
if (descriptionKey == null) {
return "";
}
return translate.from(descriptionKey);
}
public synchronized int getInstanceNumber() {
return instanceNumber;
}
public synchronized void setInstanceNumber(int instanceNumber) {
this.instanceNumber = instanceNumber;
}
protected void publishStatusChange(State previousState, Cause cause) {
log.debug("publishing status change in {}. Status is now {}", this.getName(), this.getState());
eventBus.publish(new ServiceBusMessage(this, previousState, getState(), cause));
}
}