package org.dcache.services.info.secondaryInfoProviders; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Multimap; import com.google.common.collect.MultimapBuilder; import com.google.common.collect.Multimaps; import com.google.common.collect.Ordering; import com.google.common.collect.SetMultimap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Collection; import java.util.Collections; import java.util.EnumMap; import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import org.dcache.services.info.base.StateExhibitor; import org.dcache.services.info.base.StatePath; import org.dcache.services.info.base.StateUpdate; import org.dcache.services.info.stateInfo.LinkInfo; import org.dcache.services.info.stateInfo.LinkInfoVisitor; import org.dcache.services.info.stateInfo.PoolSpaceVisitor; import org.dcache.services.info.stateInfo.SpaceInfo; /** * The NormalisaedAccessSpace (NAS) maintainer updates the <code>nas</code> * branch of the dCache state based on changes to pool space usage or * pool-link membership. * * A NAS is a set of pools. They are somewhat similar to a poolgroup except * that dCache PSU has no knowledge of them. Each NAS has the following * properties: * <ol> * <li>all pools are a member of precisely one NAS, * <li>all NAS have at least one member pool, * <li>all pools within a NAS have the same "access", where access is the * set of links that point to the pool (either directly or indirectly through * a pool group), * <li>two pools in two different NAS do not have the same access, meaning * that one of them is in a link the other is not in. * </ol> * * NAS association with a link is not exclusive. Pools in a NAS that is * accessible from one link may be accessible from a different link. Those * pools accessible in a NAS accessible through a link may share that link * with other pools. */ public class NormalisedAccessSpaceMaintainer extends AbstractStateWatcher { private static final Logger LOGGER = LoggerFactory.getLogger(NormalisedAccessSpaceMaintainer.class); /** * How we want to represent the different LinkInfo.UNIT_TYPE values as * path elements in the resulting metrics */ @SuppressWarnings("serial") private static final Map<LinkInfo.UNIT_TYPE, String> UNIT_TYPE_STORAGE_NAME = ImmutableMap.of(LinkInfo.UNIT_TYPE.DCACHE, "dcache", LinkInfo.UNIT_TYPE.STORE, "store"); /** * How we want to represent the different LinkInfo.OPERATION values as * path elements in resulting metrics */ @SuppressWarnings("serial") private static final Map<LinkInfo.OPERATION, String> OPERATION_STORAGE_NAME = ImmutableMap.of(LinkInfo.OPERATION.READ, "read", LinkInfo.OPERATION.WRITE, "write", LinkInfo.OPERATION.CACHE, "stage"); private static final String PREDICATE_PATHS[] = { "links.*.pools.*", "links.*.units.store.*", "links.*.units.dcache.*", "pools.*.space.*" }; private static final StatePath NAS_PATH = new StatePath("nas"); /** * A class describing the "paint" information for a pool. Each pool has a * PaintInfo object that describes the different access methods for this * pool. Pools with the same PaintInfo are members of the same NAS. */ protected static class PaintInfo { public static final String NAS_NAME_INACCESSIBLE = "inaccessible"; public static final String NAS_NAME_TOO_LONG_PREFIX = "complex-"; public static final int NAS_NAME_MAX_LENGTH = 100; /** * The Set of LinkInfo.OPERATIONS that we paint a pool on. */ private static final Set<LinkInfo.OPERATION> CONSIDERED_OPERATIONS = EnumSet.of(LinkInfo.OPERATION.READ, LinkInfo.OPERATION.WRITE, LinkInfo.OPERATION.CACHE); /** * The Set of LinkInfo.UNIT_TYPES that we paint a pool on. */ private static final Set<LinkInfo.UNIT_TYPE> CONSIDERED_UNIT_TYPES = EnumSet.of(LinkInfo.UNIT_TYPE.DCACHE, LinkInfo.UNIT_TYPE.STORE); private final String _poolId; private final Set<String> _links = new HashSet<>(); /** The cached copy of the NAS' */ private String _nasName; /** Store all units by unit-type and operation */ private final Map<LinkInfo.UNIT_TYPE, Multimap<LinkInfo.OPERATION, String>> _storedUnits; public PaintInfo(String poolId) { _poolId = poolId; Map<LinkInfo.UNIT_TYPE, Multimap<LinkInfo.OPERATION, String>> storedUnits = new EnumMap<>(LinkInfo.UNIT_TYPE.class); for (LinkInfo.UNIT_TYPE unitType : CONSIDERED_UNIT_TYPES) { SetMultimap<LinkInfo.OPERATION, String> map = MultimapBuilder.enumKeys(LinkInfo.OPERATION.class).hashSetValues().build(); storedUnits.put(unitType, map); } _storedUnits = Collections.unmodifiableMap(storedUnits); } /** * Add paint information for a link. Pools accessible through * the same set of links are part of the same NAS. * * @param link The LinkInfo object that describes the link */ synchronized void addLink(LinkInfo link) { invalidateNasNameCache(); _links.add(link.getId()); for (LinkInfo.OPERATION operation : CONSIDERED_OPERATIONS) { if (link.isAccessableFor(operation)) { for (LinkInfo.UNIT_TYPE unitType : CONSIDERED_UNIT_TYPES) { _storedUnits.get(unitType).putAll(operation, link.getUnits(unitType)); } } } } /** * Calculating the NAS name is a relatively heavy-weight * operation, so we cache the result. However, sometimes we * need to invalidate this cache so calls to getNasName() will * generate the name afresh. */ private void invalidateNasNameCache() { _nasName = null; } /** * Check whether the nasName cache is currently valid. */ private boolean isNasNameCacheValid() { return _nasName != null; } /** * Rebuild the nasName cached value. */ private void buildNasNameCache() { String name = Joiner.on(",").join(Ordering.natural().sortedCopy(_links)); if (name.length() > NAS_NAME_MAX_LENGTH) { _nasName = NAS_NAME_TOO_LONG_PREFIX + Integer.toHexString(name.hashCode()); } else if (!name.isEmpty()) { _nasName = name; } else { _nasName = NAS_NAME_INACCESSIBLE; } } /** * Return the name of the NAS this painted pool should be * within. * * @return a unique name for the NAS this PaintInfo is * representative of. */ synchronized String getNasName() { if (!isNasNameCacheValid()) { buildNasNameCache(); } return _nasName; } protected Set<String> getLinks() { return _links; } String getPoolId() { return _poolId; } /** * Obtain a Multimap between operations (such as read, write, ...) and the * units that select those operations for a given unitType * (such as dcache, store, ..) * * @param unitType a considered unit type. * @return the corresponding mapping or null if unit type isn't * considered. */ public Multimap<LinkInfo.OPERATION, String> getUnits(LinkInfo.UNIT_TYPE unitType) { if (!_storedUnits.containsKey(unitType)) { return null; } return Multimaps.unmodifiableMultimap(_storedUnits.get(unitType)); } @Override public int hashCode() { return _links.hashCode(); } @Override public boolean equals(Object otherObject) { if (this == otherObject) { return true; } if (!(otherObject instanceof PaintInfo)) { return false; } PaintInfo otherPI = (PaintInfo) otherObject; if (!_links.equals(otherPI._links)) { return false; } return true; } } /** * Information about a particular NAS. A NAS is something like a * poolgroup, but with the restriction that every pool is a member of * precisely one NAS. */ private static class NasInfo { private final SpaceInfo _spaceInfo = new SpaceInfo(); private final Set<String> _pools = new HashSet<>(); private PaintInfo _representativePaintInfo; /** * Add a pool to this NAS. If this is the first pool then the set of * links store unit and dCache units is set. It is anticipated that * any subsequent pools added to this NAS will have the same set of * links (so, same set of store and dCache units). This is checked. * * @param poolId * @param spaceInfo * @param pInfo */ void addPool(String poolId, SpaceInfo spaceInfo, PaintInfo pInfo) { _pools.add(poolId); _spaceInfo.add(spaceInfo); if (_representativePaintInfo == null) { _representativePaintInfo = pInfo; } else if (!_representativePaintInfo.equals(pInfo)) { throw new RuntimeException("Adding pool " + poolId + " with differeing paintInfo from first pool " + _representativePaintInfo.getPoolId()); } } /** * Discover whether any of the pools given in the Set of poolIDs has * been registered as part of this NAS. * * @param pools the Set of PoolIDs * @return true if at least one member of the provided Set is a * member of this NAS. */ boolean havePoolInSet(Set<String> pools) { return !Collections.disjoint(pools, _pools); } /** * Add a set of metrics for this NAS * * @param update */ void addMetrics(StateUpdate update, String nasName) { StatePath thisNasPath = NAS_PATH.newChild(nasName); // Add a list of pools in this NAS StatePath thisNasPoolsPath = thisNasPath.newChild("pools"); update.appendUpdateCollection(thisNasPoolsPath, _pools, true); // Add the space information _spaceInfo.addMetrics(update, thisNasPath.newChild("space"), true); // Add the list of links StatePath thisNasLinksPath = thisNasPath.newChild("links"); update.appendUpdateCollection(thisNasLinksPath, _representativePaintInfo.getLinks(), true); // Add the units information StatePath thisNasUnitsPath = thisNasPath.newChild("units"); for (LinkInfo.UNIT_TYPE type : PaintInfo.CONSIDERED_UNIT_TYPES) { Multimap<LinkInfo.OPERATION, String> unitsMap = _representativePaintInfo.getUnits(type); if (unitsMap == null) { LOGGER.error("A considered unit-type query to getUnits() gave null reply. This is unexpected."); continue; } if (!UNIT_TYPE_STORAGE_NAME.containsKey(type)) { LOGGER.error("Unmapped unit type {}", type); continue; } StatePath thisNasUnitTypePath = thisNasUnitsPath.newChild(UNIT_TYPE_STORAGE_NAME.get(type)); for (Map.Entry<LinkInfo.OPERATION, Collection<String>> entry : unitsMap.asMap().entrySet()) { LinkInfo.OPERATION operation = entry.getKey(); Collection<String> units = entry.getValue(); if (!OPERATION_STORAGE_NAME.containsKey(operation)) { LOGGER.error("Unmapped operation {}", operation); continue; } update.appendUpdateCollection( thisNasUnitTypePath.newChild(OPERATION_STORAGE_NAME.get(operation)), units, true); } } } } @Override protected String[] getPredicates() { return PREDICATE_PATHS; } @Override public void trigger(StateUpdate update, StateExhibitor currentState, StateExhibitor futureState) { Map<String, LinkInfo> currentLinks = LinkInfoVisitor.getDetails(currentState); Map<String, LinkInfo> futureLinks = LinkInfoVisitor.getDetails(futureState); Map<String, SpaceInfo> currentPools = PoolSpaceVisitor.getDetails(currentState); Map<String, SpaceInfo> futurePools = PoolSpaceVisitor.getDetails(futureState); buildUpdate(update, currentPools, futurePools, currentLinks, futureLinks); } /** * Build a mapping of NasInfo objects. * * @param links */ private Map<String, NasInfo> buildNas(Map<String, SpaceInfo> poolSpaceInfo, Map<String, LinkInfo> links) { // Build initially "white" (unpainted) set of paint info. Map<String, PaintInfo> paintedPools = new HashMap<>(); for (String poolId : poolSpaceInfo.keySet()) { paintedPools.put(poolId, new PaintInfo(poolId)); } // For each link in dCache and for each pool accessible via this link // build the paint info. for (LinkInfo linkInfo : links.values()) { for (String linkPool : linkInfo.getPools()) { PaintInfo poolPaintInfo = paintedPools.get(linkPool); /* * It is possible that a pool is accessible from a link yet * no such pool is known; for example, as the info service is * "booting up". We work-around this issue by creating a new * PaintInfo for for this pool. */ if (poolPaintInfo == null) { LOGGER.debug("Inconsistency in information: pool " + linkPool + " accessible via link " + linkInfo.getId() + " but not present as a pool"); poolPaintInfo = new PaintInfo(linkPool); paintedPools.put(linkPool, poolPaintInfo); } poolPaintInfo.addLink(linkInfo); } } // Build the set of NAS by iterating over all paint information (so, // iterating over all pools) Map<String, NasInfo> nas = new HashMap<>(); for (Map.Entry<String, PaintInfo> paintEntry : paintedPools.entrySet()) { PaintInfo poolPaintInfo = paintEntry.getValue(); String poolId = paintEntry.getKey(); String nasName = poolPaintInfo.getNasName(); NasInfo _thisNas; if (!nas.containsKey(nasName)) { _thisNas = new NasInfo(); nas.put(nasName, _thisNas); } else { _thisNas = nas.get(nasName); } _thisNas.addPool(poolId, poolSpaceInfo.get(poolId), poolPaintInfo); } return nas; } /** * Build a StateUpdate with the metrics that need to be updated. * * @param update * @param existingLinks * @param futureLinks */ private StateUpdate buildUpdate(StateUpdate update, Map<String, SpaceInfo> currentPools, Map<String, SpaceInfo> futurePools, Map<String, LinkInfo> currentLinks, Map<String, LinkInfo> futureLinks) { boolean buildAll = false; Set<String> alteredPools = null; Map<String, NasInfo> nas = buildNas(futurePools, futureLinks); /** * If the link structure has changed then we know that there may be * NAS that are no longer valid. To keep things simple, we invalidate * all stored NAS information and repopulate everything. */ if (!currentLinks.equals(futureLinks)) { update.purgeUnder(NAS_PATH); buildAll = true; } else { /** * If the structure is the same, then we only need to update NAS * that contain a pool that has changed a space metric */ alteredPools = identifyPoolsThatHaveChanged(currentPools, futurePools); } // Add those NAS that have changed, or all of them if buildAll is set for (Map.Entry<String, NasInfo> e : nas.entrySet()) { String nasName = e.getKey(); NasInfo nasInfo = e.getValue(); if (buildAll || nasInfo.havePoolInSet(alteredPools)) { nasInfo.addMetrics(update, nasName); } } return update; } /** * Build up a Set of pools that have altered; either pools that have been * added, that have been removed or have changed their details. More * succinctly, this is: * * <pre> * (currentPools \ futurePools) U (futurePools \ currentPools) * </pre> * * where the sets here are each Map's Map.EntrySet. * * @param currentPools Map between poolID and corresponding SpaceInfo for * current pools * @param futurePools Map between poolID and corresponding SpaceInfo for * future pools * @return a Set of poolIDs for pools that have, in some way, changed. */ private Set<String> identifyPoolsThatHaveChanged(Map<String, SpaceInfo> currentPools, Map<String, SpaceInfo> futurePools) { Set<String> alteredPools = new HashSet<>(); Set<Map.Entry<String, SpaceInfo>> d1 = new HashSet<>(currentPools.entrySet()); Set<Map.Entry<String, SpaceInfo>> d2 = new HashSet<>(futurePools.entrySet()); d1.removeAll(futurePools.entrySet()); d2.removeAll(currentPools.entrySet()); for (Map.Entry<String, SpaceInfo> e : d1) { alteredPools.add(e.getKey()); } for (Map.Entry<String, SpaceInfo> e : d2) { alteredPools.add(e.getKey()); } return alteredPools; } }