/*
* Copyright (c) 2012-2014, Parallel Universe Software Co. All rights reserved.
*
* This program and the accompanying materials are dual-licensed under
* either the terms of the Eclipse Public License v1.0 as published by
* the Eclipse Foundation
*
* or (per the licensee's choosing)
*
* under the terms of the GNU Lesser General Public License version 3.0
* as published by the Free Software Foundation.
*/
package co.paralleluniverse.common.spring;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jmx.export.annotation.ManagedAttribute;
/**
* A component that may have dependencies, and can signal its readiness to its dependents.
*/
public abstract class Service extends Component {
private static final Logger LOG = LoggerFactory.getLogger(Service.class);
private static final Object SERVICE_AVILABILITY_LOCK = new Object();
private volatile boolean available; // is this service, and all dependencies, available
private boolean dependenciesAvailable;
private boolean ready;
private boolean availabilityChanged;
private final Set<Service> dependsOn = new CopyOnWriteArraySet<Service>(); // services I depend on
private final Set<Service> dependedBy = new CopyOnWriteArraySet<Service>(); // services depending on me
/**
* Constructs a service with a given name.
* @param name The service's name.
*/
protected Service(String name) {
super(name);
this.ready = false;
}
void addDependedBy(Service service) {
assertDuringInitialization();
dependedBy.add(service);
}
void addDependsOn(Service service) {
assertDuringInitialization();
dependsOn.add(service);
}
/**
* Adds a service this service depends on.
* @param service The service this service depends on.
*/
public void addDependency(Service service) {
assertDuringInitialization();
LOG.info("Adding a dependency on {} to {}", service.getName(), getName());
addDependsOn(service);
service.addDependedBy(this);
}
/**
* Removes a dependency. This method <i>must</i> be called in the {@link #init() init()} method.
* @param service
*/
protected void removeDependency(Service service) {
assertDuringInitialization();
LOG.info("Service {} is not dependent on {}", this, service);
if (!dependsOn.remove(service))
LOG.warn("Service {} asked to remove dependency on {}, but wasn't dependendent on it", getName(), service.getName());
}
@ManagedAttribute(currencyTimeLimit = 0, description = "Services that depend on this service")
public List<String> getDependedBy() {
final List<String> ds = new ArrayList<String>(dependedBy.size());
for (Service s : dependedBy)
ds.add(s.getName());
return ds;
}
@ManagedAttribute(currencyTimeLimit = 0, description = "Services this service depends on")
public List<String> getDependsOn() {
List<String> ds = new ArrayList<String>(dependsOn.size());
for (Service s : dependsOn)
ds.add(s.getName());
return ds;
}
@Override
protected void init() throws Exception {
super.init();
LOG.info("Service {} dependencies: {}", getName(), getDependsOn());
}
@Override
protected void postInit() throws Exception {
super.postInit();
checkDependenciesAvailability();
checkAvailability();
runAvailableMethod();
}
private boolean checkDependenciesAvailability() {
synchronized (SERVICE_AVILABILITY_LOCK) {
for (Service dep : dependsOn) {
if (!dep.isAvailable()) {
dependenciesAvailable = false;
return false;
}
}
dependenciesAvailable = true;
return true;
}
}
private boolean checkAvailability() {
synchronized (SERVICE_AVILABILITY_LOCK) {
boolean _available = ready && dependenciesAvailable;
if (_available != available) {
availabilityChanged = true;
SERVICE_AVILABILITY_LOCK.notifyAll();
this.available = _available;
SERVICE_AVILABILITY_LOCK.notifyAll();
LOG.info("SERVICE {} IS NOW {}", this, available ? "AVILABLE" : "NOT AVAILABLE");
for (Service dependent : dependedBy)
dependent.dependencyChanged(this);
}
}
return available;
}
private void runAvailableMethod() {
// must run outside lock (or deadlock may occur, as available() is outside our control
if (availabilityChanged) {
available(this.available);
availabilityChanged = false;
for (Service dependent : dependedBy)
dependent.runAvailableMethod();
}
}
private void dependencyChanged(Service service) {
checkDependenciesAvailability();
checkAvailability();
}
/**
* Called when this service becomes available (it is ready and all its dependencies are available) or unavailable.
* @param value {@code true} if this service is now available; {@code false} otherwise.
*/
protected void available(boolean value) {
}
/**
* Sets the readiness of this service. If this service is ready and all its dependencies are available, than this service becomes available.
* @param ready
*/
protected void setReady(boolean ready) {
synchronized (SERVICE_AVILABILITY_LOCK) {
LOG.info("Service {} is now {}", getName(), ready ? "READY" : "NOT READY");
this.ready = ready;
checkAvailability();
}
runAvailableMethod(); // call outside lock
}
/**
* Availability changes between calls to this method are the responsibility of the user code, which must be able to handle a short interruption (e.g. comm services buffer their messages etc.).
*/
public void awaitAvailable() throws InterruptedException {
synchronized (SERVICE_AVILABILITY_LOCK) {
while (!available) {
LOG.info("Waiting for service {} to become available...", getName());
LOG.debug(getDependencyGraph());
SERVICE_AVILABILITY_LOCK.wait();
}
}
}
@ManagedAttribute(currencyTimeLimit = 0)
public boolean isReady() {
return ready;
}
@ManagedAttribute(currencyTimeLimit = 0)
public boolean isAvailable() {
return available;
}
@ManagedAttribute
public String getDependencyGraph() {
return getDependencyGraph(new StringBuilder(), 0).toString();
}
private StringBuilder getDependencyGraph(StringBuilder sb, int indent) {
sb.append('\n');
for(int i=0; i<indent*4; i++)
sb.append(' ');
sb.append(getName()).append(": ").append(isReady() ? "READY" : "NOT READY");
for(Service s : dependsOn)
s.getDependencyGraph(sb, indent + 1);
return sb;
}
}