/* dCache - http://www.dcache.org/ * * Copyright (C) 2014 Deutsches Elektronen-Synchrotron * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.dcache.pool.nearline.tar; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Iterables; import com.google.common.collect.Multimap; import com.google.common.collect.Multimaps; import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingDeque; import diskCacheV111.util.CacheException; import diskCacheV111.util.InvalidMessageCacheException; import diskCacheV111.util.PnfsId; import org.dcache.pool.nearline.spi.FlushRequest; import org.dcache.pool.nearline.spi.NearlineStorage; import org.dcache.pool.nearline.spi.RemoveRequest; import org.dcache.pool.nearline.spi.StageRequest; import org.dcache.util.Checksum; import org.dcache.vehicles.FileAttributes; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.collect.Iterables.filter; import static com.google.common.collect.Iterables.getFirst; /** * This class is for demonstration only. The code is a bit messy and incomplete. */ public class TarNearlineStorage implements NearlineStorage { private final String type; private final String name; private final BlockingQueue<FlushRequest> flushQueue = new LinkedBlockingDeque<>(); private final Multimap<String,StageRequest> stageRequests = Multimaps.synchronizedMultimap(ArrayListMultimap.<String,StageRequest>create()); private final ExecutorService executor = Executors.newSingleThreadExecutor(); private volatile File directory; public TarNearlineStorage(String type, String name) { this.type = type; this.name = name; } protected Iterable<URI> getLocations(FileAttributes fileAttributes) { return filter(fileAttributes.getStorageInfo().locations(), uri -> uri.getScheme().equals(type) && uri.getAuthority().equals(name)); } @Override public void flush(Iterable<FlushRequest> requests) { Iterables.addAll(flushQueue, requests); executor.execute(new FlushTask()); } @Override public void stage(Iterable<StageRequest> requests) { for (StageRequest request : requests) { File tarFile; try { FileAttributes fileAttributes = request.getFileAttributes(); URI location = getFirst(getLocations(fileAttributes), null); if (location == null) { throw new CacheException(CacheException.BROKEN_ON_TAPE, "File not on nearline storage: " + fileAttributes.getPnfsId()); } String path = location.getPath(); if (path == null) { throw new InvalidMessageCacheException("Invalid nearline storage URI: " + location); } tarFile = new File(path).getParentFile(); if (tarFile == null) { throw new InvalidMessageCacheException("Invalid nearline storage URI: " + location); } } catch (CacheException e) { request.failed(e); continue; } stageRequests.put(tarFile.getName(), request); } executor.execute(new StageTask()); } @Override public void remove(Iterable<RemoveRequest> requests) { for (RemoveRequest request : requests) { request.failed(new CacheException("Remove from tar nearline storage is not supported.")); } } @Override public void cancel(UUID uuid) { // Not implemented } @Override public void configure(Map<String, String> properties) throws IllegalArgumentException { String directory = properties.get("directory"); checkArgument(directory != null, "directory attribute is required"); this.directory = new File(directory); } @Override public void shutdown() { executor.shutdownNow(); } private class FlushTask implements Runnable { @Override public void run() { List<FlushRequest> requests = new ArrayList<>(); flushQueue.drainTo(requests); if (!requests.isEmpty()) { Map<URI, URI> uris = new HashMap<>(); String tarName = UUID.randomUUID().toString(); File tarFile = new File(directory, tarName + ".tar"); try (FileOutputStream out = new FileOutputStream(tarFile)) { try (TarArchiveOutputStream tarStream = new TarArchiveOutputStream(out)) { for (FlushRequest request : requests) { request.activate().get(); Path file = Paths.get(request.getReplicaUri()); PnfsId pnfsId = request.getFileAttributes().getPnfsId(); TarArchiveEntry entry = new TarArchiveEntry(pnfsId.getId()); entry.setSize(Files.size(file)); tarStream.putArchiveEntry(entry); Files.copy(file, tarStream); tarStream.closeArchiveEntry(); uris.put(request.getReplicaUri(), new URI(type, name, '/' + tarName + '/' + pnfsId.getId(), null, null)); } tarStream.finish(); } } catch (Exception e) { try { Files.deleteIfExists(tarFile.toPath()); } catch (IOException suppressed) { e.addSuppressed(suppressed); } for (FlushRequest request : requests) { request.failed(e); } return; } for (FlushRequest request : requests) { request.completed(Collections.singleton(uris.get(request.getReplicaUri()))); } } } } private class StageTask implements Runnable { @Override public void run() { List<String> archives; synchronized(stageRequests) { archives = new ArrayList<>(stageRequests.keySet()); } for (String archive : archives) { Map<String, StageRequest> requests = new HashMap<>(); for (StageRequest request : stageRequests.removeAll(archive)) { try { request.activate().get(); requests.put(request.getFileAttributes().getPnfsId().getId(), request); } catch (Exception e) { request.failed(e); } } File tarFile = new File(directory, archive + ".tar"); try (FileInputStream in = new FileInputStream(tarFile)) { try (TarArchiveInputStream tarStream = new TarArchiveInputStream(in)) { TarArchiveEntry entry; while (!requests.isEmpty() && (entry = tarStream.getNextTarEntry()) != null) { StageRequest request = requests.remove(entry.getName()); if (request != null) { try { request.allocate().get(); Files.copy(tarStream, request.getFile().toPath()); request.completed(Collections.emptySet()); } catch (Exception e) { request.failed(e); } } } } } catch (Exception e) { for (StageRequest request : requests.values()) { request.failed(e); } } for (StageRequest request : requests.values()) { request.failed(new CacheException(CacheException.BROKEN_ON_TAPE, "File not found: " + request.getFileAttributes().getPnfsId())); } } } } }