package com.netflix.suro.sink.remotefile;
import com.google.common.base.Preconditions;
import com.netflix.servo.annotations.DataSourceType;
import com.netflix.servo.annotations.Monitor;
import com.netflix.servo.monitor.Monitors;
import com.netflix.suro.message.MessageContainer;
import com.netflix.suro.sink.Sink;
import com.netflix.suro.sink.localfile.LocalFileSink;
import com.netflix.suro.sink.remotefile.formatter.DynamicRemotePrefixFormatter;
import org.joda.time.Period;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.util.Collections;
import java.util.Set;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
public abstract class RemoteFileSink implements Sink {
private static final Logger log = LoggerFactory.getLogger(RemoteFileSink.class);
protected final LocalFileSink localFileSink;
private final RemotePrefixFormatter prefixFormatter;
private final ExecutorService uploader;
private final ExecutorService localFilePoller;
private ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
private final boolean batchUpload;
private boolean running = false;
private static final int processingFileQueueThreshold = 1000;
private static final String processingFileQueueCleanupInterval = "PT60s";
private Set<String> processingFileSet = Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>());
private BlockingQueue<String> processedFileQueue = new LinkedBlockingQueue<String>();
public RemoteFileSink(
LocalFileSink localFileSink,
RemotePrefixFormatter prefixFormatter,
int concurrentUpload,
boolean batchUpload) {
this.localFileSink = localFileSink;
this.prefixFormatter = prefixFormatter == null ? new DynamicRemotePrefixFormatter("date(yyyyMMdd)") : prefixFormatter;
this.batchUpload = batchUpload;
Preconditions.checkNotNull(localFileSink, "localFileSink is needed");
uploader = Executors.newFixedThreadPool(concurrentUpload == 0 ? 5 : concurrentUpload);
localFilePoller = Executors.newSingleThreadExecutor();
if (!batchUpload) {
localFileSink.cleanUp(false);
}
Monitors.registerObject(
this.getClass().getSimpleName() + '-' + localFileSink.getOutputDir().replace('/', '_'),
this);
}
@Override
public void writeTo(MessageContainer message) {
localFileSink.writeTo(message);
}
@Override
public void open() {
initialize();
if (!batchUpload) {
running = true;
localFilePoller.submit(new Runnable() {
@Override
public void run() {
while (running) {
uploadAllFromQueue();
localFileSink.cleanUp(false);
}
uploadAllFromQueue();
}
});
localFileSink.open();
int schedulingSecond = new Period(processingFileQueueCleanupInterval).toStandardSeconds().getSeconds();
scheduler.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
if (processingFileSet.size() > processingFileQueueThreshold) {
String file = null;
int count = 0;
while (processingFileSet.size() > processingFileQueueThreshold &&
(file = processedFileQueue.poll()) != null) {
processingFileSet.remove(file);
++count;
}
log.info(count + " files are removed from processingFileSet");
}
}
}, schedulingSecond, schedulingSecond, TimeUnit.SECONDS);
}
}
@Override
public void close() {
try {
if (!batchUpload) {
localFileSink.close();
running = false;
localFilePoller.shutdown();
localFilePoller.awaitTermination(60000, TimeUnit.MILLISECONDS);
}
uploader.shutdown();
uploader.awaitTermination(60000, TimeUnit.MILLISECONDS);
} catch (Exception e) {
// ignore exceptions while closing
log.error("Exception while closing: " + e.getMessage(), e);
}
}
@Override
public String getStat() {
StringBuilder sb = new StringBuilder(localFileSink.getStat());
sb.append('\n').append(String.format("%d files uploaded so far", uploadedFileCount.get()));
return sb.toString();
}
public void uploadAll(String dir) {
clearFileHistory();
while (localFileSink.cleanUp(dir, true) > 0) {
uploadAllFromQueue();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// do nothing
}
}
}
private void clearFileHistory() {
processedFileQueue.clear();
processingFileSet.clear();
}
private void uploadAllFromQueue() {
String note = localFileSink.recvNotice();
while (note != null) {
uploadFile(note);
note = localFileSink.recvNotice();
}
}
private void uploadFile(final String filePath) {
// to prevent multiple uploading in any situations
final String key = filePath.substring(filePath.lastIndexOf("/"));
if (processingFileSet.contains(key)) {
return;
}
processingFileSet.add(key);
uploader.submit(new Runnable() {
@Override
public void run() {
try {
File localFile = new File(filePath);
long fileLength = localFile.length();
if (fileLength == 0) {
log.warn("empty file: " + filePath + " is abandoned");
localFileSink.deleteFile(filePath);
return;
}
String remoteFilePath = makeUploadPath(localFile);
long t1 = System.currentTimeMillis();
upload(filePath, remoteFilePath);
long t2 = System.currentTimeMillis();
log.info("upload duration: " + (t2 - t1) + " ms " +
"for " + filePath + " Len: " + fileLength + " bytes");
uploadedFileSize.addAndGet(fileLength);
uploadedFileCount.incrementAndGet();
uploadDuration = t2 - t1;
RemoteFileSink.this.notify(remoteFilePath, fileLength);
localFileSink.deleteFile(filePath);
log.info("upload done deleting from local: " + filePath);
} catch (Exception e) {
uploadFailureCount.incrementAndGet();
log.error("Exception while uploading: " + e.getMessage(), e);
} finally {
// check the file was deleted or not
if (new File(filePath).exists()) {
// something error happened
// it should be done again
processingFileSet.remove(key);
} else {
processedFileQueue.add(key);
}
}
}
});
}
private String makeUploadPath(File file) {
return prefixFormatter.get() + file.getName();
}
@Monitor(name = "uploadedFileSize", type = DataSourceType.COUNTER)
public long getUploadedFileSize() {
return uploadedFileSize.get();
}
@Monitor(name = "uploadDuration", type = DataSourceType.GAUGE)
private long uploadDuration;
@Monitor(name = "uploadedFileCount", type = DataSourceType.COUNTER)
public int getUploadedFileCount() {
return uploadedFileCount.get();
}
@Monitor(name = "uploadFailureCount", type=DataSourceType.COUNTER)
public int getUploadFailureCount() {
return uploadFailureCount.get();
}
private AtomicLong uploadedFileSize = new AtomicLong(0);
private AtomicInteger uploadedFileCount = new AtomicInteger(0);
private AtomicInteger uploadFailureCount = new AtomicInteger(0);
abstract void initialize();
abstract void upload(String localFilePath, String remoteFilePath) throws Exception;
abstract void notify(String filePath, long fileSize) throws Exception;
@Override
public long getNumOfPendingMessages() {
long numMessages = localFileSink.getNumOfPendingMessages();
if (numMessages == 0) {
return localFileSink.cleanUp(true);
} else {
return numMessages;
}
}
}