package org.dcache.pool.repository.v5; import com.google.common.collect.Sets; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.net.URI; import java.util.EnumSet; import java.util.List; import diskCacheV111.util.CacheException; import diskCacheV111.util.FileCorruptedCacheException; import diskCacheV111.util.PnfsHandler; import diskCacheV111.util.PnfsId; import org.dcache.alarms.AlarmMarkerFactory; import org.dcache.alarms.PredefinedAlarm; import org.dcache.pool.movers.IoMode; import org.dcache.pool.repository.Allocator; import org.dcache.pool.repository.ReplicaState; import org.dcache.pool.repository.ReplicaRecord; import org.dcache.pool.repository.ReplicaDescriptor; import org.dcache.pool.repository.RepositoryChannel; import org.dcache.pool.repository.StickyRecord; import org.dcache.util.Checksum; import org.dcache.vehicles.FileAttributes; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import static com.google.common.collect.Iterables.*; import static java.util.Collections.singleton; import static org.dcache.namespace.FileAttribute.*; class WriteHandleImpl implements ReplicaDescriptor { enum HandleState { OPEN, COMMITTED, CLOSED } private static final Logger _log = LoggerFactory.getLogger("logger.org.dcache.repository"); /** * Time that a new CACHED file with no sticky flags will be marked * sticky. */ private static final long HOLD_TIME = 5 * 60 * 1000; // 5 minutes /** Callback for resilience handling. Pool name can be accessed here */ private final ReplicaRepository _repository; /** Space allocation is delegated to this allocator. */ private final Allocator _allocator; /** The handler provides access to this entry. */ private final ReplicaRecord _entry; /** File attributes of the file being written. */ private final FileAttributes _fileAttributes; /** Stub for talking to the PNFS manager. */ private final PnfsHandler _pnfs; /** Sticky flags to be applied after the transfer. */ private final List<StickyRecord> _stickyRecords; /** The entry state used during transfer. */ private final ReplicaState _initialState; /** The entry state used when the handle is committed. */ private ReplicaState _targetState; /** The state of the write handle. */ private HandleState _state; /** Amount of space allocated for this handle. */ private long _allocated; /** Current thread which performs allocation. */ private Thread _allocationThread; /** Last access time of new replica. */ private Long _atime; WriteHandleImpl(ReplicaRepository repository, Allocator allocator, PnfsHandler pnfs, ReplicaRecord entry, FileAttributes fileAttributes, ReplicaState targetState, List<StickyRecord> stickyRecords) { _repository = checkNotNull(repository); _allocator = checkNotNull(allocator); _pnfs = checkNotNull(pnfs); _entry = checkNotNull(entry); _fileAttributes = checkNotNull(fileAttributes); _initialState = entry.getState(); _targetState = checkNotNull(targetState); _stickyRecords = checkNotNull(stickyRecords); _state = HandleState.OPEN; _allocated = 0; checkState(_initialState != ReplicaState.FROM_CLIENT || _fileAttributes.isDefined(EnumSet.of(RETENTION_POLICY, ACCESS_LATENCY))); checkState(_initialState == ReplicaState.FROM_CLIENT || _fileAttributes.isDefined(SIZE)); } private synchronized void setState(HandleState state) { _state = state; if (state != HandleState.OPEN && _allocationThread != null) { _allocationThread.interrupt(); } } private synchronized boolean isOpen() { return _state == HandleState.OPEN; } @Override public RepositoryChannel createChannel() throws IOException { return _entry.openChannel(IoMode.WRITE); } /** * Sets the allocation thread to the calling thread. Blocks if * allocation thread is already set. * * @throws InterruptedException if thread is interrupted * @throws IllegalStateException if handle is closed */ private synchronized void setAllocationThread() throws InterruptedException, IllegalStateException { while (_allocationThread != null) { wait(); } if (!isOpen()) { throw new IllegalStateException("Handle is closed"); } _allocationThread = Thread.currentThread(); } /** * Clears the allocation thread field. */ private synchronized void clearAllocationThread() { _allocationThread = null; notifyAll(); } /** * Allocate space and block until space becomes available. * * @param size in bytes * @throws InterruptedException if thread is interrupted * @throws IllegalStateException if handle is closed * @throws IllegalArgumentException * if <i>size</i> < 0 */ @Override public void allocate(long size) throws IllegalStateException, IllegalArgumentException, InterruptedException { if (size < 0) { throw new IllegalArgumentException("Size is negative"); } setAllocationThread(); try { _allocator.allocate(size); } catch (InterruptedException e) { if (!isOpen()) { throw new IllegalStateException("Handle is closed"); } throw e; } finally { clearAllocationThread(); } synchronized (this) { _allocated += size; } } /** * Allocate space if available. A non blocking version of {@link Allocator#allocate(long)} * * @param size in bytes * @throws InterruptedException if thread is interrupted * @throws IllegalStateException if handle is closed * @throws IllegalArgumentException if <i>size</i> < 0 * @return true if and only if the request space was allocated */ @Override public boolean allocateNow(long size) throws IllegalStateException, IllegalArgumentException, InterruptedException { if (size < 0) { throw new IllegalArgumentException("Size is negative"); } boolean isAllocated; setAllocationThread(); try { isAllocated = _allocator.allocateNow(size); } catch (InterruptedException e) { if (!isOpen()) { throw new IllegalStateException("Handle is closed"); } throw e; } finally { clearAllocationThread(); } if (isAllocated) { synchronized (this) { _allocated += size; } } return isAllocated; } /** * Freeing space through a write handle is not supported. This * method always throws IllegalStateException. */ @Override public void free(long size) throws IllegalStateException { throw new IllegalStateException("Space cannot be freed through a write handle"); } /** * Adjust space reservation. Will log an error in case of under * allocation. */ private synchronized void adjustReservation(long length) throws InterruptedException { try { if (_allocated < length) { _log.error("Under allocation detected. This is a bug. Please report it."); _allocator.allocate(length - _allocated); } else if (_allocated > length) { _allocator.free(_allocated - length); } _allocated = length; } catch (InterruptedException e) { /* Space allocation is broken now. The entry size * matches up with what was actually allocated, * however the file on disk is too large. * * Should only happen during shutdown, so no harm done. */ _log.warn("Failed to adjust space reservation because the operation was interrupted. The pool is now over allocated."); throw e; } } private void registerFileAttributesInNameSpace() throws CacheException { FileAttributes attributesToUpdate = FileAttributes.ofLocation(_repository.getPoolName()); if (_fileAttributes.isDefined(CHECKSUM)) { /* PnfsManager detects conflicting checksums and will fail the update. */ attributesToUpdate.setChecksums(_fileAttributes.getChecksums()); } if (_initialState == ReplicaState.FROM_CLIENT) { attributesToUpdate.setAccessLatency(_fileAttributes.getAccessLatency()); attributesToUpdate.setRetentionPolicy(_fileAttributes.getRetentionPolicy()); if (_fileAttributes.isDefined(SIZE) && _fileAttributes.getSize() > 0) { attributesToUpdate.setSize(_fileAttributes.getSize()); } } _pnfs.setFileAttributes(_entry.getPnfsId(), attributesToUpdate); } private void verifyFileSize(long length) throws CacheException { assert _initialState == ReplicaState.FROM_CLIENT || _fileAttributes.isDefined(SIZE); if ((_initialState != ReplicaState.FROM_CLIENT || (_fileAttributes.isDefined(SIZE) && _fileAttributes.getSize() > 0)) && _fileAttributes.getSize() != length) { throw new FileCorruptedCacheException(_fileAttributes.getSize(), length); } } @Override public synchronized void commit() throws IllegalStateException, InterruptedException, CacheException { if (_state != HandleState.OPEN) { throw new IllegalStateException("Handle is closed"); } try { _entry.setLastAccessTime((_atime == null) ? System.currentTimeMillis() : _atime); long length = _entry.getReplicaSize(); adjustReservation(length); verifyFileSize(length); _fileAttributes.setSize(length); registerFileAttributesInNameSpace(); _entry.update(r -> { r.setFileAttributes(_fileAttributes); /* In several situations, dCache requests a CACHED file * without having any sticky flags on it. Such files are * subject to immediate garbage collection if we are short on * disk space. Thus to give other clients time to access the * file, we mark it sticky for a short amount of time. */ if (_targetState == ReplicaState.CACHED && _stickyRecords.isEmpty()) { long now = System.currentTimeMillis(); r.setSticky("self", now + HOLD_TIME, false); } /* Move entry to target state. */ for (StickyRecord record : _stickyRecords) { r.setSticky(record.owner(), record.expire(), false); } return r.setState(_targetState); }); setState(HandleState.COMMITTED); } catch (CacheException e) { /* If any of the PNFS operations return FILE_NOT_FOUND, * then we change the target state and the close method * will take care of removing the file. */ if (e.getRc() == CacheException.FILE_NOT_FOUND) { _targetState = ReplicaState.REMOVED; } throw e; } } /** * Fails the operation. Called by close without a successful * commit. The file is either removed or marked bad, depending on * its state. */ private synchronized void fail() { long length = _entry.getReplicaSize(); try { adjustReservation(length); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } /* Files from tape or from another pool are deleted in case of * errors. */ if (_initialState == ReplicaState.FROM_POOL || _initialState == ReplicaState.FROM_STORE) { _targetState = ReplicaState.REMOVED; } /* If nothing was uploaded, we delete the replica and leave the name space * entry it is virgin state. */ if (length == 0) { _targetState = ReplicaState.REMOVED; } /* Unless replica is to be removed, register cache location and * other attributes. */ if (_targetState != ReplicaState.REMOVED) { try { /* We register cache location separately in the failure flow, because * updating other attributes (such as checksums) may itself trigger * failures in PNFS, and at the very least our cache location should * be registered. */ _pnfs.addCacheLocation(_entry.getPnfsId(), _repository.getPoolName()); registerFileAttributesInNameSpace(); } catch (CacheException e) { if (e.getRc() == CacheException.FILE_NOT_FOUND) { _targetState = ReplicaState.REMOVED; } else { _log.warn("Failed to register {} after failed replica creation: {}", _fileAttributes, e.getMessage()); } } } if (_targetState == ReplicaState.REMOVED) { try { _entry.update(r -> r.setState(ReplicaState.REMOVED)); } catch (CacheException e) { _log.warn("Failed to remove replica: {}", e.getMessage()); } } else { PnfsId id = _entry.getPnfsId(); _log.warn(AlarmMarkerFactory.getMarker(PredefinedAlarm.BROKEN_FILE, id.toString(), _repository.getPoolName()), "Marking pool entry {} on {} as BROKEN", _entry.getPnfsId(), _repository.getPoolName()); try { _entry.update(r -> r.setState(ReplicaState.BROKEN)); } catch (CacheException e) { _log.warn("Failed to mark replica as broken: {}", e.getMessage()); } } } @Override public synchronized void close() throws IllegalStateException { switch (_state) { case CLOSED: throw new IllegalStateException("Handle is closed"); case OPEN: fail(); setState(HandleState.CLOSED); break; case COMMITTED: setState(HandleState.CLOSED); break; } } /** * @return disk file * @throws IllegalStateException if EntryIODescriptor is closed. */ @Override public synchronized URI getReplicaFile() throws IllegalStateException { if (_state == HandleState.CLOSED) { throw new IllegalStateException("Handle is closed"); } return _entry.getReplicaUri(); } @Override public FileAttributes getFileAttributes() throws IllegalStateException { return _fileAttributes; } @Override public synchronized Iterable<Checksum> getChecksums() throws CacheException { if (!_fileAttributes.isDefined(CHECKSUM)) { _fileAttributes.setChecksums(_pnfs .getFileAttributes(_entry.getPnfsId(), EnumSet.of(CHECKSUM)) .getChecksums()); } return unmodifiableIterable(_fileAttributes.getChecksums()); } @Override public synchronized void addChecksums(Iterable<Checksum> checksums) { if (!isEmpty(checksums)) { Iterable<Checksum> newChecksums; if (_fileAttributes.isDefined(CHECKSUM)) { newChecksums = concat(_fileAttributes.getChecksums(), checksums); } else { newChecksums = checksums; } _fileAttributes.setChecksums(Sets.newHashSet(newChecksums)); } } @Override public void setLastAccessTime(long time) { if (_state == HandleState.CLOSED) { throw new IllegalStateException("Handle is closed"); } _atime = time; } @Override public long getReplicaSize() { return _entry.getReplicaSize(); } }