/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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.apache.ignite.internal.processors.igfs; import java.io.OutputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.Deque; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.SortedSet; import java.util.TreeMap; import java.util.TreeSet; import java.util.concurrent.CountDownLatch; import javax.cache.processor.EntryProcessor; import javax.cache.processor.EntryProcessorResult; import org.apache.ignite.IgniteCheckedException; import org.apache.ignite.IgniteCompute; import org.apache.ignite.IgniteException; import org.apache.ignite.IgniteInterruptedException; import org.apache.ignite.IgniteLogger; import org.apache.ignite.cluster.ClusterGroup; import org.apache.ignite.cluster.ClusterNode; import org.apache.ignite.cluster.ClusterTopologyException; import org.apache.ignite.configuration.FileSystemConfiguration; import org.apache.ignite.events.EventType; import org.apache.ignite.events.IgfsEvent; import org.apache.ignite.igfs.IgfsConcurrentModificationException; import org.apache.ignite.igfs.IgfsDirectoryNotEmptyException; import org.apache.ignite.igfs.IgfsException; import org.apache.ignite.igfs.IgfsFile; import org.apache.ignite.igfs.IgfsParentNotDirectoryException; import org.apache.ignite.igfs.IgfsPath; import org.apache.ignite.igfs.IgfsPathAlreadyExistsException; import org.apache.ignite.igfs.IgfsPathIsDirectoryException; import org.apache.ignite.igfs.IgfsPathIsNotDirectoryException; import org.apache.ignite.igfs.IgfsPathNotFoundException; import org.apache.ignite.igfs.IgfsUserContext; import org.apache.ignite.igfs.secondary.IgfsSecondaryFileSystem; import org.apache.ignite.igfs.secondary.IgfsSecondaryFileSystemPositionedReadable; import org.apache.ignite.internal.IgniteEx; import org.apache.ignite.internal.IgniteInternalFuture; import org.apache.ignite.internal.IgniteInterruptedCheckedException; import org.apache.ignite.internal.managers.eventstorage.GridEventStorageManager; import org.apache.ignite.internal.processors.cache.GridCacheInternal; import org.apache.ignite.internal.processors.cache.IgniteInternalCache; import org.apache.ignite.internal.processors.cache.distributed.near.GridNearTxLocal; import org.apache.ignite.internal.processors.igfs.client.IgfsClientAbstractCallable; import org.apache.ignite.internal.processors.igfs.client.meta.IgfsClientMetaIdsForPathCallable; import org.apache.ignite.internal.processors.igfs.client.meta.IgfsClientMetaInfoForPathCallable; import org.apache.ignite.internal.processors.igfs.client.meta.IgfsClientMetaUnlockCallable; import org.apache.ignite.internal.processors.igfs.meta.IgfsMetaDirectoryCreateProcessor; import org.apache.ignite.internal.processors.igfs.meta.IgfsMetaDirectoryListingAddProcessor; import org.apache.ignite.internal.processors.igfs.meta.IgfsMetaDirectoryListingRemoveProcessor; import org.apache.ignite.internal.processors.igfs.meta.IgfsMetaDirectoryListingRenameProcessor; import org.apache.ignite.internal.processors.igfs.meta.IgfsMetaDirectoryListingReplaceProcessor; import org.apache.ignite.internal.processors.igfs.meta.IgfsMetaFileCreateProcessor; import org.apache.ignite.internal.processors.igfs.meta.IgfsMetaFileLockProcessor; import org.apache.ignite.internal.processors.igfs.meta.IgfsMetaFileReserveSpaceProcessor; import org.apache.ignite.internal.processors.igfs.meta.IgfsMetaFileUnlockProcessor; import org.apache.ignite.internal.processors.igfs.meta.IgfsMetaUpdatePropertiesProcessor; import org.apache.ignite.internal.processors.igfs.meta.IgfsMetaUpdateTimesProcessor; import org.apache.ignite.internal.util.GridLeanMap; import org.apache.ignite.internal.util.GridSpinBusyLock; import org.apache.ignite.internal.util.lang.GridClosureException; import org.apache.ignite.internal.util.lang.IgniteOutClosureX; import org.apache.ignite.internal.util.typedef.CI1; import org.apache.ignite.internal.util.typedef.F; import org.apache.ignite.internal.util.typedef.T1; import org.apache.ignite.internal.util.typedef.internal.U; import org.apache.ignite.lang.IgniteUuid; import org.apache.ignite.transactions.TransactionConcurrency; import org.apache.ignite.transactions.TransactionIsolation; import org.jetbrains.annotations.Nullable; import static org.apache.ignite.events.EventType.EVT_IGFS_DIR_RENAMED; import static org.apache.ignite.events.EventType.EVT_IGFS_FILE_RENAMED; /** * Cache based structure (meta data) manager. */ public class IgfsMetaManager extends IgfsManager { /** Comparator for Id sorting. */ private static final Comparator<IgniteUuid> PATH_ID_SORTING_COMPARATOR = new Comparator<IgniteUuid>() { @Override public int compare(IgniteUuid u1, IgniteUuid u2) { if (u1 == u2) return 0; if (u1 == null) return -1; return u1.compareTo(u2); } }; /** IGFS configuration. */ private FileSystemConfiguration cfg; /** Metadata cache. */ private IgniteInternalCache<Object, Object> metaCache; /** */ private CountDownLatch metaCacheStartLatch; /** File ID to file info projection. */ private IgniteInternalCache<IgniteUuid, IgfsEntryInfo> id2InfoPrj; /** Predefined key for sampling mode value. */ private GridCacheInternal sampling; /** Logger. */ private IgniteLogger log; /** Delete worker. */ private volatile IgfsDeleteWorker delWorker; /** Events manager. */ private GridEventStorageManager evts; /** Local node. */ private ClusterNode locNode; /** Busy lock. */ private final GridSpinBusyLock busyLock = new GridSpinBusyLock(); /** Relaxed flag. */ private final boolean relaxed; /** Client flag. */ private final boolean client; /** Compute facade for client tasks. */ private IgniteCompute cliCompute; /** Compute facade for client tasks. */ private String metaCacheName; /** * Constructor. * * @param relaxed Relaxed mode flag. * @param client Client flag. */ public IgfsMetaManager(boolean relaxed, boolean client) { this.relaxed = relaxed; this.client = client; } /** * Await initialization. */ void awaitInit() { try { metaCacheStartLatch.await(); } catch (InterruptedException e) { throw new IgniteInterruptedException(e); } } /** {@inheritDoc} */ @Override protected void start0() throws IgniteCheckedException { metaCacheStartLatch = new CountDownLatch(1); cfg = igfsCtx.configuration(); evts = igfsCtx.kernalContext().event(); sampling = new IgfsSamplingKey(cfg.getName()); log = igfsCtx.kernalContext().log(IgfsMetaManager.class); metaCacheName = cfg.getMetaCacheConfiguration().getName(); } /** {@inheritDoc} */ @SuppressWarnings("RedundantCast") @Override protected void onKernalStart0() throws IgniteCheckedException { metaCache = igfsCtx.kernalContext().cache().getOrStartCache(metaCacheName); assert metaCache != null; igfsCtx.kernalContext().cache().internalCache(metaCacheName).preloader().startFuture() .listen(new CI1<IgniteInternalFuture<Object>>() { @Override public void apply(IgniteInternalFuture<Object> f) { metaCacheStartLatch.countDown(); } }); id2InfoPrj = (IgniteInternalCache<IgniteUuid, IgfsEntryInfo>)metaCache.<IgniteUuid, IgfsEntryInfo>cache(); locNode = igfsCtx.kernalContext().discovery().localNode(); // Start background delete worker. if (!client) { delWorker = new IgfsDeleteWorker(igfsCtx); delWorker.start(); } } /** {@inheritDoc} */ @Override protected void onKernalStop0(boolean cancel) { IgfsDeleteWorker delWorker0 = delWorker; if (delWorker0 != null) { delWorker0.cancel(); try { U.join(delWorker0); } catch (IgniteInterruptedCheckedException ignored) { // No-op. } } busyLock.block(); } /** * @return Client flag. */ boolean isClient() { return client; } /** * Run client task. * * @param task Task. * @return Result. */ <T> T runClientTask(IgfsClientAbstractCallable<T> task) { try { return runClientTask(IgfsUtils.ROOT_ID, task); } catch (ClusterTopologyException e) { throw new IgfsException("Failed to execute operation because there are no IGFS metadata nodes." , e); } } /** * Run client task. * * @param affinityFileId Affinity fileId. * @param task Task. * @return Result. */ <T> T runClientTask(IgniteUuid affinityFileId, IgfsClientAbstractCallable<T> task) { try { return (cfg.isColocateMetadata()) ? clientCompute().affinityCall(metaCacheName, affinityFileId, task) : clientCompute().call(task); } catch (ClusterTopologyException e) { throw new IgfsException("Failed to execute operation because there are no IGFS metadata nodes." , e); } } /** * Get compute facade for client tasks. * * @return Compute facade. */ private IgniteCompute clientCompute() { assert client; IgniteCompute cliCompute0 = cliCompute; if (cliCompute0 == null) { IgniteEx ignite = igfsCtx.kernalContext().grid(); ClusterGroup cluster = ignite.cluster().forIgfsMetadataDataNodes(cfg.getName(), metaCacheName); cliCompute0 = ignite.compute(cluster); cliCompute = cliCompute0; } assert cliCompute0 != null; return cliCompute0; } /** * Gets file ID for specified path. * * @param path Path. * @return File ID for specified path or {@code null} if such file doesn't exist. * @throws IgniteCheckedException If failed. */ @Nullable public IgniteUuid fileId(IgfsPath path) throws IgniteCheckedException { if (busyLock.enterBusy()) { try { validTxState(false); return fileId(path, false); } finally { busyLock.leaveBusy(); } } else throw new IllegalStateException("Failed to get file ID because Grid is stopping: " + path); } /** * Gets file ID for specified path possibly skipping existing transaction. * * @param path Path. * @param skipTx Whether to skip existing transaction. * @return File ID for specified path or {@code null} if such file doesn't exist. * @throws IgniteCheckedException If failed. */ @Nullable private IgniteUuid fileId(IgfsPath path, boolean skipTx) throws IgniteCheckedException { List<IgniteUuid> ids = fileIds(path, skipTx); assert ids != null && !ids.isEmpty() : "Invalid file IDs [path=" + path + ", ids=" + ids + ']'; return ids.get(ids.size() - 1); } /** * Gets file ID by its name from parent directory listing. * * @param parentId Parent directory ID to get child ID for. * @param fileName File name in parent listing to get file ID for. * @return File ID. * @throws IgniteCheckedException If failed. */ @Nullable public IgniteUuid fileId(IgniteUuid parentId, String fileName) throws IgniteCheckedException { if (busyLock.enterBusy()) { try { return fileId(parentId, fileName, false); } finally { busyLock.leaveBusy(); } } else throw new IllegalStateException("Failed to get file ID because Grid is stopping [parentId=" + parentId + ", fileName=" + fileName + ']'); } /** * Gets file ID by its name from parent directory listing possibly skipping existing transaction. * * @param parentId Parent directory ID to get child ID for. * @param fileName File name in parent listing to get file ID for. * @param skipTx Whether to skip existing transaction. * @return File ID. * @throws IgniteCheckedException If failed. */ @Nullable private IgniteUuid fileId(IgniteUuid parentId, String fileName, boolean skipTx) throws IgniteCheckedException { IgfsListingEntry entry = directoryListing(parentId, skipTx).get(fileName); if (entry == null) { if (log.isDebugEnabled()) log.debug("Missing file ID [parentId=" + parentId + ", fileName=" + fileName + ']'); return null; } return entry.fileId(); } /** * Gets all file IDs for components of specified path. Result cannot be empty - there is at least root element. * But each element (except the first) can be {@code null} if such files don't exist. * * @param path Path. * @return Collection of file IDs for components of specified path. * @throws IgniteCheckedException If failed. */ public List<IgniteUuid> fileIds(IgfsPath path) throws IgniteCheckedException { if (busyLock.enterBusy()) { try { validTxState(false); return fileIds(path, false); } finally { busyLock.leaveBusy(); } } else throw new IllegalStateException("Failed to get file IDS because Grid is stopping: " + path); } /** * Gets all file IDs for components of specified path. Result cannot be empty - there is at least root element. * But each element (except the first) can be {@code null} if such files don't exist. * * @param path Path. * @return Collection of file IDs for components of specified path. * @throws IgniteCheckedException If failed. */ public IgfsPathIds pathIds(IgfsPath path) throws IgniteCheckedException { // Prepare parts. String[] components = path.componentsArray(); String[] parts = new String[components.length + 1]; System.arraycopy(components, 0, parts, 1, components.length); // Get IDs. if (client) { List<IgniteUuid> ids = runClientTask(new IgfsClientMetaIdsForPathCallable(cfg.getName(), IgfsUserContext.currentUser(), path)); return new IgfsPathIds(path, parts, ids.toArray(new IgniteUuid[ids.size()])); } else { if (busyLock.enterBusy()) { try { validTxState(false); IgniteUuid[] ids = new IgniteUuid[parts.length]; ids[0] = IgfsUtils.ROOT_ID; for (int i = 1; i < ids.length; i++) { IgniteUuid id = fileId(ids[i - 1], parts[i], false); if (id != null) ids[i] = id; else break; } // Return. return new IgfsPathIds(path, parts, ids); } finally { busyLock.leaveBusy(); } } else throw new IllegalStateException("Failed to get file IDS because Grid is stopping: " + path); } } /** * Gets all file IDs for components of specified path possibly skipping existing transaction. Result cannot * be empty - there is at least root element. But each element (except the first) can be {@code null} if such * files don't exist. * * @param path Path. * @param skipTx Whether to skip existing transaction. * @return Collection of file IDs for components of specified path. * @throws IgniteCheckedException If failed. */ private List<IgniteUuid> fileIds(IgfsPath path, boolean skipTx) throws IgniteCheckedException { assert path != null; // Path components. Collection<String> components = path.components(); // Collection of file IDs for components of specified path. List<IgniteUuid> ids = new ArrayList<>(components.size() + 1); ids.add(IgfsUtils.ROOT_ID); // Always add root ID. IgniteUuid fileId = IgfsUtils.ROOT_ID; for (String s : components) { assert !s.isEmpty(); if (fileId != null) fileId = fileId(fileId, s, skipTx); ids.add(fileId); } return ids; } /** * Ensure that entry with the given ID exists in meta cache. * * @param fileId File id. * @return {@code True} in case such entry exists. * @throws IgniteCheckedException IF failed. */ public boolean exists(IgniteUuid fileId) throws IgniteCheckedException{ if (busyLock.enterBusy()) { try { assert fileId != null; // containsKey() doesn't work here since meta cache can be PARTITIONED (we do not restrict if!). return info(fileId) != null; } finally { busyLock.leaveBusy(); } } else throw new IllegalStateException("Failed to check file system entry existence because Grid is stopping: " + fileId); } /** * Gets file info by its ID. * NB: this method is used both in Tx and out of Tx. * * @param fileId File ID to get details for. * @return File info. * @throws IgniteCheckedException If failed. */ @Nullable public IgfsEntryInfo info(@Nullable IgniteUuid fileId) throws IgniteCheckedException { if (busyLock.enterBusy()) { try { if (fileId == null) return null; IgfsEntryInfo info = getInfo(fileId); // Force root ID always exist in cache. if (info == null && IgfsUtils.ROOT_ID.equals(fileId)) info = createSystemDirectoryIfAbsent(fileId); return info; } finally { busyLock.leaveBusy(); } } else throw new IllegalStateException("Failed to get file info because Grid is stopping: " + fileId); } /** * Gets files details by their IDs. * * @param fileIds file IDs to get details for. * @return Files details. * @throws IgniteCheckedException If failed. */ public Map<IgniteUuid, IgfsEntryInfo> infos(Collection<IgniteUuid> fileIds) throws IgniteCheckedException { if (busyLock.enterBusy()) { try { validTxState(false); assert fileIds != null; if (F.isEmpty(fileIds)) return Collections.emptyMap(); Map<IgniteUuid, IgfsEntryInfo> map = getInfos(fileIds); // Force root ID always exist in cache. if (fileIds.contains(IgfsUtils.ROOT_ID) && !map.containsKey(IgfsUtils.ROOT_ID)) { map = new GridLeanMap<>(map); map.put(IgfsUtils.ROOT_ID, createSystemDirectoryIfAbsent(IgfsUtils.ROOT_ID)); } return map; } finally { busyLock.leaveBusy(); } } else throw new IllegalStateException("Failed to get file infos because Grid is stopping: " + fileIds); } /** * Lock the file explicitly outside of transaction. * * @param fileId File ID to lock. * @param del If file is being locked for delete. * @return Locked file info or {@code null} if file cannot be locked or doesn't exist. * @throws IgniteCheckedException If the file with such id does not exist, or on another failure. */ public @Nullable IgfsEntryInfo lock(IgniteUuid fileId, boolean del) throws IgniteCheckedException { if (busyLock.enterBusy()) { try { validTxState(false); assert fileId != null; try (GridNearTxLocal tx = startTx()) { // Lock file ID for this transaction. IgfsEntryInfo oldInfo = info(fileId); if (oldInfo == null) return null; if (oldInfo.lockId() != null) return null; // The file is already locked, we cannot lock it. IgfsEntryInfo newInfo = invokeLock(fileId, del); tx.commit(); return newInfo; } catch (GridClosureException e) { throw U.cast(e); } } finally { busyLock.leaveBusy(); } } else throw new IllegalStateException("Failed to obtain lock because Grid is stopping: " + fileId); } /** * Create file lock ID. * * @param del If lock ID is required for file deletion. * @return Lock ID. */ private IgniteUuid createFileLockId(boolean del) { if (del) return IgfsUtils.DELETE_LOCK_ID; return IgniteUuid.fromUuid(locNode.id()); } /** * Remove explicit lock on file held by the current stream. * * @param fileId File ID. * @param lockId Lock ID. * @param modificationTime Modification time to write to file info. * @throws IgniteCheckedException If failed. */ public void unlock(final IgniteUuid fileId, final IgniteUuid lockId, final long modificationTime) throws IgniteCheckedException { unlock(fileId, lockId, modificationTime, false, 0, null); } /** * Remove explicit lock on file held by the current stream. * * @param fileId File ID. * @param lockId Lock ID. * @param modificationTime Modification time to write to file info. * @param updateSpace Whether to update space. * @param space Space. * @param affRange Affinity range. * @throws IgniteCheckedException If failed. */ public void unlock(final IgniteUuid fileId, final IgniteUuid lockId, final long modificationTime, final boolean updateSpace, final long space, @Nullable final IgfsFileAffinityRange affRange) throws IgniteCheckedException { if(client) { runClientTask(new IgfsClientMetaUnlockCallable(cfg.getName(), IgfsUserContext.currentUser(), fileId, lockId, modificationTime, updateSpace, space, affRange)); return; } validTxState(false); if (busyLock.enterBusy()) { try { if (lockId == null) return; // Temporary clear interrupted state for unlocking. final boolean interrupted = Thread.interrupted(); try { IgfsUtils.doInTransactionWithRetries(id2InfoPrj, new IgniteOutClosureX<Void>() { @Override public Void applyx() throws IgniteCheckedException { validTxState(true); // Lock file ID for this transaction. IgfsEntryInfo oldInfo = info(fileId); if (oldInfo == null) throw fsException(new IgfsPathNotFoundException("Failed to unlock file (file not " + "found): " + fileId)); if (!F.eq(lockId, oldInfo.lockId())) throw new IgniteCheckedException("Failed to unlock file (inconsistent file lock ID) " + "[fileId=" + fileId + ", lockId=" + lockId + ", actualLockId=" + oldInfo.lockId() + ']'); id2InfoPrj.invoke(fileId, new IgfsMetaFileUnlockProcessor(modificationTime, updateSpace, space, affRange)); return null; } }); } finally { validTxState(false); if (interrupted) Thread.currentThread().interrupt(); } } finally { busyLock.leaveBusy(); } } else throw new IllegalStateException("Failed to unlock file system entry because Grid is stopping: " + fileId); } /** * Lock file IDs participating in the transaction.<br/> * * @param fileIds file IDs to lock. * @return Locked file details. Resulting map doesn't contain details for not-existent files. * @throws IgniteCheckedException If failed. */ private Map<IgniteUuid, IgfsEntryInfo> lockIds(IgniteUuid... fileIds) throws IgniteCheckedException { validTxState(true); assert fileIds != null && fileIds.length > 0; Arrays.sort(fileIds); return lockIds(Arrays.asList(fileIds)); } /** * Answers if the collection is sorted. * * @param col The collection to check. * @param <T> The type of the collection elements. * @return If the collection sorted. */ private static <T extends Comparable<T>> boolean isSorted(Collection<T> col) { T prev = null; for (T t: col) { if (t == null) throw new NullPointerException("Collections should not contain nulls"); if (prev != null && prev.compareTo(t) > 0) return false; // disordered. prev = t; } return true; } /** * Lock file IDs. * * @param fileIds File IDs (sorted). * @return Map with lock info. * @throws IgniteCheckedException If failed. */ private Map<IgniteUuid, IgfsEntryInfo> lockIds(Collection<IgniteUuid> fileIds) throws IgniteCheckedException { assert isSorted(fileIds); validTxState(true); if (log.isDebugEnabled()) log.debug("Locking file ids: " + fileIds); // Lock files and get their infos. Map<IgniteUuid, IgfsEntryInfo> map = getInfos(fileIds); if (log.isDebugEnabled()) log.debug("Locked file ids: " + fileIds); for (IgniteUuid fileId : fileIds) { if (IgfsUtils.isRootOrTrashId(fileId)) { if (!map.containsKey(fileId)) map.put(fileId, createSystemDirectoryIfAbsent(fileId)); } } // Returns detail's map for locked IDs. return map; } /** * create system entry if it is absent. * * @param id System entry ID. * @return Value of created or existing system entry. * @throws IgniteCheckedException On error. */ private IgfsEntryInfo createSystemDirectoryIfAbsent(IgniteUuid id) throws IgniteCheckedException { assert IgfsUtils.isRootOrTrashId(id); IgfsEntryInfo info = IgfsUtils.createDirectory(id); IgfsEntryInfo oldInfo = id2InfoPrj.getAndPutIfAbsent(id, info); if (oldInfo != null) info = oldInfo; return info; } /** * List child files for specified file ID. * * @param fileId File to list child files for. * @return Directory listing for the specified file. * @throws IgniteCheckedException If failed. */ public Map<String, IgfsListingEntry> directoryListing(IgniteUuid fileId) throws IgniteCheckedException { if (busyLock.enterBusy()) { try { return directoryListing(fileId, false); } finally { busyLock.leaveBusy(); } } else throw new IllegalStateException("Failed to get directory listing because Grid is stopping: " + fileId); } /** * Gets first available file info for fragmentizer. * * @param exclude File IDs to exclude from result. * @return First qualified file info. * @throws IgniteCheckedException If failed to get file for fragmentizer. */ public IgfsEntryInfo fileForFragmentizer(Collection<IgniteUuid> exclude) throws IgniteCheckedException { if (busyLock.enterBusy()) { try { return fileForFragmentizer0(IgfsUtils.ROOT_ID, exclude); } finally { busyLock.leaveBusy(); } } else throw new IllegalStateException("Failed to get file for framentizer because Grid is stopping."); } /** * Gets first available file info for fragmentizer. * * @param parentId Parent ID to scan. * @param exclude File IDs to exclude from result. * @return First qualified file info. * @throws IgniteCheckedException If failed to get file for fragmentizer. */ private IgfsEntryInfo fileForFragmentizer0(IgniteUuid parentId, Collection<IgniteUuid> exclude) throws IgniteCheckedException { IgfsEntryInfo info = info(parentId); // Check if file was concurrently deleted. if (info == null) return null; assert info.isDirectory(); Map<String, IgfsListingEntry> listing = info.listing(); for (IgfsListingEntry entry : listing.values()) { if (entry.isFile()) { IgfsEntryInfo fileInfo = info(entry.fileId()); if (fileInfo != null) { if (!exclude.contains(fileInfo.id()) && fileInfo.fileMap() != null && !fileInfo.fileMap().ranges().isEmpty()) return fileInfo; } } else { IgfsEntryInfo fileInfo = fileForFragmentizer0(entry.fileId(), exclude); if (fileInfo != null) return fileInfo; } } return null; } /** * List child files for specified file ID possibly skipping existing transaction. * * @param fileId File to list child files for. * @param skipTx Whether to skip existing transaction. * @return Directory listing for the specified file.* * @throws IgniteCheckedException If failed. */ private Map<String, IgfsListingEntry> directoryListing(IgniteUuid fileId, boolean skipTx) throws IgniteCheckedException { assert fileId != null; IgfsEntryInfo info = skipTx ? id2InfoPrj.getAllOutTx(Collections.singleton(fileId)).get(fileId) : getInfo(fileId); return info == null ? Collections.<String, IgfsListingEntry>emptyMap() : info.listing(); } /** * Add file into file system structure. Do not create new transaction expecting that the one already exists. * * @param parentId Parent file ID. * @param fileName File name in the parent's listing. * @param newFileInfo File info to store in the parent's listing. * @return File id already stored in meta cache or {@code null} if passed file info was stored. * @throws IgniteCheckedException If failed. */ private IgniteUuid putIfAbsentNonTx(IgniteUuid parentId, String fileName, IgfsEntryInfo newFileInfo) throws IgniteCheckedException { if (log.isDebugEnabled()) log.debug("Locking parent id [parentId=" + parentId + ", fileName=" + fileName + ", newFileInfo=" + newFileInfo + ']'); validTxState(true); // Lock only parent file ID. IgfsEntryInfo parentInfo = info(parentId); if (parentInfo == null) throw fsException(new IgfsPathNotFoundException("Failed to lock parent directory (not found): " + parentId)); if (!parentInfo.isDirectory()) throw fsException(new IgfsPathIsNotDirectoryException("Parent file is not a directory: " + parentInfo)); IgfsListingEntry childEntry = parentInfo.listing().get(fileName); if (childEntry != null) return childEntry.fileId(); createNewEntry(newFileInfo, parentId, fileName); return null; } /** * Move routine. * * @param srcPath Source path. * @param dstPath Destination path. * @throws IgniteCheckedException In case of exception. */ public void move(IgfsPath srcPath, IgfsPath dstPath) throws IgniteCheckedException { if (busyLock.enterBusy()) { try { validTxState(false); // Prepare path IDs. IgfsPathIds srcPathIds = pathIds(srcPath); IgfsPathIds dstPathIds = pathIds(dstPath); // Source path must exists. if (!srcPathIds.allExists()) throw new IgfsPathNotFoundException("Failed to perform move because source path is not " + "found: " + srcPath); // At this point we need to understand name of resulting entry. It will be either destination leaf // or source leaf depending on existence. String dstName; if (dstPathIds.lastExists()) // Full destination path exists -> use source name. dstName = srcPathIds.lastPart(); else { if (dstPathIds.lastParentExists()) { // Destination path doesn't exists -> use destination name. dstName = dstPathIds.lastPart(); dstPathIds = dstPathIds.parent(); } else // Destination parent is not found either -> exception. throw new IgfsPathNotFoundException("Failed to perform move because destination path is not " + "found: " + dstPath.parent()); } // Lock participating IDs. final Set<IgniteUuid> lockIds = new TreeSet<>(PATH_ID_SORTING_COMPARATOR); srcPathIds.addExistingIds(lockIds, relaxed); dstPathIds.addExistingIds(lockIds, relaxed); try (GridNearTxLocal tx = startTx()) { // Obtain the locks. final Map<IgniteUuid, IgfsEntryInfo> lockInfos = lockIds(lockIds); // Verify integrity of source and destination paths. if (!srcPathIds.verifyIntegrity(lockInfos, relaxed)) throw new IgfsPathNotFoundException("Failed to perform move because source directory " + "structure changed concurrently [src=" + srcPath + ", dst=" + dstPath + ']'); if (!dstPathIds.verifyIntegrity(lockInfos, relaxed)) throw new IgfsPathNotFoundException("Failed to perform move because destination directory " + "structure changed concurrently [src=" + srcPath + ", dst=" + dstPath + ']'); // Addiional check: is destination directory? IgfsEntryInfo dstParentInfo = lockInfos.get(dstPathIds.lastId()); if (dstParentInfo.isFile()) throw new IgfsPathAlreadyExistsException("Failed to perform move because destination points " + "to existing file [src=" + srcPath + ", dst=" + dstPath + ']'); // Additional check: does destination already has child with the same name? if (dstParentInfo.hasChild(dstName)) throw new IgfsPathAlreadyExistsException("Failed to perform move because destination already " + "contains entry with the same name existing file [src=" + srcPath + ", dst=" + dstPath + ']'); // Actual move: remove from source parent and add to destination target. IgfsEntryInfo srcParentInfo = lockInfos.get(srcPathIds.lastParentId()); IgfsEntryInfo srcInfo = lockInfos.get(srcPathIds.lastId()); String srcName = srcPathIds.lastPart(); IgfsListingEntry srcEntry = srcParentInfo.listing().get(srcName); transferEntry(srcEntry, srcParentInfo.id(), srcName, dstParentInfo.id(), dstName); tx.commit(); // Fire events. IgfsPath newPath = new IgfsPath(dstPathIds.path(), dstName); IgfsUtils.sendEvents(igfsCtx.kernalContext(), srcPath, newPath, srcInfo.isFile() ? EVT_IGFS_FILE_RENAMED : EVT_IGFS_DIR_RENAMED); } } finally { busyLock.leaveBusy(); } } else throw new IllegalStateException("Failed to perform move because Grid is stopping [srcPath=" + srcPath + ", dstPath=" + dstPath + ']'); } /** * Move or rename file in existing transaction. * * @param fileId File ID to move or rename. * @param srcFileName Original file name in the parent's listing. * @param srcParentId Parent directory ID. * @param destFileName New file name in the parent's listing after moving. * @param destParentId New parent directory ID. * @throws IgniteCheckedException If failed. */ private void moveNonTx(IgniteUuid fileId, String srcFileName, IgniteUuid srcParentId, String destFileName, IgniteUuid destParentId) throws IgniteCheckedException { validTxState(true); assert fileId != null; assert srcFileName != null; assert srcParentId != null; assert destFileName != null; assert destParentId != null; if (srcParentId.equals(destParentId) && srcFileName.equals(destFileName)) { if (log.isDebugEnabled()) log.debug("File is moved to itself [fileId=" + fileId + ", fileName=" + srcFileName + ", parentId=" + srcParentId + ']'); return; // File is moved to itself. } // Lock file ID and parent IDs for this transaction. Map<IgniteUuid, IgfsEntryInfo> infoMap = lockIds(srcParentId, fileId, destParentId); IgfsEntryInfo srcInfo = infoMap.get(srcParentId); if (srcInfo == null) throw fsException(new IgfsPathNotFoundException("Failed to lock source directory (not found?)" + " [srcParentId=" + srcParentId + ']')); if (!srcInfo.isDirectory()) throw fsException(new IgfsPathIsNotDirectoryException("Source is not a directory: " + srcInfo)); IgfsEntryInfo destInfo = infoMap.get(destParentId); if (destInfo == null) throw fsException(new IgfsPathNotFoundException("Failed to lock destination directory (not found?)" + " [destParentId=" + destParentId + ']')); if (!destInfo.isDirectory()) throw fsException(new IgfsPathIsNotDirectoryException("Destination is not a directory: " + destInfo)); IgfsEntryInfo fileInfo = infoMap.get(fileId); if (fileInfo == null) throw fsException(new IgfsPathNotFoundException("Failed to lock target file (not found?) [fileId=" + fileId + ']')); IgfsListingEntry srcEntry = srcInfo.listing().get(srcFileName); // If source file does not exist or was re-created. if (srcEntry == null || !srcEntry.fileId().equals(fileId)) throw fsException(new IgfsPathNotFoundException("Failed to remove file name from the source directory" + " (file not found) [fileId=" + fileId + ", srcFileName=" + srcFileName + ", srcParentId=" + srcParentId + ", srcEntry=" + srcEntry + ']')); // If stored file already exist. if (destInfo.hasChild(destFileName)) throw fsException(new IgfsPathAlreadyExistsException("Failed to add file name into the destination " + " directory (file already exists) [fileId=" + fileId + ", destFileName=" + destFileName + ", destParentId=" + destParentId + ']')); transferEntry(srcEntry, srcParentId, srcFileName, destParentId, destFileName); } /** * Deletes (moves to TRASH) all elements under the root folder. * * @return The new Id if the artificially created folder containing all former root * elements moved to TRASH folder. * @throws IgniteCheckedException On error. */ @SuppressWarnings("RedundantCast") IgniteUuid format() throws IgniteCheckedException { if (busyLock.enterBusy()) { try { validTxState(false); IgniteUuid trashId = IgfsUtils.randomTrashId(); try (GridNearTxLocal tx = startTx()) { // NB: We may lock root because its id is less than any other id: final IgfsEntryInfo rootInfo = lockIds(IgfsUtils.ROOT_ID, trashId).get(IgfsUtils.ROOT_ID); assert rootInfo != null; Map<String, IgfsListingEntry> rootListingMap = rootInfo.listing(); assert rootListingMap != null; if (rootListingMap.isEmpty()) return null; // Root is empty, nothing to do. // Construct new info and move locked entries from root to it. Map<String, IgfsListingEntry> transferListing = new HashMap<>(rootListingMap); IgfsEntryInfo newInfo = IgfsUtils.createDirectory( IgniteUuid.randomUuid(), transferListing, (Map<String, String>) null ); createNewEntry(newInfo, trashId, newInfo.id().toString()); // Remove listing entries from root. // Note that root directory properties and other attributes are preserved: id2InfoPrj.put(IgfsUtils.ROOT_ID, rootInfo.listing(null)); tx.commit(); signalDeleteWorker(); return newInfo.id(); } } finally { busyLock.leaveBusy(); } } else throw new IllegalStateException("Failed to perform format because Grid is stopping."); } /** * Whether operation must be re-tried because we have suspicious links which may broke secondary file system * consistency. * * @param pathIds Path IDs. * @param lockInfos Lock infos. * @return Whether to re-try. */ private static boolean isRetryForSecondary(IgfsPathIds pathIds, Map<IgniteUuid, IgfsEntryInfo> lockInfos) { // We need to ensure that the last locked info is not linked with expected child. // Otherwise there was some concurrent file system update and we have to re-try. // That is, the following situation lead to re-try: // 1) We queried path /A/B/C // 2) Returned IDs are ROOT_ID, A_ID, B_ID, null // 3) But B's info contains C as child. It mean's that if (!pathIds.allExists()) { // Find the last locked index IgfsEntryInfo lastLockedInfo = null; int lastLockedIdx = -1; while (lastLockedIdx < pathIds.lastExistingIndex()) { IgfsEntryInfo nextInfo = lockInfos.get(pathIds.id(lastLockedIdx + 1)); if (nextInfo != null) { lastLockedInfo = nextInfo; lastLockedIdx++; } else break; } assert lastLockedIdx < pathIds.count(); if (lastLockedInfo != null) { String part = pathIds.part(lastLockedIdx + 1); if (lastLockedInfo.listing().containsKey(part)) return true; } } return false; } /** * Move path to the trash directory. * * @param path Path. * @param recursive Recursive flag. * @param secondaryFs Secondary file system (optional). * @return ID of an entry located directly under the trash directory. * @throws IgniteCheckedException If failed. */ IgfsDeleteResult softDelete(final IgfsPath path, final boolean recursive, @Nullable IgfsSecondaryFileSystem secondaryFs) throws IgniteCheckedException { while (true) { if (busyLock.enterBusy()) { try { validTxState(false); IgfsPathIds pathIds = pathIds(path); if (!pathIds.allExists() && secondaryFs == null) return new IgfsDeleteResult(false, null); IgniteUuid victimId = pathIds.lastId(); String victimName = pathIds.lastPart(); if (IgfsUtils.isRootId(victimId)) throw new IgfsException("Cannot remove root directory"); // Prepare IDs to lock. SortedSet<IgniteUuid> allIds = new TreeSet<>(PATH_ID_SORTING_COMPARATOR); pathIds.addExistingIds(allIds, relaxed); IgniteUuid trashId = IgfsUtils.randomTrashId(); allIds.add(trashId); try (GridNearTxLocal tx = startTx()) { // Lock participants. Map<IgniteUuid, IgfsEntryInfo> lockInfos = lockIds(allIds); if (secondaryFs != null && isRetryForSecondary(pathIds, lockInfos)) continue; // Ensure that all participants are still in place. if (!pathIds.allExists() || !pathIds.verifyIntegrity(lockInfos, relaxed)) { // For DUAL mode we will try to update the underlying FS still. Note we do that inside TX. if (secondaryFs != null) { boolean res = secondaryFs.delete(path, recursive); return new IgfsDeleteResult(res, null); } else return new IgfsDeleteResult(false, null); } IgfsEntryInfo victimInfo = lockInfos.get(victimId); // Cannot delete non-empty directory if recursive flag is not set. if (!recursive && victimInfo.hasChildren()) throw new IgfsDirectoryNotEmptyException("Failed to remove directory (directory is not " + "empty and recursive flag is not set)."); // Prepare trash data. IgfsEntryInfo trashInfo = lockInfos.get(trashId); final String trashName = IgfsUtils.composeNameForTrash(path, victimId); assert !trashInfo.hasChild(trashName) : "Failed to add file name into the " + "destination directory (file already exists) [destName=" + trashName + ']'; IgniteUuid parentId = pathIds.lastParentId(); IgfsEntryInfo parentInfo = lockInfos.get(parentId); // Propagate call to the secondary file system. if (secondaryFs != null && !secondaryFs.delete(path, recursive)) return new IgfsDeleteResult(false, null); transferEntry(parentInfo.listing().get(victimName), parentId, victimName, trashId, trashName); tx.commit(); signalDeleteWorker(); return new IgfsDeleteResult(true, victimInfo); } } finally { busyLock.leaveBusy(); } } else throw new IllegalStateException("Failed to perform soft delete because Grid is " + "stopping [path=" + path + ']'); } } /** * Remove listing entries of the given parent. * This operation actually deletes directories from TRASH, is used solely by IgfsDeleteWorker. * * @param parentId Parent ID. * @param listing Listing entries. * @return Collection of really deleted entries. * @throws IgniteCheckedException If failed. */ Collection<IgniteUuid> delete(IgniteUuid parentId, Map<String, IgfsListingEntry> listing) throws IgniteCheckedException { if (busyLock.enterBusy()) { try { assert parentId != null; assert listing != null; validTxState(false); try (GridNearTxLocal tx = startTx()) { Collection<IgniteUuid> res = new HashSet<>(); // Obtain all necessary locks in one hop. IgniteUuid[] allIds = new IgniteUuid[listing.size() + 1]; allIds[0] = parentId; int i = 1; for (IgfsListingEntry childEntry : listing.values()) allIds[i++] = childEntry.fileId(); Map<IgniteUuid, IgfsEntryInfo> locks = lockIds(allIds); IgfsEntryInfo parentInfo = locks.get(parentId); // Ensure parent is still in place. if (parentInfo != null) { Map<String, IgfsListingEntry> parentListing = parentInfo.listing(); Map<String, IgfsListingEntry> newListing = new HashMap<>(parentListing.size(), 1.0f); newListing.putAll(parentListing); // Remove child entries if possible. for (Map.Entry<String, IgfsListingEntry> entry : listing.entrySet()) { String childName = entry.getKey(); IgniteUuid childId = entry.getValue().fileId(); IgfsEntryInfo entryInfo = locks.get(childId); if (entryInfo != null) { // File must be locked for deletion: assert entryInfo.isDirectory() || IgfsUtils.DELETE_LOCK_ID.equals(entryInfo.lockId()); // Delete only files or empty folders. if (!entryInfo.hasChildren()) { id2InfoPrj.remove(childId); newListing.remove(childName); res.add(childId); } } else { // Entry was deleted concurrently. newListing.remove(childName); res.add(childId); } } // Update parent listing. id2InfoPrj.put(parentId, parentInfo.listing(newListing)); } tx.commit(); return res; } } finally { busyLock.leaveBusy(); } } else throw new IllegalStateException("Failed to perform delete because Grid is stopping [parentId=" + parentId + ", listing=" + listing + ']'); } /** * Remove entry from the metadata listing. * Used solely by IgfsDeleteWorker. * * @param parentId Parent ID. * @param name Name. * @param id ID. * @return {@code True} in case the entry really was removed from the cache by this call. * @throws IgniteCheckedException If failed. */ boolean delete(IgniteUuid parentId, String name, IgniteUuid id) throws IgniteCheckedException { if (busyLock.enterBusy()) { try { validTxState(false); try (GridNearTxLocal tx = startTx()) { Map<IgniteUuid, IgfsEntryInfo> infos = lockIds(parentId, id); IgfsEntryInfo victim = infos.get(id); if (victim == null) return false; assert victim.isDirectory() || IgfsUtils.DELETE_LOCK_ID.equals(victim.lockId()) : " isDir: " + victim.isDirectory() + ", lockId: " + victim.lockId(); // Proceed only in case both parent and child exist. if (infos.containsKey(parentId) && infos.containsKey(id)) { IgfsEntryInfo parentInfo = infos.get(parentId); assert parentInfo != null; IgfsListingEntry childEntry = parentInfo.listing().get(name); if (childEntry != null) id2InfoPrj.invoke(parentId, new IgfsMetaDirectoryListingRemoveProcessor(name, id)); id2InfoPrj.remove(id); tx.commit(); return true; } return false; } } finally { busyLock.leaveBusy(); } } else throw new IllegalStateException("Failed to perform delete because Grid is stopping [parentId=" + parentId + ", name=" + name + ", id=" + id + ']'); } /** * Update file info (file properties) in cache in existing transaction. * * @param fileId File ID to update information for. * @param props Properties to set for the file. * @return Updated file info or {@code null} if such file ID not found. * @throws IgniteCheckedException If operation failed. */ @Nullable private IgfsEntryInfo updatePropertiesNonTx(final IgniteUuid fileId, Map<String, String> props) throws IgniteCheckedException { assert fileId != null; assert !F.isEmpty(props) : "Expects not-empty file's properties"; validTxState(true); if (log.isDebugEnabled()) log.debug("Update file properties [fileId=" + fileId + ", props=" + props + ']'); try { final IgfsEntryInfo oldInfo = info(fileId); if (oldInfo == null) return null; return invokeAndGet(fileId, new IgfsMetaUpdatePropertiesProcessor(props)); } catch (GridClosureException e) { throw U.cast(e); } } /** * Update file info (file properties) in cache. * * @param fileId File ID to update information for. * @param props Properties to set for the file. * @return Updated file info or {@code null} if such file ID not found. * @throws IgniteCheckedException If operation failed. */ @Nullable public IgfsEntryInfo updateProperties(IgniteUuid fileId, Map<String, String> props) throws IgniteCheckedException { if (busyLock.enterBusy()) { try { validTxState(false); try (GridNearTxLocal tx = startTx()) { IgfsEntryInfo info = updatePropertiesNonTx(fileId, props); tx.commit(); return info; } } finally { busyLock.leaveBusy(); } } else throw new IllegalStateException("Failed to update properties because Grid is stopping [fileId=" + fileId + ", props=" + props + ']'); } /** * Reserve space for file. * * @param fileId File ID. * @param space Space. * @param affRange Affinity range. * @return New file info. */ public IgfsEntryInfo reserveSpace(IgniteUuid fileId, long space, IgfsFileAffinityRange affRange) throws IgniteCheckedException { validTxState(false); if (busyLock.enterBusy()) { try { if (log.isDebugEnabled()) log.debug("Reserve file space: " + fileId); try (GridNearTxLocal tx = startTx()) { // Lock file ID for this transaction. IgfsEntryInfo oldInfo = info(fileId); if (oldInfo == null) throw fsException("File has been deleted concurrently: " + fileId); IgfsEntryInfo newInfo = invokeAndGet(fileId, new IgfsMetaFileReserveSpaceProcessor(space, affRange)); tx.commit(); return newInfo; } catch (GridClosureException e) { throw U.cast(e); } } finally { busyLock.leaveBusy(); } } else throw new IllegalStateException("Failed to reserve file space because Grid is stopping:" + fileId); } /** * Update file info in cache. * * @param fileId File ID to update information for. * @param proc Entry processor to invoke. * @return Updated file info or {@code null} if such file ID not found. * @throws IgniteCheckedException If failed. */ @Nullable public IgfsEntryInfo updateInfo(IgniteUuid fileId, EntryProcessor<IgniteUuid, IgfsEntryInfo, IgfsEntryInfo> proc) throws IgniteCheckedException { validTxState(false); assert fileId != null; assert proc != null; if (busyLock.enterBusy()) { try { if (log.isDebugEnabled()) log.debug("Update file info [fileId=" + fileId + ", proc=" + proc + ']'); try (GridNearTxLocal tx = startTx()) { // Lock file ID for this transaction. IgfsEntryInfo oldInfo = info(fileId); if (oldInfo == null) return null; // File not found. IgfsEntryInfo newInfo = invokeAndGet(fileId, proc); if (newInfo == null) throw fsException("Failed to update file info with null value" + " [oldInfo=" + oldInfo + ", newInfo=null, proc=" + proc + ']'); if (!oldInfo.id().equals(newInfo.id())) throw fsException("Failed to update file info (file IDs differ)" + " [oldInfo=" + oldInfo + ", newInfo=" + newInfo + ", proc=" + proc + ']'); if (oldInfo.isDirectory() != newInfo.isDirectory()) throw fsException("Failed to update file info (file types differ)" + " [oldInfo=" + oldInfo + ", newInfo=" + newInfo + ", proc=" + proc + ']'); tx.commit(); return newInfo; } catch (GridClosureException e) { throw U.cast(e); } } finally { busyLock.leaveBusy(); } } else throw new IllegalStateException("Failed to update file system entry info because Grid is stopping: " + fileId); } /** * Mkdirs implementation. * * @param path The path to create. * @param props The properties to use for created directories. * @return True if a directory was created during the operation. * @throws IgniteCheckedException If a non-directory file exists on the requested path, and in case of other errors. */ boolean mkdirs(final IgfsPath path, final Map<String, String> props) throws IgniteCheckedException { validTxState(false); while (true) { if (busyLock.enterBusy()) { try { // Prepare path IDs. IgfsPathIds pathIds = pathIds(path); // Prepare lock IDs. Essentially, they consist of two parts: existing IDs and potential new IDs. Set<IgniteUuid> lockIds = new TreeSet<>(PATH_ID_SORTING_COMPARATOR); pathIds.addExistingIds(lockIds, relaxed); pathIds.addSurrogateIds(lockIds); // Start TX. try (GridNearTxLocal tx = startTx()) { final Map<IgniteUuid, IgfsEntryInfo> lockInfos = lockIds(lockIds); if (!pathIds.verifyIntegrity(lockInfos, relaxed)) // Directory structure changed concurrently. So we simply re-try. continue; // Check if the whole structure is already in place. if (pathIds.allExists()) { if (lockInfos.get(pathIds.lastExistingId()).isDirectory()) return false; else throw new IgfsParentNotDirectoryException("Failed to create directory (parent " + "element is not a directory)"); } IgfsPathsCreateResult res = createDirectory(pathIds, lockInfos, props); if (res == null) continue; // Commit TX. tx.commit(); generateCreateEvents(res.createdPaths(), false); // We are done. return true; } } finally { busyLock.leaveBusy(); } } else throw new IllegalStateException("Failed to mkdir because Grid is stopping. [path=" + path + ']'); } } /** * Set sampling flag. * * @param val Sampling flag state or {@code null} to clear sampling state and mark it as "not set". * @return {@code True} if sampling mode was actually changed by this call. * @throws IgniteCheckedException If failed. */ public boolean sampling(Boolean val) throws IgniteCheckedException { if (busyLock.enterBusy()) { try { validTxState(false); try (GridNearTxLocal tx = startTx()) { Object prev = val != null ? metaCache.getAndPut(sampling, val) : metaCache.getAndRemove(sampling); tx.commit(); return !F.eq(prev, val); } } finally { busyLock.leaveBusy(); } } else throw new IllegalStateException("Failed to set sampling flag because Grid is stopping."); } /** * Get sampling flag state. * * @return {@code True} in case sampling is enabled, {@code false} otherwise or {@code null} in case sampling * is not set. * @throws IgniteCheckedException If failed. */ public Boolean sampling() throws IgniteCheckedException { if (busyLock.enterBusy()) { try { validTxState(false); Object val = metaCache.get(sampling); return (val == null || !(val instanceof Boolean)) ? null : (Boolean)val; } finally { busyLock.leaveBusy(); } } else throw new IllegalStateException("Failed to get sampling flag because Grid is stopping."); } /** * Put new entry to meta cache immediately linking it to parent. * * @param info Info to put. * @param parentId Parent ID. * @param name Name in parent. * @throws IgniteCheckedException If failed. */ private void createNewEntry(IgfsEntryInfo info, IgniteUuid parentId, String name) throws IgniteCheckedException { validTxState(true); if (!id2InfoPrj.putIfAbsent(info.id(), info)) throw fsException("Failed to create new metadata entry due to ID conflict: " + info.id()); if (parentId != null) id2InfoPrj.invoke(parentId, new IgfsMetaDirectoryListingAddProcessor(name, new IgfsListingEntry(info))); } /** * Transfer entry from one directory to another. * * @param entry Entry to be transferred. * @param srcId Source ID. * @param srcName Source name. * @param destId Destination ID. * @param destName Destination name. * @throws IgniteCheckedException If failed. */ private void transferEntry(IgfsListingEntry entry, IgniteUuid srcId, String srcName, IgniteUuid destId, String destName) throws IgniteCheckedException { validTxState(true); if (F.eq(srcId, destId)) id2InfoPrj.invoke(srcId, new IgfsMetaDirectoryListingRenameProcessor(srcName, destName)); else { Map<IgniteUuid, EntryProcessor<IgniteUuid, IgfsEntryInfo, Void>> procMap = new HashMap<>(); procMap.put(srcId, new IgfsMetaDirectoryListingRemoveProcessor(srcName, entry.fileId())); procMap.put(destId, new IgfsMetaDirectoryListingAddProcessor(destName, entry)); id2InfoPrj.invokeAll(procMap); } } /** * Invoke lock processor. * * @param id File ID. * @param del Whether lock is taken for delete. * @return Resulting file info. * @throws IgniteCheckedException If failed. */ private IgfsEntryInfo invokeLock(IgniteUuid id, boolean del) throws IgniteCheckedException { return invokeAndGet(id, new IgfsMetaFileLockProcessor(createFileLockId(del))); } /** * Invoke some processor and return new value. * * @param id ID. * @param proc Processor. * @return New file info. * @throws IgniteCheckedException If failed. */ private IgfsEntryInfo invokeAndGet(IgniteUuid id, EntryProcessor<IgniteUuid, IgfsEntryInfo, IgfsEntryInfo> proc) throws IgniteCheckedException { validTxState(true); EntryProcessorResult<IgfsEntryInfo> res = id2InfoPrj.invoke(id, proc); assert res != null; return res.get(); } /** * Get info. * * @param id ID. * @return Info. * @throws IgniteCheckedException If failed. */ @Nullable private IgfsEntryInfo getInfo(IgniteUuid id) throws IgniteCheckedException { return id2InfoPrj.get(id); } /** * Get several infos. * * @param ids IDs. * @return Infos map. * @throws IgniteCheckedException If failed. */ private Map<IgniteUuid, IgfsEntryInfo> getInfos(Collection<IgniteUuid> ids) throws IgniteCheckedException { return id2InfoPrj.getAll(ids); } /** * A delegate method that performs file creation in the synchronization task. * * @param fs File system. * @param path Path. * @param simpleCreate "Simple create" flag. * @param props Properties.. * @param overwrite Overwrite flag. * @param bufSize Buffer size. * @param replication Replication factor. * @param blockSize Block size. * @param affKey Affinity key. * @param infos Map from paths to corresponding infos. * @param pendingEvts A non-null collection the events are to be accumulated in. * @param t1 A signle-object tuple to hold the created output stream. * @return Output stream descriptor. * @throws Exception On error. */ IgfsCreateResult onSuccessCreate(IgfsSecondaryFileSystem fs, IgfsPath path, boolean simpleCreate, @Nullable final Map<String, String> props, boolean overwrite, int bufSize, short replication, long blockSize, IgniteUuid affKey, Map<IgfsPath, IgfsEntryInfo> infos, final Deque<IgfsEvent> pendingEvts, final T1<OutputStream> t1) throws Exception { validTxState(true); assert !infos.isEmpty(); // Determine the first existing parent. IgfsPath parentPath = null; for (IgfsPath curPath : infos.keySet()) { if (parentPath == null || curPath.isSubDirectoryOf(parentPath)) parentPath = curPath; } assert parentPath != null; IgfsEntryInfo parentInfo = infos.get(parentPath); // Delegate to the secondary file system. OutputStream out = simpleCreate ? fs.create(path, overwrite) : fs.create(path, bufSize, overwrite, replication, blockSize, props); t1.set(out); IgfsPath parent0 = path.parent(); assert parent0 != null : "path.parent() is null (are we creating ROOT?): " + path; // If some of the parent directories were missing, synchronize again. if (!parentPath.equals(parent0)) { parentInfo = synchronize(fs, parentPath, parentInfo, parent0, true, null); // Fire notification about missing directories creation. if (evts.isRecordable(EventType.EVT_IGFS_DIR_CREATED)) { IgfsPath evtPath = parent0; while (!parentPath.equals(evtPath)) { pendingEvts.addFirst(new IgfsEvent(evtPath, locNode, EventType.EVT_IGFS_DIR_CREATED)); evtPath = evtPath.parent(); assert evtPath != null; // If this fails, then ROOT does not exist. } } } // Get created file info. IgfsFile status = fs.info(path); if (status == null) throw fsException("Failed to open output stream to the file created in " + "the secondary file system because it no longer exists: " + path); else if (status.isDirectory()) throw fsException("Failed to open output stream to the file created in " + "the secondary file system because the path points to a directory: " + path); IgfsEntryInfo newInfo = IgfsUtils.createFile( IgniteUuid.randomUuid(), igfsCtx.configuration().getBlockSize(), status.length(), affKey, createFileLockId(false), igfsCtx.igfs().evictExclude(path, false), status.properties(), status.accessTime(), status.modificationTime() ); // Add new file info to the listing optionally removing the previous one. assert parentInfo != null; IgniteUuid oldId = putIfAbsentNonTx(parentInfo.id(), path.name(), newInfo); if (oldId != null) { IgfsEntryInfo oldInfo = info(oldId); assert oldInfo != null; // Otherwise cache is in inconsistent state. // The contact is that we cannot overwrite a file locked for writing: if (oldInfo.lockId() != null) throw fsException("Failed to overwrite file (file is opened for writing) [path=" + path + ", fileId=" + oldId + ", lockId=" + oldInfo.lockId() + ']'); id2InfoPrj.remove(oldId); // Remove the old one. id2InfoPrj.invoke(parentInfo.id(), new IgfsMetaDirectoryListingRemoveProcessor( path.name(), parentInfo.listing().get(path.name()).fileId())); createNewEntry(newInfo, parentInfo.id(), path.name()); // Put new one. igfsCtx.data().delete(oldInfo); } // Record CREATE event if needed. if (oldId == null && evts.isRecordable(EventType.EVT_IGFS_FILE_CREATED)) pendingEvts.add(new IgfsEvent(path, locNode, EventType.EVT_IGFS_FILE_CREATED)); return new IgfsCreateResult(newInfo, out); } /** * Append to a file in DUAL mode. * * @param fs File system. * @param path Path. * @param bufSize Buffer size. * @param create Create flag. * @return Output stream descriptor. * @throws IgniteCheckedException If output stream open for append has failed. */ public IgfsCreateResult appendDual(final IgfsSecondaryFileSystem fs, final IgfsPath path, final int bufSize, final boolean create) throws IgniteCheckedException { if (busyLock.enterBusy()) { try { assert fs != null; assert path != null; // Events to fire (can be done outside of a transaction). final Deque<IgfsEvent> pendingEvts = new LinkedList<>(); SynchronizationTask<IgfsCreateResult> task = new SynchronizationTask<IgfsCreateResult>() { /** Container for the secondary file system output stream. */ private final T1<OutputStream> outT1 = new T1<>(null); @Override public IgfsCreateResult onSuccess(Map<IgfsPath, IgfsEntryInfo> infos) throws Exception { validTxState(true); final IgfsEntryInfo info = infos.get(path); final IgfsEntryInfo lockedInfo; if (info == null) return onSuccessCreate(fs, path, true/*simpleCreate*/, null, false/*overwrite*/, bufSize, (short)0, 0, null, infos, pendingEvts, outT1); else { if (info.isDirectory()) throw fsException("Failed to open output stream to the file in the " + "secondary file system because the path points to a directory: " + path); outT1.set(fs.append(path, bufSize, false, null)); // Synchronize file ending. long len = info.length(); int blockSize = info.blockSize(); int remainder = (int) (len % blockSize); if (remainder > 0) { int blockIdx = (int) (len / blockSize); try (IgfsSecondaryFileSystemPositionedReadable reader = fs.open(path, bufSize)) { IgniteInternalFuture<byte[]> fut = igfsCtx.data().dataBlock(info, path, blockIdx, reader); assert fut != null; fut.get(); } } if (info.lockId() != null) { throw fsException("Failed to open file (file is opened for writing) [path=" + path + ", fileId=" + info.id() + ", lockId=" + info.lockId() + ']'); } // Set lock and return. lockedInfo = invokeLock(info.id(), false); } if (evts.isRecordable(EventType.EVT_IGFS_FILE_OPENED_WRITE)) pendingEvts.add(new IgfsEvent(path, locNode, EventType.EVT_IGFS_FILE_OPENED_WRITE)); return new IgfsCreateResult(lockedInfo, outT1.get()); } @Override public IgfsCreateResult onFailure(@Nullable Exception err) throws IgniteCheckedException { U.closeQuiet(outT1.get()); U.error(log, "File append in DUAL mode failed [path=" + path + ", bufferSize=" + bufSize + ']', err); throw new IgniteCheckedException("Failed to append to the file due to secondary file " + "system exception: " + path, err); } }; try { return synchronizeAndExecute(task, fs, !create/*strict*/, path); } finally { for (IgfsEvent evt : pendingEvts) evts.record(evt); } } finally { busyLock.leaveBusy(); } } else throw new IllegalStateException("Failed to append to file in DUAL mode because Grid is stopping: " + path); } /** * Get info for the given path. * * @param path Path. * @return Info. * @throws IgniteCheckedException If failed. */ @Nullable public IgfsEntryInfo infoForPath(IgfsPath path) throws IgniteCheckedException { return client ? runClientTask(new IgfsClientMetaInfoForPathCallable(cfg.getName(), IgfsUserContext.currentUser(), path)) : info(fileId(path)); } /** * Get IDs for the given path. * * @param path Path. * @return IDs. * @throws IgniteCheckedException If failed. */ public List<IgniteUuid> idsForPath(IgfsPath path) throws IgniteCheckedException { return client ? runClientTask(new IgfsClientMetaIdsForPathCallable(cfg.getName(), IgfsUserContext.currentUser(), path)) : fileIds(path); } /** * Open file in DUAL mode. * * @param fs Secondary file system. * @param path Path to open. * @param bufSize Buffer size. * @return Input stream descriptor. * @throws IgniteCheckedException If input stream open has failed. */ public IgfsSecondaryInputStreamDescriptor openDual(final IgfsSecondaryFileSystem fs, final IgfsPath path, final int bufSize) throws IgniteCheckedException { if (busyLock.enterBusy()) { try { assert fs != null; assert path != null; // First, try getting file info without any transactions and synchronization. IgfsEntryInfo info = infoForPath(path); if (info != null) { if (!info.isFile()) throw fsException(new IgfsPathIsDirectoryException("Failed to open file (not a file): " + path)); return new IgfsSecondaryInputStreamDescriptor(info, lazySecondaryReader(fs, path, bufSize)); } // If failed, try synchronize. SynchronizationTask<IgfsSecondaryInputStreamDescriptor> task = new SynchronizationTask<IgfsSecondaryInputStreamDescriptor>() { @Override public IgfsSecondaryInputStreamDescriptor onSuccess( Map<IgfsPath, IgfsEntryInfo> infos) throws Exception { IgfsEntryInfo info = infos.get(path); if (info == null) throw fsException(new IgfsPathNotFoundException("File not found: " + path)); if (!info.isFile()) throw fsException(new IgfsPathIsDirectoryException("Failed to open file " + "(not a file): " + path)); return new IgfsSecondaryInputStreamDescriptor(infos.get(path), lazySecondaryReader(fs, path, bufSize)); } @Override public IgfsSecondaryInputStreamDescriptor onFailure(@Nullable Exception err) throws IgniteCheckedException { U.error(log, "File open in DUAL mode failed [path=" + path + ", bufferSize=" + bufSize + ']', err); throw new IgniteCheckedException("Failed to open the path due to secondary file system " + "exception: " + path, err); } }; return synchronizeAndExecute(task, fs, false, path); } finally { busyLock.leaveBusy(); } } else throw new IllegalStateException("Failed to open file in DUAL mode because Grid is stopping: " + path); } /** * Create lazy secondary file system reader. * * @param fs File system. * @param path Path. * @param bufSize Buffer size. * @return Lazy reader. */ private static IgfsLazySecondaryFileSystemPositionedReadable lazySecondaryReader(IgfsSecondaryFileSystem fs, IgfsPath path, int bufSize) { return new IgfsLazySecondaryFileSystemPositionedReadable(fs, path, bufSize); } /** * Synchronizes with secondary file system. * * @param fs File system. * @param path Path. * @return File info or {@code null} if file not found. * @throws IgniteCheckedException If sync task failed. */ @Nullable public IgfsEntryInfo synchronizeFileDual(final IgfsSecondaryFileSystem fs, final IgfsPath path) throws IgniteCheckedException { assert fs != null; assert path != null; if (busyLock.enterBusy()) { try { // First, try getting file info without any transactions and synchronization. IgfsEntryInfo info = infoForPath(path); if (info != null) return info; // If failed, try synchronize. SynchronizationTask<IgfsEntryInfo> task = new SynchronizationTask<IgfsEntryInfo>() { @Override public IgfsEntryInfo onSuccess(Map<IgfsPath, IgfsEntryInfo> infos) throws Exception { return infos.get(path); } @Override public IgfsEntryInfo onFailure(@Nullable Exception err) throws IgniteCheckedException { throw new IgniteCheckedException("Failed to synchronize path due to secondary file " + "system exception: " + path, err); } }; return synchronizeAndExecute(task, fs, false, path); } finally { busyLock.leaveBusy(); } } else throw new IllegalStateException("Failed to synchronize file because Grid is stopping: " + path); } /** * Create directory in DUAL mode. * * @param fs Secondary file system. * @param path Path to create. * @param props Properties to be applied. * @return {@code True} in case rename was successful. * @throws IgniteCheckedException If directory creation failed. */ public boolean mkdirsDual(final IgfsSecondaryFileSystem fs, final IgfsPath path, final Map<String, String> props) throws IgniteCheckedException { if (busyLock.enterBusy()) { try { assert fs != null; assert path != null; if (path.parent() == null) return true; // No additional handling for root directory is needed. // Events to fire (can be done outside of a transaction). final Deque<IgfsEvent> pendingEvts = new LinkedList<>(); SynchronizationTask<Boolean> task = new SynchronizationTask<Boolean>() { @Override public Boolean onSuccess(Map<IgfsPath, IgfsEntryInfo> infos) throws Exception { fs.mkdirs(path, props); assert !infos.isEmpty(); // Now perform synchronization again starting with the last created parent. IgfsPath parentPath = null; for (IgfsPath curPath : infos.keySet()) { if (parentPath == null || curPath.isSubDirectoryOf(parentPath)) parentPath = curPath; } assert parentPath != null; IgfsEntryInfo parentPathInfo = infos.get(parentPath); synchronize(fs, parentPath, parentPathInfo, path, true, null); if (evts.isRecordable(EventType.EVT_IGFS_DIR_CREATED)) { IgfsPath evtPath = path; while (!parentPath.equals(evtPath)) { pendingEvts.addFirst(new IgfsEvent(evtPath, locNode, EventType.EVT_IGFS_DIR_CREATED)); evtPath = evtPath.parent(); assert evtPath != null; // If this fails, then ROOT does not exist. } } return true; } @Override public Boolean onFailure(@Nullable Exception err) throws IgniteCheckedException { U.error(log, "Directory creation in DUAL mode failed [path=" + path + ", properties=" + props + ']', err); throw new IgniteCheckedException("Failed to create the path due to secondary file system " + "exception: " + path, err); } }; try { return synchronizeAndExecute(task, fs, false, path.parent()); } finally { for (IgfsEvent evt : pendingEvts) evts.record(evt); } } finally { busyLock.leaveBusy(); } } else throw new IllegalStateException("Failed to create directory in DUAL mode because Grid is stopping: " + path); } /** * Rename path in DUAL mode. * * @param fs Secondary file system. * @param src Source path. * @param dest Destination path. * @return Operation result. * @throws IgniteCheckedException If failed. */ public boolean renameDual(final IgfsSecondaryFileSystem fs, final IgfsPath src, final IgfsPath dest) throws IgniteCheckedException { if (busyLock.enterBusy()) { try { assert fs != null; assert src != null; assert dest != null; if (src.parent() == null) return false; // Root directory cannot be renamed. // Events to fire (can be done outside of a transaction). final Collection<IgfsEvent> pendingEvts = new LinkedList<>(); SynchronizationTask<Boolean> task = new SynchronizationTask<Boolean>() { @Override public Boolean onSuccess(Map<IgfsPath, IgfsEntryInfo> infos) throws Exception { IgfsEntryInfo srcInfo = infos.get(src); IgfsEntryInfo srcParentInfo = infos.get(src.parent()); IgfsEntryInfo destInfo = infos.get(dest); IgfsEntryInfo destParentInfo = dest.parent() != null ? infos.get(dest.parent()) : null; // Source path and destination (or destination parent) must exist. if (srcInfo == null) throw fsException(new IgfsPathNotFoundException("Failed to rename " + "(source path not found): " + src)); if (destInfo == null && destParentInfo == null) throw fsException(new IgfsPathNotFoundException("Failed to rename " + "(destination path not found): " + dest)); // Delegate to the secondary file system. fs.rename(src, dest); // Rename was successful, perform compensation in the local file system. if (destInfo == null) moveNonTx(srcInfo.id(), src.name(), srcParentInfo.id(), dest.name(), destParentInfo.id()); else { // Move. if (destInfo.isFile()) throw fsException("Failed to rename the path in the local file system " + "because destination path already exists and it is a file: " + dest); else moveNonTx(srcInfo.id(), src.name(), srcParentInfo.id(), src.name(), destInfo.id()); } // Record event if needed. if (srcInfo.isFile()) { if (evts.isRecordable(EventType.EVT_IGFS_FILE_RENAMED)) pendingEvts.add(new IgfsEvent( src, destInfo == null ? dest : new IgfsPath(dest, src.name()), locNode, EventType.EVT_IGFS_FILE_RENAMED)); } else if (evts.isRecordable(EventType.EVT_IGFS_DIR_RENAMED)) pendingEvts.add(new IgfsEvent(src, dest, locNode, EventType.EVT_IGFS_DIR_RENAMED)); return true; } @Override public Boolean onFailure(@Nullable Exception err) throws IgniteCheckedException { U.error(log, "Path rename in DUAL mode failed [source=" + src + ", destination=" + dest + ']', err); throw new IgniteCheckedException("Failed to rename the path due to secondary file system " + "exception: " + src, err); } }; try { return synchronizeAndExecute(task, fs, false, src, dest); } finally { for (IgfsEvent evt : pendingEvts) evts.record(evt); } } finally { busyLock.leaveBusy(); } } else throw new IllegalStateException("Failed to rename in DUAL mode because Grid is stopping [src=" + src + ", dest=" + dest + ']'); } /** * Update path in DUAL mode. * * @param fs Secondary file system. * @param path Path to update. * @param props Properties to be applied. * @return Update file info. * @throws IgniteCheckedException If update failed. */ public IgfsEntryInfo updateDual(final IgfsSecondaryFileSystem fs, final IgfsPath path, final Map<String, String> props) throws IgniteCheckedException { assert fs != null; assert path != null; assert props != null && !props.isEmpty(); if (busyLock.enterBusy()) { try { SynchronizationTask<IgfsEntryInfo> task = new SynchronizationTask<IgfsEntryInfo>() { @Override public IgfsEntryInfo onSuccess(Map<IgfsPath, IgfsEntryInfo> infos) throws Exception { if (infos.get(path) == null) return null; fs.update(path, props); return updatePropertiesNonTx(infos.get(path).id(), props); } @Override public IgfsEntryInfo onFailure(@Nullable Exception err) throws IgniteCheckedException { U.error(log, "Path update in DUAL mode failed [path=" + path + ", properties=" + props + ']', err); throw new IgniteCheckedException("Failed to update the path due to secondary file system " + "exception: " + path, err); } }; return synchronizeAndExecute(task, fs, false, path); } finally { busyLock.leaveBusy(); } } else throw new IllegalStateException("Failed to update in DUAL mode because Grid is stopping: " + path); } /** * Synchronize directory structure with the secondary file system. * * @param fs Secondary file system. * @param startPath Start path. * @param startPathInfo Start path info. * @param endPath End path. * @param strict Whether all paths must exist in the secondary file system. * @param created Optional map where data about all created values is put. * @return File info of the end path. * @throws IgniteCheckedException If failed. */ private IgfsEntryInfo synchronize(IgfsSecondaryFileSystem fs, IgfsPath startPath, IgfsEntryInfo startPathInfo, IgfsPath endPath, boolean strict, @Nullable Map<IgfsPath, IgfsEntryInfo> created) throws IgniteCheckedException { assert fs != null; assert startPath != null && startPathInfo != null && endPath != null; validTxState(true); IgfsEntryInfo parentInfo = startPathInfo; List<String> components = endPath.components(); IgfsPath curPath = startPath; for (int i = startPath.components().size(); i < components.size(); i++) { curPath = new IgfsPath(curPath, components.get(i)); if (created != null && created.containsKey(curPath)) // Re-use already created info. parentInfo = created.get(curPath); else { // Get file status from the secondary file system. IgfsFile status; try { status = fs.info(curPath); } catch (IgniteException e) { throw new IgniteCheckedException("Failed to get path information: " + e, e); } if (status != null) { if (!status.isDirectory() && !curPath.equals(endPath)) throw new IgniteCheckedException("Failed to create path the locally because secondary file " + "system directory structure was modified concurrently and the path is not a directory as " + "expected: " + curPath); } else { if (strict) { throw new IgniteCheckedException("Failed to create path locally due to secondary file system " + "exception: " + curPath); } else if (created != null) created.put(curPath.parent(), parentInfo); return null; } // Recreate the path locally. IgfsEntryInfo curInfo = status.isDirectory() ? IgfsUtils.createDirectory( IgniteUuid.randomUuid(), null, status.properties(), status.accessTime(), status.modificationTime() ) : IgfsUtils.createFile( IgniteUuid.randomUuid(), igfsCtx.configuration().getBlockSize(), status.length(), null, null, igfsCtx.igfs().evictExclude(curPath, false), status.properties(), status.accessTime(), status.modificationTime() ); assert parentInfo != null; IgniteUuid oldId = putIfAbsentNonTx(parentInfo.id(), components.get(i), curInfo); if (oldId != null) curInfo = info(oldId); if (created != null) created.put(curPath, curInfo); parentInfo = curInfo; } } return parentInfo; } /** * Synchronize file system structure and then execute provided task. All these actions are performed withing * the transaction. * * @param task Task to execute. * @param fs File system. * @param strict Whether paths must be re-created strictly. * @param paths Paths to synchronize. * @return Result of task execution. * @throws IgniteCheckedException If failed. */ private <T> T synchronizeAndExecute(SynchronizationTask<T> task, IgfsSecondaryFileSystem fs, boolean strict, IgfsPath... paths) throws IgniteCheckedException { return synchronizeAndExecute(task, fs, strict, null, paths); } /** * Synchronize file system structure and then execute provided task. All these actions are performed withing * the transaction. * * @param task Task to execute. * @param fs File system. * @param strict Whether paths must be re-created strictly. * @param extraLockIds Additional IDs to lock (optional). * @param paths Paths to synchronize. * @return Result of task execution. * @throws IgniteCheckedException If failed. */ @SuppressWarnings({"Contract", "ConstantConditions"}) private <T> T synchronizeAndExecute(SynchronizationTask<T> task, IgfsSecondaryFileSystem fs, boolean strict, @Nullable Collection<IgniteUuid> extraLockIds, IgfsPath... paths) throws IgniteCheckedException { assert task != null; assert fs != null; assert paths != null && paths.length > 0; // Sort paths so that we know in which order to synchronize them. if (paths.length > 1) Arrays.sort(paths); boolean finished = false; T res = null; while (!finished) { // Obtain existing IDs outside the transaction. List<List<IgniteUuid>> pathIds = new ArrayList<>(paths.length); for (IgfsPath path : paths) pathIds.add(idsForPath(path)); // Start pessimistic. try (GridNearTxLocal tx = startTx()) { // Lock the very first existing parents and possibly the leaf as well. Map<IgfsPath, IgfsPath> pathToParent = new HashMap<>(); Map<IgfsPath, IgniteUuid> pathToId = new HashMap<>(); for (int i = 0; i < paths.length; i++) { IgfsPath path = paths[i]; // Determine the very first existing parent List<IgniteUuid> ids = pathIds.get(i); if (ids.size() > 1) { // The path is not root. IgfsPath parentPath = path.parent(); IgniteUuid parentId = ids.get(ids.size() - 2); for (int j = ids.size() - 3; j >= 0; j--) { if (parentId != null) break; else { parentPath = parentPath.parent(); parentId = ids.get(j); } } assert parentPath != null && parentId != null; pathToParent.put(path, parentPath); pathToId.put(parentPath, parentId); } IgniteUuid pathId = ids.get(ids.size() - 1); if (pathId != null) pathToId.put(path, pathId); } IgniteUuid[] lockArr = new IgniteUuid[extraLockIds == null ? pathToId.size() : pathToId.size() + extraLockIds.size()]; int idx = 0; for (IgniteUuid id : pathToId.values()) lockArr[idx++] = id; if (extraLockIds != null) { for (IgniteUuid id : extraLockIds) lockArr[idx++] = id; } Map<IgniteUuid, IgfsEntryInfo> idToInfo = lockIds(lockArr); if (extraLockIds != null) { for (IgniteUuid id : extraLockIds) idToInfo.remove(id); } // Ensure that locked IDs still point to expected paths. IgfsPath changed = null; for (Map.Entry<IgfsPath, IgniteUuid> entry : pathToId.entrySet()) { if (!idToInfo.containsKey(entry.getValue()) || !F.eq(entry.getValue(), fileId(entry.getKey(), true))) { changed = entry.getKey(); break; } } if (changed != null) { finished = true; throw fsException(new IgfsConcurrentModificationException("File system entry has been " + "modified concurrently: " + changed)); } else { boolean newParents = false; // Check whether any new parents appeared before we have obtained the locks. for (int i = 0; i < paths.length; i++) { List<IgniteUuid> newIds = fileIds(paths[i], true); if (!pathIds.get(i).equals(newIds)) { newParents = true; break; } } if (newParents) continue; // Release all locks and try again. else { // Perform synchronization. Map<IgfsPath, IgfsEntryInfo> infos = new HashMap<>(); TreeMap<IgfsPath, IgfsEntryInfo> created = new TreeMap<>(); for (IgfsPath path : paths) { IgfsPath parentPath = path.parent(); if (pathToId.containsKey(path)) { infos.put(path, info(pathToId.get(path))); if (parentPath != null) infos.put(parentPath, info(pathToId.get(parentPath))); } else { IgfsPath firstParentPath = pathToParent.get(path); assert firstParentPath != null; assert pathToId.get(firstParentPath) != null; IgfsEntryInfo info = synchronize(fs, firstParentPath, idToInfo.get(pathToId.get(firstParentPath)), path, strict, created); assert strict && info != null || !strict; if (info != null) infos.put(path, info); if (parentPath != null) { if (parentPath.equals(firstParentPath)) infos.put(firstParentPath, idToInfo.get(pathToId.get(firstParentPath))); else { assert strict && created.get(parentPath) != null || !strict; if (created.get(parentPath) != null) infos.put(parentPath, created.get(parentPath)); else { // Put the last created path. infos.put(created.lastKey(), created.get(created.lastKey())); } } } } } // Finally, execute the task. finished = true; try { res = task.onSuccess(infos); } catch (Exception e) { res = task.onFailure(e); } } } tx.commit(); } catch (IgniteCheckedException e) { if (!finished) { finished = true; res = task.onFailure(e); } else throw e; } } return res; } /** * Check transaction is (not) started. * * @param inTx Expected transaction state. */ private void validTxState(boolean inTx) { assert (inTx && id2InfoPrj.tx() != null) || (!inTx && id2InfoPrj.tx() == null) : "Invalid TX state [expected=" + inTx + ", actual=" + (id2InfoPrj.tx() != null) + ']'; } /** * Start transaction on meta cache. * * @return Transaction. */ private GridNearTxLocal startTx() { return metaCache.txStartEx(TransactionConcurrency.PESSIMISTIC, TransactionIsolation.REPEATABLE_READ); } /** * Update times. * * @param path Path. * @param accessTime Access time. * @param modificationTime Modification time. * @param secondaryFs Secondary file system. * @throws IgniteCheckedException If failed. */ public void updateTimes(IgfsPath path, long modificationTime, long accessTime, IgfsSecondaryFileSystem secondaryFs) throws IgniteCheckedException { while (true) { if (busyLock.enterBusy()) { try { validTxState(false); // Prepare path IDs. IgfsPathIds pathIds = pathIds(path); // Prepare lock IDs. Set<IgniteUuid> lockIds = new TreeSet<>(PATH_ID_SORTING_COMPARATOR); pathIds.addExistingIds(lockIds, relaxed); // Start TX. try (GridNearTxLocal tx = startTx()) { Map<IgniteUuid, IgfsEntryInfo> lockInfos = lockIds(lockIds); if (secondaryFs != null && isRetryForSecondary(pathIds, lockInfos)) continue; if (!pathIds.verifyIntegrity(lockInfos, relaxed)) // Directory structure changed concurrently. So we re-try. continue; if (pathIds.allExists()) { // All files are in place. Update both primary and secondary file systems. if (secondaryFs != null) secondaryFs.setTimes(path, modificationTime, accessTime); IgniteUuid targetId = pathIds.lastExistingId(); IgfsEntryInfo targetInfo = lockInfos.get(targetId); id2InfoPrj.invoke(targetId, new IgfsMetaUpdateTimesProcessor( accessTime == -1 ? targetInfo.accessTime() : accessTime, modificationTime == -1 ? targetInfo.modificationTime() : modificationTime) ); tx.commit(); return; } else { // Propagate call to the secondary FS, as we might haven't cache this part yet. if (secondaryFs != null) { secondaryFs.setTimes(path, modificationTime, accessTime); return; } else throw new IgfsPathNotFoundException("Failed to update times (path not found): " + path); } } } catch (IgniteException | IgniteCheckedException e) { throw e; } catch (Exception e) { throw new IgniteCheckedException("setTimes failed due to unexpected exception: " + path, e); } finally { busyLock.leaveBusy(); } } else throw new IllegalStateException("Failed to update times because Grid is stopping: " + path); } } /** * @param msg Error message. * @return Checked exception. */ private static IgniteCheckedException fsException(String msg) { return new IgniteCheckedException(new IgfsException(msg)); } /** * @param err Unchecked exception. * @return Checked exception. */ private static IgniteCheckedException fsException(IgfsException err) { return new IgniteCheckedException(err); } /** * Append routine. * * @param path Path. * @param dirProps Directory properties. * @param create Create flag. * @param blockSize Block size. * @param affKey Affinity key. * @param evictExclude Evict exclude flag. * @param fileProps File properties. * @return Resulting info. * @throws IgniteCheckedException If failed. */ IgfsEntryInfo append( final IgfsPath path, Map<String, String> dirProps, final boolean create, final int blockSize, final @Nullable IgniteUuid affKey, final boolean evictExclude, @Nullable Map<String, String> fileProps) throws IgniteCheckedException { validTxState(false); while (true) { if (busyLock.enterBusy()) { try { // Prepare path IDs. IgfsPathIds pathIds = pathIds(path); // Fail-fast: create flag is not specified and some paths are missing. if (!pathIds.allExists() && !create) throw new IgfsPathNotFoundException("Failed to append because file is not found: " + path); // Prepare lock IDs. Set<IgniteUuid> lockIds = new TreeSet<>(PATH_ID_SORTING_COMPARATOR); pathIds.addExistingIds(lockIds, relaxed); pathIds.addSurrogateIds(lockIds); // Start TX. try (GridNearTxLocal tx = startTx()) { Map<IgniteUuid, IgfsEntryInfo> lockInfos = lockIds(lockIds); if (!pathIds.verifyIntegrity(lockInfos, relaxed)) // Directory structure changed concurrently. So we simply re-try. continue; if (pathIds.allExists()) { // All participants are found. Simply open the stream. IgfsEntryInfo info = lockInfos.get(pathIds.lastId()); // Check: is it a file? if (!info.isFile()) throw new IgfsPathIsDirectoryException("Failed to open file for write." + path); // Check if file already opened for write. if (info.lockId() != null) throw new IgfsException("File is already opened for write: " + path); // At this point we can open the stream safely. info = invokeLock(info.id(), false); tx.commit(); IgfsUtils.sendEvents(igfsCtx.kernalContext(), path, EventType.EVT_IGFS_FILE_OPENED_WRITE); return info; } else { // Create file and parent folders. IgfsPathsCreateResult res = createFile(pathIds, lockInfos, dirProps, fileProps, blockSize, affKey, evictExclude, null, null); if (res == null) continue; // Commit. tx.commit(); // Generate events. generateCreateEvents(res.createdPaths(), true); return res.info(); } } } finally { busyLock.leaveBusy(); } } else throw new IllegalStateException("Failed to append for file because Grid is stopping:" + path); } } /** * Create a file. * * @param path Path. * @param dirProps Directory properties. * @param overwrite Overwrite flag. * @param blockSize Block size. * @param affKey Affinity key. * @param evictExclude Evict exclude flag. * @param fileProps File properties. * @param secondaryCtx Secondary file system create context. * @return @return Operation result. * @throws IgniteCheckedException If failed. */ IgfsCreateResult create( final IgfsPath path, Map<String, String> dirProps, final boolean overwrite, final int blockSize, final @Nullable IgniteUuid affKey, final boolean evictExclude, @Nullable Map<String, String> fileProps, @Nullable IgfsSecondaryFileSystemCreateContext secondaryCtx) throws IgniteCheckedException { validTxState(false); while (true) { if (busyLock.enterBusy()) { OutputStream secondaryOut = null; try { // Prepare path IDs. IgfsPathIds pathIds = pathIds(path); // Prepare lock IDs. Set<IgniteUuid> lockIds = new TreeSet<>(PATH_ID_SORTING_COMPARATOR); pathIds.addExistingIds(lockIds, relaxed); pathIds.addSurrogateIds(lockIds); // In overwrite mode we also lock ID of potential replacement as well as trash ID. IgniteUuid overwriteId = IgniteUuid.randomUuid(); IgniteUuid trashId = IgfsUtils.randomTrashId(); if (overwrite) { lockIds.add(overwriteId); // Trash ID is only added if we suspect conflict. if (pathIds.allExists()) lockIds.add(trashId); } // Start TX. try (GridNearTxLocal tx = startTx()) { Map<IgniteUuid, IgfsEntryInfo> lockInfos = lockIds(lockIds); if (secondaryCtx != null && isRetryForSecondary(pathIds, lockInfos)) continue; if (!pathIds.verifyIntegrity(lockInfos, relaxed)) // Directory structure changed concurrently. So we simply re-try. continue; if (pathIds.allExists()) { // All participants found. IgfsEntryInfo oldInfo = lockInfos.get(pathIds.lastId()); // Check: is it a file? if (!oldInfo.isFile()) throw new IgfsPathIsDirectoryException("Failed to create a file: " + path); // Check: can we overwrite it? if (!overwrite) throw new IgfsPathAlreadyExistsException("Failed to create a file: " + path); // Check if file already opened for write. if (oldInfo.lockId() != null) throw new IgfsException("File is already opened for write: " + path); // At this point file can be re-created safely. // Add existing to trash listing. IgniteUuid oldId = pathIds.lastId(); id2InfoPrj.invoke(trashId, new IgfsMetaDirectoryListingAddProcessor( IgfsUtils.composeNameForTrash(path, oldId), new IgfsListingEntry(oldInfo))); // Replace ID in parent directory. String name = pathIds.lastPart(); IgniteUuid parentId = pathIds.lastParentId(); id2InfoPrj.invoke(parentId, new IgfsMetaDirectoryListingReplaceProcessor(name, overwriteId)); // Create the file. IgniteUuid newLockId = createFileLockId(false); long newAccessTime; long newModificationTime; Map<String, String> newProps; long newLen; int newBlockSize; if (secondaryCtx != null) { secondaryOut = secondaryCtx.create(); newAccessTime = 0L; newModificationTime = 0L; newProps = null; } else { newAccessTime = System.currentTimeMillis(); newModificationTime = newAccessTime; newProps = fileProps; } newLen = 0L; newBlockSize = blockSize; IgfsEntryInfo newInfo = invokeAndGet(overwriteId, new IgfsMetaFileCreateProcessor(newAccessTime, newModificationTime, newProps, newBlockSize, affKey, newLockId, evictExclude, newLen)); // Prepare result and commit. tx.commit(); IgfsUtils.sendEvents(igfsCtx.kernalContext(), path, EventType.EVT_IGFS_FILE_OPENED_WRITE); return new IgfsCreateResult(newInfo, secondaryOut); } else { // Create file and parent folders. T1<OutputStream> secondaryOutHolder = null; if (secondaryCtx != null) secondaryOutHolder = new T1<>(); IgfsPathsCreateResult res; try { res = createFile(pathIds, lockInfos, dirProps, fileProps, blockSize, affKey, evictExclude, secondaryCtx, secondaryOutHolder); } finally { if (secondaryOutHolder != null) secondaryOut = secondaryOutHolder.get(); } if (res == null) continue; // Commit. tx.commit(); // Generate events. generateCreateEvents(res.createdPaths(), true); return new IgfsCreateResult(res.info(), secondaryOut); } } } catch (IgniteException | IgniteCheckedException e) { U.closeQuiet(secondaryOut); throw e; } catch (Exception e) { U.closeQuiet(secondaryOut); throw new IgniteCheckedException("Create failed due to unexpected exception: " + path, e); } finally { busyLock.leaveBusy(); } } else throw new IllegalStateException("Failed to mkdir because Grid is stopping. [path=" + path + ']'); } } /** * Create directory and it's parents. * * @param pathIds Path IDs. * @param lockInfos Lock infos. * @param dirProps Directory properties. * @return Result or {@code} if the first parent already contained child with the same name. * @throws IgniteCheckedException If failed. */ @Nullable IgfsPathsCreateResult createDirectory(IgfsPathIds pathIds, Map<IgniteUuid, IgfsEntryInfo> lockInfos, Map<String, String> dirProps) throws IgniteCheckedException { // Check if entry we are going to write to is directory. if (lockInfos.get(pathIds.lastExistingId()).isFile()) throw new IgfsParentNotDirectoryException("Failed to create directory (parent " + "element is not a directory)"); return createFileOrDirectory(true, pathIds, lockInfos, dirProps, null, 0, null, false, null, null); } /** * Create file and all it's parents. * * @param pathIds Paths IDs. * @param lockInfos Lock infos. * @param dirProps Directory properties. * @param fileProps File propertris. * @param blockSize Block size. * @param affKey Affinity key (optional) * @param evictExclude Evict exclude flag. * @param secondaryCtx Secondary file system create context. * @param secondaryOutHolder Holder for the secondary output stream. * @return Result or {@code} if the first parent already contained child with the same name. * @throws IgniteCheckedException If failed. */ @Nullable private IgfsPathsCreateResult createFile(IgfsPathIds pathIds, Map<IgniteUuid, IgfsEntryInfo> lockInfos, Map<String, String> dirProps, Map<String, String> fileProps, int blockSize, @Nullable IgniteUuid affKey, boolean evictExclude, @Nullable IgfsSecondaryFileSystemCreateContext secondaryCtx, @Nullable T1<OutputStream> secondaryOutHolder) throws IgniteCheckedException{ // Check if entry we are going to write to is directory. if (lockInfos.get(pathIds.lastExistingId()).isFile()) throw new IgfsParentNotDirectoryException("Failed to open file for write " + "(parent element is not a directory): " + pathIds.path()); return createFileOrDirectory(false, pathIds, lockInfos, dirProps, fileProps, blockSize, affKey, evictExclude, secondaryCtx, secondaryOutHolder); } /** * Create file or directory. * * @param dir Directory flag. * @param pathIds Path IDs. * @param lockInfos Lock infos. * @param dirProps Directory properties. * @param fileProps File properties. * @param blockSize Block size. * @param affKey Affinity key. * @param evictExclude Evict exclude flag. * @param secondaryCtx Secondary file system create context. * @param secondaryOutHolder Secondary output stream holder. * @return Result. * @throws IgniteCheckedException If failed. */ @SuppressWarnings("unchecked") private IgfsPathsCreateResult createFileOrDirectory(boolean dir, IgfsPathIds pathIds, Map<IgniteUuid, IgfsEntryInfo> lockInfos, Map<String, String> dirProps, Map<String, String> fileProps, int blockSize, @Nullable IgniteUuid affKey, boolean evictExclude, @Nullable IgfsSecondaryFileSystemCreateContext secondaryCtx, @Nullable T1<OutputStream> secondaryOutHolder) throws IgniteCheckedException { // This is our starting point. int lastExistingIdx = pathIds.lastExistingIndex(); IgfsEntryInfo lastExistingInfo = lockInfos.get(pathIds.lastExistingId()); // If current info already contains entry with the same name as it's child, then something // has changed concurrently. We must re-try because we cannot get info of this unexpected // element due to possible deadlocks. int curIdx = lastExistingIdx + 1; String curPart = pathIds.part(curIdx); IgniteUuid curId = pathIds.surrogateId(curIdx); if (lastExistingInfo.hasChild(curPart)) return null; // Create entry in the secondary file system if needed. if (secondaryCtx != null) { assert secondaryOutHolder != null; secondaryOutHolder.set(secondaryCtx.create()); } Map<IgniteUuid, EntryProcessor> procMap = new HashMap<>(); // First step: add new entry to the last existing element. procMap.put(lastExistingInfo.id(), new IgfsMetaDirectoryListingAddProcessor(curPart, new IgfsListingEntry(curId, dir || !pathIds.isLastIndex(curIdx)))); // Events support. IgfsPath lastCreatedPath = pathIds.lastExistingPath(); List<IgfsPath> createdPaths = new ArrayList<>(pathIds.count() - curIdx); // Second step: create middle directories. long curTime = System.currentTimeMillis(); while (curIdx < pathIds.count() - 1) { lastCreatedPath = new IgfsPath(lastCreatedPath, curPart); int nextIdx = curIdx + 1; String nextPart = pathIds.part(nextIdx); IgniteUuid nextId = pathIds.surrogateId(nextIdx); long accessTime; long modificationTime; Map<String, String> props; if (secondaryCtx != null) { accessTime = 0L; modificationTime = 0L; props = null; } else { accessTime = curTime; modificationTime = curTime; props = dirProps; } procMap.put(curId, new IgfsMetaDirectoryCreateProcessor(accessTime, modificationTime, props, nextPart, new IgfsListingEntry(nextId, dir || !pathIds.isLastIndex(nextIdx)))); // Save event. createdPaths.add(lastCreatedPath); // Advance things further. curIdx++; curPart = nextPart; curId = nextId; } // Third step: create leaf. if (dir) { long accessTime; long modificationTime; Map<String, String> props; if (secondaryCtx != null) { accessTime = 0L; modificationTime = 0L; props = null; } else { accessTime = curTime; modificationTime = curTime; props = dirProps; } procMap.put(curId, new IgfsMetaDirectoryCreateProcessor(accessTime, modificationTime, props)); } else { long newAccessTime; long newModificationTime; Map<String, String> newProps; long newLen; int newBlockSize; if (secondaryCtx != null) { newAccessTime = 0L; newModificationTime = 0L; newProps = null; } else { newAccessTime = curTime; newModificationTime = curTime; newProps = fileProps; } newLen = 0L; newBlockSize = blockSize; procMap.put(curId, new IgfsMetaFileCreateProcessor(newAccessTime, newModificationTime, newProps, newBlockSize, affKey, createFileLockId(false), evictExclude, newLen)); } createdPaths.add(pathIds.path()); // Execute cache operations. Map<Object, EntryProcessorResult> invokeRes = ((IgniteInternalCache)id2InfoPrj).invokeAll(procMap); IgfsEntryInfo info = (IgfsEntryInfo)invokeRes.get(curId).get(); return new IgfsPathsCreateResult(createdPaths, info); } /** * Generate events for created file or directory. * * @param createdPaths Created paths. * @param file Whether file was created. */ private void generateCreateEvents(List<IgfsPath> createdPaths, boolean file) { if (evts.isRecordable(EventType.EVT_IGFS_DIR_CREATED)) { for (int i = 0; i < createdPaths.size() - 1; i++) IgfsUtils.sendEvents(igfsCtx.kernalContext(), createdPaths.get(i), EventType.EVT_IGFS_DIR_CREATED); } IgfsPath leafPath = createdPaths.get(createdPaths.size() - 1); if (file) { IgfsUtils.sendEvents(igfsCtx.kernalContext(), leafPath, EventType.EVT_IGFS_FILE_CREATED); IgfsUtils.sendEvents(igfsCtx.kernalContext(), leafPath, EventType.EVT_IGFS_FILE_OPENED_WRITE); } else IgfsUtils.sendEvents(igfsCtx.kernalContext(), leafPath, EventType.EVT_IGFS_DIR_CREATED); } /** * Signal delete worker thread. */ private void signalDeleteWorker() { IgfsDeleteWorker delWorker0 = delWorker; if (delWorker0 != null) delWorker0.signal(); } /** * Synchronization task interface. */ private static interface SynchronizationTask<T> { /** * Callback handler in case synchronization was successful. * * @param infos Map from paths to corresponding infos. * @return Task result. * @throws Exception If failed. */ public T onSuccess(Map<IgfsPath, IgfsEntryInfo> infos) throws Exception; /** * Callback handler in case synchronization failed. * * @param err Optional exception. * @return Task result. * @throws IgniteCheckedException In case exception is to be thrown in that case. */ public T onFailure(Exception err) throws IgniteCheckedException; } }