package org.dcache.pinmanager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Required; import org.springframework.transaction.annotation.Transactional; import java.util.Collection; import java.util.Date; import java.util.UUID; import java.util.concurrent.TimeUnit; import diskCacheV111.poolManager.PoolSelectionUnit; import diskCacheV111.util.CacheException; import diskCacheV111.util.InvalidMessageCacheException; import diskCacheV111.util.PermissionDeniedCacheException; import diskCacheV111.util.PnfsId; import diskCacheV111.util.TimeoutCacheException; import diskCacheV111.vehicles.PoolSetStickyMessage; import dmg.cells.nucleus.CellPath; import org.dcache.auth.Subjects; import dmg.cells.nucleus.CellMessageReceiver; import dmg.cells.nucleus.NoRouteToCellException; import org.dcache.cells.CellStub; import org.dcache.pinmanager.model.Pin; import org.dcache.pool.repository.StickyRecord; import org.dcache.poolmanager.PoolMonitor; import org.dcache.services.pinmanager1.PinManagerMovePinMessage; import static java.util.concurrent.TimeUnit.MINUTES; import static org.dcache.pinmanager.model.Pin.State.*; import static org.springframework.transaction.annotation.Isolation.REPEATABLE_READ; /** * Processes requests to move pins. * * The stratetegy for moving a pin is the following: * * 1. Create a record in the DB for the target pool * 2. Create a sticky flag on the target pool * 3. Change the original record to point to the target pool and * change the record created in step 1 to point to the old pool. * 4. Remove the sticky flag from the old pool. * * If the above process is aborted at any point the regular recovery * tasks of the pin manager will remove any stale sticky flag from * either the old or the new pool. * * Pin lifetime extension is treated as a special case of move (moving * to the same pool, but with a longer lifetime). */ public class MovePinRequestProcessor implements CellMessageReceiver { private static final Logger _log = LoggerFactory.getLogger(MovePinRequestProcessor.class); private static final long POOL_LIFETIME_MARGIN = MINUTES.toMillis(30); private PinDao _dao; private CellStub _poolStub; private AuthorizationPolicy _pdp; private long _maxLifetime; private TimeUnit _maxLifetimeUnit; private PoolMonitor _poolMonitor; @Required public void setDao(PinDao dao) { _dao = dao; } @Required public void setPoolStub(CellStub stub) { _poolStub = stub; } @Required public void setAuthorizationPolicy(AuthorizationPolicy pdp) { _pdp = pdp; } @Required public void setPoolMonitor(PoolMonitor poolMonitor) { _poolMonitor = poolMonitor; } @Required public void setMaxLifetimeUnit(TimeUnit unit) { _maxLifetimeUnit = unit; } public TimeUnit getMaxLifetimeUnit() { return _maxLifetimeUnit; } @Required public void setMaxLifetime(long maxLifetime) { _maxLifetime = maxLifetime; } @Required public long getMaxLifetime() { return _maxLifetime; } protected Pin createTemporaryPin(PnfsId pnfsId, String pool) { long now = System.currentTimeMillis(); return _dao.create(_dao.set() .subject(Subjects.ROOT) .pnfsId(pnfsId) .state(PINNING) .pool(pool) .sticky("PinManager-" + UUID.randomUUID().toString()) .expirationTime(new Date(now + 2 * _poolStub.getTimeoutInMillis()))); } @Transactional(isolation=REPEATABLE_READ) protected Pin swapPins(Pin pin, Pin tmpPin, Date expirationTime) throws CacheException { Pin targetPin = _dao.get(_dao.where() .id(tmpPin.getPinId()) .sticky(tmpPin.getSticky()) .state(PINNING)); if (targetPin == null) { /* The pin likely expired. We are now in a situation in * which we may or may not have a sticky flag on the * target pool, but no record in the database. To be on * the safe side we create a new record in the database * and then abort. */ _dao.create(_dao.set() .subject(Subjects.ROOT) .pnfsId(tmpPin.getPnfsId()) .pool(tmpPin.getPool()) .sticky(tmpPin.getSticky()) .state(UNPINNING)); throw new TimeoutCacheException("Move expired"); } Pin sourcePin = _dao.update(_dao.where() .id(pin.getPinId()) .sticky(pin.getSticky()) .state(PINNED), _dao.set() .pool(targetPin.getPool()) .sticky(targetPin.getSticky()) .expirationTime(expirationTime)); if (sourcePin == null) { /* The target pin will expire by itself. */ throw new CacheException("Pin no longer valid"); } return _dao.update(targetPin, _dao.set() .pool(pin.getPool()) .sticky(pin.getSticky()) .state(UNPINNING)); } private void setSticky(String poolName, PnfsId pnfsId, boolean sticky, String owner, long validTill) throws CacheException, InterruptedException, NoRouteToCellException { PoolSelectionUnit.SelectionPool pool = _poolMonitor.getPoolSelectionUnit().getPool(poolName); if (pool == null || !pool.isActive()) { throw new CacheException("Unable to move sticky flag because pool " + poolName + " is unavailable"); } PoolSetStickyMessage msg = new PoolSetStickyMessage(poolName, pnfsId, sticky, owner, validTill); _poolStub.sendAndWait(new CellPath(pool.getAddress()), msg); } protected Pin move(Pin pin, String pool, Date expirationTime) throws CacheException, InterruptedException, NoRouteToCellException { Pin tmpPin = createTemporaryPin(pin.getPnfsId(), pool); setSticky(tmpPin.getPool(), tmpPin.getPnfsId(), true, tmpPin.getSticky(), (expirationTime == null) ? - 1 : (expirationTime.getTime() + POOL_LIFETIME_MARGIN)); return swapPins(pin, tmpPin, expirationTime); } private boolean containsPin(Collection<Pin> pins, String sticky) { for (Pin pin: pins) { if (sticky.equals(pin.getSticky())) { return true; } } return false; } public PinManagerMovePinMessage messageArrived(PinManagerMovePinMessage message) throws CacheException, InterruptedException { try { PnfsId pnfsId = message.getPnfsId(); String source = message.getSourcePool(); String target = message.getTargetPool(); Collection<Pin> pins = _dao.get(_dao.where().pnfsId(pnfsId).pool(source)); /* Remove all stale sticky flags. */ for (StickyRecord record: message.getRecords()) { if (!containsPin(pins, record.owner())) { setSticky(source, pnfsId, false, record.owner(), 0); } } /* Move all pins to the target pool. */ for (Pin pin: pins) { Pin tmpPin = move(pin, target, pin.getExpirationTime()); setSticky(tmpPin.getPool(), tmpPin.getPnfsId(), false, tmpPin.getSticky(), 0); _dao.delete(tmpPin); } _log.info("Moved pins for {} from {} to {}", pnfsId, source, target); } catch (NoRouteToCellException e) { throw new CacheException("Failed to move pin due to communication failure: " + e.getDestinationPath(), e); } return message; } public PinManagerExtendPinMessage messageArrived(PinManagerExtendPinMessage message) throws CacheException, InterruptedException { try { Pin pin = _dao.get(_dao.where().pnfsId(message.getFileAttributes().getPnfsId()).id(message.getPinId())); if (pin == null) { throw new InvalidMessageCacheException("Pin does not exist"); } else if (!_pdp.canExtend(message.getSubject(), pin)) { throw new PermissionDeniedCacheException("Access denied"); } else if (pin.getState() == PINNING) { throw new InvalidMessageCacheException("File is not pinned yet"); } else if (pin.getState() == UNPINNING) { throw new InvalidMessageCacheException("Pin is no longer valid"); } if (_maxLifetime > -1) { message.setLifetime(Math.min(_maxLifetimeUnit.toMillis(_maxLifetime), message.getLifetime())); } long lifetime = message.getLifetime(); if (pin.hasRemainingLifetimeLessThan(lifetime)) { long now = System.currentTimeMillis(); Date date = (lifetime == -1) ? null : new Date(now + lifetime); move(pin, pin.getPool(), date); message.setExpirationTime(date); } else { message.setExpirationTime(pin.getExpirationTime()); } _log.info("Extended pin for {} ({})", pin.getPnfsId(), pin.getPinId()); return message; } catch (NoRouteToCellException e) { throw new CacheException("Failed to extend pin due to communication failure: " + e.getDestinationPath(), e); } } }