/* * * * Copyright 2014 Orient Technologies LTD (info(at)orientechnologies.com) * * * * 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. * * * * For more information: http://www.orientechnologies.com * */ package com.orientechnologies.orient.core.storage.impl.local.paginated; import com.orientechnologies.common.collection.closabledictionary.OClosableLinkedContainer; import com.orientechnologies.common.directmemory.OByteBufferPool; import com.orientechnologies.common.exception.OException; import com.orientechnologies.common.io.OFileUtils; import com.orientechnologies.common.io.OIOUtils; import com.orientechnologies.common.log.OLogManager; import com.orientechnologies.common.parser.OSystemVariableResolver; import com.orientechnologies.orient.core.command.OCommandOutputListener; import com.orientechnologies.orient.core.compression.impl.OZIPCompressionUtil; import com.orientechnologies.orient.core.config.OGlobalConfiguration; import com.orientechnologies.orient.core.config.OStorageConfiguration; import com.orientechnologies.orient.core.db.record.ridbag.sbtree.OIndexRIDContainer; import com.orientechnologies.orient.core.db.record.ridbag.sbtree.OSBTreeCollectionManagerShared; import com.orientechnologies.orient.core.engine.local.OEngineLocalPaginated; import com.orientechnologies.orient.core.exception.OStorageException; import com.orientechnologies.orient.core.index.engine.OHashTableIndexEngine; import com.orientechnologies.orient.core.index.engine.OSBTreeIndexEngine; import com.orientechnologies.orient.core.metadata.OMetadataDefault; import com.orientechnologies.orient.core.storage.cache.OReadCache; import com.orientechnologies.orient.core.storage.cache.local.OWOWCache; import com.orientechnologies.orient.core.storage.cache.local.twoq.O2QCache; import com.orientechnologies.orient.core.storage.fs.OFileClassic; import com.orientechnologies.orient.core.storage.impl.local.OAbstractPaginatedStorage; import com.orientechnologies.orient.core.storage.impl.local.OFreezableStorageComponent; import com.orientechnologies.orient.core.storage.impl.local.OStorageConfigurationSegment; import com.orientechnologies.orient.core.storage.impl.local.OStorageVariableParser; import com.orientechnologies.orient.core.storage.impl.local.paginated.wal.ODiskWriteAheadLog; import com.orientechnologies.orient.core.storage.impl.local.paginated.wal.OLogSequenceNumber; import com.orientechnologies.orient.core.storage.impl.local.paginated.wal.OWriteAheadLog; import java.io.*; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.concurrent.*; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; /** * @author Andrey Lomakin * @since 28.03.13 */ public class OLocalPaginatedStorage extends OAbstractPaginatedStorage implements OFreezableStorageComponent { private static String[] ALL_FILE_EXTENSIONS = { ".ocf", ".pls", ".pcl", ".oda", ".odh", ".otx", ".ocs", ".oef", ".oem", ".oet", ".fl", ".json", ".DS_Store", ODiskWriteAheadLog.WAL_SEGMENT_EXTENSION, ODiskWriteAheadLog.MASTER_RECORD_EXTENSION, OHashTableIndexEngine.BUCKET_FILE_EXTENSION, OHashTableIndexEngine.METADATA_FILE_EXTENSION, OHashTableIndexEngine.TREE_FILE_EXTENSION, OHashTableIndexEngine.NULL_BUCKET_FILE_EXTENSION, OClusterPositionMap.DEF_EXTENSION, OSBTreeIndexEngine.DATA_FILE_EXTENSION, OWOWCache.NAME_ID_MAP_EXTENSION, OIndexRIDContainer.INDEX_FILE_EXTENSION, OSBTreeCollectionManagerShared.DEFAULT_EXTENSION, OSBTreeIndexEngine.NULL_BUCKET_FILE_EXTENSION, O2QCache.CACHE_STATISTIC_FILE_EXTENSION }; private static final int ONE_KB = 1024; private final int DELETE_MAX_RETRIES; private final int DELETE_WAIT_TIME; private final OStorageVariableParser variableParser; private final OPaginatedStorageDirtyFlag dirtyFlag; private final String storagePath; private ExecutorService checkpointExecutor; private final OClosableLinkedContainer<Long, OFileClassic> files; public OLocalPaginatedStorage(final String name, final String filePath, final String mode, final int id, OReadCache readCache, OClosableLinkedContainer<Long, OFileClassic> files) throws IOException { super(name, filePath, mode, id); this.readCache = readCache; this.files = files; File f = new File(url); String sp; if (f.exists() || !exists(f.getParent())) { // ALREADY EXISTS OR NOT LEGACY sp = OSystemVariableResolver.resolveSystemVariables(OFileUtils.getPath(new File(url).getPath())); } else { // LEGACY DB sp = OSystemVariableResolver.resolveSystemVariables(OFileUtils.getPath(new File(url).getParent())); } storagePath = OIOUtils.getPathFromDatabaseName(sp); variableParser = new OStorageVariableParser(storagePath); configuration = new OStorageConfigurationSegment(this); DELETE_MAX_RETRIES = OGlobalConfiguration.FILE_DELETE_RETRY.getValueAsInteger(); DELETE_WAIT_TIME = OGlobalConfiguration.FILE_DELETE_DELAY.getValueAsInteger(); dirtyFlag = new OPaginatedStorageDirtyFlag(storagePath + File.separator + "dirty.fl"); } @Override public void create(final Map<String, Object> iProperties) { stateLock.acquireWriteLock(); try { final File storageFolder = new File(storagePath); if (!storageFolder.exists()) if (!storageFolder.mkdirs()) throw new OStorageException("Cannot create folders in storage with path " + storagePath); super.create(iProperties); } finally { stateLock.releaseWriteLock(); } } @Override protected String normalizeName(String name) { final int firstIndexOf = name.lastIndexOf('/'); final int secondIndexOf = name.lastIndexOf(File.separator); if (firstIndexOf >= 0 || secondIndexOf >= 0) return name.substring(Math.max(firstIndexOf, secondIndexOf) + 1); else return name; } public boolean exists() { if (status == STATUS.OPEN) return true; return exists(storagePath); } @Override public String getURL() { return OEngineLocalPaginated.NAME + ":" + url; } public String getStoragePath() { return storagePath; } public OStorageVariableParser getVariableParser() { return variableParser; } @Override public String getType() { return OEngineLocalPaginated.NAME; } @Override public List<String> backup(OutputStream out, Map<String, Object> options, final Callable<Object> callable, final OCommandOutputListener iOutput, final int compressionLevel, final int bufferSize) throws IOException { if (out == null) throw new IllegalArgumentException("Backup output is null"); freeze(false); try { if (callable != null) try { callable.call(); } catch (Exception e) { OLogManager.instance().error(this, "Error on callback invocation during backup", e); } final OutputStream bo = bufferSize > 0 ? new BufferedOutputStream(out, bufferSize) : out; try { return OZIPCompressionUtil .compressDirectory(new File(getStoragePath()).getAbsolutePath(), bo, new String[] { ".wal", ".fl" }, iOutput, compressionLevel); } finally { if (bufferSize > 0) { bo.flush(); bo.close(); } } } finally { release(); } } @Override public void restore(InputStream in, Map<String, Object> options, final Callable<Object> callable, final OCommandOutputListener iListener) throws IOException { if (!isClosed()) close(true, false); OZIPCompressionUtil.uncompressDirectory(in, getStoragePath(), iListener); if (callable != null) try { callable.call(); } catch (Exception e) { OLogManager.instance().error(this, "Error on calling callback on database restore"); } open(null, null, null); } @Override public OStorageConfiguration getConfiguration() { stateLock.acquireReadLock(); try { return super.getConfiguration(); } finally { stateLock.releaseReadLock(); } } @Override protected OLogSequenceNumber copyWALToIncrementalBackup(ZipOutputStream zipOutputStream, long startSegment) throws IOException { File[] nonActiveSegments; OLogSequenceNumber lastLSN; long freezeId = getAtomicOperationsManager().freezeAtomicOperations(null, null); try { lastLSN = writeAheadLog.end(); writeAheadLog.newSegment(); nonActiveSegments = writeAheadLog.nonActiveSegments(startSegment); } finally { getAtomicOperationsManager().releaseAtomicOperations(freezeId); } for (final File nonActiveSegment : nonActiveSegments) { final FileInputStream fileInputStream = new FileInputStream(nonActiveSegment); try { final BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream); try { final ZipEntry entry = new ZipEntry(nonActiveSegment.getName()); zipOutputStream.putNextEntry(entry); try { final byte[] buffer = new byte[4096]; int br = 0; while ((br = bufferedInputStream.read(buffer)) >= 0) { zipOutputStream.write(buffer, 0, br); } } finally { zipOutputStream.closeEntry(); } } finally { bufferedInputStream.close(); } } finally { fileInputStream.close(); } } return lastLSN; } @Override protected File createWalTempDirectory() { final File walDirectory = new File(getStoragePath(), "walIncrementalBackupRestoreDirectory"); if (walDirectory.exists()) { OFileUtils.deleteRecursively(walDirectory); } if (!walDirectory.mkdirs()) throw new OStorageException("Can not create temporary directory to store files created during incremental backup"); return walDirectory; } @Override protected void addFileToDirectory(String name, InputStream stream, File directory) throws IOException { final byte[] buffer = new byte[4096]; int rb = -1; int bl = 0; final File walBackupFile = new File(directory, name); final FileOutputStream outputStream = new FileOutputStream(walBackupFile); try { final BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream); try { while (true) { while (bl < buffer.length && (rb = stream.read(buffer, bl, buffer.length - bl)) > -1) { bl += rb; } bufferedOutputStream.write(buffer, 0, bl); bl = 0; if (rb < 0) { break; } } } finally { bufferedOutputStream.close(); } } finally { outputStream.close(); } } @Override protected OWriteAheadLog createWalFromIBUFiles(File directory) throws IOException { final OWriteAheadLog restoreWAL = new ODiskWriteAheadLog(OGlobalConfiguration.WAL_CACHE_SIZE.getValueAsInteger(), OGlobalConfiguration.WAL_COMMIT_TIMEOUT.getValueAsInteger(), ((long) OGlobalConfiguration.WAL_MAX_SEGMENT_SIZE.getValueAsInteger()) * ONE_KB * ONE_KB, directory.getAbsolutePath(), false, this, OGlobalConfiguration.WAL_FILE_AUTOCLOSE_INTERVAL.getValueAsInteger()); return restoreWAL; } @Override protected void preOpenSteps() throws IOException { if (configuration.binaryFormatVersion >= 11) { if (dirtyFlag.exists()) dirtyFlag.open(); else { dirtyFlag.create(); dirtyFlag.makeDirty(); } } else { if (dirtyFlag.exists()) dirtyFlag.open(); else { dirtyFlag.create(); dirtyFlag.clearDirty(); } } } @Override protected void preCreateSteps() throws IOException { dirtyFlag.create(); } @Override protected void postCloseSteps(boolean onDelete) throws IOException { if (onDelete) dirtyFlag.delete(); else { dirtyFlag.clearDirty(); dirtyFlag.close(); } } @Override protected void preCloseSteps() throws IOException { try { if (writeAheadLog != null) { checkpointExecutor.shutdown(); if (!checkpointExecutor .awaitTermination(OGlobalConfiguration.WAL_FULL_CHECKPOINT_SHUTDOWN_TIMEOUT.getValueAsInteger(), TimeUnit.SECONDS)) throw new OStorageException("Cannot terminate full checkpoint task"); } } catch (InterruptedException e) { Thread.interrupted(); throw OException.wrapException(new OStorageException("Error on closing of storage '" + name), e); } } @Override protected void postDeleteSteps() { File dbDir;// GET REAL DIRECTORY dbDir = new File(OIOUtils.getPathFromDatabaseName(OSystemVariableResolver.resolveSystemVariables(url))); if (!dbDir.exists() || !dbDir.isDirectory()) return; // RETRIES for (int i = 0; i < DELETE_MAX_RETRIES; ++i) { if (dbDir != null && dbDir.exists() && dbDir.isDirectory()) { int notDeletedFiles = 0; final File[] storageFiles = dbDir.listFiles(); if (storageFiles == null) continue; // TRY TO DELETE ALL THE FILES for (File f : storageFiles) { // DELETE ONLY THE SUPPORTED FILES for (String ext : ALL_FILE_EXTENSIONS) if (f.getPath().endsWith(ext)) { if (!f.delete()) { notDeletedFiles++; } break; } } if (notDeletedFiles == 0) { // TRY TO DELETE ALSO THE DIRECTORY IF IT'S EMPTY if (!dbDir.delete()) OLogManager.instance().error(this, "Cannot delete storage directory with path " + dbDir.getAbsolutePath() + " because directory is not empty. Files: " + Arrays.toString(dbDir.listFiles())); return; } } else return; OLogManager.instance().debug(this, "Cannot delete database files because they are still locked by the OrientDB process: waiting %d ms and retrying %d/%d...", DELETE_WAIT_TIME, i, DELETE_MAX_RETRIES); } throw new OStorageException("Cannot delete database '" + name + "' located in: " + dbDir + ". Database files seem locked"); } protected void makeStorageDirty() throws IOException { dirtyFlag.makeDirty(); } protected void clearStorageDirty() throws IOException { dirtyFlag.clearDirty(); } @Override protected boolean isDirty() throws IOException { return dirtyFlag.isDirty(); } @Override public boolean isIndexRebuildScheduled() { checkOpeness(); stateLock.acquireReadLock(); try { checkOpeness(); return dirtyFlag.isIndexRebuildScheduled(); } finally { stateLock.releaseReadLock(); } } @Override protected boolean isIndexRebuildScheduledInternal() { return dirtyFlag.isIndexRebuildScheduled(); } @Override protected void scheduleIndexRebuild() throws IOException { dirtyFlag.scheduleIndexRebuild(); } @Override public void cancelIndexRebuild() throws IOException { checkOpeness(); stateLock.acquireReadLock(); try { checkOpeness(); dirtyFlag.clearIndexRebuild(); } finally { stateLock.releaseReadLock(); } } @Override protected boolean isWriteAllowedDuringIncrementalBackup() { return true; } protected void initWalAndDiskCache() throws IOException { if (configuration.getContextConfiguration().getValueAsBoolean(OGlobalConfiguration.USE_WAL)) { checkpointExecutor = Executors.newSingleThreadExecutor(new FullCheckpointThreadFactory()); final ODiskWriteAheadLog diskWriteAheadLog = new ODiskWriteAheadLog(this); diskWriteAheadLog.addLowDiskSpaceListener(this); diskWriteAheadLog.checkFreeSpace(); writeAheadLog = diskWriteAheadLog; writeAheadLog.addFullCheckpointListener(this); } else writeAheadLog = null; long diskCacheSize = OGlobalConfiguration.DISK_CACHE_SIZE.getValueAsLong() * 1024 * 1024; long writeCacheSize = (long) Math .floor((((double) OGlobalConfiguration.DISK_WRITE_CACHE_PART.getValueAsInteger()) / 100.0) * diskCacheSize); final OWOWCache wowCache = new OWOWCache(false, OGlobalConfiguration.DISK_CACHE_PAGE_SIZE.getValueAsInteger() * ONE_KB, OByteBufferPool.instance(), OGlobalConfiguration.DISK_WRITE_CACHE_PAGE_TTL.getValueAsLong() * 1000, writeAheadLog, OGlobalConfiguration.DISK_WRITE_CACHE_PAGE_FLUSH_INTERVAL.getValueAsInteger(), writeCacheSize, diskCacheSize, this, true, files, getId()); wowCache.loadRegisteredFiles(); wowCache.addLowDiskSpaceListener(this); wowCache.addBackgroundExceptionListener(this); writeCache = wowCache; } public static boolean exists(final String path) { return new File(path + "/" + OMetadataDefault.CLUSTER_INTERNAL_NAME + OPaginatedCluster.DEF_EXTENSION).exists(); } private static class FullCheckpointThreadFactory implements ThreadFactory { @Override public Thread newThread(Runnable r) { Thread thread = new Thread(r); thread.setDaemon(true); return thread; } } }