io.vertx.core.file.impl.AsyncFileImpl.java Source code

Java tutorial

Introduction

Here is the source code for io.vertx.core.file.impl.AsyncFileImpl.java

Source

/*
 * Copyright (c) 2011-2013 The original author or authors
 * ------------------------------------------------------
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * and Apache License v2.0 which accompanies this distribution.
 *
 *     The Eclipse Public License is available at
 *     http://www.eclipse.org/legal/epl-v10.html
 *
 *     The Apache License v2.0 is available at
 *     http://www.opensource.org/licenses/apache2.0.php
 *
 * You may elect to redistribute this code under either of these licenses.
 */

package io.vertx.core.file.impl;

import io.netty.buffer.ByteBuf;
import io.vertx.core.AsyncResult;
import io.vertx.core.Future;
import io.vertx.core.Handler;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.file.AsyncFile;
import io.vertx.core.file.FileSystemException;
import io.vertx.core.file.OpenOptions;
import io.vertx.core.impl.Arguments;
import io.vertx.core.impl.ContextImpl;
import io.vertx.core.impl.VertxInternal;
import io.vertx.core.logging.Logger;
import io.vertx.core.logging.LoggerFactory;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.FileAttribute;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.HashSet;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

/**
 *
 * This class is optimised for performance when used on the same event loop that is was passed to the handler with.
 * However it can be used safely from other threads.
 *
 * The internal state is protected using the synchronized keyword. If always used on the same event loop, then
 * we benefit from biased locking which makes the overhead of synchronized near zero.
 *
 * @author <a href="http://tfox.org">Tim Fox</a>
 */
public class AsyncFileImpl implements AsyncFile {

    private static final Logger log = LoggerFactory.getLogger(AsyncFile.class);

    public static final int DEFAULT_READ_BUFFER_SIZE = 8192;

    private final VertxInternal vertx;
    private final AsynchronousFileChannel ch;
    private final ContextImpl context;
    private boolean closed;
    private Runnable closedDeferred;
    private long writesOutstanding;
    private Handler<Throwable> exceptionHandler;
    private Handler<Void> drainHandler;
    private long writePos;
    private int maxWrites = 128 * 1024; // TODO - we should tune this for best performance
    private int lwm = maxWrites / 2;
    private int readBufferSize = DEFAULT_READ_BUFFER_SIZE;
    private boolean paused;
    private Handler<Buffer> dataHandler;
    private Handler<Void> endHandler;
    private long readPos;
    private boolean readInProgress;

    AsyncFileImpl(VertxInternal vertx, String path, OpenOptions options, ContextImpl context) {
        if (!options.isRead() && !options.isWrite()) {
            throw new FileSystemException("Cannot open file for neither reading nor writing");
        }
        this.vertx = vertx;
        Path file = Paths.get(path);
        HashSet<OpenOption> opts = new HashSet<>();
        if (options.isRead())
            opts.add(StandardOpenOption.READ);
        if (options.isWrite())
            opts.add(StandardOpenOption.WRITE);
        if (options.isCreate())
            opts.add(StandardOpenOption.CREATE);
        if (options.isCreateNew())
            opts.add(StandardOpenOption.CREATE_NEW);
        if (options.isSync())
            opts.add(StandardOpenOption.SYNC);
        if (options.isDsync())
            opts.add(StandardOpenOption.DSYNC);
        if (options.isDeleteOnClose())
            opts.add(StandardOpenOption.DELETE_ON_CLOSE);
        if (options.isSparse())
            opts.add(StandardOpenOption.SPARSE);
        if (options.isTruncateExisting())
            opts.add(StandardOpenOption.TRUNCATE_EXISTING);
        try {
            if (options.getPerms() != null) {
                FileAttribute<?> attrs = PosixFilePermissions
                        .asFileAttribute(PosixFilePermissions.fromString(options.getPerms()));
                ch = AsynchronousFileChannel.open(file, opts, vertx.getWorkerPool(), attrs);
            } else {
                ch = AsynchronousFileChannel.open(file, opts, vertx.getWorkerPool());
            }
        } catch (IOException e) {
            throw new FileSystemException(e);
        }
        this.context = context;
    }

    @Override
    public void close() {
        closeInternal(null);
    }

    @Override
    public void close(Handler<AsyncResult<Void>> handler) {
        closeInternal(handler);
    }

    @Override
    public void end() {
        close();
    }

    @Override
    public synchronized AsyncFile read(Buffer buffer, int offset, long position, int length,
            Handler<AsyncResult<Buffer>> handler) {
        Objects.requireNonNull(buffer, "buffer");
        Objects.requireNonNull(handler, "handler");
        Arguments.require(offset >= 0, "offset must be >= 0");
        Arguments.require(position >= 0, "position must be >= 0");
        Arguments.require(length >= 0, "length must be >= 0");
        check();
        ByteBuffer bb = ByteBuffer.allocate(length);
        doRead(buffer, offset, bb, position, handler);
        return this;
    }

    @Override
    public AsyncFile write(Buffer buffer, long position, Handler<AsyncResult<Void>> handler) {
        Objects.requireNonNull(handler, "handler");
        return doWrite(buffer, position, handler);
    }

    private synchronized AsyncFile doWrite(Buffer buffer, long position, Handler<AsyncResult<Void>> handler) {
        Objects.requireNonNull(buffer, "buffer");
        Arguments.require(position >= 0, "position must be >= 0");
        check();
        Handler<AsyncResult<Void>> wrapped = ar -> {
            if (ar.succeeded()) {
                checkContext();
                if (writesOutstanding == 0 && closedDeferred != null) {
                    closedDeferred.run();
                } else {
                    checkDrained();
                }
                if (handler != null) {
                    handler.handle(ar);
                }
            } else {
                if (handler != null) {
                    handler.handle(ar);
                } else {
                    handleException(ar.cause());
                }
            }
        };
        ByteBuf buf = buffer.getByteBuf();
        if (buf.nioBufferCount() > 1) {
            doWrite(buf.nioBuffers(), position, wrapped);
        } else {
            ByteBuffer bb = buf.nioBuffer();
            doWrite(bb, position, bb.limit(), wrapped);
        }
        return this;
    }

    @Override
    public synchronized AsyncFile write(Buffer buffer) {
        int length = buffer.length();
        doWrite(buffer, writePos, null);
        writePos += length;
        return this;
    }

    @Override
    public synchronized AsyncFile setWriteQueueMaxSize(int maxSize) {
        Arguments.require(maxSize >= 2, "maxSize must be >= 2");
        check();
        this.maxWrites = maxSize;
        this.lwm = maxWrites / 2;
        return this;
    }

    @Override
    public synchronized AsyncFile setReadBufferSize(int readBufferSize) {
        this.readBufferSize = readBufferSize;
        return this;
    }

    @Override
    public synchronized boolean writeQueueFull() {
        check();
        return writesOutstanding >= maxWrites;
    }

    @Override
    public synchronized AsyncFile drainHandler(Handler<Void> handler) {
        check();
        this.drainHandler = handler;
        checkDrained();
        return this;
    }

    @Override
    public synchronized AsyncFile exceptionHandler(Handler<Throwable> handler) {
        check();
        this.exceptionHandler = handler;
        return this;
    }

    @Override
    public synchronized AsyncFile handler(Handler<Buffer> handler) {
        check();
        this.dataHandler = handler;
        if (dataHandler != null && !paused && !closed) {
            doRead();
        }
        return this;
    }

    @Override
    public synchronized AsyncFile endHandler(Handler<Void> handler) {
        check();
        this.endHandler = handler;
        return this;
    }

    @Override
    public synchronized AsyncFile pause() {
        check();
        paused = true;
        return this;
    }

    @Override
    public synchronized AsyncFile resume() {
        check();
        if (paused && !closed) {
            paused = false;
            if (dataHandler != null) {
                doRead();
            }
        }
        return this;
    }

    @Override
    public AsyncFile flush() {
        doFlush(null);
        return this;
    }

    @Override
    public AsyncFile flush(Handler<AsyncResult<Void>> handler) {
        doFlush(handler);
        return this;
    }

    @Override
    public synchronized AsyncFile setReadPos(long readPos) {
        this.readPos = readPos;
        return this;
    }

    @Override
    public synchronized AsyncFile setWritePos(long writePos) {
        this.writePos = writePos;
        return this;
    }

    private synchronized void checkDrained() {
        if (drainHandler != null && writesOutstanding <= lwm) {
            Handler<Void> handler = drainHandler;
            drainHandler = null;
            handler.handle(null);
        }
    }

    private void handleException(Throwable t) {
        if (exceptionHandler != null && t instanceof Exception) {
            exceptionHandler.handle(t);
        } else {
            log.error("Unhandled exception", t);

        }
    }

    private synchronized void doWrite(ByteBuffer[] buffers, long position, Handler<AsyncResult<Void>> handler) {
        AtomicInteger cnt = new AtomicInteger();
        AtomicBoolean sentFailure = new AtomicBoolean();
        for (ByteBuffer b : buffers) {
            int limit = b.limit();
            doWrite(b, position, limit, ar -> {
                if (ar.succeeded()) {
                    if (cnt.incrementAndGet() == buffers.length) {
                        handler.handle(ar);
                    }
                } else {
                    if (sentFailure.compareAndSet(false, true)) {
                        handler.handle(ar);
                    }
                }
            });
            position += limit;
        }
    }

    private synchronized void doRead() {
        if (!readInProgress) {
            readInProgress = true;
            Buffer buff = Buffer.buffer(readBufferSize);
            read(buff, 0, readPos, readBufferSize, ar -> {
                if (ar.succeeded()) {
                    readInProgress = false;
                    Buffer buffer = ar.result();
                    if (buffer.length() == 0) {
                        // Empty buffer represents end of file
                        handleEnd();
                    } else {
                        readPos += buffer.length();
                        handleData(buffer);
                        if (!paused && dataHandler != null) {
                            doRead();
                        }
                    }
                } else {
                    handleException(ar.cause());
                }
            });
        }
    }

    private synchronized void handleData(Buffer buffer) {
        if (dataHandler != null) {
            checkContext();
            dataHandler.handle(buffer);
        }
    }

    private synchronized void handleEnd() {
        dataHandler = null;
        if (endHandler != null) {
            checkContext();
            endHandler.handle(null);
        }
    }

    private synchronized void doFlush(Handler<AsyncResult<Void>> handler) {
        checkClosed();
        context.executeBlocking(() -> {
            try {
                ch.force(false);
                return null;
            } catch (IOException e) {
                throw new FileSystemException(e);
            }
        }, handler);
    }

    private void doWrite(ByteBuffer buff, long position, long toWrite, Handler<AsyncResult<Void>> handler) {
        if (toWrite == 0) {
            throw new IllegalStateException("Cannot save zero bytes");
        }
        writesOutstanding += toWrite;
        writeInternal(buff, position, handler);
    }

    private void writeInternal(ByteBuffer buff, long position, Handler<AsyncResult<Void>> handler) {

        ch.write(buff, position, null, new java.nio.channels.CompletionHandler<Integer, Object>() {

            public void completed(Integer bytesWritten, Object attachment) {

                long pos = position;

                if (buff.hasRemaining()) {
                    // partial write
                    pos += bytesWritten;
                    // resubmit
                    writeInternal(buff, pos, handler);
                } else {
                    // It's been fully written
                    context.runOnContext((v) -> {
                        writesOutstanding -= buff.limit();
                        handler.handle(Future.succeededFuture());
                    });
                }
            }

            public void failed(Throwable exc, Object attachment) {
                if (exc instanceof Exception) {
                    context.runOnContext((v) -> handler.handle(Future.succeededFuture()));
                } else {
                    log.error("Error occurred", exc);
                }
            }
        });
    }

    private void doRead(Buffer writeBuff, int offset, ByteBuffer buff, long position,
            Handler<AsyncResult<Buffer>> handler) {

        ch.read(buff, position, null, new java.nio.channels.CompletionHandler<Integer, Object>() {

            long pos = position;

            private void done() {
                context.runOnContext((v) -> {
                    buff.flip();
                    writeBuff.setBytes(offset, buff);
                    handler.handle(Future.succeededFuture(writeBuff));
                });
            }

            public void completed(Integer bytesRead, Object attachment) {
                if (bytesRead == -1) {
                    //End of file
                    done();
                } else if (buff.hasRemaining()) {
                    // partial read
                    pos += bytesRead;
                    // resubmit
                    doRead(writeBuff, offset, buff, pos, handler);
                } else {
                    // It's been fully written
                    done();
                }
            }

            public void failed(Throwable t, Object attachment) {
                context.runOnContext((v) -> handler.handle(Future.failedFuture(t)));
            }
        });
    }

    private void check() {
        checkClosed();
    }

    private void checkClosed() {
        if (closed) {
            throw new IllegalStateException("File handle is closed");
        }
    }

    private void checkContext() {
        if (!vertx.getContext().equals(context)) {
            throw new IllegalStateException("AsyncFile must only be used in the context that created it, expected: "
                    + context + " actual " + vertx.getContext());
        }
    }

    private void doClose(Handler<AsyncResult<Void>> handler) {
        ContextImpl handlerContext = vertx.getOrCreateContext();
        handlerContext.executeBlocking(res -> {
            try {
                ch.close();
                res.complete(null);
            } catch (IOException e) {
                res.fail(e);
            }
        }, handler);
    }

    private synchronized void closeInternal(Handler<AsyncResult<Void>> handler) {
        check();

        closed = true;

        if (writesOutstanding == 0) {
            doClose(handler);
        } else {
            closedDeferred = () -> doClose(handler);
        }
    }

}