package org.dcache.pool.repository;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.Callable;
import java.util.regex.Pattern;
import diskCacheV111.util.AccessLatency;
import diskCacheV111.util.CacheException;
import diskCacheV111.util.FileNotInCacheException;
import diskCacheV111.util.PnfsId;
import diskCacheV111.util.RetentionPolicy;
import dmg.cells.nucleus.CellCommandListener;
import dmg.util.Formats;
import dmg.util.command.Argument;
import dmg.util.command.Command;
import dmg.util.command.DelayedCommand;
import dmg.util.command.Option;
import org.dcache.namespace.FileAttribute;
import org.dcache.util.ByteUnit;
import org.dcache.util.Glob;
import org.dcache.vehicles.FileAttributes;
import static java.util.stream.Collectors.joining;
import static org.dcache.util.ByteUnit.*;
public class RepositoryInterpreter
implements CellCommandListener
{
private static final Logger _log =
LoggerFactory.getLogger(RepositoryInterpreter.class);
private static final Map<String,ByteUnit> STRING_TO_UNIT =
ImmutableMap.of("k", KiB, "m", MiB, "g", GiB, "t", TiB);
private Repository _repository;
private final StatisticsListener _statisticsListener = new StatisticsListener();
public void setRepository(Repository repository)
{
_repository = repository;
_repository.addListener(_statisticsListener);
}
@Command(name = "rep set sticky", hint = "change sticky flags",
description = "Changes the sticky flags on one or more replicas. Sticky flags prevent sweeper " +
"from garbage collecting files. A sticky flag has an owner (a name) and an expiration " +
"date. The expiration date may be infinite, in which case the sticky flag never expires. " +
"Each replica can have zero or more sticky flags.\n\n" +
"The command may set or clear a sticky flag of a specific replica or for a set of " +
"replicas matches the given filter cafeterias.")
public class SetStickyCommand implements Callable<String>
{
@Argument(index = -2, required = false,
usage = "Only change the replica with the given ID.")
PnfsId pnfsId;
@Argument(index = -1, valueSpec = "on|off",
usage = "Whether to set or clear the sticky flag.")
String state;
@Option(name = "o", metaVar = "name", category = "Sticky properties",
usage = "The owner is a name for the flag. A replica can only have one sticky flag per owner.")
String owner = "system";
@Option(name = "l", metaVar = "millis", category = "Sticky properties",
usage = "The lifetime in milliseconds from now. Once the lifetime expires, the sticky flag is " +
"removed. If no other sticky flags are left and the replica is marked as a cache, sweeper " +
"may garbage collect it. A sticky flag with a lifetime of -1 never expires.")
Long lifetime;
@Option(name = "al", category = "Filter options", values={"online", "nearline"},
usage = "Only change replicas with the given access latency.")
AccessLatency al;
@Option(name = "rp", category = "Filter options", values={"custodial", "replica", "output"},
usage = "Only change replicas with the given retention policy.")
RetentionPolicy rp;
@Option(name = "storage", metaVar = "class", category = "Filter options",
usage = "Only change replicas with the given storage class.")
String storage;
@Option(name = "cache", metaVar = "class", category = "Filter options",
usage ="Only change replicas with the given cache class. If set to the empty string, the condition " +
"will match any replica that does not have a cache class.")
String cache;
@Option(name = "all", category = "Filter options",
usage = "Allow using the command without any filter options and without a PNFS ID. This is a safe " +
"guard against accidentally changing all replicas.")
boolean all;
@Override
public String call() throws Exception
{
long expire;
switch (state) {
case "on":
expire = (lifetime != null) ? Math.max(0, System.currentTimeMillis() + lifetime) : -1;
break;
case "off":
expire = 0;
break;
default:
throw new IllegalArgumentException("Invalid sticky state : " + state);
}
if (pnfsId != null) {
if (!matches(pnfsId)) {
throw new IllegalArgumentException("Replica does not match filter conditions.");
}
_repository.setSticky(pnfsId, owner, expire, true);
return _repository.getEntry(pnfsId).getStickyRecords().stream()
.filter(StickyRecord::isValid).map(Object::toString).collect(joining("\n"));
}
if (al == null && rp == null && storage == null && cache == null && !all) {
throw new IllegalArgumentException("Use -all to change sticky flag for all replicas.");
}
long cnt = 0;
for (PnfsId id : _repository) {
try {
if (matches(id)) {
_repository.setSticky(id, owner, expire, true);
cnt++;
}
} catch (FileNotInCacheException ignored) {
}
}
return cnt + " replicas updated.";
}
protected boolean matches(PnfsId id) throws CacheException, InterruptedException
{
CacheEntry entry = _repository.getEntry(id);
FileAttributes attributes = entry.getFileAttributes();
return (al == null || attributes.isDefined(FileAttribute.ACCESS_LATENCY) && attributes.getAccessLatency().equals(al)) &&
(rp == null || attributes.isDefined(FileAttribute.RETENTION_POLICY) && attributes.getRetentionPolicy().equals(rp)) &&
(storage == null || attributes.isDefined(FileAttribute.STORAGECLASS) && Objects.equals(attributes.getStorageClass(), storage)) &&
(cache == null || attributes.isDefined(FileAttribute.CACHECLASS) && Objects.equals(attributes.getCacheClass(), Strings.emptyToNull(cache)));
}
}
@Command(name = "rep sticky ls", hint = "list sticky flags",
description = "List sticky flags of a replica.")
public class ListStickyCommand implements Callable<String>
{
@Argument
PnfsId pnfsId;
@Override
public String call() throws CacheException, InterruptedException
{
CacheEntry entry = _repository.getEntry(pnfsId);
return entry.getStickyRecords().stream()
.filter(StickyRecord::isValid).map(Object::toString).collect(joining("\n"));
}
}
@Command(name = "rep ls", hint = "list replicas",
description = "List the replicas in this pool.\n\n" +
"Each line has the following format:\n\n" +
" PNFSID <STATE> <SIZE> <STORAGE CLASS>\n\n"+
"STATE is a sequence of fields:\n"+
" field 1 is \"C\"\n" +
" if entry is cached and \"-\" otherwise.\n"+
" field 2 is \"P\"\n" +
" if entry is precious and \"-\" otherwise.\n"+
" field 3 is \"C\"\n" +
" if entry is being transferred \"from client\" and \"-\" otherwise.\n" +
" field 4 is \"S\"\n" +
" if entry is being transferred \"from store\" and \"-\" otherwise.\n" +
" field 5 is unused.\n" +
" field 6 is unused.\n" +
" field 7 is \"R\"\n" +
" if entry is removed but still open and \"-\" otherwise.\n"+
" field 8 is \"D\"\n" +
" if entry is removed and \"-\" otherwise.\n"+
" field 9 is \"X\"\n" +
" if entry is sticky and \"-\" otherwise.\n"+
" field 10 is \"E\"\n" +
" if entry is in error state and \"-\" otherwise.\n"+
" field 11 is unused.\n"+
" field 12 is \"L(0)(n)\"\n" +
" where is the link count.")
public class ListCommand extends DelayedCommand<Serializable>
{
@Argument(usage = "Limit to these replicas.", required = false)
PnfsId[] pnfsIds;
@Option(name = "l", valueSpec = "[s|p|l|u|nc|e...]",
usage = "Limit to replicas with these flags: \n" +
" s : sticky\n"+
" p : precious\n"+
" l : locked\n"+
" u : in use\n"+
" nc : not cached\n" +
" e : error")
String format;
@Option(name = "storage", metaVar = "GLOB", usage = "Limit to replicas with matching storage class.")
Glob si;
@Option(name = "s", valueSpec = "[k|m|g|t]", values = { "k", "m", "g", "t", "" },
usage = "Output per storage class statistics instead. Optionally use KiB, MiB, " +
"GiB, or TiB.")
String stat;
@Option(name = "sum", usage = "Include totals for all storage classes when used with -s or -binary.")
boolean sum;
@Option(name = "binary", usage = "Return statistics in binary format instead.")
boolean binary;
@Override
public Serializable execute() throws CacheException, InterruptedException
{
if (pnfsIds != null) {
return listById(pnfsIds);
} else if (binary) {
return listBinary();
} else if (stat != null) {
return listStatistics(STRING_TO_UNIT.getOrDefault(stat, BYTES));
} else {
return listAll();
}
}
private Serializable listAll() throws CacheException, InterruptedException
{
String format = Strings.nullToEmpty(this.format);
boolean notcached = format.contains("nc");
boolean precious = format.indexOf('p') > -1;
boolean sticky = format.indexOf('s') > -1;
boolean used = format.indexOf('u') > -1;
boolean broken = format.indexOf('e') > -1;
boolean cached = format.replace("nc", "").indexOf('c') > -1;
Pattern siFilter = (si == null) ? null : si.toPattern();
StringBuilder sb = new StringBuilder();
for (PnfsId id: _repository) {
try {
CacheEntry entry = _repository.getEntry(id);
ReplicaState state = entry.getState();
if (siFilter != null) {
FileAttributes attributes = entry.getFileAttributes();
String siValue = attributes.isDefined(FileAttribute.STORAGECLASS)
? attributes.getStorageClass()
: "<unknown>";
if (!siFilter.matcher(siValue).matches()) {
continue;
}
}
if (format.isEmpty() ||
(notcached && state != ReplicaState.CACHED) ||
(precious && state == ReplicaState.PRECIOUS) ||
(sticky && entry.isSticky()) ||
(broken && state == ReplicaState.BROKEN) ||
(cached && state == ReplicaState.CACHED) ||
(used && entry.getLinkCount() > 0)) {
sb.append(entry).append('\n');
}
} catch (FileNotInCacheException e) {
// Entry was deleted; no problem
}
}
return sb.toString();
}
private Object[] listBinary()
{
return getStatistics(sum).entrySet().stream()
.map(e -> new Object[] { e.getKey(), e.getValue() })
.toArray(Object[]::new);
}
private Serializable listStatistics(ByteUnit unit)
{
Map<String, long[]> stats = getStatistics(sum);
StringBuilder sb = new StringBuilder();
stats.forEach((sc, counter) -> {
sb.append(Formats.field(sc, 24, Formats.LEFT)).
append(" ").
append(Formats.field(String.valueOf(unit.convert(counter[0], BYTES)), 10, Formats.RIGHT)).
append(" ").
append(Formats.field(String.valueOf(counter[1]), 8, Formats.RIGHT)).
append(" ").
append(Formats.field(String.valueOf(unit.convert(counter[2], BYTES)), 10, Formats.RIGHT)).
append(" ").
append(Formats.field(String.valueOf(counter[3]), 8, Formats.RIGHT)).
append(" ").
append(Formats.field(String.valueOf(unit.convert(counter[4], BYTES)), 10, Formats.RIGHT)).
append(" ").
append(Formats.field(String.valueOf(counter[5]), 8, Formats.RIGHT)).
append(" ").
append(Formats.field(String.valueOf(unit.convert(counter[6], BYTES)), 10, Formats.RIGHT)).
append(" ").
append(Formats.field(String.valueOf(counter[7]), 8, Formats.RIGHT)).
append("\n");
});
return sb.toString();
}
private Serializable listById(PnfsId[] pnfsIds) throws CacheException, InterruptedException
{
StringBuilder sb = new StringBuilder();
StringBuilder exceptionMessages = new StringBuilder();
for (PnfsId pnfsId : pnfsIds) {
try {
sb.append(_repository.getEntry(pnfsId));
sb.append("\n");
} catch (FileNotInCacheException fnice) {
exceptionMessages.append(fnice.getMessage()).append("\n");
}
}
sb.append(exceptionMessages.toString());
return sb.toString();
}
}
@Command(name = "rep rmclass", hint = "remove replicas by storage class",
description = "Removed all replicas of this pool that belong to a given storage class. " +
"WARNING: This is a dangerous command and may result in data loss if misused.")
public class RemoveClassCommand extends DelayedCommand<String>
{
@Argument(usage = "A storage class.")
String storageClassName;
@Override
public String execute()
{
int cnt = 0;
for (PnfsId id: _repository) {
try {
CacheEntry entry = _repository.getEntry(id);
FileAttributes fileAttributes = entry.getFileAttributes();
if (fileAttributes.isDefined(FileAttribute.STORAGECLASS)) {
String sc = fileAttributes.getStorageClass();
if (sc.equals(storageClassName)) {
_repository.setState(id, ReplicaState.REMOVED);
cnt++;
}
}
} catch (FileNotInCacheException ignored) {
// File was deleted - no problem
} catch (IllegalTransitionException ignored) {
// File is transient - no problem
} catch (CacheException e) {
_log.error("Failed to delete {}: {}", id, e.getMessage());
} catch (InterruptedException e) {
_log.warn("File removal was interrupted.");
break;
}
}
_log.info("'rep rmclass {}' removed {} files.", storageClassName, cnt);
return cnt + " files removed.";
}
}
@Command(name = "rep rm", hint = "remove replica",
description = "Removes a replica from the pool. The replica is only "+
"removed if it is CACHED and not STICKY.")
public class RemoveCommand implements Callable<String>
{
@Argument(usage = "PNFS ID of replica to remove.")
PnfsId pnfsId;
@Option(name = "force",
usage = "Removes replica even if it is not garbage collectable. WARNING: This is " +
"a dangerous option and may result in data loss if misused.")
boolean isForced;
@Override
public String call() throws Exception
{
CacheEntry entry = _repository.getEntry(pnfsId);
if (isForced || entry.getState() == ReplicaState.CACHED && !entry.isSticky()) {
_log.warn("rep rm: removing {}", pnfsId);
_repository.setState(pnfsId, ReplicaState.REMOVED);
return "Removed " + pnfsId;
} else {
return "File is not removable; use -force to override";
}
}
}
@Command(name = "rep set precious", hint = "set replica precious",
description = "Marks a replica as precious. On tape connected pools, precious " +
"replicas are flushed to tape.")
public class SetPreciousCommand implements Callable<String>
{
@Argument(usage = "PNFS ID of replica to make precious.")
PnfsId pnfsId;
@Override
public String call() throws Exception
{
_repository.setState(pnfsId, ReplicaState.PRECIOUS);
return "";
}
}
@Command(name = "rep set cached", hint = "set replica cached",
description = "Marks a replica as cached. Unless also marked sticky, cached files " +
"can be garbage collected. WARNING: This is a dangerous command and may " +
"result in data loss if misused.")
public class SetCachedCommand implements Callable<String>
{
@Argument(usage = "PNFS ID of replica to make cached.")
PnfsId pnfsId;
@Override
public String call() throws Exception
{
_repository.setState(pnfsId, ReplicaState.CACHED);
return "";
}
}
@Command(name = "rep set broken", hint = "set replica broken",
description = "Marks a replica as broken. Broken replicas are not served to clients. Such " +
"replicas are subject to an automatic error recovery upon pool restart.")
public class SetBrokenommand implements Callable<String>
{
@Argument(usage = "PNFS ID of replica to mark as broken.")
PnfsId pnfsId;
@Override
public String call() throws Exception
{
_repository.setState(pnfsId, ReplicaState.BROKEN);
return "";
}
}
private Map<String, long[]> getStatistics(boolean isSumIncluded)
{
Map<String,long[]> map = _statisticsListener.toMap();
if (isSumIncluded) {
long[] counter = new long[10];
map.put("total", counter);
SpaceRecord record = _repository.getSpaceRecord();
counter[0] = record.getTotalSpace();
counter[1] = record.getFreeSpace();
counter[2] = _statisticsListener.getOtherBytes();
}
return map;
}
private static class Statistics
{
long bytes;
int entries;
long preciousBytes;
int preciousEntries;
long stickyBytes;
int stickyEntries;
long otherBytes;
int otherEntries;
long[] toArray()
{
return new long[] { bytes, entries, preciousBytes, preciousEntries, stickyBytes, stickyEntries, otherBytes, otherEntries };
}
}
private static class StatisticsListener implements StateChangeListener
{
private final Map<String, Statistics> statistics = new HashMap<>();
@Override
public void stateChanged(StateChangeEvent event)
{
updateStatistics(event, event.getOldState(), event.getNewState());
}
@Override
public void accessTimeChanged(EntryChangeEvent event)
{
}
@Override
public void stickyChanged(StickyChangeEvent event)
{
updateStatistics(event, event.getOldEntry().getState(), event.getNewEntry().getState());
}
private boolean isPrecious(CacheEntry entry)
{
return entry.getState() == ReplicaState.PRECIOUS;
}
private boolean isSticky(CacheEntry entry)
{
return entry.isSticky();
}
private boolean isOther(CacheEntry entry)
{
return !isPrecious(entry) && !isSticky(entry);
}
private Statistics getStatistics(FileAttributes fileAttributes)
{
String store = fileAttributes.getStorageClass() + "@" + fileAttributes.getHsm();
return statistics.computeIfAbsent(store, s -> new Statistics());
}
private void removeStatistics(FileAttributes fileAttributes)
{
String store = fileAttributes.getStorageClass() + "@" + fileAttributes.getHsm();
statistics.remove(store);
}
private synchronized void updateStatistics(EntryChangeEvent event, ReplicaState oldState, ReplicaState newState)
{
if (oldState == ReplicaState.CACHED || oldState == ReplicaState.PRECIOUS) {
CacheEntry oldEntry = event.getOldEntry();
Statistics stats = getStatistics(oldEntry.getFileAttributes());
stats.bytes -= oldEntry.getReplicaSize();
stats.entries--;
if (isPrecious(oldEntry)) {
stats.preciousBytes -= oldEntry.getReplicaSize();
stats.preciousEntries--;
}
if (isSticky(oldEntry)) {
stats.stickyBytes -= oldEntry.getReplicaSize();
stats.stickyEntries--;
}
if (isOther(oldEntry)) {
stats.otherBytes -= oldEntry.getReplicaSize();
stats.otherEntries--;
}
if (stats.entries == 0) {
removeStatistics(oldEntry.getFileAttributes());
}
}
if (newState == ReplicaState.CACHED || newState == ReplicaState.PRECIOUS) {
CacheEntry newEntry = event.getNewEntry();
Statistics stats = getStatistics(newEntry.getFileAttributes());
stats.bytes += newEntry.getReplicaSize();
stats.entries++;
if (isPrecious(newEntry)) {
stats.preciousBytes += newEntry.getReplicaSize();
stats.preciousEntries++;
}
if (isSticky(newEntry)) {
stats.stickyBytes += newEntry.getReplicaSize();
stats.stickyEntries++;
}
if (isOther(newEntry)) {
stats.otherBytes += newEntry.getReplicaSize();
stats.otherEntries++;
}
}
}
public synchronized Map<String,long[]> toMap()
{
Map<String,long[]> map = new HashMap<>();
for (Map.Entry<String, Statistics> entry : statistics.entrySet()) {
map.put(entry.getKey(), entry.getValue().toArray());
}
return map;
}
public synchronized long getOtherBytes()
{
long sum = 0;
for (Statistics stats : statistics.values()) {
sum += stats.otherBytes;
}
return sum;
}
}
}