All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.java.truevfs.kernel.impl.CacheController Maven / Gradle / Ivy

/*
 * Copyright © 2005 - 2021 Schlichtherle IT Services.
 * All rights reserved. Use is subject to license terms.
 */
package net.java.truevfs.kernel.impl;

import lombok.val;
import net.java.truecommons.cio.*;
import net.java.truecommons.io.DecoratingInputStream;
import net.java.truecommons.io.DecoratingOutputStream;
import net.java.truecommons.io.DecoratingSeekableChannel;
import net.java.truecommons.logging.LocalizedLogger;
import net.java.truecommons.shed.BitField;
import net.java.truevfs.kernel.spec.*;
import org.slf4j.Logger;

import javax.annotation.concurrent.NotThreadSafe;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.channels.SeekableByteChannel;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

import static net.java.truecommons.cio.Entry.Type.FILE;
import static net.java.truevfs.kernel.spec.FsAccessOption.*;
import static net.java.truevfs.kernel.spec.FsSyncOption.ABORT_CHANGES;
import static net.java.truevfs.kernel.spec.FsSyncOption.CLEAR_CACHE;
import static net.java.truevfs.kernel.spec.FsSyncOptions.SYNC;

/**
 * A selective cache for file system entries.
 * Decorating a file system controller with this class has the following effects:
 * 

* - Caching and buffering for an entry needs to get activated by using the methods {@code input} or {@code output} with * the access option {@link net.java.truevfs.kernel.spec.FsAccessOption#CACHE}. * - Unless a write operation succeeds, upon each read operation the entry data gets copied from the backing store for * buffering purposes only. * - Upon a successful write operation, the entry data gets cached for subsequent read operations until the file system * gets {@code sync}ed again. * - Entry data written to the cache is not written to the backing store until the file system gets `sync`ed - this is * a "write back" strategy. * - As a side effect, caching decouples the underlying storage from its clients, allowing it to create, read, update or * delete the entry data while some clients are still busy on reading or writing the copied entry data. * * @author Christian Schlichtherle */ @NotThreadSafe abstract class CacheController implements DelegatingArchiveController { private static final Logger logger = new LocalizedLogger(CacheController.class); private final Map caches = new HashMap<>(); abstract IoBufferPool getPool(); @Override public InputSocket input(BitField options, FsNodeName name) { // This class requires ON-DEMAND LOOKUP of its delegate socket! return new DelegatingInputSocket() { @Override public InputSocket socket() { assert writeLockedByCurrentThread(); EntryCache cache = caches.get(name); if (null == cache) { if (!options.get(CACHE)) { return getController().input(options, name); } cache = new EntryCache(name); } return cache.input(options); } }; } @Override public OutputSocket output(BitField options, FsNodeName name, Optional template) { // This class requires ON-DEMAND LOOKUP of its delegate socket! return new DelegatingOutputSocket() { @Override public OutputSocket socket() { assert writeLockedByCurrentThread(); EntryCache cache = caches.get(name); if (null == cache) { if (!options.get(CACHE)) { return getController().output(options, name, template); } cache = new EntryCache(name); } return cache.output(options, template); } }; } @Override public void make(final BitField options, final FsNodeName name, final Entry.Type type, final Optional template) throws IOException { assert writeLockedByCurrentThread(); getController().make(options, name, type, template); val cache = caches.remove(name); if (null != cache) { cache.clear(); } } @Override public void unlink(final BitField options, final FsNodeName name) throws IOException { assert writeLockedByCurrentThread(); getController().unlink(options, name); val cache = caches.remove(name); if (null != cache) { cache.clear(); } } @Override public void sync(final BitField options) throws FsSyncException { assert writeLockedByCurrentThread(); assert !readLockedByCurrentThread(); syncCacheEntries(options); getController().sync(options.clear(CLEAR_CACHE)); if (caches.isEmpty()) { setMounted(false); } } private void syncCacheEntries(final BitField options) throws FsSyncException { // HC SVNT DRACONES! if (0 >= caches.size()) { return; } val flush = !options.get(ABORT_CHANGES); val clear = !flush || options.get(CLEAR_CACHE); val builder = new FsSyncExceptionBuilder(); val it = caches.values().iterator(); while (it.hasNext()) { val cache = it.next(); if (flush) { try { cache.flush(); } catch (IOException e) { throw builder.fail(new FsSyncException(getMountPoint(), e)); } } if (clear) { it.remove(); try { cache.clear(); } catch (IOException e) { builder.warn(new FsSyncWarningException(getMountPoint(), e)); } } } builder.check(); } /** * A cache for the contents of an individual archive entry. */ private final class EntryCache { final CacheEntry cache = CacheEntry.Strategy.WriteBack.newCacheEntry(getPool()); final FsNodeName name; EntryCache(final FsNodeName name) { this.name = name; } void flush() throws IOException { cache.flush(); } void clear() throws IOException { cache.release(); } void register() { caches.put(name, this); } InputSocket input(final BitField options) { final class Input extends DelegatingInputSocket { final BitField _options = options.clear(CACHE); // consume final InputSocket socket = getController().input(_options, name); @Override protected InputSocket socket() throws IOException { return socket; } @Override public InputStream stream(final OutputSocket peer) throws IOException { assert writeLockedByCurrentThread(); return new DecoratingInputStream(socket().stream(peer)) { { assert isMounted(); } @Override public void close() throws IOException { assert writeLockedByCurrentThread(); in.close(); register(); } }; } @Override public SeekableByteChannel channel(OutputSocket peer) throws IOException { throw new AssertionError(); } } return cache.configure(new Input()).input(); } OutputSocket output(final BitField options, final Optional template) { // This class requires lazy initialization of its channel, but no automatic decoupling on exceptions! final class Output extends DelegatingOutputSocket { final BitField _options = options.clear(CACHE); // consume final OutputSocket socket = cache .configure(getController().output(_options.clear(EXCLUSIVE), name, template)) .output(); @Override protected OutputSocket socket() throws IOException { return socket; } @Override public OutputStream stream(final InputSocket peer) throws IOException { assert writeLockedByCurrentThread(); preOutput(); return new DecoratingOutputStream(socket().stream(peer)) { { register(); } @Override public void close() throws IOException { assert writeLockedByCurrentThread(); out.close(); postOutput(); } }; } @Override public SeekableByteChannel channel(final InputSocket peer) throws IOException { assert writeLockedByCurrentThread(); preOutput(); return new DecoratingSeekableChannel(socket().channel(peer)) { { register(); } @Override public void close() throws IOException { assert writeLockedByCurrentThread(); channel.close(); postOutput(); } }; } void preOutput() throws IOException { make(_options, template); } void postOutput() throws IOException { make(_options.clear(EXCLUSIVE), template.isPresent() ? template : Optional.of(cache)); register(); } void make(final BitField options, final Optional template) throws IOException { BitField makeOpts = options; while (true) { try { getController().make(makeOpts, name, FILE, template); return; } catch (final NeedsSyncException makeEx) { // In this context, this exception means that the entry has already been written to the // output archive for the target archive file. // Pass on the exception if there is no means to resolve the issue locally, that is if we // were asked to create the entry exclusively or this is a non-recursive file system // operation. if (makeOpts.get(EXCLUSIVE)) { throw makeEx; } val syncOpts = SyncController.modify(SYNC); if (SYNC == syncOpts) { throw makeEx; } // Try to resolve the issue locally. // Even if we were asked to create the entry EXCLUSIVEly, we first need to try to get the // cache in sync() with the virtual file system again and retry the make(). try { getController().sync(syncOpts); // sync() succeeded, now repeat the make(). } catch (final FsSyncException syncEx) { syncEx.addSuppressed(makeEx); // sync() failed, maybe just because the current thread has already acquired some open // I/O resources for the same target archive file, e.g. an input stream for a copy // operation and this is an artifact of an attempt to acquire the output stream for a // child file system. if (syncEx.getCause() instanceof FsOpenResourceException) { // OK, we couldn't sync() because the current thread has acquired open I/O resources // for the same target archive file. // Normally, we would be expected to rethrow the make exception to trigger another // sync(), but this would fail for the same reason und create an endless loop, so we // can't do this. //throw mknodEx; // Dito for mapping the exception. //throw FsNeedsLockRetryException.get(getModel()); // Check if we can retry the make with GROW set. val oldMknodOpts = makeOpts; makeOpts = oldMknodOpts.set(GROW); if (makeOpts == oldMknodOpts) { // Finally, the make failed because the entry has already been output to the // target archive file - so what?! // This should mark only a volatile issue because the next sync() will sort it // out once all the I/O resources have been closed. // Let's log the sync exception - mind that it has suppressed the make exception // - and continue anyway... logger.debug("ignoring", syncEx); return; } } else { // Too bad, sync() failed because of a more serious issue than just some open // resources. // Let's rethrow the sync exception. throw syncEx; } } } } } } return new Output(); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy