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

world.data.jdbc.internal.transport.FileBackedInputStream Maven / Gradle / Ivy

/*
 * dw-jdbc
 * Copyright 2017 data.world, Inc.

 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the
 * License.
 *
 * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
 * implied. See the License for the specific language governing
 * permissions and limitations under the License.
 *
 * This product includes software developed at data.world, Inc.(http://www.data.world/).
 */
package world.data.jdbc.internal.transport;

import javax.annotation.Nonnull;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.concurrent.Executor;
import java.util.concurrent.locks.AbstractQueuedLongSynchronizer;

import static java.util.Objects.requireNonNull;

/**
 * An {@link InputStream} that protects backend http servers from slow readers.
 * 
    *
  1. The first 'n' bytes are read into memory immediately to handle small responses.
  2. *
  3. For bigger responses, a background thread starts downloading the rest of the response to a temporary file.
  4. *
  5. Content downloaded so far can be read via the {@code FileBackedInputStream}. Readers don't have to wait * for the entire download to complete before they can make progress--a semaphore is used to ensure readers don't * get ahead of the download thread.
  6. *
*/ class FileBackedInputStream extends InputStream { private InputStream memIn; private final Sync sync; private File file; private final InputStream fileIn; /** Flag used by the main thread to tell the copyAsync thread that the main thread is done. */ private volatile boolean fileInClosed; /** Flag used by the copyAsync thread to tell the main thread that copyAsync terminated abnormally. */ private volatile Throwable throwable; FileBackedInputStream(InputStream in, int memLimit, Executor cachedThreadPool) throws IOException { requireNonNull(in, "in"); requireNonNull(cachedThreadPool, "cachedThreadPool"); // Read the first 'memLimit' bytes immediately. byte[] buf = new byte[memLimit]; int remaining = memLimit, length = 0, count; while (remaining > 0 && (count = in.read(buf, length, remaining)) != -1) { length += count; remaining -= count; } this.memIn = new ByteArrayInputStream(buf, 0, length); if (length < memLimit) { // All content fits in memory in.close(); this.sync = null; this.fileIn = null; this.fileInClosed = true; } else { // First 'memLimit' bytes are in memory. Asynchronously download the rest to a file as fast as possible. // The reader can still read from FileBackedInputStream while the download is in progress. this.sync = new Sync(); this.file = File.createTempFile("dw-jdbc", ".tmp"); this.fileIn = new FileInputStream(file); OutputStream fileOut = new FileOutputStream(file); cachedThreadPool.execute(() -> copyAsync(in, fileOut)); } } private void copyAsync(InputStream source, OutputStream target) { try (InputStream in = source; OutputStream out = target) { byte[] buf = new byte[4096]; int count; while (!fileInClosed && (count = in.read(buf)) != -1) { out.write(buf, 0, count); sync.releaseShared(count); } } catch (Throwable t) { throwable = t; } finally { sync.releaseShared(Long.MAX_VALUE); deleteTempFile(); } } @Override public int read() throws IOException { if (memIn != null) { int b = memIn.read(); if (b != -1) { return b; } // Reached 'memIn' EOF, fall through to 'fileIn' memIn = null; } if (fileIn != null) { acquire(1); return fileIn.read(); } return -1; // EOF } @Override public int read(@Nonnull byte[] b, int off, int len) throws IOException { if (memIn != null) { int count = memIn.read(b, off, len); if (count != -1) { return count; } // Reached 'memIn' EOF, fall through to 'fileIn' memIn = null; } if (fileIn != null) { acquire(len); return fileIn.read(b, off, len); } return -1; // EOF } /** Wait until the copyAsync() thread has copied enough to read 'len' more bytes, or EOF. */ private void acquire(int len) throws IOException { try { sync.acquireSharedInterruptibly(len); } catch (InterruptedException e) { throw new IOException("Interrupted while reading from file.", e); } // Rethrow on the main thread exceptions caught by the copyAsync() thread. if (throwable != null) { throw new IOException(throwable.getMessage(), throwable); } } @Override public void close() throws IOException { if (!fileInClosed) { fileInClosed = true; fileIn.close(); deleteTempFile(); } } private synchronized void deleteTempFile() { // Note that Windows won't delete an open file so we must attempt cleanup from both threads to be // sure both input and output file handles are closed at the time of the delete. if (fileInClosed && file != null && file.delete()) { file = null; } } /** A 64-bit semaphore used to make sure the file reader doesn't get ahead of the writer. */ @SuppressWarnings("serial") private static class Sync extends AbstractQueuedLongSynchronizer { @Override protected long tryAcquireShared(long acquires) { for (; ; ) { long current = getState(); long next = current - acquires; if (next < 0 || compareAndSetState(current, next)) { return next; } } } @Override protected boolean tryReleaseShared(long releases) { for (; ; ) { long current = getState(); long next; if (releases == Long.MAX_VALUE) { // Special case: allow sync.acquire() to obtain as much as it wants without blocking. next = Long.MAX_VALUE; } else { next = current + releases; if (next < current) { throw new Error("Maximum count exceeded"); // overflow } } if (compareAndSetState(current, next)) { return true; } } } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy