/*
* Licensed to CRATE Technology GmbH ("Crate") under one or more contributor
* license agreements. See the NOTICE file distributed with this work for
* additional information regarding copyright ownership. Crate licenses
* this file to you 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.
*
* However, if you have executed another commercial license agreement
* with Crate these terms will supersede the license and you may use the
* software solely pursuant to the terms of the relevant commercial agreement.
*/
package io.crate.blob.pending_transfer;
import io.crate.blob.BlobTransferTarget;
import io.crate.blob.DigestBlob;
import org.apache.logging.log4j.Logger;
import org.elasticsearch.cluster.node.DiscoveryNode;
import org.elasticsearch.common.bytes.BytesArray;
import org.elasticsearch.common.logging.Loggers;
import org.elasticsearch.transport.EmptyTransportResponseHandler;
import org.elasticsearch.transport.TransportRequestOptions;
import org.elasticsearch.transport.TransportService;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.*;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
public class PutHeadChunkRunnable implements Runnable {
private final DigestBlob digestBlob;
private final long bytesToSend;
private final DiscoveryNode recipientNode;
private final TransportService transportService;
private final BlobTransferTarget blobTransferTarget;
private final UUID transferId;
private WatchKey watchKey;
private WatchService watcher;
private static final Logger logger = Loggers.getLogger(PutHeadChunkRunnable.class);
public PutHeadChunkRunnable(DigestBlob digestBlob, long bytesToSend,
TransportService transportService,
BlobTransferTarget blobTransferTarget,
DiscoveryNode recipientNode, UUID transferId) {
this.digestBlob = digestBlob;
this.bytesToSend = bytesToSend;
this.recipientNode = recipientNode;
this.blobTransferTarget = blobTransferTarget;
this.transferId = transferId;
this.transportService = transportService;
}
@Override
public void run() {
FileInputStream fileInputStream = null;
try {
int bufSize = 4096;
int bytesRead;
int size;
int maxFileGrowthWait = 5;
int fileGrowthWaited = 0;
byte[] buffer = new byte[bufSize];
long remainingBytes = bytesToSend;
File pendingFile;
try {
pendingFile = digestBlob.file();
if (pendingFile == null) {
pendingFile = digestBlob.getContainerFile();
}
fileInputStream = new FileInputStream(pendingFile);
} catch (FileNotFoundException e) {
// this happens if the file has already been moved from tmpDirectory to containerDirectory
pendingFile = digestBlob.getContainerFile();
fileInputStream = new FileInputStream(pendingFile);
}
while (remainingBytes > 0) {
size = (int) Math.min(bufSize, remainingBytes);
bytesRead = fileInputStream.read(buffer, 0, size);
if (bytesRead < size) {
waitUntilFileHasGrown(pendingFile);
fileGrowthWaited++;
if (fileGrowthWaited == maxFileGrowthWait) {
throw new HeadChunkFileTooSmallException(pendingFile.getAbsolutePath());
}
if (bytesRead < 1) {
continue;
}
}
remainingBytes -= bytesRead;
transportService.submitRequest(
recipientNode,
BlobHeadRequestHandler.Actions.PUT_BLOB_HEAD_CHUNK,
new PutBlobHeadChunkRequest(transferId, new BytesArray(buffer, 0, bytesRead)),
TransportRequestOptions.EMPTY,
EmptyTransportResponseHandler.INSTANCE_SAME
).txGet();
}
} catch (IOException ex) {
logger.error("IOException in PutHeadChunkRunnable", ex);
} finally {
blobTransferTarget.putHeadChunkTransferFinished(transferId);
if (watcher != null) {
try {
watcher.close();
} catch (IOException e) {
logger.error("Error closing WatchService in {}", e, getClass().getSimpleName());
}
}
if (fileInputStream != null) {
try {
fileInputStream.close();
} catch (IOException e) {
logger.error("Error closing HeadChunk", e);
}
}
}
}
private void waitUntilFileHasGrown(File pendingFile) {
try {
if (watcher == null) {
initWatcher(pendingFile.getParent());
}
watchKey = watcher.poll(5, TimeUnit.SECONDS);
if (watchKey == null) {
return;
}
for (WatchEvent<?> event : watchKey.pollEvents()) {
WatchEvent.Kind<?> kind = event.kind();
if (kind == StandardWatchEventKinds.OVERFLOW) {
continue;
}
@SuppressWarnings("unchecked")
WatchEvent<Path> ev = (WatchEvent<Path>) event;
Path filename = ev.context();
if (filename.toString().equals(pendingFile.getName())) {
break;
}
}
} catch (IOException | InterruptedException ex) {
logger.warn(ex.getMessage(), ex);
}
}
private void initWatcher(String directoryToWatch) throws IOException {
FileSystem fs = FileSystems.getDefault();
watcher = fs.newWatchService();
Path path = fs.getPath(directoryToWatch);
watchKey = path.register(watcher, StandardWatchEventKinds.ENTRY_MODIFY);
}
}