/** * Copyright (c) 2015 The original author or authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.reveno.atp.core.storage; import org.reveno.atp.core.RevenoConfiguration.RevenoJournalingConfiguration; import org.reveno.atp.core.api.channel.Channel; import org.reveno.atp.core.api.storage.FoldersStorage; import org.reveno.atp.core.api.storage.JournalsStorage; import org.reveno.atp.core.api.storage.SnapshotStorage; import org.reveno.atp.core.channel.FileChannel; import org.reveno.atp.utils.Exceptions; import org.reveno.atp.utils.UnsafeUtils; import org.reveno.atp.utils.VersionedFileUtils.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; import java.nio.file.*; import java.util.*; import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; import static org.reveno.atp.utils.VersionedFileUtils.*; import static org.reveno.atp.utils.VersionedFileUtils.parseVersionedFile; public class FileSystemStorage implements FoldersStorage, JournalsStorage, SnapshotStorage { @Override public Channel channel(String address) { FileChannel fc = new FileChannel(new File(baseDir, address)); fc.extendDelta(config.maxObjectSize()); fc.channelOptions(config.channelOptions()); fc.isPreallocated(config.isPreallocated()); return fc.init(); } @Override public JournalStore[] getAllStores() { return getJournalStores(txs(), evns()); } @Override public Channel snapshotChannel(String address) { return new FileChannel(new File(baseDir, address)).init(); } @Override public SnapshotStore getLastSnapshotStore() { VersionedFile file = lastVersionedFile(baseDir, SNAPSHOT_PREFIX); if (file != null) { return new SnapshotStore(file.getName(), file.getFileDate().getTimeInMillis(), file.getVersion(), file.getRest().length == 0 ? file.getVersion() : Long.parseLong(file.getRest()[0])); } else return null; } @Override public JournalStore[] getStoresAfterVersion(long journalVersion) { final List<VersionedFile> txs = afterLastSnapshot(journalVersion, txs()); final List<VersionedFile> evns = afterLastSnapshot(journalVersion, evns()); if (txs.size() != evns.size()) throw new RuntimeException(String.format("Amount of Transaction files doesn't match to Events files [%s/%s]", txs.size(), evns.size())); return getJournalStores(txs, evns); } @Override public SnapshotStore nextSnapshotAfter(long journalVersion) { return nextSnapshotAfter("", journalVersion); } @Override public SnapshotStore nextTempSnapshotStore() { return nextSnapshotAfter("temp_", 0); } public SnapshotStore nextSnapshotAfter(String prefix, long journalVersion) { String nextFile = nextVersionFile(baseDir, prefix + SNAPSHOT_PREFIX, null, Long.toString(journalVersion)); try { new File(baseDir, nextFile).createNewFile(); } catch (IOException e) { throw new RuntimeException(e); } VersionedFile vf = parseVersionedFile(nextFile); return new SnapshotStore(nextFile, System.currentTimeMillis(), vf.getVersion(), journalVersion); } @Override public void removeSnapshotStore(SnapshotStore snapshot) { new File(baseDir, snapshot.getSnapshotPath()).delete(); } @Override public void move(SnapshotStore from, SnapshotStore to) { new File(baseDir, from.getSnapshotPath()).renameTo(new File(baseDir, to.getSnapshotPath())); } @Override public void removeLastSnapshotStore() { Optional<String> fileName = lastVersionFile(baseDir, SNAPSHOT_PREFIX); if (fileName.isPresent()) new File(baseDir, fileName.get()).delete(); } @Override public JournalStore[] getVolumes() { List<VersionedFile> txs = listVersioned(baseDir, VOLUME_TRANSACTION_PREFIX); List<VersionedFile> evns = listVersioned(baseDir, VOLUME_EVENTS_PREFIX); if (txs.size() != evns.size()) { txs = txs.subList(0, Math.max(Math.min(txs.size(), evns.size()), 0)); evns = evns.subList(0, Math.max(Math.min(txs.size(), evns.size()), 0)); } return getJournalStores(txs, evns); } @Override public void mergeStores(JournalStore[] stores, JournalStore to) { merge(Arrays.stream(stores).map(JournalStore::getEventsCommitsAddress).collect(Collectors.toList()), to.getEventsCommitsAddress()); merge(Arrays.stream(stores).map(JournalStore::getTransactionCommitsAddress).collect(Collectors.toList()), to.getTransactionCommitsAddress()); } @Override public void deleteStore(JournalStore store) { new File(store.getEventsCommitsAddress()).delete(); new File(store.getTransactionCommitsAddress()).delete(); } @Override public JournalStore nextTempStore() { VersionedFile txFile = parseVersionedFile(nextVersionFile(baseDir, TRANSACTION_PREFIX, 0)); VersionedFile evnFile = parseVersionedFile(nextVersionFile(baseDir, EVENTS_PREFIX, 0)); return store(txFile, evnFile, "tmp_"); } @Override public synchronized JournalStore nextStore() { return nextStore(0); } @Override public JournalStore nextStore(long lastTxId) { VersionedFile txFile = parseVersionedFile(nextVersionFile(baseDir, TRANSACTION_PREFIX, lastTxId)); VersionedFile evnFile = parseVersionedFile(nextVersionFile(baseDir, EVENTS_PREFIX, lastTxId)); if (txFile.getVersion() != evnFile.getVersion()) throw new RuntimeException(String.format( "Versions of Journals are not equal [tx=%d,evn=%d]", txFile.getVersion(), evnFile.getVersion())); return store(txFile, evnFile, ""); } @Override public synchronized JournalStore nextVolume(long txSize, long eventsSize) { VersionedFile txFile = parseVersionedFile(nextVersionFile(baseDir, VOLUME_TRANSACTION_PREFIX)); VersionedFile evnFile = parseVersionedFile(nextVersionFile(baseDir, VOLUME_EVENTS_PREFIX)); if (txFile.getVersion() < evnFile.getVersion()) { IntStream.range(0, (int) Math.abs(txFile.getVersion() - evnFile.getVersion())).forEach(i -> { VersionedFile file = parseVersionedFile(nextVersionFile(baseDir, VOLUME_TRANSACTION_PREFIX)); preallocateFiles(new File(baseDir, file.getName()), txSize); }); txFile = parseVersionedFile(nextVersionFile(baseDir, VOLUME_TRANSACTION_PREFIX)); } else if (txFile.getVersion() > evnFile.getVersion()) { IntStream.range(0, (int) Math.abs(txFile.getVersion() - evnFile.getVersion())).forEach(i -> { VersionedFile file = parseVersionedFile(nextVersionFile(baseDir, VOLUME_EVENTS_PREFIX)); preallocateFiles(new File(baseDir, file.getName()), eventsSize); }); evnFile = parseVersionedFile(nextVersionFile(baseDir, VOLUME_EVENTS_PREFIX)); } preallocateFiles(new File(baseDir, txFile.getName()), txSize); LOG.info("Finished preallocating journal [{}]", txFile.getName()); preallocateFiles(new File(baseDir, evnFile.getName()), eventsSize); LOG.info("Finished preallocating journal [{}]", evnFile.getName()); return new JournalStore(txFile.getName(), evnFile.getName(), txFile.getVersion(), 0); } @Override public JournalStore convertVolumeToStore(JournalStore volume) { return convertVolumeToStore(volume, 0); } @Override public JournalStore convertVolumeToStore(JournalStore volume, long lastTxId) { VersionedFile txFile = parseVersionedFile(nextVersionFile(baseDir, TRANSACTION_PREFIX, lastTxId)); VersionedFile evnFile = parseVersionedFile(nextVersionFile(baseDir, EVENTS_PREFIX, lastTxId)); new File(baseDir, volume.getTransactionCommitsAddress()).renameTo(new File(baseDir, txFile.getName())); new File(baseDir, volume.getEventsCommitsAddress()).renameTo(new File(baseDir, evnFile.getName())); return new JournalStore(txFile.getName(), evnFile.getName(), txFile.getVersion(), lastTxId); } @Override public Folder nextFolder(String prefix) { String folder = nextVersionFile(baseDir, prefix); new File(baseDir, folder).mkdir(); return new Folder(folder, prefix); } @Override public Folder getLastFolder(String prefix) { List<String> folders = listFolders(baseDir, prefix); return folders.size() == 0 ? null : new Folder(folders.get(folders.size() - 1), prefix); } @Override public FolderItem[] getItems(Folder folder) { List<FolderItem> collect = listFiles(new File(baseDir, folder.getGroupAddress()).toPath()).stream() .map(Path::toFile) .map(f -> new FolderItem(f.getName(), folder.getGroupAddress() + "/ " + f.getName())) .collect(Collectors.toList()); return collect.toArray(new FolderItem[collect.size()]); } @Override public FolderItem getItem(String name, Folder folder) { String address = folder.getGroupAddress() + "/" + name; if (new File(baseDir, address).exists()) return new FolderItem(name, address); else return null; } @Override public FolderItem createItem(Folder folder, String name) { String address = folder.getGroupAddress() + "/" + name; new File(baseDir, address).mkdir(); return new FolderItem(name, address); } public File getBaseDir() { return baseDir; } protected void merge(List<String> fromStream, String to) { if (fromStream.size() == 1) { new File(baseDir, fromStream.get(0)).renameTo(new File(baseDir, to)); } else { try { final java.nio.channels.FileChannel dest = new RandomAccessFile(new File(baseDir, to), "rw").getChannel(); final long[] offset = {0}; fromStream.forEach(f -> { try { java.nio.channels.FileChannel from = new RandomAccessFile(new File(baseDir, f), "rw").getChannel(); dest.transferFrom(from, offset[0], from.size()); offset[0] += from.size(); from.close(); new File(baseDir, f).delete(); } catch (Throwable e) { throw Exceptions.runtime(e); } }); dest.close(); } catch (Throwable e) { throw Exceptions.runtime(e); } } } protected JournalStore[] getJournalStores(List<VersionedFile> txs, List<VersionedFile> evns) { LOG.debug("evns: " + evns.size() + ", txs: " + txs.size()); List<JournalStore> collect = txs.stream().map(tx -> evns.stream() .filter(e -> e.getVersion() == tx.getVersion() && e.getFileDate().equals(tx.getFileDate())) .map(e -> store(tx, e, "")).findFirst().get()).collect(Collectors.toList()); return collect.toArray(new JournalStore[collect.size()]); } protected List<Path> listFiles(Path dir) { List<Path> result = new ArrayList<>(); try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, "*")) { for (Path entry: stream) { if (!Files.isDirectory(entry)) result.add(entry); } } catch (DirectoryIteratorException | IOException ex) { throw new RuntimeException(ex); } return result; } protected List<VersionedFile> evns() { return listVersioned(baseDir, EVENTS_PREFIX); } protected List<VersionedFile> txs() { return listVersioned(baseDir, TRANSACTION_PREFIX); } protected List<VersionedFile> afterLastSnapshot(long version, List<VersionedFile> txs) { return txs.stream().filter(f -> f.getVersion() > version).collect(Collectors.toList()); } protected JournalStore store(VersionedFile txFile, VersionedFile evnFile, String prefix) { try { new File(baseDir, prefix + txFile.getName()).createNewFile(); new File(baseDir, prefix + evnFile.getName()).createNewFile(); } catch (IOException e) { throw new RuntimeException(e); } long lastTxId = 0L; if (txFile.getRest().length > 0) { lastTxId = Long.parseLong(txFile.getRest()[0]); } else if (txFile.getRest().length != evnFile.getRest().length) { throw new IllegalArgumentException("Transaction and Event file names are not equal!"); } return new JournalStore(prefix + txFile.getName(), prefix + evnFile.getName(), txFile.getVersion(), lastTxId); } protected void preallocateFiles(File file, long size) { try { RandomAccessFile raf = new RandomAccessFile(file, "rw"); LOG.info("Preallocating started."); for (long i = 0; i < size; i += PAGE_SIZE) { raf.write(BLANK_PAGE, 0, PAGE_SIZE); } LOG.info("Preallocating finished."); raf.close(); } catch (Exception e) { throw Exceptions.runtime(e); } } public FileSystemStorage(File baseDir, RevenoJournalingConfiguration config) { if (!baseDir.exists()) { baseDir.mkdirs(); } this.baseDir = baseDir; this.config = config; } protected final File baseDir; protected final RevenoJournalingConfiguration config; protected static final String TRANSACTION_PREFIX = "tx"; protected static final String SNAPSHOT_PREFIX = "snp"; protected static final String EVENTS_PREFIX = "evn"; protected static final String VOLUME_TRANSACTION_PREFIX = "v_" + TRANSACTION_PREFIX; protected static final String VOLUME_EVENTS_PREFIX = "v_" + EVENTS_PREFIX; protected static final int PAGE_SIZE = UnsafeUtils.getUnsafe().pageSize(); protected static final byte[] BLANK_PAGE = new byte[PAGE_SIZE]; protected static final Logger LOG = LoggerFactory.getLogger(FileSystemStorage.class); }