package org.elasticsearch;
import com.obs.services.exception.ObsException;
import com.obs.services.model.GetObjectRequest;
import com.obs.services.model.ObsObject;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.message.ParameterizedMessage;
import org.elasticsearch.common.io.Streams;
import org.elasticsearch.core.internal.io.IOUtils;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.NoSuchFileException;
import java.util.ArrayList;
import java.util.List;
* Wrapper around an OBS object that will retry the {@link GetObjectRequest} if the download fails part-way through, resuming from where
* the failure occurred. This should be handled by the SDK but it isn't today. This should be revisited in the future (e.g. before removing
* the {@link Version#V_7_0_0} version constant) and removed when the SDK handles retries itself.
*
*/
class ObsRetryingInputStream extends InputStream {
private static final Logger logger = LogManager.getLogger(ObsRetryingInputStream.class);
static final int MAX_SUPPRESSED_EXCEPTIONS = 10;
private final ObsBlobStore blobStore;
private final String blobKey;
private final long start;
private final long end;
private final int maxAttempts;
private InputStream currentStream;
private int attempt = 1;
private List<IOException> failures = new ArrayList<>(MAX_SUPPRESSED_EXCEPTIONS);
private long currentOffset;
private boolean closed;
ObsRetryingInputStream(ObsBlobStore blobStore, String blobKey) throws IOException {
this(blobStore, blobKey, 0, Long.MAX_VALUE - 1);
}
ObsRetryingInputStream(ObsBlobStore blobStore, String blobKey, long start, long end) throws IOException {
if (start < 0L) {
throw new IllegalArgumentException("start must be non-negative");
}
if (end < start || end == Long.MAX_VALUE) {
throw new IllegalArgumentException("end must be >= start and not Long.MAX_VALUE");
}
this.blobStore = blobStore;
this.blobKey = blobKey;
this.maxAttempts = blobStore.getMaxRetries() + 1;
this.start = start;
this.end = end;
currentStream = openStream();
}
private InputStream openStream() throws IOException {
try (ObsReference clientReference = blobStore.clientReference()) {
final GetObjectRequest getObjectRequest = new GetObjectRequest(blobStore.bucket(), blobKey);
if (currentOffset > 0 || start > 0 || end < Long.MAX_VALUE - 1) {
assert start + currentOffset <= end :
"requesting beyond end, start = " + start + " offset=" + currentOffset + " end=" + end;
getObjectRequest.setRangeStart(Math.addExact(start, currentOffset));
getObjectRequest.setRangeEnd(end);
}
final ObsObject obsObject = SocketAccess.doPrivileged(() -> clientReference.client().getObject(getObjectRequest));
return obsObject.getObjectContent();
} catch (final ObsException e) {
if (e instanceof ObsException) {
if (404 == e.getResponseCode()) {
throw addSuppressedExceptions(new NoSuchFileException("Blob object [" + blobKey + "] not found: " + e.getMessage()));
}
}
throw addSuppressedExceptions(e);
}
}
@Override
public int read() throws IOException {
ensureOpen();
while (true) {
try {
final int result = currentStream.read();
currentOffset += 1;
return result;
} catch (IOException e) {
reopenStreamOrFail(e);
}
}
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
ensureOpen();
while (true) {
try {
final int bytesRead = currentStream.read(b, off, len);
if (bytesRead == -1) {
return -1;
}
currentOffset += bytesRead;
return bytesRead;
} catch (IOException e) {
reopenStreamOrFail(e);
}
}
}
private void ensureOpen() {
if (closed) {
assert false : "using obsRetryingInputStream after close";
throw new IllegalStateException("using obsRetryingInputStream after close");
}
}
private void reopenStreamOrFail(IOException e) throws IOException {
if (attempt >= maxAttempts) {
logger.debug(new ParameterizedMessage("failed reading [{}/{}] at offset [{}], attempt [{}] of [{}], giving up",
blobStore.bucket(), blobKey, start + currentOffset, attempt, maxAttempts), e);
throw addSuppressedExceptions(e);
}
logger.debug(new ParameterizedMessage("failed reading [{}/{}] at offset [{}], attempt [{}] of [{}], retrying",
blobStore.bucket(), blobKey, start + currentOffset, attempt, maxAttempts), e);
attempt += 1;
if (failures.size() < MAX_SUPPRESSED_EXCEPTIONS) {
failures.add(e);
}
try {
Streams.copy(currentStream, NULL_OUTPUT_STREAM);
} catch (Exception e2) {
logger.trace("Failed to fully consume stream on close", e);
}
IOUtils.closeWhileHandlingException(currentStream);
currentStream = openStream();
}
* OutputStream that just throws all the bytes away
*/
public static final OutputStream NULL_OUTPUT_STREAM = new OutputStream() {
@Override
public void write(int b) {
}
@Override
public void write(byte[] b, int off, int len) {
}
};
@Override
public void close() throws IOException {
try {
Streams.copy(currentStream, NULL_OUTPUT_STREAM);
} catch (Exception e) {
logger.trace("Failed to fully consume stream on close", e);
}
currentStream.close();
closed = true;
}
@Override
public long skip(long n) {
throw new UnsupportedOperationException("obsRetryingInputStream does not support seeking");
}
@Override
public void reset() {
throw new UnsupportedOperationException("obsRetryingInputStream does not support seeking");
}
private <T extends Exception> T addSuppressedExceptions(T e) {
for (IOException failure : failures) {
e.addSuppressed(failure);
}
return e;
}
}