package org.dcache.pinmanager;
import com.google.common.collect.ImmutableMap;
import com.google.common.primitives.Longs;
import com.google.common.util.concurrent.MoreExecutors;
import org.junit.Test;
import javax.annotation.ParametersAreNonnullByDefault;
import javax.security.auth.Subject;
import java.io.Serializable;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.AbstractExecutorService;
import java.util.concurrent.Callable;
import java.util.concurrent.Delayed;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Function;
import java.util.function.Predicate;
import diskCacheV111.poolManager.Pool;
import diskCacheV111.poolManager.PoolMonitorV5;
import diskCacheV111.pools.PoolCostInfo;
import diskCacheV111.util.CacheException;
import diskCacheV111.util.CheckStagePermission;
import diskCacheV111.util.PnfsId;
import diskCacheV111.vehicles.DCapProtocolInfo;
import diskCacheV111.vehicles.GenericStorageInfo;
import diskCacheV111.vehicles.Message;
import diskCacheV111.vehicles.PoolMgrSelectReadPoolMsg;
import diskCacheV111.vehicles.PoolSetStickyMessage;
import diskCacheV111.vehicles.ProtocolInfo;
import diskCacheV111.vehicles.StorageInfo;
import diskCacheV111.vehicles.StorageInfos;
import dmg.cells.nucleus.CellAddressCore;
import dmg.cells.nucleus.CellEndpoint;
import dmg.cells.nucleus.CellMessage;
import dmg.cells.nucleus.CellMessageAnswerable;
import dmg.cells.nucleus.CellMessageReceiver;
import dmg.cells.nucleus.SerializationException;
import org.dcache.auth.Subjects;
import org.dcache.cells.CellMessageDispatcher;
import org.dcache.cells.CellStub;
import org.dcache.pinmanager.model.Pin;
import org.dcache.pool.classic.IoQueueManager;
import org.dcache.poolmanager.PoolInfo;
import org.dcache.poolmanager.PoolMonitor;
import org.dcache.poolmanager.PoolSelector;
import org.dcache.poolmanager.SelectedPool;
import org.dcache.vehicles.FileAttributes;
import static java.util.Arrays.asList;
import static java.util.stream.Collectors.toList;
import static org.dcache.pinmanager.model.Pin.State.PINNED;
import static org.dcache.pinmanager.model.Pin.State.UNPINNING;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
public class PinManagerTests
{
final static ProtocolInfo PROTOCOL_INFO =
new DCapProtocolInfo("DCap", 3, 0,
new InetSocketAddress("127.0.0.1", 17));
final static StorageInfo STORAGE_INFO =
new GenericStorageInfo("osm", "default");
final static PnfsId PNFS_ID1 =
new PnfsId("0000D4CF1C3302B44095969C8216CE1E9175");
final static PnfsId PNFS_ID2 =
new PnfsId("00009C09780FEE044CC18940B335B2AE93E6");
final static PnfsId PNFS_ID3 =
new PnfsId("00003113FDF3F9284EE8992FC9F651082956");
final static String REQUEST_ID1 = "request1";
final static String POOL1 = "pool1";
final static String STICKY1 = "PinManager-1";
private FileAttributes getAttributes(PnfsId pnfsId)
{
FileAttributes attributes = new FileAttributes();
attributes.setPnfsId(pnfsId);
attributes.setLocations(Collections.singleton(POOL1));
StorageInfos.injectInto(STORAGE_INFO, attributes);
// Required attributes, but the values are not relevant
// when pool manager and pool are stubbed out.
attributes.setSize(0L);
attributes.setAccessLatency(StorageInfo.DEFAULT_ACCESS_LATENCY);
attributes.setRetentionPolicy(StorageInfo.DEFAULT_RETENTION_POLICY);
attributes.setChecksums(Collections.emptySet());
attributes.setCacheClass(null);
attributes.setHsm("osm");
attributes.setFlags(Collections.emptyMap());
return attributes;
}
@Test
public void testPinning()
throws CacheException, InterruptedException, ExecutionException
{
TestDao dao = new TestDao();
PinRequestProcessor processor = new PinRequestProcessor();
processor.setScheduledExecutor(new TestExecutor());
processor.setExecutor(MoreExecutors.directExecutor());
processor.setDao(dao);
processor.setPoolStub(new TestStub(new CellAddressCore("PinManager")) {
public PoolSetStickyMessage messageArrived(PoolSetStickyMessage msg)
{
return msg;
}
});
processor.setPoolManagerStub(new TestStub(new CellAddressCore("PinManager")) {
public PoolMgrSelectReadPoolMsg messageArrived(PoolMgrSelectReadPoolMsg msg)
{
msg.setPoolName(POOL1);
msg.setPoolAddress(new CellAddressCore(POOL1));
return msg;
}
});
processor.setMaxLifetime(-1);
processor.setStagePermission(new CheckStagePermission(null));
processor.setPoolMonitor(new PoolMonitorV5() {
@Override
public PoolSelector getPoolSelector(FileAttributes fileAttributes,
ProtocolInfo protocolInfo,
String linkGroup)
{
return new PoolMonitorV5.PnfsFileLocation(fileAttributes, protocolInfo, linkGroup) {
@Override
public SelectedPool selectPinPool()
{
return new SelectedPool(new PoolInfo(
new CellAddressCore(POOL1),
new PoolCostInfo(POOL1, IoQueueManager.DEFAULT_QUEUE),
ImmutableMap.of()));
}
};
}
});
Date expiration = new Date(now() + 30);
PinManagerPinMessage message =
new PinManagerPinMessage(getAttributes(PNFS_ID1),
PROTOCOL_INFO, REQUEST_ID1, 30);
Date start = new Date();
message = processor.messageArrived(message).get();
Date stop = new Date();
assertEquals(0, message.getReturnCode());
assertFalse(message.getExpirationTime().before(expiration));
Pin pin = dao.get(dao.where().id(message.getPinId()));
assertEquals(PNFS_ID1, pin.getPnfsId());
assertBetween(start, stop, pin.getCreationTime());
assertEquals(message.getExpirationTime(), pin.getExpirationTime());
assertEquals(0, pin.getUid());
assertEquals(0, pin.getGid());
assertEquals(REQUEST_ID1, pin.getRequestId());
assertEquals(POOL1, pin.getPool());
assertEquals(PINNED, pin.getState());
assertValidSticky(pin.getSticky());
}
@Test
public void testExtendLifetime()
throws CacheException, InterruptedException, ExecutionException
{
TestDao dao = new TestDao();
Pin pin = dao.create(dao.set()
.subject(Subjects.ROOT)
.requestId(REQUEST_ID1)
.expirationTime(new Date(now() + 30))
.pnfsId(PNFS_ID1)
.pool(POOL1)
.sticky(STICKY1)
.state(PINNED));
Pool pool = new Pool(POOL1);
pool.setActive(true);
pool.setAddress(new CellAddressCore(POOL1));
PoolMonitor poolMonitor = mock(PoolMonitor.class, RETURNS_DEEP_STUBS);
when(poolMonitor.getPoolSelectionUnit().getPool(POOL1)).thenReturn(pool);
MovePinRequestProcessor processor = new MovePinRequestProcessor();
processor.setDao(dao);
processor.setPoolStub(new TestStub(new CellAddressCore("PinManager")) {
public PoolSetStickyMessage messageArrived(PoolSetStickyMessage msg)
{
return msg;
}
});
processor.setAuthorizationPolicy(new DefaultAuthorizationPolicy());
processor.setMaxLifetime(-1);
processor.setPoolMonitor(poolMonitor);
Date expiration = new Date(now() + 60);
PinManagerExtendPinMessage message =
new PinManagerExtendPinMessage(getAttributes(PNFS_ID1),
pin.getPinId(), 60);
message = processor.messageArrived(message);
assertEquals(0, message.getReturnCode());
assertFalse(message.getExpirationTime().before(expiration));
Pin newPin = dao.get(dao.where().id(pin.getPinId()));
assertEquals(PNFS_ID1, newPin.getPnfsId());
assertEquals(pin.getCreationTime(), newPin.getCreationTime());
assertEquals(message.getExpirationTime(), newPin.getExpirationTime());
assertEquals(pin.getUid(), newPin.getUid());
assertEquals(pin.getGid(), newPin.getGid());
assertEquals(pin.getRequestId(), newPin.getRequestId());
assertEquals(pin.getPool(), newPin.getPool());
assertEquals(pin.getState(), newPin.getState());
assertValidSticky(newPin.getSticky());
}
@Test
public void testUnpinningByPinId()
throws CacheException, InterruptedException, ExecutionException
{
TestDao dao = new TestDao();
Pin pin = dao.create(dao.set()
.subject(Subjects.ROOT)
.requestId(REQUEST_ID1)
.expirationTime(new Date(now() + 30))
.pnfsId(PNFS_ID1)
.pool(POOL1)
.sticky(STICKY1)
.state(PINNED));
UnpinRequestProcessor processor = new UnpinRequestProcessor();
processor.setDao(dao);
processor.setAuthorizationPolicy(new DefaultAuthorizationPolicy());
PinManagerUnpinMessage message =
new PinManagerUnpinMessage(PNFS_ID1);
message.setPinId(pin.getPinId());
message = processor.messageArrived(message);
assertEquals(0, message.getReturnCode());
assertEquals(pin.getPinId(), (long) message.getPinId());
assertEquals(pin.getRequestId(), message.getRequestId());
Pin newPin = dao.get(dao.where().id(pin.getPinId()));
assertEquals(PNFS_ID1, newPin.getPnfsId());
assertEquals(pin.getPool(), newPin.getPool());
assertEquals(UNPINNING, newPin.getState());
assertEquals(pin.getSticky(), newPin.getSticky());
}
@Test
public void testUnpinningByRequestId()
throws CacheException, InterruptedException, ExecutionException
{
TestDao dao = new TestDao();
Pin pin = dao.create(dao.set()
.subject(Subjects.ROOT)
.requestId(REQUEST_ID1)
.expirationTime(new Date(now() + 30))
.pnfsId(PNFS_ID1)
.pool(POOL1)
.sticky(STICKY1)
.state(PINNED));
UnpinRequestProcessor processor = new UnpinRequestProcessor();
processor.setDao(dao);
processor.setAuthorizationPolicy(new DefaultAuthorizationPolicy());
PinManagerUnpinMessage message =
new PinManagerUnpinMessage(PNFS_ID1);
message.setRequestId(pin.getRequestId());
message = processor.messageArrived(message);
assertEquals(0, message.getReturnCode());
assertEquals(pin.getPinId(), (long) message.getPinId());
assertEquals(pin.getRequestId(), message.getRequestId());
Pin newPin = dao.get(dao.where().id(pin.getPinId()));
assertEquals(PNFS_ID1, newPin.getPnfsId());
assertEquals(pin.getPool(), newPin.getPool());
assertEquals(UNPINNING, newPin.getState());
assertEquals(pin.getSticky(), newPin.getSticky());
}
<T extends Comparable<T>> void assertBetween(T lower, T upper, T actual)
{
String message =
String.format("Expected between <%s> and <%s> but was <%s>",
lower, upper, actual);
assertTrue(message, lower.compareTo(actual) <= 0);
assertTrue(message, upper.compareTo(actual) >= 0);
}
void assertValidSticky(String sticky)
{
String message =
String.format("Expected sticky to start with PinManager but was <%s>",
sticky);
assertTrue(message, sticky.startsWith("PinManager"));
}
static long now()
{
return System.currentTimeMillis();
}
}
@ParametersAreNonnullByDefault
class TestDao implements PinDao
{
long _counter;
Map<Long,Pin> _pins = new HashMap<>();
@Override
public PinCriterion where()
{
return new TestCriterion();
}
@Override
public PinUpdate set()
{
return new TestUpdate();
}
@Override
public Pin create(PinUpdate update)
{
Pin pin = ((TestUpdate) update).createPin(_counter++);
_pins.put(pin.getPinId(), pin);
return pin;
}
@Override
public List<Pin> get(PinCriterion criterion)
{
return _pins.values().stream().filter(((TestCriterion) criterion)::matches).collect(toList());
}
@Override
public List<Pin> get(PinCriterion criterion, int limit)
{
return _pins.values().stream().filter(((TestCriterion) criterion)::matches).limit(limit).collect(toList());
}
@Override
public Pin get(UniquePinCriterion criterion)
{
return _pins.values().stream().filter(((TestCriterion) criterion)::matches).findFirst().orElse(null);
}
@Override
public int count(PinCriterion criterion)
{
return (int) _pins.values().stream().filter(((TestCriterion) criterion)::matches).count();
}
@Override
public Pin update(UniquePinCriterion criterion, PinUpdate update)
{
Pin pin = get(criterion);
if (pin == null) {
return null;
}
update((PinCriterion) criterion, update);
return ((TestUpdate) update).apply(pin);
}
@Override
public int update(PinCriterion criterion, PinUpdate update)
{
TestUpdate u = (TestUpdate) update;
int cnt = 0;
for (Map.Entry<Long, Pin> e : _pins.entrySet()) {
if (((TestCriterion) criterion).matches(e.getValue())) {
cnt++;
e.setValue(u.apply(e.getValue()));
}
}
return cnt;
}
@Override
public int delete(PinCriterion criterion)
{
int cnt = 0;
Iterator<Pin> iterator = _pins.values().iterator();
while (iterator.hasNext()) {
Pin pin = iterator.next();
if (((TestCriterion) criterion).matches(pin)) {
iterator.remove();
cnt++;
}
}
return cnt;
}
@Override
public void foreach(PinCriterion criterion, InterruptibleConsumer<Pin> f) throws InterruptedException
{
for (Pin pin : _pins.values()) {
if (((TestCriterion) criterion).matches(pin)) {
f.accept(pin);
}
}
}
private static class TestCriterion implements UniquePinCriterion
{
private final List<Predicate<Pin>> predicates = new ArrayList<>();
private Long id;
private PnfsId pnfsId;
private String requestId;
protected TestCriterion add(Predicate<Pin> predicate)
{
predicates.add(predicate);
return this;
}
@Override
public TestCriterion id(long id)
{
this.id = id;
return add(p -> p.getPinId() == id);
}
@Override
public TestCriterion pnfsId(PnfsId id)
{
pnfsId = id;
return add(p -> Objects.equals(p.getPnfsId(), id));
}
@Override
public TestCriterion requestId(String requestId)
{
this.requestId = requestId;
return add(p -> Objects.equals(p.getRequestId(), requestId));
}
@Override
public TestCriterion expirationTimeBefore(Date date)
{
return add(p -> p.getExpirationTime().before(date));
}
@Override
public TestCriterion state(Pin.State state)
{
return add(p -> p.getState() == state);
}
@Override
public TestCriterion stateIsNot(Pin.State state)
{
return add(p -> p.getState() != state);
}
@Override
public TestCriterion pool(String pool)
{
return add(p -> Objects.equals(p.getPool(), pool));
}
@Override
public TestCriterion sticky(String sticky)
{
return add(p -> Objects.equals(p.getSticky(), sticky));
}
@Override
public TestCriterion sameIdAs(UniquePinCriterion criterion)
{
TestCriterion c = (TestCriterion) criterion;
if (c.id != null) {
return id(c.id);
} else {
return pnfsId(c.pnfsId).requestId(c.requestId);
}
}
boolean matches(Pin pin)
{
return predicates.stream().allMatch(p -> p.test(pin));
}
}
private static class TestUpdate implements PinUpdate
{
private final List<Function<Pin, Pin>> updates = new ArrayList<>();
private PinUpdate add(Function<Pin,Pin> update)
{
updates.add(update);
return this;
}
@Override
public PinUpdate expirationTime(Date date)
{
return add(p -> new Pin(p.getPinId(), p.getPnfsId(), p.getRequestId(), p.getCreationTime(), date,
p.getUid(), p.getGid(), p.getState(), p.getPool(), p.getSticky()));
}
@Override
public PinUpdate pool(String pool)
{
return add(p -> new Pin(p.getPinId(), p.getPnfsId(), p.getRequestId(), p.getCreationTime(),
p.getExpirationTime(), p.getUid(), p.getGid(), p.getState(),
pool, p.getSticky()));
}
@Override
public PinUpdate requestId(String requestId)
{
return add(p -> new Pin(p.getPinId(), p.getPnfsId(), requestId, p.getCreationTime(),
p.getExpirationTime(), p.getUid(), p.getGid(), p.getState(),
p.getPool(), p.getSticky()));
}
@Override
public PinUpdate state(Pin.State state)
{
return add(p -> new Pin(p.getPinId(), p.getPnfsId(), p.getRequestId(), p.getCreationTime(),
p.getExpirationTime(), p.getUid(), p.getGid(), state,
p.getPool(), p.getSticky()));
}
@Override
public PinUpdate sticky(String sticky)
{
return add(p -> new Pin(p.getPinId(), p.getPnfsId(), p.getRequestId(), p.getCreationTime(),
p.getExpirationTime(), p.getUid(), p.getGid(), p.getState(),
p.getPool(), sticky));
}
@Override
public PinUpdate subject(Subject subject)
{
return add(p -> {
long uid = Subjects.getUid(subject);
long gid = Subjects.getPrimaryGid(subject);
return new Pin(p.getPinId(), p.getPnfsId(), p.getRequestId(), p.getCreationTime(),
p.getExpirationTime(), uid, gid, p.getState(),
p.getPool(), p.getSticky());
});
}
@Override
public PinUpdate pnfsId(PnfsId pnfsId)
{
return add(p -> new Pin(p.getPinId(), pnfsId, p.getRequestId(), p.getCreationTime(),
p.getExpirationTime(), p.getUid(), p.getGid(), p.getState(),
p.getPool(), p.getSticky()));
}
Pin apply(Pin pin)
{
for (Function<Pin,Pin> u : updates) {
pin = u.apply(pin);
}
return pin;
}
public Pin createPin(long id)
{
return apply(new Pin(id));
}
}
}
class TestExecutor
extends AbstractExecutorService implements ScheduledExecutorService
{
@Override
public void execute(Runnable runnable)
{
runnable.run();
}
@Override
public boolean awaitTermination(long timeout, TimeUnit unit)
{
return false;
}
@Override
public boolean isShutdown()
{
return false;
}
@Override
public boolean isTerminated()
{
return false;
}
@Override
public void shutdown()
{
}
@Override
public List<Runnable> shutdownNow()
{
return Collections.emptyList();
}
@Override
public <V> ScheduledFuture<V>
schedule(Callable<V> callable, long delay, TimeUnit unit)
{
return new ImmediateScheduledFuture(submit(callable));
}
@Override
public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit)
{
return new ImmediateScheduledFuture(submit(command));
}
@Override
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)
{
return new ImmediateScheduledFuture(submit(command));
}
@Override
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit)
{
return new ImmediateScheduledFuture(submit(command));
}
class ImmediateScheduledFuture<T> implements ScheduledFuture<T>
{
private Future<T> _inner;
public ImmediateScheduledFuture(Future<T> future) {
_inner = future;
}
@Override
public long getDelay(TimeUnit unit)
{
return 0;
}
@Override
public int compareTo(Delayed o)
{
return Longs.compare(getDelay(TimeUnit.MILLISECONDS), o.getDelay(TimeUnit.MILLISECONDS));
}
@Override
public boolean cancel(boolean mayInterruptIfRunning)
{
return _inner.cancel(mayInterruptIfRunning);
}
@Override
public T get()
throws InterruptedException,ExecutionException
{
return _inner.get();
}
@Override
public T get(long timeout, TimeUnit unit)
throws InterruptedException,ExecutionException, TimeoutException
{
return _inner.get(timeout, unit);
}
@Override
public boolean isCancelled()
{
return _inner.isCancelled();
}
@Override
public boolean isDone()
{
return _inner.isDone();
}
}
}
class TestEndpoint implements CellEndpoint, CellMessageReceiver
{
private final CellAddressCore _address;
protected CellMessageDispatcher _dispatcher =
new CellMessageDispatcher("messageArrived");
public TestEndpoint(CellAddressCore address)
{
_address = address;
_dispatcher.addMessageListener(this);
}
public TestEndpoint(CellAddressCore address, CellMessageReceiver o)
{
this(address);
_dispatcher.addMessageListener(o);
}
protected CellMessage process(CellMessage envelope)
{
Object result = _dispatcher.call(envelope);
if (result == null) {
return null;
}
Serializable o = envelope.getMessageObject();
if (o instanceof Message) {
Message msg = (Message)o;
/* dCache vehicles can transport errors back to the
* requester, so detect if this is an error reply.
*/
if (result instanceof CacheException) {
CacheException e = (CacheException)result;
msg.setFailed(e.getRc(), e.getMessage());
result = msg;
} else if (result instanceof IllegalArgumentException) {
msg.setFailed(CacheException.INVALID_ARGS,
result.toString());
result = msg;
} else if (result instanceof Exception) {
msg.setFailed(CacheException.UNEXPECTED_SYSTEM_EXCEPTION,
(Exception) result);
result = msg;
}
}
envelope.revertDirection();
envelope.setMessageObject((Serializable) result);
return envelope;
}
@Override
public void sendMessage(CellMessage envelope, SendFlag... flags)
throws SerializationException
{
if (!asList(flags).contains(SendFlag.PASS_THROUGH)) {
envelope.addSourceAddress(_address);
}
process(envelope);
}
@Override
public void sendMessage(CellMessage envelope,
CellMessageAnswerable callback,
Executor executor,
long timeout,
SendFlag... flags)
throws SerializationException
{
if (!asList(flags).contains(SendFlag.PASS_THROUGH)) {
envelope.addSourceAddress(_address);
}
CellMessage answer = process(envelope);
if (answer != null) {
callback.answerArrived(envelope, answer);
} else {
callback.answerTimedOut(envelope);
}
}
@Override
public Map<String,Object> getDomainContext()
{
throw new UnsupportedOperationException();
}
}
class TestStub extends CellStub implements CellMessageReceiver
{
public TestStub(CellAddressCore address)
{
setDestination("dummy");
setCellEndpoint(new TestEndpoint(address, this));
}
}