/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF 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.
*/
package org.apache.nifi.provenance;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.apache.nifi.provenance.serialization.RecordWriter;
import org.apache.nifi.provenance.toc.TocWriter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public abstract class AbstractRecordWriter implements RecordWriter {
private static final Logger logger = LoggerFactory.getLogger(AbstractRecordWriter.class);
private final File file;
private final String storageLocation;
private final TocWriter tocWriter;
private final Lock lock = new ReentrantLock();
private volatile boolean dirty = false;
private volatile boolean closed = false;
public AbstractRecordWriter(final File file, final TocWriter writer) throws IOException {
logger.trace("Creating Record Writer for {}", file);
this.file = file;
this.storageLocation = file.getName();
this.tocWriter = writer;
}
public AbstractRecordWriter(final String storageLocation, final TocWriter writer) throws IOException {
logger.trace("Creating Record Writer for {}", storageLocation);
this.file = null;
this.storageLocation = storageLocation;
this.tocWriter = writer;
}
@Override
public synchronized void close() throws IOException {
closed = true;
logger.trace("Closing Record Writer for {}", getStorageLocation());
lock();
try {
flush();
try {
// We want to close 'out' only if the writer is not 'dirty'.
// If the writer is dirty, then there was a failure to write
// to disk, which means that we likely have a partial record written
// to disk.
//
// If we call close() on out, it will in turn call flush() on the underlying
// output stream, which is a BufferedOutputStream. As a result, we will end
// up flushing the buffer after a partially written record, which results in
// essentially random bytes being written to the repository, which causes
// corruption and un-recoverability. Since we will close the underlying 'rawOutStream'
// below, we will still appropriately clean up the resources help by this writer, so
// we are still OK in terms of closing all resources held by the writer.
final OutputStream buffered = getBufferedOutputStream();
if (buffered != null && !isDirty()) {
buffered.close();
}
} finally {
final OutputStream underlying = getUnderlyingOutputStream();
if (underlying != null) {
try {
getUnderlyingOutputStream().close();
} finally {
if (tocWriter != null) {
tocWriter.close();
}
}
}
}
} catch (final IOException ioe) {
markDirty();
throw ioe;
} finally {
unlock();
}
}
protected String getStorageLocation() {
return storageLocation;
}
@Override
public File getFile() {
return file;
}
@Override
public void lock() {
lock.lock();
}
@Override
public void unlock() {
lock.unlock();
}
@Override
public boolean tryLock() {
final boolean obtainedLock = lock.tryLock();
if (obtainedLock && isDirty()) {
// once we have obtained the lock, we need to check if the writer
// has been marked dirty. If so, we cannot write to the underlying
// file, so we need to unlock and return false. Otherwise, it's okay
// to write to the underlying file, so return true.
lock.unlock();
return false;
}
return obtainedLock;
}
@Override
public void markDirty() {
this.dirty = true;
}
@Override
public boolean isDirty() {
return dirty;
}
protected void resetDirtyFlag() {
this.dirty = false;
}
@Override
public synchronized void sync() throws IOException {
try {
if (tocWriter != null) {
tocWriter.sync();
}
syncUnderlyingOutputStream();
} catch (final IOException ioe) {
markDirty();
throw ioe;
}
}
@Override
public TocWriter getTocWriter() {
return tocWriter;
}
@Override
public boolean isClosed() {
return closed;
}
protected abstract OutputStream getBufferedOutputStream();
protected abstract OutputStream getUnderlyingOutputStream();
protected abstract void syncUnderlyingOutputStream() throws IOException;
}