package org.dcache.services.info.base;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* A StateComposite is an aggregation of zero or more StateComponents. StateComposites
* form the branch nodes within the dCache state tree.
* <p>
* A Mortal StateComposite has a minimum lifetime when created. The expiry date will be
* adjusted to match any added Mortal children: the branch will always persist whilst it
* contains any children.
* <p>
* An Ephemeral StateComposite has no lifetime: it will persist without having fixed
* lifetime. However an Ephemeral StateComposite will not prevent an ancestor StateComposite
* that is Mortal from expiring. In general, a StateComposite that contains <i>only</i>
* Ephemeral children should also be Ephemeral; all other StateComposites should be Mortal.
* <p>
* A StateComposite also maintains a record of the earliest any of its children (or children of
* children) will expire. This is an optimisation, allowing a quick determination when
* a tree should next be purged and, with any subtree, whether it is necessary to purge that
* subtree.
*
* @author Paul Millar <paul.millar@desy.de>
*/
public class StateComposite implements StateComponent
{
private static final Logger LOGGER = LoggerFactory.getLogger(StateComposite.class);
/** Minimum lifetime for on-the-fly created StateComposites, in seconds */
static final long DEFAULT_LIFETIME = 10;
private final Map<String, StateComponent> _children = new HashMap<>();
private StatePersistentMetadata _metadataRef;
private Date _earliestChildExpiry;
private Date _whenIShouldExpire;
private boolean _isEphemeral;
/**
* The constructor for public use: a StateComposite with a finite lifetime.
*
* @param lifetime the minimum duration, in seconds, that this StateComposite
* should persist.
*/
public StateComposite(long lifetime)
{
if (lifetime < 0) {
lifetime = 0;
}
becomeMortal(lifetime);
_metadataRef = null; // Set when added to state tree
}
/**
* Create an Ephemeral StateComposite. These should <i>only</i> be used
* when they are to contain only Ephemeral children. Normally StateComposites
* should be created Mortal.
*/
public StateComposite()
{
this(false);
}
/**
* Create a new Ephemeral or Immortal StateComposite. Normally
* StateComposites should be mortal. (Mortal StateComposites will
* automatically extend their lives so they don't expire before their
* children.)
* @param isImmortal true for an immortal StateComposite, false for
* an ephemeral one.
*/
public StateComposite(boolean isImmortal)
{
if (isImmortal) {
becomeImmortal();
} else {
becomeEphemeral();
}
_metadataRef = null;
}
/**
* Our private usage below: build a new Mortal StateComposite with a
* link to persistentMetadata.
*
* @param ref the corresponding StatePersistentMetadata object.
* @param lifetime the minimum lifetime of this object, in seconds.
*/
private StateComposite(StatePersistentMetadata persistentMetadata, long lifetime)
{
becomeMortal(lifetime);
_metadataRef = persistentMetadata;
}
/**
* Build an Immortal StateComposite with specific metadata link.
* This should only be used by the State singleton.
* @param persistentMetadata the top-level metadata.
*/
protected StateComposite(StatePersistentMetadata persistentMetadata)
{
becomeImmortal();
_metadataRef = persistentMetadata;
}
/**
* Possibly update our belief of the earliest time that a Mortal child StateComponent
* will expire. It is safe to call this method with all child Dates: it will
* update the _earliestChildExpiry Date correctly.
* @param newDate the expiry Date of a Mortal child StateComponent
*/
private void updateEarliestChildExpiryDate(Date newDate)
{
if (newDate == null) {
return;
}
if (_earliestChildExpiry == null || newDate.before(_earliestChildExpiry)) {
_earliestChildExpiry = newDate;
}
}
/**
* @return the time when the earliest child will expire, or null if we have
* no Mortal children.
*/
@Override
public Date getEarliestChildExpiryDate()
{
return _earliestChildExpiry != null ? new Date(_earliestChildExpiry.getTime()) : null;
}
/**
* Update our whenIShouldExpire date. If the new date is before the existing
* one it is ignored.
* @param newDate the new whenIShouldExpire date
*/
private void updateWhenIShouldExpireDate(Date newDate)
{
if (newDate == null) {
return;
}
if (_whenIShouldExpire == null || newDate.after(_whenIShouldExpire)) {
_whenIShouldExpire = newDate;
}
}
/**
* Return a cryptic string describing this StateComposite.
*/
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append("StateComposite <");
sb.append(isMortal() ? "+" : isEphemeral() ? "*" : "#" );
sb.append("> {");
sb.append(_children.size());
sb.append("}");
return sb.toString();
}
/**
* When we should expire.
*/
@Override
public Date getExpiryDate()
{
return _whenIShouldExpire != null ? new Date(_whenIShouldExpire.getTime()) : null;
}
/**
* This function checks whether our parent should expunge us.
*/
@Override
public boolean hasExpired()
{
Date now = new Date();
return _whenIShouldExpire != null ? !now.before(_whenIShouldExpire) : false;
}
/**
* Make sure we never expire.
*/
private void becomeImmortal()
{
_isEphemeral = false;
_whenIShouldExpire = null;
}
/**
* Switch behaviour to be Ephemeral. That is, don't expire automatically but
* don't prevent Mortal parent(s) from expiring.
*/
private void becomeEphemeral()
{
_isEphemeral = true;
_whenIShouldExpire = null;
}
/**
* Initialise our expiry time to some point in the future.
* @param lifetime the time, in seconds.
*/
private void becomeMortal(long lifetime)
{
_whenIShouldExpire = new Date(System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(lifetime));
}
/**
* Apply the visitor pattern over our children.
* <p>
* Interesting aspects:
* <ul>
* <li> Visiting over all child only happens once the <tt>start</tt> path has been
* exhausted
* <li> There are five StateVisitor call-backs from this StateComponent
* </ul>
*
* The standard call-backs are:
* <ul>
* <li>visitCompositePreDescend() called before visiting children.
* <li>visitCompositePreLastDescend() called before visiting the last child.
* <li>visitCompositePostDescend() called after visiting children.
* </ul>
*
* The <tt>start</tt> path allows the client to specify a point within the State tree
* to start visiting. Iterations down to that level call a different set of Visitor
* call-backs: visitCompositePreSkipDescend() and visitCompositePostSkipDescend().
* These are equivalent to the non-<tt>Skip</tt> versions and allow the StateVisitor
* to represent the skipping down to the starting point, or not.
*
* @param path the path to the current position in the State.
* @param visitor the object that implements the StateVisitor class.
*/
@Override
public void acceptVisitor(StatePath path, StateVisitor visitor)
{
LOGGER.trace("acceptVisitor({})", path);
Map<String,String> branchMetadata = getMetadataInfo();
visitor.visitCompositePreDescend(path, branchMetadata);
for (Map.Entry<String, StateComponent> mapEntry : _children.entrySet()) {
String childName = mapEntry.getKey();
StateComponent child = mapEntry.getValue();
StatePath childPath = buildChildPath(path, childName);
if (visitor.isVisitable(childPath)) {
child.acceptVisitor(childPath, visitor);
}
}
visitor.visitCompositePostDescend(path, branchMetadata);
}
/**
* Simulate the effects of the StateTransition, so allowing the StateVisitor to visit the dCache
* State after the transition has taken effect.
*/
@Override
public void acceptVisitor(StateTransition transition, StatePath ourPath, StateVisitor visitor)
{
checkNotNull(transition);
LOGGER.trace("acceptVisitor; transition={}, path={})", transition, ourPath);
Map<String,String> branchMetadata = getMetadataInfo();
visitor.visitCompositePreDescend(ourPath, branchMetadata);
StateChangeSet changeSet = transition.getStateChangeSet(ourPath);
Map<String,StateComponent> futureChildren = getFutureChildren(changeSet);
for (Map.Entry<String, StateComponent> mapEntry : futureChildren.entrySet()) {
String childName = mapEntry.getKey();
StateComponent child = mapEntry.getValue();
StatePath childPath = buildChildPath(ourPath, childName);
if (visitor.isVisitable(childPath)) {
child.acceptVisitor(transition, childPath, visitor);
}
}
visitor.visitCompositePostDescend(ourPath, branchMetadata);
}
/**
* Return what this._children will look like after a StateChangeSet has been applied.
*/
private Map<String,StateComponent> getFutureChildren(StateChangeSet changeSet)
{
if (changeSet == null) {
return _children;
}
Map<String,StateComponent> futureChildren = new HashMap<>(_children);
for (String childName : changeSet.getNewChildren()) {
StateComponent childValue = changeSet.getNewChildValue(childName);
futureChildren.put(childName, childValue);
}
for (String childName : changeSet.getUpdatedChildren()) {
StateComponent childValue = changeSet.getUpdatedChildValue(childName);
// When updating a branch (i.e., not a new branch) updates to child
// StateComposite objects are children of the existing branch, not
// the future one.
if (childValue instanceof StateComposite) {
continue;
}
futureChildren.put(childName, childValue);
}
for (String childName : changeSet.getRemovedChildren()) {
futureChildren.remove(childName);
}
return futureChildren;
}
/**
* Apply a transition to our current state. Children are added, updated or removed based on
* the supplied transition.
* @param ourPath the path to this within dCache tree, or null for top-most StateComposite
* @param transition the StateTransition to apply
*/
@Override
public void applyTransition(StatePath ourPath, StateTransition transition)
{
StateChangeSet changeSet = transition.getStateChangeSet(ourPath);
if (changeSet == null) {
LOGGER.warn("cannot find StateChangeSet for path {}", ourPath);
return;
}
Date newExpDate = changeSet.getWhenIShouldExpireDate();
updateWhenIShouldExpireDate(newExpDate);
if (newExpDate == null) {
LOGGER.trace("getWhenIShouldExpireDate() returned null: no Mortal children?");
}
if (changeSet.haveImmortalChild()) {
becomeImmortal(); // this is currently irreversible
}
// First, remove those children we should remove.
for (String childName : changeSet.getRemovedChildren()) {
LOGGER.trace("removing child {}", childName);
_children.remove(childName);
}
// Then update our existing children.
for (String childName : changeSet.getUpdatedChildren()) {
StateComponent updatedChildValue = changeSet.getUpdatedChildValue(childName);
if (updatedChildValue == null) {
LOGGER.error("Attempting to update {} in {}, but value is null; wilfully ignoring this.", childName, ourPath);
continue;
}
LOGGER.trace("updating child {}, updated value {}", childName, updatedChildValue);
addComponent(childName, updatedChildValue);
}
// Finally, add all new children.
for (String childName : changeSet.getNewChildren()) {
StateComponent newChildValue = changeSet.getNewChildValue(childName);
LOGGER.trace("adding new child {}, new value {}", childName, newChildValue);
addComponent(childName, newChildValue);
}
// Now, which children should we iterate into?
for (String childName : changeSet.getItrChildren()) {
StateComponent child = _children.get(childName);
if (child == null) {
if (!changeSet.getRemovedChildren().contains(childName)) {
LOGGER.error("Whilst in {}, avoided attempting to applyTransition()" +
" on missing child {}", ourPath, childName);
}
continue;
}
child.applyTransition(buildChildPath(ourPath, childName), transition);
}
recalcEarliestChildExpiry();
}
/**
* Recalculate _earliestChildExpiryDate() by asking our children for their earliest expiring
* child.
* TODO: this isn't always necessary, but it's hard to know when. Also, it isn't clear that the
* cost of figuring out when it is necessary is less than the CPU time saved by always recalculating.
*/
private void recalcEarliestChildExpiry()
{
_earliestChildExpiry = null; // A forceful reset
for (StateComponent child : _children.values()) {
Date earliestExpires = child.getEarliestChildExpiryDate();
if (earliestExpires != null) {
updateEarliestChildExpiryDate(earliestExpires);
}
if (child.isMortal()) {
updateEarliestChildExpiryDate(child.getExpiryDate());
}
}
}
/**
* Look up persistent metadata reference for child and return it. If none is
* available, null is returned.
* @param childName the name of the child.
* @return a StatePersistentMetadata entry, or null if none is appropriate.
*/
private StatePersistentMetadata getChildMetadata(String childName)
{
return _metadataRef == null ? null : _metadataRef.getChild(childName);
}
/**
* @return our metadata info, if there is any, otherwise null.
*/
private Map<String,String> getMetadataInfo()
{
return _metadataRef == null ? null : _metadataRef.getMetadata();
}
/**
* Add a new component to our list of children.
* <p>
* @param childName the name under which this item should be recorded
* @param newChild the StateComponent to be stored.
*/
private void addComponent(String childName, StateComponent newChild)
{
StateComponent existingChild = _children.get(childName);
/**
* If we're added a StateComposite, we must be a little more careful!
*/
if (newChild instanceof StateComposite) {
StateComposite newComposite = (StateComposite) newChild;
/**
* Copy across all existing children that don't clash. Those with
* the same name are updates for those children, so we want to go with
* the values under the newComposite.
*/
if (existingChild instanceof StateComposite) {
StateComposite existingComposite = (StateComposite) existingChild;
// Copy across the existingComposite's children over to the newComposite
for (Map.Entry<String,StateComponent> entry : existingComposite._children.entrySet()) {
if (!newComposite._children.containsKey(entry.getKey())) {
newComposite._children
.put(entry.getKey(), entry.getValue());
}
}
// ... and details of the dates...
newComposite.updateEarliestChildExpiryDate(existingComposite.getEarliestChildExpiryDate());
newComposite.updateWhenIShouldExpireDate(existingComposite.getExpiryDate());
}
}
_children.put(childName, newChild);
LOGGER.trace("Child {} now {}", childName, newChild);
}
/**
* Update a StateTransition object so a new StateComponent will be added to dCache's state. The
* changes are recorded in StateTransition so they can be applied later.
* @param ourPath the StatePath to this StateComposite.
* @param newComponentPath the StatePath to this StateComponent, relative to this StateComposition
* @param newComponent the StateComponent to add.
* @param transition the StateTransition in which we will record these changes
*/
@Override
public void buildTransition(StatePath ourPath, StatePath newComponentPath,
StateComponent newComponent, StateTransition transition)
throws MetricStatePathException
{
String childName = newComponentPath.getFirstElement();
StateChangeSet changeSet = transition.getOrCreateChangeSet(ourPath);
/* If we are mortal and the new child is too, check we don't expire too soon */
if (this.isMortal() && newComponent.isMortal()) {
Date newComponentExpiryDate = newComponent.getExpiryDate();
changeSet.recordNewWhenIShouldExpireDate(newComponentExpiryDate);
}
// All parents of an Immortal Child should know not to expire.
if (newComponent.isImmortal()) {
changeSet.recordChildIsImmortal();
}
// If we currently scheduled to remove the named child, make sure we don't!
changeSet.ensureChildNotRemoved(childName);
/**
* If newComponent is one of our children, process it directly.
*/
if (newComponentPath.isSimplePath()) {
if (_children.containsKey(childName)) {
changeSet.recordUpdatedChild(childName, newComponent);
} else {
changeSet.recordNewChild(childName, newComponent);
}
if (newComponent instanceof StateComposite) {
StateComposite newComposite = (StateComposite) newComponent;
newComposite._metadataRef = getChildMetadata(childName);
}
return;
}
/**
* Otherwise, iterate down; if possible, through the existing tree, otherwise through
* a new StateComposite.
*/
StateComponent child = _children.get(childName);
if (child == null) {
// Perhaps we're already adding a StateComposite with this transition?
child = changeSet.getNewChildValue(childName);
if (child == null) {
// No? OK, create a new NewComposite and record it.
child = new StateComposite(getChildMetadata(childName), DEFAULT_LIFETIME);
changeSet.recordNewChild(childName, child);
}
}
/**
* Even if we didn't change anything, record that we're about to iterate down and do so.
*/
changeSet.recordChildItr(childName);
child.buildTransition(buildChildPath(ourPath, childName), newComponentPath.childPath(), newComponent, transition);
}
/**
* Check whether the specified StatePathPredicate has been triggered by the given StateTransition.
* <p>
* If none of our children match the top-level element of the predicate, then the answer is definitely
* not triggered.
* <p>
* If a child matches but there are more elements to consider, iterate down: ask the child whether the
* predicate has been triggered with one less element in the predicate.
* <p>
* If the predicate is simple (that is, contains only a single element) then the predicate is
* triggered depending on the activity of our children under the StateTransition.
* @param ourPath Our path
* @param predicate the predicate we are to check.
* @param transition the StateTransition to consider.
* @return true if the transition has triggered this predicate, false otherwise
*/
@Override
public boolean predicateHasBeenTriggered(StatePath ourPath,
StatePathPredicate predicate, StateTransition transition)
{
LOGGER.trace("predicateHasBeenTriggered path={}, predicate={}", ourPath, predicate);
StateChangeSet changeSet = transition.getStateChangeSet(ourPath);
if (changeSet == null) {
return false;
}
// Scan through the list of new children first.
Collection<String> newChildren = changeSet.getNewChildren();
if (newChildren != null) {
for (String newChildName : newChildren) {
if (!predicate.topElementMatches(newChildName)) {
// ignore unrelated children.
continue;
}
if (predicate.isSimplePath()) {
return true; // a new child always triggers a predicate
}
/**
* Ask this child whether the predicate is triggered. If the child says "yes", we
* concur.
*/
StateComponent child = changeSet.getNewChildValue(newChildName);
if (child.predicateHasBeenTriggered(buildChildPath(ourPath, newChildName), predicate.childPath(), transition)) {
return true;
}
// Carry on searching...
}
}
// Scan through our existing children
for (String childName : _children.keySet()) {
StateComponent child = _children.get(childName);
// If we've done nothing, it can't have changed.
if (!changeSet.hasChildChanged(childName)) {
continue;
}
// ignore unrelated children
if (!predicate.topElementMatches(childName)) {
continue;
}
/**
* If predicate's last element is one of our children...
*/
if (predicate.isSimplePath()) {
// Check various options:
// Removed children always triggers a predicate.
if (changeSet.childIsRemoved(childName)) {
return true;
}
// Has child changed "significantly" ?
StateComponent updatedChildValue = changeSet.getUpdatedChildValue(childName);
if (updatedChildValue != null && !child.equals(updatedChildValue)) {
return true;
}
} else {
// ... otherwise, try iterating down.
if (child.predicateHasBeenTriggered(buildChildPath(ourPath, childName), predicate.childPath(), transition)) {
return true;
}
}
}
return false;
}
@Override
public boolean isEphemeral()
{
return _whenIShouldExpire == null && _isEphemeral;
}
@Override
public boolean isImmortal()
{
return _whenIShouldExpire == null && !_isEphemeral;
}
@Override
public boolean isMortal()
{
return _whenIShouldExpire != null;
}
/**
* Build a child's StatePath, taking into account that a path may be null
* (this one-liner is repeated fairly often)
* @param ourPath our current path, or null if we are the root StateComposite
* @param childName the name of the child.
* @return
*/
private StatePath buildChildPath(StatePath ourPath, String childName)
{
return ourPath != null ? ourPath.newChild(childName) : new StatePath(childName);
}
/**
* Ostensibly, we iterate over all children to find Mortal children that should be
* removed. In practise, cached knowledge of Mortal child expiry Dates means this
* iterates over only those StateComponents that contain children that have actually
* expired.
*
* @param ourPath
* @param transition
* @param forced
*/
@Override
public void buildRemovalTransition(StatePath ourPath,
StateTransition transition, boolean forced)
{
LOGGER.trace("entering buildRemovalTransition: path={}", ourPath);
Date now = new Date();
// Check each child in turn:
for (Map.Entry<String, StateComponent>entry : _children.entrySet()) {
StateComponent childValue = entry.getValue();
String childName = entry.getKey();
boolean shouldRemoveThisChild = forced;
boolean shouldItr = forced;
// If *this* child has expired, we should mark it as To Be Removed.
if (childValue.hasExpired()) {
LOGGER.trace("registering {} (in path {}) for removal.", childName, ourPath);
shouldRemoveThisChild = shouldItr = true;
}
// If *this* child has some child that has expired, iterate down.
Date childExp = childValue.getEarliestChildExpiryDate();
if (childExp != null && !now.before(childExp)) {
shouldItr = true;
}
if (shouldItr || shouldRemoveThisChild) {
StateChangeSet changeSet = transition.getOrCreateChangeSet(ourPath);
if (shouldRemoveThisChild) {
changeSet.recordRemovedChild(childName);
}
if (shouldItr) {
changeSet.recordChildItr(childName);
childValue.buildRemovalTransition(buildChildPath(ourPath, childName), transition, shouldRemoveThisChild);
}
}
}
}
/**
* We are to update a StateTransition so all StateComponents that have a certain path as
* their parent are to be removed.
*/
@Override
public void buildPurgeTransition(StateTransition transition, StatePath ourPath,
StatePath remainingPath)
{
LOGGER.trace("buildPurgeTransition: path={}, remaining={}", ourPath, remainingPath);
StateChangeSet scs = transition.getOrCreateChangeSet(ourPath);
if (remainingPath == null) {
// If remainingPath is null, we should remove everything.
buildRemovalTransition(ourPath, transition, true);
} else {
String childName = remainingPath.getFirstElement();
if (_children.containsKey(childName)) {
StateComponent child = _children.get(childName);
StatePath childPath = buildChildPath(ourPath, childName);
if (child instanceof StateComposite) {
scs.recordChildItr(childName);
}
if (remainingPath.isSimplePath()) {
scs.recordRemovedChild(childName);
}
child.buildPurgeTransition(transition, childPath, remainingPath.childPath());
}
// Otherwise, we still have to iterate down...
// if we don't have the named child, do nothing
}
}
/**
* Return a hash-code that honours the equals() / hashCode() contract.
*/
@Override
public int hashCode()
{
return _children.hashCode();
}
/**
* Override the public equals method. All StateComposites are considered equal if
* they have the same children are the same type and have the same expiry date
* (if mortal).
*
* This is significant for when considering whether a StatePredicate has been triggered.
*/
@Override
public boolean equals(Object other)
{
if (other == this) {
return true;
}
if (!(other instanceof StateComposite)) {
return false;
}
StateComposite otherSc = (StateComposite) other;
return otherSc._children.equals(_children);
}
}