DefaultRollingFileOutput.java
package io.jstach.rainbowgum.rolling;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.util.function.Supplier;
import io.jstach.rainbowgum.LogConfig;
import io.jstach.rainbowgum.LogEncoder.BufferHints;
import io.jstach.rainbowgum.LogEvent;
import io.jstach.rainbowgum.LogMetrics;
import io.jstach.rainbowgum.file.FileOutput;
/*
* The active-file byte counter is tracked internally rather than stat-ing the file on
* every write, matching the "no per-event filesystem call" spirit of the rest of
* RainbowGum's output layer. Per LogOutput's own contract ("there will be no
* overlapping write/flush/close calls") this never needs synchronization - the
* appender/publisher combo already guarantees single-threaded access.
*/
final class DefaultRollingFileOutput implements RollingFileOutput {
private final Path activeFile;
private final RollingPolicy.ParsedPattern pattern;
final long maxFileSize;
private final int maxHistory;
final long totalSizeCap;
private final boolean cleanHistoryOnStart;
private final Supplier<FileOutput> supplier;
private final LogConfig config;
private FileOutput delegate;
private long bytesWritten;
private boolean needsOpen;
private boolean rollingRecovery;
private boolean started;
private boolean closed;
DefaultRollingFileOutput(Path activeFile, RollingPolicy.ParsedPattern pattern, long maxFileSize, int maxHistory,
long totalSizeCap, boolean cleanHistoryOnStart, Supplier<FileOutput> supplier, FileOutput delegate,
LogConfig config) {
this.activeFile = activeFile;
this.pattern = pattern;
this.maxFileSize = maxFileSize;
this.maxHistory = maxHistory;
this.totalSizeCap = totalSizeCap;
this.cleanHistoryOnStart = cleanHistoryOnStart;
this.supplier = supplier;
this.config = config;
this.delegate = delegate;
try {
this.bytesWritten = currentFileSize();
}
catch (RuntimeException e) {
try {
delegate.close();
}
catch (RuntimeException closeFailure) {
e.addSuppressed(closeFailure);
}
throw e;
}
}
private long currentFileSize() {
try {
return Files.size(activeFile);
}
catch (NoSuchFileException e) {
return 0L;
}
catch (IOException e) {
throw new UncheckedIOException(e);
}
}
@Override
public void start(LogConfig config) {
if (cleanHistoryOnStart) {
RollingPolicy.cleanHistory(activeFile, pattern, maxHistory, totalSizeCap);
}
delegate.start(config);
started = true;
}
@Override
public URI uri() {
return activeFile.toUri();
}
@Override
public OutputType type() {
return OutputType.FILE;
}
@Override
public BufferHints bufferHints() {
return delegate.bufferHints();
}
private void maybeRoll() {
if (needsOpen && !rollingRecovery) {
// An explicit reopen failure is not an automatic rotation failure.
openDelegate();
}
try {
if (needsOpen) {
openDelegate();
}
if (maxFileSize > 0 && bytesWritten >= maxFileSize) {
// Mark this before closing: any failure must leave the next write able to
// recover instead of silently writing to a closed output.
needsOpen = true;
rollingRecovery = true;
delegate.close();
RollingPolicy.roll(activeFile, pattern, maxHistory, totalSizeCap);
openDelegate();
}
}
catch (RuntimeException e) {
config.metrics().errorCounter(LogMetrics.ROLL_FAIL_METRIC, 1);
config.alerts().error(getClass(), "Failed to roll file '" + activeFile + "'", e);
throw e;
}
}
private void openDelegate() {
// Re-read the size because rotation might have failed before or after moving
// the active file. A failed replacement open must not rotate it a second time.
long size = currentFileSize();
FileOutput replacement = supplier.get();
try {
if (started) {
replacement.start(config);
}
}
catch (RuntimeException e) {
try {
replacement.close();
}
catch (RuntimeException closeFailure) {
e.addSuppressed(closeFailure);
}
throw e;
}
delegate = replacement;
bytesWritten = size;
needsOpen = false;
rollingRecovery = false;
}
@Override
public void write(LogEvent event, byte[] bytes, int off, int len, ContentType contentType) {
if (closed) {
return;
}
maybeRoll();
delegate.write(event, bytes, off, len, contentType);
bytesWritten += len;
}
@Override
public void write(LogEvent event, ByteBuffer buf, ContentType contentType) {
if (closed) {
return;
}
int len = buf.remaining();
maybeRoll();
delegate.write(event, buf, contentType);
bytesWritten += len;
}
@Override
public void flush() {
if (!closed && !needsOpen) {
delegate.flush();
}
}
@Override
public void close() {
if (!closed) {
closed = true;
delegate.close();
}
}
@Override
public void reopen() {
if (closed) {
return;
}
needsOpen = true;
rollingRecovery = false;
delegate.close();
openDelegate();
}
@Override
public String toString() {
return getClass().getName() + "[activeFile=" + activeFile + ", maxFileSize=" + maxFileSize + ", maxHistory="
+ maxHistory + "]";
}
}