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

brooklyn.entity.rebind.persister.MementoFileWriter Maven / Gradle / Ivy

There is a newer version: 0.7.0-M1
Show newest version
package brooklyn.entity.rebind.persister;

import java.io.File;
import java.io.IOException;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import brooklyn.util.time.Time;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Charsets;
import com.google.common.base.Stopwatch;
import com.google.common.base.Throwables;
import com.google.common.io.Files;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;

/**
 * For asynchronously writing to a file.
 * 
 * This class is thread-safe. If a write is in progress, one will be scheduled. If a write is already 
 * scheduled, we will just rely on the existing one; otherwise we will write now.
 * 
 * @author aled
 */
public class MementoFileWriter {

    protected static final Logger LOG = LoggerFactory.getLogger(MementoFileWriter.class);

    private final File file;
    private final File tmpFile;
    private final ListeningExecutorService executor;
    private final MementoSerializer serializer;
    private final AtomicBoolean executing = new AtomicBoolean();
    private final AtomicReference requireWrite = new AtomicReference();
    private final AtomicBoolean requireDelete = new AtomicBoolean();
    private final AtomicBoolean deleted = new AtomicBoolean();
    private final AtomicLong modCount = new AtomicLong();
    
    /**
     * @param file
     * @param executor A sequential executor (e.g. SingleThreadedExecutor, or equivalent)
     * @param serializer
     */
    public MementoFileWriter(File file, ListeningExecutorService executor, MementoSerializer serializer) {
        this.file = file;
        this.executor = executor;
        this.serializer = serializer;
        this.tmpFile = new File(file.getParentFile(), file.getName()+".tmp");
    }

    public void write(T val) {
        requireWrite.set(val);
        if (requireDelete.get() || deleted.get()) {
            LOG.warn("Not writing {}, because already deleted", file);
        } else if (executing.compareAndSet(false, true)) {
            if (LOG.isTraceEnabled()) LOG.trace("Submitting write task for {}", file);
            writeAsync();
        } else {
            if (LOG.isTraceEnabled()) LOG.trace("Execution already in-progress for {}; recorded write-requirement; returning", file);
        }
    }

    public void delete() {
        if (deleted.get() || requireDelete.get()) {
            if (LOG.isDebugEnabled()) LOG.debug("Duplicate call to delete {}; ignoring", file);
            return;
        }
        
        requireDelete.set(true);
        if (executing.compareAndSet(false, true)) {
            if (LOG.isTraceEnabled()) LOG.trace("Submitting delete task for {}", file);
            deleteAsync();
        } else {
            if (LOG.isTraceEnabled()) LOG.trace("Execution already in-progress for {}; recorded delete-requirement; returning", file);
        }
    }
    
    /**
     * This method must only be used for testing. If required in production, then revisit implementation!
     */
    @VisibleForTesting
    public void waitForWriteCompleted(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException {
        // Every time we finish writing, we increment a counter. We note the current val, and then
        // wait until we can guarantee that a complete additional write has been done. Not sufficient
        // to wait for `writeCount > origWriteCount` because we might have read the value when it was 
        // almost finished a write.
        
        long startTime = System.currentTimeMillis();
        long maxEndtime = (timeout > 0) ? (startTime + unit.toMillis(timeout)) : Long.MAX_VALUE;
        long origModCount = modCount.get();
        while (true) {
            if (modCount.get() > (origModCount+1)) {
                return;
            } else if (requireWrite.get() != null) {
                // must continue waiting for mods+1
            } else if (executing.get()) {
                // must wait for either this invocation to complete, or mods+1 (because might have already updated)
            } else {
                return;
            }
            
            if (System.currentTimeMillis() > maxEndtime) {
                throw new TimeoutException("Timeout waiting for pending complete of rebind-periodic-delta, after "+Time.makeTimeString(timeout, unit));
            }
            Thread.sleep(1);
        }
    }

    private void deleteAsync() {
        ListenableFuture future = executor.submit(new Callable() {
            @Override public Void call() throws IOException {
                try {
                    deleteNow();
                    return null;
                } catch (Throwable t) {
                    if (executor.isShutdown()) {
                        LOG.debug("Error deleting "+file+" (but executor shutdown)", t);
                    } else {
                        LOG.error("Error deleting "+file, t);
                    }
                    throw Throwables.propagate(t);
                }
            }});
        addPostExecListener(future);
    }
    
    private void writeAsync() {
        ListenableFuture future = executor.submit(new Callable() {
            @Override public Void call() throws IOException {
                try {
                    writeNow();
                    return null;
                } catch (Throwable t) {
                    if (executor.isShutdown()) {
                        LOG.debug("Error writing to "+file+" (but executor shutdown)", t);
                    } else {
                        LOG.error("Error writing to "+file, t);
                    }
                    throw Throwables.propagate(t);
                }
             }});
        addPostExecListener(future);
    }
    
    private void addPostExecListener(ListenableFuture future) {
        future.addListener(
                new Runnable() {
                    @Override public void run() {
                        if (LOG.isTraceEnabled()) LOG.trace("Write complete for {}", file);
                        try {
                            executing.set(false);
                            if (requireDelete.get()) {
                                if (executing.compareAndSet(false, true)) {
                                    if (LOG.isTraceEnabled()) LOG.trace("Submitting delete-task for {} (in post-exec) due to recorded delete-requirement", file);
                                    deleteAsync();
                                } else {
                                    if (LOG.isTraceEnabled()) LOG.trace("Delete-requirement for {} (in post-exec) handled by other thread; returning", file);
                                }
                                
                            } else if (requireWrite.get() != null) {
                                if (executing.compareAndSet(false, true)) {
                                    if (LOG.isTraceEnabled()) LOG.trace("Submitting write task for {} (in post-exec) due to recorded write-requirement", file);
                                    writeAsync();
                                } else {
                                    if (LOG.isTraceEnabled()) LOG.trace("Write-requirement for {} (in post-exec) handled by other thread; returning", file);
                                }
                            } else {
                                if (LOG.isTraceEnabled()) LOG.trace("No pending exec-requirements for {}", file);
                            }
                        } catch (Throwable t) {
                            LOG.error("Error in post-exec for "+file, t);
                            throw Throwables.propagate(t);
                        }
                    }
                }, 
                MoreExecutors.sameThreadExecutor());
    }
    
    private void writeNow() throws IOException {
        T val = requireWrite.getAndSet(null);
        
        Stopwatch stopwatch = new Stopwatch();
        
        // Write to the temp file, then atomically move it to the permanent file location
        Files.write(serializer.toString(val)+"\n", tmpFile, Charsets.UTF_8);
        Files.move(tmpFile, file);

        modCount.incrementAndGet();

        if (LOG.isTraceEnabled()) LOG.trace("Wrote {}, took {}ms; modified file {} times", 
                new Object[] {file, stopwatch.elapsed(TimeUnit.MILLISECONDS), modCount});
    }
    
    private void deleteNow() throws IOException {
        if (LOG.isTraceEnabled()) LOG.trace("Deleting {} and {}", file, tmpFile);
        deleted.set(true);
        requireDelete.set(false);
        
        file.delete();
        tmpFile.delete();
        
        modCount.incrementAndGet();
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy